在 C++ 中,使用 static(静态)关键字修饰的类成员称为 “静态成员”。普通成员属于具体的对象,创建 100 个 Student 对象就会有 100 份各自的 name。而静态成员不属于某个具体对象,它属于整个类,由所有对象共享唯一的一份。
在 C++ 的类中,静态成员主要包括以下 2 种:
- 静态成员变量:属于整个类、由所有对象共享同一份内存的变量,平时也常称为 “静态数据成员”。
- 静态成员函数:属于整个类、主要用来操作静态成员变量的函数。
C++ 静态成员变量
在 C++ 中,静态成员变量属于整个类。不管我们创建多少个对象,甚至一个对象都不创建,它在内存中都只有唯一的一份。从 C++17 标准开始,我们可以使用 inline static 直接在类的内部定义并初始化静态成员变量。
语法:
class 类名 {
public:
inline static 数据类型 成员名 = 初始值;
};
// 访问静态成员变量
类名::成员名说明:
前面我们学过使用 inline 定义内联函数,从 C++17 起,inline 也可以用来修饰静态成员变量。在 static 前面加上 inline 之后,我们就可以直接在类的内部给静态成员变量赋初始值,省去了跑到类的外部再单独写一行定义的麻烦。
定义好之后,访问静态成员变量主要有以下 2 种方式:
- 通过类名访问(推荐):格式为
类名::成员名(如Student::year)。因为静态成员变量属于整个类,我们使用这种写法一眼就能看出它是全类共享的静态成员。 - 通过对象名访问(了解):格式为
对象名.成员名(如stu1.year)。虽然语法上支持,但容易让人误以为它是某一个对象独有的普通成员变量,在实际开发中我们并不推荐这样写。
下面我们结合具体示例,看看多个对象是如何共享同一个静态成员变量的。
示例 1:共享静态成员变量
#include <iostream>
#include <string>
class Student {
public:
inline static int year = 2026; // 静态成员变量
std::string name; // 普通成员变量
};
int main() {
Student stu1;
stu1.name = "Jack";
Student stu2;
stu2.name = "Lucy";
std::cout << stu1.name << ":" << Student::year << '\n';
std::cout << stu2.name << ":" << Student::year << '\n';
// 修改静态成员变量的值
Student::year = 2027;
std::cout << "类名访问:" << Student::year << '\n';
std::cout << "stu1 访问:" << stu1.year << '\n';
std::cout << "stu2 访问:" << stu2.year << '\n';
return 0;
}运行结果如下。
Jack:2026
Lucy:2026
类名访问:2027
stu1 访问:2027
stu2 访问:2027分析:
在这个例子中,name 是普通成员变量,所以 stu1 和 stu2 各自拥有一份,分别保存 "Jack" 和 "Lucy"。而入学年份 year 是静态成员变量,整个 Student 类自始至终只有唯一的一份。
当我们执行 Student::year = 2027; 修改这份共享数据后,无论通过 Student::year 还是 stu1.year、stu2.year 访问,读取到的结果都会变成 2027。
在 C++17 之前的早期标准中,普通静态成员变量不能直接在类内赋初值,必须采用 “类内声明、类外定义并初始化” 的两步写法。对于这个例子来说,下面 2 种定义方式是等价的。
// 方式 1(推荐)
class Student {
public:
inline static int year = 2026;
};
// 方式 2
class Student {
public:
static int year; // 类内声明
};
int Student::year = 2026; // 类外定义大家阅读早期代码遇到方式 2 时要注意 2 个细节:① 类外写 int Student::year = 2026; 时开头不能再加 static;② 类外定义必须放在全局区域,不能写进 main() 函数内。平时在现代 C++ 开发中,我们优先使用方式 1 即可。
C++ 静态成员函数
在实际开发中,为了保护数据安全,我们经常会把静态成员变量设为 private。比如用来统计对象数量的变量 count,我们并不希望外部代码能随意修改它。那静态成员变量变成 private 之后,我们在类的外部应该怎么读取它呢?
这时我们就可以定义一个使用 static 修饰的 “静态成员函数”。
语法:
class 类名 {
public:
static 返回值类型 函数名(参数列表) {
// 函数体
}
};
// 调用静态成员函数
类名::函数名(实参);说明:
和静态成员变量一样,静态成员函数也属于整个类,而不绑定到某一个具体对象。调用静态成员函数最规范的方式同样是使用 类名::函数名()。即使当前还没有创建任何一个对象,我们也可以直接通过类名调用静态成员函数。
示例 2:统计对象的创建数量
#include <iostream>
#include <string>
class Student {
private:
std::string name;
inline static int count = 0; // 私有静态成员变量
public:
Student(std::string n) : name(n) {
count++; // 每创建一个对象加 1
}
// 静态成员函数
static int getCount() {
return count;
}
};
int main() {
// 未创建对象时直接通过类名调用
std::cout << "初始数量:" << Student::getCount() << '\n';
Student stu1("Jack");
std::cout << "当前数量:" << Student::getCount() << '\n';
Student stu2("Lucy");
std::cout << "当前数量:" << Student::getCount() << '\n';
return 0;
}运行结果如下。
初始数量:0
当前数量:1
当前数量:2分析:
刚进入 main() 函数时,虽然还没有创建任何 Student 对象,但静态成员 count 和 getCount() 都已经存在,因此我们直接调用 Student::getCount() 能正常输出初始值 0。
随后每创建一个对象,它的构造函数都会自动执行一次 count++,统计结果依次变成 1 和 2。如果我们想统计当前内存中实时存活的对象数量,还可以在类的析构函数里加上 count--,让对象销毁时自动减 1。
注意 如果把静态成员函数拆分为 “类内声明、类外定义”,static 关键字只需要写在类内的函数声明前面。比如在类的外部定义 int Student::getCount() 时,开头不能再重复写 static,否则会编译报错。
静态成员函数的访问限制
在使用静态成员函数时,有一条新手极易踩坑的规则:静态成员函数内部只能直接访问类的 “静态成员”,不能直接访问普通的 “非静态成员”。
为什么会有这个限制呢?结合前面学过的 this 指针就很容易理解了:
- 普通成员函数:必须由某个具体的对象(如
stu1)来调用,函数内部隐含了指向该对象的this指针,因此我们既可以访问当前对象的普通成员,也可以访问全类共享的静态成员。 - 静态成员函数:属于整个类,调用时甚至可以没有任何对象存在,因此静态成员函数内部根本没有
this指针。
假设程序里有 stu1 和 stu2 两个对象,它们各自拥有不同的 name。如果我们在静态成员函数里直接写 name,由于没有 this 指针指明当前是哪个对象,编译器根本无法确定到底应该读取谁的 name。下面我们通过一个例子来看看。
示例 3:访问非静态成员报错
#include <iostream>
#include <string>
class Student {
private:
std::string name = "Jack"; // 普通成员变量
inline static int count = 0; // 静态成员变量
public:
static void showCount() {
std::cout << count << '\n';
std::cout << name << '\n'; // 错误:访问非静态成员
}
};
int main() {
Student::showCount();
return 0;
}运行结果如下。
(报错)error: invalid use of member 'Student::name' in static member function分析:
在静态成员函数 showCount() 中,读取静态成员变量 count 完全正常。但执行到 std::cout << name 时,因为函数内部没有 this 指针,编译器无法确定 name 属于哪个对象。因此编译时直接报错,提示非法使用了普通成员(invalid use of member)。
对于这两类函数的访问权限,小伙伴们只需要记住一句话:普通成员函数既能访问普通成员,也能访问静态成员,而静态成员函数只能直接访问静态成员。如果确实要在静态成员函数里读取某个对象的普通成员,我们可以把该对象作为参数传进来,通过 对象名.成员名 来访问。
常见问题
1. 静态成员函数末尾可以加上 const 吗?
不可以。const 成员函数本质上是限制函数内部不能修改 this 指针指向的当前对象。而静态成员函数不属于任何具体对象,内部根本没有 this 指针,因此不能写成 static int getCount() const。
2. 在实际开发中,什么时候应该把成员设计为 static 静态成员?
判断标准很简单,主要看这份数据或操作是 “每个对象各自独立” 还是 “全类共享同一份”。比如学生的姓名、成绩是每个对象各自独立的,就应该定义为普通成员。而统计对象总数或全类共用的配置项需要所有对象共享同一份,就适合设计为静态成员。
