struct person_t {
name_t name;
age_t age;
};
void add_person_0(name_t name, age_t age);
void add_person_1(struct person_t const* person);
add_person_0("hideyosi", 16);
struct person_t person = { "hideyosi", 16 };
add_person_1(&person);
name
和 age
之间插入一项 gender
(当然前一种人员添加接口也有变动):struct person_t {
name_t name;
gender_t gender;
age_t age;
};
void add_person_0(name_t name, gender_t gender, age_t age);
void add_person_1(struct person_t const* person);
显然两种情况都好不到那里去.
对于
add_person_0
这个接口, 由于参数个数都改变了, 签名对不上号, 那么使用该接口的一方在通过动态库访问此函数时会少压入一个参数, 并且压入的第二个参数 age 对上的还是 gender 这个参数的位置. (在 C++ 中, 由于允许函数重载, 动态库中的函数名会因签名不同而被 name-mangling 成不同的函数, 因此会导致函数找不到错误. 当然这并不意味着 C 和 C++ 孰优孰劣, 因为这本来就是两个不同的语言, 只是在这一个问题上有不同的死法罢了.) 这样一调用函数程序立马崩掉不客气.而对于第二种使用, 因为使用方在初始化
person_t
时, 仍然按照旧的逻辑, 即在偏移为 0 的地方放上 name
, 在偏移为 sizeof(name_t)
的地方放上 age
, 然后将这样初始化的 person 结构体传递给接口提供方; 而此时提供方对这一片内存区的解释有很大不同, 至少它认为的 sizeof(person_t)
都比使用方给出的要多出一个 sizeof(gender_t)
. 使用这样的 person
就像拿着三年前的武汉市地图到今天的洪山广场游玩结果掉进工地不明不白地挂掉了一样.那么有没有什么无痛的方法能够在这种不同模块分离更新的环境下让程序仍然, 至少在新来的人可能都缺少性别的情况下, 能够继续像模像样地跑呢?
要找到这个方案, 自然应该先看看问题出在什么地方. 这里问题的核心围绕着内存布局, 即双方在
person_t
这一结构体到底包含了什么数据需要达成一致, 换句话说就是因为 person_t
, 双方紧耦合了.解决方案很自然的就是, 把所有跟
person_t
内存布局相关的代码全部移到一个模块 (当然是接口提供方那边) 里去, 并提供一套操作接口 (为了彻底不让使用方知道 person_t
的内存布局, 干脆只给个前置声明好了)struct person_t; // 仅前置声明
struct person_t* new_person(); // 这里不可以有参数, 否则会如 add_person_1 一样发生悲剧
void delete_person(struct person_t* person);
void set_person_name(struct person_t* person, name_t name);
void set_person_age(struct person_t* person, age_t age);
void add_person(struct person_t* person);
struct person_t* person = new_person();
if (person == NULL) {
return /* HERE MUST BE SOME ERROR */;
}
set_person_name(person, "hideyosi");
set_person_age(person, 16);
add_person(person);
delete_person(person);
gender_t
, 加就加吧, 反正都是接口提供方的事情了. 接口提供方需要做- 添加接口
set_person_gender(struct person_t* person, gender_t gender);
- 修改
new_person();
, 并且需要给出默认的gender
, 因为使用方没有及时更新而不会设置这一项 - 如果不能接受没有设置
gender
项的person_t
, 那么新版本的动态库应该在new_person
时给出一个默认值, 以便兼容旧版本代码的调用 (各种向前兼容的来源么) - 有必要的话, 修改
delete_person(struct person_t* person);
扩展阅读: 在 C 标准库中大名鼎鼎的
FILE*
也采用了类似的设计. 使用 FILE*
并不是直接访问其中的成员, 而是通过 fopen
, fclose
, fseek
等等函数去操作. 而 Linux API 就做得更绝了, 反正文件数据结构都存在内核里面, 给外面只暴露个 int file_descriptor
就行了吧.