这本书描述的是C90,C语言的版本现在有四个,分别是C90,AMD1,C99,C11(排名分先后)。
C90是旧式C的继承者,同时是C++的基础。
为什么要读这本书?
深入而完整地了解关于指针的知识,避开指针带来的痛苦。
写读书笔记要写自己读这本书的收获,而非摘录这本书的一部分内容,读书笔记如果单纯摘录,不如重新读书。
快速上手(关于C语言最基础的知识)
- 注释的运用,使用预处理指令是更好注释掉代码的方式。
- 预处理指令定义常量,#define MAXN 1000
- #include文件中放置函数原型,并且可以避免重复声明。
- 检查来源不确定的数组下标是否越界
目录
基本概念
- 编译过程
2.本书最有价值的地方在于每一章后面的习题和问题,能做一定要做。
数据
- 声明指针 int* a,b,c; 只有a是指针,char *message= "helloworld"; 定义的是message。
- typedef char *ptr_to_char; ptr_to_char便是一个char * 类型的别名。而#define d_ptr_to_char char* 这种声明,则定义了d_ptr_to_char为char*的别名,但是d_ptr_to_char a,b;时只有a是指针,而b是字符。但typedef不同,封装了这种类型。
- int const * p;和int * const p;const修饰自身前面的一个符号。故依次是常量的指针和指针常量。
- 头文件的变量的作用域会扩展到加入的其他文件。
- 链接属性,不同的源文件中如果有相同名称的变量,为了处理这种问题,有了static和extern这两种方法,一个是只让一个变量被自己的文件中的方法使用,一个是让自己可以用其他源文件中的变量。重复的使用关键字定义只有首次有效。
- 任何代码块之外定义的变量属于静态变量,存储于静态内存,无法更改类型,运行前创建,运行期间始终存在,始终保持原来的值,除非赋给他们不同的值或程序结束。
- 代码块内部的变量存储于堆栈中,执行到声明自动变量的代码块时,自动变量才被创建,当代码结束,自动变量就被销毁。而且每次创建它不一定在同一块内存中,可以用static关键字把它变成静态变量。
- 形参不能是静态的,因为实参总是在堆栈中传递给函数,用于支持递归。
- 关键字rigister可以用于自动变量的声明,提示这个变量存储在寄存器中。
- static两种用法,作用域外external变internal,作用域内动态变量变静态变量。
语句
- switch只支持整数,而且case是入口,如果没有break会从一个case顺流到下一个case。
- 没有循环体的循环中,用一个分号让它独占一行。
操作符和表达式
- 移位操作符>> <<右移左移,两边都是整数,然后右移负数时是否保持符号不变(算术移位)或者变(逻辑移位)看编译器,而且移位位数不要是负数,容易出现问题。
- 位的操纵,
- value = value | 1 << bit_number;将value的第bit_number位置1,置零同理
- value = value & ~ (1<<bit_number);置零,~每一位都取非
- value & 1 << bit_number;测试第bit_number位是否为0,若不为0,则结果不为0
- 赋值的截断 :a=x=y+3;如果x是字符型变量,a是整型,那就有可能x赋值时被截取一部分导致a和x的结果不同。
- sizeof ( int );返回int的操作数的类型长度,字节为单位。sizeof x;返回变量x占据的字节数。
- 关系运算符&&具有短路性质。即有了结果便不会再去考虑后面的值。
- 三目运算符 ?:
- 使用逗号运算符可以将多条语句整合成一条:while(x<10) b+=x,x+=1;虽然这样并不好;
- 算数转换
.
指针
- 硬件事项:边界对齐,整型值存储的起始位置只能是某些特定的字节,通常是2或4的倍数。
- 关于内存:
- 每个位置都有独一无二的地址
- 每个位置都有一个值
- 名字(我们所说的变量)与内存位置之间的关联并不是硬件提供的,而是编译器提供的。
- 硬件仍然通过地址访问内存位置。
- 不能简单的通过检查一个值得位来判断他的类型,还需要观察这个值得使用方式。
- 使用指针进行解引用访问时必须小心指针是否分配了内存。初始化过。
- NULL指针
- 要让一个指针为NULL指针,可以赋值0,判断是否是NULL指针,与0进行比较。
- 对NULL指针解引用是非法的,印证了6。
- 对数组搜索某个值,如果返回的指针是NULL,表示没找到,如果不是NULL,那么就有第几个值了,有两个意思。
- 风格良好的程序会在指针解引用之前对它进行检查,这样的初始化策略会节省大量的调试时间。
- 一个很奇怪的无用表达式 *&a=1; 就是把1赋值给a。
- 直接使用内存的地址 应当先进行类型转换 然后进行解引用 *(int *) 100 = 5;将内存地址100的位置赋值为5;
- int a = 25;int *b = &a;int **c = &b; c是指针的指针,两次解除引用指向a,**c;
- 指针的运算
- 算术运算
- 指针 +/- 整数 :只能用于指向数组元素的指针,结果依然是个指针,要避免结果指针指向数组的边界之外,否则其效果是未定义的。
- 指针 - 指针 :只用于指向同一个数组的两个元素,相减的结果的类型是ptrdiff_t,一种有符号整数类型,结果是两个指针在内存中的距离,以数组元素的长度为单位。
- 关系运算(前提依然是指向同一个数组的元素)
- < <= > >=表达式将告诉你那个指针指向数组中更前或者更后的元素,标准未定义两个任意的指针进行比较会是什么后果。
- < <= > >=表达式将告诉你那个指针指向数组中更前或者更后的元素,标准未定义两个任意的指针进行比较会是什么后果。
- 算术运算
- 每章的总结和警告的总结和编程提示的总结和题目是这本书的精髓。
函数
- 函数声明(同一源文件前面已经出现该函数的类型)的作用:向编译器提供函数的参数数量和类型,和返回值的类型。
- 函数原型
使用原型的好方法
- 函数的缺省设定:如果函数没有原型,那么编译器调用该函数时默认参数正确,且认为函数默认返回整型的值,所有的函数都应该具有原型,尤其是那些返回值非整数的值,值的类型并不是值的本质属性,而是值的解释方式。如果函数返回的是非整型值,那么结果通常不正确。
- 函数传值:c语言所有参数均以“传值调用”方式进行传递。
传参后是拷贝的值,修改或怎么样都不影响原来的值
- ADT(abstract data type): 又被称为黑盒设计,ADT的基本思想是模块具有功能说明和接口说明,前者说明模块所执行的任务,后者定义模块的使用。模块的用户不知道模块实现的任何细节,除了定义好的接口之外,用户不能以任何方式访问模块。static关键字来限制对非接口的函数和数据的访问
接口定义在头文件中,而实现的程序文件中除了接口还有static限制访问的函数和数据
- 递归:
- 追踪递归函数:通过对每次递归后分属不同次的变量区分开来,追踪它们在堆栈中的状态,关键在于理解递归过程中函数声明的变量是如何存储的
- 递归与迭代:如果递归调用是函数的最后一项任务,那么这个递归可以转化成迭代从而提高效率。
这里用迭代来计算斐波那契数列比用递归效率提高了几十万倍
- 可变参数列表:stdarg.h
- 类型va_list和三个宏va_start,va_arg,va_end;
- 声明一个var_arg的变量,它用于访问参数列表的未确定部分。
数组
- 数组名是一个指针常量,他的类型取决于数组的类型,指向第一个数组元素。
- 数组具有确定数量的元素,而指针只是一个标量值。编译器用数组名来记住这些属性,只有数组名在表达式中使用时,编译器才会为它产生一个指针常量。
- 是指针常量,而不是指针变量。
- C的下标引用和间接访问表达式是一样的,
ap[0]对等的表达式为*(ap+(0)),等同于array[2]; 同理, ap[-1]便是array[1]。
- C语言的下标引用可以作用于任意的指针,而不仅仅是数组名。作用于指针的下标引用的有效性既依赖于该指针当时恰好指向什么内容,也依赖于下标的值。
- 数组指针比下标更有效率的场合: 当你在程序中一次一步(或某个固定数字)地移动时,与固定数字(数据类型的字节数)相乘的运算在编译时完成,所以运行时需要的指令就少一些。
- 数组和指针并不是相等的
- 数组名是一个指针常量,不可以被修改,但是数组名作为实参调用了某一个函数过后传递过去的便成为了一个指针变量,具有相同的值,但是可以被修改,而且不会影响到原先作为实参调用函数的数组名的值。
- 形参写一个char const * string。对于不打算修改原string值的函数来说,这样写既是良好的文档习惯,也帮助编译器捕捉到任何试图修改这个数据的意外错误。而且这类声明允许向函数传递const参数。
- 函数原型中的一维数组形参
无须写明它的元素数目,因为函数并不为数组分配内存,它只是一个指针,指向其他地方已经分配好内存的空间。
- 数组的初始化方式取决于它们的存储类型,存储于静态内存的数组只初始化一次,在程序开始执行之前,对于自动变量(位于任何一个作用域之内的变量)来讲,因为自动变量运行时存储在堆栈中
在函数内部或者作用域的数组,要权衡得失,每次都对数组初始化是不是值得,如果答案是否定的,可以把数组声明为static,这样数组初始化只需要在程序开始前执行一次。
- 若未给出数组的长度,编译器就把数组的长度设置为刚好能容纳所有设置的初始值的长度。经常修改数组长度这个很有用
- 字符串常量和字符数组
message1是字符串变量,也就是字符数组,message2是字符串常量,内容不可被修改。
- 多维数组的存储形式:int array[3][6];
- 多维数组的数组名指向第一个元素,例如
等价于
- int matrix[3][10] ; int (*p)[10]=matrix; 使p指向matrix的第一行,声明一个指向整型数组的指针。可以在p上对它进行各种运算,
- 多维数组作为函数参数
参数类型是以下的两种之一。
- 二维数组数组名是指向一个含有n个元素数组的首个数组的指针,一维数组数组名是指向一个数组首个元素的指针,其实都是指向首个元素的指针,只是一维和二维的数组的首个元素并不相同。
- 多维数组初始化,不完整的初始化元素列表,省略掉的是尾部的几个元素,缺省为0。
- 第一维的长度可以省略掉
- 指针数组 int *api[10]; 下标引用的优先级高于间接访问
api肯定是一个数组,它的元素类型是指向整型的指针。使用指针数组的例子
- 指针数组
如下图
- 矩阵存储
- 指针数组
- 总结一定要看啊,太重要了!!!
字符串、字符和字节
- 字符串就是一串零个或多个字符,以一个位模式为全0的NUL字节结尾。
- 标准保留了所有以str开头的函数名,用于标准库将来的扩展。
- 不受限制的字符串函数
- 复制字符串
- 连接字符串
- 返回值
- 字符串比较(按照字典序)
- 复制字符串
- 长度受限的字符串函数
- 查找一个字符
- 查找任意几个字符
- 查找一个子串
- 高级字符串查找
- 查找字符串前缀
- 查找标记
- 查找字符串前缀
- 字符分类和转换
- 内存操作
- 总结