赞
踩
目录
上一节,我们学习了类和对象的上部分内容,难度一般,这一节我们要学习类和对象重点部分,理解难度大,望大家认真学习,真正进入C++的大门。
[本节目标]
1. 类的6个默认成员函数
2. 构造函数
3. 析构函数
4. 拷贝构造函数
5. 赋值操作符重载
6. 默认拷贝构造与赋值运算符重载的问题
7. const成员函数
8. 取地址及const取地址操作符重载
如果一个类中什么成员都没有,简称为空类。那么空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。
我们在写栈这一结构时,对栈的初始化和销毁经常忘记,初始化忘记会导致程序直接错误,忘记销毁会导致内存泄露,虽然刷题时网站不会提醒你内存泄露,但是如果在一个服务器上持续不断的跑,时间一长就会崩溃,这是极其严重的错误,所以,“祖师爷”做出了构造函数和析构函数,分别完成初始化工作和清理工作,替代了人工操作。
默认成员函数是:我们不写,那么编译器会自动生成
构造函数是一个特殊的成员函数,名字与类名相同, 创建类类型对象时由编译器自动调用,没有返回值,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。
构造函数是特殊的成员函数,需要注意的是,构造函数的名称虽然叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。例如,栈的定义需要初始化函数Init,构造函数替代了我们每次建立栈时的初始化操作。
其特征如下:
s1. 函数名与类名相同。
s2. 无返回值。
s3. 对象实例化时编译器自动调用对应的构造函数。
s4. 构造函数可以重载,因为构造函数可以有多种参数
- typedef int DataType;
- class Stack
- {
- public:
-
- Stack(int capacity = 4)
- {
- cout << "Stack(int capacity = 4)" << endl;
- _array = (DataType*)malloc(sizeof(DataType) * capacity);
- if (NULL == _array)
- {
- perror("malloc申请空间失败!!!");
- return;
- }
-
- _capacity = capacity;
- _size = 0;
- }
-
- /*void Init()
- {
- _array = (DataType*)malloc(sizeof(DataType) * 4);
- if (NULL == _array)
- {
- perror("malloc申请空间失败!!!");
- return;
- }
- _capacity = 4;
- _size = 0;
- }*/
-
- void Push(DataType data)
- {
- CheckCapacity();
- _array[_size] = data;
- _size++;
- }
-
- void Pop()
- {
- if (Empty())
- return;
- _size--;
- }
-
- DataType Top() { return _array[_size - 1]; }
- int Empty() { return 0 == _size; }
- int Size() { return _size; }
-
- void Destroy()
- {
- if (_array)
- {
- free(_array);
- _array = NULL;
- _capacity = 0;
- _size = 0;
- }
- }
-
-
- private:
- void CheckCapacity()
- {
- if (_size == _capacity)
- {
- int newcapacity = _capacity * 2;
- DataType* temp = (DataType*)realloc(_array, newcapacity * sizeof(DataType));
- if (temp == NULL)
- {
- perror("realloc申请空间失败!!!");
- return;
- }
- _array = temp;
- _capacity = newcapacity;
- }
- }
- private:
- DataType* _array;
- int _capacity;
- int _size;
- };
s5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
- class Date
- {
- public:
- void Print()
- {
- cout << _year << "-" << _month << "-" << _day << endl;
- }
- private:
- // 内置类型
- // C++11支持,这里不是初始化,因为这里只是声明
- // 这里给的是默认的缺省值,给编译器生成默认构造函数用
- int _year;
- int _month;
- int _day;
-
- // 自定义类型
- //Stack _st;
- };
-
- // 1、一般情况下,有内置类型成员,就需要自己写构造函数,不能用编译器自己生成的。
- // 2、全部都是自定义类型成员,可以考虑让编译器自己生成
-
- int main()
- {
- // 构造函数的调用跟普通函数也不一样
- Date d1;
- d1.Print();
-
-
- return 0;
- }
调用 printf函数,对打印出三个随机值,这是为什么呢?编译器不是自动生成了构造函数进行初始化对象操作了吗?为什么不默认初始化为0
s7. 关于编译器生成的默认成员函数,很多童鞋会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象year/month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么卵用??
C++中分为两种类型:
- 内置类型(基本类型),语言本身定义的基础类型,如int、char、double等等。任何指针类型都是内置类型。
- 自定义类型,用struct或class等定义的类型。
我们不写构造函数,那么编译器会默认自动生成构造函数,
- 若成员变量是内置类型,则不会对其做处理。
- 若是自定义类型,则会调用它的默认构造(该类内部的构造函数,就像用两个栈实现一个队列,栈定义在一个Stack类中,队列中定义两个自定义类型stack1,stack2,如果我们在队列中不写构造函数,那么编译器会自动处理自定义类型 ,调用Stack的构造函数对stack1,stack2进行初始化)C++规定必须处理自定义类型。
这就是C++的规定,但这里我们认为有些许不合理,我们会更期望内置类型默认赋值为0,而不是随机值。(注意:有些编译器会对内置类型做处理,但该行为是个性化行为,不是所有编译器都会处理,编译器版本不同处理也会不同)
所以,在C++11的标准发布的时候,对这里打了补丁:在成员声明的时候可以给缺省值,即在没有初始值时,用缺省值
- class Date
- {
- public:
-
- void Print()
- {
- cout << _year << "-" << _month << "-" << _day << endl;
- }
-
- private:
-
- // 内置类型
- // C++11支持,这里不是初始化,因为这里只是声明
- // 这里给的是默认的缺省值,给编译器生成默认构造函数用
- int _year = 1;
- int _month = 1;
- int _day = 1;
-
- // 自定义类型
- //Stack _st;
- };
也可在实例化时给初始值
- class Date
- {
- public:
- Date(int year = 1, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = day;
- }
-
- void Print()
- {
- cout << _year << "-" << _month << "-" << _day << endl;
- }
-
- private:
- // 内置类型
- // C++11支持,这里不是初始化,因为这里只是声明
- // 这里给的是默认的缺省值,给编译器生成默认构造函数用
-
- int _year = 1;
- int _month = 1;
- int _day = 1;
-
- // 自定义类型
- //Stack _st;
- };
-
- int main()
- {
-
- Date d1;
- //构造函数的调用跟普通函数也不一样
- //Date d1();
- //给出初始值
- Date d2(2023, 11, 11);
-
- return 0;
- }
这里我们可能又有疑惑了, 为什么调用构造函数要这么写?为什么不写成 d1.Date(2023, 11, 11) 呢?这样不是更明了吗?如果不传参,能写为 Date d1();吗?
s6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个(全缺省和无参只能存在一个,不然会引起冲突,编译器判断不了调用哪一个构造函数)。注意:无参构造函数、全缺省构造函数以及我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
无参实例化时,编译器会调用默认构造函数,就是上述的三种构造函数。如果没有默认的构造函数,那么我们实例化时就必须要传参。
结论:
1. 如果有内置类型,并且初始值我们不确定,那么就要自己写构造函数,不要放手给编译器,因为编译会虽然会自动生成构造函数,但是他并不会处理内置类型,内置类型的值是随机值,会对程序造成影响。
2. 内置类型成员都有缺省值,且初始化我们的要求,就可以不写构造函数。如果初始化不是我们想要的,那么还是要手写构造函数,在构造函数内进行传参赋值,若要传指定参数,在实例化的时候传参即可;如果我们不写构造函数,还想要修改成员变量初始值(缺省值也不是自己想要的),唯一途径就是在对象外访问成员变量并进行修改,如果该成员变量是私有的,那么该成员变量就真的就改不了了。所以即使内置类型成员有缺省值,只要不合乎我们的意愿,我们该写构造函数还是要写的,这就是按需求进行操作。
//对于TreeNode类,即使_val内置变量给了缺省值,只要不合乎我们的需求,我们都写写构造函数 struct TreeNode { TreeNode* _left; TreeNode* _right; int _val; TreeNode(int val = 0) { _left = nullptr; _right = nullptr; _val = val; } }; //对于Tree类中的内置变量root,因为它的初始值只需要置空就可,所以我们只用给缺省值,不用写构造函数 //完全是按需操作 class Tree { private: TreeNode* _root = nullptr; };3. 如果全部都是自定义类型的构造,且这些类型都定义了默认构造,如果带参的情况后面讲,那么可以放手给编译器,让编译器自动进行处理,不用自己写构造函数。
前面通过构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,对象在销毁时会自动调用析构函数,完成对象中的一些资源清理工作。局部对象销毁工作是由编译器完成的。
析构函数,它释放的是动态申请的资源空间(一般都是堆上的空间,栈上的空间出了作用域自动收回),不是变量。构造函数也只是初始化操作,类的实例化才在栈上开辟空间,出了作用域栈上空间自动被收回,堆上的空间需要手动释放,这就是析构函数要做的事情。
析构函数是特殊的成员函数,其特征如下:
s1. 析构函数名是在类名前加上字符 ~。
s2. 无参数无返回值,所以不可重载,因为销毁这一操作肯定不传参数。
s3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
s4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
- typedef int DataType;
- class Stack
- {
- public:
- Stack(int capacity = 4)
- {
- cout << "Stack(int capacity = 4)" << endl;
- _array = (DataType*)malloc(sizeof(DataType) * capacity);
- if (NULL == _array)
- {
- perror("malloc申请空间失败!!!");
- return;
- }
-
- _capacity = capacity;
- _size = 0;
- }
-
- /*void Init()
- {
- _array = (DataType*)malloc(sizeof(DataType) * 4);
- if (NULL == _array)
- {
- perror("malloc申请空间失败!!!");
- return;
- }
- _capacity = 4;
- _size = 0;
- }*/
-
- void Push(DataType data)
- {
- CheckCapacity();
- _array[_size] = data;
- _size++;
- }
-
- void Pop()
- {
- if (Empty())
- return;
- _size--;
- }
-
- DataType Top() { return _array[_size - 1]; }
- int Empty() { return 0 == _size; }
- int Size() { return _size; }
-
- //void Destroy()
- //{
- // if (_array)
- // {
- // free(_array);
- // _array = NULL;
- // _capacity = 0;
- // _size = 0;
- // }
- //}
-
- ~Stack()
- {
- cout << "~Stack()" << endl;
- if (_array)
- {
- free(_array);
- _array = NULL;
- _capacity = 0;
- _size = 0;
- }
- }
-
- private:
- void CheckCapacity()
- {
- if (_size == _capacity)
- {
- int newcapacity = _capacity * 2;
- DataType* temp = (DataType*)realloc(_array, newcapacity * sizeof(DataType));
- if (temp == NULL)
- {
- perror("realloc申请空间失败!!!");
- return;
- }
- _array = temp;
- _capacity = newcapacity;
- }
- }
- private:
- DataType* _array;
- int _capacity;
- int _size;
- };
s5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对会自定类型成员调用它的析构函数。
对内置类型不做处理,对自定义类型去调用他们的默认析构。对于静态的数组,不需要释放,因为它的栈上,出了作用域自动释放,只有malloc、calloc的堆上的,需要手动释放。
- class String
- {
- public:
- String(const char* str = "jack")
- {
- _str = (char*)malloc(strlen(str) + 1);
- strcpy(_str, str);
- }
- ~String()
- {
- cout << "~String()" << endl;
- free(_str);
- }
- private:
- char* _str;
- };
- class Person
- {
- private:
- String _name;
- int _age;
- };
- int main()
- {
- Person p;
- return 0;
- }
总结:
- 一般情况下,有动态申请资源就需要显示写析构函数释放资源(例如栈)
- 如果没有动态申请的资源,就不需要写析构(例如日期类,它就没有动态开辟空间)
- 需要释放资源的成员都是自定义类型,就不需要写析构
在创建对象时,可否创建一个与某一个已存在的对象一模一样的新对象呢?
拷贝构造:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式
2. 拷贝构造函数的参数只有一个且必须使用引用传参,若使用传值的方式,会引发无穷递归调用
- 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;
- }
-
- private:
- int _year;
- int _month;
- int _day;
- };
-
- int main()
- {
- Date d1;
- Date d2(d1);
- return 0;
- }
C++规定:
- 对于内置类型直接拷贝
- 对于自定义类型,如果要拷贝自定义类型,就必须要调用其拷贝构造函数去完成
小知识:
- 在类里变量的使用不受限定符的限制,
- 在下面举例的拷贝构造里,_year 和成员变量_year不一样,拷贝构造里的_year是this->_year,成员变量的_year是图纸_year
- 任何类型的指针都是内置类型
- class Date
- {
- public:
- Date(int year = 1900, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = day;
- }
-
- //d2(d1)
- Date(Date& d)
- {
- cout << "Date(Date& d)" << endl;
- _year = d._year;
- _month = d._month;
- _day = d._day;
-
- //_year 和成员变量_year不一样,第一个是this->_year,第二个是图纸的_year
- }
- private:
- int _year;
- int _month;
- int _day;
- };
-
-
- void func(Date d)
- {
-
- }
-
- void func(int i)
- {
-
- }
-
-
- int main()
- {
- //Stack st1;
- //Date d1;
- //MyQueue q;
-
- Date d1(2023, 4, 25);
- //Date d2(d1);
-
- func(d1);
- func(10);
-
- return 0;
- }
对于func(10)来说,由于10是内置类型int,所以直接按字节数拷贝。
对于func(d1)来说,由于d1是自定义类型,实参传递给形参是一个拷贝过程,所以这里就需要调拷贝构造函数,所以在进入func之前,会先进入拷贝构造函数。
解决方案:
- 指针,形参用指针接收,那么实参要传类的地址,是一个内置类型,所以直接拷贝即可,但是形式上没用引用方便。
- 引用,实参传自定义类型,形参用引用接收,形参是实参的一个别名,不发生拷贝过程。另外,在拷贝构造中如果不注意,很可能d2和d1赋值关系写反,从而导致将随机值赋值给有值的变量,所以使用const修饰是不错的选择 Date(const Dte& d),如果需要改值,那就不要加const,按需操作。我们并没用将引用的权限放大。
- 注意:Date d2(d1)是d1赋值给d2
如果是自定义类型的引用调用,那么就不需要调用拷贝构造,图中这里d就是d1的别名,d2就是this
系统生成的默认拷贝构造对两种类型都会处理 :
- 内置类型成员完成值拷贝(按字节拷贝 ,类似memcpy),浅拷贝
- 自定义类型调用其类的拷贝构造,深拷贝
拷贝构造是为自定义类型深拷贝而生的,浅拷贝基本就不需要这个函数
系统生成的拷贝构造函数并不适合所有类型,对于开辟的数组进行值拷贝,即浅拷贝,会使两个指针同时指向一个数组,这会引发什么问题呢?问题就是在析构时该空间析构了两次从而导致程序错误,另外,在一个数组内修改值会影响另一个数组。
st2先析构,然后st1再次析构一次
所以,这里只能用深拷贝,对st2的_a开一个自己的空间,并将st1的_a里的的数据拷贝到st2的_a里。
// Stack st2(st1) Stack(const Stack& st) { _a = (int*)malloc(sizeof(int) * st._capacity); if (nullptr == _a) { perror("malloc申请空间失败"); return; } memcpy(_a, st._a, sizeof(int) * st._top); _top = st._top; _capacity = st._capacity; }最终为:
#include<iostream> #include<stdlib.h> using namespace std; class Stack { public: Stack(int capacity = 4) { cout << "Stack()" << endl; _a = (int*)malloc(sizeof(int) * capacity); if (nullptr == _a) { perror("malloc申请空间失败"); return; } _capacity = capacity; _top = 0; } // st2(st1) Stack(const Stack& st) { _a = (int*)malloc(sizeof(int) * st._capacity); if (nullptr == _a) { perror("malloc申请空间失败"); return; } memcpy(_a, st._a, sizeof(int) * st._top); _top = st._top; _capacity = st._capacity; } ~Stack() { cout << "~Stack()" << endl; free(_a); _a = nullptr; _capacity = _top = 0; } private: int* _a = nullptr; int _top = 0; int _capacity; }; int main() { // 必须自己实现,实现深拷贝 Stack st1; Stack st2(st1); return 0; }解释:st1先调用构造函数,st2再调用拷贝构造函数而不会去调用构造函数,然后st2再调用析构函数销毁st2._a,最后st1再调用析构函数销毁st1._a
浅拷贝问题:
1、析构两次,报错
2、在一个数组内修改值会影响另一个数组
- Stack func()
- {
- Stack st;
- return st;
- }
-
- // 错误的
- //Stack& func()
- //{
- // Stack st;
- // return st;
- //}
-
- int main()
- {
- Date d1;
- func(d1);
-
- Stack st1;
- func(st1);
-
- func();
-
- Stack ret = func();
-
- return 0;
- }
如果func的返回值是引用,那么会出现一个问题,return后st会自动调用析构函数,_a置空,这时候就和之前在引用那里讲的 “有可能正确” 不一样了,因为这里有析构函数,直接释放了st的_a,即使栈帧没有销毁数据还有保留,_a也访问不到了,这时候返回引用啥用也没,反而会使新的对象的_a成为野指针,所以我们不能返回局部对象的引用,只能返回值,返回值就会有临时拷贝。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator 后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@,C++内并没有@该符号
- 重载操作符函数参数列表内必须有一个类类型(自定义类型)或者枚举类型的操作数
//例如该形参就不可以 bool operator<(const int& x1, const int& x2) { }
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员的重载函数时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为默认的形参this。
- .* :: sizeof ? : 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
- 操作符有几个操作数,重载函数就有几个参数,又因为形参列表里第一个参数默认为形参this指针,所以形参列表显示参数个数的比操作数少一个参数,例如 < 的操作数是2,那么形参显示的只有一个参数 bool operator(const Date&x)
- 操作符重载也可进行函数重载
举例: 如何比较一个日期类的大小?
- 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 Less(const Date& x1, const Date& x2)
- {
- if (x1._year < x2._year)
- {
- return true;
- }
- else if (x1._year == x2._year && x1._month < x2._month)
- {
- return true;
- }
- else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
- {
- return true;
- }
-
- return false;
- }
-
- int main()
- {
- Date d1(2025, 4, 25);
- Date d2(2023, 5, 25);
- cout << Less(d1, d2) << endl;
-
- return 0;
- }
对于内置类型编译器知道该怎么比较大小
但对于自定义类型编译器不知道如何比较大小,所以我们引入关键字operator
- bool Less(const Date& x1, const Date& x2)
- {
- if (x1._year < x2._year)
- {
- return true;
- }
- else if (x1._year == x2._year && x1._month < x2._month)
- {
- return true;
- }
- else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
- {
- return true;
- }
-
- return false;
- }
-
- bool operator<(const Date& x1, const Date& x2)
- {
- if (x1._year < x2._year)
- {
- return true;
- }
- else if (x1._year == x2._year && x1._month < x2._month)
- {
- return true;
- }
- else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
- {
- return true;
- }
-
- return false;
- }
-
- bool operator>(const Date& x1, const Date& x2)
- {
- if (x1._year > x2._year)
- {
- return true;
- }
- else if (x1._year == x2._year && x1._month > x2._month)
- {
- return true;
- }
- else if (x1._year == x2._year && x1._month == x2._month && x1._day > x2._day)
- {
- return true;
- }
-
- return false;
- }
-
- int main()
- {
- Date d1(2025, 4, 25);
- Date d2(2023, 5, 25);
-
- /*cout << Func1(d1, d2) << endl;
- cout << Func2(d1, d2) << endl;*/
-
- // 为什么内置类型可以直接比较,自定义类型不可以直接比较
- cout << (d1 < d2) << endl;
- cout << (operator<(d1, d2)) << endl;
- //两行代码等价,一般不写operator,因为编译器会将第一行自动转换为第二行
-
- cout << (d1 > d2) << endl;
- cout << (operator>(d1, d2)) << endl;
-
- return 0;
- }
d1 < d2 编译器会自动转换为 operator<(d1, d2),两者指令相同 。
运算符是否要重载要看是否有实际意义。例如日期相减是有意义的,但是日期相加就没有实际意义了。
在上述的代码中,operator< 是全局的函数,操作符有几个操作数参数列表就有几个参数 ,d1 < d2 编译器会自动转换为operator<(d1, d2),但是如果变量都是私有或保护变量,那么就无法在外部访问并使用成员变量,所以我们将operator放到类中,成为成员函数。又成员函数的operator内有隐含的this指针,所以操作符有几个操作数参数列表就有几个参数-1的参数。
这时我们调用d1<d2,编译器转换的就不是operator<(d1, d2),而是d1.operator<(d2)。
即有全局使用全局,无全局使用成员函数。
- class Date
- {
- public:
- Date(int year = 1900, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = day;
- }
-
- bool operator<(const Date& x)
- {
- if (_year < x._year)
- {
- return true;
- }
- else if (_year == x._year && _month < x._month)
- {
- return true;
- }
- else if (_year == x._year && _month == x._month && _day < x._day)
- {
- return true;
- }
-
- return false;
- }
-
- private:
- int _year;
- int _month;
- int _day;
- };
-
-
- int main()
- {
- Date d1(2023, 4, 26);
- Date d2(2023, 6, 21);
-
- //d1 < d2; // 转换成operator<(d1, d2);是在全局的情况
- //operator<(d1, d2);
-
- cout << (d1 < d2) << endl; // 转换成d1.operator<(d2);在成员函数的情况
- cout<<d1.operator<(d2)<<endl;
-
- return 0;
- }
赋值运算符重载格式:
1. 参数类型 const T&,传递引用可以提高传参效率
2. 返回值 T&,返回引用可以提高返回效率,有返回值目的是为了支持连续赋值
3. 检测是否自己给自己赋值
4. 返回*this
我们再来看,在C语言中我们知道变量可以连续赋值,例如 i = j = k = 0; 这是因为赋值的返回值是左式的结果,所以可以连续赋值。
现在我们写一个不是标准的赋值运算符重载进行连续赋值:
- class Date
- {
- public:
- Date(int year = 1900, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = day;
- }
-
- void operator=(const Date& x)
- {
- _year = x._year;
- _month = x._month;
- _day = x._day;
- }
-
- private:
- int _year;
- int _month;
- int _day;
- };
-
- int main()
- {
- Date d1(2023, 4, 26);
- Date d2(2023, 6, 21);
-
- Date d3, d4;
- d3 = d4 = d1;
-
- return 0;
- }
程序错误,因为operator=的返回值是void,所以不能连续赋值,问题解决的方案就是返回Date值,将operator=的返回值改为Date,返回被赋值的Date,又因为被赋值的Date被this指针指向,所以要返回Date就要返回解引用的this。
- Date operator=(const Date& x)
- {
- _year = x._year;
- _month = x._month;
- _day = x._day;
-
- return *this;
- }
连续赋值的问题解决了,但是又来了一个问题,返回Date的代价有点大,因为它不是返回的*this,而是拷贝后返回,如果Date的空间很大,那么拷贝Date会使程序的效率降低,代价有点大。我们加一个拷贝构造函数再来看
- class Date
- {
- public:
- Date(int year = 1900, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = day;
- }
-
- Date operator=(const Date& x)
- {
- _year = x._year;
- _month = x._month;
- _day = x._day;
-
- return *this;
- }
-
- Date(const Date& d)
- {
- cout << "Date(const Date& d)" << endl;
- _year = d._year;
- _month = d._month;
- _day = d._day;
- }
-
- private:
- int _year;
- int _month;
- int _day;
- };
-
-
- int main()
- {
- Date d1(2023, 4, 26);
- Date d2(2023, 6, 21);
-
- d1 = d2;
-
- Date d3, d4;
- d3 = d4 = d1;
-
- return 0;
- }
可以看出程序调用了三次拷贝构造,这是因为赋值操作符重载函数返回的是*this值,是自定义类型,我们之前在讲解引用时讲过,函数如果返回的不是引用,就会创建临时变量来保存返回值,这就会发生一次拷贝,所以这里调用了三次拷贝构造。
这时我们再来思考operator=函数,对于在全局和静态的变量,返回时空间不被销毁,这时候可以用引用返回,不会造成风险。这里,我们返回*this,虽然this出了operator=作用域会自动销毁,但是*this指的是对象,生命周期不在operator=内,我们使用引用返回,返回*this的别名,既不会产生引用造成的风险,又不会产生因拷贝而造成的效率问题。
- class Date
- {
- public:
- Date(int year = 1900, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = day;
- }
-
- Date& operator=(const Date& d)
- {
- _year = d._year;
- _month = d._month;
- _day = d._day;
-
- return *this;
- }
-
- Date(const Date& d)
- {
- cout << "Date(const Date& d)" << endl;
- _year = d._year;
- _month = d._month;
- _day = d._day;
- }
-
- private:
- int _year;
- int _month;
- int _day;
- };
-
-
- int main()
- {
- Date d1(2023, 4, 26);
- Date d2(2023, 6, 21);
-
- d1 = d2;
-
- Date d3, d4;
- d3 = d4 = d1;
-
- return 0;
- }
对于d1 = d1;自己给自己赋值,没有意义,所以我们可以在operator=内检查,我们在这里比较地址就可以高效比较,&d是取d的地址。
- Date& operator=(const Date& d)
- {
- if(this != &d)
- {
- _year = d._year;
- _month = d._month;
- _day = d._day;
- }
-
- return *this;
- }
5. 一个类如果没有显式定义赋值运算符重载,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。 内置类型成员变量直接赋值,自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
默认生成赋值重载函数与拷贝构造函数行为一样:
1、内置类型成员——值拷贝(浅拷贝)
2、自定义类型成员去调用它的赋值重载
例如Date可以不写、MyQueue自动调用它的赋值重载
6. 赋值运算符只能重载为类的成员函数,不能重载为全局函数。
因为赋值运算符重载函数是默认成员函数,如果把它写到全局里,那么编译器又会在类内默认生成一个,会与全局的运算符重载函数起冲突。
默认的成员函数都是同样道理,不能写在全局。运算符重载可以写在全局,因为它是自己实现的,但是可能会受成员函数是否公有的限制,一般是写在类里的。而赋值运算符重载就不能写在全局,因为它是默认成员函数。
为了标准些,我们将声明和定义分离
在这里我们实现运算符重载时,可以先实现 operator< 和 operator== 这两个函数,因为其他的>、>=、!=、<=都可以从这两种函数转换出来,提高复用性。我们可以将该方法推广到大多数类的运算符重载书写顺序。
类内部写声明,定义在cpp文件。注意:定义时要使用类名和作用域限定符,以澄清该函数是类内部的成员函数的定义。
由于Date类内部的成员变量都是内置类型,所以析构函数(变量在栈帧内不需要手动销毁)、拷贝构造函数(内置类型逐字节拷贝)、赋值操作符重载函数(内置类型逐字拷贝)都可以不用写,让编译器自动生成
- #pragma once
- #include<iostream>
- #include<assert.h>
- using namespace std;
-
- class Date
- {
- public:
- //友元函数声明
- friend ostream& operator<<(ostream& out, const Date& d);
- friend istream& operator>>(istream& in, Date& d);
-
-
- //构造函数
- Date(int year = 1, int month = 1, int day = 1);
-
- //拷贝构造不需要写,因为内置类型逐字节拷贝
-
- //赋值运算符重载不需要写,因为内置类型逐字节拷贝
-
- //析构函数不需要写,因为内置类型空间自动释放
-
- void Print()
- {
- cout << _year << "-" << _month << "-" << _day << endl;
- }
-
- //复用
- bool operator==(const Date& x)const;
- bool operator<(const Date& x) const;
- bool operator<=(const Date& x)const;
- bool operator>(const Date& x)const;
- bool operator>=(const Date& x)const;
- bool operator!=(const Date& x)const;
-
- static int GetMonthDay(int year, int month);
- //类 + 多少天
- Date& operator+=(int day);
- Date operator+(int day)const;
-
- Date& operator-=(int day);
- Date operator-(int day) const;
- //两个类相减
- int operator-(const Date& x) const;
-
- //前置++
- Date& operator++();
- //后置++,为了与前置++做区分,进行重载,所以增加了int这个参数,它仅仅是为了占位
- //d1.operator++(0),相当于牺牲了后置++的方便性
- Date operator++(int);
-
- Date& operator--();
- Date operator--(int);
-
-
- private:
- int _year;
- int _month;
- int _day;
- };
-
- ostream& operator<<(ostream& out, const Date& d);
- istream& operator>>(istream& in, Date& d);
- #define _CRT_SECURE_NO_WARNINGS 1
- #include"Date.h"
-
- //类外定义,类内声明
- //缺省值一般在声明处给
- Date::Date(int year, int month, int day)
- {
- if (month > 0 && month < 13 && _day > 0 && _day <= GetMonthDay(_year, _month))
- {
- _year = year;
- _month = month;
- _day = day;
- }
- else
- {
- _year = year;
- _month = month;
- _day = day;
- }
- }
-
- bool Date::operator==(const Date& x)const
- {
- return _year == x._year && _month == x._month && _day == x._day;
- }
-
- bool Date::operator<(const Date& x)const
- {
- if (_year < x._year)
- {
- return true;
- }
- else if (_year == x._year && _month < x._month)
- {
- return true;
- }
- else if (_year == x._year && _month == x._month && _day < x._day)
- {
- return true;
- }
-
- return false;
-
- }
-
- bool Date::operator<=(const Date& x)const
- {
- return *this < x || *this == x;
- }
-
- bool Date::operator>(const Date& x)const
- {
- return !(*this <= x);
- }
-
- bool Date::operator>=(const Date& x)const
- {
- return !(*this < x);
- }
-
- bool Date::operator!=(const Date& x)const
- {
- return !(*this == x);
- }
-
- int Date::GetMonthDay(int year, int month)
- {
- //如果不用static,那么每次都会创建一次数组,效率不高
- static int daysArr[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
-
- //先判断是不是特殊的2月,再判断是不是闰年,效率会比先判断闰年要高
- if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
- {
- return 29;
- }
- else
- return daysArr[month];
- }
-
- Date& Date::operator+=(int day)
- {
- if (day < 0)
- {
- return *this -= -day;
- }
- _day += day;
- while (_day > GetMonthDay(_year, _month))
- {
- _day -= GetMonthDay(_year, _month);
- ++_month;
- if (_month == 13)
- {
- ++_year;
- _month = 1;
- }
- }
-
- /** this = *this + day;*/
- return *this;
- }
-
- //这里不用引用返回,tmp出函数就销毁
- Date Date::operator+(int day)const
- {
- Date tmp(*this);
-
- tmp += day;
- /*tmp._day = day;
- while (tmp._day > GetMonthDay(tmp._year, _month))
- {
- tmp._day -= GetMonthDay(tmp._year, tmp._month);
- ++tmp._month;
- if (tmp._month == 13)
- {
- ++tmp._year;
- tmp._month = 1;
- }
- }*/
- return tmp;
- }
-
- Date& Date::operator-=(int day)
- {
- if (day < 0)
- {
- return *this += -day;
- }
- _day -= day;
- while (_day <= 0)
- {
- --_month;
- if (_month == 0)
- {
- _month = 12;
- --_year;
- }
-
- _day += GetMonthDay(_year, _month);
- }
-
- return *this;
- }
-
- Date Date::operator-(int day) const
- {
- Date tmp = *this;
- tmp -= day;
-
- return tmp;
- }
-
- int Date::operator-(const Date& d)const
- {
- Date max = *this;
- Date min = d;
- int flag = 1;
- if (*this < d)
- {
- max = d;
- min = *this;
- flag = -1;
- }
-
- int res = 0;
- while (min != max)
- {
- ++min;
- ++res;
- }
-
- return res;
- }
-
- //前置++
- Date& Date::operator++()
- {
- *this += 1;
- return *this;
- }
-
- //后置++
- Date Date::operator++(int)
- {
- Date tmp = *this;
- *this += 1;
-
- return tmp;
- }
-
- //前置--
- Date& Date::operator--()
- {
- *this -= 1;
- return *this;
- }
-
- //后置--
- Date Date::operator--(int)
- {
- Date tmp = *this;
- *this -= 1;
-
- return tmp;
- }
-
- // 流插入,全局函数
- // 因为如果是成员函数,第一个参数默认是类,而不是cout,所以顺序必须是 d1 << cout才能输出,即为d1.operator<<(out)
- // 为了改变,我们只能使用全局函数来改变参数顺序
- // void operator<<(ostream& out, const Date& d)
- // 但是又因为成员变量被private修饰,为了访问成员变量可以在类内写成员函数获得成员变量,但最方便的是友元函数
- //out不加const是因为cout是要改变out的值,加了const就会报错
-
- ostream& operator<<(ostream& out, const Date& d)
- {
- out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
- return out;
- }
-
- istream& operator>>(istream& in, Date& d)
- {
- int year, month, day;
- 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;
- }
operator+
注意:相加改变了原Date,我们实际实现的是功能是 += ,所以上面的operator+应改为operator+=,此外+=有返回值,返回值是引用,目的是为了实现连续+=,那么我们怎么实现operator+函数呢?
- Date Date::operator+(int 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;
-
- }
由于operator+中与operator+=代码重复,所以在operator+中使用operator+=很方便。
也可以先定义operaotr+,再利用operator+来定义operator+=
注意:
- 用一个已经存在的对象,初始化另一个对象——拷贝构造函数
Date d2(2023, 4, 26); Date d4 = d2;//等价于Date d4(d2);
- 已经存在的两个对象之间赋值拷贝 —— 赋值运算符重载函数
operator++
该函数的关键在于如何区分前置++、后置++,因为++操作数为1,形参的参数列表无参,根据函数重载的性质,我们只能通过参数的不同使编译器来区分前置还是后置,采取的不同是将其中一个函数的参数列表增加一个int参数,目的不是为了接收具体的值,它起到的作用仅仅是占位,又因为前置++使用频率高,所以我们将该措施实施在后置++中。
从前置和后置++的代码中,我们可以看到自定义类型前置++的效率更高,因为不创建临时变量,返回值是引用,所以在日常使用中我们常用自定义类型的前置++。
- // 前置++
- Date& Date::operator++()
- {
- *this += 1;
- return *this;
- }
-
- // 后置++
- // 增加这个int参数不是为了接收具体的值,仅仅是占位,跟前置++构成重载
- Date Date::operator++(int)
- {
- Date tmp = *this;
- *this += 1;
-
- return tmp;
- }
operator+=
operator-=
- Date& Date::operator+=(int day)
- {
- if (day < 0)
- {
- return *this -= -day;
- }
- _day += day;
- while (_day > GetMonthDay(_year, _month))
- {
- _day -= GetMonthDay(_year, _month);
- ++_month;
- if (_month == 13)
- {
- ++_year;
- _month = 1;
- }
- }
-
- /** this = *this + day;*/
- return *this;
- }
-
- Date& Date::operator-=(int day)
- {
- if (day < 0)
- {
- return *this += -day;
- }
- _day -= day;
- while (_day <= 0)
- {
- --_month;
- if (_month == 0)
- {
- _month = 12;
- --_year;
- }
-
- _day += GetMonthDay(_year, _month);
- }
-
- return *this;
- }
在 - = 运算符重载中,如果日期类 - = 负数天,会出现错误,这时应该 +=(-day)
同理,在 += 运算符重载中,如果日期类+=负数天也会出现错误,这时应该 - =(-day)
在两函数中加上if判断day是否为负数即可。
operator-
重载 operator- ,实现两个日期类的相减,直接让小的日期类++,直到两个日期类相等,记录++次数
- int Date::operator-(const Date& d)
- {
- Date max = *this;
- Date min = d;
- int flag = 1;
- if (*this < d)
- {
- max = d;
- min = *this;
- flag = -1;
- }
-
- int res = 0;
- while (min != max)
- {
- ++min;
- ++res;
- }
-
- return res;
- }
operator<< 流插入
由于每次都要调用Print()函数,并不是很方便,所以我们可以重载流插入运算符<< ,直接cout日期类
流插入--全局函数+友元函数
- 因为如果是成员函数,第一个参数默认是类,而不是cout,所以顺序必须是 d1 << cout才能输出,即为d1.operator<<(out),为了改变顺序,我们只能使用全局函数来改变参数顺序
- 但是又因为成员变量被private修饰,为了访问成员变量可以在类内写成员函数获得成员变量,但最方便的是友元函数
- out不加const是因为cout是要改变out的值,加了const就会报错
- 为了实现连续输出,我们将返回值设置为ostream,这样每次输出后返回值都是cout,将继续输出。
- //类内的友元函数声明
- friend ostream& operator<<(ostream& out, const Date& d);
-
-
- ostream& operator<<(ostream& out, const Date& d)
- {
- out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
- return out;
- }
operator>> 流提取
cin >> d;
直接使用cin输入类的信息属性,并且判断是否合法
- //类内的友元函数声明
- friend istream& operator>>(istream& in, Date& d);
-
- istream& operator>>(istream& in, Date& d)
- {
- int year, month, day;
- 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;
- }
如果一个对象被const修饰,那么当它调用成员函数时,由于this指针没有被const修饰,所以传参时const权限放大,出现错误。
只有指针和引用才涉及权限的放大、缩小、平移问题
那么怎么样才能让隐含的this被const修饰呢?祖师爷找到了一个位置:在成员函数后加上const,来修饰this,这样就能防止权限的放大,既能使被const修饰的对象调用成员函数,又能保证没有被const修饰的对象调用成员函数。
那么能不能所有成员函数都加上const呢?
不能!例如,要修改对象成员变量的函数后面就不能加上const
所以,只要成员函数内部不修改成员变量,都应该加上const,这样const对象和普通对象都可以调用成员函数
类和对象的概念多,关键点多,一时之间不清楚是很正常的,多看多练习可以帮助我们逐渐理解和掌握。
最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。