网站怎么添加栏目,中国企业公司,提供广州网站建设,网站建设的实训报告怎么写Dennis Ritchie 过世了#xff0c;他发明了C语言#xff0c;一个影响深远并彻底改变世界的计算机语言。一门经历40多年的到今天还长盛不衰的语言#xff0c;今天很多语言都受到C的影响#xff0c;C#xff0c;Java#xff0c;C##xff0c;Perl#xff0c; PHP#xf…Dennis Ritchie 过世了他发明了C语言一个影响深远并彻底改变世界的计算机语言。一门经历40多年的到今天还长盛不衰的语言今天很多语言都受到C的影响CJavaC#Perl PHP Javascript 等等。但是你对C了解吗相信你看过本站的《C语言的谜题》还有《谁说C语言很简单》这里我再写一篇关于深入理解C语言的文章一方面是缅怀Dennis另一方面是告诉大家应该如何学好一门语言。顺便注明一下下面的一些例子来源于这个slides 我自己的C语言交流裙815393895
首先我们先来看下面这个经典的代码
int main()
{
int a 42;
printf (“%d\n”, a);
}
从这段代码里你看到了什么问题我们都知道这段程序里少了一个#include stdio.h 还少了一个return 0;的返回语句。 不过让我们来深入的学习一下
这段代码在C下无法编译因为C需要明确声明函数这段代码在C的编译器下会编译通过因为在编译期编译器会生成一个printf的函数定义并生成.o文件链接时会找到标准的链接库所以能编译通过。但是你知道这段程序的退出码吗在ANSI-C下退出码是一些未定义的垃圾数。但在C89下退出码是3因为其取了printf的返回值。为什么printf函数返回3呢因为其输出了’4′, ’2′,’\n’ 三个字符。而在C99下其会返回0也就是成功地运行了这段程序。你可以使用gcc的 -stdc89或是-stdc99来编译上面的程序看结果。另外我们还要注意main()在C标准下如果一个函数不要参数应该声明成main(void)而main()其实相当于main(…)也就是说其可以有任意多的参数。
我们再来看一段代码
#include stdio.h
void f( void )
{
static int a 3;
static int b;
int c;
a; b; c;
printf ( a%d\n , a);
printf ( b%d\n , b);
printf ( c%d\n , c);
}
int main( void )
{
f();
f();
f();
} 这个程序会输出什么
我相信你对a的输出相当有把握就分别是456因为那个静态变量。对于c呢你应该也比较肯定那是一堆乱数。但是你可能不知道b的输出会是什么答案是123。为什么和c不一样呢因为如果要初始化每次调用函数里编译器都要初始化函数栈空间这太费性能了。但是c的编译器会初始化静态变量为0因为这只是在启动程序时的动作。全局变量同样会被初始化。
说到全局变量你知道 静态全局变量和一般全局变量的差别吗是的对于static 的全局变量其对链接器不可以见也就是说这个变量只能在当前文件中使用。
我们再来看一个例子
#include stdio.h
void foo( void )
{
int a;
printf ( %d\n , a);
}
void bar( void )
{
int a 42;
}
int main( void )
{
bar();
foo();
}
你知道这段代码会输出什么吗A) 一个随机值B) 42。A 和 B都对在“在函数外存取局部变量的一个比喻”文中的最后给过这个例子不过你知道为什么吗
如果你使用一般的编译会输出42因为我们的编译器优化了函数的调用栈重用了之前的栈为的是更快这没有什么副作用。反正你不初始化他就是随机值既然是随机值什么都无所谓。但是如果你的编译打开了代码优化的开关-O这意味着foo()函数的代码会被优化成main()里的一个inline函数也就是说没有函数调用就像宏定义一样。于是你会看到一个随机的垃圾数。
下面我们再来看一个示例
#include stdio.h
int b( void ) { printf (“3”); return 3; }
int c( void ) { printf (“4”); return 4; }
int main( void )
{
int a b() c();
printf (“%d\n”, a);
}
这段程序会输出什么你会说是347。但是我想告诉你这也有可能输出437。为什么呢 这是因为在C/C中表达的评估次序是没有标准定义的。编译器可以正着来也可以反着来所以不同的编译器会有不同的输出。你知道这个特性以后你就知道这样的程序是没有可移植性的。
我们再来看看下面的这堆代码他们分别输出什么呢
示例一1
int a41; a; printf ( %d\n , a);
示例二1
int a41; a printf ( %d\n , a);
示例三1
int a41; a printf ( %d\n , a);
示例四1
int a41; if (a 42) printf ( %d\n , a);
示例五1
int a41; a a; printf ( %d\n , a);
只有示例一示例三示例四输出42而示例二和五的行为则是未定义的。关于这种未定义的东西是因为Sequence Points的影响Sequence Points是一种规则也就是程序执行的序列点在两点之间的表达式只能对变量有一次修改因为这会让编译器不知道在一个表达式顺列上如何存取变量的值。比如a aa a不过在C中这样的情况很少。
下面再看一段代码假设int为4字节char为1字节
struct X { int a; char b; int c; };
printf ( %d, , sizeof ( struct X));
struct Y { int a; char b; int c; char d};
printf ( %d\n , sizeof ( struct Y));
这个代码会输出什么?
a) 910
b)12, 12
c)12, 16
答案是C我想你一定知道字节对齐是向4的倍数对齐。
但是你知道为什么要字节对齐吗还是因为性能。因为这些东西都在内存里如果不对齐的话我们的编译器就要向内存一个字节一个字节的取这样一来struct X就需要取9次太浪费性能了而如果我一次取4个字节那么我三次就搞定了。所以这是为了性能的原因。但是为什么struct Y不向12 对齐却要向16对齐因为char d; 被加在了最后当编译器计算一个结构体的尺寸时是边计算边对齐的。也就是说编译器先看到了int很好4字节然后是 char一个字节而后面的int又不能填上还剩的3个字节不爽把char b对齐成4于是计算到d时就是13 个字节于是就是16啦。但是如果换一下d和c的声明位置就是12了。
另外再提一下上述程序的printf中的%d并不好因为在64位下sizeof的size_t是unsigned long而32位下是 unsigned int所以C99引入了一个专门给size_t用的%zu。这点需要注意。在64位平台下C/C 的编译需要注意很多事。你可以参看《64位平台C/C开发注意事项》。
下面我们再说说编译器的Warning请看代码
#include stdio.h
int main( void )
{
int a;
printf ( %d\n , a);
}
考虑下面两种编译代码的方式
cc -Wall a.ccc -Wall -O a.c
前一种是不会编译出a未初化的警告信息的而只有在-O的情况下才会有未初始化的警告信息。这点就是为什么我们在makefile里的CFLAGS上总是需要-Wall和 -O。
最后我们再来看一个指针问题你看下面的代码
#include stdio.h
int main( void )
{
int a[5];
printf ( %x\n , a);
printf ( %x\n , a1);
printf ( %x\n , a);
printf ( %x\n , a1);
}
假如我们的a的地址是0Xbfe2e100, 而且是32位机那么这个程序会输出什么
第一条printf语句应该没有问题就是 bfe2e100第二条printf语句你可能会以为是bfe2e101。那就错了a1编译器会编译成 a 1*sizeof(int)int在32位下是4字节所以是加4也就是bfe2e104第三条printf语句可能是你最头疼的我们怎么知道a的地址我不知道吗可不就是bfe2e100。那岂不成了aa啦这怎么可能自己存自己的也许很多人会觉得指针和数组是一回事那么你就错了。如果是 int *a那么没有问题因为a是指针所以 a 是指针的地址a 和 a不一样。但是这是数组啊a[]所以a其实是被编译成了 a[0]。第四条printf语句就很自然了就是bfe2e104。还是不对因为是a是数组被看成int(*)[5]所以sizeof(a)是5也就是5*sizeof(int)也就是bfe2e114。
看过这么多你可能会觉得C语言设计得真扯淡啊。不过我要告诉下面几点Dennis当初设计C语言的初衷
1相信程序员不阻止程序员做他们想做的事。
2保持语言的简洁以及概念上的简单。
3保证性能就算牺牲移植性。
今天很多语言进化得很高级了语法也越来越复杂和强大但是C语言依然光芒四射Dennis离世了但是C语言的这些设计思路将永远不朽。