原文再续,书接上回!! 继续类和对象的学习。
目录
构造函数
析构函数
拷贝构造
赋值重载
运算符重载
const成员
取地址及const取地址操作符重载
当我们没有向类中写入任何成员的时候(也就是空类),类中就什么也没有吗?答案是否定的,任何类在什么都不写时,编译器会自动生成6个默认成员函数。
这六个默认成员函数,是用户不显示实现,编译器会自动生成的成员函数。你可以想象它们六兄弟是替你守江山的忠诚卫士,前4个兄弟用户实现的情景还比较多见。老五老六很少需要用户动手实现。
构造函数
注意:构造函数名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
构造函数主要完成初始化工作,它有如下特性:(代码演示以日期类为例)
●函数名与类名相同,没有返回值。注意:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦 用户显式定义编译器将不再生成!。不显示定义构造函数:#include <iostream> 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 <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 { 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<<endl; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Print(); Date d2(d1); d2.Print(); return 0; }
拷贝构造函数的函数名也和类名相同,不同点是参数类型不同,所以说和构造函数是函数重 载。
这里肯定有小伙伴存有疑问,拷贝构造函数的参数我一定要传本类类型的引用吗,我想用传值方式试试!!
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"<<endl; _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; }; void Fun1(Date a) { cout << "传值:Fun1()" << endl; } void Fun2(Date& b) { cout << "传引用:Fun2()" << endl; } int main() { Date d1; Fun1(d1); return 0; }
传引用:
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 <iostream> 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 <iostream> 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 成员函数 ,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; }
不可以,权限的放大。
取地址及const取地址操作符重载
在开头就介绍过,这两个默认成员函数使用编译器生成的默认取地址的重载即可,很少需要自己实现,知道有这两兄弟即可。Date* operator&() { return this ; } const Date* operator&()const { return this ; }