现在的位置: 首页 > 综合 > 正文

《Linux C 编程一站式学习》C基础部分摘录

2013年09月16日 ⁄ 综合 ⁄ 共 9298字 ⁄ 字号 评论关闭
1. 表 2.1. C标准规定的转义字符
\' 单引号'(Single Quote或Apostrophe)
\" 双引号"
\? 问号?(Question Mark)
\\ 反斜线\(Backslash)
\a 响铃(Alert或Bell)
\b 退格(Backspace)
\f 分页符(Form Feed)
\n 换行(Line Feed)
\r 回车(Carriage Return)
\t 水平制表符(Horizontal Tab)
\v 垂直制表符(Vertical Tab)

2.C语言中的声明(Declaration)有变量声明、函数声明和类型声明三种。如果一个变量或函数的声明要求编译器为它分配存储空间,那么也可以称为定义(Definition),因此定义是声明的一种。对变量来说,在本文件内的定义或者初始化都是要分配存储空间的,而形如extern int top;就只是声明top变量是另外个文件中定义的,在这里声明不分配存储空间。
声明一个类型是不分配存储空间的,这点一定要注意,类型定义也是一种声明,声明都要以;号结尾,结构体类型定义的}后面少;号是初学者常犯的错误。

声明和语句类似,也是以;号结尾的,但是在语法上声明和语句是有区别的,语句只能出现在{}括号中,而声明既可以出现
在{}中也可以出现在所有{}之外。



3.字符也可以用ASCII码转义序列表示,这种转义序列由\加上1~3个八进制数字组成,或者由\x或大写\X加上1~2个十六进制数字组成,可以用在字符常量或字符串字面值中。例如'\0' 表示NULL 字符(Null Character),'\11' 或'\x9' 表示Tab字符,"\11" 或"\x9" 表示由Tab字符组成的字符串。注意'0' 的ASCII码是48,而'\0' 的ASCII码是0,两者是不同的。

4.printf,我们并不关心它的返回值(事实上它也有返回值,表示实际打印的字符数),我们调用printf不是为了得到它的返回值,而是为了利用它所产生的副作用(Side Effect)--打印。C语言的函数可以有Side Effect,这一点是它和数学函数在概念上的根本区别。

5.main 函数的返回值是int 型的,return 0;这个语句表示返回值是0,main 函数的返回值是返回给操作系统看的,因为main 函数是被操作系统调用的,通常程序执行成功就返回0,在执行过程中出错就返回一个非零值。比如我们将main 函数中的return语句改为return 4;再执行它,执行结束后可以在Shell中看到它的退出状态(Exit Status):
$ ./a.out
11 and 0 hours
$ echo $?
4
$?是Shell中的一个特殊变量,表示上一条命令的退出状态。


6.其实操作系统在调用main 函数时是传参数的,main 函数最标准的形式应该是int main(int argc, char *argv[]) ,标准也允许int main(void) 这种写法,如果不使用系统传进来的两个参数也可以写成这种形式。但除了这两种形式之外,定义main 函数的其它写法都是错误的或不可移植的。

7.现在澄清一下函数声明、函数定义、函数原型(Prototype)这几个概念。比如void threeline(void) 这一行,声明了一个函数的名字、参数类型和个数、返回值类型,这称为函数原型。在代码中可以单独写一个函数原型,后面加;号结束,而不写函数体,例如: void threeline(void);
这种写法只能叫函数声明而不能叫函数定义,只有带函数体的声明才叫定义。上面讲过,只有分配存储空间的变量声明才叫变量定义,其实函数也是一样,编译器只有见到函数定义才会生成指令,而指令在程序运行时当然也要占存储空间。那么没有函数体的函数声明有什么用呢?它为编译器提供了有用的信息,编译器在翻译代码的过程中,只有见到函数原型(不管带不带函数体)之后才知道这个函数的名字、参数类型和返回值,这样碰到函数调用时才知道怎么生成相应的指令,所
以函数原型必须出现在函数调用之前,这也是遵循“先声明后使用”的原则。
由于有Old Style C语法的存在,并非所有函数声明都包含完整的函数原型,例如void threeline(); 这个声明并没有明确指出参数类型和个数,所以不算函数原型,这个声明提供给编译器的信息只有函数名和返回值类型。如果在这样的声明之后调用函数,编译器不知道参数的类型和个数,就不会做语法检查,所以很容易引入Bug。大家需要了解这个知识点以便维护别人用Old Style C风格写的代码,但绝不应该按这种风格写新的代码。

8.如果在main 函数中调用threeline 时并没有声明它,编译器认为此处隐式声明了int threeline(void);,隐式声明的函数返回值类型都是int ,由于我们调用这个函数时没有传任何参数,所以编译器认为这个隐式声明的参数类型是void ,这样函数的参数和返回值类型都确定下来了,编译器根据这些信息为函数调用生成相应的指令。然后编译器接着往下看,看到threeline 函数的原型是void
threeline(void) ,和先前的隐式声明的返回值类型不符,所以报警告。如果恰好此时我们
也没用到这个函数的返回值,所以执行结果仍然会是正确的。

9.记住这条基本原理:形参相当于函数中定义的变量,调用函数传递参数的过程相当于定义形参变量并且用实参的值来初始化。


10.我们把函数中定义的变量称为局部变量(Local Variable),由于形参相当于函数中定义的变量,所以形参也是一种局部变量。在这里“局部”有两层含义:
1、一个函数中定义的变量不能被另一个函数使用。
2、每次调用函数时局部变量都表示不同的存储空间。局部变量在每次函数调用时分配存储空间,。在每次函数返回时释放存储空间。
与局部变量的概念相对的是全局变量(Global Variable),全局变量定义在所有的函数体之外,它们在程序开始运行时分配存储空间,在程序结束时释放存储空间,在任何函数中都可以访问全局变量,正因为全局变量在任何函数中都可以访问,所以在程序运行过程中全局变量被读写的顺序从源代码中是看不出来的,源代码的书写顺序并不能反映函数的调用顺序。程序出现了Bug往往就是因为在某个不起眼的地方对全局变量的读写顺序不正确,如果代码规模很大,这种错误是很难找到的。而对局部变量的访问不仅局限在一个函数内部,而且局限在一次函数调用之中,从函数的源代码很容易看出访问的先后顺序是怎样的,所以比较容易找到Bug。因此,虽然全局变量用起来很方便,但一定要慎用,能用函数传参代替的就不要用全局变量。

在C语言中每个标识符都有特定的作用域,全局变量是定义在所有函数体之外的标识符,它的作用域从定义的位置开始直到源文件结束,而main 函数局部变量的作用域仅限于main 函数之中。如上图所示,设想整个源文件是一张大纸,也就是全局变量的作用域,而main 函数是盖在这张大纸上的一张小纸,也就是main 函数局部变量的作用域。

11.我们在初始化一个变量时都是用常量做Initializer,其实也可以用表达式做Initializer,但要注意一点:局部变量可以用类型相符的任意表达式来初始化,而全局变量只能用常量表达式(Constant Expression)初始化。例如,全局变量pi这样初始化是合法的:double pi = 3.14 + 0.0016;
但这样初始化是不合法的:double pi = acos(-1.0);
然而局部变量这样初始化却是可以的。程序开始运行时要用适当的值来初始化全局变量,所以初始值必须保存在编译生成的可执行文件中,因此初始值在编译时就要计算出来,然而上面第二种Initializer的值必须在程序运行时调用acos 函数才能得到,所以不能用来初始化全局变量。请注意区分编译时和运行时这两个概念。为了简化编译器的实现,C语言从语法上规定全局变量只能用常量表达式来初始化,因此下面这种全局变量初始化是不合法的:
int minute = 360;
int hour = minute / 60;
虽然在编译时计算出hour 的初始值是可能的,但是minute / 60不是常量表达式,不符合语法规定,所以编译器不必想办法去算这个初始值。
如果全局变量在定义时不初始化则初始值是0,如果局部变量在定义时不初始化则初始值是不确定的。所以,局部变量在使用之前一定要先赋值,如果基于一个不确定的值做后续计算肯定会引入Bug。

12.非定义的函数声明也可以写在局部作用域中,例
如:
int main(void)
{
void print_time(int, int);
print_time(23, 59);
66
return 0;
}
这样声明的标识符print_time 具有局部作域,只在main 函数中是有效的函数名,出了main 函数就不存在print_time 这个标识符了。
语句块中也可以定义局部变量,例如:
void foo(void)
{
int i = 0;
{
int i = 1;
int j = 2;
printf("i=%d, j=%d\n", i, j);
}
printf("i=%d\n", i);
}
和函数的局部变量同样道理,每次进入语句块时为变量j分配存储空间,每次退出语句块时释放变量j的存储空间。语句块也构成一个作用域,和上面的分析类似,如果整个源文件是一张大纸,foo 函数是盖在上面的一张小纸,则函数中的语句块是盖在小纸上面的一张更小的纸。语句块中的变量i和函数的局部变量i是两个不同的变量,因此两次打印的i值是不同的;语句块中的变量j在退出语句块之后就没有了,因此最后一行的printf不能打印变量j,否则编译器会报错。语句块可以用在任何允许出现语句的地方,不一定非得用在if语句中,单独使用语句块通常是为了定义一些比函数的局部变量更“局部”的变量。

13.int is_even(int x)
{
return !(x % 2);
}
函数的返回值应该这样理解:函数返回一个值相当于定义一个和返回值类型相同的临时变量并用return后面的表达式来初始化。例如上面的函数调用相当于这样的过程:
int 临时变量 = !(x % 2);
函数退出,局部变量x的存储空间释放;
if (临时变量) {
} else {
}
当if语句对函数的返回值做判断时,函数已经退出,局部变量x已经释放,所以不可能在这时候才计算表达式!(x % 2)的值,表达式的值必然是事先计算好了存在一个临时变量里的,然后函数退出,局部变量释放,if语句对这个临时变量的值做判断。注意,虽然函数的返回值可以看作是一个临时变量,但我们只是读一下它的值,读完值就释放它,而不能往它里面存新的值

14.在有多层循环或switch嵌套的情况下,break 只能跳出最内层的循环或switch,continue 也只能终止最内层循环并回到该循环的开头。

15.通常goto 语句只用于这种场合,一个函数中任何地方出现了错误条件都可以立即跳转到函数末尾做出错处理(例如释放先前分配的资源、恢复先前改动过的全局变量等),处理完之后函数返回。唯一的限制是goto 只能跳转到同一个函数中的某个标号处,而不能跳到别的函数中

16.如果用户输入合法(输入的确实是数字而不是别的字符),则scanf 函数返回1,表示成功读入一个数据。

17.一般来说,如果用char 型存ASCII码字符,就不必明确写是signed还是unsigned ,如果用char 型表示8位的整数,为了可移植性就必须写明是signed还是unsigned 。还有一点要注意,除了char 型以外的其他类型如果不明确写signed或unsigned 关键字都表示signed,这一点是C标准明确规定的,不是Implementation Defined。
除了char 型在C标准中明确规定占一个字节之外,其它整型占几个字节都是Implementation Defined。通常的编译器实现遵守ILP32或LP64规范,ILP32这个缩写的意思是int (I)、long (L)和指针(P)类型都占32位,通常32位计算机的C编译器采用这种规范,x86平台的gcc 也是如此。LP64是指long (L)和指针占64位,通常64位计算机的C编译器采用这种规范。指针类型的长度总是和计算机的位数一致

18.在C标准中没有做明确规定的地方会用Implementation-defined、Unspecified或Undefined来表述,在本书中有时把这三种情况统称为“未明确定义”的。这三种情况到底有什么不同呢?我们刚才看到一种Implementation-defined的情况,C标准没有明确规定char 是有符号的还是无符号的,但是要求编译器必须对此做出明确规定,并写在编译器的文档中。而对于Unspecified的情况,往往有几种可选的处理方式,C标准没有明确规定按哪种方式处理,编译器可以自己决定,并且也不必写在编译器的文档中,这样即便用同一个编译器的不同版本来编译也可能得到不同的结果,因为编译器没有在文档中明确写它会怎么处理,那不同版本的编译器就可以选择不同的处理方式,比如下一章我们会讲到一个函数调用的各个实参表达式按什么顺序求值是Unspecified的。Undefined的情况则是完全不确定的,C标准没规定怎么处理,编译器
很可能也没规定,甚至也没做出错处理,有很多Undefined的情况编译器是检查不出来的,最终会导致运行时错误,比如数组访问越界就是Undefined的。

19.在函数调用和返回过程中发生的类型转换往往容易被忽视,因为函数原型和函数调用并没有写在一起。例如char c = getchar(); ,看到这一句往往会想当然地认为getchar的返回值是char 型,而事实上getchar的返回值是int 型,这样赋值会引起类型转换,可能产生Bug。

20.由于类型转换和移位等问题,用有符号数做位运算是很不方便的,所以,建议只对无符号数做位运算,以减少出错的可能。

21.对于用角括号包含的头文件,gcc 首先查找-I选项指定的目录,然后查找系统的头文件目录(通常是/usr/include,在
我的系统上还包括/usr/lib/gcc/i486-linux-gnu/4.3.2/include);而对于用引号包含的头文件,gcc 首先查找包含头文件的.c文件所在的目录,然后查找-I选项指定的目录,然后查找系统的头文件目录。

假如三个代码文件都放在当前目录下:
$ tree
.
|-- main.c
|-- stack.c
`-- stack.h
0 directories, 3 files
则可以用gcc -c main.c编译,gcc 会自动在main.c所在的目录中找到stack.h。假如把stack.h移到
一个子目录下:
$ tree
.
|-- main.c
`-- stack
|-- stack.c
`-- stack.h
1 directory, 3 files
则需要用gcc -c main.c -Istack编译。用-I选项告诉gcc 头文件要到子目录stack 里找。
在#include 预处理指示中可以使用相对路径,例如把上面的代码改成#include "stack/stack.h" ,
那么编译时就不需要加-Istack选项了,因为gcc 会自动在main.c所在的目录中查找,而头文件相对
于main.c所在目录的相对路径正是stack/stack.h 。


22.重复包含头文件有以下问题:
1. 一是使预处理的速度变慢了,要处理很多本来不需要处理的头文件。
2. 二是如果有foo.h 包含bar.h ,bar.h 又包含foo.h 的情况,预处理器就陷入死循环了(其实编译器都会规定一个包含层数的上限)。
3. 三是头文件里有些代码不允许重复出现,虽然变量和函数允许多次声明(只要不是多次定义就行),但头文件里有些代码是不允许多次出现的,比如typedef类型定义和结构体Tag定义等,在一个程序文件中只允许出现一次。

23.我们把stack.c、push.c、pop.c 、is_empty.c 编译成目标文件:
$ gcc -c stack/stack.c stack/push.c stack/pop.c stack/is_empty.c
然后打包成一个静态库libstack.a :
$ ar rs libstack.a stack.o push.o pop.o is_empty.o
ar: creating libstack.a
库文件名都是以lib 开头的,静态库以.a作为后缀,表示Archive。ar命令类似于tar 命令,起一个打包的作用,但是把目标文件打包成静态库只能用ar命令而不能用tar 命令。选项r表示将后面的文件列表添加到文件包,如果文件包不存在就创建它,如果文件包中已有同名文件就替换成新的。s是专用于生成静态库的,表示为静态库创建索引,这个索引被链接器使用。ranlib命令也可以为静态库创建索引,以上命令等价于:
$ ar r libstack.a stack.o push.o pop.o is_empty.o
$ ranlib libstack.a

然后我们把libstack.a 和main.c编译链接在一起:
$ gcc main.c -L. -lstack -Istack -o main
-L选项告诉编译器去哪里找需要的库文件,-L. 表示在当前目录找。-lstack告诉编译器要链接libstack 库,-I选项告诉编译器去哪里找头文件。注意,即使库文件就在当前目录,编译器默认也不会去找的,所以-L.选项不能少。编译器默认会找的目录可以用-print-search-dirs 选项查看:
其中的libraries 就是库文件的搜索路径列表,各路径之间用:号隔开。编译器会在这些搜索路径以及-L选项指定的路径中查找用-l选项指定的库,比如-lstack,编译器会首先找有没有共享库libstack.so,如果有就链接它,如果没有就找有没有静态库libstack.a ,如果有就链接它。所以编译器是优先考虑共享库的,如果希望编译器只链接静态库,可以指定-static选项。
那么链接共享库和链接静态库有什么区别呢?在链接libc 共享库时只是指定了动态链接器和该程序所需要的库文件,并没有真的做链接,可执行文件main 中调用的libc 库函数仍然是未定义符号,要在运行时做动态链接。

有意思的是,main.c只调用了push 这一个函数,所以链接生成的可执行文件中也只有push 而没有pop 和is_empty 。这是使用静态库的一个好处,链接器可以从静态库中只取出需要的部分来做链接。如果是直接把那些目标文件和main.c编译链接在一起:
$ gcc main.c stack.o push.o pop.o is_empty.o -Istack -o main
则没有用到的函数也会链接进来。当然另一个好处就是使用静态库只需写一个库文件名,而不需要写一长串目标文件名。

24.在C语言中后缀运算符如 结构体取成员.、指针访问->、数组取下标[]、函数调用() 的优先级最高,单目运算符的优先级仅次于后缀运算符,比其它运算符的优先级都高. 大多数运算符是左结合性的,赋值运算符、条件判断运算符以及单目运算符是右结合性的。

25. 数组类型做右值使用时,自动转换成指向数组首元素的指针, 这也解释了为什么数组类型不能相互赋值或初始化, 编译器报的错是error: incompatible types in assignment 
但做左值仍然表示整个数组的存储空间,而不是首元素的存储空间,数组名做左值还有一点特殊之处,不支持++、赋值这些运算符,但支持取地址运算符&,所以&a是合法的.

26. 数组元素可以通过数组名加下标的方式访问,而字符串字面值也可以像数组名一样使用,可以加下标访问其中的字符: char c = "Hello, world.\n"[0];  但是通过下标修改其中的字符却是不允许的:  "Hello, world.\n"[0] = 'A';
这行代码会产生编译错误,说字符串字面值是只读的,不允许修改。字符串字面值还有一点和数组名类似,做右值使用时自动转换成指向首元素的指针,这种指针应该是const char *型。我们知道printf函数原型的第一个参数是const char *型,可以把char *或const char *指针传给它.

如果要定义一个指针指向字符串字面值,这个指针应该是const char *型,如果 
写成char *p = "abcd";就不好了,有隐患,例如: 

       int main(void) 
       { 
               char *p = "abcd"; 
       ... 
               *p = 'A'; 
       ... 
       } 

p指向.rodata段,不允许改写,但编译器不会报错,在运行时会出现段错误。

27.这可以算是一条规律,如果某个函数的局部变量发生访问越界,有可能并不立即产生段错误,而是在函数返回时产生段错误。

30.我们知道main函数的标准原型应该是int main(int argc, char *argv[]); 。argc 是命令行参数的个数。而argv 是一个指向指针的指针,为什么不是指针数组呢?因为前面讲过,函数原型中的[]表示指针而不表示数组,等价于char **argv。那为什么要写成char *argv[]而不写成char **argv呢?这样写给读代码的人提供了有用信息,argv 不是指向单个指针,而是指向一个指针数组的首元素。数组中每个元素都是char
*指针,指向一个命令行参数字符串。

31. #include <stdio.h>
void say_hello(const char *str)
{
printf("Hello %s\n", str);
}
int main(void)
{
void (*f)(const char *) = say_hello;
f("Guys");
return 0;
}
分析一下变量f的类型声明void (*f)(const char *),f首先跟*号结合在一起,因此是一个指针。(*f) 外面是一个函数原型的格式,参数是const char *,返回值是void ,所以f是指向这种函数的指针。而say_hello 的参数是const char *,返回值是void ,正好是这种函数,因此f可以指向say_hello 。注意,say_hello 是一种函数类型,而函数类型和数组类型类似,做右值使用时自动转换成函数指针类型,所以可以直接赋给f,当然也可以写成
void (*f)(const char *) =&say_hello;,把函数say_hello 先取地址再赋给f,就不需要自动类型转换了。可以直接通过函数指针调用函数,如上面的f("Guys") ,也可以先用*f取出它所指的函数类型,再调用函数,即(*f)("Guys")。可以这么理解:函数调用运算符()要求操作数是函数指针,所以f("Guys")
是最直接的写法,
而say_hello("Guys")或(*f)("Guys")则是把函数类型自动转换成函数指针然后做函数调用。


抱歉!评论已关闭.