【C++】构造函数与析构函数

article/2025/10/2 6:19:40

1. 概述

image-20210302105510947
构造函数:用于初始化对象,没有返回值,函数名和类名相同,只有在对象初始化的时候才会被调用。构造函数的分类:

  • 默认构造函数:是编译器自动生成,没有任何参数的构造函数。

  • 有参构造函数:如果只一个参数的构造函数叫做转换构造。

  • 拷贝构造函数:传入的参数类型和当前对象的类型一致时,这类有参构造叫做拷贝构造,是特殊的有参构造函数。之所以要传入引用,是为了防止出现”套娃“,即多次调用拷贝构造函数。

  • 移动构造:与右值相关,后续再讲解。

析构函数:用于销毁对象,没有返回值,函数名和类名相同。

构造函数和析构函数会涉及到资源的申请和释放,但是在工业环境中,不会在构造函数中申请很大的资源,因为一旦构造函数出问题了,异常处理机制是很难捕获到这种异常的。取而代之的是额外编写一个方法来申请资源,同样地也会额外写一个伪析构方法来释放资源。设计模式中的工厂模式也是为了解决这个问题的。

在C++11之前,因为语言特性问题,所以STL性能不高。而在C++11中引入了左值和右值的概念,且引入了移动构造的概念,有了移动改造使得STL性能问题得到了大大的改善,所以C++11使得C++重回神坛。

2. 构造函数

构造函数的调用

#include<iostream>
using namespace std;class A {
public :A() {cout << this <<  " : constructor" << endl;}A (int x) {cout << this << " : transform constructor" << endl;}~A() {cout << this << " : destructor" << endl;}
};int main() {A a;A b;//如下两种写法都会调用转换构造//A c(3);A c = 3;cout << "end of main" << endl;return 0;
}

运行结果:构造顺序和析构顺序是相反的
image-20210302133702562

有参构造

为什么只有一个参数的构造函数叫做转换构造呢?

A a; a = 123;,其中 a = 123; 就是将 123 赋值给对象 a,但是对象赋值只有在相同或者相近类型才可以完成,那么在逻辑上来讲 123 已经被转换为一个 A 类型的值,所以才能赋值给 A 类型的对象。而这个转换的过程就是通过转换构造函数来完成的。

#include<iostream>
using namespace std;class A {
public :A() {cout << this <<  " : constructor" << endl;}A (int x) {cout << this << " : transform constructor" << endl;}~A() {cout << this << " : destructor" << endl;}
};int main() {A a; A b;//A c(3);A c = 3;a = 123; //将123赋值给对象acout << "end of main" << endl;return 0;
}

运行结果:
image-20210302134324773
也就是说:

A(int x) {} //可以将一个整型转换为A类型
A(string name) {} //可以将一个string类型转换为A类型

程序的处理流程

int main() {A a; //调用了默认构造函数A b; //调用了默认构造函数A c = 3; //调用了转换构造a = 123; //将123赋值给对象a,这行代码的处理流程?cout << "end of main" << endl;return 0;
}

a = 123; 的处理流程:

实际上a = 123;调用了一个重载的赋值运算符:

#include<iostream>
using namespace std;class A {
public :A() {cout << this <<  " : constructor" << endl;}A (int x) {cout << this << " : transform constructor" << endl;}A &operator=(const A &a) {cout << this << " : operator=" << endl;return *this;}~A() {cout << this << " : destructor" << endl;}
};int main() {A a;A b;//A c(3);A c = 3;a = 123; //将123赋值给对象acout << "end of main" << endl;return 0;
}

运行结果:
image-20210302140237610
所以,a = 123的处理流程就是:

① 调用转换构造,将 123 转换为一个临时的匿名对象;

② 调用重载运算符=,将①中产生的临时匿名对象绑定到 operator= 方法参数 a 上;

③ 析构产生的临时匿名对象。

构造和析构的过程产生的就是中间的临时匿名对象。

拷贝构造

image-20210302140759639
为什么拷贝构造函数A(A a){} 这样写出错?

A b = a; 调用的是 b 对象的拷贝构造 A(A a'),需要传参,就是将 a 拷贝给 a' 的过程,等价于A a' = a, 又会调用 a' 的拷贝构造,也涉及到传参的问题,所以就会无限递归下去了。

拷贝构造不能传值,因为一旦传的值的类型和参数的类型一样,会继续调用参数的拷贝构造,而调用参数的拷贝构造的时候,其类型又和参数的参数的类型一样,继续调用参数的参数的拷贝构造,无限递归下去。

左值引用

#include<iostream>
using namespace std;void add_one(int x) {x += 1;return ;
}int main() {int n = 3;cout << "n = " << n << endl;add_one(n);cout << "n = " << n << endl;return 0;
}

因为是值传递,所以 n = 3
image-20210302142902814
在C++中新增了一种引用形式:左值引用。

引用,相当于别名,如下的代码中,将 n 传给引用 x,就相当于 xn 的一个别名,对 x 进行操作就是对 n 进行操作:

#include<iostream>
using namespace std;void add_one(int &x) {x += 1;return ;
}int main() {int n = 3;cout << "n = " << n << endl;add_one(n);cout << "n = " << n << endl;return 0;
}

运行结果:
image-20210302143305784
引用类似于之前提到过的指针,但是引用相较于指针,会更加方便。

引用实际上就是给原来的变量贴了个标签,传引用是不产生任何拷贝行为的


回到刚刚的拷贝构造,知道了拷贝构造是不能传值的,起码要传一个引用:

#include<iostream>
using namespace std;class A {
public :A() {cout << this <<  " : constructor" << endl;}A (int x) {cout << this << " : transform constructor" << endl;}A(const A &a) {cout << this << " : copy constructor" << endl;}A &operator=(const A &a) {cout << this << " : operator=" << endl;return *this;}~A() {cout << this << " : destructor" << endl;}
};int main() {A a;A b = a; //调用了拷贝构造A c = 3;a = 123;cout << "end of main" << endl;return 0;
}

运行结果:
image-20210302143752167
强调

  • 定义 b 对象的过程中,无论是 A b = a; 还是 A b(a); 调用的都是拷贝构造。
  • 在不是定义 b 对象的过程中,即代码的其他位置写 b = a,调用的是赋值运算符。

为什么拷贝构造一定要传const?

class A {A(A &a) {}  
};int main() {const A a;A b = a;  //会出现大bug,因为const对象不能绑定到非const的对象上return 0}

为了兼容对象的const和非const的情况,所以拷贝构造传入const

构造函数的执行流程分析

class A {
public :A() {cout << this <<  " : constructor" << endl;}A (int x) {cout << this << " : transform constructor" << endl;}A(const A &a) {cout << this << " : copy constructor" << endl;}A &operator=(const A &a) {cout << this << " : operator=" << endl;return *this;}~A() {cout << this << " : destructor" << endl;}
};

如果声明一个对象 A a;

  • 逻辑上的完成构造(功能上的构造)是在第 5 行,有一些自定义的构造行为。
  • 实际上的完成构造(编译器认为的构造)是在第 3 行,一旦进到构造函数的大括号内,则对象已经构造完成了,因为在里面是可以使用当前对象的。“对象能否使用” 即:是否可以使用当前对象的所有成员属性和成员方法。

一旦写了有参构造,编译器的默认构造就被删除了,如果想让构造的对象有默认的行为,就需要显式地写默认构造。


新增Data类,并且在类A中声明一个Data类型的成员属性

#include<iostream>
using namespace std;class Data {
public:Data(int x, int y) {this->x = x;this->y = y;}
private:int x, y;
};class A {
public :A() {cout << this <<  " : constructor" << endl;}A(int x) {cout << this << " : transform constructor" << endl;}A(const A &a) {cout << this << " : copy constructor" << endl;}A &operator=(const A &a) {cout << this << " : operator=" << endl;return *this;}~A() {cout << this << " : destructor" << endl;}Data d;
};int main() {A a;A b = a; //调用了拷贝构造A c = 3;a = 123;cout << "end of main" << endl;return 0;
}

编译会出现如下错误:
image-20210302184758623
错误出现的原因:

结合上面讲解的实际上的构造完成,那么在 16 行之后,当前对象已经被构造了,则可以访问它的所有成员,即在17行的时候,是可以访问成员属性 d 的,d 就应该已经完成了构造。

那么 d 完成了构造,到底是调用了什么构造函数呢?

因为没有显式地调用任何构造函数,就会调用默认构造函数,但是成员属性 d 对应的类 Data 中没有默认构造,因为写了有参构造,它的默认构造就被编译器删除了,所以就产生了问题。

总结来说就是,成员属性 d 对应的类 Data 没有默认构造函数,A 类的构造方法中要想访问对象的成员属性 d 行不通,无法到达第17行,因为无法完成构造行为。

这时候初始化列表就有用了。修改 A 类中的构造方法,增加初始化列表,使得显式调用 Data 类的有参构造:

class Data {
public:Data(int x, int y) : x(x), y(y) {}
private:int x, y;
};class A {
public :A() : d(3, 4) {cout << this <<  " : constructor" << endl;}A (int x) : d(x, x) {cout << this << " : transform constructor" << endl;}A(const A &a) : d(a.d) { //调用d对象的默认拷贝构造cout << this << " : copy constructor" << endl;}A &operator=(const A &a) {cout << this << " : operator=" << endl;return *this;}~A() {cout << this << " : destructor" << endl;}Data d;
};

一旦初始化列表中的内容执行完毕,实际上当前对象就构造完成,初始化列表是对当前对象的每个属性进行构造,对象的构造真正是发生在初始化列表。

编译器会自动生成默认构造和默认拷贝构造,一旦写了有参构造,编译器就会将默认构造删除,但是默认拷贝构造还是存在的。

初始化列表的构造顺序

成员属性的构造顺序和初始化列表无关,只和成员属性的声明顺序有关。

#include<iostream>
using namespace std;class Data {
public:Data(int x, int y) : x(x), y(y) {cout << "data : " << this << endl;}
private:int x, y;
};class A {
public :A() : d(3, 4), c(3, 4) {cout << this <<  " : constructor" << endl;cout << "c :" << &c << endl;cout << "d :" << &d << endl;}A(int x) : d(x, x), c(3, 4) {cout << this << " : transform constructor" << endl;}A(const A &a) : d(a.d), c(3, 4) { //调用d对象的默认拷贝构造cout << this << " : copy constructor" << endl;}A &operator=(const A &a) {cout << this << " : operator=" << endl;return *this;}~A() {cout << this << " : destructor" << endl;}Data c, d;
};int main() {A a;A b = a; //调用了拷贝构造A c = 3;a = 123;cout << "end of main" << endl;return 0;
}

运行结果:

image-20210302190834011

default 和 delete关键字

用来显式说明什么样的构造函数使用功能编译器提供的默认行为,什么样的构造函数是需要删除的。

#include<iostream>
using namespace std;class A {
public://默认构造函数被删除A() = delete;//当前构造函数要使用编译器默认自带的规则,等价于编译器提供的默认拷贝构造A(const A &) = default;
};int main() {return 0;
}

设计一个类,该类的对象不能被拷贝

方法一:删除拷贝构造:不行,依然可以通过赋值运算符进行拷贝

image-20210303124702958
但是依然不能避免对象被拷贝,可以通过赋值运算符完成对象的拷贝:

#include<iostream>
using namespace std;class A {
public:A() = default;A(const A &) = delete;
};int main() {A a;A b;a = b;return 0;
}

所以,为了完成这个功能需求——对象不能被拷贝,通常是将拷贝构造方法和赋值运算符都放到 private 访问权限内。

方法二:拷贝构造和赋值运算符都放到 private 访问权限内

image-20210303125216562

为什么赋值运算符的返回值是类引用

#include<iostream>
using namespace std;class A {
public :A() = default;A &operator=(int x) {this->x = x;return *this;}int x;
private :A(const A &) = delete;A &operator=(A &a);const A &operator=(const A &a) const;
};int main() {A a;(a = 123) = 456;cout << a.x << endl; //输出456return 0;
}

其中代码:

(a = 123) = 456;

的意思是:456 可以赋值给前面括号内部的返回值,而括号内的返回值是一个 A 类型的引用对象,因为返回的是 A 类的引用,所以括号内的表达式实际上返回的还是对象 a,也就是说将 456 赋值给对象 a

malloc和new

  • malloc只能申请存储区不能对对象进行初始化,即不会调用构造函数;
  • new既能申请存储区又能对对象进行初始化,即会调用构造函数。
#include<iostream>
using namespace std;class A {
public:A() {cout << "default constructor" << endl;}
};int main() {int n = 10;cout << "malloc int" << endl;int *data1 = (int *)malloc(sizeof(int) * n);cout << "new int" << endl;int *data2 = new int[n];cout << "malloc A" << endl;A *Adata1 = (A *)malloc(sizeof(A) * n); //这n个A对象没有被初始化,因为没有调用构造函数cout << "new A" << endl;A *Adata2 = new A[n];return 0;
}

运行结果:
image-20210302192242074

  • 空间的释放:malloc 对应 freenew 对应 delete
#include<iostream>
using namespace std;class A {
public:A() {cout << "default constructor" << endl;}~A() {cout << "deconstructor" << endl;}
};int main() {int n = 10;cout << "malloc int" << endl;int *data1 = (int *)malloc(sizeof(int) * n);cout << "free int" << endl;free(data1);cout << "new int" << endl;int *data2 = new int[n];cout << "delete int" << endl;delete[] data2;cout << "malloc A" << endl;A *Adata1 = (A *)malloc(sizeof(A) * n); //这n个A对象没有被初始化,因为没有调用构造函数cout << "free A" << endl;free(Adata1);cout << "new A" << endl;A *Adata2 = new A[n];cout << "delete A" << endl;delete[] Adata2;A *Adata3 = new A(); //new了一个单一的对象delete Adata3; //delete不用添加方括号return 0;
}

运行结果:
image-20210302193043942

  • deletefree 之间的差别:new 调用构造函数,如果想回收申请的存储区的时候,还得回收存储区内部的每个对象,就得调用每个对象的析构函数,这就是 delete,可以自动地调用每个对象的析构函数。但是 free 就不行了。
  • 关于 deletedelete[]:如果 new 的是一个数组,那么释放的时候就需要使用 delete[],表示 delete 的是一段连续的存储空间;如果 new 的是一个单一的对象,new 的时候就不需要加 []

原地构造

原地构造的语法:

new(对象地址)类构造函数

原地构造可以结合 malloc 一起使用。

A *Adata1 = (A *)malloc(sizeof(A) * n); 
for (int i = 0; i < n; i++) {new(Adata1 + i) A(); //原地构造,A()表示调用默认构造,这个位置表示的是调用哪个类的哪个构造函数
}

这个过程就是说先用 malloc 开辟一块连续的存储区,这片存储区没有被初始化,用原地构造依次地对每个位置进行初始化,完成构造行为。

原地构造在实现深拷贝的时候使用较多。

3. 析构函数

局部对象的析构函数在函数执行结束后执行

#include<iostream>
using namespace std;class A {
public :~A() {cout << "destructor" << endl;}
};int main() {A a; //调用了默认构造函数cout << "end of main" << endl;return 0;
}

运行结果:在 main 函数执行结束后,才会执行析构函数
image-20210302122106238

析构函数的调用顺序

#include<iostream>
using namespace std;class A {
public :~A() {cout << this << " : destructor" << endl;}
};int main() {A a; //调用了默认构造函数A b;cout << "&a = " << &a << endl;cout << "&b = " << &b << endl;cout << "end of main" << endl;return 0;
}

运行结果:
image-20210302122508267
为什么对象的构造顺序和析构顺序是相反的?

这是正常的语言特性。

析构顺序和声明的对象是否在栈上是没有关系的,即便将两个对象声明为全局的,析构顺序依然是反的。

从语言设计来说,b 对象有可能依赖于 a 对象的信息进行构造,所以在析构的时候,b对象也有可能依赖于 a 对象的信息才能完成正确的析构,所以在析构 b 对象之前不能先析构 a 对象。这就解释了构造顺序和析构顺序永远是反的。


http://chatgpt.dhexx.cn/article/qK9B0HhC.shtml

相关文章

何时调用构造函数和析构函数

何时调用构造函数和析构函数 构造函数的作用是保证每个对象的数据成员都有何时的初始值。 析构函数的作用是回收内存和资源&#xff0c;通常用于释放在构造函数或对象生命期内获取的资源。 一般我们都知道构造和析构的次序&#xff1a; 构造从类层次的最根处开始&#xff0c…

C++篇----构造函数和析构函数

在很多时候&#xff0c;当写了初始化&#xff0c;动态开辟的&#xff0c;需要写销毁函数&#xff0c;写了销毁函数之后&#xff0c;但是却忘记了调用这些函数&#xff0c;忘记调用初始化函数还好&#xff0c;编译器会报错&#xff0c;但是如果是忘记调用销毁函数&#xff0c;那…

c++构造函数和析构函数

一、构造函数和析构函数的特点 构造函数和析构函数是一种特殊的公有成员函数&#xff0c;每一个类都有一个默认的构造函数和析构函数&#xff1b;构造函数在类定义时由系统自动调用&#xff0c;析构函数在类被销毁时由系统自动调用&#xff1b;构造函数的名称和类名相同&#…

构造函数与析构函数

一&#xff0c;引言 由于c语言常常会忘记初始化与销毁&#xff0c;造成许多麻烦。所以c就引入了构造函数与析构函数&#xff0c;分别用来完成初始化与清理工作&#xff0c;且由编译器自动调用&#xff0c;这就避免了许多麻烦。 二&#xff0c;构造函数 构造函数是一个特殊的成…

构造函数和析构函数

文章目录 前言 1.构造函数<1>概念<2>特性 2.初始化列表<1>概念<2>特征 3.析构函数<1>概念<2.>特征 前言 如不清楚类的定义可以点击此篇文章&#xff1a;类的定义与引入 C为很么要引入构造函数和析构函数呢&#xff0c;前文讲到大佬引入了…

C++ 构造函数和析构函数 详解

目录 概述构造函数的分类1. 无参(默认)构造函数2. 有参构造函数3. 委托构造函数4. 复制(拷贝)构造函数5. 移动构造函数 构造函数调用规则析构函数 概述 C中用构造函数和析构函数来初始化和清理对象&#xff0c;这两个函数将会被编译器自动调用。对象的初始化和清理是非常重要的…

java异常处理及自定义异常

异常处理的实际上就是&#xff1a; 有风险的行为&#xff08;方法&#xff09;可能会将异常抛出&#xff08;throws&#xff09;。调用该方法的程序会尝试&#xff08;try&#xff09;去运行,运行的同时捕捉&#xff08;catch&#xff09;异常。 简而言之&#xff0c;就是对有…

java异常 — — 自定义异常

三、自定义异常 3.1、概述 为什么需要自定义异常类: Java中不同的异常类分别表示看某一种具体的异常情况&#xff0c;那么在开发中总是有些异常情况是SUN没有定义好的此时我们根据自己业务的异常情况来定义异常类。例如年龄负数问题&#xff0c;考试成绩负数问题等等。 在上…

JAVA自定义异常处理

自定义异常处理可以分为两种&#xff0c;一种是自定义编译处理&#xff0c;另一种是自定义运行处理 1.自定义编译处理需要创建一个异常类用于继承Exception类 重写构造器 在出现异常的地方用throw new 自定义对象抛出 作用&#xff1a;编译时异常时编译阶段就报错&#xff…

Java的自定义异常类

Java的异常处理机制可以让程序具有极好的容错性&#xff0c;让程序更加健壮。当程序运行出 现意外情形时&#xff0c;系统会自动生成一个 Exception对象来通知程序&#xff0c;从而实现将“业务功 能实现代码”和“错误处理代码”分离&#xff0c;提供更好的可读性。 Java把所…

Java自定义异常及统一处理,信息返回

开始操作 创建enums&#xff0c;exception包&#xff1a; enums包下&#xff1a; 创建BaseCodeEnum接口 创建Response类&#xff1a;为统一信息返回类 创建ResponseCode枚举类&#xff1a;在这里定义我们需要的异常 exception包下&#xff1a; 创建HandlerException类&#…

Java自定义异常类统一处理异常

当程序发生异常时&#xff0c;会返回一大堆不友好的内容&#xff0c;非常不美观&#xff01; 我们在写代码的时候&#xff0c;对异常处理一般是try catch或者抛出异常throws Exception。 try catch大家都知道&#xff0c;代码中大量的try catch会占用内存影响性能&#xff0c…

Java中的自定义异常

代码实现 自定义异常类型主要实现代码 public class Exception_demo extends Exception{//自定义异常&#xff0c;需要把自定义异常类继承于Exception异常类&#xff0c;自定义异常类属于异常类的子类public Exception_demo(){//构造方法也叫做构造器&#xff0c;构造方法的名…

【Java异常】自定义异常

Java中定义了大量的异常类&#xff0c;虽然这些异常类可以描述编程时出现的大部分异常情况&#xff0c;但是在程序开发中有时可能需要描述程序中特有的异常情况,例如在设计divide()方法时不允许被除数为负数。为了解决这样的问题,Java允许用户自定义异常&#xff0c;但自定义的…

JAVA项目中自定义异常

JAVA项目中自定义异常 1.数据返回处理类 Data public class R<T> implements Serializable {private static final long serialVersionUID -8497670085742879369L;ApiModelProperty(value "返回码", example "200")private Integer code200;Api…

Java自定义异常理解

前言&#xff1a;看了许多博客和书&#xff0c;都对自定异常一笔带过&#xff0c;总让人感觉莫名奇妙&#xff0c;一直在问自己一个问题&#xff0c;我们能很好的解决异常就很不错了&#xff0c;为什么还要自己自定义异常&#xff0c;让自己去自找麻烦呢&#xff1f;后来我才理…

Java自定义异常

使用Java内置的异常类可以描述在编程时出现的大部分异常情况。除此之外&#xff0c;用户还可以自定义异常。用户自定义异常类&#xff0c;只需继承Excepition类即可 在程序当中自定义异常类&#xff0c;大体可以分成几个步骤&#xff1a; 1.创建自定义异常类 2.在方法中通过t…

JAVA 基础学习之异常机制

异常机制 1、概念 异常指程序运行过程中出现的非正常现象&#xff0c;例如用户输入错误、除数为零、需要处理的文件不存在、数组下标越界等。在Java的异常处理机制中&#xff0c;引进了很多用来描述和处理异常的类&#xff0c;称为异常类。 2、异常的分类 Throwable下面又派生…

【Java】自定义异常

自定义异常&#xff1a; java提供的异常类&#xff0c;不够我们使用&#xff0c;需要自己定义一些异常类 格式&#xff1a; public class XXException extends Exception|RuntimeException{ 添加一个空参数的构造方法 添加一个带异常信息的构造方法 } 注意&#xff1a; 1.自…

Java异常详解及自定义异常

我已经不用 try catch 处理异常了&#xff01;太烦人了_51CTO博客_try catch处理什么异常 一、异常的概念 1.定义&#xff08;什么是异常&#xff1f;&#xff09; 异常是例外&#xff0c;是一个程序在执行期间发生的事件&#xff0c;它中断正在执行程序的正常指令流。软件开发…