使用常量指针封装C语言数据

4
我有一个关于在C中封装我的模块(一个.c/.h文件对)的问题。通常人们使用访问函数,如getTime(char * time)等来访问模块的内部变量,而不提供干扰它们的能力。我现在正在使用指向内部结构体的常量数据的常量指针来做同样的事情。我认为这很有效,因为其他模块可以查看结构中的数据而无法更改它,这样可以节省函数调用的开销。(这是针对嵌入式设备的,因此函数调用比较“昂贵”)。我想知道一些关于这是否是有效封装的意见。我想他们可能会将一个非常量指针设置为它,然后就可以干扰数据了?一个例子:blah.h
typedef struct {
    unsigned char data;
    unsigned int intdata;
} myStruct;

extern const myStruct * const ptrConstMyStruct;

blah.c

static myStruct thisIsMyModulesData;
const myStruct * const ptrConstMyStruct = &thisIsMyModulesData;

anotherFile.c

variable = ptrConstMyStruct->data;
ptrConstMyStruct->data = variable; //compile error!

1
你真的需要在这里使用指针吗? - Tony The Lion
我猜那就是你能做的最好的了。在C++中,我们也有const_cast可以去除const属性,但我们都要忍受它。但有一个限制,如果'data'是一个结构体,例如像ptrConstMyStruct->data->foo = variable这样的语句,你无法防止对'data'成员的修改。 - vrk001
你认为只有通过函数调用来访问模块中的数据才是“正确”的封装吗?如果你需要一个结构体,你会将结构体传递进去,然后模块会复制自己的数据,这样你甚至无法访问原始数据。我觉得我越想越明白了,我自己回答了这个问题。 - Revenant
@vrk001:在C++中,const_cast并不是你想象的那样。你可能会想到一些未定义行为。 - Kerrek SB
我一直认为UB代表未定义行为。 - olovb
4个回答

2

使用不完整类型和在头文件中仅仅前向声明结构体是首选方法。

在封装方面,const性更多的是一种声明特定函数不会改变给定对象的方式。在您的解决方案中,您必须强制转换constness,这似乎是违反直觉的。或者您从未将const指针用作可变函数的参数吗?

此外,封装是关于隐藏实现和细节的,如果您暴露了实现细节,那么这就无法做到。

编辑以解释前向声明:

MyCode.h:

struct my_struct;

MyCode.c:

struct my_struct { .... };

以上意味着MyCode的用户将能够使用指向my_struct的指针,但不能检查其成员。

它会是什么样子?我不确定如何前向声明typedef结构体。 - Revenant
我认为我使用封装这个术语来表示数据保护,这是我的错误,抱歉 :) - Revenant
@Revenant 在 Stack Overflow 和网络上搜索“不透明类型”,我认为这是最常用的术语。 (“不完整类型”是 C 标准中适用于许多其他变量类型的概念,而不仅仅是结构体。) - Lundin

1

封装允许您更改内部机制,并防止用户更改您的内部数据。一种方法是使用getter和setter函数,这将同时封装并允许您执行const操作。


我认为你所描述的是常见的方式,只是不够高效。 - Revenant
没错,这就是权衡。在大多数情况下,可维护的代码胜过效率。你是在时间关键的内部循环中执行此操作吗? - Yusuf X
1
@Revenant 它确实非常高效。这样的函数通常会被编译器的优化器内联。 - Lundin

1

你的问题中有一些陈述可能暗示了问题的真正来源。

我认为这很有效,因为其他模块可以查看数据...

如果其他模块可以查看数据,那么它就不是封装的。如果其他模块需要查看(原始)数据,那么为什么要尝试将其设置为私有?这暗示了程序设计中存在一些根本性缺陷。

extern

在C语言中,您永远不应该需要使用全局变量(也许除了MCU硬件外设寄存器)。在过去的10年中,我几乎完全使用嵌入式实时系统,并且从未使用过全局变量。同样,这表明程序设计中存在一些问题。

...它可以节省函数调用的开销。(这是针对嵌入式系统的,因为函数调用比较“昂贵”)。

不是这样的。C语言已经支持函数内联13年了,即使你使用旧的C90编译器,我敢打赌它也有内联选项,例如#pragma inline之类的。我还没有见过一个缺乏内联功能的嵌入式编译器。此外,即使是旧的编译器,在启用优化的情况下,也相当擅长执行内联,而无需程序员提供任何显式提示。

另外,函数调用开销是否成为程序的瓶颈,你是否通过基准测试/示波器测量发现了?如果没有,那么为什么要用全局变量和暴露私有数据的奇怪指针来混淆你的代码呢?这是过早的优化。


0

除非你有能力在“用户”读取结构体的位置和数据所在位置之间注入代码,否则它不是封装。这样的垫片允许您更改结构体的内部而不更改结构体的外部使用。

虽然您的解决方案改进了C语言中通常的做法,但要进行封装,您需要能够将字段从单个值更改为构造值,而不更改任何“外部”代码以证明它真正被封装。

在C语言中,通常通过将数据隐藏在void指针后面或在封装代码的外部部分使用已声明(但未定义)的结构体来执行此操作。

blah.h

struct myStruct_t;
typedef struct myStruct_t myStruct;

extern myStruct * const ptrConstMyStruct;

// if you want a constructor, you need to declare the
// method here, because only blah.c will know the struct
// size with this solution.
myStruct * new_myStruct();

// methods
myStruct_setData(myStruct* obj, char c);
char myStruct_getData(myStruct* obj);

blah.c

#include "blah.h"

struct myStruct_t {
    unsigned char data;
    unsigned int intdata;
};

static myStruct thisIsMyModulesData;

// no need to make the struct const, just the pointer
// otherwise, we would have to break the const just to change
// data, which now will only be done within this file.
myStruct * const ptrConstMyStruct = &thisIsMyModulesData;

anotherFile.c

#include "blah.h"
// anotherFile "knows" of a struct called myStruct, but
// it doesn't know what it contains, or even it's size.

// this is no longer possible
// now "data" is encapsulated, and can be changed
// by reimplementing `myStruct_getData(...)`
// (as long as we don't change the method signature).
variable = ptrConstMyStruct->data;

// this is the "new" way
variable = myStruct_getData(ptrConstmyStruct);

// with the old, compiler error because of 
// assigning a value to a const value.
ptrConstMyStruct->data = variable; //compile error!
                       ^
              (error occurs here)

// with the new, compiler error because of
// dereferencing a pointer to a unknown / incomplete type.
ptrConstMyStruct->data = variable; // compile error!
                ^
        (error occurs here)

正如您所看到的,错误的位置决定了封装与否。如果您在赋值时检查错误而不是解引用,则无法通过ptrConstMyStruct->data更改指针和数据之间的关系。


这通常被称为“不透明类型”。它是一种面向对象的设计,类似于其他语言中的抽象基类和继承。但是你不应该使用全局变量来实现这个。相反,让定义类型的模块通过一个函数分配一个不透明类型的实例,并将指向该分配实例的指针返回给调用者。(在C++中,这与抽象基类指针的工作方式非常相似) - Lundin
很高兴知道它有一个名字。我也不喜欢全局变量,但是因为这是他开始使用的,所以我还是保留了它。 - Edwin Buck

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