C语言部分学习笔记整理

xiaoxiao2021-02-28  139


———————————内存————————— ——计算机程序运行的实质 处理数据的到一定的结果 ——哈弗&&冯诺依曼结构 冯诺依曼结构:代码和数据统一存放 哈佛结构:代码和数据分开存放 代码:代码是死的 存在计算机中(flash )函数?? 数据:全局变量 局部变量 需要存放在内存中ram中 _DRAM SRAM 动态ram 静态ram Dram 需要初始化 Sram 不需要初始化 ——为什么程序运行需要内存? 程序运行需要数据 数据是存放在内存中的所以内存是程序运行的本质需求 ——如何管理内存?? 从操作系统角度来讲 有操作系统 操作系统管理硬件的所有内存,它是将内存分割成一个一个的快进行管理,这方面的管理十分复杂,我们需要开辟内存是只需要调用相应的API即可例如(malloc)(free) 无操作系统 无操作系统就是逻辑程序,我们使用内存需要自己动手去指定比如(0x d002 0010) 从语言角度来讲: 汇编语言 就和逻辑差不多 C语言 内存管理利用(malloc)(free) 其实给内存管理了一些封装 C++ ---- C#,Java: 都不用直接管理 有虚拟机(狗腿子)帮助管理 内存的开辟和释放 位 的逻辑 字 半字位宽 什么是内存?(硬件和逻辑两个角度) 从硬件角度: 其实就是计算机内部的一个硬件组成比如(一般叫内存条)按照原理分成 ——SRAM DRAM 从逻辑角度: 它是那么一种东西可以随机访问,这就表示了它与变量的配合,即可读可写,它在计算机中存储数据,表现为一个一个的单元格子,这一个一个的格子有它自己的编号,里面可以存放数据。 (变量) 内存的逻辑抽象图(内存的编程模型) 就相当于一个一个的楼房,楼房里有许多的房间,地址就相当于房间号,我们访问这些房间的时间就可以用这些房间号去访问房间的存放的东西。 逻辑上的内存大小 逻辑上内存的大小应该是无穷无尽的,因为数学的编号是无穷的,但是在32位机中,内存的大小应该<= 4G 且每个内存的地址(房间编号)都应该是与它本身绑定的,不可改变的,我们访问这个编号就相当于进了这个房子。(固定地址) 内存位宽 内存的位宽有8bit 16bit 但是8bit并联可以实现32bit 的内存。(一般内存条都是许多芯片并联而成) 位和字节 内存单元的大小单位有四个: 位:1bit 字节:8bit 字:32bit 半字: 16bit(字的大小定义是十分混乱的 所以应该对照响应的平台而定而Linux + ARM 一个字就是32bit) 内存编址,寻址,内存对齐(十分本质重要) 内存编址是以字节为单位的也就是8bit: 内存对齐 内存对齐访问,在硬件上就是效率高 内存管理之结构体 结构体是如何管理内存的?结构体也就是在内存中开辟除了一块定义的内存; 数据结构:如何组织数据(在内存中如何排布) 最简单的数据结构:数组 (就是一块数据类型相同的数据类型的集合) 数组的优势和缺陷 优势:使用起来简单方便 缺点:1:数组的数据结构都是相同的(类型) 2:数组的大小在定义时就要给出 a[10] 结构体struct struct ages { int ages; char name[20] ; int high; }; 面向过程和面向对象 C语言就是面向过程的,但是用C语言编写的Linux内核是面向对象的,所以可以用C语言实现类,实现面向对象;(结构体内嵌指针来实现面向对象) 结构体内嵌指针实现面向对象 结构体实现面向对象就是实现class 有成员变量。有成员方法 -> 类比 C语言中的变量和函数(函数就是实现方法); struct ages { int a; void (*pFunc)(void); //函数指针也属于变量 }; 内存管理之结构体 结构体是如何管理内存的?结构体也就是在内存中开辟除了一块定义的内存; struct 内存管理之栈:(stack) 栈:是一种数据结构在C语言中用来管理局部变量;栈发明是用来管理内存的; 栈的先进后出 top 指针总是在最后改变的数据的上面,bottom 指针是在栈底不动的 栈的应用举例: 局部变量:局部变量比如 int a; 那么就是在栈中分配一块内存地址与变量a相关联,为是么在不给初始化的时间变量的值是不定的呢? 因为在栈中取出的数据只是拷贝一份传递出去,而它本身是不发生改变的,所以它的值是脏的,也就是所说的栈是脏的; 栈的优点: 栈对内存的管理十分方便,分配和回收内存都不需要程序员去操心; 栈的约束:类似于数组,栈的大小不好规定,且十分怕溢出。C语言中是不对栈进行溢出检查的,所以栈如果溢出就很麻烦。比如不能对局部变量这样定义: int a[100000] ; 递归问题: 如果递归的话一定要注意递归收敛;否则在一次一次的递归中将损耗栈的内存,如果栈溢出则会产生麻烦的后果; 内存管理之堆 堆(heap)是一种内存管理方式. 堆这种管理系统的方式最大的特点是自由:可以随时通过API(malloc,free)这些接口去操作内存。 什么时间适合使用堆内存:需要内存较大的时间 内存泄漏 C/C++中使用malloc 不自己free 就会产生内存泄漏。就相当于程序在运行中,开辟了一段内存以后,没有释放。程序结束以后再次运行,又开辟一段内存,这就是内存泄漏,也就是吃内存。这是非常严重的bug ,我们必须避免; C语言堆内存管理接口: malloc malloc(10 * sizeof(int)); 复杂数据结构: 链表:是最重要的,链表在Linux内核使用中非常的多。驱动、应用编程很多时间都要用到链表、所以链表是必须掌握的。 哈希表:( 不常用,一般不需要自己写,只要能看懂,明白在什么时间需要用,人家为什么用,有没有更好的选择??) 数据结构和算法的关系? 数据和算法是相辅相成的,算法的实现依赖于数据,复杂算法的本质来源与生活中的各种问题,而这种问题依赖于复杂的数据类型去实现 我们所抱有的态度是在碰到的时间去详细研究,在某个领域有需要的去研究 在Linux内核中,比如在字符串驱动设备中,有许多哈希表的应用。我们在此情况下就需要去研究哈希表,因而明白为什么要中哈希表。尽管我们不需要自己去编写一个哈希表。因为大部分这种算法我们都是可以直接用现成的。 —————————————————————————————————位运算—————————— 位运算符 & | ^ 按位异或:异或就是相异或操作 1^1 = 01^0 = 10^1 = 10^0 =0 可以发现某位和1异或就相当于对他进行取反 << >> 寄存器的操作理念就是 //(1) 对某位进行置1置0则对应一下算法: 将一个int型数 a 对它的bit3~bit7改写 (7-3+1) = 5 -> 1 1111 (对应的是5位为1) 然后将得到的这个数进行左移 (1F << 3)| a //将某个数的bit3~bit7置一 ~(1F << 3)& a //将某个数的bit3~bit7置0 // //(2) 将一个int型数 a 对它的bit3~bit8取出 算法如下: (1) 先将所需要的数取出别的清零 (2) 然后将的到的数右移到低位 ((3f<<3) & a) >> 3 // //(3) 将一个int型数 a 对它的bit3~bit8的值加17 算法如下: 1. a.先将要置位的数取出 b.然后将取出的数右移到低位 tmp = (3f<<3) & a; tmp >>= 3; 2. 将取出的数加17 tmp += 17; 3. 将原数对应的要置位的数清零 a &= ~(3f<<3); 4. 将计算好的值写入 a |= tmp << 3; //(4) 怎么构造一个需要的数为对应的1呢 算法如下: ~((~0U)<<(m-n+1))<< n //(5) 指针到底是个什么东西 int a这个是系统在内存中开辟了一块4字节的内存然后将这块内存暂时命名为a 然后你就可以通过a去操作。 int *p这个是定义了一个指针变量p,这个变量p指向一个int 型的变量 指针和普通变量的区别:指针实质就是一个变量可以把它单做一个普通变量来理解。 为什么需要指针 在会汇编中存在间接访问(寄存器间接寻址)基于这样的要求,C也必须有间接访问的功能,Java、C# 这样的高级语言也存在间接访问,只不过被封装起来的。 指针使用三部曲 (1) 定义指针 (2)初始化指针也就是绑定指针 (3)解引用 //(6) 指针带来的符号的一些理解 1.星号 * 有两层含义 (1)*指的是与类型结合的指针的类型 比如int *a 其实说的是定义一个指针a 它指向一个整形变量 (2)*是解引用 *p int *p = &a; \\ int *p; p = &a; 的区别 2.取地址符 & 就是一个符号让编译器取出一个变量的地址 3.左值和右值 左值:其实就是对应变量的空间。(篮子) 右值:才是变量空间里所存放的值,存放的东西(篮子中的菜) a = 4; b = 5; a = b; //其实是把b这个空间中存放的数也就是5丢到a这个空间中,这其中a是任何值没有任何意义。 //(7) 野指针的问题 1.什么是野指针?? 野指针就是指向的位置是不可知的(位置是随机的); 2.指针指向的地址是不可预知的。所以会 产生三个情况。 :指针指向操作系统的内核部分(这部分内存是不允许访问的)这就会造成一个段错误。这是最好的情况,因为操作系统出面拦截了它,程序报错,我们就会去改正错误。Segmentation fault (core dumped) : 指针指向了一快恰好可以访问的地址,就比如在解引用前使用过printf函数(因为printf函数在栈中而且比较大,遵循栈的基本原则就是反复使用而且是脏的。所以会留下一些痕迹,那么指针指向这些地址)指针恰好访问了这段地址,这段地址没有什么含义,所以编译器不会报错,但是,这样隐含了一些问题。 :指针恰好指向了一块正在使用或者已经定义过的变量地址(比如指向了一个 int x)这个X正在使用,然后对它解引用以后就莫名其妙的修改了它的值。那么这样程序是必然会出错的,这样就会造成不和避免的损失。我们必须避免这样的问题。 避免野指针的办法 前辈是非常聪明的。在内存中,NULL是一块地址为0的地址。隶属于操作系统,我们就把这块地址作为一个特殊的地址,在不使用指针时,就将指针指向NULL,这样即使一不小心在解引用的时间访问了这段不可访问的地址,但操作系统会出面将程序拦截停住。 方法: 1.在定义指针的时间,初始化指针将指针指向NULL 2.在使用指针之前判断指针是否指向NULL 3.在解引用之前将指针绑定一个绝对可以绑定需要绑定的地址。 4.在解引用后将指针指向NULL int *p = NULL; int a; p = &a; if (NULL != p) { *a = 4; } p = NULL; //(8) const与指针的联系 1.const int *p -> const 修饰的是*p p是可以变的 2.int const *p -> const 修饰的是*p p是可以变的 3.int * const p -> const 修饰的是p *p是可以变的 4.const int * const p -> const 修饰的是*p p const是可以通过指针被改变的,类比与一种道德约束,const和全局变量一样存放在data区域。gcc对const的检查实在编译阶段不是在运行阶段。 //(9) 深入学习一下数组 数组的几个关键符号(每个符号分别做左值右值) a : a 做左值是表示整个数组的空间。不能做左值(数组的处理要一个一个来) a做右值的时间表示数组a的首元素的首地址 &a :&a 是一个常量(因为在编译时会自动分配一个地址给数组 不能做左值) &a做右值的时间表示整个数组的地址,其实就 a,因为数组的地址就是数组首元素的地址 a[0]:做左值表示a[0]这个空间 做右值表示a[0]这个空间存的数据 &a[0]:做左值a[0] 这个空间 做右值的时间完全等价于a //(10) 指针与数组的天生姻缘 1.以指针格式访问 *(a+3) int *p; p = a; p是一个int * 类型的 a做右值的时间表示数组a的首元素的首地址,所以是和指针相匹配的。 2.以数组下标格式访问 3.从内存角度理解数组 数组的特点:数组的特点就是内存上连续分布,而且只能是一个数据类型。这就表示它天生就是需要指针的,因为只要指向他的某个元素就可以访问它任意的元素。 4.指针和数组类型匹配的问题 指针和数组类型必须匹配 int *p; int a[15] = {0}; p = &a;//这个类型是不匹配的因为&a代表的是整个数组的指针也就是数组指针,它并不是int *型的 p = a; //这个类型就是可以匹配的,因为a代表的是数组首元素的首地址,也就是一个 int *型的。 5.指针是如何参与运算的 指针参与运算的时间,因为指针本身存储的数是地址,所以指针的运算也是地址的运算。 *(p+1) 其实不是加一 而是 p所指向的地址加1*sizeof(指针类型)比如p所指向的地址是0x00100000 那么P+1则指向 0x00100004(int *)0x00100001(chat *) 0x00100008(double *) 6.指针与强制类型转换 printf函数打印的值,其实是右值(因为你需要打印这块空间里存放的值) 数值在内存中是以二进制的序列存放的。 C语言的数据类型的本质就是帮助管理数据,内存只是一个仓库,它不会在意你丢什么样的数据进去 比如 我存放一个int 型的数据 按照float型取出,那么就会出错 指针的数据类型含义? 指针的本质是一个地址,指针代表的是两个变量。一个是指针本身,另一个是指针所指向的。 int *p = &a; 任何指针都是4个字节的,因为存的是地址,int 代表的是它做指向的变量的类型。 指针的所有解析方法都是地址 所以对指针进行强制类型转化就是改变它指向的那个变量的类型。 7.指针与sizeof()运算符 给数组传参的时间需要给数组传递长度 typedef 用户自定义类型 //(11) 指针与函数传参 (1)普通变量传参 函数传参值传递(传值调用) 形参做左值,实参做右值 (2)数组传参 数组传参传的是首元素的首地址,所以在函数内部,传进来的名字就是一个指向数组首元素首地址的指针。 传地址调用 (3)指针作为函数形参 和数组作为函数形参是一模一样的(传参的时间传数组其实就是传指针) (4)结构体作为函数形参 结构体和普通变量一样,但是最好还是传指针,那样效率很高 (5)值传递和地址传递 值传递就是拷贝一份实参的值去送给形参让他参与运算,实参本身没有发生改变 址传递是传递一个地址过去,通过解引用间接改变实参的值 //(12) 输入形参数和输出型参数 (1)函数为什么需要形参和返回值 函数名:函数名其实是一个符号,其实是一个指针常量,我们调用函数其实就是调用这个指针指向的地址,然后去加工数据, 函数体:可以把函数体比作一个机器,输出参数就是原材料,输出参数就是加工以后生成的那个玩意。 (2)函数传参中使用const指针 用法: const int *p ; (3)函数需要多个返回值的时间怎么办? 输入输出型参数,在一般程序中返回值都不是返回计算值的,都是返回一个错误标记,都是依靠指针去返回值 总结: 怎么判断输入输出参数?输入参数一般都是只做读取用的,所以当一个指针变量,用const修饰那么就是输入参数,一个普通变量传递的是一份值,那么肯定是用来做输入的,因为它无法返回给主调函数,它仅仅是一份值的拷贝。 //(13) 复杂表达式和指针的高级应用 指针数组和数组指针 // 指针数组: 实质是一个数组,用来存放指针变量的数组 // 数组指针: 实质上是一个指针,指向一个type类型的数组 // 符号表达式: 搞清楚定义的对象是谁?(1.找核心)(2.找结合)(3.然后继续向外扩张) int *p[5] 1.找核心 p是核心, 2.p左面是[ ] 右边是* [ ]的优先级很高,所以P和[ ]结合,所以P是一个数组 3.从内向外扩展,因为P是一个有5个元素的数组,向外结合,它的类型是指针,所以它是一个数组有5个元素的数组里面存放的是指针,指针指向的是int型数据。 int (*p)[5] 1.找核心 这个里面P是核心 2.找结合,因为()所以是*与P结合,所以P是一个指针 3.从内向外结合,然后和[ ]结合,就是一个指向有5个元素的数组,这个数组中存放的元素是int型的。 // 符号的优先级到底有什么用处? 搞清楚定义的对象是谁? (1.找核心)(2.找结合) (3.从内到外一层层扩展) //函数指针 实质:函数指针还是一个指针 函数指针,数组指针,普通指针本质没有区别 区别在与,它指向什么?它指向一个函数 //函数指针的书写 实质:函数指针还是一个指针 type (*pFunc)(参数列表) pFunc = 函数名; //typedef typedef int (*ptype)(参数列表); ptype p1; p1 = 函数名; //函数指针的实战 typedef int (*ptype)(参数列表); ptype p1; p1 = 函数名; //函数指针的实战(内嵌指针,实现分层结构) 上层:就是一个框架,提供处理问题的方法。 下层:调用这个方法,填充它的参数。 关键在与结构体的定义和填充 //再论typedef ADT:系统自定义 INT CHAR UDT:用户自定义 typedef 类型是一个数据模版:类型是不占内存的,用类型定义的变量才占内存 //typedef与struct 在定义struct 的时间,用typedef 定义一个同名的类型 typedef struct student { char name[20]; int age; }student, *pstudenet; //typedef与const 用typedef构造自定义类型,可增加代码的可移植性。 typedef uinsigened int U_32_INT;定义 U_32_INT a = 32; 申请变量 typedef int * PINT; PINT const p1; const PINT p1; //这两个都是*p可以改变,而p不可以改变 如要真的定义一个*p不可以改变的量泽: typedef int const *p CPINT; //二重指针与指针数组 二重指针:本质和一重指针没有区别,只是,它的指向是一个指针 用法(1):指向一个指针数组 用法(2):给函数传参的时间需要给改变这个指针的指向? //二维数组 二维数组,其实就是一维数组 指针访问:*(*(a+i) + j) //二维数组的运算 利用数组指针指向数组的一行,然后进行操作。 //(14) //堆,栈,结构体, 共用体, 枚举 //内存问题 程序为什么需要内存? 程序需要立足之地 程序是被放在内存中运行的,程序运行需要内存来存储一些临时变量。 内存本身在物理上是硬件,由硬件系统提供。 操作系统提供了许多管理内存的应用程序。我们在需要的时间向它申请即可。 //栈 栈的特点 (1)栈的使用是重复的 (2)栈的使用是脏的 (3)栈的使用是自动的,在释放后,不能再对栈进行操作,那会产生意想不到的后果。 //堆 堆的特点 ( 1 )脏内存 ( 2 )复用性 ( 3 )手动申请和释放 申请内存 int *p = (int *)malloc(1000*sizeof(int)) free(P)后要 把指针指向NULL //内存管理的三大模块 (1)代码段 这段区域,存放的是程序的代码。所以是只读的,是不允许改变的,由此:有些数据是存放在代码段的 1.定义一个字符串指针,指向一个字符串 char *p = "shixin"; 此时,shixin就存放在代码段,是不允许你改变的。 2.const 类型变量,因为const类型变量是不可以更改的,所以它存放在代码段。 由此: 实现const变量的机制有两个 *将const类型变量存放到代码段,就可实现(广泛应用于单片机) *C语言是一种编译器实现,编译器去检查,如果你修改了const类型变量,它就出手,这也就是为什么指针还是可以修改const类型变量的原因。 (2)数据段 本区域是用来存放全局变量,静态局部变量的,但是是存放的是显式初始化不为0的那些变量 (3)bss(zI)段 本区域也就是初始化0段,它本质和数据段没什么区别。但是存放的是初始化为0的段,或者默认初始化的数据,这也就是为什么全局变量不初始化为什么是0的原因。 (4)栈 栈区的特点就不多说了,当我们选择函数内部使用而且占用内存不太大的情况是可以使用栈内存的,它是自动的,不需要我们操心。 (5)堆 堆的应用很广泛,它的使用效果和数据区基本一致,但是定义的变量的生命周期不同,堆的生命周期就是malloc到free中这一段。 //字符串 在C语言中没有原生的字符串,我们定义一个字符指针指向一个字符串的头,通过一个'\0'这个魔数来代表租字符串的结尾。通过指针访问整个字符串。 //字符串和字符数组 字符数组:字符数组本身是带空间的,是可以存放东西的,它本质上是一个数组,也就是一个空间。 字符串:字符串本质上是一个指针,它指向一个字符串,这个字符串存放在代码段,所以这就是为什么直接定义字符串是不可以更改的原因。 //结构体 结构体为什么要对齐访问 对齐访问和内存完全使用是一对冲突,但是,内存的硬件原理就是一个时钟读走4字节的数据,那么平时我们5字节的数据是如何操作的呢? 1、CPU先从取4字节过来 2、CPU再取4字节过来 3、然后把刚取出来的组合起来 这样很麻烦,这也就是为什么结构体为什么对齐访问的原因。最主要的原因还是因为效率,这样CPU就可以一次取好。 (1)结构体对齐访问 结构体对齐访问原则: (1)基本的数据对齐然后向下看 (2)结构体最后的对齐是4字节对齐的(4的整数倍) (1)char 1字节对齐但是后面short以2字节对齐所以 char填充一个字节 (2)数组以10字节填充,但是最后结构体要以4字节对齐,所以是12字节 //内存对齐指令 (1)__arrtibute__((packed)) //以1字节对齐(不对齐) (2)__arrtibute__((aligned(n))) //整个结构体以n字节对齐 //内存对齐指令 (1)offsetof与conatiner_of offsetof本质其实就是构造了一个0地址的type类型的结构体,然后通过->去访问它,那么相对与0地址的偏移量的变量的地址就是该成员相对于整个结构体地址的偏移量。 #define offsetof(TYPE, MEMBER) \ ((int) &((TYPE *)0)-> MEMBER) //以下是错误的办法,因为->的优先级很高,所以0先和MEMBER结合这是错误的 #define offsetof(TYPE, MEMBER) \ ((int) &((TYPE *)0-> MEMBER)) //offsetof宏 和container_of宏 //offsetof 这个宏的功能是的到这个结构体的某个成员相对于这个结构体的偏移量,得知这个以后就可以通过指针访问这个元素。 其实就是虚拟构建了一个0地址的指针,指向这个结构体,然后通过—>访问需要的元素,的到它的地址,而这个地址相对于0就是这个成员相对于整个结构体首地址的偏移量。 //container_of 这个宏的功能是已知这个结构体的一个成员的指针,的到这个结构体的首地址 \ 然后通过offsetof宏来访问所有这个结构体的元素 #define container_of(ptr, type, member) ({ \ const typeof (((type *)0)->member) *__mptr = (ptr);\ (type *)((char *) __mptr - offsetof(type, member)); }) //union 和大小端模式 //union的用法和结构体一样,唯一不同就是,union本质是一个地址存放的变量,以不同的方式解析取出。所以可以利用这个特点去测试大小端模式。 (1)利用union定义一个int 一个char 然后判断 (2)利用指针 定义一个int 型1将它的地址用char解析出来,判断。 //通信协议的大小端模式 //在通信中,要明确规定先发低位还是高位因为在通信中发一个就要收一个否则信息顺序会有问题。 //枚举 //枚举是其实和宏定义差不多 枚举其实也是将数字符号化,编程的时间便于理解。 枚举:枚举中的成员是连续的,如果不初始化就从0开始 1。枚举常量是实体中的一种,但宏不是实体; 2。枚举常量属于常量,但宏不是常量; 3。枚举常量具有类型,但宏没有类型,枚举变量具有与普通变量相同的诸如作用域、值等性质,但宏没有,宏不是语言的一部分,它是一种预处理替换符。枚举类型主要用于限制性输入,例如,某个函数的某参数只接受某种类型中的有限个数值,除此之外的其它数值都不接受,这时候枚举能很好地解决这个问题。能用枚举尽量用枚举,否则在调试的时候你是看不到当时的值的。 4。用宏去定义一个变量如果你定义了一个相同的变量那么要看谁在前面,如果宏在前面变量会产生编译错误,而且这个错误很难查找,如果那个宏隐藏的很深的话。如果你定义的变量在前那么更可怕了,直接没有错误,但是宏定义被自定义的变量悄悄替换了。用枚举定义的话不管你定义的顺序前后关系怎样都会产生重复定义的错误。从上面的举例来看枚举比宏好用的多。宏还有一个特性是没有作用域,这里的作用域是指宏定义以后的代码都可以使用这个宏。宏可以被重复定义这个可能导致宏的值被修改。所以建议不要用宏去定义整形的变量,用枚举或者const。又会有用const还是枚举呢,世界一向如此纠结,枚举只能表示整形,const可以修饰任何类型。整形的情况下如果你要定义几种有关系的数值那么用枚举,否则用const。 //条件编译 //条件编译隶属于C语言的预处理阶段,在C语言的预处理阶段包括: (1):替换宏定义 (2):条件编译 (3):包含头文件(头文件的包含也是预处理,其实就是把头文件中的代码原封不动的复制到你需要使用的代码中) //宏定义 //宏定义是存在副作用的,宏是完全的替换。所以宏的书写一定要加括号。 宏和函数的问题: 宏是完全的替换,是在预处理阶段。不会有调用成本,也不会对宏进行静态的类型检查,而函数是在函数执行过程中跳转过去执行,执行结束以后返回,而且会对函数进行检查; 内联函数inline 内联函数综合了函数的优点(对传参进行静态的类型检查)宏的优点(没有调用成本) //函数库 //函数的库分为两个部分: (1):静态库 静态库就是通过该库的头文件,在预处理编译阶段将这个库加载到相应的代码中使用,如果是有很多代码都要使用同一个函数库,比如printf,那么就会很浪费内存,因为每次使用都要加载一次 (2):动态库 动态库就是在调用库函数时,先不加载该段代码,只在该地方加一个标记,然后在函数执行的时间,再去加载该段库函数,这样下次有同样的调用需要这段库函数时,就继续来这里使用,相比之下就省了内存。 //自己制作静态链接库 /只编译不链接,利用下面的指令指定库的路径 ar -rc libaston.a aston.o gcc test.c -lmylib -L ../mylib/ //自己制作动态链接库 // -fPIC位置无关码 -shared共享库的方式来链接 lddnm要善于利用 gcc aston.c -o aston.o -c -fPIC gcc -o libaston.so aston.o -shared echo $LD_LIBRARY_PATH 查看动态库的环境变量 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH: 增加动态库的环境变量 export LD_LIBRARY_PATH= =空就是删除所有动态库的环境变量 ----------------------------------------------------------------------- //一些概念存储作用域 //存储类 数据段:.data 这段区域存储的是未显示初始化为0的全局变量,以及未显示初始化为0的static局部变量。 其实就是不为0的全局变量和不为零的静态局部变量。 bss段: 这段区域存储的是,显示初始化为0的全局变量,以及显示初始化为0的static局部变量。 这也就是为什么全局变量定义不初始化值为0的原因。 堆: 用户自己操作的空间 栈: 栈其实就是局部变量,还有一个就是函数调用的过程传参。 文件映射区: 文件的操作其实都是在内存上操作的,用户打开一个文件,系统从硬盘中读取这个文件到内存里,然后用户在内存里对文件进行更改,结束以后,又把文件数据存放到硬盘中。 内核映射区: 操作系统有很多的进程,这些进程都以为整个内存就只有内核和它一个,但是其实是1GB的内核映射区,剩下的内存,是采用虚拟地址技术去给一个一个进程的。 OS下加载程序和裸机下加载程序的差异 OS下是不能直接运行程序的因为想要运行是需要一些协助的。这段协助功能的代码就是加载,加载会帮我们完成对全局变量的赋值,清除bss段,这些都是在编译之前完成的。 而在裸机中我们需要自己去重定位,清除bss段。 //一些概念存储作用域 //存储类的相关的关键字1 系统关键字的用法: (1)这个关键字有几种用法 (2)分别列出这几种用法 //auto: (1)aotu关键字只有一种用法:修饰局部变量 (2)auto的用法: auto就是将变量自动分配到栈上既然在栈上也就是说它的值是随机的,拥有一些栈的特点,我就不细说了。 //static: (1)static 关键字的用法有两种: 1.修饰局部变量 2.修饰全局变量 (2)static的用法: 1.在修饰局部变量时,它和普通局部变量在作用域和生命周期是一样的,不同在与存储类上。static修饰的局部变量存储在.data/ .bss段,在未显示初始化为0时就是存储在.data段,在显示初始化为0/未显示初始化(也就是不初始化)时就存储在.bss段 2.在修饰全局变量时, 和普通全局变量的区别: 作用域,连接属性 作用域上:全局变量和静态全局变量都是文件作用域 连接属性:静态全局变量无连接属性,全局变量是外链接属性。 //register: (1)register关键字的用法只有一种: (2)register的作用就是极大的增加所修饰的变量的速度因为寄存器是跟CPU总线相连接的,所以速度最快。但是我们也不能随便定义。该用的时间哟用,大多数时间都永不太到。 //一些概念存储作用域 //存储类的相关的关键字2 //extern: (1)extern关键字的用法只有一种:声明全局变量,但是不能在别的文件中声明的同时初始化。 //volatile: (1)volatile的作用是: 作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值. 简单地说就是防止编译器对代码进行优化.比如如下程序: 1 2 3 4XBYTE[2]=0x55; XBYTE[2]=0x56; XBYTE[2]=0x57; XBYTE[2]=0x58; 对外部硬件而言,上述四条语句分别表示不同的操作,会产生四种不同的动作,但是编译器却会对上述四条语句进行优化,认为只有XBYTE[2]=0x58(即忽略前三条语句,只产生一条机器代码)。如果键入volatile,则编译器会逐一的进行编译并产生相应的机器代码(产生四条代码). 1). 一个参数既可以是const还可以是volatile吗?解释为什么。 2). 一个指针可以是volatile 吗?解释为什么。 3). 下面的函数被用来计算某个整数的平方,它能实现预期设计目标吗?如果不能,试回答存在什么问题: 1 2 3 4 int square(volatile int *ptr) { return ((*ptr) * (*ptr)); } 下面是答案: 1). 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。 2). 是的。尽管这并不很常见。一个例子是当一个中断服务子程序修改一个指向一个buffer的指针时。 3). 这段代码是个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码: 1 2 3 4 5 6 7 int square(volatile int* &ptr)//这里参数应该申明为引用,不然函数体里只会使用副本,外部没法更改 { int a,b; a = *ptr; b = *ptr; return a*b; } //变量的生命周期 //变量的作用域取决于变量的存储类 局部变量的作用域是代码块,也就是{}内部; 全局变量和函数带作用域是文件域,也就是整个.c文件都可以访问但是要加声明; //相同变量名字的掩蔽规则 局部变量相同名字,如果代码域不同。互不影响,一位内在栈上使用后会释放、 交叠作用域的变量,掩蔽规则是(小代码域覆盖大代码域)。 //链接属性 //全局变量的连接属性是外链接的 //局部变量是无连接属性 //加了static的全局变量和函数是内链接的 static: (1):声明局部变量,这里是改变局部变量的存储类 (2):声明全局变量和函数,这里是改变他们的链接属性。 //最后的总结: //局部变量: 存储类是栈,作用域是代码块,声明周期是暂时的,连接属性是无链接 //静态局部变量: 存储类分为.data /.bss (显示初始化为非0的存在.data段)(显示初始化为0/未初始化的存放在.bss段),作用域是代码块,生命周期是永久的,连接属性是无连接属性。 //全局变量 存储类和静态局部变量一样,作用域是整个文件,生命周期是永久的,链接属性是外链接 //静态全局变量 存储类和静态局部变量一样,作用域是整个文件,生命周期是永久的,链接属性是内链接 ---------------------------------------------------------------------------------------------------------------------------------------------- //链表 //数组的两大缺陷 (1):数组内的元素类型必须一致 (2):数组的长度在定义时就必须给出,给出后就不可改动不具备灵活性 链表就是为了解决这种问题的 链表具备两大区域: (1)有效数据区,用来存放有效数据 (2)指针区,指针用来指向下个节点,便于访问 //双链表//内核链表 //双链表 链表的链接要非常注意操作指针的顺序 #include <stdio.h> #include <stdlib.h> /*双链表的模板*/ typedef struct node { int data; struct node *pNext; struct node *pPrev; }node; /*创建一个空的双链表*/ node * creat_node(int data) { node *p = (node *)malloc(sizeof(node)); if (NULL == p) { printf("malloc error .\n"); return NULL; } p->data = data; p->pNext = NULL; p->pPrev = NULL; return p; } /*头部插入一个节点*/ void insert_header(node *pHeader, node *new) { //头插入一个新节点 //第一种情况,头插入的节点是第一个有效节点,就不做下面一步 if ((NULL != pHeader->pNext)) pHeader->pNext->pPrev = new; //将头节点的后一个节点中的前向指针中链接给new new->pNext = pHeader->pNext; //将new与后一个节点相连 (1) pHeader->pNext = new; //注意这里的顺序不能颠倒 (2) new->pPrev = pHeader; pHeader->data ++; return ; } /*尾部插入节点*/ void insert_tail(node *pHeader, node *new) { node *p = pHeader; while (NULL != p->pNext) { p = p->pNext; } //找到了最后一个节点 p->pNext = new; new->pPrev = p; pHeader->data ++; return ; } /*向后遍历节点*/ void back_ward(node *pHeader) { node *p = pHeader; int cnt = 1; while (NULL != p->pNext) { p = p->pNext; printf("node %d :%d.\n", cnt, p->data); cnt++; } return ; } /*向前遍历节点*/ void for_ward(node *pHeader, node *pTail) { node *p = pTail; int cnt = pHeader->data; while (NULL != p->pPrev) { //注意这里的顺序(1)(2) 的顺序 printf("node %d :%d.\n", cnt, p->data); //(1) p = p->pPrev; //(2) cnt--; } return ; } /*删除节点*/ void delete_node(node *pHeader, int data) { node *p = pHeader; while (NULL != p->pNext) { p = p->pNext; if (p->data == data) //找到了这个节点 { //(1)如果他是尾节点 if (NULL == p->pNext) { p->pPrev->pNext = NULL; free(p); return ; } //(1)反之它如果不是尾巴节点 else { p->pNext->pPrev = p->pPrev; p->pPrev->pNext = p->pNext; free(p); return ; } } } printf("没找到这个节点.\n"); } int main(int argc, const char *argv[]) { node *pHeader = creat_node(0); insert_tail(pHeader, creat_node(1)); insert_tail(pHeader, creat_node(2)); insert_tail(pHeader, creat_node(3)); insert_tail(pHeader, creat_node(4)); insert_tail(pHeader, creat_node(5)); back_ward(pHeader); printf("-------------.\n"); node *p = pHeader->pNext->pNext->pNext->pNext->pNext; for_ward(pHeader, p); printf("-------------.\n"); printf("------after delete-------.\n"); delete_node(pHeader, 3); back_ward(pHeader); printf("-------------.\n"); p = pHeader->pNext->pNext->pNext->pNext; for_ward(pHeader, p); return 0; } //状态机 //状态机FSM 状态机就是根据自身状态,决定次态的东西,其实就相当于时序电路,有时钟CLK激励,然后产生状态变化 moore型和mealy型 莫尔型其实就相当于没有时钟信号,他的状态只和自己上个状态有关 米利型其实就相当于要有一个激励,次态的状态和自己本身状态和激励都有关系。
转载请注明原文地址: https://www.6miu.com/read-48399.html

最新回复(0)