目录
一. 前言
二. 构造函数和析构函数的语法
三. 构造函数的分类和调用
四. 构造函数的调用规则
五. 深拷贝和浅拷贝(面试常考)
六. 初始化列表
一. 前言
任何事物都需要有一个初始化的过程,例如手机,我们在买来使用的时候手机其实就已经被初始化了,如设置的语言为中文等等。同样,在使用后,我们为了自身数据的安全性,就会考虑清楚数据。我们的对象同样如此,也需要一系列的初始化和清理操作。
为了解决上面这两个问题,我们C++中就可以使用构造函数和析构函数来解决。如果我们 不提供构造函数和析构函数,那么编译器就会自动提供,只是都是空实现,也就是空的代码。
二. 构造函数和析构函数的语法
构造函数的主要作用就在于创建对象时,为对象的成员属性赋值,会由编译器自动调用。
析构函数的主要作用就在于执行对象销毁前的一些清理工作,也是在销毁前系统自动调用。
构造函数的语法如下:
类名 (){ }
构造函数的特点如下:
构造函数没有返回值,也不需要写void。
函数名称和类名相同。
构造函数可以有参数,因此可以发生重载。
程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次。
析构函数的语法如下:
~类名(){ }
析构函数的特点如下:
函数名称和类名相同,在类名前面加上了一个~符号。
析构函数也没有返回值,也不写void。
析构函数不可以有参数,因此不可以发生重载。
程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次。
最后再次声明:构造函数和析构函数都是必须有的实现,如果我们自己不提供,编译器也会提供空实现的构造和析构。
三. 构造函数的分类和调用
构造函数总共有两个分类方式:
按参数分:有参构造和无参构造
按类型分:普通构造和拷贝构造
如下所示:
class Person{
Person(){
cout<<"Person类的无参构造(默认构造)函数"<<endl;
}
Person(int age){
cout<<"Person类的有参构造函数"<<endl;
m_age=age; //有参构造初始化类属性
}
int m_age;
Person(const Person &p){ //拷贝构造里面的形参一般都是这种形式
cout<<"Person类的拷贝构造函数"<<endl;
m_age=p.m_age;
}
~Person(){
cout<<"Person类的析构函数"<<endl;
}
};
上面这段代码就包括了整个类的构造函数和析构函数的所有类型。
下面我们来看一下在上面这段代码的基础上该如何调用这些函数来完成我们想要实现的功能。
总共有三种调用方法,分别为:括号法,显示法,隐式转换法
下面我们看第一种调用方式,括号法:
void test01(){
Person p; //默认构造函数
//括号法调用
Person p1(10);
Person p2(p1);
}
int mian(){
test01();
return 0;
}
运行结果如下所示:
分析结果,也就是说test01里面的第一行代码是无参构造,第二行就是有参构造,第三行就是拷贝构造,并且由于这几种构造的调用都是在局部函数当中,运行完就会释放,所以编译器会自动调用析构函数,也就是上述运行结果中的三行析构函数。
下面我们来看下如何使用显示法来调用构造函数:
void test02(){
Person p1=Person(10);
Person p2=Person(p1);
}
int mian(){
test02();
return 0;
}
运行结果也是和我们所想的一致,如下所示:
这就是显示法调用,其中涉及到了一个比较重要的概念,那就是Person(10)这种形式的,在test02的第一行代码中,是作为右值的,我们只能通过Person(10)来使用Person(10),而没有通过一个变量来使用,就称这样的为一个匿名对象。
匿名对象的特点:当前行执行结束后,系统会立即回收掉匿名对象。因此匿名对象又叫临时对象。
值得注意的是,也不要利用拷贝构造函数初始化匿名对象。因为编译器会认为Person(p3)等价于Person p3
下面我们来看下隐式转换法来调用构造函数:
void test03(){
Person p1=10;
Person p2=p1;
}
int main(){
test03();
return 0;
}
以上便是三种调用构造函数的所有内容。
四. 构造函数的调用规则
在了解构造函数的调用规则前,我们首先知道一个概念那就是:
在默认情况下,C++编译器至少给一个类添加三个函数。
1)默认构造函数(无参,函数体为空)
2)默认析构函数(无参,函数体为空)
3)默认拷贝构造函数,对属性进行值拷贝
构造函数的调用规则如下:
1)如果用户定义了一个有参构造函数,C++就不再提供默认无参构造,但是会提供默认拷贝构造。
2)如果用户定义了一个拷贝构造函数,C++不会再提供其他构造函数。
五. 深拷贝和浅拷贝(面试常考)
在学习了拷贝构造函数之后,我们继续深入了解一下,如果我们自己并没有构造一个拷贝函数,而是利用编译器自带的拷贝函数,这个就叫做浅拷贝操作。
而深拷贝则就是在堆区重新申请空间,进行拷贝操作。
浅拷贝带来的主要问题就是堆区的内存重复释放。
如下所示:
#include<iostream>
using namespace std;
class Person {
public:
Person() {
cout << "Person类的无参构造" << endl;
}
Person(int age, int height) {
m_age = age;
m_height = new int(height);
cout << "Person类的有参构造" << endl;
}
~Person() { //析构函数的作用就体现出来了,清除这段数据
if (m_height != NULL) {
delete m_height;
m_height = NULL;
}
}
int m_age;
int * m_height;
};
void test01() {
Person p1(18, 180);
Person p2(p1);
}
int main() {
test01();
return 0;
}
上面这段代码就是使用的编译器自带的拷贝构造函数,因为我们没有自己定义一个拷贝构造函数,这也叫浅拷贝操作,但是当我们运行的时候,就会发生如下的错误提示:
原因就是因为浅拷贝操作,就是把p1的年龄和身高全部复制给了p2,而当这两个构造函数结束时,又会执行析构函数,将这个身高指针所指的内存空间清空,由于p2会清空一次,p1也会清空一次,这就导致了内存空间的重复释放,因此编译器就会给你报错。
而想要解决这个问题也很简单,只需我们自己定义一个拷贝构造函数,并使用深拷贝的操作如下所示,只需在上面原代码的Person类中加上这段代码即可 :
Person(const Person& p) {
m_age = p.m_age;
m_height = new int(*p.m_height);
cout << "Person类的拷贝构造" << endl;
}
重新给存放身高的指针开辟一个空间,这样p1和p2中的身高就指向了不同内存空间,也就不会出现内存空间重复释放的问题了。
六. 初始化列表
初始化列表:跟构造函数作用一致,也是用来初始化属性。
语法:
构造函数():属性1(值1),属性2(值2)。。。{ }