欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 教育 > 培训 > 类和对象进阶:构造函数和析构函数详解

类和对象进阶:构造函数和析构函数详解

2025/2/21 3:37:49 来源:https://blog.csdn.net/2301_80064645/article/details/145522523  浏览:    关键词:类和对象进阶:构造函数和析构函数详解

C++构造、析构、拷贝构造函数详解

  • C++构造、析构、拷贝构造函数详解
    • 前言
    • 引出
    • 构造函数与析构函数
      • 构造函数
        • 概念及定义
        • 特性
        • 构造函数重载
        • 编译器自动生成的构造函数
        • 显式调用
      • 析构函数
        • 概念与定义
        • 特性
        • 编译器自动生成的析构函数
    • 总结

C++构造、析构、拷贝构造函数详解


前言

如果一个类中没有任何成员变量和成员函数,则称为空类。但空类并非"完全为空",编译器会自动生成以下6个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 赋值运算符重载
  5. const成员函数
  6. 取地址及const取地址操作符重载

在这里插入图片描述

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
本文重点解析构造函数、析构函数的核心用法与原理。


引出

看一下上文中我们的示意栈类:

class Stack {//成员方法void Init(int defaultCapacity = 4) {base = (int*)malloc(sizeof(int) * defaultCapacity);if (base == nullptr) {perror("malloc failed\n");return;}this->size = 0;this->capacity = defaultCapacity;}void Destroy() {free(this->base);this->base = nullptr;this->top = this->capacity;}//成员变量int* base;int top;int capacity;
};

即便是这样实现,也会有很多不便的地方:

  1. 我们在使用时经常忘记Init或者忘记Destroy,尝尝会忘记初始化忘记释放内存,这会导致我们的程序错误或者内存泄漏。
  2. 每次使用前需要调用Init函数,使用结束后需要调用Destroy函数。
int main(){Stack st1;st1.Init();Stack st2;st2.Init();st1.Destroy();st2.Destroy();return 0;
}

可以通过 InitDestroy公有方法初始化栈,但如果每次创建对象时都调用该方法,未免有点麻烦,那能否在对象创建或销毁时,自动调用呢?

C++的构造函数和析构函数就是为了解决这样的问题。


构造函数与析构函数

构造函数


概念及定义

构造函数是一个特殊的成员函数名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次

构造函数的作用:在对象创建时自动完成初始化


特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象

特征如下:

  1. 函数名与类名相同
  2. 无返回值,也不需要写void。
  3. 对象实例化时,自动调用对应的构造函数。
  4. 构造函数可以重载,可以有缺省参数。
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数一旦用户显式定义编译器将不再生成
  6. 构造函数不开辟空间,只完成对象的初始化
class Date {
public:Date(int year = 2025, int month = 2, int day = 2) {this->_year = year;this->_month = month;this->_day = day;}void Print() {cout << this->_year << "--" << this->_month << "--" << this->_day << endl;}
private:int _year;int _month;int _day;
};
int main() {Date d1;d1.Print();return 0;
}

以上代码的运行结果如下:

在这里插入图片描述
可以看到,main函数中,d1对象创建后,我们未进行初始化,调用Print方法后,输出了构造函数缺省参数中的值。
因此我们可以看到,构造函数会在对象创建的时候自动调用,为对象进行初始化


构造函数重载
class Stack {typedef int StackDataType;
private:StackDataType* base;int top;	//top表示栈顶元素的下一个位置int capacity;
public://构造函数Stack(int defaultCapacity = 4) {	//可能需要多种方式的初始化,因此构造函数可以重载cout << "Stack defaultCapacity = 4" << endl;this->base = (StackDataType*)malloc(sizeof(StackDataType) * defaultCapacity);if (this->base == nullptr) {cout << "malloc failed" << endl;}this->capacity = defaultCapacity;this->top = 0;}Stack(StackDataType* array, int num) {	//重载版本,用现有的数组进行初始化cout << "Stack(array, num)" << endl;this->base = (StackDataType*)malloc(sizeof(StackDataType) * num);if (this->base == nullptr) {cout << "malloc failed" << endl;}assert(this->base);memcpy(this->base, array, sizeof(StackDataType) * num);this->capacity = num;this->top = 0;}//析构函数,稍后介绍。~Stack() {cout << "~Stack" << endl;free(this->base);this->base = nullptr;this->capacity = 0;this->top = 0;}
};

在这里插入图片描述
可以看到,我们用三种方式初始化了Stack类对象,三个对象,因此构造函数和析构函数自动调用三次,这就是可重载的构造函数的好处。

以上我们显式指定构造函数的情景,那么当我们不显式指定,编译器自动生成的构造函数会做些什么呢


编译器自动生成的构造函数

到这里,我们要对默认构造函数有一个全新的认识:
默认构造函数,以下三种函数都可以被称作默认构造函数。

  1. 无参构造函数。
  2. 全缺省构造函数。
  3. 我们没写编译器默认生成的构造函数

看以下运行结果。

在这里插入图片描述
上图的运行结果,当我们使用编译器自动生成的构造函数时,我们的成员变量的值出现了随机值,可见,编译器自动生成的构造函数,并未对这里的成员变量进行初始化。其实这是有渊源的。

C++标准规定:

C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char/double......以及任意类型的指针变量,自定义类型就是我们使用class/struct/union等自己定义的类型。
编译器生成默认的构造函数,对内置类型不做处理,对自定义数据类型会调用他的默认构造函数

编译器生成默认的构造函数的行为

  • 内置类型不做处理
  • 自定义数据类型会调用其构造函数

观察以下运行结果:
在这里插入图片描述
可以看到:当成员变量中,存在自定义类型时,自定义类型会调用其默认构造函数(包括三种,无参,全缺省,编译器生成), 但我们惊奇的发现,变量_year, _month, _day也被进行了初始化。

其实这是一种C++标准未定义的行为,VS的编译器,在自定义类型和内置类型同时存在时,会调用自定义类型的构造函数,同时也对内置类型进行了初始化。这样的行为只是VS编译器的个人行为,C++标准中并未这样定义。

C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char/double......以及任意类型的指针变量,自定义类型就是我们使用class/struct/union等自己定义的类型。
编译器生成默认的构造函数,对内置类型不做处理,对自定义数据类型会调用他的默认构造函数

结论

  • 一般情况下, 有内置类型成员,需要自己写构造函数,不能用编译器默认生成的
  • 类内全部都是自定义类型时, 可以考虑让编译器自己生成,前提是自定义类型的构造函数已实现

显式调用

有如下Date

//自动生成的构造函数
class Date {
public:Date(int year = 2205, int month = 2, int day = 11) {this->_year = year;this->_month = month;this->_day = day;}//语法上,该重载函数可以存在,但是,无参调用时会存在歧义,因此不能存在//Date() {		//	this->_year = 3;//	this->_month = 3;//	this->_day = 3;//}//和缺省值参数相比,这个函数就没有存在的必要了/*Date(int year, int month, int day) {	this->_year = year;this->_month = month;this->_day = day;}*/
private:int _year;int _month;int _day;
};

关于Date的构造函数注意事项已在注释中给出,我们来看以下几种显式调用。

int main(){//构造函数的调用和普通函数不同,构造函数是对象+初始化参数 或者 自动调用Date d1;//Date d1();	//不能这样写,这样会和函数声明区分不开Date d2(2025, 2025, 2025);		//构造函数的调用,对象名接参数列表Date d3;//d3.Date(2025, 2025, 2025);//这样写毫无意义,首先这种写法忽视了自动调用的问题,其次,这种写法Date为什么不叫Init
}
  1. 错误Date d1() , 这种写法会和函数声明区分不开。
  2. 正确Date d2(2025, 2025, 2025), 构造函数的调用,对象名接参数列表,
  3. 错误d3.Date(2025, 2025, 2025), 这样写毫无意义,首先这种写法忽视了自动调用的问题,其次,这种写法中,构造函数Date为什么不叫Init?

析构函数

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没的呢?

概念与定义

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。
对象在销毁时会自动调用析构函数,完成对象中资源的清理工作, 主要是堆上申请的资源需要清理,栈区的空间会在生命周期结束时自动释放

特性

析构函数是特殊的成员函数,其特征如下:

  1. 析构函数名是在类名前加上字符 ~
  2. 无参数,无返回值类型,也无需写void。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
  5. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
class Stack {typedef int StackDataType;
private:StackDataType* base;int top;	//top表示栈顶元素的下一个位置int capacity;
public://构造函数Stack(int defaultCapacity = 4) {	//可能需要多种方式的初始化,因此构造函数可以重载cout << "Stack defaultCapacity = 4" << endl;this->base = (StackDataType*)malloc(sizeof(StackDataType) * defaultCapacity);if (this->base == nullptr) {cout << "malloc failed" << endl;}this->capacity = defaultCapacity;this->top = 0;}Stack(StackDataType* array, int num) {	//重载版本,用现有的数组进行初始化cout << "Stack(array, num)" << endl;this->base = (StackDataType*)malloc(sizeof(StackDataType) * num);if (this->base == nullptr) {cout << "malloc failed" << endl;}assert(this->base);memcpy(this->base, array, sizeof(StackDataType) * num);this->capacity = num;this->top = 0;}//析构函数~Stack() {cout << "~Stack" << endl;free(this->base);this->base = nullptr;this->capacity = 0;this->top = 0;}
};

在这里插入图片描述
可以看到,我们创建了三个对象,main函数结束时,对象销毁,析构函数也就调用了三次。

析构函数较构造函数,并没有那么复杂,因此我们只要熟记特性,记得在析构函数中完成对象中资源的清理即可

编译器自动生成的析构函数

关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定义类型成员调用它的析构函数。

class Time{
public:~Time(){cout << "~Time()" << endl;
}
private:int _hour;int _minute;int _second;
};
class Date{
private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _time;
};

在这里插入图片描述

程序运行结束后输出:~Time(),但在main函数中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?

这是因为:main函数中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是内置类型,销毁时不需要资源清理,最后系统直接将其内存回收即可;
timeTime类对象,所以在d销毁时,要将其内部包含的Time类time对象销毁,所以要调用Time类析构函数

但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date类没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数。

即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁。

main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数。

注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数

总结

  1. 构造函数:初始化对象,支持重载,内置类型需手动初始化
  2. 析构函数:清理资源,动态内存必须手动释放

关键点理解编译器默认行为,根据需求选择显式实现

文章到此结束啦,欢迎各位大佬在评论区讨论交流,如果觉得文章写的不错,还请留下免费的赞和收藏

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词