gdb调试
- gcc 源程序 -g;加gdb调试信息
- gdb可执行程序;(gdb调试)
- l(ist):查看源码,按一下从main开始10行以此往后
- l n:查看n处上下10行的源码
- run:运行程序
- b(reak)行号:加断点
- i(nfo) b:查看当前断点
- d(elete) 断点序号:删除断点
- p(rint) 变量名:查看变量的值
- c(ontinue):程序继续运行
- 单步运行程序:
- n(ext):往后执行一步,不进入函数内部
- s(tep):往下执行一步,进入函数内部执行
- quit:退出
变量的命名规则
- 程序中不得出现仅靠大小写区分的相似的标识符
- 一个函数名禁止被使用于其他地方
- 所有宏定义,枚举常量,只读变量全用大写字母命名,用下划线分割
- 考录到习惯性问题,局部变量中可采用通用的命名方式,但仅限于n,i,j 等作为循环变量使用
- 结构体被定义时必须有明确的结构体名
- 定义变量的同时,不忘初始化
- 不同类型数据之间的运算要注意精度扩展的问题,一般低精度向高精度数据扩展
- 禁止使用八进制的常数(0 除外,因为严格意义上来讲 0 也是八进制数) 和八进制的转义字符
- 在计算机中,任何以 0 开头的数字都被以为是八进制格式的数(当然十六进制的0x不算)。所以当我们写固定长度的数字时,会存在一定的风险。如 code[3] = 052;(对应十进制的42)
sizeof关键字
sizeof 在计算变量所占空间大小时,括号可以省略,而计算类型大小时,不能省略。且一般情况下,sizeof时在编译时求值,所以
sizeof(i++)不会引起副作用,但是由于sizeof(i++)和sizeof(i)的结果一样,所以没有必要且不允许写这样的代码。同样“sizeof(i=1234)”这样的代码也不允许,**因为
i 的值没有被改变,并没有被赋值为1234**。sizeof操作符里面不要有其他运算符,否则不会达到预期的目的。在C99 中,计算柔性数组所占用空间大小时,sizeof是在运行时求值,此为特例。
★在测字符串长度时,strlen函数时计算字符串长度,并不包含字符串在最后的’\0’。
if、else组合
- bool变量与“零值”的比较:
bool bTestFlag = FALSE;
(A) if (bTestFlag == 0);
(B) if (bTestFlag == TRUE);
(C) if (bTestFlag)
(A) 容易产生歧义,会误认为是整型变量。
(B) FALSE在编译器里被定义为 0 ;但 TRUE 的值则不唯一,所以这种写法不妥
(C) 既不会引起误会,也不会由于 TRUE 或 FALSE 的不同定义值而出错。
- float变量与“零值”进行比较
float fTestVal = 0.0
(A) if (fTestVal == 0.0);
(B) if ((fTestVal >= -EPSINON) && (fTestVal <= EPSINON));
//EPSINON为事先定义好的精度
分析:
float与double类型的数据都是有精度限制的,不能直接拿来与0.0比
EPSINON为实现定义好的精度,如果一个数落在[0.0-EPSINON, 0.0+EPSINON]这个闭区间内,我们认为在某个精度内它的值与零相等。扩展一下,把0.0替换为你想比较的任何一个浮点数,那我们就可以比较任意两个浮点数的大小了,当然是在某个精度内
★不要在很大的浮点数和很小的浮点数之间进行运算,使用浮点数应遵循定义好的浮点数标准
五种类型的浮点异常是:无效运算、被零除、上溢、下溢和不精确
四种射入方向:向最接近的可表示的值、当有两个最接近的可表示的值时,首选“偶数”值、向负无穷大(向下)和向正无穷大(向上)以及向0(截断)
- 指针变量与“零值”进行比较
int* p = NULL; //定义指针一定要同时初始化,指针和数组部分会详细讲
(A) if (p == 0);
(B) if (p);
(C) if (NULL == p);
(A) 写法:p时整型变量?容易引起误会,尽管NULL的值和0一样,但意义不同
(B) 写法:p时bool型变量,容易引起误会不好
(C) 正确
else到底与哪个if配对?
C语言中规定:else始终与同一括号内最近的未匹配的if语句结合
使用if语句的其他注意事项
规则1:先处理正常情况,再处理异常情况。
如果把执行概率更大的代码放到后面,也就意味着if语句将进行多次无谓的比较。所以,把正常情况的处理放在if后面,而不要放在else后面规则2:确保if和else子句没有弄反
规则3:赋值运算符不能使用再产生布尔值的表达式上。
任何被认为是具有布尔值的表达式上都不能使用赋值运算。
例如一下两种情况:
if ((x = y) != 0)
{
foo();
}
或者:
if (x = y)
{
foo();
}规则4:所有if-else if 结构应该由else子句结束
不管何时一条fi语句后有一个或多个else if 语句都应该应用本规则;最后的else if 必须跟有一条else语句。
switch、case组合
既然有了if、else组合,为什么还需要switch、case组合
- 不要拿青龙偃月刀去削苹果
规则1:每个case语句的结尾绝对不要忘了加break,否则将导致多个分支重叠(除非有使用多个分支重叠)
规则2:最后必须使用default分支。即使程序真的不需要default处理,也应该保留以下语句:
default:
break;
这样做并非画蛇添足,可以避免让人误以为你忘了default处理
规则3:再switch case组合中,禁止使用return语句。
规则4:switch表达式不应是有效的布尔值。例如:
switch (x == 0) // not compliant - effectively Boolean
{
...
}
- case关键字后面的值有什么要求么
记住: case后面只能是整型或字符型的常量或常量表达式(想想字符型数据在内存里是怎么存的)。
- case语句的排列顺序
有以下几种规则:
1. 按字母或数字顺序排列各条case语句
2. 把正常情况放在前面,而把异常情况放在后面
3. 按执行频率排列case语句
- 使用case语句的其他注意事项
有以下几种规则:
1. 简化每种情况对应的操作
2. 不要为了使用case语句而刻意制造一个变量
3. 将default子句只用于检查真正的默认情况
- break与continue的区别(传送门)
break关键字很重要,表示终止本层循环。当代码执行到break时,本层循环便终止。
而continue表示终止本次(本轮)循环,当代码执行到continue时,本轮循环终止,进入下一轮循环
- 循环语句的注重点:
建议使用以下规则:
1. 在多层循环中,如果有可能,应当将最长的循环放在内层,最短的循环放在外层,以减少CPU跨切循环层的次数。
2. 建议for语句的循环控制变量的取值采用“半开半闭区间”的写法(左闭右开)。
3. 不能在for循环体内修改循环变量,防止循环失控。
4. 循环要尽可能短,要使代码清晰,一目了然。
5. 把循环嵌套控制在3层以内。
6. for语句的控制表达式不能包含任何浮点类型的对象。
void关键字
- void a
void字面意思是“空类型”,void * 则为“空类型指针”,void * 可以指向任何类型的数据。void几乎只有“注释”和限制程序的作用,因为从来没有人会定义一个void变量。
void真正发挥的作用在于:对函数返回的限定;对函数参数的限定
✦任何类型的指针都可以直接赋值给void * ,无需进行强制类型转换,但反之则不能,因为“空类型”可以包容“有类型”,而“有类型”则不能包容“空类型”,比如,我们可以说“男人和女人都是人”,但不能说“人是男人”或者“人是女人”。
- void修饰函数返回值和参数
规则1:如果函数没有返回值,那么应该将其声明为void类型。
在C语言中,凡不加返回值类型限定的函数,就会被编译器作为返回整型值处理。如果函数没有返回值,那么一定要声明为void类型。这既是程序良好可读性的需要,也是编程规范性的要求。规则2:如果函数无参数,那么应声明其参数为void
在C语言中,可以给无参数的函数传送任意类型的参数,但是在C++编译器中编译同样的代码则会出错。在C++中,不能向无参数的函数传送任何参数。所以无论在C还是在C++中,若函数不接受任何参数,则一定要指明参数为void。
- void指针
规则1: 千万小心又小心地使用void指针类型。
按照ANSI标准,不能对void指针进行算法操作,之所以这样认定,是因为它坚持:进行算法操所的指针必须是确定知道其指向数据类型大小的,也就是说必须知道内存目的地址的确切值。
在实际的程序设计中,为符合ANSI 标准,并提高程序的可移植性,我们可以这样编写实现同样功能的代码:
void* pvoid;
(char*)pvoid++; // ANSI: 正确; GNU:正确
(char*)pvoid += 1; // ANSI: 错误; GNU:正确
规则2:如果函数的参数可以是任意类型指针,那么应声明其参数为void*
典型的如内存操作函数memcpy和memset的函数原型分别为:
void * memcpy(void * dest, const void * src, size_t len);
void * memset(void * buffer, int c, size_t num);
有趣的是,memcpy和memset函数返回的也是void * 类型。
- void 不能代表一个真实的变量
void 不能代表一个真是的变量。void 体现了一种抽象,这个世界上的变量都是“有类型”的,譬如一个人不是男人就是女人(人妖不算)。
void 的出现只是为了一种抽象的需要,如果你正确地理解了面向对象中“抽象基类”的概念,也很容易理解 void 数据类型。正如不能给抽象基类定义一个实例,我们不能定义一个 void(让我们类比的称void为“抽象数据类型”)变量。
return关键字
return 用来终止一个函数并返回其后面跟的值。
✦注意:return语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时自动销毁。
const关键字也许该被替换为readonly (传送门)
const 是 constant 的缩写,是恒定不变的意思,也翻译为常量和常数等。很不幸,正是因为这一点,很多人都认为被 const 修饰的值是常量。这是不精确的,精确来说应该是只读的变量,其值在编译时不能被使用,因为编译器在编译时不知道其存储的内容。或许当初这个关键字应该被替换为 readonly。
const 推出的初始目的,正式为了取代预编译指令,消除它的缺点,同时继承它的优点。
- 节省空间,避免不必要的内存分配,同时提高效率
编译器通常不为普通 const 只读变量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高。例如:
#define M 3 // 宏常量
const int N = 5; // 此时并未将N放入内存中
...
int i = N; // 此时为N分配内存,以后不再分配
int I = M; // 预编译期间进行宏替换,分配内存
int j = N; // 没有内存分配
int J = M; // 再进行宏替换,又一次分配内存
const i 定义的只读变量从汇编的角度来看,只是给出了对应的内存地址,而不是像 #define 一样给出的时立即数,所以const定义的 只读变量在程序运行过程中只有一份备份(因为它是全局的只读变量,存放在静态区),而#define 定义的宏常量在内存中有若干个备份。#define 宏是在 预编译阶段进行替换,而 const 修饰的只读变量实在编译的时候确定其值,#define 宏 没有类型,而 const 修饰的只读变量 具有特定的类型。
- 修饰指针
先忽略类型名(编译器解析的时候也是忽略类型名),我们看const 离哪个近,”近水楼台先得月“,离谁近就修饰谁
- 修饰函数的参数
const 修饰符也可以修饰函数的参数,当不希望这个参数值在函数体内被意外改变时使用。
- 修饰函数的返回值
const 修饰符也可以修饰函数的返回值,返回值不可被改变。
最易变的关键字——volatile
volatile int i = 10;
volatile 关键字告诉编译器,i 时随时可能发生变化的,每次使用它的时候必须从内存中取出 i 的值,因此编译器生成的汇编码会重新从 i 的地址处读取数据。
这样看来,如果 i 是一个寄存器变量,表示一个端口数据或者是多个线程的共享数据,那么就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。
最会戴帽子的关键字——extern
博客园中有一篇关于extern的讲解(传送门)
struct关键字
struct是个神奇的关键字,它将一些相关联的数据打包成一个整体,方便使用。
在网络协议,通信控制,嵌入式系统,驱动开发等地方,我们经常要传送的不是简单的字节流(char型数组),而是多种数据组合起来的一个整体,其表现形式是一个结构体。经验不足的开发人员往往将所有需要传送的内容依顺序保存在char型数组中,通过指针偏移的方法传送网络报文等信息。这样做编程复杂,易出错,而且一旦控制方式和通信协议有所变化,程序就要进行非常机制的修改,非常容易出错。
这个时候只需要一个结构体就能搞定。平时我们要求函数的参数尽量不多于4个,如果函数的参数多余4个使用起来非常容易出错(包括每个参数的意义和顺序都容易出错),效率也会降低(与具体CPU有关,ARM芯片对于超过4个参数的处理就有讲究,具体请参考相关资料)。这个时候,可以用结构体压缩参数个数。
✦struct 与 class 的区别:struct 的成员默认情况下的属性是 public,而 class 成员的却是 private。
空结构体大小一般为1,在GCC里面计算的值为0,
union关键字
union 关键字的用法与 struct 的用法非常相似。
union 维护足够的空间来放置多个数据成员中的“一种”,而不是为每一个数据成员配置空间。在 union 中所有的数据成员公用一个空间,同一时间只能储存其中一个数据成员,所有的数据成员具有相同的其实地址。
一个 union 只配置一个足够大的空间来容纳最大长度的数据成员,在C++里,union 的成员默认属性为 public。union 主要用来压缩空间。如果一些数据不可能在同一时间同时被用到,则可以使用 union。
柔性数组
这边引用博客园中的一篇关于柔性数组的文章传送门
大小端模式对union类型数据的影响
下面看一个例子:
union
{int i;char a[2];
}* p, u;p = &u;
p->a[0] = 0x39;
p->a[1] = 0x38;
p.i 的值应该为多少呢?
这里需要考虑存储模式:大端模式和小端模式。
大端模式(Big_endian):字数据的高字节存储在低地址中,而字数据的低字节存放在高地址中。
小端模式(Little_endian):字数据的高字节存储在高地址中,而字数据的低字节则存放在低地址中。
union 型数据所占的空间等于其最大的成员所占的空间。对 union 型成员的存取都从相对于该联合体基地址的偏移量为 0 处开始,也就是联合体的访问不论对哪个变量的存取都是从 union 的首地址位置开始。如此一解释,上面的问题是否已经有了答案呢?
如何用程序确认当前系统的存储模式
上述问题似乎还比较简单,那来个有计数含量的:请写一个 C 函数,若处理器是 Big_endian,则返回 0 ; 若是 Little_endian ,则返回 1。
先分析下,按照上面关于大小端模式的定义,假设 int 类型变量i被初始化为1.
以大端模式存储,其内存布局如图1.3所示。
以小端模式存储,其内存布局如图1.4所示。
变量 i 占 4 字节,但只有1个字节的值为 1,另外 3 个字节的值都为 0。如果取出低地址上的值为 0,毫无疑问,这是大端模式;如果取出低地址上的值为1,毫无疑问,这是小端模式。既然如此,我们完全可以利用 union 类型数据“所有成员的起始地址一致”的特点编写程序。到现在,应该知道怎么写了吧?参考文案如下:
int checkSystem()
{union check{int i;char ch;}c;c.i = 1;return (c.ch == 1);
}
现在你可以用这个函数来测试当前系统的存储模式,当然你也可以不用功函数而直接去查看内存来确定当前系统的存储模式,如图1.5所示:
图1.5中 0x01 的值存在低地址上,说明当前系统为小端模式。
不过要说明的一点是,某些系统可能同时支持这两种存储模式,你可以用硬件跳线或在编译器的选项中设置其存储模式。
留一个问题:
在 x86 系统下,以下程序输出的值为多少?
#include <stdio.h>int main()
{int a[5] = {1,2,3,4,5};int *ptr1 = (int *)(&a+1);int *ptr2 = (int *)((int)a+1);printf("&x, %x", ptr1[-1], *ptr2);return 0;
}
位域
对于位域的使用和自定义的行为需要详细说明,且在使用前需要用代码check当前系统的模式(大端或小端模式)
enum关键字
枚举类型的使用方法
一般的定义方式如下:
enum enum_type_name
{ ENUM_CONST_1,ENUM_CONST_2,...ENUM_CONST_n
}enum_variable_name;
注意:enum_type_name是自定义的一种数据类型名,而enum_variable_name为enum_type_name类型的一个变量,也就是我们平时常说的枚举变量。实际上enum_type_name类型是对一个变量取值范围的限定,而花括号内是它的取值范围,即enum_type_name类型的变量enum_variable_name只能取值为花括号内的任何一个值(都是常量,一般用大写),如果赋给该类型变量的值不在列表中,则会报错或者警告。
enum变量类型还可以给其中的常量符号赋值,如果不赋值则会从被赋初值的那个常量开始依次加一;如果都没有赋值,他们的值从0开始依次递增1.
枚举与#define宏的区别
- #define宏常量是在 预编译阶段 进行简单替换;枚举常量则是在 编译 的时候确定其值。
- 一般在调试器里,可以调试枚举常量,但是不能调试宏常量。
- 枚举可以以此定义大量相关的常量,而#define宏一次只能定义一个。
留2个问题:
1. 枚举能做的是,#define宏能不能做到?如果能,那为什么还需要枚举?
2. sizeof(ColorVal)的值是多少?为什么?
enum Color
{GREEN = 1,RED,BLUE,GREEN_RED = 10,GREEN_BLUE
}ColorVal;