目录
1、构造函数的写法
1.2 构造函数优化写法
2、默认构造函数与默认成员函数
2.1 默认成员函数对不同类型的处理
3、对内置类型的补丁
4、析构函数
4.1 析构函数的写法
5、默认析构函数
6、初始化列表
6.1 初始化列表的写法
6.2 初始化列表的作用
6.3 回顾与总结
结语:
前言:
构造函数和析构函数都是属于类中的成员函数,在实例化一个对象时系统会自动调用该对象的构造函数,因此他常用于初始化对象成员变量,一个对象在其生命周期中只会调用一次构造函数。而析构函数与构造函数作用”相反“,他是在对象销毁时自动被系统调用的,所以他的任务是清理、释放对象申请的空间资源,一个类中只能有一个析构函数。
1、构造函数作为成员函数因此他必须写在类中。
2、构造函数的函数名必须与类名一模一样。
3、构造函数是不写返回类型的。
4、构造函数可以实现函数重载,即一个类可以有多个构造函数。
5、构造函数的形参可有可无,具体根据需求。
鉴于以上无点,可以先初步认识构造函数的写法 :
#include
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;//调用时没有传递实参,调构造函数1
Date dt2(2022,2,22);//调用时传递了实参,则调构造函数2
return 0;
}
通过调试可以观察到对象dt1和dt2具体调用的是哪个构造函数:
可以看到dt1实例化时没有传实参,因此dt1调用的是无参构造函数。而dt2传了实参(2022,2,22),因此dt2调用的是有参构造函数,他们的成员变量的值都是调用了对应的构造函数而得来的。
以上两个构造函数可以利用全省参数的概念将他们合成一个构造函数,而且同样可以实现两个构造函数的功能。
优化代码如下:
#include
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 0
Date dt2(2022,2,22);//传实参多少则dt2的成员初始化为多少
return 0;
}
注意:若写成全省参数的形式,则就不能够再写构造函数1(无参写法),两种形式的构造函数只能存在一个。因为当对象实例化并且是无参调用时,系统会不知道调用全省参数形式的函数还是构造函数1(无参写法)的函数,调用会出现歧义。
以上代码的构造函数都是自己写出来的,因此这种形式的构造函数又称为显式构造。如果我们自己不写构造函数,系统也会自动生成一个无参构造函数,又叫默认构造函数,该形式的构造函数称为隐式构造。当然,如果我们已经写了构造函数,那么系统就不会再自动生成默认构造函数了。注:我们一般把系统自动生成的默认构造函数叫做默认成员函数。
构造函数关系图:
可以发现默认构造函数的特点:只有无参调用才能调用默认构造函数。
以下代码就是调用默认成员函数的例子:
#include
using namespace std;
class Date
{
public:
//没有显式构造
private:
int _year;
int _month;
int _day;
};
int main()
{
Date dt1;
return 0;
}
可以通过调试窗口观察dt1的成员变量的值变化:
可以看到即使我们没有写构造函数,dt1成员变量的值也发生了改变,这就是因为系统自动调用了自己生成的默认成员函数,因此使dt1成员变量的值发生了改变,侧面也可以证实默认成员函数的存在。注意:因为默认成员函数是无参的,因此实例化对象时不能传参数。
但是这里即使调用了系统自动生成的默认成员函数,dt1成员初始化的值依然是随机值,这么一看默认成员函数还不如我们自己手写的构造函数有用,这里生成随机值的原因是默认成员函数对不同的成员变量类型有着不同情况的处理。
首先在C++中把类型分成了两大类,一个是内置类型(即int、char...和各种指针类型)、一个是自定义类型,就是由程序员用struct、class、union..定义的自定义类型。默认成员函数对内置类型的成员变量不做任何处理,这也是为什么上面的dt1成员会是随机值。然而默认成员函数对自定义类型成员变量处理是:会调用该成员变量自己的默认构造函数。
默认成员函数对自定义类型处理的代码例子:
#include
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实例化时不能传实参,因为要调用默认构造函数必须是无参调用。
由于默认成员函数对内置类型是不做任何处理的,因此内置类型再进行声明的时候可以对其赋值,但是这种赋值看起来像对成员进行初始化,实则上只是缺省值的一种写法。
对内置类型声明时进行赋值的测试代码如下:
#include
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缺省值。因此把给声明中给成员变量赋值的操作叫做给成员变量赋缺省值。
析构函数与构造函数作用”相反“,他是在对象销毁时自动被系统调用的,所以他的任务是清理、释放对象申请的空间资源。
1、析构函数的函数名规定要在类名的前面加个‘~’字符构成析构函数名。
2、一个类只能存在一个析构函数。
3、析构函数在对象销毁时由系统自动调用。
4、析构函数也是不写返回类型的。
5、一般是涉及到空间的开辟和释放的时候才会手动写析构函数,内置类型只要出了栈帧就会自动销毁。
6、析构函数不能带有形参。
析构函数测试代码:
#include
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销毁时系统会自动调用析构函数。
默认析构函数即我们不写析构函数时,编译器也会自动生成一个默认析构函数。默认析构函数同样对内置类型的成员变量不做处理,对自定义类型的成员变量会调用该成员的析构函数。因此涉及到空间资源时就必须要手动写析构函数了,比如上述的析构函数测试代码,如果不写析构函数,那么系统是不会处理_arr申请的空间的,就会引发内存泄漏的问题。
默认析构函数调用测试:
#include
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这个类中是没有写析构函数的,因此编译器会自动生成一个默认析构函数,并且对自定义类型的成员处理是:调用该自定义类型的析构函数。
严格来说,之前讲到在构造函数内进行初始化,这一操作并不是真正意义上的初始化,只是一种赋值行为,因为在构造函数内可以进行多次的赋值,然而初始化的真正含义是每个成员、对象只有一次初始化的机会。因此在构造函数内还隐藏了一部分,该部分就是初始化列表,而不论构造函数的初始化列表有没有内容,系统都会自动遍历一遍初始化列表。
初始化列表存在于构造函数的“中间”,即函数+形参和构造函数的大括号“{}”的中间部分, 以冒号开始,逗号进行分隔成员变量,每个成员变量后面跟小括号,小括号内就是要初始化的内容,而且每个成员变量只能写一次,对应每个成员的初始化只有一次机会。
体现初始化列表的代码如下:
#include
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;
}
运行结果:
从结果可以看到这么写也可以进行对成员变量的赋值,其实这才是正在意义上的初始化。当然,初始化列表不仅仅只有以上作用。
因为有三种情况的成员变量是必须通过初始化列表完成赋值:
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
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;
}
之前我们提到过当手动写了一个构造函数后,系统则不会在生成默认成员函数,那么如果在MyQueue类中写一个构造函数,且该构造函数内什么都不写,那么按理来说系统就不会去调用自己生成默认成员函数,也就不会对自定义类型的st1和st2进行处理。
测试在MyQueue类中写一个空白的构造函数,观察编译器是否会去调用自定义类型的默认构造函数:
#include
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++_构造函数和析构函数的讲解,构造函数和析构函数的重点在于捋清默认成员函数和默认构造函数之间的关系,其中的细节非常之多,这也正是该知识点复杂的地方。最后希望本文可以给你带来更多的收获,如果本文对你起到了帮助,希望可以动动小指头帮忙点赞+关注+收藏!如果有遗漏或者有误的地方欢迎大家在评论区补充~!谢谢大家!!