C++Primer笔记(第七章)

关键词: 成员函数, 构造函数, 友元, 静态成员

类 :

  • 定义在类内部的函数是隐式的inline函数.

成员函数:

成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象, 当我们调用一个成员函数时, 用请求该函数的对象地址初始化this.

任何对类成员的直接访问都被看作this的隐式引用, this是一个常量指针.

  • const成员函数 :

默认情况下, this的类型是指向类类型非常量版本的常量指针. 即this的类型为T *const, 所以按照初始化原则, 在默认情况下不能把this绑定到一个常量对象上, 因此就不能在常量对象上调用普通的成员函数.

如果想调用常量成员函数, 可以在参数列表后添加一个const, 表示this是一个指向常量的指针 , 也因此常量成员函数对于对象来说是只读的.

常量对象, 以及常量对象的引用或指针都只能调用常量成员函数.

类作用域和成员函数 :

编译器分两步处理类 : 首先编译成员的声明, 然后才轮到成员函数体, 因此成员函数体可以随意使用类中的其他成员而无需在意这些成员出现的次序.

在类的外部定义成员函数时, 成员函数的定义必须与声明匹配, 同时包含所属类名.

构造函数:

构造函数没有返回类型, 与类的名字相同, 不能被声明成const的.

  • 默认构造函数

    如果没有显式的定义构造函数 (只有当类没有声明任何构造函数时), 编译器会隐式的定义一个默认构造函数, 按照如下规则初始化类的数据成员:

    1. 如果存在类内的初始值, 用它来初始化成员
    2. 否则默认初始化该成员.

      在C++11中可以通过在参数列表后面写上= default来要求编译器生成构造函数.

      如果一个函数为所有参数都提供了默认实参, 则它实际上也定义了默认构造函数.

  • 成员初始化相关:

    如果没有在构造函数的初始值列表中显式的初始化成员, 则该成员将在构造函数体之前执行默认初始化.

    对于成员是const或者引用的情况, 必须将其初始化而非先定义再赋值.

    构造函数初始值列表只说明用于初始化成员的值, 而不限定初始化的具体执行顺序. 成员的初始化顺序和它们在类定义中的出现顺序一致, 当用某些成员初始化其他成员的时候, 要注意初始化顺序, 不要用未定义的值初始化已经定义的成员.

  • 委托构造函数

    简单的说就是构造函数调用其他构造函数.

    当一个构造函数委托给另一个构造函数时, 受委托的构造函数的初始值列表和函数体被一次执行, 假如函数体包含代码的话, 将先执行这些代码, 然后控制权才会交还给委托者的函数体.

  • 隐式的类类型转换(转换构造函数)

    如果构造函数只接受一个实参, 则它实际上定义了转换为此类类型的隐式转换机制, 但编译器只会执行一步类型转换.

    可以通过explicit关键字来屏蔽这个特性, 使得被修饰的构造函数不可用于隐式类型转换, 只能以直接初始化的方式使用. 该关键字只能用来修饰”转换构造函数”, 而且只能在类内声明构造函数时使用, 而在类外部定义时不应重复.

访问控制与封装:

  • 定义在public说明符之后的成员在整个程序内可被访问, public成员定义类的接口
  • 定义在private说明符之后的成员可以被类的成员函数访问, 但是不能被使用该类的代码访问, private部分封装了类的实现细节.

class和struct定义类唯一的区别就是默认的访问权限: 在struct中, 定义第一个访问说明符之前的成员是public的, 而在class中, 这些成员是private的.

友元:

通过友元使其他类或函数访问类的非公有成员, 在函数声明前加一条friend关键字即可.

友元声明只能出现在类定义的内部, 但是在类内出现的具体位置不限. 友元不是类的成员也不受它所在区域访问级别的约束.

友元的声明仅仅指定了访问的权限, 而非一个通常意义上的函数声明, 如果我们希望类的用户能够调用某个友元函数, 那么我们就必须在友元声明之外再专门对函数进行一次声明.

友元关系不存在传递性, B类被声明成A类的友元, B类的友元C类不能拥有访问A类的特性, 即每个类负责控制自己的友元类或友元函数.

如果一个类想把一组重载函数声明其友元, 它需要对这组函数中的每一个分别声明.

可变数据成员:

背景: 我们希望能修改类的某个数据成员, 即使它是const对象的成员.因此一个const成员函数可以改变一个可变成员的值.

可以通过在变量的声明中加入mutable关键字实现这个目的.

从const成员函数返回*this:

一个const成员函数如果以引用的形式返回*this, 那么它的返回类型将是常量引用, 这意味着不能将其嵌入到一组动作中的序列中去.

类的声明:

类可以通过暂时仅声明类而不定义来进行前向声明, 此时它是个不完全类型.

使用场景: 可以定义指向这种类型的指针或引用, 也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数.

名字查找与类的作用域:

声明中使用的名字, 包括返回类型或者参数列表中使用的名字, 都必须在使用前确保可见.

对于定义在类内部的成员函数来说(只适用于成员函数), 分两步解析其中名字:

  • 首先, 编译成员的声明
  • 直到类全部可见后才编译函数体
1
2
3
4
5
6
7
8
9
typedef double Money;
string bal;
class Account {
public:
Money balance() { return bal; }
private:
Money bal;
}
// 该例中, balance函数返回的是类成员而非string对象, 因为"直到类全部可见后才编译函数体, 此时先查找类中是否出现bal, 已经出现则返回"

在类中, 如果成员使用了外层作用域中的某个名字, 而该名字代表一种类型, 则类不能在之后重新定义该名字.

成员函数的匹配方式类似, 都是先从最小的作用域搜索起, 逐步向上扩大范围, 这里就不做赘述了. 当形参名和成员名相同时, 按照上面的规则就是先匹配到形参名, 如果想访问成员可以通过显式使用this指针来强制访问.(但是最好别这么写了……)

聚合类与字面值常量类

  • 聚合类:
    • 所有成员都是public的
    • 没有定义任何构造函数
    • 没有类内初始值
    • 没有基类和virtual函数
  • 字面值常量类: 数据成员都是字面值类型的聚合类或满足下述要求
    • 数据成员都必须是字面值类型
    • 类必须至少含有一个constexpr构造函数
    • 如果一个数据成员含有类内初始值, 则内置类型成员的初始值必须是一条常量表达式; 或者如果成员属于某种类类型, 则初始值必须使用成员自己的constexpr构造函数
    • 类必须使用析构函数的默认定义, 该成员负责销毁类的对象.

(没有想象出使用的场景应该是什么)

类的静态成员;

在成员的声明前加上关键字static使得其与类关联在一起, 它与对象无关, 对象中不包含与静态数据成员有关的数据, 静态成员对象被所有类对象共享.

静态成员函数也不与任何对象绑定在一起, 不包含this指针, 不能声明成const的, 且不能在static函数体内使用this指针.

可以使用作用域运算符, 类的对象, 引用或指针来访问类的静态成员.

成员函数不用通过作用域运算符就能直接使用静态成员.

一般来说, 我们不能在类的内部初始化静态成员, 必须在类的外部定义和初始化每个静态成员,

1
2
// 定义并初始化一个静态成员
double Account::interestRate = initRate();

可以给字面值常量类型的静态成员提供const整数类型的类内初始值.

静态数据成员可以使不完全类型, 可以是其所属的类类型, 而非静态数据成员则受到限制, 只能声明成它所属的类的指针或引用.

1
2
3
4
5
6
7
8
class Bar {
public:
//...
private:
static Bar mem1; // 正确, 静态成员可以是不完全类型
Bar *mem2; // 正确, 指针成员可以是不完全类型
Bar mem2; // 错误, 数据成员必须是完全类型
}

静态成员还可以作为默认实参.