4. static成员
4.1 概念
声明为static的类成员称为类的静态成员。用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
4.2 特性
一、静态成员为类对象所共享,不属于某个具体的对象
#include <iostream> using namespace std; class Test { private: static int _n; }; int main() { cout << sizeof(Test) << endl; return 0; }
结果计算Test类的大小为1,因为静态成员_n是存储在静态区的,属于整个类,也属于类的所有对象。所以计算类的大小或是类对象的大小时,静态成员并不计入其总大小之和。
二、静态成员变量必须在类外定义,定义时不添加static关键字
class Test { private: static int _n; }; // 静态成员变量的定义初始化 int Test::_n = 0;
注意
:这里静态成员变量_n虽然是私有,但是我们在类外突破类域直接对其进行了访问。这是一个特例,不受访问限定符的限制,否则就没办法对静态成员变量进行定义和初始化了。
三、静态成员函数没有隐藏的this指针,不能访问任何非静态成员
class Test { public: static void Fun() { cout << _a << endl; //error不能访问非静态成员 cout << _n << endl; //correct } private: int _a; //非静态成员 static int _n; //静态成员 };
注意
:含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量。
四、访问静态成员变量的方法
1.当静态成员变量为公有时,有以下几种访问方式:
#include <iostream> using namespace std; class Test { public: static int _n; //公有 }; // 静态成员变量的定义初始化 int Test::_n = 0; int main() { Test test; cout << test._n << endl; //1.通过类对象突破类域进行访问 cout << Test()._n << endl; //3.通过匿名对象突破类域进行访问 cout << Test::_n << endl; //2.通过类名突破类域进行访问 return 0; }
2.当静态成员变量为私有时,有以下几种访问方式:
#include <iostream> using namespace std; class Test { public: static int GetN() { return _n; } private: static int _n; }; // 静态成员变量的定义初始化 int Test::_n = 0; int main() { Test test; cout << test.GetN() << endl; //1.通过对象调用成员函数进行访问 cout << Test().GetN() << endl; //2.通过匿名对象调用成员函数进行访问 cout << Test::GetN() << endl; //3.通过类名调用静态成员函数进行访问 return 0; }
五、静态成员和类的普通成员一样,也有public、private和protected这三种访问级别
所以当静态成员变量设置为private时,尽管我们突破了类域,也不能对其进行访问。
提两个问题:
1、静态成员函数可以调用非静态成员函数吗?
答:不可以。因为非静态成员函数的第一个形参默认为this指针,而静态成员函数中没有this指针,故静态成员函数不可调用非静态成员函数。
2、非静态成员函数可以调用静态成员函数吗?
答:可以。因为静态成员函数和非静态成员函数都在类中,在类中不受访问限定符的限制。
六、拓展:设计一个类,只能在栈/堆上创建对象
思路1:将构造函数私有化,但都不用创建对象了思路2:利用静态成员函数,通过类名调用静态成员函数进行对象的创建
同时,单例模式也是类似的思想
5.日期类的实现
在学习了C++的6个默认成员函数后,我们现在动手实现一个完整的日期类,来加强对这6个默认成员函数的认识。
这是日期类中所包含的成员函数和成员变量:
class Date { public: // 获取某年某月的天数 int GetMonthDay(int year, int month); // 构造函数 Date(int year = 0, int month = 1, int day = 1); // 拷贝构造函数 Date(const Date& d); // 赋值运算符重载 Date& operator=(const Date& d); // 析构函数 ~Date(); // 打印函数 void Print() const; // 日期+=天数 Date& operator+=(int day); // 日期+天数 Date operator+(int day) const; // 日期-=天数 Date& operator-=(int day); // 日期-天数 Date operator-(int day) const; // 前置++ Date& operator++(); // 后置++ Date operator++(int); // 前置-- Date& operator--(); // 后置-- Date operator--(int); // 日期的大小关系比较 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; // 日期-日期 int operator-(const Date& d) const; private: int _year; int _month; int _day; };
5.1 构造函数、拷贝构造函数、赋值运算符重载、 析构函数
进入构造函数体,首先需要检查日期的合法性,只有当日期合法时,才能进行后续的构造操作。
// 获取某年某月的天数 static int GetMonthDay(int year, int month)//inline { // 数组存储平年每个月的天数 static int dayArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) { //闰年2月的天数 return 29; } return dayArray[month]; } // 构造函数 Date::Date(int year, int month, int day) { // 检查日期的合法性 if (year >= 0 && month >= 1 && month <= 12 && day >= 1 && day <= GetMonthDay(year, month)) { _year = year; _month = month; _day = day; } else { // 严格来说抛异常更好 cout << "非法日期" << endl; cout << year << "年" << month << "月" << day << "日" << endl; } }
GetMonthDay函数中的细节:
1.该函数可能被多次调用,但指令较多,所以我们没有将其设置为内联函数。
2.函数中存储每月天数的数组最好是用static修饰,存储在静态区,避免每次调用该函数都需要重新开辟数组。
3.逻辑与应该先判断month == 2是否为真,因为当不是2月的时候我们不必判断是不是闰年。
4.用static修饰就可以在类外面调用该函数了
注意:当函数声明和定义分开时,在声明时注明缺省参数,定义时不标出缺省参数。
拷贝构造函数:
// 拷贝构造函数 // d2(d1) Date(const Date& d) { this->_year = d._year; _month = d._month; _day = d._day; }
赋值运算符重载:
// 赋值运算符重载 // d2 = d3 -> d2.operator=(&d2, d3) Date& operator=(const Date& d) { if (this != &d) { this->_year = d._year; this->_month = d._month; this->_day = d._day; } return *this; }
析构函数:
// 析构函数 ~Date() { // 清理工作 }
5.2 打印函数
// 打印函数 void Date::Print() const { cout << _year << "年" << _month << "月" << _day << "日" << endl; }
5.3 日期 += 天数
对于+=运算符,我们先将需要加的天数加到日上面,然后判断日期是否合法,若不合法,则通过不断调整,直到日期合法为止。
调整日期的思路:
1.若日已满,则日减去当前月的天数,月加一。
2.若月已满,则将年加一,月置为1。
反复执行1和2,直到日期合法为止。
// 日期+=天数 Date& Date::operator+=(int day) { if (day<0) { // 复用operator-= *this -= -day; } else { _day += day; // 日期不合法,通过不断调整,直到最后日期合法为止 while (_day > GetMonthDay(_year, _month)) { _day -= GetMonthDay(_year, _month); _month++; if (_month > 12) { _year++; _month = 1; } } } return *this; }
注
:当需要加的天数为负数时,转而调用-=运算符重载函数。
5.4 日期 + 天数
+运算符的重载,我们可以复用上面已经实现的+=运算符的重载函数。但是要注意:虽然我们返回的是加了之后的值,但是对象本身的值并没有改变。就像a = b + 1,b + 1的返回值是b + 1,但是b的值并没有改变。所以我们还可以用const对该函数进行修饰,防止函数内部改变了this指针指向的对象。
// 日期+天数 Date Date::operator+(int day) const { Date tmp(*this);// 拷贝构造tmp,用于返回 // 复用operator+= tmp += day; return tmp; }
注意
:+=运算符的重载函数采用的是引用返回,因为出了函数作用域,this指针指向的对象没有被销毁。但+运算符的重载函数的返回值只能是传值返回,因为出了函数作用域,对象tmp就被销毁了,不能使用引用返回。
5.5 日期 -= 天数
对于-=运算符,我们先用日减去需要减的天数,然后判断日期是否合法,若不合法,则通过不断调整,直到日期合法为止。
调整日期的思路:
1.若日为负数,则月减一。
2.若月为0,则年减一,月置为12。
3.日加上当前月的天数。
反复执行1、2和3,直到日期合法为止。
// 日期-=天数 Date& Date::operator-=(int day) { if (day < 0) { // 复用operator+= *this += -day; } else { _day -= day; // 日期不合法,通过不断调整,直到最后日期合法为止 while (_day <= 0) { _month--; if (_month == 0) { _year--; _month = 12; } _day += GetMonthDay(_year, _month); } } return *this; }
注
:当需要减的天数为负数时,转而调用+=运算符重载函数。
5.6 日期 - 天数
和+运算符的重载类似,我们可以复用上面已经实现的-=运算符的重载函数,而且最好用const对该函数进行修饰,防止函数内部改变了this指针指向的对象。
// 日期-天数 Date Date::operator-(int day) const { Date tmp(*this);// 拷贝构造tmp,用于返回 // 复用operator-= tmp -= day; return tmp; }
注意
:-=运算符的重载函数采用的是引用返回,但-运算符的重载函数的返回值只能是传值返回,也是由于-运算符重载函数中的tmp对象出了函数作用域被销毁了,所以不能使用引用返回。
5.7 前置 ++
前置++,我们可以复用+=运算符的重载函数。
// 前置++ Date& Date::operator++() { // 复用operator+= *this += 1; return *this; }
5.8 后置 ++
由于前置++和后置++的运算符均为++,为了区分它们的运算符重载,我们给后置++的运算符重载的参数加上一个int型参数,使用后置++时不需要给这个int参数传入实参,因为这里int参数的作用只是为了跟前置++构成重载,仅仅是占位,与前置区分。
// 后置++ Date Date::operator++(int) { Date tmp(*this);// 拷贝构造tmp,用于返回 // 复用operator+= *this += 1; return tmp; }
注意:后置++也是需要返回加了之前的值,只能先用对象tmp保存之前的值,然后再然对象加一,最后返回tmp对象。由于tmp对象出了该函数作用域就被销毁了,所以后置++只能使用传值返回,而前置++可以使用引用返回。
由于前置效率高,且不需要进行拷贝,所以我们一般用前置++
5.9 前置 – –
前置–,我们也是可以复用前面的-=运算符的重载函数。
// 前置-- Date& Date::operator--() { // 复用operator-= *this -= 1; return *this; }
5.10 后置– –
后置–需要注意的事项和后置++是一样的,我这里就不过多阐述了。
// 后置-- Date Date::operator--(int) { Date tmp(*this);// 拷贝构造tmp,用于返回 // 复用operator-= *this -= 1; return tmp; }
5.11 日期类的大小关系比较
日期类的大小关系比较需要重载的运算符看起来有6个,实际上我们只用实现两个就可以了,然后其他的通过复用这两个就可以实现。
注意
:进行日期的大小比较,我们并不会改变传入对象的值,所以这6个运算符重载函数都应该被const所修饰。
5.11.1 >运算符的重载
先判断年是否大于,再判断月是否大于,最后判断日是否大于,这其中有一者为真则函数返回true,否则返回false。
bool Date::operator>(const Date& d) const { if (_year > d._year) { return true; } else if (_year == d._year) { if (_month > d._month) { return true; } else if (_month == d._month) { if (_day > d._day) { return true; } } } return false; }
5.11.2 ==运算符的重载
==,年月日均相等,则为真。
bool Date::operator==(const Date& d) const { return _year == d._year &&_month == d._month &&_day == d._day; }
5.11.3 >=运算符的重载
>=,即大于或者等于,满足其中之一即可。
bool Date::operator>=(const Date& d) const { return *this > d || *this == d; }
5.11.4 <运算符的重载
<,大于等于的反面即是小于。
bool Date::operator<(const Date& d) const { return !(*this >= d); }
5.11.5 <=运算符的重载
<=,大于的返回即是小于等于。
bool Date::operator<=(const Date& d) const { return !(*this > d); }
5.11.6 !=运算符的重载
!=,等于的反面即是不等于。
bool Date::operator!=(const Date& d) const { return !(*this == d); }
5.17 日期 - 日期
日期 - 日期,即计算传入的两个日期相差的天数。我们只需要让较小的日期的天数一直加一,直到最后和较大的日期相等即可,这个过程中较小日期所加的总天数便是这两个日期之间差值的绝对值。若是第一个日期大于第二个日期,则返回这个差值的正值,若第一个日期小于第二个日期,则返回这个差值的负值。
// 日期-日期 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 n = 0;// 记录所加的总天数 while (min != max) { min++;// 较小的日期++ n++;// 总天数++ } return n*flag; }
代码中使用flag变量标记返回值的正负,flag为1代表返回的是正值,flag为-1代表返回的是负值,最后返回总天数与flag相乘之后的值即可。
6.总结:
今天我们认识并具体学习了类和对象的 const成员、取地址与const取地址操作符重载、初始化列表、explicit关键字、隐式类型转换、static成员的知识,并通过实现一个完整的日期类加强了默认成员函数的认识和近期所学知识的理解。接下来,我们将继续学习类和对象的相关知识。希望我的文章和讲解能对大家的学习提供一些帮助。
当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~