目录
构造函数与析构函数
构造函数
析构函数
六大默认成员函数函数
默认构造与默认析构
内置类型缺省值
拷贝构造
拷贝构造出现场景
深浅拷贝问题
日期类
当我们在写一个栈或者队列等数据结构时,经常可能忘了Init或者Destory,当一些特殊情况需要频繁使用它们时,可能会十分复杂,为此,c++出现了构造函数和析构函数,分别负责初始化和清理。
作为特殊的成员函数,它具有以下特点:
1.构造函数的名字必须与类名一致
2.无返回值(无返回类型)
3.对象实例化后自动调用构造函数
4.支持重载
5.一个类可以有多个构造函数(多种初始化方式)
现在以栈的push为例,如果不进行初始化会怎样?这里编译器并没有检测出来我的错误,不过在调试的时候因为没有分配内存给我报了错误。
解决方案如下:
可以看到,我还是没有调用初始化函数,却能够运行,这是为什么呢?
Stack st1;//无参
Stack st2(4);//有参
谜题揭晓,这里我创建了两个重载构造函数,为了方便清楚编译器是否调用,打印出相应的代号看看
现在大家应该清楚了自动调用这个概念了吧,注意在调用无参的构造函数时是不需要在创建对象时加任何后缀的(可能这样的定义方式与函数类似?) 。这样设计的好处就是省去自己去调用st.Init函数这步,交给编译器去解决,不会出现粗心忘写的情况啦。
这里插入一个我自己感到疑虑的问题,假设重载中有全缺省和一个无参的函数,那么当我不传参数的时候调用哪个函数呢,用Stack构造函数测试发现这个调用不明确的问题,记住一点:在类型相同的情况下,全缺省和无参的同名函数只能存在一个。全缺省比无参用途更广泛,具体取决于实际情况。希望大家在编程时也注意这个问题。
与构造函数相反,析构函数充当了清理工作,在对象销毁时自动调用。为什么是清理而不是销毁,因为变量的销毁取决于它的生命周期,而析构就是将对象回归到它最初的样子,类似出厂设置。
特点:
1.析构函数在类名前加~
2.无参且无返回值
3.一个类只能有一个析构函数,若未显示定义,则会自动创建默认析构函数
4.对象销毁时自动调用
5.不支持重载
~Stack()//添加析构
{
free(a);
capacity = top = 0;
cout << "~stack" << endl;
}
可以看到,有返回的地方都调用了析构,还有之前学的this指针,以及初始化这几个方面,有没有觉得更加方便安全呢?
如果说构造函数的出现使编程更加安全高效,那么默认成员函数就是更加简洁,不过正是因为这点,才让人容易分不清其中的一些细节。
为什么叫做默认成员函数呢?主要原因是当没有写它们时,编译器会自动生成相应的成员函数。一旦定义,就不会生成。
定义一个日期类
class Date {
public:
//Date()//无参
//{
// _year = 1;
// _month = 1;
// _day = 1;
//}
Date(int year = 1, int month = 2, int day = 3)//有参
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
有的人说既然编译器默认生成我们为什么还要去定义,我们把构造函数去掉看看
咦?这是怎么回事呢?这其实是c++的一个缺陷。基于这个现象,我们引出一个新概念。
c++将类型分成了两类,内置类型(struct/union/class等自己定义的)和自定义类型(内置类型,int/double...),默认生成的构造函数
对内置类型不做处理,对自定义类型的函数进行初始化的处理。
自定义类型成员变量会去调用它的默认构造。(不用传参,全缺省,无参,默认生成)
我们可以简单实现以下用两个栈实现一个队列这个类来看以下是否调用了默认构造或析构函数。
可以发现,我们实现的类确实调用了Stack中构造和析构函数。
题外话:有人说我在类里面声明的Stack s1这个整体是成员变量,而声明是无法定义成Stack s1(n)的形式(实例化),还有一个原因是因为内置类型默认初始化是不支持传参的。我的建议是只保留一个全缺省的Stack默认构造函数,这样就可以给定默认初始值啦。
结论:一般建议,每个类提供一个默认构造函数(如果自己写了有参的构造函数就不会自动生成构造函数了),方便不传参测试调用。
我们再来看看初始化情况
可以看到,对比日期类,确实初始化了,这里我们定义的x也被初始化了0,不要以为编译器可以初始化内置类型,这可能是编译器在初始化的时候顺便做的处理,接下来看看如何正确初始化内置类型吧。
为了解决这个缺陷,c++11引入对内置类型可以给缺省值的概念。
当然,既然是缺省,肯定是不能代替构造函数的,如果你定义了构造函数初始化了这些对象,它们就不会起作用。
接下来有个问题考验大家是否真的掌握了以前的知识
Date()//空类
{ }
假设我传空类,缺省会发挥作用吗?
答案是肯定会,就算你定义了构造函数,如果没对缺省对象赋值,就会以缺省为标准。
析构函数因为不支持重载和传参且只能存在一个,比起构造函数省了很多事,同样地,它只处理自定义类型而对内置类型不做处理。
拷贝构造是构造函数的一个重载形式
拷贝构造参数只有一个且必须是类类型,在传递参数时形参必须是引用类型,否则编译器会报错。
第一点很好解释,可以理解成拷贝构造是特殊的构造函数。关键是为什么形参必须是引用类型
我们先来看看默认生成的拷贝构造函数
可以看到是完美拷贝了类中的类型。接下来我们 自己写一个拷贝构造函数。
Date(const Date a)//拷贝构造
{
cout << "Date copy" << endl;
_year = a._year;
_month = a._month;
_day = a._day;
}
编译发现错误
与默认构造/析构函数相反,对于内置类型,编译器能够直接拷贝,无需调用拷贝构造,但对于自定义类型,编译器会调用拷贝构造。
这个错误是因为函数传值传参(自定义类型)过程中调用拷贝构造造成无限递归,正确做法是在形参上加上引用,使其成为实参的别名,避免这种情况。因为引用和指针作为内置类型,是不会去调用拷贝构造的。
除此之外,拷贝构造还能写成 赋值的形式,也能运行
(注意:单独的d2 = d1的操作只会调用默认的拷贝构造,而Date d2 = d1等价于Date d2(d1)去调用拷贝构造)
大概是这个样子
至于为什么要加const,我的建议是,对于出了函数作用域不被销毁的对象,为了避免恶意篡改不需要改动的参数,函数相应的形参都应该加上const,因为一旦发生这种情况,很难检测出是哪里出了问题。
用指针实现拷贝构造
拷贝构造函数会在以下三种情况下被调用:
1. 使用已存在的对象来初始化一个新对象,例如:
MyClass obj1; // 声明一个MyClass类的对象obj1
MyClass obj2 = obj1; // 调用拷贝构造函数将obj1初始化为obj2
2. 构造函数的参数是类的对象,例如:
void func(MyClass obj); // 参数是MyClass类的对象
MyClass obj1;
func(obj1); // 调用拷贝构造函数将obj1传递给func()函数中的参数obj
3. 函数返回一个类的对象,例如:
MyClass func();
MyClass obj2 = func(); // 调用拷贝构造函数返回func()函数中创建的MyClass对象,并将其初始化为obj2。
注意:编译器自己处理内置类型赋值和调用拷贝构造是两种不同的操作,相同:都是浅拷贝。
在c语言中结构体可以直接拷贝,虽然简单,但它存在一个浅拷贝问题,且本身能够直接赋值的定义方式就是一种不好的行为。
C++中的拷贝构造函数默认是浅拷贝,只会复制对象本身的成员变量,而不会复制指针指向的数据。因此,如果一个类中存在指针类型的成员变量,默认的拷贝构造函数是无法正确地拷贝对象的,这往往会导致内存泄漏和程序崩溃等问题。
假设定义一个链表,通过浅拷贝生成的对象可能会时两个对象的指针指向同一块空间。
为了避免这种问题,我们可以通过手动编写深拷贝构造函数来进行复制,确保所有指针指向的数据都能被正确地复制。
我们用目前学的知识来给日期类加点新东西吧,添加啊一个计算多少天后/前的函数,这里涉及到一些常识和的算法知识,比如说闰年、2月和天数进位的问题。
在类里面实现一个制取天数的函数
int GetMonthDay(int year,int month)//获取日期
{
assert(month >= 1 && month < 13);//判断合法
int arr[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };//下标0置为空
if (month == 2 && (year%100 && !(year%4) || year%400 ==0))
{
return 29;
}
return arr[month];
}
计算之后天数的函数
Date& GetAfterDay(int x)
{
_day += x;//this指针
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
if (++_month > 12)
{
_year++;
_month = 1;//进位
}
}
return *this;
}
验证特别天数看是否符合
先后测试了三种特殊情况不再多加演示均是正确的,而且调用了拷贝构造函数。但是有个错误点,我们在计算日期后改变了d1的值,与我们的要求不符,接下来我们再做一次改进。
c语言中我们创建临时变量采取赋值的方式
Date tmp;
tmp._day = _day;
tmp._month = _month;
tmp._year = _year;
对于类,我们有更好的办法——拷贝构造
通过调试可以发现调用了两次拷贝构造,第一次在创建临时变量的时候,第二次是return的时候,调用拷贝构造(类似内置类型返回的值会存在一个临时变量中)。有拷贝构造就有开销,对于tmp我们无法引用返回,但对于刚才return *this (this会销毁)在出了作用域后还存在的对象,可以使用引用返回,减少开销,这也从侧面说明了自己写拷贝构造的优势所在。