文章目录
  1. 1. 定义抽象数据类型
    1. 1.1. 一、设计Sales_data类
    2. 1.2. 二、定义改进的Sales_data类
    3. 1.3. 三、定义类相关的非成员函数
    4. 1.4. 四、构造函数
    5. 1.5. 拷贝、赋值、析构
  2. 2. 访问控制与封装
    1. 2.1. 友元
  3. 3. 类的其他特性
    1. 3.1. 类成员再探
    2. 3.2. 返回*this的成员函数
    3. 3.3. 类类型
    4. 3.4. 友元再探
  4. 4. 类的作用域
    1. 4.1. 名字查找和类的作用域
  5. 5. 构造函数再探
    1. 5.1. 构造函数初始值列表
    2. 5.2. 委托构造函数
    3. 5.3. 默认构造函数的作用
    4. 5.4. 隐式的类类型转化
    5. 5.5. 聚合类
    6. 5.6. 字面值常量类
  6. 6. 类的静态成员
    1. 6.1. 声明静态成员
    2. 6.2. 使用类的静态成员
    3. 6.3. 定义静态成员
    4. 6.4. 静态成员类内初始值
    5. 6.5. 静态数据成员的特殊功能

主要内容:

  1. 定义抽象数据类型
  2. 访问控制与封装
  3. 类的其他特性
  4. 类的作用域
  5. 构造函数再探
  6. 类的静态成员

数据抽象能帮助我们将对象的具体实现和对象所能执行的操作分离开来。

定义抽象数据类型

一、设计Sales_data类

  1. 一个isbn成员函数,返回对象的ISBN编号
  2. 一个combine成员函数,用于将一个Sales_data对象加到另一个对象上
  3. 一个名为add的函数,执行两个Sales_data对象的加法
  4. 一个read函数,将数据从istream读入到Sales_data对象中
  5. 一个print函数,将Sales_data对象的值输入到ostream

二、定义改进的Sales_data类

定义在类内部的函数隐式的inline函数。

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

1
total.isbn()

则编译器负责把total的地址传给isbn的隐式参数this,可以等价的认为编译器将该调用重写成如下形式

1
Sales_data::isbn(&total);

this是常量指针,我们不允许改变this中保存的地址。

const成员函数: 紧随参数列表后面的const关键字表示const成员函数。这里的const的所用是修改隐式this指针的类型。 默认情况下,this的类型是指向类类型非常量版本的常量指针。这也意味着我们不能把this绑定到一个常量对象上。不能在一个常量对象上调用普通成员函数。

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

类的作用域和成员函数:

  1. 返回类型、参数列表、函数名都得于类内部的声明一样
  2. 如果成员被定义成常量成员函数,那么他的定义也必须在参数列表后明确指定const属性。
  3. 类外部定义的成员的名字必须包含所属的类名

三、定义类相关的非成员函数

IO类属于不能拷贝的类型,所以只能通过引用来传递他们,而且,因为读取和写入的操作会改变流的内容,所以两个函数接受都是普通引用,而非对常量的引用。

四、构造函数

类通过特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。构造函数的任务是初始化类对象的数据成员,无论何时,只要类的对象被创建,就会执行构造函数。

构造函数的名字和和类名相同,和其他函数不一样的是,构造函数没有返回类型。

构造函数不能声明为const的,当我们创建一个const对象时,直到构造函数完成初始化过程,对象才能真正取得常量属性,所以构造函数在const对象构造的过程中,是可以向其写值的。

合成的默认构造函数:类通过特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数,默认构造函数无需任何实参。编译器创建的构造函数又被称为合成的默认构造函数(synthesized default constructor)。合成默认构造函数初始化规则:

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

某些类不能依赖合成的默认构造函数,原因有三:

  1. 只有当类内没有声明任何构造函数时,编译器才会自动的生产默认构造函数。
  2. 如果累内部有内置类型或者符合类型成员,则只有当这些成员全部被赋予了类内初始值时,这个类才适合使用合成的默认构造函数。
  3. 有些类不能合成默认的构造函数

=default: 如果我们定义了其他的构造函数,同时,也需要默认构造函数,并且默认构造函数的功能等同于合成的默认构造函数,使用=default来要求编译器生成构造函数。如果=default在类的内部,则是内联的。

构造函数初始值列表:

1
Sales_data(const std::string & s,unsinged n, double p):bookNo(s),unit_sold(n),revenue(p*n){};

冒号和花括号之间的代码称为构造函数初始值列表。当某个数据成员被构造函数初始值列表忽略时,他将以合成默认构造函数相同的方式隐式初始化。

拷贝、赋值、析构

拷贝、赋值、析构发生的场景

  1. 拷贝:初始化变量、以值的方式传递、返回一个对象。(用同类型的对象或者自己类型的对象)
  2. 赋值:使用赋值运算符时发送。
  3. 析构:当对象不存在时销毁,超出作用域、vector容器销毁时存储在其中的对象销毁
  4. 构造: 用成员的值创建(不是自己的类型)

如果我们不主动定义这些操作。编译器将替我们合成默认的。编译器生成的版本将对对象的每个成员执行拷贝、赋值、销毁操作。

某些类不能依赖合成的版本:例如动态类型。vector、string能避免分配和释放内存带来的复杂性。如果类中包含vector、string,合成的版本能正常工作

访问控制与封装

使用访问说明符(access specifiers)控制类的封装性

  1. 定义在public说明符后面的成员在整个程序内可以被访问。public成员定义类的接口
  2. 定义在private说明符之后的成员可以被类的成员访问,但不能被类的使用者访问。private部分封装了类的实现细节。

使用class、struct关键字:class和struct定义的唯一区别是默认的访问权限。如果我们使用struct关键字,则定义在第一个访问说明符之前的成员是public的。如果是class,则是private。

友元

类可以允许其他的类或者函数访问它的非公有成员,方法是令其他类或者函数称为它的友元(friend),如果类想把一个函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语句即可。

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

封装的优点:

  1. 用户代码不会无意间破坏封装对象的状态
  2. 被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。

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

类的其他特性

这些特性包括:类型成员、类的成员的类内初始值、可变数据成员、从成员返回this* 、关于如何定义并使用类类型及友元类。

类成员再探

类型成员: 某种类型在类中的别名。类定义的类型名字和其他成员一样,存在访问控制。

1
2
3
4
5
6
7
8
class Screen{
public:
typedef std::string::size_type pos;
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
}

类型成员必须先定义后使用。 和类的其他成员的区别

因为我们已经提供了一个构造函数,所以编译器将不会自动生成默认构造函数,如果我们的类需要默认构造函数,必须显示的把他声明出来。

令成员作为内联函数:定义在类内部的成员函数自动是inline的。

我们无需在声明和定义的地方同时说明inline,单这么做是合法的。不过最好只在类的外部定义的地方说明inline,这样可以使类更容易理解。

inline成员函数也应该与相应的类定义在同一个头文件中。

重载成员函数:和普通成员函数一样。

可变数据成员: 有时,我们希望能修改类的某个数据成员,即使是在一个const成员函数。可以通过在变量的声明中加入mutable关键字做到这一点。一个可变数据成员永远不会是const的,即使const对象的成员。

类内初始值:必须使用等号或者花括号

返回*this的成员函数

从const成员函数返回this,一个const成员函数如果以引用的形式返回this,那么他的返回类型将是常量引用。

基于const的重载:通过成员函数是否是const的,我们可以对其进行重载。

类类型

类的声明: class Screen

对于类型Screen来说,在它的声明之后定义之前,是一个不完全类型(incomlete type)。也就是说,此时我们已知Screen是一个类类型,但是不清楚他到底包含哪些成员。

不完全类型只能在非常有限的情景下使用,可以定义指向这种类型的指针或引用,也可以声明(但不是定义)以不完全类型作为参数或者返回类型的函数

友元再探

类可以把其他的类定义成友元,也可以把其他类(之前定义过的)的成员函数定义成友元

类之间的友元:如果一个类定义了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。友元不存在传递性

令成员函数作为友元:

类的作用域

一旦遇到了类名,定义的剩余部分就在类的作用域之内了。这里的剩余部分包括参数列表、函数体。

函数的返回类型通常出现在函数名之前,因此,当成员杉树定义在类的外部,返回类型中使用的名字都位于类的作用域之外。

名字查找和类的作用域

名字查找(name lookup)

普通名字查找:

  1. 在名字所在的块中寻找其声明语句,只考虑名字的使用之前出现的声明
  2. 如果没有找到,继续查找外层作用域
  3. 如果最终没有找到匹配的声明,则程序报错

类的定义步骤:

  1. 首先,编译成员的声明
  2. 知道类全部可见后,编译函数体

编译器处理完类中的全部声明后,才会处理成员函数的定义

用于类成员声明的名字查找:上面两阶段的处理方式只适用于成员函数中适用的名字,声明中使用的名字,包括返回类型、参数列表中使用的名字,都必须在使用前确保可见。

1
2
3
4
5
6
7
8
9
typedef double Money;
string bal;

class Account
{
Money balance() { return bal;};

Money bal;
}

Money 出现在声明中,所有从使用处开始找名字,找到了外层的double。 balance 函数体,等成员声明编译完后,处理,所以bal是成员。不是外层的string。

类型名需要特殊处理:类内部不能重新定义外层作用域中的类型名字,类内部的类型名字定义放在类的开始处,

成员定义中的普通块作用域中的名字查找:

  1. 首先在成员函数内查找名字的声明,只有在函数使用前面出现的声明才被考虑
  2. 如果在成员函数内没有找到,则在类内继续查找,这是类的所有成员都可以考虑
  3. 如果类内也没有该名字的声明,在成员函数定义之前的作用域内继续查找。

不建议成员的名字作为某个成员函数的参数。

类作用域之后,在外围作用域中查找: 可以使用::访问隐藏的外层中的名字 。

构造函数再探

构造函数初始值列表

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

构造函数的初始值有时必不可少:有时候我们可以忽略数据成员的初始化和赋值之间的差异,单并非总是这样,如果成员有const或者引用的话,必须将其初始哈。

随着构造函数体的开始执行,初始化就完成了,我们初始化const或者引用类型的数据成员的唯一机会就是通过构造函数初始值。

成员是const、引用或者属于某种未提供默认构造函数的类型。我们必须通过构造函数初始值列表为这些成员提供初始值。

成员初始化顺序:成员的初始化顺序与他们在类定义中的出现顺序一致,构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。

委托构造函数

一个委托构造函数使用它所属类的其他构造函数执行他自己的初始化过程,或者把他自己的一些职责委托给了其他构造函数 。

1
2
Sales_data(std::string s, unsigned cnt , double price ):bookNo(s), units_sold(cnt),revenue(cnt*price){}
Sales_data():Sales_data("",0,0){}

当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体一次被执行,然后将控制权交给委托函数体。

默认构造函数的作用

当对象被默认初始化或者值初始化时,自动执行默认构造函数。 默认初始化发送的场景:

  1. 当我们在快作用域内,不适用任何初始值定义一个非静态变量。
  2. 当一个类本身还有一个类类型的成员,且使用合成的默认构造函数。
  3. 当类类型成员没有在构造函数初始值列表中显示的初始化时。

值初始化发生的场景:

  1. 在数组初始化的过程中,如果我们提供的初始值数量少于数组的大小时。
  2. 当我们不适用初始值定义一个局部静态变量时
  3. 当我们通过书写形如T()的表达式显示的请求值初始化时。

实际中,如果定义了其他的构造函数,那么最好定义一个默认构造函数。

1
2
Sales_data obj(); ///声明了一个函数,而非对象
Sales_data obj2; ///obj2是一个对象,默认初始化

隐式的类类型转化

如果构造函数只接受一个实参,则它实际上定义了转换为此类型的隐式转化机制,有时候,我们把这样的构造函数称为转换构造函数。

能通过一个实参调用的构造函数,定义了一条从构造函数的参数类型向类类型隐式转化的规则。

只允许一步类型转换

1
item.combine("9-999-9999"); //错误,两步转化,1、char * ->string 2. string->Sales_data

抑制构造函数定义的隐式转化: 我们可以通过将构造函数声明成explicit加以阻止。

关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式类型转换,所以无需将这些构造函数指定为explicit,只能在类内声明构造函数时使用explicit关键字,在类外部定义时,不应该重复。

explicit构造函数只能用于直接初始化:不能将explicit构造函数用于拷贝形式的初始化过程。当我们用explicit关键字声明构造函数时,它将只能以直接初始化的形式使用,而且,编译器不会再自动类型转换过程中使用该构造函数。

显示类型转换可以使用explicit构造函数。

标准库中含有显示构造函数的类:

  1. 接受一个参数const char*的string的构造函数不是explicit的
  2. 接受一个容量参数的vector的构造函数是explicit的。

聚合类

特定:

  1. 所有成员都是public
  2. 没有定义任何构造函数
  3. 没有类内初始值
  4. 没有基类、virturl函数。

可以提供一个花括号括起来的成员初始值列表。列表中成员的顺序和聚合类的成员的顺序一直, 如果初始值列表的元素的个数少于成员数量,后面的被值初始化。

字面值常量类

类的静态成员

声明静态成员

类的静态成员和类本身直接相关。静态函数没有this,静态成员函数不能声明成const的。而且我们也不能再static函数体内使用this指针

使用类的静态成员

  1. 使用作用域访问静态成员
  2. 通过对象访问静态成员
  3. 成员函数可以不通过作用域直接访问静态成员。

定义静态成员

我们可以在类的内部、外部定义静态成员函数。在类的外部定义静态成员时,不能重复static关键字,该关键字只能在类的内部声明语句中。静态成员不属于任何一个对象,他们不是在类创建的时候定义的,这意味着不能由类的构造函数初始化。

静态成员类内初始值

通常情况下,静态成员不应该在类的内部初始化,然而,我们可以为静态成员提供const整数类型的类内初始值。

即使一个常量静态数据成员在类内部被初始化了,通常情况下,他们也应该在类外部定义下该成员。

静态数据成员的特殊功能

  1. 可以是不完全类型
  2. 可以作为默认实参。