c语言-指针
指针详解
这段时间在看 Linux内核,深觉C语言功底不扎实,很多代码都看不太懂,深入学习巩固C语言的知识很有必要。先从指针开始。
什么是指针
C语言里,变量存放在内存中,而内存其实就是一组有序字节组成的数组,每个字节有唯一的内存地址。CPU 通过内存寻址对存储在内存中的某个指定数据对象的地址进行定位。这里,**数据对象是指存储在内存中的一个指定数据类型的数值或字符串,它们都有一个自己的地址,而指针便是保存这个地址的变量。**也就是说:指针是一种保存变量地址的变量。
前面已经提到内存其实就是一组有序字节组成的数组,数组中,每个字节大大小固定,都是 8bit。对这些连续的字节从0开始进行编号,每个字节都有唯一的一个编号,这个编号就是内存地址。示意如下图:

这是一个 4GB 的内存,可以存放 2^32 个字节的数据。左侧的连续的十六进制编号就是内存地址,每个内存地址对应一个字节的内存空间。而指针变量保存的就是这个编号,也即内存地址。
为什么要使用指针
在C语言中,指针的使用非常广泛,因为使用指针往往可以生成更高效、更紧凑的代码。总的来说,使用指针有如下好处:
1)指针的使用使得不同区域的代码可以轻易的共享内存数据,这样可以使程序更为快速高效;
2)C语言中一些复杂的数据结构往往需要使用指针来构建,如链表、二叉树等;
3)C语言是传值调用,而有些操作传值调用是无法完成的,如通过被调函数修改调用函数的对象,但是这种操作可以由指针来完成,而且并不违背传值调用。
如何声明一个指针
声明并初始化一个指针
指针其实就是一个变量,指针的声明方式与一般的变量声明方式没太大区别:
int *p; // 声明一个 int 类型的指针 p
char *p // 声明一个 char 类型的指针 p
int *arr[10] // 声明一个指针数组,该数组有10个元素,其中每个元素都是一个指向 int 类型对象的指针
int (*arr)[10] // 声明一个数组指针,该指针指向一个 int 类型的一维数组
int **p; // 声明一个指针 p ,该指针指向一个 int 类型的指针
指针的声明比普通变量的声明多了一个一元运算符 “*”。运算符 “*” 是间接寻址或者间接引用运算符。当它作用于指针时,将访问指针所指向的对象。在上述的声明中:p 是一个指针,保存着一个地址,该地址指向内存中的一个变量; *p 则会访问这个地址所指向的变量。
声明一个指针变量并不会自动分配任何内存。在对指针进行间接访问之前,指针必须进行初始化:或是使他指向现有的内存,或者给他动态分配内存,否则我们并不知道指针指向哪儿,这将是一个很严重的问题,稍后会讨论这个问题。初始化操作如下:
/* 方法1:使指针指向现有的内存 */
int x = 1;
int *p = &x; // 指针 p 被初始化,指向变量 x ,其中取地址符 & 用于产生操作数内存地址/* 方法2:动态分配内存给指针 */
int *p;
p = (int *)malloc(sizeof(int) * 10); // malloc 函数用于动态分配内存
free(p);
// free 函数用于释放一块已经分配的内存,常与 malloc 函数一起使用,要使用这两个函数需要头文件 stdlib.h
指针的初始化实际上就是给指针一个合法的地址,让程序能够清楚地知道指针指向哪儿。
未初始化和非法的指针
如果一个指针没有被初始化,那么程序就不知道它指向哪里。它可能指向一个非法地址,这时,程序会报错,在Linux上,错误类型是Segmentation fault(core dumped),提醒我们段违例或内存错误。它也可能指向一个合法地址,实际上,这种情况更严重,你的程序或许能正常运行,但是这个没有被初始化的指针所指向的那个位置的值将会被修改,而你并无意去修改它。用一个例子简单的演示一下:
#include "stdio.h"int main(){int *p;*p = 1;printf("%d\n",*p);return 0;
}
这个程序可以编译通过,但是运行的话会报错,报错信息如下:
[root@hly_centos learn]# gcc -o point point.c
[root@hly_centos learn]# ./point
Segmentation fault
[root@hly_centos learn]#
要想使这个程序运行起来,需要先对指针p进行初始化:
#include "stdio.h"int main(){int x = 1; int *p = &x;printf("%d\n",*p);*p = 2;printf("%d\n",*p);return 0;
}
这段代码的输出结果如下:
[root@hly_centos learn]# gcc -o point point.c
[root@hly_centos learn]# ./point
1
2
可以看到,对指针进行初始化后,便可以正常对指针进行赋值了。
NULL指针
NULL 指针是一个特殊的指针变量,表示不指向任何东西。可以通过给一个指针赋一个零值来生成一个NULL指针。
#include "stdio.h"int main(){int *p = NULL;printf("p的地址为%d\n",p);return 0;
}/**************** 程序输出:* p的地址为0
***************/
可以看到指针指向内存地址0。在大多数的操作系统上,程序不允许访问地址为0的内存,因为该内存是为操作系统保留的。但是,内存地址0有一个特别重要的意义,它表明该指针指向一个不可访问的内存位置。
指针的运算
C 指针的算术运算只限于两种形式:
指针+/-整数 :
可以对指针变量p进行 p++、p--、p + i 等操作,所得结果也是一个指针,只是指针所指向的内存地址相比于p所指的内存地址前进或者后退了i个操作数。用一张图来说明一下:

在上图中,10000000等是内存地址的十六进制表示(数值是假定的),p 是一个int类型的指针,指向内存地址 0x10000008 处。则p++将指向与p相邻的下一个内存地址,由于int型数据占4个字节,因此p++所指的内存地址为 1000000b。其余类推。不过要注意的是,这种运算并不会改变指针变量p自身的地址,只是改变了它所指向的地址。
指针-指针
只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针。两个指针相减的结果的类型是 ptrdiff_t,它是一种有符号整数类型。**减法运算的值是两个指针在内存中的距离(以数组元素的长度为单位,而不是以字节为单位),因为减法运算的结果将除以数组元素类型的长度。**举个例子:
#include "stdio.h"int main(){int a[10] = {1,2,3,4,5,6,7,8,9,0};int sub;int *p1 = &a[2];int *p2 = &a[8];sub = p2-p1; printf("%d\n",sub); // 输出结果为 6return 0;
}
指针与数组
在C语言中,指针与数组之间的关系十分密切。实际上,许多可以用数组完成的工作都可以使用指针来完成。一般来说,用指针编写的程序比用数组编写的程序执行速度快,但另一方面,用指针实现的程序理解起来稍微困难一些。
指针与数组的关系
我们先声明一个数组:
int a[10]; // 声明一个int类型的数组,这个数组有10个元素
我们可以用 a[0]、a[1]、...、a[9] 来表示这个数组中的10个元素,这10个元素是存储在一段连续相邻的内存区域中的。
接下来,我们再声明一个指针:
int *p; // 声明一个int类型的指针变量
p 是一个指针变量,指向内存中的一个区域。如果我们对指针p做如下的初始化:
p = &a[0]; // 对指针进行初始化,p将指向数组 a 的第 1 个元素 a[0]
我们知道,对指针进行自增操作会让指针指向与当前元素相邻的下一个元素,即*(p + 1)将指向 a[1] ;同样的,*(p + i)将指向a[i]。因此,我们可以使用该指针来遍历数组a[10] 的所有元素。可以看到,数组下标与指针运算之间的关系是一一对应的。而根据定义,**数组类型的变量或表达式的值是该数组第1个元素的地址,且数组名所代表的的就是该数组第1个元素的地址,**故,上述赋值语句可以直接写成:
p = a; // a 为数组名,代表该数组最开始的一个元素的地址
很显然,一个通过数组和下标实现的表达式可以等价地通过指针及其偏移量来实现,这就是数组和指针的互通之处。但有一点要明确的是,数组和指针并不是完全等价,**指针是一个变量,而数组名不是变量,它数组中第1个元素的地址,数组可以看做是一个用于保存变量的容器。**更直接的方法,我们可以直接看二者的地址,并不一样:
#include "stdio.h"
int main(){int x[10] = {1,2,3,4,5,6,7,8,9,0};int *p = x;printf("x的地址为:%p\n",x);printf("x[0]的地址为:%p\n",&x[0]);printf("p的地址为:%p\n",&p); // 打印指针 p 的地址,并不是指针所指向的地方的地址p += 2;printf("*(p+2)的值为:%d\n",*p); // 输出结果为 3,*(p+2)指向了 x[2]return 0;
}
结果如下:
[root@hly_centos learn]# gcc -o point point.c
[root@hly_centos learn]# ./point
x的地址为:0x7ffe02b98a60
x[0]的地址为:0x7ffe02b98a60
p的地址为:0x7ffe02b98a58
*(p+2)的值为:3
可以看到,x的值与x[0]的地址是一样的,也就是说数组名即为数组中第1个元素的地址。实际上,打印&x后发现,x 的地址也是这个值。而x的地址与指针变量p的地址是不一样的。故而数组和指针并不能完全等价。
(笔者注:上述输出结果是在 centos7 64bit 的环境下使用 gcc 编译器得到的,可以看到地址是一个12位的十六进制数,转换成二进制是48位,也就是说寻址空间有 256TB,但是笔者的电脑只有 8GB 内存,猜测是不是由于 linux 系统开启了内存分页机制,这里寻址的是虚拟地址?另外,在Windows下使用 vs2015 编译运行的话,则输出结果是一个 8位的十六进制数,也就是32位二进制,寻址空间为 4GB)
指针数组
指针是一个变量,而数组是用于存储变量的容器,因此,指针也可以像其他变量一样存储在数组中,也就是指针数组。 指针数组是一个数组,数组中的每一个元素都是指针。声明一个指针数组的方法如下:
int *p[10]; // 声明一个指针数组,该数组有10个元素,其中每个元素都是一个指向int类型的指针
在上述声明中,由于[]的优先级比*高,故p先与[]结合,成为一个数组 p[];再由int *指明这是一个int类型的指针数组,数组中的元素都是int类型的指针。数组的第i个元素是 *p[i],而 p[i] 是一个指针。由于指针数组中存放着多个指针,操作灵活,在一些需要操作大量数据的程序中使用,可以使程序更灵活快速。
数组指针
数组指针是一个指针,它指向一个数组。声明一个数组指针的方法如下:
int (*p)[10]; // 声明一个数组指针 p ,该指针指向一个数组
由于()的优先级最高,所以p是一个指针,指向一个int类型的一维数组,这个一维数组的长度是 10,这也是指针p 的步长。也就是说,执行p+1时,p 要跨过1个int 型数据的长度。数组指针与二维数组联系密切,可以用数组指针来指向一个二维数组,如下:
#include "stdio.h"int main(){int arr[2][3] = {1,2,3,4,5,6}; // 定义一个二维数组并初始化int (*p)[3]; // 定义一个数组指针,指针指向一个含有3个元素的一维数组p = arr; // 将二维数组的首地址赋给 p,此时 p 指向 arr[0] 或 &arr[0][0]printf("%d\n",(*p)[0]); // 输出结果为 1p++; // 对 p 进行算术运算,此时 p 将指向二维数组的下一行的首地址,即 &arr[1][0]printf("%d\n",(*p)[1]); // 输出结果为5return 0; }
指针与结构
简单介绍一下结构
结构是一个或多个变量的集合,这些变量可能为不同的类型,为了处理的方便而将这些变量组织在一个名字之下。由于结构将一组相关的变量看做一个单元而不是各自独立的实体,因此结构有助于组织复杂的数据,特别是在大型的程序中。声明一个结构的方式如下:
struct message{ // 声明一个结构 messagechar name[10]; // 成员int age;int score;
};typedef struct message s_message; // 类型定义符 typedefs_message mess = {"tongye",23,83}; // 声明一个 struct message 类型的变量 mess,并对其进行初始化
----------------------------------------------------------------------------------
/* 另一种更简便的声明方法 */
typedef struct{char name[10];int age;int score;
}message;
可以使用 结构名.成员 的方式来访问结构中的成员,如下:
#include "stdio.h"int main(){printf("%s\n",mess.name); // 输出结果:tongyeprintf("%d\n",mess.age); // 输出结果:23return 0;
}
结构指针
结构指针是指向结构的指针,以上面的结构为例,可以这样定义一个结构指针:
s_message *p; // 声明一个结构指针 p ,该指针指向一个 s_message 类型的结构
p = &mess; // 对结构指针的初始化与普通指针一样,也是使用取地址符 &
C语言中使用->操作符来访问结构指针的成员,举个例子:
#include "stdio.h"typedef struct{char name[10];int age;int score;
}message;int main(){message mess = {"tongye",23,83};message *p = &mess;printf("%s\n",p->mess); // 输出结果为:tongyeprintf("%d\n",p->score); // 输出结果为:83return 0;
}
指针与函数
**C语言的所有参数均是以“传值调用”的方式进行传递的,这意味着函数将获得参数值的一份拷贝。**这样,函数可以放心修改这个拷贝值,而不必担心会修改调用程序实际传递给它的参数。
指针作为函数的参数
传值调用的好处是是被调函数不会改变调用函数传过来的值,可以放心修改。但是有时候需要被调函数回传一个值给调用函数,这样的话,传值调用就无法做到。为了解决这个问题,可以使用传指针调用。**指针参数使得被调函数能够访问和修改主调函数中对象的值。**用一个例子来说明:
#include "stdio.h"void swap1(int a,int b) // 参数为普通的 int 变量
{int temp;temp = a;a = b;b = temp;
}void swap2(int *a,int *b) // 参数为指针,接受调用函数传递过来的变量地址作为参数,对所指地址处的内容进行操作
{int temp; // 最终结果是,地址本身并没有改变,但是这一地址所对应的内存段中的内容发生了变化,即x,y的值发生了变化temp = *a;*a = *b;*b = temp;
}int main()
{int x = 1,y = 2;swap1(x,y); // 将 x,y 的值本身作为参数传递给了被调函数printf("%d %5d\n",x,y); // 输出结果为:1 2swap(&x,&y); // 将 x,y 的地址作为参数传递给了被调函数,传递过去的也是一个值,与传值调用不冲突printf("%d %5d\n",x,y); // 输出结果为:2 1return 0;
}
指针函数
指针函数: 顾名思义,它的本质是一个函数,不过它的返回值是一个指针。其声明的形式如下所示:
ret *func(args, ...);
其中,func是一个函数,args是形参列表,ret *作为一个整体,是 func函数的返回值,是一个指针的形式。
下面举一个具体的实例来做说明:
文件:pointer_func.c# include <stdio.h>
# include <stdlib.h>int * func_sum(int n)
{if (n < 0){printf("error:n must be > 0\n");exit(-1);}static int sum = 0;int *p = ∑for (int i = 0; i < n; i++){sum += i;}return p;
}int main(void)
{int num = 0;printf("please input one number:");scanf("%d", &num);int *p = func_sum(num); printf("sum:%d\n", *p);return 0;
}
上例就是一个指针函数的例子,其中,int * func_sum(int n)就是一个指针函数, 其功能十分简单,是根据传入的参数n,来计算从0到n的所有自然数的和,其结果通过指针的形式返回给调用方。
以上代码的运行结果如下所示:
[root@hly_centos learn]# gcc -o point_func point_func.c
[root@hly_centos learn]# ./point_func
please input one number:100
sum:4950
如果上述代码使用普通的局部变量来实现,也是可以的,如下所示:
文件:pointer_func2.c# include <stdio.h>
# include <stdlib.h>int func_sum2(int n)
{ if (n < 0){ printf("error:n must be > 0\n");exit(-1);}int sum = 0;int i = 0;for (i = 0; i < n; i++){ sum += i;}return sum;
}int main(void)
{int num = 0;printf("please input one number:");scanf("%d", &num);int ret = func_sum2(num);printf("sum2:%d\n", ret);return 0;
}
本案例中,func_sum2函数的功能与指针函数所实现的功能完全一样。
[root@hly_centos learn]# gcc -o point_func point_func.c
[root@hly_centos learn]# ./point_func
please input one number:100
sum:4950
不过在使用指针函数时,需要注意一点,相信细心地读者已经发现了,对比func_sum和func_sum2函数,除了返回值不一样之外,还有一个不同的地方在于,在func_sum中,变量sum使用的是静态局部变量,而func_sum2函数中,变量sum使用的则是普通的变量。
如果我们把指针函数的sum定义为普通的局部变量,会是什么结果呢?不妨来试验一下:
文件:pointer_func3.c# include <stdio.h>
# include <stdlib.h>int * func_sum(int n)
{if (n < 0){printf("error:n must be > 0\n");exit(-1);}int sum = 0;int *p = ∑for (int i = 0; i < n; i++){sum += i;}return p;
}int main(void)
{int num = 0;printf("please input one number:");scanf("%d", &num);int *p = func_sum(num); printf("sum:%d\n", *p);return 0;
}
执行以上程序,发现仍然能得到正确的结果:
[root@hly_centos learn]# gcc -o point_func point_func.c
[root@hly_centos learn]# ./point_func
please input one number:100
sum:4950
可如果我们把main函数里面稍微改动一下:
int main(void)
{int num = 0;printf("please input one number:");scanf("%d", &num);int *p = func_sum(num);printf("wait for a while...\n"); //此处加一句打印printf("sum:%d\n", *p);return 0;
}
我们在输出sum之前打印一句话,这时看到得到的结果完全不是我们预先想象的样子,得到的并不是我们想要的答案。
[root@hly_centos learn]# gcc -o point_func point_func.c
[root@hly_centos learn]# ./point_func
please input one number:100
wait for a while...
sum:0
为什么会出现上面的结果呢?
其实原因在于,一般的局部变量是存放于栈区的,当函数结束,栈区的变量就会释放掉,如果我们在函数内部定义一个变量,在使用一个指针去指向这个变量,当函数调用结束时,这个变量的空间就已经被释放,这时就算返回了该地址的指针,也不一定会得到正确的值。上面的示例中,在返回该指针后,立即访问,的确是得到了正确的结果,但这只是十分巧合的情况,如果我们等待一会儿再去访问该地址,很有可能该地址已经被其他的变量所占用,这时候得到的就不是我们想要的结果。甚至更严重的是,如果因此访问到了不可访问的内容,很有可能造成段错误等程序崩溃的情况。
因此,在使用指针函数的时候,一定要避免出现返回局部变量指针的情况。
那么为什么用了static就可以避免这个问题呢?
原因是一旦使用了static去修饰变量,那么该变量就变成了静态变量。而静态变量是存放在数据段的,它的生命周期存在于整个程序运行期间,只要程序没有结束,该变量就会一直存在,所以该指针就能一直访问到该变量。
因此,还有一种解决方案是使用全局变量,因为全局变量也是放在数据段的,但是并不推荐使用全局变量。
函数指针
与指针函数不同,函数指针的本质是一个指针,该指针的地址指向了一个函数,所以它是指向函数的指针。
我们知道,函数的定义是存在于代码段,因此,每个函数在代码段中,也有着自己的入口地址,函数指针就是指向代码段中函数入口地址的指针。
其声明形式如下所示:
ret (*p)(args, ...);
其中,ret为返回值,*p作为一个整体,代表的是指向该函数的指针,args为形参列表。其中p被称为函数指针变量 。
关于函数指针的初始化与数组类似,在数组中,数组名即代表着该数组的首地址,函数也是一样,函数名即是该数组的入口地址,因此,函数名就是该函数的函数指针。
因此,我们可以采用如下的初始化方式:
函数指针变量 = 函数名;
下面还是以一个简单的例子来具体说明一下函数指针的应用:
文件:func_pointer.c#include <stdio.h>int max(int a, int b)
{return a > b ? a : b;
}int main(void)
{int (*p)(int, int); //函数指针的定义//int (*p)(); //函数指针的另一种定义方式,不过不建议使用//int (*p)(int a, int b); //也可以使用这种方式定义函数指针p = max; //函数指针初始化int ret = p(10, 15); //函数指针的调用//int ret = (*max)(10,15);//int ret = (*p)(10,15);//以上两种写法与第一种写法是等价的,不过建议使用第一种方式printf("max = %d \n", ret);return 0;
}
上面这个函数的功能也十分简单,就是求两个数中较大的一个数。值得注意的是通过函数指针调用的方式。
首先代码里提供了3种函数指针定义的方式,这三种方式都是正确的,比较推荐第一种和第三种定义方式。然后对函数指针进行初始化,前面已经提到过了,直接将函数名赋值给函数指针变量名即可。
上述代码运行的结果如下:
[root@hly_centos learn]# gcc -o func_point func_point.c
[root@hly_centos learn]# ./func_point
max = 15
调用的时候,既可以直接使用函数指针调用,也可以通过函数指针所指向的值去调用。(*p)所代表的就是函数指针所指向的值,也就是函数本身,这样调用自然不会有问题。
为什么要使用函数指针?
那么,有不少人就觉得,本来很简单的函数调用,搞那么复杂干什么?其实在这样比较简单的代码实现中不容易看出来,当项目比较大,代码变得复杂了以后,函数指针就体现出了其优越性。
举个例子,如果我们要实现数组的排序,我们知道,常用的数组排序方法有很多种,比如快排,插入排序,冒泡排序,选择排序等,如果不管内部实现,你会发现,除了函数名不一样之外,返回值,包括函数入参都是相同的,这时候如果要调用不同的排序方法,就可以使用指针函数来实现,我们只需要修改函数指针初始化的地方,而不需要去修改每个调用的地方(特别是当调用特别频繁的时候)。
回调函数
函数指针的一个非常典型的应用就是回调函数。
什么是回调函数?
回调函数就是一个通过指针函数调用的函数。其将函数指针作为一个参数,传递给另一个函数。
回调函数并不是由实现方直接调用,而是在特定的事件或条件发生时由另外一方来调用的。同样我们来看一个回调函数的例子:
文件:callback.c#include<stdio.h>
#include<stdlib.h>//函数功能:实现累加求和
int func_sum(int n)
{int sum = 0;if (n < 0){printf("n must be > 0\n");exit(-1);}for (int i = 0; i < n; i++){sum += i;}return sum;
}//这个函数是回调函数,其中第二个参数为一个函数指针,通过该函数指针来调用求和函数,并把结果返回给主调函数
int callback(int n, int (*p)(int))
{return p(n);
}int main(void)
{int n = 0;printf("please input number:");scanf("%d", &n);printf("the sum from 0 to %d is %d\n", n, callback(n, func_sum)); //此处直接调用回调函数,而不是直接调用func_sum函数return 0;
}
上面这个简单的demo就是一个比较典型的回调函数的例子。在这个程序中,回调函数callback无需关心func_sum是怎么实现的,只需要去调用即可。
这样的好处就是,如果以后对求和函数有优化,比如新写了个func_sum2函数的实现,我们只需要在调用回调函数的地方将函数指针指向func_sum2即可,而无需去修改callback函数内部。
以上代码的输出结果如下:
[root@hly_centos learn]# gcc -o call_back call_back.c
[root@hly_centos learn]# ./call_back
please input number:10
the sum from 0 to 10 is 45
回调函数广泛用于开发场景中,比如信号函数、线程函数等,都使用到了回调函数的知识。
相关文章:
c语言-指针
指针详解 这段时间在看 Linux内核,深觉C语言功底不扎实,很多代码都看不太懂,深入学习巩固C语言的知识很有必要。先从指针开始。 什么是指针 C语言里,变量存放在内存中,而内存其实就是一组有序字节组成的数组&…...
Jenkins集成SonarQube实现代码质量检查
文章目录 一、前提配置1.1 安装及配置SonarQube Scanner插件1.2 配置SonarQube servers 二、非流水线集成SonarQube1.1 配置非流水线任务 三、流水线集成SonarQube 一、前提配置 1.1 安装及配置SonarQube Scanner插件 (1) 点击【系统管理】>【插件管理】>【可选插件】搜…...
2023 谷歌I/O发布会新AI,PALM 2模型要反超GPT-4,一雪前耻!
文章目录 1 前言2 Google I/O 发布者大会3 PaLM 2模型3 Bard项目4 其他AI工具4.1 AI 图片编辑 Magic Editor4.2 Duet AI 办公4.3 Universal Translator 翻译工具4.4 Google 沉浸式导航4.5 Google 搜索引擎 5 讨论 1 前言 每年必看两大会,苹果发布会和谷歌发布会&am…...
MySQL和Redis如何保证数据一致性?
前言 由于缓存的高并发和高性能已经在各种项目中被广泛使用,在读取缓存这方面基本都是一致的,大概都是按照下图的流程进行操作: 但是在更新缓存方面,是更新完数据库再更新缓存还是直接删除缓存呢?又或者是先删除缓存再…...
Markdown使用(超详细)
(HBuilderX) 掌握md及HBuilderX对md的强大支持。如果没有点右键设置自动换行,可按Alt滚轮横向滚动查看。 很多人只把markdown用于网络文章发表,这糟蹋了markdown。 markdown不止是HTML的简化版,更重要的是txt的升级版…...
yolov5实现扑克牌识别的产品化过程
文章目录 介绍项目下载硬件准备软件环境素材获取自行获取素材网盘获取图片标注模型训练窗口截图窗口截图(HWND)桌面截图wgc方法最终采用的方式WGC使用方法如何保存灰度图片python 如何加载dll库图片推理扑克牌逻辑ui编写模型加密软件授权软件加密软件打包安装包制作...
第07讲:Java High Level Client,读写 ES 利器
SkyWalking OAP 后端可以使用多种存储对数据进行持久化,例如 MySQL、TiDB 等,默认使用 ElasticSearch 作为持久化存储,在后面的源码分析过程中也将以 ElasticSearch 作为主要存储进行分析。 ElasticSearch 基本概念 本课时将快速介绍一下 E…...
dockerfile暴力处理配置文件外提
前言: 一般来说,springboot打成的jar运行时,同目录/config目录下放application.yml文件会被进行加载,然后通过设置docker映射出宿主机即可做到配置文件外配的效果,但很多时候别的配置文件做不到这种效果,说…...
如何快速给出解释——正交矩阵子矩阵的特征值的模必然不大于1
Memory 首先快速回忆一下正交矩阵的定义: A为n阶实矩阵,且满足A‘AE或是说AA’E,那么A为正交矩阵。 (啊,多么简洁的定义) 其次快速想到它的性质: ① 实特征值必然 或 其他复数…...
c语言-位运算
位运算小结 位运算不管是在C语言中,或者其他语言,都是经常会用到的,所以本文也就不固定以某种语言来举例子了,原始点就从0、1开始。位运算主要包括按位与(&)、按位或(|)、按位异或(^)、取反(~)、左移(<<)、右移(>…...
【Android学习专题】安卓样式学习(学习内容记录)
学习记录内容来自《Android编程权威指南(第三版)》 样式调整和添加 调整颜色资源(res/values/colors.xml) 格式: 添加样式(res/values/styles.xml),(创建BeatBox项目时…...
普罗米修斯统计信息上报结构设计
为了实现高效的监控和警报,普罗米修斯提供了一个强大的统计信息上报机制。通过这个机制,可以将应用程序的各种统计信息发送到普罗米修斯,普罗米修斯会对这些信息进行处理,然后提供丰富的监控和警报功能。下面是基本的统计信息上报…...
两个系统之间的传值
在两个系统之间传值可以采用以下几种方式: 使用 URL 参数:可以将数据作为 URL 参数传递给另一个系统,另一个系统可以解析 URL 参数并获取数据。例如:Example Domain 使用 Cookie:可以在一个系统中设置 Cookie…...
PostgreSQL(五)JDBC连接串常用参数
目录 1.单机 PostgreSQL 连接串2.集群PostgreSQL 连接串 PostgreSQL JDBC 官方驱动下载地址: https://jdbc.postgresql.org/download/ PostgreSQL JDBC 官方参数说明文档: https://jdbc.postgresql.org/documentation/use/ 驱动类: driver-…...
如何修改浏览器中导航栏的背景色和字体
在日常使用电脑时,我们总会使用浏览器来浏览网页。而浏览器中的导航栏是用户进行网页浏览的主要界面之一,其背景色和字体的选择对用户的体验有着重要的影响。因此,为了让导航栏更加美观和易于使用,我们需要对其背景色和字体进行修…...
如何选择合适的智能氮气柜?
随着电子产品的普及,IC、半导体、精密元件、检测仪器之类的物品对湿度要求越来越高,潮湿、霉菌和金属氧化所造成的损害,随时在发生。人们对于物品的存放环境要求逐渐提高,利用防潮设备如智能氮气柜、电子防潮柜来存储产品也越来越…...
双向链表(数据结构)(C语言)
目录 概念 带头双向循环链表的实现 前情提示 双向链表的结构体定义 双向链表的初始化 关于无头单向非循环链表无需初始化函数,顺序表、带头双向循环链表需要的思考 双向链表在pos位置之前插入x 双向链表的打印 双链表删除pos位置的结点 双向链表的尾插 关…...
离线安装Percona
前言 安装还是比较简单,这边简单进行记录一下。 版本差异 一、离线安装Percona 下载percona官网 去下载你需要对应的版本 jemalloc-3.6.0-1.el7.x86_64.rpm 需要单独下载 安装Percona 进入RPM安装文件目录,执行下面的脚本 yum localinstall *.rpm修改…...
界面控件Telerik UI for WinForms使用指南 - 数据绑定 填充(二)
Telerik UI for WinForms拥有适用Windows Forms的110多个令人惊叹的UI控件,所有的UI for WinForms控件都具有完整的主题支持,可以轻松地帮助开发人员在桌面和平板电脑应用程序提供一致美观的下一代用户体验。 Telerik UI for WinForms组件为可视化任何类…...
通过栈/队列/优先级队列/了解容器适配器,仿函数和反向迭代器
文章目录 一.stack二.queue三.deque(双端队列)四.优先级队列优先级队列中的仿函数手搓优先级队列 五.反向迭代器手搓反向迭代器 vector和list我们称为容器,而stack和queue却被称为容器适配器。 这和它们第二个模板参数有关系,可以…...
数据链路层的主要功能是什么
数据链路层(OSI模型第2层)的核心功能是在相邻网络节点(如交换机、主机)间提供可靠的数据帧传输服务,主要职责包括: 🔑 核心功能详解: 帧封装与解封装 封装: 将网络层下发…...
【配置 YOLOX 用于按目录分类的图片数据集】
现在的图标点选越来越多,如何一步解决,采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集(每个目录代表一个类别,目录下是该类别的所有图片),你需要进行以下配置步骤&#x…...
06 Deep learning神经网络编程基础 激活函数 --吴恩达
深度学习激活函数详解 一、核心作用 引入非线性:使神经网络可学习复杂模式控制输出范围:如Sigmoid将输出限制在(0,1)梯度传递:影响反向传播的稳定性二、常见类型及数学表达 Sigmoid σ ( x ) = 1 1 +...
【论文阅读28】-CNN-BiLSTM-Attention-(2024)
本文把滑坡位移序列拆开、筛优质因子,再用 CNN-BiLSTM-Attention 来动态预测每个子序列,最后重构出总位移,预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵(S…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
JS设计模式(4):观察者模式
JS设计模式(4):观察者模式 一、引入 在开发中,我们经常会遇到这样的场景:一个对象的状态变化需要自动通知其他对象,比如: 电商平台中,商品库存变化时需要通知所有订阅该商品的用户;新闻网站中࿰…...
Chromium 136 编译指南 Windows篇:depot_tools 配置与源码获取(二)
引言 工欲善其事,必先利其器。在完成了 Visual Studio 2022 和 Windows SDK 的安装后,我们即将接触到 Chromium 开发生态中最核心的工具——depot_tools。这个由 Google 精心打造的工具集,就像是连接开发者与 Chromium 庞大代码库的智能桥梁…...
LLaMA-Factory 微调 Qwen2-VL 进行人脸情感识别(二)
在上一篇文章中,我们详细介绍了如何使用LLaMA-Factory框架对Qwen2-VL大模型进行微调,以实现人脸情感识别的功能。本篇文章将聚焦于微调完成后,如何调用这个模型进行人脸情感识别的具体代码实现,包括详细的步骤和注释。 模型调用步骤 环境准备:确保安装了必要的Python库。…...
【安全篇】金刚不坏之身:整合 Spring Security + JWT 实现无状态认证与授权
摘要 本文是《Spring Boot 实战派》系列的第四篇。我们将直面所有 Web 应用都无法回避的核心问题:安全。文章将详细阐述认证(Authentication) 与授权(Authorization的核心概念,对比传统 Session-Cookie 与现代 JWT(JS…...
Linux操作系统共享Windows操作系统的文件
目录 一、共享文件 二、挂载 一、共享文件 点击虚拟机选项-设置 点击选项,设置文件夹共享为总是启用,点击添加,可添加需要共享的文件夹 查询是否共享成功 ls /mnt/hgfs 如果显示Download(这是我共享的文件夹)&…...
