一、再谈构造函数
1.1 构造函数体内赋值
1.2 构造函数的初始化列表
初始化列表:是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个括号,括号中放该成员变量的初始值或表达式。初始化列表是成员变量定义的地方,在调用构造函数进入函数体前一定会先走初始化列表,尽管程序员没有显式地写出初始化列表,程序也会自动走初始化列表这个步骤,可以说初始化列表是调用构造函数必须走的一条必经之路。
值得注意的是:
1、每个成员变量在初始化列表中只能出现一次,即只能初始化一次。
2、如果出现以下几种类型的成员变量,必须要在初始化列表中进行初始化:
(1) const修饰的成员变量。
(2) 引用成员变量。
(3) 没有默认构造函数的自定义类型的成员变量。
为什么有这三种类型的成员变量就一定要在初始化列表进行初始化呢?
const成员变量:因为const修饰的变量是常变量,只有一次初始化的机会,就是创建变量的同时就初始化,这种变量在创建出来之后是不能再进行赋值的,所以创建的时候必须给一个初始值,而由于初始化列表是成员变量初始化的地方,在调用构造函数时进入函数体前会先走初始化列表,所以这种变量必须要在初始化列表初始化。
引用成员变量:引用变量在创建的同时也必须要进行初始化,创建之后就不能再对它进行赋值了,初始化列表是成员变量初始化的地方,所以引用成员变量也必须要在初始化列表进行初始化。
无默认构造函数的自定义类型的成员变量:由于该成员变量没有默认构造函数,所以当我们没有在初始化列表进行初始化该成员变量时,虽然初始化列表依然会走,但是编译器自己没办法调用它的构造函数对该变量(对象)进行初始化,所以就不要要在初始化列表显式地对该对象进行初始化。
3、 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于一个自定义类型里面的成员变量,一定会先使用初始化列表初始化。
4、成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。(切记,这里好坑)
5、成员变量声明时可以给一个默认值,这个默认值是给初始化列表的,当我们没有显式地使用初始化列表初始化这个成员变量时,这个成员变量就用这个默认值初始化,如果我们显式地使用初始化列表初始化这个成员变量时,这个默认值就不会被使用。
下面来看一个程序题:
class A { public: A(int a) :_a1(a) ,_a2(_a1) {} void Print() { cout<<_a1<<" "<<_a2<<endl; } private: int _a2; int _a1; }; int main() { A aa(1); aa.Print(); } A. 输出1 1 B.程序崩溃 C.编译不通过 D.输出1 随机值
这题的答案是D,为什么呢?原因正是第4点需要注意的地方,成员变量的定义顺序是按照成员变量声明的顺序来初始化的,与它在初始化列表出现的先后顺序没有任何关系。
1.3 explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有隐式类型转换的作用。
explicit关键字的作用是禁止隐式类型转换。
我们看一下以下代码:
class Date { public: Date(int year,int month=1,int day=1) :_year(year) ,_month(month) ,_day(day) {} private: int _year; int _month; int _day; }; int main() { Date d = 2023; return 0; } //这段代码是能够编译通过的,有人可能会有疑问,用Date d=2023 //怎么就构造出来了一个日期类对象了呢?我们看下面图片的解释
上面这个是允许隐式类型转换的条件下才可以的,但是在构造函数前加上explicit关键字意思就是禁止隐式类型转换,这段代码就会报错了。如下:
因为explicit修饰构造函数,禁止了单参构造函数类型转换的作用。
二、static成员
2.1 什么是static成员?
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。(原因:静态成员变量是属于整个类的,不属于某一个对象,所以在构造函数的地方静态成员变量不会走初始化列表,即在创建对象的时候不会进行初始化,所以必须要在类外面初始化)
题目:实现一个类,计算程序中创建出了多少个类对象。
代码实现如下:
class A { public: A(int x = 10) :_a(x) { _count++; cout << "A()" << endl; } A(const A& a) { _count++; cout << "A(const A& a)" << endl; } static int GetCount() { return _count; } private: int _a; static int _count;//注意这里需要一个静态的成员变量统计次数,它属于整个类 }; int A::_count = 0; int main() { A a; cout << A::GetCount() << endl; A aa; A ab; A x(a); cout << A::GetCount() << endl; return 0; }
2.2 静态成员的特性
静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。
静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明。
类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问。
静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
静态成员也是类的成员,受public、protected、private 访问限定符的限制。
问题:
1、静态成员函数可以调用非静态成员函数吗?
不能,因为静态成员没有this指针,而调用非静态成员函数需要传递this指针,所以静态成员函数不能调用非静态成员函数。
2、非静态成员函数可以调用类的静态成员函数吗?
可以,类的静态成员函数可以被任何类型的成员函数所调用,因为它没有this指针,所以调用静态成员函数也就不需要传递this指针。
三、友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元可以分为友元函数和友元类。
3.1 友元函数
下面就是一个友元函数的典型的应用场景。
class Date { public: static int GetMonthDay(int year, int month) { static int DayArr[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))) { return 29; } return DayArr[month]; } Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //如果重载成成员函数,那么第一个参数必然被this指针占用, //那么cout对象只能是作为第二个参数 ostream& operator<<(ostream& out) { out << _year << "/" << _month << "/" << _day << endl; return out; } //如果重载成成员函数,那么第一个参数必然被this指针占用, //那么cin对象只能是作为第二个参数 istream& operator>>(istream& in) { int year = 0; int month = 0; int day = 0; cin >> year >> month >> day; if (month > 0 && month < 13 && day>0 && day <= Date::GetMonthDay(year, month)) { _year = year; _month = month; _day = day; } else { cout << "输入的是非法日期" << endl; assert(false); } return in; } private: int _year; int _month; int _day; }; int main() { Date d; //由于第一个参数必须是this指针,所以必须要像以下d>>cin和d<<cout这样输入 //输出,但是这样子写显然不符合我们平时的cin>>d和cout<<d的习惯。 d >> cin; d << cout << endl; return 0; }
由于成员函数才一定需要传递this指针,那么这里就可以利用友元函数把cout和cin定义在类外面,这样就可以按照我们平时的使用习惯重载cout和cin了。
如下:
class Date { public: //友元函数的写法,类里面声明某个函数是这个类的友元函数,在函数前加关键字friend friend ostream& operator<<(ostream& out, const Date& d); friend istream& operator>>(istream& in, Date& d); static int GetMonthDay(int year, int month) { static int DayArr[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))) { return 29; } return DayArr[month]; } Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; ostream& operator<<(ostream& out, const Date& d) { out << d._year << "/" << d._month << "/" << d._day << endl; return out; } istream& operator>>(istream& in, Date& d) { int year = 0; int month = 0; int day = 0; cin >> year >> month >> day; if (month > 0 && month < 13 && day>0 && day <= Date::GetMonthDay(year, month)) { d._year = year; d._month = month; d._day = day; } else { cout << "输入的是非法日期" << endl; assert(false); } return in; } int main() { Date d; //这样子定义就符合我们平时使用cin和cout的使用习惯了 cin >> d; cout << d << endl; return 0; }
注意点:
1、友元函数可访问类的私有成员和保护成员,但不是类的成员函数。
2、友元函数不能用const修饰。
3、友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
4、一个函数可以是多个类的友元函数。
5、友元函数的调用与普通函数的调用原理相同。
3.2 友元类
友元类的几种特性:
1、友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
2、友元关系是单向的,不具有交换性。A类是B类的友元,A的对象可以访问B类的所有成员。但是B类的对象则不能访问A的私有成员。
比如下面的Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
3、友元关系不能传递,如果B是A的友元,C是B的友元,则不能说明C是A的友元。
4、友元关系不能继承。
class Time { //声明Date是友元类,那么Date类型的对象就能够访问我的所有成员 friend class Date; public: Time(int hour = 0, int minute = 0, int second = 0) :_hour(hour) , _minute(minute) , _second(second) {} public: int _hour; int _minute; int _second; }; class Date { public: Date(int year = 1, int month = 1, int day = 1) :_year(year) , _month(month) , _day(day) {} void Print() { //可以访问Time类型的所有成员 cout << _t._hour << ":" << _t._minute << ":" << _t._second << endl; } private: int _year; int _month; int _day; Time _t; }; int main() { Date d; d.Print(); return 0; }
四、内部类
4.1 什么是内部类?
概念:如果一个类定义在另一个类的内部,那么这个类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类不能访问内部类的私有成员。
但是内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
4.2 内部类的特性
1、内部类定义在外部类的public、protected、private都是可以的。
2、注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3、sizeof(外部类)=外部类,和内部类没有任何关系。
class A { public: A(int x = 10) :_a(x) {} //B是A的内部类,内部类天生是友元类,所以B内可以访问A的任何成员 class B { public: B(int y = 20) :_b(y) {} void Print(const A& ab) { //访问A的私有成员 //可以直接访问静态成员,不需要类名和对象名 cout << ab._a << " " << _k << endl; } private: int _b; }; private: int _a; static int _k; }; int A::_k = 5; int main() { A::B b; b.Print(A()); return 0; }