C语言是所有语言的基础。我就从这里开始吧。
Chapter 1
最简单的C程序就是打印"Hello World!".文件头引用程序中要使用的printf函数所在的库文件stdio.h: #include <stdio.h>.每个C程序都要有个主干,用main开头,后面的()大概是用于括返回值,{}中间写程序语句。printf输出字符串要用""括起来。C源码写完存好,在字符界面cc之,如果程序没错,gcc会生成一个a.out,执行a.out,就可以看到结果。逗号在printf输出的字符串里可以直接写,但是换行符需用\n表示,类似的\t表示制表符,\b表示backspace,\"表示",\\表示\。
C的数据类型包括int short long,分别表示整型,短整型,长整型。变量声明的格式是int counter; 声明用分号;结尾,事实上所有语句都用分号结尾。
while的格式:while后跟一对括号,括号里包含一个逻辑表达式,逻辑表达式的值为真则执行while所管的语句,执行一遍后再来检查此表达式,若为真则再执行,否则结束while语句,执行下一条。另一种结束while语句的方法是写break; while的特点是至少执行一次,另一个循环语句for可能一次都不执行。
char float也是数据类型。char用于单个字符,float用于浮点数,即非整数。
加减乘除分别是+-*/,需要注意的是/。若a,b都是整数类型,则a/b的结果是整数除法的结果,砍掉了余数,所以如要获得浮点数结果,则须强制类型转换。
继续说printf:有时需要输出的不只是一个字符串,还需要把变量的值打印出来,这时就需要通配符。类似于printf("%d",counter);就会打印出counter这个整型变量的值,若有多个通配符,则后面以逗号分隔开的变量挨个对号入座。还可以规定输出变量值所占的位置,比如%6d就是占6个字符位置,%f用于float变量,%6.2f指用6格打印此变量,2格的小数位。%s是字符串。
for的用法:for(expr1;expr2;expr3) expr1通常用于给控制循环的变量赋初值,expr2通常是个逻辑表达式,用于设定循环结束的边界条件,expr3通常用于改变控制变量的值。这三个expr都可以为空,只是要保证循环有限步后会结束。
逻辑表达式的值为布尔型,只有0,1两个值。0代表假,1代表真。所有非零的数作为逻辑表达式值都是1.因此逻辑表达式可以是一个数值表达式,不一定是个逻辑判断。这提供了相当的灵活性。
注意赋值和逻辑判断的区别:=用于赋值,==用于逻辑判断。后面将学到的字符的位操作也有类似的混淆可能,&是位的与操作,&&是两个逻辑表达式的并运算。
为了调试和扩展的需要,也为了程序的易读性,通常不在程序里直接使用常数值,而是在程序头做常量声明,格式是:#define SUP 255 注意没有分号结尾。
注释的用法:C语言中用/*和*/把注释括起来,也是为了易读性(readability)。Bear in mind,程序是拿来给人读的。
大小关系判断符号:大于>,小于<,不大于<=,不小于>=,等于==,不等于!=。
逻辑关系判断符号:并且&&,或者||,计算时从左至右,且得值即止。
getchar()和putchar(),是读入和输出函数。假设c是一个char变量。c=getchar();把读入的字符赋给c,putchar(c);把c打印出来。EOF是一个特殊的char值,为-1,用于标记文件尾。char的取值范围是0-255,一个byte的范围。
运算的优先级:赋值运算的优先级高于比较运算,因此c=getchar()!=EOF的意思是读入一个字符赋给c然后c跟EOF比较。
++,--,+=,-=,*=,/=这些运算符。a++是先取a的值再加1,++a是先给a加1再取a的值。a+=1等价于a=a+1,这种运算比较紧凑,且易读性强。
条件语句:C语言没有if then else,直接是
if expression
statement1;
else
statement2;。
expression的值为真则执行statement1,否则执行statement2. eles语句可无。
if else可以嵌套,配对原则是else跟最近的if配,为免混淆和易读性,做好缩进和{}。
字符用''括起来,不要跟字符串搞混了。'a'和"a"是不同的。
函数:构成函数的要件是输入和输出,输入和输出都要声明数据类型,用return语句在函数体内给出输出。函数头的例子 int power(int base, int n)。其中的base和int不是必须的,只是为了易读性。完全可以写成int power(int,int)。不需要返回值的函数声明为void,例如 void copy(char to[],char from[])。int是default return type,可以省略。
In C, we call by value. 意思是我们对call过来的是个local copy,不改变原值。这样的设置减少了很多出错的可能性,也可使程序更简洁。这是对变量,对数组就不是这样了。
字符串的结尾隐藏着一个\0,作为“字符串在这里结束”的信号。
缺省地,每个变量都local于声明它的函数,它的值不会保留,函数被call,then exit后它就被释放了,所以每次用它必须先初始化,否则它的值就是乱的。想使用一直存在的变量可以采用外部变量(external variable),外部变量必须在所有函数(包括main)的外面声明一次,若要在函数内部使用此外部变量,则必须在函数里前面加个extern声明之。除非绝对必要,不要使用外部变量,因为我不知道什么时候它就变化了,且使用外部变量不利于程序的万用性。
Chapter 2
变量的命名规则:变量名的首位可以用字母或下划线,但不推荐用下划线,因为库函数的名字常以下划线开头。变量名内部可以用字母,下划线和数字。变量名是大小写敏感的。有些词C语言已经占用了,不可以用于做变量名,例如if,else,int,float。应尽可能给变量起与它的作用相关的名,局部变量起短名,外部变量起长名。
unsigned和signed,若char为8 bits,则unsigned char的取值范围是0到255,signed char的取值范围是-128到127。
double是float的扩展数据类型,比float精度更高。float,double,long double三者的关系类似于short int,int,long int的关系。
<limit.h>和<float.h>包含着数据类型的界数据。如何得知这些数据类型的范围?先定义一个unsigned int整数,赋值为0,然后减去1,由所得可知其范围,也可知其范围大小,由此可算出signed int的范围,其它数据类型的值域可用同样的方法求得。
常数:以l或L结尾的整数为长整型,无符号数以u或U结尾,无符号长整型以ul或UL结尾,浮点数以f或F结尾,双精度浮点数以l或L结尾。八进制整数以0开头,16进制整数以0x或0X开头,非十进制整数也可加后缀L或U。
字母字符和有些非字母字符可以直接括在''里表示,有些叫escape sequence的则不能。他们包括:\a alert, \\ 右斜杠, \b 退格, \? 问号,\f formfeed, \' 单引号, \n 换行, \" 双引号,\r carriage return, \ooo 八进制数, \xhh 十六进制数, \v 垂直tab, \t 水平tab. '\0'代表以0为值的字符。注意,一般地,'\x'代表以x为值的字符。
任意字符串必须以不可见的'\0'结尾。标准库函数strlen(s)返回字符串s的长度(不包括尾部那个'\0')。strlen和其它字符串函数在<string.h>中。
另一种表示常量的方式是枚举,例子:enum family {father=1,mother,son,daughter}; 其中mother会被自动赋值为2,son被赋值为3,注意枚举语句不像#define,它需要以分号结尾。枚举列表中的名字必须两两不同,但值不必。
所有的变量必须在使用前声明。对于不想被改变的变量可在声明时前面加const后面赋值。例如: const double e=2.7182845905;
&&和||第一章提过了,这里再强调一下它们的得值即止功能。他们是从左往右计算的,一旦算到足够决定他们值的地方,就不接着往右算了,这可以用于更灵活地写代码。
不同数据类型的变量若在一个表达式里,会自动将数据范围小者转换为数据范围大者再进行运算。例如:int转为long, float转为double。C语言还提供了强制转换,只要在需转换的变量前写(类型名)即可。若将数值范围大的量赋给数值范围小的类型量,则会丢失数据。
自加和自减运算符:只能用于变量,不可用于表达式,即a++可以,(a+b)++不可以。用此运算符常可简化程序。
位运算:& 按位与, | 按位或,^按位异或,<<左移,>>右移,~按位补。&可以用mask数取截出一段。左右移空出来的位用0补(对于unsinged)。注意位运算与逻辑运算的区别。
赋值运算符:expr1 op=expr2等价于expr1=expr1 op expr2。
<ctype.h>提供了一族无关于字符集的测试和转化函数。例如isdigit()。<math.h>提供了一些常见数学函数。例如sqrt()。
条件表达式:expr1 ? expr2 : expr3.其意义是若expr1为真,则执行expr2,否则执行expr3。用它可使程序更紧凑。
运算优先度列表,用tab分组:
() [] ->
! ~ ++ -- + - *(type) sizeof
* / %
+ -
<< >>
< <= > >=
== !=
&
^
|
&&
||
?:
= += -= *= /= &= ^= |= <<= >>=
,
一元运算符& + - * 比同个模样的二元运算符优先级高。
Chapter 3
本章的名字叫control flow,应是详解各语句的用法。
一般的语句以分号结尾。{}可以当作一个语句看,不需要再加分号做结尾。
switch (expr) {
case const-expr: statements
case const-expr: statements
default: statements
} 这是switch语句的格式。特别注意不同case可以共用statements,只要冒号后面继续跟case就可以。
运算符逗号,常见于for的()里,被逗号分隔开的两个表达式从左往右计算,计算结果的类型和数值是最右边的那个表达式的类型和数值。逗号运算符不要多用,若非逻辑上紧密相连的表达式,不要往一起连。
do-while:
do
statement
while(expression);
即statement至少执行一次。
想速度从循环中跳出时用break,注意一个break能且只能跳出一层循环。这与return不同,return直接终止函数体的运行。
continue:只用于for,while中,效果是这次循环到此结束,直接执行下一次循环。
goto的用法:
go label;
......
label:
除非绝对必要,别用goto。
Chapter 4
本章的标题是:函数和程序结构。我们大概都听说过。好的程序结构是自顶向下的。一个由多个独立的小结构组合成的大结构更稳定,更容易维护,其中的小结构也容易用在别处。
在函数声明和定义处言明所用参数的类型是个好习惯,call此函数时可自动做强制类型转换以避免出错。
若函数预料之外地无返回值,很可能什么地方搞砸了,去看看。
函数声明时的返回值和call它时期待的返回值必须统一,否则会出错。
C语言中不允许在函数内定义函数。
cc命令可以同时编译数个C语言源文件(*.c),若某个源文件编译出错,修改后可再将此源文件(*.c)与其它已编译通过的对象文件(*.o)一起编译。
所有的函数和函数参数若在遇到定义前就遇到引用,默认其返回值和参数均为int类型。如果函数有参数,声明它们的类型,如果没有,用void。
automatic variables是生活在函数内部的,call结束即释放,不保留值。external variables可以在函数之间传递参数,但也有可能造成意料之外的改动。
编程作为工程的精神是:尽量让每一部分都只知道(可access)它必须知道的部分,其它的互相之间都隐藏起来。
scope:即一个对象的有效范围。automatic variables的scope是定义它的函数内。external variable和函数的scope是从声明它开始到所编译的文件结束。如果一个external variable在定义之前就被引用,或它在一个不同于定义它的文件中被使用,则必须用extern来定义它。
习惯上把所有函数和外部变量一起在头文件(header file)里声明了,起名*.h。
static variables: 一个变量既需要在某些函数间使用,又不希望别的函数知道它,可以把前者那些函数写在一个文件里,在此变量声明前加static和external即可。函数也可声明为static,这样函数所在文件之外就不能call它了。static也可用在内部变量上,它也像automatic variables只存在于特定函数内,但不同的是它保有storage,函数call完值还在。
寄存器变量(register variables):只能将automatic variables和函数的形式参数声明为寄存器变量,可能可以加快程序运行速度。
块结构(block structure):函数不能在块结构中嵌套,但变量可以在块结构中声明,且scope限于那个块。一对{}(compound statement)即可构成一个块。若在块中有与external variable同名的变量则其类型以块中的为准,但通常不建议使用重名变量。
初始化:如无显明初始化,external和static变量自动获得0初值,automatic和register变量自动获得垃圾初值。若赋初值,则external和static的必须是常数形式的初值,automatic和register的可以是由其它变量算出来的数值赋给它,甚或是函数返回值。数组的初始化:可以把{}中用逗号隔开的数依序赋给数组元素。若数组大小未指定,则编译器会自动按所赋值个数确定,若所赋值个数大于指定大小则报错,若小于则剩下的初始化为0.字符数组可以{}地赋值,也可以直接把字符串"***"赋给它,{'a','\0'}等价于"a"。
C中的函数可以迭代使用。允许迭代可以使函数比较容易写和看懂。但是定要小心处理迭代到trivial时的状态。
C的预处理:
#include"" 和#include<>都可以使编译时直接把别的文件的内容拿过来代替这一行。被include的文件若修改了,所有include它的文件都要重新编译。
宏的格式:#define name replacement text。其作用方法是:编译时所有name都被换成replacement text,其scope为从出现到编译的文件结束。定义中可以用到之前的定义。编译中的替换不替换被双引号引起来的字符串,也不替换包含这个name的tokens的一部分。replacement text的选择是任意的。
宏可以用参数,例如#define max(A,B) ((A)>(B)?A:B) 须注意A和B的括号,因为替换是literally的,如果不加括号会出问题。例如#define square(x) x*x,计算square(z+1)恐怕就不是你想要的结果。#undef可以取消定义。
定义中#的用法:#define dprint(expr) printf(#expr " = %g\n", expr)。带了#的expr会给它加上""当作一个字符串处理。
定义中##的用法:#define paste(front,back) front ## back的结果是paste(name,sir)是namesir。##吸收掉两边的空格把两边的参数连起来。
条件包含(conditional inclusion):以#if或#ifdef或#ifndef开始,以#endif结束。例子:#ifndef HDR \n #define HDR \n #endif。条件测试可以用来保证header只被include一次,也可以用来针对不同系统include进不同的header。
Chapter 5
类型名 *name; 声明了一个指向某类型的指针。void *name;声明的指针可以用于值向任何类型的量,但不能从中取出它所指量的值。
例如: int x *ip; ip=&x;把x的地址赋给指针ip。*ip在声明行的意思是“我这儿声明指针呢”,在statement里意思是指针所指地址的值。
为什么指针还要声明类型?不声明类型就不能取所指地址的值?因为存一个值可能不止需要一个字节,不声明类型就不知道要往后读几个字节和以什么方式计算其值。
*用作指针取值(dereference)时优先级仅次于括号那级,但它是从右往左算的,所以(*p)++中的括号是必要的。
指针是变量,因此一个指针的值可以赋给另一个指针。
函数不能直接改变call它的变量的值,但它可以改变作为它的arguments的指针。比如交换两数a,b的值可以写个交换两指针的函数swap(int *px,*py),然后用swap(&a,&b)貌似交换二者之值。
数组元素的值是连续存储的,声明一个数组也就是给了一个指向数组0号元素的指针,char s[];和char *s;等价。事实上:若 int a[10],*pa; pa=&a[0];则a和pa是一个东西,a[i]和*(a+i)是一个东西,pa[i]和*(pa+i)是一个东西。须注意的是pa是变量,a不是。因此写pa++是合法的,a++则不合法。若p是指向某数组middle某元素,且p[-1]存在,则p[-1]这种写法合法。
指针算术的加减不是地址的加减,而是一个元素一个元素的挪动,具体地址的操作埋在底下了。
地址算术:指针所代表的地址在两指针所指元素属于同一个数组时可以减。指针之间不能加。指针可以自加和自减。指针可以被赋0值,其更多地被用NULL代替。NULL在<stdio.h>中被定义,0是唯一可与指针比较的数值。指向同一个数组的指针可以做大小比较,等或不等比较。
若写char *p; p="hello";则p被赋予指向字符串"hello"第一个字符的指针值。而char q[]; q="hello";后q是一个数组。指针可以改为指向别的地址。数组元素可以变化但存储位置不变。
指针数组的例子:int *girls[];
指针的指针的例子:int *(*girls[]);
多维数组声明时只有第一个[]可以是空的,其它的必须在声明时指明。多维数组更准确的说法是数组的数组,因为girls[i][j]是合法的,girls[i,j]则不合法。
下面要说的比较tricky:int (*girls)[29];是一个指向包含29个元素的数组的指针,而int *girls[29];则是一个由29个指针为元素的数组。为啥?[]的优先级高于*。
int *girl_phone(unsigned int number[]) 是一个参数为整数,返回值为指针的函数。也就是说:函数的返回值可以是指针。
二维数组和指针数组的区别:声明一个指明长度的二维数组时就已经虚位以待了,而声明一个指明长度的指针数组不过是准备了几个位置做指针,还没说它们要指向哪。它们完全可以指向不同数组的子数组,且子数组的大小不必相同,这给了它灵活性。
命令行参数:在命令行环境下call函数会自动地有两个arguments,一个是argc即argument的个数,函数名默认是第一个argument,所以argc至少为1;一个是argv,一个指针数组,指向一行命令的作为string的每个argument。
指针也可以指向函数,例如(*girls)(int,int);意思是girls是一个指向有两个int arguments的函数的指针。也有返回值为指针的函数。嵌套几层容易晕。
复杂声明这一节看得我头大,这东西有意义嘛?等发现有意义再回来看。
Chapter 6
本章描述一些用于存储数据的结构。用来定义结构的命令是struct。例子:
struct point {
int x;
int y;
};
这定义了一个类型,大括号里面的是它的members。指明members用.MemberName。结构定义可以嵌套。取member的.运算符是从左往右计算的。
函数定义的名字那一行,名字前面的用于指明返回值的类型,后面小括号里的指明arguments是什么。函数返回值可以属于struct定义出来的某个类型。
如果一个大的结构要传给一个函数,一般传指针比传整个结构的copy要高效。
指向结构的指针很常用,所以有一个特别的写法。例如p是一个指向结构的指针。p->member-of-structure就代表那个member,这比(*p).member要易写。
指针的声明和赋值有个易混淆的地方,比如:
struct point origin,*pp;
pp=&origin;
和
struct point origin, *pp=&origin;
是等价的。声明行里的*是用于告诉编译器“我这儿要声明一个指针”。指针的名字并不包括那个*,而在声明行之外用那个指针,前面还有个*的话,是取地址上存储的值的意思。
(收起)