C/C++ 从入门到入土

p *p &p 的含义

1
int* p;			//定义一个int类型的指针变量p,p是指针变量的名字,专门用来存放地址
1
int* p=&a;  等价于  int *p; p=&a; 		

p 存放的是 a 的地址,*p 也等价于 a

二级指针

一级指针存放的是变量的地址,二级指针存放的是指针的地址

const

const 是 C 语言中保留的一个关键字,用来定义常量,如果一个变量被 const 修饰,那么它的值就不能被改变,难点是 const 和 指针 相关的问题。const 可用于设置权限

const 在 * 左边:p 指针是一个指向常量的指针,即指针所指向对象的值是常量

1
const int * p; 

特点:*p 不能改变,p 可以改变

  1. 指针所指地址的值不能通过指针改变
  2. 所指的地址可以改变

const 在 * 右边:p 指针本身就是一个常量指针

1
int * const p

特点:p 不可改变,*p 可以改变

  1. 指针指向的地址不能改变
  2. 指针指向的地址的值可以改变

* 在两个 const 中间:p 为指向常量的常指针

1
const int * const p;

特点:p 不可改变,*p 不可改变

  1. 指针指向的地址不能改变
  2. 指针指向的地址的值不能改变

指针数组和数组指针

指针数组:它实际上是一个数组,数组的每个元素存放的是一个指针类型的元素

1
2
3
4
char* str[3] = { "calc","notepad","mspaint" };

//优先级问题:[]的优先级比*高
//说明str是一个数组,而数组的内容是string类型的指针

数组指针:它实际上是一个指针,该指针指向一个数组

1
2
3
4
5
int a[10] = {0};
int (*p)[10] = &a; 	//p是一个数组指针,指向数组a

//由于[]优先级比*高,因此在写数组指针时必须将*p用()括起来
//p先和*结合,说明p是一个指针变量

指针数组用于参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

void fn(char* str[3]) {
	for (size_t i = 0; i < 3; i++)
	{
		printf("%s\n", str[i]);
	}
}
void main() {
	char* str[3] = { "calc","notepad","mspaint" };
	fn(str);
}   

calc
notepad
mspaint

E:\VS_Project\C_Study\x64\Debug\C_Study.exe (进程 37956)已退出,代码为 0
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

动态内存分配

C 语言内存分配情况,在 C 语言中,根据数据在内存中存在的时间(生存周期)不同,将内存空间分为三个区:

程序区:用于存储程序的代码,即程序的二进制代码

静态存储区:用于存储全局变量和静态变量,这些变量的空间在程序编译时就已经分配好了

动态存储区:用于在程序执行时分配的内存,又分为:堆区(heap)和栈区(stack)。堆区:用于动态内存分配,程序运行时由内存分配函数在堆上分配内存。在 C 语言中,只能使用指针才能动态的分配内存。栈区:在函数执行时,函数内部的局部变量和函数参数的存储单元的内存区域,函数运行结束时,这些内存区域会自动释放

动态内存分配的函数:malloc、calloc、realloc

malloc:申请完内存后,这块内存空间不会初始化

calloc:申请完内存后,这块内存空间会自动初始化

realloc:用来重新分配内存空间

通用接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>

int add(int a, int b) {
	return a + b;
}
int sub(int a, int b) {
	return a - b;
}
int mul(int a, int b) {
	return a * b;
}


void 通用接口(int (*p)(int,int),int a,int b ) {
	printf("%d\n",p(a,b));
}

void main() {
	通用接口(sub, 9, 6);
}   

函数指针数组

 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
#include <stdio.h>
#include <stdlib.h>

int add(int a, int b) {
	return a + b;
}
int sub(int a, int b) {
	return a - b;
}
int mul(int a, int b) {
	return a * b;
}


void main() {
	int (*p[3])(int, int) = {add,sub,mul};		//函数指针数组,里面存放的是函数的地址
	for (size_t i = 0; i < 3; i++)
	{
		printf("%d\n",p[i](100,20));	//将add、sub、mul全部执行一遍
	}
}   


120
80
2000

用 typedef 将 int (*p[10])(int, int) 定义成类型 p,上述代码等价于如下代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>

typedef int (*p[3])(int, int);

int add(int a, int b) {
	return a + b;
}
int sub(int a, int b) {
	return a - b;
}
int mul(int a, int b) {
	return a * b;
}


void main() {
	p p1= {add,sub,mul};		
	for (size_t i = 0; i < 3; i++)
	{
		printf("%d\n",p1[i](100,20));	//将add、sub、mul全部执行一遍
	}
}   

多线程

 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
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <process.h>

typedef int (*p[10])(int, int);

int add(int a, int b) {
	MessageBoxA(0, "乃子黑客", "乃子牛逼",MB_OKCANCEL);
}
int sub(int a, int b) {
	MessageBoxA(0, "乃子黑客", "乃子牛逼", MB_OKCANCEL);
}
int mul(int a, int b) {
	MessageBoxA(0, "乃子黑客", "乃子牛逼", MB_OKCANCEL);
}


void main() {
	p p1= {add,sub,mul};		
	HANDLE hd[3] = { 0 };
	for (size_t i = 0; i < 3; i++)
	{
		hd[i] = _beginthread(p1[i], 0, NULL);
	}
	system("pause");
	//如果不用system("pause");就用WaitForMultipleObjects(3, hd, TRUE, INFINITE);
}   

字符串操作

计算字符串长度

实现 strlen(str) 的功能,字符串都是以 ‘\0’ 结尾的,可以通过以下代码实现 strlen(str) 的功能

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <stdio.h>

int 计算长度(char* str) {
	int length = 0;
	while (*str++) {
		length++;
	}
	return length;
}


void main() {
	char str1[] = "1234";
	printf("%d", 计算长度(str1));
}   

删除字符

实现删除字符的功能,也就是实现 memset 函数的功能,代码如下:

 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
#include <stdio.h>

//双指针轮询
void 删除指定字符(char* str) {	
	char* p1 = str;
	char* p2 = str;
	while (*p1 = *p2) //每次把指针p2指向的值赋值给p1
	{
		if (*p1 != 'd')	//删除指定字符d
		{
			p1++;	//指针p1和p2的地址++
			p2++;
		}
		else {
			p2++;
		}
	}   
	 
}


void main() {
	char str1[] = "abcde";
	删除指定字符(str1);
	printf("%s", str1);
}   

双指针轮询:

image-20230106204826425

字符串加密

字符串的加密,最简单的字符串加密,可以把每个字符都加上一个固定的 ascii 码,比如,这里加上 64:

image-20230106210537116

如果我们要解密字符串,就需要再减去 64,如下:

image-20230106211002385

上面这种方式,实际上不如采用异或加密更加好,如下:

image-20230106211504018

用异或加密这个方式,只需要再次异或就可以解密,如下:

image-20230106211554707

用密码来加密,也就是采用了分段加密的思路,如下:

image-20230107100408994

如果说字符串有 12 个字符,而加密密码是 4 位,那么用每一位加密密码对每个字符进行加密,就需要分成 3 组进行加密

image-20230107100927763

如果说字符串有 14 个字符,那么显然分成 3 组进行加密还不行,加密方式如下:

image-20230107101150109

代码如下:

 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
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <process.h>

void 异或分段加密(char* str, char* pass) {
	int 加密的字符串长度 = strlen(str);
	int 密码长度 = strlen(pass);
	if (加密的字符串长度 % 密码长度 == 0)
	{
		for (size_t i = 0; i < 加密的字符串长度 / 密码长度; i++)
		{
			for (size_t j = 0; j < 密码长度; j++)
			{
				str[i * 密码长度 + j] ^= pass[j];
			}
		}
	}
	else {
		for (size_t i = 0; i < 加密的字符串长度 / 密码长度; i++)
		{
			for (size_t j = 0; j < 密码长度; j++)
			{
				str[i * 密码长度 + j] ^= pass[j];
			}
		}

		for (size_t k = 0; k < 加密的字符串长度 % 密码长度; k++)
		{
			str[k + 加密的字符串长度 / 密码长度 * 密码长度] ^= pass[k];
		}
	}

}

void main() {
	char str[] = "abcdefghijklmnopqr";
	char pass[] = "badc";
	异或分段加密(str,pass);
	printf("%s\n", str);
	异或分段加密(str, pass);
	printf("%s", str);
}   

这里要特别注意:

image-20230109095741400

字符串压缩

对字符串进行压缩,假如我们要把 “nnnnntttbbaaaiiizzz” 进行压缩,压缩成 5n3t2b3a3i3z 的这种形式,应该如何做呢,我们需要把压缩后的字符串放到一片内存中,这就需要我们提前申请一段空间,用 calloc 来实现即可

 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
char* 亚索( char *str) {
	char* 压缩完的字符串空间首地址 = calloc(strlen(str),sizeof(char));
	char* backup = 压缩完的字符串空间首地址;
	while (*str)
	{
		char* pos = str;
		int 字符数 = 1;
		while (*pos == *(pos+1))
		{
			pos++;
			字符数++;
		}

		if (字符数 == 1)
		{
			*backup++ = *str++;
			
		}
		else {
			*backup = 字符数 + '0';
			*(backup + 1) = *str;
			backup += 2;
			str += 字符数;
		}
	}
	压缩完的字符串空间首地址 = realloc(压缩完的字符串空间首地址, strlen(压缩完的字符串空间首地址) + 1);
	return 压缩完的字符串空间首地址;
}

char* 解压(char* str) {
	char* 解压后的数据空间 = calloc(1024 * 1024, sizeof(char));
	char* backup = 解压后的数据空间;
	while (*str)
	{
		if (*str >= '0' && *str <= '9')
		{
			int len = *str - '0';
			for (size_t i = 0; i < len; i++)
			{
				*backup++ = *(str + 1);
			}
			str += 2;
		}
		else {
			*backup++ = *str++;
		}
	}
	return 解压后的数据空间;

}

链表

静态链表

image-20230109192743267

image-20230109192904912

image-20230109194319605

image-20230109194905761

image-20230109195315836

image-20230109195813832

动态链表

动态链表分配分配的内存在堆里面

结构体的定义一般都是放在头文件里面,前面在静态链表中定义的结构体实际上是不规范的,这里创建头文件 linked.h

image-20230109200913934

在 linked.h 中定义结构体 NODE 节点

image-20230109201550468

然后在 linked.h 声明相关的函数

image-20230110211719308

尾部添加

然后在 test.c 中添加如下内容,实现尾部添加

 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
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>
#include "linked.h"


void main() {
	node* 头节点 = NULL;
	尾部添加(&头节点, 66);
	尾部添加(&头节点, 77);
	尾部添加(&头节点, 88);
	尾部添加(&头节点, 99);
	显示链表内容(头节点);
}

void 尾部添加(node** 头节点, int data) {
	node* 新的节点 = malloc(sizeof(node));
	新的节点->lpNext = NULL;
	新的节点->num = data;

	if (*头节点==NULL)
	{
		*头节点 = 新的节点;
	}
	else
	{
		node* p = *头节点;	//备份头节点
		while (p->lpNext != NULL)
		{
			p = p->lpNext;
		}
		p->lpNext = 新的节点;
	}
}

void 显示链表内容(node* 头节点) {
	if (头节点)
	{
		printf("数据为:%d, 当前节点地址:%p,下一个节点地址:%p\n", 头节点->num,头节点,头节点->lpNext);
		显示链表内容(头节点->lpNext);
	}
}

image-20230110144218178

头部添加

在 test.c 中实现头部添加,如下:

 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#include <stdlib.h>
#include "linked.h"


void main() {
	node* 头节点 = NULL;
	尾部添加(&头节点, 66);
	尾部添加(&头节点, 77);
	尾部添加(&头节点, 88);
	尾部添加(&头节点, 99);
	头部添加(&头节点, 11);
	显示链表内容(头节点);
}

void 尾部添加(node** 头节点, int data) {
	node* 新的节点 = malloc(sizeof(node));
	新的节点->lpNext = NULL;
	新的节点->num = data;

	if (*头节点==NULL)
	{
		*头节点 = 新的节点;
	}
	else
	{
		node* p = *头节点;	//备份头节点
		while (p->lpNext != NULL)
		{
			p = p->lpNext;
		}
		p->lpNext = 新的节点;
	}
}

void 显示链表内容(node* 头节点) {
	if (头节点)
	{
		printf("数据为:%d, 当前节点地址:%p,下一个节点地址:%p\n", 头节点->num,头节点,头节点->lpNext);
		显示链表内容(头节点->lpNext);
	}
}


void 头部添加(node** 头节点,int data) {
	node* 新的节点 = malloc(sizeof(node));
	新的节点->lpNext = NULL;
	新的节点->num = data;

	if (*头节点 == NULL)
	{
		*头节点 = 新的节点;
	}
	else
	{
		新的节点->lpNext = *头节点;
		*头节点 = 新的节点;
	}
}

image-20230110151435461

删除

在 test.c 中实现删除,如下:

 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <stdio.h>
#include <stdlib.h>
#include "linked.h"


void main() {
	node* 头节点 = NULL;
	尾部添加(&头节点, 66);
	尾部添加(&头节点, 77);
	尾部添加(&头节点, 88);
	尾部添加(&头节点, 99);
	显示链表内容(头节点);
	printf("---------------------\n");
	删除(&头节点, 99);
	显示链表内容(头节点);
}

void 尾部添加(node** 头节点, int data) {
	node* 新的节点 = malloc(sizeof(node));
	新的节点->lpNext = NULL;
	新的节点->num = data;

	if (*头节点==NULL)
	{
		*头节点 = 新的节点;
	}
	else
	{
		node* p = *头节点;	//备份头节点
		while (p->lpNext != NULL)
		{
			p = p->lpNext;
		}
		p->lpNext = 新的节点;
	}
}

void 显示链表内容(node* 头节点) {
	if (头节点)
	{
		printf("数据为:%d, 当前节点地址:%p,下一个节点地址:%p\n", 头节点->num,头节点,头节点->lpNext);
		显示链表内容(头节点->lpNext);
	}
}


void 头部添加(node** 头节点,int data) {
	node* 新的节点 = malloc(sizeof(node));
	新的节点->lpNext = NULL;
	新的节点->num = data;

	if (*头节点 == NULL)
	{
		*头节点 = 新的节点;
	}
	else
	{
		新的节点->lpNext = *头节点;
		*头节点 = 新的节点;
	}
}


void 删除(node** 头节点, int data) {		//利用双指针轮询
	node *p1=NULL, *p2=NULL;
	p1 = *头节点;	//备份

	while (p1)
	{
		if (p1->num != data) {
			p2 = p1;
			p1 = p1->lpNext;
		}
		else
		{
			break;
		}
	}

	if (p1 != *头节点) {
		p2->lpNext = p1->lpNext;
		free(p1);
	}
	else
	{
		*头节点 = p1->lpNext;
		free(p1);
	}
}

image-20230110175641813

插入

在 test.c 中实现插入功能

  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
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#include <stdio.h>
#include <stdlib.h>
#include "linked.h"


void main() {
	node* 头节点 = NULL;
	尾部添加(&头节点, 66);
	尾部添加(&头节点, 77);
	尾部添加(&头节点, 88);
	尾部添加(&头节点, 99);
	显示链表内容(头节点);
	printf("---------------------\n");
	插入(&头节点, 8888, 66);
	显示链表内容(头节点);
}

void 尾部添加(node** 头节点, int data) {
	node* 新的节点 = malloc(sizeof(node));
	新的节点->lpNext = NULL;
	新的节点->num = data;

	if (*头节点==NULL)
	{
		*头节点 = 新的节点;
	}
	else
	{
		node* p = *头节点;	//备份头节点
		while (p->lpNext != NULL)
		{
			p = p->lpNext;
		}
		p->lpNext = 新的节点;
	}
}

void 显示链表内容(node* 头节点) {
	if (头节点)
	{
		printf("数据为:%d, 当前节点地址:%p,下一个节点地址:%p\n", 头节点->num,头节点,头节点->lpNext);
		显示链表内容(头节点->lpNext);
	}
}


void 头部添加(node** 头节点,int data) {
	node* 新的节点 = malloc(sizeof(node));
	新的节点->lpNext = NULL;
	新的节点->num = data;

	if (*头节点 == NULL)
	{
		*头节点 = 新的节点;
	}
	else
	{
		新的节点->lpNext = *头节点;
		*头节点 = 新的节点;
	}
}


void 删除(node** 头节点, int data) {		//利用双指针轮询
	node *p1=NULL, *p2=NULL;
	p1 = *头节点;	//备份

	while (p1)
	{
		if (p1->num != data) {
			p2 = p1;
			p1 = p1->lpNext;
		}
		else
		{
			break;
		}
	}

	if (p1 != *头节点) {
		p2->lpNext = p1->lpNext;
		free(p1);
	}
	else
	{
		*头节点 = p1->lpNext;
		free(p1);
	}
}


void 插入(node** 头节点, int newdata, int data) {	//在数据data前面插入newdata

	node* 新的节点 = malloc(sizeof(node));
	新的节点->lpNext = NULL;
	新的节点->num = newdata;

	node* p1 = NULL, * p2 = NULL;
	p1 = *头节点;	//备份

	while (p1)
	{
		if (p1->num != data) 
		{
			p2 = p1;
			p1 = p1->lpNext;
		}
		else
		{
			break;
		}
	}

	if (p1 == *头节点)
	{
		*头节点 = 新的节点;
		新的节点->lpNext = p1;
	}
	else
	{
		p2->lpNext = 新的节点;
		新的节点->lpNext = p1;
	}
}

image-20230110205343605

查找

在 test.c 中实现查找功能,如下:

  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
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#include <stdio.h>
#include <stdlib.h>
#include "linked.h"


void main() {
	node* 头节点 = NULL;
	尾部添加(&头节点, 66);
	尾部添加(&头节点, 77);
	尾部添加(&头节点, 88);
	尾部添加(&头节点, 99);
	显示链表内容(头节点);
	printf("---------------------\n");
	查找(头节点, 66) == NULL ? NULL : printf("找到数据:%d,地址是:%p", 查找(头节点, 66)->num, 查找(头节点, 66));
}

void 尾部添加(node** 头节点, int data) {
	node* 新的节点 = malloc(sizeof(node));
	新的节点->lpNext = NULL;
	新的节点->num = data;

	if (*头节点==NULL)
	{
		*头节点 = 新的节点;
	}
	else
	{
		node* p = *头节点;	//备份头节点
		while (p->lpNext != NULL)
		{
			p = p->lpNext;
		}
		p->lpNext = 新的节点;
	}
}

void 显示链表内容(node* 头节点) {
	if (头节点)
	{
		printf("数据为:%d, 当前节点地址:%p,下一个节点地址:%p\n", 头节点->num,头节点,头节点->lpNext);
		显示链表内容(头节点->lpNext);
	}
}


void 头部添加(node** 头节点,int data) {
	node* 新的节点 = malloc(sizeof(node));
	新的节点->lpNext = NULL;
	新的节点->num = data;

	if (*头节点 == NULL)
	{
		*头节点 = 新的节点;
	}
	else
	{
		新的节点->lpNext = *头节点;
		*头节点 = 新的节点;
	}
}


void 删除(node** 头节点, int data) {		//利用双指针轮询
	node *p1=NULL, *p2=NULL;
	p1 = *头节点;	//备份

	while (p1)
	{
		if (p1->num != data) {
			p2 = p1;
			p1 = p1->lpNext;
		}
		else
		{
			break;
		}
	}

	if (p1 != *头节点) {
		p2->lpNext = p1->lpNext;
		free(p1);
	}
	else
	{
		*头节点 = p1->lpNext;
		free(p1);
	}
}


void 插入(node** 头节点, int newdata, int data) {	//在数据data前面插入newdata

	node* 新的节点 = malloc(sizeof(node));
	新的节点->lpNext = NULL;
	新的节点->num = newdata;

	node* p1 = NULL, * p2 = NULL;
	p1 = *头节点;	//备份

	while (p1)
	{
		if (p1->num != data) 
		{
			p2 = p1;
			p1 = p1->lpNext;
		}
		else
		{
			break;
		}
	}

	if (p1 == *头节点)
	{
		*头节点 = 新的节点;
		新的节点->lpNext = p1;
	}
	else
	{
		p2->lpNext = 新的节点;
		新的节点->lpNext = p1;
	}
}

node* 查找(node* 头节点, int data) {
	for (头节点; 头节点 != NULL; 头节点 = 头节点->lpNext) {
		if (头节点->num == data)
		{
			return 头节点;
		}
	}
	return NULL;
}

image-20230110211625167

环形链表

环形链表的创建和打印

 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>
#include <stdlib.h>
#include "linked.h"


void main() {
	node* 头节点 = NULL;
	尾部添加(&头节点, 88);
	尾部添加(&头节点, 99);


	//环形链表的创建
	node* p = 头节点;   
	for (; p->lpNext != NULL; p = p->lpNext);
	p->lpNext = 头节点;

	打印环形链表(头节点);
  
}

void 尾部添加(node** 头节点, int data) {
	node* 新的节点 = malloc(sizeof(node));
	新的节点->lpNext = NULL;
	新的节点->num = data;

	if (*头节点==NULL)
	{
		*头节点 = 新的节点;
	}
	else
	{
		node* p = *头节点;	//备份头节点
		while (p->lpNext != NULL)
		{
			p = p->lpNext;
		}
		p->lpNext = 新的节点;
	}
}

void 打印环形链表(node* 头节点) {
	node* p = 头节点;
	for (; p->lpNext != 头节点; p = p->lpNext)
	{
		printf("数据为:%d,当前节点地址:%p,下一个节点地址:%p\n", p->num, p, p->lpNext);
	}
	printf("数据为:%d,当前节点地址:%p,下一个节点地址:%p\n", p->num, p, p->lpNext);
}

判断是否为环形链表

 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <stdio.h>
#include <stdlib.h>
#include "linked.h"


void main() {
	node* 头节点 = NULL;
	尾部添加(&头节点, 88);
	尾部添加(&头节点, 99);


	//环形链表的创建
	node* p = 头节点;   
	for (; p->lpNext != NULL; p = p->lpNext);
	p->lpNext = 头节点;


	判断是否为环形链表(头节点) ? printf("环形") : printf("不是环形");
}

void 尾部添加(node** 头节点, int data) {
	node* 新的节点 = malloc(sizeof(node));
	新的节点->lpNext = NULL;
	新的节点->num = data;

	if (*头节点==NULL)
	{
		*头节点 = 新的节点;
	}
	else
	{
		node* p = *头节点;	//备份头节点
		while (p->lpNext != NULL)
		{
			p = p->lpNext;
		}
		p->lpNext = 新的节点;
	}
}


int 判断是否为环形链表(node* 头节点) {
	int 标志 = 0;
	for (node* p1 = 头节点, *p2 = 头节点; p1 != NULL && p2 != NULL; p1 = p1->lpNext, p2 = p2->lpNext) {
		
		if (p2->lpNext != NULL) {
			p2 = p2->lpNext;
		}

		if (p1 == p2)
		{
			标志 = 1;
			break;
		}
	}
	return 标志;
}

多线程

CreateThread

1
2
3
4
5
6
7
HANDLE CreateThread(
   PSECURITY_ATTRIBUTES psa,
   DWORD cbStack,
   PTHREAD_START_ROUTINE pfnStartAddr,
   PVOID pvParam,
   DWORD fdwCreate,
   PDWORD pdwThreadID);

具体相关解释可以查阅 windows 核心编程

CREATE_SUSPENDED 含义:

CREATE_SUSPENDED 是一个 Windows 系统进程创建标志,它指示系统在创建新进程时将其处于挂起状态。这意味着新创建的进程将不会立即开始运行,而是等待其他代码明确地将其唤醒。这可能用于在创建进程之前进行一些其他操作或设置

image-20230114212959056

发现 CREATE_SUSPENDED 的标志对应 4,所以也可以用 4 代替 CREATE_SUSPENDED

image-20230114213022080

即,上述代码等价于如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <windows.h>
#include <stdio.h>

DWORD WINAPI fn(void* p) {
	for (size_t i = 0; i < 10; i++)
	{
		MessageBoxA(0, "111", "111", 0);
	}
}

DWORD WINAPI fn1(void* p) {
	for (size_t i = 0; i < 5; i++)
	{
		MessageBoxA(0, "666", "666", 0);;
	}
}

void main() {
	HANDLE hd = CreateThread(NULL, 0, fn, NULL, 4, NULL);
	HANDLE hd1 = CreateThread(NULL, 0, fn1, NULL, 0, NULL);
	system("pause");
}

ResumeThread

ResumeThread:重新激活线程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <windows.h>
#include <stdio.h>

DWORD WINAPI fn(void* p) {
	for (size_t i = 0; i < 10; i++)
	{
		MessageBoxA(0, "111", "111", 0);
	}
}

DWORD WINAPI fn1(void* p) {
	for (size_t i = 0; i < 5; i++)
	{
		MessageBoxA(0, "666", "666", 0);;
	}
}

void main() {
	HANDLE hd = CreateThread(NULL, 0, fn, NULL, 4, NULL);
	HANDLE hd1 = CreateThread(NULL, 0, fn1, NULL, 0, NULL);
	ResumeThread(hd);
	system("pause");
}

SuspendThread

SuspendThread:将线程挂起/冻结

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <windows.h>
#include <stdio.h>

DWORD WINAPI fn(void* p) {
	for (size_t i = 0; i < 10; i++)
	{
		MessageBoxA(0, "111", "111", 0);
	}
}

DWORD WINAPI fn1(void* p) {
	for (size_t i = 0; i < 5; i++)
	{
		MessageBoxA(0, "666", "666", 0);;
	}
}

void main() {
	HANDLE hd = CreateThread(NULL, 0, fn, NULL, 0, NULL);
	HANDLE hd1 = CreateThread(NULL, 0, fn1, NULL, 0, NULL);
	SuspendThread(hd);
	system("pause");
}

结束线程的方法

ExitThread

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <windows.h>
#include <stdio.h>

DWORD WINAPI fn(void* p) { 
	int i = 0;
	while (++i) {
		if (i > 20) {
			ExitThread(0);
		}
		printf("%d\n", i);
		Sleep(50);
	}
}


void main() {
	HANDLE hd = CreateThread(NULL, 0, fn, NULL, 0, NULL);
	system("pause");
}

_endthread

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <windows.h>
#include <stdio.h>
#include <process.h>

DWORD WINAPI fn(void* p) { 
	int i = 0;
	while (++i) {
		if (i > 20) {
			_endthread(0);
		}
		printf("%d\n", i);
		Sleep(50);
	}
}


void main() {
	HANDLE hd = CreateThread(NULL, 0, fn, NULL, 0, NULL);
	system("pause");
}

TerminateThread

ExitThread 和 _endthread 都是在函数内部直接结束线程,而 TerminateThread 是在外部结束线程,强烈不推荐使用 TerminateThread

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <windows.h>
#include <stdio.h>
#include <process.h>

DWORD WINAPI fn(void* p) { 
	int i = 0;
	while (++i) {
		printf("%d\n", i);
		Sleep(50);
	}
}


void main() {
	HANDLE hd = CreateThread(NULL, 0, fn, NULL, 0, NULL);
	Sleep(2000);
	TerminateThread(hd, 0);
	system("pause");
}

线程之间的同步和异步

线程可以是同步的也可以是异步的。同步线程意味着线程之间是互相独立的,在执行线程时不会被其他线程所干扰。而异步线程则意味着线程之间是互相关联的,在执行线程时会受到其他线程的影响

同步线程的例子:假设有两个线程,一个线程负责读取文件,另一个线程负责处理文件。这两个线程之间是独立的,读取文件的线程不会影响处理文件的线程,处理文件的线程也不会影响读取文件的线程。

异步线程的例子:假设有一个线程负责读取文件,另一个线程负责保存文件。读取文件的线程会不断地将文件传递给保存文件的线程,保存文件的线程则需要不断地等待文件的到来,在这个例子中,读取文件的线程和保存文件的线程是互相关联的。

在线程的操作中,我们经常会用到 system(“pause”) 和 Sleep(),这是为啥呢 ??

system(“pause”) 和 Sleep() 其实都是暂停控制台程序的执行,由于控制台程序通常在程序执行完成后会自动结束进程,在 Windows 中,当控制台程序结束时,Windows 会自动关闭控制台窗口。这意味着,如果你的程序中存在多线程,主线程可能会在其他线程完成之前结束,这样就会导致控制台窗口立即关闭,而不能看到其他线程的执行结果。如果你想要让控制台窗口等待线程完成后再关闭,就需要使用 system(“pause”) 和 Sleep 这样的指令

如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <windows.h>
#include <stdio.h>

DWORD WINAPI fn(void* p) {
	for (size_t i = 0; i < 5; i++)
	{
		MessageBoxA(0, "111", "111", 0);
	}
}

void main() {
	HANDLE hd = CreateThread(NULL, 0, fn, NULL, 0, NULL);
	system("pause");
}

对于上述代码,如果我们把 system(“pause”); 去掉,那么就不会出现弹框,因为主线程 main 函数立马就结束了,但是子线程 fn 函数还没有执行,导致看不到弹窗结果

同步

线程的同步:当一个线程调用另一个线程时,调用线程将会等待被调用线程执行完毕后再继续执行

举个例子,假设你有一个线程 A 和线程 B。线程 A 要等待线程 B 执行完某个任务之后再继续执行,这就是同步。

代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <windows.h>

DWORD WINAPI taskB(LPVOID lpParameter) {
    printf("Task B start\n");
    Sleep(3000);
    printf("Task B end\n");
    return 0;
}

int main() {
    printf("Thread A start\n");
    HANDLE hThread = CreateThread(NULL, 0, taskB, NULL, 0, NULL);
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);
    printf("Thread A end\n");
    return 0;
}

image-20230115220318163

异步

线程的异步:当一个线程调用另一个线程时,调用线程不会等待被调用线程执行完毕,而是继续执行自己的任务

举个例子,如果线程 A 在调用线程 B 时不等待线程 B 执行完毕,而是继续执行自己的任务,那么这就是异步。

代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <windows.h>

DWORD WINAPI taskB(LPVOID lpParameter) {
    printf("Task B start\n");
    Sleep(2000);
    printf("Task B end\n");
    return 0;
}

int main() {
    printf("Thread A start\n");
    HANDLE hThread = CreateThread(NULL, 0, taskB, NULL, 0, NULL);
    CloseHandle(hThread);
    printf("Thread A end\n");
    Sleep(3000);
    return 0;
}

image-20230115221456108

WaitForMultipleObjects

WaitForMultipleObjects() 是 Windows API 中的一个函数,它允许程序等待一个或多个对象(如线程或信号量)变为信号。它可用于在进程中同步多个线程

WaitForMultipleObjects(10,hd,TRUE/false,INFINITE/10000)

10:表示有10个线程

hd:表示句柄

TRUE:表示等待所有线程执行完成

false:表示如果有一个线程执行完成,立马返回

INFINITE:表示一直等待

10000:表示等待10秒

 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
#include <windows.h>
#include <stdio.h>
#include <process.h>

int sum = 0;
HANDLE hd[10] = {0};

DWORD WINAPI inc(void* p) { 
	
	for (size_t i = 0; i < 1000; i++)
	{
		sum++;
	}

}


void main() {

	for (size_t i = 0; i < 5; i++)
	{
		hd[i] = CreateThread(NULL, 0, inc, NULL, 0, NULL);
	}
	WaitForMultipleObjects(5, hd, TRUE, INFINITE);
	
	printf("%d", sum);

}

执行上述代码后,运行结果为 5000

image-20230116214306957

image-20230116214422452

那么,出现上述情况是为什么呢 ?

对上述情况进行反汇编

image-20230116215710413

image-20230116215926346

我们发现,在 c 语言中的一条语句 sum++ 反汇编后对应三条语句,如下:

004C18A0 mov eax,dword ptr [sum (04CA138h)]
004C18A5 add eax,1
004C18A8 mov dword ptr [sum (04CA138h)],eax

也就是将内存地址 04CA138h 的值先给 eax,然后让 eax+1,再把 eax 的值赋值给内存地址 04CA138h 中

对于如下代码,就是创建了五个线程

1
2
3
4
for (size_t i = 0; i < 5; i++)
{
	hd[i] = CreateThread(NULL, 0, inc, NULL, 0, NULL);
}

然后会让这个五个线程来执行如下 for 循环

1
2
3
4
	for (size_t i = 0; i < 1000000; i++)
	{
		sum++;
	}

每个线程都会循环 1000000 次,并且执行 sum++ 操作,由于 sum++ 对应的反汇编如下:

004C18A0 mov eax,dword ptr [sum (04CA138h)]
004C18A5 add eax,1
004C18A8 mov dword ptr [sum (04CA138h)],eax

也就是说这五个线程,每个线程都会执行上面的三条汇编语句 1000000 次

而且相对这五个线程而言,他们之间互相不影响,是异步的,那么就会出现如下问题:

image-20230118092606702

在步骤 ① 中,将内存 04CA138h 的值放入 eax 中的时候,如果内存地址 04CA138h 的值是 0 ,那么就会把 0 放到 eax 中,后续会执行 eax+1 ,值由 0 变成 1,最后再把 1 放回到 04CA138 中,但是由于此时线程为异步,那么就可能导致出现问题:

我们将这五个线程编号为 1、2、3、4、5,如果 1 号线程执行完了步骤①②,这时候 eax 的值是 1,下一步就是执行步骤③,将值 1 放入到内存 04CA138 中,但在 1 号线程执行步骤 ③ 之前,此时 2 号线程同时恰好执行完步骤①,也就是将内存地址 04CA138h 的值是 0 放到 eax 中,这样就覆盖了 eax 的值 1,这样就出现了线程冲突的问题,尤其重复多次执行的情况下

原子操作

这里 sum++ 出现问题的其实就是与 sum++ 的底层其实对应着多条语句,如果要解决这个问题,我们可以采用原子操作,所谓的原子操作就是不可分割的操作。

所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)

将 sum++ 换成 InterlockedIncrement(&sum) 即可,这样就变成了原子操作

image-20230123145756333

临界区

除了用上面的原子操作外,还可以采用临界区

临界资源:一次仅允许一个进程使用的资源

临界区:每个进程中,访问临界资源的那段代码

临界区类似于高铁上的厕所,每次只能进去一个人,当一个人上完厕所离开后,下一个人才能够进去上厕所

image-20230123152158892

 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
34
35
36
#include <windows.h>
#include <stdio.h>
#include <process.h>


CRITICAL_SECTION cs;

int sum = 0;
HANDLE hd[10] = {0};

DWORD WINAPI inc(void* p) { 
	
	//进入临界区
	EnterCriticalSection(&cs);
	for (size_t i = 0; i < 1000000; i++)
	{
		sum++;
	}
	//离开临界区
	LeaveCriticalSection(&cs);
}


void main() {
	InitializeCriticalSection(&cs);

	for (size_t i = 0; i < 5; i++)
	{
		hd[i] = CreateThread(NULL, 0, inc, NULL, 0, NULL);
	}
	WaitForMultipleObjects(5, hd, TRUE, INFINITE);
	
	printf("%d", sum);

	DeleteCriticalSection(&cs);
}

互斥

除了上述方法外,还可以使用互斥

何为互斥:互斥也称为间接制约关系。当一个进程占用某个临界资源时,如果另一个进程也想使用此临界资源,那么必须等待,直到占用临界资源的进程退出临界区,释放临界资源后,另一个进程才能去访问该临界资源

引入互斥锁的目的是用来保证共享数据操作的完整性,保护临界资源。每一个临界资源都由一个互斥锁来保护,任何时刻最多只能有一个线程能访问该临界资源。线程必须先获得互斥锁才能访问临界资源。访问完资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止

CreateMutex 是创建一把锁,,但这把锁你用来锁门还是锁抽屉都由你自己决定

CreateMutex 的三个参数如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 CreateMutex(
 
 LPSECURITY_ATTRIBUTES lpMutexAttributes, 
 // 指向安全属性的指针
 
 BOOL 
 bInitialOwner, 
 // 初始化互斥对象的所有者
 
 LPCTSTR 
 lpName 
 // 指向互斥对象名的指针
 
 );

CreateMutex 不同于临界区,临界区不可以跨进程使用,而互斥可以跨越进程使用,在其他进程里面用 OpenMutex

image-20230123160247015

 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
34
35
36
37
38
39
#include <windows.h>
#include <stdio.h>
#include <process.h>


CRITICAL_SECTION cs;

int sum = 0;
HANDLE hd[10] = {0};

#define id  "naizi"
HANDLE mutex = NULL;

DWORD WINAPI inc(void* p) { 
	
	if (WaitForSingleObject(mutex, INFINITE) == WAIT_OBJECT_0) {	//等待一个互斥量mutex,并且无限等待,也就是所谓的加锁, WAIT_OBJECT_0 表示等待成功
		for (size_t i = 0; i < 1000000; i++)
		{
			sum++;
		}
	}	
	ReleaseMutex(mutex);	//解锁
}


void main() {
	
	mutex = CreateMutex(NULL, FALSE, id);	//初始化一把锁

	for (size_t i = 0; i < 5; i++)
	{
		hd[i] = CreateThread(NULL, 0, inc, NULL, 0, NULL);
	}
	WaitForMultipleObjects(5, hd, TRUE, INFINITE);
	
	printf("%d", sum);

	CloseHandle(mutex);		//关闭句柄
}

事件对象

首先我们来了解内核对象,内核是操作系统里提供底层服务的,相当于一个底层模块

而内核对象,简单的理解就是内核分配的一个内存块,他是一种由内核管理的数据结构,“对象” 可以表示操作系统中的各种资源,如文件、进程、线程、内存块等。内核对象可以被系统中的其他对象引用,并且可以被用来实现各种系统服务

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
内核对象的种类有很多, 主要有:

进程对象 (Process Object) : 用于表示操作系统中的进程, 包括进程的状态, 内存空间, 文件句柄等信息
线程对象 (Thread Object) : 用于表示进程中的线程, 包括线程的状态, 栈, 寄存器等信息
文件对象 (File Object) : 用于表示文件系统中的文件, 包括文件名, 大小, 时间戳, 访问权限等信息
目录对象 (Directory Object) : 用于表示文件系统中的目录, 包括目录名, 文件列表, 访问权限等信息
设备对象 (Device Object) : 用于表示硬件设备, 包括设备类型, 驱动程序, 访问权限等信息
驱动对象 (Driver Object) : 用于表示驱动程序, 包括驱动程序名, 版本, 访问权限等信息
事件对象 (Event Object) : 用于表示系统事件, 如系统中断, 进程终止, 线程退出等信息
信号量对象 (Semaphore Object) : 用于同步多个进程或线程之间的访问, 如控制并发访问, 保证数据完整性等


这只是主要的一些,根据不同的操作系统的实现,内核对象的种类可能会有所不同。还有一些其它的类型的内核对象,如:
内存对象 (Memory Object) : 用于管理系统中的物理内存或虚拟内存, 包括内存大小, 分配状态, 访问权限等信息
定时器对象 (Timer Object) : 用于管理系统定时器, 可用于触发系统事件, 实现超时处理等
管道对象 (Pipe Object) : 用于管理系统管道, 可用于在进程之间传递数据
信道对象 (Channel Object) : 用于管理系统信道, 可用于在进程之间传递信息
信号对象 (Signal Object) : 用于管理系统信号, 可用于给进程或线程发送信息
网络对象 (Network Object) : 用于管理网络设备, 如网卡, 路由器, 防火墙等
这些对象都是由操作系统管理的,操作系统通过管理这些对象来实现各种系统服务,如进程管理, 文件系统, 网络管理等.

这里主要了解事件对象

事件是指在操作系统中发生的各种变化,如用户输入、网络数据到达、设备驱动程序发出的中断等。事件是内核对象之一,是操作系统内核负责管理的一个重要部分。

事件主要用于在进程间传递消息或通知,它可以让应用程序在不阻塞的情况下等待外部事件的发生。事件通常与事件驱动程序或中断机制结合使用,让操作系统能够及时响应事件并执行相应的操作。

创建事件的函数原型如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
HANDLE WINAPI CreateEvent(
  __in_opt  LPSECURITY_ATTRIBUTES lpEventAttributes,
  __in      BOOL bManualReset,
  __in      BOOL bInitialState,
  __in_opt  LPCTSTR lpName
);

第一个参数是安全描述符,设为NULL表示使用默认的安全描述符,并且子进程无法继承返回的句柄。

第二个参数TRUE表示手动重置,FALSE表示自动重置。

第三个参数FALSE表示事件是未触发状态,TRUE表示事件是触发状态。

第四个参数为事件指定一个名字。如果已经存在,函数则请求事件的EVENT_ALL_ACCESS权限,此时其它参数将被忽略。(程序只运行一个实例就可以使用这个方法,检测到事件已经存在则退出)

通过例子感受事件的便利

现在有三个顾客进了烟酒店,分别要了三包不通的香烟,老板去取香烟,这三个顾客就在柜台前等待(等待事件),老板取来了香烟(事件变成触发状态),付账、拿烟、离开

第一个例子程序使用手动重置事件,事件出发后,三个顾客可以同时离开

 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <Windows.h>
#include <stdio.h>

HANDLE hEvent;//全局事件

DWORD WINAPI CustomerOne(LPVOID lp)
{
	printf("顾客1:老板,拿包8mg的红双喜\n");
	WaitForSingleObject(hEvent, INFINITE);
	printf("顾客1:这种红双喜非常清爽,谢谢。\n");
	return 0;
}

DWORD WINAPI CustomerTwo(LPVOID lp)
{
	printf("顾客2:14块的利群\n");
	WaitForSingleObject(hEvent, INFINITE);
	printf("顾客2:我喜欢这个香烟,谢谢老板。\n");
	return 0;
}

DWORD WINAPI CustomerThree(LPVOID lp)
{
	printf("顾客3:我要包8mg的中南海\n");
	WaitForSingleObject(hEvent, INFINITE);
	printf("顾客3:一般人抽不惯这个,不过我一直都抽这个。\n");
	return 0;
}

int main(int argc, char* argv[])
{
	hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
	HANDLE hThread[3];
	hThread[0] = CreateThread(NULL, 0, CustomerOne, 0, 0, NULL);
	hThread[1] = CreateThread(NULL, 0, CustomerTwo, 0, 0, NULL);
	hThread[2] = CreateThread(NULL, 0, CustomerThree, 0, 0, NULL);

	Sleep(1000);
	printf("老板:你们要的香烟。\n");
	SetEvent(hEvent);

	WaitForMultipleObjects(3, hThread, TRUE, INFINITE);
	printf("\n所有线程都退出\n");

	CloseHandle(hEvent);
	for (int i = 0; i < 3; ++i)
		CloseHandle(hThread[i]);

	return 0;
}

image-20230125103904489

第二个例子使用自动重置事件,三个顾客分别先后离开。(等待成功的副作用,所以三个线程不能同时执行。每个线程等待成功后必须重置事件为触发状态,这样另外的线程就有机会得到执行。)

 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <windows.h>
#include <stdio.h>

HANDLE hEvent;//全局事件

DWORD WINAPI CustomerOne(LPVOID lp)
{
	printf("顾客1:老板,拿包8mg的红双喜\n");
	WaitForSingleObject(hEvent, INFINITE);
	printf("顾客1:这种红双喜非常清爽,谢谢。\n");
	SetEvent(hEvent);
	printf("顾客1离开。\n");
	return 0;
}
DWORD WINAPI CustomerTwo(LPVOID lp)
{
	printf("顾客2:14块的利群\n");
	WaitForSingleObject(hEvent, INFINITE);
	printf("顾客2:我喜欢这个香烟,谢谢老板。\n");
	SetEvent(hEvent);
	printf("顾客2离开。\n");
	return 0;
}
DWORD WINAPI CustomerThree(LPVOID lp)
{
	printf("顾客3:我要包8mg的中南海\n");
	WaitForSingleObject(hEvent, INFINITE);
	printf("顾客3:一般人抽不惯这个,不过我一直都抽这个。\n");
	SetEvent(hEvent);
	printf("顾客3离开。\n");
	return 0;
}
int main(int argc, char* argv[])
{
	hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
	HANDLE hThread[3];
	hThread[0] = CreateThread(NULL, 0, CustomerOne, 0, 0, NULL);
	hThread[1] = CreateThread(NULL, 0, CustomerTwo, 0, 0, NULL);
	hThread[2] = CreateThread(NULL, 0, CustomerThree, 0, 0, NULL);
	Sleep(1000);
	printf("老板:你们要的香烟。\n");
	SetEvent(hEvent);

	WaitForMultipleObjects(3, hThread, TRUE, INFINITE);
	printf("\n所有线程都退出\n");

	CloseHandle(hEvent);
	for (int i = 0; i < 3; ++i)
		CloseHandle(hThread[i]);

	return 0;
}

事件触发和未触发指的是事件的状态。当事件触发时,意味着事件已经发生,线程或进程会收到通知并采取相应的行动。而当事件未触发时,意味着事件还没有发生,线程或进程会等待事件的发生。

信号量

以一个停车场的运作为例。简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆直接进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入外面的一辆进去,如果又离开两辆,则又可以放入两辆,如此往复。

在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用

CreateSemaphore 函数:创建信号量对象

1
2
3
4
5
6
HANDLE CreateSemaphore(
  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,  // SD
  LONG lInitialCount,                          // initial count
  LONG lMaximumCount,                          // maximum count
  LPCTSTR lpName                           // object name
)
1
2
3
4
lpSemaphoreAttributes:为信号量的属性,一般可以设置为NULL
lInitialCount:信号量初始值,表示对象计数值为多少
lMaximumCount: 此值为设置信号量的最大值,表示最多允许多少个线程获取这个信号量对象
lpName:信号量的名字,长度不能超出MAX_PATH ,可设置为NULL,表示无名的信号量。

可以跨进程使用,在其他进程中就使用 OpenSemaphore

创建完信号量后,也就是初始化信号量后,当然最后面还是要关闭这个信号量的,即用 CloseHandle 来关闭信号量

 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
34
35
36
37
38
#include <windows.h>
#include <stdio.h>
#include <process.h>


HANDLE hd[10] = { 0 };

HANDLE 信号量 = NULL;

DWORD WINAPI fn(void* p) {
	int* id = p;
	printf("%d号线程正在等待\n",*id);
	if (WaitForSingleObject(信号量,INFINITE) == WAIT_OBJECT_0)		
	{
		printf("%d线程获取到资源,开始执行\n",*id);
		Sleep(3000);
		printf("%d线程释放资源\n", *id);
		ReleaseSemaphore(信号量, 1, NULL);
	}	
}



int main(int argc, char* argv[])
{
	int a[10] = { 0,1,2,3,4,5,6,7,8,9 };

	信号量 = CreateSemaphore(NULL, 0, 3, NULL);

	for (size_t i = 0; i < 10; i++) {
		hd[i] = CreateThread(NULL, 0, fn, a+i, NULL, NULL);
	}

	WaitForMultipleObjects(10, hd, TRUE, INFINITE);
	//system("pause");
	CloseHandle(信号量);

}
输出结果
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
0号线程正在等待
1号线程正在等待
2号线程正在等待
6号线程正在等待
4号线程正在等待
5号线程正在等待
7号线程正在等待
3号线程正在等待
8号线程正在等待
9号线程正在等待

我们发现上述代码的 9 个线程都在等待,

扩展:对 WaitForSingleObject 的返回值的解释:

1
2
3
4
WaitForSingleObject 的返回值有以下三种情况:
WAIT_OBJECT_0:表示你等待的对象(比如线程、互斥体)已的正常执行完成或完成释放。
WAIT_TIMEOUT:表示你等待的对象在还没完成之前,由 WaitForSingleObject 设置的时间已经超时。
WAIT_ABANDONED:这是针对等待对象是互斥体的情况,当互斥体对象虽然没有被占用它的线程释放,但是占用它的线程已提前中止时,WaitForSingleObject 就返回此值。

对于上述代码,如果我们将代码 信号量 = CreateSemaphore(NULL, 0, 3, NULL); 改成 信号量 = CreateSemaphore(NULL, 1, 3, NULL); 结果如下:

image-20230208230232380

如果我们将代码 信号量 = CreateSemaphore(NULL, 1, 3, NULL); 改成 信号量 = CreateSemaphore(NULL, 3, 5, NULL); 结果如下,也就是停车场的例子

image-20230209103226890

当然实际上,我们不用上面的方法,而是将 信号量 = CreateSemaphore(NULL, 3, 5, NULL); 设置成 信号量 = CreateSemaphore(NULL, 0, 5, NULL); ,然后在最后添加 ReleaseSemaphore(信号量, 3, NULL); ,结果如下:

image-20230209104942243

队列

队列存在的意义主要就是处理多线程的,因为多个线程不知道谁先谁后,所以就用队列来处理,在 C++、python 等语言中有封装好的,如 python 直接 import 导入相关的包即可,但是 c 语言中没有,我们可以自己封装

队列不同于栈,栈是先进先出,而队列就是先进先出,一边进一边出,这个就像我们平时吃饭和上厕所类似,一边用来吃,一边用来拉(tmd,这个举例真绝了…)

入队

queue.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#define LEN 100

typedef struct {
	int array[LEN];	//定义一个大小100的数组
	int 拉的那头;
	int 吃的那头;

}Queue;


int 是否为空(Queue* q);

int 是否满了(Queue* q);
void 初始化队列(Queue* q);
void 打印队列(Queue* q);
void 入队(Queue* q, int data);
void 出队(Queue* q);
int 获取弹出的元素(Queue* q);
队列.c
 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include <stdio.h>
#include <stdlib.h>
#include "queue.h"
#include <Windows.h>

void main() {
	Queue q;
	初始化队列(&q);
	入队(&q, 50);
	入队(&q, 20);
	入队(&q, 80);
	打印队列(&q);

}


int 是否为空(Queue* q) {

	if (q->吃的那头 == q->拉的那头) {
		return 1;
	}
	else
	{
		return 0;
	}
}


int 是否满了(Queue* q) {
	if (q->吃的那头 == LEN) 
	{
		return 1;
	}
	else
	{
		return 0;
	}                      
}


void 初始化队列(Queue* q) {
	
	q->吃的那头 = q->拉的那头 = 0;

}


void 打印队列(Queue* q) {
	for (size_t i = q->拉的那头; i < q->吃的那头; i++)
	{	
		printf("%4d", q->array[i]);
	}
}


void 入队(Queue* q, int data) {
	if (是否满了(q) == 1)
	{
		printf("装满了,放不下了");
		return;
	}
	else
	{
		q->array[q->吃的那头] = data;
		q->吃的那头++;
	}
}


void 出队(Queue* q) {
	if (是否为空(q) == 1)
	{
		printf("没有数据");
		return;
	}
	else
	{
		int 长度 = q->吃的那头 - q->拉的那头;
		if (长度 == 1) {
			q->吃的那头 = 0;
		}
		else
		{
			for (size_t i = 0; i < 长度; i++)
			{
				q->array[i] = q->array[i+1];
			}
			q->吃的那头--;
		}
	}
}


int 获取弹出的元素(Queue* q) {

	return q->array[q->拉的那头];
}

image-20230212223209515

出队

  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
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
#include <stdio.h>
#include <stdlib.h>
#include "queue.h"
#include <Windows.h>

void main() {
	Queue q;
	初始化队列(&q);
	入队(&q, 50);
	入队(&q, 20);
	入队(&q, 80);
	打印队列(&q);
	printf("=======");
	出队(&q);
	打印队列(&q);

}


int 是否为空(Queue* q) {

	if (q->吃的那头 == q->拉的那头) {
		return 1;
	}
	else
	{
		return 0;
	}
}


int 是否满了(Queue* q) {
	if (q->吃的那头 == LEN) 
	{
		return 1;
	}
	else
	{
		return 0;
	}                      
}


void 初始化队列(Queue* q) {
	
	q->吃的那头 = q->拉的那头 = 0;

}


void 打印队列(Queue* q) {
	for (size_t i = q->拉的那头; i < q->吃的那头; i++)
	{	
		printf("%4d", q->array[i]);
	}
}


void 入队(Queue* q, int data) {
	if (是否满了(q) == 1)
	{
		printf("装满了,放不下了");
		return;
	}
	else
	{
		q->array[q->吃的那头] = data;
		q->吃的那头++;
	}
}


void 出队(Queue* q) {
	if (是否为空(q) == 1)
	{
		printf("没有数据");
		return;
	}
	else
	{
		int 长度 = q->吃的那头 - q->拉的那头;
		if (长度 == 1) {
			q->吃的那头 = 0;
		}
		else
		{
			for (size_t i = 0; i < 长度; i++)
			{
				q->array[i] = q->array[i+1];
			}
			q->吃的那头--;
		}
	}
}


int 获取弹出的元素(Queue* q) {

	return q->array[q->拉的那头];
}

image-20230212224457112

队列和多线程

队列存在的意义主要就是处理多线程的,举例如下:

文件分割

 微信 微信
0%