前言
在我们构建一个类之后即使我们在其中不定义任何的成员函数,编译器也会自动生成6个默认的成员函数
1、构造函数
2、析构函数
3、拷贝构造函数
4、赋值运算符重载函数
5、对普通对象取地址运算符重载
6、对常对象取地址运算符重载
这些函数完成了类的基本功能:包括对象的初始化,对象销毁后的清理工作等。
当然这些自动生成的成员函数功能有限有时候或许无法达到预期的效果,因此我们可以对其进行重载让其能够达到我们需要的功能。
一:构造函数
构造函数是在类实例化创建出对象时自动进行调用的,主要负责对象的初始化。
注意:构造函数只是完成对象的初始化工作,并不为对象开辟空间。
特性
- 构造函数名和类名相同
- 类实例化对象时自动调用
- 没有返回值
- 支持函数重载
默认构造函数
默认构造函数不仅仅是编译器默认生成的构造函数
- 默认构造函数有以下三种:
1.编译器默认生成的无参构造函数(前提:用户没有显示定义构造函数)
2.自定义的无参构造函数
3.自定义的全缺省构造函数
举个栗子帮助理解:
构造函数->完成对象的初始化工作(与类名相同、实例化自动调用、无返回值、支持重载)
class Date{
public:
带参构造函数
全缺省:将带参和无参合二为一(全缺省也是默认构造函数)
注意:全缺省可能会和无参冲突(全缺省和无参不能同时存在)调用时产生歧义
Date(int year = 0, int month = 1, int day = 1){
_year = year;
_month = month;
_day = day;
}
无参构造函数(没有显示定义构造函数,则编译器会自动生成一个默认的无参构造函数)
注意:默认生成的无参构造函数针对内置类型的成员变量没有做处理
注意:默认生成的无参构造函数针对自定义类型的成员变量,调用构造函数完成初始化
/*Date(){
_year = 0;
_month = 1;
_day = 1;
}*/
void Print(){
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main(){
Date d1(2020, 4, 13);// 调用:对象+参数
d1.Print();
Date d2;
d2.Print();
return 0;
}
注意:为了避免歧义,一个类中只能出现一个默认构造函数,另外自动生成的默认无参构造函数,不会进行内置类型成员变量的初始化,我们往往需要重载。
二:析构函数
析构函数是在对象销毁时自动调用的,主要负责对象的资源清理工作(创建对象时分配的空间等)
注意:析构函数只是完成对象资源清理的工作,并没有进行对象的销毁。
特性
- 析构函数名是在类名前加~
- 无返回值、无参数(析构函数无法重载)
- 一个类有且只有一个析构函数(若未显式定义则会生成默认析构函数)
- 对象销毁时自动调用析构函数
默认析构函数
若用户未显式定义析构函数,则编译器会自动生成一个默认析构函数。
默认析构函数:针对内置类型不做处理,针对自定义类型会调用对应的析构函数完成清理。
举个栗子帮助理解:
class Stack{
public:
构造函数
Stack(int n = 10){
cout << "Stack()" << this << endl;
_array = (int*)malloc(sizeof(int)*n);
_size = 0;
_capacity = n;
}
析构函数(后进先析构)
用户显示定义析构函数后,编译器不再生成默认析构函数
~Stack(){
cout << "~Stack()" << this << endl;
free(_array);
_array = nullptr;
_size = _capacity = 0;
}
void Push(int x);
void Pop();
size_t Size();
private:
int* _array;
int _size;
int _capacity;
};
int main(){
Stack st1;
Stack st2;
return 0;
}
三:拷贝构造
拷贝构造是用来实例化与某一对象相同的对象的函数
它会将该对象的数据完全复制一份相同的出来,拷贝构造其实是构造函数的一个重载。
特性
- 拷贝构造函数名与类名相同
- 拷贝构造是构造函数的重载形式
- 拷贝构造函数的参数只有一个且必须使用引用传参(传值会引发无穷递归)
无穷递归的原因:调用就是拷贝构造 ,调用函数时要先传参 ,传参的过程又是一个拷贝构造(拷贝构造中调用拷贝构造)
默认拷贝构造
用户未显示定义拷贝构造则编译器会自动生成拷贝构造(但是只能完成简单的浅拷贝)
浅拷贝: 将对象按照字节一个一个进行拷贝
- 浅拷贝问题:
对象销毁会调用析构函数,可能会导致同一块空间被释放两次而导致程序崩溃。
如果我们想要对复杂的如链表类进行拷贝构造,则需要自己定义
举个栗子帮助理解:
class Date{
public:
Date(int year = 0, int month = 1, int day = 1){
_year = year;
_month = month;
_day = day;
}
// Date d2(d1):对象初始化时自动掉用拷贝构造
// 拷贝构造传引用:Date& d2 = d1;
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(2020, 4, 14);
Date d2(d1);// 拷贝构造:d1是d2的拷贝
d1.Print();
d2.Print();
}
四:赋值运算符重载
赋值运算符重载是类似于拷贝构造的默认构造函数,不过是将拷贝构造通过 “=” 来使用。
赋值运算符重载是为了使自定义类型像内置类型一样去使用赋值运算符。
如果用户未显式定义赋值运算符重载,那么编译器将会生成一个默认赋值运算符重载(也只是完成浅拷贝),同时这个默认运算符重载是有返回值的,为了实现连续赋值。
推荐传参和返回值都用引用类型,这是为了防止连续赋值时调用拷贝构造出现问题,并且避免多余的拷贝构造发生可以提高效率。
举个栗子帮助理解:
//赋值运算符重载,自动生成的重载与他相同
Student& operator=(const Student& stu){
_num = stu._num;
_name = stu._name;
_classId = stu._classId;
return *this;
}
int main(){
Student student1(2, "张三", 2);//默认构造,全部用默认值
Student student2 = student1;
}
五:const成员
我们有时会定义常对象,即不可改变的对象,但是常对象如果调用普通成员函数则会出现问题(权限放大)。
常对象本身不可改,在传给隐式this的时候这里的this的类型也应该为const *,否则就会出现类型限定导致调用失败的问题。那么我们怎么样解决呢?这里就要用到常成员函数。
举个栗子帮助理解:
class Date{
public:
Date(int year = 0, int month = 1, int day = 1){
_year = year;
_month = month;
_day = day;
}
// const修饰*this 指向的对象
void Print()const{
cout << _year << "-" << _month << "-" << _day << endl;
}
// const Date *p1:修饰*p1 指针指向的内容
// Date const *p2:修饰*p2 指针指向的内容
// Date* const p3:修饰 p3 指针本身
private:
int _year;
int _month;
int _day;
};
// 对象调用成员函数
void func(const Date& d){
d.Print();
}
int main(){
Date d1 = (2020, 4, 20);
func(d1);
return 0;
}
在成员函数后加const将其变为常成员,常成员会将函数的形参this类型改为const *,这样在常成员函数中则不可再修改调用对象的数据。
六:取地址及const取地址操作符取地址
这两个默认成员函数是最后的两个会默认生成的成员函数,一般情况下不需要我们显式定义,除非我们想让别人通过取地址符获得指定的数据。
举个栗子帮助理解:
// 默认生成的取地址及const取地址操作符如下:
Student* operator&(){
return this ;
}
const Student* operator&() const{
return this ;
}