C语言程序设计现代方法v2 K.N.King 笔记及课后习题解答

1.C语言概述

  • 代码地址:https://github.com/junmocsq/cstudy
  • C语言优点
    • 高效
    • 可移植
    • 功能强大。C语言拥有一个庞大的数据类型和运算符集合
    • 灵活。可编写嵌入式系统到商业数据处理的各种应用程序。
    • 标准库。包含了用于输入/输出、字符串处理、存储分配以及其他使用操作的函数
    • 与Unix系统集成。
  • C语言的缺点。
    • C语言更容易隐藏错误。C语言的灵活性使得它编程出错的概率较高。
    • 难以理解。
    • 可能难以修改。没有类和包的概念,编程时不注重模块化设计会使代码那一维护
  • 高效使用C语言
    • 学习规避C语言陷阱。学习《C陷阱和缺陷》
    • 使用软件工具使程序更加可靠。lint工具检查C代码
    • 利用现有的代码库。可以减少错误和编码工作
    • 采用一套切合实际的编码规范。程序规范统一,易于阅读和修改。
    • 避免"投机取巧"和极度复杂的代码。实现功能尽量采用最简洁的方式。
    • 紧贴标准。

2.C语言基本概念

  • C语言编译为可执行二进制文件过程:
    • 预处理。首先将程序递交给预处理器。预处理器处理#号指令,修改程序代码。
    • 编译。预处理后的程序进入编译器。编译器将程序翻译成生成汇编。
    • 汇编。汇编代码转换机器指令(目标代码)
    • 链接。链接器把由编译器产生的目标代码和所需的其他附加代码整合在一起,这样才最终产生了可执行的程序。这些附加代码包括程序中用到的库函数(如printf)
  • 指令:我们把预处理器执行的命令称为指令。
  • 注释风格
/*  多行注释 
    多行注释 
    多行注释 
*/

// 单行注释
  • 变量(variable):存储单元
  • 类型:每一个变量都必须有一个类型(type)。类型用来说明变量所存储的数据的种类。
  • 声明:在使用变量之前必须对其进行声明(为编译器所做的描述)。
  • GCC 常用选项
    • -Wall 使编译器在检测到可能的错误时生成警告信息
    • -W 除了-Wall生成的警告消息外,还需要针对具体情况的额外警告消息
    • -o 指定输出文件
    • -pedantic 根据C标准的要求生成警告消息。这样可以避免在程序中使用非标准特性。
    • -ansi 禁用GCC的非标准C特性,并启用一些不太常用的标准特性
    • -std=c89 -std=c99 指定版本

3.格式化输入/输出

  • scanf
    • 使用scanf()时,程序员必须检查转换说明的数量是否与输入变量相匹配,并且检查每个转换是否适合相对应的变量。
    • 在寻找数的起始位置时,scanf()会忽略空白字符

4.表达式

  • 语句:程序运行时执行的指令
  • 表达式:表示如何计算值的公式。最简单的表达式是常量和变量
  • 运算符是构建表达式的基本工具。
  • 在c99中,除法总是向0截取,即-9/7 = -1。i%j值的符号与i相同,即-9%7=-2, 9%-7=2。需要保证 (i/j)*j + i%j == i
  • 运算符优先级:
  • 结合性:当表达式包含两个或多个相同优先级的运算符时,仅有运算符优先级是不行的,这时结合性就发挥作用了
    • 左结合性:运算符从左到右结合的。
    • 右结合性:运算符从右到左结合的。如一元运算符 -+,赋值运算符=
  • 赋值运算符要求它的左操作数必须是左值(lvalue)。左值表示存储在计算机内存中的对象,而不是常量或计算的结果。变量是左值,而诸如10和2*i这样的表达式则不是左值。
  • 右值(rvalue):表达式
  • 简单赋值:=
  • 复合赋值:+= -= *= 等等,它们是右结合的
  • 自增及自减运算符
    • 前缀自增:立即自增。
    • 后缀自增:先用当前值,稍后在自增。
  int i = 10;
  printf("i++: %d\n",i++); // 10
  printf("i: %d\n",i);     // 11
    
  printf("++i: %d\n",++i); // 12
  printf("i: %d\n",i);   // 12
  • 后缀++和后缀–比一元的正号、负号优先级高,后缀++和后缀–是左结合的。前缀++和前缀–与一元的正号、负号优先级相同,而且都是右结合的
int i = 10;
printf("%d\n", -i++); // -10
printf("%d\n", i); // 11
printf("%d\n", -++i); // -12
  • C语言没有定义子表达式的求值顺序(除了含有逻辑与和逻辑或、条件运算符以及逗号运算符的子表达式)。因此表达式 (a+b) * (c-d) 无法确认子表达式(a+b)和(c-d)的执行顺序。
a = 5;
c = (b = a+2)-(a=1); 取值可能是6,也可能是2
  • 一个好的主意就是:不要在子表达式中使用赋值表达式,而是采用分离的表达式。
  • c语言有一条不同寻常的规则,那就是任何表达式都可以用作语句。换句话说,不论表达式是什么类型,计算什么结果,我们都可以通过在后面添加分号的方式将其转换成语句。
  • 如果v有副作用,那么v += e不等价于v = v+e。
    • 计算v+=e只会求一次v值,而计算v=v+e会求两次v值。如下
    • a[i++] += 2 和 a[i++] = a[i++] +2

5.选择语句

  • 不应该以聪明才智和逻辑分析能力来评判程序员,而要看其分析问题的全面性。
  • 目前为止出现的语句:return语句,表达式语句
  • 选择语句(selection statement):if语句和switch语句允许程序在一组可选项中选择一条特定的执行路径。
  • 重复语句(iteration statement):while语句,do语句和for语句支持重复(循环)操作。
  • 跳转语句(jump statement):break语句、continue语句和goto语句导致无条件地跳转到程序中的某个位置(return语句也属于此列)。
  • 复合语句:把几条语句组合成一条语句。
  • 空语句:不执行任何操作
  • 关系运算符:< > <= >=,优先级低于算术运算符,左结合
  • 判等运算符:== !=
  • 逻辑运算符:! && ||。运算符!的优先级和一元运算符正负号相同,运算符&&和||的优先级低于关系运算符和判等运算符
  • "悬空else":else字句应该属于离它最近的且还未和其他else匹配的if语句
  • 条件表达式(三元运算符): result = a==b?a:b
  • 布尔值:c89没有,c99中为_Bool,也可以引用中的bool
  • switch语句:只能接收整数。

6.循环

  • while(){};
  • do{}while();
  • for( ; ; ){}

7.基本类型

  • 8进制:以0开头的整数
  • 16进制:0x开头
  • 长整数:14L,无符号长整数 14UL;c99中 LL表示long long int
  • 整数溢出:有符号整数运算发生溢出,程序行为是未定义的。无符号整数运算发生溢出,程序行为是有定义的:正确答案是对2^n取模,其中n是用于存储结果的位数。
  • printf:
    • 无符号 u、o、x
    • 短整数 hd ho hu hx
    • 长整数 ld lo lu lx
    • c99长长整数 lld llo llu llx
  • 浮点:一般遵循IEEE 754标准
    • 单精度:float [1.17549e-38,3.40282e+38] 精度 6位
    • 双精度:double [2.22507e-308,1.79769e+308] 精度 15位
    • 扩展双精度:long double
    • printf:double lf;long double Lf
  • c语言将字符当做小整数进行处理。[-128,127]
    • 有符号char: signed char
    • 无符号char: unsigned char
  • 字符常量:单引号括起来的单个字符。
  • 字符转义序列:回退符\b;换页符\f;垂直制表符\v;水平制表符\t;
  • 字符处理函数:
    • toupper() ctype.h
    • scanf:在读取字符前,scanf不会跳过空白字符
    • getchar和putchar
  • 强制类型转换:(类型名)表达式
  • typedef:类型定义,如 typedef int int32_t
  • sizeof运算符:允许程序存储指定类型值所需空间的大小。可以sizeof i,也可以sizeof(i)

8.数组

  • 数组初始化
int ch[10] = {1,2,3}; // 不够10位,其他未指定的为0
int ch[] = {1,2,3}; // 编译器自动确定长度
int ch[100] = {[55]=99,[66]=99}; // 指定初始化式,c99才有
  • c语言是行主序的,也就是从第0行开始,一行一行的存储
  • 多维数组初始化
int arr[2][3] ={[0][0]=1,[1][1]=99}; // 指定初始化式,c99才有
  • 常量数组 const char hex_chars[16] = {‘0’}
  • 变长数组:长度是在程序执行时计算的,而不是再程序编译时计算的。

9.函数

  • 函数的"返回类型"是函数返回值的类型。有如下规则:
    • 函数不能返回数组,但是关于返回类型没有其他限制
    • 指定返回类型是void类型说明函数没有返回值。
  • 函数声明使得编译器可以先对函数进行概要浏览,而函数的完整定义以后再给出。也叫做函数原型。
  • C99:在调用一个函数之前,必须先对其进行声明或定义。
  • 形式参数:出现在函数定义中,它们以假名字来表示函数调用时需要提供的值。
  • 实际参数:出现在函数调用中的表达式。
  • 在C语言中,实际参数是按值传递的:调用函数时,计算出每个实际参数的值并且把它赋值给相应的形式参数。
  • 虽然可以用运算符sizeof计算出数组变量的长度,但是它无法给出关于数组型形式参数的长度。
  • 如果形式参数是多维数组,声明参数时只能省略第一维的长度。int test(int a[][LEN],10)
  • C99,变长数组作为参数,可以有如下两种方式定义或声明函数:
int sum_array(int n,int a[n]);// int n必须在前面,否则顺序调换会出错,因为int a[n]前没有见过n。
int sum_array(int n,int a[*]);
int sum_array(int n,int m,int a[n][m]); // 多维数组
  • static在数组声明中使用。如果数组是多维的,static只能用在第一维
int sum_array(int a[static 3],int n); // 将static放在数字3前面表明数组a的长度至少可以保证是3。
  • c99,复合字面量:通过指定其包含的元素而创建没有名字的数组。
int b[] = {3,0,2,1,3};
total = sum_array(b,5);

total = sum_array((int[]){3,0,2,1,3},5); // 复合字面量
  • return和exit(stdlib.h)函数之间的差异是:不管那个函数调用exit函数都会导致程序终止,return语句仅当由main函数调用时才会导致程序终止。
  • 递归
  • 编译器把不跟圆括号的函数名看成是指向函数的指针。
  • 函数声明可以放在一个函数体内,这样只有这个函数可以调用它,其它函数需要调用,则需要重新声明。

10.程序结构

  • 局部变量:
    • 自动存储期限:局部变量的存储单元是在包含该变量的函数被调用时"自动"分配的,函数返回时收回分配,所以称这种变量具有自动的存储期限。
    • 块作用域。变量的作用域是可以引用该变量的程序文本的部分。
  • 静态局部变量static:始终有块作用域,它对其他函数不可见。概括来说,静态变量是对其他函数隐藏数据的地方,但是它会为将来同一个函数的再调用保留这些数据。
  • 形式参数拥有和局部变量一样的性质,即自动存储期限和块作用域。事实上,形式参数和局部变量唯一真正的区别是,在每次函数调用时对形式参数自动进行初始化。
  • 全局变量(外部变量):声明在任何函数体外。
    • 静态存储期限:存储在外部变量中的值永久保存了下来
    • 文件作用域:从变量被声明的点开始一直到所在文件的末尾。因此,跟随在外部变量声明之后的所有函数都可以访问(并修改)它。
  • 慎用全局变量,代码耦合
  • 作用域:当程序块的声明命名一个标识符时,如果此标识符已经是可见的(因为标识符拥有文件作用域,或者因为它已在某个程序块内声明),新的声明临时"隐藏"了旧的声明,标识符获得了新的含义。在程序块的末尾,标识符重新获得旧的含义

11.指针

  • 指针就是地址,而指针变量就是存储地址的变量。
  • 指针作为返回值。可以是传入的参数,也可以返回指向外部变量或指向声明为static的局部变量的指针永远不要返回指向自动局部变量的指针。
  • *p++ 相当于 *(p++),返回p指针指向的值,指针移一位

12.指针和数组

  • 在一个不指向任何数组元素的指针上执行算术运算会导致未定义的行为。此外,只有在两个指针指向同一个数组时,把它们相减才有意义。
  • 指向复合常量的指针c99
int *p = (int []){1,2,3,4,5};
  • p++:(p++)
  • –p:(–p)
  • 可用数组的名字作为指向数组第一个元素的指针
  • 对于形式参数而言,声明为数组跟声明为指针是一样的;但是对变量而言,声明为数组跟声明为指针是不同的。声明int a[10]会导致编译器预留10个整数的空间,而声明int *a只会导致编译器为一个指针变量分配空间。
  • 多维数组
int a[10][20],(*p)[20]; // *p需要括号,否则声明的就是指针数组了
p = a;
p = &a[0];
  • 变长数组
void f(int n,int m){
    int a[n],*p;
    p = a;
    int b[m][n],(*q)[n];
    q=b;
}

13.字符串

  • 字符串常量,也叫字符串字面量,是一对双引号括起来的字符序列
  • 字符串变量:字符数组,结尾加上特殊的空字符。
  • 试图改变字符串字面量会导致未定义行为。
  • C语言字符:只要保证字符串是以空字符结尾的,任何一维的字符数组都可以用来存储字符串。
  • char date[] = “csq”;char *date = “csq”。这两者有很大的区别
    • 在声明为数组时,就像任意数组元素一样,可以修改存储在date中的字符。在声明为指针时,date指向字符串字面量,它是不可以修改的。
      -在声明为数组时,date是数组名。在声明为指针时,date是变量,这个变量可以在程序执行期间指向其他字符串。
  • 直接复制和比较字符串会失败。利用= 运算符把字符串复制到字符数组中是不可能的:str1=“abc”,但是 char str1[]=“abc”
    是合法的,这是因为在声明中,=不是赋值运算符。试图使用关系运算符或判等运算符来比较字符串是合法的,但不会产生预期效果,因为比较的是指针。
  • string.h 函数
// 复制s2字符串到s1,直到s2遇到第一个空白符'\0'为止,如果s1长度小于s2,不安全。
char *strcpy(char *s1,const char *s2) 
// 安全的复制版本,只会复制n个字符,但是如果s2长于s1,会导致没有空白符,下面的使用是一种更安全的做法。
char *strncpy(char *s1,const char *s2,int n) 
strncpy(str1,str2,sizeof(str1)-1);
str1[sizeof(str1)-1]='\0';

size_t strlen(const char *s) // 字符串长度

strcat(char *s1,const char *s2) // 字符串拼接,把s2拼接到s1上
// 如果s1指向的数组没有大到可以容纳s2指向的字符串字符,那么调用strcat的结果将不可预测
strncat(char *s1,const char *s2,int n);
strncat(str1,str2,sizeof(str1)-strlen(str2)-1) // 安全使用

// 字符串比较,s1小于s2,则函数返回一个小于0的数,=则返回0,>则返回大于0的数
strcmp(const char *s1,const char *s2) 

14.预处理器

  • 预处理器的行为是由预处理指令(由#字符开头的一些东西)控制的。
  • define指令定义了一个宏——用来代表其他东西的一个名字,例如常量或者常用表达式。
  • include指令告诉预处理器打开一个特定的文件,将它的内容作为正在编译的文件的一部分“包含”进来。

预处理器的输入是一个C语言程序,程序可能包含指令。预处理器会执行这些指令,并在处理过程中删除这些指令。预处理器的输出是另一个C程序:原程序编辑后的版本,不再包含指令。预处理器的输出被直接交给编译器,编译器检查程序是否有错误,并将程序翻译为目标代码(机器指令)。

  • 预编译器输出:gcc -E xxx.c
  • 条件编译。#id、#ifdef、#ifndef、#elif、#else、和#endif指令可以根据预处理器可以测试的条件来确定一段文本块包含到程序中还是将其排除在程序之外。
  • 特殊指令:#error、#line、和#pragma
  • 指令的一些规则
    • 以#开始
    • 在指令和符号之间可以插入任意数量的空格和水平制表符
    • 指令总是以第一个换行符结束,除非明确指定要延续。如果要延续,必须在当前行的末尾加入\字符
    • 指令可以出现在程序中的任何地方。
    • 注释可以和指令放在同一行。
  • # 和## 运算符:宏定义可以包含两个专用的运算符 #和##。编译器不会识别这两种运算符,它们会在预处理时被执行
    • # 运算符将宏的一个参数转换为字符串字面量。
    • ## 运算符可以将两个记号(如标识符)“粘合”在一起,成为一个记号。
#define PRINT_INT(n) printf(#n " = %d\n", n)

int n = 199;
PRINT_INT(n / 12); // n / 12 = 16

#define MK_ID(n) i##n

int MK_ID(1),MK_ID(2),MK_ID(3);
// 预处理后变为
int i1,i2,i3;
int MK_ID(1) = 100, MK_ID(2) = 200, MK_ID(3) = 300;
PRINT_INT(MK_ID(1)); // MK_ID(1) = 100
PRINT_INT(i1); // i1 = 100
  • 宏的通用属性
    • 宏的替换列表可以包含对其他宏的调用。
    • 预处理器只会替换完整的记号,而不会替换记号的片段。
    • 宏定义的作用范围通常到出现这个宏的文件末尾
    • 宏不可以被定义两遍,除非新的定义和旧的定义是一样的。小的间隔上的差异是允许的,但是宏的替换列表(和参数,如果有的话)中的记号都必须是一致的。
    • 宏可以使用#undef指令“取消定义”。
  • 在宏定义中,当宏有参数时,仅给替换列表添加圆括号是不够的。参数的每一次出现都要添加圆括号。
  • 创建较长的宏,逗号可以用来连接表达式,但是不能用于其他语句,当涉及到其他语句时,应do while方案。
#define ECHO(s) (gets(s),puts(s))

#define ECHO(s) \
        do{\
            gets(s);\
            puts(s);\
        }while(0)
  • 预定义宏
    • __FILE__:文件名
    • __LINE__:行号
    • __DATE__:编译的日期 mm dd yyyy
    • __TIME__:编译的日期 hh:mm:ss
    • __STDC__:是否符合标准
  • c99将C的实现分为两种:托管式和独立式。托管式实现能够接收任何符合C99标准的程序,而独立式实现除了几个最基本的以外,不一定要能够使用复数类型或标准头的程序。
  • __STDC_HOSTED__:1代表托管式,0代表独立式
  • C99允许宏调用中的任意或所有参数为空。这样的调用需要和一般的调用一样多的逗号。
  • c99允许参数个数可变的宏。
#define ADD(x,y) (x+y)
i=ADD(,8) // i=(+8)

#define TEST(condition, ...) ((condition) ? \
    printf("Passed test: %s\n", #condition) : \
    printf(__VA_ARGS__))
    int voltage = 99, max_voltage = 999;
    // Passed test: voltage <= max_voltage
    TEST(voltage <= max_voltage, "Voltage %d exceeds %d\n", voltage, max_voltage); 
    voltage = 99999, max_voltage = 999;
    // Voltage 99999 exceeds 999
    TEST(voltage <= max_voltage, "Voltage %d exceeds %d\n", voltage, max_voltage); 
  • __FUNC__:函数名,有助于调试
  • 条件编译
  • defined运算符:宏是否被定义,不用管值
#if DEBUG
#endif

#if defined DEBUG
#endif
// 和 #if defined DEBUG一个意思
#ifdef DEBUG 
#endif
// 没有定义DEBUG
#ifndef DEBUG 
#endif

#elif 
#else
  • 【#error 消息】:预处理器遇到#error指令,会显示一条包含消息的出错信息
  • 【#line n】:改变程序行编号行为

15.编写大型程序

  • 源文件.c:程序实现
  • 头文件.h:共享函数原型,共享变量声明
  • 保护头文件
#ifndef TESTDEF_H
#define TESTDEF_H
...
#endif
  • 构建多文件程序
    • 编译:必须对程序中的每个源文件分别进行编译。(不需要编译头文件,编译包含头文件的源文件时会自动编译头文件的内容。)对于每个源文件,编译器会产生一个包含目标代码的文件。这些文件称为目标文件(object
      file),在UNIX上为.o,在Windows上为.obj
    • 链接。链接器把上一步产生的目标文件和库函数的代码结合在一起生成可执行的程序。链接器的一个职责是要解决编译器遗留的外部引用问题。(外部引用发生在一个文件中的函数调用另一个文件中定义的函数或者访问另一个文件中定义的变量时。)
  • makefile
  • 在c语言中编译和链接时完全独立的。头文件存在是为了给编译器而不是链接器提供信息。

16.结构、联合和枚举

  • 对于数组不能使用=运算符进行复制,但是结构体进行复制时,嵌在结构体的数组也得到了复制。
  • 除了赋值运算,c语言没有提供其他用于整个结构的操作。特别是不能使用运算符==和!=来判断两个结构体相等还是不等。
  • c99复合字面量。(struct part){1,“al”,22}
  • union联合由一个或多个成员构成的,而且这些成员可能具有不同的类型。但是,编译器只为联合中最大的成员分配足够的内存空间。联合的成员在这个空间内彼此覆盖。
  • 枚举
    • enum suit{CLUBS, DIAMONDS, HEARTS, SPADES}
    • typedef enum suit{CLUBS, DIAMONDS, HEARTS, SPADES} Suit;

17.指针的高级应用

  • 动态存储分配:在程序执行期间分配内存单元的能力。
  • 内存分配stdlib.h。获得的内存块来自于堆heap的存储池
    • malloc:分配内存块,不初始化
    • calloc:分配内存块,初始化清零
    • realloc:调整先前分配的内存块大小 void *realloc(void *ptr,size_t size)
      • 当扩展内存块时,不会对新添加的字节进行初始化
      • 当不能按要求扩大内存块时,返回空指针,并且原数据块中的数据不会改变
      • 如果以NULL为第一个参数,和malloc一样
      • 如果第二个参数为0,那么会释放掉内存块
  • 空指针(null pointer):不指向任何地方的指针。在把函数的返回值存储到指针变量中以后,需要判断该指针是否为空指针。
  • 释放存储空间 free
  • 悬空指针:调用 free(p) 函数会释放 p 指向的内存块,但不会改变p本身,此时修改和访问p都会导致未定义行为。
  • C99受限指针(restricted pointer):int *restrict
    p。如果指针p指向的对象在之后需要修改,那么该对象不会允许除指针p之外的任何方式访问(其他访问对象的方式包括让两一个指针指向同一个对象,或者让指针p指向命名变量)。
  • C99灵活数组成员:灵活数组成员必须出现在结构体的最后,而且结构必须至少还有一个其他成员。
    • 复制包含灵活数组成员的结构时,其他成员都会被复制但不复制灵活数组成员
    • 具有灵活数组成员的结构时不完整类型(incomplete type)。不完整类型缺少用于确定所需内存大小的信息。
    • 不完整类型不能作为其他结构的成员和数组的元素,但是数组可以包含指向具有灵活数组成员的结构的指针。
struct vstring{
    int len;
    char chars[]; // 灵活数组成员
}
  • 空指针为0的指针,但是地址不一定为0,因为不是所有编译器都使用零地址。例如,一些编译器使用不存在的内存地址,这样硬件就能检查出试图使用空指针访问内存的方式。

18.声明

  • 存储期限:变量的存储期限决定了为变量预留和内存被释放的时间。
    • 具有自动存储期限的变量在所属块被执行时获得内存单元,并在块终止时释放内存单元,从而会导致变量失去值。
    • 具有静态存储期限的变量在程序运行期间占用同一个的存储单元,也就允许变量无限期地保留它的值
  • 作用域:变量的作用域是指可以引用变量的那部分文本。变量可以有
    • 块作用域:变量从声明的地方一直到所在块的末尾是可见的
    • 文件作用域:变量从声明的地方一直到所在文件的末尾都是可见的
  • 链接:变量的链接确定了程序的不同部分可以共享此变量的范围。
    • 具有外部链接的变量可以被程序中的几个(或者全部)文件共享
    • 具有内部链接的变量只能属于单独一个文件,但是此文件中的函数可以共享这个变量。(如果具有相同名字的变量出现在另一个文件中,那么系统会把它作为不同的变量来处理)
    • 无链接的变量属于单独一个函数,而且根本不能共享。
  • 变量的默认存储期限、作用域和链接都依赖于变量声明的位置。
    • 在块(包括函数体)内部声明的变量具有自动存储期限、块作用域,并且无链接。
    • 在程序的最外层(任意块外部)声明的变量具有静态存储期限、文件作用域和外部链接。
  • auto存储类型只对属于块的变量有效。具有自动存储期限、块作用域,并且无链接。
  • static存储类型可用于全部变量,而无需考虑变量声明的位置。但是作用于块外部变量时,static说明变量具有内部链接。当用作块内部变量时,static存储期限从自动变为静态的。
    • 块内的static变量只在程序执行前进行一次初始化,而auto变量则会在每次出现时进行初始化(在初始化式里)。
    • 每次函数在递归调用时,它都会获得一组新的auto变量。但是,函数含有static变量,那么此函数的全部调用就会共享此static变量
    • 虽然函数不应该返回指向auto变量的指针,但是函数返回指向static变量的指针是没有问题的。
  • extern存储类型使几个源文件可以共享同一个变量。
    • extern声明中的变量始终具有静态存储期限。
    • 变量的作用域依赖于声明的位置。在块内部,那么变量具有块作用域;否则,变量具有文件作用域。
    • extern变量的链接,如果变量在文件中较早的位置(任何函数定义的外部)声明为static,那么它具有内部链接;否则(通常情况下),变量具有外部链接。
  • register存储类型,要求编译器把变量存储在寄存器中,而不是像其他变量一样保存在内存中。但是编译器还是可以把register型变量存储在内存中。register只对声明在块中的变量有效。**
    由于寄存器没有地址,所以对register变量缺乏auto变量取地址是非法的。**
  • 函数的存储类型:extern说明具有外部链接,默认也是外部链接。static使函数具有内部链接,所以在定义它的文件之外不能调用它。(但是不能阻挡函数指针进行间接调用.可以在static函数文件A中定义freturn()
    函数返回这个static函数指针f(),其他文件调用freturn()返回static函数指针(*f)(),实现了间接调用,参见1.c和2.c cc 1.c 2.c&& ./a.out)
  • 解释复杂声明
    • 始终由内往外读声明符。换句话说,定位声明的标识符,并且从此处开始解释声明
    • 在做选择时,始终使 [] 和 () 优先于 * 。如果 * 在标识符的前面,而标识符后面跟着[],那么标识符表示数组而不是指针。同样的,如果 * 在标识符前面,而标识符后面跟着()
      ,那么标识符表示函数而不是指针。(当然,可以使用圆括号来使 [] 和 () 相对于 * 的优先级无效)
  • 函数不能返回数组,但是可以返回指向数组的指针;函数不能返回函数,但可以返回指向函数的指针;函数型的数组不合法,但是数组可以返回包含指向函数的指针。
int(*f(int))[];     // int f(int)[] 不合法
int (*g(int))(int); // int g(int)(int) 不合法
int (*a[10])(int);  // int a[10](int) 不合法

int (*f(int v))[]
{
    static int arr[5] = {11, 22, 33, 44, 55};
    for(int i=0;i<5;i++){
        arr[i] += v;
    }
    return &arr;
}
// 调用例子
int (*res)[5] = f(10);
printf("%d %d\n",(*res)[0],(*res)[4]);
  • 具有自动存储期限的变量没有默认的初始值。不能预测自动变量的初始值,而且每次变量变为有效时值可能不同
  • 具有静态存储期限的变量默认情况下值为零。用calloc分配的内存是简单的给字节的位置零,而静态变量不同与此,它是基于类型的正确初始化,即整数变量初始化为0,浮点变量初始化为0.0,而指针则初始化为空指针。
  • 出于书写风格的考虑,最好为静态类型的变量提供初始化式,而不是依赖于它们一定为零的事实。
  • c99内联函数inline,建议编译器将代码内联编译。
inline double average(double a,double b){
    return (a+b)/2;
}
// average()具有外部链接,所以其他在其他源文件也可以调用它,但是它有内联定义,所以试图在其他文件中调用它将失败。
// 可以在前面加上static 变为内部链接,这样其他文件要使用average函数就可以自己定义。
  • c99的一般法则是,如果特定文件中某个函数的所有顶层声明中都有inline但没有extern,则该函数定义在该文件中是内联的。如果在程序的其他地方使用该函数(包含其内联定义的文件文件也算),则需要在另一个文件中为其提供外部定义。函数调用时,编译器可以选择进行正常调用(使用函数的外部定义)或者执行内联展开(使用函数的内联定义)。
// average.h
#ifndef AVERAGE_H
#define AVERAGE_H
inline double average(double a,double b){
    return (a+b)/2;
}
#endif

// average.c
#include "average.h"
extern double average(double a,double b);
  • 内联函数限制
    • 函数中不能定义可改变的static变量
    • 函数中不能引用具有内部链接的变量
  • 关于gcc,仅当通过-O命令行选项请求进行优化时,才会对函数进行"内联"
  • "作用域"和"链接"的区别是什么?
    • 作用域是为编译器服务的,而链接时为链接器服务的。
      编译器用标识符的作用域来确定在文件的给定位置访问标识符是否合法。当编译器把源文件翻译成目标文件时,它会注意到有外部链接的名字,并最终把这些名字存储到目标文件内的一个表中。因此,链接器可以访问到具有外部链接的名字,而内部链接的名字或无链接的名字对链接器而言是不可见的。
  • 在C语言中,const表示"只读"而不是"常量"。

19.程序设计

  • 模块:一组服务的结合。
  • 接口:描述提供的服务(头文件,包含那些程序中可以被其他文件调用的函数的原型)
  • 实现:模块的细节都包含在模块的实现中(源文件)
  • 将程序分割成模块有一系列好处:
    • 抽象:我们知道模块会做什么,但是不需要知道这些功能的实现细节。因为抽象的存在,我们不必为了修改部分程序而了解整个程序是如何工作的
    • 可复用性
    • 可维护性:将程序模块化后,程序中的错误通常只会影响一个模块的实现,因而更容易找到并修正错误
  • 高内聚性:模块中的元素应该彼此紧密相关。我们可以认为他们是为了同一目标而相互合作的。高内聚性会使模块更易于使用,同时使程序更易于理解。
  • 低耦合性:模块之间应该尽可能相互独立。低耦合性可以使程序更易于修改,并方便以后复用模块
  • 模块的类型
    • 数据池:相关变量或常量的集合。float.h limits.h。程序设计中不建议变量放在头文件中,但建议相关常量放在头文件中。
    • 库:相关函数的集合。string.h
    • 抽象对象。抽象对象是指对于隐藏的数据结构进行操作的函数的结合。(对象是一组数据以及针对这些数据操作的集合)
    • 抽象数据类型(ADT)。将具体数据实现方式隐藏起来的数据类型称为抽象数据类型。
  • 作为抽象对象的模块有一个严重的缺点:无法拥有该对象的多个实例。因为它的数据使用的是外部变量。
  • 抽象数据类型添加一个数据类型,传参进需要操作的函数。

20.底层程序设计

  • 为了可移植性,最好仅对无符号数进行移位运算。
  • 移位运算符的优先级比算术运算符的优先级低。
  • 按位求反~;按位异或 ^;
  • 优先级由高到低:~ > != (==) > & > ^ > |
  • 位的设置:i |= 1<
  • 位的清除:i &= ~(1<
  • 位的测试:if(i & 1<
  • 修改位域【连续几个位】:先清除 ,再设置
  • 读取位域:&读取
  • 结构体位域
struct file_date{
    unsigned int day:5;
    unsigned int month:4;
    unsigned int year:7;
};

// 方便获取短整数和文件日期的相互转换。机智
union int_date{
    unsigned short i;
    struct file_date fd;
};

void print_date(unsigned short n){
    union int_date u;
    u.i = n;
    printf("%d/%d/%d\n",u.fd.month,u.fd.day,u.fd.year+1980);
}
  • 大端序:左边存储高位;小端序:左边存储低位。
  • volatile类型限定符使我们可以通知编译器,程序中某些数据是"易变的"。volatile限定符通常使用在用于易变内存空间的指针的声明中:volatile char *p;
while(缓冲区未满){
    等待输入;
    buffer[i] = *p;
    if(buffer[i++]=='\n')
        break;
}

// 比较好的编译器可能会注意到这个循环既没有改变p,也没有改变*p,因此编译器可能会对程序进行优化,使*p只读一次。
// 优化后的程序不断复制同一个字符填满缓冲区,这不是我们要的程序。
在寄存器中存储*p;
while(缓冲区未满){
    等待输入;
    buffer[i] = 存储在寄存器中的值;
    if(buffer[i++]=='\n')
        break;
}

21.标准库

  • 任意包含标准头的文件必须遵守如下规则
    • 第一,该文件不能将头中定义过的宏的名字用于其他目的
    • 第二,具有文件作用域的库名(尤其是typedef名)也不可以在文件层次重新定义
  • 有一个下划线和一个大写字母开头或由两个下划线开头的标识符是为标准库保留的标识符。程序不允许为任何目的使用这种形式的标识符。
  • 由一个下划线开头的标识符被保留用作具有文件作用域的标识符和标记。除非在函数内部声明,否则不应该使用这类标识符。
  • 在标准库中所有具有外部链接的标识符被保留用作具有外部链接的标识符。特别是所有标准库函数的名字都被保留。因此,即使文件没有包含,也不应该定义名为printf的外部函数,因为在标准库中已经由一个同名的函数了。
  • 这些规则虽然并不总是强制的,但是不遵守规则可能导致程序的不可移植性。

22 输入/输出

  • 流(stream)表示任意输入的源或任意输出的目的地。
  • C程序中对流的访问是通过文件指针(file pointer)实现的。
  • 文本文件:字节表示字符,
    • 文本文件分为若干行
    • 文本文件可以包含一个特殊的"文件末尾"标记。windows的ctrl+z,大多数其他操作系统(包含unix)没有专门的文件末尾字符。
  • 二进制文件中:字节不一定表示字符;字节组还可以表示其他类型的数据,如整数和浮点数。
    • 二进制文件不分行,也没有行末标记和文件末尾标记,所有字节都是平等对待的。
  • 文件复制程序需要假定文件为二进制文件。
  • fopen打开文件,永远不要假设可以打开文件,每次都要测试fopen函数的返回值确认不是空指针。
    • 当打开文件用于读和写(模式字符串包含字符+)时,有一些特殊的规则。如果没有先调用一个文件定位函数,那么就不能从读模式转换成写模式,除非读操作遇到了文件的末尾。类似的,如果既没有调用fflush函数也没有调用文件定位函数,那么就不能从写模式转换成读模式。
  • 模式:读r(文件必须存在)、写w(没有文件则创建)、追加写a(没有文件则创建)、从文件头读写r+(文件必须存在)、从文件头读和写w+(没有文件则创建),追加读和写a+(没有文件则创建)。二进制加上b,如rb
FILE *f = fopen(const char* restrict filename,const char* restrict mode); 
int fclose(FILE *stream);
  • freopen函数为已经打开的流附上一个不同的文件。
freopen("foo.txt", "w", stdout);    // 标准输出重定向到foo.txt文件
freopen("/dev/tty", "w", stdout);   // unix恢复标准输出

freopen("foo.txt", "r", stdin);     // 从文件foo.txt读取标准输入 
freopen("/dev/tty", "r", stdin);    // unix恢复标准输入
  • 临时文件 FILE * tmpfile()
  • 文件缓冲:把写入流的数据存储在内存的缓冲区域内:当缓冲区满了(或者关闭流)时,对缓冲区进行”清洗“(写入实际的输出设备)。
    • setvbuf函数允许改变缓冲流的方法。满缓冲是默认设置
      • _IOFBF(满缓冲)。当缓冲区为空时,从流读入数据;当缓冲区满时,向流写入数据。
      • _IOLBF(行缓冲)。每次从流读入一行数据或者向流写入一行数据。
      • _IONBF(无缓冲)。直接从流读入数据或者向流写入数据,而没有缓冲区
  • 删除文件remove()和重命名文件rename()
  • fprintf()指定输出流输出。
  • fscanf()指定输入流读取
    • 字符*出现意味着屏蔽赋值。scanf("%*d%d",&i); // 12 34 ,i = 34
  • 每个流都有阈值相关的两个指示器,当打开流时会清除这些指示器。遇到文件末尾就设置文件末尾指示器,遇到读错误就设置错误指示器
    • 错误指示器(error indicator)
    • 文件末尾指示器(end-of-file indicator)
    • clearerr(fp) 会同时清除错误指示器和文件末尾指示器
    • 可以调用feof和ferror函数来测试流的指示器
  • 字符输入输出:出现错误返回EOF
    • fputc和putc ,putc是宏实现。putc(ch,fp)
    • putchar(‘A’)
    • fgetc和getc,getc是宏实现。getc(fp)
    • getchar(void)
    • ungetc(int c,FILE *stream):此函数把从流中读入的字符”放回“并清除流的文件末尾指示器。如果在输入中需要往前多看一个字符,那么这种能力可能会非常有效。
  • 行的输入输出
    • fputs(string,fp):写入流,不会自己写入换行符。错误返回EOF
    • puts(string):写入标准输出,会自己写入换行符。错误返回EOF
    • char * fgets(char * restrict s, int n, FILE * restrict stream); fgets会读入n-1个字符或者遇到换行符为止。错误返回空指针
    • char * gets(char *s);错误返回空指针
  • 块的输入输出
    • n = fread(a,sizeof(a[0]),size(a)/sizeof(a[0]),fp):返回实际读的元素(不是字节)的数量
    • fwrite(a,sizeof(a[0]),size(a)/sizeof(a[0]),fp)
  • 文件定位:每个流都有相关联的文件位置(file position)
    • fseek函数改变文件指针相关的文件位置。
      • SEEK_SET:文件起始处
      • SEEK_CUR:文件的当前位置
      • SEEK_END:文件的末尾处
      • 对于文本流而言,要么offset(第二个参数)必须为零,要么whence(第三个参数)必须是SEEK_SET,且offset的值必须通过ftell函数调用获得
    • ftell函数以长整数返回当前文件位置。如果fp是二进制流,返回以字节数来表明当前位置,零表示文件起始处。但是如果fp是文本流,ftell(fp)返回的不一定是字节数,因此最好不要对ftell函数的返回值进行算术计算。
    • rewind函数会把文件位置设置为起始处
    • 对于非常大的文件,采用另外两个函数fgetpos和fsetpos,返回值类型为fpos_t
  • 字符串的输入输出
    • sprintf函数把输出写入字符数组而不是流中
    • snprintf写入字符不会超过n-1,结尾的空字符不算;只要n不为0,都会有空字符
    • sscanf函数从字符串而不是流中读取数据。它相比于scanf和fscanf的好处之一是,可以按需要多次检测输入行,而不再只是一次。

23.库对数值和字符数据的支持

  • float.h中提供了用来定义float、double和long double类型的范围及精度的宏。
  • limits.h中提供了用于定义整数类型(包括字符类型)取值范围的宏。
// 用来判断int_max的大小,如果太小则报错
#if INT_MAX<100000
#error short int is too samll
#endif
  • math.h:当发生错误时,math.h中的大多数函数会将一个错误码存储到一个名为errno的特殊变量中。此外,一旦函数的返回值大于double类型的最大取值,math.h中的函数会返回一个特殊的值,这个值由HUGE_VAL宏定义(在math.h中)。
    • 定义域错误。函数的实参超过了函数的定义域,函数返回值由定义决定,同时EDOM(定义域错误)会被存储到errno中
    • 取值范围错误,函数的返回值超出了double类型的取值范围。如果返回值绝对值过大,函数会根据正确结果的符号返回正或负的HUGE_VAL。此外值ERANGE(取值范围错误)会被存储到errno中
    • 三角函数 acos cos tan
    • 双曲函数 cosh
    • 指数函数和对数函数
      • double exp(double x) 返回e的幂x的值
      • double frexp(double value,int *exp) 拆分浮点数为小数部分和指数部分,使得原始值等于f * 2^n,其中f为[0.5,1]或0。函数返回f,并将n存入第二个参数所指向的对象(整数)中。
      • double ldexp(double x,int exp) 将小数部分和指数部分组合成一个数。
      • double log(double x) 计算以e为底的对数
      • double log10(souble x) 计算以10为底的对数
      • double modf(double value,double *iptr) 将它的第一个参数分为整数部分和小数部分,返回其中的小数部分,并将整数部分存入第二个参数所指向的对象中。
      • 对于计算任意以a为底的对数b,可以用 log(b)/log(a) 求解
    • 幂函数
      • double pow(double x,double y) 计算x^y
      • double sqrt(double x) 计算平方根,和pow(x,0.5)值相等,但是sqrt函数比pow运行快多了,使用sqrt计算平方根更好
      • (c99)double cbrt(double x) 立方根,可用于负数,pow不能用于负数
      • (c99)double hypot(double x,double y) 计算x、y为边的直接三角形斜边长
    • 取整函数
      • double ceil(double x) 向上取整函数
      • double floor(double x) 向下取整函数
      • double fabs(double x) 绝对值
      • double fmod(double x,double y) x除以y所得的余数
    • 操作函数 c99
      • double copysign(double x,double y) 返回值为x,符号为y的符号
      • double nan(const char *tagp) nan函数将字符串转换为NaN值
    • 最大最小值 正差函数c99
      • double fdim(double x,double y) x>y : x-y 否则 +0
      • double fmax(double x,double y)
      • double fmin(double x,double y)
    • 浮点乘加 c99
      • double fma(double x,double y,double z) x*y+z
    • 比较宏 x,y都是实数,c99。如果任一操作数或两个是NaN,那么这样的比较可能导致抛出无效运算浮点异常,因为NaN的值被认为是无序的。比较宏可以避免这种异常。
      • int isgreater(x,y)
      • int isgreaterequal(x,y)
      • int isless(x,y)
      • int islessequal(x,y)
      • int islessgreater(x,y) 等价于 (x)<(y) || (x)>(y)
      • int isunordered(x,y) 其中至少一个为NaN时返回1,否则返回0
  • ctype.h 字符处理,字符分类,字符大小写转换
  • string.h 字符串处理
    • 复制函数
      • void *memcpy(void *dest,void *source,size_t n) 从源向目的地复制n个字符,如果源和目的的有重叠,函数行为未定义
      • void *memmove(void *dest,void *source,size_t n) 从源向目的地复制n个字符,如果源和目的的有重叠,也能正常工作
      • char *strcpy(char *dest,char *source) 如果源和目的的有重叠,函数行为未定义
      • char *strncpy(char *dest,char *source,size_t n)
        复制n个字符,如果n太小,结尾的空白符不会复制,如果太大,复制遇到空白符之后,会用空白符填充n个字符。如果源和目的的有重叠,函数行为未定义
    • 拼接函数
      • strcat
      • strncat 参数n会限制复制的字符数
    • 比较函数
      • int memcmp(const void *s1,const void *s2,size_t n) 比较两个字符数组的内容,不关心空白符,
      • int strcmp(const char *s1,const char *s2) 比较字符,遇到空白符停止
      • int strncmp(const char *s1,const char *s2,size_t n) 当比较字符达到n个或遇到空白字符停止
    • 搜索函数
      • void *memchr(const void *s, int c, size_t n) 如果没有搜索到c,搜索n的字符后停止
      • char *strchr(const char *s,int c) 如果没有搜索到c,搜索到首个空字符后停止
      • char *strrchr(const char *s,int c) 反向搜索字符
      • char *strpbrk(const char *s1,const char *s2) 返回参数s1中s2的任意一个字符匹配的最左字符
      • size_t strspn(const char *s1,const char *s2) 返回字符串s1中第一个不属于字符串s2的字符的下标
      • size_t strcspn(const char *s1,const char *s2) 返回字符串s1中第一个属于字符串s2的字符的下标
      • char *strstr(const char *s1,const char *s2) 字符串s1中搜索字符串s2,返回第一次出现s2的s1指针
      • char *strtok(char *s1,const char *s2) 会在s1中搜索不包含在s2的非空字符序列,包含在s2中的字符会替换为空白符
    • void *memset(void *s,int c,size_t n):将一个字符c的多个副本存储到s中
    • strlen返回字符串长度
char s2[] = "  April \t\t28,1998";
printf("--:%s--\n", strtok(s2, " \t"));    // --:April--
printf("--:%s--\n", strtok(NULL, " \t,")); // --:28--
printf("--:%s--\n", strtok(NULL, " \t"));  // --:1998--

24.错误处理

  • assert引入了额外的检查,因此会增加程序的运行时间。可以在包含了assert.h之前定义宏NDEBUG即可禁用assert
  • stdio.h的perror函数
  • string.h的strerror函数以错误码为参数,返回一个描述错误码的指针
  • singal.h 信号处理
  • 当有错误或外部事件发生时,我们称产生了一个信号
  • void signal(int sig,void (*func)(int))(int) 安装信号
  • 预定义的信号处理函数SIG_DFL和SIG_IGN
  • int raise(int sig):程序触发信号
  • setjmp.h 非局部跳转:一个函数直接跳转另一个函数
    • goto语句只能跳转同一个函数的某个标记处
    • int setjmp(jmp_buf env):标记一个位置
    • void longjmp(jmp_buf env,int val):跳转

26.其他库函数

  • stdarg.h 可变参数函数
    • va_list 类型
    • va_start(va_list ap,param) 从变量param之后的算是可变变量
    • va_args(va_list ap,类型)
    • va_end(va_list ap)
    • va_copy(va_list dest,va_list src) 复制va_list,这样就可以从中间某个位置开始
  • stdio.h 打印可变变量
    • vfprintf(FILE * stream,char * format,…)
    • vfscanf(FILE * stream,char * format,…)
  • stdlib.h 通用的实用工具
  • 数值转换函数
    • double atof(const char *nptr) , int atoi, long int atol, long long int atoll。不能指出转换错误字符
    • strtod strtol strtoul,会指出转换错误的字符起始位置,int型支持进制位数指定。
  • 伪随机生成序列函数
    • void srand(unsigned int seed) 种子值设置,相同的种子值获取的序列相同
    • int rand() 返回值为[0,RAND_MAX]之间的值
  • 与环境通信
    • exit(n) 在程序中任何位置执行此函数相当于return n,并且程序终止。程序终止时,它通常还会在后台执行一些最后的操作,包括清洗包含未输出数据的输出缓冲区,关闭打开的流,以及删除临时文件。
    • atexit(void (*func)(void)) 注册程序终止时调用的函数。
    • _Exit函数类似于exit函数,但是它不会调用atexit注册的函数,也不会调用之前传递给singal函数的信号处理函数。此外,它不需要清除缓冲区,关闭打开流,以及删除临时文件,是否会执行由具体定义实现
    abort类似于exit函数,但调用它会导致异常的程序终止,atexit注册的函数不会被调用。根据实现定义,它可能不需要清除缓冲区,关闭打开流,以及删除临时文件。abort函数返回一个由定义的状态码来指出”不成功的终止“。产生SIGABRT信号
  • 搜索排序函数
    • bserarch
    • qsort
  • 整数运算函数
    • abs
    • labs
    • llabs
    • div_t div(int numer,int denom) div.quot(商)div.rem(余数)
  • time.h 日期和时间
  • clock_t 按照时间滴答进行度量的时间值
  • time_t:紧凑的时间和日期编码
  • struct rm:把时间分解为秒,分,时等
  • clock_t clock(void):程序从开始执行到当前时刻处理器的时间
  • double difftime(time_t time1,time_t time2)
  • time_t mktime(struct tm *timeptr) :修正时间,例如tm_day大于31,会改为小于31,月数加1
  • time_t time(time_t *timer)
  • char *ctime(time_t *)
  • char *asctime(struct tm *)
  • struct tm *gmtime(time_t *) // 返回UTC时间
  • struct tm *localtime(time_t *) // 返回本地时间
  • size_t strftime(char *s,size_t maxsize,char *format,struct tm *timeptr) // 格式化时间输出

你可能感兴趣的:(Linux,c语言)