【C++基础】第四十六课:[类]类的静态成员

类的静态成员

Posted by x-jeff on July 21, 2022

【C++基础】系列博客为参考《C++ Primer中文版(第5版)》C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.类的静态成员

有的时候类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联。例如,一个银行账户类可能需要一个数据成员来表示当前的基准利率。在此例中,我们希望利率与类关联,而非与类的每个对象关联。从实现效率的角度来看,没必要每个对象都存储利率信息。而且更加重要的是,一旦利率浮动,我们希望所有的对象都能使用新值。

1.1.声明静态成员

我们通过在成员的声明之前加上关键字static使得其与类关联在一起。和其他成员一样,静态成员可以是public的或private的。静态数据成员的类型可以是常量、引用、指针、类类型等。

举个例子,我们定义一个类,用它表示银行的账户记录:

1
2
3
4
5
6
7
8
9
10
11
class Account {
public:
	void calculate() { amount += amount * interestRate; }
	static double rate() { return interestRate; }
	static void rate(double);
private:
	std::string owner;
	double amount;
	static double interestRate;
	static double initRate();
};

类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。因此,每个Account对象将包含两个数据成员:owner和amount。只存在一个interestRate对象而且它被所有Account对象共享。

类似的,静态成员函数也不与任何对象绑定在一起,它们不包含this指针。作为结果,静态成员函数不能声明成const的,而且我们也不能在static函数体内使用this指针。这一限制既适用于this的显式使用,也对调用非静态成员的隐式使用有效。比如:

1
2
3
4
5
6
7
void Account::rate(double) {
    double b;
    b = rate(); //错误,只有涉及到的rate(double)、rate()、interestRate都是非static的才正确
    b = (*this).rate(); //错误,等价于上一句,this的显式使用
    b = this->rate(); //错误,同上
    b = interestRate; //错误,只有涉及到的rate(double)、rate()、interestRate都是非static的才正确,因为如果interestRate改为非static的话,rate()返回类型就不能是static double了
}

1.2.使用类的静态成员

我们使用作用域运算符直接访问静态成员:

1
2
double r;
r = Account::rate(); //使用作用域运算符访问静态成员

虽然静态成员不属于类的某个对象,但是我们仍然可以使用类的对象、引用或者指针来访问静态成员:

1
2
3
4
5
Account ac1;
Account *ac2 = &ac1;
//调用静态成员函数rate的等价形式
r = ac1.rate(); //通过Account的对象或引用
r = ac2->rate(); //通过指向Account对象的指针

成员函数不用通过作用域运算符就能直接使用静态成员(只限于在类内定义,函数定义在类外还是不能使用静态成员):

1
2
3
4
5
6
7
class Account {
public:
	void calculate() { amount += amount * interestRate; }
private:
	static double interestRate;
	//其他成员与之前的版本一致
};

1.3.定义静态成员

和其他的成员函数一样,我们既可以在类的内部也可以在类的外部定义静态成员函数。当在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句:

1
2
3
4
void Account::rate(double newRate)
{
	interestRate = newRate;
}

自己测试发现上述代码编译出错,我自己测试的结果就是成员函数只有定义在类内时可以直接使用静态成员,定义在类外时不能。

因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类的对象时被定义的。这意味着它们不是由类的构造函数初始化的。而且一般来说,我们不能在类的内部初始化静态成员。相反的,必须在类的外部定义和初始化每个静态成员。和其他对象一样,一个静态数据成员只能定义一次。

类似于全局变量,静态数据成员定义在任何函数之外。因此一旦它被定义,就将一直存在于程序的整个生命周期中。

我们定义静态数据成员的方式和在类的外部定义成员函数差不多。我们需要指定对象的类型名,然后是类名、作用域运算符以及成员自己的名字:

1
2
3
4
5
6
7
8
//定义并初始化一个静态成员
double Account::interestRate = initRate(); 
//必须得有initRate()函数的定义(函数定义的位置无所谓,可在上述语句之前,也可在上述语句之后),否则编译报错
//比如
double Account::initRate()
{
    return 1.;
}

从类名开始,这条定义语句的剩余部分就都位于类的作用域之内了。因此,我们可以直接使用initRate函数。注意,虽然initRate是私有的,我们也能用它初始化interestRate。和其他成员定义一样,interestRate的定义也可以访问类的私有成员。

1.4.静态成员的类内初始化

通常情况下,类的静态成员不应该在类的内部初始化。然而,我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。初始值必须是常量表达式,因为这些成员本身就是常量表达式,所以它们能用在所有适合于常量表达式的地方。例如,我们可以用一个初始化了的静态数据成员指定数组成员的维度:

1
2
3
4
5
6
7
8
class Account {
public:
	static double rate() { return interestRate; }
	static void rate(double);
private:
	static constexpr int period = 30; //period是常量表达式
	double daily_tbl[period];
};

如果在类的内部提供了一个初始值,则成员的定义不能再指定一个初始值了:

1
2
//一个不带初始值的静态成员的定义
constexpr int Account::period; //初始值在类的定义内提供

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

1.5.静态成员能用于某些场景,而普通成员不能

如我们所见,静态成员独立于任何对象。因此,在某些非静态数据成员可能非法的场合,静态成员却可以正常地使用。举个例子,静态数据成员可以是不完全类型。特别的,静态数据成员的类型可以就是它所属的类类型。而非静态数据成员则受到限制,只能声明它所属类的指针或引用:

1
2
3
4
5
6
7
8
class Bar {
public:
	// ...
private:
	static Bar mem1; //正确:静态成员可以是不完全类型
	Bar *mem2; //正确:指针成员可以是不完全类型
	Bar mem3; //错误:数据成员必须是完全类型
};

静态成员和普通成员的另外一个区别是我们可以使用静态成员作为默认实参:

1
2
3
4
5
6
7
class Screen {
public:
	//bkground表示一个在类中稍后定义的静态成员
	Screen& clear( char = bkground );
private:
	static const char bkground;
};

非静态数据成员不能作为默认实参,因为它的值本身属于对象的一部分,这么做的结果是无法真正提供一个对象以便从中获取成员的值,最终将引发错误。