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 语言里没有 str 或 string 这种内置字符串类型,字符串都是用 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'
-
比较字符串:
strcmp(str1, str2)返回
0:两个字符串完全相等返回
>0:str1比str2大(按 ASCII 码比较)返回
<0:str1比str2小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、可执行程序、日志文件 |
cat、vim、cp、rm、less |
| d | 目录文件 | Directory | 存放文件 / 子目录的容器,等价 Windows 文件夹 | /home、/etc、当前目录 .、上级目录 .. |
cd、mkdir、rmdir、ls |
| l | 软链接(符号链接) | Symbolic Link | 类似 Windows 快捷方式 ,记录目标文件路径;跨分区、可链接目录,源文件删除则链接失效 | 系统库链接、自定义快捷文件 | ln -s 源文件 链接名、readlink |
| b | 块设备文件 | Block Device | 块设备 ,按块读写,有缓存;主要为硬盘、U 盘、光驱等存储硬件 | /dev/sda(硬盘)、/dev/cdrom |
mount、fdisk、blkid |
| c | 字符设备文件 | Character Device | 字符设备 ,按字符流式读写,无缓存;键盘、鼠标、终端、串口等外设 | /dev/tty(终端)、/dev/zero、/dev/null |
dmesg、stty |
| p | 管道文件 | Pipe / FIFO | 命名管道,用于进程间通信 (IPC),单向数据传输,临时交互 | 匿名管道 ` | 、命名管道 mkfifo` 创建的文件 |
| s | 套接字文件 | Socket | 本地套接字(域套接字),本机进程间双向通信,多用于服务端程序(数据库、服务进程) | /var/run/mysql.sock、/tmp/xxx.sock |
ss、netstat、程序自带交互 |
标准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.权限由内核管控,不是用户想映射啥硬件就映射啥
-
不是所有硬件都适合 mmap
-
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.互斥锁被进程抢的更快咋办?死锁,用信号量去实现
- 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 # 删除可执行文件
简单来说,就是在这里面先把编译的命令敲好,然后我直接执行这个文件,就不用一直重新编译重新敲了;
在文件考试中:疏漏的点有管道操作;
文件的时间戳;只编译修改过的文件;
- 额外加分功能(不止存命令)
-
增量编译:只编译修改过的
.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; -
只读,写直接崩溃
③ 数据段(分两块)
-
已初始化全局数据 (.data)
全局变量、静态变量:
int g=100; static int s=5;程序启动就分配内存、赋初值
-
未初始化全局数据 (.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) | 只读,运行中无法修改,多进程共用 |