嵌入式复习c语言

c语言

printf格式速查

格式符 作用 例子
%d 输出 十进制整数(int) printf("%d", 10); → 10
%ld 输出 长整型(long) 输出更大的整数
%lld 输出 超长整型(long long)
%u 输出 无符号十进制整数
格式符 作用 例子
%c 输出 单个字符 printf("%c", 'a'); → a
%s 输出 字符串 printf("%s", "hello"); → hello
格式符 作用 场景
%p 输出 内存地址(16 进制) 打印指针、数组地址
格式符 作用
%f 输出 float /double 小数
%.2f 保留 2 位小数(最常用)
%e 科学计数法输出
格式符 作用
%x 输出 16 进制小写
%X 输出 16 进制大写
%o 输出 8 进制
格式符 作用
%% 输出一个 百分号 %
复制代码
#include <stdio.h>
int fprintf(FILE *stream, const char *format, ...);
int printf(const char *format, ...);        // stdout = 标准输出
int sprintf(char *buf, const char *fmt,...);
int snprintf(char *buf,size_t n,...);

数组算法逻辑

排序逻辑:

冒泡逻辑:两个两个比较,把更小/更大的往前/往后放;

快排:选一个基准,小的放左边,大的放右边,左右区间递归重复分区

数组插入排:分有序和无序区;那无序放有序区元素;插入到合适位置然后让所有人后移动插入;如果是来一个元素,那就先找到合适的位置然后就在这个位置插入,让所有人往后移动;

选择排:

折半排序:就是二分插入排序;用二分找位置,然后用插入排序插入这个位置,别的往后挪

查找逻辑

直接顺序查找;

二分查找:数组要有序;取到中间值区对比目标,大了去左边找,小了去右边找。然后缩小范围,直接缩小到这个数字的范围内,比如左边的范围内;就找数字;

哈希;

数组操作

数组逆序;从数组两端往中间遍历进行交换,i<j就交换,然后i=j就退出;

数组去重:遍历判断重复元素,删掉重复保留一个

数组求和/平均值:循环累加总和,总和除以元素个数;

最大最小值:直接首元素为最值:然后逐个遍历对比更新;

经典数字题

水仙花思想:拆分十位,百位,千位;其实就是/10取余这种;然后每个个位的数字立方再相加等于原数;

素数:只能被1和自己整除;遍历2~根号之间的判断;

十进制转二进制输出:除2取余;再逆序输出;

斐波那契数列 : 前两项相加等于后一项:1 1 2 3 5 8...

最大公约数、最小公倍数 : 辗转相除法求公约数

累加,累乘,也就是阶乘思想,就是sum=0,然后每次保存上一次的和数 sum =sum +i

变量,计数器思维题

字符串常用:

字符串长度: 遍历到\0结束计数

字符串反转: 头尾字符互换

大小写转换 : ASCII 码加减 32 转换 都直接char去+-就行;

log 的计算方法,log2 的n次方,他就是2的多少次方是n,这种,比如n是6,那这个表达式就是3;

位与进制转换

进制转换:进制只是一种表达形式,本质都是一个数;

2-10-8-16

2,8,16,互转直接8421法则完毕

然后所有进制转10进制 就你乘以2的0123次方这种;你底色进制是谁就底数是谁

10转2进制就是除2取余,转16进制就是除以16取余,转8就是除以8取余; 你转谁除数就是谁

注意:ox33,一个3是一字节。ox16也是一字节一字节,这种,一个是一字节;

正负数,原码,反码,补码;

位与或非-应用,保留和置于0/1,加上移动位置操作,而且记住,他移动位置是需要整体移动的!!,溢出去的会被挤出去;

内存里面都是8位一个字节,然后比如你电脑是32位,也就是存储8个字节,你的指针都是8个字节的指针,每一个字节里面都是8位;这里面你可以用16进制表示也可以用2进制;

大小端

小端是数据的低位存内存低位,高存高,比如4411,存进去是1144,读的话内存里面从低位开始读,那就是1144,就逆序了;如果你数字是112233,那存完就是332211,倒着

大端是数据低位存高位,高位存低位,那4411存进去是4411,读完也是4411,比较符合阅读习惯

指针

理解清楚

int a =1; int *p= &a p是&a ; *p=1;

int **p1 = &p; p1是&p, *p1是p保存的&a * * p1是&a保存的东西是1;

p+1 是4字节一次

p1+1 是4字节一次,但是数组的时候就是数组总元素*4一次

所以:可以看出p 和 *p1是一个东西;所以你free哪个都行;

二级:

结构体指针+1 直接跳过一整个结构体大小

数组指针+1 直接跳过一整个结构体大小

运算符的优先级(重点)

第一梯队(最高). -> 成员访问,这是最顶的双目 p->next、 优先于解引用,这种是结构体变量

第二梯队:所有单目运算符

包含:&``*``!``++``--``~ 正负号(+-) sizeof求字节数

算术双目* / + -

移位<< >>

关系><>=<==!=

位运算 & ^ |

逻辑&& ||

三目?:

赋值=系列

,运算符,是双目优先级最底层 (1+2,3+4) 整个逗号表达式的值 = 最后一个表达式的值 =7

注意:但是赋值和,就不是 int c; c = 1,2,3; 最后c=1;

for(i=0,j=10; i<j; i++,j--); 还可以这么写;这里用在数组的首尾交换很方便,到中间就能停;还有二分查找边界收缩,快排哨兵遍历;

for循环中间条件空着等于一直为真;

下标 []``*

所以:*arr[0]``*(arr[0])``(*arr)[0]

复制代码
.``->``*

p->next、 优先于解引用,这种是结构体变量

数组

arr 👉 首元素地址,类型是 int*

&arr 👉 数组的地址是数组指针存的,人家的访问内存的大小是一个数组的大小哦;默认是首地址;

只是这两个的地址数字是一模一样的;

int arr3; arr + 0 → 指向第0个元素 arr + 1 → 跳到下一个 int(+4字节) &arr + 0 → 指向整个数组 &arr + 1 → 直接跳过整个数组(+12字节)

1.插入逻辑:找位置,判断位置合不合法;然后插入元素,后面的往后移动,长度++

2.删除逻辑:找位置,判断位置合不合法;删除元素;然后后面的往前覆盖;长度--;

3.更改逻辑:找位置,判断位置合不合法;更新元素值;

4.更改逻辑:找位置,判断位置合不合法;更新元素值;

指针数组 是数组

本质是数组 ,里面的每个元素都是指针,数组就是元素类型+长度;

数组的本质,就是把「同类型的元素」在内存中做「连续、线性的内存分配」,并不是什么 "额外的容器"

类型 *数组名个数;

普通数组:char arr5;存的是字符,里面全部都是字符,但是指针数组是一个个地址;

eg char *arr5;

1.因为数组的概念是同一数据类型,你在命名的时候就是把里面的元素等同了这个类型。

2.所以,如果你是指针数组 ,那里面的元素都是指针类型,记住,是指针类型,所以你存储的时候,就是说你存一个指针,必须用二级指针去存。因为他数组里面的每一个元素都是指针;

3.所以,在示例中,arr是首元素地址,你**p指arr的时候也是 p和arr是等价;类型的;然后就是 *p这个东西是首元素的值,当然也就是你存的第一个指针的地址的值;

4.计算大小;示例里面,这个相当于数组里面5个元素,元素是char类型;也就是8位,然后进制只是表现形式,所以你的数组的内存应该是0,8,0,8,0尾巴,反正就是往前不停的+8位则可;

5.p = 指针的地址

*p = 指针存的地址

**p = 最终的数据

6.这里主打就是一步步推导

一些歧义:

void m(int arr[5]),在编译器看来,它等价于 int *arr,但它会默认你要访问 5 个 int 元素

m(&arr1) 这么调用你是从第二个元素开始,但是实则你的数组后面的元素不够五个了;则他不够被访问了;所以内存溢出了;

数组指针 是指针

数组类型的指针;

int (*p)3 =arr

p 指向的是 arr 的第一行(整个数组)

p 的值(地址)等于 arr 首元素的地址

//这里的意思是一个数组类型的指针,然后3个int的大小,一次性访问是访问12个字节,p+1也是12个字节,指向的是arr一整个数组的地址,所以他最好去指向二维数组,这样就能进行行眺,因为二维数组眺一行就是一个访问的大小,如果非得访问一位数组,那你必须是加上&arr这种,因为他要一整个数组的地址,他一次就访问完毕了;也取不到里面的值;

一般指向2维数组最常见,但是1维也ok,但是类型不匹配,

就是为了解决这个函数传参问题,你传了数组的地址过来,然后你需要用东西去接收一下数组的地址;在函数体内部是用数组指针接受更好;一次性就接收完毕了;而且跳跃也是一次眺这么多东西,他访问内存大小是这么大,间隔偏移也是这么多;本身变量是计算机里面的变量;

字符串

1.C 语言里没有 strstring 这种内置字符串类型,字符串都是用 char*char[] 实现的

2.字符串存在栈区上的数组里面,保证数组长度>=字符串长度+1 ,+1是因为有个**/0截止符**;支持strcpy,scanf;要么数组存,要么直接指针存,因为字符串再c里面是常量,是直接拿到的地址;字符数组是栈区,可以修改,但是常量就不行了;

3**.求长度 strlen(str);**

4.复制字符串 strcpy(dest, src) ;

char dest[10];

char src[] = "hello";

strcpy(dest, src); // 把src复制到dest,自动补'\0'

  1. 比较字符串:strcmp(str1, str2)

    返回 0:两个字符串完全相等

    返回 >0str1str2 大(按 ASCII 码比较)

    返回 <0str1str2

    char name[20]; // 输入

    printf("请输入你的名字:");

    scanf("%s", name); // 长度

    printf("名字长度:%d\n", (int)strlen(name)); // 复制

    char copy[20];

    strcpy(copy, name);

    printf("复制后的字符串:%s\n", copy); // 拼接

    char greeting[30] = "你好,";

    strcat(greeting, name);

    printf("%s\n", greeting); // 比较

    ```if (strcmp(name, "admin") == 0) {``

    printf("欢迎管理员!\n");

    }

    return 0;

    比较字符串:strncmp(str1, str2,size)

函数

主要是定参函数和不定参这些,然后常用函数,比如string库里面的memset以及还有stdbli库里面的malloc分配内存,还有字符串里面的各种strcpy函数等等;

递归

递归思想

指针函数

返回值是指针的函数

函数指针

函数类型的指针,一个指针指向一个函数;

typedef R (*TypeName)(参数列表);

gcc概念:

编译运行其实一共四步;

1.预处理 gcc -E

预处理:在这里进行从.c-.i然后宏定义替换,头文件替换

2.编译 gcc -S

编译这里:进行从.i-.s,这里进行语法错误检查,编译成汇编的硬件语言,你可能需要进行

3.汇编 gcc -C

汇编这里:从.s-.o进行到二进制;给机器能看明白;

4.连接:gcc 文件.o

这里是**.o的汇集**,一个.o可能不够,工程项目多个.o然后进行了组合,把二进制编在一起,然后写一个Main,然后进行定义第一条还是第几条的入口。便利执行。这种会生成a.out文件

头文件

1.里面主要是做函数声明,以及自定义变量的定义和声明;

2.然后就可以导入别的文件里面使用,自己的就是"",标准库一般就是<>

3.如果导入两次就会引起变量二次命名的错误;所以;我们需要在头文件里面写一个#if #else去控制,条件编译去控制,这样就能无论你导入多少次,他在预处理只是展开一次罢了;

4.再一个,你里面写的**#ifdefine i** 这个i这种,你要尽量和文件名是一致的,不然的话可能有歧义,就是和别的头文件一块导入的时候,如果你们两个头文件里面的这个宏定义相同,那就问题很大;尽量不要冲突

结构体

struct 结构体名字 {

跟建表一样:值得一提的就是比如字符数组和字符串数组

char name 20 ;

char *name20;

结构体传参 = 传副本

1.这是字符数组和指针数组的区别 ,第一个只能存20个字符,也就差不多一个名字吧,因为汉字可能多占一点;普通结构体变量 访问可以直接**.name** 名字,.叫成员运算符

2.但是指针数组里面存的是指针;每个指针都是一个字符串地址,因为字符串不用取地址,是常量,直接声明+定义就可以了;所以这可以存多个字符串非常方便;

3.本身 char *p 10= {"he","egh","ejhr"}可以存十个字符串;里面每个元素的类型都是char *哦 ,你要存这个字符串数组的地址你需要二级指针去存的;

4.在用指针数组的时候,赋值就不能直接赋值 了,因为他是个数组;你要么循环遍历赋值;要么就是直接字符串拷贝 ,单独赋值用strcpy(stu.name,"hdhh"); 用这种,给字符串数组赋值,更快更好;要么就是直接结构体变量赋值的时候记得多打{},不然他以为一个{}就是一个字符串数组呢;

5.插入 p->name1 = "hhh"; 这种也能插入;这里的意思是,因为元素是char *指针,你直接给他地址才对,所以这个操作就是你吧hhh的地址给他了;所以不用取值,取值你也不知道是啥;这就需要堆指针数组比较了解;

对了,因为字符串是常量,你写的时候他已经自动声明了;

**6.strcpy(stu.name,"hdhh");**你要是在外面直接用这个strcpy这种东西,他会直接报段错误,因为你没给原来的元素初始化,你直接给了这个函数强行给野指针初始化;这种行为不对;要么你就直接改指向赋值;要么就先要空间再用strcpy;因为野指针,他是没办法要空间的

注意:野指针可以赋值,但是没办法解引用以及访问谁,->这种不生效;

7.分配空间的头文件是<stdlib.h> 这个可以用malloc和free

8.访问的时候,你stu.name0或者p->name0这种,然后**%s** ,因为他是一个char *的地址,你要%s,他就是读到\0停止的一个东西,他就是去打印内容的

最重要的一点,人家原来%s 然后你直接"ddd",里面的数据也印证了这个他就是直接把字符串的地址给你,他要的就是地址,因为他是char*

9.%p → 把传进来的值当成地址打印(显示 scanf...)

%s → 把传进来的值当成字符串首地址,去读内容打印

结构体变量。成员 → 访问的是【成员的值】 ;

共用体

union,这个东西的内存计算和结构体不一样;他是共用;直接用最大元素的内存空间存储;谁来了直接覆盖;

结构体是每个元素都+起来,然后做一个字节对齐;

scanf

**scanf里面没有返回值,只有0/1;**他支持一次性输入多个元素

scanf("格式1 格式2 格式3", &变量1, &变量2, &变量3);

字符串数组不用 & ,因为他们本身就是地址,他们是常量;

学生结构体+增删改查

其实就是结构体数组,然后对数组进行增删改查;

分配到堆内存;自己释放,如果有指针,释放完毕要置空,不然就是空悬指针了,也是野指针的一种;

计算长度;

crud;

free;

静态,动态库

写源码,编译,创建库,把自己的一块编译

静态库是编译的时候就加入程序

动态库是运行的时候加入程序,两个都得找;

gcc -c fun1.c -o fun1.o

gcc -c fun2.c -o fun2.o

ar crs libmylib.a(库文件名不能丢掉lib前缀) fun1.o

然后就可以和你的测试函数一块去编译;

动态库

解决方法:

方法1: sudo cp libmyfun.so /lib (不推荐)

方法2: export LD_LIBRARY_PATH=. (临时)

方法3: sudo vi /etc/ld.so.conf.d/my.conf

(推荐)

sudo ldconfig //刷新

gcc -fPIC -c fun1.c -o fun1.o

//-fPIC:生成与位置无关的代码

gcc -fPIC -c fun2.c -o fun2.o

gcc -shared -o libmyfun.so(动态

库文件名) fun1.o fun2.o

malloc

自动申请分配,申请完要释放;

宏定义和typedef

数据结构

研究数据在计算机中的存储组织管理的形式

研究数据和数据的关系;

线性:数据的前驱树和后继树满足1:1的关系;一前对一后

非线性:树:1:n 图 n:m;

逻辑关系:线性。非线性

三种数据结构,表栈树图;

然后就是存储方式,顺序和链式

存储关系:

运算关系:

表:

逻辑关系:线性。

存储关系:顺序存储,链式存储(内存不连续);顺序表(连续空间)和链表

运算关系:crud 排序;

树:

逻辑关系:非线性。

存储关系:顺序存储,链式存储;顺序表(连续空间)和链表

运算关系:crud 排序;

图:

逻辑关系:线性

存储关系:顺序存储,链式存储;顺序表(连续空间)和链表

运算关系:遍历计数

内存概念

多多理解:

结构体只是告诉编译器有这个类型,而不是说是这个变量占了内存;实则是你不分配变量,永远不占内存;

枚举也是一样,你只是在头文件去声明,然后呢直接在别的文件去用

很多表本质上直接用结构体,用的最多,这一板块主要是结构体,指针,typdef,还有逻辑和内存;算大小也要记住了;

顺序表

顺序表 :可以用结构体数组/数组 实现顺序表 ,因为要逻辑和内存连续;因为行这种,最适合做结构体数组;

1.存储密度高2.元素访问方便3.线性表的顺序存储

这个就类似你之前写的结构体数组套结构体

插入删除比较慢,因为他是连续存储,你删了一个就不是连续存储啦!中间眺位置了!

所以链表就是不是连续存储;而设计的;

创建,crud,算长度,判空满,清空(就是你直接把指针置为1),销毁(把空间直接free,头节点也要free),逆序,打印。

链表

单链表,双向链表,循环链表,双向循环链表;

线性表的链式存储 简单来说就是逻辑是前驱后继连续,但是内存存储不是连续的;他就是纯结构体实现,一个数据域一个指针域

1.表就是不连续的顺序表,也就是除了不连续存储其他都一样;他逻辑必须得是线性 的;但是实则存储不线性;实则内存是分布在存储器的各个部位;

2.实则就是指针互指,1->2 2->3 3->4这种;他的数据域和指针域也是来自于指针;

3.每一个指针叫一个节点,头节点,比如我每一个都有指针域;然后最后一个指针域为空;这样就那啥遍历链表的时候就知道什么时候喊停了;头节点只是为了好计算;他的数据域没有意义;也不算长度;只是指针域有效;

4.链表不用判空判满;插入就是改地址;可以排序;

5.有头链表,无头链表;

6.所以他就是结构体实现最好,直接结构体+指针,因为链表不是要存两个东西吗,你直接里面存有效数据;数据域,一个然后存一个指针,这个指针指向下一个节点;

作业:

1.链表插入排序;思路:四个针;

2.合并两个有序链表,然后合并成一个新的顺序链表;归并排序,思路:两个有序链表互相比较,然后插入那个大的前面;

实现:

1.先创建节点;

typedef int data_t;

typedef struct node{

data_t data;//Int的数据

struct node *next;//指针域;用指针存下一个的地址

}node_t

2.创建空链表 也就是分配空间

只有头节点;然后给结构体开空间;返回头节点指针就行;指针最方便的就是在内存上他的访问大小和存储一样,所以他偏移就很好;

返回值是指针的函数,用指针函数;不用参数;malloc这个你需要去转换他的指针类型 ;你传结构体进来分配空间,那就得强转 成结构体类型哦;如果失败,你必须让指针指空,不然他就是野指针;

如果你分配了空间然后free了,指针没置为null;那他就叫空悬指针

设置头节点的数据域-1本身也没啥意义;设置指针域是null;返回头节点首地址;

1.不过为什么你都没定义data 和 next,为什么你head ->data就出来是数据域了?嗷嗷他是给结构体分配了空间所以你创建表就是去访问这个结构体;你的这个指针是给这个结构体分配,也就相当于是指向了这片空间首地址;所以你就直接访问到了这个结构体;

3.计算长度

判断指针合法性;

头节点不算有效节点;要循环遍历,遍历一个节点设一个flag++。最后一个节点的->next是空就停;

4.判空不判满

判空,把头节点传进来;然后直接看-》next是不是空,然后设置返回值;

**访问:**让她指下一个就直接;给一个指针赋值,赋值的东西就是人家的上一个的next地址;比较他指向的是下一个,你找个指针存就行;最后p = p->next等于空的时候;就停了,这个p也分配不了;

5.排序

6.crud;

插入:头插尾插法;头插只用找到头节点;然后尾插必须要遍历链表;其实你只需要告诉我你插入节点的数据域,位置;准备一个新指针(节点)

按位插入删除

用位置去操作;你直接先传位置,然后--就能控制指针偏移;你插入位置是1,吧然后P=p-next,指针一直在往后走每一个新针都是上一个的指针域,然后是1的话他就循环一次就走一次,指0,(从head开始);2次就是直接走两次到1;

实则这种插入也是你插入到这个节点的后面;

作业and练习:

1.然后判断链表是否有环,如何判断?

看谁被2次访问;快慢指针;只要快慢指针相遇就有环了;如果循环结束还没闭合就是没环;

2.判断两条单向链表是否相交;

相交有很多种交法;比如前合。后开,前开后合;

3.实现约瑟夫环;就是一堆人围城一圈;然后比如报数这种;

直接两个针,给一个单环链表,插入操作,然后让一个针指头,一个针指尾巴,两个副本针去动他们,如果不是最后一个,就开始迭代,用总人数进行插入,用报数人数控制循环,比如隔3个就让第一个p副本针动两次,指到第三个,然后让另一个副本针pre带过来等于当前这个,然后保存下一个的地址,pre->next=p->next然后就把pfree掉,然后给p=pre->next就可以从下一个开始继续走路了;

4.插入,归并排序;

也就是合并两个有序链表

全局变量:

主要是用来通信;expert;不要在头文件定义哦

1.只能在栈顶位置插入删除;2.特点:先进后出;3.也是学两种存储:顺序和链式;4.特殊的线性表;

1.栈的顺序存储:逻辑和内存都线性连续;因为插入删除只能在栈顶,所以你要找到栈顶位置;用结构体数组去封装;思路就是:从0开始入栈,然后往后插入嘛就正常你在后面插入,删除你就要去尾巴开始删除,其实完美适用了数组的优点;清除一致,然后销毁传二级指针;

1.进栈push:先约定栈顶位置开始是-1,判满;记住top满了才会溢出一个,没满就是当前最高元素位置;top是从0 开始 ,也就是说当元素有20个的时候,top是19;

2.出栈pop:判空;

3.判空判满,顺序表是要判断的哦

直接空栈,啥都没有;空的话栈顶位置是0号位;栈顶元素会变的你存的多了他就上去了;

4.清空销毁

5.数组你直接memset,是string里面的函数,直接全部清0;

2.链栈

单链表存

其实继续保持栈的特性,只是链存;先进后出嘛;只能栈顶删除,就用单链表就行;他直接一个单链表的结构体,然后一个top指针指也就是把head换一下;叫top了,然后直接在top后面进行头插,12345进来是54321,然后正常删除;在头处删除,直接定义一个针等于top->next;就行了,不停free这个针就行,free了记得保存一下这个针的数据哦;查找也是一个针遍历,清除就是和删除一样。链表只判空

因为栈直接先进后出,就是一个压的过程,也可以双向链表存,或者循环链表存,但是你回头访问干嘛呢,他一次就完了也不浪费空间,他就是头部入和出;你要是双向你还得维护prev指针;循环链表的话直接违背了栈逻辑,你栈底都可以访问栈顶了;直接不知道哪里是哪里

队列

只入队不出,和又入又出,的判空判满,满都是rear+1%size;

队列你进入了又删掉的话,前面的删掉的空间又浪费了,所以我们要循环队列的去存

顺序存: 数组就是去实现循环队列的

思路:

数组,满足队列特点,比如:先进先出,尾进头出,你从0进来是尾巴,然后让尾下标++;头就指顶元素,然后删除的时候直接从头下标开始删除也就是f++,直接让头针往后走;他判空满,无非就是头尾相等就是空 尾巴等于size-1就是满;

但是你删除的时候你必须是头针++,如果你让所有元素往前覆盖的话,那时间复杂度就大了;覆盖无数次;

而且普通数组还会指针溢出;

而且只插入不出队伍的话还好,普通数组存队列和循环队列是一样的,但是你边插入边删除的时候就有问题;你插入3个,满了,删除两个,插入一个,你循环队列ok,普通数组说插入不了了;尾针动不了;越界了,你循环队列的尾巴可以进行等于+1取余size,然后就从0开始;

数组实现循环队列:

但是你普通数组,他就会浪费空间,所以我们直接浪费一个空间 ,不让rear指真尾就行,指空格,这样判空还是相等判满就是;rear+1%size=front ;就是5个元素本身5个格子,我们给6个,让rear每次都指第六个格子也就是下标为5;我删除就头++,然后

链式存 估计有单链队列,双链队列,循环队列(数组),消息队列,双端队列。

思路:

1.单链表存

就直接头针后面进行尾插,一直尾插才能保持顺序12345,尾针一直p=p->next就行;然后只用判空,删除就直接在头那删除

两个针,然后你的结构体要一个空间,你的头节点要搞一个空间

本质上就是队尾插入,队头删除;然后依次实现crud,以及清除,销毁,最后是

记得这个得搞两个结构体,分配两次空间,虽然你也能定义一个链表

作业:

1.栈实现十进制转二进制

2.表达式求值,比如4+2*3-10/5 栈实现;

3.迷宫求解算法;

4.结构队列结合linux的多线程和线程锁,实现线程间的消息队列;

5.QT中,事件循环的机制,队列在事件循环机制中的使用;

6.所有和时间有关的都和队列有关

1.非线性:因为他一个前驱可能有多个后继,完全不逻辑线性也不内存线性

2.节点 度 叶子节点,深度。一个结构体:一个data,两个针,左针和右针;这些东西都是要求的。度是子树个数;

3.二叉树;每个节点都至多有两个子节点,所有叶子节点同一层;

满二叉树,每个节点都有两个子节点,所有叶子节点同一层;

完全二叉树;只有最后两层可以,度小于2;

二叉查找(排序树);bst,左小于根,根小于右;前中后遍历;递归思想。已知两个求另外一个;

4.基于bst的增删改查;思路其实和递归很像,你递归一次变一次头节点;如果只删一个尽量别动头节点;

5.删除有点逻辑要判断三种情况,一个结构体,两个针,然后在创建传的参数要二级指针去存;因为这是首地址;就是整个结构体的地址,然后我需要把这个结构体这片空间的地址保存下来,这样我就可以直接操作结构体里面的函数了;

霍夫曼编码:

意思就是字符里面字母出现的次数;按照编码计算大小;就实现了无损压缩数据;就是把字母的出现次数作为权值,然后我们自己找最小的,然后进行了一个构造哈夫曼然后算出来他的wpl,叶子节点的带权路径这种了然后就是最优二叉树;也就是把哈夫曼树构造出来了;

出现频率越高,编码越短,频率低就编码长;这样里面的数据就实现了无损压缩

哈夫曼树(最优二叉树)带权路径;

构造哈夫曼树;用最小值,进行往上走,最小值做最低叶子节点;然后匹配最小的往上走;

wpl树的带权路径,是只是叶子节点的带权路径之和;

一个树的带权路径总和,就是根到全部;

递归

递归思想:

进去之后直接执行到底;然后为空自动返回;

排序

查找:二分,哈希;

内核链表

不在应用层了,在内核;

主要是独立出来,就是数据结构体里面有链表;数据和链表直接分开;给出来结构体成员的地址,去反着访问结构体的地址;

独立出来是因为,我要换数据

//哈夫曼实现,以及内核链表;

比如普通链表人家里面的数据是Int之类的;你想改成浮点还是什么很麻烦;要么你就改成直接内核链表,这样就可以比较独立;想改哪些就改哪些

unix系统编程

主要是调用函数;

文件操作 文件i/o类型

linux里面7种文件

首字符标识 文件类型 英文名称 核心特征 & 用途 典型示例 常用查看 / 操作命令
- 普通文件 Regular File 最常用,存放文本、程序、日志、二进制数据;分文本文件二进制文件 .txt.sh.jpg、可执行程序、日志文件 catvimcprmless
d 目录文件 Directory 存放文件 / 子目录的容器,等价 Windows 文件夹 /home/etc、当前目录 .、上级目录 .. cdmkdirrmdirls
l 软链接(符号链接) Symbolic Link 类似 Windows 快捷方式 ,记录目标文件路径;跨分区、可链接目录,源文件删除则链接失效 系统库链接、自定义快捷文件 ln -s 源文件 链接名readlink
b 块设备文件 Block Device 块设备 ,按读写,有缓存;主要为硬盘、U 盘、光驱等存储硬件 /dev/sda(硬盘)、/dev/cdrom mountfdiskblkid
c 字符设备文件 Character Device 字符设备 ,按字符流式读写,无缓存;键盘、鼠标、终端、串口等外设 /dev/tty(终端)、/dev/zero/dev/null dmesgstty
p 管道文件 Pipe / FIFO 命名管道,用于进程间通信 (IPC),单向数据传输,临时交互 匿名管道 ` 、命名管道 mkfifo` 创建的文件
s 套接字文件 Socket 本地套接字(域套接字),本机进程间双向通信,多用于服务端程序(数据库、服务进程) /var/run/mysql.sock/tmp/xxx.sock ssnetstat、程序自带交互
标准i/o 带缓存,用户

1.标准c库中定义

errno是错误码,如果打印出来了某个数,去查一下就知道错误原因了;

绝对路径写法;

linux里面看函数具体信息就是man;

输入输出是相对于程序来说的;输入到程序是输入,从程序显示是输出;

fopen(流,模式),成功就是输出流,也就是FILE *fp 这就是一个结构体,指针,FILE是一个结构体,成功返回的是有效指针,失败返回的是NULL;

errno定义错误号;想看的话就用strerror看(errno)

linux里面:所有者,所属组(分组管理),其他用户

ugo rwx 421 a所有

char *p = malloc(100); // 申请 100 字节空间 strcpy(p, "hello"); // 存数据

频繁增删改才用数据结构去存;

流操作

文件操作

1.先打开,然后接受一下指针;2.然后我判断一下打开成功了没有;然后我打印我的错误吗EOF 已经打开的文件就直接读;

3.进行读写等等操作;

注意:linux下一切皆文件;打开终端的时候有输入文件,输出文件,还有错误文件;系统自动打开的文件有自己的结构体指针;就类似文件结构体一样;

4.比如读输入文件叫stdin;里面就是写scanf,这种;显示一般是打印;stdout是标准输出;输入输出都是文件,你就fgets 和fpts就行了;还有stderr也是文件,是标准错误文件;他们的file* 的名字就叫自己;

5.缓存区:读到换行会刷新到硬盘,读到内存满了也会,是在应用层,防止过度操作硬盘,还有就是。这个东西在打开文件就自动创建了;

1.全缓冲:读满了放磁盘

2.行缓冲:一行读到换行符的时候;放磁盘 默认都是行缓存;

3.无缓冲:比如有错,马上报错不能返回到disk;stderr标准错误输出文件,他就没有缓存区;

4.也可以强制执行fflash强制刷新;

5.缓存区还得访问操作系统的接口才能写道磁盘

6.我读和写都可以先从缓存;因为你直接调用系统接口的话我们从用户要切换到内核空间,频繁切换cpu消耗非常大,所以用缓存;printf是写道标准输出stdout文件里面,这个文件默认是行缓存;减少系统调用次数,节省资源提高效率。

文件io 系统

也是一组输入输出函数;他是linux系统提供的;

底层是硬件,然后上层是os去控制(os是内核空间),->应用层程序;(用户空间);分空间是为了做rbac

用户空间;1.app->标准c库都在这(别人写好的依赖);-》接口(系统调用)-》操作系统-》硬件;

2.app->系统调用;不推荐,你用c库有缓存区才去系统调用,不要天天进行系统调用,太爆炸了;用户要切内核,非常麻烦;

系统调用也有直接和间接;主要的接口有:

api:打开,读写;open, close,

1.open:返回文件描述符 ;是一个非负整数;文件描述符从0分配,012已经被占用了,打开的第一个文件是3往后;打开一个终端,shell会帮忙打开* *0 号**:标准输入(stdin)1 号**:标准输出(stdout)2 号**:标准错误(stderr);失败返回-1;

2.参数有,char*pathname,int flags; 这个flags也是个模式就是用什么方式打开;是三个宏定义,只读o_rdonly,只写,读写。但是这里只写不会重新创建;你可以用这个宏;叫o_create;不存在去创建新的;也就是新的参数,第三个参数;

3.如果错了,会返回-1和errno,你可以打印看看;用strerror;也可以用perror()查看,不用传参;这里面可以写一个你自定义的字符串,提示词,这是系统调用的函数;

4.open可以打开文件和目录;

要写硬件驱动,是操作系统对硬件,还有操作系统对软件,叫系统驱动;

开源 RISC-V + 开源工具 + MPW 流片

文件描述符;

read, write
复制代码
ssize_t read(int fd, void *buf, size_t count);
参数说明
int fd:文件描述符(open、socket、pipe 返回的句柄)
void *buf:缓冲区,存放读到的数据
size_t count:期望读取的字节数
返回值
返回类型:ssize_t(有符号整数)
① 返回正数 n > 0
成功读到 n 个字节,n ≤ count。
管道 / 套接字:数据不足时会返回小于 count 的值,不会填满缓冲区。
② 返回 0
读到文件末尾 EOF:
普通文件:文件读完;
管道 / 套接字:对端关闭写端。
③ 返回 -1
读取出错,同时设置 errno:
常见错误:
EINTR:读取中途被信号中断;
EBADF:fd 无效或不可读;
EAGAIN/EWOULDBLOCK:非阻塞 IO,当前无数据可读。

函数stat

stat

  • 参数:文件路径字符串

  • 特点:遇到软链接会穿透,去查链接指向的真实文件属性

  • 返回值:成功返回0,失败-1+errno错误码,剩下的四个返回值都一样;

  • 功能:把文件的属性信息给结构体buf

  • 这里注意一下,就是结构体他就类似一个图纸,然后我给变量的时候才分配内存;

fstat

  • 参数:已经 open 打开后的文件描述符 fd(不用路径)

  • 适用:文件已经打开,手里只有 fd 时查属性

lstat

  • 参数:文件路径

  • 关键:不穿透软链接,直接获取软链接文件自身的属性(区分 stat),要对文件进行打开操作,别的不用,具体man观察;

fstatat

  • 拓展版,支持相对目录、自定义标志(AT_SYMLINK_NOFOLLOW= 不跟软链接,等价 lstat 效果)

结构体存文件所有信息:

文件大小、权限、属主、时间(atime/mtime/ctime)、文件类型(普通 / 目录 / 管道 / 链接)

  • 给路径 + 跟软链接:stat

  • 给fd 文件描述符:fstat

  • 给路径 + 不跟软链接:lstat

无论是标准库还是系统库,实则都是打开-关闭,偏移,读,写,看报错;只是接口不一样;多用多记;这个函数可以获取文件的信息;

文件权限掩码

文件掩码,用umask;就是你直接用你设置的权限去与上umask;取反,有的是0002,这个可以改 0644 &~umask

截至今日作业:哈夫曼树的创建,文件像素打码,就是做到指定位置去打码这种 ,以及英语单词。还有ai课程的学习;还有注意关注whv抽签;

内核创建文件、文件夹时,先拿系统默认最大权限,减去 umask 掩码,得到最终实际权限

吧权限和文件掩码相与,然后直接与宏相比;

老师的文件掩码就是;权限与宏,然后获取到文件类型;如果这个值等于某个宏。就是某个文件类型

文件映射mmap

用Mmap这个东西;可以实现用户层之间操作硬件;他就是将硬件设备映射成文件地址;然后也让用户操作地址,这样就可以不用系统调用什么read write了,你用户可以之间crud文件;返回的就是映射区的首地址;

硬件-内核-用户

mmap函数(参数,)

1.mmap 把内核里的硬件物理地址 → 映射到用户进程虚拟地址空间

2.映射成功后:用户直接指针读写 = 读写硬件寄存器,无 read/write 系统调用

3.不是任意 /dev 设备都能 mmap 硬件

4.权限由内核管控,不是用户想映射啥硬件就映射啥

  1. 不是所有硬件都适合 mmap

  2. mmap 依然依赖系统调用

使用场景

用户操作硬件

大文件读写

进程间共享ipc

匿名内存分配

把cat,ls等等linux命令自己按c编程实现;

进程线程

概念

1.进程,主要是多任务编程;同时执行多个任务;线程就是轻量级的进程;

程序就是可执行的二进制码;静态存在磁盘;进程是程序的一次执行过程;

2.进程的状态,就绪,执行,等待,暂停,死亡/僵尸进程进程退出资源没释放一般父进程收子进程,孤儿进程父进程死了孤儿收,隐匿进程去收孤儿进程,隐匿进程一般不停,因为要一直收;

虚拟内存和物理内存有一点点映射关系,你还是用磁盘的内存给虚拟是扩大的一个过程;

3.单核cpu是没法多任务的;;他就是疯狂给时间片,时间到了你必须让出来,执行下一个;多核才行;

4.保存在磁盘,会分一个内存体;操作系统分配资源是按进程分配的;他是资源分配的最小单位;

函数调用是栈操作,不记录地址你回不来

进程里面的东西:

1.正文段, 代码

**2.数据段,**全局变量,静态变量等等

**3.系统数据段;**以下,进程属性

pcb进程控制块就是有pid等进程的属性,信息等,里面存那个文件描述符;

cpu的寄存器;pc程序计数器(保存当前指令的地址展示现在进程执行到哪里了,比如还有下一条,那就是别的寄存器存储;)cpu模式可能不止一种,你进管理员的时候你就要切换;

堆栈(函数的返回地址,形参这种,局部变量);

创建

fork;就绪->cpu调用->运行->可中断/不可中断/正常运行->停止态,你强制停止但不释放,随时可以继续,需要你发信号->死亡(需要去收拾)->

进程属性速查
字段 释义
yangm 用户名:进程所属系统账号
3534/3541 PID 进程号,系统用来标识这个程序,kill 杀进程就填这个数字
0.1/0.0 %CPU:CPU 占用百分比(很小,几乎不耗算力)
0.2/0.1 %MEM:物理内存占用百分比
13700/14572 VSZ:进程总虚拟内存 (KB)
5152/3580 RSS:实际占用物理内存 (KB)
pts/2 终端:在第 2 号伪终端运行(你当前 ssh / 本地 bash 窗口)
Ss/R+ 进程状态:•S睡眠休眠、s含子进程•R正在 CPU 运行、+前台执行
11:27 进程启动时间(11 点 27 分启动)
0:00 累计 CPU 运行时长,刚开所以用时 0
bash / ps aux 进程命令 第一行:bash 终端 shell;第二行:你刚敲的ps aux查询命令

进程的优先级:他的值越小,优先级越高,优先级可以修改

linux里面

查看进程ps aux:

动态显示进程top

修改进程优先级:nice/renice;

杀死进程 kill 选项 pid 终结进程,但是资源不回收;

kill -l 可以看所有的选项

信号主要是用于内核与进程键盘直接的通信;

进程类型:

1**.交互进程**:

就是用户和进程有交互,比如scanf这种;既可以是前台进程也可以是后台进程;前台进程 霸占终端,不可以敲新命令;

前台进程:霸占终端,不可以敲新命令,ctrl c等是可以进行控制的;可以在终端输入,也可以在终端输出

后台进程: 1.前台进程末尾加 & → 直接后台运行 2.他只能从终端输出,不能从终端输入;

2.批处理进程

与某个终端无关,它是交给一个队列进行执行;普通用户基本上操作不到;

3**.守护进程**:

概念:与终端无关的进程周期性的去执行系统启动时运行,系统关闭时结束; 一般有**周期任务的进程就可以设置为守护进程;也可以等待处理某些发送事情;后台进程;**步骤如下:

1.创建子进程,父进程退出;fork() 目的让子进程变成孤儿进程

2.创建新会话;也就是在子进程新开一个终端;setsid()

会话组:(终端打开自动创建,一个终端一个会话;shell是会话组的组长;这个终端里面的进程都属于这个会话;终端关闭,会话就结束了;所有里面的进程都得结束;都是为了更好的管理进程;)会话组管理进程组,进程组管理进程;组长进程的id就是组id,第一个进程也是会话组的id;

进程组:进程也会先创建一个进程组;剩下的进程都属于这个组;每次新创建的组都会加到这个进程组里面;

3.修改工作目录;就是父子进程都是当前目录,你守护的目录不能被删,最好是/目录,这个不会被删;你守护很多时候都是系统启动在后台跑的一些驱动啥的;chdir()

4.修改文件权限掩码,我们文件权限本身是设置的权限&(权限掩码取反);默认掩码是002;你要去改,你不能和别人一样;最好是用户设置多少就是多少;更改掩码的函数umask(0);这就是让守护进程创建的文件的权限是什么就是什么;

5.关闭文件描述符;就是关闭原来的会话带来的文件的资源;但是你不知道原来的父进程打开了多少个文件,你要知道的话就循环的删掉了;所以要用一个统计函数getdtablesize();这个就能拿到数据,然后你去删就行;就比如打开的原来的会话/终端。的012至少要关掉;

fork 出的父子进程:文件描述符完全共享;独立进程(无亲缘):默认不共享。

6.获取时间,用timelocaltime这种,去获取文件的当前时间再赚localtime

比如每隔一秒写个时间;有点意思是写log的感觉;

复制代码
include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <sys/stat.h>
int main()
{
  // 1.创建子进程
  int p = fork();
  if (p == -1)
  {
    strerror(errno);
    exit(-1); // 非0就是异常退出;
  }
  if (p == 0) // 是子进程返回;
  {
    // 2.子进程要去创建新对话;
    int pid = setsid();
    if (pid == -1)
    {
      strerror(errno);
      exit(-1);
    }
    else
 {
      printf("新会话的id是%d\n", pid);
    }
    // 3.修改工作目录
    if (chdir("/") == -1)
    {
      strerror(errno);
      exit(-1);
    }
    //4.umask
    umask(0);
​
    //5.关闭前会话文件
    int fd = getdtablesize();
    for (int i = 0; i < fd; i++)
    {
​
      close(i);
    }
/ 6.获取当前时间,每隔一秒打印一下时间
    //int s = time(NULL);           // 拿到秒数
    //struct tm *t = localtime(&s); // 转换格式用他的返回值接受
    // 写道我的目录下
    //
   // char *arr[100] = {t->tm_year,t->tm_mon,t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec}
​
    FILE *fd_time = fopen("time.txt","a+");
    while (1)
    {
         time_t s ;
         s = time(NULL);
        struct tm *t = localtime(&s);
​
        char arr[100];
        sprintf(arr,"%d-%d-%d %d:%d:%d \n",
        t->tm_year+1900, t->tm_mon+1, t->tm_mday,
        t->tm_hour+12,t->tm_min,t->tm_sec);
​
​

4.实现多任务。就fork函数,在正在调用的进程里面创建进程,就是子进程

5.fork创建的子进程会把父进程的全部复制过来,只有一点点东西不一样,比如pid;

6.fork创建了进程之后返回就有两个值了,因为有两个进程了

成功是子进程返回0,父进程返回孩子的pid;失败返回-1,和errno

7.子进程拷贝父进程,fork也会拷贝,但是执行是在fork之后执行;不然就开始繁殖了;fork之后,现在是两个进程同时进行

8.根据返回值不同,你可以控制子进程和父进程;getpid是获取自己进程的pid号,,getppid是获取父进程的id

9.所以进程都有父进程,比如交互进程的总父进程是shell;

10。如果你父进程不等子进程你fork先结束了父进程;那子进程就是孤儿进程了;先后不确定,是要看cpu先调度谁;孤儿进程有Init进程回收,他是所有进程的父进程;他的id是1

11.看似有两个返回值,像是一个函数返回了两个东西,实则是两个进程返回的东西,可以当成两个c函数,一个从头的父进程,一个从fork开始的子进程;

12.你循环fork之后你的子进程下次再复制的时候你就需要一块复制,然后子进程也要去fork,最终是2次方的累加和;

进程退出

1.kill;

2.exit这个就是用来结束进程的函数;正常结束就是exit(0),异常结束就是exit(1);他结束之前会清理缓存区;缓存区有东西他会帮你完成

3._exit();这个带下划线的;他是直接结束进程,不清理缓存区;

4.父进程回收子进程资源的时候:有个函数叫wait();运行态-》死亡态;

5.exit函数:功能:退出进程,一个清理一个不清理缓存;参数:一个Int的状态;返回值;他还会返回一个正常,异常返回等的信息,会给父进程

6.wait函数:功能:阻塞父进程一直等子进程结束,子进程不结束就卡死父进程 ;结束了就把一个子进程收走**,他一次只能收走一个;** 参数:pid+一个Int的状态+;返回值成功返回子进程id,失败返回-1errno;

7.waitpid是可以指定等哪个进程结束的;这个可以设置第三个参数,就不用非得阻塞了;

waitpid,1.参数pid:>0等进程id和pid相等的进程结束也就是指定/-1等任意子进程结束 2. 状态信息首地址,他要望着里面存状态;3.options;很多宏;0阻塞等待/wnohang非阻塞等待;

返回值:成功子进程的pid,指定宏是wnohang时,没有子进程返回0,否则返回pid;失败返回-1+errno,非阻塞下,子进程没结束,返回0,子进程结束了返回子进程id

8.vfork,创建子进程,并且让父进程阻塞

9.物理内存如何映射到虚拟内存?

exec函数族:

允许再进程中执行新的程序;新程序会替换掉,简单来说就是**再这个进程里面,换掉正在运行的程序;pid不变;**替换代码段,正文段数据段等等;

主要有六个函数:

1.int exec: 不定参函数里面,一定以null结束;错误才会返回,成功不返回;

调用execl,还有execv这些函数的时候:代码段就会被替换;

2.echo $PATH ;就是一个环境变量;这里面就是系统的环境变量;

system函数:

自动创建子进程进去跑命令,源程序继续运行;

自己的问题:

1.umask,这个为什么一定要进行掩码相与,为守护进程;

线程

概念

1.进程是独立的内存空间,运行要切换,但是线程就是为了提效,他们共用空间就不用换了;很多线程是共享同一片进程的内存空间的;他每一个线程是没有自己占独立的空间的;

2.存储进程的是pc,存储线程信息的是:也是一个结构体,比如tid,优先级。

3.一个进程:正文,数据,系统数据;线程共享的是;基本都共享,打开的文件描述符也是共享的;

4.有个信号处理函数,进程线程都能看到这个函数并且运行;

5.线程私有资源:tid,pc(指令集集合,要知道执行到哪了),cpu就是考这个在读,寄存器,优先级,栈,errno是私有的;

6.线程库:也是标准库,在用户空间:

原来进程是主线程,没有子线程就是单任务,多个子线程就是多任务;线程的运行地点,是在回调函数的指令里面;

7.主线程是main函数执行,子线程是回调函数;

8.线程也是同时执行;

9.回调函数是特定事件发生才回调用,你写的时候也是,他的函数体应该是发生条件;函数名就是函数首地址;回调函数里面写业务代码;

10.理论上来说你光创建你cpu不知道,你子线程和进程是随机参与cpu调度的;你只是设置延时函数也不太好,所以你就用那个等待回收函数也就是阻塞函数;就类似进程的wait

11.你要连接线程库 -lpthread;线程是库函数调用不是系统调用

复制代码
gcc test.c -o test -pthread
    int pthread_create(
    pthread_t *thread,        // 输出参数:存放新线程ID
    const pthread_attr_t *attr,  // 线程属性,NULL用默认属性
    void *(*start_routine)(void *), // 线程入口函数
    void *arg                 // 传给线程函数的参数
);

意思是头文件是声明函数在哪,我真要运行是要去连接真正的库的

12.pthread_detach非阻塞 回收,pthread_join是阻塞回收,一旦调用直接把线程给系统回收掉;

他的结束有三种:

1.他结束调用退出,然后给进程里面的join函数回收,给他返回值;

2.回调函数结束;

3.调用取消线程的函数;

步骤:1.创建线程,这个函数有四个参数,创建成功是返回0,失败返回errno然后让线程id号未定义;2.创建回调函数3.退出,

主线程回收;join是回收,你的退出的返回值,是要指定退出某个线程;这个函数也有返回值,他会返回错误类型;参数是id和退出码;//如果有推出信息,在退出函数里面写;这个join函数是阻塞的,他会阻塞父进程一直等子线程返回东西给他才结束;

复制代码
//创建线程;
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
​
void *hui(void *arg){
printf("这里是子线程\n");
//执行从这开始
};
int main(){
  pthread_t tid;//创建好的线程id的首地址会在这;
  void *arg;
  //创建线程
   pthread_create(&tid,NULL,hui,NULL);
   //sleep(1);
  // printf("这里是父进程继续\n");
   pthread_join(tid,NULL);//最后一个参数是要回收状态码;
   printf("这里是父进程继续\n");
​
   //  pthread_t t, void **res
  //printf("%d",tid);//print pthread tid
​
}
​
互斥操作

同时 ,读写因为速度不一致导致资源被破坏 ,我们要读到完整的,数据完整性。 所以读写一块也有问题;都写 也是一样的;所以你在写buf的时候直接保护起来互斥锁去解决;自己抢一把锁;

1.没人叫就一直抢,这就是锁的弊端,因为没人唤醒他;有几个线程就得上几把锁;不然有人再抢锁之外就会出现互斥操作;主要是用于循环抢;

2.而同步是可以唤醒操作的;

步骤:

1.初始化锁;2.上锁。3.解锁;

条件变量+互斥锁

专门解决「线程需要等待某个条件满足才继续干活」的场景。 线程是同一时间只允许一个线程访问,然后不会让线程主动等待;让线程主动阻塞,等待某个条件为真才唤起;

eg:先上锁,然后条件,条件为真,写唤起读,然后把条件标志flag置为0,就是给个结构体里面有个flag,写读操作都重新来一次,然后再去判断flag去执行写和读;本身写读线程要抢,然后我条件变量,0时写,读挂起,写完把flag变成1,然后读条件成立,读开始写,然后写完把flag变成0,唤起写;读是挂起的;记得条件变量也要初始化,要销毁,也是资源;这个条件变量和信号量很像 类似是互斥锁+2个条件变量实现同步+互斥;那边是同步两个信号量实现同步+互斥

锁 + 条件变量 :面向 「临界区 + 状态等待」 ,聚焦「共享资源保护 + 资源状态变化」,是面向线程状态 的设计。纯同进程多线程开发(Linux 线程编程) 优先:互斥锁 + 条件变量 代码范式标准、团队易维护,是 pthread 体系标准方案。他常用于线程

信号量 :面向 「计数器 + 流量控制」 ,用数字来管控准入数量、执行顺序,是面向计数 的设计。**需要跨进程通信 / 同步 优先:**信号量** 锁 + 条件变量一般不跨进程,System V 信号量、POSIX 信号量都支持跨进程。

操作系统题、同步经典题:常考信号量

Linux 应用多线程开发:主流考互斥锁 + 条件变量

作业:三个线程任务abc,a打印a,以abc的顺序'

类似实现,两个线程

复制代码
#include <stdio.h>
#include <errno.h>
#include <string.h> //print的库
#include <fcntl.h>  // open() 函数 + 所有 flags 宏
#include <unistd.h> // close() 函数
#include <stdlib.h>
#include <sys/stat.h> //这是stat的库
#include <time.h>
#include <sys/types.h>
#include <pthread.h>
#include <semaphore.h>
int buff[1000];
sem_t * sem;//定义信号量
int num = 30000000;
char *buf[1000];//意思是这里面每一个都是字符串
pthread_t tid;//定义这个容易变成野指针
//定义双信号量,首地址
sem_t rs;//记住直接定义实体
sem_t ws;
​
pthread_mutex_t mutex;//锁的结构体变量
pthread_cond_t wv;//条件变量的结构体变量
pthread_cond_t rv;//条件变量的结构体变量
struct data{
  int flag;//条件
  char buf[64];//临界资源
 };
  struct data *p;
​
// 互斥锁+条件变量  实现同步
void *fun2(void *arg)
{
  while (1)
  {
    // 给我们读上锁
    pthread_mutex_lock(&mutex);         // 这是阻塞的等锁;
    while(p->flag==0) //为0挂起,为1开始读
      pthread_cond_wait(&rv, &mutex);//阻塞等待,为0才读
      //fputs(p->buf, stdout); // 每次读一下他写了啥,读buf个 直接显示
            // 去除 fgets 带回的换行符
        size_t len = strlen(p->buf);
        if (len > 0 && p->buf[len - 1] == '\n')
            p->buf[len - 1] = '\0';
​
      if(strcmp(p->buf,"quit") == 0)
              exit(0);
      //把flag置为1
fputs(p->buf, stdout); // 每次读一下他写了啥,读buf个 直接显示
      p->flag=0;
      // 读完之后解锁
      pthread_mutex_unlock(&mutex);
      pthread_cond_signal(&wv); // 唤醒写线程
  }
  return NULL;
}
int main()
{
          p = malloc(sizeof(struct data));//让指针等于现在的结构体首地址
          p->flag=0;//初始化条件
  // 定义条件变量和锁
  // 初始化锁
  pthread_mutex_init(&mutex, NULL);
  // 初始化条件变量
  //p->flag=0;//初始化条件
​
  pthread_cond_init(&wv, NULL);
  pthread_cond_init(&rv, NULL);
  // 先创建线程;
  pthread_create(&tid, NULL, fun2, NULL);
​
  // 主线程写,子线程读
  while (1)
  {
   // 给我们写上锁
    pthread_mutex_lock(&mutex);
    // 只上锁,全都是要抢的;所以我们用条件变量;
    while(p->flag!=0) //如果不等于0,条件为真,就一直执行就是1的时候就挂起,0的时候写
      pthread_cond_wait(&wv, &mutex);// 阻塞等待条件变量为1的时候写;
​
​
      fgets(p->buf, sizeof(p->buf), stdin); // 从终端输入到buf里面一次一块十个字节
      // 写完之后解锁
      p->flag=1;//所以一进来就写,写完弄成1
      pthread_mutex_unlock(&mutex); // 解锁之后唤醒等待的读的线程
      pthread_cond_signal(&rv);     // 唤醒读线程
​
  }
  free(p);//手动分配手动结束;
  pthread_mutex_destroy(&mutex);//销毁锁
  pthread_mutex_destroy(&wv);//销毁wv
  pthread_mutex_destroy(&rv);//销毁rv条件变量
  return 0;
​
}
​
同步操作

读写要有先后顺序 问题,先写在读,你不写读了也有问题;用信号量实现

如果10个资源就满了,新来的就等;等空出来;信号量代表的是可用资源的个数;申请到一个资源的时候就减少,第11个就没有了,就等着;然后>0就有资源;

1.初始化资源。 sem_init();创建无名信号量

参数:第一个参数是:指向你的信号量首地址 ,要操作哪个信号量。第二个是给进程共享还是线程共享 是谁同步要定义先后顺序就给谁0是线程 非0是进程,第三个是个数;你初始化了才算是一个有效的信号量;

返回值:成功0,错误-1 errno

2.申请资源p操作--:sem_wait()阻塞,简单来说就是量>0就申请到做减法,如果没申请到就阻塞等着,等>0的时候;参数就是一个信号量地址;有资源才返回。用的多些;

sem_trywait()非阻塞;有没有资源都返回;

3.释放资源v操作++ :有人等就直接给等的人不会加的,等也要排队,没人等就++;等是阻塞的等,你不能自己先结束了;不能等不到先死了;sem_post();参数信号量首地址;成功0,失败-1和errno;

4.他也是资源你需要销毁sem_destroy() ;这些操作都要头文件semaphore.h

eg:读线程,写线程;比如写一个你从标准输入里面写进buf,然后读buf长度;两个线程完成;

这两个是就是解决竞态问题

1.进程线程他是在内存新开空间吗?不是,是同一片内存;

2.互斥锁被进程抢的更快咋办?死锁,用信号量去实现

  1. STM32 部署 TinyML 轻量化 AI,薪资直接上浮 30% , 车载自动驾驶、国产芯片(RISC-V)、工业机器人 ,

`作业:1.实现:计算3000 0000~3000 1000数之间的质数(生产者/消费者模型);写一个读一个;就是循环写;

以下是非循环写:一次性写完一次性读完;

复制代码
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <time.h>
#include <sys/types.h>
#include <pthread.h>
#include <semaphore.h>
​
int buff[1000];
sem_t sem;
pthread_t tid;
int num = 30000000;
​
// 判断质数的辅助函数
int is_prime(int n) {
    if (n <= 1) return 0;
    if (n == 2) return 1;
    if (n % 2 == 0) return 0;
    for (int j = 3; j * j <= n; j += 2) {
        if (n % j == 0)
            return 0;
    }
    return 1;
}
​
void *fun(void *arg){
    // 等待主线程写完数据
    sem_wait(&sem);
    printf("这里是子线程,开始判断质数\n");
​
    for (int i = 0; i < 1000; i++) {
        printf("当前数是: %d\n", buff[i]);
        if (is_prime(buff[i])) {
            printf("当前数是质数: %d\n", buff[i]);
        }
    }
​
    return NULL; // 规范返回值
}
​
int main(){
    // 初始化信号量:线程间共享,初始值0(子线程必须等主线程post才能执行)
    sem_init(&sem, 0, 0);
​
    // 主线程写数据
    printf("主线程开始写数据\n");
    for (int i = 0; i < 1000; i++) {
        buff[i] = num++;
    }
    printf("主线程写完数据,通知子线程\n");
    sem_post(&sem); // 通知子线程:数据写完了
​
    // 创建子线程
    pthread_create(&tid, NULL, fun, NULL);
​
    // 等待子线程执行完毕
    pthread_join(tid, NULL);
​
    // 销毁信号量
    sem_destroy(&sem);
    printf("程序执行完毕\n");
    return 0;
}

`2.这是生产消费者模型;就是循环往复,写完就读,读完还能写,不存在时间片片完你还没读完然后直接写的问题;所以要双信号量;

分析思考步骤:1.创建双线程;2.主循环写,子循环读;3.主写前先申请--,写完唤起读信号量++,读前先判断读信号量然后做--,读完唤起写信号量去++;4.主线程阻塞等待子线程结束/非阻塞等待子线程结束;5.销毁信号量资源;6.buff全局定义;我for循环,写一个就给一个。7.质数判断逻辑抽成函数;8.理解线程间通信,循环唤起互相工作模式;公私有的东西;

复制代码
int is_prime(int n) {
  if (n <= 1) return 0;
  if (n == 2) return 1;
  if (n % 2 == 0) return 0;
  for (int j = 3; j * j <= n; j += 2) {
      if (n % j == 0)
          return 0;
  }
  return 1;
}
​
void *func(void *arg)
{
  // 去掉外层while(1),只执行一轮
  for (int i = 0; i <1000; i++)
  {
    sem_wait(&rs);
    if (is_prime(buff[i]))
      printf("%d这个数是质数\n",buff[i]);
    else
      printf("%d这个数不是质数\n",buff[i]);
    sem_post(&ws);
  }
  return NULL;
}
​
void zhi_shu(){
  sem_init(&rs,0,0);
  sem_init(&ws,0,1);
​
  int p = pthread_create(&tid,NULL,func,NULL);
  if (p == -1)
  {
    perror("create thread err");
    return;
  }
​
  // 去掉外层while(1),只写一轮1000个
  for (int i = 0; i <1000; i++)
  {
    sem_wait(&ws);
    buff[i]=num++;
    sem_post(&rs);
  }
​
  // 等待子线程执行完毕再收尾
  pthread_join(tid, NULL);
  sem_destroy(&ws);
  sem_destroy(&rs);
  return;
}

通信

线程通信:**

通过回调函数传参就是通信;就比如互相私有的栈互传,但是直接操作不合适,这种操作只能是用来传文件描述符;线程也有线程处理函数;

通信方式:

**1.全局变量/静态变量 通信;**同一个进程里面的线程共享进程地址空间;就开始同步互斥机制;

2.堆内存传递;就是直接创建进程的时候进行传参;就是函数传参,比如单传就传地址, 多个值传就直接传结构体首地址;

3.结构体报告数据传参,多个值放进去,然后传结构体首地址;

4.线程特有:条件变量+互斥锁

进程通信:

进程之间是互不干涉的;他不可以通过全局变量通信, 会被拷贝;

目的:数据传输,通知,共享;

1.管道:本地通信

无名管道 :pipe 特殊的文件(创建在内存) ,一旦创建就要两个文件描述符 ;读端和写端,写端写,读端读 ;父子关系都能看见;他是一个半双工模式 ;单向去,单向回;能读写,不能同时;0读1写 环形队列的思路

可以父子之间通信,非父子不行;

1.创建管道int pipe(int pipefd2),然后创建子进程;

2.然后你分配读写任务,都往管道里面写;然后文件描述符是共享的;可以做偏移;写的时候不保证原子性,可以被打断;

3.记得关闭你不需要的东西;一直写的话就写满了,读不出去,他就容易满;他pipe里面写了读掉自己会删这种;

4.如果管道破裂了,也就是满了;进程会立马结束;管道大小是默认容量 64KB ,他的这个64b是算是内核的管道缓冲区;读就是刷新到用户内存缓冲区了;

**5.写端为空,读端要阻塞;写端关闭读端从0开始;**读关了,写存在,就暴了;

有名管道:fifo

**可以任意进程间通信;在文件系统可见;**是确实创建了一共文件

1.创建有名管道:系统调用函数;mkfifo函数;参数是一样,队列思路;

2.然后和用无名一样用;

只开读端、不开写端 → 读阻塞

只开写端、不开读端 → 写阻塞

作业:进程管道通信

问题:为什么文件权限要umask取反再与;还有创建守护进程的时候就让她自己创建的完全ok;这种;

复制代码
    // 去除 fgets 带回的换行符
        size_t len = strlen(p->buf);
        if (len > 0 && p->buf[len - 1] == '\n')
            p->buf[len - 1] = '\0';//就是直接赋值;用数组下标赋值
            
            
信号:内核和用户

信号用于内核进程和用户进程的通信 。**这两层本身没区别,只是为了分权限,本质都在内存;**1.软件层次模拟中断。2.kill -l

3.常用信号:sigkill sigstop sigchild 子进程发生改变

sigpipe 管道破裂信号 sigalrm闹钟 sigcont进程继续 sigio异步通知

4.对信号的响应方式

1.忽略:在函数指针里面给函数传参数;

2.默认操作:也是传参。经常有杀死和停止信号

3.捕捉并处理:自己定义函数,给这个函数指针,你去实现处理

理解:子进程发信号,父进程捕捉+处理;

typedef void (*sighandler_t)(int); 给void (*)(int)起别名叫sighandler_t

sighandler_t signal(int signum, sighandler_t handler);

信号处理函数,这个针指向一个返回值是void,然后参数是int的函数;用的时候传函数地址; sighandler_t 这个是这个函数指针的名字;

sighandler_t signal(要处理的信号,4.响应方式是宏值);

你自己的回调函数传参一个int,传信号就行;

这个函数返回的就是回调函数首地址,就处理的首地址;错误返回errno

alarm(2)设置闹钟 2秒时间到了就结束了;**进程会死;**你可以配合睡眠,做一个每秒干嘛这种;这就是时钟信号;

你处理的回调函数:可以根据传参不同进行不同信号的处理;

作业:父进程接受到子进程的结束信号,父进程再去回收;

复制代码
pid_t pid;
int ret;
void sig_handler(int sig)
{
  if (sig == SIGCHLD)//你不用发SIGCHLD,这个系统会自动发;你接收到处理一下就好了
  {
    // 如果是闹钟信号的话,你就直接打断回收,然后你在父进程里面调用函数就行;
    // 就是如果是闹钟,你就直接回收,别的另说;
    waitpid(ret, NULL, 0);
    printf("父进程回收完成,即将退出\n");
    exit(0);
  }
}
​
void signal_pro()
{
​
  // 现在是父进程
  // 创建一个子进程
  signal(SIGCHLD, sig_handler);//先制定规则
  ret = fork();
  if (ret == 0)
  {
    alarm(2);
    // 子进程返回0,子进程闹钟吧
    while (1)
    {
      printf("hah\n"); // 打印两个哈哈结束
      fflush(stdout);
      sleep(1);
    }
    exit(0);
​
  }
//alarm(n) 写在哪个进程里,倒计时结束后,SIGALRM 就发给哪个进程,别的进程收不到。
//alarm 本身不会杀进程,只是发信号;进程会不会结束,看这个信号的处理方式。
  else if (ret > 0)
  {
    // fu进程发信号
    // signal(SIGALRM, sig_handler);
    while (1)
    {
      sleep(1);
    }  
    exit(0);
    
  }
  else
    exit(0);
}

1.疑问,这个signal是谁在执行,如何实现的进程间通信?

**2.SystemV通信方式:共享内存

共享内存: 内核准备的用于进程间通信的内存;内核映射到用户空间的mmap区域;类似于这个;直接操作用户空间就能操作内核;就是跨权限

1**.创建共享空间**,然后让内核去映射。shmget()/shmat()/shmdt()/shmctrl()

2.用户直接操作内核,操作完要删除空间;read 和 write是在做拷贝;但是共享内存就可以直接读写;

3.shmget() 这个就是创建共享内存他的id要相同;也就是key值。同一个key才能通信

ftok获取ftok()函数的参数是 1.文件/目录路径首地址,2.整型,只有低8位有效 是0-255的整数;返回值是key,如果你不用ftok,用那个宏,你就只能亲缘进程通信;

参数 1:key 2:size共享内存大小 ,页的倍数 3:shmflg 权限 IPC_CREAT 不存在则创建 IPC_EXCL:和 IPC_CREAT 搭配,存在则报错 (防止重复创建)权限位:类似文件权限,如 0666(所有进程可读可写)用或就行;

返回值:成功返回共享内存id,也就是shmid是int。 失败返回-1;

4.shmat() 挂载 ,也就是映射 把共享内存挂载 到进程地址空间(拿到可用指针) 参数:1. shmid ,2. shmaddr 映射方式,自己映射更好NULL,3. shmflg 挂载标志 0默认,可读可写, SHM_RDONLY 只读挂载。

返回值:类型void *(通用指针)成功:返回共享内存映射到当前进程的起始虚拟地址 。失败:返回 (void *)-1,并设置 errno

5.shmdt()卸载: 使用完毕卸载共享内存

6.shmctl()删除: 最后删除共享内存(或查看属性)

7.逻辑就是开一片空间,让不同的进程映射同一片空间;

8.你写进去的会一直保存,读完不会读出去;

作业:循环写和读共享内存;

消息队列:

概念:存消息的链表,写进程吧东西写进队列,读进程把队列里面的东西读出来;特点:可以无限制大小,因为是列表;可以进程通信;称之为MQ;异步通信的中间件;也是生产者消费者模型;他也是由一个消息队列的id标识;id 相同就是同一个消息队列;

函数:

1.msgget()创建消息队列,

参数,

1.key值,可以用ftok,获取 这个是文件转换成key值,也可以用宏值(就亲缘);.就是当前目录

2.用宏值;就是亲缘通信;不用宏值就0

int msgid = msgget(key,ipc)接收一下返回值;

返回值:

1.成功:int消息队列id;

2.失败-1, errno;

2.msgsnd()发送消息,

参数

1.消息队列标识符,也就是get的返回值;

2.消息缓冲区结构体 存放消息的地方,这个结构体里面的参数是:

固定是long类型,后面你自己拼;

3.消息正文的字节数正文大小是一整个结构体大小-类型大小,因为这边约定只有消息正文的参数,不包含第一个类型大小;

4.位标志,阻塞 发完再返回 0 /非阻塞没发完也可以返回;

返回值 成功0,失败-1;

注意:一次只能发一个消息,读也是只能读一个;

3.msgrcv()读消息队列

基本一样;要两个进程哦;

参数

1:消息队列id

2.结构体指针

3.正文大小

4.消息类型 0是第一条,

5.阻塞读0,等发了再读。得非阻塞

返回值 成功0,失败-1;

注意:一次只能读一个消息;

注:奥对你想看到读的阻塞效果,你就得去不存在创建,然后不要抱错就行;

4.msgctl()删除消息队列

参数,1.消息队列id, 2.属性:可以写,可以更新,可以删除,用宏 IPC_RMID ;3.buf消息队列结构体指针:删除的话给null就行

返回值

注意:broker

ipcs -q,查看消息队列

ipcs -aux,查看共享内存

ipcrm -q 你的id ; 这个命令,队列,共享内存他都可以删除

本质上都是管道思想;

示例读写代码:

复制代码
​
//读进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
​
// 标准消息结构:首成员必须是 long
struct msg {
    long type;       // 消息类型,固定 long
    char s[32];   // 消息正文
​
​
};
​
key_t key1;
​
// 读进程
int main()
{
  // 先拿key;
  key1 = ftok(".", 10);
if (key1 == -1)
  {
        perror("ftok");
        return 1;
  }
​
​
  // 接收消息队列的id
  int msgid = msgget(key1,IPC_CREAT |  0666);
​
  if (msgid == -1)
  {
        perror("msgget");
        return 1;
  }
​
  printf("%d\n", msgid);
​
  // 这里定义结构体,读出来的数据放这里面
  struct msg msg;
  if (msgrcv(msgid, &msg, sizeof(msg) - sizeof(long), 0, 0) == -1)
    perror("msgrcv");
// 读一次打印一次
  printf("%ld %s\n", msg.type, msg.s);
  msgrcv(msgid, &msg, sizeof(msg) - sizeof(long), 0, 0);
  printf("%ld %s\n", msg.type, msg.s);
  msgrcv(msgid, &msg, sizeof(msg) - sizeof(long), 0, 0);
  printf("%ld %s\n", msg.type, msg.s);
​
​
  //读完之后做一下删除
  msgctl(msgid, IPC_RMID, NULL);
  return 0;
}
​
​
//写进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
// 标准消息结构:首成员必须是 long
struct msg {
    long type;       // 消息类型,固定 long
    char s[32];   // 消息正文
};
​
key_t key1;  // 全局 key
​
// 写进程
int main()
{
  // 先拿key;
  key1 = ftok(".", 10);
  if (key1 == -1)
  {
    printf(strerror(errno));
  }
​
​
  // 接收消息队列的id
  int msgid = msgget(key1, IPC_CREAT | 0666);
  if (msgid == -1)
  {
    printf(strerror(errno));
  }
  printf("%d\n", msgid);
​
  // 去发送消息;也就是写进程;
  // 先写消息
  struct msg msg = {1, "100"};
  struct msg msg1 = {2, "200"};
  struct msg msg2 = {3, "300"};
​
  if (msgsnd(msgid, &msg, sizeof(msg) - sizeof(long), 0) == -1)
    printf(strerror(errno));
  msgsnd(msgid, &msg1, sizeof(msg1) - sizeof(long), 0);
  msgsnd(msgid, &msg2, sizeof(msg2) - sizeof(long), 0);
 return 0;
}
    
​
信号灯:

概念:主要是实现进程间的同步和互斥; 是信号量的集合,他给每个信号量有个编号;一次性创建多个信号量 ;他即可以同步互斥也可以通信

应用场景:直接创建多个信号量,以后用到同步互斥就可以直接调你写好的.c文件;

//把学到的知识汇总一遍;然后统一整理发送;做成动画去处理;

常用于共享内存,一旦共享,有多个进程要操作文件/空间的时候都要做这个操作;信号量再信号灯里面可能是结构体数组的方式;

函数:

1.创建信号灯int semget();

参数:1.key,要么是宏ipc_private亲子/ftok任意;2.集合当中信号量的总个数;3.权限,方式;

返回值:

成功返回信号灯的id,失败返回-1;

2.semop();操作:比如p,v操作;

参数:1.信号灯id,2.结构体数组地址,3.信号量的总个数,你要让这里面有几个信号量

结构体成员:都是shot类型:{

1.信号量编号。0是一个

2.根据这个值不同决定是p还是v操作;<0是P操作,>0是v操作 ,等于 0:函数直接返回,继续往下执行 -1和+1是pv操作;

3.操作标志位:两个宏;SEM_UNDO,这个是进程结束的时自己把信号量回收;就是我不手动操作,人系统跟着操作;默认用这个就行

}

返回值:成0,失败-1;

3**.semctl()信号灯控制**

1.信号灯编号

2.信号量编号

3.宏值,进行对信号量的操作,获取,设置,删除IPC_RMID;全是宏

4.一个共用体,给信号设置一些东西的,你要定义共用体的变量, 你要去设置一下你的共用体此刻要用谁,然后直接把共用体变量传过去就行;

返回值 :成功0/返回对应数值,失败-1。

根据semctl的宏值不同设置不同的函数

makefile的使用

多文件

目标: 依赖文件 编译命令 # 注意:前面是【Tab键】,不能用空格

main: main.c gcc main.c -o main

清理编译产物 clean: rm -f main

make

编译生成可执行文件 main make clean # 删除可执行文件

简单来说,就是在这里面先把编译的命令敲好,然后我直接执行这个文件,就不用一直重新编译重新敲了;

在文件考试中:疏漏的点有管道操作;

文件的时间戳;只编译修改过的文件;

  1. 额外加分功能(不止存命令)
  • 增量编译:只编译修改过的 .cpp 文件,不用全量重编,速度更快;

  • 自带清理:写 make clean 一键删除可执行文件、中间 .o 垃圾文件;

  • 多目标管理:同时管理多个程序、动态库、静态库编译;

  • 批量操作:一键编译、一键运行、一键打包。

格式

1.先创建文件,必须叫makefile;然后把你的.c放进来;

target : dependency_files

<tab> command

复制代码
CC = gcc  这个编译器给他命个名字,在下面的gcc就用$(CC)就是gcc,因为不同平台编译器名字不一样;
CFLAGS = -c -wall -o
​
示例:  test.out : fun1.0 fun2.o main.o //目标加依赖
​
 按回车再按tab;gcc fun1.o fun2.o main.o -o test(指定输出)//gcc去编译;
fun1.o :fun1.c 
​
•       gcc -c fun1.c -o fun1.o//这里是把.c到.o,所以是编译,上面是链接,所以这里是-c
​
fun2.o :fun2.c 
​
•       gcc -c fun2.c -o fun2.o
​
main.o :main.c 
​
•       gcc -c main.c -o main.o
​
clean:
​
•       rm *.o 这个伪目标就能直接给我删除;

调用makefile的时候就直接make就行,你的makefile建立在哪个文件夹下就是哪个;他也只更新最新的;make clean就直接把.o删除了;makefile默认读到第一个目标;所以你的总目标一定要放在首位;想单独执行某个,就单独去执行;

示例:hello.o : hello.c hello.h

让.c生成.o;

用户区五大段**

① 代码段 (.text 正文段)

  • 存放:编译后的机器指令(main、自定义函数二进制代码)

  • 属性:只读、共享,多个进程跑同一个程序共用同一份代码

  • 不能改:运行中修改指令会触发段错误 SIGSEGV

② 只读常量段 (.rodata)

  • 字符串常量"abc"const int a=10;

  • 只读,写直接崩溃

③ 数据段(分两块)

  1. 已初始化全局数据 (.data)

    全局变量、静态变量:

    复制代码
    int g=100; static int s=5;

    程序启动就分配内存、赋初值

  2. 未初始化全局数据 (.bss)

    复制代码
    int g; static int s;

    只记录大小,磁盘不占空间,进程启动 OS 清零

.data+.bss = 你说的进程全局数据区

④ 堆 (heap) 向上增长(地址变大)

  • malloc/free、new/delete 手动申请释放

linux32位下

内存段名称 标准地址区间 归属范围 核心存放内容 关键特点 生长方向
内核空间 0xC0000000 ~ 0xFFFFFFFF(最高 1GB) 所有进程共享 操作系统内核代码、页表、硬件驱动、进程 PCB、文件描述符、中断、系统调用相关数据 用户态进程无权直接访问,所有进程的内核空间内容完全一致,程序仅在系统调用时陷入该空间
栈(Stack) 内核空间下方,高地址区域 每个线程独立拥有 函数局部变量、函数形参、函数返回地址、函数调用栈帧、临时寄存器上下文 自动管理(函数结束自动销毁)、空间极小(默认几 MB)、分配速度极快,栈溢出会直接导致程序崩溃 向下生长(地址从高到低)
动态链接库 / 共享库(.so) 栈下方、堆上方的中间区域 多个进程共享 系统标准库(如 libc.so)、pthread 线程库、第三方动态链接库、嵌入式外设库 同一份库文件内存仅加载一次,可被多个进程共用,大幅节省内存,程序运行时动态加载链接
堆(Heap) 动态库下方,低地址侧区域 进程内所有线程共享 C 语言malloc/calloc/realloc、C++new手动申请的内存块 手动管理(申请后必须手动free/delete释放,否则内存泄漏)、空间极大(受系统物理内存限制)、分配速度慢于栈 向上生长(地址从低到高)
BSS 段(未初始化全局数据段) 堆下方,数据段相邻区域 进程独有 未初始化的全局变量、未初始化的静态局部变量 程序加载时仅分配内存空间,不存储实际数据,默认全部赋值为 0,不占用磁盘可执行文件的体积
数据段(Data 段 / 初始化数据段) BSS 段下方 进程独有 已初始化的全局变量、已初始化的静态局部变量、字符串常量(如"hello" 程序加载时直接从磁盘可执行文件中读取初始值写入内存,全程占用磁盘和内存空间
代码段(Text 段 / 正文段) 用户空间最低地址区域 可被多个同程序进程共享 程序编译后的二进制机器指令、所有函数的执行代码、只读常量 只读属性,运行中无法修改(修改会触发段错误),多个运行同一程序的进程可共用同一份代码段,节省内存

c语言下

变量 / 数据类型 对应内存段 补充说明
函数内定义的局部变量、函数形参 栈(Stack) 函数执行结束自动释放,无法在函数外访问
malloc/calloc/realloc/new 申请的内存块 堆(Heap) 内存块本身在堆,指向该内存的指针变量本身在栈
未初始化的全局变量、未初始化的static静态变量 BSS 段 程序启动时自动清零,默认值为 0
已初始化的全局变量、已初始化的static静态变量 数据段(Data) 初始值在程序编译时就已确定,加载时直接写入内存
字符串常量(如char *str = "abc";中的"abc" 数据段(Data) 只读属性,无法通过指针修改字符串内容
所有函数的执行代码、编译后的机器指令 代码段(Text) 只读,运行中无法修改,多进程共用