[C++] 类和对象(4): 初始化列表、构造函数细节、static成员、友元、内部类...
Table of Contents

一、构造函数补充 Link to 一、构造函数补充

1.1 初始化列表 Link to 1.1 初始化列表

类的六大默认成员函数之一的 默认构造函数 , 是对象定义(实例化)时, 自动调用对 对象成员变量进行初始化的函数

而对象的定义其实是对对象整体的定义, 构造函数的内容是对象成员变量的赋值

|inline

这就出现了一个问题: 对象的成员变量是在哪里定义的?

这就要提出一个名词: 初始化列表. 对象成员变量的定义就是在这里实现的

1.1.1 什么是初始化列表 Link to 1.1.1 什么是初始化列表

初始化列表, 是位于构造函数 ()之下,{}之上, 定义对象成员变量的一个列表. 具体的位置是在这里:

|inline

初始化列表 以一个冒号:开始, 以逗号,分割成员变量, 成员变量以此形式位列其中: 成员变量名(初始化内容) 当调用默认构造函数, 但不进入默认构造函数内容时, 成员变量就已经定义好了

根据调试时对象的监视, 成员变量在初始化列表中未显式定义时, 编译器也是会自动经过初始化列表定义的, 但是未作初始化

这也是 编译器自动生成的默认构造函数对内置类型变量不做处理, 对自定义类型调用其默认构造函数处理 的原因

默认构造函数都是编译器自动生成的, 初始化列表肯定也是自动生成的 自动生成的初始化列表只对内置类型成员变量进行定义, 而不初始化

初始化列表中未显式定义的成员变量, 编译器也会自动定义

1.1.2 初始化列表的用途 Link to 1.1.2 初始化列表的用途

经过上面对初始化列表的介绍 可以发现对于一般的内置类型, 好像没有必要使用初始化列表进行赋初值, 赋初值的操作在构造函数内部也能够实现 那么, 初始化列表是不是就没有用呢?

答案肯定是有用 因为不是所有的变量都可以定义和赋值分离的

比如这样的变量, 还能不通过初始化列表, 而是在构造函数内部赋初值吗?

|inline

这三种类型的成员变量中 const修饰的&引用类型的很显然定义与赋值是不能分离的, 定义时必须初始化 类A的对象由于没有默认构造函数, 显然也是必须传参才能实例化的

所以, 类似这样的变量作为成员变量是必须要显式在初始化列表进行定义并初始化

即:

|inline

而在成员变量声明中的这个东西:

|inline

类的成员变量在声明处给了缺省值, 而这个缺省值就是给初始化列表使用的:

1.1.3 初始化列表执行顺序 Link to 1.1.3 初始化列表执行顺序

判断一下以下代码的输出结果是什么?

|inline

|small

1随机值

初始化列表中, _a1的定义在_a2之前, 为什么_a1有数值 _a2是随机值?

因为, 成员变量的初始化顺序是按照声明顺序来的, 并不是变量在初始化列表中的编写顺序

所以, 在上面一段代码中, _a2 先初始化为_a1, _a1后初始化, 所以 _a1有数值, _a2是随机值

1.2 构造函数中的隐式类型转换 及 explicit Link to 1.2 构造函数中的隐式类型转换 及 explicit

以只有一个成员变量的类为例 在对象实例化时, 不仅可以这样实例化:

|inline

还可以这样实例化:

|inline

第一种方式是正常调用了构造函数

而第二种方式则经历了一个过程, 隐式类型转换的过程 这种方式其实是先将 2022 使用构造函数构造了一个临时对象, 再将这个临时对象 拷贝构造至c2(编译器未优化) 如果编译器对这个过程进行了优化, 那么就直接是 对c2进行构造了

直接使用数值对象实例化是可以的, 不过如果想要禁止这种方法 可以将构造函数用 explicit 修饰 可以禁止隐式类型转换:

|inline

1.2.1 构造函数中隐式类型转换的意义 Link to 1.2.1 构造函数中隐式类型转换的意义

既然 可以直接用数值进行对象的实例化, 那么是否可以直接 类引用数值呢

答案也是可以的, 不过需要用 const 关键词修饰:

|inline

如果不加 const 修饰就会报错:

|medium

因为 直接使用数值进行对象实例化, 数值会先构造成一个临时对象, 临时对象其实是具有常性的 如果不用const修饰就加以引用, 其实是一种权限放大的操作, 是错误的

另外, 给临时对象起别名, 会使临时对象的生命周期延长至别名生命周期结束


直接使用数值(常量)是可以进行对象实例化的, 也就同时意味着可以直接使用常量 传参, 但是参数的声明需要用const修饰

PS: 直接使用常量传参在 string 中, 非常有意义:

|small

二、static 修饰类成员 Link to 二、static 修饰类成员

static 在类内使用, 用来修饰成员变量或者成员函数, 被修饰的成员会被存放入静态区 static的类成员称为类的静态成员 用static修饰的成员变量, 称之为静态成员变量static修饰的成员函数, 称之为静态成员函数

2.1 static 修饰成员变量 Link to 2.1 static 修饰成员变量

static 修饰 成员变量, 与其修饰普通变量的用法不同

随便举个例子:

|inline

static 修饰成员变量时, 需要在类外手动定义(不用加static)之后才能使用

成员变量 是在对象实例化时, 才会通过初始化列表定义的。类中只是成员变量的声明, 并不是定义

并且, 静态成员变量是不通过初始化列表定义的

初始化列表的作用是, 定义每个对象的成员变量, 使对象成功实例化。每个对象都有其各自的成员变量

静态成员变量是被 static 修饰的成员变量, 是不能频繁被定义的, 只能被定义一次 所以并不是每个对象都有各自的静态成员变量, 静态成员变量整个类中只有一个 所以 静态成员变量 的定义不经过初始化列表, 而是需要在类外手动定义

经过定义的静态成员变量 属于整个类 即, 属于此类的所有对象都可以访问类的公用的静态成员变量

以此类为例:

|inline

此类, 每调用一次 默认构造函数 , 静态成员变量_x自增1

|inline

实例化四个对象, _x自增四次, 所以四个对象访问的_x都是 4

并且, 直接通过类 访问 _x 也是没问题的

可以证明, 静态成员变量不属于任何一个对象, 而是属于整个类

2.2 static 修饰成员函数 Link to 2.2 static 修饰成员函数

类中, 为了限制直接对成员变量进行访问, 所以一般都会将成员变量设为私有, 包括静态成员变量

而静态成员变量又不属于任何一个对象, 所以静态成员变量设置为私有的话, 是无法通过某个对象或类来直接访问的:

所以, 对于静态成员变量通常会通过函数来专门操作

|inline

但是, 对于 静态成员变量, 它是可以在没有对象的情况下通过类来访问的 而一般的成员函数只能通过对象来调用 为了可以 直接通过类调用成员函数, 可以在成员函数前加上 static 进行修饰, 被称为 静态成员函数

|inline

静态成员函数 可以通过类直接调用 Link to 静态成员函数 可以通过类直接调用

非静态成员函数不能通过类直接调用

|inline

静态成员变量可以通过类直接调用

|inline

静态成员函数 只能操作静态成员变量 Link to 静态成员函数 只能操作静态成员变量

静态成员函数, 只能访问静态成员变量, 不能访问非静态成员变量, 因为静态成员函数没有 this指针

|inline

三、友元 Link to 三、友元

友元分为: 友元函数友元类

友元提供了一种突破封装的方法。即, 友元函数或友元类, 即使定义在类外部, 也可以操作类的成员

不过, 友元会破坏类的封装, 会提高类的耦合度。而类追求的是 低耦合,高内聚, 所以友元能少用就少用

3.1 友元函数 Link to 3.1 友元函数

既然友元会破坏封装, 那么就最好在必要情况下在使用友元, 否则不要轻易使用

什么是必要情况下?

3.1.1 日期类 >>、<< 重载 Link to 3.1.1 日期类 >>、<< 重载

之前对实现日期类的时候, 对几乎所有 有意义的运算符都实现了重载 不过, 却没有对 流提取>>流插入<< 这两个运算符重载, 进而实现类似与内置类型一般 cin 直接输入cout 直接输出的功能

下面就来对日期类实现一下 coutcin 的功能:

对内置类型使用的 cout <<cin >>, 其中的 <<>> 也是重载, 因为其原本的意义应该是<< 按位左移>> 按位右移

|inline

既然是重载, 那么 coutcin 就属于操作数, coutcin 是什么类型的操作数呢?

在上图中可以看出, cout 属于 ostream类, cin 属于 istream类

所以对于流插入<<流提取>> 应该这样定义重载函数:

|inline

ostream&istream& 作为类型, 分别取 _cout_cincoutcin的别名

定义完之后会发现, 无法正常使用:

|huge

原因很清楚: 因为运算符重载默认, 第一个参数为左操作数, 第二个参数为右操作数 所以 对象d 需作左操作数, ostream 类型的 cout 需作右操作数

为了正常使用, 应该让 ostream 类型的 cout 作为左操作数 所以, 就只能在类外重载>><<运算符

在类内, 非静态成员函数 默认第一个参数是 this指针, 这个规则是无法修改的 所以, 要想实现正常的 coutcin, 那么只能在类外实现 >><< 的重载函数

所以, 在类外应该这样定义:

|inline

但是, 在编译器中无法编译通过:

函数定义在类外, 无法访问类内私有成员

如果想要正常使用, 只需要将函数添加为友元函数就可以了!

|inline

然后:

|medium

这样就可以正常的使用了


在上面对日期类中 >><< 的重载, 想要正常使用两个运算符, 就必须使用友元:

 |large

像这样的函数, 就被称为友元函数

即, 在类外定义的, 在类内加上 关键词friend 重新声明的函数

但是, 关于 友元函数 有几个规则需要注意:

  1. 友元函数, 虽在类内声明, 并且可以访问类内私有和保护的成员, 但 友元函数不属于类的成员函数
  2. 一个函数, 可以同时作为多个类的友元函数
  3. 友元函数不能被 const 修饰 因为友元函数没有 this指针
  4. 友元函数可以在内类任意位置声明, 且不受类访问限定符的限制

3.2 友元类 Link to 3.2 友元类

理解了友元是什么意思之后, 友元类 也就非常容易理解了

其实就是类可以作为另一个类的友元类

还是以日期类为例, 将日期类作为时间类的友元类

|huger

友元类的所有成员函数, 都可以作为另一个类的友元函数使用

PS: 友元类只有单向性: 例如, Date类 作为 Time类 的友元类, 使 Date类的成员函数 可访问 Time类的成员, 但是 Time类的成员函数 是不能访问 Time类的成员的

四、内部类 Link to 四、内部类

内部类, 顾名思义就是 定义在类内部的类

|inline

内部类是一个独立的类, 它不属于外部类, 所以 外部类的成员函数无法访问内部类的成员

并且, 内部类就是外部类的友元类, 但 外部类不是内部类的友元类

|large

类B的对象, 可访问 类A对象的成员

内部类还需要注意:

  1. 内部类可以定义在外部类的 publicprivateprotected, 并且受这些访问限定符的限制

  2. 内部类可以直接访问外部类的 static修饰成员、枚举成员等, 不需要 类名或对象

  3. 对右内部类的类使用 sizeof(), 结果是只有外部类的大小, 不算内部类。 因为内部类是独立的, 并不属于外部类

    |wide

    sizeof(A) 不计算 类B的大小

Thanks for reading!

[C++] 类和对象(4): 初始化列表、构造函数细节、static成员、友元、内部类...

Tue Jun 28 2022
4088 字 · 17 分钟