C++之友元函数与前向引用

一,友元函数

1.定义:

友元函数是在类声明中由关键字friend修饰说明的非成员函数,在它的函数体中能够通过对象名访问 private 和 protected成员

2.作用:

增加灵活性,使程序员可以在封装和快速性方面做合理选择。

访问对象中的成员必须通过对象名。

3.友元函数的定义与语法

友元函数通过在类的定义中使用 friend 关键字来声明。

语法:

cpp 复制代码
class MyClass {
    // ...
    friend return_type function_name(parameters);
    // ...
};
  • friend: 声明这是一个友元函数。

  • return_type: 函数的返回类型。

  • function_name: 函数的名称。

  • parameters: 函数的参数列表,通常需要包含一个该类的对象,以便访问其成员。

主要使用场景:

  • 运算符重载: 当需要重载某些运算符(如 << 或 >>)时,左操作数可能不是类的对象(例如 std::cout),这时就无法将其定义为成员函数。友元函数则可以很好地解决这个问题。

  • 类之间的紧密协作: 当两个或多个类需要紧密地协同工作,并且需要频繁地访问对方的私有成员时,使用友元函数可以作为一种连接它们的桥梁。

  • 提高效率: 避免了为访问私有数据而创建不必要的公有成员函数(getter/setter),从而可能提高程序的运行效率。

4.友元函数的类型

友元函数主要可以分为以下几种类型:

  1. 全局函数作为友元函数: 一个普通的全局函数可以被声明为一个或多个类的友元。

  2. 另一个类的成员函数作为友元函数: 一个类的成员函数可以被声明为另一个类的友元。

  3. 友元类: 一个类可以被声明为另一个类的友元,这意味着友元类的所有成员函数都可以访问另一个类的私有和保护成员。

友元函数举例

示例 1: 全局友元函数

在这个例子中,将创建一个全局函数 printWidth 作为 Box 类的友元,用来访问其私有成员 width。

cpp 复制代码
#include <iostream>

class Box {
private:
    double width;

public:
    Box(double w) : width(w) {}

    // 声明全局函数 printWidth 为友元
    friend void printWidth(Box box);
};

// 定义友元函数
void printWidth(Box box) {
    // 作为友元函数,可以直接访问 Box 类的私有成员 width
    std::cout << "Width of box : " << box.width << std::endl;
}

int main() {
    Box myBox(10.5);
    printWidth(myBox); // 调用友元函数
    return 0;
}

代码解释:

  1. 在 `Box` 类内部,我们使用 `friend void printWidth(Box box);` 声明了 `printWidth` 函数是 `Box` 类的友元。

  2. `printWidth` 函数是在类外部定义的普通函数,但它可以直接访问 `Box` 对象的私有成员 `width`。

示例 2: 另一个类的成员函数作为友元函数
cpp 复制代码
#include <iostream>

// 需要提前声明 ClassB,因为 ClassA 的成员函数引用了它
class ClassB;

class ClassA {
private:
    int numA;

public:
    ClassA() : numA(12) {}

    // 声明 ClassB 的成员函数 showA 为友元
    friend void ClassB::showA(ClassA& a);
};

class ClassB {
public:
    void showA(ClassA& a);
};

// 定义 ClassB 的成员函数
void ClassB::showA(ClassA& a) {
    // 由于是 ClassA 的友元,可以访问其私有成员 numA
    std::cout << "ClassA::numA = " << a.numA << std::endl;
}

int main() {
    ClassA objectA;
    ClassB objectB;
    objectB.showA(objectA);
    return 0;
}

代码解释:

  1. 由于 ClassA 中声明了 ClassB 的成员函数为友元,而 ClassB 的定义在后面,所以需要使用 class ClassB; 进行前向声明。

  2. 在 ClassA 中,friend void ClassB::showA(ClassA& a); 将 ClassB 的 showA 成员函数声明为友元。

  3. ClassB::showA 函数因此可以访问 ClassA 对象的私有成员 numA。

示例 3: 友元类

若一个类为另一个类的友元,则此类的所有成员都能访问对方类的私有成员。

声明语法:将友元类名在另一个类中使用friend修饰说明。

当一个类需要完全访问另一个类的所有私有和保护成员时,可以将整个类声明为友元。

cpp 复制代码
#include <iostream>

class ClassA {
private:
    int private_data;

public:
    ClassA() : private_data(10) {}

    // 声明 ClassB 为友元类
    friend class ClassB;
};

class ClassB {
public:
    void displayData(ClassA& a) {
        // 作为友元类,ClassB 的所有成员函数都可以访问 ClassA 的私有成员
        std::cout << "The value of private_data from ClassA is: " << a.private_data << std::endl;
    }
};

int main() {
    ClassA objA;
    ClassB objB;
    objB.displayData(objA);
    return 0;
}

代码解释:

在 ClassA 中,friend class ClassB; 将 ClassB 声明为一个友元类。这使得 ClassB 的所有成员函数(在这个例子中是 displayData)都可以访问 ClassA 对象的私有成员 private_data。

5.友元函数的特点和注意事项

  • 非成员函数: 友元函数不是类的成员函数。
  • 作用域: 友元函数的作用域不属于声明它的类。
  • 访问方式: 友元函数需要通过对象、对象指针或对象引用来访问类的成员
  • 单向性: 友元关系是单向的。如果类 A 是类 B 的友元,不代表类 B 也是类 A 的友元。
  • 非传递性: 友元关系不能传递。如果 A 是 B 的友元,B 是 C 的友元,不意味着 A 是 C 的友元。
  • 破坏封装性: 友元机制在一定程度上破坏了类的封装性,因为它允许外部函数访问类的内部实现细节。因此,应该谨慎使用,避免滥用。

二,前向引用

在 C++ 的编程世界里,我们都遵循一个基本原则:"先声明,后使用"。无论是变量还是函数,编译器都需要在见到其实际使用之前,了解它的存在和类型。但当两个类需要互相"认识"对方时,这个简单的原则就会带来一个棘手的编译难题------循环依赖。

这时候,一个重要的 C++ 特性就登场了:前向声明

问题的根源:互相引用的类

想象一下,我们有两个类,A 和 B。类 A 的某个成员函数需要接收一个 B 类型的对象作为参数,而类 B 的某个成员函数也需要一个 A 类型的对象。

如果我们像下面这样编写代码:

cpp 复制代码
// 编译会失败的代码
class A {
public:
    void f(B b); // 错误发生在这里
};

class B {
public:
    void g(A a);
};

当你尝试编译这段代码时,编译器会立刻报错。在处理 class A 的定义时,当它遇到 void f(B b); 这一行,它会问:"B 是什么?" 由于此时 class B 还没有被定义,编译器对 B 一无所知,于是抛出错误,例如:

error C2061: syntax error : identifier 'B'

这就是一个典型的循环依赖问题。A 的定义依赖 B,而 B 的定义又依赖 A,形成了一个无法解开的"死结"。

解决方案:前向声明

为了解开这个"死结",我们可以使用前向声明。

前向声明就像一个"承诺",你告诉编译器:"嘿,请相信我,有一个叫做 B 的类,我稍后会给你它的完整定义。现在你只需要知道 B 是一个类的名字就行了。"

语法非常简单,就是在需要使用它的类定义之前,写上一行:

class ClassName;

现在,我们用前向声明来修正上面的代码:

cpp 复制代码
// 使用前向声明,编译成功
class B; // 这就是对类 B 的前向声明

class A {
public:
    void f(B b);
};

class B {
public:
    void g(A a);
};

// 成员函数的定义可以放在两个类都定义完整之后
void A::f(B b) { /* ... */ }
void B::g(A a) { /* ... */ }

通过在 class A 之前加上 class B;,我们告诉编译器 B 是一个类名。这样,当编译器看到 void f(B b); 时,它虽然不知道 B 的具体大小和内部结构,但它知道 B 是一个类型,这就足以让声明通过了。

前向声明的局限性:不完整的类型

前向声明虽然强大,但它不是万能的。它只为编译器引入了一个标识符,这个被声明的类被称为"不完整的类型"。

因为编译器只知道它的名字,而不知道它的任何细节(如成员变量、大小等),所以你不能对一个不完整的类型执行以下操作:

  1. 创建该类的对象(实例化): 编译器不知道需要为对象分配多大的内存。

  2. 访问其成员: 编译器不知道该类有哪些成员。

  3. 将其用作基类: 派生类需要知道基类的所有细节。

  4. 使用 sizeof 获取其大小

错误示例:

cpp 复制代码
class Fred; // 前向声明

class Barney {
    Fred x; // 错误!类 Fred 的声明尚不完善
};

class Fred {
    Barney y;
};

在 class Barney 中,Fred x; 试图创建一个 Fred 类的对象。但此时 Fred 只是一个不完整的类型,编译器不知道 x 应该占用多少空间,因此编译会失败。

那么,对于一个不完整的类型,我们可以做什么呢?

你应该记住这条核心规则:当你使用前向声明时,你只能使用被声明的符号(类名),而不能涉及类的任何实现细节。

具体来说,你可以:

  1. 声明指向该类型的指针 (Fred*)。

  2. 声明对该类型的引用 (Fred&)。

  3. 将其作为函数的参数或返回类型(必须是指针或引用形式)。

这是因为无论指针或引用指向什么类型的对象,它们自身的大小是固定的。

完整的实战示例

让我们通过一个更实际的例子来巩固理解:Person(人)和 Apartment(公寓)。一个人住在一个公寓里,一个公寓里也住着一个房客。

cpp 复制代码
#include <iostream>
#include <string>

// 1. 对 Apartment 进行前向声明,因为 Person 类中会使用到它
class Apartment;

class Person {
private:
    std::string m_name;
    Apartment* m_apartment; // 2. 定义一个指向 Apartment 的指针,这是允许的

public:
    Person(const std::string& name) : m_name(name), m_apartment(nullptr) {}

    void moveTo(Apartment* apt); // 声明成员函数

    void greetTenant();
};

class Apartment {
private:
    std::string m_unit;
    Person* m_tenant; // 定义一个指向 Person 的指针

public:
    Apartment(const std::string& unit) : m_unit(unit), m_tenant(nullptr) {}

    void setTenant(Person* person) {
        m_tenant = person;
    }

    const std::string& getUnit() const { return m_unit; }
    Person* getTenant() const { return m_tenant; }
};

// 3. 成员函数的定义必须放在相关类(如此处的 Apartment)已经完整定义之后
void Person::moveTo(Apartment* apt) {
    m_apartment = apt;
    apt->setTenant(this); // 此时可以安全地调用 apt 的成员函数
}

void Person::greetTenant() {
    if (m_apartment && m_apartment->getTenant()) {
        std::cout << m_name << " says: Hello, resident of unit " << m_apartment->getUnit() << "!" << std::endl;
    }
}

int main() {
    Person alice("Alice");
    Apartment apt404("404");

    alice.moveTo(&apt404);

    alice.greetTenant();

    return 0;
}

代码分析:

  1. 我们首先前向声明了 class Apartment;,这样 Person 类就可以安全地定义 Apartment* m_apartment; 成员。

  2. 在 Person 和 Apartment 类中,我们都使用了指针 (Person*, Apartment*) 来引用对方,这完美地规避了不完整类型的限制。

  3. 注意 Person::moveTo 函数的定义被放在了 Apartment 类的完整定义之后。这是因为在该函数内部,我们需要调用 apt->setTenant(this),这要求编译器必须知道 Apartment 类的所有成员细节。

总结

前向声明是 C++ 中一个基础且重要的工具,它不仅是解决类之间循环依赖问题的关键,也是大型项目中减少头文件包含、从而加快编译速度的有效手段。

核心要点回顾:

  • 目的:在类被完整定义之前,向编译器引入其名称。

  • 语法:class ClassName;

  • 用途:主要用于解决类之间的循环依赖。

  • 限制:被前向声明的类是"不完整的类型",你不能创建它的对象或访问其成员。

  • 规则:对于不完整的类型,你只能定义其指针或引用,或在函数签名中使用它们。

相关推荐
ajassi20003 小时前
开源 C# 快速开发(十二)进程监控
开发语言·开源·c#
库库8393 小时前
Java微服务知识点详细总结
java·开发语言·微服务
txwtech3 小时前
第4篇 vs2019+QT调用SDK连接海康相机显示图片
开发语言·数码相机·qt
王嘉俊9253 小时前
Flask 入门:轻量级 Python Web 框架的快速上手
开发语言·前端·后端·python·flask·入门
做运维的阿瑞4 小时前
Python 面向对象编程深度指南
开发语言·数据结构·后端·python
木木子99994 小时前
Python的typing模块:类型提示 (Type Hinting)
开发语言·windows·python
她说人狗殊途4 小时前
Spring IoC容器加载过程 vs Bean生命周期对应关系图
java·开发语言·rpc
MediaTea4 小时前
Python 编辑器:PyCharm
开发语言·ide·python·pycharm·编辑器
0wioiw04 小时前
Go基础(⑦实例和依赖注入)
开发语言·golang