🍕前言
本文章继自类和对象(中),完成收尾工作。
一、🍕再谈构造函数
1.1构造函数体赋值
在学习过的类和对象的基础知识中,构造函数内部通常是给成员变量一个初始值。虽然调用完构造函数后,变量有了初始值,但是不能称其为对对象的初始化,只能称其为对变量的赋值。
因为初始化只能初始化一次,而赋值可以多次赋值。
所以下面给出一个真正的初始化操作:初始化列表。
1.2初始化列表
初始化列表是构造函数的一部分,与构造函数体赋值并没有冲突,可以共存,只是初始化列表的每个变量都只能出现一次(因为初始化只初始化一次)。
初始化列表格式:
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
比如:
class Date { public: Date(int year, int month, int day) : _year(year) , _month(month) , _day(day) {} private: int _year; int _month; int _day; };
构造函数的大括号内部仍然可以像构造函数一样写其他的东西。
注意:
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
- (1)引用成员变量
(2)const成员变量
(3)自定义类型成员(且该类没有默认构造函数时)
以下面的代码为例:
class B { public: class A { public: A(int a = 1) :_a(a) {} private: int _a; }; B(int a, int ref,int n) : _ref(ref) , _n(10) {} private: A _aobj; //没有默认构造函数 int& _ref; // 引用 const int _n; // const };
不同于其他内置类型,其他内置类型可以只定义不初始化,比如:
int a; double b; char c;
对于引用成员变量和const成员变量来说,它们有一个共同点:
在定义的时候必须初始化。
所以在类的构造函数的初始化列表中必须有这两个成员变量的存在。
而对于自定义类型,前面我们说过,编译器自己生成的默认构造函数对内置类型不做处理,对自定义类型调用它的默认构造函数。
在初始化列表中,如果该自定义类型没有默认构造函数,(默认构造函数包括:无参构造函数,全缺省构造函数和编译器自己生成的构造函数。其中如果我们不写构造函数,编译器才会自己生成)那么在初始化列表必须要对自定义类型初始化。
如果不初始化,会报错,比如:
在这个例子中,A类的构造函数并不是默认构造函数,而是一个普通的构造函数,并且在B类的构造函数的初始化列表中并没有对A类进行初始化,这就会产生报错。
总结:
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
类中包含以下成员,必须放在初始化列表位置进行初始化:
(1)引用成员变量
(2)const成员变量
(3)自定义类型成员(且该类没有默认构造函数时)
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
第四点是非常重要的一点,下面给一道题:
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项。首先排除程序会崩溃。
程序崩溃的原因不多,主要就是动态申请的资源泄露了或者产生死循环等等。
明显本题并没有以上情况。这道题既然能放在这里,说明该题可能会有坑,可以排除A,那就从CD之间选择。认真看代码,发现没有什么语法错误,不会编译不通过,只能选一个看似最离谱的答案D。
解析:类A实例化一个对象aa并显式地传递一个1给形参a,调用它的构造函数,此时在初始化列表中
看似先初始化a1,再初始化a2,实际上是先初始化a2,再初始化a1,因为初始化的顺序取决于成员变量的声明顺序的!
所以:成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
1.3explicit关键字
explicit关键字的作用在于禁止将会禁止构造函数的隐式转换。
对于什么是隐式转换,请看下面的代码:
class Date { public: explicit Date(int year) :_year(year) {} //赋值运算符重载 Date& operator=(const Date& d) { if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; } private: int _year; int _month; int _day; }; int main() { Date d2 = 2; }
实例化的类对象中,看似是将变量2赋值给对象d2,实际上的情况是:对于不同类型的赋值,会在中间生成一个临时空间,整型2发生了隐式类型转换,升级成了类对象,这个类对象2调用构造函数,此时这块临时空间就是一个对象,然后该临时空间再调用拷贝构造函数拷贝给d2。
这个过程是:构造–>拷贝构造。
但是编译器会自动优化,优化成直接进行构造。
对于编译器的优化,我们会在下面的篇幅详细地讲到。
对于explicit关键字,就是禁止编译器对于这种隐式类型转化。
当我们加上exiplict关键字后,编译器会直接报错。
二、🍕static成员
被static修饰的成员变量叫做静态成员变量,被修饰的成员函数叫做静态成员函数。普通成员变量属于对象自己所有,静态成员变量属于每个类对象所共有的。
注意:1.静态成员变量一定要在类外面进行初始化。
2.静态成员变量或静态成员函数可以直接通过类域访问。(因为它属于整个类)
对于静态成员变量, 如果设置成公有,那就跟全局变量几乎一样,都可以被类外面进行访问。
下面有一道面试题:实现一个类,计算程序中出现了多少类对象。
为了完成这道题目,我们实现的类不能在外面计算出现的类的次数。只能在类内计算,如果调用了构造函数或者拷贝构造函数,就让一个计数变量++,如果调用了析构函数,就让计数变量–。很明显需要用静态成员函数才能实现,为了获得该成员变量,我们需要在对象内实现一个静态成员函数,来跟静态成员变量进行配套使用。
静态成员函数的特点:
1.没有this指针
2.指定类域和访问限定符即可访问。
所以可以通过下面的代码来计算类的对象的个数。
class A { public: A() { ++_scount; } A(const A& t) { ++_scount; } ~A() { --_scount; } static int GetACount() { return _scount; } private: static int _scount; };
另一道题目:
设计一个类,在类外面只能在栈/堆上创建一个对象。
class A { public: static A GetStackObj() { A a; return a; } static A* GetHeapObj() { return new A; } private: A() {} private: int _a1; int _a2; }; int main() { A::GetStackObj(); A::GetHeapObj(); return 0; }
解析:将构造函数设置成私有的,那么所有的类型的对象都不能在类外面实例化
但是又将栈/堆的成员函数设置成静态公有的,使得我们可以在类外面直接通过类作用限定符
//直接调用该静态成员函数,达到题目要求。
再补充几点:
1.不能通过在类中给静态成员变量缺省值,因为这个地方只是静态成员变量的声明,并且缺省值实际上是给初始化列表的,对于普通成员变量来说,可以在构造函数的初始化列表中进行初始化,而静态成员变量则无法实现。
2.可以通过普通成员函数访问静态成员函数,因为对于静态成员函数来说,只要给定类域和访问限定符,就可以访问静态成员函数,在类中是不受访问限定符的限制的,也不受类域的限制,因为对于类来说这是一个整体;但不能通过静态成员函数访问普通成员函数,因为静态成员函数没有this指针,无法访问普通成员函数和成员变量。
三、🍕友元
友元的出现突破了类的封装机制,友元就像是类的朋友,可以随意访问类的非公有成员变量和成员函数,所以友元不适合多用。
友元分为友元函数和友元类。
友元函数:
我们知道,一般内置类型的输出可以使用
cout << 内置类型
的方式进行输出。但是对于一个自定义类型,却不能定义成类的成员函数:operator<<。因为在成员函数中有一个隐含的this指针,该指针会一直占用第一个位置,导致左操作数只能是cout。
所以如果要定义成成员函数,只能实现成 d1 << cout。(假设d1是一个日期类对象)。
如果定义成全局函数,就不再有this指针,左操作数就可以正常地中作为cout。但是重载成全局函数又导致我们无法访问到类地成员变量,这就需要友元函数的出现。
友元函数关键字:friend。
例如:
class Date { //友元函数的声明 friend ostream& operator<<(ostream& _cout, const Date& d); friend istream& operator>>(istream& _cin, Date& d); public: 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& _cout, const Date& d) { _cout << d._year << "-" << d._month << "-" << d._day; return _cout; } istream& operator>>(istream& _cin, Date& d) { _cin >> d._year; _cin >> d._month; _cin >> d._day; return _cin; } int main() { Date d; cin >> d; cout << d << endl; return 0; }
注意:
1.友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
2.友元函数可访问类的私有和保护成员,但不是类的成员函数
3.友元函数不能用const修饰
4.友元函数可以在类定义的任何地方声明,不受类访问限定符限制
5.一个函数可以是多个类的友元函数
6.友元函数的调用与普通函数的调用原理相同
四、🍕内部类
如果一个类定义在外部类的内部,那么这个类就叫做内部类。内部类不属于外部类,外部类不能通过任何途径访问内部类的成员。
注意:内部类天生就是外部类的友元,但外部类不是内部类的友元。
特性
sizeof(外部类) == 外部类,计算一个类的大小,与内部类完全没有任何关系,结果是外部类的大小。
比如:
class A { private: static int k; int h; public: class B // B天生就是A的友元 { public: void foo(const A& a) { cout << k << endl;//OK cout << a.h << endl;//OK } }; }; int A::k = 1; int main() { A a; cout << sizeof(a) << endl; return 0; }
结果是4;
解析:对于外部类来说,内部类和外部类没有声明关系,因为内部类只是一个声明,并没有真正意义地创建内部类对象。相当于A类是一个图纸,通过A类这个图纸建造了a这所房子,但是B是A类的图纸里面的一张图纸,并没有通过B这个图纸真正创造一所房子。
特性2:
内部类受外部类的访问限定符的限制。
特性3:
建议内部类的成员变量声明在外部类,这样内部类既可以使用它的成员变量,外部类也可以使用内部类的成员变量。
class Solution { public: int Sum_Solution(int n) { Sum a[n]; return _sum; } class Sum { public: Sum() { _sum+=_i; ++_i; } }; private: static int _i; static int _sum; }; int Solution::_i = 1; int Solution::_sum = 0;
五、🍕匿名对象
请看下面的代码以及注释:
class A { public: A(int a = 0) :_a(a) { cout << "A(int a)" << endl; } ~A() { cout << "~A()" << endl; } private: int _a; }; class Solution { public: int Sum_Solution(int n) { //... return n; } }; int main() { A aa1; // 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义 // A aa1(); // 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字, // 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数 A(); A aa2(2); //不能这样调用: //Solution.Sum_Solution(10); //Solution::.Sum_Solution(10); //对于第1种情况,不能直接使用类型调用它的成员函数 //对于第二种情况,只有static成员函数或者成员变量可以这样调用,因为静态成员函数没有this指针,而普通成员函数需要传递this指针,如果这样调用,就无法传递this指针。 Solution().Sum_Solution(10); return 0; }
匿名对象特性:
1.匿名对象的生命周期在当前行
有名对象的生命周期在当前的局部作用域
2.匿名对象类似于临时对象,都具有常性
3const引用延长了匿名对象的生命周期
1.匿名对象的生命周期只有1行 A(2); 2.匿名对象具有常性,如果这样定义结果是权限放大了,不行 A& pa = A(3); const A& ra = A(3);//这样就可以,const修饰的变量具有常性,这样是权限平移。 3.const引用修饰的匿名对象延长了它的生命周期,使其生命周期延长到当前函数作用域。
六、🍕拷贝对象时编译器的一些优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝。
几种优化情况:
1.同一行一个表达式中连续的构造+构造会优化成为1个构造。
2.同一行一个表达式中连续的构造+拷贝构造会优化成为1个构造
3.不同行的构造+赋值重载不会优化
class A { public: A(int a = 0) :_a(a) { cout << "A(int a)" << endl; } A(const A& aa) :_a(aa._a) { cout << "A(const A& aa)" << endl; } A& operator=(const A& aa) { cout << "A& operator=(const A& aa)" << endl; if (this != &aa) { _a = aa._a; } return *this; } ~A() { cout << "~A()" << endl; } private: int _a; }; void f1(A aa) {} A f2() { A aa; return aa; } int main() { // 隐式类型,连续构造+拷贝构造->优化为直接构造 f1(1); // 一个表达式中,连续构造+拷贝构造->优化为一个构造 f1(A(2)); // 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造 A aa2 = f2(); // 一个表达式中,连续拷贝构造+赋值重载->无法优化 A aa2; aa2 = f2(); return 0; }
七、🍕再谈类和对象
类是对某一类实体(对象)来进行描述的,描述该对象具有那
些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。
🍕总结
类和对象收尾工作到此结束。