C++类和对象(中)| 深挖四大默认成员函数:构造/析构/拷贝/赋值重载原理全解

1. 类的默认成员函数

默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个类,我们不写的情况下编译器会默认⽣成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可。其次就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值,这个我们后⾯再讲解。默认成员函数很重要,也⽐较复杂,我们要从两个⽅⾯去学习:

第⼀:我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。

第⼆:编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?

1.1构造函数

什么是构造函数?

构造函数是类的特殊成员函数,在对象创建时的一瞬间自动调用,它的名字看起来会认为它创建什么东西,实则是进行的初始化的工作。核心作用是:初始化对象的成员变量,为对象分配内存资源。

简单来说就是:对象创建之后的初始化工作,全部由构造函数完成。

构造函数的语法规则:

1. 函数名必须和类名完全一致(大小写敏感);

2. 没有返回值(连 void 都不能写,底层由编译器隐式处理返回);

3. 对象实例化时自动执行,无法手动主动调用(除特殊场景);

4. 可以重载(一个类可以拥有多个参数不同的构造函数)。

5. 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤⼾显式定义编译器将不再⽣成。

6. ⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调⽤的构造就叫默认构造。

7. 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤初始化列表才能解决,初始化列表,我们下个章节再细细讲解。说明:C++把类型分成内置类型(基本类型)和⾃定义类型。内置类型就是语⾔提供的原⽣数据类型,如:int/char/double/指针等,⾃定义类型就是我们使⽤class/struct等关键字⾃⼰定义的类

无参数的构造函数

在这里跟C语言的写法还是有点不一样的,在C语言当我们创建一个变量对象s1时,我们就会紧接着下一步初始化,也就是init(&s1),但是在C++当中,创建变量函数和初始化是合并在一起的,

这也是C++的独特之处,当我们创建变量对象的同时,它会自动调用构造函数Data,从而进行变量的赋值。

全缺省函数的构造函数

这里我们写了一个全缺省函数,等价于同时拥有无参构造函数

传实参的构造函数

我们需要注意的是:这三种构造函数只能存在一种,并且还要强调一下编译器默认的构造函数不仅仅只有无参的构造函数,还有全缺省函数和无实参的构造函数,简单总结来说:无实参传入的构造函数都是编译器的默认构造函数。

对于自定义类型的成员变量时,不需要进行构造函数,比如我们用两个栈实现队列的这道题中

这里我们在写栈的构造函数时,因为需要创建内存空间,所以我们自己写构造函数。

**myQueue s1; 创建对象时:

  1. 先构造成员 q1、q2(Stack类型),再执行 myQueue 自身构造函数;**

2. Stack 类手写了带参构造 Stack(int n=4) (全缺省构造),没有手动写无参构造,编译器也不再生成默认无参构造;

3. 成员 q1、q2 初始化时自动调用 Stack(int n=4) ,用缺省参数 n=4 完成内存开辟;

4. myQueue 空构造函数 myQueue(){} 后执行,内部没有内置基础类型成员,不用额外初始化。

注意:成员变量初始化永远在本类构造函数体执行之前:先q1->q2,后进入Queue(){};

为啥Stack能直接构造q1和q2

Stack(int n=4)是全缺省构造函数,等价于同时拥有无参构造函数,所以当写了自定义类型的Stack q1和q2就会自动调用Stack的构造函数进行初始化。

1.2析构函数

析构函数和构造函数的作用完全相反

构造函数负责对象诞生、申请资源,析构函数就是对象的「收尾管理员」,在对象销毁时自动执行,核心职责:释放堆内存、关闭文件/套接字、归还系统资源。

很多工程内存泄漏、堆重复崩溃问题,根源都是析构写法不规范。本文结合 Stack/myQueue 组合类案例,从语法、调用顺序、默认生成、实战坑点全维度梳理析构。

析构函数的基础语法规则

  1. 析构函数名是在类名前加上字符 ~。

  2. ⽆参数⽆返回值。 (这⾥跟构造类似,也不需要加void)

  3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。

  4. 对象⽣命周期结束时,系统会⾃动调⽤析构函数。

  5. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。

  6. 还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数。

  7. 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如Date;如果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,如MyQueue;但是有资源申请时,⼀定要⾃⼰写析构,否则会造成资源泄漏,如Stack。

  8. ⼀个局部域的多个对象,C++规定后定义的先析构。

由于 Stack 在构造阶段使用 malloc 向堆申请动态数组资源,必须自定义析构函数,在对象生命周期结束时执行 free 归还堆内存,防止内存泄漏。
myQueue 内部包含两个 Stack 自定义类型成员 q1 、 q2 ,实例化 myQueue s1 时遵循C++组合类构造规则:先构造成员、后构造本体, q1 与 q2 依靠 Stack 的全缺省构造完成初始化并分配堆内存;
当程序退出 main 作用域,局部对象 s1 自动销毁,析构执行顺序与构造相反:先执行 myQueue 默认析构函数体,随后逆序销毁成员,优先析构 q2 ,再析构 q1 ,两个栈对象在自身析构中释放各自申请的堆空间。

**C++语法硬性规则:构造正序、析构逆序
构造流程(创建s1)

  1. 按类内定义顺序:先 q1 构造、再 q2 构造**

**2. 全部成员构造完,执行 myQueue{} 构造体
顺序:q1 → q2 → myQueue
析构流程(销毁s1)

  1. 先走 myQueue 析构函数体**

2. 成员倒着销毁:后构造的q2先销毁,先构造的q1后销毁
顺序:myQueue → q2 → q1

1.3拷贝构造函数

如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值,则此构造函数也叫做拷⻉构造函数,也就是说拷⻉构造是⼀个特殊的构造函数。

拷⻉构造的特点:

  1. 拷⻉构造函数是构造函数的⼀个重载。

  2. 拷⻉构造函数的第⼀个参数必须是类类型对象的引⽤,使⽤传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调⽤。 拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值。

    //拷贝构造函数的格式
    类名(const 类名&对象,其他参数类型);

我们在这里直接给出拷贝构造的格式,记住这个格式,在这里我们也不讲解不引用的情况,这里比较复杂,我们就直接一步到位,记住它的本质拷贝构造函数就这么写,需要注意的是,它的第一个参数都必须引用类对象,其他的参数取自身的类型即可,一般拷贝构造函数不会有其它的参数,只会存在这一个引用对象的参数。

  1. C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成。

  2. 若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造。

  3. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。像Stack这样的类,虽然也都是内置类型,但是a指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要_我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现MyQueue的拷⻉构造。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写拷⻉构造,否则就不需要。

  4. 传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。

内置类型+没有资源指向的成员变量

像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。我们可以对比一下写和不写拷贝函数构造的结果是否相同。

自己手动写的拷贝构造函数:

我们在这里写了一个日期类的拷贝构造函数,因为它没有空间的开辟,在这段代码当中,没有写析构函数,用的是编译器默认的析构函数,随后,我们写了拷贝构造函数,需要注意的是拷贝构造函数只能写在类当中,因为这里的成员变量是私有的,只能在类里面才能访问。还有一点需要回顾的是,当作为成员函数时,它的第一个参数是隐藏的this指针,它指向的是我们在下面创建的s2的内存地址。

编译器默认的拷贝构造函数:

看,当我们用编译器自己默认的拷贝函数构造的函数,它一样是可以对新创建的s2对象把s1的东西拷贝一份到s2当中去,所以我们对于没有任何资源指向的且全部都是内置类型的成员变量,我们直接用编译器默认的就可以了。

内置类型+有资源指向的成员变量

像Stack这样的类,虽然也都是内置类型,但是a指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要_我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。

为什么在这里我们需要自己手动写拷贝构造函数呢?我们想一下这里如果我们用编译器默认的拷贝构造函数的话,是可以完成浅拷贝的,而这里它们用的是同一块内存空间,当我们写的析构函数去清理资源时,先清理的是后定义的s2对象,当清理过这个对象之后,这时的空间已经没有了,那么我们再次释放空间,此时s1指向的不就是一块不存在的区域吗?此时不就成为野指针了吗?所以在这里我们不能使用编译器默认的,就要我们去手动写,开辟出一个新的空间,其容量和个数跟s1一样,下面就是自己手动写的拷贝构造函数。

下面我们将整个过程的路径以视频的形式展现给大家:

http://"C:\Users\y3562\Desktop\e6121de087d699d9861e459fd18bc540.mp4"

下面我对这个路径进行讲解一下:首先我们创建了对象s1,编译器自动调用构造函数,让后接着创建了s2对象并将s1拷贝过去,此时进入拷贝构造函数,进行了复制拷贝,最后开始清理资源空间,析构函数的规定是先清理后定义的对象,所以先清理s2,让后紧接着清理s1,清理了2次,在视频当中也可以看出来,进入了2次析构函数。

自定义类型

像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现MyQueue的拷⻉构造。

由于自定义类型都有自己的构造函数,析构函数,拷贝构造函数,所以不用写,它们会调用它们自己的构造函数,析构函数和拷贝构造函数。

http://"C:\Users\y3562\Desktop\edd1d57982fc754f598ef35ad935adbb.mp4"

这个路径的解读:首先我们创建了对象s1,接着编译器自动调用构造函数,而构造函数首先对成员变量进行初始化,所以先初始化变量q1,让后初始化q2,因为myQueue没有内置类型,只有这两个自定义类型成员变量,所以编译器直接调用它们自己的构造函数,紧接着我们创建了对象s2,把s1拷贝到s1,同样,也是调用这两个自定义类型的拷贝构造函数,从而在之后的析构函数也是调用这个自定义类型的析构函数,正所谓myQueue是个躺赢选手啊,正所谓想躺赢,必须有一个人在默默的付出,而这个自定义类型Stack就是那个默默付出的那个。

1.4赋值运算符重载

1 运算符重载

• 当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编译报错。

• 运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。

• 重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。

• 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。

• 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。

• 不能通过连接语法中没有的符号来创建新的操作符:⽐如operator@。

• .* :: sizeof ?: . 注意以上5个运算符不能重载。(选择题⾥⾯常考,⼤家要记⼀下)

• 重载操作符⾄少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: intoperator+(int x, int y),如果没有类类型的参数,只有这些内置类型的参数,那么我们直接用普通的+-等操作符就可以了,就没有必要写operator操作符了,大材小用。

• ⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,⽐如Date类重载operator-就有意义,但是重载operator+就没有意义。

• 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分。

• 重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调⽤时就变成了 对象<<cout,不符合使⽤习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。

运算符重载写在全局当中

写在全局当中有一个缺陷,就是它想要访问类里面的成员变量,就需要手动把私有改为公有,但是这会导致成员变量存在被修改的情况,所以我们一般不使用全局,下面我们将运算符重载写在类里面,充当成员函数的情况。

写在类里面,充当成员函数

下面就是作为成员函数,这样写的好处是可以不用修改访问权限,在同一个类域里面可以直接访问成员变量,当充当成员函数时,需要注意的是第一个参数不用写,因为隐藏的指针this会指向s1的地址,只需要传第二个参数即可。

2赋值运算符重载

赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷⻉赋值,这⾥要注意跟拷⻉构造区分,拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象。赋值运算符重载的特点:

  1. 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const 当前类类型引⽤,否则会传值传参会有拷⻉

  2. 有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋值场景。

  3. 没有显式实现时,编译器会⾃动⽣成⼀个默认赋值运算符重载,默认赋值运算符重载⾏为跟默认拷⻉构造函数类似,对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的赋值重载函数。

  4. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的赋值运算符重载就可以完成需要的拷⻉,所以不需要我们显⽰实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的赋值运算符重载完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的赋值运算符重载会调⽤Stack的赋值运算符重载,也不需要我们显⽰实现MyQueue的赋值运算符重载。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写赋值运算符重载,否则就不需要。

我们写了一下赋值运算符的重载代码,我们可以看到确实s2的值赋值给了s1,说到这里我们需要澄清一个混淆点,也就是开头所说的赋值运算符重载和拷贝构造函数的区分。

赋值运算符重载:两个已经存在的对象进行赋值

拷贝构造:一个已经存在的对象初始化一个要创建的对象

这种首先只初始化了s1,而下面的Data s2=s1;指的是将s1拷贝给一个将要创建的s2对象,需要和赋值运算区别开。

1.5取地址运算符重载

1 const成员函数

• 将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后⾯。

• const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 constDate* const this

2 取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动⽣成的就可以够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当前类对象的地址,就可以⾃⼰实现⼀份,胡乱返回⼀个地址。

相关推荐
混迹中的咸鱼1 小时前
游戏开发核心架构指南
c++·游戏·架构
-凌凌漆-2 小时前
【Qt】C++中protected与private的区别
开发语言·c++·qt
草莓熊Lotso2 小时前
【Linux网络】深入理解 HTTP 协议(四):完善 C++ HTTP 服务器:从协议原理到生产级实现
linux·运维·服务器·c语言·网络·c++·http
牛油果子哥q2 小时前
【C++前置声明与头文件】C++前置声明与头文件深度精讲:重复包含、循环依赖、重复定义报错、工程编译架构与实战解决方案
开发语言·c++
少司府2 小时前
C++进阶:map和set的使用
开发语言·数据结构·c++·容器·stl·set·map
程序喵大人2 小时前
C++ 程序员转型 AI Infra 学习路线
c++·人工智能·学习·ai infra
cpp_25012 小时前
P11375 [GESP202412 六级] 树上游走
数据结构·c++·算法·题解·洛谷·树形结构·gesp六级
此生决int2 小时前
算法从入门到精通——字符串
数据结构·c++·算法·蓝桥杯
basketball6162 小时前
设计模式入门:7. 策略模式详解 C++实现
c++·设计模式·策略模式