C++构造函数

深拷贝和浅拷贝

  • 浅拷贝
    • 就是对变量的简单按位复制内存
    • 在C++中,默认的拷贝构造函数制作按位复制,也就是浅拷贝
  • 深拷贝
    • 深拷贝除了将所有的成员变量拷贝给新对象外,对于指针等指向外部资源的成员变量,还会为新对象分配一块新的内存,将指针指向的内容也拷贝一份,这样原有对象和新对象的内存都是互相独立的,避免两个对象互相影响和double free的错误。
    • 在C++中,深拷贝必须由程序员显式地实现

拷贝构造函数和赋值构造函数

  • 拷贝构造函数
    • 是对构造函数的重载,函数原型为
      1
      myClass(const myClass & obj){}
      注意输入的参数应该是引用,如果是值传递的话,在值传递时就会触发拷贝构造函数,导致无限递归,造成崩溃。
    • 会在下面的情况被调用
      • 用类的一个对象去初始化另一个对象时(myClass obj2(obj1)myClass obj3 = obj1两种写法都是)
      • 当函数的形参是类的对象时(也就是值传递),引用传递不会调用拷贝构造函数
      • 当函数返回值是类的对象时
  • 赋值构造函数
    • 是赋值操作符的重载,函数原型为
      1
      2
      3
      4
      myClass& operator=(const myClass & obj) {
      ...
      return *this;
      }
    • 会在用一个已有对象来给另一个已经创建好的对象赋值时被调用

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <iostream>
#include <cstring>
class A {
public:
A() {
data = new char[100];
std::cout << "构造函数 \n";
}

~A() {
delete[] data;
std::cout << "析构函数 \n";
}

A(const A& a) {
data = new char[100];
memcpy(data, a.data, 100);
std::cout << "拷贝构造函数 \n";
}

A& operator=(const A& a) {
if (this != &a) {
if (!data) {
data = new char[100];
}
memcpy(data, a.data, 100);
}
std::cout << "赋值构造函数 \n";
return *this;
}
private:
char* data{nullptr};
};

A copyTest(A a) {// 这里会调用拷贝构造函数
std::cout << "in copyTest \n";
return a; // 这里也会调用拷贝构造函数
}

int main() {
A a; // 构造函数
A b(a); // 拷贝构造函数
std::cout << "=== \n";
A c = b; // 拷贝构造函数
std::cout << "=== \n";
copyTest(c); // 拷贝构造函数
std::cout << "=== \n";
c = a; // 赋值构造函数
}
/* 完整的输出为
构造函数
拷贝构造函数
===
拷贝构造函数
===
拷贝构造函数
in copyTest
拷贝构造函数
析构函数
析构函数
===
赋值构造函数
析构函数
析构函数
析构函数
*/

移动构造函数和移动赋值函数

与拷贝构造函数和赋值构造函数的区别在于输入的参数为右值引用,且移动构造和移动赋值函数会直接将资源的所有权转移到当前对象,省去拷贝的步骤,能够大幅提升效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class A {
public:
A() {
data = new char[100];
std::cout << "构造函数 \n";
}

~A() {
delete[] data;
std::cout << "析构函数 \n";
}

A(A&& a) {
data = a.data;// 直接将资源的所有权转移给当前对象
a.data = nullptr;
std::cout << "移动构造函数 \n";
}

A& operator=(A&& a) {
if (this != &a) {
data = a.data; // 直接将资源的所有权转移给当前对象
a.data = nullptr;
}
std::cout << "移动赋值函数 \n";
return *this;
}

private:
char* data{nullptr};
};

int main() {
A a; // 构造函数
A d; // 构造函数
A b(std::move(a)); // 移动构造函数
std::cout << "=== \n";
A c = std::move(b); // 移动构造函数
std::cout << "--- \n";
c = std::move(d); // 移动赋值函数
}

std::move的理解

  • std::move的作用是将参数转换为右值引用,以便利用移动构造函数或移动赋值运算符。
  • 移动语义的目的是避免不必要的拷贝,特别是在处理大型对象或资源密集型对象时。
  • 移动后对象的状态应为有效但不确定(通常是安全但不可预测的),即移动后的对象处于“可析构”但“不可用”的状态。
  • std::move仅仅是一个类型转换,实际的移动操作由移动构造函数或移动赋值运算符完成。

delete,explicitdefault

  • delete是C++11引入的新特性,在成员函数后面使用=delete修饰,表示禁用该函数。智能指针unique_ptr就是通过禁用拷贝构造函数来实现的。

    1
    A& operator=(const A& a) = delete;
  • explicit则在函数前面修饰,用于禁止隐式类型转换

    • 在 C++ 中,任何只有一个参数(或者除了第一个参数外,其它参数都有默认值)的构造函数,默认就是一个转换构造函数(converting constructor),它允许编译器在需要的时候把那个参数类型 隐式 地转换成该类的对象,例如
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      class A {
      public:
      A(int a) { a_ = a; }
      ~A() {}

      private:
      int a_;
      };

      int main() {
      A a(1);
      A b = 100; // 会触发隐式类型转换
      return 0;
      }
    • 但这样的隐式有时候并不是我们所期望的,可能只是代码写错了,为了避免这种情况,可以用explicit修饰构造函数
      1
      2
      3
      4
      5
      class A {
      public:
      explicit A(int a) { a_ = a; }
      ...
      }
    • 除了构造函数外,explicit还可以用于修饰类型转换函数(C++11)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      struct B {
      explicit operator bool() const { return some_flag; }
      };

      B b;
      if (b) { // 错误:operator bool() 是 explicit 的,不能隐式转换
      }
      bool x = static_cast<bool>(b); // OK:显式转换

  • default一般用于修饰构造函数,要求编译器生成默认的构造函数。

    • 对于普通的构造函数,如果类有成员变量,就按照它们的默认构造来初始化。
      • 如果成员是内置类型(如 intdouble),那它们不会被自动初始化,值是未定义的垃圾值
      • 如果成员是类类型(比如 std::string),会调用该成员的默认构造函数
    • 对于默认的拷贝构造函数,逐成员拷贝,即调用每个成员的拷贝构造。
    • 对于默认的赋值构造函数,逐成员赋值,即调用每个成员的赋值构造。

    如果类中没有写任何构造函数,那么编译器会隐式地生成默认构造函数,如果你定义了一个有参构造函数,编译器就不会再生成默认构造函数了;但你可以用 = default 要求编译器还是生成一个,这是 default关键字最重要的应用场景。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class A {​
    public:​
    A(int a) { a_ = a; }​
    ~A() {}​

    private:​
    int a_;​
    };​
    int main() {
    A a;
    }

    上面的代码会由于A没有无参数的构造函数而导致报错,可以在A中添加
    1
    A() = default;

    解决,但是默认的构造函数并不会将a_初始化。
    此外default也可以修饰拷贝构造函数。


C++构造函数
https://guts.homes/2025/06/12/cpp-constructor/
作者
guts
发布于
2025年6月12日
许可协议