目录
类的六个默认成员函数
空类
如果一个类中什么成员都没有,是会被成为空类
空类内容
空类并不是什么内容都没有,任何类在什么都不写的时候,编译器会自动生成以下6个默认成员函数
默认构造小细节
如果用户没有显式实现的话,编译器会自动生成的成员函数称为默认成员函数
无参的构造函数跟全缺省的构造函数也可以称为默认构造函数,注意,是构造!!!,并且默认构造函数只能有一个
也可以这么说,不用我们传参数的也可以默认构造函数
拷贝构造
一,为什么
先来串代码
class Date {
public:
Date(int year = 1900, 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;
};
void func(Date d) {
d.print();
}
int main() {
Date d1(2024,7,13);
func(d1);
return 0;
}
我们可以知道这个是属于结构体传参的类型,在进行普通传值传参的时候,我们都知道,我们会先拷贝这个值,在进行传值操作。
那么结构体传参的结果又是如何呢?
答案是:会把原来的d1拷贝过去
值拷贝也是浅拷贝
那么正如上述所示,是不是所有结构体传值传参都没有问题了呢
答案也肯定不是的
void func1(Date d){
d.print();
}
void func2(Stack st){
st.print();
}
int main(){
Date d1(2024,7,13);
func1(d1);
Stack st1;
func2(st1);
return 0;
}
上面给出两段代码,运行之后,能发现程序是崩溃的,我们来分析一下
这是日期类的拷贝对象,当我们拷贝过去之后,出了作用域用析构函数后删除,没有什么问题,你清空你的字节,我清空我的字节
但当轮到栈类的时候呢?
我们可以知道当st运用完出了作用域后便会调用析构函数,并将所指向的地方改为nullptr。
但当st1也出完作用域后,也会调用析构函数,但两次析构的地方是同一块地方,意思就是虽然你st的_a已经置空了,意味着我已经析构了一次这个地址了,但是我的st1所指向的地址还是那块地址呀,是,虽然你st已经不指向那块地址了,但当我的st1再次析构的时候,就会报错,原因就是那个地方被析构了两次,最后一次析构的是已经被free掉的地址。
那么我们应该如何解决这个问题呢,这就用到了我们的拷贝构造,所以我们的拷贝构造就是为此而生。
二,是什么
1,拷贝构造是构造函数的一种重载形式
2,拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
下面用Date类来举例说明
首先,dc是d1的别名,Date(Date& dc)里面的this就是指的是d
调用func函数的时候,先传参,先把参数d1传递给d,期间先进行了拷贝构造,即d1的数据通过dc,传递给了d。
以上便是拷贝构造的大致行为,这么干的原因都是因为防止有指针指向同一块地方的时候出现析构两次的现象或者其它不合法的行为
现在再回过头来看看Stack是如何的,难道也只是复制一个指针指向同一块地吗
不是的
代码实现则为
Stack(Stack& stt) {
//进行深拷贝
_a = (int*)malloc(sizeof(int) * stt._capacity);
if (_a == nullptr) {
perror("malloc fail");
exit(-1);
}
memcpy(_a, stt._a, sizeof(int) * stt._top);
_top = stt._top;
_capacity = stt._capacity;
}
拷贝构造也对内置类型完成值拷贝,对自定义类型成员完成调用自定义类型的拷贝构造
三,拷贝构造的返回
stack func(){
Stack st;
return st;
}
int main(){
func();
return 0;
}
我们可以知道,st在func里面是返回不了的,但是它又能够正常运行,原因是什么
原因就是在return st过程中完成了一次拷贝构造,将拷贝的东西送还回去了,st依旧是出了作用域就被销毁。
可将上述代码改成
Stack& func(){
static Static st;
return st;
}
int main(){
func();
return 0;
}
改变的理由是:
1,添加static,只要添加了静态成员变量,即使出了作用域,st也依旧存在
2,使用引用,使用引用后可以减少一次由于返回时所产生的拷贝构造
运算符重载
是什么
我们依旧拿时间类来做例子
我们知道内置类型对象可以直接使用各种运算符
但,自定义类型呢?答案肯定是不可以的
eg:
int main(){
Date d1;
Date d2(2024,7,13);
d1>d2; //?
d1==d2; //?
return 0;
}
编译器都不知道你要比的是什么鬼,编译器怎么可能知道它要怎么比较,混蛋
因此,我们需要重载运算符,让编译器也能看懂的我们的自定义的运算重载符。
函数名字为:关键字operator + 需要重载的运算符符号
函数原型是:返回值类型 operator操作符
原本的大于比较和重载后的大于比较
提前说明:此处是将私有成员变量给放出来了(即先屏蔽了private),否则是不能够在类外访问私有成员变量的
bool Greater(Date x, Date y) {
if (x._year > y._year)
return true;
else if (x._year == y._year && x._month > y._month)
return true;
else if (x._year == y._year && x._month == y._month && x._day > y._day)
return true;
else
return false;
}
//运用重载运算符后
bool operator>(Date x, Date y) {
if (x._year > y._year)
return true;
else if (x._year == y._year && x._month > y._month)
return true;
else if (x._year == y._year && x._month == y._month && x._day > y._day)
return true;
else
return false;
}
本质上并没有多大的区别
但是,重载运算符更加可以如此
也就是说重载后的运算符也可以像内置类型一样可以直接比较大小
运算符重载和函数重载没有什么关系的
运算符重载:自定义类型可以直接使用运算符
函数重载:可以允许参数不同的同名函数
上述的运算符重载还存在一定的问题
1,成员变量不是私有的
2,还要频繁的调用拷贝构造
第一个问题的解决方案是:
将函数放到类里面去,但这样又会产生另外一个问题,就是如果把函数放到类里面的时候,其本质上就会多一个函数
就是说运算符一次只能比较两个参数,你的参数列表里突然多了一个参数,函数就无法辨别了
所以,为了解决这个问题,我们可以将一个参数抽调出来了
此刻的d1>d2就实质上是
d1.operator>(d2);
//详细版
d1.operator>(&d1,d2);
第二个问题的解决方案是:
给参数加引用和const
bool operator>(const Date& y) {
if (_year > y._year)
return true;
else if (_year == y._year && _month > y._month)
return true;
else if (_year == y._year && _month == y._month && _day > y._day)
return true;
else
return false;
}
下面结合一个案例来更加熟悉运算符重载
几天后的日期
int getMonthday(int year,int month) {
assert(year >= 1 && month > 0 && month < 13);
int getMonth[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 &&
((year % 4 == 0 && year % 100 != 0) ||
year % 400 == 0)) {
return 29;
}
return getMonth[month];
}
Date& operator+=(int day) {
_day += day;
while (_day > getMonthday(_year, _month)) {
_day -= getMonthday(_year, _month);
_month++;
if (_month > 12) {
_year++;
_month = 1;
}
}
return *this;
}
Date operator+(int day) {
Date tmp(*this);
tmp += day;
return tmp;
}
上面的案例中,重载运算符+=是相当于日期自己会随着运算而改变,而运算符+是相当于日期自己不会改变。
赋值重载
赋值重载就是运算符重载的一种
Date& operator=(const Date& d) {
if (this != &d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
如果不写赋值重载,编译器会自动生成一个赋值重载
跟拷贝构造类似,默认的operator=内置类型进行值拷贝,自定义类型调用它的赋值
所以在日期类Date的项目中,可以不写赋值重载,因为它都是内置类型
构造函数修改优化
原本的构造函数如下:
Date(int year = 1900, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
很显然,只是简单的构造,没有什么条件限制。
那么,当我如此时,会如何?
Date d1(2024, 13, 13);
//超出月份
d1.print();
Date d2(2023, 2, 29);
//23年是平年,2月份应该只有28天
d2.print();
结果如下:
因此我们便会觉得特别荒唐,所以应该给它添上限制条件。
Date(int year = 1900, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
if (_year < 1 || _month>13 || _month<1 ||
_day>getMonthday(_year, _month)) {
assert(false);
}
}
解释assert()里为false的缘故:、
因为断言assert 的机制是当参数为假时,便触发,因为参数直接是false,所以能直接触发
在此的前提条件是 if 的判断条件为真,即日期为违规日期。
运算符重载复用
+=与+的分别复用,分析对比
+复用+=
Date& operator+=(int day) {
_day += day;
while (_day > getMonthday(_year, _month)) {
_day -= getMonthday(_year, _month);
_month++;
if (_month > 12) {
_year++;
_month = 1;
}
}
return *this;
}
Date operator+(int day) {
Date tmp(*this);
tmp += day;
return tmp;
}
+=复用+
Date operator+(int day) {
Date tmp(*this);
tmp._day = this->_day + day; //改进:tmp._day+=day;
while (tmp._day > getMonthday(tmp._year, tmp._month)) {
tmp._day -= getMonthday(tmp._year, tmp._month);
tmp._month++;
if (tmp._month > 12) {
tmp._year++;
tmp._month = 1;
}
}
return tmp;
}
Date& operator+=(int day) {
//*this 要变
*this = *this + day;
return *this;
}
可以看到,两种不同的代码,表现出来的作用却是相同的,那么我们便会想,谁的效率会更好一点
分析
两种代码没有太多的不同之处,但是我们可以很清楚的知道,它们所用的拷贝构造数目是不相同的
先来看+复用+=
再来看+=复用+
结论
从分析情况可以看出, +复用+= 比 +=复用+ 少用了一次拷贝构造,在Date这个类里面还显得有点微不足道,但是如果这个是一个链表,或者是一棵树,并且链表或者树有成百上千甚至上万个值的时候,你再来一次拷贝构造,效率就会低的不敢想象。
所以我们在实践中,采用复用的时候,尽量采用+复用+=,也就是先实现+=,再实现+
以此类推,-=和-也是一样的道理,尽量采用-复用-=。
Date& operator-=(int day) {
_day -= day;
while (_day <= 0) {
_month--;
if (_month < 1) {
_year--;
_month = 12;
}
_day += getMonthday(_year, _month);
}
return *this;
}
Date operator-(int day) {
Date tmp(*this); //还可以Date tmp = *this;
tmp -= day;
return tmp;
}
返回类型 Date与Date&
原本这个应该放在拷贝构造返回那里讲解,但是还缺少一定的案例,所以无法使我本人通透
还是可以接着用运算符重载+与+=
我们可以知道,运算符重载+的返回类型是Date,而运算符重载+=的返回类型是Date&
两者区别是&,意味着取不取别名,多一次与少一次拷贝构造的区别。
返回类型两个分别是 tmp和*this
区别就在于一个是函数内才定义的,一个是早就定义好的,并且存在于函数外的
所以给早就定义好的,并且存在于函数外的用引用返回Date& ,不但方便,还能少一次拷贝构造
而定义于函数内的,如果采用引用返回的话,这个变量出了作用域就销毁,那么你取的这个别名也就没有任何意义了,也是被销毁了,所以必须要用一次拷贝构造,让其他变量把它带出来。
日期相差天数
int operator-(const Date& d) {
int flag = 1; //符号值,用来判断大减小还是小减大
Date max = *this;
Date min = d;
if (*this < d) {
max = d;
min = *this;
flag = -1;
}
int count = 0;
while (min < max) {
++min;
count++;
}
count *= flag;
return count;
}
前置++与后置++
前置++,例如++a,意味着,先加后用
后置++,例如a++,意味着,先用后加
可能有会在运算符重载的时候,将前置++设为++operator
这个是禁止的!!!因为已经违反了复用语句的规范
所以两个++都是operator++
为了做出区别,后置++要赋予一个参数,这个参数默认是1,但是可以随便我们。
这个可以认为是特殊处理。-
Date& operator++() {
*this += 1;
return *this;
}
Date operator++(int) {
Date tmp = *this;
*this += 1;
return tmp;
}
前置--和后置--也是一样的道理,这里就不加以阐述了 。
以上便是本次博文的学习内容了,如果有不正确的地方,还望大佬指点出来,谢谢阅读