① 程序设计基础 — 指针进阶课 2
new / delete、悬空指针。指针难管,是因为“一块内存可能被好几个名字指着”。new 出来的指针,拷贝对象时到底复制了什么?析构时会不会删两次?答案就是深浅拷贝和三法则。
new 成员就自己写三个函数,别用默认。int a = 10;
int b = a; // b 是 a 的副本,改 b 不影响 a
string s1 = "hello";
string s2 = s1; // s2 是副本,各自管自己的内存
struct Point { int x, y; };
Point p1 = {1, 2};
Point p2 = p1; // 逐个成员复制:p2.x=1, p2.y=2
对于没有指针成员的类,编译器自动生成的“逐成员复制”完全够用。问题出在类里有指针的时候。
void f(string s) 一调用就复制整个字符串。改成 void f(const string& s) 只传引用,不复制 —— 这就是 0007 里说的“引用传大对象”。
class IntArray {
public:
int* data; // 指向堆上的数组
int size;
IntArray(int n) : size(n) { data = new int[n](); } // 全初始化为 0
~IntArray() { delete[] data; }
};
int main() {
IntArray a(5);
a.data[0] = 100;
IntArray b = a; // 默认拷贝构造:只复制指针
cout << b.data[0]; // 100 —— b.data 和 a.data 是同一块内存!
}
这就是浅拷贝:只复制了指针的值(地址),没有复制指针指向的那块数据。两个对象共用一块堆内存。
main 结束时先析构 b,delete[] b.data 把内存还了;再析构 a,又 delete[] a.data 同一个地址 —— 重复释放,程序崩溃。new 的,谁 delete。现在两个人以为自己都该 delete 同一块内存。
class IntArray {
public:
int* data;
int size;
IntArray(const IntArray& other) { // 拷贝构造
size = other.size;
data = new int[size]; // 自己申请一块新内存
for (int i = 0; i < size; i++)
data[i] = other.data[i]; // 逐个复制内容(深拷贝)
}
};
const IntArray&:按值传参会无限递归(复制形参又要调用拷贝构造);IntArray b = a;、f(a)(按值传参)、return a;;data 指向的内容也复制一份,才叫深拷贝;只复制指针就是浅拷贝。
class IntArray {
public:
int* data;
int size;
IntArray& operator=(const IntArray& other) {
if (this == &other) return *this; // ① 自己给自己赋值
delete[] data; // ② 先释放自己原有的内存
size = other.size; // ③ 再深拷贝
data = new int[size];
for (int i = 0; i < size; i++)
data[i] = other.data[i];
return *this; // ④ 支持链式赋值 a = b = c
}
};
a = a 时如果不先判断 this == &other,就会把自己的数据删掉再复制自己;delete[] 再 new;反过来如果 new 失败,对象就处于半破坏状态;IntArray& 加 return *this;,否则 a = b = c 链式赋值不成立。
只要类里管理了裸指针(new 出来的),下面三个函数就必须一起写:
| 函数 | 什么时候被调用 | 你该做什么 |
|---|---|---|
析构函数 ~IntArray() | 对象销毁时 | 释放你 new 的内存 |
拷贝构造 IntArray(const IntArray&) | 用对象初始化另一个对象 | 深拷贝,申请自己的内存 |
拷贝赋值 operator= | 两个已有对象之间赋值 | 自赋值判断 + 先释放旧内存再深拷贝 |
vector<int> 或 string 代替裸指针。它们自己实现了三法则,你什么都不用写 —— 这也是为什么标准库容器比裸数组好。
class IntArray {
private:
int* data;
int size;
public:
IntArray(int n) : size(n) { data = new int[n](); }
IntArray(const IntArray& other) : size(other.size) {
data = new int[size];
for (int i = 0; i < size; i++) data[i] = other.data[i];
}
IntArray& operator=(const IntArray& other) {
if (this == &other) return *this;
delete[] data;
size = other.size;
data = new int[size];
for (int i = 0; i < size; i++) data[i] = other.data[i];
return *this;
}
~IntArray() { delete[] data; }
int& operator[](int i) { return data[i]; }
int getSize() const { return size; }
};
下面用它跑一遍:
int main() {
IntArray a(3);
a[0] = 10; a[1] = 20; a[2] = 30;
IntArray b = a; // 拷贝构造:深拷贝
b[0] = 99; // 改 b 不影响 a
IntArray c(1);
c = a; // 拷贝赋值:深拷贝
c[1] = 77;
cout << a[0] << " " << b[0] << " " << c[1];
// 输出:10 99 77 —— 三个对象互不干扰
}
new 的裸指针,就别用默认拷贝:默认的只复制地址,两个对象共用一块内存。const 引用:写成按值 IntArray(IntArray other) 会无限递归。if (this == &other) return *this;,不然后面 delete[] 会删掉自己的数据。new 就写三个”:析构、拷贝构造、拷贝赋值,缺一个就会在某个场景下退化成浅拷贝。new 出来的指针成员,用默认拷贝会怎样?operator= 里为什么要先判断 this == &other?MyString 类,内部用 char* data; 存字符串。new char[...])、析构函数、拷贝构造、拷贝赋值。MyString a("hello"); MyString b = a; b.data[0] = 'H'; 打印两者,确认互不影响。
char* data 换成 vector<char>,然后把三法则全删掉。delete[],先怀疑浅拷贝留下的重复指针。const 类名&。operator= 里的顺序对不对:判断自赋值 → delete[] → new → 逐个复制 → return *this。cout << "copy ctor";,看看到底哪个函数被调用了、调了几次。类名(const 类名&),在用对象初始化新对象时调用。operator=:先判自赋值,再释放旧内存,再深拷贝,返回 *this。vector / string 托管数据,三法则一个都不用写。sort、find、set、map 都写好了,一行调用就能用;这一课还会顺带讲清楚“迭代器”到底是什么东西。