读了一遍著名的《the C programming language》,果真如据说的同样,讲解基础透彻,案例简单典型,确实本身C语言还有不少细节点不是很清楚。程序员
总结一下阅读的收获(部分原书不清晰的知识点在网络上搜索后补充,引用出处忘记了,原做者看到可联系添加)数组
1.声明缓存
1.1 变量声明网络
在C语言中,全部变量都必须先说明后使用,说明一般放在函数开始处的可执行语句以前。数据结构
1.2 外部变量函数
在每个函数中都要对所要访问的外部变量进行声明。声明所使用的外部变量的类型,在声明时能够用extern显式说明,也能够经过上下文隐式说明。若是外部变量的定义在源文件中出如今使用它的函数以前,则extern声明能够省略。布局
若是程序包含几个源文件,某个变量在file1中定义,在file2与file3中使用,那么file2和file3文件中就须要extern声明来链接该变量的出现。测试
变量extern声明和函数声明放在头文件中。优化
1.3 声明的风格编码
声明的旧风格:print();(省去的返回值int,简化编码)
声明的新风格:void print(void)
新风格的优势:编译器帮助更好地检查函数调用
如:
print(5.5);
在旧风格下,该次调用编译器不会报错(由于参数列表没有内容意味着不对参数列表进行检查)
在新风格下,编译器会报错
1.4 声明的重要性(编译器检查之反例)
在同一源文件中,函数的声明必须与其定义一致,不然编译错误。但若是函数是独立编译的,则这种不匹配就不会检测出来。由于C语言会对没有函数声明的函数自动进行 隐式声明,那么编译器不只不会对返回值进行检测,更不会对参数列表进行检测。
main.c
1 int main() 2 { 3 printf(“%f”,getNum(123)); 4 }
get_num.c
1 double get_num(void) 2 { 3 return 3.14; 4 }
编译不会有任何错误,务必记得使用函数声明
在缺省的状况下,外部变量与函数具备以下性质:全部经过名字对外部变量与函数的引用(即便这种引用来自独立编译的函数)都是引用的同一对象。
2. 内存布局
2.1 int的字节长度
在C语言中int的长度
1. long int型至少应该与int型同样长,而int型至少与short int型同样长;
2. C/C++规定int的字长与机器字长相同
3. 操做系统字长与机器字长未必一致
4. 编译器根据操做系统字长来定义int字长
由上面4点可知,在一些没有操做系统的嵌入式计算机系统上,int的字长与处理器字长相同;有操做系统时,操做系统字长与处理器字长不必定一致,此时编译器根据操做系统的字长来定义int字长。好比你在64位机器上运行DOS的16位系统,那么全部for dos的C/C++编译器中的int都是16位的;在64位机器上运行win32系统,那么全部for win32的C/C++编译器中的int都是32位的。
缘由:
操做系统决定了软件层面对于硬件的管理方式,那么对于64位的机器,若是管理方式仍然是16位的(如内存访问地址仅为216),那么该64位机器其实也只发挥了16位的做用。
对于不一样的平台,有其相应的指令集,也就有其对应的编译器。C语言在源代码层面具备可移植性就是这个缘由,只要在不一样的平台,使用不一样的编译器,即便最终获得的二进制机器码不一样,程序运行结果也必定是相同的。
所以,定义数据结构时(尤为是嵌入式)时,优秀的程序员不会以下定义(32位):
1 typedef struct tagTypeExample{ 2 3 unsigned short x; 4 5 unsigned int y; 6 7 }TypeExample;
他们这样定义:
1 #define unsigned short UINT16 2 3 #define unsigned int UINT32 4 5 typedef struct tagTypeExample{ 6 7 UINT x; 8 9 UINT y; 10 11 }TypeExample;
这样,换平台的话,只须要改变宏定义便可
2.2 float和double的范围和有效数字
根据IEEE754的标准,float和double的内存布局以下:
符号位S(1 bit) + 指数(8 bits) + 尾数(23 bits)
float长度32位
计算方式:
指数上为移码,偏移数为127
尾数上省去了最高位的1,仅做为小数点后的二进制表示
(-1)^S(1+尾数)*2^(指数-偏移数)
例:3.0 = 11 = (-1)^0 * (1.1) * 2^(128 -127)
故:符号位为0,指数为1298,尾数为:1+22个0
因此:3.0的float为:0 10000000 10000000000000000000000 = 0x40400000
验证:
1 #include <stdio.h> 2 3 union test{ 4 float a; 5 int b; 6 }; 7 int main() 8 { 9 union test t; 10 t.b = 0x40400000; 11 printf("%f\n",t.a); 12 }
结果输出为:3.000000
所以,指数-127~128,范围为:2^-127~2^128
尾数:隐藏的1永远不变,不会影响精度。2^23 = 8388608,一共七位,这意味着最多能有7位有效数字,但绝对能保证的为6位,也即float的精度为6~7位有效数字.
一样,对于内存布局为:
符号位S(1 bit) + 指数(11 bits) + 尾数(52 bits)
的double类型来讲,分析是相同的。
范围:-2^1023 ~ +2^1024
有效数字:2^52 = 4503599627370496,一共16位,同理,double的精度为15~16位。
3. 类型
3.1 char变量的可移植性
定义变量时,只是用关键字char,缺省状况下,根据编译器定义为signed或者unsigned,这样会致使不一样机器上char有不一样的取值范围。
若显式地将字符声明为signed或者unsigned,则可提升平台可移植性,但机器处理signed和unsigned的能力不一样,会致使效率受损。还有不一样处理字符的库函数的参数声明为char,显式声明会带来兼容性问题。
结论:保证可移植性的最佳方法仍是定义为char型同时只是用signed char和unsigned char的交集字符,在进行算术运算时,显式使用。
3.2 运算份量在运算前完成提高
若是某个算数运算符有一个浮点份量和一个整数运算份量,那么合格整数运算份量在开始运算以前会被转换为浮点类型
表达式先进行类型转换,再计算 z = ( n > 0) ? f : n
不管n是否为正,z的类型都是float
3.3 浮点常量
浮点常量携程带小数点。如:3.0
3.4 使用unsigned char类型来接受ASCII码的问题
1 int main() 2 { 3 unsigned char c; 4 while( (c=getchar()) != EOF){ 5 putchar(c); 6 } 7 return 0; 8 }
EOF宏定义的值为-1,而unsigned char 没法接受-1,因此永远没法到达文件结尾
3.5 常量表示
<limits.h>与<float.h>包含了全部这些类型的符号常量以及机器与编译程序的其余性质
long常量要以字母L或l结尾
无符号数以u或U结尾
后缀ul或UL用于表示unsigned long常量
浮点常量的表示方式:123.4或1e-2,无后缀为 double,后缀f或F为float,后缀l或L为long double
3.6 进制表示
十进制 31 八进制 037 十六进制 0x1F 二进制 0b00011111
3.7 位模式表示
使用位模式来指定字符对应的ASCII码
ASCII码纵向制表符
11 ‘\v’
= ‘\xb’ (‘\xhh’ hh为1至多个十六位进制数)
= ‘\013’(‘\ooo’为1至3个八进制数)
3.8 字符串表示
C语言对字符串长度无限制,但程序必须扫描完整的字符串(’\0’结束符)才能决定这个字符串的长度
strlen 返回字符串长度,不包括结束符
3.9 枚举类型
枚举常量
1.enum boolean { NO,YES };
枚举值从0开始递增 NO = 0 YES = 1
2.enum month { JAN = 1, FEB , MAR , APR };
JAN =1 FEB = 2 MAR = 3 APR = 4
3.enum escapes { BELL = ‘\a’ , BACKSPACE = ‘\b’ , TAB = ‘\t’ , NEWLINE = ‘\n’ };
显示指定枚举值
尽可能用const、enum、inline替换#define,宁肯以编译器替换预处理器(EFFECTIVE C++)
宏在预处理阶段进行替换工做,它替换代码段的文本,程序运行的时候,宏已经不存在了。而枚举是在程序运行以后起做用的,枚举常量存储在数据段的静态存储区中,宏占用代码段的空间,而枚举除了占用空间,还消耗CPU资源。
枚举类型值能自动生成,这是相对于#define的优点
3.10 自动变量
只在函数内部定义使用的变量。它只是容许在定义它的函数内部使用它,在函数外的其余任何地方都不能使用的变量。系统自动完成对自动变量存储空间的分配和回收,它的生命周期是从它们被定义到定义它们的函数返回。这个过程是经过一个堆栈机制来实现的,为自动变量分配内存就是压栈,返回就是退栈。
3.11 静态变量
不像自动变量使用堆栈机制使用内存,而是在静态存储区分配固定的内存。持续性是程序运行的整个周期。做用域为定义它的函数的内部。
经过extern访问其余文件中定义的全局变量,若是使用static在函数外面声明变量,则其余文件不容许使用该变量。const int a声明在函数外也只能在定义它的文件中使用。
3.12 寄存器变量
提升访问效率,具体是否使用寄存器由编译器决定,其地址不能被访问。
1 register int a; 2 int ra asm(“ebx”);
3.13 易失变量
强制访问操做,防止编译器在优化,告诉编译器从内存中取值,而不是从寄存器或缓存。
3.14 非自动变量
非自动变量包括:全局变量+静态变量
非自动变量只初始化一次,在程序开始以前进行,且初始化符为常量表达式,其缺省为0
自动变量进行其所在函数即初始化,其初始化符能够是任意表达式,未经初始化值为未定义
4.类型转换
4.1 int到char的转换
实质上仅仅是一个ASCII码表的映射关系的转换。
C语言内部内置了这种映射关系, 使用char类型管理字符,实际上是在管理ASCII码的值,最终输出时完成到字符的映射就好了。
将int赋值给char时,实质上作的是内存截断
例:
1 a = 0x62; 2 char c = a; 3 pritnf(“%c”,c);
结果为a
1 a = 0xFF62; 2 char c = a; 3 printf(“%c”,c);
结果同为a
ASCII码表 0~255(0~127标准ASCII码 128~255 扩展ASCII码)
4.2 char到int的转换
C语言未指定char类型是有符号还有无符号,因此把char类型的值转换为int类型的值时,视机器不一样而有所变化。
某些机器最左边为1,那么就被转换为负整数,而另外一些则提高,在最左边添加0
代码:
1 int main() 2 { 3 char a =0xFF; 4 int b = a; 5 printf(“%d”,b); 6 return 0; 7 }
本机测试结果为-1,被转换为了0xFFFFFFFF,被转换为了负整数
4.3 强制类型转换
强制类型转换的精肯定义:
表达式首先被赋给类型名指定类型的某个变量(会自动构造对应的内存布局),而后再将其用在整个构造所在的位置。
参数是经过函数原型声明,那么一般状况下,当函数被调用时,系统对参数自动进行强制类型转换,可是对于printf来讲,%f等格式控制符仅仅是决定对对应参数的解释方式,是不会进行强制类型转换的。
运算前,对于运算份量先把“低”的类型提成为“高”的类型
4.4 float的自动转换
注意:在表达式中的float类型的运算份量不自动转换成double类型,这与原来的定义不一样。通常而言,数学函数要用双精度。使用float类型的主要缘由是为了使用较大的数组时节省存储空间,有时也为了机器执行时间(双精度运算特别费时)
4.5 unsigned类型的自动转换
包含unsigned类型的运算份量时,转换规则要复杂一些。主要问题是,有符号值与无符号值之间的比较取决于机器由于它们取决于各个整数类型的大小。
若int为16位,long为32位
则-1L<1U 由于unsigned int会转化为signed long类型
-1L>1UL,由于-1L会被转化为unsigned long类型
5.运算
5.1 赋值运算
赋值运算的结合次序:从右到左
5.2 位运算
例:使用bitcount函数统计变量中值为1 的位的个数。
方法1:每一位进行匹配移位,直到x为0
1 int bitcount( unsigned int x) 2 { 3 int b; 4 for( b = 0; x != 0; x >>== 1) 5 if( x & 01) 6 b ++; 7 return b; 8 }
方法2:去除变量最右端的1直到变量大小为0
1 int bitcount( unsigned int x ) 2 { 3 for( b = 0; x != 0; b++ ) 4 x &= (x -1); 5 return b; 6 }
5.3 表达式先进行类型转换后进行运算
如计算:
z = ( n > 0) ? f : n;
不管n是否为正,z的类型都是float
5.4 函数调用中变量的求值次序
在函数调用中各个变量的求值次序也是未指定的
printf(“%d %d \n”, ++n, power(2,n));
错误,不一样编译程序会决定是否在power(2,n)以前对n执行++操做
故改写为:
++n; printf(“%d %d \n”, n , power(2,n));
5.5 加1、减一运算的反作用
a[i] = i++;
数组下标是旧值仍是新值,编译程序对之能够有不一样的解释,并视为不一样的解释,产生不一样的结果。
5.6三元运算符的使用
三元运算符的使用,能够有效节省代码长度,如:
for(int i =0; i <5; i++) pritnf(“%d %s”,i,(i != 4) ? " " : "\n";
6.语句
6.1 if , while , for的条件测试部分真的意思是”非0”
6.2 switch语句
switch语句中case情形的做用就像标号同样,在某个case情形的代码执行完后,就进入下一个case情形执行,除非显示控制转出。
6.3 for循环中可使用逗号运算符”,”,支持多个表达式
6.4 变量和函数能够一块儿声明
double sum,atof(char []);
7.其余
7.1(标准库函数)printf
%d 十进制 %o 八进制 %x 十六进制 %f = %lf
加h短整型 加l长整型
printf中的宽度、精度可由*号来控制
例如:
printf(“%.*s”,max,s);
%后跟-符号表述左对齐,
如:
int a = 1; printf(“%-4d”,a);//输出在最左,没法展现出4位字宽
定义如printf这样的带有变长参数表的函数时,参数表至少有一个参数
void minprintf( char *fmt,...);
7.2 定义与声明的区别
定义:变量创建或分配存储单元的位置
声明:指明变量性质的位置,不分配存储单元
7.3 变量的定义不是只能出如今函数开始的部分
7.4 数组初始化
数组初始化,没有被初始化的部分自动置0
字符数组的初始化
char pattern[] = “ould”; = char pattern[] = {’o’,’u’,’l’,’d’,’\0’};
7.5 宏定义
7.5.1 宏定义中的加1、减一
#define max(A,B) = ((A) > (B) ? (A) : (B))
该宏对于传入的i++情形,会加两次
7.5.2 宏定义中的字符串
参数名以#为前缀,那么它们将被由实际参数替换的参数扩展成带引号的字符串
#define dprint(expr) printf(#expr “ = %g \n”, expr);
dprint(x/y);
printf(“x/y”” = %g \n”, x/y);
输出结果为:x/y = ?
7.5.3 ##为宏扩展提供了一种链接实际参数的手段
#define paste(front,back) front ## back paste(name,1) ; //获得name1
7.5.4 #if语句中包含一个常量整数表达式(其中不得包含sizeof,强制类型转换运算符或枚举常量),在#if语句中可使用一个特殊的表达式defined(名字)。
#if !defined(EDR) #define HDR #endif #if .. .. #elif .. .. #else .. #endif
两个特殊的表达式
#ifdef = #if defined(***) #ifndef = #if !defined(***)
7.6 取地址运算符只能应用于内存对象,不能对表达式、常量或寄存器变量进行操做
7.7 const限定符
1. 用const修饰通常变量
const修饰的变量必须在声明的时候进行初始化,一旦一个变量被const修饰后,在程序中除初始化外对这个变量进行的赋值都是错误的。
2. const与指针搭配使用
指针常量,即指针自己的值不可改变
常量指针,即指针指向的变量的值是不能够改变的
const int *p 和int const *p1;const形容的指针指向的内容
int * const p2 = &b; const形容的是指针自己
const修饰的变量须要初始化
3. 做为函数参数
4. 节省空间,避免了没必要要的内存分配
const定义常量从汇编角度来看,只是给出了对应的内存地址,而不是像#define同样给出了当即数,因此,const定义的常量在程序运行过程当中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。
5. 编译器一般不为普通const常量分配存储空间,而是将他们保存在符号表中。这使得它成为一个编译期间的常量。没有了存储与读内存的操做,使得它的效率也很 高。
6. 阻止用户修改函数返回值
7. 结构体中const成员变量的初始化
struct A s = {10,2};
与结构体的初始化相同
8. const是只读变量
const int n= 5 int a[n];
错误。const是只读变量,而很是量。
9. const变量 & const限定的内容
typedef char * pStr; char string[4] = “abc”; const char *p1 = string; const pStr p2 = string; p1++;//正确 p2++;//错误
分析:
1)const使用的基本形式:const char m 限定m不变
2)替换1式中的m,const char *pm;限定*pm不可变。固然pm是可变的,所以p1++是正确的。
3)替换1式char const newType m;限定m不可变,问题中的pStr是一种新类型,所以问题中的p2不可变。p2++是错误的。
10. 字符串常量与字符数组
char *c = “Hello World”;字符常量
char c[] = “Hello World”;字符数组
字符串常量存在静态存储区(只读的,rodata段中)
字符数组放在动态存储区
因此字符串常量不能被修改 c[1] = ‘a’