2.6 对象与存储期
在中,我们介绍了声明会引入对象。对象占据了内存中的一块空间存储数据。对象的类型决定了对象的大小和存储的数据类型。
对象的性质
C++ 中,对象有以下性质:
- 大小:对象在内存中占据一定的空间,这个空间的大小就是对象的大小。
- 对齐要求:对象在内存中的存储位置有一定的要求,类似于大号的箱子不能放在小号的格子里,这个要求就是对齐要求。
- 存储期:对象占用空间的一段时间,这个时间段称为对象的存储期。
- 生存期:对象在占用空间里有效的一段时间,这个时间段称为对象的生存期。
- 值:对象的值是对象存储的数据,是对象的内容。
- 类型:对象的类型决定了对象的值的形式,以及对象可以进行的操作。
对象不一定具有名字。例如,我们之前提过的没有名称的函数参数,表达式的值等。
类型作为对象的性质,除了规定了对象的值的形式、对象可以进行的操作以外,也规定了对象的大小和对齐。也就是说,相同类型的对象,其大小和对齐要求一定是相同的。
不完整类型
目前,我们了解到 void
类型是没有值的,它也是一个不完整类型。不完整类型没有大小,也没有对齐要求。因此,我们不能使用 sizeof
和 alignof
运算符来获取不完整类型的大小和对齐要求。当然,也不能声明一个不完整类型的对象。
sizeof
表达式
在 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 用法的统一
这里 sizeof
表达形式有两种,记忆使用时会带来一些不便。不妨考虑这样: 一元表达式可以是括号 ()
括起来的表达式,也即一个基础表达式。那么,可以用 sizeof (expression)
来获取表达式的大小,这样其形式上就和 sizeof (type_id)
一致了。例如:
int a;
size_t size_of_a = sizeof (a); // 获取对象 a 的大小
size_t size_of_int = sizeof (int); // 获取 int 类型的大小
这就免去了程序员耗费心智区分两种形式的麻烦。但是当看到没有括号的 sizeof
表达式时,也不要忘记其含义。
在 sizeof
表达式计算的时候,作为其组成部分的对象不会被求值。例如:
int a = 1;
size_t size_of_a = sizeof (a++); // 获取对象 a 的大小
// 此时 a 的值仍然是 1
上面的代码中,a++
不会被求值,从而其副作用(让 a
增加1)不会发生,a
的值仍然是 1。
alignof
表达式
在 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; // ![code error] // 错误:a 在这里是不可见的,并且上个循环中的 a 存储期已经结束,这个循环 a 的存储期还没有开始
int a = i; // a 的存储期开始
} // a 的存储期结束
// i 的存储期结束
动态存储期和线程存储期
在 C++ 中,还有两种存储期:动态存储期和线程存储期。目前还没有介绍足够读者理解这两种存储期的概念,因此这两种存储期我们会在后面的章节中详细介绍。
生存期
TODO: 改进描述
对于我们已经提过的 int
、char
、bool
、size_t
这些类型,它们的存储期开始也直接意味着生存期开始、生存期结束也直接意味着存储期结束。因此,对于目前我们已经介绍的类型,存储期和生存期是一致的。
而对于自定义类型,其生存期和存储期的关系则更为复杂,我们会在后面的章节中详细介绍。