目录
1、线程和进程的概念,区别、以及什么时候用线程什么时候用进程
1.1 概念
1.2 区别
1.3 选择
2、TCP/IP分几层,每层的核心任务是什么
1.tcp/ip模型
tcp:
udp:
tcp、udp的区别
tcp/udp的连接过程:
3、http是干什么的,中文名是什么,主要用在什么地方? http的端口号是什么,端口号是干什么用的。
1、概念
请求格式的结构:
2、特点
4、线程什么时候互斥,什么时候同步
5、同步有哪些方法?
6、进程间为什么要通信?有几种方式?
7、static关键字
什么时候用:
8、extern关键字
9、排序
1. 插入排序
2.选择排序
3、冒泡排序
4、快速排序
5、 二分查找
10、linux当中的栈默认有多大
11、列表和数组的区别?各自的优缺点
列表和数组的优缺点
12、查进程用什么命令?
13、应用层的协议是什么?
14、查看网络状态用什么命令?
15、对目录的操作流程是什么
16、线程的作用
线程的函数:
17、const关键字
补充:const *p的用法
18、数据结构的框架
栈:
队列
树
二叉树
19、链表的作用
链表的优缺点
20、ifndef
21、包头文件“”和<>有啥区别?
静态库和动态库
22、C语言编译四个阶段
23、define
带参宏和带参函数的区别
24、设计一个点对点通信的软件,选择TCP还是UDP,为什么?
25、链表头插和尾插哪种效率高
头插法:
尾插法:
链表查重的结束条件
26、如何调试段错误(GDB)
GDB常用命令
27、内存分布
28、signal的作用
29、在C中,extern的用法
30、volatile·关键字
31、线程间加锁
什么是死锁
自旋锁和互斥锁的区别
32、strcpy和strncpy的区别
strcpy和memcpy的区别
33、声明和定义的区别
34、进程
35、内存碎片、泄漏、溢出有什么区别
36、linux怎么获得内存大小
37、men1和men2的区别
38、linux常用的命令功能
39、熟悉那些进程命令?网络命令?查看网卡用什么命令?
40、单链表和双链表的区别
41、怎么删除/查看第n个节点
42、阻塞
43、数据结构、表、栈、队列之间的区别?
44、如何定义一个指向5个整型元素的数组指针?
45、描述http的交互过程
46、栈和队列的相同点和不同点
47、父子进程运行时的关系
48、数据库基本用法
数据库的基本指令
49、指针数组和数组指针,指针函数和函数指针
50、内存大小端
测试大小端
51、select、poll、epoll
52、IO模型
53、原子操作
54、wireshark
55、线程池
56、中断和信号的区别
1、线程和进程的概念,区别、以及什么时候用线程什么时候用进程
1.1 概念
进程,就是正在执行的程序。是操作系统执行和资源分配的基本单位;3G
线程是CPU调度的基本单位,是进程中的一个执行流;8M
1.2 区别
1.线程是进程的一部分,同一进程可以包含一个或多个线程。
2.进程拥有独立的资源(地址空间、文件描述符和打开的文件等);而线程共享相同的资源
3.进程在操作系统中被视为一个独立的实体,需要操作系统进行调度和切换;而线程是调度的基本单位,线程的切换相对于进程来说更加高效。
4.进程之间的数据通信需要采用特定的机制;而线程通过共享进程的地址空间来实现数据共享,线程之间的通信更加方便。
5.进程是独立运行的,一个进程的崩溃不会对其他进程产生影响;而线程是共享进程资源的,一个线程的崩溃可能导致整个进程的崩溃。
1.3 选择
对资源的管理和保护要求高,不限制开销和效率时,使用多进程。
要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程
2、TCP/IP分几层,每层的核心任务是什么
1.tcp/ip模型
网际互联模型,分为4层
应用层,传输层,网络层,接口层(链路层和物理层)
1. 应用层:直接与应用程序交互,常见的应用层协议有HTTP(网页浏览)、FTP(文件传输协议)、SMTP(电子邮件传输)、DNS(域名解析)等
2. 运输层:提供了节点间的数据传送,主要功能是确保数据包能够正确、可靠地到达目的地,这一层最重要的两个协议是TCP(传输控制协议)和UDP(用户数据报协议)。TCP提供面向连接的、可靠的服务,通过序列号、确认机制和重传机制保证数据完整性;而UDP则提供无连接的、不可靠但速度快的服务。
3. 网络层:主要负责主机到主机之间的通信,IP(互联网协议)是这一层的核心协议;
4. 网络接口层(包括数据链路层和物理层)::接收IP数据报并进行传输,从网络上接收物理帧,抽取IP数据报转交给下一层,对实际的网络媒体的管理
总而言之:每层都为上一层提供了特定的服务,并使用下一层提供的服务,共同构成了一个可靠且有序的通信系统。这样设计的好处在于,各层相对独立,上层的变化不会影响到下层的工作,同时也可以根据需要灵活地替换或添加新的协议。
tcp:
TCP协议全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。
tcp三次握手
1. TCP连接过程
如下图所示,可以看到建立一个TCP连接的过程为(三次握手的过程):
第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN。此时客户端处于 SYN_SENT 状态。
(首部的同步位SYN=1,初始序号seq=client_isn,SYN=1的报文段不能携带数据,但要消耗掉一个序号。)
第二次握手:(ACK+SYN)服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。
(在确认报文段中SYN=1,ACK=1,确认号ack=client_isn+1,初始序号seq=server_isn。)
第三次握手:客户端收到 SYN + ACK 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED(已建立) 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。
(确认报文段SYN=0,ACK=1,确认号ack=server_isn+1,序号seq=client_isn+1(初始为seq=client_isn,第一个报文段消耗了一个序号,所以第二个报文段要+1),此时ACK报文段可以携带数据,不携带数据则不消耗序号。)
补充:
为什么 TCP 建立连接需要三次握手,而不是两次?
答:这是因为这是为了防止出现失效的连接请求报文段被服务端接收的情况,从而产生错误。
四次握手也不行,因为多余的握手会导致过多的网络开销和资源消耗。三次握手就已经能够确认双方的发送和接收能力是正常的。同时四次握手与两次握手(偶数次握手)一样,会很容易受到SYN洪水攻击。
tcp四次挥手
客户端或服务端均可主动发起挥手动作。
第一次挥手:(FIN)客户端向服务器发送一个FIN(结束)请求,表示客户端不再发送数据。此时客户端处于FIN_WAIT_1状态。
第二次挥手:(ACK)服务器收到请求后,回复客户端一个ACK响应确认,但这个响应可能还携带有未传输完的数据。此时服务器处于CLOSE_WAIT状态。注意,在第三次挥手之前,数据还是可以从服务器传送到客户端的。
第三次挥手:(FIN+ACK)服务器完成数据传输后,向客户端发送一个FIN请求,表示服务器也没有数据要发送了。此时服务器状态变为LAST_ACK状态。
第四次挥手:(ACK)客户端收到服务器的请求后,回复服务器一个ACK响应确认。此时客户端处于TIME_WAIT状态,需要经过一段时间确保服务器收到自己的应答报文后,才会进入CLOSED状态。
补充
为什么“握手”是三次,“挥手”却要四次?
TCP建立连接时之所以只需要"三次握手",是因为在第二次"握手"过程中,服务器端发送给客户端的TCP报文是以SYN与ACK作为标志位的。SYN是请求连接标志,表示服务器端同意建立连接;ACK是确认报文,表示告诉客户端,服务器端收到了它的请求报文。
即SYN建立连接报文与ACK确认接收报文是在同一次"握手"当中传输的,所以"三次握手"不多也不少,正好让双方明确彼此信息互通。
TCP释放连接时之所以需要“四次挥手”,是因为FIN释放连接报文与ACK确认接收报文是分别由第二次和第三次"握手"传输的。
为何建立连接时一起传输,释放连接时却要分开传输?
建立连接时,被动方服务器端结束CLOSED阶段进入“握手”阶段并不需要任何准备,可以直接返回SYN和ACK报文,开始建立连接。
释放连接时,被动方服务器,突然收到主动方客户端释放连接的请求时并不能立即释放连接,因为还有必要的数据需要处理,所以服务器先返回ACK确认收到报文,经过CLOSE-WAIT阶段准备好释放连接之后,才能返回FIN释放连接报文。所以是“三次握手”,“四次挥手”。
udp:
无连接,不可靠传输,面向数据报,全双工.
tcp、udp的区别
1、TCP面向连接;UDP是无连接的,这种方式为UDP带来了高效的传输效率,但也导致无法确保数据的发送成功。
2、TCP是可靠的通信方式。通过TCP连接传送的数据,TCP通过超时重传、 数据校验等方式来保证可靠传输;而UDP不保证可靠交付
3、TCP面向字节流;UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低。
4、每一条TCP连接只能是点到点的;而UDP不建立连接,所以可以支持一对一,一对多,多对一和多对多的交互通信,也就是可以同时接受多个人的包。
5、TCP需要建立连接,首部开销20字节相比8个字节的UDP显得比较大。
6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道。
tcp/udp的连接过程:
tcp报文
TCP报文由如下部分组成:
1. 端口号
16位源端口:发送方主机的应用程序的端口号
16位目的端口:目的主机的应用程序的端口号
2. 序列号
32位TCP序列号:表示本报文段所发送数据的第一个字节的编号
3. 确认号
32位TCP确认序号:接收方期望收到发送方下一个报文段的第一个字节数据的编号
4. 首部长度
4位TCP首部长度:数据偏移是指数据段中的“数据”部分起始处距离TCP报文段起始处的字节偏移量。确定TCP报文的报头部分长度,告诉接收端应用程序,数据(有效载荷)从何处开始
5. 保留字段
6位保留字段:为TCP将来的发展预留空间,目前必须全部为0
6. 标志位字段
6位标志位:共有6个标志位,每个标志位占1个bit
7. 窗口大小
16位窗口大小:表示发送该TCP报文的接受窗口还可以接受多少字节的数据量。该字段用于TCP的流量控制
8. 校验和
16位校验和字段:用于确认传输的数据有无损坏 。发送端基于数据内容校验生成一个数值,接收端根据接受的数据校验生成一个值。两个值相同代表数据有效,反之无效,丢弃该数据包。校验和根据 伪报头 + TCP头 + TCP数据 三部分进行计算
9.紧急指针
16位紧急指针字段: 仅当标志位字段的URG标志位为1时才有意义。指出有效载荷中为紧急数据的字节数。当所有紧急数据处理完后,TCP就会告诉应用程序恢复到正常操作。即使接收方窗口大小为0,也可以发送紧急数据,因为紧急数据无须缓存
10. 选项字段
选项字段:长度不定,但长度必须是32bits的整数倍。内容可变,因此必须使用首部长度来区分选项的具体长度
IP头结构的定义
- 固定头:20 个字节
- 变长头:20 + 额外最多40字节
/IP头定义,共20个字节/
3、http是干什么的,中文名是什么,主要用在什么地方? http的端口号是什么,端口号是干什么用的。
1、概念
中文名:超文本传输协议、HTTP是基于B/S架构进行通信的
交互流程
一:客户端和服务器端建立连接
二:客户端发送请求数据到服务器端(HTTP协议)
三:服务器端接受到请求后,进行处理,然后将处理结果响应客户端(HTTP协议)
四:关闭客户端和服务器端的链接(HTTP1.1后不会立即关闭)
请求格式的结构:
请求头:请求方式、请求的地址和HTTP协议版本
请求行:消息报头,一般用来说明客户端要使用的一些附加信息。
空行:位于请求行和请求数据之间,空行是必须的。
请求数据:非必须。
是互联网上应用最为广泛的一种网络协议。HTTP是万维网(WWW)的数据通信的基础,用于从WWW服务器传输超文本到本地浏览器的传输协议
HTTP协议是一个应用层协议,是请求/响应模型的,客户端发起请求,服务器回送响应。服务器无法主动将消息推送给客户端。
作用:
1、数据传输
- HTTP 负责在客户端(如浏览器)和服务器之间传输超文本,包括网页、图像、视频、音频等各种类型的文件。它使得用户能够通过浏览器访问互联网上的各种资源。
- 2、资源定位
- 通过统一资源定位符(URL)来确定要访问的资源位置。URL 中包含了服务器的地址、资源的路径等信息,HTTP 协议根据这些信息找到目标资源并进行传输。
运用场景:
1、网页浏览
浏览器与服务器之间的通信就是通过 HTTP 协议进行的。
2、移动应用开发
- 许多移动应用也会使用 HTTP 协议与服务器进行通信。
3、API 调用
- 开发人员可以使用 HTTP 协议来调用各种 API(应用程序接口
4、物联网(IoT)
- 在物联网领域,设备之间的通信也可以使用 HTTP 协议。
http请求方式:GET:请求指定的资源,并返回消息主体。
HTTP默认端口号为80,用于浏览器与服务器之间的通信;
端口号就是为了区分同一计算机上的不同进程
HTTPS的端口号为443。
2、特点
1. HTTP是无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。
2. HTTP是媒体独立的:只要客户端和服务器知道如何处理的数据内容,任何类型的数据都可以通过HTTP发送。客户端以及服务器指定使用适合的MIME-type内容类型。
3. HTTP是无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。同一个客户端的这次请求和上次请求是没有对应关系。
4、线程什么时候互斥,什么时候同步
互斥:对临界资源的排他性访问;
同步:有一定先后顺序的对临界资源的排他性访问;
当多个线程同时访问一个共享资源,并且对该资源的访问会导致不一致的结果或数据损坏时,就需要使用互斥机制来保证同一时间只有一个线程能够访问该资源。
线程同步主要是为了协调多个线程的执行顺序,确保它们按照特定的顺序执行,或者在某个特定的条件满足时才执行特定的操作。(需要一定的执行顺序的时候)
5、同步有哪些方法?
互斥是一种同步机制,常见的互斥方式有以下几种:
1、互斥锁
互斥锁就是控制对共享资源的使用。互斥锁只有两种状态:加锁、解锁。
2、条件变量
条件变量是用来等待而不是用来上锁的。条件变量用来锁定一个线程,直到某个特殊的条件发生为止。通常条件变量和互斥锁同时发生。
3、信号量
信号量是一种特殊的变量,用来控制对临界资源的使用,在多个进程或线程都要访问临界资源的时候,就需要控制多个进行或线程对临界资源的使用。信号量机制通过p、v操作实现。p操作:原子减1,申请资源,当信号量为0时,p操作阻塞;v操作:原子加1,释放资源。(无名信号量:线程;有名信号量:进程间通信)
4、读写锁
读写锁和互斥锁类似,不过读写锁允许更改的并行性。互斥锁要么是加锁状态,要么是不加锁状态。而读写锁可以有三种状态:
读模式下的加锁、写模式下的加锁、不加锁状态
一次只有一个线程可以占有写模式下的读写锁,但是可以有多个线程占有读模式下的读写锁。
读写锁特点:
a、如果有线程读数据,则允许其他线程读数据,但不允许写
b、如果有线程写数据,则不允许其他线程进行读和写
6、进程间为什么要通信?有几种方式?
进程的目的是为了让程序能够并发执行
进程间通信的目的一般是为了共享数据、数据传输、消息通知、进程控制等。每个进程的用户地址空间是独立的,不能互相访问。
每一个进程都拥有自己的独立的进程虚拟地址空间,造成了进程独立性,不能互相访问。但内核空间是每个进程共享的,因此进程间要通信必须通过内核。
常用的通信方式:管道,消息队列,共享内存,信号量,信号,socket套接字
1、管道:包括无名管道和有名管道,无名管道半双工,只能用于具有亲缘关系的进程直接的通信(父子进程或者兄弟进程),可以看作一种特殊的文件;有名管道可以允许无亲缘关系进程间的通信。
2、消息队列:消息的链接表,放在内核中。消息队列独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除;消息队列可以实现消息的随机查询,可以按照消息的类型读取。
3、信号量semaphore:是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步。
4、信号:用于通知接收进程某个事件的发生。
5、内存共享:使多个进程访问同一块内存空间。
6、套接字socket:用于不同主机直接的通信。
7、static关键字
用来控制变量的存储方式和作用范围。
1、static修饰局部变量时,会改变局部变量的存储位置,,只被初始化一次,从而使得局部变量的生命周期变长。
其本质是:
普通的局部变量创建后是放在栈区中,这种局部变量进入作用域时创建,出了作用域就销毁;
但static修饰后的局部变量则放在静态区中,它改变了局部变量的存储位置,从而使得变量的生命周期延长,延长至程序结束才销毁。
2、static修饰全局变量和函数时,会改变全局变量的链接属性,从而使得全局变量的作用域变小。
只可以被文件内所用函数访问,避免命名冲突。将模块的全局变量限制在模块内部(仅供.c使用),不能跨 文件共享
其本质是:
全局变量本身是具有外部链接属性的,在A文件中定义的全局变量,在B文件中可以通过【链接】来使用;
但如果全局变量被static修饰,那这个外部链接属性就会被修改成内部链接属性,此时这个全局变量就只能在自己的源文件中使用;
什么时候用:
修饰局部变量、修饰全局变量、修饰函数
8、extern关键字
extern
关键字是C语言中的一个修饰符,用于声明全局变量和函数的引用。它用于告诉编译器,某个全局变量或函数的定义位于其他文件中。
-
全局变量声明:在文件中使用
extern
关键字声明一个全局变量,即使该变量在其他文件中定义。 -
函数声明:在文件中使用
extern
关键字声明一个函数,即使该函数在其他文件中定义。
9、排序
1. 插入排序
思想:首先把第一个位置上的数假设为最小数,用a[1]位置上的数与他比较,如果a[1]上的数比它小,则将a[0]的值赋给a[1]将a[1]的值放在a[0]的位置,再用a[2]与a[1]比较,如果·a[2]比它小则交换两个数,再把它与a[0]上的数比较,如此循环,直到最后两个数比较完。时间复杂度:最坏情况下为O(N*N),此时待排序列为逆序,或者说接近逆序最好情况下为O(N),此时待排序列为升序,或者说接近升序。
代码:
void InsertSort(int* a, int len)
{
int i,j;
for(i = 1;i < len;++i)
{
int t = a[i]; // 拿数,记录此时 i 位置上的值
j = i; // 准备要放的位置
while(j>0 && t<a[j-1]) // 将 i 位置上的值依次与前面的数进行比较,直到有一个数比t
小或者到了a[0]的位置
{
a[j] = a[j-1]; // 挪位置
--j;
}
a[j] = t;
}
return 0;
}
}
2.选择排序
思想:给合适的位置选择合适的数(用后面的数依次跟指定位置上的数比较,如果后面的数比指定位置的数小,就交换两个数,依次重复,一直到下标的倒数第二个)
时间复杂度:最坏情况:O(N^2)
最好情况:O(N^2)
代码实现思路:
(1)有len个数,len个数需要交换len-1轮,且数组下标从0开始,那么最外层循环是 i 的
取值是 [0,len-1),
(2)定义 j 变量,表示当前最小数的下标,每轮循环时,默认假设初始值是最小值,即初始赋值 j = i+1
(3)遍历除当前最小数以外剩余的数,如果发现有比当前最小数更小的数,则记录更小的数的下标,然后两数交换
void SelectSort(int* arr, int n)
{
int i,j;
for(i = 0;i < len-1;++i) // 控制位置,i表示下标,给i号位置找数,当找到了前面len-1
个数,最后一个数就确定了
{
for(j = i+1;j<len;++j) // 表示给i号位置找合适的数,要和后面所有的数都比较
{
if(a[j]<a[i]) // a[i]表示要找合适数的那个位置的原先的值, 依次用j号位置
的值去和i号位置的值进行比较
{
int temp = a[i]; // 引入一个中间值
a[i] = a[j]; // 交换a[i]和a[j]
a[j] = temp;
}
}
}
}
3、冒泡排序
思路:一次冒一个数,相邻两个元素两两比较,小的放前,大的放后。(0和1上的位置的值比较,1和2位置上的值比较,依次比较,每次找出来的是最大数--- 升序)
时间复杂度:最坏情况:O(N^2)
最好情况:O(N)
void BubbleSort(int* arr, int n)
{
int i,j;
for (i = 1;i<len;++i) // 控制趟数
{
for(j = 0;j<len-i;++j) //一趟的比较过程
{
if(a[j]>a[j+1])
{
int temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
}
}
4、快速排序
1、选出一个key,一般是最左边或是最右边的。
2、定义一个begin和一个end,begin从左向右走,end从右向左走。(需要注意的是:若选择最左边的数据作为key,则需要end先走;若选择最右边的数据作为key,则需要bengin先走)。
3、在走的过程中,若end遇到小于key的数,则停下,begin开始走,直到begin遇到一个大于key的数时,将begin和right的内容交换,end再次开始走,如此进行下去,直到begin和end最终相遇,此时将相遇点的内容与key交换即可。(选取最左边的值作为key)
4.此时key的左边都是小于key的数,key的右边都是大于key的数
5.将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作,此时此部分已有序
#include<stdio.h>
void printArray(int *begin,int *end)
{
while(begin<=end)
{
printf("%d ",*begin++);
}
putchar('\n');
}
void swap(int *a,int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void quickSort(int *begin,int *end)
{
int *p = begin;
int *q = end;
int *k = begin;
if(begin>=end)
{
return ;
}
while(begin<end)
{
while(begin < end && *end >= *k) 从最后一个数开始找第一个比标准值小的数
{
--end;
}
while(begin<end && *begin <= *k) 从第一个数开始找第一个比标准值大的数
{
++begin;
}
swap(begin,end); 将此时两个数的值交换
}
swap(k,begin); 相遇时将标准值与此时相遇位置上的数交换
quickSort(p,end-1); 将左一半与又一办重复
quickSort(begin+1,q);
}
int main(void)
{
int a[] = {3,5,1,4,8,0,9,7,2,6};
int len = sizeof(a)/sizeof(a[0]);
quickSort(a,a+len-1);
printArray(a,a+len-1);
return 0;
}
5、 二分查找
前提:数据得是有序的。
思想:先找到中间位置的数,用这个数和要找的数比较。(每次折一半,直到找到对应数组,或者结束),如果输入的数比中间数小,则输入数在数组的前一半,缩小范围,如果输入的数比中间数大,则表示输入数在数组的后一半,缩小范围继续查找。
int search(int *a, int len, int n)
//a是数组,len是数组的大小,n 是需要查找的值
{
int end = len-1;
int begin = 0;
int ret = 0;
while(begin <= end)
{
int mid = (begin+end)/2;
if(n>a[mid])
{
begin = mid + 1; // 要查找的数比中间的数大,说明要找的数在后面一半,缩
小区间
}else if(n<a[mid])
{
end = mid - 1; // 要查找的数比中间的数小,说明要找的数在前面面一半,
缩小区间
}else // 要找的数和中间值相等,表示找到了
{
ret = 1;
}
}
if(ret = 1) // 如果成立表示是从前面break出来的
{
printf("找到了!\n");
}else
{
printf("没有这个数!\n");
}
}
10、linux当中的栈默认有多大
8192k = 8192/1024=8M;
11、列表和数组的区别?各自的优缺点
数组与列表都是具有相同数据类型的多个元素构成的整体。
-
列表:
- 定义:列表是一种有序的、可变的集合,可以包含不同类型的元素(异质性)。
- 特性:支持动态扩容,可以随时添加、删除或更改元素,且内存布局不连续。
-
数组:
- 定义:数组是一种固定大小的、同类型元素的集合(同质性)。
- 特性:在内存中连续存储,元素类型相同,内存布局连续,因此访问速度更快。
3、列表不能对整体进行数值运算,但是数组能对整体进行数值运算 。
4、数组底层使用C语言编写,运算速度快,并且具有强大的运算能力。
列表和数组的优缺点
-
列表:
-
优点:
- 灵活性高,可以包含任意类型的元素。
- 提供了丰富的内置操作和方法,满足更多的编程需求。
- 支持动态扩容,可以根据需要动态分配内存。
-
缺点:
- 内存开销较大,因为每个元素都是独立的对象。
- 进行大量数值计算时效率较低。
-
-
数组:
-
优点:
- 内存使用高效,因为所有元素类型相同且内存布局连续。
- 计算效率高,特别是NumPy数组,支持矢量化操作。
- 适用于大量数值计算和科学计算的场景。
-
缺点:
- 灵活性较低,只能包含相同类型的元素。
- 插入和删除元素时可能导致其他元素的移动,时间复杂度较高。
-
12、查进程用什么命令?
ps:
ps
: 用于显示当前终端会话中的进程信息。ps aux
:显示所有用户的所有进程信息。
top:
top
: 实时显示系统中各个进程的资源占用情况,包括CPU、内存等。
pstree:
pstree
:以树状结构显示进程间的关系,帮助理解进程的层次结构。- 例如,
pstree -p
会显示进程的PID和父子关系。
13、应用层的协议是什么?
应用层协议(ALP)(Application Layer Protocol)是定义了运行在不同端系统上的应用程序进程如何相互传递报文的规则集合。这些协议规定了交换的报文类型、报文类型的语法、字段的语义以及进程何时、如何发送报文和对报文进行响应的规则。
1、DNS 域名解析协议:提供域名到IP地址之间的解析服务。
2.FTP 文件传输协议
3.HTTP 超文本传输协议
5.SMTP 电子邮件协议
6.DHCP 动态主机配置协议
7.TFTP 简单文本传送协议
下载用:HTTP/HTTPS:HTTP是最常用的网络协议之一,也支持文件的下载;
FTP/SFTP
14、查看网络状态用什么命令?
1、ifconfig
ifconfig命令用于显示或设置网络设备。
2、nstat
nstat是一个简单的监视内核的SNMP计数器和网络接口状态的实用工具。
3、netstat
netstat命令用于显示与IP、TCP、UDP和ICMP协议相关的统计数据。
4、ping
该命令用于检查网络上某台主机是否为活动状态或是否发生故障.其原理是利用了TCP/IP协议中的IP层中的ICMP协议从特定的主机上返回响应.
5、 route:
该命令用于查看那系统的路由表信息.
15、对目录的操作流程是什么
1、打开目录(opendir)
2、遍历目录—读目录(readdir)
3、关闭目录(closedir)
16、线程的作用
线程可以提高程序并发性、资源共享、减少系统开销、提高响应性、简化编程模型、利用多核处理器以及实现异步操作等。
- 提高并发性:
- 线程允许程序同时执行多个任务,从而提高了程序的并发性。这对于需要同时处理多个输入/输出操作或执行多个计算任务的应用程序来说尤为重要。
- 资源共享:
- 线程共享进程的资源,如内存、文件句柄和信号量等。这意味着多个线程可以访问和操作相同的数据,而无需进行额外的数据复制或同步操作。
- 减少系统开销:
- 与进程相比,线程的创建和销毁开销较小。因为线程共享进程的地址空间,所以不需要为每个线程分配独立的内存空间。
- 提高响应性:
- 在图形用户界面(GUI)应用程序中,线程可以用于处理用户输入、更新屏幕显示等任务,从而提高应用程序的响应性。即使一个线程正在执行耗时的计算任务,另一个线程仍然可以响应用户的操作。
- 简化编程模型:
- 线程提供了一种简单而强大的方式来并行化程序。通过多线程编程,开发者可以更容易地实现复杂的并发任务,而无需深入了解底层的硬件或操作系统细节。
- 利用多核处理器:
- 现代计算机通常具有多个处理器核心,线程允许程序同时利用这些核心进行并行计算。这可以显著提高程序的执行速度,特别是在处理大量数据或执行复杂计算时。
- 实现异步操作:
- 线程可以用于实现异步操作,如网络请求、文件读写等。这些操作通常需要等待外部事件(如网络响应或文件读取完成),而线程允许程序在等待这些事件的同时继续执行其他任务。
线程的函数:
创建线程:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);
获取线程自身ID:pthread_t pthread_self(void);
线程终止:void pthread_exit(void *value_ptr);
取消一个执行中的线程:int pthread_cancel(pthread_t thread);
等待线程结束:int pthread_join(pthread_t thread, void **value_ptr);
分离线程:int pthread_detach(pthread_t thread);
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
17、const关键字
const修饰变量声明为常量,在程序运行期间其值不可被修改(限定一个变量不允许被改变。)
1、防止意外修改
2、定义变量为常量
3、修饰函数的参数,表示在函数体能不能修改这个参数的值
4、节省空间,避免不必要的内存分配
补充:const *p的用法
1.const int *p
这种写法能把*p的值给限制了,但不能限制p的值,但p的值又不会影响a的结果。总结:能修改p的值,但不能修改*p的值了
2.int* const p
const是限制了p这个变量,对*p这个指针变量没有任何影响。总结: 这种写法是限制了p这个变量,对*p这个指针变量没有任何影响。
3.const int* const p
const int*把指针p给限制了, const p 把p这个变量给限制了。总结:const int*把指针p给限制了, const p 把p这个变量给限制了。
18、数据结构的框架
数据结构分为:线性表、树、图
1.线性表:
(1)顺序表:动态数组
(2)链表:单向链表、双向链表、循环链表、静态链表
(3)栈:顺序栈、链栈
(4)队列:顺序队列、双端队列、循环队列
2.树:
(1)二叉树
概念:满二叉树、完全二叉树、非完全二叉树
种类:二叉排序树、二叉平衡树、红黑树、哈夫曼树
遍历方式:前序遍历(递归/非递归)、中序遍历()、后序遍历()、层序遍历(使用队列)
(2)树
存储方式:图形表示法、广义表表示法、左孩子右兄弟表示法
遍历方式:同二叉树
(3)森林
由一棵或多棵树构成,存储方式和遍历方式同树3.图
存储方式:邻接矩阵、邻接表、十字链表
遍历方式:广度优先遍历BFS、深度优先遍历 DFS
特殊应用:最小生成树
栈:
概念:
- 数据结构中的栈(Stack)是一种特殊的线性表,其特殊性在于它只能在一端进行插入和删除操作,这一端被称为栈顶,另一端则被称为栈底。栈遵循“后进先出”(Last In First Out,LIFO)的原则,即最后一个被放入栈中的元素总是第一个被取出。
名词解释:
- 栈顶:允许插入删除的一端。
- 栈底:不允许插入删除的一端。
- 空栈:不包含任何元素的空表。
队列
定义
队列是一种先进先出的数据结构。第一个入队列的元素将是第一个被移除的元素。可以将队列想象成一个排队的人,先到的人先得到服务。
在队列中,允许插入的一端称为队尾,而允许删除的一端称为队头。当一个元素被添加到队列时,它会被放置在队尾;而当一个元素从队列中被移除时,它会从队头被取出。
队列的基本操作包括:
入队(Enqueue): 将一个元素添加到队列的末尾。
出队(Dequeue): 移除并返回队列的头部元素。
查看队头(Peek/Front): 返回队列的头部元素,但不移除它。
查看队尾(Back): 返回队列的尾部元素,但不移除它。
检查队列是否为空(IsEmpty): 判断队列中是否有元素。
获取队列大小(Size): 返回队列中元素的数量。
初始化队列(Init): 初始化队列,准备使用。
销毁队列(Destroy): 清理队列,释放相关资源。
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 节点结构
typedef struct Node {
int data; // 存储数据
struct Node *next; // 指向下一个节点的指针
} Node;
// 队列结构
typedef struct {
Node *front; // 队头指针
Node *rear; // 队尾指针
int size; // 当前队列大小
} Queue;
// 初始化队列
void init(Queue *queue) {
queue->front = NULL; // 队头指针初始化为NULL
queue->rear = NULL; // 队尾指针初始化为NULL
queue->size = 0; // 当前队列大小初始化为0
}
// 销毁队列
void destroy(Queue *queue) {
Node *current = queue->front;
while (current != NULL) {
Node *next = current->next; // 保存下一个节点
free(current); // 释放当前节点
current = next; // 移动到下一个节点
}
queue->front = NULL; // 清空队头指针
queue->rear = NULL; // 清空队尾指针
queue->size = 0; // 重置队列大小
}
// 入队(Enqueue)
bool enqueue(Queue *queue, int value) {
Node *newNode = (Node *)malloc(sizeof(Node)); // 创建新节点
if (newNode == NULL) {
return false; // 内存分配失败
}
newNode->data = value; // 设置节点数据
newNode->next = NULL; // 新节点的下一个指针设为NULL
if (queue->rear != NULL) {
queue->rear->next = newNode; // 如果队列不为空,将新节点添加到队尾
} else {
queue->front = newNode; // 如果队列为空,新节点是队头
}
queue->rear = newNode; // 更新队尾指针
queue->size++; // 更新队列大小
return true;
}
// 出队(Dequeue)
bool dequeue(Queue *queue, int *value) {
if (queue->front == NULL) {
return false; // 队列为空,无法出队
}
Node *nodeToDelete = queue->front; // 获取队头节点
*value = nodeToDelete->data; // 返回队头数据
queue->front = queue->front->next; // 更新队头指针
if (queue->front == NULL) {
queue->rear = NULL; // 如果队列为空,队尾也设为NULL
}
free(nodeToDelete); // 释放队头节点
queue->size--; // 更新队列大小
return true;
}
// 查看队头(Peek/Front)
bool peekFront(Queue *queue, int *value) {
if (queue->front == NULL) {
return false; // 队列为空,无法查看队头
}
*value = queue->front->data; // 返回队头元素
return true;
}
// 查看队尾(Back)
bool peekBack(Queue *queue, int *value) {
if (queue->rear == NULL) {
return false; // 队列为空,无法查看队尾
}
*value = queue->rear->data; // 返回队尾元素
return true;
}
// 检查队列是否为空(IsEmpty)
bool isEmpty(Queue *queue) {
return queue->size == 0; // 如果大小为0,则队列为空
}
// 获取队列大小(Size)
int size(Queue *queue) {
return queue->size; // 返回当前队列大小
}
// 测试链表实现的队列
int main() {
Queue queue;
init(&queue); // 初始化队列
// 入队操作
enqueue(&queue, 10);
enqueue(&queue, 20);
enqueue(&queue, 30);
// 查看队头和队尾
int value;
if (peekFront(&queue, &value)) {
printf("队头: %d\n", value);
}
if (peekBack(&queue, &value)) {
printf("队尾: %d\n", value);
}
// 出队操作
while (dequeue(&queue, &value)) {
printf("出队: %d\n", value);
}
// 检查队列是否为空
printf("队列是否为空: %s\n", isEmpty(&queue) ? 1 : 0);
// 销毁队列
destroy(&queue);
return 0;
}
树
树是n(n>=0)个结点的有限集。当n = 0时,称为空树。在任意一棵非空树中应满足:
1、有且仅有一个特定的称为根的结点。
2、当n>1时,其余节点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每个集合本身又是一棵树,并且称为根的子树。
显然,树的定义是递归的,即在树的定义中又用到了自身,树是一种递归的数据结构。树作为一种逻辑结构,同时也是一种分层结构,具有以下两个特点:
1、树的根结点没有前驱,除根结点外的所有结点有且只有一个前驱。
2、树中所有结点可以有零个或多个后继。
因此n个结点的树中有n-1条边。
2、基本术语
1、考虑结点K。根A到结点K的唯一路径上的任意结点,称为结点K的祖先。如结点B是结点K的祖先,而结点K是结点B的子孙。路径上最接近结点K的结点E称为K的双亲,而K为结点E的孩子。根A是树中唯一没有双亲的结点。有相同双亲的结点称为兄弟,如结点K和结点L有相同的双亲E,即K和L为兄弟。
2、树中一个结点的孩子个数称为该结点的度,树中结点的最大度数称为树的度。如结点B的度为2,结点D的度为3,树的度为3。
3、度大于0的结点称为分支结点(又称非终端结点);度为0(没有子女结点)的结点称为叶子结点(又称终端结点)。在分支结点中,每个结点的分支数就是该结点的度。
4、结点的深度、高度和层次。
结点的层次从树根开始定义,根结点为第1层,它的子结点为第2层,以此类推。双亲在同一层的结点互为堂兄弟,图中结点G与E,F,H,I,J互为堂兄弟。
结点的深度是从根结点开始自顶向下逐层累加的。
结点的高度是从叶结点开始自底向上逐层累加的。
树的高度(或深度)是树中结点的最大层数。图中树的高度为4。
5、有序树和无序树。树中结点的各子树从左到右是有次序的,不能互换,称该树为有序树,否则称为无序树。假设图为有序树,若将子结点位置互换,则变成一棵不同的树。
6、路径和路径长度。树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的,而路径长度是路径上所经过的边的个数。
注意:由于树中的分支是有向的,即从双亲指向孩子,所以树中的路径是从上向下的,同一双亲的两个孩子之间不存在路径。
7、森林。森林是m (m≥0)棵互不相交的树的集合。森林的概念与树的概念十分相近,因为只要把树的根结点删去就成了森林。反之,只要给m棵独立的树加上一个结点,并把这m棵树作为该结点的子树,则森林就变成了树。
二叉树
二叉树的定义
二叉树是另一种树形结构,其特点是每个结点至多只有两棵子树( 即二叉树中不存在度大于2的结点),并且二叉树的子树有左右之分,其次序不能任意颠倒。
(1)顺序存储结构
二叉树的顺序存储是指用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素,即将完全二叉树上编号为i ii的结点元素存储在一维数组下标为i − 1的分量中。
(2)链式存储结构
既然顺序存储适用性不强,我们就要考虑链式存储结构。二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域是比较自然的想法,我们称这样的链表叫做二叉链表。
其中data是数据域,lchild 和rchild都是指针域,分别存放指向左孩子和右孩子的指针。
以下是我们的二叉链表的结点结构定义代码。
遍历二叉树
二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
1、先序遍历(根左右)
先序遍历的操作过程如下:
若二叉树为空,则什么也不做,否则,
1)访问根结点;
2)先序遍历左子树;
3)先序遍历右子树。
void PreOrder(BiTree T){
if(T != NULL){
visit(T); //访问根节点
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchild); //递归遍历右子树
}
}
中序遍历(左根右)
中序遍历( InOrder)的操作过程如下:
若二叉树为空,则什么也不做,否则,
1)中序遍历左子树;
2)访问根结点;
3)中序遍历右子树。
void InOrder(BiTree T){
if(T != NULL){
InOrder(T->lchild); //递归遍历左子树
visit(T); //访问根结点
InOrder(T->rchild); //递归遍历右子树
}
}
后序遍历(左右根)
后序遍历(PostOrder) 的操作过程如下:
若二叉树为空,则什么也不做,否则,
1)后序遍历左子树;
2)后序遍历右子树;
3)访问根结点。
void PostOrder(BiTree T){
if(T != NULL){
PostOrder(T->lchild); //递归遍历左子树
PostOrder(T->rchild); //递归遍历右子树
visit(T); //访问根结点
}
}
19、链表的作用
实现数据元素按一定顺序的存储,允许在任意位置插入和删除节点
包括单向节点、双向节点、循环节点
链表的优缺点
优点:链表实现数据元素储存的顺序储存,是连续的
1、动态类型数据结构:链表能够在运行时发展和收缩,这是由于通过分配和不分配内存执行的动态组织。因此,不需要为链表提供主要大小。
2、不浪费内存:链表提供了有效内存利用率的实现,因为此列表的大小在运行时上升或下降;因此没有记忆的浪费。此外,不需要预先分配内存。
3、实现:线性类型的数据结构(如队列和堆栈)通常通过链表简单地执行。
4、插入和删除操作:在此链表中,可以轻松执行插入和删除查询操作。我们不需要在元素的惰性和删除操作后移动元素,只需要修改后续指针中可用的地址。
缺点:因为含有大量的指针域,所以占用空间大,同时因为只有头结点(后面说明)是明确知道地址的,所以查找链表中的元素需要从头开始寻找,非常麻烦。
20、ifndef
条件指示符#ifndef 的最主要目的是防止头文件的重复包含和编译。
21、包头文件“”和<>有啥区别?
区别主要在于编译器查找头文件的路径不同。
“ ”先在当前文件夹找,再到系统默认路径下寻找,
<>引用标准库头文件,编译器会直接到系统默认路径下寻找
静态库和动态库
静态库和动态库的编译方式和载入时机都是不一样的,
载入时机;静态库是在我们编译源文件时,就将静态库和源文件一起编译,生成可执行的二进制文件。 而动态库是在我们程序运行时才会去加载,编译得到的可执行程序的文件大小一定是静态库大于动态库的,因为动态库编译时并不会加载库文件,只有 运行时才会加载。
静态库:gcc main.c -o main.o -c
动态库:gcc -shared -fPIC -o libadd.so myadd.c 【-shared表示生成动态库,fPIC为参数,libadd.so为动态库的命名规范,myadd.c表示要编译的源文件,如果有多个源文件要同时编译问一个动态库,直接后面追加即可】
22、C语言编译四个阶段
预处理、编译、汇编和链接
gcc -E hello.c -o hello.i ----- 预处理
gcc -S hello.i –o hello.s ----- 编译
gcc -c hello.s -o hello.o ----- 汇编
gcc hello.o -o hello_elf ------链接
1、预处理
预处理是指在编译之前对源代码进行的处理。它主要包括以下几个步骤:
(1)处理头文件引用(头文件展开):将所有被#include包含的头文件内容复制到源文件中,形成一个整体的源代码文件。
(2)宏替换:将所有定义的宏进行替换,例如将所有出现的宏名替换为对应的宏定义内容。
(3)条件编译:根据预处理指令(如#ifdef、#ifndef、#if、#elif等)的条件判断,选择性地编译代码段。
(4)删除注释:删除所有的注释内容,包括单行注释(//)和多行注释(/* */)。
2. 编译
编译是将预处理后的源代码翻译成汇编语言的过程。编译器会对源代码进行词法分析、语法分析和语义分析,生成相应的中间代码。它主要包括以下几个步骤:
词法分析:将源代码划分为一个个的标记(token),如关键字、标识符、运算符等。
语法分析:根据语法规则,将标记组合成语法树,检查代码是否符合语法规范。
语义分析:对语法树进行语义检查,包括类型检查、变量声明检查等。
3. 汇编
汇编是将编译生成的中间代码翻译成机器指令的过程。
4. 链接
链接是将多个目标文件和库文件合并成一个可执行文件的过程。链接器将各个目标文件中的符号引用与其定义进行关联,解析符号引用,生成最终的可执行文件。它主要包括以下几个步骤:
符号解析:将各个目标文件中的符号引用与其定义进行关联。
重定位:将目标文件中的地址引用转换为实际的内存地址。
符号解析冲突处理:处理多个目标文件中相同符号的定义冲突。
23、define
定义宏用define 去定义
宏是在预编译的时候进行替换。
宏定义的作用范围,从定义的地方到本文件末尾。
如果想在中间终止宏的定义范围:#undef PI //终止PI的作用
带参宏
#define S(a,b) a*b
注意带参宏的形参 a和b没有类型名,
S(2,4) 将来在预处理的时候替换成 实参替代字符串的形参,其他字符保留,2 * 4。
带参宏和带参函数的区别
带参宏被调用多少次就会展开多少次,执行代码的时候没有函数调用的过程,不需要压 栈弹栈。所以带参宏,是浪费了空间,因为被展开多次,节省时间。
带参函数,代码只有一份,存在代码段,调用的时候去代码段取指令,调用的时候要压 栈弹栈。有个调用的过程。所以说,带参函数是浪费了时间,节省了空间。
带参函数的形参是有类型的,带参宏的形参没有类型名。
如果功能实现的代码相对简单,并且不需要开辟太多的空间,可以选择使用带参宏,但 是大多数情况都会使用函数。
- 宏是没法调试的。
- 宏由于类型无关,也就不够严谨
24、设计一个点对点通信的软件,选择TCP还是UDP,为什么?
-
如果软件需要确保数据的完整性和顺序性:
- 选择TCP协议。TCP的可靠性和有序性可以保证数据在传输过程中不会丢失或出错,并且会按照发送顺序到达接收方。
-
如果软件对实时性要求较高且可以容忍一定数据丢失:
- 选择UDP协议。UDP的低延迟和高吞吐量使得数据可以更快地传输到接收方,适用于实时性要求较高的应用场景。同时,由于UDP没有拥塞控制和流量控制的机制,可以减少网络延迟和抖动。
-
如果软件需要同时满足可靠性和实时性要求:
- 可以考虑结合使用TCP和UDP协议。例如,在传输控制信息时使用TCP以确保可靠性,在传输实时数据时使用UDP以减少延迟。
25、链表头插和尾插哪种效率高
头插法:
- 插入速度快(不需要遍历旧链表)
- 首元结点每次插入都会变化,首元结点永远是最新的元素
- 遍历时是按照插入相反的顺序进行
- 由于首元结点不断在变化,所以需要额外的维护首元结点的引用,否则找不到首元结点,链表就废了
尾插法:
- 插入速度慢(需要遍历旧链表到最后一个元素)
- 首元结点永远固定不变
- 遍历时是按照插入相同的顺序进行
链表查重的结束条件
1、遍历完整个链表:
2、达到特定条件:
26、如何调试段错误(GDB)
借助linux当中的gdb来调试
第一步:后面加上 -g
gcc test.c -o test -g
第二步:使用gdb来运行test文件
gdb test
最后:输入run+回车
GDB常用命令
- run 或 r: 启动程序。
- start: 在第一次运行时不加载任何断点。
- continue 或 c:继续执行程序,直到遇到下一个断点。
- step 或 s: 单步执行,进入函数内部。
- next 或 n: 单步执行,不进入函数内部。
- finish 或 f: 执行到当前函数结束。
- until 或 u <line>:继续执行直到到达指定行号。
- kill: 终止当前运行的程序。
- quit 或 q: 退出GDB。
- break / b:设置断点
- list 或 l:显示当前行的源代码
-
注:()括号里面是该指令的全称
💜 l(list) 行号/函数名 —— 显示对应的code,每次10行💜r(run) —— F5【无断点直接运行、有断点从第一个断点处开始运行】
💜b(breakpoint) + 行号 —— 在那一行打断点
💜b 源文件:函数名 —— 在该函数的第一行打上断点
💜b 源文件:行号 —— 在该源文件中的这行加上一个断点吧
💜info b —— 查看断点的信息breakpoint already hit 1 time【此断点被命中一次】
💜d(delete) + 当前要删除断点的编号 —— 删除一个断点【不可以d + 行号】若当前没有跳出过gdb,则断点的编号会持续累加
💜d + breakpoints —— 删除所有的断点💜disable b(breakpoints) —— 使所有断点无效【默认缺省】
💜enable b(breakpoints) —— 使所有断点有效【默认缺省】
💜disable b(breakpoint) + 编号 —— 使一个断点无效【禁用断点】
💜enable b(breakpoint) + 编号 —— 使一个断点有效【开启断点】相当于VS中的空断点
💜enable breakpount —— 使一个断点有效【开启断电】💜n(next) —— 逐过程【相当于F10,为了查找是哪个函数出错了】
💜s(step) —— 逐语句【相当于F11,】
💜bt —— 看到底层函数调用的过程【函数压栈】
💜set var —— 修改变量的值
💜p(print) 变量名 —— 打印变量值
💜display —— 跟踪查看一个变量,每次停下来都显示它的值【变量/结构体…】
💜undisplay + 变量名编号 —— 取消对先前设置的那些变量的跟踪
排查问题三剑客🗡
💜until + 行号 —— 进行指定位置跳转,执行完区间代码💜finish —— 在一个函数内部,执行到当前函数返回,然后停下来等待命令
💜c(continue) —— 从一个断点处,直接运行至下一个断点处【VS下不断按F5】
27、内存分布
栈区是用来存放局部变量、函数参数和返回值地址的;8M
BSS:未初始化操作的全局变量和静态变量放在该区,会被自动初始化为0。
数据区:初始化后的全局变量和静态变量还有常量放在该区。
const修饰的变量,取决于它定义的地方,局部定义的就存在栈区,全局定义的就存放在静态区。
28、signal的作用
软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。进程之间可以互相通过系统调用kill发送软中断信号。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。
29、在C中,extern的用法
1、用于跨文件引用全局变量,即在本文件中引用一个已经在其他文件中定义的全局变量。
2、声明一个变量或函数是定义在其他文件中的。他的作用是跨文件共享数据和函数声明。
注意:引用时不能初始化,
30、volatile·关键字
易失性修饰词
- 告诉编译器当前变量是易变的,每次读取此变量值不要从缓存或者寄存器中读取,而是直接从内存中读取 。(是告诉编译器,他后面定义的变量随时都会改变,因此编译后面的程序每次需要存储或者读取这个变量的时候,都需要直接从变量地址中读取数据。)
- 多线程应用中被几个任务共享的变量
- 阻止编译器对该变量的过度优化
volatile 关键字可以保证并发编程三大特征(原子性、可见性、有序性)中的可见性和有序性,不能保证原子性。
volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据
一般说来,volatile用在如下的几个地方:
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2、多任务环境下各任务间共享的标志应该加volatile;
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
31、线程间加锁
确保了线程安全地访问和修改共享资源,避免了数据竞争和执行顺序的问题,从而保证了程序的正确性和数据的完整性。
如果不加锁,可能会发生当前线程读到了其他线程的数据。锁可以让一段代码执行完再去执行其他线程
什么是死锁
死锁是一种因为线程/进程之间进行资源竞争,造成相互等待资源,“永久”阻塞的一种现象。
什么时候会形成死锁
形成死锁要同时满足4个条件:
互斥条件:一个资源只能被一个线程拥有。
请求与保持条件:线程因为申请资源而造成阻塞时,其所拥有的资源不会释放。
不剥夺条件:线程所拥有的资源在其未使用完之前,不能被强行剥夺。
循环等待条件:线程与线程之间形成一种首尾相接相互等待对方资源的情况。
自旋锁和互斥锁的区别
自旋锁与互斥锁很重要的一个区别在于,线程在申请自旋锁的时候,线程不会被挂起,它处于忙等待的状态,一般用于等待时间比较短的情形。
读写锁
1、只要没有线程持有给定的读写锁用于写,那么任意数目的线程可以持有读写锁用于读。
2、仅当没有线程持有某个给定的读写锁用于读或用于写时,才能分配读写锁用于写。
3、读写锁用于读称为共享锁,读写锁用于写称为排它锁。
32、strcpy和strncpy的区别
strcpy() 不会检查目标字符串的大小,复制整个源字符串直到遇到 \0。
strncpy() 接受一个额外的参数 n,用于指定最多复制的字符数,即使源字符串长度大于 n。
strcpy和memcpy的区别
1)复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
2)复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
3)用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy
33、声明和定义的区别
声明是用来告诉编译器变量的名称和类型,而不分配内存。
定义是为了给变量分配内存,可以为变量赋初值。
区别:
变量/函数可以声明多次,变量/函数的定义只能一次。
声明不会分配内存,定义会分配内存。
声明是告诉编译器变量或函数的类型和名称等,定义是告诉编译器变量的值,函数具体干什么。
注意:extern 变量类型 变量名 仅是声明。
34、进程
目的是为了对并发执行的程序进行控制。进程实体由程序段、数据段、PCB三部分构成。
数据段就是各种数据。
程序段就是一系列操作计算机的指令。
PCB 即进程控制块,控制运行程序段的时机。
进程一般有三种状态:执行,阻塞,就绪
35、内存碎片、泄漏、溢出有什么区别,越界访问和野指针
内存泄漏(Memory Leak):
- 内存泄漏是指程序中已分配的内存没有得到正确释放,导致随着程序的运行,这些内存无法再被使用。
内存溢出(Memory Overflow):
- 内存溢出是指程序尝试写入的内存超过了为其分配的内存空间
内存碎片(Memory Fragmentation):
- 内存碎片是指内存中由于不规则的分配和释放,导致许多小的、未使用的内存块(碎片)散布在已使用的内存块之间。
区别:
- 内存碎片是内存分配问题,导致内存使用效率低下,但不直接导致程序错误。
- 内存泄漏是内存管理问题,导致程序随着时间推移逐渐失去可用内存,可能导致性能下降或失败。
- 内存溢出是编程错误,导致程序尝试访问未分配的内存区域,可能导致程序崩溃或安全问题。
查内存泄漏用的工具:valgrind
越界访问是指程序试图读取或写入不属于其合法范围的内存地址。常见的越界访问情况包括数组越界、指针越界和字符串越界。
越界访问可能产生以下几种危害:
- 程序崩溃:访问非法内存地址可能导致程序崩溃。
- 数据损坏:越界写入可能会覆盖其他变量的数据,导致数据损坏。
- 安全漏洞:攻击者可以利用越界访问漏洞进行恶意攻击,如缓冲区溢出攻击
野指针:野指针就是指向的内存地址是未知的(随机的,不正确的,没有明确限制的)。
注:野指针不会直接引发错误,操作野指针指向的内存区域才会出问题。
int a = 100;
int *p;
p = a; //把a的值赋值给指针变量p,p为野指针, ok,不会有问题,但没有意义
p = 0x12345678; //给指针变量p赋值,p为野指针, ok,不会有问题,但没有意义
*p = 1000; //对野指针进行赋值操作就不可以了
指针未初始化:指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它所指的空间是随机的。
int main()
{
int * p;
*p = 20;
return 0;
}
指针越界访问:指针指向的范围超出了合理范围,或者调用函数时返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。
int main()
{
int arr[10] = {0};
int *p = arr;
for(int i = 0; i <= 11; i++)
{
*(P++) = i;//当指针指向的范围超出数组arr的范围,p变成野指针。
}
return 0;
}
.指针释放后未置空:有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。其实它们只是把指针所指的内存给释放掉,但并没有把指针本身忘记。此时指针指向的就是无效内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。
int main()
{
int *p = NULL;
p = malloc(10 * sizeof(int));
if (!p)
{
return;
}
//成功开辟内存,可以操作内存。
free(p);
p = NULL;
return 0;
}
malloc free
函数功能
malloc()函数的功能是:向内存申请一块连续可用的空间,并返回指向块开头的指针.
free()函数的功能是:释放以前由malloc()函数动态开辟的内存空间,使其可以重新被分配.
36、linux怎么获得内存大小
free命令是Linux系统中用于显示内存使用情况的工具。通过执行free -h命令,我们可以获取物理内存的总大小、已使用大小和可用大小等信息,并以人类可读的方式显示。
- malloc中有多少空间没有被覆掉怎么办?
37、men1和men2的区别
man1和man2的主要区别在于它们分别包含了用户级命令和系统调用的文档,这反映了操作系统提供的两个不同层次的接口。
man1(标准命令)
man2(系统调用)
38、linux常用的命令功能
- ls:列出目录内容。
- cd:切换当前工作目录。
- pwd:显示当前工作目录的路径。
- mkdir:创建新目录。
- rm:删除文件或目录。
- cp:复制文件或目录。
- mv:移动文件或目录,或者对文件或目录进行重命名。
- cat:查看文件内容。
- touch:创建一个空文件,或者更新现有文件的时间戳。
- find:按照指定条件搜索文件。
- tar:用于归档和解压文件。
- chmod:修改文件或目录的权限。
- ps:显示当前运行的进程。
- top:动态显示系统中的进程信息。
- kill:终止指定的进程。
- apt-get/yum:包管理器,用于安装、升级和移除软件包。
- ifconfig/ip:查看和配置网络接口信息。
- ping:向目标主机发送ICMP请求以测试连接。
- netstat:显示网络相关的统计信息。
• su/sudo: 切换用户或以超级用户权限执行命令。
• history: 显示最近执行的命令历史记录。
• ln: 创建链接(硬链接或符号链接)。
• file: 确定文件类型。
• tail: 显示文件末尾的内容。
• head: 显示文件开头的内容。
• wc: 计算文件的行数、字数和字节数。
• sort: 对文件内容进行排序。
• diff: 比较两个文件的差异。
• tar: 打包和解包文件。
• gzip/gunzip: 压缩和解压缩文件。
• date: 显示或设置系统日期和时间。
• echo: 在终端输出文本。
• locate: 快速定位文件。
• history: 显示和管理命令历史记录。
• man: 查看命令的在线帮助文档。
39、熟悉那些进程命令?网络命令?查看网卡用什么命令?
1、查看进程信息:ps
2、查看进程占用信息:top
3、终止进程:kill // kill [-signal] pid
sleep 命令暂停进程
4、route
命令用于配置和显示路由表信息
5、ping
查看或配置网卡信息:ifconfig/ip addr
6、pgrep命令
作用:pgrep命令查找当前运行的进程
40、单链表和双链表的区别
1、双向链表:从双向链表中的任意一个结点开始,都可以很方便地访问前驱结点和后继结点。
2、单向链表:单个结点创建非常方便,普通的线性内存通常在创建的时候就需要设定数据的大小,结点的访问方便,可以通过循环或者递归的方法访问到任意数据。
1、双向链表:增加删除节点复杂,需要多分配一个指针存储空间。
2、单向链表:结点的删除非常方便,不需要像线性结构那样移动剩下的数据,但是平均的访问效率低于线性表。
41、怎么删除/查看第n个节点
定义快慢两个指针,快指针始终比慢指针多走n+1步,遍历n次,将快指针抵达链表的尾部,再对慢指针走到的地方进行更新到n+1处,从而达到删除第n个节点的目的
采用快慢指针的方法来处理本题,定义一个fast指针和一个slow指针,一开始我们让fast和slow都指向链表的head,然后让fast指针向前移动n个节点,然后我们让fast和slow一起向前移动,直到fast为NULL的时候,此时我们的slow指针恰好就在待删除节点的前一个节点,此时我们就可以直接将待删除的结点删掉了
42、阻塞
阻塞是被动的,是线程在等待某种事件或者资源的表现,恢复之后会继续从阻塞时执行到的位置继续向下执行,而不是重新开始执行。
一个线程/进程经历的5个状态,创建,就绪,运行,阻塞,终止
同步:指用户线程发起I0请求后,需要等待或者轮询内核I0操作完成后才能继续执行
异步:指用户线程发起10请求后仍继续执行,当内核10操作完成后会通知用户线
程或者调用用户线程注册的回调函数
同步阻塞调用:得不到结果不返回,线程进入阻塞态等待。
同步非阻塞调用:得不到结果不返回,线程不阻塞一直在CPU运行。
异步阻塞调用:去到别的线程,让别的线程阻塞起来等待结果,自己不阻塞。
异步非阻塞调用:去到别的线程,别的线程一直在运行,直到得出结果。
并发是指一个时间段内,有几个程序都在同一个CPU上运行,但任意一个时刻点上只有一个程序在处理机上运行。
并行是指一个时间段内,有几个程序都在几个CPU上运行,任意一个时刻点上,有多个程序在同时运行,并且多道程序之间互不干扰。
43、数据结构、表、栈、队列之间的区别?
1.链表:
对应顺序存储结构,存储的数据在内存中不连续,使用指针对数据进行访问
2.栈:
只能在一端进行操作,如插入和删除,按照先进后出的原则存储数据
3.队列:
特点是先进先出,后进后出,只能在队首操作,如删除、读取等,在队尾添加。队列的存储方式既可以使用线性表,也可以用链表。
44、如何定义一个指向5个整型元素的数组指针?
int (*p)[5];
- 首先定义一个具有5个整型元素的数组。
- 然后定义一个指向该数组的指针,并将其指向数组。
45、描述http的交互过程
HTTP协议工作过程
当我们在浏览器输入一个网址,此时浏览器就会给对应的服务器发送一个HTTP请求,对应的服务器收到这个请求之后,经过计算就会返回一个HTTP响应,并且当我们访问一个网站的时候,可能涉及到不止一次的HTTP请求和相应的交互过程。
HTTP协议的重点特点:一发一收,一问一答
URL基本介绍
平时我们上网的网址就是URL,互联网上的每一个文件都有一个统一的URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它
URL的标准格式如下:
协议类型:[//服务器地址][:端口号][/资源层级UNIX文件路径]文件名[?查询字符串][#片段标识符]
HTTP中的方法,就是HTTP请求报文中的首行的第一部分。
虽然HTTP中的方法有很多,但是我们主要用到的只有两个GET和POST
- GET:获取资源
- POST:传输资源
46、栈和队列的相同点和不同点
栈与队列的相同点:
1.都是线性结构。
2.插入操作都是限定在表尾进行。
3.都可以通过顺序结构和链式结构实现。、
4.插入与删除的时间复杂度都是O(1),在空间复杂度上两者也一样。
栈与队列的不同点:
1.删除数据元素的位置不同,栈的删除操作在表尾进行,队列的删除操作在表头进行。
2.顺序栈能够实现多栈空间共享,而顺序队列不能。
4.栈先进后出,队列先进先出
47、父子进程运行时的关系
创建进程
1.fork()函数
返回值:如果返回值大于零,表明处于父进程上下文环境中,返回值是子进程的ID.如果返回值是零,表明处于子进程上下文环境中.其他返回值(小于零)表明调用fork()函数出错,仍处于父进程上下文环境中.
函数说明:
由fork()函数创建的新进程被称为子进程.fork()函数被调用一次,但返回两次,两次的返回值不同,子进程的返回值是0,父进程的返回值是新进程的进程ID.
一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID.
一个进程只会有一个父进程,所以任意一个子进程都可以通过调用getppid()函数获取其父进程的进程ID.
fork()函数调用成功后,将为子进程申请PCB和用户内存空间.子进程是父进程的副本.在用户空间将复制父进程用户空间所有数据(代码段、数据段、BBS、堆、栈),复制父进程内核空间PCB中的绝大多数信息.
子进程在创建后和父进程同时执行,竞争系统资源,谁先谁后,取决于内核所使用调度算法.子进程的执行位置为fork返回位置.
48、目录操作
1、 pwd 命令 以绝对路径的方式显示用户当前工作目录
/2、mkdir 命令 用来创建目录
3、cd 命令 用来切换工作目录至"参数"
4、tree 命令 以树状图列出目录的内容
5、rmdir命令 用来删除空目录
48、数据库基本用法
数据库流程
(1)定义数据库指针
sqlite3 *db;
(2)打开数据库
函数:sqlite3_open
函数原型:int sqlite3_open(char *path,sqlite3**db);
函数功能:打开指定 path 路径+文件名称的数据库,并将打开的地址指向db 变量的句柄。
(3)执行sql语句
函数:sqlite3_exec
函数原型:int sqlite3_exec(sqlite3*db,char *sql,callback fun,void*arg,char ** errmsg);
函数功能:在db 数据库上执行 sql非查询语句。并将结果返回。
注意:如果是需要返回结果的,如select语句,第三个参数需要传回调函数接口
(4)关闭数据库
函数:sqlite3_close
函数原型:int sqlite3_close(sqlite3*db);
事物是一种确保数据库操作序列的原子性,一致性,隔离性和持久性(ACID属性)的机制。
START TRANSACTION; 开启事物
commit;提交事物
提高读数据库的效率
数据库的基本指令
SELECT column1, column2 FROM table_name WHERE condition; 查找
INSERT INTO table_name (column1, column2) VALUES (value1, value2); 插入
UPDATE table_name SET column1 = value1, column2 = value2 WHERE condition; 更新
DELETE FROM table_name WHERE condition; 删除
CREATE TABLE table_name (column1 datatype, column2 datatype, ...); 创建
DROP TABLE table_name; 删除表
SELECT * FROM table_name WHERE condition; 过滤查找
49、指针数组和数组指针,指针函数和函数指针
指针数组: 指针数组是一个数组,其中的每个元素都是指针类型。
int *p a[5];
数组指针:是指向数组的指针,它可以指向数组的首地址。通过数组指针可以访问数组中的元素,可以进行指针运算来遍历数组。
int (*p) a[5];
数组指针的作用主要有以下几个方面:
1、遍历数组:通过数组指针,可以使用指针算术运算来遍历数组中的元素,实现对数组的遍历操作。
2、传递数组给函数:通过将数组指针作为参数传递给函数,可以在函数内部对数组进行操作,而无需复制整个数组。
3、动态分配内存:通过数组指针,可以使用动态内存分配函数(如malloc)来动态创建数组,灵活地管理内存空间。
指针函数:本质是一个函数,此函数返回某一类型的指针。
函数指针:本质是一个指针,指向函数的指针变量,其包含了函数的地址,通过它来调用函数。
常量指针
定义:指向常量的指针。该指针指向一个常量,常量的值不可变,不可以通过该指针修改其值,但是该指针可以指向其他常量。(指针地址可以变,值不能变)
int a = 0,b = 0;
const int *p = &a; //修饰的是*p
*p = 1; //错误,不可修改常量值
*p = &b; //正确,可以指向另一个常量
//注意:两种写法的意义相同,都是常量指针。
const int* a;
int const* a;
指针常量
定义:指针本身是常量。该指针只能指向某个常量,不可再指向其他常量。(指针地址不可变,值可变。注:指针常量在定义时要赋初值。)
int a = 0,b = 0;
int* const p = &a; 修饰p
*p = 1; //正确,可以修改值
*p = &b; //错误,不可改变地址
如果关键字 const 出现在 * 号左边,表示被指物是常量,侧重点是常量,值不可变;如果出现在 * 号右边,表示指针自身是常量,侧重点是指针,地址不可变;如果出现在 * 号两边,表示被指物和指针都是常量,地址和值都不可变。(简化记忆:const 与 * 号,谁在左边就以谁为侧重点。)
50、内存大小端
✨注意:计算机读数据永远是从低地址开始的!!
数据的高位是数据的左边位置的数,数据的低位是数据右边位置的数,数据的高位和低位又称高字节和低字节。
大端存储,是将数据的低位字节放到高地址处,高位字节放到低地址处。
小端存储,是将数据的低位字节放到低地址处,高位字节放到高地址处。
小端存储,0x12345678;
内存地址 数据
0x00 0x78 低地址
0x01 0x56
0x02 0x34
0x03 0x12 高地址
测试大小端
#include"stdio.h"
#include"windows.h"
//**方法一**
int main(void)
{
int n = 0x11223344;
char a = n;
if (a ==68)
printf("是小端"); //低地址存低数据
if (a == 17)
printf("是大端");
system("pause");
return 0;
}
//**方法二**
union A
{
char a ;
int b ;
}A;
int main(void)
{
A.a = 1;
printf("%d\n",A.a);
int x = A.a;
A.b = 2;
printf("%d\n", A.b);
printf("%d\n", A.a);
if (A.a == 1)
{
printf("是大端");
}
else
printf("是小端"); //我们同时发现 联合体后者会改变影响了前边的数;
system("pause");
return 0;
}
#endif
//**方法三 指针方法**
int main(void)
{
int b = 0x11223344;
char* a = (char*)(&b);
if(* a == 1*16+1)
printf("是大端\n");
else if (*a == 4*16+4)
printf("是小端\n");
system("pause");
return 0;
}
51、select、poll、epoll
如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,也就是轮询。poll、epoll 和 select 可以用于处理轮询,应用程序通过 select、epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。
- select的可移植性较好,可以跨平台;
- select可设置的监听时间timeout精度更好,可精确到微秒,而poll为毫秒。
select:单个进程可监视的fd数量被限制,即能监听端口的大小有限。
对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:
需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
poll:它没有最大连接数的限制,“水平触发”
表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善
52、IO模型
1、阻塞IO模型
2、非阻塞IO模型
3、复用IO模型
4、驱动IO模型
5、异步IO模型
53、原子操作
原子操作指的是由多步操作组成的一个操作。如果该操作不能原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
54、wireshark
1、sudo wireshark # 启动wireshark
2、可以通过Filter进行过滤,可供选择的筛选项有协议、IP、端口、http模式等。筛选我们需要捕获的接口,点击Start即可捕获数据。
Wireshark 是一款开源的网络封包分析工具,具有强大的网络协议分析能力。
作用:网络故障排查、安全分析和监控、网络性能优化、协议开发和调试
55、线程池
概念:线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。
线程池就是一个容纳多个线程的容器,对于一线线程我们可以多次对此线程进行重复使用,从而省去频繁创建线程对象的操作。
频繁的进行进程的创建与销毁将带来很多开销。不但如此,进程间频繁的切换也将减低 CPU 的利用率。 如果能复用之前创建的进程,而不是为每个并发任务创建一个进程,能有效降低进程创建与销毁的开销并减少进程间切换,从而减少对 CPU 资源的浪费。
56、中断和信号的区别
信号(软中断)与中断的相似点:
1,都采用相同的异步通信方式
2,当检测出有信号或中断请求时,都暂停正在执行的程序而转去执行相应的处理程序
3,都在处理完毕之后返回到原来的断点
4,对信号或中断都可进行屏蔽
信号与中断的区别:
1,中断有优先级,而信号没有,所有的信号都是平等的
2,信号处理程序是在用户态下运行的,而中断处理程序是在核心态下运行的
3,中断响应是及时的,而信号响应通常都有较大的事件延迟
57、inline
内联函数的本质:inline关键字会告诉编译器,此函数是内联函数,希望编译器尽可能的在编译阶段将此函数展开到调用此函数的地方,且对此内联函数按照常规函数一样进行语法检查,比如入参类型,入参个数等。
内联函数会被语法检查,所以内联函数编译阶段被处理。
58、指针和数组
指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
指针的大小在32位平台是4个字节,在64位平台是8个字节
1、定义了指针一定要初始化,如果不明确它的指向,用NULL初始化;
2、明确指针指向类型,不要乱标指针类型;
3、对于不属于自己的空间,尽管我还保留着这块空间的地址,我们应该及时置空;
数组是在内存中连续存储的具有相同类型的一组数据的集合
区别
同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝
数组:数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的,多维数组在内存中是按照一维数组存储的,只是在逻辑上是多维的。
数组的存储空间,不是在静态区就是在栈上。
指针:指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。
指针:由于指针本身就是一个变量,再加上它所存放的也是变量,所以指针的存储空间不能确定。
数组所占存储空间的内存:sizeof(数组名)
数组的大小:sizeof(数组名)/sizeof(数据类型)
在32位平台下,无论指针的类型是什么,sizeof(指针名)都是4,在64位平台下,无论指针的类型是什么,sizeof(指针名)都是8。
数组传参时,会退化为指针,所以我们先来看看什么是退化!
(1)退化的意义:C语言只会以值拷贝的方式传递参数,参数传递时,如果只拷贝整个数组,效率会大大降低,并且在参数位于栈上,太大的数组拷贝将会导致栈溢出。
(2)因此,C语言将数组的传参进行了退化。将整个数组拷贝一份传入函数时,将数组名看做常量指针,传数组首元素的地址。
当函数参数部分是一级指针时,可以接受什么参数例如:test(int*p)
(1)可以是一个整形指针
(2)可以是整型变量地址
(3)可以是一维整型数组数组名
59、文件io和标准io操作
一般标准IO指的是C语言的IO操作,文件IO一般指的是Linux系统调用的IO操作。标准IO因为是C语言提供的标准库,所以可以在其他操作系统平台编译后可以执行,但是文件IO只能在Linux下使用,标准IO意味着可以在多个平台移植使用。
文件IO中用文件描述符(fd)表示一个打开的文件,可以访问不同类型的文件(如普通文件、设备文件和管道文件)。而标准IO中用FILE(流)表示一个打开的文件,通常只用来访问普通文件
标准IO
step1 : 打开文件FILE ---- fopen
step2 : io 操作,读写操作
fgetc / fputc ---- 一个字符
fgets / fputs ---- 一次一行
fread / fwrite ---- 自定义大小,操作二进制文件
step3 : 关闭文件 ---- fclose
fopen() 函数:
#include <stdio.h>
FILE* fopen(const char *pathname, const char *mode);
/*参数:
pathname:文件路径名。
mode:文件打开模式,可以取以下值:
"r":只读模式打开文件。
"w":只写模式打开文件,如果文件不存在则创建文件,如果文件已存在则清空文件。
"a":追加模式打开文件,如果文件不存在则创建文件。
"r+":读写模式打开文件。
"w+":读写模式打开文件,如果文件不存在则创建文件,如果文件已存在则清空文件。
"a+":读写模式打开文件,如果文件不存在则创建文件,并在文件末尾追加数据。
更多模式可以查阅相关文档。
返回值:返回一个文件指针(FILE *),用于后续的文件操作。如果打开文件失败,返回 NULL,并设置 errno 来指示错误原因。
常见用法:用于打开文件,获取文件指针,以便后续的文件操作。
*/
fclose() 函数:
#include <stdio.h>
int fclose(FILE *stream);
/*
参数:
stream:文件指针,需要关闭的文件。
返回值:返回 0 表示成功关闭文件,返回 EOF 表示失败,并设置 errno 来指示错误原因。
常见用法:用于关闭一个已经打开的文件。
*/
fread() 函数:
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
/*
参数:
ptr:指向存储读取数据的缓冲区(这个缓冲区不是上面说的标准IO的,是自己定义的temp buffer)。
size:每个数据项的大小(以字节为单位)。
nmemb:要读取的数据项的数量。
stream:文件指针,指向要读取数据的文件。
返回值:返回实际读取的数据项数量,如果到达文件末尾或出错返回值小于 nmemb,同时设置 errno 来指示错误原因。
常见用法:用于从文件中读取数据。
*/
fwrite() 函数:
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
/*
参数:
ptr:指向要写入数据的缓冲区(这个缓冲区不是上面说的标准IO的,是自己定义的temp buffer)。
size:每个数据项的大小(以字节为单位)。
nmemb:要写入的数据项的数量。
stream:文件指针,指向要写入数据的文件。
返回值:返回实际写入的数据项数量,如果出错返回值小于 nmemb,同时设置 errno 来指示错误原因。
常见用法:用于向文件中写入数据。
*/
fseek() 函数:
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
/*
参数:
stream:文件指针,指向要定位的文件。通常由 fopen 函数返回。
offset:偏移量,以字节为单位。它表示从 whence 指定的位置开始偏移的字节数。
whence:偏移起始位置,可以取以下值:
SEEK_SET:文件开头。
SEEK_CUR:当前文件位置。
SEEK_END:文件末尾。
返回值:返回 0 表示成功,返回非 0 表示失败,并设置 errno 来指示错误原因。
常见用法:用于在文件中移动文件指针,读取文件中的特定位置的数据,以便进行随机位置访问。
*/
fgetc和fputc
#include <stdio.h>
int main(int argc, char *argv[])
{
FILE * fp = fopen("1.txt","r"); // 打开 1.txt 文件,r 代表只读
if(NULL == fp)
{
printf("fopen error\n");
return 1;
}
while(1)
{
int c = fgetc(fp); // 将读到的字符存到 c
if(EOF == c) // 判断结束
{
break;
}
printf("%c\n",c);
}
fclose(fp); // 关闭文件
return 0;
}
#include <stdio.h>
int main(int argc, char *argv[])
{
FILE * fp = fopen("1.txt","w");
if(NULL == fp)
{
printf("fopen error\n");
return 1;
}
int ret = fputc('h',fp); //将‘h’写入fp
if(-1 == ret)
{
printf("fputc error\n");
return 1;
}
fputc('e',fp);
fputc('l',fp);
fputc('l',fp);
fputc('o',fp);
fclose(fp);
return 0;
}
fgets和fputs
#include <stdio.h>
int main(int argc, char *argv[])
{
if(argc<3)
{
printf("usage:./a.out srcfile dstfile\n");
return 1;
}
FILE * src_fp = fopen(argv[1],"r");
FILE * dst_fp = fopen(argv[2],"w");
if(NULL == src_fp ||NULL == dst_fp)
{
printf("fopen error\n");
return 1;
}
char buf[1024]={0};
while(1)
{
char* s = fgets(buf,sizeof(buf),src_fp); //将src_fp中的东西写入buf
if(NULL == s)
{
break;
}
fputs(buf,dst_fp); //向dst中写入buf里的东西
}
fclose(dst_fp);
fclose(src_fp);
return 0;
}
文件io
特性:
1.没有缓存区
2.操作对象不在是流(FILE*),而是文件描述符(fd) int 0-1023
很小的非负的整数 int 0~1023
内核每打开一个文件就会获得一个文件 描述符
每个程序在启动的时候操作系统默认为其打开 三个描述符与流对象匹配:
0 ==> STDIN_FILENO ==> stdin
1 ==>STDOUT_FILENO == >stdout
2 ==>STDERR_FILENO ==> stderr
注:因为0,1,2被这三个描述符占了,所以我们写的文件一般从3开始。
open() 函数:
#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);
参数:
pathname:文件路径名。
flags:文件打开标志,可以取以下值的组合:
O_RDONLY:只读模式打开文件。
O_WRONLY:只写模式打开文件。
O_RDWR:读写模式打开文件。
O_CREAT:如果文件不存在,则创建文件。
O_TRUNC:如果文件存在且为只写或读写打开,则将文件长度截断为 0,即清空文件内 容。
O_APPEND:在文件末尾追加数据。
等等,还有其他的标志,可根据需要组合使用。
mode:文件权限,仅在 O_CREAT 标志被设置时有效,通常为八进制数。
返回值:返回一个新的文件描述符(非负整数),如果打开文件失败,返回 -1,并设置 errno 来指示错误原因。
常见用法:用于打开文件,获取文件描述符,以便后续的文件操作。
close()
#include <unistd.h>
int close(int fd);
参数:
fd:文件描述符,需要关闭的文件。
返回值:返回 0 表示成功关闭文件,返回 -1 表示失败,并设置 errno 来指示错误原因。
常见用法:用于关闭一个已经打开的文件。
read() 函数:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数:
fd:文件描述符。
buf:用于存放读取数据的缓冲区。
count:要读取的字节数。
返回值:返回实际读取的字节数,如果到达文件末尾返回 0,出错返回 -1,并设置 errno 来指示错误原因。
常见用法:用于从文件中读取数据。
write() 函数:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数:
fd:文件描述符。
buf:要写入的数据。
count:要写入的字节数。
返回值:返回实际写入的字节数,出错返回 -1,并设置 errno 来指示错误原因。
常见用法:用于向文件中写入数据。
lseek() 函数:
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
参数:
fd:文件描述符。
offset:偏移量。
whence:偏移起始位置,可以取以下值:
SEEK_SET:文件开头。
SEEK_CUR:当前文件位置。
SEEK_END:文件末尾。
返回值:返回新的文件偏移量(以字节为单位),出错返回 -1,并设置 errno 来指示错误原因。
常见用法:用于在文件中移动文件指针。
-
fcntl() 函数:
#include <fcntl.h> int fcntl(int fd, int cmd, ... /* arg */ ); 参数: fd:文件描述符。 cmd:操作指令,可以取以下值: F_SETFD:设置文件描述符标志。 F_GETFD:获取文件描述符标志。 F_SETFL:设置文件状态标志。 F_GETFL:获取文件状态标志。 F_DUPFD:复制一个文件描述符,指向相同的文件。 返回值:根据操作指令的不同有不同的含义,出错返回 -1,并设置 errno 来指示错误原因。 常见用法:用于对文件描述符进行各种控制操作,提供了对文件描述符的附加功能和细粒度控制。它允许你设置或获取文件描述符的各种状态和属性。 如果 cmd 参数需要额外的参数,则需要将其作为可变参数传递给 fcntl() 函数。 等等,还有其他的操作指令,具体可以查阅相关文档
区别
- 文件IO不带缓冲,执行效率低;标准IO带缓冲,执行效率高。
缓冲:理解为一块内存,标准IO可以写完数据后找到合适时机一起放入磁盘,而文件IO是每写一个数据都会放一次磁盘,分为全缓冲(大小4096,向文件写入时):写满,程序结束,主动刷新(fflush),执行.行缓冲(1024,向终端输出时用):写满,结束,主动刷新(fflush),换行符,执行。无缓冲:每次执行。程序正常结束也会刷新缓冲区)
文件IO属于系统调用,可以访问不同类型的文件,如普通文件,设备文件(open时不能create),管道文件,套接字文件等;标准IO属于C库只能访问普通文件。
目录操作
一般流程:
1、打开目标目录
2、读取目录,,
3、关闭目录
opendir
DIR *opendir(const char *name);
功能:打开一个目录获得一个目录流指针
参数:
name :目录名
返回值:成功返回目录流指针,失败返回NULL
readdir
struct dirent *readdir(DIR *dirp);
功能:从目录流中读取文件信息并将保存信息的结构体
参数:
dirp:目录流指针
返回值:包含文件信息的结构体,出错或者读到目录流末尾返回NULL
closedir
int closedir(DIR *dirp);
功能:关闭之前已经打开的目录流对象
参数:opendir的返回结果中目录流对象
返回值:成功 0,失败 -1;
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <dirent.h>
int main(int argc, char *argv[])
{
DIR * dir = opendir("./"); // 打开当前目录
if(NULL == dir)
{
fprintf(stderr,"opendir errpr\n");
return 1;
}
while(1)
{
struct dirent * info = readdir(dir); // 读取目录中的文件
if(NULL == info)
{
break;
}
switch(info->d_type) // 判断文件类型
{
case DT_DIR:
printf("目录文件 ");
break;
case DT_REG:
printf("普通文件 ");
break;
case DT_UNKNOWN:
printf("其他文件 ");
break;
default:
printf("未知文件 ");
}
printf("%s\n",info->d_name);
}
closedir(dir);
return 0;
}
软硬链接
软链接是一个特殊的文件类型,它包含了 对另一个文件或目录的引用(即路径名)。
当你通过软链接访问文件或目录时,系统会根据软链接中的路径去找到并访问实际的目标文件或目录。
使用场景
快捷方式
大家平时都使用过快捷方式吧,就比如我们打开一个文件,该文件实际存储在我们的 D盘
,但是我们却可以在桌面打开使用他
共享文件
在多用户环境中,可能需要在多个用户之间共享文件。通过创建指向共享文件的软链接,每个用户都可以在自己的工作区域中访问这些文件,而无需复制它们到各自的目录中。
跨文件系统访问
由于软链接可以跨文件系统创建,因此它们可以用于 在不同文件系统之间建立文件或目录的引用关系。这对于需要在不同分区或不同存储设备之间共享数据的场景特别有用。
环境变量和路径设置
在配置环境变量或系统路径时,可以使用软链接来指向特定的目录或文件。这有助于简化配置过程,并确保系统能够正确地找到所需的资源。
硬链接的概念
硬链接用于文件和目录链接的一种机制。它 允许一个文件在文件系统中拥有多个文件名(或路径)
。这些文件名指向同一个文件数据在磁盘上的物理位置。
区别
当我们创建软链接时,我们实实在在的创建了个文件,该文件存储的是我们目标文件的路径。
当我们创建硬链接时,我们没有创建任何文件,硬链接和目标文件指向磁盘上同一个文件,我们只是新创建了 一个文件名 和 inode_number
的映射关系,存入目录中。
硬链接与源文件相互独立。删除源文件不会影响硬链接的使用,只要存在至少一个硬链接指向文件的 inode_number
,文件的数据就会存在。
软链接依赖于源文件。如果源文件被删除或移动,软链接将指向一个无效的路径,导致无法通过软链接访问到目标文件。此时,软链接会失效。
60、进程的空间分布
c语言中进程内存空间分段(堆区、栈区、全局静态区、代码区、文字常量区)
进程的内存分配包含:
- 堆 heap
- 栈 stark
- 全局静态区(.bss、.data)
- 文字常量区(.rodata)
- 代码段