1、数据类型

    1.1、基本数据类型

    1. 数据类型分2类:基本数据类型+复合类型
    2. 基本类型:char short int long float double
    3. 复合类型:数组 结构体 共用体 C语言没有类,c++有)

    1.1.1、内存占用与sizeof运算符

    1. 数据类型就好像一个一个模子,这个模子实例化出C语言的变量,变量存储在内存中,
    2. 需要占用一定空间。变量占用多少空间是数据类型决定的。每种数据类型,在不同机器
    3. 平台占用内存是不同的,我们一般默认以32CPU来描述的。
    1. int mian (void)
    2. {
    3. int len;
    4. len=sizeof(char);
    5. printf("sizeof(char)=%d\n",sizeof(char));
    6. len=sizeof(int);
    7. printf("sizeof(int)=%d\n",sizeof(int));
    8. len=sizeof(float);
    9. printf("sizeof(float)=%d\n",sizeof(float));
    10. }

    1.1.2、有符号数和无符号数

    1. 对于char short int long 等整形类型的数,都分有符号和无符号数。
    2. 而对于float double这种浮点数来说,只有有符号数,没有无符号数。
    3. C语言来说,数(变量)存储是二进制方式存储的,整形数和浮点数存储方式上的不同

    总结:存取方式有两种:一种整形另一种浮点型,绝对不可以改变一个变量存取方式,
    在整形或浮点型之内,char short int long 只是范围大小精度不同。

    1.2、空类型(关键字void)

    1.2.1、位操作符
    要点: 要置1用|, 清零用&, 取反用^, ~和<<>>用来构建特定的二进制数。

    1.2.3、提高:宏定义完成位运算

    操作系统究竟是个什么玩意?
    (1)人类社会最开始时人人都干活,这时候没有专业分工,所有人都直接做产生价值的工作。当时是合适的,因为当时生产力低下,人口稀少。这就像裸机程序一样(裸机程序的特点是:代码量小,功能简单、所有代码都和直接目的有关,没有服务性代码)。
    (2)后来人口增加生产力提高,有一部分人脱离了直接产生价值的体力劳动专职指挥(诞生了阶级)。本质上来说是合理的,因为资源得到了更大限度的使用,优化了配置,提升了整体效率。程序也是一样,当计算机技术发展,计算机性能和资源大量增加,这时候写代码也要产生阶级也要进行分工,不然如果所有代码都去参加直接性的工作,则整体系统效率不高。(因为代码很难进行资源的优化配置)。
    (3)解决方案就是操作系统。操作系统就是分出来的管理阶级,操作系统的代码本身并不直接产生价值,它的主要任务是管理所有资源,它主要为直接产生价值、直接劳动的那些程序(各种应用程序)提供服务。所以操作系统既是管理者也是服务者。
    (4)裸机程序就好象小公司,操作系统下的程序就好象大型跨国公司;裸机程序就好象小国家,操作系统下程序就好象大国家;如果我们要做一个产品,软件系统到底应该是裸机还是基于操作系统呢?本质上取决于产品本身的复杂度。只有极简单的功能、使用极简单的CPU(譬如单片机)的产品才会选择用裸机开发;一般的复杂性产品都会选择基于操作系统来开发。

    操作系统的调用通道:API函数

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

    C库函数和API的关系

    (1)单纯的API只是提供了极简单没有任何封装的服务函数,这些函数应用程序是可用的,但是不太好用。应用程序为了好用,就对这个API进行了二次封装,把它变得好用一些,于是就成了C库函数。
    (2)有时完成一个功能,有相应的库函数可以完成,也有API可以完成,用哪个都行。譬如读写文件,API的接口是open write read close;库函数的接口是fopen fwrite fread fclose。fopen本质上是使用open实现的,只是进行了封装。封装肯定有目的(添加缓冲机制)。

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

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

    操作系统的重大意义:软件体系分工

    (1)有了操作系统后,我们做一个产品可以首先分成2部分:一部分人负责做操作系统(开发驱动的);一部分人负责用操作系统实现具体功能(开发应用)。实际上上层应用层的功能进一步复杂化后又分了好多层。

    函数为什么需要返回值

    (1)函数在设计的时候设计了参数和返回值,参数是函数的输入,返回值是函数的输出。
    (2)因为函数需要对外输出数据(实际上是函数运行的一些结果值)因此需要返回值
    (3)形式上来说,函数被另一个函数所调用,返回值作为函数式的值返回给调用这个函数的地方
    总结:函数的返回值就是给调用它的人返回一个值

    main函数被谁调用

    (1)main函数是特殊的,首先这个名字是特殊的。因为C语言规定了main函数是整个程序的入口。其他的函数只有直接或间接被main函数所调用才能被执行,如果没有被main直接/间接调用则这个函数在整个程序中无用。
    (2)main函数从某种角度来讲代表了我当前这个程序,或者说代表了整个程序。main函数的开始意味着整个程序开始执行,main函数的结束返回意味着整个程序的结束。
    (3)谁执行了这个程序,谁就调用了main。
    (4)谁执行了程序?或者说程序有哪几种被调用执行的方法?

    linux下一个新程序执行的本质

    (1)表面来看,linux中在命令行中去./xx执行一个可执行程序
    (2)我们还可以通过shell脚本来调用执行一个程序
    (3)我们还可以在程序中去调用执行一个程序(fork exec)
    总结:我们有多种方法都可以执行一个程序,但是本质上是相同的。linux中一个新程序的执行本质上是一个进程的创建、加载、运行、消亡。linux中执行一个程序其实就是创建一个新进程然后把这个程序丢进这个进程中去执行直到结束。新进程是被谁开启?在linux中进程都是被它的父进程fork出来的。
    分析:命令行本身就是一个进程,在命令行底下去./xx执行一个程序,其实这个新程序是作为命令行进程的一个字进程去执行的。
    总之一句话:一个程序被它的父进程所调用。
    结论:main函数返回给调用这个函数的父进程。父进程要这个返回值干嘛?父进程调用子进程来执行一个任务,然后字进程执行完后通过main函数的返回值返回给父进程一个答复。这个答复一般是表示子进程的任务执行结果完成了还是错误了。(0表示执行成功,负数表示失败)

    实践验证获取main的返回值

    (1)用shell脚本执行程序可以获取程序的返回值并且打印出来
    (2)linux shell中用$?这个符号来存储和表示上一个程序执行结果。

    argc、argv与main函数的传参
    谁给main函数传参
    (1)调用main函数所在的程序的它的父进程给main函数传参,并且接收main的返回值。
    4.8.3.2、为什么需要给main函数传参
    (1)首先,main函数不传参是可以的,也就是说父进程调用子程序并且给子程序传参不是必须的。 int main(void)这种形式就表示我们认为不必要给main传参。
    (2)有时候我们希望程序有一种灵活性,所以选择在执行程序时通过传参来控制程序中的运行,达到不需要重新编译程序就可以改变程序运行结果的效果。

    表面上:给main传参是怎样实现的?
    (1)给main传参通过argc和argv这两个C语言预订的参数来实现
    (2)argc是int类型,表示运行程序的时候给main函数传递了几个参数;argv是一个字符串数组,这个数组用来存储多个字符串,每个字符串就是我们给main函数传的一个参数。argv[0]就是我们给main函数的第一个传参,argv[1]就是传给main的第二个参数····

    本质上:给main传参是怎样实现的?
    (1)上节课讲过,程序调用有各种方法但是本质上都是父进程fork一个子进程,然后字进程和一个程序绑定起来去执行(exec函数族),我们在exec的时候可以给他同时传参。
    (2)程序调用时可以被传参(也就是main的传参)是操作系统层面的支持完成的。

    给main传参要注意什么
    (1)main函数传参都是通过字符串传进去的。
    (2)程序被调用时传参,各个参数之间是通过空格来间隔的。
    (3)在程序内部如果要使用argv,那么一定要先检验argc。

    题目:写个计算器,然后运行时可以 ./calculator 3 + 5,程序执行返回8


    void类型的本质
    C语言属强类型语言
    (1)编程语言分2种:强类型语言和弱类型语言。强类型语言中所有的变量都有自己固定的类型,这个类型有固定的内存占用,有固定的解析方法;弱类型语言中没有类型的概念,所有变量全都是一个类型(一般都是字符串的),程序在用的时候再根据需要来处理变量。
    (2)C语言就是典型的强类型语言,C语言中所有的变量都有明确的类型。因为C语言中的一个变量都要对应内存中的一段内存,编译器需要这个变量的类型来确定这个变量占用内存的字节数和这一段内存的解析方法。

    数据类型的本质含义
    (1)数据类型的本质就决定变量的内存占用数,和内存的解析方法。
    (2)所以得出结论:c语言中变量必须有确定的数据类型,如果一个变量没有确定的类型(就是所谓的无类型)会导致编译器无法给这个变量分配内存,也无法解析这个变量对应的内存。因此得出结论不可能有没有类型的变量。
    (3)但是C语言中可以有没有类型的内存。在内存还没有和具体的变量相绑定之前,内存就可以没有类型。实际上纯粹的内存就是没有类型的,内存只是因为和具体的变量相关联后才有了确定的类型(其实内存自己本身是不知道的,而编译器知道,我们程序在使用这个内存时知道类型所以会按照类型的含义去进行内存的读和写)。

    void类型的本质
    (1)void类型的正确的含义是:不知道类型,不确定类型,还没确定类型。
    (2)void a;定义了一个void类型的变量,含义就是说a是一个变量,而且a肯定有确定的类型,只是目前我还不知道a的类型,还不确定,所以标记为void。

    为什么需要void类型
    (1)什么情况下需要void类型?其实就是在描述一段还没有具体使用的内存时需要使用void类型。
    (2)void的一个典型应用案例就是malloc的返回值。我们知道malloc函数向系统堆管理器申请一段内存给当前程序使用,malloc返回的是一个指针,这个指针指向申请的那段内存。malloc刚申请的这段内存尚未用来存储数据,malloc函数也无法预知这段内存将来被存放什么类型的数据,所以malloc无法返回具体类型的指针,解决方法就是返回一个void 类型,告诉外部我返回的是一段干净的内存空间,尚未确定类型。所以我们在malloc之后可以给这段内存读写任意类型的数据。
    (3)void
    类型的指针指向的内存是尚未确定类型的,因此我们后续可以使用强制类型转换强行将其转为各种类型。这就是void类型的最终归宿,就是被强制类型转换成一个具体类型。
    (4)void类型使用时一般都是用void ,而不是仅仅使用void。


    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地址开始的一段内存)。

    从指针角度理解NULL的本质
    (1)int
    p; // p是一个函数内的局部变量,则p的值是随机的,也就是说p是一个野指针。
    (2)int p = NULL; // p是一个局部变量,分配在栈上的地址是由编译器决定的,我们不必关心,但是p的值是(void )0,实际就是0,意思是指针p指向内存的0地址处。这时候p就不是野指针了。
    (3)为什么要让一个野指针指向内存地址0处?主要是因为在大部分的CPU中,内存的0地址处都不是可以随便访问的(一般都是操作系统严密管控区域,所以应用程序不能随便访问)。所以野指针指向了这个区域可以保证野指针不会造成误伤。如果程序无意识的解引用指向0地址处的野指针则会触发段错误。这样就可以提示你帮助你找到程序中的错误。

    为什么需要NULL
    (1)第一个作用就是让野指针指向0地址处安全。
    (2)第二个作用就是一个特殊标记。按照标准的指针使用步骤是:
    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)。原因是第一种写法中如果不小心把==写成了=,则编译器不会报错,但是程序的意思完全不一样了;而第二种写法如果不小心把==写成了=则编译器会发现并报错。