C++_构造函数与析构函数

2023-12-16 17:12

本文主要是介绍C++_构造函数与析构函数,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

       

目录

1、构造函数的写法

1.2 构造函数优化写法

2、默认构造函数与默认成员函数

2.1 默认成员函数对不同类型的处理

3、对内置类型的补丁

4、析构函数

4.1 析构函数的写法

5、默认析构函数

6、初始化列表

6.1 初始化列表的写法

6.2 初始化列表的作用

 6.3 回顾与总结

 结语:


前言:

        构造函数和析构函数都是属于类中的成员函数,在实例化一个对象时系统会自动调用该对象的构造函数,因此他常用于初始化对象成员变量,一个对象在其生命周期中只会调用一次构造函数。而析构函数与构造函数作用”相反“,他是在对象销毁时自动被系统调用的,所以他的任务是清理、释放对象申请的空间资源,一个类中只能有一个析构函数。

1、构造函数的写法

        1、构造函数作为成员函数因此他必须写在类中。

        2、构造函数的函数名必须与类名一模一样。

        3、构造函数是不写返回类型的。

        4、构造函数可以实现函数重载,即一个类可以有多个构造函数。

        5、构造函数的形参可有可无,具体根据需求。

        鉴于以上无点,可以先初步认识构造函数的写法 :

#include<iostream>
using namespace std;class Date
{
public:Date()//构造函数1(无参){_year = 0;_month = 0;_day = 0;}Date(int year, int month, int day)//构造函数2(有参){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;
};int main()
{//实例化对象就会自动调用构造函数,因此在实例化时可以执行直接传参Date dt1;//调用时没有传递实参,调构造函数1Date dt2(2022,2,22);//调用时传递了实参,则调构造函数2return 0;
}

        通过调试可以观察到对象dt1和dt2具体调用的是哪个构造函数:

        可以看到dt1实例化时没有传实参,因此dt1调用的是无参构造函数。而dt2传了实参(2022,2,22),因此dt2调用的是有参构造函数,他们的成员变量的值都是调用了对应的构造函数而得来的。

1.2 构造函数优化写法

        以上两个构造函数可以利用全省参数的概念将他们合成一个构造函数,而且同样可以实现两个构造函数的功能。

        优化代码如下:

#include<iostream>
using namespace std;class Date
{
public:Date(int year=0, int month=0, int day=0)//写成全缺省的形式,并且缺省参数赋值为0{_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;
};int main()
{Date dt1;//若不传实参,则dt1的成员依旧初始化为0 0 0Date dt2(2022,2,22);//传实参多少则dt2的成员初始化为多少return 0;
}

        注意:若写成全省参数的形式,则就不能够再写构造函数1(无参写法),两种形式的构造函数只能存在一个。因为当对象实例化并且是无参调用时,系统会不知道调用全省参数形式的函数还是构造函数1(无参写法)的函数,调用会出现歧义。

2、默认构造函数与默认成员函数

        以上代码的构造函数都是自己写出来的,因此这种形式的构造函数又称为显式构造。如果我们自己不写构造函数,系统也会自动生成一个无参构造函数,又叫默认构造函数,该形式的构造函数称为隐式构造。当然,如果我们已经写了构造函数,那么系统就不会再自动生成默认构造函数了。注:我们一般把系统自动生成的默认构造函数叫做默认成员函数

        构造函数关系图:

        可以发现默认构造函数的特点:只有无参调用才能调用默认构造函数。 

        以下代码就是调用默认成员函数的例子:

#include<iostream>
using namespace std;class Date
{
public://没有显式构造                       
private:int _year;int _month;int _day;
};int main()
{Date dt1;return 0;
}

        可以通过调试窗口观察dt1的成员变量的值变化:

        可以看到即使我们没有写构造函数,dt1成员变量的值也发生了改变,这就是因为系统自动调用了自己生成的默认成员函数,因此使dt1成员变量的值发生了改变,侧面也可以证实默认成员函数的存在。注意:因为默认成员函数是无参的,因此实例化对象时不能传参数。

        但是这里即使调用了系统自动生成的默认成员函数,dt1成员初始化的值依然是随机值,这么一看默认成员函数还不如我们自己手写的构造函数有用,这里生成随机值的原因是默认成员函数对不同的成员变量类型有着不同情况的处理。

2.1 默认成员函数对不同类型的处理

        首先在C++中把类型分成了两大类,一个是内置类型(即int、char...和各种指针类型)、一个是自定义类型,就是由程序员用struct、class、union..定义的自定义类型。默认成员函数对内置类型的成员变量不做任何处理,这也是为什么上面的dt1成员会是随机值。然而默认成员函数对自定义类型成员变量处理是:会调用该成员变量自己的默认构造函数

        默认成员函数对自定义类型处理的代码例子:

#include<iostream>
using namespace std;class Time
{
public:Time(){cout << "Time()" << endl;//观察是否调用了该构造函数_hour = 0;_minute = 0;_second = 0;}
private:int _hour;int _minute;int _second;
};class Date
{
public:private:int _year;int _month;int _day;Time t;//Date类中有一个成员变量的类型是自定义类型Time
};int main()
{Date dt1;return 0;
}

        运行结果:

         从结果可以看到,在屏幕上打印了Time类中的默认构造函数里的打印内容,而且Date类里是没有写任何构造函数的,说明Date类中自己生成了构造函数并且让成员变量t调用了Time中的默认构造。注意:若dt2实例化时不能传实参,因为要调用默认构造函数必须是无参调用

3、对内置类型的补丁

        由于默认成员函数对内置类型是不做任何处理的,因此内置类型再进行声明的时候可以对其赋值,但是这种赋值看起来像对成员进行初始化,实则上只是缺省值的一种写法。

        对内置类型声明时进行赋值的测试代码如下:

#include<iostream>
using namespace std;class Date
{
public:void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}Date(){_year = 2023;}private://成员声明时进行赋值int _year=2022;int _month=2;int _day=22;
};int main()
{Date dt1;dt1.Print();return 0;
}

        运行结果:

         从结果中可以得出,当在声明中给_year赋值2022时,由于我们又写了一个默认构造函数把_year的值改成了2023,导致最终打印的是默认构造函数中的2023,这里可以理解为没有用到_year的缺省值。然而在声明时给_month和_day赋予的值,并没有在构造函数中对他们的值进行修改,以至最后打印出2和22,这里可以理解为用到了_month和_day缺省值。因此把给声明中给成员变量赋值的操作叫做给成员变量赋缺省值。

4、析构函数

        析构函数与构造函数作用”相反“,他是在对象销毁时自动被系统调用的,所以他的任务是清理、释放对象申请的空间资源。

4.1 析构函数的写法

        1、析构函数的函数名规定要在类名的前面加个‘~’字符构成析构函数名。

        2、一个类只能存在一个析构函数。

        3、析构函数在对象销毁时由系统自动调用。

        4、析构函数也是不写返回类型的。

        5、一般是涉及到空间的开辟和释放的时候才会手动写析构函数,内置类型只要出了栈帧就会自动销毁。

        6、析构函数不能带有形参。

        析构函数测试代码:

#include<iostream>
using namespace std;class Stack
{
public:Stack(int n=4)//构造函数{cout << "Stack(int n=4)" << endl;//观察系统是否调用该函数_arr = (int*)malloc(sizeof(int) * n);if (_arr == nullptr){perror("malloc");return;}_Top = 0;_capacity = n;}~Stack()//析构函数{cout << "~Stack()" << endl;//观察系统是否调用该函数free(_arr);_arr = nullptr;_Top = _capacity = 0;}private:int* _arr;int _Top;int _capacity;
};int main()
{Stack st1;return 0;
}

        运行结果:

         从结果可以看到,在对象st1销毁时系统会自动调用析构函数。

5、默认析构函数

        默认析构函数即我们不写析构函数时,编译器也会自动生成一个默认析构函数。默认析构函数同样对内置类型的成员变量不做处理,对自定义类型的成员变量会调用该成员的析构函数。因此涉及到空间资源时就必须要手动写析构函数了,比如上述的析构函数测试代码,如果不写析构函数,那么系统是不会处理_arr申请的空间的,就会引发内存泄漏的问题。

        默认析构函数调用测试:

#include<iostream>
using namespace std;class Stack
{
public:Stack(int n=4)//构造函数{cout << "Stack(int n=4)" << endl;//观察系统是否调用该函数_arr = (int*)malloc(sizeof(int) * n);if (_arr == nullptr){perror("malloc");return;}_Top = 0;_capacity = n;}~Stack()//析构函数{cout << "~Stack()" << endl;//观察系统是否调用该函数free(_arr);_arr = nullptr;_Top = _capacity = 0;}private:int* _arr;int _Top;int _capacity;
};class MyQueue
{Stack st1;//st1和st2是MyQueue类中的自定义类型的成员变量Stack st2;
};int main()
{MyQueue mq;return 0;
}

        运行结果:

        从结果可以看出,在MyQueue这个类中是没有写析构函数的,因此编译器会自动生成一个默认析构函数,并且对自定义类型的成员处理是:调用该自定义类型的析构函数。 

6、初始化列表

        严格来说,之前讲到在构造函数内进行初始化,这一操作并不是真正意义上的初始化,只是一种赋值行为,因为在构造函数内可以进行多次的赋值,然而初始化的真正含义是每个成员、对象只有一次初始化的机会。因此在构造函数内还隐藏了一部分,该部分就是初始化列表,而不论构造函数的初始化列表有没有内容,系统都会自动遍历一遍初始化列表。

6.1 初始化列表的写法

        初始化列表存在于构造函数的“中间”,即函数+形参和构造函数的大括号“{}”的中间部分, 以冒号开始,逗号进行分隔成员变量,每个成员变量后面跟小括号,小括号内就是要初始化的内容,而且每个成员变量只能写一次,对应每个成员的初始化只有一次机会。

        体现初始化列表的代码如下:

#include<iostream>
using namespace std;class Date
{
public://初始化列表的形式Date():_year(2022)//对_year成员进行初始化,值为2022,_month(2),_day(22){}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}private:int _year;int _month;int _day;
};int main()
{Date dt1;dt1.Print();return 0;
}

        运行结果:

        从结果可以看到这么写也可以进行对成员变量的赋值,其实这才是正在意义上的初始化。当然,初始化列表不仅仅只有以上作用。

6.2 初始化列表的作用

        因为有三种情况的成员变量是必须通过初始化列表完成赋值:

        1、引用成员变量

        2、被const修饰的成员变量

        3、没有默认构造函数的自定义类型成员变量

        首先第一种情况和第二种情况的示例图:

        可以看到,错误的原因在于a和b作为引用和被const修饰过的变量,在声明的时候就必须要进行初始化,这也是引用和const关键字的规定。

        解决方法:在初始化列表为a和b进行初始化即可:

class Date
{
public://初始化列表的形式Date():_year(2022)//对_year成员进行初始化,值为2022, _month(2), _day(22), b(_year)//b作为a的引用, a(10)//a的值初始化为10{}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}private:const int a;int& b;int _year;int _month;int _day;
};int main()
{Date dt1;dt1.Print();return 0;
}

        第三种情况的示例图:

        上述错误的原因在于Stack类中没有默认构造函数,而MyQueue类里的自定义类型成员就无法调用Stack类里的默认构造函数了。因此该情况的解决方法是:在MyQueue类中的初始化列表进行对成员st1和st2的初始化,并且小括号内要给一个值作为“实参”传递给形参int n。

        代码如下:

#include<iostream>
using namespace std;class Stack
{
public:Stack(int n)//构造函数{cout << "Stack(int n=4)" << endl;//观察系统是否调用该函数_arr = (int*)malloc(sizeof(int) * n);if (_arr == nullptr){perror("malloc");return;}_Top = 0;_capacity = n;}~Stack()//析构函数{cout << "~Stack()" << endl;//观察系统是否调用该函数free(_arr);_arr = nullptr;_Top = _capacity = 0;}private:int* _arr;int _Top;int _capacity;
};class MyQueue
{
public://在MyQueue类里使用初始化列表对st1和st2进行初始化MyQueue():st1(4)//把4当成实参传给Stack(int n),st2(4){}
private:Stack st1;Stack st2;
};int main()
{MyQueue mq;return 0;
}

 6.3 回顾与总结

        之前我们提到过当手动写了一个构造函数后,系统则不会在生成默认成员函数,那么如果在MyQueue类中写一个构造函数,且该构造函数内什么都不写,那么按理来说系统就不会去调用自己生成默认成员函数,也就不会对自定义类型的st1和st2进行处理。

        测试在MyQueue类中写一个空白的构造函数,观察编译器是否会去调用自定义类型的默认构造函数:

#include<iostream>
using namespace std;class Stack
{
public:Stack(int n = 4)//Stack类中的构造函数{cout << "Stack(int n=4)" << endl;//观察系统是否调用该函数_arr = (int*)malloc(sizeof(int) * n);if (_arr == nullptr){perror("malloc");return;}_Top = 0;_capacity = n;}private:int* _arr;int _Top;int _capacity;
};class MyQueue
{
public:MyQueue()//在MyQueue类中自己定义一个构造函数{;}
private:Stack st1;Stack st2;
};int main()
{MyQueue mq;return 0;
}

         运行结果:

        从结果得出,编译器竟然还是去调用了st1和st2的默认构造函数,原因是初始化列表在起作用,虽然没在初始化列表中明确的对成员进行初始化,但是编译器还是会遍历初始化列表,所以可以理解成在遍历初始化列表的时候发生了让自定义类型成员变量去调用他的默认构造这一动作。

 结语:

        以上就是关于C++_构造函数和析构函数的讲解,构造函数和析构函数的重点在于捋清默认成员函数和默认构造函数之间的关系,其中的细节非常之多,这也正是该知识点复杂的地方。最后希望本文可以给你带来更多的收获,如果本文对你起到了帮助,希望可以动动小指头帮忙点赞👍+关注😎+收藏👌!如果有遗漏或者有误的地方欢迎大家在评论区补充~!谢谢大家!!

这篇关于C++_构造函数与析构函数的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/501238

相关文章

GO语言中函数命名返回值的使用

《GO语言中函数命名返回值的使用》在Go语言中,函数可以为其返回值指定名称,这被称为命名返回值或命名返回参数,这种特性可以使代码更清晰,特别是在返回多个值时,感兴趣的可以了解一下... 目录基本语法函数命名返回特点代码示例命名特点基本语法func functionName(parameters) (nam

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

Python Counter 函数使用案例

《PythonCounter函数使用案例》Counter是collections模块中的一个类,专门用于对可迭代对象中的元素进行计数,接下来通过本文给大家介绍PythonCounter函数使用案例... 目录一、Counter函数概述二、基本使用案例(一)列表元素计数(二)字符串字符计数(三)元组计数三、C

C++ STL-string类底层实现过程

《C++STL-string类底层实现过程》本文实现了一个简易的string类,涵盖动态数组存储、深拷贝机制、迭代器支持、容量调整、字符串修改、运算符重载等功能,模拟标准string核心特性,重点强... 目录实现框架一、默认成员函数1.默认构造函数2.构造函数3.拷贝构造函数(重点)4.赋值运算符重载函数

Python中的filter() 函数的工作原理及应用技巧

《Python中的filter()函数的工作原理及应用技巧》Python的filter()函数用于筛选序列元素,返回迭代器,适合函数式编程,相比列表推导式,内存更优,尤其适用于大数据集,结合lamb... 目录前言一、基本概念基本语法二、使用方式1. 使用 lambda 函数2. 使用普通函数3. 使用 N

MySQL中REPLACE函数与语句举例详解

《MySQL中REPLACE函数与语句举例详解》在MySQL中REPLACE函数是一个用于处理字符串的强大工具,它的主要功能是替换字符串中的某些子字符串,:本文主要介绍MySQL中REPLACE函... 目录一、REPLACE()函数语法:参数说明:功能说明:示例:二、REPLACE INTO语句语法:参数

C++ vector越界问题的完整解决方案

《C++vector越界问题的完整解决方案》在C++开发中,std::vector作为最常用的动态数组容器,其便捷性与性能优势使其成为处理可变长度数据的首选,然而,数组越界访问始终是威胁程序稳定性的... 目录引言一、vector越界的底层原理与危害1.1 越界访问的本质原因1.2 越界访问的实际危害二、基

python中update()函数的用法和一些例子

《python中update()函数的用法和一些例子》update()方法是字典对象的方法,用于将一个字典中的键值对更新到另一个字典中,:本文主要介绍python中update()函数的用法和一些... 目录前言用法注意事项示例示例 1: 使用另一个字典来更新示例 2: 使用可迭代对象来更新示例 3: 使用

c++日志库log4cplus快速入门小结

《c++日志库log4cplus快速入门小结》文章浏览阅读1.1w次,点赞9次,收藏44次。本文介绍Log4cplus,一种适用于C++的线程安全日志记录API,提供灵活的日志管理和配置控制。文章涵盖... 目录简介日志等级配置文件使用关于初始化使用示例总结参考资料简介log4j 用于Java,log4c

C++归并排序代码实现示例代码

《C++归并排序代码实现示例代码》归并排序将待排序数组分成两个子数组,分别对这两个子数组进行排序,然后将排序好的子数组合并,得到排序后的数组,:本文主要介绍C++归并排序代码实现的相关资料,需要的... 目录1 算法核心思想2 代码实现3 算法时间复杂度1 算法核心思想归并排序是一种高效的排序方式,需要用