在 C++ 面向对象编程中,赋值操作 是对象间数据传递的核心行为,而 ** 浅赋值(浅拷贝)与 深赋值(深拷贝)是处理对象成员(尤其是指针成员)的两种核心机制。二者的核心区别在于:是否为指针成员分配独立的内存空间,错误使用浅赋值会导致内存泄漏、重复释放、数据污染等严重问题。
本文将从概念、原理、代码示例、应用场景四个维度,彻底讲清浅赋值与深赋值的区别与实践。
一、基础概念:什么是赋值操作?
C++ 中,当使用=运算符将一个对象赋值给另一个同类型对象时,会调用拷贝赋值运算符。
- 若我们不手动定义,编译器会自动生成一个默认拷贝赋值运算符,这是浅赋值的源头;
- 浅赋值与深赋值,本质是拷贝赋值运算符对对象成员的两种不同处理方式。
二、浅赋值(浅拷贝)
1. 核心定义
浅赋值是逐字节拷贝 对象的成员变量:对于普通成员(int、char 等)直接拷贝值;对于指针成员 ,仅拷贝指针存储的内存地址 ,不拷贝指针指向的实际数据。
2. 核心特点
- 两个对象的指针成员指向同一块内存空间;
- 操作简单,编译器自动生成的默认赋值运算符就是浅赋值;
- 致命缺陷:修改一个对象的指针数据,另一个对象会同步变化;对象销毁时,同一块内存会被重复释放,导致程序崩溃。
3. 代码示例(浅赋值的问题)
cpp
#include <iostream>
#include <cstring>
using namespace std;
// 测试类:包含指针成员
class Student {
public:
char* name; // 指针成员(核心风险点)
int age;
// 构造函数:为指针分配内存
Student(const char* n, int a) {
age = a;
// 为name指针分配堆内存
name = new char[strlen(n) + 1];
strcpy(name, n);
}
// 析构函数:释放指针内存
~Student() {
// 浅赋值下,同一块内存会被释放两次!
delete[] name;
cout << "对象已销毁,内存释放成功" << endl;
}
};
int main() {
Student s1("张三", 20);
Student s2("李四", 18);
// 编译器自动生成的默认赋值运算符 → 浅赋值
s2 = s1;
// 问题1:修改s1的name,s2的name同步变化(指向同一块内存)
strcpy(s1.name, "王五");
cout << "s1姓名:" << s1.name << endl; // 输出:王五
cout << "s2姓名:" << s2.name << endl; // 输出:王五(预期应为张三)
// 问题2:程序结束时,s1和s2的析构函数会重复释放同一块内存 → 程序崩溃
return 0;
}
4. 浅赋值适用场景
仅当类中没有指针、引用等动态分配的成员变量时,浅赋值完全安全(如仅包含 int、double、普通数组的类)。
三、深赋值(深拷贝)
1. 核心定义
深赋值是手动重载拷贝赋值运算符 :对于指针成员,先分配一块独立的新内存 ,再将原指针指向的数据拷贝到新内存中。最终两个对象的指针成员指向不同的内存空间。
2. 核心特点
- 两个对象拥有独立的指针数据,互不干扰;
- 避免内存重复释放、数据污染问题;
- 需要手动编写拷贝赋值运算符,不能依赖编译器默认实现。
3. 代码示例(深赋值解决问题)
cpp
#include <iostream>
#include <cstring>
using namespace std;
class Student {
public:
char* name;
int age;
// 构造函数
Student(const char* n, int a) {
age = a;
name = new char[strlen(n) + 1];
strcpy(name, n);
}
// 析构函数
~Student() {
delete[] name;
cout << "对象已销毁,内存释放成功" << endl;
}
// 【关键】手动重载拷贝赋值运算符 → 实现深赋值
Student& operator=(const Student& s) {
// 1. 防止自赋值(如 s1 = s1)
if (this == &s) {
return *this;
}
// 2. 释放当前对象原有的指针内存(避免内存泄漏)
delete[] this->name;
// 3. 分配新内存,拷贝数据(深赋值核心步骤)
this->age = s.age;
this->name = new char[strlen(s.name) + 1];
strcpy(this->name, s.name);
// 4. 返回当前对象(支持连续赋值:s1 = s2 = s3)
return *this;
}
};
int main() {
Student s1("张三", 20);
Student s2("李四", 18);
// 调用重载的赋值运算符 → 深赋值
s2 = s1;
// 验证:修改s1的数据,s2不受影响
strcpy(s1.name, "王五");
cout << "s1姓名:" << s1.name << endl; // 输出:王五
cout << "s2姓名:" << s2.name << endl; // 输出:张三(符合预期)
// 程序结束:两个对象的指针指向不同内存,分别释放,无崩溃
return 0;
}
4. 深赋值核心步骤(必记)
重载拷贝赋值运算符时,必须遵循 4 步标准流程:
- 判断自赋值:避免自己释放自己的内存;
- 释放原有内存:防止当前对象的指针内存泄漏;
- 分配新内存 + 拷贝数据:实现真正的深赋值;
- 返回当前对象引用:支持连续赋值语法。
四、浅赋值 vs 深赋值:核心对比表
表格
| 特性 | 浅赋值(浅拷贝) | 深赋值(深拷贝) |
|---|---|---|
| 实现方式 | 编译器自动生成,无需手动编写 | 必须手动重载拷贝赋值运算符 |
| 指针成员处理 | 仅拷贝内存地址,共享数据 | 分配新内存,拷贝数据,独立存储 |
| 数据安全性 | 低,易出现数据污染、重复释放 | 高,对象数据完全独立 |
| 内存开销 | 小 | 稍大(需要分配独立内存) |
| 适用场景 | 无指针 / 动态内存的类 | 包含指针、动态数组、字符串的类 |
五、关键总结
- 编译器默认的赋值都是浅赋值 ,只要类中有指针成员、动态分配内存 (
new/malloc),必须手动实现深赋值; - 浅赋值的核心风险:指针共享内存,导致数据篡改、内存重复释放;
- 深赋值的核心逻辑:为指针分配独立内存,拷贝实际数据,从根源解决浅赋值问题;
- 面向对象编程中,遵循三法则:如果类需要手动实现析构函数、拷贝构造函数、拷贝赋值运算符中的任意一个,通常三个都需要手动实现(核心都是为了处理深拷贝)。
总结
- 浅赋值逐字节拷贝,指针共享内存,仅适用于无动态成员的类;
- 深赋值手动重载赋值运算符,指针独立分配内存,解决动态内存安全问题;
- 含指针 / 动态内存的类,禁止使用编译器默认浅赋值,必须实现深赋值。