创建类型 miniWiki

抽象数据类型

类 (class) 机制最基本的用途是在 charintdouble内置类型 (built-in types) 之外,创建新的抽象数据类型 (Abstract Data Type, ADT)

这里的抽象体现在:

  • 类的使用者 (user) 只需要了解并且只能访问类的接口 (interface),这些接口
    • 包括公共方法成员公共数据成员,以及二元运算符非成员接口函数
    • 通常在后缀为 .h.hpp头文件 (header) 中以源代码形式给出声明 (declaration)
  • 类的实现者 (implementor) 负责提供类的实现 (implementation)
    • 包括私有方法成员私有数据成员,以及成员方法和非成员接口函数的定义 (definition)
    • 通常在后缀为 .cc.cpp.cxx源文件 (source file) 中以源代码形式给出,也可以只提供编译生成的目标文件静态库动态库

这样做的好处是:

  • 使用者代码只依赖于接口,而不依赖于实现
    • 实现者不需要将算法细节暴露给使用者,有助于保护知识产权。
    • 使用者实现者可以同时、独立地开发和测试。
    • 实现发生变化时,不需要重新编译使用者的源代码,而只需将(重新编译实现代码)所得的目标文件链接进使用者的目标文件。
  • 有助于减少不同类之间的依赖,允许各自独立变化。

访问控制

访问修饰符

一个类可以含有零个或多个访问修饰符 (access specifier),每种访问修饰符出现的次数顺序不限。 每个修饰符的作用范围起始于自己,终止于下一个修饰符或类的末尾。

访问修饰符 从类的外部 从类的内部
public 可以直接访问 可以直接访问
private (除友元外)无法直接访问 可以直接访问

默认访问级别

classstruct 都可以用来定义一个类。对于访问控制,二者的区别仅在于:

关键词 隐含的第 0 个访问修饰符
struct public
class private

friend

定义一个类时,可以用 friend 将其他(可见的)类或函数声明为它的友元,从而允许这些友元访问其私有成员。 友元声明不是函数声明。 通常,将友元声明集中放在类定义的头部或尾部。

⚠️ 友元机制破坏了类的封装,因此要少用。

类型成员

一个类可以含有类型成员,可以是已知类型的别名 (alias),也可以是定义在其内部的嵌套类 (nested class)。 类型成员必须在使用前被定义,因此通常将它们集中定义在类的头部。 类型成员与数据成员函数成员遵循相同的访问控制规则:

class Screen {
 public:
  typedef std::string::size_type Position;
 private:
  Position cursor_ = 0;
  Position height_ = 0;
  Position width_ = 0;
};

数据成员

函数成员

声明与定义

所有成员函数都必须在类的内部(通常位于头文件中)进行声明,但其定义可以放在类的外部(通常位于源文件中)。

this 指针

静态成员函数外,所有成员函数都是通过隐式指针 this 来访问调用它的那个对象的。

SalesData total;
total.isbn()
// 相当于
SalesData::isbn(&total)

const 成员函数

默认情况下,this 是指向 non-const 对象的指针,这使得相应的成员函数无法被 const 对象调用。 如果要使 this 为指向 const 对象的指针,只需要在函数形参列表后面紧跟 const 关键词。

inline 成员函数

成员函数可以是内联的 (inline)

  • 定义在类的内部的成员函数是隐式内联的。
  • 定义在类的外部的成员函数也可以是内联的,只需要在(位于同一头文件中的)函数定义前加上 inline 关键词。

其他接口函数

除了公共的方法成员,还可以在类的外部定义接口函数,最典型的是重载为普通函数的运算符。 如果需要在这些函数的实现中访问类的私有成员,则应将它们声明为 friend

static 成员

静态 (static) 成员由一个的所有对象共享,因此不属于其中任何一个对象:

  • 静态数据成员存储于所有对象的外部,不计入对象的大小。
  • 静态方法成员独立于所有对象,形参列表不含 this 指针

访问静态成员

在类的外部,静态成员可以通过紧跟在类名后面的作用域运算符 (scope operator) :: 来访问,也可以(像非静态成员一样)通过对象或指向该对象的指针引用来访问。

在类的内部,静态成员可以被所属类的成员函数直接访问,不需要借助于作用域运算符。

定义静态成员

关键词 static 仅用于在类的内部声明静态成员,而不需要在类的外部定义静态成员时重复。

静态数据成员必须在类的外部进行定义初始化。 与非内联成员函数类似,每个静态数据成员都只能被定义一次,因此应当将它们的定义放在同一个源文件中。

定义静态数据成员时,可以访问该类的私有成员。

类内初始化

通常,静态数据成员不可以在类的内部进行初始化,但有两个例外:

  • 可以static const 整型数据成员指定类内初始值。
  • 必须static constexpr 数据成员指定类内初始值。

用作类内初始值的表达式必须是 constexpr,被其初始化的静态数据成员也是 constexpr,可以用于任何需要 constexpr 的地方:

// account.h
class Account {
 private:
  static constexpr int kLength = 30;  // kLength 是 constexpr
  double table[kLength];  // 数组长度必须是 constexpr
};

即使一个静态数据成员已经在类内被初始化,通常也应在类外给出定义。 如果其初始值已经在类内给定,则类外不得再给定初始值:

// account.cpp
#include "account.h"
constexpr int Account::kLength;

特殊用法

静态数据成员的类型可以是它自己所属的那个类:

class Point {
 private:
  static Point p1_;  // 正确: 静态数据成员 可以是 不完整类型
  Point* p2_;        // 正确: 指针成员 可以是 不完整类型
  Point  p3_;        // 错误: 非静态数据成员 必须是 完整类型
};

静态数据成员可以(在声明前)被用作默认实参:

class Screen {
 public:
  Screen& clear(char c = kBackground);
 private:
  static const char kBackground;
};

构造函数

构造函数 (constructor) 是一种用于构造对象的特殊成员函数:以类名为函数名,没有返回类型。

在构造过程中,需要修改数据成员的值,因此构造函数不可以被声明为 const

默认构造函数

默认构造函数 (default constructor) 是指形参列表为空所有形参都有默认实参值的特殊构造函数。

如果没有显式地定义任何构造函数,那么编译器会隐式地定义一个合成的 (synthesized) 默认构造函数。

自 C++11 起,允许(并且推荐)在形参列表后紧跟 = default;显式地生成该构造函数。

初始化列表

class Point {
  double x_;
  double y_;
};

初始化列表 (initializer list) 位于形参列表函数体之间,用于值初始化 (value initialize) 数据成员:

// 推荐:在『初始化列表』中『初始化』数据成员
Point::Point(const double& x, const double& y)
    : x_(x), y_(y) {
}

初始化列表中成员按照它们在类的定义中出现的顺序依次进行构造。

没有出现在初始化列表中的数据成员会被默认初始化 (default initialize),即调用默认构造函数,然后才会进入函数体

// 语义相同, 但『默认初始化』得到的值,立即被函数体内的『赋值』覆盖,浪费了计算资源
Point::Point(const double& x, const double& y) {
  x_ = x;
  y_ = y;
}

const 成员、引用成员、没有默认构造函数的成员必须利用初始化列表进行初始化。

委托构造函数

委托构造函数 (delegated constructor) 在其初始化列表调用另一个构造函数,从而将构造任务委托给那个构造函数:

class Point {
 public:
  Point(const double& x, const double& y)  // 双参数构造函数
      : x_(x), y_(y) {
  }
  Point()  // 默认构造函数
      : Point(0.0, 0.0) /* 委托给双参数构造函数 */ {
  }
 private:
  double x_;
  double y_;
};

explicit 构造函数

默认情况下,只需要传入一个实参的构造函数定义了一种由形参类型当前类型的隐式类型转换。 编译器只会进行一次这种转换。

如果需要(通常应该)禁止这种隐式类型转换,只需要在构造函数头部加上关键词 explicit

namespace std{
template <class T, class Allocator = std::allocator<T>>
class vector {
 public:
  // 禁止 std::size_type 到 std::vector 的隐式类型转换:
  explicit vector(std::size_type count);
};
}  // namespace std