类和对象2(难点)
1.类的默认成员函数
默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数,对于一个类,我们不写的情况下编译器会默认生成以下6个成员函数,最重要的是前四个,C++11以后还会增加两个默认成员函数分别是移动构造和移动赋值
6个默认成员函数:
一:初始化和清理
构造函数主要完成初始化工作
析构函数主要完成清理工作
二:拷贝复制
拷贝构造是使用同类对象初始化创建对象
赋值重载主要是把一个对象赋值成另一个对象
三:取地址重载
主要是普通对象和const对象取地址,很少自己实现
学习时要思考问题
1.我们不写的时候,编译器默认生成的函数行为是什么,是否满足我们的需求
2.编译器默认生成的函数不满足我们的要求,需要自己实现,如何自己实现?
2.构造函数
构造函数是特殊的成员函数,虽然叫构造,但是构造函数的主要任务并不是开空间创造对象(我们常使用的局部对象是栈帧创建的时候空间就开好了),而是对象实例化时初始化对象,构造函数的本质是替代我们以前Date类中写的Init函数的功能,构造函数自动调用的特点就完美的替代了Init
构造函数的特点:
1.函数名和类名相同
2.无返回值(返回值啥都不需要给,也不需要写void)
3.对象实例化时系统会自动调用相应的构造函数
4.构造函数可以重载
5.如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义,编译器将不再生成
6.无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数都叫做默认构造函数,但是这三个函数有且只有一个存在,不能同时存在。无参函数和全缺省函数虽然构成函数重载,但是调用的时候会发生歧义,其实就是不传参就可以调用的函数就叫做默认构造
7.编译器默认生成的构造对内置类型成员变量的初始化没有要求,也就是是否初始化是不确定的,如果这个成员变量没有默认构造函数那么就会报错,初始化成员变量需要用初始化列表才能解决
补充:C++把类型分为内置类型(基本类型)和自定义类型,内置类型就是语言提供的原生数据类型如int/指针等,自定义类型就是使用class/struct等关键字自己定义的类型
class Date
{
public:
void Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
Date() { //赋值构造
_year = 1;
_month = 1;
_day = 1;
}
Date(int year, int month, int day) {//带参构造
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
对象实例化一定会调用构造函数确保实例化是正确的
class Date
{
public:
void Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
Date(int year, int month, int day) {//带参构造
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1;
//这里会出现错误,因为只有带参数的构造
//只有我们不进行自己构造的时候才默认构造不带参数的
}
3.析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++ 规定对象在销毁时会自动调用析构函数,完成对对象中资源的清理释放工作。析构函数的功能类比我们之前 Stack 实现的 Destroy 功能,而像 Date 没有 Destroy,其实就是没有资源需要释放,所以严格说 Date 是不需要析构函数的。
析构函数的特点:
-
析构函数名是在类名前加上字符~。
-
无参数无返回值。(这里跟构造类似,也不需要加 void)
-
一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
-
对象生命周期结束时,系统会自动调用析构函数。
-
跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用他的析构函数。
-
还需要注意的是我们显式写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
就拿手写栈来举例子,手写栈的析构函数为
~Stack() {
free(a);
_a = nullptr;
_capacity = _top = 0;
}
拷贝构造
int main() {
Date d1(2020, 2, 4);//普通构造
Date d2(d1);//拷贝构造
}
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。
拷贝构造的特点:
- 拷贝构造函数是构造函数的一个重载。
普通构造:用字面量 / 多参数创建全新空白对象;
拷贝构造:用已存在的同类对象复制生成新对象; 二者函数名都是类名,参数列表不同,构成重载,编译器自动匹配调用。
- 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。拷贝构造函数也可以多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。
//Date d1(d2)
Date(Date& d) {//这里必须用引用不能用传值
_year = d._year;
_month = d._month;
_day = d._day;
}
--------------------------
假如使用了传值
Date(Date d)
调用该函数的时候,值传递需要拷贝实参生成形参d
拷贝自定义对象必须调用拷贝构造函数(第三条)
于是再次执行Date(Date d)无限递归,导致栈溢出编译器报错
----------------------------------------
多参拷贝构造规则
允许多个形参,但是第一个必须是本类引用,剩下的参数必须给默认值
否则不被认定为是拷贝构造
// 合法拷贝构造Date(const Date& d, int addYear = 0){}
// 非法:第二个参数无默认值,不属于拷贝构造
Date(const Date& d, int addYear){}
-
C++ 规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
-
若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝 / 浅拷贝 (一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
-
显式定义析构函数并且释放资源一般是和自定义拷贝构造是绑定的
-
传值返回会产生一个临时对象调用拷贝构造,传值引用返回返回的是对象的别名,没有产生拷贝。如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个类似于野指针的野引用,传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能引用返回
补充:浅拷贝和深拷贝的区别
浅拷贝(编译器默认拷贝构造的行为)
只复制对象内存表层的值:
-
对于普通内置变量(
int/double):直接复制数值; -
对于指针成员:只复制指针本身的地址,不会重新开辟堆内存。
结果:两个对象的指针指向同一块堆资源。
深拷贝(手动实现拷贝构造)
不仅复制表层变量,当成员是堆指针时:
-
给新对象单独开辟一块全新堆内存;
-
将源对象堆里的数据完整复制到新堆空间; 结果:两个对象拥有独立的堆资源,互不干扰。
运算符重载
1.当运算符被用于类类型的对象的时候,C++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符的时候,必须转化成调用对应的运算符重载,如果没有对应的运算符重载,则会发生编译报错
2.运算符重载是具有特殊名字的函数,他的名字是operator和后面要定义的运算符共同构成,和其他函数一样,他也有返回类型和参数列表以及函数体
3.重载运算符函数的参数个数和该运算符作用的运算对象数量一样多,一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧传给第二个参数
4.如果一个重载运算符函数是成员函数,则他的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数的时候参数比运算对象少一个
5.运算符重载之后,其优先级和结合性和对应的内置类型运算符保持一致
还是以Date日期类举例子
Date d1(2020, 2, 4);//普通构造
Date d3(2020, 2, 5);
bool ret = d3 > d1;
会产生报错,错误原因
二进制“>”: 没有找到接受“Date”类型的左操作数的运算符(或没有可接受的转换)
没有与这些操作数匹配的 “>” 运算符
这就用到了我们的运算符重载,先用相等做例子
bool operator==(const Date& d1,const Date& d2){
return d1._year==d2._year
&& d1._month==d2._month
&&d1._day == d2._day;
}
但是这里会发生报错,原因是operator的参数过多,说明是几元操作就是几个参数,但是有一个隐含的this,所以要发生改变,this默认指向第一个参数
bool operator>(const Date& d2) {
if (this->_year > d2._year) {
return true;
}
else if (this->_month > d2._month) {
return true;
}
else if (this->_day > d2._day) {
return true;
}
else {
return false;
}
}
补充:不能通过链接语法中没有的符号来创建新的操作符,如operator@
.* :: sizeof ?: . 以上五个运算符不能重载
重载操作符至少要有一个类类型的参数,不能通过运算符重载改变内置类型对象的含义
一个类需要重载哪些运算符是看哪些运算符重载之后有意义,比如Date类中的operator-有意义而+没有意义
重载++运算符时,有前置++和后置++,运算符重载都是operator++,没办法很好的区分,所以C++规定,后置++重载的时候,增加一个int实参,和前置++构成函数重载,方便区分
// 1. 前置++:++d 无参数
Date& operator++(){
// 日期+1(简化逻辑,只简单日+1)
_day++;return *this; // 返回自增后的自身,引用返回支持连续++
}
// 2. 后置++:d++ 多加一个int占位参数
Date operator++(int){
Date temp = *this; // 先保存原始值
_day++; // 自身自增
return temp; // 返回修改前的旧对象,值返回不能引用
}
赋值运算符重载的特点:
1.赋值运算符重载是一个运算符重载,规定必须重载为成员函数,赋值运算符重载的参数建议写成 const 当前类类型引用,否则传值传参会有拷贝
2.有返回值,且建议写成当前类型引用,引用返回可以提高效率,有返回值的目的是支持连续赋值操作(d1 = d2 =d3)
3.默认的赋值运算符重载也是浅拷贝
//拷贝构造
Date d1(2002.2.2);
Date d2(d1);//是创造一个新的
//赋值是存在的两个对象的拷贝
Date d3(2005.2.3);
d1 = d3;
/////////////////////////////////////////////
Date& operator= (const Date& d){
_year = d._year;
_month=d._month;
_day = d._day;
return *this;
}
&spm=1001.2101.3001.5002&articleId=163002813&d=1&t=3&u=441c1538ec3446f784024e61c71e951b)
1581

被折叠的 条评论
为什么被折叠?



