Object
C++程序创建、销毁、引用、访问和操作 对象 。
在C++中,对象具有
-
大小(可通过
sizeof确定); -
对齐要求(可通过
alignof确定); - 存储期 (自动、静态、动态、线程局部);
- 生存期 (受存储期或临时对象限制);
- 类型 ;
- 值(可能是不确定的,例如 默认初始化 的非类类型);
- 可选的 名称 。
以下实体不是对象:值、引用、函数、枚举项、类型、非静态类成员、模板、类或函数模板特化、命名空间、参数包以及 this 。
一个 变量 是通过 声明 引入的、非非静态数据成员的对象或引用。
目录 |
对象创建
对象可以通过 定义 、 new 表达式 、 throw 表达式 、更改 联合体 的活动成员以及需要创建 临时对象 的表达式求值来显式创建。在显式对象创建中,被创建的对象具有唯一性定义。
隐式生存期类型的对象也可以通过以下方式隐式创建:
- 除了在常量求值期间,对于类型为 unsigned char 或 std::byte (C++17 起) 的数组,启动其生命周期的操作会在该数组中创建此类对象,
- 对以下分配函数的调用,会在分配的存储空间中创建此类对象:
-
- operator new (常量求值期间除外)
- operator new[] (常量求值期间除外)
- std::malloc
- std::calloc
- std::realloc
| (自 C++17 起) |
- 调用以下 对象表示 复制函数,此时这些对象会在目标存储区域或结果中被创建:
| (自 C++20 起) |
|
(since 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
|
与
T
的对象表示相对应的
obj
字节序列
|
与
T
的值表示相对应的
obj
比特位序列
|
| 位域对象 bf | 由 bf 所占用的 N 位序列,其中 N 是位域的宽度 | bf 对象表示中参与表示 bf 值的比特位集合 |
类型或对象在对象表示中不属于值表示部分的位称为 填充位 。
对于 可平凡复制 类型,值表示是对象表示的一部分,这意味着复制对象在存储中占用的字节就足以生成具有相同值的另一个对象(除非该对象是潜在重叠子对象,或者该值是其类型的 陷阱表示 且加载到CPU时会引发硬件异常,例如SNaN(信令非数字)浮点值或NaT(非事物)整数)。
尽管大多数实现不允许整数类型存在陷阱表示、填充位或多重表示,但存在例外情况;例如在Itanium架构上,整数类型的值 可能成为陷阱表示 。
反之则不一定成立:两个具有不同对象表示的 TriviallyCopyable 类型对象可能表示相同的值。例如,多个浮点数位模式可表示相同的特殊值 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() { 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 在何种情况下具有零大小由实现定义。
有关更多详细信息,请参阅 空基类优化 。
任何具有非零大小的非位域对象必须占据一个或多个字节的存储空间,包括被其任何子对象(完全或部分)占用的每个字节。若对象为可平凡复制 或标准布局 (since 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" 和 "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; // 使用 Derived1 类型创建的对象 obj1 Derived2 obj2; // 使用 Derived2 类型创建的对象 obj2 Base1& b1 = obj1; // b1 引用对象 obj1 Base2& b2 = obj2; // b2 引用对象 obj2 std::cout << "Expression type of b1: " << typeid(decltype(b1)).name() << '\n' << "Expression type of b2: " << typeid(decltype(b2)).name() << '\n' << "Object type of b1: " << typeid(b1).name() << '\n' << "Object type of b2: " << typeid(b2).name() << '\n' << "Size of b1: " << sizeof b1 << '\n' << "Size of b2: " << sizeof b2 << '\n'; }
可能的输出:
Expression type of b1: Base1 Expression type of b2: Base2 Object type of b1: Derived1 Object type of b2: Base2 Size of b1: 8 Size of b2: 1
严格别名
使用与对象创建时类型不同的表达式访问该对象,在许多情况下属于未定义行为,有关例外情况和示例列表,请参阅
reinterpret_cast
。
对齐
每个 对象类型 都具有称为 对齐要求 的属性,这是一个非负整数值(类型为 std::size_t ,且始终是2的幂次),表示可以分配该类型对象的连续地址之间的字节数。
|
类型的对齐要求可通过
|
(自 C++11 起) |
每个对象类型都会对其所有对象施加对齐要求
;可以通过
alignas
请求更严格的对齐(具有更大的对齐要求)
(since 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 类型被要求正确处理过度对齐类型。 |
(since C++11) |
|
是否支持过度对齐类型由实现定义,包括 new 表达式 和 (C++17前) std::get_temporary_buffer 。 |
(C++11起)
(C++20前) |
注释
C++ 中的对象与 面向对象编程(OOP) 中的对象具有不同含义:
| C++中的对象 | 面向对象编程中的对象 |
|---|---|
|
可具有任意对象类型
(参见 std::is_object ) |
必须具有类类型 |
| 不存在“实例”概念 |
具有“实例”概念(且存在如
instanceof
等机制检测“实例-属于”关系)
|
| 不存在“接口”概念 |
具有“接口”概念(且存在如
instanceof
等机制检测接口实现情况)
|
| 多态需通过虚成员显式启用 | 多态始终处于启用状态 |
在缺陷报告
P0593R6
中,曾考虑在常量求值期间创建字节数组或调用
分配函数
(可能是用户定义且为
constexpr
)时发生隐式对象创建。然而,这种允许导致了常量求值中的非确定性,这在某些方面是不被期望且无法实现的。因此,
P2747R2
禁止了在常量求值中进行此类隐式对象创建。尽管整篇论文并非缺陷报告,我们仍有意将此变更视为缺陷修复。
缺陷报告
以下行为变更缺陷报告被追溯应用于先前发布的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语言的有效类型不兼容 |
添加隐式对象创建机制 |
参见
|
C 文档
关于
Object
|