C语言的理解

一直想整理一套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的方式,有效使用。

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

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

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

转载于:https://my.oschina.net/luckystar/blog/56281

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值