在前两期的 类和对象(上篇) 和 类和对象(中篇) 我们学习了有关类和对象的大部分知识,这一篇我将会带大家完善这方面的有关知识,并完成我们日期类的完整实现。
一、初始化列表
1. 构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
2. 初始化列表
所以我们引入一个概念:初始化列表,初始化列表是每个对象的成员定义的地方,不管我们写不写,每个成员都要走初始化列表。
其用法是,以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个 “成员变量” 后面跟一个放在括号中的初始值或表达式。
例如以下日期类:
// 初始化列表 class Date { public: Date(int year, int month, int day) : _year(year) , _month(month) , _day(day) {} private: int _year; int _month; int _day; };
使用初始化列表需要注意的:
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。
- C++11 支持在成员声明处给缺省值,这个缺省值也会给初始化列表,如果初始化列表没有显示给值,就用这个缺省值;如果显示给值了,就不用这个缺省值。
// 初始化列表 class Date { public: Date(int year , int month , int day) : _year(year) , _month(month) ,_day(day) {} void Print() { cout << _year << "年" << _month << "月" << _day << "日" << endl; cout << _x << endl; } private: int _year ; int _month; int _day ; int _x = 10; };
例如以上代码,_x 没有显式给值,但是它在声明处给了缺省值,这个缺省值最终也会给初始化列表定义 _x。
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const 成员变量
- 自定义类型成员(且该类没有默认构造函数时)
例如以下这段代码:
class A { public: A(int a) :_a(a) {} private: int _a; }; class Date { public: Date(int year, int month, int day, int& i) : _year(year) , _month(month) ,_aa(1) ,_refi(i) { // 赋值,并不是初始化 _day = day; } private: // 每个成员声明 int _year; int _month; int _day; // 必须定义时初始化 const int _x = 10; //const成员变量;此处是给缺省值,也可以在初始化列表中初始化 int& _refi; // 引用成员变量 A _aa; //自定义成员变量(没有默认构造) }; int main() { int n = 0; Date d1(2023, 7, 28, n); return 0; }
const 成员变量必须定义时初始化是因为一旦初始化就不能被修改;引用成员变量也一样,引用成员变量是一个变量的别名,需要定义的时候就初始化;因为 _aa 是个自定义成员变量,而且它没有默认的构造函数(因为它的构造函数中没有给缺省值,所以无法调到),所以也要在定义的时候初始化;
所以以上三种类型必须在定义的时候初始化,而初始化列表就是每个成员定义的地方,所以我们要在初始化列表给它们初始化的值,也可以在声明处给缺省值(C++11支持),例如以上代码中 const 成员变量 _x 就是给了缺省值,但缺省值最终也会给初始化列表初始化。
- 能用初始化列表就用初始化初始化列,有些场景还是需要初始化列表和函数体混着用。
例如 Stack 类,以下场景:
typedef int DataType; class Stack { public: Stack(size_t capacity = 4) :_array((DataType*)malloc(sizeof(DataType)* capacity)) , _size(0) , _capacity(capacity) { if (_array == NULL) { perror("malloc申请空间失败!!!"); return; } memset(_array, 0, sizeof(DataType) * _capacity); } private: DataType* _array; int _size; int _capacity; };
- 对于自定义类型成员变量,一定会先使用初始化列表初始化。
例如以下代码,时间类显式写了构造函数并用初始化列表初始化;
class Time { public: Time(int hour = 0) :_hour(hour) {} private: int _hour; }; class Date { public: Date(int day) :_day(day) {} private: int _day; Time _t; }; int main() { Date d(1); }
我们在日期类中声明时间类的自定义类型,但是不在日期类的初始化列表初始化它,我们观察 _t 对象中的成员变量的值被初始化为什么:
通过调试窗口可以观察到,它会调它的构造函数并走它的初始化列表,并使用缺省值 0 初始化;
那么我们在日期类的初始化列表给它初始化呢?我们也不妨试试,结果如下:
如上图,我们在日期类的初始化列表中给它初始化,它还是会走时间类的初始化列表,但是没有用缺省值进行初始化,而是用我们给定的值进行初始化。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
例如以下的日期类,我们观察 _a1 和 _a2 的结果会是什么呢?
class A { public: A(int a) :_a1(a) , _a2(_a1) {} private: int _a2; int _a1; }; int main() { A aa(1); }
我们对它进行打印观察:
结果是 1 和 随机值,就是因为初始化列表是按照声明的顺序进行初始化,先对 _a2 进行初始化,此时 _a1 还没有被初始化,所以用 _a1 对 _a2 进行初始化是随机值;然后再对 _a1 进行初始化,此时 _a1 被初始化为 1.
二、explicit 关键字
1. 单参数构造函数的隐式类型转换
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
我们在以前也学过隐式类型转换,例如一个 int 类型的值赋给 double 类型,中间会发生隐式类型转换;同样道理,对象的构造函数也会完成隐式类型的转换。
单个参数构造函数的隐式类型转换,例如以下 A 类:
class A { public: A(int i) :_a(i) { cout << "A(int i)" << endl; } A(const A& aa) :_a(aa._a) { cout << "A(const A& aa)" << endl; } ~A() { cout << "~A()" << endl; } private: int _a; };
我们可以对对象进行这样的实例化:A aa1(1);
这是我们已经知道的,但是我们还可以这样对对象实例化:A aa2 = 2;
,这就是单参数构造函数的隐式类型转换。
对于我们的理解,A aa2 = 2;
应该是用 2 调用 A 构造函数生成一个临时对象,再用这个对象去拷贝构造 aa2,但是编译器会优化,优化用 2 直接构造对象 aa2 ,例如以下代码,我们对对象实例化观察对象调用了哪些函数:
int main() { A aa1(1); cout << "-----------------------------------" << endl; A aa2 = 2; cout << "-----------------------------------" << endl; return 0; }
结果如下图:
我们观察可以看出,它们两个是等价的,所以说明了编译器对 aa2 对象的实例化进行了优化。
对于除第一个参数无默认值其余均有默认值,例如以下日期类:
class Date { public: // 虽然有多个参数,但是创建对象时后两个参数可以不传递 Date(int year, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) { cout << "Date(int year, int month = 1, int day = 1)" << endl; } Date& operator=(const Date& d) { cout << "Date& operator=(const Date& d)" << endl; if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; } private: int _year; int _month; int _day; };
我们对对象进行这样的实例化:Date d1(2022);
这对我们来说也没问题,但是我们这样 d1 = 2023;
呢,编译器又会做什么优化处理呢?我们执行观察一下:
我们可以观察到,在 d1 = 2023;
的时候,我们用一个整型变量给日期类型对象赋值, 实际编译器背后会用 2023 构造一个无名对象,最后用无名对象给 d1 对象进行赋值,这也是编译器的单参数构造函数的隐式类型转换。
2. explicit 关键字
对于上述代码可读性不是很好,所以C++中可以用 explicit 修饰构造函数,将会禁止构造函数的隐式转换。
例如上述的日期类中,我们在构造函数前用 explicit 关键字修饰,那么d1 = 2023;
这段代码就不会发生单参数构造函数的隐式类型转换,例如:
explicit Date(int year, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) {}
编译器会报以下错误:
同理,我们对上面的 A 类的构造函数也用 explicit 关键字修饰,如:
explicit A(int i) :_a(i) { cout << "explicit A(int i)" << endl; }
我们对对象进行 A aa2 = 2;
的实例化时,编译器也会报类似的错误:
因为 explicit 修饰构造函数,禁止了单参构造函数类型转换的作用。
3. 多参数的隐式类型转换(C++11)
在 C++11 中,C++11 支持多参数的隐式类型转换,例如以下的 B 类:
class B { public: B(int b1, int b2) :_b1(b1) , _b2(b2) { cout << "B(int b1, int b2)" << endl; } B(const B& b) :_b1(b._b1) ,_b2(b._b2) { cout << "B(const B& b)" << endl; } private: int _b1; int _b2; };
我们有以下的实例化对象:
int main() { B bb1(1, 1); B bb2 = { 2, 2 }; const B& ref2 = { 3,3 }; return 0; }
其中 B bb1(1, 1);
是正常的实例化对象,没有进行隐式类型的转换;而 B bb2 = { 2, 2 };
和 const B& ref2 = { 3,3 };
则进行了多参数的隐式类型转换,我们执行程序观察结果:
如上图,三个实例化都是只是调用了构造函数,说明编译器对其进行了优化。
当我们对构造函数加上 explicit 关键字后,编译器就无法对 B bb2 = { 2, 2 };
和 const B& ref2 = { 3,3 };
进行多参数的隐式类型转换了。
三、static 成员
概念:用 static 修饰类的成员变量,称之为静态成员变量;用 static 修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
例如我们需要统计一个类中创建出了多少对象,并计算正在使用的还有多少个对象;我们可能会想到以下的思路:
// 累积创建了多少个对象 int n = 0; // 正在使用的还有多少个对象 int m = 0; class A { public: A() { ++n; ++m; } A(const A& t) { ++n; ++m; } ~A() { --m; } private: }; // A& Func(A& aa) A Func(A aa) { return aa; }
我们创建了两个全局变量,m 和 n 分别统计正在使用的还有多少个对象和累积创建了多少个对象 ;并在构造函数、拷贝构造函数和析构函数体内做相应统计;
这种方法虽然能统计出来,但是 m 和 n 是全局变量,说明我们可以随意修改数据,变得不安全,所以这种方法不好。
所以就有静态成员变量这个概念,我们在类中定义静态成员变量,静态成员变量属于所有类的对象,属于整个类。 例如我们将上面的 m 和 n 声明为静态成员变量:
class A { public: A() { ++n; ++m; } A(const A& t) { ++n; ++m; } ~A() { --m; } // 静态成员函数的特点:没有this指针 static int GetM() { // x++; // 不能访问非静态,因为没有this return m; } static int GetN() { return n; } private: // 静态成员变量属于所有 A 类的对象,属于整个类 // 声明 // 累积创建了多少个对象 static int n; // 正在使用的还有多少个对象 static int m; int _x = 0; }; // 定义 int A::n = 0; int A::m = 0;
如上代码,静态成员的变量需要在类外定义;我们在类中也定义了静态成员函数,静态成员函数的特点是没有 this 指针,所以它不能访问非静态成员变量,假设我们声明了一个 _x 成员变量,GetM
函数是无法访问 _x 的;但是它可以访问静态成员的变量;例如如果我们想要拿到 m 和 n 的值,可以通过函数 GetM
和 GetN
拿到,而这两个静态成员函数都是返回静态成员变量。
那么我们为什么不将这两个函数直接定义成成员函数呢?因为我们需要统计的是累计创建了多少对象,而需要访问成员函数就必须得实例化一个对象出来,假如我们没有实例化对象,就不能得到 m 和 n 的值;而定义成静态成员函数只需要指定类域即可得到 m 和 n 的值,例如以下代码:
int main() { // 匿名对象 A(); A(); // error //cout << aa1.GetM() << ' ' << aa1.GetN() << endl; // 可以访问 cout << A::GetM() << ' ' << A::GetN() << endl; // 实例化对象 A aa1; cout << aa1.GetM() << ' ' << aa1.GetN() << endl; cout << A::GetM() << ' ' << A::GetN() << endl; return 0; }
以上代码中,我们实例化了两个匿名对象(下面的内容会讲到匿名对象),匿名对象也会调用构造函数,但是我们如果不将 GetM
和 GetN
函数定义成静态成员函数,就无法得到 m 和 n 的值,因为没有实例化的对象,匿名对象的生命周期只有一行;只有我们实例化 aa1 对象才可以得到 m 和 n 的值,但是这样又调用了一次构造函数,并不是我们想要的结果。
四、友元
1. 友元函数
我们在运算符重载中,还有两个运算符没有重载:流插入和流提取。
假设我们在类内部实现流插入和流提取运算符重载:
// 流插入重载 void operator<<(ostream& out) { out << _year << '.' << _month << '.' << _day << endl; } // 流提取重载 void operator>>(istream& in) { in >> _year >> _month >> _day; }
这时候我们要注意,this 指针抢占了成员函数的第一个参数的位置,导致 cout 参数变成在第二个参数位置,参数的顺序不一样,所以我们在使用中应该是 d << cout
, 虽然可以使用,但是不符合我们使用的习惯和价值。
所以要将 operator<< 重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。
我们先看如何使用:
class Date { // 友元函数 friend ostream& operator<<(ostream& out, const Date& d); friend istream& operator>>(istream& in, Date& d); public: //日期类的构造函数 Date(int year = 2023, int month = 7, int day = 29) :_year(year) , _month(month) , _day(day) {} private: int _year; int _month; int _day; }; // 流插入重载 inline ostream& operator<<(ostream& out, const Date& d) { cout << d._year << '.' << d._month << '.' << d._day << endl; return out; } // 流提取重载 inline istream& operator>>(istream& in, Date& d) { in >> d._year >> d._month >> d._day; return in; }
如上代码,流插入和流提取重载是放在全局域中,此时我们需要在类的内部声明友元函数,可以在任意位置,此处我们在最上面声明两个重载的友元,此时两个重载函数就可以正常访问类的成员变量,从而不受访问限定符的影响,cout 的参数又可以在第一个形参位置,这才是符合我们的要求。这里使用 ostream& 和 istream& 类型返回是为了支持连续插入和提取。
最后总结,友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加 friend 关键字。
2. 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
例如,这里有个时间类,在时间类中声明日期类为友元类:
class Time { // 声明日期类为时间类的友元类,则在日期类中就直接访问 Time 类中的私有成员变量 friend class Date; public: Time(int hour = 0, int minute = 0, int second = 0) : _hour(hour) , _minute(minute) , _second(second) {} private: int _hour; int _minute; int _second; };
在日期类中定义时间类的自定义类型,访问时间类的成员变量:
class Date { public: Date(int year = 2023, int month = 7, int day = 29) : _year(year) , _month(month) , _day(day) {} void SetTimeOfDate(int hour, int minute, int second) { // 直接访问时间类私有的成员变量 _t._hour = hour; _t._minute = minute; _t._second = second; } private: int _year; int _month; int _day; Time _t; };
上述代码是没有问题的,在日期类中可以直接访问时间类的成员变量,因为日期类是时间类的友元,但是在时间类中却无法访问日期类的成员变量,因为时间类不是日期类的友元。
最后对友元进行总结,友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
五、内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。 内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意: 内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
例如以下两个类,A类和B类,B类是A类的内部类:
class A { public: class B { public: void FuncB() { A aa; aa._a = 1; } private: int _b; }; void PrintA() { cout << _a << endl; } private: int _a; };
A类受B类域和访问限定符的限制,B类可以访问A类的成员变量,A类却不能访问B类的成员变量;其实他们是两个独立的类;内部类默认就是外部类的友元类
如果计算上面代码中A类的大小,会是多少呢?
int main() { cout << sizeof(A) << endl; // 实例化 aa A aa; // 实例化 bb1 A::B bb1; return 0; }
结果如下,虽然B类是A类的内部类,但是实际上它们是两个独立的类,所以没有计算B类的大小,A类的大小只包括成员变量 _a 的大小;
六、匿名对象
以前我们定义的对象大多数都是有名对象,有名对象的特点是生命周期在当前局部域;而匿名对象的特点是生命周期只在定义的那一行。
例如有一个A类,匿名对象的生命周期只有这一行,下一行它就会自动调用析构函数:
// 有名对象 A aa(6); // 匿名对象 A(7);
但是我们不可以这样定义对象:
A aa1();
因为编译器无法识别这是一个函数声明,还是对象定义;
七、日期类
下面我们用前面所学的知识完善我们的日期类,我们将声明和定义分开写在两个文件中,声明写在 .h 文件,定义写在 .cpp 文件中:
声明:
#pragma once #include <iostream> #include <assert.h> using namespace std; class Date { // 友元函数 friend ostream& operator<<(ostream& out, const Date& d); friend istream& operator>>(istream& in, Date& d); public: // 获取月份的天数 int GetMonthDay(int year, int month) { static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; int day = days[month]; // 二月并且是闰年 if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) { day += 1; } return day; } //检查日期是否合法 bool CheakDate() { if (_year >= 1 && _month > 0 && _month < 13 && _day > 0 && _day <= GetMonthDay(_year, _month)) { return true; } return false; } //日期类的构造函数 Date(int year = 2023, int month = 7, int day = 29) :_year(year) , _month(month) , _day(day) { assert(CheakDate()); } void Print() const; //运算符重载 bool operator==(const Date& d) const; bool operator!=(const Date& d) const; bool operator>(const Date& d) const; bool operator>=(const Date& d) const; bool operator<(const Date& d) const; bool operator<=(const Date& d) const; Date operator+(int day) const; Date& operator+=(int day); Date operator-(int day) const; Date& operator-=(int day); Date& operator++(); //前置 Date operator++(int); //后置 Date& operator--(); //前置 Date operator--(int); //后置 int operator-(const Date& d) const; private: int _year; int _month; int _day; }; // 流插入重载 inline ostream& operator<<(ostream& out, const Date& d) { cout << d._year << '.' << d._month << '.' << d._day << endl; return out; } // 流提取重载 inline istream& operator>>(istream& in, Date& d) { in >> d._year >> d._month >> d._day; assert(d.CheakDate()); return in; }
函数的实现:
#include "ClassAndObj_3.h" //打印日期 void Date::Print() const { cout << _year << '.' << _month << '.' << _day << endl; } // == 运算符重载 bool Date::operator==(const Date& d) const { return _year == d._year && _month == d._month && _day == d._day; } // != 运算符重载 bool Date::operator!=(const Date& d) const { return !(*this == d); } // > 运算符重载 bool Date::operator>(const Date& d) const { if (_year > d._year || (_year == d._year && _month > d._month) || (_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); } // < 运算符重载 bool Date::operator<(const Date& d) const { return !(*this >= d); } // <= 运算符重载 bool Date::operator<=(const Date& d) const { return (*this < d) || (*this == d); } // + 运算符重载 Date Date::operator+(int day) const { Date ret(*this); ret += day; return ret; } // += 运算符重载 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; } } return *this; } // - 运算符重载 Date Date::operator-(int day) const { Date ret(*this); ret -= day; return ret; } // -= 运算符重载 Date& Date::operator-=(int day) { if (day < 0) { return *this += -day; } _day -= day; while (_day <= 0) { _month--; if (_month == 0) { _year--; _month = 12; } _day += GetMonthDay(_month, _year); } return *this; } // 前置++ 重载 Date& Date::operator++() { return *this += 1; } // 后置++ 重载 Date Date::operator++(int) { Date ret(*this); *this += 1; return ret; } // 前置-- 重载 Date& Date::operator--() { return *this -= 1; } // 后置-- 重载 Date Date::operator--(int) { Date ret(*this); *this -= 1; return ret; } //计算两个日期之间相差的天数 int Date::operator-(const Date& d) const { int flag = 1; Date max = *this; Date min = d; if (*this < d) { max = d; min = *this; flag = -1; } int n = 0; while (min != max) { min++; n++; } return n * flag; }
如上我们的日期类就基本完善了。这也意味着我们的类和对象的知识也就学完啦,感觉有帮助的小伙伴点个赞吧~