指针:程序员的望远镜

指针

    • 1. 前言
    • 2. 指针概述
      • 2.1 内存与地址
      • 2.2 取地址
      • 2.3 指针是什么?
      • 2.4 解引用(间接访问)
      • 2.5 指针的大小
    • 3. 指针类型的作用
      • 3.1 指针+-整数
      • 3.2 指针的解引用
    • 4. 野指针
      • 4.1 野指针成因
      • 4.2 如何规避野指针
    • 5. 指针运算
      • 5.1 指针+-整数
      • 5.2 指针-指针
      • 5.3 指针的关系运算
    • 6. 指针和数组
      • 6.1 数组名
      • 6.2 使用指针访问数组
    • 7. 二级指针
    • 8. 指针数组
      • 8.1 指针数组的定义
      • 8.2 指针数组的使用
    • 9. const修饰指针
    • 10. 字符指针
    • 11. 数组指针
      • 11.1 数组指针的定义
      • 11.2 数组指针的使用
    • 12. 数组参数、指针参数
      • 12.1 一维数组传参
      • 12.2 二维数组传参
      • 12.3 一级指针传参
      • 12.4 二级指针传参
    • 13. 函数指针
      • 13.1 函数指针的定义
      • 13.2 函数指针的使用
    • 14. 函数指针数组
      • 14.1 函数指针数组的定义
      • 14.2 函数指针数组的使用
    • 15. 指向函数指针数组的指针
    • 16. 回调函数
    • 17. 综合练习

在这里插入图片描述

1. 前言

大家好,我是努力学习游泳的鱼,今天我们来学习C语言的重头戏:指针。指针时C语言里的重难点,是很多初学者的拦路虎,有些同学就是被C语言的指针劝退的,我在初学指针时也走了不少弯路。但是,不要怕!指针真的没有那么难!觉得指针难,是因为学习的方法不对。这篇文章里,我会尽可能用最通俗易懂的语言,来详细讲解指针的方方面面,希望能帮助到有需要的朋友。文章较长,建议先收藏,防止迷路。如果你觉得这篇文章帮助到了你,麻烦点个免费的赞支持一下博主。感谢大家的支持!
在这里插入图片描述

2. 指针概述

2.1 内存与地址

提到指针,首先要认识内存。

组装电脑时需要插入内存条,这是电脑必不可少的硬件。内存的作用是存储数据,我们在编程中创建的变量都是放在内存中的。
常见的内存条,大小有8G16G等等。那么大的一块空间是如何管理的呢?内存被划分为一个个很小的内存单元,每个内存单元大小是一个字节,并且对应一个编号。

那么编号是怎么产生的呢?现在电脑常见的配置有64位机器和32位机器。这里的6432指的是地址线的条数,每条地址线可以产生高电势和低电势,对应着二进制中的10。以32位机器为例,32位机器可以产生的编号包括32位全032位全1的总共232个二进制数。64位机器同理。

这里的编号就是传说中的地址

总结:内存是一个存储器,分为一个个很小的内存单元,每个内存单元大小是一个字节,并且唯一对应一个由3264)根地址线产生的二进制编号,这个编号就是地址。

这就很容易理解后面会讲到的指针变量的大小:32位机器的地址(也就是刚刚讲到的编号)是32位0/1序列,比如10000100101011110100001010111101,每个0或者1是一个比特位(bit)。比特位是计算机单位中最小的,一个二进制位(0/1)就是一个比特位。所以,32位机器的地址大小就是32bit,也就是4个字节。同理64位机器的地址就是64比特,也就是8个字节。

2.2 取地址

&是C语言提供的操作符,用于取出操作数的地址。其实,我们已经见过这个操作符了,scanf函数里就会用到scanf("%d", &num);。用法非常简单,在它后面直接跟你想取地址的对象。比方说:

int a = 0;
&a;

这就取出了a的地址。

这里需要说明一下,aint类型,大小是4个字节,也就是需要占用4个内存单元(前面说了一个内存单元大小是1个字节),用&取出来的只是第一个内存单元的地址。

我们还可以把地址打印出来,地址的打印格式是%p

#include <stdio.h>int main()
{int a = 0;printf("%p\n", &a);return 0;
}

2.3 指针是什么?

我们拿到了a的地址后,会想要把它存起来,这就需要定义一个变量。用来存放地址的变量叫做指针变量,也叫指针。所以,我们可以这样理解:指针就是地址

没错,指针变量,也就是指针,等价于地址,等价于内存的编号,这只是不同的叫法而已,意思是完全一样的。所以不要把指针想的太高大上,它只是一个普普通通的编号而已。

那么指针变量如何定义呢?这样写int* pa = &a;
这一行代码蕴含着很多的信息。定义了一个指针变量,名字是pa,并且初始化为变量a的地址。这里的pa的类型是int*。其中这个*表示pa是指针变量,而int表示pa指向的对象(即a)是int类型的。

举一反三:

char ch = 'w';
char* pch = &ch;

这里的*表示pch是一个指针变量。char表示pch指向的对象(即ch)是char类型的。

2.4 解引用(间接访问)

我们拿到了一个变量的地址,就可以通过这个地址来访问这个变量。这里就要介绍另外一个重要的操作符**是解引用操作符,又称间接访问操作符。在*后面跟指针,就能找到指针指向的空间。

#include <stdio.h>int main()
{int a = 0;int* pa = &a;*pa = 1;printf("%d\n", a);return 0;
}

这里就直接把pa指向的对象a改成了1

2.5 指针的大小

那么指针变量的大小是多大呢?

其实前面已经剧透过了。32位机器是4个字节,而64位机器是8个字节,这是由于32位机器产生的地址是320/1组成的二进制序列,每个0/1是一个比特位,总共32个比特位,即4个字节。同理64位机器产生的地址是64个比特位,即8个字节。

注意:指针变量的大小跟指针指向的变量的大小无关,只跟机器是32位还是64位有关。

下面来验证一下这一点。

#include <stdio.h>struct Stu
{char name[20];int age;float score;
};void test()
{printf("hehe\n");
}int main()
{printf("%d\n", sizeof(char*));printf("%d\n", sizeof(short*));printf("%d\n", sizeof(int*));printf("%d\n", sizeof(long*));printf("%d\n", sizeof(long long*));printf("%d\n", sizeof(float*));printf("%d\n", sizeof(double*));printf("%d\n", sizeof(long double*));//结构体指针printf("%d\n", sizeof(struct Stu*));//数组指针int arr[10] = { 0 };printf("%d\n", sizeof(&arr));//函数指针printf("%d\n", sizeof(&test));return 0;
}

32位环境下:
在这里插入图片描述
64位环境下:
在这里插入图片描述

测试结果:在32位(X86)环境下,全是4;在64位环境下(X64)全是8
注:如未特殊声明,以下环境均为X64

3. 指针类型的作用

指针的大小跟指针的类型无关,只和环境(32位还是64位虚拟地址空间)有关。也就是说,相同的环境下,不同类型的指针的大小是相同的,比如X86环境下,char*int*的大小都是4个字节,那么为什么还要区分不同的指针类型呢?不同的指针类型有什么区别呢?

3.1 指针±整数

一般来说,如果我们会用一个整型指针来存储一个整型变量的地址,再对这个整型指针解引用,就能够访问这个整型变量。比如:

int main()
{int a = 0x11223344;int* pa = &a;*pa = 0;return 0;
}

0x11223344是一个十六进制数字。这里补充一个知识:十六进制数字都是以0x开头的。一个十六进制位的大小是4个比特位,所以两个十六进制位的大小是8个比特位,即一个字节。对于0x1122334411是一个字节,22是一个字节,33是一个字节,44是一个字节,总共是4个字节,刚好能够存放在一个int类型的变量中。

执行int a = 0x11223344;后,通过调试看,我们把0x11223344放到了变量a中,并且在内存中也找到了a的位置。
在这里插入图片描述

在这里插入图片描述
接着执行int* pa = &a;,就把a的地址存储在pa中。
在这里插入图片描述

最后*pa = 0;,由于paint*类型的指针,对它解引用就能够访问一个int,会把变量a改成0。即,对int*类型的指针解引用,能访问4个字节。
在这里插入图片描述
在这里插入图片描述
如果是char*类型的指针呢?结果又会如何呢?

int main()
{int a = 0x11223344;char* pa = &a;*pa = 0;return 0;
}

先执行int a = 0x11223344;
在这里插入图片描述
在这里插入图片描述
接着执行char* pa = &a;。由于char*的指针也是8个字节(X64),存储a的地址不成问题。
在这里插入图片描述
最后,重头戏来了!*pa = 0;我们对于pa这个char*的指针解引用,编译器会认为,我们想要找一个char类型的变量。而char类型的变量只有1个字节,所以只会访问1个字节,把这1个字节的空间存储的数据改成0
在这里插入图片描述
在这里插入图片描述

综上,对一个int*的指针解引用,能访问4个字节。对一个char*的指针解引用,能访问1个字节。

这就是指针类型的第一个作用:

指针类型决定了,指针在被解引用的时候,访问的权限。

指针类型是一种看待内存空间的角度。对一个字符指针来说,内存空间存储的都是字符,对这个指针解引用,会访问1个字符,即1个字节的空间。对一个整型指针来说,内存空间存储的都是整型,对这个指针解引用,会访问1个整型,即4个字节的空间。

3.2 指针的解引用

阅读下面的代码:

#include <stdio.h>int main()
{int a = 0;int* pa = &a;char* pc = &a;printf("%p\n", pa);printf("%p\n", pc);printf("%p\n", pa + 1);printf("%p\n", pc + 1);return 0;
}

pa是一个整型指针,pc是一个字符指针,对它们分别+1的结果相同吗?
在这里插入图片描述
papc存储的地址是相同的,都是&a,但是pa+1跳过了4个字节,pc+1跳过了1个字节。

这就是指针类型的第二个作用:

指针类型决定了,指针向前或者向后走一步,走多大距离。

对于一个整型指针,向前走一步会跳过一个整型,即跳过4个字节。对于字符指针,向前走一步会跳过一个字符,即跳过1个字节。
本质上,对一个int*指针+1会在地址上+1*sizeof(int),即跳过4个字节, 对一个char*指针+1会在地址上+1*sizeof(char),即跳过1个字节。后面会讲,如果对int*指针+n会在地址上+n*sizeof(int),对char*指针+n会在地址上+n*sizeof(char),以此类推。
明白了指针类型的作用后,我们就可以在不同的场景下选择合适的指针类型来解决问题了。
比如:

我们有一个数组int arr[10] = {0};,这个数组有10int,总共40个字节,如何以字节为单位访问,把这40个字节的数据都改成'x'的ASCII码值呢?

首先我们需要一个char*的指针,才能一次访问1个字节,+1后也会跳过1个字节。char* p = (int*)arr;,数组名arr表示数组首元素的地址,是int*类型的,需要强制类型转换成char*。一开始让p指向这个地址,对p指针解引用能访问1个字节,从而把这个字节改成'x'的ASCII码值,接着对p指针+1都会跳过1个字节,就可以以字节为单位访问arr数组的40字节的空间了。

int main()
{int arr[10] = {0};char* p = (int*)arr;int i = 0;for (; i<40; ++i){*p = 'x';++p;}return 0;
}

同理,如果我们想按照整型的方式来访问arr,每次把4个字节的数据改成0x11223344,就应该使用int*类型的指针。

int main()
{int arr[10] = {0};int *p = arr;int i = 0;for (; i<10; ++i){*p = 0x11223344;++p;}return 0;
}

4. 野指针

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。

4.1 野指针成因

  1. 指针未初始化。
int main()
{int* p; // 局部变量未初始化,默认是随机值*p = 20; // 不能这样访问return 0;
}

由于指针p是个局部变量,而且没有初始化,存放的是随机值。如果我们把这个随机值当做一个地址,这个地址对应的内存空间不属于我们,是不能访问的,如果强行对这个地址解引用,就会造成非法访问。此时的p就是野指针。

  1. 指针越界访问。
#include <stdio.h>int main()
{int arr[5] = {1,2,3,4,5};int *p = arr;int i = 0;for (; i<10; ++i){printf("%d ", *p);++p;}return 0;
}

由于数组arr里只有5个元素,p指针以整型为单位向后访问时,只能访问5次,第6次访问(i==5)时,p指针已经超出了数组的范围,形成了越界访问。此时的p就是野指针。

  1. 指针指向的空间释放。
int* test()
{int a = 10;return &a;
}int main()
{int *p = test();*p = 100;return 0;
}

局部变量a在进入test函数是创建,test函数调用完毕后就销毁了。如果把a的地址放到p指针里,p指针就指向了一块已经销毁的空间,这块空间的使用权限不属于我们。如果强行对p解引用,就形成了非法访问。此时的p就是野指针。

4.2 如何规避野指针

我们一定要小心,不要在代码中出现野指针。规避野指针有以下几点经验。

  1. 指针初始化。

当我们知道应该如何对指针初始化时,应对其初始化。如:int* p = &a;
当我们不知道应该如何对指针初始化时,应初始化成NULL。如:int *q = NULL;而使用前需检查,不是NULL时才能使用if (NULL != q)

  1. 小心指针越界。

尤其是使用指针访问数组时,一定要检查是否越界。

  1. 指针指向空间释放后,置NULL。

假设我们已经对指针p进行了各种操作,已经不想使用这个指针了,则应该置空p = NULL

  1. 避免返回局部变量的地址。

局部变量的作用域是变量所在的局部范围,如果出了作用域就销毁了。如果一个局部变量在销毁之后,仍然有指针指向它,这个指针就是野指针。我们应避免出现这种情况。

  1. 指针使用之前检查有效性。

使用一个指针之前,要检查其是否为空,非空才可使用。我们应避免对空指针解引用。

5. 指针运算

5.1 指针±整数

一个int*指针±n,会向后(前)跳n*sizeof(int)字节。其他类型的指针同理。
假设p指针是int*类型的,则p+5就会向后跳5int,即向后跳5*sizeof(int)=20字节。

5.2 指针-指针

相同类型指向同一块空间的指针可以相减。指针-指针的绝对值是指针和指针之间元素的个数

假设创建一个数组int arr[10] = {0};,则&arr[9]-&arr[0]计算的是两个指针之间元素的个数,由于arr[9]arr[0]之间差9int,所以相减的结果是9

当然,如果反过来,&arr[0]-&arr[9]得到的结果就是-9,因为随着数组下标的增长,地址是由低到高变化的。&arr[0]-&arr[9]是低地址-高地址,得到的结果是负数。

我们可以使用指针-指针求字符串的长度。比如对于字符串"abcdef",内存空间实际存储的是[a b c d e f \0],那么\0的地址减a的地址就是中间字符的个数,也就是字符串的长度。

#include <stdio.h>int my_strlen(char* str)
{char* start = str;// 找\0while (*str){++str;}return str - start;
}int main()
{char arr[] = "abcdef";int len = my_strlen(arr);printf("len = %d\n", len);return 0;
}

5.3 指针的关系运算

两个指针是可以比较大小的。

举个例子:随着数组下标的增长,地址是由低到高变化的。创建一个数组int arr[10] = {0};那么就有:&arr[0]<&arr[1]&arr[9]>&arr[8]等等。由于数组名是数组首元素的地址,所以有arr==&arr[0]

有了以上三种指针之间的运算,我们来分析下面的代码。

#define N_VALUES 5int main()
{float values[N_VALUES];float *vp;// 写法1for (vp = &values[0]; vp < &values[N_VALUES];){*vp++ = 0;}return 0;
}

这段代码使用指针vp来遍历数组valuesvp被初始化为首元素地址,把改地址对应的元素置成0后,访问下一个位置。当vp指向values[N_VALUES](即数组最后一个元素的下一个位置)时就越界了,不再继续访问,跳出循环。

注意!有朋友可能会认为这段代码有问题,因为访问了values[N_VALUES],似乎越界了。事实上,这段代码是没有问题的,因为虽然越界了,但是没有修改该处的值。如果我们把values[N_VALUES]的值修改了,那vp就是野指针了,造成了非法访问。
如果我们想用指针从后往前遍历数组,就可以这么写:

// 写法2
for (vp = &values[N_VALUES]; vp > &values[0];)
{*--vp = 0;
}

一开始指针vp指向了最后一个元素的下一个位置,进入循环后立刻自减,访问最后一个元素,以此类推。最后一次进入循环后,自减后指向values[0],不再满足vp > &values[0],跳出循环。

但是有朋友可能会认为这么写有点别扭,于是对这段代码简化如下:

// 写法3
for (vp = &values[N_VALUES-1]; vp >= &values[0]; vp--)
{*vp = 0;
}

一开始让vp指向values[N_VALUES-1](最后一个元素),访问后再往前走,直到vp指向values[0]后,把values[0]置成0,再往前走,指向values[-1](第一个元素的前一个位置),不满足vp >= &values[0]后跳出循环。
这种写法,实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准规定:

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

在写法3中,最后一次执行循环的判断部分时,是拿&values[-1](第一个元素的前一个位置的地址)与&values[0]比较,是不被允许的。

6. 指针和数组

6.1 数组名

对于数组的详细讲解,请阅读【C语言】数组。
先来复习一个问题:数组名是什么?
数组名表示数组首元素的地址,但是有两个例外。

  1. sizeof(数组名),数组名表示整个数组,计算的是整个数组的大小,单位是字节。
  2. &数组名,数组名表示整个数组,取出的是整个数组的地址。

6.2 使用指针访问数组

先创建一个数组int arr[10] = {1,2,3,4,5,6,7,8,9,10};,计算数组元素个数:int sz = sizeof(arr) / sizeof(arr[0]);
我们用指针来访问数组,首先需要一个指针p。数组名arr表示首元素的地址(类型是int*),我们就用这个地址来初始化指针pint* p = arr;。又因为数组在内存中是连续存放的,我们有了数组首元素的地址,就能够找到后面所有元素的地址。

我们可以在for循环内,用循环变量i产生0~sz-1的数。那么p+i就跳过了iint类型的数据,就指向了数组中下标为i的元素。再对其解引用,*(p+i)就能访问数组中下标为i的元素了。

#include <stdio.h>int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int sz = sizeof(arr) / sizeof(arr[0]);int* p = arr;int i = 0;for (; i < sz; ++i){printf("%d ", *(p + i));}return 0;
}

7. 二级指针

当我们有一个变量int a = 10;时,我们取出它的地址并存放在一个指针变量中int *pa = &a;,此时pa是一个一级指针。pa也是一个变量,也有地址,我们取出pa的地址,存放在另一个指针中int** ppa = &pa;,此时ppa就是一个二级指针,类型是int**

我们如何理解int**类型呢?可以拆分成int* *。后面这个单独的*表示ppa是一个指针变量,前面的int*表示ppa指向的对象(即pa)是int*类型的。

同理,我们还可以取出ppa的地址,存放在一个三级指针里:int*** pppa = &pa;。这样就能无限套娃了。

我们对二级指针ppa解引用,由于ppa存放的是pa的地址,我们就能访问pa了。比如*ppa = NULL;就等价于pa = NULL;

8. 指针数组

8.1 指针数组的定义

如果写int arr[5];,则arr是存放整型的数组,简称整型数组;如果写char ch[6];,则ch是存放字符的数组,简称字符数组。

那什么是指针数组呢?就是存放指针的数组

比如写:int* arr[10];arr就是一个整型指针数组,有10个元素,每个元素是int*类型的。

8.2 指针数组的使用

我们可以用一个指针数组来模拟二维数组。
假设有三个整型数组,分别是data1data2data3,由于数组名表示首元素地址,所以data1data2data3就分别表示对应的数组首元素地址,我们把它们都存放在一个数组指针arr里。那么,arr[i]就可以访问到数组data1data2data3arr[i][j]就可以访问到data1data2data3的元素。

#include <stdio.h>int main()
{int data1[] = { 1,2,3,4,5 };int data2[] = { 2,3,4,5,6 };int data3[] = { 3,4,5,6,7 };int* arr[] = { data1, data2, data3 };int i = 0;for (; i < sizeof(arr) / sizeof(arr[0]); ++i){int j = 0;for (; j < sizeof(data1) / sizeof(data1[0]); ++j){printf("%d ", arr[i][j]);}printf("\n");}return 0;
}

对比一下二维数组的访问,是不是非常像?

#include <stdio.h>int main()
{int arr[3][5] = { {1,2,3,4,5}, {2,3,4,5,6}, {3,4,5,6,7} };int i = 0;for (; i < sizeof(arr) / sizeof(arr[0]); ++i){int j = 0;for (; j < sizeof(arr[0]) / sizeof(arr[0][0]); ++j){printf("%d ", arr[i][j]);}printf("\n");}return 0;
}

9. const修饰指针

假设我们有一个整数,int a = 10;由于a是个变量,可以直接修改a = 20;,如果我们不想修改这个变量,可以加const来修饰,如const int a = 10;,此时如果强行修改a,如a = 20;就会报编译错误。对于const修饰的变量a,我们称为常变量。

此时a真的不能被修改了吗?也不见得。我们再用一个指针来存储它的地址:int *pa = &a;,接着对它解引用来间接地修改:*pa = 20;这么写就强行把常变量a给修改了。

如果我们想用一个指针来存储变量a的地址,又不想通过解引用指针的方式来修改变量a,就可以使用const修饰指针。正确的写法是const int *pa = &a;或者int const *pa = &a。这两种写法const都放在*左边,修饰的是*pa,就不能通过解引用pa来修改a了,但是仍然可以改变pa的值。比如:int b = 20; pa = &b;这样pa就指向b了。

如果把const放在*右边,即int* const pa = &a;,那么const修饰的就是pa,此时pa的值就不能修改了,比如不能写:int b = 20; pa = &b;,但是我们可以通过解引用pa的方式来修改a,如:*pa = 100;,就把a改成了100
如果在*的左右两边都加上const,如:const int* const pa = &a;,那么我们既不能修改pa的值,也不能通过解引用pa的方式来修改a。既不能写int b = 20; pa = &b;,也不能写*pa = 100;

10. 字符指针

char*类型的指针可以存储一个字符的地址。根据我们所学的知识,我们已经可以看懂下面的代码。

char ch = 'w';
char* pch = &ch;
*pch = 'e';

字符指针还有一个更加常见的用法。当我们直接写出一个常量字符串,比如"abcdef"时,它的值是这个字符串的首字符(即'a')的地址。如果我们想存储这个地址,就需要用到字符指针。如:char* p = "abcdef";。此时指针p就指向了字符'a',相当于指向了字符串"abcdef"
当我们用%s的格式打印字符串时,只需要字符串的起始地址,即字符串首字符的地址,程序就会从这个起始地址指向的字符开始,一直向后打印字符,直到遇到\0停止打印。如:当字符指针p指向了字符串"abcdef"的首字符(即a),我们打印字符串就写printf("%s\n", p);

对于char* p = "abcdef";,我们把一个常量字符串首字符的地址存储在一个字符指针中,由于常量字符串时不能修改的,如果写*p = 'w';,强行修改常量字符串,程序就会崩溃。为了防止这种危险的行为,我们一般会使用const来修饰这个指针,即const char *p = "abcdef";

下面代码输出的结果是什么呢?

#include <stdio.h>int main()
{const char* p1 = "abcdef";const char* p2 = "abcdef";if (p1 == p2){printf("p1 == p2\n");}else{printf("p1 != p2\n");}char arr1[] = "abcdef";char arr2[] = "abcdef";if (arr1 == arr2){printf("arr1 == arr2\n");}else{printf("arr1 != arr2\n");}return 0;
}

由于p1p2指向的都是常量字符串"abcdef",这个常量字符串是不能修改的,所以没必要存在两份,只需保存一份就行了,p1p2指向的是内存中同一块空间,这块空间存放"abcdef"这个字符串。

反观arr1arr2,是两个数组,必然是两块不同的空间,数组名表示首元素地址,所以arr1arr2不相等。

11. 数组指针

11.1 数组指针的定义

数组指针,即存放数组地址的指针。

我们直接对数组名取地址,取出的是数组的地址。如:创建数组int arr[10] = {0};,对数组取地址&arr;即为数组的地址。

如果我们想把数组的地址存起来,就需要数组指针。对于上面的例子,正确的写法:int (*p)[10] = &arr;。括号里的*pp先和*结合,说明p是一个指针。向外一看,看到了[10],这里的方括号说明p是一个数组指针,指向了一个数组,方括号里的10说明p指向的数组有10个元素。再往前一看,看到了int,说明数组元素的类型是int

对于int (*p)[10],我们把指针变量的名字p去掉,就能得到数组指针类型是int (*)[10]。这个类型中,括号里的*表示这是一个指针类型,向外一看[10],方括号表示这个类型创建的指针变量可以存放一个数组的地址,数组有10个元素。再向前一看,这个int表示数组的元素类型是int

由于指针类型决定了指针+1跳过几个字节,数组指针类型+1跳过整个数组。比如int (*)[10]类型,+1跳过整个数组,即跳过10int,即40个字节。

11.2 数组指针的使用

我们可以用数组指针来访问数组的元素。先举个一维数组的例子。

假设有个一维数组int arr[10] = {1,2,3,4,5,6,7,8,9,10};,我们来写一个函数,打印出这个数组的元素。假设用数组指针的方式,那应该这样调用这个函数:print(&arr, sizeof(arr)/sizeof(arr[0]));。接下来实现print函数。

我们需要一个数组指针来接收数组的地址int (*parr)[10]。对parr解引用,即*parr就能找到整个数组,相当于数组名,而数组名表示首元素地址。根据以上分析,*parr就相当于&arr[0],对首元素地址+i就能找到下标为i的元素的地址,再对其解引用就能找到下标为i的元素。

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

接下来我们用数组指针访问二维数组。

假设有一个二维数组int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6}, {3,4,5,6,7}};,接下来我们要写一个函数打印这个数组,这个函数应这么调用:print(arr, sizeof(arr)/sizeof(arr[0]), sizeof(arr[0])/sizeof(arr[0][0]));。其中,sizeof(arr)/sizeof(arr[0])数组的总大小/数组第一行的大小)是数组的行数,sizeof(arr[0])/sizeof(arr[0][0])数组第一行的大小/数组第一行第一个元素的大小)是数组的列数。
arr是数组名,表示数组首元素的地址。二维数组的首元素就是它的第一行,而第一行是5int的一维数组。所以arr5int的一维数组的地址,用数组指针parr来接收,加上类型应该这样写:int (*parr)[5]

那如何使用parr来访问arr呢?parr是数组第一行的地址,parr+i就跳过了i行,即parr+i是数组第i行的地址。对其解引用,即*(parr+i)就能找到第i行,相当于第i行的数组名,就是第i行首元素的地址。*(parr+i)+j就是第i行首元素的地址跳过j个元素,即*(parr+i)+j是第i行第j个元素的地址。再对其解引用*(*(parr+i)+j)就能访问第i行第j个元素。

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

12. 数组参数、指针参数

12.1 一维数组传参

假设我们有个一维数组int arr[10];,我们想调用一个test函数,把数组名arr传过去,即test(arr);,试问,test函数应该用什么类型的形参来接收呢?

数组传参,可以数组接收,所以可以写成void test(int arr[10]) {}。又因为,使用数组名传参时,实际传递的是数组首元素的地址,并不会再函数内部创建一个新的数组,所以可以省略数组的大小,即void test(int arr[]) {}。甚至可以乱写数组的大小,但是不建议,比如void test(int arr[100]) {}

由于使用数组名传参时,实际传递的是首元素的地址,我们可以直接使用指针来接收void test(int* p) {}

除了上面提到的写法,其余写法都是错误的。

12.2 二维数组传参

假设我们有个二维数组int arr[3][5],我们想调用一个test函数,把数组名arr传过去,即test(arr);,试问,test函数应该用什么类型的形参来接收呢?

数组传参,可以数组接收,所以可以写成void test(int arr[3][5]) {}。对于二维数组,行可以省略,列不能省略,所以也可以写成void test(int arr[][5]) {}。对于省略的行,也可以乱写,但是不建议,比如void test(int arr[100][5]) {}
由于使用数组名传参时,实际传递的是首元素的地址,二维数组的首元素就是第一行的地址,我们可以直接使用数组指针来接收void test(int (*p)[5]) {}

除了上面提到的写法,其余写法都是错误的。

12.3 一级指针传参

假设我们有一个函数,新参是一个一级指针void test(int* ptr) {},试问,实参部分可以怎么写呢?

可以直接传一个一级指针过去。如int a = 10; test(&a);,或int* p = &a; test(p);
当然也可以传一个数组过去,由于数组名表示首元素地址,只需要首元素是int类型就行了,如int arr[10]; test(arr);

12.4 二级指针传参

假设我们有一个函数,新参是一个二级指针void test(int** ptr) {},试问,实参部分可以怎么写呢?

可以直接传一个二级指针过去。如

int a = 10;
int* pa = &a;
int** ppa = &pa;
test(ppa);

当然,对于以上代码,也可以直接传递一级指针的地址,即test(&pa)

除此之外,还可以传一个数组过去。数组名表示首元素的地址,只需要首元素是int*类型,首元素地址就是int**类型。所以我们需要一个指针数组int* arr[10];,然后传过去就行了test(arr);

13. 函数指针

13.1 函数指针的定义

假设我们有一个函数Add

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

我们如何拿到函数的地址呢?只需要对函数名取地址即可。&Add就是函数的地址。除此之外,函数名也表示函数的地址,也就是说,直接写出函数名Add也表示函数的地址。

如果我们想把函数的地址存起来,就需要函数指针变量。假设我们用变量pf来存放Add的地址,应该如何书写它的类型呢?首先,我们要确保pf是个指针,就用括号把这玩意和*括起来,即(*pf),接着向外一看,这是一个函数指针,就需要一个圆括号(对比数组指针的方括号):(*pf)(),圆括号内写函数的形参:(*pf)(int, int)。再往前一看,是函数的返回类型intint (*pf)(int, int)。所以完整的写法是:int (*pf)(int, int) = Add;

对于函数指针,如果去掉变量名,剩下的就是函数指针类型,如以上的pf的类型就是int (*)(int, int)

13.2 函数指针的使用

对函数指针解引用,就可以找到对应的函数。如上面的例子中,写*pf就可以调用这个函数了,即int ret = (*pf)(2, 3);,此时ret就是5。需要注意的是,使用函数指针调用函数,是可以省略*的,也就是说,直接写int ret = pf(2, 3);也可以调用Add函数。事实上,这个*就是摆设,你甚至可以写很多个*,比如int ret = (********pf)(2, 3);,当然这是开个玩笑,建议别这么写。

14. 函数指针数组

14.1 函数指针数组的定义

函数指针数组,就是存放函数指针的数组。比如,假设我们有几个函数,它们的参数和返回类型都是一样的。

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;
}

函数名表示函数的地址,要想把这些函数的地址都存起来,就需要一个函数指针数组。这个函数指针数组的类型应该怎么写呢?假设数组名是pf,由于是一个数组,就要先和方括号结合,即pf[4],方括号里的4表示数组有4个元素,当然我们如果要对这个数组初始化,就可以省略数组元素个数,即pf[]。接下来写数组的元素类型,由于数组的元素是函数指针,所以先与*结合,说明它是个指针(*pf[])。向外一看,是个函数指针,所以需要圆括号,圆括号里写函数的形参类型(*pf[])(int, int)。再向前一看,是函数的返回类型int (*pf[])(int, int)

14.2 函数指针数组的使用

访问这个数组的元素也很简单,使用for循环访问就行了。由于访问的元素都是函数指针,直接在后面加圆括号就可以调用对应的函数。

#include <stdio.h>int main()
{int (*pf[])(int, int) = { Add, Sub, Mul, Div };int sz = sizeof(pf) / sizeof(pf[0]);int i = 0;for (i = 0; i < sz; i++){int ret = pf[i](8, 2);printf("%d\n", ret);}return 0;
}

当然,我们可以把上面的代码改造成一个计算器程序,此时函数指针数组就被称作转移表。

#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;// 转移表int (*pfArr[])(int, int) = { 0, Add, Sub, Mul, Div };do{menu();printf("请选择:>");scanf("%d", &input);if (input >= 1 && input <= 4){printf("请输入2个操作数:>");scanf("%d %d", &x, &y);ret = pfArr[input](x, y);printf("ret = %d\n", ret);}else if (input == 0){printf("退出计算器\n");break;}else{printf("选择错误,重新选择\n");}} while (input);return 0;
}

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

假设还是上面的4个函数,AddSubMulDiv。我们把这4个函数的地址存起来,可以用函数指针数组int (*pfArr[])(int, int) = { Add, Sub, Mul, Div };。如果再取出这个函数指针数组的地址,即&pfArr,就需要存放到指向函数指针数组的指针。

假设这个指向函数指针数组的指针变量名是p,那么应该如何写它的类型呢?首先,它是个指针,所以用括号把它和*括起来(*p)。向外一看,它指向一个数组,所以需要一个方括号,里面放数组的元素个数(*p)[4]。每个元素的类型是什么呢?是函数指针类型,即int (*)(int, int),我们假设这个函数指针类型创建一个变量,名字叫pf,即int (*pf)(int, int),再把其中的pf替换成前面写出来的(*p)[4]就得到了int (*(*p)[4])(int, int)。完整的写法是int (*(*p)[4])(int, int) = &pfArr;

如何使用这个指向函数指针的数组来访问前面的4个函数呢?对数组指针解引用,相当于数组名,由数组名可以访问这个数组的元素,而这个数组的元素都是函数指针,就可以调用这些函数。

再详细一点,p是指向函数指针数组pfArr的指针,那么*p就相当于数组名pfArr,而(*p)[i]就相当于pfArr[i],即函数指针数组的元素,再用这些函数指针调用函数即可。

#include <stdio.h>int main()
{int (*pfArr[])(int, int) = { Add, Sub, Mul, Div };int (*(*p)[4])(int, int) = &pfArr; // p是指向函数指针数组的指针int i = 0;for (i = 0; i < 4; i++){int ret = (*p)[i](8, 2);printf("ret = %d\n", ret);}return 0;
}

16. 回调函数

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

如以下的test函数就是回调函数。

#include <stdio.h>// 回调函数
void test()
{printf("hehe\n");
}void print_hehe(void (*p)())
{if (1){p();}
}int main()
{print_hehe(test);return 0;
}

我们可以使用回调函数实现计算器。

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

接下来我们来研究以下库函数qsort,并且自己用实现一个类似的。

qsort是一个库函数,是基于快速排序算法的排序函数。以下是该函数的声明:void qsort(void* base, size_t num, size_t width, int (*cmp)(const void* e1, const void* e2));
四个参数,从左到右依次是:

  1. void* base:待排序数据的起始位置。
  2. size_t num:数据的元素个数。
  3. size_t width:一个元素的字节大小。
  4. int (*cmp)(const void* e1, const void* e2):一个函数指针,指向了比较函数。e1e2分别指向一个元素,假设e1指向data1e2指向data2,若data1>data2,则该比较函数返回值为正数;若data1<data2,则该比较函数返回值为负数;若data1=data2,则该比较函数返回值为0

对于最后一个参数,要求qsort函数的使用者自定义一个比较函数,调用qsort函数时,需要把比较函数的地址作为参数传递给qsort,此时这个比较函数就是回调函数。
我们已经会使用冒泡排序来排序一个整型数组了,代码如下:

void bubble_sort(int arr[], int sz)
{int i = 0;for (i = 0; i < sz - 1; i++){int flag = 1; // 假设已经有序int j = 0;for (j = 0; j < sz - 1 - i; j++){if (arr[j] > arr[j + 1]){flag = 0;// 交换int tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;}}if (flag == 1){// 已经有序了break;}}
}void print_arr(int arr[], int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}printf("\n");
}int main()
{int arr[] = { 10,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;
}

接下来我们参考qsort,把冒泡排序也改造得通用一点。有以下几点需要改进:

  1. 参数应该改进为4个,来应对各种情况,简单起见,使用以下参数:void* base, int num, int width, int (*cmp)(const void* e1, const void* e2)
  2. 相邻元素两两比较时,不再使用><=来比较,而是使用回调函数cmp
  3. 若相邻两个元素不满足升序,交换时可以一个字节一个字节交换。

代码如下:

void Swap(char* buf1, char* buf2, int width)
{int i = 0;for (i = 0; i < width; i++){char tmp = *(buf1 + i);*(buf1 + i) = *(buf2 + i);*(buf2 + i) = tmp;}
}void bubble_sort(void* base, int num, int width, int (*cmp)(const void* e1, const void* e2))
{int i = 0;for (i = 0; i < num - 1; i++){int flag = 1; // 假设已经有序int j = 0;for (j = 0; j < num - 1 - i; j++){if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0){flag = 0;// 交换Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);}}if (flag == 1) // 已经有序了{break;}}
}

其中Swap函数还有另一种写法。

void Swap(char* buf1, char* buf2, int width)
{int i = 0;for (i = 0; i < width; i++){char tmp = *buf1;*buf1 = *buf2;*buf2 = tmp;buf1++;buf2++;}
}

接下来我们来调用这个改进后的bubble_sort函数。

排序整型数组:

#include <stdio.h>int cmp_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]);}printf("\n");
}int main()
{int arr[] = { 10,9,8,7,6,5,4,3,2,1,0 };int sz = sizeof(arr) / sizeof(arr[0]);bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);print_arr(arr, sz);return 0;
}

排序结构体:

#include <stdio.h>
#include <string.h>typedef struct Stu
{char name[20]; // 名字int age; // 年龄double score; // 成绩
}Stu;int cmp_stu_by_age(const void* e1, const void* e2)
{return ((Stu*)e1)->age - ((Stu*)e2)->age;
}int cmp_stu_by_name(const void* e1, const void* e2)
{return strcmp(((Stu*)e1)->name, ((Stu*)e2)->name);
}void print_stu_info(Stu arr[], int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("arr[%d] name:%s age:%d score:%lf\n", i, arr[i].name, arr[i].age, arr[i].score);}
}int main()
{Stu arr[] = { {"zhangsan", 20, 30.5}, {"lisi", 30, 90.0}, {"wangwu", 25, 70.5} };int sz = sizeof(arr) / sizeof(arr[0]);bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);printf("按照年龄排序\n");print_stu_info(arr, sz);bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);printf("按照名字排序\n");print_stu_info(arr, sz);return 0;
}

17. 综合练习

假设有一个数组int a[] = {1,2,3,4};,试问以下表达式的结果是多少?以下论述大小时,均省略单位(字节)。

  • sizeof(a):数组名直接放到sizeof内部,表示整个数组,计算的是整个数组的大小,即16
  • sizeof(a+0):数组名表示首元素地址,+0相当于没加,还是首元素地址,大小就是4/8
  • sizeof(*a):数组名表示首元素地址,对其解引用就是首元素,计算的是首元素的大小,即4
  • sizeof(a+1):数组名表示首元素地址,+1后跳过一个元素,即第二个元素的地址,大小是4/8
  • sizeof(a[1]):计算的是第二个元素的大小,即4
  • sizeof(&a):对数组名取地址,取出的是数组的地址,计算数组的地址大小是4/8
  • sizeof(*&a):取出数组的地址,再解引用,可以拿到整个数组,计算整个数组的大小是16
  • sizeof(&a+1)&a取出数组的地址,+1后跳过整个数组,指向了数组后面,仍然是地址,大小是4/8
  • sizeof(&a[0]):取出第一个元素的地址,大小是4/8
  • sizeof(&a[0]+1):取出第一个元素的地址,+1后跳过一个元素,指向了第二个元素,仍然是地址,大小是4/8

假设我们有一个字符数组char arr[] = {'a', 'b', 'c', 'd', 'e', 'f'};,试问下面表达式的结果是多少?以下论述大小时,均省略单位(字节)。

  • sizeof(arr):计算数组的大小,即6
  • sizeof(arr+0):数组首元素的地址,大小是4/8
  • sizeof(*arr):对首元素地址解引用,得到首元素,大小是1
  • sizeof(arr[1]):计算下标为1的元素大小,即1
  • sizeof(&arr):计算数组的地址的大小,即4/8
  • sizeof(&arr+1):数组的地址+1后跳过整个数组,仍是地址,大小是4/8
  • sizeof(&arr[0]+1):首元素地址+1跳过1个元素,指向第2个元素,仍是地址,大小是4/8
  • strlen(arr)arr是数组名,表示首元素地址,strlen会从这个地址开始向后数字符,直到遇到\0才停止。由于我们不知道数组后面内存空间存放的数据是什么,最终的结果是随机值。
  • strlen(arr+0)arr+0也是首元素地址,同上,结果是随机值。
  • strlen(*arr):对首元素地址解引用,得到首元素'a',即把'a'的ASCII码值97传递给strlen函数,strlen会把97当做地址,向后数字符,直到数到\0,这会造成内存的非法访问。
  • strlen(arr[1]):同上,把'b'的ASCII码值98传给了strlen,也会造成内存的非法访问。
  • strlen(&arr):取出数组的地址,值和数组首元素地址相同,结果和strlen(arr)相同。
  • strlen(&arr+1):取出数组的地址,+1后跳过了整个数组,从该位置向后找\0,结果是随机值。
  • strlen(&arr[0]+1):取出首元素地址,+1后跳过了一个元素,指向了第二个元素,会从该位置向后找\0,结果是随机值。

假设我们有一个字符数组char arr[] = "abcdef";,试问下面表达式的结果是多少?以下论述大小时,均省略单位(字节)。

  • sizeof(arr):由于字符串结尾默认隐藏一个\0,故大小是7
  • sizeof(arr+0)arr+0是数组首元素的地址,故大小是4/8
  • sizeof(*arr)*arr是数组首元素,故大小是1
  • sizeof(arr[1])arr[1]是数组的第二个元素,故大小是1
  • sizeof(&arr)&arr是数组的地址,故大小是4/8
  • sizeof(&arr+1)&arr+1\0后面的地址,故大小是4/8
  • sizeof(&arr[0]+1)&arr[0]+1是数组第二个元素的地址,故大小是4/8
  • strlen(arr):从首元素地址开始向后找\0,结果是6
  • strlen(arr+0):同上,结果是6
  • strlen(*arr):把a的ASCII码值当做地址向后找\0,造成内存的非法访问。
  • strlen(arr[1]):把b的ASCII码值当做地址向后找\0,造成内存的非法访问。
  • strlen(&arr):数组的地址和首元素地址值相同,故结果相同,为6
  • strlen(&arr+1):数组的地址+1跳过整个数组,从\0后面开始向后找\0,结果是随机值。
  • strlen(&arr[0]+1):数组第二个元素向后找\0,结果是5

假设我们有一个字符指针char* p = "abcdef";,试问下面表达式的结果是多少?以下论述大小时,均省略单位(字节)。

  • sizeof(p):计算指针变量的大小为4/8
  • sizeof(p+1)pa的地址,+1后跳过一个字符,指向了b,仍是指针,故大小是4/8
  • sizeof(*p):即a,大小是1
  • sizeof(p[0]):转换为*(p+0),即*p,同上,大小是1
  • sizeof(&p):二级指针,大小是4/8
  • sizeof(&p+1):跳过了p,仍是二级指针,大小是4/8
  • sizeof(&p[0]+1)p[0]*(p+0),即*p,即a,再取地址,是a的地址,+1后跳过一个字符,指向了b,仍是地址,故大小是4/8
  • strlen(p):从a的位置向后数字符,直到遇到\0,结果是6
  • strlen(p+1):从b的位置向后数字符,直到遇到\0,结果是5
  • strlen(*p):把a的ASCII码值作为地址传递给strlen,造成内存的非法访问。
  • strlen(p[0]):同上,把a的ASCII码值作为地址传递给strlen,造成内存的非法访问。
  • strlen(&p):从指针p的位置向后找\0,结果是随机值。
  • strlen(&p+1)p向后跳过一个char*的大小,指向p后面,向后找\0,结果是随机值。
  • strlen(&p[0]+1):从b的位置向后找\0,结果是5

假设我们有一个二维数组int a[3][4] = {0};,试问下面表达式的结果是多少?以下论述大小时,均省略单位(字节)。

  • sizeof(a):计算整个数组大小,即48
  • sizeof(a[0][0]):计算第一行第一个元素的大小,即4
  • sizeof(a[0])a[0]是第一行的数组名,sizeof(a[0])就是第一行的数组名单独放在sizeof内部,计算的是第一行的大小,即16
  • sizeof(a[0]+1)a[0]是第一行的数组名,表示第一行首元素的地址,+1后指向了第一行第二个元素,仍是地址,故大小是4/8
  • sizeof(*(a[0]+1)):由上一条分析,*(a[0]+1)就是第一行第二个元素,故大小是4
  • sizeof(a+1)a表示二维数组第一行的地址,a+1表示二维数组第二行的地址,故大小是4/8
  • sizeof(*(a+1)):由上一条分析,*(a+1)就能拿到第二行,故大小是第二行的大小,即16
  • sizeof(&a[0]+1)a[0]是第一行的数组名,对第一行的数组名取地址就能拿到第一行的地址,再+1后就是第二行的地址,故大小是4/8
  • sizeof(*(&a[0]+1)):由上一条分析,*(&a[0]+1)就能拿到第二行,故大小是第二行的大小,即16
  • sizeof(*a)a是第一行的地址,*a就能拿到第一行,故大小是第一行的大小,即16
  • sizeof(a[3]):看似越界了,但也可以计算大小。相当于第四行的数组名(虽然第四行不存在),计算的是第四行的大小,即16

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

做CRM客户管理系统前要分析哪些数据?

01 做CRM客户管理系统前要分析哪些数据&#xff1f; “数据是21世纪的石油” 进行数据分析是做crm至关重要的步骤&#xff0c;根据我们团队的多年经验&#xff0c;本文总结出了两个需要重点考虑的方面&#xff1a; 1、客户基本信息 包括客户的基本档案、经营状况、客户特征…

QT使用QImage做图片切割

#include "mainwindow.h" #include "ui_mainwindow.h" #include <QFileDialog> #include <QDebug>MainWindow::MainWindow(QWidget *parent) :QMainWindow(parent),ui(new Ui::MainWindow) {ui->setupUi(this);// 选择本地图片文件QString …

Middleware ❀ Kafka功能与使用详解

文章目录 1. 概述1.1. 消息队列1.2. 应用场景1.3. 工作模式1.4. 基础结构1.4.1. 结构组件1.4.2. 数据同步1.4.3. ACK机制1.4.4. 分区机制1.4.4.1. 使用Partition Key写入1.4.4.2. 轮询写入 - 默认规则1.4.4.3. 指定Partition写入 1.4.5. Offset偏移量1.4.5.1. 消息顺序性1.4.5.…

说说你了解的 CDC

分析&回答 什么是 CDC CDC,Change Data Capture,变更数据获取的简称&#xff0c;使用CDC我们可以从数据库中获取已提交的更改并将这些更改发送到下游&#xff0c;供下游使用。这些变更可以包括INSERT,DELETE,UPDATE等。用户可以在以下的场景下使用CDC&#xff1a; 使用f…

(数学) 剑指 Offer 39. 数组中出现次数超过一半的数字 ——【Leetcode每日一题】

❓ 剑指 Offer 39. 数组中出现次数超过一半的数字 难度&#xff1a;简单 数组中有一个数字出现的次数超过数组长度的一半&#xff0c;请找出这个数字。 你可以假设数组是非空的&#xff0c;并且给定的数组总是存在多数元素。 示例 1: 输入: [1, 2, 3, 2, 2, 2, 5, 4, 2] 输…

做西班牙语翻译好不好?西班牙语薪酬如何?

众所周知&#xff0c;西班牙语作为全球应用广泛的“小语种”&#xff0c;市场对它的翻译需求极为庞大。西班牙语翻译是你寻求优秀事业的新赛道&#xff01;那么&#xff0c;西班牙语翻译薪酬如何&#xff0c;北京西语翻译哪家好&#xff1f; 美国的调查数据告诉你&#xff0c;西…

拼多多开放平台的API接口可以获取拼多多电商数据。以下是API接口流程

使用拼多多开放平台的API接口可以获取拼多多电商数据。以下是一般的API接口流程&#xff1a; 1. 注册开发者账号&#xff1a;首先&#xff0c;您需要在拼多多开放平台注册一个开发者账号。通过开发者账号&#xff0c;您可以获得API密钥和其他必要的信息。 2. 鉴权与认证&…

算法:图解位运算以及鸽巢原理应用

文章目录 实现原理基础位运算位图思想找最右侧数按位异或 算法思路典型例题基础位运算只出现一次的数字只出现一次的数字III 经典题型判断字符是否唯一两整数之和只出现一次的数字II消失的两个数字 鸽巢原理总结 本篇总结位运算中常见的算法题和思路&#xff0c;首先总结位运算…

(AS笔记)上传aar包到Maven中央仓库

目录 一、SonaType账户注册与登录 &#xff08;1&#xff09;注册 &#xff08;2&#xff09;登录 二、创建工单 &#xff08;1&#xff09;Github子域名验证 &#xff08;2&#xff09;自定义域名验证 三、登录Nexus Repository Manager 四、GPG签名生成和发布 五、Andr…

Redis 管道

1. 面试题 1.1 如何优化频繁命令往返造成的性能瓶颈? 1.2 问题由来 Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。一个请求会遵循以下步骤&#xff1a; 1 客户端向服务端发送命令分四步(发送命令→命令排队→命令执行→返回结果)&#xff0c;并监听Socket返…

mfc140u.dll丢失如何修复?解析mfc140u.dll是什么文件跟修复方法分享

大家好&#xff01;今天&#xff0c;我将和大家分享一下关于计算机中mfc140u.dll丢失的6种解决方法。希望我的分享能对大家在计算机使用过程中遇到问题时提供一些帮助。 首先&#xff0c;我想请大家了解一下什么是mfc140u.dll文件。mfc140u.dll是一个动态链接库文件&#xff0…

docker作业

目录 1、使用mysql:5.6和 owncloud 镜像&#xff0c;构建一个个人网盘。 1.1启动镜像 1.2启动cloud镜像 1.3浏览器访问 ​编辑 2、安装搭建私有仓库 Harbor 2.1下载docker-compose 2.2 磁盘挂载&#xff0c;保存harbor 2.3 修改配置文件 2.4安装 2.5浏览器访问 2.6 新…