对象存储和生存周期,以及RAII

Last Updated: 2023-12-28 03:49:34 Thursday

-- TOC --

这是C++的一个核心概念,在C语言内,这个概念不是特别清晰,到了C++时代,这个概念被清晰的提了出来。还有一个对应的Object Life Cycle概念,它相对涵盖的范围小一点点。

对象存储和生存周期:

  1. The object’s storage duration begins, and storage is allocated.
  2. The object’s constructor is called. (C++)
  3. The object’s lifetime begins.
  4. You can use the object in your program.
  5. The object’s lifetime ends.
  6. The object’s destructor is called. (C++)
  7. The object’s storage duration ends, and storage is deallocated.

只有storage这个前提存在,才有可能孕育life;只有life结束,才能deallocate storage!

C语言的Object Storage Duration

C语言中的各种变量或结构体内存块,在C++的语境中,都成了object。这没有关系,不影响我们理解整个概念。先把C语言范畴的Object Storage Duration搞清楚,C++只是在这个基础上有一些补充和完善,主要是带有constructor和destructor的对象。

Automatic Storage Duration

还记得我们从来都不会在C语言中使用的auto关键词吗?就是这个意思。

所有函数内部的变量,在函数执行结束后,就会变为不可用(调用栈空间回退),这就是automatic storage duration的意思。但其实,这个automatic的范围可以比函数更小,一个{}的范围(code block)就够了。

请看下面的测试代码:

#include <stdio.h>

int main(void) {
    int a = 1;
    {
        int a = 2;
        printf("%d\n", a);
    }
    printf("%d\n", a);
    return 0;
}

编译后执行,能够正常打印出两个a的值:

$ gcc test.c
$ ./a.out
2
1

可见{}括起来的范围,是独立的,里面的变量a的生存周期,就在这个范围内。

参考:Block Scoped Variables不是Redefinition

Static Storage Duration

全局变量,静态变量(单文件范围,或单函数范围),他们的生存周期,与进程相同,只有进程退出了,他们的空间才会释放,因此用static这个词,静态的,不会动的。

C++的静态成员变量,也是一样。

Thread Local Storage Duration

变量的生存期,与线程的启停相同。

C语言实现_Thread_local数据

Dynamic Storage Duration

C语言使用malloc/calloc申请的内存块(用free释放),都属于Dynamic Storage Duration。(C++推荐使用new和delete)

下面比较C和C++对于dynamic storage duration变量(或对象)的申请释放代码:

// 指向int数据的指针
// C
int a = 24;
int *p = &a; // p必须要指向一块内存
// 或者
int *p = (int*)malloc(sizeof(int));
*p = 24;
free(p);
// C++
int *p = new int{24};
delete p;
// 指向int数组的指针
// C
int p[100] = {0};
// 或者
int *p = (int*)malloc(sizeof(int)*100);
memset(p, 0, sizeof(int)*100);
free(p);
// C++
int *p = new int[100]{};  // all zero
delete[] p;

可以看出C++语句的表达力的确要比传统古老的C要强一些的。

C++引入的新东西

以上介绍的C语言中的4种Storage Duration,也是C++的全部storage类型。

C++引入的新东西,是那些既包含数据有包含函数的Fully Featured Class!基于这些class创建的对象,编译器需要在其生存周期开始的时候,调用constructor,在生命周期结束后,调用destructor。

记住这句话:变量就是有个名字的对象!

下面的测试代码,将所有这些storage duration一网打尽:

$ cat test.cpp
#include <cstdio>
#include <pthread.h>


struct tt {
    tt(const char *name):
        a{},
        name{name} {
        printf("tt %s begin...\n", name);
    }

    ~tt() {
        printf("tt %s end...\n", name);
    }

    int a;

private:
    const char *name;
};


tt t1{"static"};
thread_local tt t2{"thread local"};


void* athread(void *args) {
    if (args != NULL)  // kill unused-variable warning
        return NULL;
    // use thread local t2
    t2.a = 24;
    return NULL;
}

int main(void){
    tt t3{"automatic"};
    tt *t = new tt{"dynamic"};
    {
        tt t5{"automatic in {}"};
    }
    delete t;

    pthread_t tid;
    pthread_create(&tid, NULL, athread, NULL);
    pthread_join(tid, NULL);
    return 0;
}

比较有趣的是那个thread local对象,只有在有线程访问它的时候,编译器才会创建这个对象,线程退出,这个对象也消失。

同样是pthread_create接口,gcc编译和g++编译,效果不一样。C++的语法要求更严格!

下面是测试代码的运行情况:

$ g++ -Wall -Wextra test.cpp -o test
$ ./test
tt static begin...
tt automatic begin...
tt dynamic begin...
tt automatic in {} begin...
tt automatic in {} end...
tt dynamic end...
tt thread local begin...
tt thread local end...
tt automatic end...
tt static end...

class type成员变量

我觉得有必要专门说明一下成员变量的生命周期。

对象的constructor调用之前,这些成员变量的空间就被自动allocate,在对象destructor调用之后,这些变量的空间被自动deallocate。

如果成员变量是另一个class type对象,这个被包含的对象的constructor会先被调用。这个被包含的对象的destructor也会滞后调用,在包含此对象的destructor被调用之后调用。

下面是测试代码:

#include <cstdio>

struct AA {
    AA() {
        printf("AA constructor\n");
    }
    ~AA() {
        printf("AA destructor\n");
    }
};

struct BB {
    BB() {
        printf("BB constructor\n");
    }
    ~BB() {
        printf("BB destructor\n");
    }
private:
    struct AA aa;
};

int main(void) {
    struct BB bb;
    return 0;
}

输出:

$ g++ -Wall -Wextra test_cd.cpp -o test_cd
$ ./test_cd
AA constructor
BB constructor
BB destructor
AA destructor

bb对象包含一个aa对象作为私有成员,创建bb然后释放bb,从打印能够看出,aa的constructor总是先被调用,aa的destructor总是后被调用。而且,它们的调用,都是编译器自动安排的。

All members are constructed before the enclosing object’s constructor. All members are destructed after the object’s destructor is invoked.

编译器操作automatic变量仅仅只需要扩展或收缩stack。C++引入了对象,对象可能拥有资源,编译器操作这些automatic对象,就不仅仅是扩展和收缩stack,还有调用对象的constructor和destructor。

如果这个被包含的class type对象在初始放在了初始化列表中执行,它也是在外层对象的constructor之前被调用。

Exception

这也是C++引入的新东西。

当代码触发异常,开始寻找用哪里的catch来处理异常的时候:

The runtime seeks the closest exception handler to a thrown exception. If there is a matching exception handler in the current stack frame, it will handle the exception. If no matching handler is found, the runtime will unwind the call stack until it finds a suitable handler. Any objects whose lifetimes end are destroyed in the usual way.

找到catch后,调用栈中的那些生命周期结束的对象,会被销毁。传统的存放在stack中的变量,自动就没有了,C++的fully-featured-class对象,其destructor会被调用,如果成员也是对象,其destructor也会接着被调用。

constructor可能会申请或直接获取dynamic资源,并确保在destructor中释放,这就是RAII(Resource Allocation Is Initialization)思想!

当在C++中使用exception时,RAII对象会比较安全,不会出现memory leak的情况。但如果不是RAII对象,同时还使用exception,当throw,stack unwinding时,就很危险了,非常容易出现memory leak的情况。因为没有自动释放资源的地方,需要手动处理。

推荐学习:详解C++异常处理

以上就是我对Object's Storage Duration in C++的全部总结!

RAII

RAII是一种思想,即利用局部对象(变量)自动被销毁的特点,对象在被销毁的时候,会自动调用destructor,在destructor中释放这个对象的所有资源。因此,要充分利用这一点:

  1. 直接用对象,别用对象指针;
  2. 将资源都托管在对象中,对象销毁即资源释放,比如著名的smart pointer对象;
  3. 我没有找到为什么不要在constructor中申请动态资源有什么不好的理由?

RAII,Resource Acquisition Is Initialization

在申请资源的时候,初始化某个对象,将资源放入对象内部,利用对象的destructor释放资源。RAII并不神秘,q其思想也不复杂,只是这个名字本身,带来了一些些困扰....:)

本文链接:https://cs.pynote.net/sf/c/cpp/202208252/

-- EOF --

-- MORE --