C语言进阶之我与指针的爱恨情仇(1)

一.前言

        我们在初阶《指针》初阶C语言-指针-CSDN博客已经讲过了一些基础知识,知道了关于指针的一些概念->

1.指针就是个变量,用来存放地址,地址唯一标识一块内存空间

2.指针的大小是固定的4/8个字节(32位平台/64位平台)

3.指针是有类型的,指针的类型决定了指针的+-整数的步长,指针解引用操作时候的权限

4.指针的运算

        接下来,我们继续探讨指针更深层的秘密->

二.字符指针

        指针有一种类型叫做指针类型——char*。char* 的两种常见用法->

#include <stdio.h>

int main()
{
	char ch = 'w';
	char* pc = &ch;
	"abcdef";//这是一个字符串,当它做一个表达式时,它的值是首字符的地址值。
	char* ps = "abcdef";//所以可以用字符指针来接收

	return 0;
}

        "abcdef"也可以看成是一个数组(内存中的一块连续空间),而这个整体可以看成数组名(因为拿到了数组名就相当于拿到了数组首元素地址,这两个是等价的),所以可以做以下操作->

        要访问这段空间,可以->

        但事实上,这样写是有风险的,因为ps指向的对象是一个常量字符串,它不是变量,是不能被修改的,所以为了不小心对ps解引用使用写权限,程序就会崩溃->

        退出码不是0,说明程序不是正常退出。 所以为了安全,我们应该使用const修饰ps->

        所以,不论说字符指针指向的是字符串的首元素还是字符串都是正确的,都是能正常访问的。 但是需要注意的是,ps存的是字符串首元素的地址,它并不能把整个字符串存进去,因为有首元素的地址加之字符串在内存空间中是连续的,会误以为,ps存入了整个字符串。

        下面有一道面试题,问最后的输出是什么->

#include <stdio.h>

int main()
{
	char str1[] = "hello bit.";
	char str2[] = "hello bit.";
	const char* str3 = "hello bit.";
	const char* str4 = "hello bit.";
	if (str1 == str2)
		printf("str1 and str2 are same\n");
	else
		printf("str1 and str2 are not same\n");

	if (str3 == str4)
		printf("str3 and str4 are same\n");
	else
		printf("str3 and str4 are not same\n");

	return 0;
}

         str1和str2是两个数组,分别代表了内存中的两块不同空间,当if  str1 == str2时,数组名比较,比较的是地址是否相同,因为二者是内存种不同的两块空间(虽然存的内容是一样的),所以地址是不一样的。对于str3和str4,它们指向的是常量字符串,所以内存中肯定有一块空间,是用来存放这个常量字符串的,由于,常量字符串是只有可读属性,不具有可写属性,所以是没必要将一份相同的常量字符串保存两份的,因此,str3和str4存的都是字符串首元素的地址,二者相等是没问题的。

三.指针数组

        指针数组是数组,由字符数组 - 存放字符的数组、整型数组 - 存放整型的数组,类比出指针数组 - 存放指针的数组,存放的元素在数组中的元素都是指针类型的。

int* arr1[5];    //整型指针的数组
char* arr2[10];    //字符指针的数组

        但是指针数组可以用来模拟二维数组,关于这方面的介绍可以看初阶C语言-指针-CSDN博客->

int main()
{
	int arr1[5] = { 1, 2, 3 ,4 ,5 };
	int arr2[5] = { 2, 3, 4 ,5 ,6 };
	int arr3[5] = { 3, 4, 5 ,6 ,7 };

	int* arr[3] = { arr1, arr2, arr3 };

	return 0;
}
#include <stdio.h>

int main()
{
	char* arr[3] = { "张三", "李四", "王五" };
	int i = 0;
	for (i = 0; i < 3; ++i)
	{
		printf("%s ", arr[i]);
	}

	return 0;
}

 四.数组指针

        数组指针是指针,也可以由字符指针 - 指向字符的指针,整型指针 - 指向整型的指针,浮点型指针 - 指向浮点数的指针类比出数组指针 - 指向数组的指针。

#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	printf("%p\n", arr);
	printf("%p\n", arr + 1);

	printf("%p\n", &arr[0]);
	printf("%p\n", &arr[0] + 1);

	printf("%p\n", &arr);
	printf("%p\n", &arr + 1);


	return 0;
}

        我们发现,数组的地址和数组的首元素地址是一样的,但他们的区别在于->数组首元素地址+1,走了4字节,数组的地址+1走了40个字节,因为指针的类型决定了指针+1,到底+几个字节,所以能看到,数组首元素的地址是int*,数组的地址是数组指针类型。那我们应该用什么类型的指针来接受&arr呢->

int* p[10] = &arr;

        这样写是不可以的,这样写的话,p就是指针数组,p会先跟[]结合形成数组,int*是数组的元素类型,所以应该是*跟p优先结合成指针,指向的类型是int [10]->

#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	int(*p)[10] = &arr;
	printf("%p", p);

	return 0;
}

        语法规定了是这样写,注意不要写成int[10] (*p);关于int(*p)[10],可以理解成指针p指向了一个有10个元素的数组,每个元素的类型是int。

        通过报错信息可以发现,在定义数组指针的时候,指向的数组大小必须指定(且是常量),否则编译器会默认为0。 

        接下来讲解一些数组指针的应用场景->

#include <stdio.h>

int main()
{
	int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

	int(*p)[10] = &arr;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", (*p)[i]);
	}

	return 0;
}

        (*p)等价于(*&arr),然后*和&相抵消,得到arr,数组名即数组首元素的地址,但这样访问数组明显是不方便的,不常用的,没有int *p = arr方便。 

        二维数组之前是这样打印的,二维数组传参,形参也是使用二维数组的形式,之前提过,此时数组名(实参)是数组首元素的地址,那在形参就可以写成指针的形式(这也是数组传参的本质),先复习一下一维数组传参->

         此时,形参写的也是数组形式,本质上传arr数组名,即数组首元素的地址,那还原回去用指针接收也是没问题的->

         再回到刚刚的二维数组传参,二维数组的数组名,它到底是谁的地址?到底用什么类型的指针接收?二维数组的首元素是它的第0行(arr[0]),所以二维数组的数组名是第0行的地址即&arr[0],arr[0]是第0行的数组,所以用成指针做形参,它应该是指向一个数组,应该用数组指针->

#include <stdio.h>

void Print(int (*p)[5], int r, int c)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
		{
			printf("%d ", p[i][j]);
		}
		printf("\n");
	}
}

int main()
{
	int arr[3][5] = { {1, 2, 3, 4, 5},{2, 3, 4,5, 6},{3, 4, 5, 6, 7} };
	Print(arr, 3, 5);

	return 0;
}

         p是一行数组的地址,它+i(步长),走的是i行,对其*拿到第i行首元素的地址,+j,才能访问那一行前j个元素。(也就是说当p是一行数组的地址时,它的下一个元素也是一行数组,只有*p拿到那一行首元素的地址,才能方便访问那一行的元素。同时也应该知道p是一个数组指针,对其解引用,得到的是其首元素的地址,因为p = &arr,*p = arr)。

        学了指针数组和数组指针我们来一起回顾并看看下面代码的意思->

int arr[5];    //数组
int *parr1[10];    //指针数组
int (*parr2)[10];    //数组指针
int (*parr3[10])[5];    //存放数组指针的数组

        最后一个是parr3先跟[10]结合表示是数组,数组元素是int (*)[5],即数组指针->

 五.数组参数、指针参数

        写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数应该如何设计呢->

5.1一维数组传参

#include <stdio.h>

#include <stdio.h>
void test(int arr[])//ok?
{}
void test(int arr[10])//ok?
{}
void test(int* arr)//ok?
{}
void test2(int* arr[20])//ok?
{}
void test2(int** arr)//ok?
{}

int main()
{
	int arr[10] = { 0 };
	int* arr2[20] = { 0 };
	test(arr);
	test2(arr2);

	return 0;
}

第一个形参的部分写成数组,本质是指针,所以[]可以不用写大小,是对的。

第二个也是对的,实事上,不会真的去创建一个数组接收,大小是没有意义的,写多少都可以。

第三个是一维数组传参的本质,它传递的是数组首元素的地址,所以也是对的。

第四个,形参用数组的形式接收是没问题的。

第五个,传递的是数组首元素的地址,由于数组首元素是指针,所以用二级指针接收指针的地址,没问题。

5.2二维数组传参

void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int* arr)//ok?
{}
void test(int* arr[5])//ok?
{}
void test(int(*arr)[5])//ok?
{}
void test(int** arr)//ok?
{}
int main()
{
	int arr[3][5] = { 0 };
	test(arr);
}

第一个就是数组传参,形参写成数组的形式,是没问题的。

第二个第三个是行可以省略,列不能省略,且列必须是5,因为本质是int(*arr)[5],这里的[5]要和int arr[][5]对应上,列修改成6,那个数组指针指向的数组元素也会是6。

第四个不行的,因为指针类型不一样。

第五个也是不可以的,形参是数组指针都不是指针类型。

第六个是正确的形式。

第七个形参是二级指针,指针类型也没对上。

5.3一级指针传参

#include <stdio.h>

void print(int* p, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d\n", *(p + i));
	}
}

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9 };
	int* p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	//一级指针p,传给函数
	print(p, sz);

	return 0;
}

        一级指针传参,形参写成一级指针即可。

思考:当一个函数的参数部分为一级指针时,函数能接收什么参数呢?

void test(int *p)
{
    ///...
}
int a = 10;
int* ptr = &a;
int arr[10] = { 0 };
test(&a);
test(ptr);
test(arr);

         只要能传一个整型指针类型过去就行。

5.4二级指针传参

#include <stdio.h>

void test(int** ptr)
{
	printf("num = %d\n", **ptr);
}

int main()
{
	int n = 10;
	int* p = &n;
	int** pp = &p;
    int* arr[5];
	test(pp);
	test(&p);
    test(arr);

	return 0;
}

思考:当函数的参数为二级指针时,可以接受什么参数?

void test(char **p)
{
 
}

int main()
{
 char c = 'b';
 char*pc = &c;
 char**ppc = &pc;
 char* arr[10];
 test(&pc);
 test(ppc);
 test(arr);//Ok?

 return 0;
}

 六.函数指针

数组指针 - 指向数组的指针 - 存放的是数组的地址,&数组名就是数组的地址。

函数指针 - 指向函数的指针 - 存放的是函数的地址,那怎么的到函数的地址呢?

        数组名是数组首元素地址,那函数名呢?是函数的地址吗? 

#include <stdio.h>

int test(int x, int y)
{
	return x + y;
}

int main()
{
	printf("%p\n", test);
	printf("%p\n", &test);
	int (*pf1)(int, int) = test;
	int (*pf2)(int, int) = &test;

	return 0;
}

         ->&函数名和函数名都是函数的地址,没什么区别。

        假设我要用一个函数指针变量pf1来接收test函数的地址,那我首先是pf1 = test,因为pf1是指针,->(*pf1),函数要函数作用符(),参数列表和返回类型吧->int (*p) (int int) = test。

        同时也是没有任何的警告,说明test和&test类型是一致的。

        注意:(*pf1)的括号是不可以省略的,如果写成int *pf1(int, int),它就变成了一个函数声明(pf2是函数名,返回类型是int*)。 同时,函数指针的参数只需要标明参数类型(也可以写成(int x, int y)),因为没有调用函数,并没有传参的过程发生。

        接下来就是如何利用pf1来调用函数->

        *pf1找到函数的地址,调用函数,再开始传参...函数栈帧结束,返回值被寄存器返回回来,被ret接收。 但其实*pf1的*是多余的,写或者不写都可以,甚至想写几个就写几个->

         这个和一维数组形参,int arr[100]的这个100就是个摆设,不写或者写成100000都行。但是注意别写成*pf1(2,5),优先级得注意。

        阅读两段有趣的代码->

//代码1
(*(void (*)())0)();
//代码2
void (*signal(int, void(*)(int)))(int);

第一个是先看认识的0,0的左边是(void (*)()),里面是函数指针类型,->(类型)0,就是把0强转成函数指针类型,再*解引用,即调用地址0处的函数,至于最右边的()表示调用的函数无参,是因为0被强转成函数指针类型的时候,函数的参数是(),无参。

第二个先看signal,先跟()结合,说明是个函数,然后看()内是只有类型,说明是函数声明,它的参数是int类型和函数指针类型,该类型是void(*)(int),函数的返回类型也是void (*)(int)。

         返回类型可以写成void (*) (int) signal(int, void(*)(int))吗?虽然明面上不可以,但是可以类型重命名->

typedef void (*)(int) pfun_t;//这是错误的
typedef void (*pfun_t)(int);//这是对的,理解的时候可以按照上面那一种方式理解,但不能那样写。

void (*pfun_t)(int);

         此时,pfun_t是函数指针变量。

typedef void (*pfun_t)(int);

        此时,pfun_t是函数指针类型。typedef是类型重定义。

七.函数指针数组

指针数组 - 数组

char* arr1[5];        //字符指针数组 - 存放的是字符指针

int* arr2[5];        //整型指针数组 - 存放的是整型指针

函数指针数组 - 数组

//存放的是函数指针即函数的地址

         由之前的函数指针知识可以知道,函数指针类型是 type_t (*)(type_t...)。

#include <stdio.h>

int Add(int x, int y)
{
	return x + y;
}

int Sub(int x, int y)
{
	return x - y;
}

int main()
{
	int (*pf1)(int, int) = &Add;
	int (*pf2)(int, int) = &Sub;
	int (*pfArr[2])(int, int) = { &Add, &Sub };

	return 0;
}

         我们知道pf1和pf2的类型都是int (*)(int, int),即他们是同一种类型,即可以用数组来存储,数组类型 + 数组名->int (*pfArr[2])(int, int) = { &Add, &Sub };。

        函数指针数组的用途就是:转移表

eg:计算器

#include <stdio.h>

int Add(int x, int y)
{
	return x + y;
}

int Sub(int x, int y)
{
	return x - y;
}

int Mul(int x, int y)
{
	return x * y;
}

int Div(int x, int y)
{
	return x / y;
}

void menu()
{
	printf("-----------------------------\n");
	printf("-----	1.add	2.sub	-----\n");
	printf("-----	3.mul	4.div	-----\n");
	printf("-----	0.exit          -----\n");
	printf("-----------------------------\n");
}

int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	do
	{
		menu();
		printf("请输入:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			ret = Add(x, y);
			printf("ret = %d\n", ret);
			break;
		case 2:
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			ret = Sub(x, y);
			printf("ret = %d\n", ret);
			break;
		case 3:
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			ret = Mul(x, y);
			printf("ret = %d\n", ret);
			break;
		case 4:
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			ret = Div(x, y);
			printf("ret = %d\n", ret);
			break;
		case 0:
			printf("退出计算器\n");
			break;
		default:
			printf("选择错误,重新选择\n");
			break;
		}
	} while (input);

	return 0;
}

        随着计算器的函数越来越多,这个switch分支语句就越来越多,而且代码有点重复->

        这些函数有一些特点,除了函数名不一样,函数返回值和参数是一样的,所以可以改造成-> 

#include <stdio.h>

int Add(int x, int y)
{
	return x + y;
}

int Sub(int x, int y)
{
	return x - y;
}

int Mul(int x, int y)
{
	return x * y;
}

int Div(int x, int y)
{
	return x / y;
}

void menu()
{
	printf("-----------------------------\n");
	printf("-----	1.add	2.sub	-----\n");
	printf("-----	3.mul	4.div	-----\n");
	printf("-----	0.exit          -----\n");
	printf("-----------------------------\n");
}

int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	do
	{
		menu();
		printf("请输入:>");
		scanf("%d", &input);
		//函数指针数组
		int (*pfArr[])(int, int) = { NULL, &Add, &Sub, &Mul, &Div };
		if (input == 0)
		{
			printf("退出计算器\n");
		}
		else if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数\n");
			scanf("%d%d", &x, &y);
			ret = pfArr[input](x, y);
			printf("ret = %d\n", ret);
		}
		else
		{
			printf("选择错误,请重新选择\n");
		}
	} while (input);

	return 0;
}

         后续如果要加功能,直接在菜单上多添加几个选项,在函数指针数组后面接着续函数地址即可。但是这样的缺点是,函数的返回类型和参数必须是一样的,才能放入函数指针数组内。此时我们把这种情况下的函数指针数组叫做转移表。

八.指向函数指针数组的指针

        指向函数指针数组的指针是一个指针指向一个数组,数组的元素都是函数指针。

	int a = 10;
	int b = 20;
	int c = 30;
	int* arr[] = { &a, &b,&c };//整型指针数组
	int* (*p)[3] = &arr;//p是指针,是指向整型指针数组的指针

函数指针数组 - 数组 - 存放的是函数的地址,如刚刚写过的一个->int (*pfArr[])(int, int);,pfArr是函数指针数组,&pfArr,就需要函数指针数组指针来指向它->int (*(*p)[])(int, int) = &pfArr;

        这个比较难,不太重要,这个阶段基本用不到。

九.回调函数 

        回调函数就是一个通过函数指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,这就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或者条件发生时由另一方调用的,用于对该事件或条件进行响应。

        我们能有办法把上述四个case语句的函数封装成一个函数吗?这四段代码只有调用的函数不一样,所以我们能够封装成一个calc()函数,参数就传对应函数地址就完成了任务。 

void calc(int (*pf)(int, int))
{
	int x = 0;
	int y = 0;
	int ret = 0;
	printf("请输入两个操作数\n");
	scanf("%d%d", &x, &y);
	ret = pf(x, y);
}

         当指针pf调用Add函数时,Add函数就被称为回调函数。这里的实现方是Add函数,我们没有直接调用Add函数,而是在特定的的事件发生(input == 1),由另一方calc函数通过函数指针调用。

        再来演示一下qsort函数的使用->

qsort - 是一个库函数,底层使用的是快速排序的方式,对数据进行排序的这个函数可以直接使用,这个函数可以用来排序任意类型的数据。

排序算法有很多,我们当前学过的有冒泡排序,先回忆一下冒泡排序->

#include <stdio.h>

void bubble_sort(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz - 1; ++i)
	{
		int j = 0;
		for (j = 0; j < sz - i - 1; ++j)//每一趟的比较次数
		{
			if (arr[j + 1] < arr[j])
			{
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
}

void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; ++i)
	{
		printf("%d ", arr[i]);
	}
}

int main()
{
	int arr[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
	int sz = sizeof arr / sizeof arr[0];
	bubble_sort(arr, sz);//核心思想是两两相邻元素比较
	print_arr(arr, sz);

	return 0;
}

        这个函数不好的地方在于,参数是写死的,只能排序int类型的数组。而qsort函数是可以排序任意类型的数据的,我们先来看看qsort是如何做到的->

void qsort (void* base,//待排序数组的第一个元素的地址
            size_t num,//待排序数组元素个数
            size_t size,//待排序数组一个元素的大小
            int (*compar)(const void*,const void*)//函数指针- 指向一个函数,这个函数是用来比较两个元素的
            );

        第四个参数是用来告诉qsort我应该怎么来比较大小的,因为比较不同的数据的大小方式是不一样的。所以你只要提供了比较大小的方法,传给compar即可。

         函数指针compar的参数是两个待比较元素的地址。void*的指针不能直接解引用,也不能进行+-整数操作,它的作用是用来存放任意类型数据的地址->

char c = 's';
char* pc = &c;
int* p = &c;//两边数据类型不一样,vs报警告
void* pc_ = &c;//这个不报警告
int a = 20;
pc_ = &a;//不报警告

        void*是无具体类型的指针,可以接收任意类型的地址,它就像个垃圾桶一样。

        compar的返回值是有要的, 1大于p2时返回大于0的数,等于则返回0,小于则返回<0的数。

int com_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}
#include <stdio.h>
#include <stdlib.h>//包含qsort函数的头文件

int com_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}

void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; ++i)
	{
		printf("%d ", arr[i]);
	}
}

void test1()
{
	int arr[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
	int sz = sizeof arr / sizeof arr[0];
	qsort(arr, sz, sizeof arr[0], com_int);
	print_arr(arr, sz);
}

int main()
{
	test1();

	return 0;
}

#include <stdio.h>
#include <stdlib.h>

//测试qsort排序结构体数据
struct Stu
{
	char name[20];
	int age;
};
//结构体比较大小可以按照年龄比,也可以按照名字比较
com_stu_by_age(const void* e1, const void* e2)
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}

void test2()
{
	struct Stu arr[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 12} };
	int sz = sizeof arr / sizeof arr[0];
	qsort(arr, sz, sizeof arr[0], com_stu_by_age);
}

int main()
{
	test2();

	return 0;
}

        排序之前是按照数组初始化的顺序,再看排序后的->

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct Stu
{
	char name[20];
	int age;
};
//结构体按照名字比较
int com_stu_by_name(const void* e1, const void* e2)
{
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}

void test3()
{
	struct Stu arr[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 12} };
	int sz = sizeof arr / sizeof arr[0];
	qsort(arr, sz, sizeof arr[0], com_stu_by_name);
}

int main()
{
	test3();

	return 0;
}

         排序之后的数组->

         qsort之所以能实现这种功能,是因为抽象出了比较方法(函数),你把比较方法传进来就能进行比较了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/907132.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

对象池的作用以及简单示例

在游戏当中&#xff0c;有很多时候需要重复地创建或删除某些游戏对象&#xff0c;此时会耗费系统资源&#xff0c;从而影响性能&#xff0c;利用对象池可以解决这个问题。对象池能够节省内存&#xff0c;优化程序流畅程度。 把对象放在一个集合里&#xff0c;通过集合来管理对象…

java并发编程-volatile的作用

文章目录 volatile的作用1.改变线程间的变量可见性2.禁止指令重排序 参考的学习视频 volatile的作用 1.改变线程间的变量可见性 每个线程都有一个专用的工作集内存&#xff0c;下图里面粉色的表示专用工作集内存&#xff0c;黄色的是共享内存工作区&#xff0c;如果加入了vol…

Netty 组件介绍 - EventLoop

概要 把 channel 理解为数据的通道把 msg 理解为流动的数据&#xff0c;最开始输入是 ByteBuf&#xff0c;但经过pipeline 的加工&#xff0c;会变成其它类型对象&#xff0c;最后输出又变成 ByteBuf把 handler 理解为数据的处理工序 工序有多道&#xff0c;合在一起就是 pi…

oracle 对应的JDBC驱动 版本

下载网址&#xff1a;JDBC and UCP Downloads page

整理 【 DBeaver 数据库管理工具 】的一些基础使用

目录 连接设置切换工作空间SQL编辑器&#xff08;写sql语句&#xff09;打开方式新建查询&#xff08;sql编辑器&#xff09;打开写的 sql 查询&#xff08;项目浏览器&#xff09; 备份sql文件查看历史执行语句自动保存sql语句的文件&#xff08;编辑器&#xff09;关闭自动生…

(九)JavaWeb后端开发——Servlet

目录 1.Servlet由来 2.Servlet快速入门 3.Servlet执行原理 4.Servlet生命周期 1.Servlet由来 在JaveEE API文档中对Servlet的描述是&#xff1a;可以运行在服务器端的微小程序&#xff0c;但是实际上&#xff0c;Servlet就是一个接口&#xff0c;定义了Java类被浏览器访问…

LLC Power Switches and Resonant Tank 笔记

1.概述 上面是一个典型的LLC电路。注意Lm是励磁电感&#xff0c;就是次级线圈空载时的主变压器电感&#xff0c;据说在计算谐振频率时无需关心。然后&#xff0c;作为DCDC电源&#xff0c;它通过调整谐振频率&#xff0c;来改变输出的电流。负载越大&#xff0c;频率越低&#…

项目模块十三:Util模块

一、项目设计思路 用于之后协议使用的工具类 二、静态成员函数 1、分割函数 static size_t Split(const string &src, const string &sep, vector<string> *array) string.find(const string &str, size_t pos 0) string.substr(size_t pos 0, size_t…

Chrome与夸克谁更节省系统资源

在当今数字化时代&#xff0c;浏览器已经成为我们日常生活中不可或缺的一部分。无论是工作、学习还是娱乐&#xff0c;我们都依赖于浏览器来访问互联网。然而&#xff0c;不同的浏览器在性能和资源消耗方面存在差异。本文将探讨Chrome和夸克两款浏览器在系统资源消耗方面的表现…

Qt第三课 ----------输入类的控件属性

作者前言 &#x1f382; ✨✨✨✨✨✨&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f382; ​&#x1f382; 作者介绍&#xff1a; &#x1f382;&#x1f382; &#x1f382; &#x1f389;&#x1f389;&#x1f389…

后端:Spring、Spring Boot-实例化Bean依赖注入(DI)

文章目录 1. 实例化Bean2. 使用FactoryBean3. 依赖注入(DI)3.1 AutoWired 属性注入(查找顺序&#xff1a;先类型&#xff0c;后名字)3.2 AutoWired 在构造函数&参数上的使用3.3 Inject和Resource 进行依赖注入3.4 Value 进行注入 1. 实例化Bean 默认使用无参构造函数&…

qt QPicture详解

1、概述 QPicture类是Qt框架中的一个重要图形类&#xff0c;它主要用于记录和回放QPainter的绘图指令。这个类能够跨平台、无分辨率依赖地绘制图形&#xff0c;非常适合用于实现打印预览和图像操作等场景。QPicture可以将绘图操作序列化为一种独立于平台的格式&#xff0c;保存…

【计算机网络教程】课程 章节测试1 计算机网络概述

一. 单选题&#xff08;共16题&#xff09; 1 【单选题】以下关于TCP/IP参考模型缺点的描述中&#xff0c;错误的是&#xff08; &#xff09;。 A、在服务、接口与协议的区别上不很清楚 B、网络接口层本身并不是实际的一层 C、它不能区分数据链路和物理层 D、传输层对…

金融标准体系

目录 基本原则 标准体系结构图 标准明细表 金融标准体系下载地址 基本原则 需求引领、顶层设计。 坚持目标导向、问题导向、结果 导向有机统一&#xff0c;构建支撑适用、体系完善、科学合理的金融 标准体系。 全面系统、重点突出。 以金融业运用有效、保护有力、 管理高…

Linux练习作业

1.搭建dns服务器能够对自定义的正向或者反向域完成数据解析查询。 2.配置从DNS服务器&#xff0c;对主dns服务器进行数据备份 环境准备 主从服务器都需要进行的操作#关闭防火墙、SELinnux systemctl stop firewalld setenforce 0#软件安装 yum install bind -y实验一&#…

【STL_list 模拟】——打造属于自己的高效链表容器

一、list节点 ​ list是一个双向循环带头的链表&#xff0c;所以链表节点结构如下&#xff1a; template<class T>struct ListNode{T val;ListNode* next;ListNode* prve;ListNode(int x){val x;next prve this;}};二、list迭代器 2.1、list迭代器与vector迭代器区别…

《Qwen2-VL》论文精读【上】:发表于2024年10月 Qwen2-VL 迅速崛起 | 性能与GPT-4o和Claude3.5相当

1、论文地址Qwen2-VL: Enhancing Vision-Language Model’s Perception of the World at Any Resolution 2、Qwen2-VL的Github仓库地址 该论文发表于2024年4月&#xff0c;是Qwen2-VL的续作&#xff0c;截止2024年11月&#xff0c;引用数24 文章目录 1 论文摘要2 引言3 实验3.…

LiveQing视频点播流媒体RTMP推流服务功能-支持电子放大拉框放大直播视频拉框放大录像视频流拉框放大电子放大

LiveQing视频点播流媒体RTMP推流服务功能-支持电子放大拉框放大直播视频拉框放大录像视频流拉框放大电子放大 1、鉴权直播2、视频点播3、RTMP推流视频直播和点播流媒体服务 1、鉴权直播 云直播服务-》鉴权直播 -》播放 &#xff0c;左键单击可以拉取矩形框&#xff0c;放大选中…

Zypher Research:服务器抽象叙事,GameFi 赛道的下一个热点?

继链抽象、账户抽象的概念后&#xff0c;Zypher Network 进一步提出了服务器抽象的概念&#xff0c;并基于 zk 技术率先推出了应用于 Web3 游戏领域的服务器抽象方案。基于该方案&#xff0c;游戏开发者能够在完全去中心化的环境下创建、运行游戏&#xff0c;而不需要依赖传统的…

【SpringCloud详细教程】-01-一文了解微服务

精品专题&#xff1a; 01.《C语言从不挂科到高绩点》课程详细笔记 https://blog.csdn.net/yueyehuguang/category_12753294.html?spm1001.2014.3001.5482 02. 《SpringBoot详细教程》课程详细笔记 https://blog.csdn.net/yueyehuguang/category_12789841.html?spm1001.20…