1.类的默认成员函数:默认成员函数就是用户没有显示实现时编译器自动生成的,我们不写时,编译器默认生成6个函数,分别是:构造函数、析构函数、拷贝构造函数、赋值重载函数和两个取地址重载函数,其中最重要的是前四个。C++11以后还会增加两个默认成员函数,移动构造和移动赋值,得等之后才会涉及。
*构造函数:完成初始化工作。
*析构函数:主要完成清理工作。
*拷贝构造函数:使用同类对象初始化创建对象。
*赋值重载:把一个对象赋值给另一个对象。
*取地址重载函数:主要是普通对象和const对象取地址,这两个很少会自己实现。
默认成员函数很重要,也比较复杂,我们要从两个方面去学习:
第一:我们不写时,编译器默认生成的函数行为是什么?是否符合我们的需求。
第二:编译器默认生成的函数如果不满足我们的需求的话应该如何去自己实现?
2.构造函数:构造函数特殊的成员函数,需要注意的是构造函数虽然名叫做构造,但是构造函数的主要任务并不是开空间创建对象,而是实例化对象和初始化对象。构造函数本质就替代了上一期的类Stack中的Init函数,自动调用的特点就完美替代了Init。
构造函数的特点:
(1)函数名与类名相同。
(2)无返回值。
(3)对象实例化时系统会自动调用对应的构造函数。
(4)构造函数可以重载。以下附上调用方法:
#include <iostream>using namespace std;class Date
{
public://1.无参构造函数:Date()//函数名与类名相同{_year = 1;_month = 1;_day = 1;}//2.带参构造函数:Date(int year, int month, int day){_year = year;_month = month;_day = day;}//3.全缺省构造函数://Date(int year = 1, int month = 1, int day = 1)//可以代替上面两个//{// _year = year;// _month = month;// _day = day;//}void Print(){cout << _year << "," << _month << "," << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;//注意不能写成Date d1(),因为会被编译器误会成函数的声明。d1.Print();Date d2(2024, 9, 23);//有参需要传参数则实例化时直接传,构造函数会自动调用。d2.Print();/*Date d3(2024);d3.Print();*/return 0;
}
(5)如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义则编译器不生成。
(6)无参构造函数、全缺省构造函数、编译器默认生成的函数,都叫做默认构造函数,不用传实参就可以调用的构造函数就是默认构造函数。但是这三个函数有且只有一个存在,不能同时存在。无参构造函数和全缺省函数虽然构成函数重载,但是调用时会有歧义。
(7)我们不写,编译器默认生成的构造函数对内置类型成员变量的初始化没有要求,也就是说是否初始化是不确定的,得看编译器。对于自定义的成员变量,要求调用这个成员变量的默认构造函数初始化,如果这个成员变量没有默认构造函数就会报错。我们要初始化这个成员变量就得用初始化列表才能解决。初始化列表之后会说明。(内置类型就比如int、char这些是内置类型,自定义类型就比如结构体和类这些)
假如不去写默认构造函数而去使用编译器提供的:
#include <iostream>using namespace std;class Date
{
public:void Print(){cout << _year << "," << _month << "," << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;//注意不能写成Date d1(),因为会被编译器误会成函数的声明。d1.Print();return 0;
}
结果是编译器没有完成初始化。也就是说对于内置类型,使用默认生成的构造函数,编译器可能不能满足我们的需求。
//两个Stack实现队列:
#include <stdlib.h>
typedef int STDateType;
class Stack
{
public :Stack(int n = 4){_a = (STDateType*)malloc(sizeof(STDateType) * n);if (_a==nullptr){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}
private:STDateType* _a;size_t _capacity;size_t _top;
};class MyQueue
{
public://编译器默认生成MyQueue的构造函数时调用了Stack的构造,完成两个成员的初始化。
private:Stack pushst;Stack popst;
};
int main()
{MyQueue mq;return 0;
}
编译器默认生成MyQueen的构造函数时调用了Stack的构造,完成两个成员的初始化。也就是说对于自定义类型,它会调用成员的构造函数,如果成员没有的话会报错。
总结:大多数情况下,构造函数需要我们自己去实现,少数情况类似MyQueue且Stack有默认构造时,MyQueue自动生成就可以用。
3.析构函数:与构造函数的功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁它就被释放了。C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。
特点:
1.析构函数是在类名前加上字符~。
2.无参无返回值。
3.一个类只有一个析构函数,若无显式定义,系统会自动生成默认的析构函数。
4.对象生命周期结束时,系统会自动调用析构函数。
5.跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员函数不做处理,自定义类型的成员会调用它的析构函数。
6.还需要注意我们显示地写析构函数,对于自定义类型成员也会调用它的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
7.如果类中没有申请资源时,析构函数可以不写,直接使用编译器默认生成的析构函数,如果默认生成的析构可以用,也不需要显示写析构,但是有资源申请时,一定要自己写析构,否则会造成资源泄漏。就比如类Stack,有指针的创建,所以写析构函数时就应该把指针释放掉。
#include <stdlib.h>
typedef int STDateType;
class Stack
{
public :Stack(int n = 4){_a = (STDateType*)malloc(sizeof(STDateType) * n);if (_a==nullptr){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}~Stack(){free(_a);_a = nullptr;_top = 0;_capacity = 0;}
private:STDateType* _a;size_t _capacity;size_t _top;
};int main()
{Stack s1;return 0;
}
如果我在mian函数中实例化了多个类对象, 有一个规则,后定义的先析构。
#include <stdlib.h>
typedef int STDateType;
class Stack
{
public :Stack(int n = 4){_a = (STDateType*)malloc(sizeof(STDateType) * n);if (_a==nullptr){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}~Stack(){free(_a);_a = nullptr;_top = 0;_capacity = 0;}
private:STDateType* _a;size_t _capacity;size_t _top;
};class MyQueue
{
public://编译器默认生成了MyQueue的构造函数调用了Stack的构造,完成两个成员的初始化。//编译器默认生成了MyQueue的析构函数调用了Stack的析构,释放Stack内部的资源。~MyQueue(){cout << "~MyQueue()" << endl;}
private:Stack pushst;Stack popst;
};
int main()
{MyQueue mq;//不需要自己写析构,使用默认生成的。Stack s1;return 0;
}
如果我给MyQueue写了一个析构函数但是里面什么都不做就打印一句话,编译器能否完成对MyQueue的析构?
答案是可以,不仅调用了原来的默认析构函数,还调用了自己写的显示析构函数。
4. 拷贝构造函数:每次调用函数传值的话会形成拷贝构造,传引用的话就不会。
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数叫做拷贝构造函数,是一种特殊的构造函数。也就是说利用自己的这个类型去传参,然后初始化自己,初始化自己传的这个函数就叫做拷贝构造函数。
特点:
(1)拷贝构造函数是构造函数的一个重载。具体实现:
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d, int x = 0)//第一个参数必须是类类型对象的引用,如果有剩下的参数得有缺省值{_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(2024, 7, 21);d1.Print();Date d2(d1);//拷贝d1初始化d2d2.Print();return 0;
}
(2)最难理解的,拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式的话会发生无穷递归的情况,编译器会报错。因为C++规定函数的传值传参会调用拷贝构造。
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d, int x = 0)//第一个参数必须是类类型对象的引用,如果有剩下的参数得有缺省值{_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "," << _month << "," << _day << endl;}
private:int _year;int _month;int _day;
};
void Func(Date d)
{cout << "&d" << endl;d.Print();
}
int main()
{Date d1(2024, 7, 21);Func(d1);return 0;
}
F10调试之后在用F11进入Func函数可以发现它是先跳到拷贝构造函数里面去的。也就是说如果是传值调用的话会优先调用拷贝构造函数。这时候,如果把拷贝构造函数第一个参数设为传值调用的话,那么给d2构造的话,就会有Date(d1),然后就会先调用一个拷贝构造函数Date d2(d1),由于调用新的拷贝构造时是传值调用,就会再重新调用一个新的而不是先执行这个先被调用出来的Date d2(d1),用画图的方式呈现的话就会是:
始终记住C++规定函数的传值传参会调用拷贝构造,由于拷贝构造函数写的是Date(const Date d),所以每次调用了一次拷贝构造函数就会由于传的是值而再调用一次且不执行里面的语句就会出现无穷递归的情况发生。如果是引用传参传的是别名的话,就不会调用新的拷贝构造,所有得传别名。
(3)C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以自定义类型传值传参和传值返回都会调用拷贝构造函数来完成。
(4)若未显式定义拷贝构造,编译器会自动生成拷贝构造函数,自动生成的拷贝构造会对内置类型的成员变量进行值拷贝/浅拷贝(就是把相应的值进行拷贝),对自定义类型成员变量会调用它的拷贝构造。
(5)像Date这样的类成员变量全是内置类型且没有指向什么资源的,编译器自动生成的拷贝构造函数就可以满足需要的拷贝。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝并不满足我们的需求,所以需要我们自己去实现深拷贝(对指向的资源进行拷贝)。下面看下例子:
typedef int STDateType;
class Stack
{
public:Stack(int n = 4){_a = (STDateType*)malloc(sizeof(STDateType) * n);if (_a == nullptr){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}void PushStack(STDateType x){if (_top == _capacity){int newcapacity = _capacity * 2;STDateType* tmp = (STDateType*)realloc(_a, newcapacity * sizeof(STDateType));if (tmp == NULL){perror("realloc fail");return;}_a = tmp;_capacity = newcapacity;}_a[_top++] = x;}~Stack(){free(_a);_a = nullptr;_top = 0;_capacity = 0;}
private:STDateType* _a;size_t _capacity;size_t _top;
};int main()
{Stack st1;st1.PushStack(1);st1.PushStack(2);Stack st2(st1);return 0;
}
在Stack st2(st1) 处设置断点然后调试一下:
Stack里面是内置类型的成员变量,所以可以使用自动生成的拷贝构造函数进行拷贝构造,但是,
调试完成之后程序崩溃了。仔细看上图,可以发现_a的值是一样的,由于_a是一个指针变量,这就导致了实例化出来的两个栈指向同一块空间,每一个对象在程序完成之后会调用析构函数,这就导致这一块空间被析构了两次,程序崩溃。所以我们要自己写一个拷贝构造函数来满足栈Stack的拷贝构造要求。所以自己实现的拷贝构造函数如下:
Stack(const Stack& st)
{//需要对_a创建同样大的资源再拷贝值_a = (STDateType*)malloc(sizeof(STDateType*) * st._capacity);if (_a == nullptr){perror("malloc空间申请失败");return;}memcpy(_a, st._a, sizeof(STDateType) * st._top);_top = st._top;_capacity = st._capacity;
}
继续第五点,然后就是像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显示实现MyQueue的拷贝构造。这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么它就需要显示写拷贝构造,否则就不需要。
(6)传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名,没有产生拷贝。但是如果返回对象是当前函数局部域的局部对象,函数结束就销毁了,这时就会产生野引用的情况,类似野指针。传引用返回可以减少拷贝构造,但一定得确保返回对象,在当前函数结束后还在才可以用传引用返回。可以在局部变量前加上static使其成为静态的变量。
5.赋值运算符重载:
5.1 运算符重载:
*当运算符被用于类类型对象时,C++允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应运算符重载则会报错。
*运算符重载是具有特殊名字的函数,有operate加上后面要定义的运算符组成,和其他函数一样,它也具有其他返回类型和参数列表以及函数体。
*重载运算符的参数个数和该运算符的作用的运算对象一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符左侧对象传给第一个参数,右侧运算对象传给第二个参数。
*如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载函数作为成员函数时,参数比运算对象少一个。
*运算符重载之后,其优先级和结合性与对应内置类型运算符保持一致。
*不能通过连接语法中没有的符号来创建新的操作符。
*sizeof、.*、?: 、. 、::这五个运算符不能重载。
*重载运算符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如int operate+(int x,int y) 。
*一个类需要重载哪些运算符,是看哪些重载之后的运算符有意义,比如Date类重载operate-就有意义,重载operate+就没有意义。
5.2 赋值运算符重载:赋值运算符重载是一个默认成员函数,用于两个已经存在的对象直接的拷贝赋值,注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。
//赋值重载拷贝:
Date d1;
Date d2;
d1 = d2;
//拷贝构造:
Date d3(d1);
Date d4 = d2;
赋值运算符重载的特点:
1.赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算符重载的参数建议写成const类类型的引用,否则传值传参会有拷贝。
//赋值运算符传参:
void operator = (const Date & d)
{_year = d._year;_month = d._month;_day = d._day;
}
2.有返回值,且建议写成当前类类型的引用。引用返回可以提高效率,有返回值的目的是为了支持连续赋值场景。出现返回值的场景可能会是连续的赋值比如:d1=d2=d3这样的。
类比整型的赋值:i=j=k=1; 1赋值给k,k作为返回值赋值给j,j再返回赋值给i。所以在完成d2=d3拷贝构造时,最后要把d2的值返回然后赋给d1,由于d3传给了d的别名,所以d2传给了this指针,这个指针里面放的是d2的地址,所以就可以返回this的解引用。
Date operator = (const Date& d)
{_year = d._year;_month = d._month;_day = d._day;return *this;
}
但是传返回值也会调用一次拷贝构造函数,所以最好是返回引用。
Date& operator = (const Date& d)//传值返回也会有拷贝
{_year = d._year;_month = d._month;_day = d._day;return *this;
}
3.没有显示实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型的成员变量完成值拷贝,对自定义类型的成员变量会调用它的拷贝构造。
6. 取地址运算符重载:
6.1 const成员函数:
*将const修饰的成员函数称为const成员函数,const修饰成员函数放到成员函数参数列表后面。
*const修饰的实际是该成员函数隐含的this指针,表明该成员函数中不能对类的任何成员进行修改。例如用Date类实例化一个d1:const Date d1(2024, 10, 3),然后有一个成员函数Print打印日期:
void Date::Print()
{
cout << _year << "/" << month << "/" << _day << endl;
}
d1.Print(),这里取d1地址的类型传过去,d1地址类型是const Date*,指向内容不能修改,但是Print用来接收的指针是Date* const this,const修饰的是指针本身,指针本身不能修改而已不是里面的内容不能修改,所以权限放大了,就不行。所以就得把接收的指针变成:const Date* const this,但是this指针在实参和形参的位置都不能去修改,所以就把const加在函数的参数列表后面。
void Date::Print()const
{cout << _year << "" << _month << "//" << day << endl;
}
6.2 取地址运算符重载:
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成就够我们使用了,不需要去显示实现,除非一些特定情景不想让人取到地址。
//取地址运算符重载:
class Date
{
public:Date* operator& (){return this;//return nullptr;}const Date* operator&()const{return this;//return nullptr;}
};