一、程序调试的debug宏
1、程序调试的常见方案
单步调试、裸机LED调试、打印信息、log文件
- 利用调试器进行单步调试(譬如IDE中,Jlink)适用于新手,最大的好处就是直观,能够帮助找到问题。缺点是限制性大、速度慢。
- 裸机使用LED、蜂鸣器等硬件调试,适合单片机裸机程序。
- printf函数打印调试,比较常用,作为程序员必须学会使用打印信息调试。好处是具有普遍性,几乎在所有的情况下都能用。
- log文件(日志文件)是系统运行过程中在特定时候会打印一些调试信息,日志文件记录下来这些调试信息以供后续查找追查问题。适合于系统级或者大型程序的调试。
2、打印信息不能太多也不能太少
- 调试信息太少会不够信息找到问题所在。
- 调试信息太多会有大量的无用的信息淹没有用信息,导致有用信息无法看见,等于没有。
3、调试(DEBUG)版本和发行(RELEASE)版本的区别
- DEBUG版本就是包含了调试信息输出的版本,在程序测试过程中会发布debug版本,这种版本的程序运行时会打印出来调试信息/log文件,这些信息可以辅助测试人员判断程序的问题所在。DEBUG版本的坏处是输出调试信息占用了系统资源,拖慢了系统运行速度。因此DEBUG版本的性能低于RELEASE版本。
- RELEASE版本就是最终的发布版本,相较于DEBUG版本的功能代码是一样的,但是去掉了所有的调试信息。适合最终测试通过要发布的程序,因为去掉了调试信息所以程序的运行效率要更高。
- DEBUG和RELASE版本其实是一套源代码。源代码中是有很多的打印调试信息的语句的,如何来控制生成DEBUG和RELEEASE版本?靠条件编译,靠一个宏。
4、debug宏的实现原理
(1)DEBUG宏大概的原理是:
#ifdef DEBUG
#define dbg() printf()
#else
#define dbg()
#endif
(2)工作方式
- 如果要输出DEBUG版本,则在条件编译语句前加上#define DEBUG即可,这样程序中的调试语句dbg()就会被替换成printf从而输出;
- 如果要输出RELEASE版本则去掉#define DEBUG,则dbg()会被替换成空,则程序中所有的dbg()语句无效,这样的程序编译时就会生成没有任何调试信息的代码。
二、debug宏的使用方法
1、应用程序中DEBUG宏
#ifdef DEBUG
#define DBG(...) fprintf(stderr, " DBG(%s, %s(), %d): ", __FILE__, __FUNCTION__, __LINE__); fprintf(stderr, __VA_ARGS__)
#else
#define DBG(...)
#endif
- 注:__FILE__等是C语言中的预定义宏,具有特殊的含义,譬如__FILE__表示当前正在编译的c文件的文件名。
2、内核中的DEBUG宏
#ifdef DEBUG_S3C_MEM
#define DEBUG(fmt, args...) printk(fmt, ##args)
#else
#define DEBUG(fmt, args...) do {} while (0)
#endif
三、void类型的本质
1、C语言属强类型语言
(1)强类型语言和弱类型语言
- 强类型语言中所有的变量都有自己固定的类型,这个类型有固定的内存占用,有固定的解析方法;
- 弱类型语言中没有类型的概念,所有变量全都是一个类型(一般都是字符串的),程序在用的时候再根据需要来处理变量。
(2)C语言是典型的强类型语言
- C语言中所有的变量都有明确的类型。
- 因为C语言中的一个变量都要对应内存中的一段内存,编译器需要这个变量的类型来确定这个变量占用内存的字节数和这一段内存的解析方法。
2、数据类型的本质含义
(1)数据类型决定变量的内存占用数,和内存的解析方法。
(2)如果一个变量没有确定的类型(就是所谓的无类型)会导致编译器无法给这个变量分配内存,也无法解析这个变量对应的内存。因此得出结论不可能有没有类型的变量。
(3)C语言中可以有没有类型的内存。
- 在内存还没有和具体的变量相绑定之前,内存就可以没有类型。
- 实际上纯粹的内存就是没有类型的,内存只是因为和具体的变量相关联后才有了确定的类型(其实内存自己本身是不知道的,而编译器知道,我们程序在使用这个内存时知道类型所以会按照类型的含义去进行内存的读和写)。
3、void类型的本质
(1)void类型的正确的含义是:不知道类型,不确定类型,还没确定类型。
(2)void a;
- 定义了一个void类型的变量,含义就是说a是一个变量,而且a肯定有确定的类型,只是目前还不知道a的类型,还不确定,所以标记为void。
4、为什么需要void类型
(1)什么情况下需要void类型?
- 在描述一段还没有具体使用的内存时需要使用void类型。
(2)void的一个典型应用案例就是malloc的返回值。
- malloc函数向系统堆管理器申请一段内存给当前程序使用,malloc返回的是一个指针,这个指针指向申请的那段内存。
- malloc刚申请的这段内存尚未用来存储数据,malloc函数也无法预知这段内存将来被存放什么类型的数据,所以malloc无法返回具体类型的指针,解决方法就是返回一个void *类型,告诉外部返回的是一段干净的内存空间,尚未确定类型。
- 所以在malloc之后可以给这段内存读写任意类型的数据。
(3)void *类型的指针指向的内存是尚未确定类型的,因此我们后续可以使用强制类型转换强行将其转为各种类型。
- 这就是void类型的最终归宿,就是被强制类型转换成一个具体类型。
(4)void类型使用时一般都是用void *,而不是仅仅使用void。
四、语言中的NULL
1、NULL在C/C++中的标准定义
(1)NULL不是C语言关键字,本质上是一个宏定义
#ifdef _cplusplus // 条件编译
#define NULL 0
#else
#define NULL (void *)0 // 这里对应C语言的情况
#endif
- C++的编译环境中,编译器预先定义了一个宏_cplusplus,程序中可以用条件编译来判断当前的编译环境是C++的还是C的。
- NULL的本质是0,但是这个0不是当一个数字解析,而是当一个内存地址来解析的,这个0其实是0x00000000,代表内存的0地址。(void *)0这个整体表达式表示一个指针,这个指针变量本身占4字节,地址在哪里取决于指针变量本身,但是这个指针变量的值是0,也就是说这个指针变量指向0地址(实际是0地址开始的一段内存)。
2、从指针角度理解NULL的本质
(1)为什么要让一个野指针指向内存地址0处?
- 主要是因为在大部分的CPU中,内存的0地址处都不是可以随便访问的(一般都是操作系统严密管控区域,所以应用程序不能随便访问)。
- 所以野指针指向这个区域可以保证野指针不会造成误伤。如果程序无意识的解引用指向0地址处的野指针则会触发段错误。
3、为什么需要NULL
- 让野指针指向0地址处安全。
- 一个特殊标记。
int *p = NULL; // 定义p时立即初始化为NULL
p = xx;
if (NULL != p)
{*p // 在确认p不等于NULL的情况下才去解引用p
}
p = NULL // 用完之后p再次等于NULL
- 一般比较一个指针和NULL是否相等不写成if (p == NULL),而写成if (NULL == p)。
- 原因是第一种写法中如果不小心把==写成了=,则编译器不会报错,但是程序的意思完全不一样了;而第二种写法如果不小心把==写成了=则编译器会发现并报错。
4、注意不要混用NULL与'\0'
- '\0'是一个转义字符,对应的ASCII编码值是0,本质就是0;
- '0'是一个字符,对应的ASCII编码值是48,本质是48;
- 0是一个数字,它就是0,本质就是0;
- NULL是一个表达式,是强制类型转换为void *类型的0,本质是0。
- '\0'用法是C语言字符串的结尾标志,一般用来比较字符串中的字符以判断字符串有没有到头;
- '0'是字符0,对应0这个字符的ASCII编码,一般用来获取0的ASCII码值;
- 0是数字,一般用来比较一个int类型的数字是否等于0;
- NULL是一个表达式,一般用来比较指针是否是一个野指针。
五、运算中的临时匿名变量
1、C语言和汇编的区别
- 语言叫高级语言,汇编语言叫低级语言。
- 低级语言的意思是汇编语言和机器操作相对应,汇编语言只是CPU的机器码的助记符,用汇编语言写程序必须拥有机器的思维。因为不同的CPU设计时指令集差异很大,因此用汇编编程的差异很大。
- 高级语言(C语言)它对低级语言进行了封装(C语言的编译器来完成),给程序员提供了一个靠近人类思维的一些语法特征,人类不用过于考虑机器原理,而可以按照自己的逻辑原理来编程。譬如数组、结构体、指针……
- 更高级的语言如java、C#等只是进一步强化了C语言提供的人性化的操作界面语法,在易用性上、安全性上进行了提升。
2、C语言的一些“小动作”
(1)高级语言中有一些元素是机器中没有的。
(2)高级语言在运算中允许我们大跨度的运算。
- 意思就是低级语言中需要好几步才能完成的一个运算,在高级语言中只要一步即可完成。
- 譬如C语言中一个变量i要加1,在C中只需要i++即可,看起来只有一句代码。
- 但实际上翻译到汇编阶段需要3步才能完成:第1步从内存中读取i到寄存器,第2步对寄存器中的i进行加1,第3步将加1后的i写回内存中的i。
3、使用临时变量来理解强制类型转换
4、使用临时变量来理解不同数据类型之间的运算
六、c库函数和API函数的关系
1、操作系统的调用通道:API
- 操作系统负责管理和资源调配,应用程序负责具体的直接劳动,之间的接口就是API函数。
- 当应用程序需要使用系统资源(譬如内存、譬如CPU、譬如硬件操作)时就通过API向操作系统发出申请,然后操作系统响应申请帮助应用程序执行功能。
2、C库函数和API的关系
- API只提供极简单没有任何封装的服务函数,这些函数应用程序是可用的,但是不太好用。对这个API进行二次封装,把它变得好用一些,于是就成了C库函数。
- 完成一个功能,有相应的库函数可以完成,也有API可以完成,用哪个都行。譬如读写文件,API的接口是open write read close;库函数的接口是fopen fwrite fread fclose。fopen本质上是使用open实现的,只是进行了封装。封装肯定有目的(添加缓冲机制)。
3、不同平台(windows、linux、裸机)下库函数的差异
- 不同操作系统的API是不同的,完成一个任务所调用的API不同。
- 不同操作吸人的库函数也是不同的,但相似性高一些。在一个操作系统上写的应用程序不可能直接在另一个操作系统上面编译运行,要进行移植。
- 跨操作系统可移植平台,譬如QT、譬如Java语言。
七、main函数返回给谁
1、函数为什么需要返回值
- 函数在设计的时候设计了参数和返回值,参数是函数的输入,返回值是函数的输出。
- 因为函数需要对外输出数据(实际上是函数运行的一些结果值)因此需要返回值。
- 函数被另一个函数所调用,返回值作为函数式的值返回给调用这个函数的地方。
- 函数的返回值就是给调用它的人返回一个值。
2、main函数被谁调用
- main函数是特殊的。C语言规定main函数是整个程序的入口。其他的函数只有直接或间接被main函数所调用才能被执行。
- main函数从某种角度来讲代表了当前这个程序,或者说代表了整个程序。main函数的开始意味着整个程序开始执行,main函数的结束返回意味着整个程序的结束。
- 谁执行了这个程序,谁就调用了main。
- 谁执行了程序?或者说程序有哪几种被调用执行的方法?
3、linux下一个新程序执行的本质
(1)linux中在命令行中去./xx执行一个可执行程序。
(2)可以通过shell脚本来调用执行一个程序。
(3)可以在程序中去调用执行一个程序(fork exec)。
(4)总结
- 有多种方法都可以执行一个程序,但是本质上是相同的。
- linux中一个新程序的执行本质上是一个进程的创建、加载、运行、消亡。
- linux中执行一个程序其实就是创建一个新进程然后把这个程序丢进这个进程中去执行直到结束。新进程是被谁开启?在linux中进程都是被它的父进程fork出来的。
(5)分析
- 命令行本身就是一个进程,在命令行底下去./xx执行一个程序,其实这个新程序是作为命令行进程的一个字进程去执行的。即一个程序被它的父进程所调用。
- main函数返回给调用这个函数的父进程。父进程调用子进程来执行一个任务,然后子进程执行完后通过main函数的返回值返回给父进程一个答复。这个答复一般是表示子进程的任务执行结果完成了还是错误了。(0表示执行成功,负数表示失败)
八、argc、argv与main函数的传参
1、谁给main函数传参
- 其父进程给main函数传参,并且接收main的返回值。
2、为什么需要给main函数传参
(1)main函数不传参是可以的,也就是说父进程调用子程序并且给子程序传参不是必须的。 int main(void)这种形式就表示我们认为不必要给main传参。
(2)有时候我们希望程序有一种灵活性,所以选择在执行程序时通过传参来控制程序中的运行,达到不需要重新编译程序就可以改变程序运行结果的效果。
3、给main传参是怎样实现的?
(1)给main传参通过argc和argv这两个C语言预订的参数来实现
(2)argc是int类型,表示运行程序的时候给main函数传递了几个参数;
(3)argv是一个字符串数组,这个数组用来存储多个字符串,每个字符串就是我们给main函数传的一个参数。
- argv[0]就是我们给main函数的第一个传参,argv[1]就是传给main的第二个参数····
4、给main传参是怎样实现的?
- 程序调用有各种方法但是本质上都是父进程fork一个子进程,然后字进程和一个程序绑定起来去执行(exec函数族),我们在exec的时候可以给他同时传参。
- 程序调用时可以被传参(也就是main的传参)是操作系统层面的支持完成的。
5、给main传参要注意什么
- main函数传参都是通过字符串传进去的。
- 程序被调用时传参,各个参数之间是通过空格来间隔的。
- 在程序内部如果要使用argv,那么一定要先检验argc。
1)NULL不是C语言关键字,本质上是一个宏定义
#ifdef _cplusplus // 条件编译
#define NULL 0
#else
#define NULL (void *)0 // 这里对应C语言的情况
#endif
- C++的编译环境中,编译器预先定义了一个宏_cplusplus,程序中可以用条件编译来判断当前的编译环境是C++的还是C的。
- NULL的本质是0,但是这个0不是当一个数字解析,而是当一个内存地址来解析的,这个0其实是0x00000000,代表内存的0地址。(void *)0这个整体表达式表示一个指针,这个指针变量本身占4字节,地址在哪里取决于指针变量本身,但是这个指针变量的值是0,也就是说这个指针变量指向0地址(实际是0地址开始的一段内存)。
2、从指针角度理解NULL的本质
(1)为什么要让一个野指针指向内存地址0处?
- 主要是因为在大部分的CPU中,内存的0地址处都不是可以随便访问的(一般都是操作系统严密管控区域,所以应用程序不能随便访问)。
- 所以野指针指向这个区域可以保证野指针不会造成误伤。如果程序无意识的解引用指向0地址处的野指针则会触发段错误。
3、为什么需要NULL
- 让野指针指向0地址处安全。
- 一个特殊标记。
int *p = NULL; // 定义p时立即初始化为NULL
p = xx;
if (NULL != p)
{*p // 在确认p不等于NULL的情况下才去解引用p
}
p = NULL // 用完之后p再次等于NULL
- 一般比较一个指针和NULL是否相等不写成if (p == NULL),而写成if (NULL == p)。
- 原因是第一种写法中如果不小心把==写成了=,则编译器不会报错,但是程序的意思完全不一样了;而第二种写法如果不小心把==写成了=则编译器会发现并报错。
4、注意不要混用NULL与'\0'
- '\0'是一个转义字符,对应的ASCII编码值是0,本质就是0;
- '0'是一个字符,对应的ASCII编码值是48,本质是48;
- 0是一个数字,它就是0,本质就是0;
- NULL是一个表达式,是强制类型转换为void *类型的0,本质是0。
- '\0'用法是C语言字符串的结尾标志,一般用来比较字符串中的字符以判断字符串有没有到头;
- '0'是字符0,对应0这个字符的ASCII编码,一般用来获取0的ASCII码值;
- 0是数字,一般用来比较一个int类型的数字是否等于0;
- NULL是一个表达式,一般用来比较指针是否是一个野指针。