C++程序可以创建、销毁、引用、访问并操作对象
在C++中,一个对象可以拥有这些性质
- 大小(可以用
sizeof
获取) - 对齐要求(可以使用
alignof
获取) - 存储期(自动、静态、动态、线程局部);
- 生存期(与存储期绑定或者临时)
- 类型;
- 值(可能是不确定的,例如默认初始化的非类类型);
- 名字(可选)。
以下实体都不是对象:值,引用,函数,枚举项,类型,类的非静态成员,模板,类或函数模板的特化,命名空间,形参包,和 this。
变量由声明所引入,是一个对象或不是对非静态数据成员的引用。
对象创建
对象能由定义、new 表达式、throw表达式
、更改联合体
的活跃成员和求值要求临时对象
的表达式显示创建。显式对象创建中创建的对象时唯一定义的
吟诗生存期类型
的对象也可以由下列操作隐式创建
- 开始 char、unsigned char 或 std::byte (C++17 起)数组生存期的操作,该情况下在该数组中创建这种对象,
- 调用下列分配函数,该情况下在分配的存储中创建这种对象:
- operator new
- operator new[]
- std::malloc
- std::calloc
- std::realloc
- std::aligned_alloc (C++17 起)
- 调用下列对象表示复制函数,该情况下在目标存储区域或结果中创建这种对象:
- std::memcpy
- std::memmove
- std::bit_cast (C++20 起)
#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;
}
对象表示与值表示
- 对于一个 T 类型的对象,其`对象表示 是和它开始于同一个地址,且长度为 sizeof(T) 的一段 unsigned char(或等价的 std::byte) (C++17 起)类型的对象序列。
- 对象的值表示 则是用于持有它的类型 T 的值的位的集合。
- 对于
可平凡赋值
类型,其值表示是对象表示的一部分,这意味着复制该对象在存储中所占据的字节就足以产生另一个具有相同值的对象(除非这个值是该类型的一个“陷阱表示”,将它读取到 CPU 中会产生一个硬件异常,就像浮点值的 SNaN(“Signaling NaN 发信非数”)或整数值的 NaT(“Not a Thing 非事物”))。 - 反过来不一定是对的:可平凡复制 (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() {
static_assert(sizeof(S) == 8);
S s1 = {'a', 3.14};
S s2 = s1;
reinterpret_cast<unsigned char*>(&s1)[2] = 'b'; // 更改填充的第 2 字节
assert(s1 == s2); // 值并未更改
}
- 对于
char
、signed char
和unsigned char
类型的对象,除非它们是大小过大的位域,否则其对象表示的每个位都参与其值表示,而且每一个位模式都表示一个独立的值(没有填充位或陷阱位,不允许值的多种表示)
子对象
一个对象可以拥有子对象。子对象包括
- 成员对象
- 基类子对象
- 数组对象
不是其他任何对象的子对象的对象称为完整对象
如果子对象是下面之一,则它潜在重叠
- 基类子对象,或
- 声明有 [[no_unique_address]] 属性的非静态数据成员。
(C++20 起)
完整对象、成员对象和数组对象也被称为最终派生对象,也便和基类子对象分开。既非潜在重叠亦非位域的对象的大小不能为零(基类子对象的大小可能为零,即使无 [[no_unique_address]] 也是如此 (C++20 起):参见空基类优化
一个对象能含有其他对象,该情况下被含有的对象内嵌于前序对象。若符合下列条件,则对象 a 内嵌于另一对象 b :
- a 是 b 的子对象,或
- b 为 a 提供存储,或
- 存在对象 c ,其中 a 内嵌于 c 而 c 内嵌于 b 。
任何两个具有交叠的生存期的(非位域)对象必然有不同的地址,除非其中一个对象内嵌于另一个对象,或者两个对象都是同一个完整对象中的不同类型的子对象,且其中一个是大小为零的子对象。
static const char c1 = 'x';
static const char c2 = 'x';
assert(&c1 != &c2); // 值相同,地址不同
多态对象
声明或继承了至少一个虚函数的类类型的对象是多态对象
每个多态对象中,实现都会储存额外的信息(在所有现存的实现中,如果没被编译器优化掉的话,这就是一个指针),它被用于进行虚函数的调用,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';
}
对齐
- 每个对象类型都有被称为对齐要求的性质,它是一个整数(类型为 std::size_t,总是 2 的幂),表示这个类型的不同对象所能分配放置的连续相邻地址之间的字节数
- (自C++11起)可以用
alignof
或者std::alignment_of
来查询类型的对其要求。可以使用指针对齐函数std::align
来获取某个缓冲区中经过适当对齐的指针, - 每个对象类型在该类型的所有对象上强制该类型的对齐要求,(C++11 起) 可以使用 alignas 来要求更严格的对齐(更大的对齐要求)。
- 为了使类中的所有非静态成员都符合对齐要求,会在一些成员后面插入一些填充。
#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 << "sizeof(S) = " << sizeof(S)
<< " alignof(S) = " << alignof(S) << '\n';
std::cout << "sizeof(X) = " << sizeof(X)
<< " alignof(X) = " << alignof(X) << '\n';
}
最弱的对齐(最小的对齐要求)是char、signed char 和 unsigned char
的对齐,等于 1 ;所有类型中最大的基础对齐(fundamental alignment)是实现定义的,并等于 std::max_align_t
的对齐。 (C++11 起)
当使用 alignas 使某个类型的对齐比 std::max_align_t 的更严格(更大)时,称其为具有扩展对齐要求的类型。
- 具有扩展对齐的类型或包含具有扩展对齐的非静态成员的类类型称为
过对齐
类型。 - new 表达式、 (C++17 前)std::allocator::allocate 和 std::get_temporary_buffer (C++20 前) 是否支持过对齐类型是由实现定义的。
- 以过对齐类型实例化的分配器 (Allocator) 允许在编译期发生实例化失败,在运行时抛出 std::bad_alloc 异常,静默忽略不支持的对齐要求,也允许正确地处理它们
alignof
作用
- 查询类型的对齐要求
语法
alignof( 类型标识 )
返回值:
- 返回 std::size_t 类型的值。
解释:
- 返回由类型标识所指示的类型的任何实例所要求的对齐字节数,该类型可以是完整对象类型、元素类型完整的数组类型或者是到这些类型之一大的引用类型
- 若类型为引用类型,则运算符返回被引用类型的对齐;
- 若类型为数组类型,则返回元素类型的对齐要求。
#include <iostream>
struct Foo {
int i;
float f;
char c;
};
// 注:下面的 `alignas(alignof(long double))` 如果需要可以简化为
// `alignas(long double)`
struct alignas(alignof(long double)) Foo2 {
// Foo2 成员的定义...
};
struct Empty {};
struct alignas(64) Empty64 {};
int main()
{
std::cout << "对齐字节数" "\n"
"- char :" << alignof(char) << "\n"
"- 指针 :" << alignof(int*) << "\n"
"- Foo 类 :" << alignof(Foo) << "\n"
"- Foo2 类 :" << alignof(Foo2) << "\n"
"- 空类 :" << alignof(Empty) << "\n"
"- alignas(64) Empty:" << alignof(Empty64) << "\n";
}
#include <iostream>
#include <cstddef>
struct Storage {
char a;
int b;
double c;
long long d;
};
struct alignas(std::max_align_t) AlignasStorage {
char a;
int b;
double c;
long long d;
};
int main() {
std::cout << alignof(Storage) << std::endl;
std::cout << alignof(AlignasStorage) << std::endl;
return 0;
}
std::alignment_of
头文件
<type_traits>
原型
template< class T > struct alignment_of;
作用:
- 和
alignof
一样
#include <iostream>
#include <type_traits>
class A {};
int main()
{
std::cout << std::alignment_of<A>::value << '\n';
std::cout << std::alignment_of<int>() << '\n'; // 另一种语法
std::cout << std::alignment_of_v<double> << '\n'; // c++17 另一种语法
}
std::align
头文件
<memory>
原型
void* align( std::size_t alignment, std::size_t size, void*& ptr, std::size_t& space );
参数:
alignment
- 欲求的对齐量size
- 要被对齐的存储的大小ptr
- 指向至少有 space 字节的连续存储的指针
space
- 要在其中操作的缓冲区的大小返回值:
- ptr 的调整值,或若提供空间太小则为空指针值。
作用:
- 给定指针 ptr 指定大小为 space 的缓冲区,返回按指定 alignment 为 size 字节数对齐的指针,并减小 space 参数对齐所用的字节数。返回首个对齐的地址。
- 仅以给定对齐量对齐入缓冲区的所需字节数合适,函数才会修改指针。若缓冲区太小,则函数不做任何事并返回 nullptr
- 若 alignment 不是二的幂,则行为未定义。
#include <iostream>
#include <memory>
template <std::size_t N>
struct MyAllocator
{
char data[N];
void* p;
std::size_t sz;
MyAllocator() : p(data), sz(N) {}
template <typename T>
T* aligned_alloc(std::size_t a = alignof(T))
{
if (std::align(a, sizeof(T), p, sz))
{
T* result = reinterpret_cast<T*>(p);
p = (char*)p + sizeof(T);
sz -= sizeof(T);
return result;
}
return nullptr;
}
};
int main()
{
MyAllocator<64> a;
// 分配一个 char
char* p1 = a.aligned_alloc<char>();
if (p1)
*p1 = 'a';
std::cout << "allocated a char at " << (void*)p1 << '\n';
// 分配一个 int
int* p2 = a.aligned_alloc<int>();
if (p2)
*p2 = 1;
std::cout << "allocated an int at " << (void*)p2 << '\n';
// 分配一个 int ,对齐于 32 字节边界
int* p3 = a.aligned_alloc<int>(32);
if (p3)
*p3 = 2;
std::cout << "allocated an int at " << (void*)p3 << " (32 byte alignment)\n";
}
std::aligned_storage
头文件
<type_traits>
原型
template< std::size_t Len, std::size_t Align = /*default-alignment*/ > struct aligned_storage;
官方文档:https://zh.cppreference.com/w/cpp/types/aligned_storage