零碎的编程基础学习笔记

纯随手笔记,没有章法没有顺序。几乎没有可读性。


学习笔记

存储类别

const:保证指针只想的内存区域不可修改

static:如果修饰块作用域、函数原型作用域、函数作用域则赋予变量静态存储期,不改变变量的作用域,变量成为静态变量。若修饰全局变量,则该变量的作用域缩小为当前文件(内部链接),可以想象成私有化。

自动变量:auto修饰,起强调作用。特点:自动存储期、块作用域、无链接,不会自动初始化

寄存器变量:使用register建议计算机将变量放入寄存器。其修饰的变量不可获取地址

静态变量:在程序运行期间一直在内存中存在,例如函数内变量用static修饰,下次调用函数时这个变量的值还是上次调用该函数时的值。具有静态存储期的变量会被自动初始化,这点不同于自动变量。

外部链接:全局变量,若指出该变量是外部变量(来此外部),用extern声明。(例如在其他文件中定义的全局变量,在本文件中用extern再次声明,第一次声明叫定义式声明,第二次声明叫引用式声明。只有在定义式声明中才能初始化变量)。

函数也有外部函数(默认)、静态函数、内联函数(c99)。

静态函数使用static修饰,外部函数在同一程序其他文件中也可以extern修饰引用声明表明当前文件中所使用的函数定义在别处

misc

fgets会保留换行,gets_s不保留换行,但需要实现错误处理函数,较为麻烦。总体而言fgets更好

const修饰的内容是不可变的,比如const int a[]那就是a的数组内容不可变,int const a*那就是a的指向不能变

ctype.h是C标准函数库中的头文件,定义了一批C语言字符分类函数,用于测试字符是否属于特定的字符类别,如字母字符、控制字符等等。既支持单字节字符,也支持宽字符。

宽字符是宽度始终为16 位的多语言字符代码。 字符常量的类型是 char ;对于宽字符,该类型是 wchar_t 。 由于宽字符始终具有固定大小,因此使用宽字符集可以简化使用国际字符集进行的编程。 宽字符串文本 L”hello” 将成为类型为 wchar_t 的六个整数的数组。

  1. 作用域:块,函数作用域(goto),函数原型作用域,文件作用域
  2. 链接:外部链接,内部链接,无链接
    在文件中直接定义的全局变量是外部链接,其他文件也可以访问得到(一般称之为全局作用域),而如果是static定义的全局变量则是内部链接(一般称之为文件作用域)
  3. 存储期:静态存储期,线程存储期,自动存储期,动态分配存储期
    注意:全局变量的static关键字声明的是变量或函数的链接属性!而局部变量的static关键字则是声明静态存储期,作用域还是上层块,但是存放在静态内存区。全局变量默认都是静态的,都是存放在静态存储区中的
  4. 存储类别:自动(关键字auto显示声明,自动变量不会自动初始化!),寄存器(关键字register),静态块作用域(块作用域的静态变量,其实就是函数内部声明的静态变量,放在静态内存里面,不会随函数结束而消失),静态外部链接(外部链接的静态变量,也就是普通的全局变量,全局变量的初始化只能是常量表达式,不能带变量,这个很容易理解,如果需要强调引用外部定义的全局变量,可以用extern关键字),静态内部链接(内部链接的静态变量,也就是用static修饰的全局变量,静态存储期)

函数的关键字: inline,static(限定作用域),extern

一定要注意size_t类型的参与运算!这是一个无符号整型,不要拿来和有符号整型做运算!相关函数malloc,sizeof,strlen等等

限定符:const,volatile(允许其他程序修改这个值,防止编译器优化),restrict(c99允许编译器优化。只能用于指针,该指针是访问这片地址的唯一且初始的方式),_Atomic(c11 ,跟stdatomic.h里面的函数一起用 比如 原子操作_Atomic int hogs; atomic_store(&hogs,12))

二进制方式打开的文件可以看见\r\n^Z,而使用文本方式打开的文件可以看到\r,其他的都看不到了。

c11特性:fopen的x模式,独占打开;任何w模式都会截断文件内容

c99:stdbool.h 新增_Bool类型,占1bit的位字段

对齐特性c11:_Alignof(float)查询float的对齐要求,_Alignas(8) 指定对齐要求

在宏定义中用#号来做字符串化,##叫做预处理粘合剂

c11泛型选择表达式_Generic(x,int:0,float:1,default:3)

_Noreturn修饰函数,说明该函数不会将控制返回给上层函数

assert.h中的assert()函数可以在运行时断言,_Static_assert(a==b,”error msg”)是在编译时就完成断言

随机数

linux中unistd.h提供sleep(),time.h提供time(),stdlib.h提供srand()和rand。rand里面有使用静态变量,所以只需要一次下种,后续调用rand都不用下种。

数据结构与算法

红黑树

红黑树是一种自平衡的二叉查找树,具有良好的最坏情况运行时间,能够在对数时间内完成查找、插入和删除操作。红黑树的每个节点都有一个颜色属性(红色或黑色),通过颜色约束来保持树的平衡。

排序算法

  • 锦标赛排序:通过构建锦标赛树(类似于堆)来选择最大或最小元素,适用于外部排序。
  • 桶排序:将数据分到有限数量的桶中,每个桶再分别排序,适用于数据分布均匀的情况。

进程管理

多进程管理

在C语言中,常用的多进程管理函数包括:

  • fork():创建子进程。
  • execve():在子进程中执行新的程序。
  • wait()waitpid():等待子进程结束。
  • exit()_exit():终止进程。

当一个进程通过fork()创建子进程后,如果父进程正常退出,子进程会被init进程收养,从而避免僵尸进程的产生。

进程通信

管道

  • 无名管道:用于父子进程之间的通信。
  • 有名管道:通过mkfifo创建,可以像文件一样使用readwrite进行读写。
1
2
3
4
5
open(const char *path, O_RDONLY); // 只读
open(const char *path, O_RDONLY | O_NONBLOCK); // 非阻塞只读
open(const char *path, O_WRONLY); // 只写
open(const char *path, O_WRONLY | O_NONBLOCK); // 非阻塞只写
//这些宏定义在fcntl.h

数据表示

负数的表示

负数在计算机中通常以补码形式表示,补码等于二进制反码加1。

浮点数标准

  • 单精度浮点数:1位符号,8位指数,23位尾数,精度为7-8位。
  • 双精度浮点数:1位符号,11位指数,52位尾数,精度为15位。

中间的指数部分8位,需要映射,比如8位存储的数值是128,那就映射位128-127=1;这样可以避免在中间的8位上直接存储负数

掩码操作

  • 设置位:使用或操作。
  • 清空位:将掩码取反后与操作。
  • 切换位:使用异或操作。
  • 检查位:使用与操作。

结构体位字段

结构体中的位字段允许对数据进行更精细化的操作。

对齐指定

使用_Alignas_Alignof来指定对齐方式。

1
2
#include <stdalign.h>
char _Alignas(double) cz;

GCC扩展

__attribute__机制

GCC的__attribute__可以修饰函数和变量,常用的修饰符包括:

  • 函数修饰符noreturnformatconstconstructordestructor
  • 变量修饰符alignedpacked
1
__attribute__(())

信号通信

信号处理

  • 发送信号:使用killraisesignal.h
  • 接受信号:使用alarmpauseunistd.h
1
2
void (*signal(int sig, void (*func)(int)))(int);
// 声明的函数是signal,signal函数有两个传参,一个int,一个函数指针。他的返回值是带一个int形参的返回值是void的函数,这个函数就是func会直接执行,如果中间执行有问题signal则会返回SIG_ERR用于验错

这个通信的使用方式就是:1. 定义信号处理函数 2. 注册信号处理函数if(signal(SIGALRM,sighander)==SIG_ERR)pass()else{pass()} 3. 等待信号处理。信号列表可以翻man手册,alarm信号可以用来设置超时什么的功能。

信号处理的使用步骤:

  1. 定义信号处理函数。
  2. 注册信号处理函数。
  3. 等待信号处理。

文件操作

IO还可以选择级别:有底层IO和标准IO可选。在大多数情况下都是使用标准IO,因为不能保证所有操作系统都使用相同的底层IO

文件打开模式

  • r+:读写模式。
  • w+:截断读写模式(清空原有内容)。
  • x:要求文件存在且未被占用(C11特性)。

如果要打开的文件是纯linux的文件,则二进制打开方式和文本打开方式没有区别

标准IO与底层IO

  • 标准IO:使用fopen()fclose()fread()fwrite()等函数。
  • 底层IO:使用open()read()write()等函数。
1
2
3
4
5
6
7
8
9
fopen()、getc()、putc()、exit()、fclose()

fprintf()、fscanf()、fgets()、fputs()

rewind()、fseek()、ftell()、fflush()

fgetpos()、fsetpos()、feof()、ferror()

ungetc()、setvbuf()、fread()、fwrite()

文件流与缓冲区

  • 文件流:C语言将文件视为流,可以使用setvbuf()为流指定缓冲区。
  • 缓冲区修改:修改缓冲区内容后使用fflush()刷新缓冲区。

getc()和putc()函数需要参数指定来源输入来源,而getchar()和putchar()是默认的标准输入stdin,getc()如果读取到EOF则说明到达文件尾部

fopen()与fclose()要成对使用。fclose()也存在返回值,如果文件关闭成功则为0。在编程规范中需要将该返回值考虑进函数设计当中。

rewind()函数的功能是移动指标到文件开头,fseek(fp,offset_l,SEEK_END),ftell(fp)功能是返回当前指针所在位置(适用于以二进制方式打开的文件)。fflush()是刷新缓冲区

dgetpos()和fsetpos()函数:不用long这个数量级不够大的数据类型,而使用了新类型fpos_t,使用并不复杂,可以直接看说明书

ungetc()可以将字符放回输入缓冲区。典型的使用场景:要读取某个字符前的所有内容,而不破坏缓冲区的剩下内容,在读到指定字符后将指定字符放回。

setvbuf()函数 指定或创建一个缓冲区

fread()和fwrite()函数会使用二进制形式存储和读取数据。也就是数据序列化与反序列化。返回值是成功的数据块数量

feof()和ferror()函数用于区分到达文件结尾的eof还是读取错误的eof

C语言把东西看作流,一个文件可以是一个流,可以用setvbuf为这个流指定一个缓冲区,这个流可以用来当作输出流,也可以用来当作输出流。

常用的流还有:标准输出流stdin,通常来源于键盘,标准输出流和错误输出流,通常是显示器。流与流的对接就像对接水管一样,可以把文件的流对接到stdin上完成读取文件功能,也可以将stdout流对接到文件流上完成文件写入功能。

如果文件流用setvbuf指定缓冲区,然后修改缓冲区的内容,再fflush,文件的内容会被修改吗?不能!这是未定义行为

fopen函数是C标准库里面的,而open函数是操作系统linux提供的,是低级操作。fopen返回的是文件流,有缓冲;而open返回的是文件描述符,无缓冲。一般来说,普通文件用fopen打开,而设备文件只能用open打开。

预处理指令

常用预处理指令

预处理指令:#define、#include、#ifdef、#else、#endif、#ifndef、#if、#elif、#line、#error、#pragma
关键字:_Generic、_Noreturn、_Static_assert
函数/宏:sqrt()、atan()、atan2()、exit()、atexit()、assert()、memcpy()、
memmove()、va_start()、va_arg()、va_copy()、va_end()

##号可作为黏合剂,例子:#define XNAME(n) x##n 意味者后面的int XNAME(1) = 10; 效果等于int x1=10;

…三个点表示可变参数,也可以用在宏定义里面

宏定义展开规则

  1. 一般的展开规律像函数的参数一样:先展开参数,再分析函数,即由内向外展开
  2. 当宏中有#运算符的时候,不展开参数
  3. 当宏中有##运算符的时候,先展开函数,再分析参数
  4. ##运算符用于将参数连接到一起,预处理过程把出现在##运算符两侧的参数合并成一个符号,注意不是字符串

当遇到宏嵌套时,如A(B(C(x)))从最外层的宏A开始找到最内层宏C(含有# ##的宏可以看成最内层,因为# ##会破坏更内层的宏),然后展开最内层宏C。
再然后循环上述过程,从最外层宏A……

assert库

assert()是在运行时检查,还有_Static_assert()函数则在编译时检查表达式

stdarg.h

可变参数库,可变参数的三个小点必须是最后一个参数,且前面必有一个int型的形参,用以处理可变参数的数量

1
2
3
4
5
6
7
8
9
10
int test3(int k, ...) {
va_list ap;
va_start(ap, k);
double x;
int y;
x = va_arg(ap, double); // 检索第一个double
y = va_arg(ap, int); // 检索第一个int
va_end(ap);
return 0;
}

多线程编程

线程创建与管理

linux c的线程库pthread.h:

  • 创建线程:使用pthread_create()
  • 线程退出:使用pthread_exit()
  • 线程挂起:使用pthread_join()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <pthread.h>
typedef struct thread_struct {
int id;
char message[20];
} thread_struct;
void *test_thread(void *arg) { // 子线程
// 作为线程的函数通常形参通过结构体传入,返回值用void *来接收。
thread_struct *msg = (thread_struct *)arg;
printf(
"this is a thread, and it will be exit soon:%d, and get the "
"message:%s\n",
msg->id, msg->message);
sleep(10);
// pthread_exit(NULL); // 线程的主动行为
return (void *)2; // 这个返回值可以通过join接收
}
int test6() { // 主线程
//
pthread_t id = 0;
int return_value = 0;
thread_struct goarg = {9, "yes\0"};
if (pthread_create(&id, NULL, test_thread, &goarg) != 0)
printf("creat thread failed.\n");
// 如果取消线程,返回值是-1
sleep(1);
pthread_cancel(id);
printf("thread_id:%lu, wait for thread to exit.\n", id);
pthread_join(id, (void **)&return_value);
// 第二个参数接收线程的返回值(该函数会阻塞)
// 返回值的类型为二级指针,可以用强制转换的方式传递。
// 如果设置create的参数可以设置线程的属性为非join的,这样线程结束之后会自动回收相关内存资源。
printf("thread has been exited:%d\n", return_value); // 打印返回值
}

共享内存

同机子的多个进程之间通信的效率最高的方式。

具体步骤是:创建共享内存,映射共享内存,撤销共享内存。

创建:shmget,映射:shmat,取消映射:shmdt(),删除共享内存shmctl()去控制

多个进程共有的信息是shmid,比如两个进程都是这个shmid那么就可以共享内存

1
2
3
4
5
6
7
8
9
10
11
#include <sys/ipc.h>
#include <sys/shm.h>
#define MY_SHM_ID 887878
int test4() {
shmget(MY_SHM_ID, 1024, 0600 | IPC_CREAT); // 申请
char *buf;
buf = (char *)shmat(MY_SHM_ID, 0, 0);
// 然后向这个buf里面共享,另一个进程也按这个SHM_ID创建,则可以实现通信
shmdt(buf);
shmctl(MY_SHM_ID, IPC_RMID, 0);
}

消息队列

存在内存拷贝 所以比共享内存要慢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define MSG_MAX_SIZE 1024
typedef struct msg {
int msgtype;
char msg[MSG_MAX_SIZE];
} Msg;
#define MY_MSG_KEY 10010
int test5() {
int msdid = msgget(MY_MSG_KEY, IPC_CREAT | 0644); // 创建消息队列
Msg msg = {1, "offline"};
Msg *msgrevd = malloc(sizeof(Msg));
msgsnd(msdid, &msg, 8, 0);
// msgrcv(msdid, msgrevd, MSG_MAX_SIZE, 0, IPC_NOWAIT);
msgctl(MY_MSG_KEY, IPC_RMID, 0);

return 0;
}

守护进程

守护进程的实现

  1. 利用主进程推出,子进程被init接管的这个模式完成对终端的脱离。
  2. 在子进程中使用setsid函数完成创建新会话。(因为子进程会继承父进程的会话,组,控制终端,也需要脱离开来),setsid的作用就是创建一个新会话并让该进程担任会话组的组长。
  3. 要改变当前的工作目录,如果长期占用mount的目录则会导致无法umount
  4. 还要设置文件掩码,umask
  5. 打开的文件也是继承而来的,也需要退出。

计算机网络

OSI七层模型与TCP/IP四层模型

  • 应用层表示层会话层 —— 对应TCP/IP的应用层
  • 传输层 —— 对应TCP/IP的传输层
  • 网络层 —— 对应TCP/IP的网络层
  • 数据链路层物理层 —— 对应TCP/IP的网卡层硬件层

数据链路层

  • 以太网帧格式:802.3以太网:7个字节前导码,1个字节帧开始符(这8字节是物理层加的)6字节目标mac,6字节源mac,2字节以太网类型,500数据负载,4字节冗余校验FCS 共1518字节 12帧间距 以太网类型:0800 ipv4,86DD ipv6
  • CRC冗余校验:用于数据校验。CRC冗余校验码:约定一个除数,比如5位,那么就有4位的CRC余数。验证过程即mod=0,纠错码的方式:重复把戏,匹配把戏,简单求和把戏,阶梯求和把戏,矩阵定位把戏,海明码(海明码的生成稍稍有些复杂,需要复习)

无线

ieee 802.11 无线lan标准/b/a/g/n/ac/ax对应wifi1,2,3,4,5,6

PPPoE帧占8字节,占用数据的1500字节当中,所以mtu调整到1492

image-20240402123543037

else

以太网以MAC作为设备标识,而IP层以IP作为设备唯一标识。

在网络层,ICMP报文头8字节,IP报文头20字节,留给上层数据1492-28 = 1464

利用ping命令可以测试到目标路径的指定MTU是否过大,ping binbla.com -f -l 1464
image-20240403120830278

在windows 中使用``tracert binbla.com` 命令来观察从当前到目标host的路由途径。而linux 中使用routetrace命令,发送的是UDP报文。

在tracert路由追踪中,根据生存时间(TTL)来逐个确认中间路由。当TTL归零则不再路由转发直接返回结果或不返回结果(显示*号)。

这个protocol字段有些奇怪,既然已经是IPV4的报文首部,为什么还要多此一举。4表示IP协议,6表示TCP协议,17表示UDP协议。

值得记住的是IHL字段,在没有可选项的报文中,该字段的值通畅设置为5,单位4字节,表示IP首部共长20字节。

标识,表示同一个数据包的分片

flag:数据包是否可以分片,TTL生存时间,两个地址

image-20240403235434327

IPv6的报文首部

DNS与PTR

ARP(ipv6中用ICMPv6替代功能) ,查询IP对应的MAC地址。ARP广播式发送到同网段所有设备,收到目标设备的响应。Proxy ARP路由器的功能:可以将ARP请求转发给临近的网段。

RARP:不能通过dhcp分配IP地址的嵌入式设备,用这个协议向RARP服务器请求IP地址。

ICMP:可以用来确认IP包是否到达目标地址,通知在发送过程中IP包被废弃的具体原因。做网络调试会经常用到,但不能过分依赖,毕竟部分设备可以不理会ICMP报文。

ICMP是传送层(TCP/UDP)内容,但行驶的是网络层(IP)的功能

ICMP在v4上只是辅助功能,而ICMPv6却是主要功能,替代ARP的邻居探索消息,ICMP的重定向,ICMP路由器选择消息,自动设置IP地址

image-20240407120617442

也就是,从路由器拿取前缀,自己选一个地址,通过路由器告知他人。

实际上大部运营商提供的光猫上网服务都是锥形NAT的,而3G、4G网络、公共WiFi等因为安全因素都是对称式NAT。