读书笔记 ——《C Primer Plus》(第5版)

本笔记是以c99作为标准,而不是最新的c11标准。

c语言关键字

ANSI C一共只有32个关键字,加上ISO推出的C99标准,新增了5个关键字,一共有37个关键字(C99标准)。

把这些关键字按照不同的类型分类,可以分成:数据类型关键字、控制语句关键字、存储类型关键字、函数说明关键字。

数据类型关键字(共15个)

关键字备注
char字符类型
short短整型
int整型
long长整型
float单精度浮点数(6 位有效数字)
double双精度浮点数(10位有效数字)
signed声明为有符号类型
unsigned声明为无符号类型
struct结构体类型
union联合类型
enum枚举类型
void空类型
_Bool布尔类型(c99新增,需要包含stdbool.h)
_Complex复数类型(c99新增)
_Imaginary虚数类型(c99新增)

控制语句关键字(共12个)

关键字备注
forfor循环
dodo..while循环结构
whilewhile循环
break跳出循环
continue结束当前循环,继续下一个循环
ifif条件语句
else条件语句否定分支
goto无条件跳转
switch开关语句
case开关语句分支
default开关语句的默认分支
return返回语句

存储类型关键字(共9个)

关键字备注
auto声明自动变量
extern重新声明已经在其他地方定义过的变量
register声明寄存器变量
static静态类型
sizeof计算数据类型或变量长度(即所占字节数)
typedef数据类型别名
const声明只读变量
volatile说明变量在程序执行中可被隐含地改变
restrict仅用于指针,表明指针是访问一个数据对象唯一的方式

函数说明关键字(共1个)

关键字备注
inline内联函数声明

ps:前两章节,都是介绍c语言,所以,忽略掉这两章的内容,该笔记重点记录从第三章开始的内容。

第三章:数据和C

  • C仅保证short类型不会比int类型长,并且long类型不会比int类型短。这样做是为了适应不同的机器。
    个人理解:C并没有规定int类型必须是4字节,具体每个类型占用多少字节,需要根据不同的平台定义。但是C保证int至少有16位,short至少是16位,long至少为32位,long long至少是64位。特别的,char类型肯定为1个字节,因为C把char类型的长度定义为1字节。

  • _Bool类型由C99引入,用于表示布尔值,即逻辑值true(真)与false(假)。因为C用值1表示true,用值0表示false,所以_Bool类型实际上也是一种整数类型。只是原则上它仅仅需要1位来进行存储。因为对于0和1而言,1位的存储空间已经够用了。
    提醒:如果需要用到bool变量,需要包含stdbool.h头文件

  • 如果想把一个比较小的值作为long对待,可以使用L作为后缀。例如:long a = 123L,与之类似,使用LL后缀表示long long,使用ULL表示unsinged long long。

  • 打印占位符:

    • %d打印有符号整型
    • %o打印八进制
    • %x打印16进制
    • %u打印无符号整型
    • %ld打印长整型(long int)
    • %lld打印long long
    • %c打印字符
    • %s打印字符串
    • %f打印float和double(%e表示以指数形式打印)
    • %lf打印long float。
  • 可移植的类型,需要包含头文件inttypes.h,可以明确知道一个int是否是16位还是32位,例如:int16_t,uint32_t。

  • 默认情况下,编译器会把浮点常量当做double类型对待。C可以使用F后缀告诉编译器把一个浮点常量做为float看待,例如:1.23F,如果想做为long double看待,可以使用后缀L,例如:5.14L。
    提醒:这里的L和前面的long类型的L是有区别的。一个是long int,一个是long double。

  • 浮点数需要注意上溢和下溢的情况,另外浮点数会存在一个特殊的值NaN(Not a Number),例如:90度的正切值(tan90°)就是不存在的。

  • 注意,使用%d显示float值不会把该float值转换为近似的int值,而是显示垃圾值。与之类似,使用%f显示int值也不会把该int值转换为浮点值。而且,参数的数目不足和类型不匹配所造成的结果也将随平台不同而不同。例如:int a=100; printf("a=%f\n",a);此时可能输出a=0.000000。

  • C有多种数据类型。基本的数据类型包含两大类:整数类型和浮点类型。整数类型的两个重要特征是其类型的大小以及它是有符号还是无符号的。最小的整数类型是char,因实现不同可以是有符号或无符号的(使用singed 或 unsigned)。

第四章:字符串和格式化输入/输出

  • scanf()开始读取输入以后,会在遇到的第一个空白字符空格(blank)、制表符(tab)或者换行符(newline)处停止读取。一般情况下,使用%s的scanf()只会把一个单词而不是把整个语句作为字符串读入。C使用其他读取输入函数(例如gets())来处理一般的字符串。

  • C头文件limits.h和float.h分别提供有关整数类型和浮点类型的大小限制的详细信息。例如:#define INT_MAX 2147483647

  • 不能在引号括起来的字符串中间断行,但是C的新方法:如果在一个用双引号引起来的字符串后面跟有另一个用双引号引起来的字符串,而且二者之间仅用空白字符分隔,那么C会把该组合当作一个字符串来处理。例如:

      printf("this is test
      text.");//错误
      printf("this is test"
      "text.")//正确
    
  • 使用scanf()的两点规则:1、如果使用scanf()读取某种基本变量类型的值,请在变量名之前加上一个&;2、如果使用scanf()把一个字符串读进一个字符数组中,请不要使用& 。

      int a;
      char b[32];
      scanf("%d %s",&a,b);
    

第五章:运算符、表达式和语句

  • C大约有40个运算符,包括赋值运算符=,算术运算符+、-、*、/(C没有指数运算符,但是提供了pow()函数,例如:pow(3,5))、关系运算符(>,<,>=,<=,==,!=)、逻辑运算符(非!,与&&,或||)以及其他运算符,sizeof()(计算右操作数的字节大小),(type)指派运算符(用于类型转换,例如:a = (int)3.14;),逗号运算符等;

  • C99要求使用“趋零截尾”,例如:-3.8转换成-3

  • C规定sizeof返回size_t类型的值。这是一个无符号整数类型,但它不是一个新类型。相反,与可移植类型(如int32_t等)相同,它是根据标准类型定义的。

  • 取模运算符%用于整数运算。该运算符计算出用它右边的整数去除它左边的整数得到的余数。例如,13%5(读作“对13除以5取模”)所得值为3,因为13除以5得2并余3。不要对浮点数使用该运算符,那将是无效的,另外,对负数取模,结果都是负数

  • 要一次使用太多增量运算符,因为多个增量的顺序无法保证(少一点奇技淫巧)。

  • 关于类型转换:类型级别从高到低的顺序是long doubledoublefloatunsigned long longlong longunsigned longlongunsigned intint。一个可能的例外是当long和int具有相同大小时,此时unsigned int比long的级别更高。之所以short和char类型没有出现在此清单里,是因为它们已经被提升到int或unsigned int。

  • 应该避免自动类型转换,尤其是避免降级。当需要准确的类型转换,可以使用指派运算符(type)。

  • 如果函数不接受参数,函数头里的圆括号将包含关键字void,例如:int testFunc(void);

  • 函数中的变量名字是局部的。这意味着在一个函数里定义的名字不会与其他地方相同的名字发生冲突。
    个人理解:函数的形参(形式参数)是局部变量,是函数所私有的,如同在函数内部定义的局部变量一样。

  • i++++i区别:

    1. 单独一行使用时(没有左操作数),都是表示 i=i+1;
    2. 有左操作数时,a=i++,相当于 a=i;i=i+1;;而a=++i;,相当于i=i+1;a=i;
类别运算符结合性
后缀() [] -> . ++ --从左到右
一元+ - ! ~ ++ -- (type)* & sizeof从右到左
乘除* / %从左到右
加减+ -从左到右
移位<< >>从左到右
关系< <= > >=从左到右
相等== !=从左到右
位与 AND& 从左到右
位异或 XOR^从左到右
位或 OR|从左到右
逻辑与 AND&&从左到右
逻辑或 OR||从左到右
条件?:从右到左
赋值= += -= *= /= %= >>= <<= &= ^= |=从右到左
逗号,从左到右

第六章:C控制语句:循环

  • 一般地,所有的非零值都被认为是真,只有0被认为是假。C对真的范围放得非常宽!

  • 逗号运算符把两个表达式链接为一个表达式,并保证最左边的表达式最先计算。它通常被用在for循环的控制表达式中以包含多个信息。整个表达式的值是右边表达式的值。

  • for循环中,如果中间的那个控制表达式为空会被认为是真,即这个for循环则会永远执行(死循环);

第七章:C控制语句:分支和跳转

  • C有一系列标准的函数可以用来分析字符。ctype.h头文件包含了这些函数的原型。这些函数接受一个字符作为参数,如果该字符属于某特定的种类则返回非零值(真),否则返回零(假)。例如,如果isalpha()函数的参数是个字母,则它返回一个非零值。

  • C99标准为逻辑运算符增加了可供选择的拼写法。它们在iso646.h头文件里定义。如果包含了这个头文件,就可以用and代替&&,用or代替||,用not代替!

  • switch 标签页范围从3-999

第八章:字符的输入和输出、输入确认

  • 缓冲分为两类:完全缓冲(fully buffered)I/O行缓冲(line-buffered)I/O。对完全缓冲输入来说,缓冲区满时被清空(内容被发送至其目的地)。这种类型的缓冲通常出现在文件输入中。缓冲区的大小取决于系统,但512字节和4096字节是常见的值。对行缓冲I/O来说,遇到一个换行字符时将被清空缓冲区。键盘输入是标准的行缓冲,因此按下回车键将清空缓冲区。

  • 不必定义EOF,因为stdio.h负责定义它。#define EOF (-1)

  • 重定向是一个命令行概念,要通过在命令行键入特殊符号来指示它,一般为>输出重定向,<输入重定向。

第九章:函数

  • ANSI C形式要求在每个变量前声明其类型。也就是说,不能像通常的变量声明那样使用变量列表来声明同一类型的变量。

      void test(int a,b,c);//错误
      void test(int a, int b, int c);//正确
    
  • 当函数的实际返回值类型与函数原型声明的返回值类型不同时,实际返回值是把指定的要返回的值赋给一个具有所声明的返回类型的变量时得到的数值。例如:

      int test(){
          double a=1.2;
          return a; // 相当于把a的值赋值给一个int变量,然后返回这个int变量
      }
    
  • return语句的另一作用是终止执行函数,并把控制返回给调用函数的下一个语句。即使return语句不是函数的最后一个语句,其执行结果也是如此。

  • 不要把函数声明和函数定义混淆。函数声明只是将函数类型告诉编译器,而函数定义部分则是函数的实际实现代码。

  • 如果实参类型与形参类型不匹配,但都是数值类型,编译器会把实际参数值转换成和形式参数类型相同的数值。

  • C编译器会假设没有用函数原型声明函数,它就不会进行参数检查。因此,为了表示一个函数确实不使用参数,需要在圆括号内加入void关键字。
    个人理解:为了让编译器为你检查函数参数,最好先申明一个函数原型。

  • 有一种方法可以不使用函数原型却保留函数原型的优点:可以在首次调用某函数之前对该函数进行完整的定义。

  • 递归一般可以代替循环语句使用,两种方式各有优缺点,递归执行效率没有循环语句高
    提醒:递归效率比循环低,是因为每次递归调用都拥有自己的变量集合,所以就需要占用较多的内存;每次递归调用需要把新的变量集合存储在堆栈中。其次,由于进行每次函数调用需要花费一定的时间,所以递归的执行速度较慢。

  • 一个程序中的每个C函数和其他函数之间是平等关系。每一个函数都可以调用其他任何函数或被其他任何函数调用。

  • ANSI C中的%p格式对地址进行输出。例如:printf("ptr = %p\n",p); //p必须是一个指针变量

  • 指针的值是一个地址,在大多数系统内部,它由一个无符号整数表示。但是,这并不表示可以把指针看作是整数类型。一些处理整数的方法不能用来处理指针,反之亦然。例如:可以进行两整数相乘,而指针则不能。因此指针的确是一种新的数据类型,而不是一种整数类型。

第十章:数组和指针(重点)

  • 只读数组(例如:const char str[10]),程序从数组中读取数值,但是程序不向数组中写数据。

  • 当数值数目少于数组元素数目时,多余的数组元素被初始化为0。也就是说,如果不初始化数组,数组元素和未初始化的普通变量一样,其中存储的是无用的数值;但是如果部分初始化数组,未初始化的元素则被设置为0。

  • 声明数组的两种方式:1、直接指定数组的大小,例如:int a[3] = {1,2,3};2、省略括号中的数字,从而让编译器会根据列表中的数值数目来确定数组大小,例如:int a[]={1,2,3}
    注意: 第二种方式需要在声明的同时并初始化数组。

  • C99规定,在初始化列表中使用带有方括号的元素下标可以指定某个特定的元素,例如:int arr[10]={[7]=100};,而未经初始化的元素都被设置为0

  • C不支持把数组作为一个整体来进行赋值,也不支持用花括号括起来的列表形式进行赋值(初始化的时候除外)。

      int a[10];
      int b[10];
      b=a;//错误
      a={1,2,3};//错误
    
  • 编译器不检查索引的合法性。在标准C中,如果使用了错误的索引,程序执行结果是不可知的。也就是,程序也许能够运行,但是运行结果可能很奇怪,也可能会异常中断程序的执行。
    注意:不检查数组边界。

1、变长数组

  • C99引入变长数组,即:int a=10; int arr[a];,但是,变长数组声明时,不能进行初始化。
  • 变长数组必须是自动存储类的,这意味着它们必须在函数内部或作为函数参量声明,而且声明时不可以进行初始化。
  • 变长数组中的“变”并不表示在创建数组后,您可以修改其大小。变长数组的大小在创建后就是保持不变的。“变”的意思是说其维大小可以用变量来指定
  • 声明一个含有二维变长数组的函数,int sum(int rows,int cols,int arr[rows][cols]);,但是在参量列表中,代表数组大小的变量声明需要早于变长数组。
  • 可以省略函数原型中的参数名称,但是需要用星号来省略维数,例如:int sum(int ,int ,int arr[*][*]);

2、数组与指针的关系

关于指针和数组的理解:

我们可以想象这样一个场景,就是我们学生时代的班级,假设一个班有35个同学,有5个小组,每个小组有7个同学。一个一维数组(int group[7])就像是班级里面的一个组,我们在每个组的第一个座位前钉一个牌子,比如:一组二组等,牌子上写的名字就是数组名group,它也就等同于指向这个组的指针,即一个组第一个同学的座位。而二维数组(int class[5][7])就可以想象成整个班级,班级名(比如三年二班)就相当于二维数组的数组名class,它也等同于指向班级第一组第一个同学的座位。

关于const数组/指针的理解,const int group[7]表示这个组的同学都已经固定好了,不能修改。const int *const pGroup表示不仅不能修改这一组的同学,连每个组牌子上的名字也不能修改。

下图是一个二维数组的内存结构:

二维数组

此时,指针和数组名存在以下关系:

int zippo[4][2] = { {1,2},{3,4},{5,6},{7,8} };
int (*pz)[2] = zippo;//pz指向一个包含2个int值的数组
  • 数组与指针的关系:数组名是该数组首元素的地址。对于数组而言,地址会增加到下一个元素的地址,而不是下一个字节。同时,可以用指针标识数组的每个元素,并得到每个元素的数值。从本质上说,对同一个对象有两种不同的符号表示方法。
    例子:定义ar[n]时,意思是*(ar+n),即“寻址到内存中的ar,然后移动n个单位,再取出数值”。

3、指针数组和数组指针

  • 指针数组:array of pointers,即用于存储指针的数组,也就是数组元素都是指针
  • 数组指针:a pointer to an array,即指向数组的指针

注意的是它们用法的区别,下面举例说明。

  1. int* a[4] 指针数组
    表示:数组a中的元素都为int型指针
    元素表示:*a[i]*(a[i])是一样的,因为[]优先级高于*

  2. int (*a)[4] 数组指针
    表示:指向数组a的指针
    元素表示:(*a)[i]

4、总结

  • 在函数原型或函数定义头的场合中(并且也只有在这两种场合中),可以用int *ar代替int ar[]

  • 处理数组的函数实际上是使用指针做为参数的,也就是说把数组作为参数传递给函数时,会把数组转换为指针处理。

  • 指针的两个操作:1、赋值,把一个地址赋值给指针;2、求差值,求出两个元素之间的距离(并非是字节数差距);

  • 使用指针时,特别注意:不能对未初始化的指针取值。另外切记:当创建一个指针时,系统只分配了用来存储指针本身的内存空间,并不分配用来存储数据的内存空间。因此在使用指针之前,必须给它赋予一个已分配的内存地址。

  • 可以使用两个const来创建指针,这个指针既不可以更改所指向的地址,也不可以修改所指向的数据。例如:const char *const p;

  • 一般地,声明N维数组的指针时,除了最左边的方括号可以留空之外,其他都需要填写数值。 这是因为首方括号表示这是一个指针,而其他方括号描述的是所指向对象的数据类型,但是,最左边方括号的维数需要用第二个参数来传递(如下面例子的int rows)。

      int sum(int arr[][12][20][30],int rows);
      int sum(int (*p)[12][20][30],int rows); //效果同上
    
  • 变长数组允许动态分配存储单元。这表示可以在程序运行时指定数组的大小。常规的C数组是静态存储分配的,也就是说数组大小在编译时已经确定。

  • C把数组名解释为该数组首元素的地址。也就是说,数组名和指向首元素的指针是等价的。通常,数组和指针是紧密联系的。如果ar是数组,那么表达式ar[i]*(ar+i)是等价的。

第十一章:字符串和字符串函数

  • 字符串常量(string constant),又称字符串文字(string literal),是指位于一对双引号中的任何字符。双引号里的字符加上编译器自动提供的结束标志\0字符,作为一个字符串被存储在内存里。程序中使用了几个这样的字符串常量,大多数是用作函数printf()puts()的参数。注意,还可以用#define来定义字符串常量。

  • 如果字符串文字中间没有间隔或者间隔的是空格符,ANSI C会将其串联起来。

      char str[50] = "hello, and " "how are " "you " "today";
      char str[50] = "hello, and how are you today"; //效果相同
    
  • 字符串常量属于静态存储(static storage)类。静态存储是指如果在一个函数中使用字符串常量,即使是多次调用了这个函数,该字符串在程序的整个运行过程中只存储一份。整个引号中的内容作为指向该字符串存储位置的指针。例如:

      printf("%s,%p,%c\n","We","are",*"family");
      //输出:We,0x0040c010,f
      //%s输出字符串“We”
      //%p产生一个地址,
      //%c则产生这个指针指向的值,即字符串“family”的第一个字符。
    
  • 字符串数组的初始化:

      const char str[32] = "hello world";
      const char str[] = "hello world"; //让编译器决定数组的大小
    

1、字符数组和字符串指针的区别

  • 数组初始化是从静态存储区把一个字符串复制给数组,而指针初始化只是复制字符串的地址
  • 字符串数组的数组名是个常量,而指针名是一个变量。因此:数组名可以赋值给指针,反之则不行。例如:

      chat a1[] = "i love you";
      char *a2 = "i hate you"; //注意:这种方式等价于 const char *a2;不允许修改a2指向的内容
      a2 = a1; //正确,类似于x=3
      a1 = a2; //错误,类似于3=x
    
  • 矩形字符串数组和不规则字符串数组的区别

矩形数组和不规则数组

2、字符串和数字之间的转换

  • ANSI C提供了这些函数的更复杂版本:strtol()strtoul()strtod()。其中,strtol()函数把一个字符串转换为long型值,strtoul()函数把一个字符串转换为unsigned long型值,strtod()函数把一个字符串转换为double型值。这些函数的复杂性在于它们还可以识别并报告字符串中非数字部分的第一个字符。strtol()和strtoul()函数还允许您指定数字的基数。

  • 很多实现中都用itoa()和ftoa()函数把整数和浮点数转换为字符串。但是,这两个函数并不是ANSI C库里的函数;如果要求兼容性更好,可以使用sprintf()函数来完成这些功能。

  • C库里有许多处理字符串的函数。在ANSI C中,这些函数都是在string.h文件中声明的。C库里还有一些处理字符的函数,它们是在ctype.h文件里声明的。

第十二章:存储类、链接和内存管理(重点)

  • C为变量提供了5种存储模型:自动、寄存器、具有代码块作用域的静态(空链接的静态)、具有外部链接的静态,以及具有内部链接的静态。

  • 变量的作用域:代码块作用域、函数原型作用域、文件作用域。
  • 变量的链接:外部链接,内部链接,或空链接。具有代码块作用域或者函数原型作用域的变量有空链接,意味着它们是由其定义所在的代码块或函数原型所私有的。具有文件作用域的变量可能有内部或者外部链接。一个具有外部链接的变量可以在一个多文件程序的任何地方使用。一个具有内部链接的变量可以在一个文件的任何地方使用。
  • 变量的存储时期:静态存储时期、自动存储时期
  • 如果一个变量具有静态存储时期,它在程序执行期间将一直存在。具有文件作用域的变量具有静态存储时期。注意对于具有文件作用域的变量,关键词static表明链接类型,并非存储时期。一个使用static声明了的文件作用域变量具有内部链接,而所有的文件作用域变量,无论它具有内部链接,还是具有外部链接,都具有静态存储时期。
存储模型(存储类)存储时期作用域链接声明方式备注
自动自动代码块代码块内局部变量
寄存器自动代码块代码块内,使用关键字register 
具有外部链接的静态静态文件外部所有函数之外全局变量
具有内部链接的静态静态文件内部所有函数之外,使用关键字static静态变量
空链接的静态静态代码块代码块内,使用关键字static局部静态变量
  • 寄存器变量可能被存储在cpu的寄存器中或者cpu缓存中,所以,无法获得寄存器变量的地址

  • “静态”是指变量的位置固定不动,从一次函数调用到下一次调用,计算机都记录着它们的值。如果不显式地对静态变量进行初始化,它们将被初始化为0。

  • 声明有两种:1、定义声明;2、引用申明,使用extern。

  • 除非在第二个文件中也声明了该变量(通过使用extern),否则在一个文件中定义的外部变量不可以用于第二个文件。一个外部变量声明本身只是使一个变量可能对其他文件可用

  • C语言中有5个作为存储类说明符的关键字,它们是auto、register、static、extern以及typedef。关键字typedef与内存存储无关,由于语法原因被归入此类。特别地,不可以在一个声明中使用一个以上存储类说明符,这意味着不能将其他任一存储类说明符作为typedef的一部分。

关键字备注
auto表明变量具有自动存储时期,只能用于具有代码块作用域的变量声明中,主要用来明确指出意图,使程序易读。
register只能用于具有代码块作用域的变量,它将一个变量归类于寄存器存储类,相当于请求将该变量存储在寄存器中,以更快的存取。
static用于具有代码块作用域的变量时,使该变量具有静态存储时期,并仍具有代码块作用域和空链接。作用于具有文件作用域的变量时,表示该变量具有内部链接。
extern表明声明一个已经在别处定义过的变量,如果包含extern的声明具有文件作用域,所指向的变量必然具有外部链接。
  • 自动变量具有代码块作用域、空链接和自动存储时期。它们是局部的,为定义它们的代码块(通常是一个函数)所私有。寄存器变量与自动变量具有相同的属性,但编译器可能使用速度更快的内存或寄存器来存储它们。无法获取一个寄存器变量的地址。

  • 具有文件作用域的变量对文件中在它的声明之后的所有函数可见。如果一个文件作用域变量具有外部链接(全局变量),则它可被程序中的其他文件使用。如果一个文件作用域变量具有内部链接(静态变量),它只能在声明它的文件中使用。

  • C标准使用了一个新类型:指向void的指针。这一类型被用作“通用指针”。函数malloc()可用来返回数组指针、结构指针等等,因此一般需要把返回值的类型指派为适当的类型。

  • malloc()可能无法获得所需数量的内存。在那种情形下,函数返回空指针,程序终止。

  • c99有三个类型限定词:const、volatile、restrict。const限制变量只读;volatile告诉编译器变量可以被程序改变以外还可以被其他代理改变;restrict只能用于指针,并表明指针是访问一个数据对象唯一且初始的方式,可以使编译器尝试优化代码。其中volatile和restrict都是为了编译器优化而设置。可以同时使用多个类型限定词。

  • 一个位于左边任意位置的const使得数据成为常量,而一个位于右边的const使得指针自身成为常量。

  • 在文件之间共享const数据时要小心。可以使用两个策略:

    1. 在一个文件中进行定义声明,在其他文件中进行引用声明(使用关键字extern);
    2. 将常量放在一个include文件中,并使用static(表明使用静态内部存储模型)

第十三章:文件输入/输出

  • C程序自动打开3个文件。这3个文件被称为标准输入(standard input),标准输出(standard output)和标准错误输出(standard error output)。默认的标准输入是系统的一般输入设备,通常为键盘;默认的标准输出和标准错误输出是系统的一般输出设备,通常为显示器。

  • exit()函数关闭所有打开的文件并终止程序。exit()函数的参数会被传递给一些操作系统,包括UNIX、Linux和MS DOS,以供其他程序使用。通常的约定是正常终止的程序传递值0,非正常终止的程序传递非0值。不同的退出值可以用来标识导致程序失败的不同原因。这也是UNIX和DOS编程的通常做法。但并非所有的操作系统都识别相同范围内的可能返回值。

  • returnexit()都可以用来退出main函数,但是区别有两点:1、如果main()在一个递归程序中,exit()仍然会终止程序;但return将控制权移交给递归的前一级,直到最初的那一级,此时return才会终止程序。2、即使在除main()之外的函数中调用exit(),它也将终止程序。

  • fopen()函数:小心!如果使用任何一种“w”模式打开一个已有的文件,文件内容将被删除,以便程序以一个空文件开始操作。

  • 同时可以打开的文件数目是有限制的。这个限制取决于系统和实现;范围通常是10到20之间。可以使用同一个文件指针指向不同的文件,但前提是不能同时打开这些文件。

  • 调用fflush()函数可以将缓冲区中任何未写的数据发送到一个由fp指定的输出文件中去。这个过程称为刷新缓冲区(flushing a buffer)。如果fp是一个空指针,将刷新掉所有的输出缓冲。对一个输入流使用fflush()函数的效果没有定义。只要最近一次使用流的操作不是输入操作,就可以对一个更新流(任何读-写模式的流)使用这个函数。

  • C程序将输入看作字节流;流的来源可以是文件、输入设备(如键盘),甚至可以是另一个程序的输出。与之类似,C程序将输出也看作字节流;流的目的地可以是文件,视频显示等等。

  • ANSI C提供两种打开文件的模式:二进制模式和文本模式。以二进制模式打开一个文件时,可以逐字节地读取它。以文本模式打开一个文件时,会把文件内容从具体系统的文本表示法映射到C表示法。对于UNIX和Linux系统,这两种模式是相同的。

第十四章:结构和其他数据形式

  • 声明结构的过程和定义结构变量的过程可以被合并成一步。

      struct book {
          char title[20];
          char author[10];
          float value;
      } library;
    
      // 忽略结构体名称亦可
      struct book {
          char title[20];
          char author[10];
          float value;
      } library;
    
  • 结构体的初始化:

    • 顺序初始化: 按照成员定义的顺序,从前到后逐个初始化;允许只初始化部分成员;在被初始化的成员之前,不能有未初始化的成员。

        struct book library = {"c primer plus","domicat",1.23};
      
    • 乱序初始化(C99),语法与数组指定初始化项目相似。可以只初始化部分成员。

        struct name {
            char first[10];
            char second[10];
        };
      
        struct book library = {
            .value = 3.14,
            .title = "c primer plus",
            .author = "domi"
        }
      
        // 如果是嵌套结构体
        struct book library = {
            .value = 3.14,
            .title = "c primer plus",
            .author = "domi",
            .player.first = "domi",
            .player.second = "cat"
        }
        //或者
        struct book library = {
            .value = 3.14,
            .title = "c primer plus",
            .author = "domi",
            .player = {
                first = "domi",
                second = "cat"
            }
        }
      
  • 和数组不同,一个结构的名字并不是该结构的地址,所以,获取一个结构的地址时,必须使用&运算符。

  • 在一些系统中,结构的大小有可能大于它内部各成员大小之和,那是因为系统对数据的对齐存储需求会导致缝隙。即结构体内存对齐

  • 结构体数组以及结构体数组的初始化:结构体数组和其他基础类型的数组其实并无区别,在结构体数组定义时,以及申请了相应的存储空间。另外,如果要对一个结构体数组进行置零初始化。最快速的办法是:memset(ptr,0,sizeof(*ptr)); 或者 struct book libs[20] = {0};

伸缩型数组成员

  • C99具有一个称为伸缩型数组成员(flexible array member)的新特性。利用这一新特性可以声明最后一个成员是一个具有特殊属性的数组的结构。注意:它和变长数组的概念要区分,不能混淆。声明一个伸缩型数组成员有三个规则:

    • 伸缩型数组成员必须是结构体的最后一个数组成员。
    • 结构体中必须至少有一个其他成员。
    • 伸缩型数组就像普通数组一样被声明,除了它的方括号内是空的。
      struct flex{
          int count;
          double average;
          double scors[];	//伸缩型数组成员
      }
    
  • 有伸缩型数组成员的结构体需要和malloc配合使用。c99的意图并不是申明结构体类型的变量,而是需要声明一个指向该结构体类型的指针,然后配合malloc来分配该结构体实际需要的内存。

      struct flex *pf;
      pf = malloc(sizeof(struct flex) + 5*sizeof(double));
      pf->count = 5;
    
      int i;
      for(i=0;i<5;++i){
          pf->scors[i] = 10;
      }
      pf->average = 10;
    

注意:sizeof(struct flex)中的的大小并没有包含double scors[]的大小,也就是说double scors[]没有占用内存空间。

联合

  • 联合与结构体类似,区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而联合体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。

  • 结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),联合体占用的内存等于最长的成员占用的内存。联合体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。

枚举

  • 通过使用关键字enum,可以创建一个新“类型”并指定它可以具有的值,实际上,enum常量是int类型的,因此在使用int类型的任何地方都可以使用它)。枚举类型的目的是提高程序的可读性。它的语法与结构的语法相同。

      // 枚举的声明和定义
      enum eColor {red,orange,yellow,green,blue};
      enum eColor color;
    
  • C允许对枚举变量使用运算符++,而C++不允许。

      for(color=red;color<=blue;color++){
          // TODO ...
      }
    
  • 默认时,枚举列表中的常量被指定为整数值 0、1、2等等,当然也可以指定其他值。

      enum eColor {red=100,orange=101};		//此时yellow=102,green=103,blue=104
      enum eColor {red,orange=100,green=500};	//此时red=0,yellow=101,blue=501
    

typedef

  • typedef的作用就是为某一种数据类型定义一个别名。它和#define有点相似,但有3个不同之处:

    • typedef仅限于数据类型,而不是对值。
    • typedef的解释由编译器,而不是预处理器。
    • 在受限范围内,typedef比define更灵活。
  • 表示一个数组的[]和表示一个函数的()具有同样的优先级,这个优先级高于间接运算符*的优先级。[]()是从左到右结合的。

  • 指向函数的指针中保存着函数代码起始处的内存地址。函数名与数组名类似,函数名就是函数的地址。

第十五章:位操作

  • 已知一个负数,求该负数的二进制补码,以一个字节为例:

    • 字节能容纳的最大值+该负值+1,结果即为补码;例如:255+(-95)+1=161,(1010 0001)
    • 取该负值绝对值,将绝对值的二进制最高位改为1,其他位取反,最后加1,结果即为补码;例如-95的补码:(0101 1111)->(1010 0000)+1->(1010 0001)
  • 位逻辑运算符

    • 取反 ~
    • &
    • |
    • 异或 ^(相同为假,不同为真)
  • 位常用用法(假设mask=0000 0010)

    • 掩码:flags & mask,该操作会使flags的除了第二个位1之外所有的位都置0。
    • 打开位:flags | mask,该操作会使flags第二个位置为1(打开了该位),其他位不变。
    • 关闭位:flags & (~mask),该操作会使flags的第二个位置为0(关闭了该位),其他位不变。
    • 转置:flags ^ mask,该操作会使flags中与掩码位对应为1的位被转置,对应掩码位为0的位不变。
    • 查看一个位的值:(flags & mask)==mask,该操作会检查flags的第二位的值是否为1。
  • 位移运算符

    • << 左移,左移n位,可以表示为一个值乘以2的n次幂
    • >> 右移,右移n位,可以表示为一个值除以2的n次幂

    注意:这里涉及到逻辑位移和算数位移的区别,逻辑位移不考虑符号位,但是算术位移要考虑符号位。C语言中移位操作符实现的是逻辑左移和算术右移,但是算术左移和逻辑左移的效果相同(都是在最后边补0),算术右移和逻辑右移的效果不同,要实现逻辑右移可将操作数强制类型转化为无符号数。这里的二进制都是补码形式的

    我们只需要记住: 算术左移和算术右移主要用来进行有符号数的倍增、减半; 逻辑左移和逻辑右移主要用来进行无符号数的倍增、减半。

  • 逻辑右移和算数右移

    例如,8位二进制数11001101分别右移一位。
    逻辑右移就是[0]1100110
    算术右移就是[1]1100110

位字段

  • 位字段由一个结构体声明建立,该结构申明为每个字段提供标签,并决定每个字段的宽度。位字段只能定义为以下3中类型:1.int; 2.signed int; 3.unsigned int

      struct {
          unsigned int a:1;
          unsigned int b:1;
      }
    
  • 位字段访问:
    1. 位字段通过“.”号访问:flags.a, flags.b等等。
    2. 位字段没有独立的地址,不能进行取址操作。
    3. 位字段没有独立的存储空间,不能进行sizeof()操作。
  • 无名字段与0字段:

      struct  {
        unsigned int   a  : 1;      
        unsigned int      : 3;      //无名字段,不可访问,仅起占位作用
        unsigned int      : 0;      //0字段,下一个位字段在新机器字边界
        unsigned int    b : 7;
      }flags;
    

    受0字段作用, 位字段flags.b将在下一个机器字边界开始存储。

第十六章:C预处理器和C库

  • 预处理器指令从#开始,到其后第一个换行符为止。也就是说,指令的长度仅限一行代码(逻辑行)。

  • 宏的名字不允许有空格。且命名遵循C变量的命名规则。

  • 预处理器不进行计算,而只是进行字符串替换

  • 在宏定义中使用参数需要特别注意,参数需要用括号括起来,另外需要注意替代部分的空格处理。

      #define SQUARE(x) x*x 		//错误,例如SQUARE(1+2)--> 1+2*1+2=5
      #define SQUARE(x) (x)*(x)	//正确
    
  • #预处理运算符,可以把语言符号转换成字符串。

      #define SQUARE(x) printf("the square of " #x " is %d\n",(x)*(x))
    
      int a=100;
      SQUARE(a);	//输出 the square of a is 100
      SQUARE(1+2);//输出 the square of 1+2 is 100
    
      相当于:
      printf("the square of " "a" "is %d\n",(a)*(a))
    
  • ##预处理运算符,可以把两个符号语言组成单个符号语言。

      #define XNAME(n) x##n
      #define PRINT_XN(n) printf("x"#n"=%d\n",x##n);
    
      int XNAME(1) = 1;	//变成 int x1 = 1;
      int XNAME(2) = 2; 	//变成 int x2 = 2;
      PRINT_XN(1)			//变成 printf("x1=%d\n",x1);
      PRINT_XN(2)			//变成 printf("x2=%d\n",x2);
    
  • 预处理器发现#include后,会寻找后跟的文件名并把这个文件的内容包含到当前文件中。被包含的文件的文本将替换源代码文件中的#include指令,就像把被包含的文件中的全部内容复制到源文件中这个特定的位置一样。

  • #include的搜索路径

      #include <stdio.h>	//搜索系统目录
      #include "hot.h"	//搜索当前目录
      #include "/usr/biff/p.h" //搜索/usr/biff目录
    
  • 不要在头文件中定义变量或函数,因为很多.c文件都可以包含.h文件,也就是说这个变量会在很多.c文件中存在一个副本。假如这是一个多文件项目,在连接阶段,连接器就会抱怨存在多个相同变量名的全局变量,导致连接出错。所以.h文件中一般只能包含全局变量的声明,函数声明,宏定义一类的,在.h文件中定义变量是不被推荐的。

  • 使用头文件的两种特殊情况:

    • 在包含函数声明的源代码文件(.c文件)定义一个具有文件作用域、外部链接的变量(全局变量),然后在与源代码文件相关的头文件中进行引用申明,例如:extern int status。
    • 使用文件作用域、内部链接和const限定词的变量(例如:const static int status = 10;),使用const可以避免变量被意外改变,使用static使每个包含了头文件的源代码文件都有一份该常量的副本。
  • 其他预处理指令

    • #undef 取消之前的宏定义,例如:#undef LIMIT
    • #ifdef 和 #else 和 #endif#ifndef#if和#elif 条件编译
    • 预定义宏 __DATE__、__FILE__、__LINE__、__STDC_VERSION__、__TIME___、__func__
    • #line 用于重置__FILE__、__LINE__的文件名和行号,例如:#line 100 "test.c"把当前行重置为100,文件名重置为test.c
    • #error 使预处理器发出一条错误信息,例如:#error not c99
    • #pragma
  • C和C++对待void类型的指针是不同的,两种语言都可以把任意类型的指针赋值给void*,但是,把一个void*指针赋值给一个指针或另一个类型的指针时,C++需要一次强制类型转换。而C没有这个需要。

  • 不能把一个数组的值直接赋值给另一个数组,但是,可以使用string.h中的memcpy()memmove()进行赋值。这两个方法左右一样,但是区别在于:当内存发生局部重叠的时候,memmove保证拷贝的结果是正确的(相当于字节复制到一个缓冲区,然后再复制到最终目的地),memcpy不保证拷贝的结果的正确(当源地址在目的地址之前时)。 memcpy复制内存发生重叠

  • 可变参数stdarg.h

      double sum(int,...);
      double sum(int lim, ...){
          va_list ap;				//声明用于存放参数的变量
          va_list apcopy;			//演示复制
          double total = 0;
          int i;
          va_start(ap, lim);		//把ap初始化为参数列表
          va_copy(apcopy, ap);	//apcopy是ap的副本
          for(i=0;i<lim;++i){
              total += va_arg(ap,double);//访问参数列表中的每一个项目
          }
          va_end(ap);				//清理
          return total;
      }
    

附录

  • C++中的布尔类型是bool,并且true和false都是关键字。在C中,布尔类型是_Bool,但是包含了stdbool.h头文件就可以使用bool、true和false。

其他

  1. 关于打印long long类型的变量:主要的区分在于操作系统,如果在win系统下,那么无论什么编译器,一律用%I64d;如果在linux系统,一律用%lld
  2. 指针长度问题:如果64位处理器上运行的是64位操作系统,那么几乎可以肯定应该是8字节。如果运行的是32位操作系统,那么应该是4字节
  3. C语言中:char s[];char* s效果等级,但是有一点需要注意:char *s = "hello";在声明的同时进行赋值,它其实等价于const char *s = "hello";,因为字符串hello是常量,不能被改变。这就决定了 char数组(char a[])的数组名可以赋值给s,但是反过来s不能赋值给数组a。可以参考我的C Primer plus笔记
  4. 多维数组不存在到多级指针的转化规则,强转只会导致问题,所以int a[2][3];不等价于int **p;务必注意。所有有以下结论:假设T为任意类型,不管是T类型的几维数组,数组名就是一个该类型的指针。因此int a[2][3];等价于int *p;

    但是为什么char [][]又等价于char**呢?这是因为C语言中操作字符串是通过它在内存中的存储单元的首地址进行的,这是字符串的终极本质。所以char [][] == char *a[] == char**

  5. 关于char []、char*、char *[]、char**,可参考这篇文章

  6. 关于结构体的typedef:
     typedef struct a{
         int id;
         int idx;
     } Sa,*pSa; 
    

    上面这个typedef的意义:

    • 1、给结构体a取一个别名 Sa;
    • 2、给结构体a的指针类型取一个别名pSa(它是个指针类型),即 pSa 就是 a* 的一个替代(不推荐);
  7. typedef 定义函数类型:

    • tpyedef自定义函数指针类型
    • typedef自定义函数类型

    例如:

     #include <stdio.h>
    
     typedef int (*fp_t)(char c);
     typedef int f_t(char c);
    
     int func(char c) { printf("f0, c = %c\n", c); return 0;}
     int main() {
         fp_t fp;
         fp = func;
         f_t *fp1;
         fp1 = func;
         return 0;
     } 
    
  8. c语言一维数组做参数传递给函数

    • 1、C语言中,当一维数组做函数参数时,编译器总是把它解析成一个指向其首元素的指针。
    • 2、实际传递的数组大小与函数形参指定的数组大小没有关系。