总结
自动生成的赋值重载函数对成员变量的处理规则和析构函数一样 – 对内置类型以字节方式按值拷贝,对自定义类型调用其自身的赋值重载函数;我们可以理解为:需要写析构函数的类就需要写赋值重载函数,不需要写析构函数的类就不需要写赋值重载函数。
日期类的实现
其实这个没什么好说的,无非就算根据我们前面所讲的运算符重载以及赋值重载等知识来实现日期之间的加减等功能。相当于是对之前知识的巩固与练习,这里就不多赘述了,直接放上代码:
class Date { public: Date(int year = 2023, int month = 1, int day = 17) { 如果只是这样写的话,就算是非法日期也会输出,建议这里还要检查以下日期的合法性 //_year = year; //_month = month; //_day = day; if ((year >= 1) && (month >= 1 && month <= 12) && (day >= 1 && day <= GetMonthDay(year, month))) { _year = year; _month = month; _day = day; } else { cout << "日期非法" << endl; } } int GetMonthDay(int year,int month) { static int arrDay[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; if ((month==2)&&(year % 400 == 0 || (year % 4 == 0 && year % 100 != 0))) { return 29; } return arrDay[month]; } bool operator==(const Date& d) { return _year == d._year && _month == d._month && _day == d._day; } bool operator!=(const Date& d) { return !(*this == d); } Date& operator=(const Date& d) { //要先判断一下是否是自我赋值 if (*this == d) { return *this; } //不是自我赋值再进行下一步 _year = d._year; _month = d._month; _day = d._day; return *this; } Date& operator+=(int day) { //如果当前天数加上day超过本月该有的天数,则月份加一 _day += day; while (_day > GetMonthDay(_year, _month)) { _day -= GetMonthDay(_year, _month); _month++; if (_month == 13) { ++_year; _month = 1; } } return *this; } bool 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 operator>=(const Date& d) { return !(*this < d) || *this == d; } Date& operator-=(int day)//这里是日期和天数之间的减法 { //这里先写日期和天数之间的相减,日期减天数,核心是借位 _day -= day; while (_day <= 0) { //本月天数不够,就向之前的月份借,首先月份减减 _month--; //这里处理一些特殊情况,比如_month=0; if (_month == 0) { --_year; _month = 12; } _day += GetMonthDay(_year, _month); } return *this; } int operator-(const Date& d) { //两个日期之间相减,这里有一个很巧妙的办法,就是复用++,将小的日期++n次,直到和日期较大的相等 //这里首先假设左值的日期大于右值 Date max = *this; Date min = d; int flag = 1;//这里设立flag是为了判断是左边日期大于右边,还是右边大于左边 if (*this < d) { max = d; min = *this; flag = -1; } int n = 0; while (min != max) { ++n; ++min; } return n*flag;//如果返回值是负数,说明是当前日期小于比较日期 } //前置++不用写参数,返回++后的值 Date& operator++() { *this += 1; return *this; } //后置++需要多写一个整形类型的参数作为区分 Date operator++(int i) { //返回的是++前的值,所以需要使用到拷贝构造 Date ret(*this); *this += 1; return ret;//这个ret是一个临时变量,出了函数以后就不存在了,所以只能传值返回 } //前后置--与前后置++是同一个道理这里就不在实现了 void Print() { cout << _year << " " << _month << " " << _day << endl; } private: int _year; int _month; int _day; };
取地址以及const取地址重载
const成员函数
我们将const修饰的成员函数称之为const成员函数,const修饰类的成员函数时,其实是修饰隐藏的this指针,表明该成员函数不能对类的任何成员进行修改。
可以看到,这里我定义了一个const类型的只读日期类,甚至连打印都做不到,这是为什么?因为类的成员函数第一个参数是隐藏的this指针,而日期类this指针的完整类型是:Date const*this,这个const修饰的是this指针本身,也就是说this指针指向的值是可以修改的。不可修改的只读变量作为参数传给可以修改的指针,这样就有权限放大的问题。但是this指针又是隐藏的参数,我们不能显示的去写。为了解决这个问题,C++规定可以在函数名的最后写一个const用于修饰隐藏的this指针:
建议对于不用修改对象的函数都加上const(也就是不用修改this指针代表的对象),这样无论传来的参数是不是const类型的都可以使用,因为权限可以平移和缩小,但是不能放大。
最后,我们来做几个思考题:
const对象可以调用非const成员函数吗?-- 不可以,权限扩大;
非const对象可以调用const成员函数吗?-- 可以,权限缩小;
const成员函数内可以调用其它的非const成员函数吗?-- 不可以,权限扩大;
非const成员函数内可以调用其它的const成员函数吗?-- 可以,权限缩小;
取地址重载
取地址重载也是C++默认的六大成员函数之一,是运算符重载的一种:
Date* operator&() { return this; }
const取地址重载
const 取地址重载也是C++的默认六个成员函数之一,它是取地址重载的重载函数,其作用是返回 const 对象的地址:
const Date* operator&()const { return this; }
其实这两个默认成员函数很少让我们自己实现,除了某些特定场景不允许获得该对象的地址,任何取地址行为都直接返回空以外,想不到别的应用场景了。
初始化列表
基础知识
通过前面我们已经知道,在创建对象时编译器会自动调用构造函数对对象的各个变量赋一个合适的初值:
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
上述函数调用之后,对象已经有了一个合适的初始值,但是这并不能称为对对象中的成员变量进行初始化,构造函数中的语句只能称之为赋初值,不能称为初始化,因为初始化只能初始化一次,而构造函数体内能多次赋值 。
此外前面也有说过类中只是成员变量的声明并没有定义,并不会占用内存空间,只有当实例化出对象以后才会占用内存空间,而实例化对象时是整个对象一起定义的,那么类中的成员变量又是在哪单独定义的呢?
C++类对象中的成员变量通过初始化列表定义和初始化,初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式;
class Date { Date(int year=2023,int month=2,int day=17) :_year(year) ,_month(month) ,_day(day) {} private: int _year; int _month; int _day; };
特性
初始化列表有以下几个特性:
1.初始化列表是成员初始化的地方,所以每个变量(无论是内置类型还是自定义类型)都会一定会走一次初始化列表,无论我是否显示写了初始化列表,所以每个成员都只能在初始化列表中出现一次:
就像世界上很多事只有第一次才让人充满感触一样,要牢记初始化只能有一次
2.如果我显示写了初始化列表,那么编译器就会调用我显示写的;否则对于内置类型编译器会使用随机值来初始化,对于自定义类型的话编译器就会去调用它的默认构造函数,如果没有默认构造函数编译器就会报错:
可以看到对于内置类型_a当我不在初始化列表中初始化它,编译器就会用一个随机值来初始化,而我不在初始化列表中初始化,编译器就会自己去找自定义类型自己的默认构造函数,当自定义类型既没有在初始化列表中显示定义又没有默认构造函数时就会报错:
3.如果类中包含以下成员就必须要显示定义在初始化列表中:
1.引用成员变量
2.const成员变量
3.自定义类型成员(且该类没有默认构造函数时)
**引用是给变量取别名,一旦它成为了某一个变量的别名就不能再成为另一个变量的别名,也就是说它只有一次初始化的机会并且必须在定义的时候初始化,const作为只读常量,也是必须要在定义的时候就初始化,并且只能初始化一次。**前面说了构造函数只是赋值并不是初始化,真正的初始化只有在初始化列表中,所以引用成员变量和const成员变量都必须显示的写在初始化列表中。
此外构造函数的初始化列表是可以和函数体的赋值一起使用的,这样的使用方法在有资源申请的类会十分好用,这里以Stack为例:
class Stack { public: Stack(int capacity=4) :_top(0) ,_capacity(capacity) { _a = (int*)malloc(sizeof(int) * capacity); if (_a == NULL) { perror("malloc fail\n"); exit(-1); } } ~Stack() { free(_a); _a = NULL; _top = _capacity = 0; } void Push(int x) { _a[_top++] = x; } private: int* _a; int _top; int _capacity; };
4.尽量使用初始化列表,因为无论我们是否显示定义初始化列表,成员变量都会走一次初始化列表。
5.成员变量在类中声明的顺序就算初始化的顺序,也就是说初始化看的不是初始化列表中显示定义的顺序而是看类的声明顺序:
class A { public: A(int a=1) :_a2(a) ,_a1(_a2) {} void Print() { cout << _a1 << " " << _a2 << endl; } private: int _a1; int _a2; }; int main() { A _aa; _aa.Print(); return 0; }
可以看到我首先声明的是_a1,但是在显示写初始化列表时是将 _a2的初始化写在前面,如果是按照声明顺序初始化的话,就应该是先初始化 _a1,也就是说此时 _a2的值还是一个随机值,而我用 _a2的值初始化 _a1得到的也就是一个随机值。输出结果表示 _a1的值确实是一个随机值,也就是说初始化列表的顺序只与类的声明顺序有关。