目录
一、C
1.C语言和C++的区别
C语言面向过程,关注函数的调用和数据的处理,C++继承了 C 语言并增加了面向对象的特性:类、对象、封装、继承、多态。C语言注重底层的数据处理与硬件的直接交互,适用于嵌入式开发和对性能要求较高的场景。
2.声明和定义
- 变量定义:用于为变量分配存储空间,为变量指定初始值。程序中,变量有且仅有一个定义。
- 变量声明:用于向程序表明变量的类型和名字。
定义也是声明:当定义变量时我们声明了它的类型和名字。extern声明不是定义:通过使用extern关键字声明变量名而不定义它。变量在使用前就要被定义或者声明。在一个程序中,变量只能定义一次,却可以声明多次。定义分配存储空间,而声明不会。
- 当extern只声明没有初始化时:是告诉编译器变量在其他地方定义了;
- 当extern声明且有初始化式时,就被当作定义去理解,即使前面加了extern。
3.const和define关键字
define是预编译指令,const是普通变量的定义,define定义的宏是在预处理阶段展开的,而const定义的只读变量是在编译运行阶段使用的。
const定义的是变量,而define定义的是常量。define定义的宏在编译后就不存在了,它不占用内存,因为它不是变量,系统只会给变量分配内存。
const定义的常变量本质上仍然是一个变量,具有变量的基本属性,有类型、占用存储单元。const定义的是变量,而宏定义的是常量,所以const定义的对象有数据类型,而宏定义的对象没有数据类型。所以编译器可以对前者进行类型安全检查,而对后者只是机械地进行字符替换,没有类型安全检查。这样就很容易出问题,即“边际问题”。
const关键字
1.cosnt修饰全局变量:此时全局变量只能使用,不能修改;如果直接修改全局变量,会编译报错;如果用全局变量的地址来修改值,运行时程序异常结束。
2.const修饰局部变量:不能直接通过局部变量进行修改值,会编译报错;可以通过局部变量的地址修改局部变量的值。
3.const修饰指针:
(1)int i = 1;int* const p = &i;const修饰了指针p,p无法指向其他地址,但是p所在地址的变量值,也就是*p可以修改。总结:const p,p无法赋值,*p可以赋值,i可以赋值
(2)int i= 1;const int* p = &i;或者int const *p = &i;const修饰了*p,*p就是i,所以*p不能修改,i不能修改,但是p可以修改,也就是p可以再指向其他地址,总结:const *p,p可以赋其他地址的值,*p和i不可以赋值
(3)const int* const p;*p和p都被const修饰了,所以,指针不能变,指针所指的变量也不能变。
4.const修饰数组:const int a[]
表示数组中的每个元素都是const int,无法修改,所以必须在初始化的时候进行赋值,否则数组创建之后就无法赋值了。
define关键字:不需要分配空间,在预编译时会直接替换为相应的值或表达式
1.define定义标识符:理解为替换
#define name stuff——用name替换stuff
#define MAX 100 //定义一个全局变量MAX值为100
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
2.define定义宏:理解为带参数的替换,类似函数,但是要区分宏和函数
#define name( parament-list ) stuff——
#define SQUARE1(X) X*X
#define SQUARE2(X) (X)*(X)
SQUARE1(1+4) = 1+4*1+4 = 9
SQUARE2(1+4)=(1+4)*(1+4)=25
宏和函数对比
宏 | 函数 | |
# 和 ## 区别
#和##是用来操作宏定义中参数的特殊符号。
1. #(字符串化运算符):将参数转换为一个字符串常量;
#define STR(x) #x
STR(hello world) // 将会被转换为 "hello world"
2. ##(连接运算符):用于将两个参数连接成一个符号。
#define CONCAT(x, y) x##y
int CONCAT(a, b) = 10;
// 上面这一行宏定义等价于以下代码
// int ab = 10;
4.extern关键字
简单说extern关键字可以在多个文件中共享变量和函数,方便了模块化编程和代码重用。在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。
用于声明一个变量或函数具有外部链接性(external linkage),即这些变量或函数可以被其他文件访问。用于表示一个变量或函数在其他文件或模块已经定义,也可以在当前文件或模块使用。
用于声明全局变量或函数:在一个文件中声明一个全局变量或函数为extern,表示该变量或函数在其他文件中定义,这样就可以在当前文件中使用该变量或函数。
用于引用全局变量或函数:在当前文件中引用其他文件中已经定义的全局变量或函数。
5.static关键字
一、静态局部变量:本质上是改变存储位置,表现为延长了生命周期。
局部变量 | 静态局部变量 | |
生命周期 | 随着函数的调用而开始,随着函数的结束而结束 | 从定义开始,持续到程序运行结束 |
作用域 | 在定义它的函数内部 | 限定在定义它的函数内部,但它可以在函数调用结束后保留其值,供下一次该函数调用时使用。 |
存储位置 | 栈 | 静态区(全局区) |
初始值 | 定义时不会被自动初始化,在使用之前需要手动为其赋值。 | 在定义时会被自动初始化为0或NULL。 |
静态局部变量使用场景?
- 保持状态信息:当需要在函数调用之间保留某些状态或信息时,静态局部变量非常有用。例如,可以使用静态局部变量来统计函数被调用的次数、保存上一次调用的结果等。
- 限制作用域:静态局部变量的作用域仅限于定义它的函数内部,不会对其他函数可见。这样可以避免全局命名空间的污染,同时允许函数内部使用一个独立的状态变量。
- 线程安全性:多线程环境下,静态局部变量是线程安全的,因为每个线程独立拥有一份副本,互不干扰。
二、静态全局变量:本质上是改变了链接属性
全局变量 | 静态全局变量 | |
作用域 | 整个源文件,可以被该源文件中的所有函数访问 | 作用域也是整个源文件,但只能在定义它的源文件中访问,其他源文件无法访问。 |
生命周期 | 从程序开始执行到程序结束,即整个程序的运行期间都存在。 | 从程序开始执行到程序结束,但它仅限于定义它的源文件内部可见,其他源文件无法访问。 |
链接属性 | 外部链接 | 内部链接 |
可见性 | 对整个程序是可见的,可以在不同的源文件中共享数据 | 对于其他源文件是不可见的,只在定义它的源文件内部起作用。 |
静态全局变量使用场景?
- 实现模块内的变量共享:在一个源文件中需要多个函数之间共享数据,但不希望这些数据被其他文件访问,可以使用静态全局变量。静态全局变量的作用域仅限于定义它的源文件,可以确保数据的私有性和封装性。
- 防止命名冲突和污染:全局变量容易引起命名冲突和全局命名空间的污染。使用静态全局变量可以将变量的作用域限制在当前源文件内部,避免与其他文件中的全局变量发生冲突。
- 函数局部变量的共享:在某些情况下,可能需要在不同的函数调用之间共享局部变量的值,可以使用静态全局变量来实现。将变量定义为静态全局变量后,不管函数何时被调用,静态全局变量的值都会被保留下来,可以在函数调用之间进行共享。
三、静态函数:本质上是改变了链接属性和作用域
全局函数 | 静态函数 | |
链接属性 | 外部链接 | 内部链接 |
作用域 | 可以被其他源文件引用。 | 只能在当前源文件中调用该静态函数,其他源文件无法直接调用。 |
静态函数使用场景?
- 辅助函数:将一些只在当前源文件内部使用的辅助函数声明为静态函数,可以避免这些函数被其他源文件引用,减少全局命名空间的污染。
- 提供模块内部接口:在模块化编程中,可以将模块内部的一些功能函数声明为静态函数,通过一个公开的接口函数来调用这些静态函数。这样,模块外部只能通过接口函数来访问模块内部的功能,实现了信息隐藏和封装。
6.volatile关键字 ——易变的
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改。volatile 提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有 volatile 关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。所以遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
volatile关键词影响编译器编译的结果
7.new和malloc,delete和free
new | malloc | |
类型 | 是关键字,需要编译器支持 | 是库函数,需要头文件支持。 |
申请大小 | 申请内存无需指定内存大小,编译器会根据类型信息自行计算。 | 由我们计算需要申请的字节数,需要显式指出所需内存的尺寸。 |
返回类型 | 申请什么类型就返回什么类型并且可以初始化 | 只管申请空间,返回void*需要程序员显示的转换成要用的类型,并且无法初始化 |
释放 | delete | free |
申请失败 | 抛出bad_ alloc异常 | 返回NULL |
重载 | 可以 | 不可以 |
内存区域 | 自由存储区 | 堆区 |
8.如何防止重复引用头文件
1.使用条件编译:
#ifndef _TEST_
#include _TEST_
... ...
#endif
2.根据编译器的支持使用:#pragma once
9.C语言编译过程
10.内存分配
C语言:内存分配---栈区、堆区、全局区、常量区和代码区https://blog.youkuaiyun.com/MQ0522/article/details/114823770
11.内存泄漏
内存泄漏指的是在程序运行过程中,动态分配的内存没有被正确释放或者释放时机不当,导致这部分内存无法再被程序所使用,从而造成内存的浪费和不断增加的内存占用。
导致内存泄漏的情况:
- 动态内存分配未释放:通过malloc()、calloc()、realloc()等函数分配的内存,在使用完毕后应该通过
free()
函数进行释放。如果忘记释放这块内存,就会造成内存泄漏。 - 丢失指针:如果一个指针指向了一块动态分配的内存,然后该指针被重新赋值或者丢失,导致之前分配的内存没有释放,也会发生内存泄漏。
- 循环引用:在使用垃圾回收机制不可用的语言中,如果存在循环引用的数据结构(如链表、树等),并且没有正确断开这些引用关系,就会导致无法回收的内存泄漏。
避免内存泄漏的措施:
- 在动态分配内存后,确保在不再需要时及时释放内存,遵循申请与释放的对应原则。
- 注意检查指针的赋值和生命周期,确保指针指向正确的内存区域,并在不再使用时及时将其置为NULL。
- 对于循环引用的情况,可以手动断开引用关系,或者使用弱引用、弱代指针等机制来解决。
- 使用内存管理工具和调试器来检测和跟踪内存泄漏问题,如Valgrind等。
内存泄漏会导致的问题:
- 内存耗尽:重复发生内存泄漏会导致系统的可用内存逐渐减少,最终可能耗尽系统的内存资源,使程序崩溃或系统变得不稳定。
- 性能下降:内存泄漏会使程序占用的内存逐渐增加,导致程序运行速度变慢,甚至引起内存碎片化,影响程序的性能。
- 资源泄漏:如果内存泄漏发生在使用其他资源的上下文中(如文件句柄、网络连接等),可能会导致资源的泄漏,使得这些资源无法被释放,进而影响系统的可用性。
12.指针和引用的区别
指针 | 引用 | |
概念 | C语言中就存在的概念 | C++中才有的概念 |
定义和声明 | int* ptr; | int &ref=num; |
初始化 | 可以先定义,再初始化,可以重复赋值。 | 必须定义时初始化,一旦被初始化,就不能改变 |
空值 | 可以指向空地址,表示未指向任何对象 | 不可为NULL,必须始终指向一个对象 |
多级 | 可以多级 | 只能一级 |
内存管理 | 需要手动管理,申请和释放 | 不需要管理内存,只是原变量的一个别名 |
优点 | 更灵活,可以指向空地址和动态分配的内存 | 更简洁、直观,无需操作符,并且更安全,不会出现空指针问题 |
13.char和字符串
首先char是C语言中的字符类型,表示单个字符,占一个字节,用单引号表示;
另外,C语言中没有字符串类型,所谓字符串可以理解为字符的合集或者字符的数组,可以用双引号表示也可以用数组表示,字符串可以用数组接收,也可以用指针接收。
char arr[20] = "Hello, World!";
char* str = "Hello, World!";
char arr[ ] = {'b','i','t'};
14.#include<>和#include" "
< >:直接去库函数头文件所在的目录下查找;
" " :先在源文件所在的目录下查找,如果找不到,则在库函数的头文件目录下查找。
15.链表
//链表
#include <stdio.h>
#include <stdlib.h>
typedef struct student
{
//数据域
int num;//学号
int score;//成绩
char name[20];//姓名
//指针域
struct student* next;
}STU;
//链表的创建
void link_creat_head(STU** p_head, STU* p_new)
{
STU* p_mov = *p_head;
if (*p_head == NULL)//新节点来时,链表为空,新节点就作为头节点
{
*p_head = p_new;
p_new->next = NULL;
}
else//新节点来的时候链表不为空,把新节点放到链表尾部
{//既然是放到尾部,所以需要知道尾在哪?
while (p_mov->next != NULL)//p_mov的next为NULL时,p_mov就是尾
{
p_mov = p_mov->next;//如果不为NULL,就一直往后找
}
p_mov->next = p_new;
p_new->next = NULL;
}
}
//链表的遍历
void link_print(STU* head)
{
STU* p_mov;//定义一个新的指针
p_mov = head;//来保存链表头指针,防止遍历过程中链表被修改
while (p_mov!=NULL)
{
printf("num = %d score = %d name = %s\n", p_mov->num, p_mov->score, p_mov->name);
p_mov = p_mov->next;//指针后移
}
}
//链表的释放
//用指针p遍历链表,每次把p赋给q,释放q即可
void link_free(STU **p_head)
{
STU *pb = *p_head;//定义一个指针,保存头节点的地址
while (*p_head!=NULL)//头节点不为空就继续释放
{
pb = *p_head;
*p_head = (*p_head)->next;
free(pb);
}
}
int main()
{
STU* head = NULL;
STU* p_new = NULL;
int num, i;
printf("请输入链表的初始个数:\n");
scanf("%d", &num);
for (size_t i = 0; i < num; i++)
{
p_new = (STU*)malloc(sizeof(STU));
printf("请输入学号,分数和姓名:\n");
scanf("%d %d %s", &p_new->num, &p_new->score, &p_new->name);
link_creat_head(&head, p_new);//将新节点p_new加入链表中
}
link_print(head);
}
二、操作系统
1.进程与线程
2.并发与并行
并发:指的是宏观上两个程序在同时运行,比如单核CPU上的多任务,但是微观上是两个程序在交错执行;并发不能提高计算机的性能,只能提高效率
并行:是物理意义是的多个任务同时运行,比如多核CPU处理多任务,实际上是每个程序都在一个核上运行且互不影响;并行真正提高了计算机效率
3.用户态和内核态
之所以有区分是为了避免操作系统和关键数据被用户程序破坏
内核态:是操作系统管理程序执行时的状态,可以执行包含特权指令在内的一切指令,可以访问系统内所有的内存空间;
用户态:是用户程序执行时的状态,不能访问特权指令,只能访问用户地址空间;
从用户态切换到内核态的方法有三种:系统调用、异常和外部中断。
- 系统调用是操作系统的最小功能单位,是操作系统提供的用户接口,系统调用本身是一种软中断。
- 异常,也叫做内中断,是由错误引起的,如文件损坏、缺页故障等。
- 外部中断,是通过两根信号线来通知处理器外设的状态变化,是硬中断。
4.进程间通信方法
首先是每个进程都有不同的用户地址空间,进程间的通信就是不同进程之间的信息交换和数据共享。进程间通信需要通过内核,在内核中开辟一块缓冲区,进程A把数据从自己的用户空间拷贝到内核缓冲区,进程B可以从内核缓冲区把数据读走,这样就实现了进程间的通信。所以进程间通信的本质就是进程之间看到一份公共资源,所以提供这份公共资源的方式和提供者不同造成了通信方式的不同。
1.管道通信:作用于有血缘关系的进程之间,实现数据传递。是一种半双工的通信方式,数据只能在一个方向上流动。管道中的数据一旦被读走便不存在。父进程通过管道写入数据后,在子进程中可以通过管道读取数据,实现了进程间的通信。
2.共享内存实现通信:是最快的通信方式。通过使多个进程共同访问同一块内存空间,不同进程之间可以及时的看到对方进程对共享内存中数据的更新。(实现的方式是:通过映射同一块物理内存到不同进程的虚拟内存空间中,各个进程可以直接对共享内存读写操作,无需进行额外的数据拷贝。)
3.消息队列:
CPU进程调度的策略
是操作系统用来决定将CPU时间片分配给哪个进程
1.先来先服务:
2.短作业优先:
3.优先级调度:
4.时间片轮转:
5.多级反馈队列调度:
6.最高响应比优先:
7.最短剩余时间优先:
三、计算机网络
1.五层和七层
2.三次握手
- 第一次握手:客户端将标志位SYN置为1,随机产生一个值序列号seq=x,并将该数据包发送给服务端,客户端 进入syn_sent状态,等待服务端确认。
- 第二次握手:服务端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务端将标志位SYN和 ACK都置为1,ack=x+1,随机产生一个值seq=y,并将该数据包发送给客户端以确认连接请求,服务端进入syn_rcvd状态。
- 第三次握手:客户端收到确认后检查,如果正确则将标志位ACK为1,ack=y+1,并将该数据包发送给服务端,服务端进行检查如果正确则连接建立成功,客户端和服务端进入established状态,完成三次握手,随后客户端和服务端之间可以开始传输 数据了
3.四次挥手
- 第一次挥手:客户端发送一个FIN,用来关闭客户端到服务端的数据传送,客户端进入fin_wait_1状态。
- 第二次挥手:服务端收到FIN后,发送一个ACK给客户端,确认序号为收到序号+1,服务端进入Close_wait状态。此时TCP连接处于半关闭状态,即客户端已经没有要发送的数据了,但服务端若发送数据,则客户端仍要接收。
- 第三次挥手:服务端发送一个FIN,用来关闭服务端到客户端的数据传送,服务端进入Last_ack状态。
- 第四次挥手:客户端收到FIN后,客户端进入Time_wait状态,接着发送一个ACK给服务端,确认后,服务端进入Closed状态,完成四次挥手。
4.get与post区别
首先都是HTTP协议中请求数据的方式。
GET | POST | |
产生数据包 | 1个TCP数据包 | 2个TCP数据包 |
数据大小 | 限制为 URL 的最大长度,因此传输数据量较小。 | 没有这种限制,可以传输更大量的数据。 |
工作流程 | 把http header和data一并发送出去,服务器响应200 | 浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据) |
适用于 | 传输少量数据,且数据对安全性要求不高的情况 | 传输大量数据,或者数据对安全性要求较高,如用户登录、密码修改等场景。 |
四、通信协议
1.MODBUS-RTU
Modbus是应用层软件通讯协议
- 物理层:MODBUS-RTU使用串行通信方式,在物理层上使用RS-232、RS-485等标准串口进行数据传输。其中,RS-485多用于长距离通信和多设备共享一个总线的情况。
- 数据帧结构:MODBUS-RTU使用了简单而紧凑的数据帧结构。每个数据帧包含了地址、功能码、数据以及错误检测等字段。通常情况下,从站会接收主站发送的请求并做出响应。
- 功能码:MODBUS-RTU定义了一系列功能码,用于表示不同的操作类型,例如读取寄存器、写入寄存器等。主站通过发送不同功能码的请求来指示从站执行相应的操作。
- 数据格式:MODBUS-RTU使用16进制表示数据,并采用大端字节序(高字节在前)进行传输。数据可以是开关量、模拟量等不同类型的数据。
- 错误检测:MODBUS-RTU使用循环冗余校验(CRC)来检测数据传输中的错误,保证数据的可靠性。接收方会对接收到的数据进行CRC校验,判断是否存在错误。
2.RS485和RS232
RS485 | RS232 | |
传输规范 | 半双工同步协议 | 异步串口协议 |
工作模式 | 半双工 | 全双工 |
信号线 | 一对双绞线:A、B | RXD、TXD、GND三条线 |
传输距离 | 1200米 | 15米 |
传输速率 | 10Mbps | 20Kbps |
电气电平值 | 逻辑"1"以两线间的电压差为+(2-6)V表示 逻辑"0"以两线间的电压差为 -(2-6)V表示 | 负逻辑关系 即:逻辑"1",-(5-15)V;逻辑"0 " +(5- 15)V |
通信能力 | 允许连接多达128个收发器 | 只允许一对一通信。 |
抗干扰性 | 采用平衡驱动器和差分接收器的组合,抗噪声干扰性好。 | 使用一根信号线和一根信号返回线而构成共地的传输形式,这种共地传输容易产生共模干扰。 |
五、Linux
1.Linux系统中实现多并发