本篇将会对类和对象的主要知识收尾,先会对构造函数进行补充,分别补充了构造函数体赋值、初始化列表、explicit 关键字,然后介绍 static 成员知识以及友元、内部类还有匿名对象等知识点,目录如下:
目录
1. 构造函数补充
1.1 构造函数体赋值
1.2 初始化列表
1.3 explicit 关键字
2. static 成员
3. 友元
3.1 友元函数
3.2 友元类
4. 内部类
5. 匿名对象
1. 构造函数补充
前面由于对于默认函数的知识还不够充分,所以就没有将构造函数的剩余部分加上,接下来将会把构造函数的所有知识都进行补充完毕。
1.1 构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中的各个成员变量一个合适的初始值。虽然通过调用构造函数对象就已经存在一个初始值,但是我们不能将其称为对对象中的成员变量进行初始化,构造函数体中的语句只能将其称之为赋初值,而不能称为初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
如下:
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; // 在调用一次构造函数体时确实将函数进行了多次赋值 _year = 1; } private: int _year; int _month; int _day; };
1.2 初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式。
首先我们先需要探讨为什么需要初始化列表:
如上,当我们在成员变量中定义了一个由 const 修饰的成员变量的时候,我们可以发现,不管是编译器默认生成的构造函数还是由自己定义的成员函数来说,都编译不成功。这是因为有些成员变量必须在定义的时候初始化,所以我们需要在某个地方将这些必须在定义的时候初始化的变量进行初始化,这就诞生出了初始化列表。
初始化列表的形式如下:
class A { public: // 定义以下的构造函数,使得不会存在默认的构造函数 A(int a) :_a(a) {} private: int _a; }; class Date { public: Date(int year, int month, int day, int& x) :_year(year) ,_month(month) ,_day(day) ,_p((int*)malloc(sizeof(int))) // 同样在初始化列表中对指针进行分配空间 ,_num(1) ,_ref(x) ,_a(1) { //... 函数体中也可以在次进行赋值 _year = 1; } private: int _year = 1; // _year的缺省值,其实是初始化列表的缺失值, //当初始化列表对_year进行处理 int _month; // 那么就不会走_year = 1 int _day; int* _p; // 必须走初始化列表 const int _num; int& _ref; A _a; // 没有默认构造的类,有默认构造的类可以不初始化 };
以上就是初始化列表的一般使用形式了,其中,仍然存在一些需要注意的点:
1. 每个成员变量只能在初始化列表中出现一次,但是可以在函数体中可以出现多次
2. 类中包含这些成员,必须放在初始化列表中:引用成员变量、const 成员变量、自定义类型成员(无默认构造函数)
对于初始化列表的使用:
尽量选择在初始化列表出就将所有成员变量进行初始化,因为无论构造函数是否存在初始化列表,都是会进行初始化列表的操作,虽然成员变量未定义在初始化列表中,但还是会进行操作,不管给的是一些随机值。
初始化列表进行初始化的顺序,在初始化列表的初始化中,并不是根据从上而下的初始化顺序,而是按照成员变量声明的顺序进行初始化的,所以建议在初始化列表中,按照成员变量声明的顺序写下来。
1.3 explicit 关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造参数,还具有类型转换的作用(C++11标准后对于多参数同时具有类型转换的作用)。对于这句话的解释如下:
如上图,对于 a1 来说,我们将3传入进去进行拷贝构造,然后 a1 中的 _a 变量就被赋值为了3,但是当我们将 4 直接赋值给 a2,又是如何进行赋值的呢,因为2作为一个常量,并不属于 A 类型的对象,为什么可以调用类似拷贝构造一样的赋值呢?
这是因为在对 a2 进行赋值的时候,先会对2进行类型转换,也就是将2构造成一个a的零时对象,然后将 a2 与该临时对象进行调用拷贝构造(也许在Debug的时候,发现并不会调用拷贝构造,那是因为编译器将其优化了,同一个表达式连续步骤的构造,很可能会合二为一),所以最后 a2 中的成员变量被拷贝成功。(但是最好将a2需要赋值的常量数字设置为与a2成员对象相同类型的变量,比如a2的成员变量为int型,那么最好传入一个int型的对象,最好不要传入其他类型的参数,有些编译器可能会报错)
对于 a3 来说,当成员变量为什么常引用也可以赋值成功呢,这是因为对2进行类型转化的时候生成的是一个临时变量,临时变量具有常性。
当一个类的成员变量有多个时,该如何进行构造呢?如下:
当类中的成员变量有多个时,我们可以使用类似数组的形式将其赋值给对象(该形式是C++11标准之后才有的形式)。
当我们不想使用常量直接赋值给一个对象的时候,我们可以使用 explicit 关键字修饰构造函数,这样就不会进行类型转换后调用拷贝构造。如下:
所以,使用 explicit 修饰的构造函数,将会禁止构造函数的隐式转化。
2. static 成员
static 成员的概念:
声明为 static 的类成员称之为类的静态成员,用 static 修饰的成员变量,称之为静态成员变量;用 static 修饰的成员函数,称之为成员函数。静态成员变量一定要在类外进行初始化。
静态成员变量所具有的特点之一就是为所有成员共享,不属于某个具体的对象,那我们如何计算一个类在程序中创建了多少个类对象呢。如下程序:
class A { public: A(int x = 0, int z = 0) :_a(x) ,_ca(z) { _num++; } A(const A& a) { _a = a._a; _ca = a._ca; _num++; } static int GetNum() { return _num; } private: int _a; int _ca; static int _num; }; int A::_num = 0; int main() { A a1(3,5); A a2 = {4, 6}; const A& a3 = {5, 7}; cout << a1.GetNum() << endl; return 0; }
如上程序,就可以计算出一个类构造了多少个对象。
那么 static 成员的特点又有哪一些呢:
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存在在静态区;
2. 静态成员变量必须在类外定义,定义时不添加 static 关键字,类中只是声明;
3. 类静态成员既可用 类名: :静态成员 或 对象.静态成员来访问
4. 静态成员函数没有隐藏的 this 指针,不能访问任何非静态成员;
5. 静态成员也是类的成员,受 public、protected、private 访问限定符的限制。
3. 友元
友元提供了一种突破封装的方式,可以让我们在类外也使用被 private 修饰的变量,但是友元会增加耦合度,破坏封装,所以在很多时候并不建议使用友元。
友元分为:友元函数和友元类
3.1 友元函数
当我们尝试使用 operator 操作符去构造 operator<< 重载成员函数。虽然确实可以做到,但是因为 cout 的输出流对象和隐含的 this 指针在抢占第一个参数的位置(若使用 operator<< ,那么就是 this 指针占据第一个参数),但实际使用的过程中,我们应该使用 cout 作为第一个参数,所以我们不得不将 operator 重载成全局函数。虽然将 operator 重载成全局函数,但会导致类外没有办法访问类中被 private 修饰的成员,这个时候就需要使用友元来解决问题。
如下:
当我们在类域中定义时,就只能将其写成这个样子,但是这并不是 cout 的使用风格,这个时候我们就需要将 operator<< 在类外定义,然后在类域中进行友元声明,如下:
如上,当我们使用友元修饰一个函数之后,友元函数就可以直接访问类的私有成员了,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加 friend 关键字。
对于友元函数的说明:
1. 友元函数可以访问类的私有保护成员,但不是类的成员函数。
2. 友元函数不可以使用 const 修饰
3. 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
4. 一个函数可以是多个类的友元函数
5. 友元函数的调用和普通函数的调用相同
3.2 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的所有非公有成员。
但是对于友元类来说,存在以下几点特性:
1. 友元关系是单向的,不具有交换性
2. 友元关系不具有传递性,如 a 是 b 的友元,b 是 c 的友元,a 不是 c 的友元
3. 友元关系不能继承。
如下:
class Time { friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类 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 = 1900, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) {} void SetTimeOfDate(int hour, int minute, int second) { // 直接访问时间类私有的成员变量,想要访问,需要在 Date 类中存在 Time 类的对象 _t._hour = hour; _t._minute = minute; _t._second = second; } private: int _year; int _month; int _day; Time _t; };
对于如上的函数而言,当我们想要在 Date 类中定义 Time 类的友元时,那么 Time 中也可以访问 Date 类中的成员对象了,但是我们需要注意一个先后的问题,假若我们想要在 Time 中定义与 Date 对象有关的函数时,只能在类中进行声明,而不能进行定义,因为在使用函数时,会在函数以上找有关 Date 对象的定义,而不会向下寻找,所以这时候会报错,解决办法就是将声明与定义分离。
4. 内部类
内部类:若一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
如下就是一个内部类:
class A { public: A(int y =0, int yy = 0) :_a(y) ,_aa(yy) {} // 内部类 class B { public: void Print(){ // ... } B(int x = 0) :_b(x) {} private: int _b; }; private: int _a; int _aa; }; int main() { A::B b(1); A a(1,2); b.Print(); return 0; }
如上所示,A 中 B 就是一个内部类,将 B 定义在 A 类中,若想要定义一个 B 类型的对象,我们就需要先通过类 A 加上作用域限定符然后才能访问到类 B。但是若我们想要计算一下 A 类占据的内存大小呢?如下:
如上图,我们计算出来的结果显示为 8,仅仅只是 A 的两个成员变量的大小。这是因为在 A 中即使定义了一个类,计算大小时,也并不会计算这个类的大小,因为 B 只是在 A 中的一个声明,当 B 被编译器编译后,其实什么也不会产生。
内部类的特性:
1. 内部类天生就是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的成员。
2. 内部类可以定义在外部类的 public、private、protected。
3. 内部类可以直接访问外部类中的 static 成员,不需要外部类的对象。
4. sizeof(外部类) = 外部类,和内部类没有任何关系。
5. 匿名对象
在Cpp存在的匿名对象就是只定义一次,并且匿名对象的生命周期也是只在定义的这一行中,走过这一行之后声明周期也就结束了。如下:
当我们 Debug 该程序的时候,发现走过匿名对象之后,该对象就被销毁了。匿名对象的定义方式如上两种,类(),或类(成员变量的参数)。
匿名函数的作用为:当我们只想使用类中的某个成员函数时,我们就可以直接创建一个匿名对象,然后通过该匿名对象调用该函数。