c语言-指针

news2024/11/29 22:42:55

指针详解

​ 这段时间在看 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);    // 输出结果为 6

    return 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 要跨过1int 型数据的长度。数组指针与二维数组联系密切,可以用数组指针来指向一个二维数组,如下:

 #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]);       // 输出结果为 1
     p++;       // 对 p 进行算术运算,此时 p 将指向二维数组的下一行的首地址,即 &arr[1][0]
     printf("%d\n",(*p)[1]);           // 输出结果为5
 
     return 0;                                                                               
 } 

指针与结构

简单介绍一下结构

​ 结构是一个或多个变量的集合,这些变量可能为不同的类型,为了处理的方便而将这些变量组织在一个名字之下。由于结构将一组相关的变量看做一个单元而不是各自独立的实体,因此结构有助于组织复杂的数据,特别是在大型的程序中。声明一个结构的方式如下:

struct message{            // 声明一个结构 message
    char name[10];             // 成员
    int age;
    int score;  
};

typedef struct message s_message;     // 类型定义符 typedef

s_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);    // 输出结果:tongye
  printf("%d\n",mess.age);     // 输出结果:23

  return 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);      // 输出结果为:tongye
    printf("%d\n",p->score);         // 输出结果为:83

    return 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     2

  swap(&x,&y);                    // 将 x,y 的地址作为参数传递给了被调函数,传递过去的也是一个值,与传值调用不冲突
  printf("%d %5d\n",x,y);         // 输出结果为:2     1
  return 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 = &sum;
    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,来计算从0n的所有自然数的和,其结果通过指针的形式返回给调用方。
​ 以上代码的运行结果如下所示:

[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_sumfunc_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 = &sum;
    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

​ 回调函数广泛用于开发场景中,比如信号函数、线程函数等,都使用到了回调函数的知识。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

跨域时怎么处理 cookie?

前言 一个请求从发出到返回&#xff0c;需要浏览器和服务端的协调配合。浏览器要把自己的请求参数带给服务端&#xff0c;服务端校验参数之后&#xff0c;除了返回数据&#xff0c;也可能会顺便把请求是否缓存&#xff0c;cookie等信息告诉浏览器。当请求是跨域请求的时候&…

uniapp仿淘宝购物车demo

项目是基于uview2.0的ui组件&#xff0c;并且在一定程度上修改过原本组件的代码&#xff08;app-navbar是使用u-navbar在进行二次封装的组件&#xff1b;u-number-box也进行了修改&#xff09;&#xff0c;符合项目需求&#xff08;这个看个人项目需求在进行修改&#xff09; u…

【 在线音乐平台(onlinemusic) 】

文章目录 一、核心功能二、效果演示三、创建项目四、数据库设计及配置数据库4.1 数据库和表设计4.2 配置连接数据库 五、创建配置类六、具体功能实现6.1 注册模块6.2 登录模块拓展&#xff1a;登录注册加密(MD5&#xff0c;BCrypt) 6.3 退出模块6.4 上传音乐模块知识拓展1&…

RabbitMQ详解(六):RabbitMQ集群搭建

集群 官方参考文档&#xff1a;https://www.rabbitmq.com/clustering.html RabbitMQ这款消息队列中间件产品本身是基于Erlang编写&#xff0c;Erlang语言天生具备分布式特性&#xff08;通过同步Erlang集群各节点的magic cookie来实现&#xff09;。因此&#xff0c;RabbitMQ天…

什么是柔性玻璃?

柔性玻璃(Flexible glass)是一种新型薄膜玻璃基板(Thin film glass substrate)材料&#xff0c;厚度极薄可以弯曲。 柔性玻璃定义有广义和狭义之分&#xff1a; 广义柔性玻璃泛指所有制成微米尺寸具有可弯曲特性的玻璃材料&#xff0c;如玻璃纤维、光纤、玻璃棉、玻璃布等。这些…

第10课【STM32 USB通讯协议实战】HID键盘+CDC虚拟串口组合设备

目录 前言USB设备类别未定义设备设备描述符/配置描述符分析如何配置从机类型如何配置设备专用的描述符如何配置从机端点 HID设备特点设备描述符/配置描述符分析HID报文描述符短条目前缀可选数据表现形式 层次结构实例分析总结 CDC设备特点设备描述符/配置描述符分析设备类特定请…

【LED子系统】四、核心层详解(一)

个人主页&#xff1a;董哥聊技术 我是董哥&#xff0c;嵌入式领域新星创作者 创作理念&#xff1a;专注分享高质量嵌入式文章&#xff0c;让大家读有所得&#xff01; 文章目录 1、前言2、leds_init分析2.1 相关数据结构2.1.1 class 2.2 实现流程 3、leds_class_dev_pm_ops分析…

Mysql出现问题:ERROR 1062 (23000): Duplicate entry ‘‘ for key ‘PRIMARY‘解决方案

回城传送–》《数据库问题解决方案》 ❤️作者主页:小虚竹 ❤️作者简介:大家好,我是小虚竹。Java领域优质创作者🏆,CSDN博客专家🏆,华为云享专家🏆,掘金年度人气作者🏆,阿里云专家博主🏆,51CTO专家博主🏆 ❤️技术活,该赏 ❤️点赞 👍 收藏 ⭐再看,养成…

QT中的模态对话框及非模态对话框

QT中的模态对话框及非模态对话框 [1] QT中的模态对话框及非模态对话框[2] Qt工作笔记-主界面往模式对话框emit信号&#xff0c;有注意的问题正常情况下&#xff1a;不正常情况下&#xff1a;下面给出正常情况下的代码&#xff1a; [1] QT中的模态对话框及非模态对话框 原文链接…

KVM软件安装/Guest OS图形模式安装

KVM软件安装 首先你的Linux操作系统得带有图形化界面 虚拟机开启硬件虚拟化 关闭防火墙和selinux [rootserver-d ~]# systemctl stop firewalld [rootserver-d ~]# systemctl disable firewalld Removed symlink /etc/systemd/system/multi-user.target.wants/firewalld.ser…

RK3568修改调试串口的波特率

概述 使用了临滴 RK3568 开发板,其调试串口的默认波特率是 1500000 &#xff0c;但并不是所有的 USB 转 TTL 都能使用这么高的波特率&#xff0c;所以我们就将波特率修改为 115200 这个比较通用的波特率。 RK3568 调试串口修改波特率的方法 ddr 运行阶段串口波特率的修改 ddr…

linux利用定时任务提权

背景&#xff1a; 运维为了防止数据丢失等&#xff0c;写个定时任务进行数据的打包压缩。由于数据打包压缩命令为tar&#xff0c;tar可以尝试加参数调用其他命令执行。 压缩命令&#xff1a;tar zxf 1.tar.gz /var/www/* 查看定时任务&#xff1a;cat /etc/crontab root权限下…

WordPress入门之WordPress站点基本设置

在Wordpress站点搭建过程中,我们需要快速去熟悉Wordpress,并进行一些简单的基本设置,在开始设置之前,大家可以先熟悉左边的菜单栏的每个选项,了解它们都是做什么的,今天就简单为大家介绍Wordpress入门之Wordpress站点基本设置。 一、设置个人资料 建议大家完善基本信息…

电容笔一定要防误触吗?苹果平板平替电容笔排行

至于用ipad作为学习工具的学生们&#xff0c;更是将它当成了一种必不可少的工具。但是&#xff0c;由于苹果原装电容笔的价格过高&#xff0c;没有人能负担得起。所以&#xff0c;最好的办法就是使用普通的电容笔。我是IPAD的忠实用户&#xff0c;也是数码爱好者&#xff0c;这…

10.BOM浏览器对象模型

BOM 浏览器对象模型 1. BOM 概述 1.1 什么是 BOM BOM&#xff08;Browser Object Model&#xff09;即浏览器对象模型&#xff0c;它提供了独立于内容而与**浏览器窗口进行交互的对象&#xff0c;其核心对象是 window BOM 由一系列相关的对象构成&#xff0c;并且每个对象都…

在Bamboo上怎么使用iOS的单元测试 | 京东云技术团队

作者&#xff1a;京东零售 吴滔 本教程将使用北汽登录模块为例&#xff0c;一步一步和大家一起搭建单元测试用例&#xff0c;并在Bamboo上跑起来&#xff0c;最终测试结果和代码覆盖率会Bamboo上汇总。 模块名称&#xff1a;BQLoginModule,是通过iBiu创建的一个模块工程 一 建…

浅尝Kubernetes

第一节 内容编排与Kubernetes 为什么要用k8s 集群环境容器部署的困境&#xff0c;假设我们有数十台服务器。分别部署Nginx&#xff0c;redis&#xff0c;mysql&#xff0c;业务服务。如何合理的分配这些资源。这里就需要用到容器编排 容器编排 在实际集群环境下&#xff0…

JAVA-抽象类和接口

文章目录 前言 大家好呀,今天给大家带来抽象类和接口的讲解,那么废话不多说,跟着我一起去学习吧! 1.1抽象类的概念 在面向对象的概念中&#xff0c;所有的对象都是通过类来描绘的&#xff0c;但是反过来&#xff0c;并不是所有的类都是用来描绘对象的&#xff0c;如果 一个类…

Java 11新特性:模块化系统和本地变量类型推断

作为Java语言的最新版本&#xff0c;Java 11带来了许多新特性&#xff0c;其中最引人注目的是模块化系统和本地变量类型推断。这两个新特性对Java开发人员来说具有重要意义&#xff0c;因此在本文中&#xff0c;我们将详细探讨这两个新特性及其对Java开发的影响。 章节1&#…

记录一次uniapp实现APP自动升级

描述 app的版本管理和升级&#xff0c;是一个不可或缺的功能&#xff0c;而uniapp则是提供了一整套的流程&#xff0c;由于官方文档过于复杂&#xff0c;而且写的云里雾里的&#xff0c;所以个人记录一次我的操作&#xff0c;直到配置成功。 总体 一共分为2个部分&#xff0…