代码仓库shanchuann/CPP-Learninng

在C++面向对象编程中,拷贝构造函数是一种特殊的构造函数,用于用已存在的对象创建新对象。它是对象生命周期管理的核心机制之一,理解拷贝构造函数的原理和使用场景,对编写安全、高效的C++代码至关重要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Int {
private:
int value;
public:
Int() :value(0) { std::cout << "Default create Int" << "at" << this << std::endl; }
Int(int v) :value(v) { std::cout << "Create Int with value " << value << " at " << this << std::endl; }
~Int() { std::cout << "Destroy Int with value " << value << " at " << this << std::endl; }
};

int main() {
Int a(10);
Int b(20);
Int c(b); //拷贝构造
return 0;
}

image-20251029153158801

定义与作用

拷贝构造函数是类的特殊成员函数,其核心作用是:当需要通过一个已存在的对象(源对象)初始化一个新对象(目标对象)时,自动被调用以完成新对象的创建

1
2
Int b(20);
Int c(b); // 用已存在的对象b初始化新对象c,触发拷贝构造

这里Int c(b);就是通过对象b创建新对象c,此时会调用Int类的拷贝构造函数。

声明形式与语法规则

拷贝构造函数的声明有严格的语法要求,其标准形式为:

1
2
类名(const 类名& 源对象引用);
Int(const Int& it) :value(it.value) { }
  1. 参数必须是本类对象的引用
    若参数为传值方式(而非引用),会导致“拷贝构造函数的无限递归调用”。因为传值参数本身需要用源对象拷贝初始化,这又会触发拷贝构造函数,形成递归死循环。

  2. 通常使用const修饰参数
    const保证源对象在拷贝过程中不会被修改,符合“只拷贝不修改源对象”的语义,同时也允许用const对象初始化新对象。

  3. 无返回值
    与普通构造函数一样,拷贝构造函数没有返回值,且函数名与类名相同。

当我们不使用&作为形参类型时,调用拷贝构造时,Int类型会被递归调用

1
2
3
4
5
class Int{
private:
int val;
Int xd;
};

当我们定义Int a

会有

1
2
3
4
Int a;
a.val
a.xd -> .val
.xd

因此在类型中定义他自身是不允许的。但是当定义为Int *xd;时,不会引起无限的递归活动。

初始化列表

构造函数初始化列表是一种特殊的语法,用于在对象创建时直接初始化类的成员变量,而非在构造函数体内通过赋值操作初始化。

在普通函数中不能使用该方案,构造函数的初始化列表是用于对象创建时初始化成员(仅在成员诞生时执行一次)的专属语法,而普通函数调用时由于构造函数,对象已存在、成员已完成初始化,其对成员的操作只能是赋值而非初始化,且 C++ 语法明确限制普通函数不能使用这种初始化列表结构。

默认拷贝构造函数

当类中没有显式定义拷贝构造函数时,编译器会自动生成一个“默认拷贝构造函数”。其行为是对源对象的成员变量进行逐个拷贝(浅拷贝),即:

  • 对于基本类型成员(如intdouble),直接复制值;
  • 对于类类型成员,调用该成员的拷贝构造函数;
  • 对于指针成员,仅复制指针的地址(而非指向的内容)。

调用场景

除了显式用已存在对象初始化新对象(如Int c(b);),拷贝构造函数还会在以下场景被自动调用:

1. 用赋值语法初始化新对象

注意:=初始化新对象时,本质是拷贝构造而非赋值。例如:

1
Int d = b; // 等价于Int d(b); 调用拷贝构造函数

这里d是新创建的对象,=表示“初始化”而非“赋值”(赋值针对已存在的对象)。

2. 函数参数为类对象(传值传递)

当函数参数是类对象且以传值方式传递时,编译器会通过拷贝构造函数生成一个“参数副本”(形参)。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Int {
private:
int value;
public:
Int() :value(0) { std::cout << "Default create Int" << "at" << this << std::endl; }
Int(int v) :value(v) { std::cout << "Create Int with value " << value << " at " << this << std::endl; }
Int(Int& it) :value(it.value) { std::cout << "Copy create Int with value " << value << " at " << this << std::endl; }
~Int() { std::cout << "Destroy Int with value " << value << " at " << this << std::endl; }
void Print() const {
std::cout << "Print value: " << value << std::endl;
}
};
void func(Int it) {
it.Print();
}
int main() {
Int a{ 10 };
func(a);
}
1
2
3
4
5
Create Int with value 10 at 00CFF8C8
Copy create Int with value 10 at 00CFF7E4
Print value: 10
Destroy Int with value 10 at 00CFF7E4
Destroy Int with value 10 at 00CFF8C8

3. 函数返回值为类对象(传值返回)

当函数返回类对象时,编译器会通过拷贝构造函数生成一个“返回值临时对象”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Int {
private:
int value;
public:
Int() :value(0) { std::cout << "Default create Int" << "at" << this << std::endl; }
Int(int v) :value(v) { std::cout << "Create Int with value " << value << " at " << this << std::endl; }
Int(Int& it) :value(it.value) { std::cout << "Copy create Int with value " << value << " at " << this << std::endl; }
~Int() { std::cout << "Destroy Int with value " << value << " at " << this << std::endl; }

void Print() const {
std::cout << "Print value: " << value << std::endl;
}
};
Int func(Int it) {
it.Print();
return it;
}
int main() {
Int a{ 10 };
func(a);
Int c = 0;
c = func(a);
}

当我们想要定义void func(Int it)的返回类型为Int并向Int c返回it值一般来说不被允许,因为it的生存期仅存在于func()函数内

image-20251029154618727

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Create Int with value 10 at 010FF8AC
Copy create Int with value 10 at 010FF790
Print value: 10
Copy create Int with value 10 at 010FF7C8
Destroy Int with value 10 at 010FF790
Destroy Int with value 10 at 010FF7C8
Create Int with value 0 at 010FF8A0
Copy create Int with value 10 at 010FF790
Print value: 10
Copy create Int with value 10 at 010FF7B0
Destroy Int with value 10 at 010FF790
Destroy Int with value 10 at 010FF7B0
Destroy Int with value 10 at 010FF8A0
Destroy Int with value 10 at 010FF8AC

但编译器会在主函数的栈帧中调用构造函数创建一个不具名的对象(将亡值,右值),用于存放it的值,在func函数的栈帧释放后将其又赋值给c

注:现代编译器(如GCC、Clang)可能通过“返回值优化(RVO)”省略拷贝构造的调用,直接在目标对象地址构造,以提升效率。

在C++11之前的右值和C++11中的纯右值是等价的。C++11中的将亡值是随着右值引用的引入而新引入的。换言之,“将亡值”概念的产生,是由右值引用的产生而引起的,将亡值与右值引用息息相关。

Opeator与将亡值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Int {
private:
int value;
public:
Int() :value(0) { std::cout << "Default create Int" << "at" << this << std::endl; }
Int(int v) :value(v) { std::cout << "Create Int with value " << value << " at " << this << std::endl; }
Int(Int& it) :value(it.value) { std::cout << "Copy create Int with value " << value << " at " << this << std::endl; }
~Int() { std::cout << "Destroy Int with value " << value << " at " << this << std::endl; }
Int& operator=(const Int& it) {
if (this != &it) {
value = it.value;
}
std::cout << this << " operator= " << &it << std::endl; //将亡值赋值给c
return *this;
}
void Print() const {
std::cout << "Print value: " << value << std::endl;
}
};
1
2
3
4
5
6
7
8
9
10
Create Int with value 10 at 00EFFD88
Create Int with value 0 at 00EFFD7C
Copy create Int with value 10 at 00EFFC84
Print value: 10
Copy create Int with value 10 at 00EFFCA4
Destroy Int with value 10 at 00EFFC84
00EFFD7C operator= 00EFFCA4
Destroy Int with value 10 at 00EFFCA4
Destroy Int with value 10 at 00EFFD7C
Destroy Int with value 10 at 00EFFD88

在返回对象时,不可使用引用:引用的本质为指针,以引用返回会导致value从已经释放掉的值获取,这将导致其为随机值.当函数结束但变量仍然存在,则可以使用引用的形式返回

1
2
3
4
Int& func(int x){
static Int tmp;
return tmp;
}

4. 用对象数组初始化时

当使用对象数组初始化另一个对象数组时,每个元素的初始化都会调用拷贝构造函数:

1
2
Int arr1[2] = {Int(10), Int(20)};
Int arr2[2] = arr1; // 每个元素调用拷贝构造

浅拷贝与深拷贝的问题

默认拷贝构造函数的“浅拷贝”行为在多数场景下是正确的(如用户代码中的Int类,仅含int成员),但当类中包含指针成员需要管理动态资源(如堆内存、文件句柄)时,浅拷贝会导致严重问题。

浅拷贝的风险

假设Int类包含一个指向堆内存的指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Int {
private:
int* value; // 指针成员,指向堆内存
public:
Int(int v) {
value = new int(v); // 分配堆内存
cout << "Create Int at " << this << endl;
}
~Int() {
delete value; // 释放堆内存
cout << "Destroy Int at " << this << endl;
}
// 未定义拷贝构造函数,使用默认版本
};

当执行Int b(20); Int c(b);时:

  • 默认拷贝构造函数会让c.value = b.value(即两者指向同一块堆内存);
  • 程序结束时,bc的析构函数会先后释放同一块内存,导致“重复释放内存”错误(undefined behavior)。

深拷贝的解决方案

为避免浅拷贝问题,需显式定义拷贝构造函数,实现“深拷贝”——即对指针成员指向的内容进行拷贝,而非仅复制指针地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Int {
private:
int* value;
public:
// 构造函数
Int(int v) : value(new int(v)) {
cout << "Create Int at " << this << endl;
}
// 自定义拷贝构造函数(深拷贝)
Int(const Int& other) : value(new int(*other.value)) {
cout << "Copy Create Int at " << this << endl;
}
// 析构函数
~Int() {
delete value;
cout << "Destroy Int at " << this << endl;
}
};

此时Int c(b);会通过自定义拷贝构造函数为c分配新的堆内存,并复制b.value指向的内容,避免重复释放问题。

拷贝构造与赋值运算符的区别

很多初学者会混淆拷贝构造函数和赋值运算符,两者的核心区别在于操作对象的状态

场景 拷贝构造函数 赋值运算符
作用对象 新对象(正在创建) 已存在的对象
调用时机 用已存在对象初始化新对象时 两个已存在对象之间赋值时
语法形式 Int c(b);Int c = b; c = b;(c已创建)
1
2
3
4
5
Int a(10); // 调用普通构造函数
Int b(a); // 调用拷贝构造函数(b是新对象)

Int c(20); // 调用普通构造函数
c = a; // 调用赋值运算符(c已存在)

禁用拷贝构造函数

在某些场景下(如单例模式、资源独占类),我们可能希望禁止对象被拷贝。此时可通过delete关键字显式禁用拷贝构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
class NoCopy {
public:
// 禁用拷贝构造函数
NoCopy(const NoCopy&) = delete;
// 通常需同时禁用赋值运算符
NoCopy& operator=(const NoCopy&) = delete;
};

int main() {
NoCopy x;
NoCopy y(x); // 编译错误:拷贝构造函数已禁用
return 0;
}

const与成员方法

在C++中,const与成员方法的结合(即const成员函数)是一种重要的机制,用于保证成员函数不会修改类的成员变量,从而增强代码的安全性、可读性和 const 正确性(const-correctness)。

const成员函数的声明和定义需在参数列表后添加const关键字,形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass {
private:
int value;
public:
// 声明为const成员函数
int getValue() const; // 承诺不修改成员变量
};

// 定义时也需带const
int MyClass::getValue() const {
return value; // 仅读取,不修改
}

保证“只读”行为

const成员函数的核心语义是:函数内部不能修改类的非静态成员变量,也不能调用非const成员函数(因为非const成员函数可能修改成员)。这种限制由编译器强制检查,若在const成员函数中尝试修改成员变量,会直接编译报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass {
private:
int value;
public:
void wrong() const {
value = 10; // 编译错误:const成员函数不能修改成员变量
}
void callNonConst() const {
modify(); // 编译错误:const成员函数不能调用非const成员函数
}
void modify() { // 非const成员函数(可能修改成员)
value = 20;
}
};

调用规则

  • const对象(或const引用、const指针)只能调用const成员函数,不能调用非const成员函数(防止修改对象状态);
  • 非const对象可以调用const成员函数和非const成员函数。
1
2
3
4
5
6
7
8
9
10
11
int main() {
MyClass obj; // 非const对象
const MyClass cobj; // const对象

obj.getValue(); // 合法:非const对象调用const成员函数
obj.modify(); // 合法:非const对象调用非const成员函数

cobj.getValue(); // 合法:const对象调用const成员函数
cobj.modify(); // 编译错误:const对象不能调用非const成员函数
return 0;
}

mutable成员变量

若类中存在需要在const成员函数中修改的成员变量(如缓存计数器、日志状态等),可使用mutable关键字修饰该成员。mutable成员不受const成员函数的限制,允许被修改:

1
2
3
4
5
6
7
8
9
10
11
12
class Counter {
private:
mutable int accessCount; // mutable成员,可在const函数中修改
int data;
public:
Counter(int d) : data(d), accessCount(0) {}

int getData() const {
accessCount++; // 合法:修改mutable成员
return data;
}
};

const成员函数的设计是C++ const正确性的核心体现:

  1. 明确意图:向开发者和编译器声明“此函数仅读取数据,不修改对象状态”,增强代码可读性;
  2. 编译器检查:通过编译期限制防止意外修改,减少bug;
  3. 支持const对象:使得const对象可以安全调用成员函数,拓展了函数的适用场景(如函数参数为const引用时,只能调用其const成员函数)。

实际开发中,仅读取成员变量的函数(如getter)应始终声明为const,而修改成员变量的函数(如setter)则保留为非const。