问题引入: 


在C++中,如果一个类中什么成员都没有,简称空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。这6个默认成员函数包括:构造函数,析构函数,拷贝构造函数,赋值重载,以及两个取地址。


本篇我们将重点讨论构造函数,析构函数以及拷贝构造函数。

目录

1.类的6个默认成员函数

2.构造函数

3.析构函数

4.拷贝构造函数

1.类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_类与对象[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_02

2.构造函数

2.1构造函数的概念

在正常情况下我们初始化是如下这段代码,这是很正常的。结果也如我们所想的那样。

class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}

private:
int _year;
int _month;
int _day;

};
int main()
{
Date d1;
d1.Init(2022, 05, 16);
d1.Print();

return 0;
}

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_03

运行结果:

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_04[ C++ ] C++类与对象之 类中6个默认成员函数(1)_构造函数_05


那么有没有一种可能性,是我们忘记初始化,那就成了随机值了。甚至有时会崩溃掉。 

因此C++的大佬们为了弥补这个问题,从而设计出来了构造函数。


[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_06[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_07

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

2.2构造函数的特性


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


其特征如下:


1. 函数名与类名相同。


2. 无返回值。


3. 对象实例化时编译器自动调用对应的构造函数。


4. 构造函数可以重载。




class Date
{
public:
//自动调用
Date()
{
_year = 1;
_month = 1;
_day = 1;
}

void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}

private:
int _year;
int _month;
int _day;

};
int main()
{
Date d1;
d1.Print();

return 0;
}

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_类与对象_08

 此时我们可以通过调试来看一下d1。

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_构造函数_09[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_10

 [ C++ ] C++类与对象之 类中6个默认成员函数(1)_构造函数_11[ C++ ] C++类与对象之 类中6个默认成员函数(1)_构造函数_12

 d1在实例化时会自动调用对应的构造函数,是完成初始化工作的。并不是开空间创建对象。

2.2.2构造函数可以重载

class Date
{
public:
//自动调用
// 1.无参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}

// 2.带参构造函数
//全缺省参数
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}

void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}

private:
int _year;
int _month;
int _day;

};
int main()
{
Date d1;
d1.Print();

Date d2(2022,05,16);
d2.Print();
return 0;
}

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_类与对象_13

 运行结果:

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_14[ C++ ] C++类与对象之 类中6个默认成员函数(1)_构造函数_15

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_16[ C++ ] C++类与对象之 类中6个默认成员函数(1)_类与对象_17


 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明

例如:以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象

Date d3();

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_类与对象_18

这样就保证了对象一定会初始化。同时构造函数也可以重载。


2.全缺省:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。


这样我们就可以将上述两个无参和有参进行合并成一个全缺省参数的构造函数

//全缺省参数
Date(int year = 1,int month = 1 ,int day = 1)
{
_year = year;
_month = month;
_day = day;
}

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_构造函数_19

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_类与对象_20[ C++ ] C++类与对象之 类中6个默认成员函数(1)_类与对象_21

我们发现,这样就很方便了,我们可以自主的进行传参。 

 [ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_22[ C++ ] C++类与对象之 类中6个默认成员函数(1)_构造函数_23



3.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。


这句话意思就是我们如果不写,编译器会自动生成一个无参的构造函数,如果我们自己写构造函数,编译器就不生成了。 

例如:这段代码,我们并没有自主的写构造函数,但是C++会自动给我生成一个构造函数。

class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}

private:
int _year;
int _month;
int _day;

};
int main()
{
Date d1;
d1.Print();

return 0;
}

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_24

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_25[ C++ ] C++类与对象之 类中6个默认成员函数(1)_构造函数_26

但是我们发现,这个构造函数好像并没有给我初始化,打印的值是随机值,那这是为什么呢?

这是因为:C++把变量分成两种:

1、内置类型/基本类型:int,char,double,指针......

2、自定义类型:class/struct去定义类型对象

C++默认生成构造函数对于内置类型成员变量不做处理,对自定义类型成员变量才会处理。

因此这里年月日属于内置类型,因此没有处理。我们来举一个自定义类型的,看是否处理。

class A
{
public:
A()
{
cout << "A()" << endl;
_a = 0;
}
private:
int _a;
};

class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}

private:
int _year;
int _month;
int _day;

A _aa;

};
int main()
{
Date d1;
d1.Print();

return 0;
}

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_构造函数_27

这段代码中。A就是一个自定义类型,我们来通过调试看看_aa是否会处理。

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_28[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_29

 我们发现_aa被初始化为了0。因此也验证了我们上述说到的内置类型不会被处理,自定义类型才会被处理。

3.析构函数

3.1概念


前面通过了解构造函数,我们知道一个对象是怎么来的,那一个对象又是怎么销毁的呢?




析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。


3.2特性


析构函数是特殊的成员函数。


特征如下:


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


2. 无参数无返回值。


3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。


4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。



class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}

~Date()
{
cout << "~Date" << endl;
}

private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_30

运行结果:

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_31[ C++ ] C++类与对象之 类中6个默认成员函数(1)_类与对象_32

我们发现的确调用了析构函数。打印出了~Date。

在上述说到的,析构函数是要完成对资源的清理工作,Date类里面都是对象成员本身,会随着栈帧的销毁随之销毁。那么什么需要资源清理呢?一般对于malloc,或者new的类才需要写析构函数。因此不是所有的类都需要析构函数。

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_类与对象_33[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_34

class Stack
{
public:
Stack(int capacity = 10)
{
_a = (int*)malloc(sizeof(int) * capacity);
assert(_a);

_top = 0;
_capacity = capacity;
}

~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}


private:
int* _a;
int _top;
int _capacity;

};
int main()
{
Stack st;
return 0;
}

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_构造函数_35

因此我们可以发现:构造函数就相当于我们的初始化,析构函数就相当于我们的Destory。



问:st1,st2谁先构造?谁先析构?


[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_36[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_37

 为了验证这个,我们可以传一个值观察

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_38[ C++ ] C++类与对象之 类中6个默认成员函数(1)_类与对象_39​编辑[ C++ ] C++类与对象之 类中6个默认成员函数(1)_类与对象_40[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_41

我们发现先析构的是传入2的值。因此我们得到结果:st1先构造,st2先析构

结论:先构造的后析构。


5、编译器生成的默认析构函数,对会自定类型成员调用它的析构函数。这与构造函数是类似的。


4.拷贝构造函数

4.1概念


构造函数只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。


4.2特征 


拷贝构造函数也是特殊的成员函数,其特征如下:


1. 拷贝构造函数是构造函数的一个重载形式


2. 拷贝构造函数的参数只有一个必须使用引用传参,使用传值方式会引发无穷递归调用


拷贝构造就相当于ctrl+c,ctrl+v。 


拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调


传值构造会引发无穷递归。

void Func(Date d)
{

}
int main()
{
Date d1(2022, 05, 16);
Func(d1);

return 0;
}

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_42

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_构造函数_43[ C++ ] C++类与对象之 类中6个默认成员函数(1)_构造函数_44

要进入Func函数要先进入Date函数,因此如果传值传参的话就会引发无穷递归

 [ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_45[ C++ ] C++类与对象之 类中6个默认成员函数(1)_构造函数_46

如果使用传值传参VS2019也会报错

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_类与对象_47[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_48



因此这里我们为了避免无穷递归,我们可以使用引用传参。 


class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}

//Date d2(d) 错误的 不能传值传参
Date(const Date &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}

void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}

private:
int _year;
int _month;
int _day;
};

int main()
{
Date d1(2022, 05, 16);

Date d2(d1);


return 0;
}

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_49

内置类型是直接拷贝,自定义类型对象,拷贝初始化规定要调用拷贝构造完成。

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_类与对象_50[ C++ ] C++类与对象之 类中6个默认成员函数(1)_构造函数_51  [ C++ ] C++类与对象之 类中6个默认成员函数(1)_类与对象_52[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_53

那栈的拷贝构造能不能这样写?

class Stack
{
public:
Stack(int capacity = 10)
{
_a = (int*)malloc(sizeof(int) * capacity);
assert(_a);

_top = 0;
_capacity = capacity;
}
Stack(const Stack& st)
{
_a = st._a;
_top = st._top;
_capacity = st._capacity;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}


private:
int* _a;
int _top;
int _capacity;

};
int main()
{
Stack st1(1);
Stack st2(st1);

return 0;
}

[ C++ ] C++类与对象之 类中6个默认成员函数(1)_构造函数_54

答案:不能

原因:此时st1和st2所指的空间是同一块空间,构造函数是没有问题的,但是到析构函数的时候,st1析构的时候已经将这块空间释放了,到st2析构的时候就会出现错误。这个属于浅拷贝,在后面我们需要使用深拷贝来对这个问题进行处理。[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_55[ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_56

 [ C++ ] C++类与对象之 类中6个默认成员函数(1)_析构函数_57[ C++ ] C++类与对象之 类中6个默认成员函数(1)_构造函数_58


(本篇完)