八、一些杂散但值得讨论的问题

8.1、操作系统的理解:

<1>它是一个管理阶级者,管理所有资源,负责调配优化等操作。这样想象,就像裸机一样的话,要实现LED闪烁的进程、串口传输的进程、蜂鸣器等这些,他们都要抢占一些资源,这个时候没有操作系统,就乱成一锅粥,当有了OS的时候,它就专门负责资源的调配,让各个任务都能很好的实施,起一个决策者的作用。

<2>如果我们要做一个产品,软件系统到底应该是裸机还是基于操作系统呢?本质上取决于产品本身的复杂度。只有极简单的功能、使用极简单的CPU(譬如单片机)的产品才会选择用裸机开发;一般的复杂性产品都会选择基于操作系统来开发。

<3>操作系统负责管理和资源调配,应用程序负责具体的直接劳动,他们之间的接口就是API函数。当应用程序需要使用系统资源(譬如内存、譬如CPU、譬如硬件操作)时就通过API向操作系统发出申请,然后操作系统响应申请帮助应用程序执行功能。

8.2、C库函数和API的关系:

<1>从内核的角度看,需要考虑提供哪些有用的API函数,并不需要关注它们如何被使用。故编程时用API函数会感觉到不好用,没有做优化。系统只负责做出一个可以用的API,没有考虑到用户使用的方便性。所以c库函数对API做了一些优化,让用户使用库函数更容易达到我们想要的目的。

<2>库函数实质还是用的API,或者调用了一个API,也或者调用了更多的API,只不过是做了更多的优化。比如 库函数fopen ,而API是open.

<3>有的库函数没有用到API,比如strcpy函数(复制字符串)和atoi函数(转换ASCII为整数),因为它们并不需要向内核请求任何服务。

8.3、不同平台(windows、linux、裸机)下库函数的差异

(1)不同操作系统API是不同的,但是都能完成所有的任务,只是完成一个任务所调用的API不同。

(2)库函数在不同操作系统下也不同,但是相似性要更高一些。这是人为的,因为人下意识想要屏蔽不同操作系统的差异,因此在封装API成库函数的时候,尽量使用了同一套接口,所以封装出来的库函数挺像的。但是还是有差异,所以在一个操作系统上写的应用程序不可能直接在另一个操作系统上面编译运行。于是乎就有个可移植性出来了。

(3)跨操作系统可移植平台,譬如QT、譬如Java语言。

8.4、

<1>main()函数的写法

int main(int argc, char **argv) ;
int main(int argc ,char *argv[ ] ); 

这两种写法是一样的。二重指针等同于指针数组。

<2>不管是主函数还是功能函数,它都应该有一个返回值,而主函数的返回值是给调用的那个人的。main函数从某种角度来讲代表了我当前这个程序,或者说代表了整个程序。main函数的开始意味着整个程序开始执行,main函数的结束返回意味着整个程序的结束。谁执行了这个程序,谁就调用了main。谁执行了程序?或者说程序有哪几种被调用执行的方法?一个程序当然会运行,那么就是调用了main( ).

<3>inux下一个新程序执行的本质

(1)表面来看,linux中在命令行中去./xx执行一个可执行程序

(2)我们还可以通过shell脚本来调用执行一个程序

(3)我们还可以在程序中去调用执行一个程序(fork exec)

总结:我们有多种方法都可以执行一个程序,但是本质上是相同的。linux中一个新程序的执行本质上是一个进程的创建、加载、运行、消亡。linux中执行一个程序其实就是创建一个新进程然后把这个程序丢进这个进程中去执行直到结束。

新进程是被谁开启?在linux中进程都是被它的父进程fork出来的。

分析:命令行本身就是一个进程,在命令行底下去./xx执行一个程序,其实这个新程序是作为命令行进程的一个字进程去执行的。

总之一句话:一个程序被它的父进程所调用。

结论:main函数返回给调用这个函数的父进程。父进程要这个返回值干嘛?父进程调用子进程来执行一个任务,然后字进程执行完后通过main函数的返回值返回给父进程一个答复。这个答复一般是表示子进程的任务执行结果完成了还是错误了。(0表示执行成功,负数表示失败,正规的要求返回失败的原因,返回-1表示什么,返回-2又表示什么,然后父进程好做相应的处理)

(4) main函数的返回值应当是int 型。父进程要求是int型的,如果写成 float 型,则返回就为0,这样是要出错的。

8.5 用shell脚本来看main()的返回值。如:#!/bin/sh 其文本格式为 .sh

./a.out
echo $?

8.6、argc、argv与main函数的传参:当我们的父进程不需要传参时,就用 int main(void);当我们需要传参时,就应该是int main(int argv ,函数char *argc[ ]);它默认本身就是一个参数,占了argv[0]这个位置,它里面存的是 ./a.out(这个相应变化)

如:./a.out boy girl ;

argv=3; 
argc[0]="./a.out"; 
argc[1]="boy"; 
argc[2]="girl" ;
 printf("%s\n",argc[0]);

解释:argv表示传了多少个参数,argc实质是存的一个指针,也就是一个地址,只是没有一个被绑定的变量名而已。这个地址指向一个字符串,一般字符串都和指针相关。所以可以称之为字符串数组,每一个都存了一个字符串。

在程序内部如果要使用argc,那么一定要先检验argv,先检验个数,然后使用。

8.7、void类型的本质:即使空型又是未知类型,看具体情况。比如一个函数void表示不返回, void * malloc(20);就是未知类型。

(1)编程语言分2种:强类型语言和弱类型语言。强类型语言中所有的变量都有自己固定的类型,这个类型有固定的内存占用,有固定的解析方法;弱类型语言中没有类型的概念,所有变量全都是一个类型(一般都是字符串的),程序在用的时候再根据需要来处理变量。就如:makefile、html语言。

(2)C语言就是典型的强类型语言,C语言中所有的变量都有明确的类型。因为C语言中的一个变量都要对应内存中的一段内存,编译器需要这个变量的类型来确定这个变量占用内存的字节数和这一段内存的解析方法。

(3)void类型的正确的含义是:不知道类型,不确定类型,还没确定类型、未知类型,但是将来一定有类型。

*(4)void a;(编译器可以通过)定义了一个void类型的变量,含义就是说a是一个指针,而且a肯定有确定的类型,只是目前我还不知道a的类型,还不确定,所以标记为void。void “修饰”的是指针,因为指针就是内存地址,它不知道指向的另一个变量是哪一种类型,而变量一定是确定的,void a;就是错误的。

8.9、C语言中的NULL

NULL在C/C++中的标准定义

(1)NULL不是C语言关键字,本质上是一个宏定义,其保护指针的作用,不要让他乱开枪。

(2)NULL的标准定义:

ifdef _cplusplus // 条件编译c++环境
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地址开始的一段内存)。如 char *p=NULL;因为0地址本身就不是我们来访问的,所以 *p时是不可访问的。在程序运行的逻辑上就不会出错。

正规写:

int *p = NULL;// 定义p时立即初始化为NULL
p = xx;
if (NULL != p)
{
*p // 在确认p不等于NULL的情况下才去解引用p
}

(1)'\0'是一个转义字符,他对应的ASCII编码值是0,内存值是0,一个char空间。

(2)'0'是一个字符,他对应的ASCII编码值是48,内存值是int型48,一个char空间。

(3)0是一个数字,没有ASCll编码, 内存值是int型0,一个int空间。

(4)NULL是一个表达式,是强制类型转换为void *类型的0,内存值是0(内存地址),一个int空间。

8.9.1、运算中的临时匿名变量

<1>“小动作”:高级语言在运算中允许我们大跨度的运算。意思就是低级语言中需要好几步才能完成的一个运算,在高级语言中只要一步即可完成。譬如C语言中一个变量i要加1,在C中只需要i++即可,看起来只有一句代码。但实际上翻译到汇编阶段需要3步才能完成:第1步从内存中读取i到寄存器,第2步对寄存器中的i进行加1,第3步将加1后的i写回内存中的i。

<2> float a=12.3; int b=(int)a; (int )a 就是匿名变量;先找一个内存空间,里面存(int)a; 然后把这个值赋值给b;最后匿名值销毁。

float a; int b=10; a=b/3;左边是3.00000; 右边是3;其中有个匿名变量,先找一个内存空间,里面存 b/3; 然后把它再转换成float型,再赋值个a;最后匿名值销毁。

8.9.2 分析DEBUG宏

学习级:

define DEBUG #undef DEBUG 是注销 DEBUG 宏
ifdef DEBUG
define debug(x) printf(x)
else
define debug(x)
endif

应用级:

ifdef DEBUG
define DBG(...) fprintf(stderr, " DBG(%s, %s( ), %d): ", FILE, FUNCTION, LINE); fprintf(stderr, VA_ARGS)
else
define DBG(...)
endif

解 释:
<1>...表示变参,提示编译器不要对参数个数斤斤计较,不要报错; 其实完全可以把 ...换成 cdw 也是可以的,只是非要装一下而已。

<2> FILE 和 FUNCTION和 LINE 都是c库函数的宏定义,分别表示要输出的这句话属于哪个文件名、属于哪个函数名、在第几行。

<3> 在 fprintf(stderr,"cdw");其中stderr是c库函数中宏定义了的,这是VC6.0找到的 #define stderr (&_iob[2]) ;也就是说stderr是一个被宏定义了的指针,它是标准错误输出流对象(stderr),输出到屏幕上。

fprintf()是C/C++中的一个格式化写—库函数,位于头文件中,其作用是格式化输出到一个流/文件中;(重点是流/文件)

printf()函数是格式化输出函数, 一般用于向标准输出设备按规定格式输出(重点是标准输出设备,有时候输出的不一定显示在屏幕上,只是编译器规定显示到屏幕上而已。)

总结:也就是说printf()其实不是输出屏幕上的,只是这个标准输出设备中,编译器规定显示到屏幕上而已,而真正输出到屏幕是fprintf(stderr,"cdw");其中stderr就是输出到屏幕上的流。它也可以fprintf( FILE *stream, const char *format,...),这个就是输出到文件流中的。

比如:一般情况下,你这两个语句运行的结果是相同的,没有区别,只有一下情况才有区别:运行你的程序的时候,命令行上把输出结果进行的转向,比如使用下面的命令把你的程序a.c运行的结果转向到记事本文件a.txt:a.exe > a.txt

在这样的情况,如果使用printf输出错误信息,会保存到a.txt文件里面,如果使用fprintf输出错误,会显示在屏幕上。

<4>上面中的VA_ARGS也是一个宏定义,表示预处理时实际的参数。
如:DBG("tiaoshi.\n");

则允许的效果是 DBG(debug.c, main( ), 14): tiaoshi.

内核级:

ifdef DEBUG_S3C_MEM
define DEBUG(fmt, args...)printk(fmt, ##args)
else
define DEBUG(fmt, args...)do {} while (0)
endif

往期文章列表:****往期热文:
基础C语言知识串串香(1)

基础C语言知识串串香(2)

基础C语言知识串串香(3)

基础C语言知识串串香(4)

基础C语言知识串串香(5)

基础C语言知识串串香(6)

基础C语言知识串串香(7)

基础C语言知识串串香(8)

基础C语言知识串串香(9)

基础C语言知识串串香(10)

基础C语言知识串串香(11)

基础C语言知识串串香(12)


===========我是华丽的分割线===========


更多知识:
点击关注专题:嵌入式Linux&ARM

或浏览器打开:https://www.jianshu.com/c/42d33cadb1c1

或扫描二维码: