在C语言中释放对象的设计模式

3
在C编程中,通常会创建数据结构,初始化后在不再需要时释放。例如,如果我们想要创建一个动态double数组,通常会声明:
struct vector {
    double *data;
    int size;
    int capacity;
}
typedef struct vector vector;

vector *v_new(int n) {
    vector *v = malloc(sizeof(vector));
    v->data = malloc(n * sizeof(double));
    v->size = n;
    v->capacity = n;
    return v;
}

这个问题涉及到自由函数常见的模式。在 C 语言中,函数 free 接受 NULL 指针并且不执行任何操作。设计 v_free 函数时,这种方式是否是一个常见的模式,还是通常期望非空指针?为了清楚起见,你是否希望采用以下实现方式:
void v_free(vector *v) {
    if (v != NULL) {
        free(v->data);
    }
    free(v);
}

这个还是那个?
void v_free(vector *v) {
    free(v->data);
    free(v);
}

这个问题被问到是因为我们开始在法国的预科学校教授C语言给本科生,而我们对“C设计模式”没有太多经验。
感谢您的建议。

2
如果无论 vector 是否被分配,都调用 v_free,那么第一种情况将是更好的方法。它将避免在空指针上访问成员变量,这是未定义行为。 - TruthSeeker
避免在释放嵌套分配(例如您的“data”成员)时出现空指针引用是可以的,显然第一段代码已经准备好处理了。但是,请不要忘记这两个代码示例都无法避免不确定/悬空指针的恶意行为。理想情况下,您的代码应该“知道”指针是非空且非悬空的,或为空。往往是悬空指针和不确定指针会给您带来麻烦,只有调用者才能承担防范这些问题的责任。 - WhozCraig
1
我的两分钱,还是选择第二个。检查NULL并不完全可靠。如果你free了一个指针,它就不会被设置为NULL,这样就容易遭受双重free的风险,这是UB。如果你未能初始化一个指针,它可能没有被初始化为NULL,所以你仍然容易free一个无效的指针。总之,程序员始终应该做正确的事情。话虽如此,我对第一种方法也没有意见。 - yano
2个回答

5

如果 v 是 NULL,那么您将无法访问 v->data。因此,如果有可能出现这种情况,您必须进行检查,这个版本更好的写法如下:

void v_free(vector *v) {
    if (v != NULL) {
        free(v->data);
        free(v);
    }
}

如果这里v永远不应该为NULL,那么最好添加一个assert语句以使这个假设明确:
void v_free(vector *v) {
    assert(v != NULL);
    free(v->data);
    free(v);
}

这样程序员就会注意到他们在做错什么。


请注意,两个版本都无法检测到悬空指针,即指向已销毁对象的指针。这包括指向已释放内存(即,你在这里有双重释放),或者通过指针指向不再处于作用域内的本地变量。


除非你不确定,否则没有必要在每个函数中编写assert(v != NULL)。程序无论如何都会崩溃,程序员可以使用调试器查看它为什么崩溃。 - user253751
1
@user253751 在发布版本中应该禁用断言,而且它提供了一个自说明的代码,比注释更好。虽然在v为空时free(v->data);不太可能崩溃,但也不能完全保证。 - hyde
1
@TruthSeeker 在运行内存保护操作系统的真实计算机上,实际上是可以的。即使 C 语言没有保证,我也不会惊讶地发现这个保证出现在操作系统或编译器手册中的某个地方。假设您只打算检测编程错误,并且您不是编写嵌入式应用程序或操作系统内核,那么这不是您需要防御的 UB 形式之一。 - user253751
即使是广为人知的与NULL相关的Linux内核优化漏洞,如果在普通进程(而非内核)中运行,也会导致崩溃。 - user253751
1
@hyde,应该是v->data而不是&v->data,尝试获取地址会导致程序崩溃。&v->data将是从null的偏移量。 - user253751
显示剩余2条评论

3
这个问题涉及到自由函数的通用模式。在C语言中,函数free接受NULL指针并且什么也不做。设计v_free函数时是否采用这种通用模式,还是通常期望非空指针?这将因人而异。
我的观点是,除非有充分的理由,否则要进行防御性编程。做一些能够更容易调试错误的事情。让v_free在空指针上出错,可以使用像assert这样简单的方式。
void v_free(vector *v) {
    assert(v != NULL);

    free(v->data);
    free(v);
}

考虑如果我们悄悄地忽略了空指针的情况,那么调用者是有意传递了空指针,还是出现了错误呢?我们不知道。如果是出现了错误,程序将继续运行,并可能在其他地方神秘地崩溃。这会使调试变得更加困难。
考虑如果我们假设 v_free 总是接收到非空指针。如果它确实这样做了,free(v->data) 就是未定义行为。最好的情况是产生混乱的错误,最坏的情况是程序将继续运行,并可能在其他地方神秘地崩溃。这会使调试变得更加困难。
但是,如果我们提供一个错误处理,就可以阻止并暴露错误。对于所有的向量函数都应该这样做。
“但是如果我想传递一个空指针怎么办?” 那应该很少见,不要针对它进行优化。让调用者进行检查。如果他们真的需要经常这样做,他们可以编写一个小包装函数。
void v_free_null(vector *v) {
    if( v == NULL ) {
        return;
    }
    v_free(v);
}

在真实系统中(与死亡站9000相反),它将已经在空指针上崩溃。 - user253751
@user253751,“Deathstation 9000”是指“Windows”吗? - Schwern
不,它是一个假设的机器,在面对未定义行为时总是采取最糟糕的行动(通常涉及谋杀程序员)。 - user253751

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接