静态成员
接下来这一节又是一个小知识点——静态成员。
假设我们要为 String
添加一个“寻找字符”的功能,即寻找字符 c
第一次出现在这个字符串的第几位。它的实现是这样的:
unsigned String::find(char c) {
for (unsigned i{0}; i <= len; i++) {
if (c == str[i]) return i;
}
return /* what? */;
}
那么现在问题就出现了,如果找不到这样的字符串该返回什么?当然,按照习惯来说,大家喜欢返回 -1
;但注意到这里的返回值类型是 unsigned
,直接返回 -1
会发生一个隐式类型转换(这会把 -1
转换到无符号类型 unsigned
的最大值)貌似不太合适——因为这种隐式转换并不美观,而且对用户也不友好。
于是我们的解决办法是定义一个特殊的常量 npos
来表示找不到这样的值。
constexpr unsigned npos{4294967295}; // unsigned 类型最大值
unsigned String::find(char c) {
for (unsigned i{0}; i <= len; i++) {
if (c == str[i]) return i;
}
return npos;
}
但是这样又在全局定义域里多引入了一个名字。为了解决这个问题,静态成员(Static member)就派上用场了。形象地讲,静态成员就是将类当做命名空间来用。我们需要做的是把 npos
放到 String
的定义里面,但加上 static
关键字修饰(以区分普通成员):
class String {
private:
unsigned len;
public:
static constexpr unsigned npos{4294967295};
char* str;
String();
// [...] 其余成员函数声明
};
然后在使用的时候,就像命名空间一样使用静态成员:
#include <iostream>
int main() {
String a("Hello");
// String::npos 指明 npos 是“命名空间” String 的名字
if (a.find('a') == String::npos) {
std::cout << "string doesn't contain char a" << std::endl;
}
}
所以说,所谓的静态成员跟咱们一般说的非静态成员完全没有关系——我觉得甚至不能叫做“成员”。静态成员只是说这个变量或者函数没有定义在全局作用域,而是放在了一个“里层”的作用域里面防止名字冲突。当我们要访问这个名字的时候,需要加上 类名::
才可以。所以抛开作用域不同这一点之外,它与全局变量、函数没有区别。所以它们都拥有静态存储期。有人说,静态成员是“所有该类对象共有的成员”,我个人并不喜欢这种说法:因为静态成员跟这个类的对象毫无关系。
静态成员也可以是函数。
显然,静态成员函数和普通的非成员函数没有区别。它没有 this
指针,不能访问非静态的成员,更不能带有整体 const
限定。
不过需要注意的就是,静态成员变量在使用的时候有一些很奇怪的特性。请容许我稍微多费一些口舌:
- 成员列表中的静态成员变量大多只能是声明而非定义,以下特例除外:
- 内联的 静态成员变量允许类内定义……
- 只读成员变量一般允许类内定义(并带有一些附加限制)。
我将这些奇怪的特性总结为以上三条规则。让我们一个个来看:
第一条,静态成员大多只能是声明而不是定义。
上面这段代码,看上去没什么问题,但实际上会报一个未定义错误。这是因为,当写下不带初始化器的静态成员声明时,编译器并不会把它当成一个完整的定义,而是当做一个声明(就类似 void f();
这样)。这时如果使用了这个变量就会导致未定义错误。那么解决的办法就是写一个定义了:
像这样,将静态成员的定义单独以类外定义的形式给出总是 OK 的。但如果想要写成下面这种类内定义的形式就有些问题了:
上面的代码是编译错误。但解决的办法也很简单,参考第二条规则:内联的静态成员变量允许类内定义。让静态成员变量成为内联的很简单,只需要用 inline
关键字修饰一下:
这里的内联是链接时允许重复定义的意思。对于非内联的变量,如果在类定义内给出变量定义,则会潜在地导致其在不同翻译单元内重复定义,从而导致链接错误。于是 C++ 直接在语法层面避免了这个错误。更多的信息在链接这一章内给出。
最后一条规则给出一个常用的例外:只读成员大多是容许类内定义的;所以最初我们的 String::npos
可以直接在类内加上初始化器。
静态成员变量只读性 | 内联性 | 可以类内定义? | 可以类外定义? |
---|---|---|---|
非只读 | 非内联 | 否 | 是 |
inline 修饰 | 是 | 是 | |
const 限定 | 非内联 | 是※ | 是 |
inline 修饰 | 是 | 是 | |
constexpr 限定 | 内联 | 是 | 否 |
※ 对于 const 非内联静态成员数据,若要ODR-使用它,则必须存在一个类外定义。