对象
C++ 程序可以创建、销毁、引用、访问并操作对象。
在 C++ 中,一个对象拥有这些性质:
- 大小(可以使用
sizeof
获取); - 对齐要求(可以使用
alignof
获取); - 存储期(自动、静态、动态、线程局部);
- 生存期(与存储期绑定或者为临时)
- 类型;
- 值(可能是不确定的,例如默认初始化的非类类型);
- 一个可选的名字。
以下实体都不是对象:值,引用,函数,枚举项,类型,类的非静态成员,模板,类或函数模板的特化,命名空间,形参包,和 this。
变量 由声明引入,是对象或者是并非非静态数据成员的引用。
对象创建
对象可以使用定义、new 表达式、throw 表达式、更改联合体的活跃成员和求值要求临时对象的表达式显式创建。显式对象创建完全定义了所创建的对象。
隐式生存期类型的对象也可以由下列操作隐式创建:
- 在常量求值以外的场合 (C++26 起)开始类型 unsigned char 或 std::byte (C++17 起) 的数组生存期的操作,此时在该数组中创建这种对象,
- 调用下列分配函数,此时在分配的存储中创建这种对象:
- operator new(在常量求值以外的场合) (C++26 起)
- operator new[](在常量求值以外的场合) (C++26 起)
- std::calloc
- std::malloc
- std::realloc
(C++17 起) |
- 调用下列对象表示复制函数,此时在目标存储区域或结果中创建这种对象:
(C++20 起) |
|
(C++23 起) |
同一存储区域中可以创建零或多个对象,只要能给予程序有定义的行为即可。如果无法这样创建,例如操作冲突,那么程序行为未定义。如果多个这种隐式创建的对象的集合会给予程序有定义行为,那么不指定这些集合中的哪一个被创建。换言之,不要求隐式创建的对象是唯一定义的。
在指定的存储区域内隐式创建对象后,一些操作会生成指向已适当创建的对象 的指针。已适当创建的对象与存储区域拥有相同地址。类似地,当且仅当不存在能给予程序有定义行为的指针值时,行为才未定义;而如果有多个给予程序有定义行为的值,那么不指定产生哪个值。
#include <cstdlib> struct X { int a, b; }; X* MakeX() { // 可能的有定义行为之一: // 调用 std::malloc 隐式创建一个 X 类型对象和它的子对象 a 与 b,并返回指向该 X 对象的指针 X* p = static_cast<X*>(std::malloc(sizeof(X))); p->a = 1; p->b = 2; return p; }
调用 std::allocator::allocate,或者联合体类型的隐式定义的复制/移动特殊成员函数,也能创建对象。
对象表示与值表示
某些类型和对象具有对象表示 和值表示,它们在下表中定义:
实体 | 对象表示 | 值表示 |
---|---|---|
完整对象类型 T
|
一个 T 类型的非位域完整对象占据的 N 个 unsigned char 对象,其中 N 是 sizeof(T)
|
T 的对象表示中有参与表示 T 的值的所有位的集合
|
T 类型的非位域完整对象 obj
|
obj 中对应 T 的对象表示的字节
|
obj 中对应 T 的值表示的位
|
位域对象 bf | bf 占据的 N 个位的序列,其中 N 是该位域的宽度 | bf 的对象表示中有参与表示 bf 的值的所有位的集合 |
类型或对象的对象表示中不属于值表示的位是填充位。
对于可平凡复制类型,它的值表示是对象表示的一部分,这意味着复制该对象在存储中所占据的字节就足以产生另一个具有相同值的对象(除非这个值是该类型的一个“陷阱表示”,将它读取到 CPU 中会产生一个硬件异常,就像浮点值的 SNaN(“Signaling NaN 发信非数”)或整数值的 NaT(“Not a Thing 非事物”)。
尽管大多数实现都不允许整数的陷阱表示、填充位或多重表示,也还存在例外;例如 Itanium 上的整数类型值就可以是陷阱表示。
反过来不一定是对的:可平凡复制类型的两个具有不同对象表示的对象可能表现出相同的值。例如,浮点数有多种位模式都表示相同的特殊值 NaN。更常见的是,会为了满足对齐要求和位域的大小等目的而引入填充位。
#include <cassert> struct S { char c; // 1 字节值 // 3 字节填充位(假设 alignof(float) == 4) float f; // 4 字节值 (假设 sizeof(float) == 4) bool operator==(const S& arg) const // 基于值的相等性 { return c == arg.c && f == arg.f; } }; void f() { static_assert(sizeof(S) == 8); S s1 = {'a', 3.14}; S s2 = s1; reinterpret_cast<unsigned char*>(&s1)[2] = 'b'; // 修改部分填充位 assert(s1 == s2); // 值并未更改 }
对于 char,signed char,和 unsigned char 类型的对象,除非它们是大小过大的位域,否则它的对象表示的每个位都参与它的值表示,而且每一种位模式都表示一个独立的值(没有填充位或陷阱位,不允许值的多种表示)。
子对象
一个对象可以拥有子对象。子对象包括:
- 成员对象
- 基类子对象
- 数组元素
不是其他任何对象的子对象的对象称为完整对象。
完整对象、成员对象和数组元素也被称为最终派生对象,以便和基类子对象区分开。
对于某个类,
被统称为该类的潜在构造的子对象。
大小
如果一个子对象是基类子对象或声明有 [[no_unique_address]]
属性的非静态数据成员 (C++20 起),那么它是潜在重叠的子对象。
只有在满足以下所有条件时,对象 obj 的大小才有可能为零:
- obj 是潜在重叠的子对象。
- obj 的类型是没有虚成员函数和虚基类的类类型。
- obj 没有非零大小的子对象,也没有非零长度的无名位域。
对于满足以上所有条件的对象 obj:
- 如果 obj 是没有非静态数据成员的标准布局 (C++11 起)类类型的基类子对象,那么它的大小为零。
- 否则,由实现定义在哪些情况下 obj 的大小为零。
详情见空基类优化。
任何非零大小的非位域对象都必须占据一个或更多字节的存储,其中包括它的子对象(全部或部分)占据的所有字节。如果对象具有可平凡复制或标准布局 (C++11 起)类型,那么占据的存储必须连续。
地址
一个对象的地址 是该对象占据的第一个字节的地址,除非该对象是位域或大小为零的子对象。
一个对象能含有其他对象,该情况下被含有的对象内嵌于 前述对象。如果满足以下任意条件,那么对象 a 内嵌于另一对象 b:
- a 是 b 的子对象。
- b 为 a 提供存储。
- 存在对象 c,其中 a 内嵌于 c 而 c 内嵌于 b。
以下对象是潜在非独立对象:
- 字符串字面量对象。
|
(C++11 起) |
- 潜在非独立对象的子对象。
对于任何两个具有交叠的生存期的对象:
- 如果满足以下任意条件,那么它们的地址可能相同:
- 其中一个对象内嵌于另一个对象。
- 其中一个是大小为零的子对象,并且两个对象的类型不相似。
- 两个对象都是潜在非独立对象。
- 否则,它们的地址必然不同,并且占据的存储也不会共用字节。
// 字符串字面量都是独立的 static const char test1 = 'x'; static const char test2 = 'x'; const bool b = &test1 != &test2; // 始终是 true // 从 “r”, “s” and “il” 访问得到的字符 'x' 的地址可能相同 // (也就是说这些对象可能会共用存储) static const char (&r) [] = "x"; static const char *s = "x"; static std::initializer_list<char> il = {'x'}; const bool b2 = r != il.begin(); // 结果未指定 const bool b3 = r != s; // 结果未指定 const bool b4 = il.begin() != &test1; // 始终是 true const bool b5 = r != &test1; // 始终是 true
多态对象
声明或继承了至少一个虚函数的类类型的对象是多态对象。每个多态对象中,实现都会储存额外的信息(在所有现存的实现中,如果没被编译器优化掉的话,这就是一个指针),它被用于进行虚函数的调用,RTTI 功能特性(dynamic_cast
和 typeid
)也用它在运行时确定对象创建时所用的类型,而不管使用它的表达式是什么类型。
对于非多态对象,值的解释方式由使用对象的表达式所确定,这在编译期就已经决定了。
#include <iostream> #include <typeinfo> struct Base1 { // 多态类型:声明了虚成员 virtual ~Base1() {} }; struct Derived1 : Base1 { // 多态类型:继承了虚成员 }; struct Base2 { // 非多态类型 }; struct Derived2 : Base2 { // 非多态类型 }; int main() { Derived1 obj1; // object1 创建为类型 Derived1 Derived2 obj2; // object2 创建为类型 Derived2 Base1& b1 = obj1; // b1 指代对象 obj1 Base2& b2 = obj2; // b2 指代对象 obj2 std::cout << "b1 的表达式类型:" << typeid(decltype(b1)).name() << '\n' << "b2 的表达式类型:" << typeid(decltype(b2)).name() << '\n' << "b1 的对象类型:" << typeid(b1).name() << '\n' << "b2 的对象类型:" << typeid(b2).name() << '\n' << "b1 的大小:" << sizeof b1 << '\n' << "b2 的大小:" << sizeof b2 << '\n'; }
可能的输出:
b1 的表达式类型:Base1 b2 的表达式类型:Base2 b1 的对象类型:Derived1 b2 的对象类型:Base2 b1 的大小:8 b2 的大小:1
严格的别名使用
在很多情况下,通过类型与对象的创建类型不同的表达式来访问对象都是未定义行为,它的例子和例外请参考 reinterpret_cast。
对齐
每个对象类型都具有被称为对齐要求 的性质,它是一个非负整数(类型是 std::size_t,总是 2 的幂),表示这个类型的不同对象所能分配放置的连续相邻地址之间的字节数。
可以用 |
(C++11 起) |
每个对象类型在该类型的所有对象上强制该类型的对齐要求;可以使用 alignas
来要求更严格的对齐(更大的对齐要求) (C++11 起)。尝试在没有满足某个对象类型的对齐要求的存储中创建该类型的对象是未定义行为。
为了使类中的所有非静态成员都符合对齐要求,会在一些成员后面插入一些填充位。
#include <iostream> // S 类型的对象可以在任何地址上分配 // 因为 S.a 和 S.b 都可以在任何地址上分配 struct S { char a; // 大小:1,对齐:1 char b; // 大小:1,对齐:1 }; // 大小:2,对齐:1 // X 类型的对象只能在 4 字节边界上分配 // 因为 X.n 必须在 4 字节边界上分配 // 因为 int 的对齐要求(通常)就是 4 struct X { int n; // 大小:4,对齐:4 char c; // 大小:1,对齐:1 // 三个字节的填充位 }; // 大小:8,对齐:4 int main() { std::cout << "alignof(S) = " << alignof(S) << '\n' << "sizeof(S) = " << sizeof(S) << '\n' << "alignof(X) = " << alignof(X) << '\n' << "sizeof(X) = " << sizeof(X) << '\n'; }
可能的输出:
alignof(S) = 1 sizeof(S) = 2 alignof(X) = 4 sizeof(X) = 8
最弱的对齐(最小的对齐要求)是 char、signed char 和 unsigned char 的对齐,等于 1;所有类型中最大的基础对齐 由实现定义,并等于 std::max_align_t 的对齐 (C++11 起)。
基础对齐对于所有类型的存储期的对象都得到支持。
当使用 规定分配器 (Allocator) 类型应能正确处理过对齐类型。 |
(C++11 起) |
new 表达式和 (C++17 前)std::get_temporary_buffer 是否支持过对齐类型是由实现定义的。 |
(C++11 起) (C++20 前) |
注解
C++ 中的对象和面向对象编程(OOP)中的对象的含义有所不同:
C++ 中的对象 | OOP 中的对象 |
---|---|
可以具有任何对象类型 (见 std::is_object) |
必须具有类类型 |
没有“实例”的概念 | 有“实例”的概念(并且有类似 instanceof 这样可以检测“是...的实例”关系的机制)
|
没有“接口”的概念 | 有“接口”的概念(并且有类似 instanceof 这样可以检测是否有实现接口的机制)
|
需要通过虚成员显式启用多态 | 始终具有多态 |
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
缺陷报告 | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 633 | C++98 | 变量只能是对象 | 也可以是引用 |
CWG 734 | C++98 | 未指明在同一作用域内定义的多个变量 在保证值相同时是否可以具有相同的地址 |
在它们的生存期交叠时地址 保证不同,与值是否相同无关 |
CWG 1189 | C++98 | 两个类型相同的基类子对象可以有相同的地址 | 它们的地址不会相同 |
CWG 1861 | C++98 | 大小过大的窄字符类型的位域的对象表示的每个位依然都参与它的值表示 | 允许存在填充位 |
CWG 2489 | C++98 | char[] 不能提供存储,但是可以在它的存储中隐式创建对象 | 不能在 char[] 的存储中隐式创建对象 |
CWG 2519 | C++98 | 对象表示的定义没有考虑位域 | 考虑位域 |
CWG 2719 | C++98 | 在没有正确对齐的存储中创建对象的行为不明确 | 此时行为未定义 |
CWG 2753 | C++11 | 不明确初始化器列表的基底数组是否可以与字符串字面量共享存储 | 它们可以共享存储 |
CWG 2795 | C++98 | 当确定生存期交叠的两个对象地址是否可以相同时,如果它们 之一是大小为零的子对象,那么它们可以具有相似的不同类型 |
只允许不相似类型 |
P0593R6 | C++98 | 先前的对象模型不支持标准库所要求的许多 有用的手法,并且与 C 中的有效类型不兼容 |
添加了隐式对象创建 |