C语言指针与数组
在上一篇对指针介绍的文章当中,我们初次了解到了指针,并且知道了地址和内存间的关系,懂得了如何取地址和对指针的解引用,算是对指针有了一个初步的了解。而今天让我们对指针进行更深一步的了解吧~
一、指针与数组名
我们知道,指针变量是一个用来存放地址的变量,比如我们定义一个整形变量a,那么我们想要用指针变量来存放它的地址,就需要写成int* pa = &a的形式。而这样的形式仅仅能存放一个变量的地址,那么如果我们想要存放数组的地址呢?有些人可能会说,那就int* arr = &arr呗,那么这样的写法是对的嘛?让我们来看一个例子。欸?为什么不能把arr的地址存到指针变量当中呢,而此时当我们把取地址符&去掉后,我们会发现此时代码竟然不报错了!
为什么不报错了呢?让我们反推一下,我们知道int* arr一定是一个存储地址的指针变量,而此时代码正确,就充分说明了我们传入Print函数中的arr就是一个地址。那么arr作为一个被我们定义的数组,arr数组名地址究竟是代表整个数组?还是数组的某个元素?
既然实践出真知,让我们写一段代码分成几种情况分别来看一看数组名到底代表什么吧~
int main()
{int arr[5] = { 1,2,3,4,5 };printf("%p\n", arr);printf("%p\n", &arr[0]);return 0;
}
我们会发现对数组的第一个元素取地址,与数组名的地址是相同的,那么也就说明数组名所代表的就是数组首元素的地址~......欸?或许有些人就要问了:明明之前用sz求数组元素个数的时候,sizeof(arr)/sizeof(arr[0])中sizeof(arr)表示的是整个数组的大小呀,其实sizeof(arr)是一个例外的情况。在一般情况下数组名代表的就是数组首元素地址,而有两个例外的情况:
- sizeof(数组名):在这种情况下,数组名表示的是整个数组的大小,单位是字节,一般用于计算数组的元素个数。
- &数组名:这种情况下取地址符取的是整个数组的地址。
对于第一个例外我们已经充分了解过啦,那么让我们来说一下第二个情况&数组名。
依然是不变的实践出真知~让我们敲一个代码运行一下看看。
在这里我们可以看到&arr虽然表示整个数组,但输出地址还是数组首元素的形式,没错~让我们更深一步的探索&arr与arr的差别,再一次实践出真知:
int main()
{int arr[5] = { 1,2,3,4,5 };printf("arr: %p\n", arr);printf("&arr[0]: %p\n", &arr[0]);printf("&arr: %p\n", &arr);printf("arr+1: %p\n", arr+1);printf("&arr[0]+1: %p\n", &arr[0]+1);printf("&arr+1: %p\n", &arr+1);return 0;
}
这时候就能看出&arr和arr的差别了,我们定义的数组有五个元素,数组名+1表示的是跳过一个元素,而&数组名+1跳过了整个数组,这就是arr和&arr最本质的差别啦~
二、指针访问数组与数组传参
前面我们充分了解了数组名和&数组名以及两种例外情况,现在再来让我们学习一下如何使用刚刚的知识用指针来对数组进行访问吧~我们来尝试一下使用指针对数组元素进行输入并且输出。
int main()
{int arr[10] = { 0 };int i = 0;int sz = sizeof(arr) / sizeof(arr[0]);for (i = 0; i < sz; i++){scanf("%d", arr + i);}printf("\n");for (i = 0; i < sz; i++){printf("%d ", *(arr + i));}return 0;
}
我们又学会了如何用指针访问数组,那么我们来看看数组的传参。
在进行传参的时候,是将传递的实参进行复制,然后将这个复制的值进入函数中变成一个形参。但我们想把一个数组传递进去的时候,如果把每一个元素都一个个的复制,一个个的放进函数,要是数组中元素很多,那么会使传参的工作变得繁琐,所以在进行传参时候,数组会退化为指针。
int sz(int* arr)
{int num = sizeof(arr) / sizeof(arr[0]);return num;
}
int main()
{int arr[10] = { 0 };int i = 0;int sz1 = sizeof(arr) / sizeof(arr[0]);int sz2 = sz(arr);printf("%d\n%d", sz1, sz2);return 0;
}
由这段代码我们能够清晰的看出数组退化成指针的过程。在将数组传参到函数里后,再使用sizeof(arr)/sizeof(arr[0])求出的值却是1,这就证明了数组的退化,因为在传参的时候将数组名传递给函数,本质上传递的是数组首元素的地址,而用数组首元素去除以数组首元素,得到的值理所应当就是1。
三、指针数组
指针数组,顾名思义是由多个指针类型元素组成的数组,也就是说指针数组是一个存放指针的数组。
我们在定义整型数组时,需要写成的格式是:int 数组名[元素个数]。定义字符数组时需要写成的格式是:char 数组名[元素个数]。前面是int还是char取决于想要创造的数组类型,那么由此我们能猜想出指针数组的写法:如果我们想要定义一个用于存放整型指针变量的数组,那么元素对应的类型就是int*,所以格式是:int* 数组名[元素个数]。
让我们试着用指针数组来模拟一维数组:
int main()
{int a = 1;int b = 2;int c = 3;int ar[3] = { a,b,c };int* arr[3] = { &ar[0],&ar[1],&ar[2]};int sz = sizeof(arr) / sizeof(arr[0]);int i = 0;for (i = 0; i < sz; i++){printf("地址为:%p ", *(arr + i));printf("所指向内容的值为:%d \n", *(*(arr + i)));}return 0;
}
在这段代码中我们定义了三个整形变量,再用一个整型数组来存放三个整形变量。然后我们又创造了一个整型指针数组,用来存放整型数组中各个元素的地址。需要注意的是:使用数组存放变量,输出时与arr[i]对应的格式是*(arr+i),而此时指针数组中存放的是地址,使用*(arr+i)的格式得到的是存放进指针数组中的指针变量,想要得到指针变量对应的值需要进一步对指针变量再一次解引用,写成*(*(arr + i))的形式。
我们接着再用指针数组模拟一个二维数组:
int main()
{int arr1[] = { 1,2,3 };int arr2[] = { 4,5,6 };int arr3[] = { 7,8,9 };int* parr[3] = { arr1, arr2, arr3 };//数组名是数组元素的地址,所以可存放int i = 0;int j = 0;printf("用数组形式:\n");for (i = 0; i < 3; i++){for (j = 0; j < 3; j++){printf("%d ", parr[i][j]);}printf("\n");}printf("用指针形式:\n");for (i = 0; i < 3; i++){for (j = 0; j < 3; j++){printf("%d ", *(*(parr+i)+j));}printf("\n");}return 0;
}
注:*(*(parr+i)+j)的形式,代表的是第i行第j个元素。
四、数组指针
我们之前学习指针的时候了解到:整形指针变量用来存放整形变量的地址,并且能够指向整形数据。字符指针变量用来存放字符型变量的地址,并且能够指向字符型数据。那么以此类推我们也应该能够看出,数组指针其实指的是一个指针变量,数组指针变量应该用来存放数组的地址,并且能够指向数组的数据。
数组指针的写法:
int (*p)[5];
注释:(*p)中,p先和*结合来说明p是一个指针变量,然后这个指针变量指向的是一个大小为5的整型的数组,因p为指针变量,指向数组,故而名为数组指针。
我们知道只要是指针就需要初始化,如果不初始化就会变成“野指针”,那么数组指针应该如何初始化呢?我们知道一个整形指针的初始化是将一个整形变量的地址给它:
int a = 5;
int* p = &a;
那么对应的,数组指针初始化应该就是将一个数组的地址传给数组变量,那有人会想:数组名不就是一个地址嘛,直接让int (*p)[5]=arr应该就可以了吧,不不不,大错特错。我们要清楚的了解arr,&arr,和&arr[]之间的区别。数组名确实是一个指针,但是它指的是数组的首元素地址,而这时候我们要给数组指针的是一个完整的数组,所以我们需要给它&arr:
int arr[5] = {0};
int(*p)[5] = &arr;
我们能看到&arr和p是一模一样的。所以数组指针变量用来存放数组的地址,并且能够指向数组的数据。
五、二维数组传参
其实上面在指针数组那一段中就出现了二维数组传参,这里我们更细致的讲解一下二维数组的传参到底该怎么用,该怎么写。在我们理解了指针数组和数组指针后我们能够更好地理解二维数组的传参。比如我们先定义一个三行三列的整型数组,那么就需要定义一个数组arr[3][3],用图片表达出来就像这个样子:
列\行 | 0 | 1 | 2 |
0 | 1 | 2 | 3 |
1 | 4 | 5 | 6 |
2 | 7 | 8 | 9 |
那除了arr[3][3]这种写法,我们再往回看一下刚刚讲到的指针数组的写法:int* 数组名[元素个数],并且再来看一下我们刚刚写的一段用指针数组模拟的二维数组的代码:
int arr1[] = { 1,2,3 };int arr2[] = { 4,5,6 };int arr3[] = { 7,8,9 };int* parr[3] = { arr1, arr2, arr3 };
通过这段代码我们不难看出,指针数组模拟二维数组时将二维数组分成了三个一维数组,而每一个一维数组代表每一行元素,接着我们来看这三个一维数组,每一个一维数组的类型都是int [3],而将int [3]以int* 数组名[元素个数]的形式来表示就是这样:int (*)[3]。这代表二维数组传参和指针数组传参一样,都是传递地址,所以用指针作为形参来传参也是可以的。写成代码是这样的:
void Print(int(*arr)[3], int x, int y)
{int i = 0;int j = 0;for (i = 0; i < x; i++){for (j = 0; j < y; j++){printf("%d ", *(*(arr + i) + j));//表示第i行第j列的元素}printf("\n");}
}
int main()
{int arr[3][3] = { {1,2,3},{4,5,6},{7,8,9} };Print(arr, 3, 3);return 0;
}
六、函数指针变量
通过上面学习的数组指针变量来看,不难猜出:
函数指针变量也是一种指针变量,作用是存放函数的地址,并且通过地址能调用对应的函数。可能有人看到这里,心里会想:函数也有地址吗?听过整形变量有地址,听过字符型变量有地址,甚至听过指针变量也有地址,但也没听过函数的地址呀。这次你就听到了~其实只要你把东西写进了代码中,那计算机就会为其分配新的空间。并且有关函数的地址,在之前有一篇关于函数递归的文章中我们就已经提到过了:我们每次递归一个函数,这个函数再次被调用,计算机就会为这个函数分配新的空间。通过函数递归的知识也变相证明了函数也是由地址的。多说无益,让我们用代码来尝试一下,看看函数的地址到底长什么样。
int Add(int a,int b)
{return a + b;
}
int main()
{printf("%p\n", &Add);printf("%p\n", Add);return 0;
}
编译结果是:
我们会发现&Add和Add是一样的,这也就证明了函数是有地址的,并且函数名就代表了函数的地址。那么既然要了解函数指针变量,还是先从函数指针变量的创建上开始。我们知道整型数组指针的定义格式为int(*)[ ],函数指针变量也和数组指针变量相似,但是由于函数的调用需要传参,所以整型函数指针变量的定义格式为:int(*函数名)(参数,参数)。让我们举个例子:还是拿加法函数Add来做示范:
int Add(int a,int b)
{return a + b;
}
int main()
{int a;int b;scanf("%d %d", &a, &b);int (*padd)(int, int) = Add;//也可以写成int a,int b 但也可以省略printf("%d", (*padd)(a,b));return 0;
}
由这段代码中我们能了解到,关于如何创建函数指针padd,并且如何通过函数指针padd来调用指针对应的函数。
七、函数指针数组
顾名思义,函数指针数组是一个数组,用于存放函数的指针(地址),并且可以通过对数组的访问来使用存在函数指针数组中的函数。
对于函数指针数组的定义,其实并不难,也并不需要过多的考虑,我们知道如果定义一个整型变量格式是int a;那么定义整型数组格式就是int arr[]。那我们还知道函数指针变量的定义格式为: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;
}
int Div(int a, int b)
{return a / b;
}
int main()
{int (*p[4])(int, int) = { Add,Sub,Mul,Div };int a;int b;scanf("%d %d", &a, &b);printf("%d", (*p[0])(a,b));return 0;
}
这样就成功的将加减乘除四个函数存放到函数指针数组p里面去了,并且通过*p[0],*p[1],*p[2],*p[3]来调用相应的加减乘除这四种算法。通过这段代码确实能够看懂函数指针数组的定义以及用法,但并不能充分的了解函数指针数组的作用,有人可能会觉得调用函数指针数组来使用加减乘除四个函数,还不如直接调用函数来的实在。那我们接着把这个加减乘除函数制作成一个计算机的代码,然后通过计算机代码让大家了解函数指针数组的重要性。
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;
}
int Div(int a, int b)
{return a / b;
}
int main()
{int a;int b;int k = 0;int num = 0;int m = 1;do{printf("***********************\n");printf("**** 1.Add 2.Sub ****\n");printf("**** 3.Mul 4.Div ****\n");printf("******** 0.quit *******\n");printf("***********************\n");printf("请选择:>");scanf("%d", &k);switch (k){case 1:printf("请输入两个运算数:");scanf("%d %d", &a, &b);num = Add(a, b);printf("num = %d\n", num);break;case 2:printf("请输入两个运算数:");scanf("%d %d", &a, &b);num = Sub(a, b);printf("num = %d\n", num);break;case 3:printf("请输入两个运算数:");scanf("%d %d", &a, &b);num = Mul(a, b);printf("num = %d\n", num);break;case 4:printf("请输入两个运算数:");scanf("%d %d", &a, &b);num = Div(a, b);printf("num = %d\n", num);break;case 0:printf("退出游戏");break;default:printf("选择错误,重新选择:\n");break;}} while (k);return 0;
}
这样就成功的创造出了一个用四个函数制作成的计算机,可以进行加减乘除四种运算并且可以选择开始和退出。虽然这个程序运行起来没有什么问题,但是我们仔细观看会发现,在switch选择语句中的case1,case2,case3,case4中一共只有四行代码,有三行都是重复的,这样就大大降低代码的效率,并且使代码也过多的冗余,导致代码非常长。那么这个时候就需要我们使用函数指针数组了~我们可以把四个函数都存放在函数指针数组中,通过输入的数字调用对应的函数,这样就能够实现用一段代码来替换这四段代码。
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;
}
int Div(int a, int b)
{return a / b;
}
int main()
{int a;int b;int k = 1;int (*p[5])(int x, int y) = { 0,Add,Sub,Mul,Div };do{printf("***********************\n");printf("**** 1.Add 2.Sub ****\n");printf("**** 3.Mul 4.Div ****\n");printf("******** 0.quit *******\n");printf("***********************\n");printf("请选择:>");scanf("%d", &k);if (k >= 1 && k <= 4){printf("请输入两个运算数:");scanf("%d %d", &a, &b);printf("结果为:%d\n", p[k](a, b));}else if (k == 0){printf("退出计算器");}else{printf("输入错误,请重新输入\n");}} while (k);return 0;
}
这段代码使代码量大大减少,也能够更具像的体现出函数指针数组的重要性和作用。
练习题:
字符串旋转
写一个函数,判断一个字符串是否为另外一个字符串旋转之后的字符串。
例如:给定s1 =AABCD和s2 = BCDAA,返回1,给定s1=abcd和s2=ACBD,返回0。
AABCD左旋一个字符得到ABCDAAABCD左旋两个字符得到BCDAAAABCD右旋一个字符得到DAABC
思路:等你读到这里应该已经对指针和数组之间的关系掌握了不少了,利用指针与数组和数组传参的知识解题吧~我们可以知道一个字符串左旋是将第一个字符放到最右边,然后其余字符依次往前移一位,那么我们可以先定义一个字符型变量用来接收第一个字符,然后将所有其余字符向前移动一位,最后再把第一个字符利用接收的字符型变量放在最后一位,而右旋只是改变了转移的字母,两者其实差别不大~都可以用同一个思路。
int Print(char* arr1, char* arr2, int sz)
{int i = 0;int j = 0;for (j = 0; j < sz ; j++)//判定左旋{char tmp = *arr1;for (i = 0; i < sz - 1; i++){*(arr1 + i) = *(arr1 + i + 1);}*(arr1 + i) = tmp;if (strcmp(arr1, arr2) == 0)return 1;}for (j = 0; j < sz; j++)//判定右旋{char tmp = *(arr1 + sz - 1);for (i = sz - 1; i > 0; i--){*(arr1 + i) = *(arr1 + i - 1);}*arr1 = tmp;if (strcmp(arr1, arr2) == 0)return 1;}
}
int main()
{char arr1[] = "AABCD";char arr2[] = "BCDAA";//左旋可得char arr3[] = "DAABC";//右旋可得char arr4[] = "ABBCD";//得不到int sz = strlen(arr1);if ((Print(arr1, arr2, sz)) == 1){printf("YES\n");}else{printf("NO\n");}if ((Print(arr1, arr3, sz)) == 1){printf("YES\n");}else{printf("NO\n");}if ((Print(arr1, arr4, sz)) == 1){printf("YES\n");}else{printf("NO\n");}return 0;
}
注:有人可能将右旋的代码写成了以下的格式:
for (j = 0; j < sz; j++)//判定右旋{char tmp = *(arr1 + sz - 1);for (i = 0; i < sz - 1; i++){*(arr1 + i + 1) = *(arr1 + i);}*arr1 = tmp;if (strcmp(arr1, arr2) == 0)return 1;}
看似正确,也同样和左旋是一样的方法,但是!右旋是将所有字母都向后移一位,如果还让每一个字母的下一位等于上一位,比如AABCD,第一次转换变成AABCD,第二次就变成了AAACD,第三次变成AAAAD,所以我们需要转变一下思路,让移动从右往左~(其实还有使用strcpy,strcat的方法,更加的简便轻松,但是这不是我们这次要讲的东西,那么欲知后事如何,请听下回分解啦,我们下次再见~~~)
相关文章:

C语言指针与数组
在上一篇对指针介绍的文章当中,我们初次了解到了指针,并且知道了地址和内存间的关系,懂得了如何取地址和对指针的解引用,算是对指针有了一个初步的了解。而今天让我们对指针进行更深一步的了解吧~ 一、指针与数组名 我们知道&am…...

Stream 33
package Array.collection;import java.util.*; import java.util.stream.Stream;public class stream1 {public static void main(String[] args) {//、如何茯取List集合的Stream流?List<String> names new ArrayList<>();Collections. addAll(names,"方法…...
【Python Loguru】实现日志工具和日志饶接
【Python Loguru】实现日志工具和日志饶接 说明 代码使用第三方库Loguru实现了logging工具,可以直接改名字后就使用,并实现了日志绕接,使用前需要先安装loguru工具。 Code import os import json from loguru import logger# config_file…...

【Linux】-----进度条小程序
目录 前言 基本知识 Ⅰ、回车和换行 Ⅱ、缓冲区 两个有意思的现象 简单定义 刷新缓冲区 简易倒计时程序 进度条代码 多文件下makefile写法 一代(无任何场景) procs1.h代码 procs1.c代码 主函数main1.c 一代运行结果: 二代 (搭配下载场景) procs2.c代…...

普通人有必要学Python吗?学了之后能做什么?
目录 首先来说一下极其推荐的方向: 1、数据分析 2、科学计算 3、大数据框架 4、脚本开发 5、爬虫 6、Web框架 总结: 如果你还没有开始使用Python,答应我,把这个回答看完,如果你真的学习并深入使用过Python&…...

2023-2024年 Java开发岗面试题经验分享
在各行各业中,面试前我们总会思索一个问题:究竟什么样的求职者能获得面试官的青睐?作为求职者,我们又该如何准备,以应对各种面试官的挑战?在这激烈的竞争里,如何才能让自己从众多应聘者中脱颖而…...
JavaScript中URL和Blob
JavaScript中URL和Blob 常用于处理文件数据、图像数据、音频数据等。Blob对象通常用于在客户端处理文件,如上传文件、下载文件、处理图像等操作。Blob对象可以通过Blob构造函数创建,也可以通过其他方式获取,比如从File对象中获取。 使用场景…...
平舌、翘舌音学习: z、c、s--zh、ch、sh
平舌音翘舌音不分怎么办? 尝试整理了,如下一些材料: 一、 策略篇: 一年级拼音如何区分掌握:平舌音和翘舌音? 喜马拉雅, 平舌音翘舌音教学:普通话声母zh以及zh ch sh与z c s的发音练…...

Windows(Win10、Win11)本地部署开源大模型保姆级教程
目录 前言1.安装ollama2.安装大模型3.安装HyperV4.安装Docker5.安装聊天界面6.总结 点我去AIGIS公众号查看本文 本期教程用到的所有安装包已上传到百度网盘 链接:https://pan.baidu.com/s/1j281UcOF6gnOaumQP5XprA 提取码:wzw7 前言 最近开源大模型可谓闹…...

快速排序(下)
快速排序(下) 前言 在上一篇文章中我们了解了快速排序算法,但那是Hoare的版本,其实还有别的版本:一种是挖坑法,它们的区别主要在于如何找基准值。霍尔的版本思路难理解但代码好理解,挖坑法则是…...

LazyLLM:长上下文场景下提高LLM推理效率
LazyLLM旨在优化大型语言模型(LLM)在处理长文本语境下的推理效率。传统上,LLM的推理过程分为预填充和解码两个阶段,其中预填充阶段负责计算并存储输入提示的所有token的键值(KV)缓存,这一步骤在…...

PDF文件点击打印无反应?是何原因造成能解决吗?
PDF无法打印怎么处理?在我们工作中,经常会遇见各种各样的文件问题,当我们想要将PDF文件打印出来纸质版使用,却不知什么原因,显示PDF无法打印,这时应该怎么处理呢? 一般情况下,PDF文件…...

初学者友好!从零到一快速上手PyCharm安装的超详细图解+避坑指南教程
一,pycharm的官网下载 下载地址:www.jetbrains.com/pycharm/ 本文将从 Python解释器安装到Pycharm专业版安装和配置汉化等使用都进行了详细介绍,希望能够帮助到大家。 Python解释器&Pycharm安装包&Pycharm破姐插件我都打包好了。 …...

AI大模型需要什么样的数据?
数据将是未来AI大模型竞争的关键要素 人工智能发展的突破得益于高质量数据的发展。例如,大型语言模型的最新进展依赖于更高质量、更丰富的训练数据集:与GPT-2相比,GPT-3对模型架构只进行了微小的修改,但花费精力收集更大的高质量…...

Java每日一练_模拟面试题1(死锁)
一、死锁的条件 死锁通常发生在两个或者更多的线程相互等待对方释放资源,从而导致它们都无法继续执行。死锁的条件通常被描述为四个必要条件,也就是互斥条件、不可剥夺条件、占有并等待条件和循环等待条件。 互斥条件:资源不能被共享&#x…...

第三方库认识- Mysql 数据库 API 认识
文章目录 一、msyql数据库API接口1.初始化mysql_init()——mysql_init2.链接数据库mysql_real_connect——mysql_real_connect3.设置当前客户端的字符集——mysql_set_character_set4.选择操作的数据库——mysql_select_db5.执行sql语句——mysql_query6.保存查询结果到本地——…...

Python兼职接单全攻略:掌握技能,拓宽收入渠道
引言 随着Python在数据处理、Web开发、自动化办公、爬虫技术等多个领域的广泛应用,越来越多的人开始利用Python技能进行兼职接单,以此拓宽收入渠道。本文将详细介绍Python兼职接单的注意事项、所需技能水平、常见单子类型、接单途径及平台,帮…...

一键编译并启动一个 ARM Linux qemu 虚拟机
需要事先自己编译 qemu-system-arm 可执行文件; 1,编译创建ARM 虚拟机 #!/usr/bin/bash sudo lssudo apt-get install gcc-arm-linux-gnueabi#wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.10.tar.gztar zxf linux-kernel-v5.10…...

KubeVirt虚拟机存储及网络卸载加速解决方案
1. 方案背景 1.1. KubeVirt介绍 随着云计算和容器技术的飞速发展,Kubernetes已成为业界公认的容器编排标准,为用户提供了强大、灵活且可扩展的平台来部署和管理各类应用。然而,在企业的实际应用中,仍有许多传统应用或遗留系统难…...

JVM—对象已死?
参考资料:深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明 在堆里面存放着 Java 世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”。 1、如何判…...
基于大模型的 UI 自动化系统
基于大模型的 UI 自动化系统 下面是一个完整的 Python 系统,利用大模型实现智能 UI 自动化,结合计算机视觉和自然语言处理技术,实现"看屏操作"的能力。 系统架构设计 #mermaid-svg-2gn2GRvh5WCP2ktF {font-family:"trebuchet ms",verdana,arial,sans-…...

阿里云ACP云计算备考笔记 (5)——弹性伸缩
目录 第一章 概述 第二章 弹性伸缩简介 1、弹性伸缩 2、垂直伸缩 3、优势 4、应用场景 ① 无规律的业务量波动 ② 有规律的业务量波动 ③ 无明显业务量波动 ④ 混合型业务 ⑤ 消息通知 ⑥ 生命周期挂钩 ⑦ 自定义方式 ⑧ 滚的升级 5、使用限制 第三章 主要定义 …...

CMake基础:构建流程详解
目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...

Python爬虫(一):爬虫伪装
一、网站防爬机制概述 在当今互联网环境中,具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类: 身份验证机制:直接将未经授权的爬虫阻挡在外反爬技术体系:通过各种技术手段增加爬虫获取数据的难度…...

微信小程序云开发平台MySQL的连接方式
注:微信小程序云开发平台指的是腾讯云开发 先给结论:微信小程序云开发平台的MySQL,无法通过获取数据库连接信息的方式进行连接,连接只能通过云开发的SDK连接,具体要参考官方文档: 为什么? 因为…...

有限自动机到正规文法转换器v1.0
1 项目简介 这是一个功能强大的有限自动机(Finite Automaton, FA)到正规文法(Regular Grammar)转换器,它配备了一个直观且完整的图形用户界面,使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...
PAN/FPN
import torch import torch.nn as nn import torch.nn.functional as F import mathclass LowResQueryHighResKVAttention(nn.Module):"""方案 1: 低分辨率特征 (Query) 查询高分辨率特征 (Key, Value).输出分辨率与低分辨率输入相同。"""def __…...
Go 语言并发编程基础:无缓冲与有缓冲通道
在上一章节中,我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道,它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好࿰…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
Go 并发编程基础:通道(Channel)的使用
在 Go 中,Channel 是 Goroutine 之间通信的核心机制。它提供了一个线程安全的通信方式,用于在多个 Goroutine 之间传递数据,从而实现高效的并发编程。 本章将介绍 Channel 的基本概念、用法、缓冲、关闭机制以及 select 的使用。 一、Channel…...