首先本文是对参考中三个连接的博客进行的整理,非常感谢三位博主的努力,每次都感叹网友的力量实在太强大了……
第一章 快速上手
1. 在C语言中用/*和*/来注释掉这段代码,这个实际上并不是十分的安全,要从逻辑上删除一段C代码,最好的办法是使用#if指令:
#if 0
Statement
#endif
2. 其他语言中,无返回值的函数称为过程(procedure)。
3. 数组做参数的时候是以引用(reference)的方式传递的,即地址传递。而标量和常量都是传值调用,被调用的函数无法修改调用函数以传值形式传递给它的参数,然而当被调用函数修改数组参数的其中一个元素时,调用函数所传递的数组就会被实际修改。
4. 字符是以一串NUL字节结尾的字符。NUL作为字符串终止符,它本身并不被看作字符的一部分,NUL表示字符串结尾,NULL表示空指针。
5.当传递一个数组时,可以无需指定数组的长度(如果需要长度,则需要再增加一个长度的参数)。
int read(int a[], int len);
(在函数内部最好进行出错检查)
6.使用scanf函数应该注意:使用所有格式码(除了%c之外)时,输入值之前的空白(空格、制表符、换行符等)会被跳过,值后面的空白表示该值的结束,因此,用%s格式码输入字符串时,中间不能包含空白。
7.编译器通常不对数组下标的有效性进行检查。
8.注释是不安全的,是不允许嵌套的,总是与第一个*/相结合。
9.
int ch;
while((ch = getchar()) != EOF && ch != ‘\n‘);
ch被声明为整型,但是又用来读取字符的原因:
EOF是一个整型数值,它的位数比字符类型要多,把ch声明为整型可以防止从输入读取的字符意外的解释为EOF,但是同时意味着接收字符的ch必须足够大,足以容纳EOF。
第二章 基本概念
1.C语言在实现的过程中,存在两种环境,一种是翻译环境(源代码被转换为可执行的机器指令),另一种是执行环境(用于执行实际代码),这两种环境不必位于同一台机器上,例如交叉编译。
2.翻译包括两个阶段:编译与链接,其中编译包括:预处理,解析,优化(可选)。
3.执行包括几个阶段:首先,程序必须再入到内存中。在宿主环境中(也就是具有操作系统的环境),这个任务由操作系统完成。那些不是存储在堆栈中的尚未初始化的变量将在这个时候得到初始值。然后,便开始执行程序代码。在绝大多数机器里,程序将使用一个运行时堆栈(stack),它用于存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程中将一直保留它们的值。
环境:翻译环境: 源代码转化成可执行的机器指令
执行环境:用于实际执行代码
翻译:源文件-〉目标文件-〉可执行文件(通过链接器将多个目标文件捆绑在一起)
编译过程:预处理器-〉源代码经过解析产生目标代码(这个过程中是绝大多数错误和警告产生的地方)-〉优化器(就是对目标代码进行进一步优化,使效率更高)
执行:首先,程序被加载到内存,那些不是存储在栈中的未被初始化的变量将在这个时候被初始化;然后,程序的执行便开始了,负责处理一些日常事务,如收集命名行参数以便使程序能够访问他们,并开始调用main函数;现在,开始执行程序代码,在绝大多数机器里,程序将使用一个运行时堆栈,用于存储函数的局部变量和返回地址。程序同时也使用静态内存,存储与静态内存中的变量在整个程序执行过程中将一直保持不变。最后,程序终止,正常终止的话,是main函数返回。
4. 三字母词:几个字符组合起来表示另外一个字符
??( [ ??< { ??= #
??) ] ??> } ??/ \
??! | ??‘ ^ ??- ~
print("You sure??!"); -→ You sure|
转义字符:‘\‘(反斜杠)加上一个或者多个字符组成。
5.标识符是由大小写字母、数字、下划线组成,但是不能以数字开头的,并且不能采用关键字来作为标识符。
6.推荐的良好的程序风格(不一定要一样,只是为了程序的阅读和维护)
(1)空行用于分隔不同的逻辑代码段,按照功能进行分段;
(2)在括号和表达式之间留下一个空格,可以使表达式更加突出;
(3)在绝大部分操作符的使用中,中间都隔以空格;
(4)嵌套于其它语句的语句将缩进;
(5)尽量将注释成块出现;
(6)在函数的定义中,返回类型出现于独立的一行中,函数的名字在下一行的起始处。
7.把一个大型程序放入一个单一的源文件中的优缺点:
优点:容易找到修改函数的所在,链接所需要的时间会少点;
缺点:不易阅读,不易维护,不利于多人合作。
第三章 数据
1. 变量三属性:作用域(Scope),链接属性(linkage),存储类型(Storage Class)。
2. 1、C语言中仅有4种基本数据类型:整型、浮点型、指针、聚合类型(数组和结构等)。
变量的最小范围:
char 0-127
signed char -127-127
unsigned char 0-255
short int -32767-32767
unsigned short int 0-65535
int -32767-32767
unsigned int 0-65535
整形数字之后添加字符L或者l,表示整数为long type;添加字符U或u,表示为unsigned type。长整型至少和整型一样长,整型至少和短整型一样长。可移植性问题:把存储与char变量的值,限制在signed char 和unsigned char 两者的交集之中,这样可以获得最大程度的可移植性,又不牺牲效率。并且只有当char 类型显示声明为signed 或 unsigned 时,才对它执行算术运算。使用字符常量所产生的都是正确的值,所以它能提高程序的可移植性。
3. 如果一个多字节字符常量前面有一个L,表示是宽字符常量(wide character literal)。如L‘X‘, L‘love‘,当运行环境支持宽字符集时,就可以使用它们。
4. 8进制在表示的时候需要前面加一个0,如067;C/C++不允许反斜杠加10进制数字表示字符,所以8进制数表示的时候,可以省去零,如\67。
5. 枚举类型的值,实际上为数字;可以对枚举的符号名显式的指定一个值,后面枚举变量的值在此值基础上加1。
6. 浮点数default类型为double,后面跟L或l时是long double type,跟F或f时是float type。
7. 指针可以高效的实现tree和list数据结构。
8. char *msg = "Hello World!";等价于:
char *msg; msg = "Hello World!";
第一种看似赋值给了*msg,但本质上是赋给了msg。
9. typedef最重要的用途是定义struct。
10. int const *pa;指向整型常量的pointer,可以修改pointer value,但不可以修改它所指向的value。
int *const pb;指向整型的常量pointer,无法修改pointer value,但可以修改它所指向整型value。
int const *const pc;pointer vale和指向的整型的value都不可被修改。
const修饰的对象不变,上例前两个为:*pa和pb,也就是说*pa和pb的内容不变。
11. #define MAX 50
int const max = 50;
这种状况下,#define更合适,它的使用范围没有被限定;而const变量只能被用于使用变量的地方。创建新的类型名的时候,应该使用typedef而不是#define,因为后者无法正确处理指针类型。
Cpp代码
- #define d_ptr_to_char char *
- d_ptr_to_char a, b;
这样a是正确声明了,但是b却声明为了字符类型。
12. Internal链接属性的标识符在同一源文件内的所有声明中都指向同一实体。
External链接属性的标识符不论声明多少次,位于几个源文件都表示同一实体。
具有external链接属性的标识符,前面加上static关键字可以是它的链接属性变为internal,static只对default属性为external的声明有效果。
13. Static:当用于函数定义时,或用于代码块之外的变量声明时,链接属性从external变为internal,标识符的存储类型和scope不受影响,只能在源文件中访问;当用于代码块内部的变量声明时,static关键字用于修改变量的存储类型,变更为静态变量,变量的链接属性和作用域不受影响。
Type 声明位置 Stack Scope 如果声明为static
全局 所有代码块之外 否 声明处到文件尾 不允许从其他源文件访问
局部 代码块起始处 是 整个代码块 不在stack中,value在程序运行中一直保持
形参 函数头部 是 整个函数 不允许
14.存储变量的三个地方:普通内存,运行时堆栈,硬件寄存器(变量存储类型是指存储变量值的内存类型)。
变量的存储类型取决于它们声明的位置:
(1)凡是在任何代码块之外声明的变量总是存储在静态内存中(程序运行前创建,并且如果没显式赋值则创建时缺省的赋于一个初始值0,在程序执行完毕后销毁);
(2)在代码块内部声明的变量是存储在堆栈中(在程序执行到声明自动变量的代码块时创建,在离开这个代码块时销毁,如果加上关键字static可以将其修改为静态变量,但是不改变该变量的作用域,仅仅改变其存储类型)。
第四章 语句
1. C没有专门的赋值语句,而是采用表达式语句代替。
2. C没有bool类型,而是用整数类型代替,零为假,非零为真。
3. 跳出多层loop的方法:
A. 使用goto语句。
B. 设置status flag,在每个循环中都去判断status flag。
C. 把所有loop放在一个单独的函数中,使用return语句跳出loop。
else语句从属于最靠近它的不完整的if语句。
4.break和continue
break用于永久终止循环,continue用于终止当前循环,这两条语句出现在嵌套的循环内部,它只对最内层的循环起作用。
5.当循环体为空的时候,单独用一行来表示一条空语句是比较好的做法,这样让人一目了然。
6.for语句与while语句执行过程的区别在于出现continue语句时,在for语句中,continue语句跳过循环体的剩余语句,直接回到调整部分(即for语句的第三个表达式);在while语句中调整部分是循环体的一部分,所以continue将会把它也跳过。
7.在while语句和do语句之间的选择:当需要循环体至少执行一次时,选择do语句。
8.对于switch语句:
switch(expression)
statement;
expression的结果必须是整型值。
switch语句执行时是贯穿所有的case标签。
9.switch语句的case标签只是决定语句列表的进入点,而不是划分它们,要划分它们,则需要break语句的帮助。
10.最好在switch语句中的最后一个case语句也加上一个break,这个有利于未来的维护,避免错误。
11.每个switch语句只能出现一条default子句,最好在每个switch语句中就加上一条default子句,这样可以避免一些不必要的错误。
12.C语言不具备任何输入/输出语句,I/O是通过调用库函数来实现的,也不具备异常处理语句,也是通过调用库函数来完成的。
第五章 操作符和表达式
1. 移位计数
int count_one_bits(unsigned value)
{
int ones;
for( ones = 0; value != 0; value = value>>1)
{
If(value%2 != 0)
ones = ones + 1;
}
return ones;
}
更有效率的方法:
int counter(unsigned value)
{
int counter = 0;
while(value)
{
counter++;
x = x&(x-1);
}
return counter;
}
此外x&(x-1)还可以快速判定x是否为2^n。
2. 位操作符
指定bit置1: value = value | 1<<bit_number
指定bit清0: value = value & ~(1<<bit_number)
检测指定bit: value & 1<<bit_number
3. sizeof():判断操作数类型长度,以byte为单位;操作数既可以使表达式,也可以是类型名。sizeof后如果是类型必须加括弧,如果是变量名可以不加括弧。这是因为sizeof是个操作符不是个函数。如果是指针变量则返回4(指针变量本身存储大小)。
sizeof()的操作数为数组名时,返回的是该数组的长度。联合类型操作数的sizeof是其最大字节成员的字节数。结构类型操作数的sizeof是这种类型对象的总字节数,包括任何垫补在内。判断表达式的长度并不会对表达式求值。sizeof 操作符不能返回动态地被分派了的数组或外部的数组的尺寸 。
4. 短路求值(short-circuited evaluation):"&&","||","?:",","
(L→R)左操作数的结果决定是否对表达式进一步求值
5. 利用‘.‘和‘->‘访问结构体的区别:
s为struct变量时候,使用s.a访问s中的成员a;
s为指向struct变量的指针时,使用s->a访问成员a。
7. 左值:表示一个存储位置,可以出现在赋值符的左边或右边;
右值:表示一个value,只能出现在赋值符的右边。
8. 算术转换(arithmetic conversion):操作数类型的转换。(类型的提升)
常用方法是在执行运算之前把其中一个(或多个)操作数类型转换为左值的类型。
9. 复杂表达式的求值顺序3要素:操作符的优先级,操作符的结合性,操作符是否控制执行顺序(短路求值)。
优先级只对相邻操作符的执行顺序有效。
非法表达式的求值顺序(规范没有定义)由编译器决定。
如果顺序会对导致结果产生区别,最好使用临时变量
10.C语言的算术操作符中只有%操作符要求两边都必须是整型数,其他操作符都是既适用于浮点类型又适用于整数类型。
11.C语言中的左移操作时,右边空出来的位均用0补齐,然而右移操作分为两类:逻辑右移和算术右移。 逻辑右移是左边移入的位用0填充,算术移位是左边移入的位由原先该值的符号位决定的,符号位为1的移入的位均为1,符号位为0的移入的位均为0。
注:无符号值执行的所有移位操作就是逻辑移位,而有符号值的移位操作是取决于编译器;因此如果一个程序如果使用了有符号数的右移移位操作,它就是不可移植的。应该避免下面类型的移位操作,这个移位的位数也是由编译器决定的。
12.C语言中位操作符要求操作数必须是整数类型;
13.关于位操作中的常用操作:
(1)置位:
value = value | 1 << bit_number;
(2)清零:
value = value & ~(1 << bit_number);
(3)测试:
value & 1 << bit_number;
14.赋值操作符的结合性是从右到左(求值的顺序),在使用赋值运算时应该考虑截短问题的存在;
15.使用复合赋值符能够使源代码更加容易阅读和书写;
16.sizeof是单目操作符而不是一个函数;
17.关于++操作符和--操作符的理解
关于前缀形式的++操作符和后缀形式的++操作符,在操作数之前的操作符在变量值被使用之前增加它的值,在操作数之后的操作符在变量值被使用后才增加它的值;对于前缀和后缀形式的增值操作符都复制一份变量值的拷贝,用于周围表达式的值正是这份拷贝,因此这些操作符的结果不是被它们所修改的变量,而是变量值的拷贝,故它们不能像下面所示的形式进行操作:
a++ = 10; ++a = 10;
(a++和++a的结果都是a的拷贝,并不是变量的本身,因此无法进行赋值)。
18.条件操作符能够产生更小的目标代码,从而提高效率。
19.逗号操作符能够使代码更加易于维护:
逗号运算符 (,)
顺序执行两个表达式
expression1, expression2
说明
, 运算符使它两边的表达式以从左到右的顺序被执行,并获得右边表达式的值。 逗号运算符的优先级别在所有运算符中最低。, 运算符最普通的用途是在 for 循环的递增表达式中使用。例如:
for (i = 0; i < 10; i++, j++)
{
k = i + j;
}
每次通过循环的末端时, for 语句只允许单个表达式被执行。, 运算符被用来允许多个表达式被当作单个表达式,从而规避该限制。
再如下面实例:
int x,y,z;
x=y=1;
z=x++,y++,++y;
printf("%d,%d,%d\n",x,y,z);
上面的表达式中应该等价于这样的结合:(z=x++),y++,++y;如果这样写的话,则答案很清晰,为:2,3,1。
例题1:(a = 3,b = 5,b+ = a,c = b* 5),求逗号表达式的值?
答案:40。前两个表达式只是赋值,从第三个开始计算,b+=a,即b=b+a,即b=5+3,b=8,求最后一个表达式,c=b*5=8*5=40.因为逗号表达式的值是最后一个表达式的值,所以整个逗号表达式的值为40,其他各变量最后的值依次为:a=3,b=8,c=40。
例题2:若已定义x和y为double类型,则表达式:x=1,y=x+3/2的值是
A) 1 B) 2 C) 2.0 D) 2.5
分析:该表达式是一个逗号表达式,所以先运算x=1,结果变量x中的值为1.0,然后运算y=x+3/2,其结果是变量y中的值为2.0(这个运算过程可参阅本专题的“整数除法的注意事项”——整数相除,舍入法取整数部分),注意此时表达式y=x+3/2的值即等于变量y的值为2.0。最后,整个逗号表达式的值应该等于最后一个表达式的值2.0,所以,正确答案是C)。
例题3:若t为double类型,表达式t=1,t+5,t++的值是
A) 1 B) 6.0 C) 2.0 D) 1.0
分析:该题的表达式也是一逗号表达式,运算过程同例题1。需要注意的是,其中的第二个表达式(t+5)对整个表达式的运算结果不产生任何影响,因为它没有改变变量x的值(x的值仍为1.0),最后一个表达式(t++)的值为变量x进行自增运算前的值1.0,所以整个表达式的值为1.0。
例题4:有如下函数调用语句
func(rec1,rec2+rec3,(rec4,rec5));
该函数调用语句中,含有的实参个数是3。
20.C的整型算术运算总是至少以缺省整型类型的精度来进行的。
第六章 指针
1. 边界对齐:boundary alignment(内存对齐)。
2. 不能简单的通过检查一个值的位来判断它的类型。
数据的意义不在于它的类型,而在于它被使用的方式。
3. 指针的初始化用&操作符来完成,它用于产生操作数的内存地址。
4. 声明的指针必须初始化才可以使用,不然不能确定指针指向的地方。
5. 安全策略:让函数返回独立的值。首先是函数返回的status value, 用于判断操作是否成功;其次是形参pointer,用于在操作成功时返回结果。
6. 指针变量可以做左值,是因为它们是变量(存储地址)。
7. 指针的强制类型转换:
* 100 = 120; // 非法语句,因为间接访问表达式(*)只能作用于指针类型表达式。
* (int *) 100 = 120; // 合法语句,把100从"整型"转换为"指向整型的指针"。(这仅仅是个例子)
8. 指针运算:
指针 +/- 整数:适用于指向数组中某个元素的指针,且要保证不越界。
指针 – 指针:适用于当两个指针都指向同一数组中的元素时。
关系运算:<, <=, >, >=,适用于指向同一数组中的元素的两个指针,表示的意义为哪个指针指向的数组元素更靠前或靠后。
指针运算只有作用于数组中,其结果才是可以预测的。
9.引起段错误原因是引用非法地址,总线错误原因是数据在内存中的存储地址处在错误的边界上。
10.对一个NULL指针进行解引用的操作是非法的,在对指针进行解引用操作之前,首先必须确保它不是NULL指针。
11.&ch能够作为一个右值来使用,但是不能用来当左值来使用,至于原因:我们知道左值意味着位置,&操作符的的结果应该放在哪个位置,肯定位于一个地方,但是你无法知道在什么地方,因此这个表达式并未标识内存的位置,所以无法作为一个合法的左值。
由此可知,作为一个左值,必须要有对它的存储位置有个清晰的定义(关于左值,右值在上一章有提到)。
12.在函数内,如果传进来一个指针,应该对其进行检查是否是NULL指针;如果两个指针所指向的不是同一个数组中的元素,那么它们之间相减的结果是未定义的。
13.注意:
1. 可以把指针初始化为0、NULL或某个地址,具有值NULL的指针不指向任何值。
2. 当把0赋值给指针时,编译器先把0转换为指向合适数据类型的指针。
3. 值0是唯一能够直接赋给指针变量的整数值。
4. 切忌使用未初始化的指针会给系统带来隐藏的危害,一但指针指向非法区域,会造成系统崩溃。
第七章 函数
1. C函数的实现运用了堆栈。
函数在调用的时候,要为被调用的函数分配内存空间(堆栈),相关寄存器的值也必须保存;在函数返回之后,释放内存空间,恢复相关寄存器的原始值。
2. K&R C 中,形参的类型是以单独列表的形式声明的。
int * Find_int ( key, array, array_len ) int key; int array[]; int arrary_len;
{
// Function Body
}
这种声明形式,新标准仍兼容。
3. 函数可以分为:真函数(有返回值,默认类型为整型)和过程函数(无返回值)。
4. 函数在使用之前,必须进行"声明"。
5. 无参函数: int * func (void); // 关键字void 提示没有任何参数。
6. C函数的所有参数均为传值调用,函数获得的参数值只是实参的拷贝。
参数为指针时传递的值为指针对象的地址,这个行为被称为传址调用,也就是许多其他语言所实现的var参数。
使用指针作为函数参数对结构进行传值,可以提高程序的运行效率。
指针形参应尽可能的声明为const,防止函数改变指针形参指向对象的值。
7. C可以用于设计和实现抽象数据类型(ADT, abstract data type), 因为它可以限制函数和数据定义的作用域。(Black Box: 黑盒的功能通过规定的接口访问)
8. static的合理使用可以限制对模块的访问,限制对那些非接口的函数和数据的访问。static声明的函数或数据,只能在文件内部被访问。
9. 许多问题是以递归的形式进行解释的,这只是因为它比非递归形式更为清晰;但迭代的方式往往比递归更有效率。(用递归的方法去做斐波那契数列是非常浪费资源的<二的几何次方>)
10. stdarg.h:实现了可变参数列表。
#define va_start(ap, parmN)(ap = ...)
#define va_arg(ap, type)(*((type *)(ap))++)
#define va_end(ap)
Stdarg.h中声明了类型va_list和三个宏:va_start, va_arg 和 va_end。通过在函数中声明va_list 变量,与三个宏配合使用,访问参数的值。此宏不可以判断参数的个数和类型。
va_start(va_list, value); // value是参数列表中省略号钱最后一个有名字的参数。初始化过程是把va_list 指向可变参数的第一个参数,所以第一个参数必须是一个有命名的参数。
va_arg(va_list, type); // type是va_list中下一个参数的type。
va_end(va_list); // 访问完所有的参数之后,调用va_end结束。
可以在访问参数的过程中中止,但参数必须从第一个参数开始依次访问。所有作为可变参数传递给函数的值都会执行默认参数类型提升。
由于参数列表中的可变参数部分没有原型(type),所以,所有可变参数传递给函数的时候都将执行缺省参数类型提升。(默认参数类型提升:default argument promotion,在参数产地给函数之前,char和short提升为int,float提升为double…..)
11. 递归函数的两个条件:(1).有限制条件(2).每次操作后越来越接近这个限制条件。
15. 递归函数运行设计一些开销:参数必须压到堆栈中,为局部变量分配内存空间,应该慎重使用递归的方法,可用迭代法代替。
16.当程序调用一个无法见到原型的函数时,编译器便认为该函数返回一个整型值。
17.函数传递的参数不能超过5个。
第八章 数组
1. 不可以使用"="把一个数组的所有元素复制给另一个数组。
2. int array[10];
int *ap = array + 2 ;
C的下标引用和间接表达式是一样的。
ap[0],这个表达式是完全合法的,这种情况下对等的表达式为*(ap+(0))。
2[array],这个表达式也是合法的,转换为间接表达:*(2+(array))。
3. 下标不会比指针更有效率,但指针有时会比下标更有效率。(效率:指针≥下标,如对数组进行循环赋值时。)当你根据某个固定的数目在一个数组中移动时,使用指针代码将比使用下标产生更加有效率的代码,当这个增量是1,并且机器具有地址自动增量模型时,这点表现更加突出。
4. int a[5]; // 初始化,分配了内存,*a完全合法。
int *b; // 未初始化,*b指向内存中不确定的位置。
声明数组时,同时分配了内存空间,用于存储数组元素;声明指针时,只分配了存储指针本身的内存空间,其指向的内存位置是未知的。
5. 函数形参中声明为指向const的指针:
A. 有助与使用者仅观察该函数形参就能发现该数据不会被修改。
B. 编译器可以捕捉任何试图修改该数据的意外错误。
C. 这类声明允许函数传递const参数。
6. char message[] = "Hello"; // char数组初始化的一种方法,并非字符串常量。
7. int matrix[3][10];
==> int (*p)[10] = matrix; // 指针p指向matrix的第一行。
==> int *pi = &matrix[0][0]; // 指针pi指向matrix第一行第一个元素。
==> int *pt = matrix[0]; // 指针pt指向matrix的第一行。
指向整型指针的指针(int **p),和指向整型数组的指针是不等价的(int (*p)[10])。
多维数组做函数参数时,必须显式的指明第二维和以后维度的长度。这是因为多维数组的每个元素本身是另外一个数组,编译器需要知道他的维数,以便为函数形参的下标表达式进行求值。
8. char const keyword[] = {
"do", "for", "if", "register", "return"};
数组keyword的元素个数为:sizeof(keyword)/sizeof(keyword[0]);。
9. 指针数组:数组的成员为指针。声明方式:int *pt[]。
10. 数组名是指向某种类型的指针常量,表示数组第一个元素的地址,数组名是一个常量是不能被修改的。
11. 声明为寄存器的指针比静态内存和堆栈中的指针效率更高。
12. 如果你可以通过测试一些已经初始化并经过调整的内容来判断循环是否终止,那么你就不需要一个单独的计数器。
13. 函数中的形式参数传递的是数组时,不指定数组大小的原因是因为数组传递的时候是以指针的形式传递的。
14. 多位数组的名称表示的是,指向第一个数组的指针。
15. max[3,4]等价于max[3]。
16. 多维数组中初始化时,记得要把花括号加上(更容易区分,还有就是可以给缺少元素的初始化为0)。
17. 只要有可能函数的形式参数都应该声明为const。
18.只有两种场合下,数组名并不用指针常量来表示——就是当数组名作为sizeof操作符或单目操作符&的操作数时,sizeof返回整个数组的长度,而不是指向数组的指针的长度,取一个数组名的地址所产生的是一个指向数组的指针。
19.除优先级以外,间接引用与下标引用完全一样;间接引用与下标引用可以进行相互转换。
20.数组与指针的区别:
(1)声明一个数组时,编译器将根据声明所指定的元素数量为数组保留内存空间,然后再创建数组名,它的值是一个常量,指向这段空间的起始位置;
(2)声明一个指针变量时,编译器只为指针本身保留内存空间,它并不为任何整型值分配内存空间,而且,指针变量并未被初始化为指向任何现有的内存空间,如果是一个自动变量,它根本就不会被初始化。
21.如果在程序的执行每次进入该函数(代码块)时,每次都对数组进行重新初始化不是什么有必要,就可以把数组声明为static,这样数组的初始化只需在程序开始前执行一次。
22.
char message[] = "hello";
char *message = "hello";
前者初始化一个字符数组的元素,而后者是一个真正的字符串常量,这个指针变量被初始化指向这个字符串常量的存储位置。
23.在C中,多维数组的元素存储顺序按照最右边的下标率先变化的原则,称为行主序。
24.二维数组名是一个指向数组的指针;
int matrix[3][10];
int *p = matrix;
int **p = matrix;
int (*p)[10] = maxtrix;
以上三种声明只有一个是合法的,首先我们知道matrix是一个指向数组的指针,第一个p是一个指向整型的指针,而第二个是为指向整型指针的指针,只有三个是指向整型数组的指针,故第三个是合法的。
25.作为函数参数的多维数组名的传递方式和一维数组名相同——实际传递的是个指向数组第一个元素的指针,但是两者之间的区别是:多维数组的每个元素本身是另外一个数组,编译器需要知道它的维数,以便为函数形参的下标表达式进行求值。
26.数组形参既可以声明为数组,也可以声明为指针,这两种声明形式只有当它们作为函数的形参时才是相等的。
第九章 字符串、字符和字节
1. strlen()返回的是无符号整型数。
无符号数之间进行的操作,结果还是无符号数。尽量不要在表达式中使用无符号数(可能导致表达式的结果不可预料);如:strlen(x) – strlen(y),他的结果永远是大于等于0的。如要避免上述问题需要进行类型的强制转换。
2. 字符串以NUL结尾,可通过判断是否为NUL计算长度。
3. 复制: char *strcpy( char *dst, char const *src ); // strlen(src)<strlen(dst)时dst数据丢失,strlen(src)>strlen(dst)时dst数据溢出。
连接: char *strcat( char *dst, char const *src );
比较: int strcmp( char const *s1, char const *s2 ); // 相等返回0
长度受限的字符串函数:
char *strncpy( char *dst, char const *src, size_t len ); // 如果strlen(src)小于len,dst数组会用NUL填充到长度len。要保证len小于等于strlen(dst).
char *strncat( char *dst, char const *src, size_t len );
int strncmp( char const *s1, char const *s2, size_t len );
4. 字符串查找:
char *strchr( char const *str, int ch ); // return第一次出现的位置
char *strrchr( char const *str, int ch ); // return最后一次出现的位置
char *strpbrk( char const *str, char const *group ); // 查找一组字符中任一字符第一次出现的位置
char *strstr( char const *s1, char const *s2 ); // 查找一个子串第一次出现的位置
size_t strspn( char const *str, char const *group ); // 第一次匹配的相对位置
size_t strcspn( char const *str, char const *group ); // 第一次不匹配的相对位置
char *strtok( char *str, char const *group ); // 函数会修改所处理的字符串
for( token = strtok(source, sep); token != NULL; token = strtok(NULL, sep)); // 如果strtok的第一个参数为NULL,函数会从同一个字符串中上一个保存的位置开始继续查找;如果没有结果,返回NULL指针。
5. char *strerror( int error_number ); // strerror把一个错误代码作为他的参数,返回一个指向字符串的指针,该字符串用于描述这个错误。
6. 字符操作函数: iscntrl, isspace, isdigit, isxdigit, islower, isupper, isalpha, isalnum, ispunct, isgraph, isprint;使用这些函数可以增强程序的可移植性。
7. 内存操作:以下函数遇到NULL字符不会停止
void *memcpy( void *dst, void const *src, size_t length );
void *memmove( void *dst, void const *src, size_t length );
void *memcmp( void const *a, void const *b, size_t length );
void *memchr( void const *a, int ch, size_t length );
void *memset( void *a, int ch, size_t length );
memcpy的效率高于memmove,但memmove在dst和src存储位置发生重叠时,可以继续使用。
8.NUL字节是字符串的终止符,但它本身并不是字符串的一部分,所以字符串的长度并不包括NUL字节。
9.用于复制字符串的strcpy原型:
char *strcpy(char *dst, char const *src);
由于dst参数将进行修改,所以它必须是一个字符数组或者是一个指向动态分配内存的数组指针,不能使用字符串常量;使用这个函数时,必须保证目标字符数组的空间足以容纳需要复制的字符串。
10.strcpy与strcat都返回一个第一个参数的一份拷贝,就是一个指向目标字符数组的指针,因此可以嵌套调用这两个函数。
11.在使用strncpy函数时,最好按如下的方法:
strncpy(buffer, name, size);
buffer[size - 1] = ‘\0‘;
这样可以保证buffer中的字符串是以NUL结尾的;
12.strncat与strncpy不同,strncat总是在结果字符串后面加上一个NUL字符。
第十章 结构和联合
1. 下标操作和点操作具有相同的优先级,都是从左到右进行操作。但是点操作符的优先级高于间接访问操作符。
2. "–>"操作符的左操作数必须是一个指向结构的指针。
3. 结构体自引用结构体是非法的,可以通过引用指向结构体的指针解决。
4. 不完全声明:先声明一个作为结构标签的标识符,然后可以把标签用在不需要知道这个结构长度的声明中(如指针)。
struct B;
struct A {
struct B *pt;
};
Struct B {
sturct A *pt;
};
在A的成员列表中需要标签B的不完全声明,A声明之后,B的成员列表也可以被声明。
5. 内存对齐:编译器为一个结构变量分配内存空间时,需满足它们的内存对齐要求。(程序的可移植性)
结构体的成员声明顺序会影响到结构体所需的存储空间。
struct A {char a; int b; char c;}; struct B {char a; char b; int c;};
在一个int长度为4B,并且其存储位置必须为4的整数倍的机器上,sizeof(A)的长度为12,sizeof(B)的长度为8。
sizeof能够得出一个结构的整体长度,包括因内存对齐而跳过的字节。确定结构成员的实际位置,可以使用offsetof宏(stddef.h)。
offsetof(type, member); // type是结构的类型,member是成员名。
10. 结构体指针做函数参数时,加上const用于禁止修改指针指向结构的数据。
11. 位段:能够把长度为奇数的数据封装在一起,节省存储空间;可以很方便的访问一个整型值(register)。(位段是结构的一种)
位段成员必须声明为:int,signed int,unsigned int型,在成员名之后必须有一个冒号和一个整数,整数是该段所占用位的数目。
struct RegisterA {unsigned aa:1; unsigned ab:2; unsigned ac:5;};
12. 联合union:成员储存在内存中的同一位置。联合变量的初始化必须是第一个成员的类型,而且必须在一对"{}"中。
union {int a; float b; char c[4];}x = {5};
union的一个重要的作用是多选一。比如人的性别是两种不同的属性结构,在定义一个人的结构时就可以使用联合来选择男女这两种属性结构
13. 如果你想在多个元文件中使用同一类型的数据结构,你应该把标签声明或typedef形式的声明放在一个头文件中。
14. 结构成员可以是标量,数组,指针甚至是其他结构。
15. 注意结构的不完整声明
1、C提供两种类型的聚合数据类型,数组和结构,数组是相同类型元素的集合,而使用结构能够把不同类型的值存储在一起;数组是通过下标引用或指针间接访问元素的,而结构可以通过成员名字来访问的,当然结构也可以通过指针来进行间接访问的。
2、不同的结构声明即使它们的成员列表相同也被认为是不同的类型:
struct {
int a;
int b;
float c;
}x;
struct {
int a;
int b;
float c;
}y[20],*z;
对于以上的描述,这两个声明被编译器当作两种截然不同的类型,即使它们的成员列表完全相同,因此变量y和z的类型和x的类型不同,因此下面这条语句不成立:
z=&x;
16.作为函数参数的结构,如果采用传递结构,则需要复制整个结构的长度到堆栈中,再丢弃,这样的效率比较低,而传递结构指针比结构小得多,因此能够提高效率,但是向函数传递结构指针也有不足之处,就是可以对调用的变量进行修改(可以用const声明结构指针来防止)。
17.联合的声明与结构类似,联合的所有成员引用的是内存中相同的位置,可用于某一时刻,只有一个字段被使用,提高效率。
18.如果联合的各个成员具有不同的长度,联合的长度就是它最长成员的长度。
19.联合变量可以被初始化,但是这个初始值必须是联合第一个成员的类型,而且它必须位于一对花括号里面。
例如:(把x.a初始化为5)
union{
int a;
int b;
char c[4];
}x={5};
我们不能把这个类量初始化为一个浮点值或字符值,如果给出的初始值是任何其他类型,它就会转换为一个整数赋给x.a。
第十一章 动态内存分配
1. void *malloc( size_t size ); // memory allocate
void free( void *pointer ); // free memory allocate
void指针表示可以转换为任何其他类型的指针。
free函数的参数可以是NULL,要么就必须是malloc、calloc、realloc返回的指针。释放一块内存空间是不允许的,动态分配的内存必须整块的释放;而且动态分配的内存必须在使用完之后被释放,不然会引起内存泄漏(memory leak)
2. 在使用函数分配的内存前,必须先确保返回的指针不为NULL。
3. void *calloc( size_t num_elements, size_t element_size ); // 元素的数量和大小
void realloc( void *ptr, size_t new_size );
calloc函数在分配内存空间之后会在返回指针之前把内存空间初始化为0。
realloc函数不会改变原内存中存储的数据。当用于扩大内存空间时,可以在原内存空间之后增加;当原内存空间无法修改时,realloc将会分配另外一块符合要求的内存空间,并把原内存空间的内容复制到新内存空间上。因此,在使用realloc之后,原有内存空间的指针不能继续使用,应使用realloc返回的指针。realloc用于修改一个原先已经分配的内存块的大小,在使用realloc之后,就不能再使用指向旧内存块的指针,而是应该使用realloc返回的新指针,如果realloc的第一个参数是NULL,那么它就跟malloc一样;
4. a > b ? 1 : ( a < b ? -1 : 0); // 三种结果都可以兼顾到。
5. 动态内存分配允许程序为一个长度在运行时才知道的数组分配内存空间。
6. malloc 函数用来实现从内存中提取一块合适的内存,并向该程序返回一个指向这个内存的指针。这块内存现在并没有进行任何初始化。当一块以前使用的内存没有使用的时候,程序调用free函数将它归还给内存池供以后使用。
malloc函数当没有内存可以分配时,就会返回一个null指针,所以对null指针的判断很重要。
malloc和calloc的区别:一是,后者在返回指针之前先把内存初始化为0;二是,calloc包括需要元素的数量和每个元素的字节数,根据这个值,它能计算出到底需要分配多少内存。
realloc用于修改原先已经分配好的内存大小。可以扩大也可以缩小,扩大时,前面的存储内容不变,后面的不被初始化;缩小时,尾部的内存便被砍掉。如果原先的内存不能改变大小,该函数将会重新分配一块新的内存,将原先的内存中的内容复制过来。因此使用realloc 后就不能在使用指向旧内存的指针了,而是应该使用realloc 返回的的新指针。
7. 如果偶尔调用了malloc,程序将由于语法错误而无法编译,在alloc中必须加入#undef指令,这样他才能调用malloc而不至于出现语法错误。
8. 不要使用已经被释放的内存。
9. 分配的内存在使用完毕后不进行释放将会产生内存泄露。一个持续分配却一点都不是放内存的程序最终将耗尽可用的内存。
10. 数组在声明时,它所需要的内存在编译时候就被分配;数组有其优点与缺点:优点在于是简单,缺点是(1)无法预知长度,数组无法处理程序所需要使用的元素
11.动态分配内存的常见错误:
(1)对NULL指针进行解引用,即忘记检查所请求的内存是否成功分配;
(2)操作内存时超出了分配内存的边界;
(3)试图释放一块动态分配的内存的一部分,因此传递给free的指针必须是一个从malloc、calloc、realloc函数返回的指针;
(4)一块动态内存被释放后被继续使用;
(5)内存泄漏,因此应保证内存不再使用时,释放内存。
第十二章 使用结构和指针
1. 链表的存储空间是动态分配的。
2. 单链表是一种使用指针来存储值的数据结构。
3. 双向链表:一个指针指向前一个链表节点,另一个指针指向后一个链表节点。
4. 语句的提炼用来简化程序,消除冗余的语句。
5. 在链表中,每个节点包含一个链表下一个节点的指针,连表最后一个指针字段的值为null。为了记住链表的起始位置,可以使用一个根指针(root pointer),跟指针指向链表的第一个节点。注意跟指针只是一个指针,它不包含任何数据。
6. 链表就是一些包含数据的独立数据结构(通常称为节点)的集合;链表中的节点可能分布于内存的各个地方,不一定是物理上相邻,单链表只能一个方向进行遍历。
7. 链表中如果你想遍历其他的节点,你只能从根节点开始。可以对链表进行排序。
8.语句提炼 :如果语句对if语句执行没有影响我们可以将这个语句提前,如果语句在if执行后对这条语句没有影响,可以将这条语句放到后面。
9.不要仅仅以代码的大小衡量代码的效率。
10.在对链表的操作过程中,应该注意以下几点:
(1)动态分配内存时务必检查是否分配成功(检查返回值是否为NULL);
(2)操作的过程中,应该避免对NULL指针进行解引用,所以对于有可能出现NULL指针的地方都要进行判断。
第十三章 高级指针话题
1. int* f, g; // 并没有声明两个指针,星号只作用于f。
int *f(); // 函数操作高于间接访问操作符,所以函数返回的是一个整形指针。
int *f[]; // 下标优先级更高,所以f是一个数组,元素是整型指针。
int (*f)(); // f为一个函数指针
int (*f[])(); //声明合法,数组f的元素是函数指针。
函数和变量都必须先声明后使用,函数的类型可以认为是返回值的类型。
2. _cdecl:c declaration,表示默认方式:参数由右向左依次入栈,参数由调用者清除(手动清除)。
3. 函数指针的初始化:
int f(int);
int (*pf)(int) = &f;或者int (*pf)(int); pf=f;
函数名在使用时总被编译器转换为函数指针,所以前一种初始化方式中的&操作符是可选的,&操作符只是显式的说明编译器将要执行的任务。
4. 回调函数:callback function,函数指针作为参数被调用时,指针指向的函数被称为回调函数。回调函数分离实现了调用函数和被调用函数,调用函数不用关心谁是被调用函数(函数指针,回调函数),只需知道有一个具有特定原型和限制条件的被调用函数。
回调函数实现了调用函数的通用性,使得调用函数可以支持多种数据类型和多种逻辑情况。
调用函数常把与回调函数相关的参数类型声明为void*,表示为指向未知类型的指针,增强了调用函数的泛用度。
5. 函数指针最常见的两个用途是转换表和作为参数传递给另一个函数。转移表:实质上是一个函数指针数组,通过确定数组中的元素来选择调用相应的函数。使用转移表时应该检验下标的有效性。
转换表最好用个例子来解释。下面的代码段取自一个程序,它用于实现一个袖珍式计算器。程序的其他部分已经读入两个数(op1和op2)和一个操作数(oper)。下面的代码对操作符进行测试,然后决定调用哪个函数。
switch( oper ){
case ADD:
result = add( op1, op2);
break;
case SUB:
result = sub( op1, op2);
break;
case MUL:
result = mul( op1, op2);
break;
case DIV:
result = div( op1, op2);
break;
......
对于一个新奇的具有上百个操作符的计算器,这条switch语句将非常长。
为什么要调用函数来执行这些操作呢? 把具体操作和选择操作的代码分开是一种良好的设计方法,更为复杂的操作将肯定以独立的函数来实现,因为它们的长度可能很长。但即使是简单的操作也可能具有副作用,例如保存一个常量值用于以后的操作。
为了使用 switch 语句,表示操作符的代码必须是整数。如果它们是从零开始连续的整数,我们可以使用转换表来实现相同的任务。转换表就是一个函数指针数组。
创建一个转换表需要两个步骤。首先,声明并初始化一个函数指针数组。唯一需要留心之处就是确保这些函数的原型出现在这个数组的声明之前。
double add (double,double);
double sub (double,double);
double mul (double,double);
double div (double,double);
......
double ( *oper_func[] )( double, double)={
add,sub,mul,div,...
};
初始化列表中各个函数名的正确顺序取决于程序中用于表示每个操作符的整型代码。这个例子假定ADD是0 ,SUB是1,MUL是2,依次类推。
第 2 个步骤是用下面这条语句替换前面整条 switch 语句!
result = oper_func[ oper ]( op1,op2 );
oper从数组中选择正确的函数指针,而函数调用操作符执行这个函数。
6. 命令行参数:int main( int argc, char *argv[] )
argc: argument count, argv: argument variables,char指针数组。
7. 字符串常量实质是一个指针。
"xyz" + 1; // 字符串"xyz"的地址加1。
*"xyz"; // 表达式结果为‘x‘。
"xyz"[2]; // 表达式结果为‘z‘。
8. 函数只能返回标量值,不能返回数组。
9. 对函数指针进行操作之前,必须把它初始化为指向某个函数,函数指针的初始化也可以通过赋值操作进行完成;在函数指针的初始化之前,具有函数的原型是很重要的。
10. 把具体操作和选择操作份开始一个良好的程序设计方案。
11. 只有当确实需要时,才应该使用多层间接访问,不然程序将会变得更庞大,更缓慢并且难以维护。
12. 不同寻常的代码应该加上相应的注释。
14.调用函数:
int ans;
ans = f(25);
ans = (*pf)(25);
ans = pf(25);
第一种调用函数过程:首先函数名f被转换为一个函数指针,该指针指定函数在内存中的位置,然后函数操作符调用该函数,执行开始于这个地址的代码;
第二种调用函数过程:执行函数操作符之前,将函数指针转换为函数名,其后的过程与前者一样;
第三种调用函数过程:省略了将函数名转换为函数指针,直接执行开始与这个地址的代码。
第十四章 预处理器
1. 预定义符号:
符号 示例 含义
__FILE__ "name.c" 进行编译的源文件名
__LINE__ 25 文件当前行的行号
__DATE__ "Jan 31 1997" 文件被编译的日期
__TIME__ "18:04:30" 文件被编译的时间
__STDC__ 1 如果编译器遵循ANSI C,值就为1,否则未定义。
Note:前后各2个下划线。
2. #define reg register
#define do_forever for(;;)
#define CASE break; case
不要在宏定义的末尾加上分号,这会破坏代码的可阅读性。
3. 宏仅仅是替换。定义宏时要使用括号:替换的数据使用括号,整个宏使用括号。宏适于类型无关的,宏的命名约定:一种方式是都大写,
4. 宏不可以出现递归,即不可自我调用。
5. "#argument"结构被预处理器翻译为:"argument",即转换为字符串。
"##"结构被预处理处理为把它两边的符号连接成一个符号。
#define paster(n) printf("token"#n"=%d\n", token##n)
int token9 = 10;
paster(9); // Result show in Screen: "token9=10"
6. 预处理移除指令:#undef symbol
7. 条件编译:
#if constant-expression
Statements
#elif constant-expression
Statements
#else
Statements
#endif
是否定义symbol:
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
在"#define symbol"中虽然symbol的值是一个空字符而不是1,但是symbol被定义。
8. #error指令用于生成错误信息。
#error text of error message
#line指令通知预处理器number是下一行输入的行号,如有string,则会把string作为当前的文件名。(修改了__LINE__和__FILE__)
#line number "string"
#progma指令用于支持因编译器而异的特性。
9. c与处理器要做的事情:删除注释,插入#include包含的内容文件的内容,定义和#define指令定义的符号以及确定代码的部分内容是否应该根据一些条件编译指令进行编译
10. #define的基本用法:
#define name stuff
使用#define指令,可以把任何文本替换到程序中;(如果定义中的stuff非常长,可以分成几行,除了最后一行外,每行的末尾都要加一个反斜杠,,并且不要加上;号)。
11.#define定义符号和宏的三个步骤:
(1)在调用宏时,首先对参数进行检查,看看是否包含了任何由#define定义的符号,如果是,它们首先替换;
(2)替换文本随后被插入到程序中原来文本的位置,对于宏,参数名被它们的值所替代;
(3)最后,再次对结果文本进行扫描,看看它是否包含了任何由#define定义的符号,如果是就重复上述处理过程。
在这个过程中,应注意以下问题:
(1)宏参数和#define定义的可以包含其他#define定义的符号,但是宏不可以出现递归;
(2)当预处理器搜索#define定义的符号时,字符串常量的内容并不进行检查。
12.宏参数插入到字符常量中的两个技巧:
(1)只有当字符串常量作为宏参数给出时才能使用
#define PRINT(FORMAT, VALUE) \
printf("The value is "FORMAT"\n", VALUE)
PRINT("%d", x + 3);
(2)使用预处理把一个宏参数转化为一个字符串(#argument被预处理器翻译为“argument”) 将输出:The value of x + 3 is 25
#define PRINT(FORMAT, VALUE) \
printf("The value is "FORMAT"\n", VALUE)
PRINT("%d", x + 3);
13.##结构作用是将位于它两边的符号连接成一个符号(允许宏定义从分离的文本片段创建标识符)
#define ADD_TO_SUM(sum_number, value) \
sum ## sum_number += value
ADD_TO_SUM(5, 25);
产生的结果是将25的加到sum5中(这种连接必须产生一个合法的标识符,否则结果是未定义的)。
14.宏经常应用于执行简单的计算,至于为何不用函数来完成一些简单的运算,有以下两个原因:
(1)用于调用和从函数返回的代码有可能比实际执行这个小型计算工作的代码更大,因此使用宏比使用函数在程序的规模和速度方面都更胜一筹;
(2)函数的参数必须声明为一个特定的类型,但是宏是与类型无关的;
但是宏也有其不利之处:使用宏会增加程序的长度,并且使用具有副作用的参数可能在宏的使用过程中产生不可预料的结果。
15.宏和函数的不同之处:
16.条件编译可以用于调试程序和在编译时选择不同的代码部分。
17.编译器支持两种不同类型的文件包含:函数库文件和本地文件:
#include <filename>
#include "filename"
前者编译器是在编译器定义的“一系列标准位置”查找函数库头文件,后者编译器是源文件所在的当前目录下进行查找。
18.可以使用条件编译的方法来解决多重包含的问题
#ifndef _HEADERNAME_H
#define _HEADERNAME_H 1
#endif
用上述的方法就可以很好的消除多重包含的危险。
第十五章 输入/输出函数
0. stdio.h:Refer File
typedef struct {
short level; /* fill/empty level of buffer */
unsigned flags; /* File status flags */
char fd; /* File descriptor */
unsigned char hold; /* Ungetc char if no buffer */
short bsize; /* Buffer size */
unsigned char *buffer; /* Data transfer buffer */
unsigned char *curp; /* Current active pointer */
unsigned istemp; /* Temporary file indicator */
short token; /* Used for validity checking */
} FILE;
#define feof(f) ((f)->flags & _F_EOF)
1. void perror(char const *message); // 报告错误
2. void exit(int status); // 中止执行,status返回给操作系统
3. Fully Buffered:完全缓冲,读取和写入的动作在一块被称为缓冲区(buffer)的内存block进行,当buffer 写满的时候才会执行刷新操作(flush)。 这就导致我们写入buffer的数据,并不会立马写入(显示或存储);但一次性把写满的buffer写入比逐片把程序的输出写入效率要高。同理,buffer为空时通过从设备或文件读取一块较大的输入,重新填充buffer。在使用printf函数进行debug的时候,最好在调用printf之后强制刷新(fflush)缓冲区。
printf ("something is wrong");
fflush (stdout);
4. 流分为两种:Text stream 和 Binary stream。
5. FILE数据结构用于访问流,每个流都应有相应的FILE与其关联。
Three stream:standard input(stdin),standard output(stdout),standard error(stderr); 它们都是指向FILE结构实例的指针。
6. EOF:提示到达文件尾,它的实际值比一个字符要多几位,这是为了避免二进制值被错误的解释为EOF(EOF不在0~255之内),使用feof(FILE *stream)宏判断是否到了文件尾。
7. 文件流I/O操作步骤:
A. 声明一个FILE指针;
B. 调用fopen函数打开流,返回FILE结构的指针(为了打开一个流,必须指定需要访问的文件或设备,以及访问方式,如读、写、读写…);
C. 根据需要对流进行操作;
D. 调用fclose函数关闭流;
8. 标准流的I/O不需要打开或关闭,即所谓的I/O函数,可直接使用。(stdin,stdout)
9. FILE *fopen(char const *name, char const *mode); //失败返回NULL,应该始终检查fopen函数返回的指针是否为NULL。
FILE *freopen(char const *name, char const *mode, FILE *stream); // 函数首先试图关闭这个流,然后用指定的文件和模式重新打开这个流;失败返回NULL,成功返回他的第三个参数值。
10. int fclose(FILE *f); // 对于输入流,fclose函数在文件关闭之前刷新buffer;如果执行成功,fclose返回0,否则返回EOF。
11. 字符I/O:
int fgetc(FILE *stream); //不存在字符,返回EOF,是函数。
int getc(FILE *stream); //不存在字符,返回EOF,是宏。
int getchar(void); // stdin:标准输入流。
int fputc(int character, FILE *stream); //是函数。
int putc(int character, FILE *stream); //是宏。
int putchar(int character ); // stdout:标准输出流。
int ungetc(int character, File *stream); // 把一个先前读入的字符返回到流中。
如果一个流允许退回多个字符,那么这些字符再次被读取的顺序是退回时的反序。
12. 未格式化的行I/O:
char *fgets(char *buffer, int buffer_size, FILE *stream); //到达文件尾部返回NULL,否则返回他的第一个参数,这个返回值通常用来检查是否到了文件尾。
char *gets(char *buffer);
int fputs(char const *buffer, FILE *stream); //写入错误返回EOF,否则返回一个非负值
int puts(char const *buffer);
fgets无法将字符串读入到一个长度小于两个字符的缓冲区,因为其中一个字符需要为NUL字节保留。
Buffer的长度由常量MAX_LINE_LENGTH决定,也就是读取一行文本的最大长度。
13. 格式化的行I/O:
int fscanf( FILE *stream, char const *format, … ); // scan form file
int scanf( char const *format, … );
int sscanf( char const *string, char const *format, … ); // scan from string
上述格式化输入:返回值为被转换的输入值数目。
int fprintf( FILE *stream, char const *format, … );
int printf( char const *format, … );
int sprintf( char *buffer, char const *format, … );
上述格式化输出:返回值是实际打印或存储的字符数。
14. 二进制I/O:二进制避免了在数值转换为字符串的过程中所涉及的开销和精度损失。
size_t fread(void *buffer, size_t size, size_t count, FILE *stream);
size_t fwrite(void *buffer, size_t size, size_t count, FILE *stream);
buffer为缓冲区,被解释为一个数组,size是缓冲区每个字符的字节数,count指定传输缓冲区中多少值,返回值是实际读取或写入的元素数目。
15. int fflush(FILE *stream); // 迫使一个输出流缓冲区内的数据进行物理写入,无论它满或没满。
long ftell(FILE *stream); // 返回流的当前位置,ftell的值总可以用于fseek
int fseek(FILE *stream, long offset, int from); // 用于在流中定位,from:SEEK_SET, SEEK_CUR, SEEK_END,如果form是SEEK_SET,offset必须是同一个流中以前调用ftell返回的值。
void rewind(FILE *stream); // 将读/写指针设置回指定流的的起始位置,同时清除流的错误提示标志
int fgetpos(FILE *stream, fpos_t *position); // 功能类似ftell
int fsetpos(FILE *stream, fpos_t const *postion); // 功能类似fseek
16. 改变缓冲方式:
void setbuf(FILE *stream, char *buf); // 设置另一个数组对流进行缓冲,长度必须为BUFSIZ(stdio.h); 如果参数为NULL,将关闭流的所有缓冲方式。
int setvbuf(FILE *stream, char *buf, int mode, size_t size); // mode:指定缓冲的类型,IOFBF指定一个完全缓冲的流,IONBF指定一个不缓冲的流,IOLBF指定一个行缓冲流(每当有换行符写入到缓冲区,缓冲区进行刷新);size为buffer的长度。
如果需要一个很大的缓冲区,它的长度应该是BUFSIZ的整数倍。
17. 流错误函数:
int feof(FILE *stream); // 处于尾部返回为真,这个状态可通过fseek、rewind、fsetpos清除。
int ferror(FILE *stream); // 报告流的错误状态,如出现任何读/写错误返回真
void clearerr(FILE *stream); // 清除流的错误标志
18. Temp File:
FILE *tmpfile(void); // 文件被关闭或程序终止时临时文件被自动删除;创建的文件以wb+模式打开,可用于二进制和文本数据。
char *tmpnam(char *name); // 创建临时文件的名字,调用次数不超过TMP_MAX时,每次都产生一个新的不同名字。参数为NULL是,返回一个指向静态数组的指针,数组包含了被创建的文件名。
19. 文件操作函数:执行成功返回0,失败返回非零值
int remove(char const *filename); // 文件被打开时调用remove,其结果取决于编译器。
int rename(char const *oldname, char const *newname);
20. 流错误函数,临时文件函数tmpfile,文件操纵函数。
21. 良好的编程实践要求任何可能产生错误的操作,都应该在执行之后进行检查,确定它是否成功。
22. 注意只有当库函数失败时,errno才能被设置,当函数成功运行时,errno的值不会被修改。
23. exit 函数中的参数和main中的参数状态是一致的,用于提示程序是否正常完成,这个函数没有返回值,当exit结束时,程序已经消失,所以他无返回值而言。
24. 标准i/o函数库还引用了缓存i/o的概念,提高了绝大多数程序的效率。
25. 这个函数库存在两个缺点:1。它在某种特定的类型的机器上实现的,并没有对其他不同特性的机器多作考虑。2.设计这发现上述问题后,试图去修正,但是只要他们这么作了这个函数库就不标准了,程序的可移植性就会降低。
26. 使用标准 输入输出时,这种缓存坑引起混淆,只有当他们与交互设备并无联系时,才会进行完全缓存。
27. 事实上,如果程序失败,缓存奴输出可能不被写入,这就可能使得关于程序出现错误的位置不正确,这个的解决方法是在用于调适的printf后面加上fflush, fflush迫使缓存区的内容立即写入,不管他立即已满。
28. 标准错误就是错误信息写入的地方。
29. 打开流和关闭流,对关闭流是否进行检验的标准是:问两个问题,操作成功应该执行什么 ,操作失败应该执行什么;如果答案一样的话,可以不进行检验否则进行检验。
30. fget fput是真正的函数,但是getc putc getchar putchar 都是定义的宏。
31. 二进制数据避免了在数值转换为字符串过程中所涉及到的开销和精度损失,但是这些机巧只能将数据被另外一个数据顺序读取时才能使用。
32. fflush迫使一个输出流的缓存区内的数据进行物理写入,不管他是否已满。
33. 随机访问是通过读取和写入先前定位到文件中需要的位置来实现。
第十六章 标准库函数
1. stdlib.h:
int abs( int value );
long int labs( long int value );
div_t div( int numerator, int denominator );
ldiv_t ldiv( long int numer, long int denom );
int rand( void );
void srand( unsigned int seed );
void srand( (unsigned int) time(0) ); // 每天的时间做为随机数产生器的种子
int atoi( char const *string );
long int atol( char const *string);
long int strtol( char const *string, char **unused, int base ); // base为将要用的进制
unsigned long int strtoul( char const *string, char **unused, int base );
2. math.h:
double exp(double x); // e值的x次幂
double log(double x); // 返回以e为底x的对数
double log10(double x);
double frexp( double value, int *exponent ); // 计算一个指数(exponent)和小数(fraction)
double ldexp( double fraction, int exponent );
double modf( double value, double *ipart ); // 把一个浮点值分成整数和小数两个部分
double pow(double x, double y); // 返回x的y次方
double sqrt(double x); // 返回x的平方根
double floor(double x); // 返回不大于参数的最大整数
double ceil(double x); // 返回不小于参数的最小整数
double fabs(double x); // 返回参数的绝对值
double fmod(double x, double y); // 返回x/y所产生的余数,商必须为整数
3. clock_t clock(void); // 返回处理器时钟滴答的次数,除以CLOCKS_PER_SEC转换为秒数
time_t time(time_t *returned_value); // 参数为NULL时返回当前时间,不为NULL参数存储当前时间
char *ctime(time_t const *time_value); // 格式化为字符串
double difftime(time_t t1, time_t t2); // 返回t1-t2的时间差,并转换为秒
sturct tm *gmtime(time_t const *time_value); // convert to Greenwich Mean time
struct tm *localtime(time_t const *time_value); // convert to Local time
time_t mktime(struct tm *tm_ptr); // convert to time_t structure
char *asctime(sturct tm *tm); // covert to string format
sturct tm成员:tm_sec,tm_min,tm_hour,tm_mday,tm_mon,tm_year,tm_wday,tm_yday,tm_isdat。
strftime函数把一个tm结构转换为一个根据某个格式字符串而定的字符串。
4. setjmp的第一次调用确立一个执行点,如果调用longjmp,程序的执行流会在该地点恢复执行。(不能返回到一个已经不再处于活动状态的函数)
jmp_buf restart;
value = setjmp(restart);
longjmp(restart, 1);
5. 信号处理函数:signal handler,用于信号发生时程序调用这个函数进行处理。异步信号的处理函数中调用exit或abort函数是不安全的。
6. 终止执行:stdlib.h
void abort( void ); // 不正常的终止一个正在执行的程序
void atexit( void (func)( void )); // 把一个函数注册为退出函数;atexit函数中不要再调用exit函数,可能会导致无限循环。
void exit( status ); // exit函数被调用时,所有被atexit注册的退出函数将按照注册顺序被反序依次调用。
7. 断言:assert.h
void assert( int expression ); //判断表达式的真假。为假:向标准错误打印一条诊断信息并终止程序。
程序在测试完成之后,可以在编译事通过定义NDEBUG消除所有的断言;使用-DNDEBUG编译器命令行选项,或这在源文件的头文件中assert.h被包含之前增加下面的定义:#define NDEBUG,当NDEBUG被定义之后,预处理器将丢弃所有的断言。
8. 环境:stdlib.h
char *getenv( char const *name ); //获取环境变量
9. 执行系统命令:stdlib.h
void system( char const *command ); // system可以使用NULL参数,用于询问命令处理器是否实际存在。
10. 排序与查找:stdlib.h
void qsort(void *base, size_t n_elements, size_t el_size, int (*fcmp)(const void *, const void * ));
void *bsearch(const void *key, const void *base, size_t nelem, size_t width, int (*fcmp)( const void *, const void * ));
11. Locale.h:一组特定的参数,每个国家可能各不相同。
char *setlocale( int category, char const *locale );
整型函数库:算术<stdlib.h>取绝对值,除法运算(对整型的运算包含商和余数,返回一个结构),随机数的<stdlib.h>其中有个小技巧:使用每一天的时间作为随机数产生的种子 ;字符串转换<stdlib.h>将字符串转换为数值。
浮点型函数库,<math.h>包含了剩余数学函数的声明,这些函数的绝大多数返回值都是double型,注意区别定义域错误和范围错误;包含三角函数,双曲函数,对数和指数函数,浮点形式,幂函数,底数,顶数,绝对值和余数<math.h>转换为double型的字符串转换函数,(书上标记的是在<stdlib.h>中,本人认为是笔误应该在<math.h>中)。
日期和时间函数:<time.h>处理器时间,当天时间其中有一个difftime函数用来计算两个时间的差值。
第十七章 经典抽象数据类型
1. 内存存储方案:静态数组,动态分配的数组,动态分配的链式结构。
2. 堆栈:后进先出(LIFO)
先检测,后压栈;压栈:top标志先加1,后赋值。
先检测,后出栈;出栈:先清值,top标志后减1。(top标志减1即可,不必删除元素)
3. 队列:先进先出(FIFO),rear进front出。
循环数组(circular array):数组的头尾相连,可用来做队列的储存,组成环形队列。
环形队列为空时:front – rear = 1,有一个元素时,front=rear。
重新定义队列满:数组中的一个元素始终保留不用,这个元素始终在队列空间未使用部分的rear和front之间;则当(rear+1)%QUEUR_SIZE == front为真时,队列为空;当(rear+2)%QUEUR_SIZE == front为真时,队列为满;
4. 二叉搜索树(binary search tree):每个节点最多具有两个孩子,节点值大于左孩子,小于右孩子。
前序遍历(pre-order):节点→左子树→右子树
中序遍历(in-order):左子树→节点→右子树
后序遍历(post-order):左子树→右子树→节点
层次遍历(breadth-first):顶层→第二层(左右)→......→最底层(左右)
5. 数组表示二叉搜索树
规则:根节点从1开始
A. 节点的双亲节点是N/2
B. 节点N的左孩子是2N
C. 节点N的右孩子是2N+1
规则:根节点从0开始:
A. 节点的双亲节点是(N+1)/2 - 1
B. 节点N的左孩子是2N+1
C. 节点N的右孩子是2N+2
链式二叉树比数组更适合做树的存储。
6. #define可以近似的模拟泛型机制。泛型是OOP处理的比较完美的问题之一。
7. 使用断言检查内存是否分配成功是危险的。
第十八章 运行时环境
1. 虚拟内存:由操作系统实现,在需要时把程序的活动部分放入内存并把不活动的部分复制到硬盘中,这样就可以允许系统运行大型的程序。
2. 是链接器而不是编译器决定外部标识符的最大长度
3. 不能链接由不同编译器产生的程序。
4. 优化程序:优化算法比优化代码更有效果。
终于整理完了,又花了一下午时间……
这里还有一篇
C语言中的高级声明--《c和指针》摘要
参考:
①yanghaoran321:http://my.csdn.net/yanghaoran321
②arkhe:http://www.cnblogs.com/arkhe/articles/2615965.html
③zhghost:http://blog.csdn.net/zhghost/article/details/5287865