目录
[5.2 赋值运算符重载](#5.2 赋值运算符重载)
一、了解类的默认成员函数
默认成员函数其实就是我们不写,但编译器自己会生成的成员函数就是默认成员函数。默认成员函数是特殊的成员函数
一般我们只使用四个默认成员函数。
这四个默认成员函数分别为:
1、构造函数:进行初始化工作
2、析构函数:为对象完成空间资源的释放和清理
3、拷贝构造函数:使用一个已经存在的同类对象初始化创建另一个同类对象
4、赋值运算符重载函数:两个已存在的对象,对象1赋值给对象2
二、构造函数
构造函数虽然名字里面有构造两字,但是他不会创建对象,他只会初始化对象。因为我们使用的局部对象或变量的空间,在栈帧创建时就已经创建好了的。而构造函数只是在这些自定义的对象实例化时进行初始化而已。
构造函数的特点:
1、 函数名与类名相同。
比如:
cpp
class A
{
A(....)//A是构造函数的函数名,()里面是需要的初始化成员变量的形参
{
/.../ //函数体
}
};
2、无返回值。(也不需要写void,C++的规定)
3、对象实例化时系统会⾃动调⽤对应的构造函数
4、构造函数可以重载。
可以重载三个:
1、无参构造函数
cpp
class A
{
public:
A()//无参构造函数
{
_a = 2;
}
private:
int _a;
}:
2、全缺省构造函数
cpp
class A
{
public:
A(int a = 1,int b = 1)//全缺省构造函数
{
_a = a;
_b = b;
}
private:
int _a;
int _b
}:
3、带参构造函数
cpp
class A
{
public:
A(int a,int b)//带参构造函数
{
_a = a;
_b = b;
}
private:
int _a;
int _b
}:
5、 如果类中没有显式定义构造函数,则C++编译器会自动生成⼀个无参的默认构造函数,⼀旦用户显式定义编译器将不再生成。
也就是如果用户自己显示定义了上面的三个构造函数中一个的话,那编译器就不会自动生成默认构造函数,如果用户没有显示定义以上三个构造函数,那编译器就会自动生成默认构造函数。
6、默认构造函数有三个,无参构造函数、全缺省构造函数和用户不写时编译器自动生成的构造函数。这三个默认构造函数只能存在一个,否者在调用时会产生歧义也就是编译器不知道调用哪个。也可以这样理解:不需要实参就能调用的构造函数是默认构造函数。
7、编译器自己生成的默认构造函数,对内置类型初始化是不确定的。对自定义类型的成员会调用他自己的构造函数。如果自定义类型的成员没有默认构造函数,那么就会报错。
三、析构函数
析构函数和构造函数两个功能是相反的,构造函数是在自定义类型实例化时进行初始化,而析构函数是在栈帧销毁之前就会自动调用析构函数,完成对对象的资源释放。这里资源的释放是指需要额外开空间的对象,比如malloc这类。实际上没有空间资源释放的类,我们可以不用写析构函数,编译器自动生成的析构函数就够用了。
析构函数的特点:
1、 析构函数名是在类名前加上字符~。
假设类名是A
~A()//在类名前面加~
{
//...//函数体
}
2、无参数无返回值。(这里跟构造一样,也不需要加void)
3、⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数
4、对象⽣命周期结束时,系统会⾃动调⽤析构函数。
5、跟构造函数类似,我们不写析构函数时编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员变量会调⽤他的析构函数。
这个意思就是说编译器自动生成的默认析构函数,不会主动释放内置类型成员指向的动态资源(如堆内存),但会对内置类型成员执行 "销毁"(即释放其本身占用的栈空间);对于自定义类型成员,默认析构函数会自动调用该成员的析构函数。
6、还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数。
比如:当一个类A的成员变量是是另一个类B的对象,并且在调用析构函数时,自定义成员不会调用当前类的析构函数,而是调用他自己的析构函数。简洁来说就是,各个类只会调用自己的析构函数。
cpp
class B
{
public:
B(int n = 2)
{
_ptr = (int*)malloc(sizeof(int) * n);
}
~B()
{
free(_ptr);
_ptr = nullptr;
}
private:
int* _ptr;
};
class A
{
public:
A(int a =1 )
{
_a = a;
}
private:
int _a;
B _b;
};
int main()
{
A a(2);
return 0;
}
在析构时,成员变量_b会自动调用B的析构函数
7、如果没有需要资源释放的类,我们可以不用写析构函数,编译器自动生成的析构函数就够用了
8、如果一个局部域里有多个对象需要析构,C++规定后定义的先析构
比如:int main() {
A d1(...);
A d2(...);
return 0;
}
编译器会先析构d2,后析构d1。
四、拷贝构造函数
拷贝构造函数其实就是特殊的构造函数,他只是构造函数的重载。它的作用是:用一个已存在的对象,初始化新对象
拷贝构造函数的特点:
1、 拷⻉构造函数是构造函数的⼀个重载。
2、 拷⻉构造函数的第⼀个参数必须是类类型对象的引⽤,一定不能使用传值返回,编译器会报错,因为会造成无限递归。拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值。
比如:
cpp
A(int a =1)
{
_a = a;
}
A(const A& d,int b =10)//多参数拷贝
//其实const可以写不用写,但是为了保险起见,我们不改变对象的数据的话,最好还是加上
{
_a = d._a + 10;
}
3、C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以自定义类型传值传参和传值返回都会调用拷贝构造完成。
可以自行调试来求证
4、若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
5、当我们不显示实现拷贝构造函数时,编译器就会自动生成默认的拷贝构造函数。如果有资源释放的类就需要拷贝构造函数,反之就可以不用先显示实现,因为编译器自动生成的拷贝构造函数能完成值拷贝/浅拷贝。
6、 传值返回会产生⼀个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于⼀个野引用,类似⼀个野指针⼀样。传引用返回可以减少拷贝,但是⼀定要确保返回对象,在当前函数结束后还在,才能用引用返回。
传值返回:
cpp
A Func()
{
A a(10);
return a;
//传值返回:拷贝a生成临时对象,调用拷贝构造
}
传引用返回错误示范:
cpp
A& Func()
{
A a(10);// 局部对象a(Func栈帧内)
return a;// 返回a的引用(别名)
}
因为局部对象a是Func函数里的,当出了Func函数的局部域就会销毁,就没有a这个对象了,就会造成野引用
传引用返回正确示范:
cpp
A& Func()
{
_a = _a + 1;
return *tihs;
}
this指针不是Func函数的局部变量,所以出了作用域不会销毁,传引用返回就减少了拷贝
五、赋值运算符重载
5.1运算符重载
运算符重载的意义:
因为我们无法使用自定义类型来直接使用运算符,编译器会报错。所以C++支持运算符重载来用于自定义类型的运算。
运算符重载的使用:
运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。
比如:
cpp
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 1)
{
_a = a;
}
A(const A &d)
{
_a = d._a;
}
int operator-(const A &d)//运算符重载函数
{
int a = _a - d._a;
return a;
}
private:
int _a;
};
int main()
{
A d1;
A d2(3);
int ret = d1 - d2;
cout << ret << endl;
return 0;
}
int operator-(const A &d)这就是一个运算符重载函数
运算符重载的特点:
1、重载运算符函数的参数个数和该运算符作用的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。
2、如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。如上代码所示
3、运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。
4、不能通过重载语法中没有的运算符符号来创建新的操作符:⽐如operator¥
5、.* :: sizeof ?: . 这五个运算符不能重载,这五个运算符依次是:成员指针运算符、域作用解析符、长度运算符、三目运算符(条件运算符)和成员访问运算符
6、重载操作符⾄少有⼀个类类型参数(至少有this指针,也就是如果没有自定义类型的参数的话,那么就必须是成员函数),不能通过运算符重载改变内置类型对象的含义,如:operator+(int x, int y),也就是不能改变运算符他自身的定义
7、是否要重载取决于自定义类是否需要,如果不需要就不用重载,我们只重载有意义的运算符
8、当我们重载相同的运算符可以使用函数重载的方式来定义不同的运算方式(如增加形参等)
9、重载<<(流插入运算符)和>>(流提取运算符)时,需要重载为全局函数,因为重载为成员函数它的第一个形参是this,调用时就会变为
对象<<cout
对象>>cin
看起来就很别扭,所以一般我们把>重载为全局函数,把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。就和内置类型用法类似了。
5.2 赋值运算符重载
用于完成两个已经存在的对象直接的拷贝赋值,与拷贝构造函数的区别是,拷贝构造是用一个已经存在的同类型对象来初始化一个新创建的对象。而赋值运算符重载是对两个及以上已经存在的对象进行拷贝赋值。
赋值运算符重载函数的特点:
1、赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const 当前类类型引用,因为传值传参会有拷贝。
2、 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值的目的是为了支持连续赋值场景。
比如:
cpp
class A
{
public:
A(int a = 1)
{
_a = a;
}
A(const A& d)
{
_a = d._a;
}
A& operator=(const A& d)
{
_a = d._a;
return *this;
}
private:
int _a;
};
int main()
{
A d1(5);
A d2;
A d3;
d3 = d2 = d1;
return 0;
}
3、 没有显式实现时,编译器会自动生成⼀个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。
4、如果一个类有资源的释放就需要用户显示写赋值运算符重载,如果没有就可以使用编译器自动生成的默认赋值运算符重载函数。
六、取地址运算符重载
6.1、const成员函数
用const修饰的函数叫const成员函数,const修饰函数放在函数参数列表的后面。
const实际上是修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。
比如:
cpp
class A
{
public:
A(int a = 1)
{
_a = a;
}
A(const A& d)
{
_a = d._a;
}
A& operator=(const A& d)const
{
_a = d._a;
return *this;
}
private:
int _a;
};
int main()
{
A d1(5);
A d2;
A d3;
d3 = d2 = d1;
return 0;
}
比如const 修饰A类的赋值运算符成员函数,operator-隐含的this指针是A* const this,我们用const修饰,本质上是const A* const this,但是this指针是隐藏的,所以c++规定把const修饰放在函数参数列表的后面
当我们没有使用const修饰成员函数时,我们可以通过该成员函数修改该对象的值。所以在我们不需要修改对象数据的成员函数参数后我们可以用const修饰。
6.2、取地址符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。除非一些很特殊的场景,比如我们不想让别人取到当前类对象的地址,就可以自己实现⼀份,胡乱返回⼀个地址。
取地址符重载分为普通对象取地址重载和const取地址符重载
比如:
cpp
class Date
{
public :
Date* operator&()//普通取地址运算符重载
{
return this;
// return nullptr;
}
const Date* operator&()const//const取地址符重载
{
return this;
// return nullptr;//当我们不想让别人取到当前对象地址,那我们就可以随意给返回值
}
private :
int _year ; // 年
int _month ; // ⽉
int _day ; // ⽇
};
针对普通对象取地址符重载我们虽然说不想修改对象的内容,但是如果我们使用const修饰成员函数的话,那么this就是只读指针了,返回的是普通对象,就会进行权限放大,就会编译报错。