又到了一年一度的金三银四换坑的日子,这本书虽然不敢说对得起“深度”二字,但是对于即将去笔试面试的求职者给予不少的帮助,重要地是短时间内能够知道很多要考的细节,有很多你平时没有注意到的很多知识点都能够从中获取。
-
二进制中数据是以补码的形式存储,补码是绝对值的原码取反得到反码,然后+1得到。
对于-0和+0,
十进制数 | 原码 | 反码 | 补码 |
---|
+0 | 00000000 | 00000000 | 00000000 |
-0 | 10000000 | 11111111 | 00000000 |
之所以补码相同是应为 -0
的反码 +1
后溢出了,最高位会被去掉,所以补码相同。
-
有符号和无符号之间运算,会将有符号转换成无符号再运算。
-
float变量与0比较,不能采用 ==
,因为浮点数是有精度,应该采用范围比较,比如
if((i<0.000001) && (i> -0.000001))
同样也不要在一个很大的浮点数和很小的浮点数之间运算。
-
判断指针是否为空,比较好的写法:if(NULL ==p); if(NULL != p);
。
-
空语句建议这样写,能突出该语句为空语句:NULL; // ARM中NOP指令
-
if
语句在做判断的时候,将出现概率比较高的语句放在前面,可读性和性能都会提高。
-
case
语句的排列选择,为了可读性,按数字大小排,为了性能,按出现概率排。
-
switch
中的 default
语句处理默认情况,通常是用来处理错误,所以不要偷懒去把 case
的语句放在default
处理。
-
在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨切循环层的次数。
-
空指针类型void*
定义的指针,任何类型的指针无需强制类型转换即可赋值给它(赋值后还是void
*型,还可以被别的指针类型赋值)。但是空指针不能直接赋值给其他指针。(可以说 void
涵盖的范围更大)
-
函数默认返回值为 int
型。
-
ANSI标准,不能对 void
指针进行算法操作,比如:
void *i;
i++;
-
如果函数的参数可以是任意类型指针,那么应声明其参数为 void *。
-
void不能代表一个真实的变量(C语言要求类型),是一个抽象的概念,可以理解为一个可以动态变换的盒子,你可以赋值各种指针类型,但是要使用它,你必须强制类型转换成固定的类型。
-
return语句不可返回指向“栈内存”(比如局部定义数组地址)的“指针”,因为该内存在函数体结束时被自动销毁。
-
const定义只读变量,具有不可变性,但其仍是变量,所以不能用来定义数组,下面的写法是错误的。
const int i = 10;
int a[i];
-
若使用 const
修饰函数的参数和返回值,当为指针时,有意义,一般数值没有意义(因为传入的是副本内容)当返回为 const
指针时,表示对返回指针所指向的数据内容不要进行修改,有修改则程序会报错!
-
volatile
关键字是一种类型修饰符,用它修饰的变量表示可以被某些编译器,未知的因素更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
volatile int i;
volatile
关键字告诉编译器 i
是随时可能发生变化的,每次使用它的时候必须从内存中取出i
的值,因而编译器生成的汇编代码会重新从i
的地址处读取数据放在 i
中。这样看来,如果是一个寄存器变量或者表示一个端口数据或者是多个线程的共享数据,就容易出错,所以说 volatile
可以保证对特殊地址的稳定访问。
如果不加可能会被优化,int i = 10; int j = i; int k = i;
// 在后两句中,由于i
没有被用作左值,所以两次赋值会可能被优化使用同一个数据(而不是每次重新从内存中取),这样的做法提高了效率,若是在两个语句之间该值被改变,就不能得到新的值。
-
const volatile int i=10;
中的i是什么属性?存在的意义何在?
使用 const
告诉编译器,你的程序不应该修改他的值。(并不代表别的非你的程序的东西不会修改)
volatile
告诉编译器,其值是很容易发生改变,不要做太多优化。
两者并不矛盾,因为该值可能会被非你的程序改变,比如被一些硬件设备改变(单片机中寄存器),这个值也是容易改变的,但是不应该被你的程序改变。
-
一个空的结构体变量的大小是1byte
。(因为编译器认为每个变量应该有大小,而 1byte
是最小的)
-
union
和 struct
类似,但是所有的成员共用一个结构体,其大小等于最大成员的大小。注意大小端模式的影响。
-
储存模式:
大端模式(Big endian):字数据的高字节存储在低地址中,而字数据的低字节则存放在高地址中。
小端模式(Little-endian):字数据的高字节存储在高地址中,而字数据的低字节则存放在低地址中。
-
enum color_type
{
RED,
G,
B,
F,
H,
K
}color_variabnle;
color_variabnle
是 color_type
类型的一个变量,花括号内是该变量取值范围,color_variabnle
只能取花括号内任何一个值。
括号内的成员都是常量(枚举常量)。所以sizeof(color_variabnle)
为4个字节(就是int型)
可以想宏定义的常量一样使用枚举常量(不用写枚举变量名。直接用)。
#define
在预编译时替换,而 enum
则是在编译的时候确定其值。使用结构体成员的时候,可以
-
const stu stu3;
// stu是结构体类型名,判断 const
修饰的是那个,stu
也是忽略(不管是不是指针)后,再看修饰哪个。
-
typedef不支持如下的用法(类型扩展):
typedef int INT32;
unsigned INT32 i = 0;
-
C语言中的注释内容在编译的时候会使用空格代替,所以分析一些写在语句间的注释时,用空格代替就可以知道结果了。
-
如下的写法会出现错误,/*会被当做注释的开始
y = x
-
出色的注释
-
规则1
注释应当准确、易懂,防止有二义性。错误的注释不但无益反而有害。
-
规则2
边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要及时删除。
-
规则3
注释是对代码的“提示”,而不是文档。程序中的注释应当简单明了,注释太多了会让人眼花缭乱。
-
规则4
一目了然的语句不加注释。例如: i++; /*i加1*/
多余的注释
-
规则5
对于全局数据(全局变量、常量定义等)必须要加注释。
-
规则6
注释采用英文,尽量避免在注释中使用缩写,特别是不常用缩写。因为不一定所有的编译器都能显示中文,别人打开你的代码,你的注释也许是一团乱码。还有,你的代码不一定是懂中文的人阅读。
-
规则7
注释的位置应与被描述的代码相邻,可以与语句在同一行,也可以在上行,但不可放在下方。同一结构中不同域的注释要对齐。
-
规则8
当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读。
-
规则9
注释的缩进要与代码的缩进一致。
-
规则10
注释代码段时应注重“为何做(why)“,而不是“怎么做(how)”.说明怎么做的注释一般停留在编程语言的层次,而不是为了说明问题。尽力阐述“怎么做”的注释一般没有告诉我们操作的意图,而指明“怎么做”的注释通常是冗余的。
-
规则11
数值的单位一定要注释。注释应该说明某数值的单位到底是什么意思。比如:关于长度的必须说明单位是毫米,米,还是千米等;关于时间的必须说明单位是时,分,秒,还是毫秒等。
-
规则12
对变量的范围给出注释。
-
规则13
对一系列的数字编号给出注释,尤其在编写底层驱动程序的时候(比如管脚编号)
-
规则13
对于函数的入口出口数据给出注释。
-
C语言里以反斜杠()表示断行。编译器会将反斜杠剔除掉,跟在反斜杠后面的字符自动接续到前一行。但是注意:反斜杠之后不能有空格,反斜杠的下一行之前也不能有空格。
但是对于一些完整的语句,比如以下是不需要的\
if(a
)
{
b=3;
}
有时间再看看编译器是如何处理空格的(或许得看编译原理)。
-
逻辑运算符,在判断语句中,||
一边为真即结束,&&
一边为假,即结束。
-
右移运算符作用在负数时,最高位补 0
还是 1
由编译器决定。
-
char d[10]= {"abcdefg"}; //
是可以的,{}
相当于有打包的作用,但是char
换成int
,就会报错。
-
预处理命令
#undef
撤销已定义的宏名
#line
改变当前行数和文件名称,它们是在编译程序中预先定义的标识符命令的基本形式如下:
#line number["filename"]
#error
编译程序时,只要遇到 #error
就会生成一个编译错误提示消息,并停止编译.(比如可以用来判断某些宏定义是否定义等等)
#pragma
为实现时定义的命令,它允许向编译程序传送各种指令例如,编译程序可能有一种选择,它支持对程序执行的跟踪。可用#pragma
语句指定一个跟踪选择。
-
ANSI 标准C还定义了如下几个宏:
_LINE_
表示正在编译的文件行号
_FILE_
表示正在编译的文件的名字
_DATE_
表示编译时刻的日期字符串,例如: “25 Dec 2007”
_TIME_
表示编译时刻的时间字符串,例如: “12:30:55”
_STDC
_ 判断该文件是不是定义成标准C程序
-
const
修饰的只读变量不能用来作为定义数组的维数,也不能放在 case
关键字后面。
-
编译器对注释的处理先于预处理指令被处理,所以使用 define
来定义注释符号,然后在程序中使用新定义的注释符号会报错。
-
使用 define
宏定义表达式,记住别吝啬括号。
-
#include ".h"
,先在当前路径找,找不到再按系统指定路径。<>系统规定的路径找
#include 也支持相对路径,.代表当前目录,…代表上层目录。
-
#pragma 的指令比较复杂,也会根据带参数的不同有不同功能。
#pragma message(“hello,V00”) // 可以使用其在编译输出端口输出括号内的信息
-
#pragma code_seg另一个使用得比较多的pragma参数是code seg。
格式如:#pragma code seg( [“section-name”[,“section-class”]]它能够设置程序中函数代码存放的代码段,当我们开发驱动程序的时候就会使用到它。
-
#pragma once (比较常用)只要在头文件的最开始加入这条指令就能够保证头文件被编译一次,这条指令实际上在Visual C++6.0中就已经有了,但是考虑到兼容性并没有太多的使用它。
-
#运算符可以把语言符号转换成字符串。
#define SQR(x) printf(“The square of “#x” is %d.\n”,(x)*(x))
SQR(8) 输出 The square of 8 is %d.
-
##运算符将两个语言符号组结合成一个。
#define X(n) x##n
则X(8) 会展开成x8
-
指针在定义的时候一定要赋值,不然就赋值为NULL,否则会存储任意的地址(如果通过这个任意地址访问,可能会导致系统崩溃)
-
数组名作为右值,代表的是数组首元素的首地址。数组名不能作为左值。
-
&a代表的是数组的首地址,&a[0]代表的是数组首元素的地址(等同于a)。(值是一样,但意义不一样,运算的结果就不同)
-
函数指针、函数指针数组、函数指针数组的指针
-
野指针很可怕,不用的时候把它置为NULL地址处(初始化和使用完)
-
静态区:保存自动全局变量和static变量(包括static全局和局部变量)。静态区的内容在总个程序的生命周期内都存在,由编译器在编译的时候分配。
栈:保存局部变量。栈上的内容只在函数的范围内存在,当函数运行结束,这些内容,也会自动被销毁。其特点是效率高,但空间大小有限。
堆: 由malloc系列函数或new操作符分配的内存。其生命周期由free或delete决定。在没有释放之前一直存在,直到程序结束。其特点是使用灵活,空间比较大,但容易出错。
-
容易出现野指针的地方:
1、结构体内有指针变量,在定义的时候初始化(赋值NULL或者分配内存),防止野指针。
2、给结构体指针分配了内存,但是结构体内的指针并没有分配,也是野指针
-
一般在函数入口处使用assert(NULL != p)对参数进行校验。在非参数的地方使用if (NULL!=p)来校验。但这都有一个要求,即p在定义的同时被初始化为NULL了。比如上面的例子,即使用if (NULL !=p)校验也起不了作用,因为name指针并没有被初始化为NULL,其内部是一个非NULL的乱码。
assert是一个宏,仅在Debug版本有用,Release无效。用来调试用。
-
使用malloc分配的内存使用完后一定要使用free释放,一定要一一对应,否则会造成内存泄漏或者别的错误。
free的作用是斩断指针对内存的所有权关系,并不会改变指针的值和内存的值。(所以需要手动把指针赋值为NULL)
-
编码风格
1、函数要有注释(说明创建时间、作者、函数作用、参数说明、修改历史等)
2、函数间空格隔开
3、变量定义与执行语句隔开
4、逻辑不相关的语句用空行隔开
5、修改别人的代码时不要轻易删除,先注释
6、缩进采用四个空格(可以考虑IDE定义tab键为空格)
-
一般来说,函数的入口参数,目的参数在前,源参数在后(和#define的使用类似)
-
如果传入的是指针,且只是读,加const
-
函数入口处,使用assert检查指针,而不推荐使用if