对象
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 或 联合体 类型的隐式定义的复制/移动特殊成员函数也可以创建对象。
[编辑] 对象表示和值表示
某些类型和对象具有对象表示和值表示,它们在下表中定义
实体 | 对象表示 | 值表示 |
---|---|---|
完整对象类型 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 上的整数类型的值可能是一个陷阱表示。
反之不一定成立:具有不同对象表示的 平凡可复制 类型的两个对象可能表示相同的值。例如,多个浮点位模式表示相同的特殊值 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 起)。
所有类型的存储期对象都支持基本对齐。
如果使用 分配器 类型需要正确处理过度对齐类型。 |
(自 C++11 起) |
如果 new 表达式和(直到 C++17) std::get_temporary_buffer 是否支持过度对齐类型是实现定义的。 |
(自 C++11 起) (直到 C++20) |
[编辑] 注解
C++ 中的对象与 面向对象编程 (OOP) 中的对象具有不同的含义
C++ 中的对象 | OOP 中的对象 |
---|---|
可以具有任何对象类型 (参见 std::is_object) |
必须具有类类型 |
没有“实例”的概念 | 具有“实例”的概念(并且存在诸如 instanceof 之类的机制来检测“实例”关系) |
没有“接口”的概念 | 具有“接口”的概念(并且存在诸如 instanceof 之类的机制来检测是否实现了接口) |
多态性需要通过虚成员显式启用 | 多态性始终启用 |
在缺陷报告 P0593R6 中,隐式对象创建被认为发生在常量求值期间创建字节数组或调用分配函数(可能是用户定义的并且是constexpr
)。但是,这种允许导致常量求值中的不确定性,这是不希望的并且在某些方面是无法实现的。因此,P2747R2 禁止了常量求值中的这种隐式对象创建。我们有意将这种更改视为缺陷报告,尽管整篇论文并非如此。
[编辑] 缺陷报告
以下行为变更缺陷报告被追溯应用于先前发布的 C++ 标准。
DR | 应用于 | 已发布行为 | 正确行为 |
---|---|---|---|
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 文档 关于 对象
|