原文再续,书接上回!! 继续类和对象的学习。
目录
构造函数
析构函数
拷贝构造
赋值重载
运算符重载
const成员
取地址及const取地址操作符重载
当我们没有向类中写入任何成员的时候(也就是空类),类中就什么也没有吗?答案是否定的,任何类在什么都不写时,编译器会自动生成6个默认成员函数。
这六个默认成员函数,是用户不显示实现,编译器会自动生成的成员函数。你可以想象它们六兄弟是替你守江山的忠诚卫士,前4个兄弟用户实现的情景还比较多见。老五老六很少需要用户动手实现。
注意:构造函数名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
构造函数主要完成初始化工作,它有如下特性:(代码演示以日期类为例)
●函数名与类名相同,没有返回值。 注意:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成!。 不显示定义构造函数:#include
using namespace std;class Date { public:void Print(){cout << _year << "-" << _month << "-" << _day << endl;} private:int _year;int _month;int _day; };int main() {Date d1;d1.Print();return 0; }
在没有写构造函数的情况下,应该调用默认的构造函数初始化对象。但是从结果来看好像编译器并没有帮我们完成这件事,这里先带着疑问继续探索!
无参构造://无参构造Date(){//...}
注意:通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明 。
Date d1;d1.Print();
效果和不显示定义是一样的!
有参构造://有参构造Date(int year, int month, int day){_year = year;_month = month;_day = day;}
Date d3(2020,10,3);d3.Print();
全缺省构造:
//全缺省构造 Date(int year = 2020, int month = 12, int day = 26){_year = year;_month = month;_day = day;}
Date d4;d4.Print();
Date d2(1,1,1);d2.Print();
半缺省构造:
//半缺省构造Date(int year, int month = 10, int day = 3){_year = year;_month = month;_day = day;}
Date d4(1999);d4.Print();
●构造函数可以重载。
class Date { public:Date(){_year = 1900;_month = 1;_day = 1;}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(2000,2,3);d2.Print();return 0; }
●对象实例化时编译器自动调用对应的构造函数,保证每个数据成员都有 一个合适的初值。
问题:在未显示定义构造的情景下,编译器生成的默认构造好像并没有什么作用。
答:默认生成的构造函数只处理自定义类型,内置类型不做处理。
内置类型:内置类型就是语言提供的数据类 型,如:int/char...
自定义类型:使用class/struct/union等自己定义的类型。#include
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 { private://内置类型int _year;int _month;int _day;//自定义类型Time _t; }; int main() {Date d1;return 0; }
通过调试发现,编译器生成默认的构造函数会对自定类型成员_t调用它的默认成员函数!!内置类型确实没有做任何处理。根据这一现象,内置类型成员变量在类中声明时可以给默认值。
//内置类型int _year = 2001;int _month = 10;int _day = 3;//自定义类型Time _t;
注意:这里给的是缺省值,并不是初始化!
小总结:
♠构造函数在不显示定义的情况下,会自动生成。
♠默认构造函数对内置类型不做处理,自定义成员调用它自己的默认构造函数。为了同时处理内置类型,可以在声明的时候给缺省值。
♠构造函数在对象整个生命周期内只调用一次。
♠注意:不仅仅是编译器自动生成的构造函数才是默认的,无参、全缺省、用户没显示定义编译器自动生成的构造函数,都是默认构造函数!
注意:析构函数的任务不是完成对对象本身的销毁(局部对象销毁工作是由编译器完成的),而是对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数主完成对象中资源的清理,其特征如下: (代码演示以顺序表为例)
●析构函数名是在类名前加上字符 ~,无参数无返回值类型。typedef int DataType; class SeqList { public://全缺省构造SeqList(int capacity = 3){_arr = (DataType*)malloc(sizeof(DataType) * capacity);if (nullptr == _arr){perror("malloc:");exit(-1);}_capacity = capacity;_size = 0;}void PushBack(DataType data){_arr[_size] = data;_size++;}void Print(){for (int i = 0; i < _size; i++){cout << _arr[i] << " ";}}//析构~SeqList(){if (_arr){free(_arr);_arr = NULL;_capacity = 0;_size = 0;}} private:DataType* _arr;int _capacity;int _size; }; int main() {SeqList s;s.PushBack(1);s.PushBack(2);s.PushBack(3);s.Print();return 0; }
默认构造完成初始化工作!
向顺序表中插入数据!
打印顺序表中的数据!
●对象生命周期结束时,C++编译系统系统自动调用析构函数!
●一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。 还是上面的代码,将显示定义的析构函数删掉,当对象生命周期结束时,自动调用析构函数,但是并没有完成资源清理的任务!
原因:内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可。对象销毁时,要保证每个自定义对象都可以正确销毁,创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数。
class Time{public:~Time(){cout << "~Time()析构函数调用" << endl;_hour = 0;_minute = 0;_second = 0;}private:int _hour = 1;int _minute = 1;int _second = 1;};class Date{private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;};int main(){Date d;return 0;}
注意:析构函数不能重载! 总结:如果类中没有申请资源,可以不写析构函数,使用编译器生成的默认析构函数就可以,就好比日期类。当类中有资源申请时,就要考虑资源泄露的问题,一定要写析构函数,比如顺序表类,栈类等。
拷贝构造函数只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时,编译器自动调用。
拷贝构造函数主要完成的工作是,创建对象时,创建一个与已存在对象一某一样的新对象。
先从语法来看,拷贝构造函数的参数只有一个,是类类型对象的引用。拷贝构造函数是构造函数的一个重载形式。(代码演示以日期类为例)
class Date { public:Date(int year = 2000, int month = 10, int day = 3){_year = year;_month = month;_day = day;}Date(const Date& d) {_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "/" << _month << "/" << _day<
拷贝构造函数的函数名也和类名相同,不同点是参数类型不同,所以说和构造函数是函数重 载。
这里肯定有小伙伴存有疑问,拷贝构造函数的参数我一定要传本类类型的引用吗,我想用传值方式试试!!
Date(const Date d) {_year = d._year;_month = d._month;_day = d._day;}
报错的原因是什么呢?
答:参数传值,形参是实参的一份拷贝,所以传值会引发对象的拷贝,倒置无穷递归。
class Date { public://构造函数Date(int year = 2000, int month = 10, int day = 3){_year = year;_month = month;_day = day;}//拷贝构造Date(const Date& d) {cout << "传值,形参拷贝实参,调用拷贝构造Date"<
传引用:
Fun2(d3);
传引用,形参是实参的别名。没有引发对象的拷贝!
上面的程序很好的证明,传值方式会调用拷贝构造,因为形参拷贝实参时用已存在的类类型对象创建新对象。而调用拷贝构造传值还会重复这个动作,就引发了无穷递归!
●不显示定义拷贝构造函数。
注意:编译器默认生成的拷贝构造函数,内置类型是按照字节方式直接拷贝的,自定 义类型是调用它自己拷贝构造函数完成拷贝的。//不显示定义默认拷贝构造 class Time { public:Time(){_hour = 12;_minute = 30;_second = 30;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;} private:int _hour;int _minute;int _second; }; class Date { private:// 基本类型int _year = 2001;int _month = 10;int _day = 3;// 自定义类型Time _t; };int main() {Date d1;Date d2(d1);return 0; }
通过调试发现,默认生成的拷贝构造和构造、析构不同的地方是,它不仅仅拷贝自定义类型,内置类型的数据也进行了拷贝!
妙哉,妙哉!这样说来不用自己动手写拷贝构造,用编译器自动生成的不就可以了?
像日期类这样不设计资源申请的,用编译器自动生成的拷贝是没有问题的。但是如果涉及到资源申请,则拷贝构造就一定要写!比如下面的场景:(代码以栈为例)
typedef int DataType; class Stack { public://构造函数Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc");exit(-1);}_size = 0;_capacity = capacity;}void Push(const DataType& data){_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);Stack s2(s1);return 0; }
内置类型是按照字节方式直接拷贝, 我们通过直观的发现问题似乎也不大,就是s2对象拷贝的空间和对象s1是一块空间。
继续调试,程序崩溃掉了,错误大意是断言失败!
原因是在对象生命周期结束时,C++编译系统系统自动调用析构函数! 先建立的对象后销毁,后建立的对象先销毁!也就是说在析构的过程中,存在同一块地址空间二次释放的问题。上述的拷贝方式称为浅拷贝,面对这种场景,浅拷贝显然是有问题的。需要用户显示定义拷贝构造函数!(针对上述问题,显示定义拷贝构造如下)
//显示定义拷贝构造Stack(const Stack& s){_array = (DataType*)malloc(s._capacity * sizeof(DataType));for (int i = 0; i < s._size; i++){_array[i] = s._array[i];}_size = s._size;_capacity = s._capacity;}
这种拷贝方式叫做深拷贝!
分别向两个对象中插入数据也不会相互影响:
s1.Push(2);s1.Push(3);s1.Push(4);s2.Push(5);s2.Push(6);s1.print();s2.print();
除了上述的代码情景,还有一种场景没有谈到,当函数返回值类型为类类型对象时,也会调用拷贝构造函数。
class Date { public:Date(int year, int minute, int day){cout << "调用构造Date():" << endl;}Date(const Date& d){cout << "调用拷贝构造Date(const Date& d):" << endl;}~Date(){cout <<"调用析构~Date():"<< endl;} private:int _year;int _month;int _day; }; Date Test(Date d) {return d; } int main() {Date d1(2022,12,28);Test(d1);return 0; }
小总结:
●涉及资源的申请,就要写拷贝构造,反之可以不写。
●拷贝构造调用场景如:
♦使用已存在对象创建新对象。 ♦函数参数类型为类类型对象。 ♦函数返回值类型为类类型对象。 ●拷贝构造函数的参数只有一个且必须是类类型对象的引用。 ●编译器默认生成的拷贝构造函数,内置类型按照字节方式拷贝的,自定义类型调用其拷贝构造函数。
赋值重载也是默认成员函数之一,相对于其它函数,函数名比较特殊,函数名字为:关键字operator后面接赋值重载的运算符符号。
语法:返回值类型 operator操作符(参数列表)。 参数类型:const T&,传递引用可以提高传参效率。 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值。#include
using namespace std;class Date { public://构造函数Date(int year = 1900, int month = 1, int day = 1){cout << "构造函数Date()" << endl;_year = year;_month = month;_day = day;}//拷贝构造Date(const Date& d){cout << "拷贝构造Date(const Date& d)" << endl;_year = d._year;_month = d._month;_day = d._day;}//赋值重载Date& operator=(const Date& d){cout << "赋值重载" << endl;if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;} private:int _year;int _month;int _day; }; int main() {Date d1(2001,10,3);Date d2(d1);Date d3;d3 = d2;return 0; }
观察上述代码效果,调用了显示定义的赋值重载,完成了对象间的赋值。那么既然赋值重载也是默认成员函数,不显示定义,编译器会自动生成。
屏蔽掉显示定义的赋值重载函数后,对象间依然完成了赋值:
其实,当用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注 意:内置类型直接赋值,而自定义类型调用对应类的赋值运算符重载完成赋值。
class Time { public: Time() {_hour = 1;_minute = 1;_second = 1; }Time& operator=(const Time& t){cout << "_t赋值重载operator=" << endl;if (this != &t){_hour = t._hour;_minute = t._minute;_second = t._second;}return *this;} private:int _hour;int _minute;int _second; }; class Date { private:// 基本类型int _year = 2001;int _month = 10;int _day = 3;// 自定义类型Time _t; }; int main() {Date d1;Date d2;d1 = d2;return 0; }
对于上述日期类,没有涉及到资源的申请,赋值重载是否实现都可以; 一旦涉及到资源管理则必须要实现。 (代码演示以栈为例)
typedef int DataType; class Stack { public:Stack(int capacity = 10){_arr = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _arr){perror("malloc faile:");exit(-1);}_size = 0;_capacity = capacity;}void Push(const DataType& data){_arr[_size] = data;_size++;}~Stack(){if (_arr){free(_arr);_arr = nullptr;_capacity = 0;_size = 0;}} private:DataType* _arr;int _size;int _capacity; }; int main() {Stack s1;s1.Push(1);s1.Push(2);Stack s2;s2 = s1;return 0; }
出现的错误和拷贝构造资源申请,未显示定义拷贝构造的错误类似。
当s1赋值给s2时,编译器会将s1中内容原封不动的拷贝到s2中,这样会导致两个问题:
♠s2原本的内存空间丢失,造成了内存泄露。
♠s1/s2共享同一块内存空间,最后销毁时导致同一快空间释放了两次。
显示定义:
typedef int DataType; class Stack { public:Stack(int capacity = 10){_arr = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _arr){perror("malloc faile:");exit(-1);}_size = 0;_capacity = capacity;}Stack& operator=(const Stack& s){if (this != &s){_arr = (DataType*)malloc(10 * sizeof(DataType));for (int i = 0; i < s._size; i++){_arr[i] = s._arr[i];}_size = s._size;_capacity = s._capacity;}return *this;}void Push(const DataType& data){_arr[_size] = data;_size++;}~Stack(){if (_arr){free(_arr);_arr = nullptr;_capacity = 0;_size = 0;}} private:DataType* _arr;int _size;int _capacity; }; int main() {Stack s1;s1.Push(1);s1.Push(2);Stack s2;s2 = s1;return 0; }
● 赋值运算符只能重载成类的成员函数不能重载成全局函数 。
用户在类外实现全局的赋值运算符重载,会和编译器在类中生成的默认赋值运算符重载冲突,故赋值运算符重载只能是类的成员函数。
除了赋值运算符之外,像+、-、*、/等这样的运算符一直伴随着我们的学习生活,对于我们来说“见其知其意”,为了增强代码的可读性引入了运算符重载。
运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
以日期类为例,学习运算符重载:
Date.h(预览日期类大概功能)
#pragma once#include
using namespace std;class Date { public://全缺省构造Date(int year = 2020, int month = 12, int day = 28){_year = year;_month = month;_day = day;}//计算某年某月的天数int GetMonthDay(int year, int month){//平年各月的天数int LeapYear[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//闰年2月29天if (month == 2 && ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)){return 29;}return LeapYear[month];}//赋值重载//析构函数//拷贝构造void Print(){cout << _year << "/" << _month << "/" << _day << endl;}//运算符重载bool operator==(const Date& d) ;bool operator> (const Date& d);bool operator>=(const Date& d);bool operator<(const Date& d);bool operator<=(const Date& d);bool operator!=(const Date& d);//日期+=天数Date& operator+=(int day);//日期+天数Date operator+(int day);//日期-天数Date& operator-=(int day);//日期-天数Date operator-(int day);//前后置++Date& operator++();Date operator++(int);//前后置--Date& operator--();Date operator--(int);//日期-日期 返回天数int operator-(Date date); private:int _year;int _month;int _day; }; ♥默认成员函数
日期类的实现不涉及资源的申请,拷贝构造,赋值运算符重载,析构函数使用编译器自动生成的即可。
♥计算某年某月的天数
重点在于区分平年和闰年的2月,闰年的2月有29天,平年的2月有28天。其余的月份是相同的。
int GetMonthDay(int year, int month){//平年各月的天数int LeapYear[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//闰年2月29天if (month == 2 && ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)){return 29;}return LeapYear[month];}
♥运算符重载
语法:返回值类型 operator操作符(参数列表)。形参看起来少一个,其实还有隐藏的this指针。
bool Date::operator==(const Date& d) {return _year == d._year&& _month == d._month&& _day == d._day; } bool Date::operator> (const Date& d) {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 Date::operator>=(const Date& d) {return *this > d || *this == d; } bool Date::operator<(const Date& d) {return !(*this >= d); } bool Date::operator<=(const Date& d) {return *this < d || *this == d; } bool Date::operator!=(const Date& d) {return !(*this == d); }
小技巧:实现了前两个运算符重载后,后面的重载可以复用前面已经实现过的。但是一定要确保已经实现的没有错误!
♥日期+、+=、-、-=天数
这里要注意的环节就是,日期+-过程中的进位、借位问题。生活常识我们都知道,一个日期的天数、月份、年份均不能是负数,是0也不合适,例如:2020年0月0日,-1年-4月-6日,每个月的天数不能超过该年月的天数,其年份的月数也不可能超过12!还有要注意的就是,-和-=,+和+=的区别,-=和+=是改变了自身的数据,-和+是在源数据的基础上通过+、-操作获得一个数据。
//日期+=天数 Date& Date::operator+=(int day) {_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);_month++;if (_month == 13){_month = 1;_year++;}}return *this; } //日期+天数 Date Date::operator+(int day) {Date ret(*this);ret += day;return ret; }//日期-天数 Date& Date::operator-=(int day) {//要减去的天数可能大于月天数_day -= day;while (_day <= 0){_month--;if (_month == 0){_year--;_month = 12;}//_day是负数_day += GetMonthDay(_year, _month);}return *this; } //日期-天数 Date Date::operator-(int day) {Date tmp(*this);tmp -= day;return tmp; }
♥前后置++、--
为了让前置++(--)与后置++(--)形成能正确重载 。C++规定:后置++(--)重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器 自动传递!!!Date& Date::operator++() {*this += 1;return *this; } Date Date::operator++(int) {Date ret(*this);*this += 1;return ret; } // 前置 Date& Date::operator--() {*this -= 1;return *this; }// 后置 Date Date::operator--(int) {Date tmp = *this;*this -= 1;return tmp; }
♥日期-日期
两个日期的相减可以计算出这之间的天数,首先要处理的就是大小日期的问题。当小日期-大日期的时候就说明得出的天数是我们已经度过的过去天数,大日期-小日期就是未来在过多少天才能到达大日期。这里用了一个标记变量来标记是哪一种情况。计算日期的天数,只要小日期还不等于大日期,就计数,最后得到的天数和标记就是我们要得到的综合信息。
//日期-日期 返回天数 int Date::operator-(Date date) {Date max = *this;Date min = date;int flag = 1;int count = 0;if ((*this) < date){max = date;min = *this;int flag = -1;}while (max != min){++min;++count;}return flag * count; }
const修饰的“成员函数”称之为const成员函数,const实际修饰的是该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
例如:
class Date { public:Date(int year, int month, int day) {_year = year;_month = month;_day = day;}void Print() const{cout << "Print()const" << endl;_year = 2021;_month = 10;_day = 3;} private:int _year; int _month; int _day; };
const修饰this指针
void Print() const 等价于 void Print(const Date* this)
♣ const对象可以调用非const成员函数吗? 权限的放大,不可以! ♣非const对象可以调用const成员函数吗? 权限的缩小,可以! ♣ 非const成员函数内可以调用其它的const成员函数
bool Date::operator==(const Date& d) const {return _year == d._year&& _month == d._month&& _day == d._day; } bool Date::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 Date::operator>=(const Date& d) {return *this > d || *this == d; }
可以,权限的缩小。
♣const成员函数内可以调用其它的非const成员函数吗?
bool Date::operator> (const Date& d) {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 Date::operator>=(const Date& d) const {return *this > d || *this == d; }
不可以,权限的放大。
在开头就介绍过,这两个默认成员函数使用编译器生成的默认取地址的重载即可,很少需要自己实现,知道有这两兄弟即可。Date* operator&(){return this ;}const Date* operator&()const{return this ;}