5. 内部类
• 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
• 内部类默认是外部类的友元类。
• 内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
• 代码示例:
cpp
#include <iostream>
using namespace std;
class Outer {
public:
// 内部类定义在外部类内部
class Inner
{
public:
void show()
{
cout << "Inner class method" << endl;
}
};
void useInner()
{
Inner in;
in.show();
}
};
int main() {
Outer::Inner obj; // 通过外部类访问内部类
obj.show();
return 0;
}
• 内部类默认是外部类的友元类,因此内部类可以直接访问外部类的私有成员:
cpp
#include <iostream>
using namespace std;
class Outer
{
private:
int value = 100;
public:
class Inner
{
public:
void accessOuter(Outer& o)
{
// 内部类作为友元,可直接访问外部类的私有成员
cout << "Outer private value: " << o.value << endl;
}
};
};
int main()
{
Outer o;
Outer::Inner in;
in.accessOuter(o);
return 0;
}
6.匿名对象
• 用类型(实参)定义出来的对象叫做匿名对象,匿名对象的特点不用取名字,例如A();相比之前我们定义的类型对象名(实参)定义出来的叫有名对象。
• 匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。
• 代码示例:
cpp
#include <iostream>
using namespace std;
class A {
public:
A(int x = 0)
: _x(x)
{
cout << "A(int)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
void show() const
{
cout << "x = " << _x << endl;
}
private:
int _x;
};
// 匿名对象作为函数参数
void func(const A& a)
{
cout << "func called" << endl;
}
int main()
{
// 匿名对象:A(10) 没有名字,生命周期仅限当前这一行
A(10).show(); // 临时调用成员函数,行末即析构
func(A(20)); // 匿名对象作为函数实参,函数调用结束后析构
cout << "main continues" << endl;
return 0;
}
• 上例中 A(10) 和 A(20) 都是匿名对象,它们没有对象名,生命周期只持续到当前语句结束。当该行执行完毕后,编译器会立即调用析构函数释放资源,因此匿名对象适合"临时用一下"的场景。
• 下面通过一个对比示例,直观展示有名对象和匿名对象在生命周期与资源释放时机上的差异:
cpp
#include <iostream>
using namespace std;
class A {
public:
A(int x = 0) : _x(x)
{
cout << "A(int) 构造 _x = " << _x << endl;
}
~A()
{
cout << "~A() 析构 _x = " << _x << endl;
}
void show() const
{
cout << "show() _x = " << _x << endl;
}
private:
int _x;
};
void func(const A& a)
{
cout << "func 调用中" << endl;
}
int main()
{
cout << "--- 有名对象 ---" << endl;
A named(1); // 有名对象,生命周期从定义处开始
named.show(); // 可以多次使用
named.show(); // 作用域结束前一直有效
cout << "--- 匿名对象 ---" << endl;
A(2).show(); // 匿名对象,仅当前这一行有效,行末立即析构
func(A(3)); // 匿名对象作为实参,函数调用结束后析构
cout << "--- main 结束前 ---" << endl;
// 此时 named 仍存活,直到 main 结束才析构
return 0;
}
// 输出结果:
// --- 有名对象 ---
// A(int) 构造 _x = 1
// show() _x = 1
// show() _x = 1
// --- 匿名对象 ---
// A(int) 构造 _x = 2
// show() _x = 2
// ~A() 析构 _x = 2 // 匿名对象 A(2) 行末立即析构
// A(int) 构造 _x = 3
// func 调用中
// ~A() 析构 _x = 3 // 匿名对象 A(3) 函数调用结束后析构
// --- main 结束前 ---
// ~A() 析构 _x = 1 // 有名对象 named 在 main 结束时才析构
• 对比可见:有名对象 named 从定义处开始存活,直到其作用域(这里是 main 函数)结束才调用析构函数释放资源,期间可以反复使用;而匿名对象 A(2)、A(3) 没有名字,生命周期只持续到当前语句结束,行末或函数调用结束后编译器会立即调用析构函数释放资源。因此,如果只是临时用一下对象,使用匿名对象可以更及时地释放资源、减少内存占用。
7.对象拷贝时的编译器优化
• 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝。
• 如何优化 C++ 标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化。
• 不同编译器对拷贝优化的典型行为对比如下:
| 编译器 | 默认优化行为 | 关闭优化的编译选项 |
|---|---|---|
| GCC | 默认开启拷贝省略(copy elision),对连续表达式中的拷贝进行合并优化;在 C++17 下对按值返回的临时对象保证省略拷贝。 | -fno-elide-constructors |
| Clang | 默认行为与 GCC 基本一致,开启拷贝省略;对临时对象和按值返回的优化较为积极。 | -fno-elide-constructors |
| MSVC | 默认开启返回值优化(RVO)和命名返回值优化(NRVO),但具体行为受 /O1、/O2 等优化级别影响。 | /Zc:noElideConstructors(或使用 /Od 关闭优化) |
• 下面通过一个具体示例,直观对比开启优化(默认)和关闭优化(-fno-elide-constructors)时,传参和返回值过程中的拷贝次数差异。
cpp
#include <iostream>
using namespace std;
class A {
public:
A(int x = 0) : _x(x) {
cout << "A(int)" << endl;
}
A(const A& a) : _x(a._x) {
cout << "A(const A&) 拷贝构造" << endl;
}
~A() {
cout << "~A()" << endl;
}
int _x;
};
// 传参:按值接收,会触发拷贝构造
void func(A a) {
cout << "func 内 _x = " << a._x << endl;
}
// 返回值:返回局部对象,可能触发拷贝构造
A getA() {
A tmp(10);
return tmp;
}
int main() {
cout << "--- 传参 ---" << endl;
A obj1(1);
func(obj1); // 把 obj1 拷贝给形参 a
cout << "--- 返回值 ---" << endl;
A obj2 = getA(); // 把 getA 的返回值拷贝给 obj2
cout << "--- main 结束 ---" << endl;
return 0;
}
• 编译运行对比:
bash
# 默认开启优化(拷贝省略生效)
g++ test.cpp -o test_default
./test_default
关闭拷贝优化
g++ test.cpp -fno-elide-constructors -o test_noelide
./test_noelide
• 开启优化(默认)时,编译器会尽量省略中间拷贝,输出大致如下:
text
--- 传参 ---
A(int)
func 内 _x = 1
--- 返回值 ---
A(int)
--- main 结束 ---
~A()
~A()
~A()
• 关闭优化(-fno-elide-constructors)时,传参和返回值过程中的拷贝不会被省略,输出大致如下:
text
--- 传参 ---
A(int)
A(const A&) 拷贝构造
func 内 _x = 1
--- 返回值 ---
A(int)
A(const A&) 拷贝构造
--- main 结束 ---
~A()
~A()
~A()
~A()
• 对比可见:开启优化后,传参时省略了形参的拷贝构造,返回值时也省略了临时对象的拷贝构造,整体少调用了两次拷贝构造,析构次数也随之减少,程序运行效率更高。
• Linux 下可以将上面代码拷贝到 test.cpp 文件,编译时用 g++ test.cpp -fno-elide-constructors 的方式关闭构造相关的优化。