目录
类的6个默认成员函数
一、构造函数
概念
特性
二、析构函数
特性
三、拷贝构造函数
概念
特性
四、赋值运算符重载
运算符重载
前置++和后置++重载
= 赋值运算符重载
五、& 取地址操作符重载
六、const & 取地址操作符重载
认识const成员
const & 运算符重载
日期类的实现
类的6个默认成员函数
概念:
若一个类中没有写任何成员,我们简称为空类。然而空类并不是什么都没有。任何类在什么都不写时,编译器会自动生成6个默认成员函数!
默认成员函数
用户没有显式实现,编译器会自动生成的函数称为默认成员函数!
下面依次介绍类中的6个默认成员函数 :
1、构造函数
2、析构函数
3、拷贝构造函数
4、赋值运算符重载
5、& 取地址运算符重载
6、const & 取地址运算符重载
一、构造函数
概念
概念:
构造函数是一个特殊的成员函数,名字与类名相同,在创建类的对象的时,由编译器自动调用。保证每个数据成员都有一个合适的初始值,且构造函数在对象整个生命周期内只调用一次!
注意:
1、构造函数本质就是初始化函数!
2、若用户显式的写了构造函数,编译器会调用用户所写的构造函数。若用户没有写构造函数,编译器会调用编译器默认生成的构造函数!
3、编译器默认生成的构造函数对内置类型(int ,double ,char ……等)是不做处理的。而对于自定义类型会去调用它的构造函数!
4、构造函数是支持函数重载的!
分类:
默认构造函数分为三种:
1、无参构造
2、全缺省构造
3、编译器默认生成的构造
无参构造、全缺省构造、编译器默认生成的构造,都可以认为是默认构造函数。并且默认构造函数只能有一个!
特性
构造函数是特殊的成员函数,注意的是构造函数虽然名为构造,实际上它的主要任务是初始化对象的!
特性1
1、函数名与类名相同
2、无返回值(这里的无返回值不是void而是直接不写返回值)
3、对象实例化时编译器会自动调用对应的构造函数
4、构造函数支持重载
代码:
#include <iostream> using namespace std; //写一个日期类 class Date { public: //用户自己显式写构造函数 //构造函数名与类名相同 //无返回值 //支持函数重载 // //无参构造函数 Date() { _year = 0; _month = 0; _day = 0; } //带参构造函数 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;//调用无参构造函数 d1.Print();//打印 Date d2(202., 5, 5);//调用带参构造函数 d2.Print();//打印 //注意:若实例化对象调用无参构造,是不需要后面写括号的 //若加上括号,就跟函数无法区分了! Date d3();//这样书写我们无法确定它是函数还是实例化对象并调构造函数 return 0; }
特性2
5、如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数。一旦用户显式定义编译器将不再生成!
代码:
#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;//对象实例化的同时会自动调用构造函数 d1.Print();//打印 //打印结果是随机值 //出现随机值的原因是 //编译器自动生成的无参构造函数对内置类型(int char ……等,语言自带的类型)不做处理 return 0; }
特征3
6、编译器自动生成的默认构造函数,对内置类型不做处理,对自定义类型是去调用它的默认构造函数!
注意1:
C++把类型分为内置类型(基本类型)和自定义类型,内置类型就是语言提供的数据类型(int、char 、float、double ……等)。自定义类型就是我们自己使用 class、struct、union ……等 自己定义的类型 !而编译器默认生成的构造函数对内置类型不做处理,对自定义类型会去调用该自定义类型的构造函数!
代码:
#include <iostream> using namespace std; //时间类 class Time { public: Time() { cout << "Time 自定义类型调用它的构造函数" << endl; _hour = 0; _minute = 0; _second = 0; } private: int _hour; int _minute; int _second; }; //日期类 class Date { //编译器自动生成的默认构造函数 //对内置类型不做处理 //对自定义类型会调用它的构造函数 public: void Print() { cout << _year << " - " << _month << " - " << _day << endl; } private: // 基本类型(内置类型) int _year; int _month; int _day; // 自定义类型 Time _t; }; int main() { //实例化对象的同时编译器会调用它的构造函数 //对内置类型不做处理打印的结果是随机值! Date d; d.Print();//打印 return 0; }
注意2:
C++11中针对内置类型不初始化的缺陷又打了补丁,内置类型成员在类中声明时可以给默认值。即:用默认值来初始化内置类型本身
代码:
#include <iostream> using namespace std; //时间类 class Time { public: Time() { cout << "Time 自定义类型调用它的构造函数" << endl; _hour = 0; _minute = 0; _second = 0; } private: int _hour; int _minute; int _second; }; //日期类 class Date { //编译器自动生成的默认构造函数 //对内置类型不做处理 //但在C++11打了补丁,可以给内置类型默认值 //不做初始化的时候,默认用它的默认值初始化 //对自定义类型会调用它的构造函数 public: void Print() { cout << _year << " - " << _month << " - " << _day << endl; } private: //在C++11中,可以给内置类型给默认值进行初始化 // 基本类型(内置类型) int _year = 2023; int _month = 5; int _day = 5; // 自定义类型 Time _t; }; int main() { //实例化对象的同时编译器会调用它的构造函数 //给了内置类型默认值,若不进行初始化,默认使用它的默认值! Date d; d.Print();//打印 return 0; }
特征4
7、自己写的 无参的构造函数、全缺省的构造函数 、以及没写编译器自动生成的构造函数,都称为默认构造函数,并且默认构造函数只能有一个
代码:
#include <iostream> using namespace std; class Date { //默认构造函数分为:无参的构造、全缺省构造、编译器自动生成构造 都是默认构造 //且一个类只能有一个构造函数 //虽然构造函数支持函数重载 //但是若有多个构造函数,在实例化对象的时候会出现问题 public: Date() { _year = 2023; _month = 5; _day = 5; } Date(int year = 2023, int month = 5, int day = 5) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { //实例化对象会发生调用不明确 //有多个构造函数 Date d1; //error(报错):类 "Date" 包含多个默认构造函数 return 0; }
二、析构函数
概念
概念:
析构函数是一个特殊的成员函数,函数名是在类名的前面加 ~ 符号,创建的类对象在生命周期结束的时会自动调用析构函数。析构函数是完成对象中资源的清理工作!
理解:
1、析构函数不是完成对 对象本身的销毁,局部对象销毁工作是由编译器完成的。对象在销毁时会自动调用析构函数,完成对象中资源的清理工作!
2、比如在对象中存在动态开辟的空间析构函数的作用就是释放这些空间!
注意:
1、析构函数名是在类名前加上~
2、析构函数在对象生命周期结束的时候会自动调用!
3、析构函数不支持重载
4、一个类只能有一个析构函数,若用户显式的写了编译器是不会自动生成的。若用户没有写,编译器会自动生成一个析构函数,并在对象销毁时调用!
5、析构函数对内置类型不处理,因为内置类型出了作用域编译器就将它销毁!而对于自定义类型会去调用它的析构函数
6、如果类中没有申请空间的资源时,析构函数是可以不显式写的,直接使用编译器生成的默认析构函数!
特性
特性1
1、析构函数名是在类名前加上字符 ~
2、无参数无返回值类型
3、析构函数不支持重载
4、一个类只能有一个析构函数,若未显式定义、系统会自动生成默认的析构函数
5、对象生命周期结束时,C++编译器会自动调用析构函数
代码:
#include <iostream> using namespace std; typedef int DataType; //写一个简易栈 class Stack { public: Stack(size_t capacity = 3) { _array = (DataType*)malloc(sizeof(DataType) * capacity); if (NULL == _array) { perror("malloc申请空间失败!!!"); return; } _capacity = capacity; _size = 0; } void CheckCapacity() { if (_size == _capacity) { DataType* tmp = (DataType*)realloc(_array, sizeof(DataType) * _capacity * 2); if (nullptr == tmp) { perror("realloc tail "); return; } _array = tmp; _capacity *= 2; } } void Push(DataType data) { CheckCapacity(); _array[_size] = data; _size++; } //自己写了析构函数编译器就不会默认生成 // 自己写的析构函数 ~Stack() { if (_array) { free(_array); _array = NULL; _capacity = 0; _size = 0; } } private: DataType* _array; int _capacity; int _size; }; int main() { Stack s; s.Push(1); s.Push(2); }
特性2
5、若我们不自己写析构函数,编译器会自动生成一个析构函数。编译器默认生成的析构函数,对于内置类型是不做处理的,内置类型是出了作用域编译器就会将它销毁。对于自定义类型去调用它的默认析构函数!
代码:
#include <iostream> using namespace std; //时间类 class Time { public: //自己写的析构函数 ~Time() { cout << "~Time()" << endl; } private: int _hour; int _minute; int _second; }; //日期类 class Date { //析构函数对自定义类型是去调用它的析构函数 //对于内置类型,不做处理,是自动去销毁! private: // 基本类型(内置类型) int _year = 2023; int _month = 5; int _day = 5; // 自定义类型 Time _t; }; int main() { //当对象销毁的时候会调用它的析构函数 //而d对象里面有自定义类型 //而析构函数的对于自定义类型会去调用它的析构函数 Date d; return 0; }
三、拷贝构造函数
概念
在创建对象时是可以创造一个与已经存在的对象一模一样的新对象!而要创建该对象就需要用到拷贝构造函数
概念:
拷贝构造函数,是用已经存在的对象去初始化新对象。函数只有单个形参,该形参是对本类类型对象的引用(一般用const修饰),在用已经存在的类类型对象去初始化新对象时,由编译器自动调用!
注意:
1、拷贝构造是一个特殊的成员函数,是用已经存在的对象去初始化新对象。
2、编译器会自动调用
3、若用户没有显式定义,编译器会自动生成一个默认拷贝构造
4、若用户现式定义了拷贝构造,编译器不会自己生成,直接调用我们写的拷贝构造
5、只有一个形参,且参数只能是该类 类型对象的引用!
特性
特性1
1、拷贝构造函数是构造函数的重载形式
2、拷贝构造函数的参数只有一个且必须是类类型对象的引用,若使用传值方式编译器会直接报错,;因为会引发无穷递归调用!
代码:
#include <iostream> using namespace std; class Date { public: //构造函数 Date(int year = 2023, int month = 5, int day = 8) { _year = year; _month = month; _day = day; } //拷贝构造函数是构造函数的重载形式 //Date(const Date d) // 错误写法:编译报错,会引发无穷递归 Date(const Date& d) // 正确写法 { _year = d._year; _month = d._month; _day = d._day; } //打印 void Print() { cout << _year << " - " << _month << " - " << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1;//创建d1对象 d1.Print();//打印d1 Date d2(d1);//用d1对象去初始化d2对象(调用拷贝构造函数) d2.Print();//打印d2 return 0; }
无穷递归的原因
拷贝构造函数单个参数必须是类类型对象的引用,为什么不能传值呢?
因为函数形参是实参的一份临时拷贝。C++中规定,只要是自定义类型进行拷贝就要先去调用它的拷贝构造函数进行拷贝。即自定义类型在传参的时候会去调用它的拷贝构造函数,而调用拷贝构造再进行传值的时候又会去调用拷贝构造,就会形成死递归!
用自定义类型做返回值的时候也会调用它的拷贝构造!
特性2
3、若未显式定义,编译器会生成默认的拷贝构造函数!
4、编译器默认生成的拷贝构造函数按字节序进行拷贝(也就是一个字节一个字节进行拷贝)这种叫做浅拷贝或者值拷贝!
5、编译器默认生成的拷贝构造函数,对内置类型是按照字节方式直接拷贝(浅拷贝)。对自定义类型是调用其拷贝构造函数完成拷贝的!
代码:
#include <iostream> using namespace std; //时间类 class Time { public: //构造函数 Time() { _hour = 11; _minute = 23; _second = 16; } //时间类的拷贝构造 //拷贝构造 Time(const Time& t) { _hour = t._hour; _minute = t._minute; _second = t._second; cout << "调用了Time类的拷贝构造函数" << endl; } //打印 void Print() { cout << _hour << "时" << _minute << "分" << _second << "秒" << endl; } private: int _hour; int _minute; int _second; }; //日期类 class Date { public: void Print() { cout << _year << "年" << _month << "月" << _day << "日" <<" "; _t.Print(); } private: // 基本类型(内置类型) int _year = 2023; int _month = 5; int _day = 8; // 自定义类型 Time _t; }; int main() { //若没有显示定义拷贝构造函数 // 编译器会默认生成一个拷贝构造 // 默认生成的拷贝构造 // 对于内置类型是进行字节拷贝的 // 对于自定义类型会去调用它的拷贝构造来完成拷贝 Date d1; d1.Print();//打印 // 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数 // 但Date类并没有显式定义拷贝构造函数, //则编译器会给Date类生成一个默认的拷贝构造函数 //而Date类中有个Time自定义类型 //用d1拷贝构造d2时,也会去调用自定义类型的拷贝构造 Date d2(d1); d2.Print();//打印 return 0; }
特性3
6、类中如果没有涉及资源申请时,拷贝构造函数我们自己写或者用编译器生成的都可以。一旦涉及到资源申请时,则拷贝构造是一定要自己写的,编译器自己生成的浅拷贝没法完成工作。这里是需要深拷贝需要自己写
代码:
#include <iostream> using namespace std; // 这里程序会崩溃,因为深拷贝的问题,编译器默认生成的浅拷贝解决不料! typedef int DataType; class Stack { public: //构造函数 Stack(size_t capacity = 10) { //进行了动态申请空间资源 _array = (DataType*)malloc(capacity * sizeof(DataType)); if (nullptr == _array) { perror("malloc申请空间失败"); return; } //注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 //时,则拷贝构造函数是一定要写的,否则就是浅拷贝。 _size = 0; _capacity = capacity; } //没有写拷贝构造,编译器会默认生成一个拷贝构造 //按照字节序进行拷贝 浅拷贝 //压栈 void Push(const DataType& data) { // CheckCapacity();//扩容函数 _array[_size] = data; _size++; } //析构函数 ~Stack() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } private: DataType* _array; size_t _size; size_t _capacity; }; int main() { Stack s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack s2(s1); return 0; }
注意:
上述写的栈是需要我们自己写拷贝构造函数的,因为栈里面涉及动态内存开辟的问题,如果我们不写,默认使用编译器默认生成的函数将会出现拷贝上的问题,因为编译器默认生成的是按照字节的内容进行字节序拷贝,是浅拷贝,而我们用动态申请的空间是用一个指针成员变量来进行指向的,用编译器默认生成的函数是不会重新去开辟一块空间,是直接将这个 指针中的内容直接拷贝到第二个指针中!导致两个指针同时指向一块在堆区申请的空间!所以我们要自己进行写拷贝构造函数,用来满足深拷贝!
7、拷贝构造函数的典型调用场景
场景一:使用已存在对象创建新对象
用类类型定义一个新对象并用已经存在的对象进行初始化,会调用拷贝构造!
场景二:函数参数类型为类类型对象
当一个函数的形参是类类型的对象,因为形参是实参的一份临时拷贝,当实参在对形参传递时会调用拷贝构造!
场景三:函数返回值为类类型对象
当函数的返回值是类类型对象时,返回的时候会创建一个临时变量来保存返回值,然后进行返回,而将返回值给临时变量也是一个拷贝的过程,所以会调用拷贝构造!
代码:
#include <iostream> using namespace std; class Date { public: Date(int year, int minute, int day) { cout << "Date(int,int,int):" << this << endl; } Date(const Date& d) { cout << "Date(const Date& d):" << this << endl; } ~Date() { cout << "~Date():" << this << endl; } private: int _year; int _month; int _day; }; //函数参数类型为类类型对象 //函数返回值是类类型 //都会调用拷贝构造 Date Test(Date d) { Date temp(d); return temp; } int main() { Date d1(2022, 1, 13); //用已经存在的对象去初始化新对象 Date d2 = d1; Test(d1); return 0; }
建议:
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时 根据实际场景,能用引用的尽量使用引用!
四、赋值运算符重载
运算符重载
C++中为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回类型和参数列表与普通函数类似!
函数名称为:operator后面跟要重载的运算符号。
函数原型:返回值类型 operator运算符(参数列表)
注意:
1、不能通过连接其它符号来创建新的操作符:例如operator@
2、重载操作必须有一个类类型参数
3、用于内置类型的运算符,其含义不能改变,例如:内置类型+ 不能改变其含义
4、做为类成员函数重载时,其形参看起来比操作数少1,因为成员函数的第一个参数为隐式的this
5、 .* 、 :: 、 sizeof 、 ?: 、 . 注意这五个运算符不能进行重载!
设计一个日期类,对该类运算符进行重载:
1、将运算符重载到全局位置,此时为了很好的使用该运算符,就需要将类的成员公有化!
代码:
//将运算符重载到全局中,此处需要类成员是公有的 //重载==运算符 #include <iostream> using namespace std; //定义日期类 class Date { public: //构造函数 Date(int year = 0, int month = 0, int day = 0) { _year = year; _month = month; _day = day; } public: int _year; int _month; int _day; }; //利用operator关键字来重载运算符 //重载==运算符 bool operator==(Date& d1, Date& d2) { return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day; } int main() { //创建对象 Date d1(2023, 5, 12); Date d2(2023, 6, 12); //重载运算符使用方法 // 1、以函数的形式直接用(不推荐) operator==(d1, d2); //2、直接以运算符的方式使用(推荐) d1 == d2; //两者皆可打印出结果 cout << operator==(d1, d2) << endl; cout << (d1 == d2) << endl; return 0; }
若是将运算符重载到全局处,我们就必须将成员给成公有的,这样成员是很危险的!所以我们可以将运算符重载到类里面,就很好的规避了这个问题!
2、将运算符重载到类里面
代码:
//将运算符重载到类中,就可以访问私有的成员变量 //重载==运算符 #include <iostream> using namespace std; //定义日期类 class Date { public: //构造函数 Date(int year = 0, int month = 0, int day = 0) { _year = year; _month = month; _day = day; } //利用operator关键字来重载运算符 //重载==运算符 //第一个参数是隐含的this指针,是调用对象的地址 bool operator==(const Date& d2) { return _year == d2._year && _month == d2._month && _day == d2._day; } public: int _year; int _month; int _day; }; int main() { //创建对象 Date d1(2023, 5, 12); Date d2(2023, 6, 12); //重载运算符使用方法 // 1、对象调用成员函数的形式直接用(不推荐) d1.operator==(d2); //2、直接以运算符的方式使用(推荐) d1 == d2; //两者皆可打印出结果 cout << d1.operator==(d2) << endl; cout << (d1 == d2) << endl; return 0; }
前置++和后置++重载
概念:
前置++:返回的是+1之后的值
后置++:返回的是+1之前的值
规定:
C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器 自动传递
代码:
#include <iostream> using namespace std; class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // 前置++:返回+1之后的结果 // 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率 Date& operator++() { _day += 1; return *this; } // 后置++: // 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载 // C++规定:后置++重载时多增加一个int类型的参数, //但调用函数时该参数不用传递,编译器自动传递 // 注意:后置++是先使用后+1,因此需要返回+1之前的旧值, //故需在实现时需要先将this保存一份,然后给this + 1 //而tmp是临时对象,因此只能以值的方式返回,不能返回引用 Date operator++(int) { Date tmp(*this); _day += 1; return tmp; } //打印 void Print() { cout << _year << " - " << _month << " - " << _day << endl << endl; } private: int _year; int _month; int _day; }; int main() { Date d; Date d1(2022, 1, 13); d = d1++; // d: 2022,1,13 d1:2022,1,14 d.Print(); d1.Print(); d = ++d1; // d: 2022,1,15 d1:2022,1,15 d.Print(); d1.Print(); return 0; }
总结:
在重载两者运算符时,前置++的参数只有一个隐含的this指针,而后置++的参数除了隐含的this指针之外,还有一个int的参数,而该参数只是为了跟前置++做区分而写的,不需要我们自己传参,编译器会默认传!
= 赋值运算符重载
1、赋值运算符重载格式
参数类型:
const 类& (传递对象的引用可以提高传参效率)
赋值运算符重载,是类的默认成员函数,若我们显示书写,必须写在类里面,而传递的参数有两个,一个是隐含的this指针,另一个是const修饰的对象的引用,将该对象的值赋值给this所指向的对象!
返回值类型:
对象& (返回引用可以提高返回效率,有返回值目的是为了支持连续赋值)
返回的是对象的引用,实际上返回的是this指针指向对象的引用,效率高,若直接返回对象,会调用拷贝构造,效率下降!
检测是否自己给自己赋值:
判断一下是否是自己给自己赋值,若是自己给自己赋值则不进行赋值!
返回*this:
为了符合连续赋值的含义,要返回*this
实现连续赋值,要返回this指针指向的对象的引用,*this就是取到了该对象(对指针解引用取到的是它所指向的内容)
代码:
//赋值运算符重载(写在类里面) #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& d) { _year = d._year; _month = d._month; _day = d._day; } //赋值运算符重载 (要写在类里面) //返回值是类的引用 //参数是const修饰的类的引用 Date& operator=(const Date& d) { //判断自己不要给自己赋值 if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; } //打印 void Print() { cout << _year << "年" << _month << "月" << _day << "日" << endl; } private: int _year; int _month; int _day; }; int main() { Date d1(2023, 5, 12); Date d2; //若用一个已经存在的对象去初始化新对象会调用拷贝构造 //用两个已经存在的对象进行赋值才会调用赋值运算符重载 d2 = d1; d2.Print(); return 0; }
2、赋值运算符只能重载到类里面不能重载到全局函数
赋值运算符是不能重载成全局的!
代码:
//赋值运算符不能重载到全局位置(会编译错误) #include <iostream> using namespace std; class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } int _year; int _month; int _day; }; // 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数 Date& operator=(Date& left, const Date& right) { if (&left != &right) { left._year = right._year; left._month = right._month; left._day = right._day; } return left; } int main() { return 0; } // 编译失败: // error (报错): “operator =”必须是非静态成员
解析原因:
赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户在类外自己实现 一个全局的赋值运算符重载,编译器会在类中默认实现一个赋值运算符重载。用户实现的和编译器在类中生成的默认赋值运算符重载冲突了,故赋值 运算符重载只能是类的成员函数。
3、若用户没有显示写,编译器会生成一个默认赋值运算符重载
在类中用户不显示写,编译器会生成一个默认的赋值运算符重载!
处理方式:
编译器默认生成的赋值运算符重载,对内置类型成员是进行逐字节拷贝的!而对于自定义类型会去调用它的赋值运算符重载!(编译器生成的是按字节进行拷贝的)
代码:
//编译器默认生成的赋值运算符重载 //对于内置类型会按照字节进行拷贝 //对于自定义类型会调用它的默认赋值运算符重载 #include <iostream> using namespace std; //时间类 class Time { public: Time() { _hour = 1; _minute = 1; _second = 1; } //赋值运算符重载 Time& operator=(const Time& t) { if (this != &t) { _hour = t._hour; _minute = t._minute; _second = t._second; } return *this; } //打印 void Print() { cout << _hour << "时" << _minute << "分" << _second << "秒" << endl; } private: int _hour; int _minute; int _second; }; //日期类 class Date { public: //构造函数 Date(int year = 0, int month = 0, int day = 0) { _year = year; _month = month; _day = day; } //打印 void Print() { cout << _year << "年" << _month << "月" << _day << "日" << endl; _t.Print(); } private: // 基本类型(内置类型) int _year = 2023; int _month = 5; int _day = 12; // 自定义类型 Time _t; }; int main() { //编译器默认生成的赋值运算符重载 //对内置类型按字节拷贝 //对自定义类型调用它的赋值运算符重载 Date d1(2023,5,12); Date d2; d2 = d1; d2.Print();//打印 return 0; }
注意:
对于日期类,这样的没有额外开销也就是浅拷贝的类而言,我们不写赋值运算符重载,用编译器默认的就已经足够了,但是对于进行了额外开销也就是需要深拷贝的类而言编译器默认生成的赋值运算符重载函数,是无法满足我们的需求的,对于这种类,我们就必须要自己写赋值运算符重载了!
如下代码中的类就必须要我们自己写赋值运算符重载了!
代码:
// 这里会发现下面的程序会崩溃掉 //编译器默认生成的赋值运算符重载是不能满足我们的需求的! #include <iostream> using namespace std; typedef int DataType; class Stack { public: //构造函数 Stack(size_t capacity = 10) { _array = (DataType*)malloc(capacity * sizeof(DataType)); if (nullptr == _array) { perror("malloc申请空间失败"); return; } _size = 0; _capacity = capacity; } //入栈 void Push(const DataType& data) { // CheckCapacity(); _array[_size] = data; _size++; } //析构函数 ~Stack() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } private: DataType* _array; size_t _size; size_t _capacity; }; int main() { Stack s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack s2; //默认生成的只是将内容按照字节拷贝过去 //并不会给_array指针去申请空间 //两个对象中的指针共同指向一块空间 //是有问题的! s2 = s1; return 0; }
看图理解:
总结:
如果类中未涉及到资源管理也就是只进行浅拷贝,赋值运算符我们自己是否实现都可以;一旦涉及到资源管理也就是深拷贝,则必须要自己实现!
五、& 取地址操作符重载
在C++中取地址运算符重载也是类的默认成员函数之一,用户不显示写,编译器会默认生成。若用户显式写,编译器不会生成。若用户显式写了类的默认成员函数则必须写在类中,不可写在类外面(写在类外面,编译器会默认生成一个默认成员函数,在调用该函数时会出现调用不明确的错误)
代码:
#include <iostream> using namespace std; class Date { public: //我们显式写,编译器不会生成 //取地址运算符重载 Date* operator&() { return this; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1; cout << &d1 << endl; return 0; }
默认生成的取地址运算符:
一般情况下,取地址默认成员函数,不需要我们自己写,默认使用编译器生成就足够了!
代码:
#include <iostream> using namespace std; class Date { private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { //直接使用编译器默认生成的取地址 //也是没有任何问题的 Date d1; cout << &d1 << endl; return 0; }
特殊情况:
一般使用编译器默认生成的取地址函数重载就够了,但是在某些特殊情况下,用户需要自己重载,比如:不想让别人取到对象的地址 ……等!
代码:
#include <iostream> using namespace std; class Date { public: //特殊情况下,不想让别人取到对象的地址,自己写 Date* operator&() { return nullptr; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1; //编译器调用我们自己写的默认取地址重载函数 //取不到对象的地址 cout << &d1 << endl; return 0; }
总结:
对于取地址运算符重载默认成员而言,一般情况下不需要自己写,直接使用编译器默认生成的就足够了,只有极少数特殊情况下需要自己写(按需所取即可)
六、const & 取地址操作符重载
认识const成员
概念:
用const 修饰的变量及参数,说明其是不可被改变的!
const 修饰*this:
而在类中用 const 修饰的成员函数 称之为 const成员函数,在类中const修饰成员函数,实际上修饰的是该成员函数隐含的this指针(const修饰的是*this,表示this指针指向的内容不可被修改)表示在该成员函数中不能对类的任何成员进行修改!
理解:
在类和对象 - 上文章中,我们说到,隐含的this指针,默认是用const修饰的,而这个默认的const修饰的是this指针本身,即不可修改this指针的指向!
而此处的const,是修饰*this 的,即this指针指向的内容是不可被改变的!也就是类的成员不可通过该成员函数进行修改!
const书写位置:
修饰*this的const是要写在成员函数括号后面的,表示修饰*this ,tihs指针指向的内容不可被修改!
代码:
#include <iostream> using namespace std; class Date { public: //构造 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } //以下两个Print函数 // 构成函数重载 // 因为参数类型不同 // 一个是const修饰,一个不是 void Print() { cout << "Print()" << endl; cout << "year:" << _year << endl; cout << "month:" << _month << endl; cout << "day:" << _day << endl << endl; } //const修饰的是隐含的this指针 //修饰的是*this 指针指向的内容不可被修改 //也就是不能通过该成员函数去修改类的成员 void Print() const { cout << "Print()const" << endl; cout << "year:" << _year << endl; cout << "month:" << _month << endl; cout << "day:" << _day << endl << endl; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { //const修饰的对象 //会去调用const成员函数 Date d1(2022, 1, 13); d1.Print(); const Date d2(2022, 1, 13); d2.Print(); return 0; }
const & 运算符重载
概念:
它也是默认成员函数之一,用来取到const修饰的对象的地址,若不写编译器会自动生成!
一般情况下默认使用编译器生成的就足够了,特殊情况下需要自己写,如:不让别人取到对象的地址……等!
代码:
#include <iostream> using namespace std; class Date { public: Date(int year = 0, int month = 0, int day = 0) { _year = year; _month = month; _day = day; } //此处括号后面const修饰的是隐含的this指针 //修饰*this this指向的内容不可被修改 //即该成员函数不可修改类的成员 //显式写,编译器不会生成 //默认使用我们写的! const Date* operator&()const { cout << "const Date* operator&()" << endl; return this; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1(2023,4,4); const Date d2(2023,5,15);//const修饰的对象,要在定义的时候初始化 //因为const修饰的后期是不可改变的,所以必须要有初始值! //若没写编译器默认生成 //若写了会调用我们所写的成员函数 cout << &d1 << endl; cout << &d2 << endl; return 0; }
总结:
我们不写编译器会默认生成,写了会默认调用我们写的默认成员函数,一般情况下,const取地址运算符重载,不用我们自己写,编译器默认生成的就可以了!特殊情况下我们需要自己写:比如不想让别人取到地址……等!(则情况而定)!
日期类的实现
用以上知识手动实现一个日期类
代码:
#define _CRT_SECURE_NO_WARNINGS 1 //Date 类的实现 #include <iostream> using namespace std; #include <assert.h> //日期类 class Date { public: //构造函数 Date(int year = 0, int month = 0, int day = 0) { _year = year; _month = month; _day = day; } //拷贝构造 Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } //赋值运算符重载 Date& operator=(const Date& d) { if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; } //&取地址运算符重载 Date* operator&() { return this; } //const &取地址运算符重载 const Date* operator&()const { return this; } //析构函数 ~Date() { _year = 0; _month = 0; _day = 0; } //1、比较运算符重载 // > 大于运算符重载 bool 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; } return false; } // == 等于运算符重载 bool operator==(const Date& d)const { return _year == d._year && _month == d._month && _day == d._day; } // >= 大于等于运算符重载 bool operator>=(const Date& d)const { return *this > d || *this == d; } // < 小于运算符重载 bool operator<(const Date& d)const { return !(*this >= d); } // <= 小于等于运算符重载 bool operator<=(const Date& d)const { return !(*this < d); } // != 不等于运算符重载 bool operator!=(const Date& d)const { return !(*this == d); } //2、算术运算符重载 //计算天数函数 static int GetMonthDay(int year, int month) { static int MonthDay[13] = { 0,31,29,31,30,31,30,31,31,30,31,30,31 }; if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) { return 28; } return MonthDay[month]; } // + 加法运算符重载(日期+天数) Date operator+(int day)const { if (day < 0) { day = -day; return *this - day; } Date tmp = *this; tmp._day += day; while (tmp._day > GetMonthDay(tmp._year, tmp._month)) { tmp._day -= GetMonthDay(tmp._year, tmp._month); ++tmp._month; if (tmp._month == 13) { tmp._year++; tmp._month = 1; } } return tmp; } // += 加等运算符重载 Date& operator+=(const int day) { *this = *this + day; return *this; } // - 减运算符重载 Date operator-(int day)const { if (day < 0) { day = -day; return *this + day; } Date tmp = *this; tmp._day -= day; while (tmp._day <= 0) { --tmp._month; if (tmp._month == 0) { --tmp._year; tmp._month = 12; } tmp._day += GetMonthDay(tmp._year, tmp._month); } return tmp; } // -= 减等运算符重载 Date& operator-=(const int day) { *this = *this - day; return *this; } //前置++ 运算符重载 Date& operator++() { return *this += 1; } //后置++ 运算符重载 Date operator++(int) { Date tmp = *this; *this += 1; return tmp; } //前置-- 运算符重载 Date& operator--() { return *this -= 1; } //后置-- 运算符重载 Date operator--(int) { Date tmp = *this; *this -= 1; return tmp; } // - 运算符重载(日期-日期) int operator-(const Date& d)const { Date max = *this; Date min = d; int flag = 1; if (d > *this) { max = d; min = *this; flag = -1; } int day = 0; while (min != max) { ++min; ++day; } return day*flag; } //流插入运算符重载 friend ostream& operator<<(ostream& out, Date& d); //流提取运算符重载 friend istream& operator>>(istream& in, Date& d); private: int _year; int _month; int _day; }; //流插入运算符重载 ostream& operator<<(ostream& out,Date& d) { out << d._year << "年" << d._month << "月" << d._day << "日"; return out; } //流提取运算符重载 istream& operator>>(istream& in, Date& d) { //判断输入的是否合法 int year = 0; int month = 0; int day = 0; in >> year >> month >> day; //年月日合法再进行赋值 if ((month > 0 && month < 13) && (day > 0 && day <= d.GetMonthDay(year, month))) { d._year = year; d._month = month; d._day = day; } else { cout << "输入的日期非法" << endl; assert(false); } return in; } void Test1(void) { Date d1(2023, 3, 16); ++d1; cout << d1 << endl; Date d2(2020, 5,16); cout << d2 << endl; Date d3; cin >> d3; cout << d3 << endl; } int main() { Test1(); return 0; }