【C++】构造与析构
1、默认成员函数
class test
{};
假若一个类什么成员都没有,那便称为空类。
但编译器也会为空类生成6个默认成员函数。
2、构造函数
2.1 构造函数概念
构造函数是一个特殊的成员函数,名字与类名相同
创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次
类对象被创建的时候,编译系统对象分配内存空间,并自动调用构造函数,由构造函数完成成员的初始化工作
构造函数的作用:初始化对象的数据成员
2.2 构造函数特性
- 名字与类名相同,可以有参数,但是不能有返回值(连void也不行)。
- 构造函数是在实例化对象时自动执行的,不需要手动调用。
- 作用是对对象进行初始化工作,如给成员变量赋值等。
- 如果定义类时没有写构造函数,系统会生成一个默认的无参构造函数,默认构造函数没有参数,不做任何工作。
- 如果定义了构造函数,系统不再生成默认的无参构造函数.
- 对象生成时构造函数自动调用,对象一旦生成,不能在其上再次执行构造函数
- 一个类可以有多个构造函数,为重载关系。
2.3 构造函数分类
2.3.1 默认构造
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数
#include<iostream>
using namespace std;
class build
{
public:
// 无参构造函数
// 如果创建一个类你没有写任何构造函数,则系统自动生成默认的构造函数,函数为空
// 如果自己显示定义了一个构造函数,则不会调用系统的构造函数
build()
{}
};
2.3.2 有参构造
一般构造分为两种:
- 列表初始化
- 内部赋值
两者不能混用
#include<iostream>
using namespace std;
class build
{
public:
//一般构造函数
//可以采用两种方式进行对类内成员的赋值,一种是列表初始化,而另一种是内部赋值,但二者不能同时存在
build(int a,int b)
{
_x = a;
_y = b;
}//内部赋值
build(int a,int b):_x(a),_y(b)
{}//列表初始化
private:
int _x;
int _y;
};
2.4 拷贝构造函数
2.4.1 概念
拷贝构造函数:
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
- 一种特殊的构造函数,当对象之间复制时会自动调用拷贝构造函数。
- 若类中没有显示定义拷贝构造函数,则系统会自动生成默认拷贝构造函数。
- 使用场合:旧对象初始化新对象
2.4.2 特性
拷贝构造函数是构造函数的一个重载形式
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date d) // 错误写法:编译报错,会引发无穷递归
Date(const Date& d) //正确写法
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
若未显式定义,编译器会生成默认的拷贝构造函数。
默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
注意:
在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的
这时就有以下问题:
既然会生成默认的拷贝构造函数,那是不是意味着没有显式实现拷贝构造函数的必要了呢?
默认的拷贝构造函数的值拷贝会将内容原封不动地给构造的对象,这就使得拷贝的对象与原来的对象指向同一块内存空间
这样就会导致在程序退出时内存的多次释放,从而造成程序的崩溃
注意:
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝
对应的解决方法就是深拷贝:
- 深拷贝就是对于对象中的动态成员,并不只是简单的赋值,而是重新分配空间,即资源重新分配
2.4.3 使用场景
拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用 尽量使用引用
2.4.4 重载
拷贝构造函数重载与普通的函数重载基本是没有区别,就是同一个函数名因为参数不同代表不同的函数,只是这里的构造函数都没有返回值
3、析构函数
3.1 概念
我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的
而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
3.2 特性
析构函数名是在类名前加上字符 ~
无参数无返回值类型
一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数
注意:析构函数不能重载
对象生命周期结束时,C++编译系统系统自动调用析构函数
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
//在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
//因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。
//但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁
//main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
3.3 要点
注意:
- 创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类