静态成员

接下来这一节又是一个小知识点——静态成员。

假设我们要为 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;
    }
}

所以说,所谓的静态成员跟咱们一般说的非静态成员完全没有关系——我觉得甚至不能叫做“成员”。静态成员只是说这个变量或者函数没有定义在全局作用域,而是放在了一个“里层”的作用域里面防止名字冲突。当我们要访问这个名字的时候,需要加上 类名:: 才可以。所以抛开作用域不同这一点之外,它与全局变量、函数没有区别。所以它们都拥有静态存储期。有人说,静态成员是“所有该类对象共有的成员”,我个人并不喜欢这种说法:因为静态成员跟这个类的对象毫无关系。

静态成员也可以是函数。

#include <iostream>
struct A {
    static void f() { // 静态成员函数,用 static 修饰
        std::cout << "Hello" << std::endl;
    }
    static void g();  // 你也可以把定义放在类外
};
void A::g() { }       // 类外定义需要加上 类名::,但不写 static
int main() {
    A::f();           // 如同带有命名空间一样调用
}

显然,静态成员函数和普通的非成员函数没有区别。它没有 this 指针,不能访问非静态的成员,更不能带有整体 const 限定。

不过需要注意的就是,静态成员变量在使用的时候有一些很奇怪的特性。请容许我稍微多费一些口舌:

  1. 成员列表中的静态成员变量大多只能是声明而非定义,以下特例除外:
  2. 内联的 静态成员变量允许类内定义……
  3. 只读成员变量一般允许类内定义(并带有一些附加限制)。

我将这些奇怪的特性总结为以上三条规则。让我们一个个来看:

第一条,静态成员大多只能是声明而不是定义。

struct A {
    static int a; // 这是声明,不是定义!
};
int main() {
    A::a = 42;    // 错误:A::a 未定义
}

上面这段代码,看上去没什么问题,但实际上会报一个未定义错误。这是因为,当写下不带初始化器的静态成员声明时,编译器并不会把它当成一个完整的定义,而是当做一个声明(就类似 void f(); 这样)。这时如果使用了这个变量就会导致未定义错误。那么解决的办法就是写一个定义了:

struct A {
    static int a; // 是声明,不是定义
};
int A::a{0};      // 我们要在类外定义它(不带 static
int main() {
    A::a = 42;    // OK 了
}

像这样,将静态成员的定义单独以类外定义的形式给出总是 OK 的。但如果想要写成下面这种类内定义的形式就有些问题了:

struct A {
    static int a{0}; // 错误:非内联非只读的静态成员不能定义
};

上面的代码是编译错误。但解决的办法也很简单,参考第二条规则:内联的静态成员变量允许类内定义。让静态成员变量成为内联的很简单,只需要用 inline 关键字修饰一下:

struct A {
    static inline int a{0}; // 可以类内定义了
};
int main() {
    A::a = 42; // OK
}

这里的内联是链接时允许重复定义的意思。对于非内联的变量,如果在类定义内给出变量定义,则会潜在地导致其在不同翻译单元内重复定义,从而导致链接错误。于是 C++ 直接在语法层面避免了这个错误。更多的信息在链接这一章内给出。

最后一条规则给出一个常用的例外:只读成员大多是容许类内定义的;所以最初我们的 String::npos 可以直接在类内加上初始化器。

静态成员变量只读性内联性可以类内定义?可以类外定义?
非只读非内联
inline 修饰
const 限定非内联
inline 修饰
constexpr 限定内联

※ 对于 const 非内联静态成员数据,若要ODR-使用它,则必须存在一个类外定义。

最近更新:
代码未运行