the C programming language 阅读笔记1

时间:2020-12-11 23:52:46

读了一遍著名的《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 

 int main()
{
printf(“%f”,getNum());
}

  get_num.c

 double get_num(void)
{
return 3.14;
}

  编译不会有任何错误,务必记得使用函数声明

  在缺省的情况下,外部变量与函数具有如下性质:所有通过名字对外部变量与函数的引用(即使这种引用来自独立编译的函数)都是引用的同一对象。

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位):

 typedef struct tagTypeExample{

 unsigned short x;

 unsigned int y;

 }TypeExample;

他们这样定义:

 #define unsigned short UINT16

 #define unsigned int UINT32

 typedef struct tagTypeExample{

 UINT x;

 UINT y;

 }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

  验证:

 #include <stdio.h>

 union test{
float a;
int b;
};
int main()
{
union test t;
t.b = 0x40400000;
printf("%f\n",t.a);
}

  结果输出为: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码的问题

 int main()
{
unsigned char c;
while( (c=getchar()) != EOF){
putchar(c);
}
return ;
}

  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 寄存器变量

  提高访问效率,具体是否使用寄存器由编译器决定,其地址不能被访问。

 register int a;
int ra asm(“ebx”);

3.13 易失变量

  强制访问操作,防止编译器在优化,告诉编译器从内存中取值,而不是从寄存器或缓存。

3.14 非自动变量

  非自动变量包括:全局变量+静态变量

  非自动变量只初始化一次,在程序开始之前进行,且初始化符为常量表达式,其缺省为0

  自动变量进行其所在函数即初始化,其初始化符可以是任意表达式,未经初始化值为未定义

4.类型转换

4.1 int到char的转换

  实质上仅仅是一个ASCII码表的映射关系的转换。

  C语言内部内置了这种映射关系, 使用char类型管理字符,其实是在管理ASCII码的值,最终输出时完成到字符的映射就行了。

  将int赋值给char时,实质上做的是内存截断    

  例:

 a = 0x62;
char c = a;
pritnf(“%c”,c);

  结果为a 

 a = 0xFF62;
char c = a;
printf(“%c”,c);

  结果同为a

  ASCII码表 0~255(0~127标准ASCII码  128~255 扩展ASCII码)

4.2 char到int的转换

  C语言未指定char类型是有符号还有无符号,所以把char类型的值转换为int类型的值时,视机器不同而有所变化。

  某些机器最左边为1,那么就被转换为负整数,而另一些则提升,在最左边添加0

  代码:

 int main()
{
char a =0xFF;
int b = a;
printf(“%d”,b);
return ;
}

  本机测试结果为-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

 int bitcount( unsigned int x)
{
int b;
for( b = ; x != ; x >>== )
if( x & )
b ++;
return b;
}

  方法2:去除变量最右端的1直到变量大小为0

 int bitcount( unsigned int x )
{
for( b = ; x != ; b++ )
x &= (x -);
return b;
}

5.3 表达式先进行类型转换后进行运算

  如计算:

z = ( n > ) ? f : n;

  无论n是否为正,z的类型都是float

5.4 函数调用中变量的求值次序

  在函数调用中各个变量的求值次序也是未指定的

printf(“%d %d \n”, ++n, power(,n));

  错误,不同编译程序会决定是否在power(2,n)之前对n执行++操作

  故改写为:

++n;
printf(“%d %d \n”, n , power(,n));

5.5 加一、减一运算的副作用

a[i] = i++;

  数组下标是旧值还是新值,编译程序对之可以有不同的解释,并视为不同的解释,产生不同的结果。

5.6三元运算符的使用

  三元运算符的使用,可以有效节省代码长度,如: 

for(int i =; i <; i++)
pritnf(“%d %s”,i,(i != ) ? " " : "\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 = ;
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’,’\’};

7.5 宏定义

  7.5.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,) ; //得到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 = {,};

  与结构体的初始化相同

  8.  const是只读变量

const int n=
int a[n];

  错误。const是只读变量,而非常量。

  9.      const变量 & const限定的内容

typedef char * pStr;
char string[] = “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’