1.构造函数
构造函数是一个特殊的成员函数,函数名和类名相同,构造函数的作用是初始化,以下是构造函数的一些特点:
1. 函数名与类名相同。
2. ⽆返回值。(返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
3. 对象实例化时系统会⾃动调⽤对应的构造函数。
4. 构造函数可以重载。
5. 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤⼾显 式定义编译器将不再⽣成。
6. ⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函 数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成 函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫 默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调 ⽤的构造就叫默认构造。
7. 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始 化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤ 初始化列表才能解决,初始化列表,我们下个章节再细细讲解
说明:C++把类型分成内置类型(基本类型)和⾃定义类型。内置类型就是语⾔提供的原⽣数据类型, 如:int/char/double/指针等,⾃定义类型就是我们使⽤class/struct等关键字⾃⼰定义的类型。
好,那有没有人有疑问,什么叫做默认构造函数呢?没有参数或所有参数都有默认值(全缺省)的情况下就是默认构造函数,我们自己写的也算。
这里简单举两个例子,例1:
class A {
public:A(int a) {_a = a;}
private:int _a;
};
int main()
{A(5);return 0;
}
这里面的A(int a)对应的就是一个构造函数,但他不是默认的,接下来写一个默认的:
class A {
public:A(int a=5) {_a = a;}
private:int _a;
};
int main()
{A(5);return 0;
}
这个就是默认构造函数,因为他是全缺省的,接着:
#include<iostream>
using namespace std;
class A {
public:A(int a=5) {_a = a;}int _a;
};
class B {
public:A a;int b;
};
int main()
{A(5);B b;cout << b.a._a;return 0;
}
这个代码就可以正常运行,因为b在初始化时会默认调用A的默认构造函数,运行结果如下:
那接着我们来验证一下,如果不给A默认构造函数的话呢?
此时A没有默认构造函数,所以报错了,那我们将A的那个构造函数注释掉呢?
他又可以正确运行了,这就告诉我们,当我们显式的实现了构造函数之后,他将会删除掉之前的默认构造函数
注:这里我为了能够答应出来_a的值专门在声明的时候给了个初值,方便打印
2.析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁,⽐如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会⾃动调⽤析构函数,完成对象中资源的清理释放⼯作。
特点:
1. 析构函数名是在类名前加上字符~。
2. ⽆参数⽆返回值。(这⾥跟构造类似,也不需要加void)
3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。
4. 对象⽣命周期结束时,系统会⾃动调⽤析构函数。
5. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会 调⽤他的析构函数。
6. 还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类 型成员⽆论什么情况都会⾃动调⽤析构函数。
7. 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如Date;如 果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,如MyQueue;但是有资源申请时,⼀定要 ⾃⼰写析构,否则会造成资源泄漏,如Stack。
8. ⼀个局部域的多个对象,C++规定后定义的先析构。
举个例子:
#include<iostream>
using namespace std;
class A {
public:int* a=new int;~A(){delete a;a = nullptr;}
};int main()
{return 0;
}
一般只有动态开辟了空间,我们才需要显式的写析构函数。
3.拷贝构造函数
如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值,则此构造函数 也叫做拷⻉构造函数,也就是说拷⻉构造是⼀个特殊的构造函数。
特点:
1. 拷⻉构造函数是构造函数的⼀个重载。
2. 拷⻉构造函数的第⼀个参数必须是类类型对象的引⽤,使⽤传值⽅式编译器直接报错,因为语法逻 辑上会引发⽆穷递归调⽤。拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引 ⽤,后⾯的参数必须有缺省值。
3. C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返 回都会调⽤拷⻉构造完成。
4. 若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成 员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造。
5. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完 成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。像Stack这样的类,虽然也都是内置类型,但 是_a指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要 我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型 Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现 MyQueue的拷⻉构造。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就 需要显⽰写拷⻉构造,否则就不需要。
6. 传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤),没 有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤ 引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少 拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。
首先,什么是拷贝,上面的字太多了,肯定有人不想看,我就细细的给大家讲一下。
A(A& a) {this->_a = a._a;cout << "A的拷贝构造函数\n";}
这就是拷贝构造,注意,这里一定要传引用,不知道引用是什么的同学可以去找其他文章看看。
假如我们不传引用会发生什么呢?这里会报错
为什么呢?首先,在调用拷贝构造函数时会出现形参,形参的值要从实参那里拷贝,然后拷贝的时候就会形成新的拷贝构造函数,就会形成一个死循环。
即 每次调用拷贝构造函数之前都要先传值传参,传值传参是一种拷贝,又形成一种新的拷贝构造,就形成了无穷递归
两种拷贝构造的调用方式:
接着我们把我们写的拷贝构造函数注释掉来看一下
为什么呢?原因在于c++他自己也提供了一个默认的拷贝构造函数,他能够完成大部分的工作,就是让里面的成员变量相等(内置类型),对于自定义类型,会调用这个自定义类型自己的拷贝构造函数,这种叫浅拷贝,既然都叫浅拷贝了,她一定会有自己的缺陷,没错那就是指针,我们接着往下看一段代码:
#include<iostream>
using namespace std;
class A {
public:int*_a;A(){_a = (int*)malloc(4);}~A(){free(_a);_a = NULL;}
};int main()
{A a1;A a2 = a1;return 0;
}
运行结果,why?
这就是编译器自己提供的拷贝构造函数的缺陷,再拷贝时她会这样:
但我们实际上的情况是想把地址内的值拷贝给他,而不是地址,那么在析构时呢?没错,这部分会被析构两次,会被free两次,会产生一些不可预测的后果,所以会报错,这时,就需要我们手动的去实现叫做深拷贝的一个东西。
这里是一个示例代码,实际过程中我们可能需要加几个成员例如capacity,size等,确定所开空间大小。
#include<iostream>
using namespace std;
class A {
public:int*_a;A(){_a = (int*)malloc(4);}A(A& a){_a = (int*)malloc(sizeof(*a._a));*_a = *(a._a);}~A(){free(_a);_a = NULL;}
};int main()
{A a1;A a2 = a1;return 0;
}
什么时候需要我们实现显示的拷贝构造呢?首先就是动态内存分配过的,还有一个可以参考的地方就是,如果你显式的实现了析构函数,就肯定需要显式的实现拷贝构造函数