如何使用C结构体进行良好的编程实践?

5
在C语言中,您可以定义结构体来保存各种变量;
typedef struct {
    float       sp;
    float       K;                 // interactive form - for display only
    float       Ti;                //  values are based in seconds
    float       Td;
} pid_data_t;

假设KTiTd不应该公开设置,只能用于存储已被操作的值。因此,我希望这些值不会被以下内容更新;

pid_data_t = pid_data;
pid_data.K = 10;         // no good! changing K should be done via a function

我希望它们可以通过一个函数来设置;
int8_t pid_set_pid_params(float new_K_dash, float new_Ti_dash, 
    float new_Td_dash)
{
    …                             // perform lots of things
    pid_data->K  = new_K_dash;
    pid_data->Ti = new_Ti_dash;
    pid_data->Td = new_Td_dash;
}

有什么想法吗?我知道C++使用类似于获取/设置属性的方法,但是想知道C语言的做法。

如果您的API客户端知道您结构的布局,则无法真正防止他更改它。您可以做的是隐藏答案所描述的结构的私有部分,使其不在API界面中显示。 - millimoose
4个回答

5

您的公共接口应该只提供一个不透明指针(可能是DATA*data_handle),以及处理器函数create_data()set_data_value()read_data_value()free_data()等,这些函数可以对不透明指针进行操作。

就像FILE*一样。

只要不给客户端提供内部头文件即可 :-)

// library.h

typedef struct data_t * data_handle;

data_handle create_data();
void        free_data(data_handle);

私有实现(不要发布):

#include "library.h"

struct data_t
{
  /* ... */
};

data_handle create_data() { return malloc(sizeof(struct data_t)); }
void        free_data(data_handle h) { free(h); }
/* etc. etc. */

typedef struct data_t *data_handle - Cat Plus Plus
@mriksman:不,客户端C代码中你只会使用data_handledata_handle h = create_data();等等。你通过适当的访问器函数访问所有内容,这些函数都是在data_handle上操作的。 - Kerrek SB
@KerrekSB 谢谢 :) 所以在你的例子中,h 本质上就是我所说的 new_data1;我可以创建一个 h1,一个 h2 等等。今晚我会试一下,并了解一下 mallocfree() 是什么! - mriksman
@mriksman:哦,你不知道malloc()……这只是一个例子。你的实际设计可能需要其他东西;但至少这样做可以确保不向用户提供他们不需要的任何信息,而且他们能做的越少,你就越自由地发展你的代码库。Datawolf的想法是另一个选择。在精神上类似但有所不同。 - Kerrek SB
如果你计划开发嵌入式系统,你还需要学习很多(不冒犯的说)。一旦你完成了这些学习,答案对你来说就显而易见了 :-) - Kerrek SB
显示剩余6条评论

2

在 C 语言中,按照惯例...

对于像这样的 OO C...

我会有一个 pid_data_create(&data) // 初始化你的结构体

然后 pid_data_set_proportional_gain(&data, 0.1);

等等...

所以基本上实现了类似 C++ 的类... 将所有函数的前缀设置为 "class" / "struct" 名称,并始终将结构体 * 作为第一个参数传递。

此外,它应该存储函数指针以实现多态性,并且不应直接调用这些函数指针,再次,有一个以你的结构体作为参数的函数,然后可以进行函数指针调用(可以检查空值、虚拟继承/虚函数和其他内容)。


2

实现这一目的的规范方法是使用不透明指针和公共结构体,以及分配器、获取器和设置器来处理私有元素。大致如下:

foo.h

typedef struct Foo {
    /* public elements */
} Foo;

Foo *new_Foo(void);
void Foo_something_opaque(Foo* foo);

foo.c

#include "foo.h"

typedef struct Private_Foo_ {
    struct Foo foo;
    /* private elements */
} Private_Foo_;

Foo *new_Foo(void)
{
    Private_Foo_ *foo = malloc(sizeof(Private_Foo_));
    /* initialize private and public elements */
    return (Foo*) foo;
}

void Foo_something_opaque(Foo *foo)
{
    Private_Foo_ *priv_foo = (Private_Foo_*) foo;
    /* do something */
}

这个方法可行是因为C语言保证了结构体变量的地址始终等于第一个结构体元素的地址。我们可以利用这一点,在私有Foo_结构体中包含一个公共的Foo,将整个结构体的指针分配出去,编译单元无法访问Private_Foo_结构体定义,只能看到一些没有上下文的内存。
需要注意的是,C++在幕后工作方式相当类似。
更新
正如KereekSB所指出的那样,如果在数组中使用它,它会破坏它。
我的建议是:不要创建Foo f[],虽然很诱人,而是创建指向Foo的指针数组:Foo *f[]
如果非要在数组中使用它,请执行以下操作:
foo_private.h
typedef struct Private_Foo_ {
    /* private elements */
} Private_Foo_;

static size_t Private_Foo_sizeof(void) { return sizeof(Private_Foo_); }

foo_private.h被编写成可以编译为对象文件的方式。使用一些辅助程序链接并使用Private_Foo_sizeof()的结果从某个foo.h.in文件生成实际的、平台相关的foo.h。

foo.h

#include

#define FOO_SIZEOF_PRIVATE_ELEMENTS <generated by preconfigure step>

typedef struct Foo_ {
    /* public elements */
    char reserved[FOO_SIZEOF_PRIVATE_ELEMENTS];
} Foo;

Foo *new_Foo(void);
void Foo_something_opaque(Foo* foo);

foo.c

#include "foo.h"
#include "foo_private.h"

Foo *new_Foo(void)
{
    Foo *foo = malloc(sizeof(Foo));
    /* initialize private and public elements */
    return (Foo*) foo;
}

void Foo_something_opaque(Foo *foo)
{
    Private_Foo_ *priv_foo = (Private_Foo_*) foo.reserved;
    /* do something */
}

个人认为这真的很混乱。虽然我喜欢智能容器(不幸的是,C没有标准容器库)。无论如何:在这样一个容器中,它通过像函数一样创建

Array *array_alloc(size_t sizeofElement, unsigned int elements);
void *array_at(Array *array, unsigned int index);
/* and all the other functions expected of arrays */

可以参考libowfaw的实现示例。对于类型Foo,提供一个函数非常容易。

Array *Foo_array(unsigned int count);

所有以“双下划线字母”开头的名称都是保留的。 - Kerrek SB
@KerrekSB:没错...已经改成单下划线了,应该没问题。这些标识符也不会从编译单元中导出(至少不应该)。 - datenwolf
很不幸,所有以“下划线-大写字母”开头的名称也被保留了! :-) - Kerrek SB
有什么冲突吗?理论上,您不会将私有头文件发送到客户端,对吧?它仅在库内使用。 - Kerrek SB
@KerrekSB:把它看作是对库实现者的一个亮点,不断提醒他,他正在稍微不同的上下文中工作。这不是为了编译器/链接器,而是为了人类。 - datenwolf
显示剩余2条评论

0

面向对象是一种思考和建模的方式,数据封装可以通过这种方式实现,其中结构化数据不应该直接被用户修改:

my_library.h

#ifndef __MY_LIBRARY__
#define __MY_LIBRARY__
typedef void MiObject;

MyObject* newMyObject();

void destroyMyObject(MyObject*)

int setMyObjectProperty1(MyObject*,someDataType1*);

/*Return a pointer to the data/object,  classic pass by value */
someDataType1* getMyObjectProperty2Style1(MyObject*);

int setMyObjectProperty2(MyObject*,someDataType2*);

/* The data/object is passed through reference */
int getMyObjectProperty2Style2(MyObject*,someDataType2**);

    /* Some more functions here */
#endif

my_library.c

struct _MyHiddenDataType{
    int a;
    char* b;
    ..
    ..
};

MyObject* newMyObject(){
struct _MyHiddenData* newData = (struct _MyHiddenData*)malloc(sizeof(struct _MyHiddenData);
//check null, etc
//initialize data, etc
return (MyObject*)newData;
}

int setMyObjectProperty1(MyObject* object,someDataType1* somedata){
    struct _MyHiddenData* data = (struct _MyHiddenData*)object;
    //check for nulls, and process somedata
    data->somePropery=somedata;
}

someDataType1* getMyObjectProperty2Style1(MyObject*){
    struct _MyHiddenData* data = (struct _MyHiddenData*)object;
    //check for nulls, and process somedata
    return data->someProperty;
}
/* Similar code for the rest */

这样,您就将结构属性封装起来,就好像它们是私有的一样。同样,my_libray.c内部的静态函数将表现为私有函数。仔细研究C语言,您会发现,您的想象力是您所能做到的极限。


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