在C++中,友元(friend)提供了一种突破类的访问限定符的机制,使得外部函数或其他类可以访问类的私有(private)和受保护的成员(protected)。友元可以是友元函数或友元类,而这种友元关系是在类定义中通过关键字
friend
显式声明的。
友元函数是一个外部函数,但通过友元声明,它可以访问类的私有和受保护的成员。友元函数不属于类的成员函数,它可以在类的任意地方声明,而不受访问限定符(public
、private
、protected
)的限制。
#include<iostream>
using namespace std;
// 前置声明,避免类A的友元函数不识别类B
class B;
class A {
// 友元声明,允许函数 func 访问A类的私有成员
friend void func(const A& aa, const B& bb);
private:
int _a1 = 1;
int _a2 = 2;
};
class B {
// 友元声明,允许函数 func 访问B类的私有成员
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
int _b2 = 4;
};
// 友元函数定义,能够访问A和B类的私有成员
void func(const A& aa, const B& bb) {
cout << "A::_a1: " << aa._a1 << endl; // 访问A类的私有成员
cout << "B::_b1: " << bb._b1 << endl; // 访问B类的私有成员
}
int main() {
A aa;
B bb;
func(aa, bb); // 调用友元函数,访问A和B类的私有成员
return 0;
}
A::_a1: 1
B::_b1: 3
解释:
func
被声明为 A
和 B
类的友元,因此它可以访问 A
类和 B
类的私有成员变量 _a1
和 _b1
。func
是一个独立于类的外部函数,但通过友元声明,它获得了访问类的私有数据的权限。友元类允许一个类的所有成员函数访问另一个类的私有和受保护成员。友元类的成员函数并不需要逐一声明为友元,只要类被声明为友元,所有的成员函数都能访问另一个类的私有和受保护成员。
#include<iostream>
using namespace std;
class A {
// 友元类B声明,允许B类的所有成员函数访问A类的私有成员
friend class B;
private:
int _a1 = 1;
int _a2 = 2;
};
class B {
public:
// 可以访问A类的私有成员
void func1(const A& aa) {
cout << "A::_a1: " << aa._a1 << endl; // 访问A类的私有成员
cout << "B::_b1: " << _b1 << endl; // 访问B类的私有成员
}
void func2(const A& aa) {
cout << "A::_a2: " << aa._a2 << endl; // 访问A类的私有成员
cout << "B::_b2: " << _b2 << endl; // 访问B类的私有成员
}
private:
int _b1 = 3;
int _b2 = 4;
};
int main() {
A aa;
B bb;
bb.func1(aa); // 通过B类的成员函数访问A类的私有成员
bb.func2(aa); // 通过B类的成员函数访问A类的私有成员
return 0;
}
A::_a1: 1
B::_b1: 3
A::_a2: 2
B::_b2: 4
解释:
B
类被声明为 A
类的友元类,因此 B
类的所有成员函数都可以访问 A
类的私有成员 _a1
和 _a2
。B
类的成员函数声明为 A
类的友元,只要 B
类是 A
类的友元,B
类的所有成员函数都可以访问 A
类的私有数据。单向关系:友元关系是单向的,如果 A
是 B
的友元,那么 B
类的成员可以访问 A
类的私有成员,但 A
类不能访问 B
类的私有成员,除非 B
类也将 A
类声明为友元。
示例:单向友元关系
class A;
class B {
friend class A; // B 声明 A 为友元
private:
int _b1 = 1;
};
class A {
public:
void accessB(B& bb) {
// A 可以访问 B 的私有成员
cout << "B::_b1: " << bb._b1 << endl;
}
};
int main() {
A aa;
B bb;
aa.accessB(bb); // A 类访问 B 的私有成员
return 0;
}
输出:
B::_b1: 1
不具有传递性:友元关系不具有传递性。如果 A
是 B
的友元,B
是 C
的友元,A
不能访问 C
类的私有成员。
友元增加耦合性:虽然友元机制提供了访问类私有成员的便利,但过度使用友元会导致类与类之间的耦合增加,破坏了类的封装性。因此,友元不宜滥用,应该谨慎使用。
友元在某些情况下能提供方便,比如当需要两个类之间进行紧密合作时,使用友元可以简化代码,减少冗长的接口设计。
#include<iostream>
using namespace std;
class Account;
class Transaction {
public:
void deposit(Account& account, double amount);
void withdraw(Account& account, double amount);
};
class Account {
friend class Transaction; // 声明 Transaction 类为友元类
public:
Account(double balance) : _balance(balance) {}
void showBalance() const {
cout << "Balance: " << _balance << endl;
}
private:
double _balance;
};
void Transaction::deposit(Account& account, double amount) {
account._balance += amount; // 直接访问 Account 类的私有成员
}
void Transaction::withdraw(Account& account, double amount) {
if (amount <= account._balance) {
account._balance -= amount;
} else {
cout << "Insufficient balance" << endl;
}
}
int main() {
Account myAccount(1000.0);
Transaction trans;
trans.deposit(myAccount, 500.0); // 存款
myAccount.showBalance(); // 输出:1500
trans.withdraw(myAccount, 200.0); // 取款
myAccount.showBalance(); // 输出:1300
return 0;
}
Balance: 1500
Balance: 1300
解释:
Transaction
类被声明为 Account
类的友元类,因此 Transaction
类的成员函数 deposit
和 withdraw
可以直接访问 Account
类的私有成员 _balance
。
friend
关键字来声明友元函数或友元类,使得类之间的合作更加简便。内部类(Nested Class)是指一个类定义在另一个类的内部。在C++中,内部类和外部类是独立的类,尽管它们之间有一定的联系,但内部类不属于外部类的对象,它有自己的内存布局和独立性。使用内部类通常是为了封装和简化类之间的关联。
5.1 内部类的基本概念
private
或 protected
访问限定符下,限制其他类对其的访问。以下是一个包含内部类的简单示例,展示了如何在外部类中定义内部类,以及如何让内部类访问外部类的私有成员。
#include<iostream>
using namespace std;
class A {
private:
static int _k; // 外部类的静态成员
int _h = 1; // 外部类的非静态成员
public:
// 定义内部类 B
class B {
public:
// 内部类方法可以访问外部类的私有成员,因为 B 是 A 的友元类
void foo(const A& a) {
cout << "A::_k = " << _k << endl; // 访问外部类的静态成员
cout << "A::_h = " << a._h << endl; // 访问外部类的非静态成员
}
};
};
// 初始化外部类的静态成员
int A::_k = 1;
int main() {
cout << "Size of A: " << sizeof(A) << endl; // 输出 A 类的大小
A::B b; // 创建内部类 B 的对象
A aa; // 创建外部类 A 的对象
b.foo(aa); // 使用内部类对象调用其方法,访问外部类的私有成员
return 0;
}
Size of A: 4
A::_k = 1
A::_h = 1
解释:
B
被定义在外部类 A
的 public
区域中,但它依然是 A
的友元类,可以访问 A
类的私有成员变量 _k
和 _h
。A::B b
来实例化内部类 B
,然后通过内部类的成员函数 foo
访问外部类对象的私有成员。sizeof(A)
表示 A
类的大小,由于 A
只有一个整数成员 _h
,因此其大小为4字节。内部类作为外部类的一部分,可以被放置在 private
或 protected
访问区域中,这样可以控制内部类的可见性。
private
区域#include<iostream>
using namespace std;
class Outer {
private:
class Inner { // 内部类定义在 private 区域
public:
void display() {
cout << "Inner class method called." << endl;
}
};
public:
void createInner() {
Inner in; // 外部类的方法中可以创建内部类的对象
in.display();
}
};
int main() {
Outer outer;
outer.createInner(); // 通过外部类的方法调用内部类的方法
// Outer::Inner in; // 错误!内部类在 private 区域,外部无法访问
return 0;
}
Inner class method called.
解释:
Inner
定义在 Outer
类的 private
区域,外部类的方法 createInner()
可以创建 Inner
类的对象并调用其方法。Inner
类会导致编译错误,因为它是 private
的。使用内部类的一个常见场景是当两个类紧密相关时,可以将一个类封装到另一个类中。这样做的目的是让外部类管理内部类的访问,使得内部类只为外部类所用。
#include<iostream>
using namespace std;
class Manager {
private:
class Task {
public:
void performTask() {
cout << "Performing task." << endl;
}
};
public:
void assignTask() {
Task t; // 外部类方法可以使用内部类
t.performTask();
}
};
int main() {
Manager mgr;
mgr.assignTask(); // 调用外部类的方法,执行内部类中的任务逻辑
return 0;
}
Performing task.
解释:
Task
类被封装在 Manager
类的 private
区域,表示 Task
只为 Manager
类服务,外部无法直接访问它。Task
类专属于 Manager
类,外部无法创建 Task
对象,只能通过 Manager
类的方法来间接使用它。内部类默认是外部类的友元类,这意味着内部类可以访问外部类的私有和受保护成员。这种设计允许内部类和外部类之间进行紧密的合作,使得内部类可以像外部类的成员函数一样访问其内部数据。
#include<iostream>
using namespace std;
class Container {
private:
int _data = 100;
public:
// 定义内部类
class Helper {
public:
void showData(const Container& c) {
cout << "Container::_data = " << c._data << endl; // 访问外部类的私有成员
}
};
};
int main() {
Container c;
Container::Helper h; // 创建内部类对象
h.showData(c); // 调用内部类的方法,访问外部类的私有成员
return 0;
}
Container::_data = 100
解释:
Helper
类作为 Container
的内部类,默认是 Container
的友元,因此它可以访问 Container
类的私有成员 _data
。h
,可以调用 showData
方法来访问外部类 Container
的私有数据。内部类可以用于一些特定场景下的封装和逻辑简化,比如下面的例子中,通过内部类 Sum
来计算 1 到 n 的累加和。
#include<iostream>
using namespace std;
class Solution {
// 内部类 Sum,用于进行累加操作
class Sum {
public:
Sum() {
_ret += _i; // 每创建一个对象,累加一次当前的 _i
++_i; // 自增 i
}
};
static int _i; // 用于计数的静态变量
static int _ret; // 用于存储结果的静态变量
public:
int Sum_Solution(int n) {
Sum arr[n]; // 创建 n 个 Sum 对象,触发累加逻辑
return _ret; // 返回累加的结果
}
};
// 初始化静态变量
int Solution::_i = 1;
int Solution::_ret = 0;
int main() {
Solution sol;
cout << "Sum of 1 to 5: " << sol.Sum_Solution(5) << endl; // 1 + 2 + 3 + 4 + 5 = 15
return 0;
}
Sum of 1 to 5: 15
解释:
Sum
在创建对象时会自动进行累加操作,创建 n
个 Sum
对象等价于对 1
到 n
进行累加。
_i
用于记录当前的计数,_ret
用于存储累加的结果。匿名对象是C++中的一种特殊对象,和普通的有名对象不同,匿名对象没有名字,仅在表达式中被使用,生命周期非常短暂。它的生命周期只限于当前语句,当语句执行结束后,匿名对象就会自动被销毁并调用析构函数。匿名对象的典型用法是临时定义对象,完成某项任务后立即销毁。
A()
或 A(1)
这样的表达式。
A obj(1);
A(1);
在C++中,通过 A()
或 A(1)
这样的语法直接调用构造函数来创建匿名对象,匿名对象没有名字,生命周期仅限于当前行,结束后立即调用析构函数进行销毁。
#include<iostream>
using namespace std;
class A {
public:
// 带参数的构造函数
A(int a = 0) : _a(a) {
cout << "A(int a) 构造函数被调用, _a = " << _a << endl;
}
// 析构函数
~A() {
cout << "~A() 析构函数被调用, _a = " << _a << endl;
}
private:
int _a;
};
int main() {
A aa1; // 有名对象 aa1 的创建
// 不能这样定义对象,因为编译器无法确定是函数声明还是对象定义
// A aa1();
// 创建匿名对象并立即销毁
A(); // 调用默认构造函数,匿名对象创建并立即销毁
A(1); // 调用带参数的构造函数,匿名对象创建并立即销毁
A aa2(2); // 有名对象 aa2 的创建,生命周期为整个作用域
// 匿名对象用于调用函数,完成任务后立即销毁
Solution().Sum_Solution(10);
return 0;
}
A(int a) 构造函数被调用, _a = 0
~A() 析构函数被调用, _a = 0
A(int a) 构造函数被调用, _a = 1
~A() 析构函数被调用, _a = 1
A(int a) 构造函数被调用, _a = 2
~A() 析构函数被调用, _a = 2
解释:
A()
和 A(1)
创建的是匿名对象,它们在当前语句结束后立即调用析构函数。aa1
和 aa2
是在整个作用域内存在的,它们在作用域结束时调用析构函数。匿名对象的一个常见应用场景是用来临时调用某个类的成员函数,执行完任务后不需要该对象的存在。例如:
class Solution {
public:
int Sum_Solution(int n) {
return n * (n + 1) / 2;
}
};
int main() {
// 使用匿名对象调用 Sum_Solution 函数
int result = Solution().Sum_Solution(10); // 匿名对象创建后立即销毁
cout << "Sum of 1 to 10: " << result << endl;
return 0;
}
Sum of 1 to 10: 55
解释:
Solution()
被创建,用于调用 Sum_Solution
函数。函数调用结束后,匿名对象立即销毁,不再占用资源。在某些情况下,我们不需要为对象命名,只是想要使用对象来执行一些操作,匿名对象可以帮助避免命名冲突或不必要的命名。特别是在返回一个对象并立即使用时,匿名对象是理想的选择。
class A {
public:
A(int a) : _a(a) {
cout << "A(int a) 构造函数被调用, _a = " << _a << endl;
}
~A() {
cout << "~A() 析构函数被调用, _a = " << _a << endl;
}
private:
int _a;
};
// 函数返回一个匿名对象
A createA() {
return A(100); // 返回匿名对象
}
int main() {
createA(); // 调用 createA 函数,返回的匿名对象立即销毁
return 0;
}
A(int a) 构造函数被调用, _a = 100
~A() 析构函数被调用, _a = 100
解释:
createA
返回一个匿名对象,返回后立即销毁。生命周期短暂:匿名对象的生命周期只在当前语句结束时有效,不能跨语句使用匿名对象。如果需要在多行代码中使用对象,必须创建有名对象。
错误示例:
A obj = A(1); // 正确,有名对象 obj
A(1).foo(); // 匿名对象调用方法
// A(1); // 错误:匿名对象无法在下一行使用
编译器解析问题:在C++中,有些语法可能导致编译器误判为函数声明而不是对象创建。因此,注意避免如下情况:
错误示例:
A aa1(); // 被误判为函数声明,实际上不是对象的创建
正确用法:
A aa1(1); // 明确创建对象
匿名对象的返回值优化(RVO):现代C++编译器通常会对匿名对象进行优化,在返回对象时避免多余的拷贝操作。这种优化称为返回值优化(RVO)。
在C++中,编译器会尽量减少不必要的对象拷贝,特别是在函数参数传递和返回值的场景下,拷贝省略(Copy Elision)、返回值优化(RVO)和命名返回值优化(NRVO)等机制被广泛应用。这些优化机制显著提升了代码执行效率,减少了不必要的资源消耗。
让我们通过代码示例演示编译器在对象拷贝时进行的优化操作。
#include<iostream>
using namespace std;
class A {
public:
A(int a = 0) : _a1(a) {
cout << "A(int a) 构造函数被调用, _a = " << _a1 << endl;
}
A(const A& aa) : _a1(aa._a1) {
cout << "A(const A& aa) 拷贝构造函数被调用" << endl;
}
A& operator=(const A& aa) {
cout << "A& operator=(const A& aa) 赋值运算符被调用" << endl;
if (this != &aa) {
_a1 = aa._a1;
}
return *this;
}
~A() {
cout << "~A() 析构函数被调用" << endl;
}
private:
int _a1;
};
void f1(A aa) {}
A f2() {
A aa;
return aa;
}
int main() {
// 传值传参
A aa1;
f1(aa1);
cout << endl;
// 隐式类型,连续构造+拷贝构造 -> 优化为直接构造
f1(1);
cout << endl;
// 一个表达式中,连续构造+拷贝构造 -> 优化为一个构造
f1(A(2));
cout << endl;
cout << "***********************************************" << endl;
// 传值返回,连续拷贝构造 -> 优化为一个构造
f2();
cout << endl;
// 传值返回,连续拷贝构造 -> 优化为一个构造
A aa2 = f2();
cout << endl;
// 连续拷贝构造+赋值重载 -> 无法优化
aa1 = f2();
cout << endl;
return 0;
}
让我们先看一个传值传参的流程图。在 f1(aa1)
中,按值传递时应调用拷贝构造函数,但编译器会优化该过程,直接在调用时构造对象,避免多余的拷贝。
在 f2
函数中,返回局部对象 aa
。理论上应调用拷贝构造函数,但编译器可以通过 RVO 优化,直接在调用者的内存中构造对象 aa2
,避免多次构造与析构。
当调用 A aa2 = f2()
时,编译器可以省略拷贝,直接在 aa2
的内存中构造返回的局部对象,避免中间的拷贝过程。这种优化通过 拷贝省略 完成。
在 aa1 = f2()
这种场景下,由于涉及到赋值运算符,编译器无法进行拷贝省略优化,因此会调用赋值运算符。
flowchart TD
subgraph 赋值操作
A4(创建局部对象 aa)
B4(拷贝构造 aa)
C4(赋值运算符 operator=)
D4(无优化,执行拷贝和赋值)
end
A4 --> B4 --> C4 --> D4
根据编译器和编译选项(如-O2
、-O3
),现代C++编译器(GCC、Clang、MSVC)会自动进行对象拷贝优化,减少不必要的对象构造、拷贝和析构。以下是编译器的常见优化策略:
为了更好地理解这些优化机制,我通过Mermaid生成了流程图,展示了传参、返回值优化和赋值操作的具体执行流程。通过这些图示,你可以更直观地了解编译器在对象拷贝时如何减少多余操作。
通过编译器优化,如拷贝省略、RVO 和 NRVO,现代C++编译器可以大幅提升对象拷贝的效率,减少多余的构造和析构操作。编译器自动优化这些拷贝操作,使得传值传参和返回值操作更加高效。在实际开发中,理解这些优化机制可以帮助我们编写出更加高效、性能优越的C++代码。
你可以通过查阅 cppreference的RVO文档 进一步了解这些机制【链接】。