Skip to content

2.8 对象与存储期

This content is not available in your language yet.

在初识声明中,我们介绍了声明会引入对象。对象占据了内存中的一块空间存储数据。对象的类型决定了对象的大小和存储的数据类型。

C++ 中,对象有以下性质:

  • 大小:对象在内存中占据一定的空间,这个空间的大小就是对象的大小。
  • 对齐要求:对象在内存中的存储位置有一定的要求,类似于大号的箱子不能放在小号的格子里,这个要求就是对齐要求。
  • 存储期:对象占用空间的一段时间,这个时间段称为对象的存储期。
  • 生存期:对象在占用空间里有效的一段时间,这个时间段称为对象的生存期。
  • 值:对象的值是对象存储的数据,是对象的内容。
  • 类型:对象的类型决定了对象的值的形式,以及对象可以进行的操作。

对象不一定具有名字。例如,我们之前提过的没有名字的函数参数,表达式的值等。

类型作为对象的性质,除了规定了对象的值的形式、对象可以进行的操作以外,也规定了对象的大小和对齐。也就是说,相同类型的对象,其大小和对齐要求一定是相同的。

目前,我们了解到 void 类型是没有值的,它也是一个不完整类型。不完整类型没有大小,也没有对齐要求。因此,我们不能使用 sizeof 和 alignof 运算符来获取不完整类型的大小和对齐要求。当然,也不能声明一个不完整类型的对象。

在 C++ 中,我们可以使用 sizeof 运算符来获取对象的大小。sizeof 表达式的形式是:

sizeof (type_id)

或者

sizeof expression

其中,sizeof 是一个关键字,在这里作为一个运算符。type_id 需要是一个完整类型,expression 是一元表达式。

sizeof 表达式是一个一元表达式,其值是一个 size_t 类型的值,表示对象的大小。size_t 是一个无符号的整数类型(不会有负数),其值足以表达当前平台上任意对象的大小。例如:

int a;
size_t size_of_a = sizeof a; // 获取对象 a 的大小
size_t size_of_int = sizeof (int); // 获取 int 类型的大小

上面的代码中,size_of_a 和 size_of_int 都是 size_t 类型的值,分别表示对象 a 和 int 类型的大小。当然,由于 a 的类型是 int,所以这两个值是相等的。

C++ 规定 sizeof(char) 的值为 1,其他类型的大小则由实现决定。但这并不是实现随便拍脑门决定,在后面我们会详细介绍各种基础类型。

在 sizeof 表达式计算的时候,作为其组成部分的对象不会被求值。例如:

int a = 1;
size_t size_of_a = sizeof (a++); // 获取对象 a 的大小
// 此时 a 的值仍然是 1

上面的代码中,a++ 不会被求值,从而其副作用(让 a 增加1)不会发生,a 的值仍然是 1。

在 C++11 中,我们可以使用 alignof 运算符来获取对象的对齐要求。alignof 表达式的形式是:

alignof (type_id)

这里,alignof 是一个关键字,在这里作为一个运算符。type_id 需要是一个完整类型。

alignof 表达式的值是一个 size_t 类型的值,表示对象的对齐要求。例如:

size_t align_of_a = alignof (int); // 获取 int 类型的对齐要求

在初识作用域中,我们介绍了作用域的概念,作用域决定了对象能否被访问。而对象的存储期和生存期,一定程度上是作用域的一个延伸。

这里,需要引入一个新的概念,块作用域。块作用域规定了对象在一个块内有效,块结束时,块作用域内的对象就会被销毁。例如:

{
int a = 1; // a 的存储期开始
} // a 的存储期结束
// 这里 a 已经被销毁

技术性地说,块作用域的定义是:

  • 选择语句、循环语句的边界
    • 作为上述语句的选择体/循环体的语句的边界
  • 复合语句的边界

组成了一个块作用域,这些语句本身在这个块作用域中。具体而言:

// 在 if 之前,进入了 if 语句的块作用域
if (int a = 1; a > 0)
// 在这个花括号之前,进入了 if 语句里面的选择体,一个复合语句的块作用域
{
int b = a;
}
// 在这个花括号之后,离开了此复合语句的块作用域
else
int c = a;
// 在这个分号之后,离开了 if 语句的块作用域(if 语句的 else 部分对应的选择体)

注意,命名空间虽然涉及了花括号 {},但是这不是复合语句,也不会引入块作用域。

如果声明一个对象,且对象直接处于一个命名空间(包括全局命名空间)内。那么这个对象的存储期是静态存储期。静态存储期的对象在程序开始时分配空间,在程序结束时回收。例如:

// main.cpp 开始
int a = 1; // a 的存储期是静态存储期
void foo() {
// a 在这里也是可见的
}
// main.cpp 结束

此外,可以在块作用域内部使用 static 关键字来声明一个静态存储期的对象。例如:

void foo() {
static int a = 1; // a 的存储期是静态存储期
}

这里关键字 static 修饰 a 的声明,指定 a 的存储期为静态存储期。注意, static 不是类型的一部分,a 的类型仍然是 int。

如果声明一个对象,并且对象在是块作用域内(并且没有被static修饰),那么这个对象的存储期是自动存储期。自动存储期的对象在被声明时分配空间,在其声明时处于的块作用域结束时回收。例如:

{
int a = 1; // a 的存储期是自动存储期
} // a 的存储期结束

如果有多层块作用域,那么对象的存储期是其生命时的块作用域。例如:

{
int a = 1; // a 的存储期开始
{
int b = 2; // b 的存储期开始
} // 内层 b 的存储期结束
} // 外层 b 的存储期结束

在循环语句中,循环体会引入一个块作用域,因此循环体内的对象的存储期仅有一次循环。例如:

for (int i = 0; i < 10; ++i) { // i 的存储期开始
if ( i > 0 ) a += i; // 错误:a 在这里是不可见的
// 上个循环中的 a 存储期已经结束,这个循环 a 的存储期还没有开始
int a = i; // a 的存储期开始
} // a 的存储期结束
// i 的存储期结束

在 C++ 中,还有两种存储期:动态存储期和线程存储期。目前还没有介绍足够读者理解这两种存储期的概念,因此这两种存储期我们会在后面的章节中详细介绍。

对于我们已经提过的 int、char、bool、size_t 这些类型,它们的存储期开始也直接意味着生存期开始、生存期结束也直接意味着存储期结束。因此,对于目前我们已经介绍的类型,存储期和生存期是一致的。

而对于自定义类型,其生存期和存储期的关系则更为复杂,我们会在后面的章节中详细介绍。