【C++】——类和对象(下)
一、再探构造函数
1、初始化列表
前面我们讲解构造函数的时候有提到初始化列表,在前面的构造函数中初始化成员变量主要是使用函数体内进行初始化,构造函数有一种初始化方式,就是初始化列表。初始化列表是以一个冒号开始,然后是 用逗号进行分隔的数据成员列表,每个成员变量后面跟着一个放在括号中的初始值或表达式。

2、初始化列表的使用
1、 每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为每个成员变量 定义初始化的地方。
那么上面这段话咋理解呢,前面半句的话就是成员变量在初始化列表中只能初始化一次,后面的半句的话是咋样的呢?
我们前面学习的被const关键字定义的变量我们在定义的时候必须要进行初始化。

可以看到编译器对于const没有进行初始化的直接就报错了,那么要是我们的类中的成员变量要是有个变量被const的修饰的话,那么其也会有报错。

那么这种情况就需要使用到初始化列表才可以了,其初始化就类似于在定义的时候就进行了初始化这样。

2、引用成员变量、const成员变量、没有默认构造的类类型变量,就必须放在初始化列表位置进行初始化,否则就会报错。
前面我们讲到使用栈实现队列的类的时候,我们讲到队列类中的成员变量是也是自定义类型的,对队列初始化的时候我们没有显示的实现构造函数,那么其就会去调用栈中的构造函数。不过要注意的是要是栈中没有默认构造函数,那么我们就需要在队列中去写构造函数。
所以对于成员变量是自定义类型的而且没有默认构造的类类型变量,我们也需要在初始化列表进行初始化。

3、C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表的成员使用的。
如下:

可以看到我们上面的Date类中,我们在成员变量声明中给day给了缺省值,所以我们在构造函数中,我们不传参的话就会使用这个缺省值 ,要注意的是这个缺省值是给初始化列表使用的。
我们还可以对const变量和类对象一个缺省值,而且缺省值可以是一个常量也可以是一个表达式。
当我们给一个类对象缺省值的时候,和我们在初始化列表的方式差不多。
4、尽量使用初始化列表进行初始化,这是因为那些我们不在初始化列表初始化的成员其会走初始化列表,如果这个成员在声明的位置给了缺省值,那么初始化列表会用这个缺省值初始化,如果没有给缺省值,对于没有显示的在初始化列表初始化的内置类型是否初始化取决于编译器,C++中并没有显示的在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有的话就会造成编译错误。

我们建议是要不就是全部初始化列表显示初始化,要么就是全部给缺省值,不要二者混用。
5、初始化列表中按照成员变量在类中的声明顺序进行初始化的,跟成员变量在初始化列表中出现的先后顺序是无关的。建议声明和初始化列表的顺序保持一致。
下面我们来看一道题:


首先我们的类中有两个成员变量,而且其在我们的构造函数中的初始化列表都有显示,然后我们的成员变量在声明的时候都有缺省值,所以我们的这个缺省值就不用理。然后就是我们上面说到的我们的初始化是按照声明的顺序进行初始化的,所以在初始化的时候会先进行_a2(_a1),此时我们的_a1还没有初始化的,所以我们的_a2初始化的值应该是一个随机值,然后才到我们的_a1进行初始化,所以_a1是1,然后_a2是一个随机值。
代码运行如下:

二、类型转换
1、单参数内置类型转换
C++中支持内置类型隐式转换成类类型对象,不过需要有相关的内置类型为构造函数。
如下:

那么我们实例化对象然后对成员变量进行初始化的时候可以这么写:
![]()
我们还可以和下面一样这么写:

那么这个是咋样的呢?这个是隐式类型转换,其是基于我们的构造函数来写的,如果没有构造函数的话,就不能这么写,那么我们有了这个构造函数之后,它会先经过这个函数其构造一个临时对象,然后这个临时对象再通过拷贝构造给aa2。所以其基本过程就是先进行构造然后再进行拷贝构造,不过我们的编译器会有优化,直接变成构造。
我们在构造和拷贝构造函数中加一个打印,然后我们看看编译器实际上会使用几次构造函数和拷贝构造函数。
如下:

可以看到我们的编译器优化后,隐式类型转换是没有经过拷贝构造的,这是我们的编译器进行优化的结果,并不是其没有经过拷贝构造,对于一些编译器是没有这个优化的。
那么有了这个类型转换我们可以将const和引用结合在一块使用:

要注意到是普通的引用不可以这么用,这是因为我们在类型转换的时候会产生一个临时对象,然后这个临时对象具有常性,那么我们将这个临时对象直接拷贝就扩大了这个临时对象的权限了。
我们的不同的类之间也可以进行类型转换,不过需要借助对象的构造函数。

2、多参数类型转换
前面我们的构造函数的参数都是一个的情况,那么要是我们的构造函数有多个参数的时候,要如何呢?在C++98之前是不支持多参数的情况的,在C++11后,就支持这种情况了。
不过其在语法上有点不一样:

就是在赋值的时候我们要使用一个大括号将我们的参数按顺序放在大括号里面。
3、explicit
当我们不想支持类型转换的时候,那么我们可以在对应的构造函数的前面加上explic关键字。

可以看到我们加了这个关键字后直接就报错了。
三、static成员
1、静态成员变量
使用static修饰的成员变量,其称为静态成员变量,静态成员变量一定要在类的外面进行初始化。
静态成员变量为所有类对象所共享,其不属于某个具体的对象,不存在对象中,其存放在静态区。
那么今静态成员变量其有啥作用呢?
当我们要统计一个类创建了多少个对象的时候,那么我们就可以使用静态成员变量来记录。
如下:

在构造和拷贝构造的情况下,那么我们的计数变量就进行+1,析构的话就-1。那么我们该如何访问这个成员变量呢?
那么其分两种情况:
1、 静态成员变量是公有的
这种情况我们有很多种方式来访问静态成员变量。


我们发现无论通过那个具体的对象去访问或者指定的类域去访问都可以访问到这个静态成员变量。其实际上就是在告诉编译器这个变量是定义在那个类的,其从哪里来的。还有就是这个成员变量是所有对象共享的。
2、当静态成员变量是私有的
这种情况的话,那么我们就,可以写一个成员函数,通过这个成员函数来访问这个静态成员变量。

这种方式的话,我们要在这个类已经有对象实例化的情况下才适用,这是因为我们是通过成员函数来访问的,所以对象不创建,那么就没有这个函数。
还有一种方式就是我们使用静态成员函数来访问。
2、静态成员函数
用static修饰的成员函数,称为静态成员函数,静态成员函数和普通的成员函数最大的区别就是其没有this指针。
静态成员函数中可以访问其他的静态成员,但是不能访问非静态成员,这是因为其没有this指针。
那么我们结合是上面两个理论,那么我们可以在类中定义一个静态成员函数,来访问我们的静态成员变量。

3、静态成员特点
除了上面的四点,我们的静态成变量和静态成员函数还有下面几个特点:
1、非静态的成员函数,其可以访问任意的成员变量和静态成员函数。
2、突破类域就可以访问静态成员,可以用类名::静态成员.成员变量的方式
3、静态成员也是类的成员,其也会受到访问限定符的限制。
4、静态成员变量不能在声明位置给缺省值初始化,这是因为缺省值是给构造函数初始化列表的,而静态成员变量其不属于某个对象的,其不走构造函数初始化列表。
4、练习
题目链接:求1+2+3+...+n_⽜客题霸_⽜客⽹

这个题目其要求很简单,但是要命的是其限制了我们的方法,我们不可以使用等差数列公式、循环、递归等方式进行求解。所以我们就需要另想办法。
我们可以利用我们上面的静态成员的特性来解决这个问题。
我们的静态成员是存放在静态区的,那么我们可以定义一个类sum,然后这个类中我们声明两个静态成员变量_ret和_i,然后我们的每次进行构造就+1,然后让我们的_ret+_i,直到我们的i>n的时候就可以不加了。
代码如下:
class Sum{public:Sum(){_ret+=_i;_i++;}static int Get(){return _ret;}private:static int _ret;static int _i;
};
int Sum::_i=1;
int Sum::_ret=0;
class Solution {
public:int Sum_Solution(int n) {Sum arr[n];return Sum::Get();}
};
上面要注意的是我们使用了一个变长数组,这是C99后支持的,在一些OJ平台上是支持的,我们常用的VS就不支持这种语法。
四、友元
1、概念
我们前面有讲到友元函数的部分知识,现在我们来详细学习友元的内容。我们在类外面定义的函数是无法直接访问到类里面的成员的,但是我们可以让这个函数成为我们这个类的朋友,那么我们就允许这个函数访问我们的成员了。我们称为友元函数,在语法上,我们讲这个函数的声明放在类中,一般习惯放在类的开头,然后我们在函数的声明前面加一个friend关键字即可。这种方式就叫友元声明。
下面为一些理论知识:
1、友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类。在函数声明或 者类声明的前面加上friend,并且把友元放在一个类的里面。
2、外部友元可以访问类的私有和保护成员,友元函数仅仅是一种声明,它不是类的成员。
3、友元函数可以在类定义的任何地方声明。
4、一个函数可以是多个类的友元函数
对于第四点要注意,我们要是在类A中声明了一个友元函数,然后这个友元函数其声明中,参数即有类A又有类B,而且类A在类B的前面,那么我们在类A的前面就要加上类B的声明,这是因为要是没有这个声明的话,编译器走到这个友元函数的声明的时候不认识这个类B。

5、友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成 员。
6、友元类的关系是单向的,不具有交换性,比如A是B的友元,不过B不是A的友元。
7、友元类不能传递,例如A是B的友元,B是C的友元,但是A就不是C的友元。
8、友元有时提供了便利,但是友元会增加耦合度,破坏了封装,所以友元不建议多用。
五、内部类
定义:
如果一个类定义在另一个类的内部,这个类就叫做内部类。
特点:
1、内部类是一个独立的类,跟定义在全局相比,其只受外部类域和访问限定符限制,所以外部定 义的对象不包含内部类。
2、内部类默认是外部类的友元。
3、内部类本质上也是一种封装,当A类跟B类紧密关联,B类实现出来主要就是给A类使用,那么 可以考虑将B类设计为A的内部类,如果放在private/protected位置,那么B类就是A类的专属内 部类,其他地方用不了。

可以看到我们的类B就是为了访问A的成员变量,所以我们可以将其封装在类A中。
那么我们的类A的对象实例化出来的大小是咋样的呢,需要带上类B的么?
答案是不需要,这是因为我们的内部类其实际上还是一个独立的类,所以在实例化上的对象是不包含类B的。
六、匿名对象
在前面,我们用类型+对象名(实参)的对象我们称为有名对象,在某些编程过程中,编译器自己产生的临时的对象叫临时对象,而我们用的类型(实参)定义出来的对象叫做匿名对象。

第二个就是匿名对象,匿名对象的生命周期只在当前这行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。
那么匿名对象到底有啥用处呢?

七、对象拷贝时编译器优化
现代编译器为了尽可能的提高程序的效率,在不影响正确性的情况下,其会尽可能的减少一些传参和传值返回的过程中的拷贝。
对于如何优化C++标准并没有严格的规定,各个编译器会根据情况自行处理,当前主流的的编译器对于一些连续一个表达式步骤中的连续拷贝会进行合并并且进行优化,有些更新会很激进的编译器还会进行跨行表达式的合并优化。

可以看到有些会走构造,有些会走拷贝构造,或者析构,这都是编译器优化的结果。
