通用总结:
- 如何判断一段代码的效率是否要更好,比较不同版本生成的汇编指令数量,越少越好。但是不能刻意为了效率好导致代码可读性很差!
- c语言中,静态内存编译器一般会对变量初始化为0.动态内存由于编译时不能确定所以不会自动帮助初始化。
- 无符号数字类型不适合减法运算后进行比较,因为不存在负值
- 数组名是一个常量指针,不能作为左值适用
- 每章总结后面的警告总结和编程提示的总结,重点关注。
第一章 简介
要从逻辑上删除一段c代码,更好的办法是使用#if预处理指令。
例如
#if 0
statements
#endif
预处理指令由预处理器解释。预处理器读入源代码,根据预处理指令对其进行修改,然后把修改过的源代码递交给编译器。
第二章 基本概念
第三章 数据
杂项知识点:
c语言中基础类型只有,整形,浮点,指针,复合类型。其他的类型都是在此基础上的包装。
**char **和 枚举 本质上也是整形。
整形字面量后加字母表示具体类型。例如 1L(long) 2U(unsigned)
指针常量与非指针常量在本质上是不同的。在c语言中把指针常量表达为数值字面值的形式几乎没有用处,所以c语言内部并没有特地的定义这个概念。(数值字面值例如:oxff2044ec)
指针的声明*号应该靠近变量,而不是靠近类型。比如
int *a,b,c;
a是指针,b和c是整形变量
c语言中编码的声明不写类型默认是整型。
c语言编译器可以确认4种不同类型的作用域:文件作用域,函数作用域,代码块作用域,原型作用域。
字符串常量:
双括号包围,\0这个null字符结尾。之所以是\0这个空字符是因为它是非打印字符。空字符里面也还有\0这个null字符。字符串常量c语言本身没有说明不能修改,但是很多编译器不允许修改。即便是char指针指向的也不能进行修改。*
字符串常量本身就是一个指向字符串的指针。如果一个字符串常量出现于一个表达式中时,表达式所使用的值就是这些字符所存储的地址,而不是这些字符本身。
链接属性:
标识符的连接属性决定如何处理在不同文件中出现的标识符。标识符的作用域与它的链接属性有关,但这两个属性并不相同。
链接属性一共有3中:
external(外部) :外部 链接属性的标识符不论声明多少次,位于几个源文件都表示同一个实体。
internal(内部):内部链接属性的标识符在同一个源文件内的所有声明中都指同一个实体,但位于不同源文件的多个声明则分属不同的实体。
none(无):没有链接属性的标识符总算被当作单独的个体,也就是说该标识符的多少个声明被当作独立不同的实体。
关键字extern和static用于在声明中修改标识符。如果某个声明在正常情况下具有external链接属性,在它前面加上static关键字可以使它的链接属性变为internal。
当extern关键字用于源文件中一个标识符的第一次声明,如果作用于该标识符的第二次或以后的声明,它并不会更改由第一次声明所指定的熟悉链接。
属于文件作用域的声明在缺省情况下为external链接属性。
例子:
demo1.c
#include <stdio.h>
int x;
int main(void) {
// extern int x;
printf("%d \n",x);
print();
return 0;
}
demo2.c
int x =10;
void print(void) {
printf("hello world!\n");
}
输出:
10
hello world!
这个例子表现出extern链接属性的功能。在c++中,引用的这个x前需要明确写明extern关键字,不然编译器提示重复定义。
存储类型:
变量的存储类型是指存储变量值的内存类型
c语言中有三个地方可用于存储变量:普通内存,运行时堆栈,硬件寄存器。分别对应静态变量,自动变量,寄存器变量。
在代码块外声明的变量总算静态变量,代码块内的是自动变量,代码块内被register标识的被声明为寄存器变量(不应该声明太多,太多编译器只会使用先标识的几个)。
在方法内变量通过加static关键字可以把自动变量变成静态变量,修改变量的存储类型并不表示修改该变量的作用域。
初始化:
c语言中,内置类型变量是否自动初始化取决于变量定义的位置。函数体外定义的变量初始成0;函数体内定义的变量不进行自动初始化。除了用作赋值操作的左操作数,其他任何使用未初始化变量的行为都是未定义的,不要依赖未定义行为。
c++语言中,内置类型和c一样。类类型变量在定义时,如果没有提供初始化式,则会自动调用默认构造函数进行初始化(不论变量在哪里定义)。如果某类型没有默认构造函数,则定义该类型对象时必须提供显示初始化式。
static关键字:
当用于不同的上下文环境时,static关键字具有不同的意思。(注意理解链接属性)
当它用于函数时,或用于代码块之外的变量时,static关键字用于修改标识符的链接属性,从external改为internal,但标识符的存储类型和作用域不受影响 。用这种方式声明的函数或变量只能在声明它们的原文件中访问。
当它用于代码块内的变量声明时,static关键字用于修改变量的存储类型,从自动变量修改为静态变量,但变量的链接属性和作用域不受影响。用这种方式声明的变量在程序执行之前创建,并在程序的整个执行期间一直存在,而不是每次在代码块开始执行时创建,在代码块执行完毕后销毁。
第四章 语句
杂项知识点:
c语言并不具备布尔类型,而是用整型来代替。这样,表达式可以是任何能够产生整型结果的表达式---零值表示“假”,非零值表示“真”。
但是在某些情况下判断两边都是整型不好理解到底是判断数值还是判断布尔值,为了解决这种模糊的情况,布尔值的最佳实践方法:
c#define FALSE 0
#define TRUE 1
...
if(flag == FALSE)...
if(!flag)...
if(flag == TRUE)...
if(flag)...
通过这种方式表示布尔值。上面的例子2种写法看似等价,但是如果flag设置为任意的整型值,那么第二对语句就不是等价的,只有当flag确实是TRUE或者FALSE,或者是关系表达式或逻辑表达式的结果值时,两者才是等价的。
为了解决这些问题,就是避免混合使用整型值和布尔值。最好不要使用第二种写法
c拥有所以你期望的关系操作符,但它们的结果是整型值0或1,而不是布尔值“真”或“假”。关系操作符就是用这种方式来实现其他语言的关系操作符的功能。
第五章 操作符和表达式
位移操作符注意事项:
1.位移操作 右边不能出现负数。
赋值操作符注意事项:
在下面的语句中,任务a和x被赋予相同的值的说法是不正确的:
a = x = y + 3;
如果x是一个字符型变量,那么y+3的值就会被截去一段,以便容纳于字符类型的变量中。那么a所赋的值就是这个被截短后的值。在下面这个常见的错误中,这种截短正事问题的根源:
char ch;
...
while((ch = getchar()) != EOF)...
EOF需要的位数比字符型值所能提供的位数要多,这也是getchar()返回一个整型值而不是字符值的原因。然而,把getchar的返回值首先存储于ch中将导致被截短。然后这个被截短的值被提升为整型并与EOF进行比较。根据读入的字符集是否是有符号会有不一样的结果。有符号就终止,无符号会停不下来。
a += b 操作符有时会比 a = a + b编译器运行时编译器效率更好一点,而且书写也更简便
sizeof操作符:
sizeof(int);
sizeof x;
第一个表达式表示返回整型变量的字节数。
第二个表达式返回变量x所占据的字节数。
逗号操作符:
1.while(int i = 0, i+=6, i< 10);
2.while(x < 10)
b += x,
x += 1;
逗号操作符2个举例,逗号操作符就是按逗号顺序进行执行这一行语句。
下标引用:
array[下标] 等于 *(array + 下标), c语言里下标引用就是使用间接访问符实现的。
2[array];//符合语法规则
array[2];
*(a + (array));//三个个语句语义相等
左值和右值:
通过第五章和第六章的阅读,左值就是油明确存储地址的值,右值是没有明确存储地址。左值可以作为右值使用。不同的符号在左右不一样的位置含义不同。
举例
a + b;//右值,计算出的结果不知道存储地址
&c;//左值,本身表示存储内容的地址
int *a;//左值,变量本身就是存储内容的东西,值为指向a的存储地址。
++a;//右值,表示a变量地址后一个地址拷贝,但是这个拷贝的具体是哪个地址,不知道
*++a;//左值,表示a变量地址后一个地址拷贝的值,就是a地址后面的那个地址。右值时表示a变量地址后面地址存储的具体内容
具体参考第六章6.11指针表达式
第六章 指针
未初始化和非法指针
定义后未初始化的指针可能会产生非法访问内存异常或者无意修改别的内存地址导致的问题。
非法指针举例
int *a;
...
*a = 12;//把12存储在a所指向的内存地址。但是a指向哪个地址?
NULL指针注意事项参数数据6.6节
第七章 函数
函数的参数
c语言的规则:所有参数都是传值调用。如果是数组会转换成对应的指针,但是传递的指针也是一份拷贝。此处需要记住2个规则:
1.传递给函数的标量参数是传值调用的。
2.传递给函数的数组参数在行为上就像它们是通过传址调用的那样。
(修改指针所指向的地址不影响传入的实际指针,但是修改指针指向的值,会影响传入的实际指针指向的值,c++应该也是一样的规则)
第八章 数组
在c语言中,数组使用指针在有些时候要比使用下标效率会高一点,因为下标会转换成指针形式。但是并不是说使用指针比下标好,只有在正确使用的情况下会好一点。
虽然数组可以给指针赋值,但指针和数组不是相等的概念。他们在本质的概念上 不同。数组是个固定大小的内存段,指针是指向别的地址的一个固定内存。
声明数组参数:
在函数中参数是数组,正确的声明方式是声明为指针,因为数组实参实际传递的就是数组头指针的拷贝。
int strlen(char *string);
在函数中由于使用的是指针传参,所以无法知道数组的真正长度。如果函数需要知道数组的长度,它必须作为一个显示的参数传递给函数。
数组的初始化:
静态数组如果未被初始化,数组元素的初值将会自动设置为零。当文件被载入到内存中准备执行时,初始化后的数组值和程序指令一样也被载入到内存中。因此当程序执行时,静态数组已经初始化完毕。
动态数组由于是存储在堆栈中,只有运行时才能确定,编译器无法在编译时帮它初始化,所以如果代码中未初始化,运行时就未初始化。如果初始化量大,建议加static,这样只初始化一次,提高每次调用效率。
c语言中数组初始化列表如果超过数组声明大小,视为非法。如果不够后面的自动认为是0。(c++规则一样)
字符数组初始化:
char message[] = {'h','e','l','l','o'};//这么书写太麻烦
char message2[] = "hello";//上下文是数组的时候会认为是初始化列表,而不是字符串常量
char *message3 = "hello";//如果是指针,此处会认为是字符串常量
多维数组的内存存储实现:
多维数组在内存中是一段连续内存地址,大小为维度的乘积。明白这点后,别与理解下面的例子
int m[2][2] = {1,2,3,4};
int a = m[0][2];//取出来的值为3,原因就是多维数组是连续的内存地址,所以取到的是下一维度的值
*(matrix + 1) + 5 //等于 &matrix[1][5]
*(*(matrix + 1) + 5))//等于 matrix[1][5]
函数参数的多维数组
void func2(int (*mat)[10]);//正确 多维数组第一维可以不需要长度,第二维必须写明长度 第一种写法是数组指针的形式, 指针数组是没有括号 int *p[10];
void func2(int mat[][10]);//正确
void func2(int **mat);//这种写法错误,指向指针的指针和指向数组的指针不是一回事
指针数组
定义方式 int *p[10],指针数组指的是一个包含一组指针的数组。
数组指针 int (*mat)[10],数组指针指的是一个数组的指针引用。
指针数组和数组指针是完全不同的两中东西,没有任何关系,代表不同的事物。
指针数组常用于字符串数组的表示当中。
第九章 字符串
字符串函数使用注意事项
strlen():该函数返回类型是size_t,这个类型是在头文件stddef.h中定义的,它是衣蛾无符号整数类型。
if (strlen(x) - strlen(y) >= 0)... //由于是无符号类型,表达式永远为真
字符串拷贝和字符串连接函数使用前要注意目标容器大小足够使用。
字符串函数的使用时要仔细查看说明,防止理解错误导致异常问题。
字符串常量
字符串常量是不能进行修改的。如果用一个字符串常量给一个char进行初始化,这个char不能修改里面的内容,因为是常量。但如果字符串常量给一个char数组进行初始化,这个常量会解析为初始化列表而不是字符串常量。
字符串常量也是一个常量指针
"xyz"+1;//表示指针移动一个位置,指向y字符
*"xyz";//解地址操作,结构为x字符
"xyz"[2];//下标操作,表示z字符
"0123456789ABCDEF"[vlaue % 16];//把数字转换成16进制字符表示
第十章 结构和联合
结构体声明:
//标准声明方式例子
struct SIMPLE {
int a;
int b;
int c;
}x,y;
//使用
struct SIMPLE z;
struct SIMPLE *d;
//推荐声明方式,在使用时不用加struct关键字
typedef struct {
int a;
char b;
float c;
} Simple;
//使用
Simple x;
Simple *y;
结构的自引用:
strcut SELF_REF1 {
int a;
struct SELF_REF1 b;//这种写法非法,因为递归持有SELF_REF1,无限循环
int c;
}
strcut SELF_REF1 {
int a;
struct SELF_REF1 *b;//这种写法正确,此处指针指向SELF_REF1类型,不会递归无限引用
int c;
};
//错误写法
typedef struct {
int a;
SELF_REF3 *b;//SELF_REF3在末尾才定义,所以在结构声明的内容它尚未定义
int c;
}SELF_REF3;
//正确写法
typedef struct SELF_REF3_TAG{
int a;
SELF_REF3_TAG *b;
int c;
}SELF_REF3;
不完整的声明:
如果每个结构体都引用了其他结构的标签,哪个结构应该首先声明?为了解决这个问,出现不完成的声明。
struct B;//不完成声明
struct A {
struct B *b;
};
struct B {
struct A *a;
}
结构体初始化:
结构的初始化方式和数组的初始化类似。一个位于一对花括号内部,由逗号分隔的初始化值列表可用于结构哥哥成员的初始化。
typedef struct {
int a;
char b;
float c;
} Simple;
struct INIT_EX{
int a;
short b[10];
Simple c;
}x = {
10,
{1,2,3,4,5},
{25,'x',1.9}
}
结构指针注意事项:
1.由于结构指针指向的数结构体数据,内部内存比较复杂,所以指针的加减运算不适用。
2.结构体访问不同类型成员的规则和注意事项参数书籍第十章10.2节
结构体的存储分配:
结构的实际存储内存大小跟系统的字节对齐有关,一般都会比实际的成员大小加起来大。例如:
struct ALING {
char a;
int b;
char c;
};
struct ALING2 {
int b;
char a;
char c;
};
printf("%d----%d\n", sizeof(struct ALING), sizeof(struct ALING2));
//结果 12----8
出现这个结果的原因就是字节对齐时,由于类型的不同,在进行的填充。sizeof()返回的是实际包含填充后的内存大小。
**因此,结构体的声明时,让那些对边界要求最严格的成员首先出现,对边界要求最弱的成员最后出现,可以节省内存。其次应该让同一类型排列在一起,不要分开排列。(要求严格的一般为基础类型,符合类型一般属于要求弱) **
位段:
位段的声明和结构类型,但它的成员是一个或多个位的字段。这些不同长度的字段实际上存储于一个或多个整形变量中。
首先,位段成员必须声明为int,signed int或 unsigned int。其次,在成员名的后面是一个冒号和一个整数,这个整数指定该位段所占用的位的数目。(建议用 signed 或者unsigned 明确表示有无符号,注重可移植的程序避免使用段位。c++中也叫位域)
struct CHAR {
unsigned ch :7;
unsigned font :6;
unsigned size :19;
}
struct CHAR chl;
十一章 动态内存分配
动态内存分配函数
malloc:分配一个要求的连续内存。申请失败返回NULL
free:释放分配的内存
calloc:分配一个要求的连续内存,并初始化为0。申请失败返回NULL
relloc:修改原先已经分配的内存块大小。如果它用于扩大一个内存块,那么这块内存原先的内容依然保留,新增加的内存添加到原先内存块的后面,新内存并未以任何方法进行初始化。如果它用于缩小一个内存块,该内存块尾部的部分内存便被拿掉,剩余的依然保留。如果原先的内存块无法改变大小,realloc将分配另一块正确大小的内存,并把原先那块内存的内容复制到新的块上。因此,在使用realloc之后,你就不能再使用指向旧内存的指针,而是应该改用realloc返回的新指针。
使用sizeof计算数据类型的长度,提高程序的可移植性。
十二章 使用结构和指针
十三章 高级指针话题
函数指针
在初始化函数指针时,对应的函数必须先声明,不然编译器无法对比类型是否一致。
在c语言中,函数名被使用时总是由编译器把它转换为函数指针。
函数指针的使用常用场景:1.作为参赛传递给函数 2.用于转换表
转换表
创建一个转换表需要2个步骤。首先,声明并初始化一个函数指针数组。唯一需要留心之处就是确保这些函数的原型出现在这个数组的声明之前。
第二个步骤是用下面的这条语句替换switch语句。
switch(oper) {
case ADD:
result = add(op1, op2);
break;
case SUB:
result = sub(op1, op2);
break;
case MUL:
result = mul(op1, op2);
break;
case DIV:
result = div(op1, op2);
break;
}
double add(double, double);
double sub(double, double);
double mul(double, double);
double div(double, double);
...
double (*oper_func[])(double, double) = {
add,sub,mul,div,...
}
result = oper_func[oper](op1, op2);//代替switch条件语句
注意事项:
在转换表中,越界下标引用就像在其他任何数组中一样是不合法的。
十四章 预处理器
预定义符号
__FILE__ 进行编译的源文件 “name.c”
__LINE__ 文件当前的行会 12
__DATE__ 文件被编译的日期 "Jan 31 1997"
__TIME__ 文件被编译的时间 "18:04:30"
__STDC__ 如果编译器遵循ANSIC,其值就为1,否则未定义 1
#define
正式写法 #define name stuff
参数写法 #define name(parameter-list) stuff
有了这条指令以后,每当有符号name出现在这条指令后,预处理器就会把它替换成stuff。允许把参数替换到文本中,参数列表由逗号隔开,参数与名称之间不能有空格,有空格会被理解为stuff的一部分。参数是存文件替换,与执行时的结果要看表达式具体运算。一般约定宏定义的名称都为大写用来与普通的函数区别
#define reg register
#define do_forever for(;;)
#define CASE break;case//switch 替换使用
#define DEBUG_PRINT print("File %s line %d:" \
"x=%d,y=%d,z=%d", \
__FILE__, __LINE__, \
x,y,z)//如果多行使用 \ 反斜杠链接,不要加分号;否则替换的文本处如果也写分号,最终就会多一个,变成2个语句
//调试时打印信息使用
x *= 2;
y += x;
z = x * y;
DEBUG_PRINT;
#define SQUARE(x) x * x
a = 5;
SQUARE(a + 1);//执行结果为11,不是36
#define SQUARE(x) (x * x)//在使用数值表达式进行求值的宏定义都应该加上括号
#define PRINT(VALUE) print("The value is" #VALUE)//#argument可以把一个宏参数转换为一个字符串, 预处理会自动把字符串进行连接
//##结构则执行一种不同的任务,它把位于它两边的符号连接成一个符号。
#define ADD_TO_SUM(sum_number, value) \
sum ## sum_number += value
ADD_TO_SUM(5,23);//实际含义为 sum5 += 25;
宏与函数
宏非常频繁的用于执行简单的计算,比如在两个表达式中寻找较大(较小)的一个。宏没有类型的概念,定义的函数可以适用于很多类型,但不适合用于复杂的函数替代。
带副作用的宏参数
当宏参数在宏定义中出现的次数超过一次时,副作用就是表达式求值时出现的永久性效果。例如:
x + 1//可以重复执行几百次,它每次获得的结果都是一样的
x++//有副作用,x值会增加
#undef
用于移除一个宏定义。如果一个现存的名字需要被重新定义,那么它的旧定义首先必须用#undef移除
写法 #undef name
条件编译 #if ... #endif
规范写法
if constant-expression
statements
endif
使用条件编译,你可以选中代码的一部分是被正常编译还是完全忽略。
constant-expression 常量表达式,由预处理器进行求值。如果值非0(真),那么statements部分就被正常编译,否则预处理器就安静地删除它们。如果表达式在编译时不能确定是具体的某一个常量值,编译时是不可预测的。
elif和#else是条件分支语句,类似 else if ...else
#ifdef和#ifndef 是否被定义
ifdef 测试一个符号是否已被定义。
ifndef 测试一个符号是否没被定义。
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
每对定义的两条语句是等价,但#if形式功能更强。
#include
编译器支持两种不同类型的#include 文件包含:函数库文件和本地文件。
标准库文件以一个.h后缀结尾。
函数库文件包含语法:
#include <filename>
本地文件包含语法:
#include "filename"
本地文件编译器一般会在本地进行查找,如果找不到就去函数库标准位置进行查找。
#error
允许你生产错误信息。写法:
#error text of error message
十五章 输入\输出函数
本章内容较多,很多东西需要时还是查看书籍原文
io流输入输出基本都使用缓冲区进行读写操作
EOF表示文件流结尾,实际值比一个字节要多几位,防止二进制值被错误的解释
15.5小节 流I\O总览,方便理解流程
打开流
fopen函数打开一个特定的文件,并把一个流和这个文件相关联。
FILE* fopen(char const *name, char const *mode);
mode类型:
r:文本读取 rb:二进制读取
w:文本写入 wr:二进制写入
a:文本添加 ab:二进制添加
a+:表示该文件打开用于更新,并且流既允许读也允许写。但是,如果你已经从该文件读入了一些数据,那么在你开始向它写入数据之前,你必须调用其中一个文件定位函数(fseek,fsetpos,rewind)。在你向文件写入一些数据之后,如果你又想从该文件读取一些数据,你首先必须调用fflush函数或者文件定位函数之一。
如果fopen指向成功,它返回一个FILE结构指针,该结构代表这个新创建的流。如果失败返回NULL指针,errno会提示问题的性质。
fopen函数用于打开(或重新打开)一个特定的文件流。
FILE *freopen (char const *filename, char const *mode, FILE *stream);
最后一个参数就是需要打开的流。它可能是一个先前从fopen函数返回的流,也可能是标准流stdin、stdout、或stderr。
这个函数首先试图关闭这个流,然后用指定的文件和模式重新打开这个流。如果打开失败,函数返回一个NULLZ值。如果打开成功,函数就返回它的第3个参数值。
关闭流
int fclose(FILE *f);
fclose函数在这个文件关闭之前刷新缓冲区。如果它执行成功,fclose返回零值,否则返回EOF。
15.8小节 字符i\o 说明为什么getchar标准输入函数返回为什么是int,而不是char
撤销字符i\o
ungetc函数把一个先前读入的字符返回到流中,这样它可以在以后被重新读入。
int ungetc(int character, FILE *stream);
“退回”字符和流的当前位置有关,所以如果用fseek,fsetpos或rewind函数改变了流的位置,所有退回的字符都将被丢弃。
15.10小节 格式化的行i\o 描述了格式化字符串的格式规则
十六章 标准函数库
与15章类似,本章包含很多函数使用,需要使用时参考原文
整型函数 字符串转换 排序和查找 使用stdlib.h(头文件里还有很多别的标准函数)
浮点型函数 使用math.h
如果一个函数的参数不在该函数的定义域之内,称为定义域错误。
如果一个函数的结果值过大或过小,无法用double类型表示,这称为范围错误。
日期和事件函数 使用time.h
十七章 经典抽象数据类型
本章具体数据类型实现阅读原文实现