从C语言到C++⑤(第二章_类和对象_中篇)(6个默认成员函数+运算符重载+const成员)(上):https://developer.aliyun.com/article/1513646
3. 拷贝构造函数(默认成员函数)
我们在创建对象的时候,能不能创建一个与已存在对象一模一样的新对象呢?
Date d1(2023, 5, 3); d1.Print(); Date d2(d1);//把d1拷贝给d2 d2.Print(); Date d3 = d1;//把d1拷贝给d3 (这也是拷贝构造,后面学的赋值是两个已存在的对象) d3.Print();
当然可以,这时我们就可以用拷贝构造函数。
3.1 拷贝构造函数概念
拷贝构造函数:只有一个形参,该形参是对本类类型对象的引用(一般常用 const 修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。实现一个上面日期类的拷贝构造:
Date(const Date& d) // 这里要用引用,否则就会无穷递归下去 { _year = d._year; _month = d._month; _day = d._day; }
3.2 拷贝构造函数特性和用法
拷贝构造函数也是一个特殊的构造函数,所以他符合构造函数的一些特性:
① 拷贝构造函数是构造函数的一个重载形式。函数名和类名相同,没有返回值。
② 拷贝构造函数的参数只有一个,并且必须要使用引用传参, 使用传值方式编译器直接报错,因为会引发无穷递归调用。
拷贝构造函数的用法:
#include <iostream> using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date(const Date& d)// 这里一定要用引用,否则就会无穷递归下去,加const是为了原来的d1被修改 { _year = d._year; _month = d._month; _day = d._day; } void Print() { printf("%d-%d-%d\n", _year, _month, _day); } private: int _year; int _month; int _day; }; int main() { Date d1(2023, 5, 3); d1.Print(); Date d2(d1);//把d1拷贝给d2 d2.Print(); Date d3 = d1;//把d1拷贝给d3 d3.Print(); return 0; }
为什么必须使用引用传参呢?
调用拷贝构造,需要先传参数,传值传参又是一个拷贝构造。
调用拷贝构造,需要先传参数,传值传参又是一个拷贝构造。
调用拷贝构造,需要先传参数,传值传参又是一个拷贝构造。
……
一直在传参这里出不去了,所以这个递归是一个无穷无尽的。
注意:如果参数在函数体内不需要改变,建议把 const 加上。
3.3 默认生成的拷贝构造
默认生成拷贝构造:
① 内置类型的成员,会完成按字节序的拷贝(把每个字节依次拷贝过去)。
② 自定义类型成员,会再调用它的拷贝构造。
拷贝构造我们不写生成的默认拷贝构造函数,对于内置类型和自定义类型都会拷贝处理。但是处理的细节是不一样的,这个跟构造函数和析构函数是不一样的。
(把上面的代码中自己写的拷贝构造屏蔽了,运行结果还是一样:)
#include <iostream> using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //Date(const Date& d)// 这里一定要用引用,否则就会无穷递归下去,加const是为了原来的d1被修改 //{ // _year = d._year; // _month = d._month; // _day = d._day; //} void Print() { printf("%d-%d-%d\n", _year, _month, _day); } private: int _year; int _month; int _day; }; int main() { Date d1(2023, 5, 3); d1.Print(); Date d2(d1);//把d1拷贝给d2 d2.Print(); Date d3 = d1;//把d1拷贝给d3 d3.Print(); return 0; }
所以为什么要写拷贝构造?写它有什么意义?这里没有什么意义。当然,这并不意味着我们都不用写了,有些情况还是不可避免要写的比如实现栈的时候,栈的结构问题,导致这里如果用默认的拷贝构造,会程序崩溃。按字节把所有东西都拷过来会产生问题,如果 Stack st1 拷贝出另一个 Stack st2(st1) ,会导致他们都指向那块开辟的内存空间,导致他们指向的空间被析构两次,导致程序崩溃,然而问题不止这些。
其实这里的字节序拷贝是浅拷贝,下面几章我们会详细讲一下深浅拷贝,这里的深拷贝和浅拷贝先做一个大概的了解。对于常见的类,比如日期类,默认生成的拷贝构造能用。但是对于栈这样的类,默认生成的拷贝构造不能用。
4. 运算符重载
C++为了增强代码的可读性引入了运算符重载 , 运算符重载是具有特殊函数名的函数 ,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
运算符重载简单来说:就是能让自定义类型和内置类型一样使用运算符。
4.1 运算符重载的概念
运算符重载是具有特殊函数名的函数,能让自定义类型和内置类型一样使用运算符。
函数名为 :关键字 operator 后面接需要重载的运算符符号 。 比如:
operator+ operator> operator==
函数原型:返回值类型 operator 操作符 ( 参数列表 )
返回值类型:看操作符运算后返回的值是什么。
参数:操作符有几个操作数,它就有几个参数。
注意事项:
- 不能通过连接其他符号来创建新的操作符,比如operator@,只能对已有的运算符进行重载,也不能对内置类型进行重载。
- 重载操作符必须有一个类类型或枚举类型的操作数。
- 用于内置类型的操作符,其含义不能改变。比如内置的整型 +,不能改变其含义。
- 作为类成员的重载函数时,其形参看起来比操作数数目少 1,成员函数的操作符有一个默认的形参 this,限定为第一个形参。
- 不支持运算符重载的 5 个运算符:(这个经常在笔试选择题中出现)
. (点运算符) :: (域运算符) .* (点星运算符)(目前博客没讲过的) ?: (条件运算符) sizeof
虽然点运算符( . )不能重载,但是箭头运算符( -> )是支持重载的,解引用(*)是可以重载的,不能重载的是点星运算符( .* )
4.2 运算符重载示例
我们重载一个判断日期类相等的运算符:==
#include <iostream> using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //private: int _year; int _month; int _day; }; bool operator==(const Date& d1, const Date& d2) { return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day; } int main() { Date d1(2023, 5, 2); Date d2(2023, 5, 3); cout << (d1 == d2) << endl;//这里的流插入运算符比我们重载的==优先级高,所以要加括号 return 0; }
这里运算符重载成全局的,不得不将成员变成是公有的,得把 private 注释掉,那么问题来了,封装性如何保证?这里其实可以用 "友元" 来解决,如果现在不知道也没关系,后面会讲。用友元也是不好的,所以一般直接重载成成员函数:
#include <iostream> using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } bool operator==(const Date& d) { return _year == d._year && _month == d._month && _day == d._day; } private: int _year; int _month; int _day; }; int main() { Date d1(2023, 5, 2); Date d2(2023, 5, 3); cout << (d1 == d2) << endl; //编译器自动转化为: // cout << (d1.operator==(d2)) << endl; return 0; }
既然要当成员函数,就得明白这里的 this 指的是谁。需要注意的是,左操作数是 this 指向的调用函数的对象。(关于运算符重载我们下一篇还会完整的实现一个日期类,重载各种运算符,比如日期减日期)
5. 赋值运算符重载(默认成员函数)
5.1 赋值运算符重载概念
赋值运算符重载主要是把一个对象赋值给另一个对象。
如果你不写,编译器会默认生成。
赋值运算符只能重载成类的成员函数,不能重载成全局函数。原因:赋值运算符如果不显式在类内实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
要分清和拷贝构造的区别:
int main() { // 一个已经存在的对象初始化一个马上创建实例化的对象 Date d1(2023, 5, 3); Date d2(d1); // 拷贝构造 Date d3 = d1; // 拷贝构造 // 两个已经存在的对象,之间进行赋值拷贝 Date d4(2023, 5, 4); d1 = d4; // 赋值 让 d1 和 d4 一样 return 0; }
5.2 赋值运算符重载使用
赋值运算符重载主要有以下四点:
① 参数类型
② 返回值
③ 检查是否给自己复制
④ 返回 *this
#include <iostream> using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date& operator=(const Date& d) { if (this != &d) // 防止自己跟自己赋值(这里的&d是取地址) { _year = d._year; _month = d._month; _day = d._day; } return *this; // 返回左操作数d1 } void Print() { cout << _year << "年" << _month << "月" << _day << "日" << endl; } private: int _year; int _month; int _day; }; int main() { Date d1(2023, 5, 3); Date d2(2023, 5, 4); d1 = d2; d1.Print(); d2.Print(); return 0; }
从C语言到C++⑤(第二章_类和对象_中篇)(6个默认成员函数+运算符重载+const成员)(下):https://developer.aliyun.com/article/1513648?spm=a2c6h.13148508.setting.33.5e0d4f0eCWTp6I