类 (class) 机制最基本的用途是在 char
、int
、double
等内置类型 (built-in types) 之外,创建新的抽象数据类型 (Abstract Data Type, ADT)。
这里的抽象体现在:
.h
或 .hpp
的头文件 (header) 中以源代码形式给出声明 (declaration)。.cc
或 .cpp
或 .cxx
的源文件 (source file) 中以源代码形式给出,也可以只提供编译生成的目标文件、静态库、动态库。这样做的好处是:
一个类可以含有零个或多个访问修饰符 (access specifier),每种访问修饰符出现的次数和顺序不限。 每个修饰符的作用范围起始于自己,终止于下一个修饰符或类的末尾。
访问修饰符 | 从类的外部 | 从类的内部 |
---|---|---|
public | 可以直接访问 | 可以直接访问 |
private | (除友元外)无法直接访问 | 可以直接访问 |
class
和 struct
都可以用来定义一个类。对于访问控制,二者的区别仅在于:
关键词 | 隐含的第 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