9

为了获得干净的代码,使用一些 OO 概念可能很有用,即使在 C 中也是如此。我经常编写由一对 .h 和 .c 文件组成的模块。问题是模块的用户必须小心,因为私有成员在 C 中不存在。使用 pimpl 习惯用法或抽象数据类型是可以的,但它添加了一些代码和/或文件,并且需要更重的代码。我讨厌在不需要访问器时使用访问器。

这是一个想法,它提供了一种让编译器抱怨对“私人”成员的无效访问的方法,只需要一些额外的代码。这个想法是定义两次相同的结构,但为模块的用户添加了一些额外的“const”。

当然,使用演员表仍然可以写入“私人”成员。但重点只是避免模块用户出错,而不是安全地保护内存。

/*** 2DPoint.h module interface ***/
#ifndef H_2D_POINT
#define H_2D_POINT

/* 2D_POINT_IMPL need to be defined in implementation files before #include */
#ifdef 2D_POINT_IMPL
#define _cst_
#else
#define _cst_ const
#endif

typedef struct 2DPoint
{
    /* public members: read and write for user */
    int x;
    
    /* private members: read only for user */
    _cst_ int y;
} 2DPoint;

2DPoint *new_2dPoint(void);
void delete_2dPoint(2DPoint **pt);
void set_y(2DPoint *pt, int newVal);


/*** 2dPoint.c module implementation ***/
#define 2D_POINT_IMPL
#include "2dPoint.h"
#include <stdlib.h>
#include <string.h>

2DPoint *new_2dPoint(void)
{
    2DPoint *pt = malloc(sizeof(2DPoint));
    pt->x = 42;
    pt->y = 666;

    return pt;
}

void delete_2dPoint(2DPoint **pt)
{
    free(*pt);
    *pt = NULL;
}

void set_y(2DPoint *pt, int newVal)
{
    pt->y = newVal;
}

#endif /* H_2D_POINT */


/*** main.c user's file ***/
#include "2dPoint.h"
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    2DPoint *pt = new_2dPoint();

    pt->x = 10;     /* ok */
    pt->y = 20;     /* Invalid access, y is "private" */    
    set_y(pt, 30);  /* accessor needed */
    printf("pt.x = %d, pt.y = %d\n", pt->x, pt->y);  /* no accessor needed for reading "private" members */

    delete_2dPoint(&pt);
    
    return EXIT_SUCCESS;
}

现在,问题来了:这个技巧是否符合 C 标准?它适用于 GCC,编译器不会抱怨任何事情,即使有一些严格的标志,但我怎么能确定这真的没问题?

4

4 回答 4

7

这几乎可以肯定是未定义的行为。

禁止写入/修改声明为的对象const,这样做会导致 UB。此外,您采用的方法重新声明struct 2DPoint为两种技术上不同的类型,这也是不允许的。

请注意,这(通常作为未定义的行为)并不意味着它“肯定不会工作”或“它必须崩溃”。事实上,我觉得它起作用是很合乎逻辑的,因为如果一个人聪明地阅读源代码,他可能很容易发现它的目的是什么,以及为什么它可能被认为是正确的。然而,编译器并不智能——充其量,它是一个有限自动机,不知道代码该做什么;它只(或多或少)遵守语法的句法和语义规则。

于 2012-12-26T16:55:46.430 回答
3

这违反了 C 2011 6.2.7 1。

6.2.7 1 要求同一结构在不同翻译单元中的两个定义具有兼容的类型。不允许有const一个而不是另一个。

在一个模块中,您可能会引用这些对象之一,并且这些成员对于编译器来说似乎是 const。当编译器写入对其他模块中的函数的调用时,它可能会将来自 const 成员的值保存在寄存器或其他缓存中,或者保存在源代码后面的部分或完全评估的表达式中,而不是函数调用。那么,当函数修改成员并返回时,原来的模块不会有改变的值。更糟糕的是,它可能会使用更改值和旧值的某种组合。

这是非常不恰当的编程。

于 2012-12-26T17:29:11.247 回答
0

用 Bjarne Stroustrup 的话来说:C 不是为支持OOP 而设计的,虽然它支持OOP,这意味着可以用 C 编写 OOP 程序,但很难做到。因此,如果您必须用 C 编写 OOP 代码,使用这种方法似乎没有错,但最好使用更适合该目的的语言。

通过尝试用 C 编写 OOP 代码,您已经进入了一个必须覆盖“常识”的领域,因此只要您负责正确使用它,这种方法就可以了。您还需要确保它被彻底和严格地记录在案,并且与代码有关的每个人都知道它。

编辑哦,您可能必须使用演员才能绕过const. 我不记得 C 风格的演员表是否可以像 C++ 一样使用const_cast

于 2012-12-26T16:55:21.810 回答
-1

您可以使用不同的方法 - 声明两个structs,一个用于没有私有成员(在标头中)的用户,另一个用于在您的实现单元中内部使用的私有成员。所有私有成员都应该放在公共成员之后。

您总是将指针传递给struct并在需要时将其转换为内部使用,如下所示:

/* user code */
struct foo {
    int public;
};

int bar(void) {
    struct foo *foo = new_foo();
    foo->public = 10;
}

/* implementation */
struct foo_internal {
    int public;
    int private;
};

struct foo *new_foo(void) {
    struct foo_internal *foo == malloc(sizeof(*foo));
    foo->public = 1;
    foo->private = 2;
    return (struct foo*)foo;  // to suppress warning
}

C11 允许未命名的结构字段(GCC 有一段时间支持它),因此如果使用 GCC(或符合 C11 的编译器),您可以将内部结构声明为:

struct foo_internal {
    struct foo;
    int private;
};

因此不需要额外的努力来保持结构定义的同步。

于 2012-12-26T19:30:23.107 回答