目录
前言
一、类的6个默认成员函数
二、构造函数
2.1 概念
2.2 特性
三、析构函数
1. 概念
2. 特性
四、拷贝构造函数
1. 概念
2. 特征
五、赋值运算符重载
1. 运算符重载
2. 赋值运算符重载
六、实现一个完整的日期类
Date.h
Date.cpp
总结
前言
上一节,我们学习了类和对象的上部分内容,难度一般,这一节我们要学习类和对象重点部分,理解难度大,望大家认真学习,真正进入C++的大门。
[本节目标]
1. 类的6个默认成员函数
2. 构造函数
3. 析构函数
4. 拷贝构造函数
5. 赋值操作符重载
6. 默认拷贝构造与赋值运算符重载的问题
7. const成员函数
8. 取地址及const取地址操作符重载
一、类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。那么空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。
我们在写栈这一结构时,对栈的初始化和销毁经常忘记,初始化忘记会导致程序直接错误,忘记销毁会导致内存泄露,虽然刷题时网站不会提醒你内存泄露,但是如果在一个服务器上持续不断的跑,时间一长就会崩溃,这是极其严重的错误,所以,“祖师爷”做出了构造函数和析构函数,分别完成初始化工作和清理工作,替代了人工操作。
默认成员函数是:我们不写,那么编译器会自动生成
二、构造函数
2.1 概念
构造函数是一个特殊的成员函数,名字与类名相同, 创建类类型对象时由编译器自动调用,没有返回值,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。
2.2 特性
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。例如,栈的定义需要初始化函数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();吗?
- 我们不能写为Date d1()的形式,因为该形式会与函数声明的形式冲突,d1 是函数名,形参列表无参,返回值是Date,这样编译器就区分不开了。若我们不想对构造函数传参,就直接实例化即可,不能带括号,若要对构造函数传参,那么这时才需要括号进行传参。
- 如果我们改用d1.Date(2023, 11, 11)的形式,那么如果Date名字改为Init,我们是不是很熟悉呢?d1.Init(2023, 11, 11) 这不正是在没有构造函数之前,我们每次都要初始化的操作吗?这不就回到起点了吗?我们的初衷不就是免去我们每次实例化对象时的初始化操作吗?,因为我们可能一时疏忽而忘记初始化操作。所以,如果这样写,我们还要构造函数干什么呢?这就是为什么构造函数的调用形式跟我们的惯性思维冲突。
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. 如果全部都是自定义类型的构造,且这些类型都定义了默认构造,如果带参的情况后面讲,那么可以放手给编译器,让编译器自动进行处理,不用自己写构造函数。
三、析构函数
前面通过构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
1. 概念
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,对象在销毁时会自动调用析构函数,完成对象中的一些资源清理工作。局部对象销毁工作是由编译器完成的。
析构函数,它释放的是动态申请的资源空间(一般都是堆上的空间,栈上的空间出了作用域自动收回),不是变量。构造函数也只是初始化操作,类的实例化才在栈上开辟空间,出了作用域栈上空间自动被收回,堆上的空间需要手动释放,这就是析构函数要做的事情。
2. 特性
析构函数是特殊的成员函数,其特征如下:
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;
}
总结:
- 一般情况下,有动态申请资源就需要显示写析构函数释放资源(例如栈)
- 如果没有动态申请的资源,就不需要写析构(例如日期类,它就没有动态开辟空间)
- 需要释放资源的成员都是自定义类型,就不需要写析构
四、拷贝构造函数
1. 概念
在创建对象时,可否创建一个与某一个已存在的对象一模一样的新对象呢?
拷贝构造:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
2. 特征
拷贝构造函数也是特殊的成员函数,其特征如下:
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成为野指针,所以我们不能返回局部对象的引用,只能返回值,返回值就会有临时拷贝。
五、赋值运算符重载
1. 运算符重载
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;
}
2. 赋值运算符重载
赋值运算符重载格式:
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. 赋值运算符只能重载为类的成员函数,不能重载为全局函数。
因为赋值运算符重载函数是默认成员函数,如果把它写到全局里,那么编译器又会在类内默认生成一个,会与全局的运算符重载函数起冲突。
默认的成员函数都是同样道理,不能写在全局。运算符重载可以写在全局,因为它是自己实现的,但是可能会受成员函数是否公有的限制,一般是写在类里的。而赋值运算符重载就不能写在全局,因为它是默认成员函数。
六、实现一个完整的日期类
为了标准些,我们将声明和定义分离
Date.h
在这里我们实现运算符重载时,可以先实现 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);
Date.cpp
#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;}elsereturn 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+
- 返回日期类,日期相加具有实际意义,这儿又要考虑闰年的情况,所以写一个子函数GetMonthDay 来判断每个月的时长
- 在GetMonthDay函数定义一个静态数组储存每个月的时间,因为该函数会被频繁调用,使用静态数组会提高效率,当然,也可以使用内联函数。
- GetMonthDay中如果为闰年且月份为2,那么返回29,注意将月份是否为2放最前判断,由短路功能,可以提高效率,不需要每次判断是否为闰年。
- 由于GetMonthDay的返回值为int,所以不需要引用返回,并且返回29时不能使用引用,当然也可以用const int &,但是就显得画蛇添足了。
注意:相加改变了原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成员
1. const修饰类的成员函数
如果一个对象被const修饰,那么当它调用成员函数时,由于this指针没有被const修饰,所以传参时const权限放大,出现错误。
那么怎么样才能让隐含的this被const修饰呢?祖师爷找到了一个位置:在成员函数后加上const,来修饰this,这样就能防止权限的放大,既能使被const修饰的对象调用成员函数,又能保证没有被const修饰的对象调用成员函数。
那么能不能所有成员函数都加上const呢?
不能!例如,要修改对象成员变量的函数后面就不能加上const
所以,只要成员函数内部不修改成员变量,都应该加上const,这样const对象和普通对象都可以调用成员函数
总结
类和对象的概念多,关键点多,一时之间不清楚是很正常的,多看多练习可以帮助我们逐渐理解和掌握。
最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!