目录
一、类的6个默认成员函数
二、构造函数
2.1概念
2.2七大特性
三、析构函数
3.1概念
3.2特性
四、拷贝构造函数
4.1概念
4.2特性
五、赋值运算符重载
5.1运算符重载
5.2赋值运算符重载
5.3前置++和后置++重载
六、const成员函数
七、取地址及const取地址操作符重载
一、类的6个默认成员函数
对于日常认知,空类好像没有作用,但其实他也有实际的意义,任何类在什么都不写时,编译器会自动生成6个默认成员函数
默认成员函数:用户没有显示的去实现该函数,而是编译器会生成的成员函数称为默认成员函数。其实就是祖师爷给编译器设定好的默认成员函数,我们自己不去定义这些函数时,编译器会自动帮我们实现。
接下来详细介绍他们~
二、构造函数
2.1概念
先来回顾一下日期类的代码:
#include <iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2022, 1, 12);
d1.Print();
Date d2;
d2.Init(2023, 11, 24);
d2.Print();
return 0;
}
在上述Date类中,需要通过Init方法给对象设置日期,但是每次创建对象时都需要调用该方法才能设置好日期,有点烦。那么能不能在创建对象时直接就设置好日期呢?
而构造函数就解决了这一问题。构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次
那么接下来讲讲它的特点,以及如何使用的。
2.2七大特性
构造函数虽然名称叫做构造,但其主要任务并不是开空间创建对象,而是初始化对象,即你在创建构造函数时可以为对象初始化。
其特征如下:
1.函数名与类名相同
2.无返回值
3.对象实例化时编译器自动调用对应的构造函数
4.构造函数可以重载
通过代码来演示:
#include <iostream>
using namespace std;
class Date
{
public:
//1.无参构造函数
Date()//函数名与类名相同,无返回值
{
cout << "2023-11-24" << endl;
}
//1.带参构造函数
Date(int year, int month, int day)//函数名与类名相同,无返回值
{
_year = year;
_month = month;
_day = day;
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用无参构造函数,没有任何参数,但是编译器依然会自动调用构造函数
//只需要在构造函数中设置输出,不需要额外写一个打印函数,然后还要调用打印函数。在运行时依然会有打印结果
//就可验证编译器会自动调用构造函数
Date d2(2023, 11, 24);//调用带参的构造函数,创建对象时直接设置日期
//且编译器会自动调用构造函数
return 0;
}
没有设置打印的方法,但是在构造函数中设置了输出,创建对象后,也不需要去调用该构造函数,然而在运行时,依然会有结果,说明编译器自动调用了该构造函数 ,且该无参构造与有参构造也构成了重载函数。
运行结果:
注意:通过无参构造函数创建对象后,对象后面不用跟括号,否则就成了函数声明。 !!!
5.如果类中没有显示定义构造函数,则c++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义构造函数,编译器则不会自动生成
举例:
#include <iostream>
using namespace std;
class Date//类中没有构造函数
{};
int main()
{
Date d1;//创建对象时,编译器会自动生成一个无参的默认构造函数
return 0;
}
此运行没有任何问题。
#include <iostream>
using namespace std;
class Date
{
public:
//1.用户显示定义了构造函数,编译器则不会生成默认构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//d1对象只能调用无参构造函数,而却又显示定义了构造函数,运行时则会报错
return 0;
}
运行结果:
总结一下,无参构造函数可以自己构造,也可由编译器自动生成,但是在创建无参对象时,需要调用无参构造函数,可以调用自己构建的无参构造函数,且当自己构造了无参构造函数,也可以构造有参构造函数,因为构造函数可以重载,若自己没有构造无参构造函数,那么就不能显示构造构造函数,即不能构造有参构造函数,而是由编译器默认构造。
想必有疑问的是,对于编译器生成的默认构造参数它到底有啥作用呢? 见下
6.c++把类型分为内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char/double...,自定义类型就是自己定义的类型,如:class/struct/union等。在这里,编译器生成的默认构造函数会对自定义类型成员调用它的默认成员函数。
那我们用显示定义的构造函数来演示其对自定义类型成员的默认成员函数的调用更加的形象
#include <iostream>
using namespace std;
class Time
{
public:
Time()//构造函数
{
cout << "Time()" << endl;
}
};
class Date
{
public:
Date()//显示定义构造函数
{
cout << "Date()" << endl;
}
private:
//自定义类型
Time _t;
//内置类型
int _year = 1970;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1;//会调用自定义类型成员的默认成员函数
return 0;
}
运行结果:
当然,在该自定义类型中依然不能显示定义有参构造函数 。通过结果发现先调用了_t对象的构造函数,再调用d1对象的构造函数,符合会调用自定义类型成员的默认成员函数。若没有显示定义d1对象的构造函数,则编译器会生成默认构造函数,该默认构造函数同样也会调用其自定义类型成员的默认成员函数。
注意:在VS2013下,默认构造函数不会对内置类型成员做处理,只对自定义类型处理,而在一些高版本下,当只有内置类型成员时,默认构造函数不会对其做处理,当既存在内置类型成员,又存在自定义类型成员时,默认构造函数就会对他们都做处理。
VS2022下,对上述代码进行调试:
发现内置类型都被初始化了0。一方面本来设定内置类型不用初始化,另一方面默认构造函数又给内置类型初始化了,那么这样默认构造函数就能够随意去改变内置类型的值吗,早在2011年,为了解决该问题,c++11中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。 对于缺省函数,如果给了默认值优先使用,否则使用缺省值。
#include <iostream>
using namespace std;
class Date
{
public:
Date()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
//内置类型
int _year = 1970;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1;
return 0;
}
运行结果:
7. 无参的构造函数和全缺省的构造函数、我们没写编译器默认生成的构造函数,都可以称为默认构造函数,并且默认构造函数只能有一个。
#include <iostream>
using namespace std;
class Date
{
public:
Date()//无参构造函数
{
cout << _year << "-" << _month << "-" << _day << endl;
}
Date(int year = 1900, int month = 1, int day = 1)//全缺省的构造函数
{
_year = year;
_month = month;
_day = day;
}
private:
//内置类型
int _year = 1970;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1;
return 0;
}
运行结果:
三、析构函数
3.1概念
前面已经讲解了如何实例化对象,那么对象是如何销毁的呢,其成员是如何做处理的,对于其被初始化的成员以及所开的空间又是如何做处理的。
析构函数正是对对象销毁前做处理工作,其功能与构造函数相反,完成的是对对象中资源的清理工作,对象在销毁时会自动调用析构函数。即当对象的生命周期要结束时,会调用析构函数对对象中的成员做处理,清理成员的值,清理所开的空间。
3.2特性
析构函数同样也是特殊的成员函数,其特性如下:
1.构造函数名与类名相同,而析构函数名与构造函数名相比在构造函数名前多了一个字符~。
2.无参数无返回值类型
3.一个类只能有一个析构函数。若未显示定义,系统会自动生成默认的析构函数。由此可知析构函数不能重载。
4.对象生命周期结束时,c++编译系统会自动调用析构函数。
且看例子:
#include <iostream>
using namespace std;
typedef int DataType;
class stack
{
public:
stack(DataType capacity = 3)//缺省构造函数
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (nullptr == _array)
{
perror("malloc fail");
exit(-1);
}
}
//压栈
void push(DataType data)
{
_array[_size] = data;
_size++;
}
//其他方法...
~stack()//显示定义析构函数
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
//内置类型
DataType* _array;
int _capacity = 0;
int _size = 0;
};
int main()
{
stack s;
s.push(1);
s.push(2);
return 0;
}
调试结果:
进行调试阶段,当压了两次栈后,_size 的值为2。
当执行完析构函数后,_size的值为0.
至于_capacity 的值为什么是0,因为最开始给了默认值为0,然后没有写扩容的检查方法,但也不影响演示。 如果将显示定义的析构函数去掉,编译器也会自动生成默认的析构函数,只不过这个过程演示不出来。
5.与构造函数一样,编译器生成的默认析构函数,对自定义类型成员调用它的析构函数
这里同样用显示定义的析构函数来演示调用其自定义类型成员的析构函数更形象:
class Time
{
public:
~Time()//显示定义析构函数
{
cout << "~Time()" << endl;
}
};
class Date
{
public:
~Date()//显示定义析构函数
{
cout << "~Date()" << endl;
}
private:
//内置类型
int _year = 1970;
int _month = 1;
int _day = 1;
//自定义类型
Time _t;
};
int main()
{
Date d1;//会调用自定义类型成员的默认成员函数
return 0;
}
运行结果:
通过运行结果,与构造函数的调用顺序不一样,先调用了d1对象的析构函数,再调用了d1对象中_t对象中的析构函数 ,那是因为main函数会先识别在它的作用域中的对象,在main函数中,只有d1对象,所以在d1对象销毁时会先调用d1对象的析构函数,但是发现在调用析构函数时,d1对象中还有一个Time类的_t对象,然后才会调用_t对象的析构函数。
6.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如上面,Date类的析构函数可以不写;但是有资源申请时,一定要显示定义,否则会造成资源泄露。因为默认析构函数只对普通成员做清理工作,而对于有成员以realloc、malloc形式自己申请的资源,默认析构函数不会做处理,像stack类就需要自己定义析构函数去做清理工作。
四、拷贝构造函数
4.1概念
只有单个形参,该形参是对类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。即已经创建了一个类类型的对象d1,再用d1对象拷贝给同类类型的对象从而出创建出新对象d2,这时会调用一个拷贝构造函数,该拷贝构造函数形参是对同类类型的引用
4.2特性
拷贝构造函数也是特殊的成员函数,其特征如下:
1.拷贝构造函数是构造函数的一个重载形式。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷的递归调用。
3.若未显示定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
4.跟构造、析构一样,默认拷贝构造会对其自定义类型成员调用它的默认成员函数。
先看正确的代码:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 2023, int month = 11, int day = 26)//全缺省构造函数
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(const Date& d)//传引用,采用传值会报错
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
//内置类型
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用构造函数
Date d2(d1);//将d1对象拷贝给d2对象,会调用拷贝构造函数
return 0;
}
若为传值方式,则会发生以下递归:
当自定义类型进行传值拷贝时,则会引发对象的拷贝,上图,d1对象拷贝给d2对象时,会调用拷贝构造函数,接着d1要传值拷贝给dd,此时则会引发对象的拷贝,d1对象拷贝给d对象(名字随便取),则又会调用拷贝构造函数,接着d1又要传值给dd对象,此时又会引发对象的拷贝,d1对象又拷贝给d对象,以此无穷递归。而对于传引用,只是d1对象的别名,不会引发对象的拷贝。
总而言之,通过拷贝对象来创建新对象会调用拷贝构造函数,而传值时拷贝,会创建新对象。
为什么要存在拷贝构造函数?
先给代码,再看图:
#include <iostream>
using namespace std;
typedef int DataType;
class stack
{
public:
stack(DataType capacity = 3)//缺省构造函数
{
cout << "stack(DataType capacity = 3)" << endl;
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (nullptr == _array)
{
perror("malloc fail");
exit(-1);
}
}
stack(const stack& d)//拷贝构造函数
{
cout << "stack(const stack& d)" << endl;
_array = (DataType*)malloc(sizeof(DataType) * d._capacity);
if (nullptr == _array)
{
perror("malloc fail");
exit(-1);
}
memcpy(_array, d._array, sizeof(DataType) * d._size);
_capacity = d._capacity;
_size = d._size;
}
//压栈
void push(DataType data)
{
_array[_size] = data;
_size++;
}
//其他方法...
~stack()//显示定义析构函数
{
//cout << "~stack()" << endl;
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
//内置类型
DataType* _array;
int _capacity = 0;
int _size = 0;
};
class MyQueue
{
public:
MyQueue()
{
cout << "MyQueue()" << endl;
}
MyQueue(const MyQueue& dd) : _pushst(dd._pushst)//显示定义拷贝构造函数,将传入的对象dd的成员变量_pushst拷贝到当前对象的成员变量_pushst中。
//意思先大概了解,用法后面章节会讲解
{
cout << "MyQueue(const stack& dd)" << endl;
}
//自定义类型成员
stack _pushst;
int _size = 0;
};
int main()
{
stack s1;
stack s2(s1);
MyQueue q1;
MyQueue q2(q1);
return 0;
}
无拷贝构造函数的示意图:
对于上述图,若没有拷贝构造函数,将s1对象拷贝给s2对象,s1对象中的_array指向malloc的空间,那么s2对象中的_array也会指向malloc的空间,那么在对象s1销毁时,会调用析构函数,那么_array指向的空间就会释放,然后s2对象销毁时,同样会调用析构函数,也会对_array进行free,这就导致了对已释放的空间再释放,就会报错。其实拷贝构造函数主要就是解决这一问题而存在
有拷贝构造函数的示意图:
对于有拷贝构造函数而言,则就不会存在重复空间的释放,由代码也可以看出,d1对象拷贝给d2对象,d2对象会重新malloc一个空间,但里面的内容和d1对象的一样,当他们销毁时,调用各自的析构函数,不会影响彼此。
跟构造、析构一样,默认拷贝构造会对其自定义类型成员调用它的默认成员函数:
上述代码中,依然显示的定义了拷贝构造函数来进行演示会更加形象,MyQueue q1;会调用MyQueue构造函数,又因MyQueue类中含有自定义类型成员,所以在调用MyQueue构造函数的时候,先去调用自定义类型成员的构造函数了。MyQueue q2(q1);q1对象拷贝给q2对象会调用MyQueue拷贝构造函数,又因MyQueue类中含有自定义类型成员,所以在调用MyQueue拷贝构造函数的时候,先去调用自定义类型成员的拷贝构造函数了。
验证输出结果:
总结一下:
1. 像Date类我们可以称之为浅拷贝(可写拷贝构造函数,也可不写,由编译器默认生成,只适用于内置类型成员),像stack类我们称之为深拷贝(就是自己用realloc、malloc申请的资源,需要自己写拷贝构造函数),2.拷贝时不能传值,可以传指针或者引用,但对于拷贝构造函数的定义而言,必须传引用,3.默认拷贝构造会对其自定义类型成员调用它的默认成员函数。
5.拷贝构造函数典型调用场景
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)//全缺省构造函数
{
cout << "Date(int,int,int)" << endl;
}
//拷贝构造函数
Date(const Date& dd) //传引用,采用传值会报错
{
cout << "Date(const Date& dd)" << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
//内置类型
int _year;
int _month;
int _day;
};
Date Test(Date d)//函数参数类型为类类型对象
{
Date temp(d);//使用已存在对象创建新对象
return temp;//函数返回值类型为类类型对象
}
int main()
{
Date d1(2023, 11, 26);
Test(d1);//函数参数类型为类类型对象
return 0;
}
运行结果及其如何调用:
创建对象d1调用构造函数。
将对象d1传值拷贝给d对象,引发新对象创建,会调用拷贝构造函数。
再将对象拷贝给temp对象,又引发新对象创建,会调用拷贝构造函数。
其实在传值返回temp对象时会创建临时对象,temp对象会传值拷贝给临时对象,会引发拷贝构造函数调用,但是却没有该输出结果,原因是编译器在这里做了返回值优化,给干掉了。
接着temp对象销毁调用析构函数。
d对象销毁调用析构函数。
最后d1对象销毁调用析构函数
由于以上代码并没有传引用,导致不断的调用成员函数,所以为了提高效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
五、赋值运算符重载
5.1运算符重载
对于内置类型(int、double等)的成员可以使用运算符进行彼此间的操作,这是语言本身支持的,他们的行为很简单,编译器会自动识别,所以在编译时也会转换成汇编指令,而对于自定义类型的成员,语言本身不支持他们进行运算符的运算,因为编译器不能够识别他们的行为要表达什么,对象中的行为是如何定义的,编译器不能够识别。祖师爷早就意识到这一方面的问题,所以C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有返回值类型,函数名字以及参数列表。
运算符重载的原型使用:
函数原型:返回值类型 operator操作符(参数列表),例如:Date operator>(Date& x, Date& y);
函数名:operator操作符
运算符重载表达的意思:
先拿简单的运算符代码分析:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)//全缺省构造函数
{
_year = year;
_month = month;
_day = day;
}
//private:
//内置类型
int _year;
int _month;
int _day;
};
bool operator==(const Date& x, const Date& y)
{
return x._year == y._year
&& x._month == y._month
&& x._day == y._day;
}
int main()
{
Date d1(2023, 11, 26);
Date d2(2023, 11, 26);
cout << operator==(d1, d2) << endl;
cout << (d1 == d2) << endl;
return 0;
}
运行结果:
1.我们要表达两个对象是否相等,一方面,operator加运算符是不是解决了编译器本身不能识别自定义类型运算符的操作。另一方面,operator加运算符是不是很明显要表达什么意思,在这里表达是否相等的意思;
2.结果用operator==(d1,d2)的返回值来判断d1,d2是否相等,然而祖师爷觉得这样还是欠缺点可读性,于是便直接使用d1==d2来判断d1,d2是否相等更加直观--符合我们的直观感受,但两者都可以使用,其次,d1 == d2需要其中需要加括号,因为<<流插入的运算符优先级比==的高,不然就会先把d1对象流入到控制台中,留下一对孤魂野鬼在外面,而不是将d1==d2的返回值流入到控制台;
3.operator在这里定义的是一个全局函数,当通过外部对象去访问class类的私有成员时,是无法访问的,所以在这里,就将其成员改为了公有的,但是,这一问题导致了class类的封装性下降了。
那么为了针对这一封装性问题,operator可以写到类中去,这样就可以访问类中私有成员,还有几点运算符重载的注意事项:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数,就是传参时必须有一个类类型的对象或者隐藏的对象传过去,由this指针接收,否则会报错。
- 用于内置类型的运算符,其含义不能改变,例如:内置的整形+,就是相加的意思,不能改变其含义,不要与运算符重载混淆
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . 注意以上5个运算符不能重载,在选择题中也经常出现。
拿代码分析运算符重载作为类成员函数:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)//全缺省构造函数
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& y)//隐藏Date* const this指针
{
return _year == y._year
&& _month == y._month
&& _day == y._day;
}
private:
//内置类型
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 11, 26);
Date d2(2023, 11, 26);
cout << d1.operator==(d2) << endl;
cout << (d1 == d2) << endl;
return 0;
}
运行结果:
也没多大变化,形参少了一个,隐藏了一个指向d1的this指针,有一个点注意的是此时不能直接使用operator==(d1, d2)作为返回值判断是否相等了,正因为operator函数中隐藏了一个this指针,d1显示传过去并不会传给隐藏的this指针,而operator中没有多余的参数来接受d1,就会引发参数不匹配,所以必须用实例化的对象来调用类中的成员函数,即d1.operator(d2),这样d1就会隐藏的传给this指针。
上面举了简单的运算符==来简介了一下运算符重载存在的理由及基本事项,接下来就是关于赋值运算符的分析
5.2赋值运算符重载
1.赋值运算符重载格式
- 参数类型:const 类类型&,传引用防止调用拷贝构造
- 返回值类型:类类型&,若返回对象所在的作用域销毁时,对象还存在,为了防止调用拷贝构造,就用传引用返回。
- 检测是否自己给自己赋值
- 返回*this:要复合连续赋值的含义
ok,直接代码分析:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)//全缺省构造函数
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& dd)
{
_year = dd._year;
_month = dd._month;
_day = dd._day;
}
Date& operator=(const Date& dd)//返回值传引用
{
if (&dd != this)//防止给自己赋值
{
_year = dd._year;
_month = dd._month;
_day = dd._day;
}
return *this;
}
private:
//内置类型
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2023, 11, 26);
d1.operator=(d2);
d1 = d2;
return 0;
}
调试结果:
赋值前:
赋值后:
2.在进行自定类型赋值时,用户没有显示实现重载时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝,且默认规定赋值运算符重载是成员函数,只能重载成类的成员函数不能重载成全局函数 ,若在全局区定义就会与默认生成的赋值运算符重载冲突,从而报错。
在Date类中,这里依然显示定义了赋值运算符重载来演示更加形象 :
#include <iostream>
using namespace std;
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time& operator=(const Time& dd)//返回值传引用
{
if (&dd != this)//防止给自己赋值
{
_hour = dd._hour;
_minute = dd._minute;
_second = dd._second;
}
return *this;
}
private:
//内置类型
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date& operator=(const Date& t)//显示定义赋值运算符重载,否则默认生成,会调用自定义类型的成员函数
{
cout << "Date& operator&(const Date& t)" << endl;
_t = t._t; // 调用Time类的赋值运算符重载
return *this;
}
private:
int _year = 2023;
int _month = 11;
int _day = 27;
//自定义类型
Time _t;
};
int main()
{
Date d1;
Date d2;
//d1.operator=(d2);
d1 = d2;
return 0;
}
运行及调试结果:
这里由于_t = t._t;这条语句在显示定义的赋值运算符重载内部,所以会先调用该赋值运算符重载,然后再去调用自定义类型的成员函数。如果没有显示写该赋值运算符重载,编译器会在Date类中默认生成一个赋值运算符重载然后去调用自定义类型的成员函数。
3.跟拷贝构造,析构一样,如果类中有涉及到深拷贝的资源管理,就必须要自己去写赋值运算符重载,这里就不在过多演示了。
5.3前置++和后置++重载
前置++原型: 返回值类型 operator++();
#include <iostream>
using namespace std;
class Date
{
public:
Date& operator++()
{
_day += 1;
return *this;//this指针指向的是d1对象,该函数结束时,该对象不会销毁,因为该对象在main函数中
//为了防止调用拷贝构造,提高效率,就传引用返回
}
private:
int _year = 2023;
int _month = 11;
int _day = 27;
};
int main()
{
Date d1;
Date d2;
d1 = ++d1;
return 0;
}
调试结果:
前置++实现了对d1对象中的天数进行++,前置++重载只有一个隐藏的this指针参数,返回值就是this指针指向的对象。问题来了,那么对于后置++该怎么写,然道也和前置++的原型一样吗,那怎么做区分,我们规定运算符重载的原型是:返回值类型 operator运算符,所以后置++重载的原型离不开这个规则,但是又为了和前置++做区分,规定:后置++重载时在参数列表中多增加了一个int类型参数,但调用函数时该参数不需要我们传值给它,而由编译器自动传递。
后置++的原型为:返回值类型 operator++(int);
#include <iostream>
using namespace std;
class Date
{
public:
//Date& operator++()
//{
// _day += 1;
// return *this;//this指针指向的是d1对象,该函数结束时,该对象不会销毁,因为该对象在main函数中
// //为了防止调用拷贝构造,提高效率,就传引用返回
//}
Date operator++(int)
{
//因为后置++是先使用再++,所以得另外创建一个对象,该对象保存原来对象的值,返回值也是该对象
Date temp(*this);//将d1对象初始化temp对象,编译器会默认生成拷贝构造,并调用。
this->_day += 1;//这里才是d1对像的成员要+1的地方
return temp;//temp对象在该函数中创建,函数结束,该对象会销毁,所以得传值返回
}
private:
int _year = 2023;
int _month = 11;
int _day = 27;
};
int main()
{
Date d1;
Date d2;
//d1 = ++d1;
d1 = d1++;//返回的temp对象给d1,实质返回的是原来没有进行++的d1对象,赋值完后,再进行++,则是改变d1对象成员的值
return 0;
}
调试结果:
六、const成员函数
拿代码分析来引出const成员函数:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 11, 28);
d1.print();
return 0;
}
正常运行结果:
对于以上代码是正常运行,但是我再创建d2对象时加上const修饰,即const Date d2(2023,11,28);
const Date d2(2023, 11, 28);
d2.print();
则编译结果:
意思就是d2现在是一个const Date类型的对象,而函数print();中的this指针类型是Date* const类型,那么将d2的地址传给this指针时,就是要将一个const Date*类型(指向对象的值不能修改)传给Date* const类型(指向对象的值可以修改),就是我们所说的权限放大了,那么怎样才能将const Date类型对象传给this指针呢,这里就引出了const修饰成员函数。
看代码:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << "print()" << endl;
cout << _year << "-" << _month << "-" << _day << endl;
}
void print() const
{
cout << "print() const" << endl;
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 11, 28);
d1.print();
const Date d2(2023, 11, 28);
d2.print();
return 0;
}
运行结果:
结果可以发现,d1对象调用了print();函数,而d2对象调用的时print() const函数。
这里给出const成员的定义:将const修饰的成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改,也也就是说被const修饰的成员函数this指针的类型变为了 const Date* const this。
const修饰成员函数原型:返回值类型 函数名(参数) const ,例如:void print() const;
所以d2对象可以传给被const修饰的成员函数,这算是权限的平移,我们知道权限可以平移,也可以缩小,但是不能放大,那么非const对象可以调用const成员函数就是权限的缩小。
所以在这里给出结论:
1.能定义成const成员函数都应该定义,这样const对象和非const对象都可以调用。
2.要修改成员变量的成员函数不能定义成const成员函数。
七、取地址及const取地址操作符重载
这两个运算符一般不需要重载,编译器会默认生成取地址的重载,只有特殊情况,才需要重载,比如想让别人获取指定的内容。
原型:返回值类型 operator&() 与 返回值类型 operator&() const;
#include <iostream>
using namespace std;
class Date
{
public:
Date()//自定义构造函数
{}
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
const Date d2;
cout << &d1 << endl;
cout << &d2 << endl;
return 0;
}
对于以上代码,d2对象是一个const类型的对象,而编译器默认生成的构造函数,其中this指针默认是非常量类型指针,所以得自己定义一个构造函数,在自定义构造函数中,this指针的类型取决于正在被构造的对象的类型。