ISO/IEC 14882:2011之条款3.7——存储周期
3.7 存储周期
1、存储周期【译者注:这里的存储周期是指存储持久性,即对象的存储生命周期】是一个对象的属性,它定义了包含此对象的存储的最小潜在生命周期。存储周期由用于创建该对象的构造决定,并作为以下之一:
—— 静态存储周期
——线程存储周期
——自动存储周期
—— 动态存储周期
2、静态、线程或自动存储周期与由声明(3.1)所引入的以及由实现(12.2)所隐式创建的对象相关联。动态存储周期与由operator new(5.3.4)所创建的对象相关联。
3、存储周期类别也应用于引用。一个引用的生命周期即为其存储周期。
3.7.1 静态存储周期
1、所有不具备动态存储周期的变量没有线程存储周期,而非局部变量具有静态存储器周期。这些实体的存储延续到程序的周期(3.6.2,3.6.3)。
2、如果具有静态存储周期的的一个变量具有带副作用的初始化或析构器,那么它不应该被消除,即使它以未被使用的方式出现,除了一个类对象或其拷贝/搬移可以如12.8中所指定的那样被消除之外。
3、关键字static可以被用来以静态存储周期来声明一个变量。[注:6.7描述了局部静态变量的初始化;3.6.3描述了局部静态变量的销毁。 ——注结束]
4、应用于一个类定义的一个类数据成员的关键字static为该数据成员给出了静态存储周期。
3.7.2 线程存储器周期
1、所有用thread_local关键字声明的变量具有线程存储周期。这些实体的存储将延续到它们所在的线程的周期(它们在那个线程中被创建)。每个线程都具有截然不同的对象或引用,以及对所声明的名字的使用,该名字引用与当前线程相关联的实体。
2、具有线程存储周期的一个变量应该在其第一次使用一次定义规则(3.2)之前被初始化,并且,如果被构造,那么应该在线程退出时被销毁。
3.7.3 自动存储周期
1、显式地用register声明的或没有显式地用static或extern声明的语句块作用域的变量具有自动存储周期。这些实体的存储延续到它们所在的语句块(它们在那个语句块中被创建)。
2、[注:这些变量如6.7中所描述的那样被初始化和销毁。 —— 注结束]。
3、如果具有自动存储周期的一个变量含有带副作用的初始化或一个析构器,那么它不应该在其语句块结束之前被销毁,也不应该作为优化而被消除,即使它以未被使用的形式出现,除了一个类对象或其拷贝/搬移可以如12.8中所指定的那样被消除之外。
3.7.4 动态存储周期
1、对象可以在程序执行期间使用new-expression(5.3.4)被动态创建(1.9),以及使用delete-expression(5.3.5)来销毁。一个C++实现提供了对动态存储的访问和管理,通过全局分配函数operator new和operator new[]以及全局析构函数operator delete和operator delete[]。
2、库提供了全局分配和释放函数的默认定义。一些全局分配和释放函数是可被替换的(18.6.1)。一个C++程序应该提供一个可替换的分配或释放函数的最多一个定义。任一这样的函数定义替代在库中提供的默认版本(17.6.4.6)。以下分配和释放函数(18.6)是在一个程序的每个翻译单元中的全局作用域中隐式声明的。
void* operator new(std::size_t); void* operator new[](std::size_t); void operator delete(void*); void operator delete[](void*);
这些隐式声明仅仅引入了函数名operator new、operator new[]、operator delete和operator delete[]。[注:隐式声明并不引入std、std::size_t或任何其它库用于声明这些名字的名字。从而,一个new-expression,delete-expression或引用这些函数的其中之一而并没有包含头文件<new>的函数调用也是良好形式的。然而,引用std或std::size_t是不良形式的,除非该名字已通过包含相应的头文件而被声明。 ——注结束]分配及/或释放函数也可以为任一类而声明和定义(12.5)。
3、在一个C++程序中所定义的任一分配和/或释放函数,包括库中的默认版本,应该遵循3.7.4.1和3.7.4.2中所指定的语义。
3.7.4.1 分配函数
1、一个分配函数应该是一个类成员函数或一个全局函数;如果一个分配函数在一个名字空间中定义而不是在全局作用域,或在全局作用域声明为静态的,那么该程序是不良形式的。返回类型应该是void*。第一个形参应该是类型std::size_t(18.2)。第一个形参不应该具有一个相关联的默认实参(8.3.6)。第一个形参的值应该被看作为所请求的分配的大小。一个分配函数可以是一个函数模板。这样的一个模板应该将其返回类型和第一个形参声明为上述所指定的样子(即,模板形参类型不应该用在返回类型和第一个形参类型中)。模板分配函数应该具有两个或更多的形参。
2、分配函数试图分配所请求的存储空间大小。如果分配成功,那么它将返回一个存储块的起始地址,该存储块的长度至少要与所请求的大小一样大。对于从分配函数所返回的所分配存储的内容没有限制。成功调用一个分配函数后,所分配存储的次序、连续性和初始值是未指定的。所返回的指针应该被适当地对齐,这样它可以被转为任一具有一个基础对齐要求(3.11)的完整对象类型的一个指针,然后被用于在所分配的存储中访问对象或数组(直到该存储通过对一个相应的析构函数被显式释放)。即使所请求的空间大小是零,请求也会失败。如果请求成功,那么所返回的值应该是一个非空指针值(4.10)p0,不同于任一先前所返回的值p1,除非值p1后面被传给operator delete。解引用以零大小的一个请求所返回的一个指针的效果是未定义的。[注:该目的是为了让operator new()可通过调用std::malloc()或std::calloc()来实现,这样规则本质上是一样的。C++在做一次零大小请求上与C不同,C++返回的是一个非空指针。][译者注:在Apple LLVM3.1中,C++的operator new与C语言的malloc用零大小分配空间,返回的指针值均非空。]
3、失败分配存储的一个分配函数可以调用当前已安装的new处理例程函数(18.6.2.3),如果有的话。[注:一个程序提供的分配函数可以使用std::get_new_handler函数(18.6.2.4)获得当前安装的new_handler的地址。——注结束]如果用一个非抛出exception-specification(15.4)来声明的一个分配函数分配内存失败,那么它将返回一个空指针。任何其它失败分配存储的分配函数将仅通过抛出一个异常来指示失败,该异常的类型将匹配一个std::bad_alloc(18.6.2.1)类型的一个处理例程(15.3)。
4、一个全局分配函数仅作为一个new表达式(5.3.4)的结果而被调用,或直接使用函数调用语法调用(5.2.2),或间接通过C++标准库中的函数调用来调用。[注:特别地,一个全局分配函数不会为带有静态存储周期(3.7.1)的对象、带有线程存储周期(3.7.2)的对象或引用、std::type_info类型的对象(5.2.8),或通过一个throw表达式(15.1)异常而被抛出的一个对象的拷贝而被调用以分配存储。——注结束]
3.7.4.2 释放函数
1、释放函数应该类成员函数或全局全局函数;一个程序是不良形式的,如果释放函数在一个名字空间作用域而不是全局作用域被声明或在全局作用域中被静态声明。
2、每个释放函数应该返回void,并且其形参应该是void*。一个释放函数可以有多个形参。如果一个类T有一个成员释放函数命名为operator delete,带有一个形参,那么那个函数是一个通常的(非placement)释放函数。如果类T并不声明这样一个operator delete,而是声明一个命名为operator delete的成员释放函数,带有两个形参,而第二个形参具有std::size_t类型(18.2),那么这个函数是一个通常的释放函数。类似的,如果一个类T具有一个命名为operator delete[]的成员释放函数而恰好带有一个形参,那么那个函数是一个通常的(非placement)释放函数。如果类T并不声明这样的一个operator delete[],而是声明了一个命名为operator delete[]的成员释放函数,那么这个函数是一个通常的释放函数。一个释放函数可以是一个函数模板的一个实例。第一个形参和返回类型都不该依赖于一个模板形参。[注:即,一个释放函数模板第一个形参应该具有类型void*,并且一个void返回类型。——注结束]一个释放函数模板应该具有两个或更多的函数形参。一个模板实例一直不会是一个通常的释放函数,不管其签名是咋样的。
3、如果一个释放函数通过抛出一个异常而终止,那么行为是未定义的。提供给一个释放函数的第一个实参的值可以是一个空指针值;如果这样,并且如果释放函数是在标准库中提供的,那么调用是无效的。否则,如果提供给标准库中的operator delete(void*)的值并不是先前调用对标准库中的operator new(std::size_t)或operator new(std::size_t, const std::nothrow_t&)所返回的值,那么行为是未定义的,而如果提供给标准库中的operator delete[](void*)的值并不是先前调用对标准库中的operator new(std::size_t)或operator new(std::size_t, const std::nothrow_t&)所返回的值,那么行为也是未定义的。
4、如果给到一个标准库中的一个释放函数的实参是一个非空指针(4.10),那么释放函数应该释放由该指针所引用的存储,将所有引用那个被释放存储的任意部分指针标记为无效。使用一个无效指针值的效果(包括将它传递到一个释放函数)是未定义的。[注:在某些实现中,它会引起一个系统生成的运行时错误。]
3.7.4.3 安全派生的指针
1、一个可追踪的指针对象是
——一个对象指针类型(3.9.2)的一个对象,或
——一个整型类型的一个对象,它至少与std::intptr_t一样大,或
——一个字符类型数组中的一列元素,而该序列的大小和对齐方式匹配某些对象指针类型。
2、一个指针值对于一个动态对象是一个安全派生的指针,只要它具有一个对象指针类型并且它为以下之一:
——从对一个::operator new(std::size_t)的C++标准库实现所返回的值[注:这章节并不引入对于并非用::operator new来分配的存储空间的解引用指针的限制。这维持着许多C++实现的能力以使用用其它语言编写的二进制库和组件。特别地,这应用于C二进制,因为解引用指针到用malloc分配的存储空间是不受限制的];
——取一个对象的(或其子对象之一的)地址结果,该对象由解引用一个安全派生的指针值所返回的结果的一个左值所指定。
——使用一个安全派生的指针值的良好定义的指针算术(5.7)的结果;
——一个安全派生的指针值的一个良好定义的指针转换(4.10,5.4)的结果;
——一个安全派生的指针值的一个reinterpret_cast的结果;
——一个安全派生的指针值的一个整型表示的一个reinterpret_cast的结果;
——一个对象的值,该值从一个可追踪的指针对象拷贝,而在拷贝时,源对象包含一个安全派生的指针值的一个拷贝。
3、一个整型值是一个安全派生指针的的一个整型表示,只要其类型至少与std::intptr_t一样大并且它是下列之一的值:
——一个安全派生指针值的一个reinterpret_cast的结果;
——一个安全派生指针值的一个整型表示的一个有效转换的结果;
——一个对象的值,其值从一个可追踪的指针对象被拷贝,而在拷贝时,包含一个安全派生指针值的一个整型表示的源对象;
——一个加法或位操作的结果,其操作数之一是一个安全派生的指针值P的一个整型表示,如果被reinterpret_cast<void*>转换的那个结果与来自reinterpret_cast<void*>(P)的一个可计算的安全派生的指针相比是相同的。
4、一个实现可以有松弛的指针安全,在这种情况下,一个指针值的有效性并不依赖于它是否为一个安全派生的指针。另外,一个实现也可以有严格的指针安全,在这种情况下,一个非安全派生的指针值的一个指针值是一个无效的指针值,除非所引用的完整的对象是动态存储周期的,并且先前被声明为可获得的(20.6.4)。[注:使用一个无效指针值的结果(包括将它传递到一个释放函数)是未定义的,见3.7.4.2。这个为真,即使不安全派生的指针值可能与某些安全派生的指针值比较起来相同。 ——注结束]一个实现是松弛还是严格的指针安全是由实现定义的。
3.7.5 子对象的周期
1、成员子对象、基类子对象以及数组元素的存储周期与其完整的对象一样(1.8)。