对象
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 起) |
|
(自 C++23 起) |
可以在同一个存储区域中创建零个或多个对象,只要这样做会给程序定义的行为。如果无法创建,例如由于操作冲突,程序的行为将是未定义的。如果多个这样的隐式创建对象的集合会给程序定义的行为,则未指定创建哪个集合。换句话说,隐式创建的对象不需要是唯一定义的。
在指定存储区域内隐式创建对象后,某些操作会生成指向合适创建对象的指针。合适创建对象与存储区域具有相同的地址。类似地,只有在没有指针值可以为程序提供定义的行为时,行为才未定义,并且如果有多个值可以为程序提供定义的行为,则未指定生成哪个指针值。
#include <cstdlib> struct X { int a, b; }; X* MakeX() { // One of possible defined behaviors: // the call to std::malloc implicitly creates an object of type X // and its subobjects a and b, and returns a pointer to that X object X* p = static_cast<X*>(std::malloc(sizeof(X))); p->a = 1; p->b = 2; return p; }
调用std::allocator::allocate或隐式定义的union类型的复制/移动特殊成员函数也可以创建对象。
[编辑] 对象表示和值表示
某些类型和对象具有对象表示和值表示,它们在下面的表格中定义
实体 | 对象表示 | 值表示 |
---|---|---|
完整对象类型T |
由非位字段完整对象类型T 占用的N个unsigned char对象的序列,其中N为sizeof(T) |
T 的对象表示中用于表示类型T 的值的位集 |
类型T 的非位字段完整对象obj |
与T 的对象表示相对应的obj的字节 |
与T 的值表示相对应的obj的位 |
位字段对象bf | 由bf占用的N个位的序列,其中N是位字段的宽度 | T 的对象表示中用于表示bf的值的位集 |
类型或对象的对象表示中不属于值表示的位称为填充位。
对于TriviallyCopyable类型,值表示是对象表示的一部分,这意味着复制存储中对象占用的字节足以生成具有相同值的另一个对象(除非对象是潜在重叠的子对象,或者值是其类型的陷阱表示,将它加载到 CPU 会引发硬件异常,例如 SNaN(“信号非数字”)浮点值或 NaT(“非事物”)整数)。
尽管大多数实现不允许陷阱表示、填充位或整数类型的多个表示,但存在例外;例如,Itanium 上整数类型的值可能是陷阱表示。
反过来并不一定成立:具有不同对象表示的两个TriviallyCopyable类型的对象可能表示相同的值。例如,多个浮点位模式表示相同的特殊值NaN。更常见的是,填充位可能会被引入以满足对齐要求、位字段大小等。
#include <cassert> struct S { char c; // 1 byte value // 3 bytes of padding bits (assuming alignof(float) == 4) float f; // 4 bytes value (assuming sizeof(float) == 4) bool operator==(const S& arg) const // value-based equality { 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'; // modify some padding bits assert(s1 == s2); // value did not change }
对于类型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 起) |
- 潜在非唯一对象的子对象。
对于任何两个具有重叠生命周期的非位字段对象
- 如果满足以下任何条件,它们可能具有相同的地址
- 其中一个嵌套在另一个中。
- 它们中的任何一个是大小为零的子对象,并且它们的类型不相似。
- 它们都是潜在非唯一对象。
- 否则,它们始终具有不同的地址,并且占用不相交的存储字节。
// character literals are always unique static const char test1 = 'x'; static const char test2 = 'x'; const bool b = &test1 != &test2; // always true // the character 'x' accessed from “r”, “s” and “il” // may have the same address (i.e., these objects may share storage) static const char (&r) [] = "x"; static const char *s = "x"; static std::initializer_list<char> il = {'x'}; const bool b2 = r != il.begin(); // unspecified result const bool b3 = r != s; // unspecified result const bool b4 = il.begin() != &test1; // always true const bool b5 = r != &test1; // always true
[编辑] 多态对象
声明或继承至少一个虚函数的类类型的对象是多态对象。在每个多态对象中,实现会存储额外的信息(在每个现有实现中,它是一个指针,除非被优化掉),它由虚函数调用和 RTTI 特性(dynamic_cast
和 typeid
)使用,以在运行时确定创建对象的类型,无论表达式如何使用它。
对于非多态对象,值的解释是从使用对象的表达式中确定的,并在编译时决定。
#include <iostream> #include <typeinfo> struct Base1 { // polymorphic type: declares a virtual member virtual ~Base1() {} }; struct Derived1 : Base1 { // polymorphic type: inherits a virtual member }; struct Base2 { // non-polymorphic type }; struct Derived2 : Base2 { // non-polymorphic type }; int main() { Derived1 obj1; // object1 created with type Derived1 Derived2 obj2; // object2 created with type Derived2 Base1& b1 = obj1; // b1 refers to the object obj1 Base2& b2 = obj2; // b2 refers to the object 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
请求更严格的对齐(对齐要求更大)(自 C++11 起)。尝试在不满足对象类型对齐要求的存储空间中创建对象会导致未定义的行为。
为了满足 类 的所有非静态成员的对齐要求,可以在某些成员之后插入 填充位。
#include <iostream> // objects of type S can be allocated at any address // because both S.a and S.b can be allocated at any address struct S { char a; // size: 1, alignment: 1 char b; // size: 1, alignment: 1 }; // size: 2, alignment: 1 // objects of type X must be allocated at 4-byte boundaries // because X.n must be allocated at 4-byte boundaries // because int's alignment requirement is (usually) 4 struct X { int n; // size: 4, alignment: 4 char c; // size: 1, alignment: 1 // three bytes of padding bits }; // size: 8, alignment: 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 这样的机制来检测“实例-of”关系) |
没有“接口”的概念 | 有“接口”的概念(并且存在像 instanceof 这样的机制来检测是否实现了接口) |
多态需要通过虚成员显式启用 | 多态始终启用 |
在缺陷报告 P0593R6 中,在创建字节数组或调用 分配函数(可能由用户定义且为 constexpr
)时,被认为发生了隐式对象创建。但是,这种允许在常量评估中造成了不确定性,这在某些方面是不可取且不可实现的。因此,P2747R2 在常量评估中禁止了这种隐式对象创建。尽管整个论文并非如此,但我们有意地将这种更改视为缺陷报告。
[编辑] 缺陷报告
以下更改行为的缺陷报告被追溯应用于先前发布的 C++ 标准。
DR | 应用于 | 发布的行为 | 正确行为 |
---|---|---|---|
CWG 633 | C++98 | 变量只能是对象 | 它们也可以是引用 |
CWG 734 | C++98 | 未指定在 同一作用域中定义的变量 是否保证具有相同值 |
的变量具有相同的地址 地址保证不同, 无论其值如何, |
只要它们的生存期重叠。 | C++98 | CWG 1189 相同类型的 |
两个基类子对象 可能具有相同的地址 |
它们始终具有 | C++98 | 不同的地址 CWG 1861 对于窄字符类型的 |
超大位字段, |
对象表示的所有位 | C++98 | 都参与了值表示 允许填充位 |
CWG 2489 char[] 不能提供存储,但对象 |
可以在其存储空间中隐式创建 | C++98 | 对象不能在 | char[] 的存储空间中隐式创建 |
CWG 2519 | C++98 | 对象表示的定义没有涉及位字段 涉及位字段 |
CWG 2719 在未对齐的存储空间中创建对象的 |
行为尚不清楚 | 在这种情况下,行为是 | 未定义的 CWG 2753 |
C++11 |
不清楚初始化列表的 | C++98 | 后备数组是否可以与字符串字面量共享存储空间 它们可以共享存储空间 CWG 2795 |
在确定两个生存期重叠的对象 |
是否可以具有相同的地址时, | C++98 | 如果其中任何一个是 大小为零的子对象, 它们可以具有类似的不同类型 |
只允许非类似类型 |
P0593R6
先前的对象模型不支持标准库所需的许多
|