本文将系统梳理C++中初始化列表、类型转换、static成员、友元、内部类、匿名对象及编译器优化七大核心知识点,通过概念解析、代码示例与对比分析,帮助你彻底掌握类与对象的底层调用机制。
1. 再探构造函数:初始化列表
1.1 初始化列表的基本概念
• 定义:构造函数初始化的另一种方式,以冒号 : 开头,后跟逗号分隔的成员变量初始化列表,每个成员变量后接括号包裹的初始值或表达式。
• 语法:
cpp
class Date {
public:
Date(int year, int month, int day)
: _year(year), _month(month), _day(day) {}
private:
int _year;
int _month;
int _day;
};
• 规则:
每个成员变量在初始化列表中只能出现一次。
引用成员变量、const 成员变量、没有默认构造的类型成员变量,必须在初始化列表中初始化,否则编译报错。
C++11 支持在成员变量声明时给缺省值,用于未在初始化列表中初始化的成员。
1.2 初始化列表的执行逻辑
无论是否显式写初始化列表,每个构造函数都有初始化列表;无论是否在列表中初始化,每个成员变量都要走初始化列表。
-
显式在初始化列表初始化的成员:直接使用列表中给出的值。
-
未显式在初始化列表初始化的成员:
◦ 若在类中声明时有缺省值 → 使用缺省值初始化。
◦ 若没有缺省值:
内置类型:值未定义(可能是随机值或0,取决于编译器)。
自定义类型:调用其默认构造函数;若该类型无默认构造函数,编译报错。
引用/const/无默认构造的成员:必须在初始化列表初始化,否则编译报错。
1.3 初始化顺序
• 初始化列表中成员的初始化顺序,严格按照类中声明的顺序执行,与列表中书写顺序无关。
• 建议:声明顺序和初始化列表顺序保持一致,避免逻辑错误。
1.4 代码示例与错误分析
cpp
#include <iostream>
using namespace std;
class Time {
public:
Time(int hour) : _hour(hour) {
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date {
public:
// 构造函数
Date(int& x, int year = 1, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
, _t(12) // 初始化无默认构造的Time对象
, _ref(x) // 初始化引用成员
, _n(1) // 初始化const成员
{}
void Print() const {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
Time _t; // 无默认构造
int& _ref; // 引用
const int _n; // const
};
int main() {
int i = 0;
Date d1(i);
d1.Print();
return 0;
}
代码分析
- Time 类
◦ 定义了一个带参数的构造函数,因此没有默认构造函数。
◦ 这意味着创建 Time 对象时,必须提供一个 int 类型的参数。
- Date 类
◦ 包含三个内置类型成员 _year, _month, _day。
◦ 包含一个 Time 类型的成员 _t,它没有默认构造函数。
◦ 包含一个引用成员 int& _ref。
◦ 包含一个 const 成员 const int _n。
◦ 其构造函数使用了初始化列表,这是处理以下特殊成员的唯一方式:
◦ _t(12):显式调用 Time 的带参构造函数,解决了其无默认构造的问题。
◦ _ref(x):初始化引用成员,使其绑定到传入的参数 x。
◦ _n(1):初始化 const 成员。
- main 函数
◦ 定义了一个 int 变量 i。
◦ 创建 Date 对象 d1,并将 i 作为参数传入,满足了 Date 构造函数对引用参数的要求。
◦ 调用 Print 成员函数,输出日期。
运行结果
程序运行后,会先输出 Time(),然后输出 1-1-1。
⚠️注意:
• 若 Date 构造函数的初始化列表中遗漏 _t、_ref 或 _n,会导致编译错误:
◦ C2512: Time 没有合适的默认构造函数可用。
◦ C2530: Date::_ref 必须初始化引用。
◦ C2789: Date::_n 必须初始化常量限定类型的对象。
1.5 缺省值与初始化列表的优先级
cpp
class Date {
public:
Date() : _month(2) {
cout << "Date()" << endl;
}
void Print() const {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 声明时的缺省值
int _year = 1;
int _month = 1;
int _day;
Time _t = 1;
const int _n = 1;
int* _ptr = (int*)malloc(12);
};
int main() {
Date d1;
d1.Print(); // 输出: 1-2-随机值
return 0;
}
• _year:有缺省值 1,未在列表中初始化 → 使用缺省值。
• _month:在列表中显式初始化为 2 → 覆盖缺省值。
• _day:无缺省值,未在列表中初始化 → 值未定义(随机值)。
1.6 选择题解析
cpp
#include <iostream>
using namespace std;
class A {
public:
A(int a) : _a1(a), _a2(_a1) {}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2 = 2;
int _a1 = 2;
};
int main() {
A aa(1);
aa.Print();
}
- 初始化顺序规则(铁律)
成员变量初始化顺序 == 类里声明的顺序,和初始化列表顺序无关!
这里声明顺序是:
-
int _a2 = 2;
-
int _a1 = 2;
所以初始化顺序:
先 _a2 → 后 _a1
- 真正执行过程
构造函数初始化列表:
cpp
: _a1(a), _a2(_a1)
第一步:初始化 _a2
• 按照声明顺序,先跑 _a2(_a1),但此时 _a1 还没有初始化!它不会用你写的 =2 缺省值!
• 缺省值只有在初始化列表完全没写这个成员时才会生效。
→ 所以 _a2 被赋值为 _a1 未初始化的随机值
第二步:初始化 _a1
• 执行 _a1(a) , a=1 , 所以 _a1 = 1
输出结果:1 随机值
最重要结论
-
初始化顺序 = 声明顺序
-
初始化列表里写了的成员,声明时的缺省值 = 无效
-
用一个还没初始化的成员去初始化另一个 → 得到随机值
2. 类型转换
2.1 内置类型 → 类类型(隐式转换)
• 条件:需要一个以对应内置类型为参数的构造函数。
• 示例:
cpp
#include <iostream>
using namespace std;
class A {
public:
A(int a1) : _a1(a1) {}
private:
int _a1 = 1;
};
int main() {
A aa1 = 1; // 隐式转换:int -> A
const A& aa2 = 1;// 绑定到临时对象
}
- 构造函数是什么?
A(int a1) : _a1(a1) {}
• 这是单参数构造函数,因为只有一个参数,所以 C++ 允许它做隐式类型转换。
- 成员 _a1 = 1 是什么?
int _a1 = 1;
• 这是类内初始化(缺省值),但在初始化列表里写了 _a1(a1),所以 缺省值被覆盖,不生效。
- 第一行:A aa1 = 1; 这行是 C++ 非常经典的 隐式类型转换
过程: 1 是 int , 编译器发现 A 有构造函数 A(int) , 编译器偷偷做:A aa1 = A(1); 直接用 1 构造出一个 A 对象 , 这就叫:int → 类类型 的隐式转换
- 第二行:const A& aa2 = 1;
过程:
-
1 先隐式转换成临时对象 A(1)
-
这个临时对象 没有名字、生命周期很短
-
普通引用 A& 不能绑定临时对象
-
但 const A& 可以绑定临时对象,并且会延长临时对象的生命周期,直到 aa2 销毁
所以:const A& aa2 = 1; 等价于:const A& aa2 = A(1);
- 禁止隐式转换的方法:explicit
如果你不想让 A aa1 = 1 这种写法通过:explicit A(int a1) : _a1(a1) {}
加了 explicit:
• A aa1(1) ✅ 可以 A aa1 = 1 ❌ 报错(不能隐式转换) const A& aa2 = 1 ❌ 也报错
-
最核心总结
-
单参数构造函数默认支持隐式类型转换
-
A aa = 数值 等价于 A aa(数值)
-
临时对象不能用普通引用绑定,只能用 const 引用绑定
-
const A& = 数值 会延长临时对象生命周期
-
explicit 可以关闭隐式转换
这段代码的真正意义就是:演示单参数构造函数的隐式转换 + const 引用绑定临时对象
2.2 禁止隐式转换:explicit 关键字
• 在构造函数前加 explicit,则该构造函数不再支持隐式类型转换,只能显式调用。
cpp
explicit A(int a1) : _a1(a1) {}
// A aa1 = 1; // 编译错误
A aa1(1); // 正确,显式构造
2.3 多参数类型转换(C++11及以后)
• 支持多参数构造函数的隐式转换,使用列表初始化语法。
cpp
A(int a1, int a2) : _a1(a1), _a2(a2) {}
A aa3 = {2, 2}; // C++11 多参数隐式转换
编译器会自动理解为:A aa3 = A(2, 2);
同样可以被 const& 绑定 :const A& ref = {2,2}; 合法,因为会生成临时对象。
想禁止?加 explicit explicit A(int a1, int a2) {}
加了 explicit: A aa3(2,2) ✅ 可以 A aa3 = {2,2} ❌ 报错(不能隐式转换)
2.4 类类型 → 类类型(隐式转换)
• 需要相应的构造函数支持,例如用另一个类的对象作为参数。
cpp
class B {
public:
// 用 A 类型对象构造 B
B(const A& a) : _b(a.Get()) {}
private:
int _b = 0;
};
int main() {
A aa3 = {2, 2};
B b = aa3; // A → B 隐式转换
const B& rb = aa3;// 绑定临时对象
}
这是:类类型 A → 类类型 B 的隐式类型转换
触发条件只有一个:B 类有一个构造函数,参数是 A(或 const A&)
执行过程:B b = aa3;
编译器看到:右边是 A 类型,左边是 B 类型,发现 B 有构造函数:B(const A& a)
于是自动变成:B b = B(aa3); 这就叫:A 自动隐式转换成B
第二行:const B& rb = aa3;
过程: aa3 是 A, 隐式转换成 临时 B 对象, 普通引用 B& 不能绑临时对象, const B& 可以绑临时对象,并延长生命周期
等价于:const B& rb = B(aa3);
禁止这种转换?加 explicit explicit B(const A& a) : _b(a.Get()) {}
加完后: B b(aa3); ✅ 可以 B b = aa3; ❌ 报错(不能隐式转换)
超级总结
-
A → B 隐式转换:只要 B 有构造函数 B(const A&)
-
B b = a_obj 等价于 B b(a_obj)
-
const B& = a_obj 可以绑定临时对象
-
explicit 能关闭所有隐式转换
3. static 成员
3.1 静态成员变量
• 定义:用 static 修饰的成员变量,为所有类对象所共享,不属于某个具体对象,存放在静态区。
• 规则:
必须在类外进行初始化,类内仅声明。
不能在声明位置用缺省值初始化,因为静态成员不属于对象,不走构造函数初始化列表。
访问方式:类名::静态成员 或 对象.静态成员。
3.2 静态成员函数
• 定义:用 static 修饰的成员函数,没有 this 指针。
• 规则:
只能访问其他静态成员(变量或函数),不能访问非静态成员。
非静态成员函数,可以访问任意静态成员变量和静态成员函数。
访问方式与静态成员变量相同。
受 public/protected/private 访问限定符的限制。
3.3 代码示例:统计对象个数
cpp
#include <iostream>
using namespace std;
class A {
public:
A() { ++_scount; } // 普通构造
A(const A& t) { ++_scount; } // 拷贝构造
~A() { --_scount; } // 析构
static int GetACount() {
return _scount;
}
private:
static int _scount; // 静态成员变量(类内声明)
};
// 类外初始化静态变量
int A::_scount = 0;
int main() {
cout << A::GetACount() << endl; // 还没创建任何对象 → 输出 0
A a1, a2; // 调用 2 次普通构造 → _scount = 2
A a3(a1); // 调用拷贝构造 → _scount = 3
cout << A::GetACount() << endl; // 输出3
cout << a1.GetACount() << endl; // 输出3,静态函数也可以用对象调用,但还是访问同一个 _scount
return 0;
}
关键点:
- static int _scount;
静态成员变量,属于整个类,不属于某一个对象。 所有 A 对象共用这一个变量。
- 构造 & 拷贝构造都会 ++_scount
创建对象 → 计数+1 拷贝对象 → 计数+1
-
析构 --_scount 对象销毁 → 计数-1
-
静态成员函数 GetACount() 可以直接用 类名:: 调用,不需要对象。
考点:
-
静态成员变量属于类,所有对象共享一份。
-
必须 类外初始化。
-
静态成员函数 没有 this 指针,只能访问静态成员。
-
这是 C++ 实现 对象计数 的标准写法。
3.4 代码示例:用静态成员实现求和
求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
数据范围: 0<n≤200 进阶: 空间复杂度 O(1) ,时间复杂度 O(n)
写法一:
cpp
class Sum {
public:
Sum() {
_ret += _i;
++_i;
}
static int GetRet() {
return _ret;
}
private:
static int _i;
static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class Solution {
public:
int Sum_Solution(int n) {
// 变长数组,创建n个Sum对象,调用n次构造函数
Sum arr[n];
return Sum::GetRet();
}
};
• 原理:创建 n 个 Sum 对象,触发 n 次构造函数,累加 1 到 n 的和。
• _i 和 _ret 都是 静态成员,所有对象共用一份。
每创建一个 Sum 对象,就执行一次:_ret += _i; _i++;
Sum arr[n]; 会创建 n 个 Sum 对象 → 调用 n 次构造函数。
• 第1个对象:_ret = 1,_i = 2
• 第2个对象:_ret = 3,_i = 3
• 第3个对象:_ret = 6,_i = 4
• ......
• 第 n 个对象:_ret = 1+2+...+n
最后返回 _ret,就是答案。
写法二:
cpp
class Solution {
private:
static int _i; // 从 1 开始加
static int _ret; // 存放总和
// 嵌套类:真正负责累加的类
class Sum
{
public:
Sum()
{
_ret += _i; // 把当前数字加进去
++_i; // 下一个数
}
};
public:
int Sum_Solution(int n) {
Sum a[n]; // 核心:创建 n 个对象
return _ret;
}
};
// 静态成员变量初始化
int Solution::_i = 1;
int Solution::_ret = 0;
Sum a[n]; 这句话会:
-
定义一个长度为 n 的 Sum 数组
-
自动调用 n 次构造函数
-
每次构造都执行:_ret += _i; _i++;
这就相当于把循环:for(int i=1; i<=n; i++) ret += i; 偷偷用构造函数实现了
运行过程举例(n=3)
• 第 1 个对象:_ret=1, _i=2
• 第 2 个对象:_ret=3, _i=3
• 第 3 个对象:_ret=6, _i=4
返回 6,正确。
这个写法的 3 个考点
-
静态成员变量属于整个类,所有对象共享
-
嵌套类(内部类) 的使用
-
数组定义会自动调用 n 次构造函数→ 用构造函数实现循环逻辑
4. 友元(Friend)
4.1 核心概念
友元是一种突破类访问限定符封装的方式,分为:
• 友元函数:在函数声明前加 friend,放在类内部。
• 友元类:在类声明前加 friend,放在类内部。
4.2 核心特性
• 外部友元函数可访问类的私有和保护成员,但友元函数不是类的成员函数,只是一种声明。
• 友元函数可以在类定义的任何地方声明,不受访问限定符限制。
• 一个函数可以是多个类的友元函数。
• 友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。
• 友元类的关系是单向的,不具有交换性。例如:A类是B类的友元,但B类不是A类的友元。
• 友元类关系不能传递。例如:A是B的友元,B是C的友元,但A不是C的友元。
• 友元有时提供了便利,但会增加耦合度,破坏封装,所以友元不宜多用。
示例1:友元函数(同时作为两个类的友元)
cpp
#include <iostream>
using namespace std;
// 前置声明,否则A的友元函数声明时编译器不认识B
class B;
class A
{
// 友元声明
// 在 class A 中声明 func 为友元函数,允许它访问 A 的私有成员 _a1 和 _a2。
friend void func(const A& aa, const B& bb);
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
// 友元声明
// 在 class B 中也声明 func 为友元函数,允许它访问 B 的私有成员 _b1 和 _b2。
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 << aa._a1 << endl; // 访问A的私有成员
cout << bb._b1 << endl; // 访问B的私有成员
}
int main()
{
A aa;
B bb;
func(aa, bb); // 输出: 1 和 3
return 0;
}
核心知识点总结
- 友元函数的作用
突破类的封装限制,允许外部函数访问类的私有和保护成员。
友元函数不是类的成员函数,它只是被授予了访问权限。
- 友元函数的声明
在类内部使用 friend 关键字声明。 声明位置不受访问限定符(public/private/protected)的限制。
- 友元函数的特性
一个函数可以是多个类的友元。 友元关系是单向的,不具有交换性。 友元关系不能传递。
- 前置声明的必要性
当友元函数的参数涉及到另一个尚未完整定义的类时,必须先进行前置声明,否则编译器无法识别该类类型。
示例2:友元类
cpp
#include <iostream>
using namespace std;
class A
{
// 友元类声明:B是A的友元
friend class B;
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
void func1(const A& aa)
{
// B是A的友元,可直接访问A的私有成员
cout << aa._a1 << endl; // 输出: 1
cout << _b1 << endl; // 输出: 3
}
void func2(const A& aa)
{
cout << aa._a2 << endl; // 输出: 2
cout << _b2 << endl; // 输出: 4
}
private:
int _b1 = 3;
int _b2 = 4;
};
int main()
{
A aa;
B bb;
bb.func1(aa);
bb.func2(aa);
return 0;
}
class A
• 在 class A 中使用 friend class B; 声明 B 是 A 的友元类。
• 这意味着 B 的所有成员函数都可以直接访问 A 的私有成员 _a1 和 _a2。
• 友元关系是单向的:B 是 A 的友元,但 A 不是 B 的友元。
class B
• B 的成员函数 func1 和 func2 都接收一个 A 类型的常量引用 aa。
• 因为 B 是 A 的友元,所以可以直接访问 aa._a1 和 aa._a2。
• 同时,它们也可以访问 B 自己的私有成员 _b1 和 _b2。
核心知识点总结
- 友元类的作用
◦ 让一个类的所有成员函数都能访问另一个类的私有和保护成员。
◦ 本质上是对封装的一种"让步",用于解决特定场景下的访问需求。
- 友元类的特性
◦ 单向性:A 声明 B 是友元,B 可以访问 A,但 A 不能访问 B。
◦ 不可传递性:如果 A 是 B 的友元,B 是 C 的友元,A 并不自动成为 C 的友元。
◦ 不继承性:友元关系不能被继承。
- 友元类 vs 友元函数
◦ 友元函数:单个函数获得访问权限。
◦ 友元类:整个类的所有成员函数都获得访问权限。
5. 内部类(Nested Class)
5.1 核心概念
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
• 内部类是一个独立的类,跟定义在全局相比,它只是受外部类的类域限制和访问限定符限制。
• 外部类定义的对象中不包含内部类。
• 内部类默认是外部类的友元类。
• 内部类本质也是一种封装。当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类。如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
示例1:内部类作为外部类的友元
cpp
#include <iostream>
using namespace std;
class A
{
private:
static int _k;
int _h = 1;
public:
// B默认是A的友元
class B
{
public:
void foo(const A& a)
{
// 可直接访问A的私有成员
cout << _k << endl; // OK,访问静态成员
cout << a._h << endl; // OK,访问非静态成员
}
int _b1;
};
};
// 静态成员类外初始化,静态成员变量 _k 必须在类外进行初始化。
int A::_k = 1;
int main()
{
// 打印外部类A的大小,不包含内部类B
cout << sizeof(A) << endl; // 输出: 4(仅包含int _h)
// 定义内部类对象
A::B b;
A aa;
b.foo(aa); // 输出: 1 和 1
return 0;
}
• 内部类 B 定义在外部类 A 的内部。
• 在 C++ 中,定义在类内部的类(内部类)默认是外部类的友元,因此 B 的成员函数可以直接访问 A 的私有成员。
• foo 函数中:
cout << _k << endl;:直接访问 A 的静态私有成员 _k。
cout << a._h << endl;:通过 A 对象 a 访问其非静态私有成员 _h。
• sizeof(A):外部类 A 的大小只包含其非静态成员 int _h,不包含内部类 B 的任何成员,因此输出为 4 字节。
• A::B b;:使用 外部类名::内部类名 的方式来定义内部类的对象。
• b.foo(aa);:调用内部类的成员函数,成功访问外部类的私有成员。
核心知识点总结
- 内部类的定义
◦ 定义在另一个类(外部类)内部的类,称为内部类或嵌套类。
◦ 内部类是一个独立的类,其对象的大小不包含在外部类对象中。
- 内部类的访问权限
◦ 内部类默认是外部类的友元类,因此可以直接访问外部类的所有成员(包括私有和保护成员)。
◦ 反之,外部类不能直接访问内部类的私有成员。
- 内部类的使用方式
◦ 定义内部类对象时,需要使用作用域解析符 ::,格式为:外部类名::内部类名 对象名;。
- 内存布局
◦ 外部类对象的大小只包含其自身的非静态成员变量,与内部类无关。
示例2:内部类实现 1+2+...+n(无循环/乘除)
cpp
class Solution {
// 内部类Sum
class Sum
{
public:
Sum()
{
// 每次构造,累加并递增
_ret += _i;
++_i;
}
};
static int _i;
static int _ret;
public:
int Sum_Solution(int n) {
// 变长数组,创建n个Sum对象,调用n次构造函数
Sum arr[n];
return _ret;
}
};
// 静态成员初始化
int Solution::_i = 1;
int Solution::_ret = 0;
原理:
• Sum arr[n]; 会创建 n 个 Sum 对象,自动调用 n 次构造函数。
• 每次构造都执行 _ret += _i; ++_i;,从而实现从1到n的累加。
6. 匿名对象(Anonymous Object)
6.1 核心概念
• 用 类型(实参) 定义出来的对象叫做匿名对象。
• 相比 类型 对象名(实参) 定义的有名对象,匿名对象没有名字。
• 匿名对象的生命周期只在当前一行,一般临时定义一个对象当前用一下即可,下一行就会自动调用析构函数。
6.2 示例:匿名对象的使用
cpp
// 定义了一个类 A,包含一个带默认参数的构造函数和析构函数,用于打印构造和析构的日志。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
// 定义了一个 Solution 类,用于演示匿名对象调用成员函数。
class Solution {
public:
int Sum_Solution(int n) {
//...
return n;
}
};
int main()
{
// 有名对象
A aa1;
// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
// A aa1();
// 定义匿名对象,生命周期只在当前行
A(); // 构造后立即析构
A(1); // 构造后立即析构
// 有名对象,定义了一个有名对象 aa2,调用带参数的构造函数。
A aa2(2);
// 匿名对象在这样场景下很好用,临时调用成员函数
Solution().Sum_Solution(10);
return 0;
}
使用 类型() 或 类型(实参) 的形式定义匿名对象。
• 匿名对象没有名字,其生命周期仅限于当前语句,语句执行完毕后立即调用析构函数。
• 因此,A(); 和 A(1); 都会先打印构造信息,紧接着打印析构信息。
Solution().Sum_Solution(10);创建一个 Solution 类型的匿名对象,并立即调用其 Sum_Solution 成员函数。这种方式常用于临时调用成员函数,无需单独定义一个有名对象。
核心知识点总结
- 匿名对象的定义
语法:类名() 或 类名(构造参数)。
特点:没有名字,生命周期仅在当前语句,语句结束后立即销毁。
- 匿名对象的用途
作为函数实参,避免创建临时有名对象。
临时调用成员函数,如 Solution().Sum_Solution(10);。
作为表达式的一部分,例如 A obj = A(5);(可能被编译器优化为直接构造)。
- "最令人头疼的解析"
当代码 A aa1(); 出现时,编译器优先将其解析为函数声明,而非对象定义。
若要定义一个使用默认构造函数的对象,应写为 A aa1;。
运行结果:
cpp
A(int a) // A aa1;
A(int a) // A();
~A() // A() 语句结束,匿名对象析构
A(int a) // A(1);
~A() // A(1) 语句结束,匿名对象析构
A(int a) // A aa2(2);
A(int a) // Solution() 构造匿名对象
~A() // Solution() 语句结束,匿名对象析构
~A() // main函数结束,aa2 析构
~A() // main函数结束,aa1 析构
7. 对象拷贝时的编译器优化
7.1 核心概念
• 现代编译器为了尽可能提高程序效率,在不影响正确性的情况下,会尽可能减少一些传参和传返回值过程中可以省略的拷贝。
• C++标准并没有严格规定如何优化,各个编译器会根据情况自行处理。
• 当前主流的相对新一点的编译器,对于连续一个表达式步骤中的连续拷贝会进行合并优化;有些更新"激进"的编译器还会进行跨行表达式的合并优化。
• 在Linux下,可以用 g++ test.cpp -fno-elide-constructors 的方式关闭构造相关的优化,方便观察原始行为。
7.2 示例代码与优化分析
cpp
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0)
:_a1(a)
{
cout << "A(int a)" << 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 = 1;
};
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;
// 传值返回
// 不优化的情况下传值返回,编译器会生成一个拷贝返回对象的临时对象作为函数调用表达式的返回值
// 无优化 (vs2019 debug)
// 一些编译器会优化得更厉害,进行跨行合并优化,将构造的局部对象aa和拷贝构造的临时对象优化为直接构造 (vs2022 debug)
f2();
cout << endl;
// 返回时一个表达式中,连续拷贝构造 + 拷贝构造 -> 优化为一个拷贝构造 (vs2019 debug)
// 一些编译器会优化得更厉害,将构造的局部对象aa和拷贝的临时对象和接收返回值对象aa2优化为一个直接构造 (vs2022 debug)
A aa2 = f2();
cout << endl;
// 一个表达式中,开始构造,中间拷贝构造 + 赋值重载 -> 无法优化 (vs2019 debug)
// 一些编译器会优化得更厉害,进行跨行合并优化,将构造的局部对象aa和拷贝临时对象合并为一个直接构造 (vs2022 debug)
aa1 = f2();
cout << endl;
return 0;
}
- 传值传参部分
① A aa1; f1(aa1);
• 无优化:
A(int a) // 构造 aa1
A(const A& aa) // 拷贝构造 f1 的形参
~A() // 销毁 f1 的形参
• 有优化:
这一段没有优化空间,输出与无优化一致。
② f1(1);
• 无优化:
A(int a) // 用 1 构造临时对象
A(const A& aa) // 拷贝构造 f1 的形参
~A() // 销毁临时对象
~A() // 销毁 f1 的形参
• 有优化(合并构造+拷贝为直接构造):
A(int a) // 直接构造 f1 的形参
~A() // 销毁 f1 的形参
③ f1(A(2));
• 无优化:
A(int a) // 构造临时对象 A(2)
A(const A& aa) // 拷贝构造 f1 的形参
~A() // 销毁临时对象
~A() // 销毁 f1 的形参
• 有优化(合并构造+拷贝为直接构造):
A(int a) // 直接构造 f1 的形参
~A() // 销毁 f1 的形参
- 传值返回部分(核心差异)
① f2();
• 无优化:
A(int a) // 构造 f2 内的局部对象 aa
A(const A& aa) // 拷贝构造返回值临时对象
~A() // 销毁局部对象 aa
~A() // 销毁返回值临时对象
• 有优化(NRVO):
A(int a) // 直接在返回值临时对象的内存上构造 aa
~A() // 销毁返回值临时对象
② A aa2 = f2();
• 无优化:
A(int a) // 构造 f2 内的局部对象 aa
A(const A& aa) // 拷贝构造返回值临时对象
~A() // 销毁局部对象 aa
A(const A& aa) // 用临时对象拷贝构造 aa2
~A() // 销毁返回值临时对象
• 有优化(RVO/NRVO):
A(int a) // 直接在 aa2 的内存上构造对象
③ aa1 = f2();
• 无优化:
A(int a) // 构造 f2 内的局部对象 aa
A(const A& aa) // 拷贝构造返回值临时对象
~A() // 销毁局部对象 aa
A& operator=(const A& aa) // 临时对象赋值给 aa1
~A() // 销毁返回值临时对象
• 有优化(NRVO):
A(int a) // 直接在返回值临时对象的内存上构造 aa
A& operator=(const A& aa) // 临时对象赋值给 aa1
~A() // 销毁返回值临时对象
- 程序结束时
• 无优化:
~A() // 销毁 aa2
~A() // 销毁 aa1
• 有优化:
输出与无优化一致。
总结表
|------------|-------------------|-----------------------|----------------|
| 代码片段 | 无优化(VS2019 Debug) | 有优化(VS2022 / GCC -O2) | 优化点 |
| f1(1) | 构造→拷贝→析构×2 | 构造→析构 | 合并临时对象与形参 |
| f1(A(2)) | 构造→拷贝→析构×2 | 构造→析构 | 合并临时对象与形参 |
| f2() | 构造→拷贝→析构×2 | 构造→析构 | NRVO,省略局部对象拷贝 |
| A aa2=f2() | 构造→拷贝×2→析构×3 | 构造 | RVO,直接在目标对象上构造 |
| aa1=f2() | 构造→拷贝→赋值→析构×2 | 构造→赋值→析构 | NRVO,省略局部对象拷贝 |
优化场景总结
|--------------|---------------------------------|-------------|
| 场景 | 无优化行为 | 优化后行为 |
| f1(1) | 构造临时对象 -> 拷贝构造形参 | 直接构造形参 |
| f1(A(2)) | 构造临时对象 -> 拷贝构造形参 | 直接构造形参 |
| A aa2 = f2() | 构造局部对象 -> 拷贝构造临时对象 -> 拷贝构造aa2 | 直接构造aa2 |
| aa1 = f2() | 构造局部对象 -> 拷贝构造临时对象 -> 赋值给aa1 | 直接构造临时对象后赋值 |
以上就是C++类与对象中初始化列表、隐式类型转换、static成员、友元、内部类、匿名对象以及编译器优化的核心知识点。熟练掌握这些内容,不仅能应对各类面试考点,更能在实际开发中写出更高效、更健壮的C++代码。