Lesson28 类与对象 - Part2
28.2 对象的初始化和清理
在 C++ 中,每个对象都会有初始化设置和对应的清理设置,因为使用一个没有初始化的对象或变量,后果是未知的。而在使用完对象后不加清理便销毁也是危险的。
28.2.1 聚合初始化
In general programming, an aggregate data type (also called an aggregate) is any type that can contain multiple data members. Some types of aggregates allow members to have different types (e.g. structs), while others require that all members must be of a single type (e.g. arrays).
——13.8 — Struct aggregate initialization – Learn C++
聚合体指的是可以包含多个数据成员的数据类型。有些聚合体允许允许成员具有不同的类型(例如结构体),而其他类型则要求所有成员必须是单一类型(例如数组)。
To simplify a bit, an aggregate in C++ is either a C-style array (17.7 – Introduction to C-style arrays), or a class type (struct, class, or union) that has:
- No user-declared constructors (14.9 – Introduction to constructors)
- No private or protected non-static data members (14.5 – Public and private members and access specifiers)
- No virtual functions (25.2 – Virtual functions and polymorphism)
The popular type
std::array
(17.1 – Introduction to std::array) is also an aggregate.——13.8 — Struct aggregate initialization – Learn C++
根据本节的学习情况,我们只需要知道下列是聚合体:
- 数组
- 没有访问权限为
private
或protected
的非静态数据成员 且 没有用户声明的构造函数 (下一小节介绍) 的class
和struct
。
聚合体使用一种称为聚合初始化(列表初始化)的初始化形式,允许我们直接初始化聚合的数据成员。为此,我们需要提供一个初始化列表,一个用花括号{}
括起来、以逗号分隔的值列表。比如:
1 | struct Employee |
自 C++11 后,一般变量也可以使用聚合初始化。这时一般变量只有一个数据成员,所以可且只可提供一个初始化值。
1 int x { 5 };
上面展示的是结构体的聚合初始化。那么类的聚合初始化呢?
这就有点难了。因为类成员的访问权限默认为 private
,并且最佳实践都建议我们对成员变量设为 private
,这时的类不再是一个聚合体,也就不能使用聚合初始化了。
Not allowing class types with private members to be initialized via aggregate initialization makes sense for a number of reasons:
- Aggregate initialization requires knowing about the implementation of the class (since you have to know what the members are, and what order they were defined in), which we’re intentionally trying to avoid when we hide our data members.
- If our class had some kind of invariant, we’d be relying on the user to initialize the class in a way that preserves the invariant.
——14.9 — Introduction to constructors – Learn C++
聚合初始化需要知道成员和它们的定义顺序是什么,这和类的 “数据隐藏” 的特征相违背;同时聚合初始化会覆盖成员的值。
28.2.2 构造函数
如何初始化类中 private
/protected
的成员变量呢?C++ 提供了构造函数 (Constructor),可以帮助我们完成这一目标。
构造函数是一种特殊的成员函数,主要作用在于创建类对象时为对象的成员变量赋值。
Constructors are non-static member functions declared with a special declarator syntax, they are used to initialize objects of their class types.
——Constructors and member initializer lists - cppreference.com
构造函数是一个非静态成员函数,用以初始化类对象。
构造函数具有与类相同的名称,没有返回值,也不写 void
。(构造函数 (C++) | Microsoft Learn)
Constructors have no names and cannot be called directly. They are invoked when initialization takes place, and they are selected according to the rules of initialization.
——Constructors and member initializer lists - cppreference.com
构造函数没有自己的函数名,无法被用户直接调用。构造函数在初始化阶段被编译器按初始化顺序自动调用。
默认构造函数没有参数(所以也叫 “无参构造”),按以下方式定义:
1 | class_name () {} ; |
If all of the parameters in a constructor have default arguments, the constructor is a default constructor (because it can be called with no arguments).
1
2
3
4 // Default constructor
Fraction(int numerator = 0, int denominator = 1)
: m_numerator{ numerator }, m_denominator{ denominator }
{}——14.11 — Default constructors and default arguments – Learn C++
如果一个构造函数的所有参数都有默认值,则该构造函数是一个默认构造函数。
如果类中未声明构造函数,则编译器将调用隐式默认构造函数,后者是一个 “空实现”,不做任何操作。
如果你依赖于隐式默认构造函数,请确保在类定义中初始化成员。 如果没有初始化表达式,成员会处于未初始化状态。比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using namespace std;
class Box {
public:
int Volume() {return m_width * m_height * m_length;}
private:
int m_width { 0 };
int m_height { 0 };
int m_length { 0 };
};
int main() {
Box box1; // Invoke compiler-generated constructor
cout << "box1.Volume: " << box1.Volume() << endl; // Outputs 0
}一般而言,即使不依赖于隐式默认构造函数,也最好以这种方式初始化成员。
但我们也可以定义带参数的构造函数(有参构造):
1 | class_name c_func (parameter_list) : member_init_list { function_bodies } |
由此,构造函数也可以重载:
1 | public: |
A class should only have one default constructor. If more than one default constructor is provided, the compiler will be unable to disambiguate which should be used.
——14.11 — Default constructors and default arguments – Learn C++
默认构造函数没有重载,不区分空参数表和全默认的参数表。一个类应该只有一个默认构造函数。
在调用用户定义的构造函数时,我们有三种调用方式:
-
函数调用风格
1
class_name c_func(10);
⚠️调用默认构造函数并尝试使用括号时,系统会发出警告:
1
warning C4930: prototyped function not called (was a variable definition intended?)
我们可以将
myclass md();
解释为函数声明或是对默认构造函数的调用。 因为 C++ 语法分析程序更偏向于声明,因此表达式会被视为函数声明。 此语句是最棘手的分析问题的示例。—— 构造函数 (C++) | Microsoft Learn
所以调用默认构造函数时,不要加括号!
-
类对象赋值风格
1
class_name c_func = class_name(10);
class_name(10)
是一个临时对象 (Temporary class object, OR Anonymous object)。临时对象是一个没有名称且仅存在于单个表达式持续时间内的对象,用于表达式时,临时对象是一个右值。临时对象在定义点创建,并在定义它们的完整表达式的末尾被销毁。因为我们没有明确指定要构造的类型,编译器将根据函数参数推断出必要的类型,然后隐式地将
10
转换为class_name
对象。-
隐式的类对象赋值风格
1
2class_name c_func = 10;
//和class_name c_func = class_name(10);等价
-
28.2.3 拷贝构造函数
我们知道,在 C++ 中,有两种常用的函数传参方式:按值传递和按引用传递。27.2.3 介绍了传值的构造函数,那么这一节我们来介绍传引用的构造函数。后者有专有名词,叫作 “拷贝构造函数 (Copy constructor)”。
A copy constructor is a constructor that is used to initialize an object with an existing object of the same type. After the copy constructor executes, the newly created object should be a copy of the object passed in as the initializer.
——14.14 — Introduction to the copy constructor – Learn C++
拷贝构造函数执行后,新创建的类对象将被初始化为传入的已存在的类对象。
拷贝构造函数按如下方式定义:
1 | // Copy constructor |
实际上就是传入要拷贝类对象的引用,通过引用访问类对象获取数据,然后将数据初始化给新的类对象。
It is a requirement that the parameter of a copy constructor be an lvalue reference or const lvalue reference. Because the copy constructor should not be modifying the parameter, using a const lvalue reference is preferred.
——14.14 — Introduction to the copy constructor – Learn C++
拷贝构造函数的参数必须是一个
const
引用,以免意外污染源数据。
A copy constructor should not do anything other than copy an object. This is because the compiler may optimize the copy constructor out in certain cases. If you are relying on the copy constructor for some behavior other than just copying, that behavior may or may not occur.
——14.14 — Introduction to the copy constructor – Learn C++
除了拷贝操作以外,不要在拷贝构造函数里定义任何操作,因为特定的编译器会优化掉拷贝构造函数,然后你定义的其他行为就被编译器忽略掉了。
如果用户没有提供拷贝构造函数,编译器会自动提供。编译器提供的函数称为 “隐式拷贝构造函数”
If you do not provide a copy constructor for your classes, C++ will create a public implicit copy constructor for you. By default, the implicit copy constructor will do memberwise initialization. This means each member will be initialized using the corresponding member of the class passed in as the initializer. In the example below,
fCopy.m_numerator
is initialized usingf.m_numerator
, andfCopy.m_denominator
is initialized usingf.m_denominator
.
1
2
3
4
5
6
7
8
9
10
11
12
13 public:
Fraction(int numerator=0, int denominator=1)
: m_numerator{numerator}, m_denominator{denominator}
{
}
Fraction(const Fraction& fraction)
: m_numerator{ fraction.m_numerator }
, m_denominator{ fraction.m_denominator }
{
}
Fraction f { 5, 3 }; // Calls Fraction(int, int) constructor
Fraction fCopy { f }; // Calls Fraction(const Fraction&) copy constructor——14.14 — Introduction to the copy constructor – Learn C++
默认情况下,隐式拷贝构造函数将执行成员初始化。这意味着每个成员将被初始化为传入类中相应的成员的数据。
同样有三种调用风格:
-
函数调用风格
1
class_name p1 (p2);
-
类对象赋值风格
1
class_name p1 = class_name(p2);
-
隐式类对象赋值风格
1
class_name p1 = p2;
-
⚠️使用拷贝构造函数创建的对象不能用于初始化临时对象(或者作为左值)。例如:
1
2
3
4
5
6 Fraction f{5, 3}; // Calls Fraction(int, int) constructor
Fraction fCopy{f}; // Calls Fraction(const Fraction&) copy constructor
Fraction f1 = Fraction(fCopy);
Fraction f2 = Fraction(f1);
//Fraction(f2);// error: redeclaration of 'Fraction f2'
//Fraction(f2) = 10;// error: redeclaration of 'Fraction f2'因为 C++ 的分析程序更倾向于将
Fraction(f2)
视为函数声明。
什么时候会调用拷贝构造函数呢?一般有三种情况:
- 使用一个已经创建完毕的对象来初始化一个新对象
- 按值传递的方式给函数传参
- 按值返回局部对象
我们以一个示例程序来说明后两点:
1 |
|
- 首先,程序先执行
generateFraction
函数,对类对象f
初始化,调用一次默认构造函数;然后在main
函数中创建临时类对象,调用一次拷贝构造函数,将f
传出函数(f
之后被销毁)。 - 然后,临时对象用于初始化类对象
f2
,调用一次拷贝构造函数。 - 最后,当将
f2
传递给printFraction
时,拷贝构造函数第三次被调用。
一种可能的输出是:
1 | Copy constructor called |
如果在特定开发环境中运行该程序,可能会发现复制构造函数只调用了两次。这是一种称为复制省略的编译器优化。
28.2.4 深拷贝和浅拷贝
** 浅拷贝 (Shallow copy)** 就是简单的赋值拷贝操作。这是 C++ 编译器提供的默认拷贝构造函数所使用的拷贝方式。
Because C++ does not know much about your class, the default copy constructor it provides use a copying method known as a memberwise copy (also known as a shallow copy). This means that C++ copies each member of the class individually.
——21.13 — Shallow vs. deep copying – Learn C++
In shallow copy, an object is created by simply copying the data of all variables of the original object.
![]()
——Shallow Copy and Deep Copy in C++ - GeeksforGeeks
浅拷贝通过简单地复制原始对象所有变量的数据来创建一个对象。
当类中没有涉及指针和动态内存分配时,这种拷贝方式十分有效。但一旦和指针扯上关系,浅拷贝就有很多不足:
- 浅拷贝只复制数值。在复制指针变量时,由于只拷贝了地址,副本和原本的指针变量会指向同一个内存位置 (如上图所示),在副本中的修改也会反映到原本上。这会导致意料外的数据污染。
- 单纯的数值更改还不是最恼人的。假如这个副本由于某种原因(如生存期)被销毁,根据上面的说法,其中的指针指向的内存位置也会被回收,原本的指针变量忽然变成了野指针!程序中出现了一个未定义行为!
Doing a shallow copy on pointer values in a copy constructor is almost always asking for trouble.
——21.13 — Shallow vs. deep copying – Learn C++
在复制构造函数中对指针值进行浅拷贝几乎总是自找麻烦。
所以,涉及到指针的拷贝时,我们需要使用深拷贝 (Deep copy)
深拷贝会在堆区申请空间,进行拷贝操作。这种拷贝不仅包括了原类对象内部的数据,也包括和该对象有关的外部数据。这就是 “深拷贝” 所说的 “深”:不仅拷贝数值,还拷贝关系。
A deep copy allocates memory for the copy and then copies the actual value, so that the copy lives in distinct memory from the source. This way, the copy and source are distinct and will not affect each other in any way.
——21.13 — Shallow vs. deep copying – Learn C++
深拷贝为副本分配内存,然后复制实际值,这样副本就存在于与原本不同的内存中。副本和原本相互独立,彼此之间不会以任何方式产生影响。
In Deep copy, an object is created by copying data of all variables, and it also allocates similar memory resources with the same value to the object.
![]()
——Shallow Copy and Deep Copy in C++ - GeeksforGeeks
深拷贝时,程序会为副本类对象分配和原本相近的内存资源。
编译器不生成可以深拷贝的构造函数。为了进行深拷贝,我们需要自己写一个拷贝构造函数。
1 |
|
实施深拷贝时,我们为需要用到指针的成员分配自动内存 (new
),然后将原成员数据传递给副本。对于非指针的成员,浅拷贝已经足够使用。
Classes in the standard library that deal with dynamic memory, such as std::string
, handle all of their memory management. So instead of doing your own memory management, you can just initialize or assign them like normal fundamental variables!
——21.13 — Shallow vs. deep copying – Learn C++
优先使用标准库中的类,而不是自己进行内存管理。比如 C++ 中的 str::string
。顺带一提,如果使用 C 语言风格处理字符串,那么用户定义的拷贝构造函数可能长这样:
1 | // first we need to deallocate any value that this string is holding! |
以下表格展示了浅拷贝和浅拷贝的区别:
No. | Shallow Copy | Deep copy |
---|---|---|
1. | When we create a copy of object by copying data of all member variables as it is, then it is called shallow copy | When we create an object by copying data of another object along with the values of memory resources that reside outside the object, then it is called a deep copy |
2. | A shallow copy of an object copies all of the member field values. | Deep copy is performed by implementing our own copy constructor. |
3. | In shallow copy, the two objects are not independent | It copies all fields, and makes copies of dynamically allocated memory pointed to by the fields |
4. | It also creates a copy of the dynamically allocated objects | If we do not create the deep copy in a rightful way then the copy will point to the original, with disastrous consequences. |
(来源:Shallow Copy and Deep Copy in C++ - GeeksforGeeks)
28.2.5 析构函数
假设我们有一段程序,使用类存储一些需要发送到服务器的数据。由于网络连接成本较高,所以我们希望延迟提交,先积攒一些数据再打包发送。程序设计的类可能是这样的:
1 | // This example won't compile because it is (intentionally) incomplete |
这样设计有一个问题:如果用户由于某些原因提前退出了程序,那么当前所有未发送的数据包将全部丢失!我们当然可以补救,比如说设置一些判断条件,达到条件强制发送数据。但如果这是一个开发中项目的模块呢?模块间的关系会越来越复杂,我们无法确保程序每次退出前都能运行到 n.sendData()
。
因此我们需要在对象被销毁前先进行清理工作,先做完自己需要做的事之后再销毁。
To generalize this issue, classes that use a resource (most often memory, but sometimes files, databases, network connections, etc…) often need to be explicitly sent or closed before the class object using them is destroyed. In other cases, we may want to do some record-keeping prior to the destruction of the object, such as writing information to a log file, or sending a piece of telemetry to a server. The term “clean up” is often used to refer to any set of tasks that a class must perform before an object of the class is destroyed in order to behave as expected.
——15.4 — Introduction to destructors – Learn C++
使用资源(通常是内存,但有时是文件、数据库、网络连接等)的类通常需要在销毁使用它们的类对象之前被显式地发送或关闭。在其他情况下,我们可能在对象被销毁之前进行一些记录。术语 “清理” 通常用来指代一个类在销毁其对象之前必须执行的一系列任务,以确保按预期行为。
C++ 中提供了 ** 析构函数 (Destructor)** 用以解决类对象的清理问题。它的设计目的就是允许类在对象被销毁之前执行任何必要的清理操作。
和构造函数类似,析构函数是一种特殊的成员函数。当非聚合类的对象被销毁时,该函数会被自动调用。
析构函数的声明:
1 | ~d_func() {function_bodies} |
- 和构造函数一样,析构函数必须与类名相同,前面加一个波浪号 (
~
)。没有返回值,也不写void
。 - 析构函数严格不接受任何参数,因此也不支持重载。一个类只能有一个析构函数。这和构造函数不一样。
Generally you should not call a destructor explicitly (as it will be called automatically when the object is destroyed), since there are rarely cases where you’d want to clean up an object more than once.
Destructors may safely call other member functions since the object isn’t destroyed until after the destructor executes.
——15.4 — Introduction to destructors – Learn C++一般情况下,不应该显式调用析构函数,因为对象被销毁时它将自动被调用,而且很少会有需要多次清理对象的情况。
析构函数可以安全地调用其他成员函数,因为对象在析构函数执行后才被销毁。
If a non-aggregate class type object has no user-declared destructor, the compiler will generate a destructor with an empty body. This destructor is called an implicit destructor, and it is effectively just a placeholder.
If your class does not need to do any cleanup on destruction, it’s fine to not define a destructor at all, and let the compiler generate an implicit destructor for your class.
——15.4 — Introduction to destructors – Learn C++如果用户没有定义析构函数,那么编译器会生成一个隐式析构函数。隐式析构函数是一个空函数,什么也不做。如果确定你的类在析构时不需要进行任何清理工作,则无需定义析构函数。
现在我们可以使用析构函数来优化开头的示例类了:
1 | // This example won't compile because it is (intentionally) incomplete |
NetworkData
对象将始终在被销毁之前发送它拥有的任何数据!
28.2.6 构造函数调用规则
默认情况下,C++ 编译器会为一个类添加至少三个函数:
- 默认构造函数
- 默认析构函数
- 默认拷贝构造函数
如果用户定义了三个函数中的一个,那么编译器的添加行为会发生变化:
- 如果用户定义了有参构造函数,则编译器不再提供默认构造函数,但是会提供默认拷贝构造函数
- 如果用户定义了拷贝构造函数,则编译器不再提供任何构造函数。
If a class requires a user-defined destructor, a user-defined copy constructor, or a user-defined copy assignment operator, it almost certainly requires all three.
——The rule of three/five/zero - cppreference.com
根据 “三法则”,如果你需要自己定义构造函数、拷贝构造函数或析构函数中的任意一个,那么另外两个函数多半也需要你来定义(而不是编译器默认提供)。
根据 C.20: If you can avoid defining default operations, do,如果标准库提供的类自带构造函数、拷贝构造函数和析构函数,就不要自己去定义它们。
28.2.7 成员初始化列表
我们在定义有参构造函数时已经提到 member_init_list
(成员初始化列表)。下面我们讨论初始化规则。
一个完整的有参构造函数示例如下:
1 | Foo(int x, int y) |
成员初始化列表定义在构造函数参数之后。它以冒号 (:
) 开头,然后列出每个要初始化的成员以及该变量的初始化值,成员和值之间用逗号 (,
) 分隔。
在这里必须使用聚合初始化形式(最好使用花括号,但括号也行)(使用等号的初始化在这里不起作用)。另外请注意,成员初始化列表不以分号结尾。
当实例化 foo
时,初始化列表中的成员使用指定的初始化值进行初始化。在这种情况下,成员初始化列表将 m_x
初始化为 x
的值(即 6
),并将 m_y
初始化为 y
的值(即 7
)。然后执行构造函数的主体。
C++ 为程序员提供了自定义列表格式的自由。常见的书写格式有:
1 | Foo(int x, int y) : m_x { x }, m_y { y } |
1 | Foo(int x, int y) : |
1 | Foo(int x, int y) |
这种自由是由 C++ 标准所允许的,因为编译器在初始化时不按构造函数提供的成员初始化列表顺序初始化。成员初始化列表中的成员总是按照在类内部定义的顺序进行初始化。例如:
1 | class Foo |
在 Foo
中,我们本来期望函数先初始化 m_y
,再初始化 m_x
,但实际运行起来却不是这样:
1 | Foo(-858993460, 7) # an example output |
程序先初始化了 m_x
,但这个时候用来初始化它的 m_y
自己却没有初始化,于是 m_x
被初始化为了一个奇怪的值。
To help prevent such errors, members in the member initializer list should be listed in the order in which they are defined in the class. Some compilers will issue a warning if members are initialized out of order.
It’s also a good idea to avoid initializing members using the value of other members (if possible). That way, even if you do make a mistake in the initialization order, it shouldn’t matter because there are no dependencies between initialization values.
——14.10 — Constructor member initializer lists – Learn C++
为了帮助防止此类错误,成员初始化列表中的成员应按照它们在类中定义的顺序列出。避免使用其他成员的值来初始化成员(如果可能的话)也是一个好主意。
成员可以通过几种不同的方式初始化:
- 如果一个成员在成员初始化列表中列出,则使用该初始化值
- 否则,如果成员被默认成员初始化 (default member initializer),则使用该初始化值
- 否则,成员将被默认初始化(对于基本数据类型来说,这意味着它未被初始化 (no initializer))。
这意味着如果一个成员在构造函数的成员初始化器列表中列出,同时被默认成员初始化,则成员初始化器列表的值优先。
这里是一个展示所有三种初始化方法的示例:
1 |
|
-
当构造
foo
时,只有m_x
出现在成员初始化列表中,因此m_x
首先被初始化为6
。 -
m_y
不在成员初始化列表中,但它被默认成员初始化,因此它被初始化为2
。 -
m_z
既不在成员初始化列表中,也没有被默认成员初始化,因此它使用默认初始化。
在定义构造函数时,我们也可以在函数体内为成员赋值:
1 | class Foo |
尽管在这个情况下,这将产生预期的结果,但在需要初始化成员(例如对于 const
或引用的数据成员)的情况下,赋值将不起作用。因此优先使用成员初始化列表来初始化成员,而不是在构造函数体中分配值。
新手程序员有时会在构造函数体中为成员变量赋值。
28.3 类对象作为类成员
C++ 类中的成员可以是另一个类的对象,我们称该成员为对象成员 (Members that point to or reference objects),该过程称为类的聚合 (Aggergation)。
To qualify as an aggregation, a whole object and its parts must have the following relationship:
- The part (member) is part of the object (class) 该(成员)部分属于类(对象)的一部分
- The part (member) can (if desired) belong to more than one object (class) at a time 该(成员)部分可以同时属于几个类(对象)
- The part (member) does not have its existence managed by the object (class) 该(成员)部分的存在不由类(对象)管理
- The part (member) does not know about the existence of the object (class) 该(成员)部分始终不知道类(对象)的存在
In an aggregation, we also add parts as member variables. However, these member variables are typically either references or pointers that are used to point at objects that have been created outside the scope of the class. (成员)部分可以作为成员变量添加。这些成员变量通常是引用或指针,用于指向在类作用域之外创建的对象。
例如:
1 | class A { |
类 B
中有对象 a
作为成员,A
为对象成员。
现在来看看这个示例程序:
1 |
|
- 首先,
bob
独立于department
创建。bob
调用一次构造函数 - 然后,
bob
被传递给department
的拷贝构造函数。department
调用一次拷贝构造函数 - 当
department
被销毁时,m_teacher
引用被销毁。department
调用一次析构函数 - 但
bob
本身并未被销毁,因此它仍然存在,直到在main()
中独立销毁。这时bob
调用一次
对象成员 “早出晚归”,构造先于使用它的类,析构晚于使用它的类。这从类的聚合的性质来看很自然。
对象成员的内存释放不由调用它的类管理,所以如果忘记了(或无法)在类外释放对象成员的内存,就可能导致内存泄露。
建议使用 “组合” 而非 “聚合”,以避免内存泄露。
这里提到了 “组合 (composition)” 概念。与聚合不同,组合中的成员部分一次只能属于一个(类)对象,并由(类)对象管理。我们之前定义的成员变量就属于一种 “组合”。当(类)对象被创建 / 销毁时,组合的成员也会同步被创建 / 销毁。
28.4 静态成员
当数据成员被 static
修饰时,该成员即为静态成员 (Static Member)。
When a data member is declared as static
, only one copy of the data is maintained for all objects of the class. Static data members are not part of objects of a given class type.
——Static Members (C++) | Microsoft Learn
Inside a class definition, the keyword static
declares members that are not bound to class instances. Static members of a class are not associated with the objects of the class: they are independent variables with static or thread(since C++11) storage duration or regular functions.
——static members - cppreference.com
一旦成员被 static
修饰,那么该成员便不再独属于一个类对象,而是称为一个独立的对象 / 函数,提供给所有用到该成员的类对象。换句话说,这是一个共享的成员。
静态成员分为两类:
-
静态成员变量
- 所有类对象共享一份数据
- 在编译阶段已经分配内存
- 类内声明,类外初始化
静态成员变量与任何对象无关,即使没有定义类对象,成员依然存在。这个成员在整个程序中仅有一个,具有静态存储期。
——static members - cppreference.com
因为静态成员变量本质上就是全局变量,所以你必须在类外全局范围内显式定义(可以同时初始化)静态成员
-
静态成员函数
- 所有类对象共享一个函数
- 静态成员函数只能访问静态成员变量,不能访问非静态成员变量。
静态成员函数同样和任何对象无关。在使用指针时,应将静态成员函数视为普通的函数,而不是成员函数。
如果想要使用静态成员,有三种方式:::
、.
和 ->
。
示例:
1 | class X { static int n; }; // declaration (uses 'static'), incomplete type |
类内的静态成员声明不是定义。类内声明时,静态成员只有不完整的类型,需要在类外定义时补充类型。
::
是作用域解析运算符。
推荐使用类名和作用域解析运算符 (::
) 访问静态成员。这样不需要创建类成员也可以使用静态成员。
静态成员定义不受访问权限限制:即使它在类中声明为 private
(或 protected
),我们也可以定义和初始化其值。
由于静态成员依然遵循类成员访问规则,因此对于 private
和 protected
的静态成员,我们在类外依然不能直接访问(除了定义和初始化)