1.操作系统是什么玩意
1.1、像人类社会一样的计算机软件系统(有些人只埋头干活,有些人只做管理)
(1)人类社会最开始时人人都干活,这时候没有专业分工,所有人都直接做生产价值的工作。当时是适合的,因为当时生产力低下,人口稀少。这就像裸机程序一样(裸机程序的特点是:代码量小、功能简单、所有代码都和直接目的有关,没有服务性代码)。
(2)后来人口增加生产力提高,有一部分人脱离了直接生产价值的体力劳动专职指挥(诞生了阶级)。
本质上来说是合理的,因为资源得到了最大限度的使用,又花了配置,提升了整体效率。
程序也是一样,当计算机技术发展,计算机性能和资源大量增加,这时候写代码也要产生阶级,也要进行分工,不然所有代码都去参加直接性的工作,则整体系统效率不高。(因为代码很难进行资源的优化配置)。
(3)解决方案就是操作系统。操作系统就是分出来的管理阶级,操作系统的代码本身并不直接产生价值,它的主要任务是管理所有资源,它主要为直接产生价值、直接劳动的那些程序(各种应用程序)提供服务。所以操作系统既是管理者也是服务者(党)。
(4)我们要做的一个产品,软件系统到底应该是裸机还是基于操作系统呢?本质上取决于产品的复杂度。只有极简单的功能、使用极简单的CPU(比如单片机)的产品才会选择用裸机开发;一般的复杂性产品都会选择基于操作系统来开发的。
1.2、操作系统的调用通道:API函数
(1)操作系统负责管理和资源调配,应用程序负责具体的直接劳动,他们之间的接口就是API函数。当应用程序需要使用系统资源(比如内存、CPU、硬件操作)时就通过API向操作系统发出申请,然后操作系统响应申请帮助应用程序执行功能。
1.3、C库函数和API的关系
(1)单纯的API只是提供了简单没有任何封装的服务函数,这些函数应用程序是可用的,但是不太好用。应用程序为了好用,就对这个API进行了二次封装,把它变得好用一些,于是就成了C库函数。
(2)有时完成一个功能,有相应的库函数可以完成也有API可以完成,用哪个都行。比如读写文件,API的接口是open、write、read、close;库函数的接口是fopen、fwrite、fread、fclose;fopen本质上是用open实现的,只是进行了封装。封装肯定有目的(添加缓冲机制)。
1.4、不同平台(Windows、Linux、裸机)下库函数差异
(1)不同操作系统的API是不同的,但是都能完成所有任务,只是完成一个任务所调用的API不同。
(2)库函数在不同操作系统下也不同,但是相似性要更高一些。这是人为的,因为人下意识想要屏蔽不同操作系统的差异,因此在封装API成库函数的时候,尽量使用了同一套接口,所以封装在操作系统上面编译运行。于是乎就有个可移植性出来了。
(3)跨操作系统可抑制平台,比如qt、JAVA。
1.5、操作系统的重大意义:软件体系分工
操作系统的重大意义:软件体系分工有了操作系统后,我们做一个产品首先分成2部分:一部分负责做操作系统(开发驱动的),一部分负责操作系统实现具体功能(开发应用)。实际,上层应用层的功能进一步复杂化后又分了好多层。
2.main函数返回值给谁
2.1、函数为什么需要返回值
(1)函数在设计的时候设计了参数和返回值,参数是函数的输入,返回值是函数的输出。
(2)因为函数需要对外输出数据(实际上是函数运行的一些结果值)因此需要返回值。
(3)形式上来说,函数被另一个函数所调用,返回值作为函数式的值返回给调用这个函数的地方。
总结:函数的返回值就时给调用它的人返回一个值。
2.2、main函数被谁调用
(1)main函数是特殊的,首先这个名字是特殊的。因为C语言规定了main函数是整个程序的入口。其他的函数只有直接或间接被main函数所调用才能被执行,如果没有被main直接/间接调用则这个函数在整个程序中无用。
(2)main函数从某种角度来讲代表了我当前这个程序,或者说代表整个程序。main函数的开始意味着整个程序开始执行,main函数的结束返回意味着整个程序的结束。
(3)谁知行了这个程序,谁就调用了main。
(4)谁执行了程序?或者说程序有哪几种被调用执行的方法。
2.3、Linux下一个新程序执行的本质
(1)从表面来看,Linux中在命令行中去./xx执行一个可执行程序。
(2)我们还可以通过shell脚本来调用执行一个程序。
(3)我们还可以在程序中去调用执行一个程序(fork exec)
(4)总结:我们有很多种方法都可以执行一个程序,但是本质上是相同的。Linux中一个新程序的执行本质上是一个进程的创建、加载、运行、消亡。Linux中执行一个程序其实就是创建一个新进程然后把这个程序丢进这个进程去执行直到结束。新进程是被谁开启?在Linux中进程都是被它的父进程fork出来的。
(5)分析:命令行本身就是一个进程,在命令行低下去./xx执行一个程序,契税这个新程序是作为命令行程序的一个子进程去执行的。总之,一个程序被它的父进程调用。
(6)main函数返回给调用这个函数的父进程。父进程要这个返回值干嘛?父进程调用子进程来执行一个任务,然后子进程执行完后通过main函数的返回值返回给父进程一个答复。这个答复一般是表示子进程的任务执行结果完成了还是错误了。(0表示执行成功,负数表示失败)。
2.4、实践验证获取main的返回值
(1)用shell脚本执行程序可以获取程序的返回值并且打印出来。
(2)Linux的shell中用$?这个符号来存储和表示上一个程序执行结果。
touch return.sh
vi return.sh
#!/bin/sh
./a.out
echo $?
gcc xx.c
source return.sh
3.argc、argv与main函数的传参
3.1、谁给main函数传参
调用main函数所在的程序的它的父进程给main函数传参,并且接收main的返回值。
3.2、为什么需要给main函数传参
(1)首先,main函数不传参是可以的,也就是说父进程调用子程序并且给子程序传参不是必须的。int main (void)这种形式就表示我们认为不必要给main传参。
(2)有时候我们希望程序有一种灵活性,所以选择在执行程序时通过传参来控制程序中的运行。达到不需要重新编译程序程序就可以改变运行结果的效果。
3.3、表面上:给main函数传参是怎样实现的
(1)给main传参通过argc和argv这两个C语言预设的参数来实现。
(2)argc是int类型,表示运行程序的时候给main函数传递几个参数:argv是一个字符串数组,这个数组用来存储多个字符串,每个字符串就是我们给main函数传的一个参数。argv[0]就是我们给main函数的第一个传参,argv[1]就是我们给main函数的第二个传参……
int main(int argc, char *argv[])
{
int i = 0;
printf("main函数传参个数是:%d.\n", argc);
for (i = 0; i<argc; i++)
{
printf("第%d个参数是%s.\n", i, argv[i]);
}
return 0;
}
命令:./a.out abc xxx 33
3.4、本质上:给main函数传参时怎样实现的
(1)程序调用有各种方法但是本质上都是父进程fork一个子进程,然后子进程和一个程序绑定起来去执行(exec函数族),我们在exec的时候可以给他同时传参。
(2)程序调用时可以被传参(也就是main的传参)是操作系统层面的支持完成的。
3.5、给main函数传参要注意什么
(1)main函数传参都是通过字符串传进去的
(2)程序被调用时传参,各个参数之间是通过空格来间隔的。
(3)在程序内部如果要使用argv,那么一定要先检查argv(if语句检查)。
题目:写个计算器,然后运行时./calculator 3+5
,程序执行返回8。
4.void类型的本质
4.1、C语言属强类型语言
(1)编程语言分2种:强类型语言和弱类型语言。①强类型语言中所有的变量都有自己固定的类型,这个类型有固定的内存占用,有固定的解析方法;②弱类型语言中没有类型的概念,所有变量都是一个类型(一般都是字符串类型),程序在用的时候在根据需要来处理变量。
(2)C语言就是典型的强类型语言,C语言中所有的变量都有明确的类型的概念,因为C语言中的一个变量都要对应内存中的一段内存,编译器需要这个变量的类型来确定这变量占用内存的字节数和这一段内存的解析方法。
4.2、数据类型的本质含义
(1)数据类型的本质就决定变量的内存占用数和内存的解析方法。
(2)所以得出结论:C语言中变量必须有确定的数据类型,如果一个变量没有确定的类型(就是所谓的无类型)会导致编译器无法给这个变量分配内存,也无法解析这个变量对应的内存。因此得出结论不可能存在没有类型的变量。
(3)但是C语言中可以有没有类型的内存。在内存还没有和具体的变量相绑定之前,内存就可以没有类型。实际上纯粹的内存就是没有类型的,内存只是和具体的变量相关联后才有了明确的类型(其实内存自己本身是不知道自己存的东西的类型的,而编译器知道,我们程序在使用这个内存时知道类型所以会按照该类型的含义去进行内存的读和写)。
4.3、void类型的本质
(1)void类型的正确含义是:不知道类型,不确定类型,还没确定类型。
(2)void a;定义了一个void类型的变量,含义就是说a是一个变量,而且a肯定有确定的类型,只是我目前不知道,还不确定,所以标记为void。
4.4、为什么需要void类型
(1)什么情况下需要void类型?其实就是在描述一段还没有具体使用的内存时需要使用void类型。
(2)void的一个典型应用案例就是malloc的返回值,我们知道malloc函数向系统堆管理器申请一段内存给当前程序使用,malloc返回的是一个指针,这个指针指向申请的那段内存。malloc刚申请的这段内存尚未使用来存储数据,malloc函数也无法预知这段内存将来被存放什么类型的数据,所以malloc无法返回具体类型的指针,解决方法就是返回一个void *类型,告诉外部我返回的是一段干净的内存空间,尚未确定类型。所以我们在malloc之后可以给这段内存读写任意类型的数据。
(3)void *类型的指针指向的内存是尚未确定类型的,因此我们后续可以使用强制类型转换强行将其转为各种类型。这就是void类型的最终归宿,就是被强制类型转换成一个具体类型。
(4)void类型使用时一般都是用void *,而不是仅仅使用void。int *p = (int *) malloc (4)
5.C语言中的NULL
5.1、NULL在C/c++中的标准定义
(1)NULL不是C语言的关键字,本质上是一个宏定义
(2)NULL的标准定义:
#ifdef __cplusplus
#define NULL 0
#else
#define NULL (void *)0
#endif
解释:
①C++的编译环境中,编译器先定义了一个宏__cplusplus,程序中可以用条件编译来判断当前的编译环境是C++还是C的。
②NULL的本质是0,但是这个0不是当一个数字解析,而是当一个内存地址来解析的,这个0其实是0x00000000代表内存0地址。(void *)0这个整体表达式表示一个指针,这个指针变量本身占4字节,地址在哪里取决于指针变量本身,但是这个指针变量的值是0,也就是说这个指针变量指向0地址(实际是0地址开始的一段内存)。
5.2、从指针角度理解NULL的本质
(1)int *p;
p是一个函数内部的局部变量,则p的值是随机的,也就是说p是一个野指针。
(2)int *p = NULL;
p是一个局部变量,分配在栈上的地址是由编译器决定的,我们不必关心,但是p的值是(void *)0,实际就是0,意思是指针p指向内存的0地址处。这时候b就不是野指针了。
(3)为什么要让一个野指针指向内存地址0处?
主要是因为大部分的CPU中,内存的0地址处都不是随便访问的(一般都是操作系统严密管控区域,所以应用程序不能随便访问)。所以野指针指向了这个区域可以保证野指针不会造成误伤。如果程序无意识的解引用指向0地址处的野指针则会触发段错误。这样就可以提示你帮助你找到程序中的错误。
5.3、为什么需要NULL
(1)第一个作用就是让野指针指向0地址处安全。
(2)第二个作用就是一个特殊的标记。按照标准的指针使用步骤是:
int *p = NULL; // 定义时立即初始化为NULL
p = xx;
if (NULL != p)
{
*p; // 在确认p不等于NULL的情况下去解引用p
}
p = NULL;
注意:一般比较一个指针和NULL是否相等不写成if (p == NULL);而写成if (NULL != p)。原因是第一种写法如果不小心把==写成了=,则编译器不会报错,但是程序意思完全不一样了,而第二种写法则编译器会发现并报错。
5.4、注意不要混用NULL和'\0'
(1)'\0'和'0'和0和NULL几个区分开来
(2)'\0'是一个转义字符,它对应的ASCII编码值是0,本质就是0;
(3)'0'是一个字符,它对应的ASCII编码值是48,本质是48;
(4)0是一个数字,它就是0,本质是0。
(5)NULL是一个表达式,是强制类型转换为void 类型的0,本质是0.
(6)总结:*
①'\0'用法是C语言字符串的结尾标志,一般用来比较字符串中的字符以判断字符串有没有到头;
②'0'是字符0,对应0这个字符的ASCII编码值,一般用来获取0的ASCII编码值(48);
③0是一个数字,一般用来比较一个int类型的数字是否等于0;
④NULL是一个表达式,一般用来比较指针是否是一个野指针。
6.运算中的临时匿名变量
6.1、C语言和汇编的区别(汇编完全对应机器操作,C对应逻辑操作)
(1)C语言叫做高级语言,汇编语言叫做低级语言。
(2)低级语言的意思是汇编语言和机器操作相对应,汇编语言只是CPU的机器码的助记符,用汇编语言写程序必须拥有机器的思维。因为不同的CPU设计时指令集差异很大,所以用汇编编程的差异也很大。
(3)高级语言(C语言)它对低级语言进行了封装(C语言的编译器来完成),给程序员提供了一个靠近人类思维的一些语法特征,人类不用过于考虑机器原理,而可以按照自己的逻辑原理来编程。比如数组、结构体、指针……
(4)更高级的语言如JAVA、C#等知识进一步强化了C语言提供的人性化的操作界面语法,在易用性上、安全性上进行了提升。
6.2、C语言的一些“小动作”
(1)高级语言中有一些元素是机器中没有的。
(2)高级语言在运算中允许我们大跨度的运算。意思就是低级语言中需要好几步才能完成的一个运算,在高级语言中只要一步即可完成。比如C语言中一个变量i要加1,在C中只需要i++即可,看起来只有一句代码,但是实际上翻译到汇编阶段需要3步才能完成,第1步从内存中读取i到寄存器,第2步对寄存器中的i进行加1,第3步将加1后的i写回内存中的i。
6.3、使用临时变量来理解强制类型转换
(1)显式转换float a =12.34; int b = (int)a;
(int)a强制类型转换并赋值在底层实际分了4个步骤:
①第一步现在另外的地方找一个内存构建一个临时变量x(x的类型是int,x的值等于a的整数部分);
②第二步将float a的值的整数部分赋值给x;
③第三步将x赋值给b;
④第四步销毁x。
最后结果:a还是float而且值保持不变,b是a的整数部分。
(2)隐式转换
int b;
float a;
b = 10;
a = b / 3; // a = 3.000000
①第一步先算b/3;
②第二步将第一步的结果强制类型转换为float生成一个临时变量;
③第三步将第二步生成的临时变量赋值给a;
④第四步销毁临时变量。
(链表与多进程准备学完单片机再回来更)