前言:
在本篇文章中,我们首先要接着前2篇中关于类和对象的内容继续往下,C++的类和对象应该是我开始学习程序以来第二个感觉到很难完全掌控和灵活使用的知识点(上一个是文件操作和预处理部分),故在这里我会花费大量的时间去理解和分析类和对象的语法特点和知识体系,有问题的地方倘若各位发现请及时指出。
首先我们依旧拿出这张大图,上面代表着类的6个默认的成员函数,他们的特点是:用户自己显式定义的时候会使用用户显式定义的,当用户没写的时候就会使用编译器自己默认生成的,这就是他们的共同特点。
1.赋值重载续operator=:
在上一篇文章中,我们介绍了运算符重载,它解决了C++对于自定义类型使用常规的内置类型的运算符的方式,而在类中默认的是赋值运算符重载。
注意细节:我们赋值重载的返回值应该仍然是类对应的类型的引用返回,这样才能满足赋值运算符连续赋值的功能,如下:
date& operator=(date& dd)//赋值重载函数 { cout << 3 << endl; _a = dd._a; _b = dd._b; _c = dd._c; return *this; }
1.问题一:赋值运算符和拷贝构造有什么区别呢?
我们不妨拿下面的例子来看:
class date { public: date(int a = 10, int b = 20, int c = 30)//构造函数(全缺省) { cout << 1 << endl; _a = a; _b = b; _c = c; } date(date& dd)//构造拷贝函数 { cout << 2 << endl; _a = dd._a; _b = dd._b; _c = dd._c; } date& operator=(date& dd)//赋值重载函数 { cout << 3 << endl; _a = dd._a; _b = dd._b; _c = dd._c; return *this; } ~date()//析构函数 { cout << 4 << endl; } private: int _a; int _b; int _c; }; int main() { date q1; date q2 = q1; cout << "----------------" << endl; date q3; cout << "----------------" << endl; q3 = q1; cout << "----------------" << endl; return 0; }
在这里,我们重点来看q2=q1以及q3=q1的区别,为了表示我们的变量赋值过程中进入了哪些函数,我们在每个函数内部都打印一个数字作为标识。打印的结果如下:
我们发现一个特别有趣的现象,即q2=q1调用的不是我们的赋值重载运算符,而q3=q1调用的是我们的赋值重载运算符,出现这样问题的原因在于我们的编译器的优化,同时它也反映了赋值重载和拷贝的区别,首先,我们的q2是不存在的,我们是创建一个q2让其等于q1,常规的过程应该是首先创建一个q2对象然后调用赋值重载函数把q1的值给q2,但在仔细一想,我们的构造和赋值完全可以用一个拷贝构造代替,故编译器就为我们将其优化为了一个拷贝构造,再看q3,q3首先是自己先创建出来的对象,然后我们让q3=q1,这里由于不涉及到先创建q3再调用赋值重载的问题,故我们直接调用赋值重载函数赋值即可。
由此,我们总结出来:
拷贝构造是一个已经存在的对象去拷贝初始化另一个对象,而赋值重载是两个已经存在的对象,一个给另一个赋值,再选择调用哪个时也是优先考虑对象是否已经被创建!!!
2.问题二:默认的operator=是如何使用的呢?
倘若我们不写赋值重载,编译器会自动默认生成一个赋值重载函数,跟拷贝重载的的行为类似,默认的operator=对内置类型会完成值拷贝,而对于自定义类型会调用它的赋值重载函数(显式或非显式),
故根据这一条,我们总结出:不需要开辟空间的,只进行浅拷贝的就不需要写赋值重载函数,而涉及到开辟空间的,由于会出现多次释放的问题,故必须自己写赋值重载函数,让其进行深拷贝而不是简单的传值的浅拷贝!!!!!这条结论很关键,要反复思考形成一种行为的反射。
故我们可以总结:构造和析构行为类似,而拷贝构造和赋值重载行为类似。
3.问题三:运算符重载的使用意义以及如何更加规范的使用运算符重载!!!
举一个简单的例子,我们判断a>b可以写一个函数,那a<=b是不是就是a>b反过来呢?这就是运算符重载的一个重要的思路:复用,由于反复进行自定义类型的各种操作符,代码的不仅多而且十分冗长,故我们完全可以利用一个完整写下来的函数来进行逻辑复用,就像我上面举得例子一样:下面让我们来看一个实例:
bool Date::operator>(const Date& d) const//>运算符重载 { if (_year > d._year) { return true; } else if (_year == d._year && _month > d._month) { return true; } else if (_year == d._year && _month == d._month && _day > d._day) { return true; } else { return false; } } bool Date::operator==(const Date& d) const//==运算符重载 { if (_year == d._year && _month == d._month && _day == d._day) { return true; } return false; } bool Date:: operator != (const Date& d) const//!=运算符重载 { return !(*this == d); } bool Date::operator >= (const Date& d) const//>=运算符重载 { return *this > d || *this == d; } bool Date::operator < (const Date& d) const//<运算符重载 { return !(*this >= d); } bool Date::operator <= (const Date& d) const//<=运算符重载 { return !(*this > d); }
在这里,我仅仅写下了> == 的逻辑,就利用逻辑复用解决了剩下的全部比较操作符,这个复用思路在书写C++类的函数的时候都适用,应当反复思考成为自己的一种代码思路去进行,减少重复代码的书写,更加提高效率。
4.问题四:前置符号和后置符号如何在自定义类型赋值重载中区分呢?!!!
根据我们的赋值重载的概念可知,operator表示++,只能是operator++,同理–也是如此,那怎样才能做到区分呢?
我们可以联想到我们学到的函数重载的知识,我们是如何做到区分同名函数的呢?没错,通过让参数不同从而进行区分,故对于后置运算我们统一在参数的括号里面加一个对应的数据类型(int,且只能是int,别的数据类型是不允许的)如下:
Date& Date::operator++()//前置++ { *this +=1; return *this; } Date Date::operator++(int)//后置++,注意,由于前置加加和后置加加的运算符重载是相同的写法,故为了区分,我们采用重载函数的方式,给后置加加补上一个int类型,这样就可以区分前置和后置了 { Date q(*this); *this +=1; return q; } Date Date::operator--(int)//后置-- { Date q(*this); *this -=1; return q; } Date& Date::operator--()//前置-- { *this -=1; return *this; }
我这里以这个例子,在这里我们发现我的后置统一在系数加了一个int,从而达到了区分前置和后置的区别,然后在这里,我直接对自定义类型加减是因为我前面实现了一个关于+ -的运算符重载,如下:
Date& Date::operator+=(int day)//日期+=天数 { if (day < 0)//为了处理我们输入一个负数,导致我们对应的天数出现负数的bug的情况 { return *this -= (-day); } _day += day; while (_day > GetMonthDay(_year, _month)) { _day -= GetMonthDay(_year, _month); _month++; if (_month == 13) { _year++; _month = 1; } } return *this; } Date& Date::operator-=(int day)// 日期-=天数 { if (day < 0)//同理,这里也是为了处理这种情况 { return *this += (-day); } _day -= day; while (_day <= 0) { _month--; if (_month == 0) { _year--; _month = 12; } _day += GetMonthDay(_year, _month); } return *this; }
故这里不要被迷惑,自定义类型是不能对其自己进行运算符重载的,还是要自己写出对对应的函数,后续再使用前置和后置自定义计算的时候,只要像内置类型那样去使用即可。
!!!!在最后,需要注意的一点:赋值重载运算符是不能作为全局函数使用的,只能在类里书写,因为在全局书写则类里面也会自动生成一个,这就导致编译器不知道应该使用哪个好了,所以赋值重载必须作为成员函数使用!!!
5.函数运算符重载的一个重要作用!!!
倘若我们想要创建一个顺序表,并且按照C语言那种常规的遍历去打印一遍顺序表,我们会面临一个问题,如下:
class List { private: int* _arr1; int _size; int _capacity; public: List(int size = 0, int capacity = 4) :_arr1(nullptr), _size(size), _capacity(capacity) { _arr1 = (int*)malloc(sizeof(int) * _size); if (_arr1 == nullptr) { perror("malloc failed"); exit(-1); } } void Backpush(int x) { _arr1[_size++] = x; } }; #include<iostream> using namespace std; int main() { List q1; q1.Backpush(1); q1.Backpush(2); q1.Backpush(3); q1.Backpush(4); for (int i = 0; i < q1._size; i++) { cout << q1._arr1[i] << endl; } return 0; }
报错信息:
在这里我们想遍历一遍顺序表,但由于我们的private的限制,我们是没法在类外部直接访问里里面的成员的,那我们要是想访问又该如何修改呢?如下,让我们添加两个函数:
int size() { return _size; } int& operator[](int i) { return _arr1[i]; }
通过第一个函数我们可以带回来我们顺序表的元素个数,通过第二个函数的赋值重载我们可以把每一个元素带回来,这是我们之前直接访问所做不到的,但有了这两个函数我们就可以这样写:
for (int i = 0; i < q1.size(); i++) { cout << q1[i] << " "; }
结果为:
2.const成员:
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
常见的用法如下:
Date Date::operator-(int day) const// 日期-天数 { Date q(*this); q -= day; return q; }
即在函数的声明的后面加上一个const
1.注意const成员函数其实就是在修饰隐藏的this指针,而不是你传入的其他参数,哪怕你再传入一个对应类的对象的引用来,你依旧可以修改它的成员的数值,但你是没法修改*this对应的成员数值的,如下:
在这里,你会发现,我们传入一个对象的引用,哪怕是加上const,依旧是可以改变它里面的成员的,但当我们想改变this对应的成员的时候,就会为我们进行错误红线的提示,且报错的内容如下:
此时我们的this是不能轻易改变的。
2.注意:权限的问题
注意,我们由const修饰的对象,它是不能进入到非const修饰的成员函数里面的,那样属于是将const对象的权限放大了,在前面的知识中我们知道,权限在计算机中只能缩小或者平替,但不能放大,但我们的非const的对象由权限规则,既可以进入const修饰的成员函数,也可以进入非const修饰的成员函数,如下:
class date { public: date(int a = 10, int b = 20, int c = 30)//构造函数(全缺省) { cout << 1 << endl; _a = a; _b = b; _c = c; } date(date& dd)//构造拷贝函数 { cout << 2 << endl; _a = dd._a; _b = dd._b; _c = dd._c; } date& operator=(date& dd)//赋值重载函数 { cout << 3 << endl; _a = dd._a; _b = dd._b; _c = dd._c; return *this; } ~date()//析构函数 { cout << 4 << endl; } void change(date& qq) const { qq._a = 12; qq._b = 13; qq._c = 20; } private: int _a; int _b; int _c; }; int main() { date q1; date q2 = q1; cout << "----------------" << endl; date q3; cout << "----------------" << endl; q3 = q1; cout << "----------------" << endl; date q4; q1.change(q4); const date q4; q4 = q2; return 0; }![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/370ab321096042e1bc6bec386cdfd1f7.png#pic_center)
我们看报错的结果如下:
在这里,q1使用由const修饰的函数change是可以的,但由const修饰的对象q4是没法使用没有由const修饰的成员函数operator=的,这便是权限的问题
**同理,我们在const修饰的成员函数内是不能调用非const的成员函数,这是由于我们的*this的对象本身倘若不是const还好,但倘若是const类型的对象,根本没法进入非const修饰的函数内部,这样就发生了权限的扩大。
但相应的,非const函数内部是可以存在由const修饰的成员函数的,因为进入非const成员函数一定是非const的成员对象,这类是可以进入const的成员函数的,属于是权限的缩小,**如下:
void Print() { cout << 1 << endl; } void mycopy(date& qq) const { Print(); }
报错信息是:
3.注意:const的位置不同,其代表的意思也是不同的
我们常见的const位置有两个地方,在一个函数中:
例如:
int func() const { /; }
或者是:
const int func() const { //; }
那么,这两个函数是相同的么?
显然,它们是不同的,其原因在于,第一个const的位置所修饰的是this指针的对象,而第二个是针对返回值进行处理的,第一个const保证了我们的this指针指向的对象是不能被修改的,而第二个指针保证了我们函数的返回值是不能被修改的,这是两种完全不同的情况,第一种暂且不说了,让我们继续分析第二种:
那么,第二种的const有必要加么?
我们之前已经学到,函数传值返回时,倘若数据出了作用域就被销毁的话,我们的返回值是会被临时拷贝一份临时的变量作为返回值返回,而由我们之前学到的,临时变量是具有常属性的,故其实本质上我们的返回值本身就是带上const的,再加上反而是多此一举,但倘若我们是引用返回,我们的返回值本身就是存在的,相应的它的自身属性也不会被改变,故这个时候我们要根据需要是否看这个引用返回的对象是否需要加上常属性const,从而让其不被修改
4.同一个函数。在后面加上const与不加的会构成重载么?
这个问题,我们首先回忆我们函数重载的知识点:在函数重载中,我们知道,编译器处理识别时是根据函数参数的不同来将其标识为不同的个符号,从而让函数构成重载,而在这里我们加上const与不加上const的这两种给了编译器识别的方式,故他们两个是可以构成重载的,而且不同的对象会根据自身的特点去选择调用哪个函数,比如const类型的对象就会调用const成员函数,而非const的对象就会优先调用非const修饰的成员函数。
那这样的函数重载有何意义呢?
大多数情况下,这样写的意义不大,但依旧拿我们的顺序表为例子:
const int& operator[](int i) const { return _arr1[i]; } int& operator[](int i) { return _arr1[i]; } for (int i = 0; i < q1.size(); i++) { q1[i]++; cout << q1[i] << " "; } for (int i = 0; i < q1.size(); i++) { q1[i]++; }
你会发现,我们的q1[i]++变的可以被修改了,按理来说,返回const类型的引用应该是不能被修改的,但由于我们重载了一个operator[]的函数,无论是能否改变的q1[i],它都会自动匹配到对应的重载函数中,故对于读和写分离的函数来说,写两个重载一个只读一个只写是最为合适的。
3.取地址&运算符重载和const修饰的&运算符重载(不是特别重要)
正如我在前面所说的,取地址运算符重载也是默认的成员函数,编译器可以默认生成,而我们只需要直接使用即可,故我们平时根本不需要显式写出取地址运算符重载函数,但倘若你不想让他人通过取地址符号找到对应的地址,我们就可以显式实现,如下:
int* operator&() { return nullptr; }
这样,一旦使用取地址符号,就会取到空指针而不是本来的自定义类型的指针了。
以上就是我们的类和对象的6种默认函数的全部,下面我们让我们讲一讲类和对象的一些其他的知识点: