【C++重要!!!】赋值与初始化的区别
在C++中,区分赋值(assignment)和初始化(initialization)是一个非常重要的问题,因为它们在语义、调用时机和涉及的函数上有着本质的区别。
1. 定义
-
初始化(Initialization):
- 指在对象创建时为其赋予初始值的过程。
- 发生在对象生命周期的开始,通常由构造函数(包括默认构造函数、拷贝构造函数等)负责。
- 语法上常见于声明时使用
=
或括号。
-
赋值(Assignment):
- 指在对象已经存在的情况下,修改其值的过程。
- 发生在对象创建之后,通常由赋值运算符(
operator=
)负责。 - 语法上表现为对已声明的对象使用
=
。
2. 主要区别
特性 | 初始化(Initialization) | 赋值(Assignment) |
---|---|---|
时机 | 对象创建时 | 对象已存在后 |
涉及的函数 | 构造函数(默认、拷贝、移动等) | 赋值运算符(operator= ) |
资源管理 | 直接构造,无需清理旧资源 | 需要先清理旧资源,再赋值新值 |
语法示例 | T x = y; 或 T x(y); | x = y; |
性能影响 | 通常更高效(无需处理旧状态) | 可能涉及资源释放和重新分配 |
3. 如何区分:代码中的表现
在代码中,区分赋值和初始化主要看对象是否已经存在以及语法上下文。
(1)初始化示例
Test t1; // 默认初始化,调用默认构造函数
Test t2 = Test(); // 初始化,调用默认构造函数(可能优化为直接构造)
Test t3(t1); // 初始化,调用拷贝构造函数
Test t4 = t1; // 初始化,调用拷贝构造函数(不是赋值!)
Test t5 = getObj(); // 初始化,调用拷贝构造函数或移动构造函数
- 这些都是在对象声明时进行的操作,触发构造函数。
- 注意:
Test t4 = t1;
看起来像赋值,但它是拷贝初始化,等价于Test t4(t1);
。
(2)赋值示例
Test t1; // 默认构造
t1 = Test(); // 赋值,调用赋值运算符
Test t2 = t1; // 初始化(拷贝构造)
t2 = t1; // 赋值,调用赋值运算符
t1 = getObj(); // 赋值,调用赋值运算符
- 这些操作发生在对象已经构造之后,修改其状态,触发
operator=
。
4. 结合 Test
类分析
让我们用您提供的 Test
类来具体说明:
#include <iostream>
using namespace std;
class Test {
public:
Test() : m_num(new int(100)) {
cout << "construct: my name is jerry" << endl;
}
Test(const Test& a) : m_num(new int(*a.m_num)) {
cout << "copy construct: my name is tom" << endl;
}
~Test() {
delete m_num;
cout << "destruct Test class ..." << endl;
}
int* m_num;
};
Test getObj() {
Test t;
return t;
}
(1)初始化场景
int main() {
Test t = getObj(); // 初始化
cout << "t.m_num: " << *t.m_num << endl;
return 0;
}
- 行为:
Test t = getObj();
是初始化。 - 调用:触发拷贝构造函数(或因 RVO 优化直接构造)。
- 输出(无 RVO):
construct: my name is jerry
copy construct: my name is tom // 返回时的临时对象
destruct Test class ... // getObj() 中的 t
copy construct: my name is tom // main 中的 t
destruct Test class ... // 临时对象
t.m_num: 100
destruct Test class ... // main 中的 t
- 原因:
t
在声明时被构造,没有先存在再修改的过程。
(2)赋值场景
int main() {
Test t; // 默认构造
t = getObj(); // 赋值
cout << "t.m_num: " << *t.m_num << endl;
return 0;
}
- 行为:
Test t;
是初始化,调用默认构造函数。t = getObj();
是赋值,调用默认赋值运算符(浅拷贝)。
- 输出(假设未定义
operator=
):construct: my name is jerry // Test t construct: my name is jerry // getObj() 中的 t copy construct: my name is tom // 返回时的临时对象 destruct Test class ... // getObj() 中的 t 析构 destruct Test class ... // 临时对象析构 // 此时 t.m_num 是野指针,访问未定义行为
- 问题:默认赋值运算符执行浅拷贝,导致
t.m_num
指向已释放的内存。
如果定义赋值运算符:
Test& operator=(const Test& a) {
if (this != &a) {
delete m_num;
m_num = new int(*a.m_num);
}
return *this;
}
输出变为:
construct: my name is jerry // Test t
construct: my name is jerry // getObj() 中的 t
copy construct: my name is tom // 返回时的临时对象
destruct Test class ... // getObj() 中的 t 析构
destruct Test class ... // 临时对象析构
t.m_num: 100
destruct Test class ... // t 析构
5. 区分的关键点
- 语法位置:
- 声明时带
=
或括号(如T x = y;
):初始化。 - 已声明对象后用
=
(如x = y;
):赋值。
- 声明时带
- 对象状态:
- 初始化:对象尚未存在,直接构造。
- 赋值:对象已有状态,需要清理旧资源。
- 调用的函数:
- 初始化:构造函数(
Test()
、Test(const Test&)
等)。 - 赋值:
operator=
。
- 初始化:构造函数(
6. 常见误区
T x = y;
不是赋值:- 很多人看到
=
就认为是赋值,但这是拷贝初始化,调用拷贝构造函数。 - 只有在
x
已存在时,x = y;
才是赋值。
- 很多人看到
- 编译器优化:
- RVO 或拷贝省略可能让初始化看起来没有调用拷贝构造函数,但逻辑上仍是初始化。
7. 总结
- 初始化:对象创建时,使用构造函数(
Test t = getObj();
)。 - 赋值:对象存在后修改,使用赋值运算符(
t = getObj();
)。 - 在您的原始代码中,只有初始化(
Test t = getObj();
),所以不需要赋值运算符也能正常运行。