4.8一些杂散但是值得讨论的问题
4.8.1操作系统究竟是什么玩意?
4.8.1.1像人类社会一样的计算机软件系统(有些人只埋头苦干,有些人只做管理)
- (1)人类社会最开始时人人都干活,这时候没有专业分工,所有人都直接做产生价值的工作。当时时合适的,因为当时生产力低下,人口稀少。就像裸机程序一样(逻辑程序的特点:代码量小,功能简单、所有代码都和直接目的有关,没有服务性代码)
- (2)后来人口增加生产力提高,有一部分人脱离了直接产生价值的体力劳动专职指挥(诞生了阶级)。本质上来说是合理的,因为资源得到了优化配置,提升了整体的效率。当计算机技术发展,计算机性能和资源大量增加,譬如CPU的性能越来月增加,内存越来越大。这时候写代码也要产生阶级,进行分工。不然如果所有代码都具有参加直接性的工作,则整体效率不高。(因为代码很难进行资源的优化配置)
- (3)解决方案就是操作系统。操作系统就是分出来管理阶级,操作系统的代码本身并不直接产生价值,它的主要任务是管理所有资源,它主要为直接产生价值的,直接劳动的那些程序(各种应用程序)提供服务。操作系统既是管理者也是服务者。
- (4)逻辑程序就好像小公司,操作系统下的程序就好像大型跨国公司;逻辑程序就像小国家,操作系统下程序就好像大国家;如果我们要做一个产品,那么软件系统是裸机还是基于操作系统呢?本质上是取决于产品本身的复杂度。只有极简单的功能,使用极简单的CPU(譬如单片机)的产品才会选择用裸机开发;一般的复杂性产品都会选择基于操作系统来开发
4.8.1.2、操作系统的调用通道:API函数
(1)操作系统负责管理和资源调配,应用程序负责具体的直接劳动,他们之间的接口就是API函数,当应用程序需要自己使用系统资源时(譬如内存、CPU、硬件)就通过API向操作系统发出申请,然后操作系统就相应申请帮助应用程序实现功能。
4.8.1.3、C库函数和API的关系
- (1)单纯的API只是提供了极简单的没有任何封装的服务函数,这些函数是可用的,但是不太好用。应用程序为了好用,就对这个API进行了二次封装,把他变得好用一些,于是就成了C库函数
- (2)有时候完成一个功能,有相应的库函数可以完成,也可以有相应的API完成,用那个都行。譬如读写文件,API接口是open write read close;库函数的接口是fopen fwrite fread fclose 。fopen本质上是open实现的,只是进行了封装。封装肯定有目的(添加缓冲机制)。
4.8.1.4、不同平台(windows、linux、裸机)下库函数的差异
- (1)不同的操作系统API是不同的,但是都能完成所有任务,只是完成一个任务所调用的API是不同的。
- (2)库函数在不同的操作系统下也不同,但是相似性要更高一些。这是因为,人下意识想要屏蔽不同操作系统的差异,因此在封装出来的库函数挺像的,但是还是存在差异,所以我们在一个操作系统上写的应用程序不可能直接在另一个操作系统下编译运行。于是乎就有个可移植性出来。
- (3)跨操作系统可移植平台(譬如QT、譬如JAVA)
4.8.1.5操作系统的重大意义:软件体系分工
(1)有了操作系统后,我们做铲平可以分为两个部分,一部分人负责做操作系统(开发驱动);一部分负责操作系统实现具体功能(开发应用)。实际上上层应用层的功能进一步复杂化后又分了很多层。
4.8.2、main函数的返回值返回给了谁?
4.8.2.1、函数为什么需要有返回值
- (1)函数设计的时候设计了参数和返回值,参数是函数输入,返回值是函数输出
- (2)因为函数需要对外输出数据(实际上是函数运行的一些结果)因此需要返回值
- (3)形式上来说,函数被另一个函数所调用,返回值就作为函数的值返回给调用这个函数的地方
总结:函数的返回值就是给调用他的人返回一个值
4.8.2.2、main函数被谁调用
- (1)main函数是特殊的,首先这个名字是特殊的。因为C语言规定了整个程序的入口。其他的函数只有直接或者间接被main函数调用才能被执行,如果没有被main函数直接/间接调用则这个函数在整个程序中无用。
- (2)main函数从某种角度来讲代表了当期那这个程序,或者说代表了整个程序。main函数的开始一意味着整个程序开始执行,main 函数的结束意味着整个程序的结束
- (3)谁执行了这个程序,谁就调用了main
- (4)谁执行了程序?或者说程序有哪几种被调用执行的方法?
4.8.2.3、linux下一个新程序执行的本质
- (1)表面上来看,linux中在命令行中./xx执行一个可执行程序。
- (2)我们还可以通过shell脚本来调用执行一个程序
- (3)我们还可以在程序中调用执行一个程序(fork exec)
总结:我么有多种方法执行一个程序,但是本质上是相同的。linux中一个新程序的执行本质上是一个进程的创建、加载、运行、消亡。linux中执行一个程序其实就是创建一个新进程,然后把这个程序丢进去执行知道结束。新进程被谁开启。linux中进程都是被它的父进程fork出来的。
分析:命令行本身就是一个进程,在命令行低下./xx执行一个进程,其实就是新进程作为命令行进程的一个子进程去执行的。
总之一句话:一个程序被它的父进程所调用
结论:main函数返回给调用这个函数的父进程。父进程要这个返回值干什么?父进程调用子进程来完成一个任务,然后子进程执行完后通过main函数的返回值来给父进程一个答复。这个答复一般表示子进程的任务执行结果时成功了还是错误了。(0表示执行成功了,负数表示执行失败了)
4.8.2.4实践验证获取main的返回值
- (1)用shell脚本执行程序,可以获取程序的返回值,并且打印出来
- (2)linux的shell中用 $? 这个符号来存储和表示上一个程序执行的结果
/*在 .sh 文件中的代码如下 */
#!/bin/sh ./a.out echo $?
4.8.2.5、启示
- (1)任何人任何事物都是有妈生的,不会无缘无故的出现或者消亡
- (2)看起来没用,改掉或者去掉没错的,也不见得真的就没错,要大胆总结更要小心求证。
4.8.3、argc、argv与main函数的传参
4.8.3.1、谁给main函数传参
- (1)调用main函数所在的程序的它的父进程给main函数传参,并且接收它的返回值。
4.8.3.2、为什么需要给main函数传参
- (1)首先,main函数不传参是可以的,也就是说父进程调用子进程并且给子进程传参不是必须的。int mian(void)这种形式就表示我们认为不必要给 main函数传参
- (2)有时候我们希望程序有一种灵活性,所以选择在执行程序时通过出参来控制程序中的运行,达到不需要重新编译程序就可以改变程序运行结果的效果。
4.8.3.3、表面上:给main函数进行传参是怎样实现的?
- (1)给main函数传参通过argc和argv这两个c原因预定的参数来实现
- (2)argc是int类型,表示我们运行程序的时候,我们给main函数传了几个参数。argv是一个字符数组,用来存储多个字符串,每个字符串就是我们给main函数传的一个参数。argv[0]就是我们给main函数的第一个传参,argv[1]就是我们给main函数传的第二个参数。
4.8.3.4、本质上:给main函数传参是怎样实现的?
- (1)上节课讲过,程序调用有各种方法,但是本质上都是父进程fork一个子进程,然后子进程可一个程序绑定以来一起去运行(exec函数族),我们子啊exec的时候可以给他同时传参。
- (2)程序调用是可以被传参(也就是main的传参)是操作系统层面的支持完成的
4.8.3.5、给main传参要注意什么?
- (1)main函数传参都是通过字符串传进去的。
- (2)程序被调用时传参,各个参数之间通过空格来间隔的
- (3)程序在使用argv之前要检验argc
4.8.4、void类型的本质
4.8.4.1、C语言属于强类型语言
- (1)编程语言分为强类型语言和弱类型语言。强类型语言所有的变量都有自己固定的类型,这个类型有固定的内存占用,有固定的解析方法,弱类型语言没有类型这个概念(一般都是字符串的),程序在用的时候在根据自己的需要来处理变量。
- (2)C语言是一种典型的强类型语言,C语言中所有的变量都必须要有类型,因为C语言中每一各变量都要对应内存中的一段内存,编译器需要这个变量类型来确定这个变量占用的字节数和这一段内存的解析方法。
4.8.4.2、数据类型的本质含义
- (1)数据类型的本质就是决定变量的内存占用数,和内存解析方法。
- (2)所以得出结论:C语言中变量必须有确定的数据类型,如果一个变量你没有确定的类型,就是所谓的无类型。会导致编译器无法给这个变量分配内存,也无法解析这个变量对应的内存。因此得出结论,不可能有没有类型的变量。
- (3)但是C语言中可以有没有类型的内存。在内存还没有和具体变量相绑定之前内存就可以没有类型。实际上纯粹的内存就是没有类型的。内存只是因为和具体的变量相关联后才有了确定的类型(其实内存自己本身是不知道的,而编译器知道,我们程序在使用这个类型时会按照类型的含义去进行内存的读和写)。
4.8.4.3、void类型的本质含义
- (1)void类型的正确含义是:不知道类型,不确定类型,还没确定类型
- (2)void a; 定义了void类型的变量,含义就是说a就是一个变量,而且a肯定有确定的类型,知识目前我们还不知道a的类型,还不确定,所以标记为void
4.8.4.4、为什么需要void类型
- (1)什么情况下需要void类型?其实就是在描述一段没有具体使用的内存时需要使用void类型。
- (2)void的一个典型应用案例就是malloc的返回值。我们知道malloc函数向系统堆管理器申请一段内存给当前程序使用,malloc返回的是一个指针,这个指针指向申请的那段内存。malloc刚申请的这段内存尚未用来存储数据。我们也无法预知将来这段内存会用来存什么类型的数据,所以不能返回一个具体类型的指针。所以解决方案就是返回一个void *类型,告诉编译器我们返回的是一段干净的内存空间,尚未确定类型,所以当我们在malloc之后可以给这段内存读写任意类型的数据。
- (3)void * 类型的指针指向的内存是尚未确定类型的,因此我们后续可以使用强制类型转换成各种类类型,这就是void 类型的归宿,就是强制类型转换成一个具体类型。
4.8.5、C语言中的NULL
4.8.5.1、NULL在C/C++中的标准定义
- (1)NULL不是C语言的关键字,本质上是一个宏定义
- (2)NULL的标准定义:
#ifdef _cplusplus //条件编译
#define NULL 0
#else
#define NULL (void *)0 //这里对应的是C语言的情形
#endif
解释:C++的编译环境中,编译器预先定义了一个宏_cplusplus,程序中可以用条件编译来判断当前的编译环境是c++还是C的。
NULL的本质解析:NULL的本质是0,但是这个0不是当一个数字解析,而是当一个内存地址来解析的,这个0其实是0x00000000,代表的是内存的0地址。
(void *)0这个整体表达式表示一个指针,这个指针变量本身占4个字节,地址在哪里取决于指针变量本身,但是这个指针变量的值是0,也就是说这个指针变量指向0地址(实际上是0地址开始的一段内存)。
4.8.5.2、从指针角度理解NULL的本质
- (1)int *p; //p是一个函数内的局部变量,则p的值是随机的,也就是说p是一个野指针
- (2)int* p = NULL; //p是一个局部变量;分配在栈上的地址是由编译器决定的,我们并不关心,但是p的值是(void*)0实际就是0,意思是指针p指向内存的0地址处,这时候p就不是野指针了。
- (3)为什么要让一个野指针指向内存地址0处,主要是因为在大部分的CPU当中,内存的0地址处都不是可以随便访问的(一般都是操作系统严密管控的,所以应用程序不能随便访问)。所以野指针指向了这个区域可以保证野指针不会造成误伤。如果程序员无意识的解引用指向0地址的指针则会触发段错误,这样可以提示到你帮助你找到程序中的错误。
4.6.5.3、为什么需要NULL
- (1)第一个作用就是让野指针指向0地址比较安全
- (2)第二个作用就是一个特殊标记,按照标准的使用步骤
int *p =NULL;
p = xx;
if(NULL != p)
{
*p = xxxx; //再确认p不等于NULL的情况下再去解引用
}
P = NULL; //用完后p再次等于NULL
注意:一般我们比较一个指针和NULL是否相等不写成if(p != NULL),而写成if(NULL != p) 原因是第一种写法如果把==写成了=编译器不会报错,但是意思已经完全改变。而第二种会报错,提示你找到错误并改正。
4.8.5.4、注意不要混用NULL与‘\0’
- (1)‘\0’ 和 '0' 和 0 和 NULL几个区分开
- (2)‘\0’是一个转义字符,他对应的ASCII编码值是0,本质就是0
- (3)‘0’是一个字符,他对应的ASCII编码值是48,本质是48
- (4)0是一个数字,他本质就是0
- (5)NULL是一个表达式,是强制类型转换为void *类型的0,本质是0
总结:‘\0’用法是C语言字符串结尾标志,一般用来比较字符串中的字符以判断字符串有没有到头
‘0’是字符0,对应0这个字符的ASCII编码;一般用来获取0 的ASCII的码值
0是数字,一般用来比较一个int类型的数字是否等于0
NULL是一个表达式,一般用来比较一个指针是否是一个野指针
4.8.6、运算中的临时匿名变量
4.8.6.1、C语言和汇编的区别(汇编完全对应机器操作,C对应逻辑操作)
- (1)C语言叫做高级语言,汇编语言叫低级语言。
- (2)低级语言的意思是汇编语言和机器操作相对应,汇编语言只是CPU的机器码的助记符,用汇编语言写程序必须拥有机器的思维,因为不同的CPU设计时指令的差异比较大,所以用汇编编程差异很大。
- (3)高级语言(c语言)它对低级语言进行了封装(C语言的编译来完成的),给程序员提供了一个靠近人类思维的一些语法特征,这样人类可以不用过于考虑机器的原理。而可以按照自己的逻辑原理来编程。譬如说数组、结构体、指针............
- (4)更高级的语言如java、C#等知识进一步强化了C语言提供的人性化的操作界面语法,在易用性上、安全性上进行了提升。
4.8.6.2、C语言的一些“小动作”
- (1)高级语言中有一些元素是机器中没有的
- (2)高级语言在运算中允许我们大跨度的运算。意思就是低级语言中需要好几步才能完成的运算,在高级语言中只要一步即可完成。譬如在C语言中有个i++,在C语言中只需要i++即可,看起来只有一句代码。但实际上翻译到汇编需要3步才能完成;第1步从内存中读取i到寄存器,第二步对寄存器中的i进行加1,第三步,将加1后的i写入内存i中;
4.8.6.3、使用临时变量来理解强制类型转换
4.8.6.4、使用临时变量来理解不同数据类型之间的运算
4.8.7、顺序结构
4.8.7.1、最前线的顺序结构:三种结构之一
- (1)代码执行的时候如果没有遇到判断跳转或者循环,默认是顺序执行的。执行完再执行下一句
- (2)顺序结构说明CPU的工作状态,就是以时间轴来顺序执行所有的代码语句直到停机。
4.8.7.2、选择和循环结构内部的顺序结构
- (1)譬如if(){ }内部是if的代码段,在代码段的内部还是按照顺序结构来执行的。
- (2)switch case 内部也是一样的,也是按照顺序结构来运行的
- (3)while for 内部也是按照顺序结构来执行的
4.8.7.3、编译过程中的顺序结构
- (1)一个C程序有多个.c文件组成,编译的时候多个.c文件是独立分开编译的。每个c文件编译的时候,编译器是按照从前到后的顺序逐行进行编译的
- (2)编译时候的顺序编译会导致函数或者变量必须事先声明才能被调用,这也是函数/变量声明的来源
- (3)链接过程中呢?应该说连接过程连接器实际上是在链接脚本的指导下完成的。所以说链接时的顺序是由链接脚本决定的。如果链接脚本明确指定了顺序就会按照链接脚本中的顺序来链接,如果没有链接脚本那么连接器就会自动的进行链接
4.8.7.4、思考:为什么本质都是顺序结构?
- (1)顺序结构本质上符号CPU的设计原理,CPU是人设计的,所以CPU的设计符合人的思考原理
4.8.8、程序调试的debug宏
4.8.8.1、程序调试的常见方案:单步调试、打印信息、log文件
- (1)利用调试器进行单步调试(譬如在IDE中,Jlink)适用于新手,最大的好处就是直观,能够帮保护你找到问题。缺点是限制性大、速度慢
- (2)裸机使用LED、蜂鸣器等硬件调试,适合单片机裸机调试
- (3)printf函数打印调试,比较常用,作为程序员必须要学会打印信息调试。好处是具有普遍性,几乎在所有的情况下都能用
- (4)log文件(日志文件)是系统运行过程中在特定的时候会打印一些调试信息,日志文件记录下来后供我们后续查找追查问题。适合于系统级的调试,或者是大型程序的调试
4.8.8.2、打印信息不能太多也不能太少
- (1)调试信息太少会不够信息,找不到问题
- (2)调试信息太多会有大量的无用信息淹没有用信息,导致有用信息无法看见,等于没有。
4.8.8.3、调试(DEBUG)版本和发行(RELEASE)版本的区别
(1)DEBUG版本就是包含了调试信息输出的版本,在程序测试过程中会发布debug版本,这种版本的程序运行时会打印出来调试信息/log文件,这些信息会辅助测试人员判断问题所在DEBUG版本的坏处是输出调试信息占用了系统资源,拖慢了系统运行的速度,因此DEBUG版本的新能低于RELEASE版本
(2)RELEASE版本就是最终的发布版本,相较于DEBUG版本的功能代码是一样的,但是去掉了提哦啊哈斯信息,适合最终测试通过,要发布的程序,因为去掉了调试信息,所以执行效率会高很多。
(3)DEBUG版本和RELEASE版本其实是一套源代码。源代码中是有很多的打印调试信息的语句的。如何来控制和生成DEBUG和RELEASE版本呢?就要靠条件编译
4.6.6.4、debug宏的实现原理
- (1)DEBUG宏的大概实现原理是:
#define DEBUG
#define dbg() printf()
#else
#define dbg()
#endif
- (2)工作方式是:如果我们要输出DEBUG版本则条件编译语句前加上#define DEBUG,这样程序中的dbg()就会替换成printf从而输出,如果我们要输出RELEASE版本则去掉#define DEBUG 则dbg()则会替换为空,则程序中的dbg()语句就会蒸发了,这样的程序编译时就会生成没有任何调试信息的代码
4.8.8.5、debug宏的使用方法
4.5.8.6、分析几个DEBUG宏
(1)应用程序中的DEBUG宏
#define DEBUG #ifdef DEBUG #define DBG(...) fprintf(stderr, " DBG(%s, %s(), %d): ", __FILE__, __FUNCTION__ , __LINE__); fprintf(stderr, __VA_ARGS__) #else #define DBG(...) #endif
注: _FILE_等是C语言中的宏定义库,就是说这个东西是宏定义,但是是IC语言自己定义的,这些宏具有特殊含义,譬如_FILE_表示当前正在编译的C文件的文件名
示例代码:
#include <stdio.h> #define DEBUG #ifdef DEBUG #define DBG(...) fprintf(stderr, " DBG(%s, %s(), %d): ", __FILE__, __FUNCTION__ , __LINE__); fprintf(stderr, __VA_ARGS__) #else #define DBG(...) #endif int main(void) { DBG("tiaoshi.\n"); return 0; }
结果:DBG(debug.c, main(), 23): tiaoshi.
(2)内核中的DEBUG宏
#ifdef DEBUG _S3C_MEM #define DEBUG(fmt, args...) printk(fmt, ##args) #else #define DEBUG(fmt, args...) do {} while (0) #endif