C语言的理解

中山野鬼 发布于 2012/03/09 15:14
阅读 1K+
收藏 13

一直想整理一套C语言的材料。因为,工作这么多年,回头看看一些传统C教程,参照工作和C的国际标准,发现错误不少。另外经常接触和有意识的培训些C的新学者,发现很多思维并不妥当(针对C)。

因此,就借宝地,写点对C的理解,也希望大家多多提意见。但希望大家注意,我只是针对C,甚至不要联系到C++上。C++和 C我仍然坚持认为,完全是两个东西,只是很多库可以相互用,语法中有相似的地方。

一、类型,变量,值

写这个,是因为早期有个说法(显然是高人说的,不是我说的),程序=数据+算法。因此先从数据谈。

1、变量,我的理解:实质是存储空间。无论什么变量。

由此,当你声明了一个变量时,你应该非常明确,针对指定编译器,这个变量的存储位置或存储类型。

这里我特地强调,指定的,编译器。谈到指定,是因为,不同编译器对相同代码的看法不同。例如通常,对于一个函数接口如下

int func_demo(int a,int b,int c,int d, int e,int f)

只会将前4个放入寄存器,其余通过栈进行传递,而也有一些特殊CPU,寄存器可以放8个非指针型,4个指针型。因此,对于e,f这两个变量,究竟放哪不是由你代码决定的。也不是连接器决定的。而是编译器决定的。

很多书籍强调什么”形参,实参“,参来参去,至少把以前的我搞晕呼了。如果你能搞清楚,函数调用子函数时,例如

int i;

for (i = 0  ;i < 10 ;i++){

    sub_func(i);

}

这个i在这个函数里,属于这个函数的局部变量,是存放在栈里(特定编译器,有些不是,但我不再强调”特定编译器“了),而sub_func函数在自身实现体里,如下

void sub_func(int input_data){

    printf("test to show input_data: %d\n",input_data);

}

此时input_data就是放在寄存器里的。你就不用再琢磨,哪个是形参,哪个是实参。也不要琢磨,变量i ,和变量 input_data是否是不同的变量,是否子函数的值改变是否影响外层。

通常,C语言的所有变量,所对应的存储空间有几种。

数据区,常量区(我对变量的认识不同),堆栈,寄存器。这些都是编译器的事情,和连接没关系。对应.o文件里都有相关的记录信息。数据区主要指全局或静态变量,堆栈主要是放函数的内部变量。涉及到一些函数与函数的传递,通常优先放在寄存器里,当然你也可以强制制定

registor int i;

这样的变量,强制放在寄存器里。无论是局部还是全局变量。

static很多人搞不清楚,说实话,C国际标准,有个中文版,号称国家标准,我本人英语真的很差,但那翻译的更差,只能诸位入门用。而英文版,鉴于我有限的英语抽象思维水平,认为也没说清楚。

但至少有两个内容时明确的。”静态,局部“

先说局部。局部其实也很简单。以下一个例子
static int s_total = 0;
int test_func(int a){
    return s_total += a;
}
假设上面的内容都在a.c中。则在b.c中,你尝试
extern int s_total;(引用非本文件域的变量)
你就会发现,在连接时找不到s_total。这是因为,C标准默认函数外的变量,存在extern 属性,只有显示的声明 static,才不可被外部作用域的代码访问。

静态也很好理解,如同我认为,变量是指一个存储空间,则这个静态意味着,这个存储空间不变。因此,对于函数体内的静态变量,会不和堆栈有关系,而是存放在堆里,落在编译时,会放在数据区。

例如

int ext_total = 0;

int func_test(int a){

    static int total = 0;

    return total += a;

}

此时,这个total变量的存储空间是独立的,因此每次调用都会对这个存储空间进行操作,你可以看做,这个total和函数外的一个变量存储空间ext_total的存储空间类型近似。而

int func_test2(int a){

    int total = 0;

    return total += a;

}

这个total就不一样了。解释这个问题,需要解释下,函数调用怎么实现的。(注意这个概念几乎是绝大多数高级语言,无论是C++,还是 JAVA函数最终执行时的共同原理,有人说这属于《编译原理》的范畴,我更倾向属于《操作系统》或《计算机组成原理》的内容)

大家都知道堆栈,因为CPU的中有个类似SP命名的寄存器。一个函数调用另一个函数,总要有信息交互而且很多(包括上下文切换),由于栈可以说是最简单的数据结构,所以这个交互空间用栈这个数据结构。函数A调用函数B至少要保存以下几个内容。

1、我在哪调用的。B函数返回是,PC(指令地址寄存器)需要转移到调用B的下一条语句(属于函数A)

2、我想传哪些(在寄存器里放不下的)额外数据给你

而对于编译器而言,当发现函数B里有局部变量时,通常就借用这个堆栈的空间,将堆栈的指针移动一下,空出一些空间,临时给予这些局部变量作为存储空间,而这个空间只有被调用时,才能根据SP所指向地址,确定,因此理论上说,每次调用一个函数。存放在堆栈里的该函数的局部变量的实际地址是变化的,当然实际情况如果恰巧SP没有变化,则也会出现相同的情况,但只是碰巧,概率还挺大,可绝对不能保证这个概率百分百发生。这样操作是有以下理由的。

1、不是每个函数都会在执行时调用。没有必要对函数内的所有变量在编译时就划分空间。

2、这样相对有更快的速度。因为虽然使用堆栈,导致变量的访问存在一个二次地址计算的问题,全局变量在连接时会分配相对本程序的内部静态地址。但毕竟堆栈通常放在L1 CACHE里。程序中的函数,则未必。特别是CACHE的工作机制特殊,或cache line 较小时。这里不细说CACHE机制对程序速度的影响以及应对策略。就举个具体我以前工作中的实际数据。

从L1读一个变量到 寄存器,2个 周期(主频)

从L2读一个变量到寄存器,8个周期,

从外部MEM读一个变量到寄存器,你就等吧。数据我就不说了,相对L1不止几十倍。我强调是一个变量,不是一批变量的平均传输。如果有人有意见,说DDR3 都 有 2GHz了,那不妨请你去理解一下SDRAM的工作机制,以及DDR,DDR2,DDR3的操作差异性,再琢磨只读一个32位数要多少时间。且不谈CPU内部的总线控制器的调度耗时。

废话这么多。只是想让新学者明确一个概念,变量只是个存储空间。而其存储空间的性质是由编译器通过类型决定的,编译器又受到硬件特性的约束。而实际硬件,如果不谈数据运算操作。那么看到的都只是空间。唯一属性只有位宽。由此,C语言存在强制转换的使用。

对新手我不建议使用强制转换,甚至理论上应该针对所有WARNING,进行调整代码,使得WARING 0,因为对于其他高级语言,很多C的 WARNING,是这个类型不匹配,会直接当作ERROR处理掉。以防止你出错。而你对强制转换没概念时,则可能引发错误。不过可笑的是,如果要去掉这写WARNING,你必须要使用强制转化的方式。不是我调戏新手,确实如此。

例如

unsigned int test_2(int a){

    return a;

}

此时肯定会有warning。要想去掉,应该

unsigned int test_2(unsigned int a){

    return a;

}

unsigned int test_2(int a){

    return (unsigned int )a;

}

有人说是我在讲废话,或者说,C编译器,或C标准太差了。我会在类型说明中展开讨论。

关于变量的命名

有什么匈牙利法,或i,j,k法。其实方法不重要。重要是,给别人看的,最低底线是,别你的文档解释自相矛盾,且不可扩展引述,比如前缀是 g_,你说是全局变量,结果一个局部变量你也这么定义。比如 前缀是 _r的,你说是寄存器变量,结果有个_r的不是。

至于函数内,变量的命名,应该以能清晰说明逻辑原理的规则。此时匈牙利法就很麻烦。

关于变量的声明和使用

新版本(也不新了),支持变量声明不放函数体内最顶端。这是个包容的做法。但对于新程序员建议不要使用。严格按照以下规则处理。

1、函数体内,存在较长逻辑周期的变量,都集中到顶端。

2、函数体内,较短逻辑周期的变量,用{ }包一下,再声明。甚至可以写成如下宏

#define TEST_1(a,b) do { int tmp1; tmp1 = a; a = b; b =tmp1;}while(0)

这样的好处是,调用时, TEST_1(a,b);你必须强制加;号,则和普通代码一样,但也有缺点,例如这样你就惨了,TEST_1(3,4);我会在“值”的里面扩展建议常量的处理方法,此处不展开。

但想很抬杠的说一点,很多人,批评C的#define ,甚至很多C程序员自己也开始拒绝使用#define ,但我只能说,#define 真的很好用,准确说。真的能很好的表达逻辑,前提是,你要会使用。任何反驳#define 不好的人,我认为不是#define的错,也不是他们的观点错。而是他们的思维方式不够 #define ,没有按照#define的方式,有效使用。

关于变量,总结一下,希望大家理解,它就是存储空间。只不过有时,你不关注而已。

关于上述两个建议,谈不上有什么理论支撑,只能说是“血泪”的教训。

好的代码书写习惯,不仅方便阅读,更重要是匹配目标逻辑,防止出错,特别当工程大到一定程度时。
最后说一点,对于新手。
变量的命名也好,变量的定义也要,都不是非常随便的。我现在带一个小伙子,特地在培养他一个思维习惯,要明确告诉我,一个函数中,为什么一定要用这个变量,为什么不声明这个变量,逻辑就无法实现。逻辑冗余的变量,编译器会优化掉,但会让你理解代码,修正代码,带来麻烦。

 

 

 

 

 

加载中
0
中山野鬼
中山野鬼
这里,没有展开讨论“值”和“类型”会另外独立发贴。口水多。望大家见谅。
0
大东哥
大东哥

你这个是针对多“新”的新手啊?有其他编程语言经验,想转C的还差不多。

 

0
gomoh
gomoh

请问一下如何使用下面的结构,other_struct是另一种结构

typedef struct TE

{

int  i_te;

struct  other_struct oth_te[];

} _te;

0
中山野鬼
中山野鬼
楼上的,代码不全吧。很难回答。
中山野鬼
中山野鬼
@gomoh : 这种定义方法是在实际变量定义时在进行确认数组的大小。为什么我不建议用也是这个原因。会导致阅读代码的理解困难。
gomoh
gomoh
@lucky_star : 在你这里没有找到答案,不过还是谢谢你,我已经解决这个问题了,这种类型的结构是非常有用的,改为指针是完不成这种结构提供的功能的。
中山野鬼
中山野鬼
@gomoh : 而且我个人建议不要这样操作,由于C标准里面对这类写法有一定明确的建议或约束。此处最好还是用指针,不要用[]
中山野鬼
中山野鬼
@gomoh : 类似这个结构体的见过。不过完全这样的确实第一次见。
gomoh
gomoh
就是这样一个结构 other_struct 是自定义结构,随便定义 呵呵
下一页
0
中山野鬼
中山野鬼

引用来自“大东哥”的答案

你这个是针对多“新”的新手啊?有其他编程语言经验,想转C的还差不多。

 

基本上我带初学C的,都这么教。其他教材他们也可以看,不过如果学着学着,对我说”我想吐了“,确实遇到过,我也不带了。此人以后不是个C程序员的料,我不废口水。当然要有正规计算机系的专业背景。至少参与过《编译原理》《操作系统》《数据结构》《计算机组成原理》4门课的考试,分数不限,是否作弊不限,且不谈入门,至少知道门长啥样。现在很多”三本“的学校。计算机系我觉得就是一个职高的计算机应用基础培训班。无语的很。
0
拉菲一箱
拉菲一箱

确实 自己知道是一回事,能讲出来是一回事。

收藏下慢慢看 方便给别人讲 呵呵

0
周翼翼
周翼翼
//情况一
int main()
{
  int i;

  for (i = 0  ;i < 10 ;i++){

    sub_func(i);

  }
}
//情况二  int i;
int main()
{
  for (i = 0  ;i < 10 ;i++){

    sub_func(i);

  }
}

void sub_func(int input_data){

    printf("test to show input_data: %d\n",input_data);

}

我对这一段表示怀疑.基本上,sub_fun的"实参"i,可能来自两个地方:

1.情况一表示的是i是在某个函数内定义的,所以它位于栈内,gcc -S 你会看来类似 movl $0,28(%esp)这个的句子,这就是i=0;

2.情况二表示的是i是在全局定义的.gcc -S你会看到movl $0,_i这样的句子,这是i=0;

也就是说所谓实参的两种可能情况.

至于sub_fun里面的形参input_data,据我所知,都是在栈内定义的.调用者通过

movl 28(%esp),(%esp);实参在栈.若实参是全局,则movl _i,(%esp)
movl %eax,(%esp)

将参数放入栈.而在sub_fun的函数体内,它是通过

movl 8(%ebp),%eax

这类代码来取得调用者传进来的东西的.

简单的来说,函数的"形参"是在栈内的."实参"可能在栈内,也可能在全局数据段.不过,即使"实参"也在栈内,他们也不会相互影响.因为他们在不同的活动记录内.

另外:

registor int i;

理论上是会把i放在寄存器,但是事实未必如此.因为寄存器是很少的,编译器会"酌情"给你放到寄存器,换句话说,一般不让你在寄存器.

0
中山野鬼
中山野鬼

呵呵。楼上。这就是我建议用形参和实参来分析变量的原因之一。搞清楚每个变量的存储位置,可能更容易理解。

关于registor的事情。这个我赞同。但是对于寄存器比较多的CPU,还是可以用的。我们在 ARM上写的代码,就有4个变量全部使用全局寄存器。基本上整体速度提升一倍。因为这4个变量使用频率太高。不过悲剧的是,ARM的编译器我们使用不够精通,没有配置好参数,暂时没找到办法,有时会自动帮我们把这几个变量压栈,而这几个变量在这个函数里还需要使用,害的我们内核代码需要全部反汇编查一边。

返回顶部
顶部