当前位置: C语言 -- 基础 -- 对象的存储期限

对象的存储期限

对象的生存期(lifetime)是程序执行过程中保证为其保留存储空间的时间段。对象在整个生存期内地址保持不变,并保留最后存储的值。如果对象在生存期外被引用,其行为是未定义的。当指针指向对象的生存期结束时,指针值是不确定的。

:“地址不变”是指一次程序执行过程中不同时间构造的指向相同对象的两个指针比较结果相等;但在同一程序的两次不同执行中,地址可能不同。对于volatile对象,最后一次存储不需要在程序中显式表示。

对象的存储期限(storage duration)决定了对象的生存期。C语言中存在4种类型的存储期限:静态存储期限(static storage duration)、线程存储期限(thread storage duration)、自动存储期限(automatic storage duration)、动态存储期限(allocated storage duration)。


一、静态存储期限

具有静态存储期限的对象必须具备以下两个条件:

标识符声明时未使用存储类说明符_Thread_local

标识符具有外部链接,或者内部链接,或者标识符声明时使用了存储类说明符static

extern int value;

int main(void)
{
    static int number;
    ...
}

valuenumber都具有静态存储期限。

具有静态存储期限的对象在程序启动前会被初始化一次,其生存期是整个程序执行过程。


二、线程存储期限

如果一个对象标识符在声明时使用了存储类说明符_Thread_local,那么该对象具有线程存储期限。

_Thread_local int value;

void func(void)
{
    static _Thread_local int number;
    ...
}

valuenumber都具有线程存储期限。


具有线程存储期限的对象,其初始化式在程序执行前评估;其生存期是创建其线程的整个执行过程;其值在线程启动时使用先前确定的值初始化。具有线程存储期限的相同标识符在不同线程中表示不同对象;如果在表达式中使用该标识符,是指使用与评估表达式所在线程相关联的对象。如果试图从与对象关联线程以外的线程访问具有线程存储期限的对象,其行为由实现定义。

_Thread_local int i = 5;

/* 线程一中执行的函数。*/
void threadOne(void)
{
    static _Thread_local int m = 10;
    
    i++;
    printf("m = %d\n", m);      //将输出m = 10。
    printf("i = %d\n", i);      //将输出i = 6。
}

/* 线程二中执行的函数。*/
void threadTwo(void)
{
    static _Thread_local int m = 20;
   
    printf("m = %d\n", m);      //将输出m = 20。
    printf("i = %d\n", i);      //将输出i = 5。
}

注:使用Pelles C编译。

标识符i具有文件作用域,所以每个线程开始时i的初始值都是5;由于其具有线程存储期限,所以线程一中对i值的修改,不会影响其它线程中的i值。标识符m具有块作用域,且具有线程存储期限,所以在线程一中m的初始值为10,在线程二中m的初始值为20


三、自动存储期限

如果一个对象标识符声明时无链接,并且没有使用存储类说明符static,那么该对象具有自动存储期限。如果试图从与对象关联线程以外的线程访问具有自动存储期限的对象,其行为将由实现定义。

int main(void)
{
    int number = 5;
    ...
}

number具有自动存储期限。


具有自动存储期限对象的生存期从进入关联程序块开始,直至该程序块执行结束,例如:

{
    int a;
    ...
}   //a的生存期在此结束。
...

如果程序块是递归进入的,该块内具有自动存储期限的对象每次都会重新声明;如果没有初始化,对象的初始表示是不确定的;如果对象进行了初始化,但未使用存储类说明符constexpr初始化,每次进入该块,都会重新执行初始化;如果使用存储类说明符constexpr初始化,在编译时会对初始化进行一次评估,每次进入该块,对象具有固定值。

for(int i=0; i<10; i++)
{
    int a;			//a会声明10次。
    int b = i;			//b会初始化10次。
    constexpr int c = 10;	//合法,每次c值都是10。
    constexpr int d = i;	//非法,i不是常量表达式。
    ...
}

对于具有变长数组类型的对象,其生存期从对象的声明开始直至程序的执行离开其作用域。如果作用域是递归进入的,每次进入都会创建一个新对象;对象的初始表示是不确定的。

{
    int m = 5;
    int arr[m];
    ...
    goto here;  //arr[m]的生存期在此结束。
    ...
} 
here : ; 

结构或者联合类型的非左值表达式(non-lvalue expession),其中结构或者联合包含数组类型成员,引用对象具有自动存储期限和临时生存期(temporary lifetime),其生存期从表达式评估开始至完整表达式评估结束。

/*自定义包含数组类型成员的结构。*/
typedef struct {int arr[2];} Temp;

/*返回包含数组类型成员的结构的函数。*/
Temp func(int x, int y)
{
    return (Temp){x, y};
}

int main(void)
{
    int x = *func(2,5).arr;

    ...
}

函数调用表达式func(2,5)是非左值表达式,其返回值类型是包含数组的结构类型,因此func(2,5)具有自动存储期限和临时生存期(temporary lifetime)

具有临时生存期对象的行为就像是为了有效类型而使用其值的类型声明的一样。任何试图修改具有临时生存期对象的行为都是未定义行为。具有临时生存期的对象可能没有唯一的地址。


四、动态存储期限

动态存储期限对象的存储空间可以使用内存管理函数按要求分配和释放。分配成功后返回的指针可以赋值给任何具有基本对齐要求的对象指针,然后用于访问分配的存储空间。每次分配都会生成一个指向与其他对象不相交的对象的指针。生成的指针指向分配空间的起点(最低字节地址)。如果无法分配空间,则返回一个空指针。如果请求的空间大小为0,其行为由实现定义:可能返回一个空指针,表示出错;也可能其行为与空间大小为非0值的情况相同,但返回的指针不能用于访问对象。连续调用aligned_alloccallocmallocrealloc函数分配的存储空间的顺序和连续性是不确定的。具有动态存储期限对象的生存期从存储空间成功分配开始到取消分配(存储空间被释放或者程序执行结束。)为止。

int *ptr;
    
ptr = malloc(10*sizeof(int));   //生存期开始。
...
free(ptr);                      //生存期结束。

对特定内存区域进行分配或者取消分配操作的函数调用应按单一总顺进行,每次取消分配调用应与该顺序中的下一次分配调用(如果存在)同步。



主要参考资料:

1、ISO/IEC 9899:2024

2、ISO/IEC 9899:2018

3、cppreference.com : Storage-class specifiers