首页 > 代码库 > C语言不完全类型与延迟定义
C语言不完全类型与延迟定义
一直以为我的C语言学的还可以,虽说不是出神入化,但是至少比较熟悉吧。但是前一段时间看了一篇微信推文,再百度了一下C语言不完全类型。发现我居然C语言不完全类型和用途甚广的延迟定义都没概念。这两天仔细查阅了相关概念并用代码实验了一下。
本文结构如下:
- C语言不完全类型概念介绍
- 一个故事
- 延迟定义的优点
- 思考…
C语言不完全类型
不完全类型也就是不知道变量的所有的类型信息。比如可以声明一个数组,但是不给出该数组的长度;声明一个指针,但是不给出该指针的类型;声明一个结构体类型,但是不给出完整的结构体定义,只说它是一个结构体。但是最终你必须得给出完整的类型信息。要不然编译会报错的。编译器在编译某个单元时,如果遇到一个不完全类型的定义的类型或变量(假设它叫p),它会把这当作正常现象,然后继续编译该单元,如果在本单元内找不到p完整的类型信息,它就去其它编译单元找。如果把整个编译过程分为编译、链接两个过程。在编译阶段遇到不完全类型是正常的,但是在链接过程中,所有的不完全类型必须存在对应的完整类型信息,否则报错。
举个例子,下面的代码先声明了一个不完全类型的变量字符数组str,没有给出它的长度信息。然后再定义了一次str数组,这次给出的长度信息。
char str[];//不完全类型定义
char str[10];//终于遇到了str数组的完整类型信息,编译器松了一口气
注意:不完全类型定义不适合局部变量,如果把上面两行代码放在一个函数体中,会出现符号重定义错误。
不完全类型由于不包含具体的类型信息,所以不能通过sizeof来获得其大小。(编译器君的旁边:我连它的完整类型都不知道,我怎么告诉你它的大小。)下面的代码不能编译通过。它会报错 error: invalid application of `sizeof‘ to an incomplete type 不能对不完全类型使用sizeof。
#include<stdio.h>
char str[];
int n =sizeof(str);
char str[10];
int main()
{
printf("%d ",n);
return 0;
}
如果把int n = sizeof(str)放到charstr[10]后面就没事了。
再举一个结构体的例子。下面的代码先声明了一个不完全类型的结构体s。然后又定义了该结构体。
struct s;
struct s{
int a;
int b;
};
一个故事,不完全类型的应用:延迟定义
(哈哈,终于要讲应用了,好激动!)
C语言的不完全类型的一般用作模块化编程中。现在假设你要实现一个保存字符的栈给你的程序员队友用。于是你写出了下面的代码。
//Stack.h
typedef structNode{
char data;
struct Node* next;
}Node;
typedef struct {
Node* top;//只需要栈顶指针
int num;
}Stack;
/*为了快速返回栈中元素的个数,你用一个int变量来记录栈中元素的个数,在入栈和出栈时对num进行增减操作,这样在返回栈中元素的个数的时候,就可以直接返回num变量了。*/
/构造一个空栈S
voidInitStack(Stack **s);
//销毁栈S
voidDestroyStack(Stack **s);
//判断栈是否为空
intStackEmpty(Stack *s);
//返回栈的长度
intStackLength(Stack *s);
//用e返回栈顶元素
int GetTop(Stack*s,char *e);
//入栈
void Push(Stack*s,char e);
//弹出栈顶元素,并用e返回其值
int Pop(Stack*s,char *e);
为了不暴露实现细节,你将实现栈操作的各种实现代码封装成到了一个库文件中(静态、动态都行)。然后将这个.h文件和库文件信心满满的提交给了队友。并暗示自己“我果然是个牛x的程序员,小美一定会崇拜我、尊敬我、对我欲罢不能!不行,我要谦虚,当作什么也没发生”。
几个小时候,队友皱着眉头过来了。你心想“不对啊,怎么不是崇拜的眼神”。然后发生了下面的一段对话。
“你封装的什么啊?连个栈中元素的个数都统计不对”,队友很生气
“怎么可能?为了快速返回栈中元素的个数,我特意用了一个int变量来保存栈中元素的个数”你也很生气,明明用一个单独变量来保存元素个数是你的得意之处,居然被人说不对。
“我知道,可就是不对”,队友坚持说你不对。
然后为了找出问题,你和队友一起检查他写的代码。你发现他写了如下代码。
…
Push(s);
++(s->num);
…
Pop(s,&e);
--(s->num);
原来队友在压栈和出栈时都对栈s的num进行了加减操作。
你:“为什么你要动我的num?”
队友:“你的num不是用来保存栈中元素的个数的吗?我压栈、出栈当然要对它进行增减啊!”
你:“我在实现代码中已经这么做了。”
原来你以为理所当然的事,队友却和你想的不一样。然后你告诉他不要多事,num的大小由你来维持。你们终于又可以一起愉快的玩耍了。
但是你在队友的代码里还是发现像下面一些不爽的代码,这让你感到膈应。
e =s->top->data;
int len =s->num;
为了获得栈顶元素,他没有用你提供的GetTop函数,而是自作主张的直接通过指针直接访问栈内部的top指针。而且没用你给的StackLength函数,而是直接访问栈结构体的num成员。虽然你可以和他沟通,让他不要自己去访问你的实现细节。但是你不能保证他会完全听你的。能不能从技术上解决这个问题呢?个人觉得,团队协作时,能从技术上解决的事,不必从沟通上解决。这样可以减少团队沟通成本。扯多了,回到刚才的问题——我们能否从技术上解决这个问题?
问题分析和解决方法
回头看看我们刚才写的代码。队友会直接访问我们封装的内部细节,是因为他看到了我们给的.h文件中结构体的详细定义。那么能不能去掉.h文件中Stack结构体的定义呢?显然不行,因为我们给出的几个函数接口都要用到Stack类型的参数,如果用户看不到Stack的定义,那他怎么定义Stack类型的变量然后传递给这些接口函数呢?答案是,我们给出Stack结构体类型的定义,但是不给出Stack结构体的详细信息。也就是前面讲到的不完全类型定义。改进后的代码如下。
//Stack.h
typedef structSqStack Stack;
//…各个函数接口定义
//Stack.c
typedef structNode{
char data;
struct Node* next;
}Node;
struct SqStack{
Node* top;
int num;
};
在改进后的代码中,我们只是定义了一个不完全类型struct SqStack结构体,并用typedef将其和Stack名的类型等价。而将SqStack结构体的详细定义放到了.c文件中。这样将.h文件和库文件提交给队友后,他再也看不到Stack的详细信息了。这下,他就没有直接访问结构体内部的冲动了。(看你丫还怎么访问?老老实实用哥提供的函数接口吧!)队友看不到Stack结构体的内部是怎么定义的,自然也不知道怎么访问,而且任何通过Stack类型指针变量的访问方式都会让编译器报错。这下,我再也不用担心队友的愚蠢了。
上面讲到的,在头文件中只定义一个结构体的不完全类型,而将结构体的详细定义推迟到在.c文件的方式就叫“延迟定义”。
延迟定义带来的其它好处
在上面的实例过程中,虽然我们是为了隐藏实现细节才使用的延迟定义。但是延迟定义也带来了其它好处。另一个好处就是,我们可以以很小的工作量代价来更改实现。比如我们发现Stack结构的实现有bug需要修正,或者我们想将内部的栈的链式存储改为顺序存储。我都可以直接在.c文件中修改,然后重新编译生成新的库文件,不需要修改.h文件,因为我们的接口函数并不需要变。将库文件提交给用户,用户只需替换原来旧的库文件,而不需要修改代码。如果是动态链接库,用户只需替换库文件就行。如果是静态库,用户只需替换库后重新链接一下。至始至终,用户都不需要修改代码,不需要重新编译自己的客户端代码。
如果没有用延迟定义,将内部细节放在.h文件中,那修改细节后,用户的客户端代码也需要重新编译。
延迟定义的优点
我们来总结一下延迟定义的优点:
1. 隐藏了内部实现细节,强制用户按接口规则访问。减少沟通成本。
2. 便于修改。
上面两点都是实现模块化编程所必须的。而且个人认为,站在客户的角度,知道的细节越少越好,知道的越多,要记忆和思考的东西也越多。就像电视剧中的经典台词:“有时候知道的太多并不是好事”。角色被人干掉的理由也是“你知道的太多了”,或者“你知道了你不该知道的事情”。我只按照既定的规则来访问别人提供的代码,有什么问题直接问别人,而不是看实现代码。按照既定规则来访问也便于职责划分。如果非要总结延迟定义带来的第3个好处,个人认为是:便于职责划分,清晰职责边界。
思考…
Q:既然延迟定义便于模块化编程,那C++、Java等面向对象的语言中有用到延迟定义类似的技术吗?
A:当然有,只是面向对象语言中不叫“延迟定义”。而是叫封装、访问权限、接口和多态。类的封装和访问权限可以阻止客户访问类的实现细节,而接口和多态可以隐藏实现细节。当然它们的好处不只这些。