1.字符指针

字符指针(Character Pointer)char*是指向字符数据的指针变量。

使用实例:

int main() {char ch = 'w';// 声明一个字符变量ch并赋值为 'w'char *pc = &ch; // 声明一个字符指针pc并将其指向ch的地址*pc = 'w';// 通过指针pc修改ch的值为 'w'char arr[] = "abcdef";// 声明一个字符数组arr并初始化为字符串 "abcdef"char* p = arr; // 声明一个字符指针p,将其指向数组arr的首地址return 0;}

还有一种使用方式如下:

#include int main() {const char *pstr = "hello world."; // 声明一个指向常量字符的指针pstr,并将其指向字符串常量"hello wrold."printf("%s\n", pstr);// 输出字符串"hello world."*pstr = 'w';// 尝试修改常量字符串,会导致编译错误,因为常量字符串不可修改printf("%c\n", *pstr);// 尝试访问常量字符串的第一个字符,不会发生修改,输出仍为原始字符'h'return 0;}

通过指针修改常量字符串的操作是非法的,会导致编译错误。常量字符串应该被视为只读数据。如果需要修改字符串内容,应该使用字符数组而不是指向常量字符的指针。

代码 const char* pstr = "hello world.";

特别容易让同学以为是把字符串 hello world 放到字符指针 pstr 里了,但是/本质是把字符串 hello world. 首字符的地址放到了pstr中。

下面的结果是什么?

#include int main() {char str1[] = "hello world.";char str2[] = "hello world.";const char *str3 = "hello world.";const char *str4 = "hello world.";if (str1 == str2)printf("str1 and str2 are same\n");elseprintf("str1 and str2 are not same\n");if (str3 == str4)printf("str3 and str4 are same\n");elseprintf("str3 and str4 are not same\n");return 0;}//str1 and str2 are not same//str3 and str4 are same
  • str1str2 是字符数组,它们包含了相同的字符串 “hello world.”。
  • str3str4 是指向常量字符的指针,它们也指向相同的字符串 “hello world.”。

然后,程序使用条件语句比较了这四个变量的指针值,并输出比较结果:

  • 通过 str1 == str2 比较,它们是两个不同的字符数组,所以输出 “str1 and str2 are not same”。
  • 通过 str3 == str4 比较,它们指向相同的常量字符,所以输出 “str3 and str4 are same”。

尽管 str1str2 的内容相同,但它们是两个不同的数组,因此它们在内存中的地址也不同。而 str3str4 是指向相同字符串常量的指针,它们指向的地址相同。

如果想比较字符串的内容而不是地址,应该使用字符串比较函数 strcmp(),例如 strcmp(str1, str2)

2.指针数组

在C语言中,指针数组是指一个数组,其元素都是指针类型的变量。指针数组可以存储指向不同类型对象的指针。

那么指针数组是指针还是数组?

是数组。是存放指针的数组。

例如:

int main() {//整型数组-存放整型的数组int arr[10];//字符数组-存放字符的数组char arr2[5];//指针数组-存放指针的数组int *arr3[5]; //存放整型指针的数组char *arr4[6];//存放字符指针的数组return 0;}

例如:

#include int main() {int a = 10;int b = 20;int c = 30;int d = 40;int e = 50;int *arr3[5] = {&a, &b, &c, &d, &e};//存放整型指针的数组int i = 0;for (i = 0; i < 5; i++) {printf("%d ", *(arr3[i]));}return 0;}

指针数组使用场景:用一个一维数组模拟二维数组

#includeint main() {//用一维数组模拟一个二维数组int arr1[] = {1, 2, 3, 4, 5};int arr2[] = {2, 3, 4, 5, 6};int arr3[] = {3, 4, 5, 6, 7};int arr4[] = {4, 5, 6, 7, 8};int *arr[4] = {arr1, arr2, arr3, arr4};int i = 0;for (i = 0; i < 4; i++) {int j = 0;for (j = 0; j < 5; j++) {printf("%d ", *(*(arr + i) + j));}printf("\n");}//int i = 0;//for (i = 0; i < 4; i++)//{//int j = 0;//for (j = 0; j < 5; j++)//{//printf("%d ", arr[i][j]);//}//printf("\n");//}return 0;}

输出结果:

1 2 3 4 5 2 3 4 5 6 3 4 5 6 7 4 5 6 7 8 

3.数组指针

3.1 数组指针的定义

数组指针是指针?还是数组?

答案是:指针。

我们已经熟悉: 整型指针: int * pint; 能够指向整型数据的指针。

浮点型指针: float * pf; 能够指向浮点型数据的指针。 那数组指针应该是:能够指向数组的指针

下面代码哪个是数组指针?

int *p1[10];int (*p2)[10];//p1, p2分别是什么?

解释:

int (*p)[10];

p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。

这里要注意:[]的优先级要高于号的,所以必须加上()来保证p先和结合。

int main(){int arr[10] = {1,2,3,4,5};int (* pa)[10] = &arr;//取出的是数组的地址存放到pa中,pa是数组指针变量char arr[5];char (*p1)[5] = &arr;}

3.2 &数组名VS数组名

对于下面的数组:

int arr[10];

arr&arr 分别是啥?

我们知道arr是数组名,数组名表示数组首元素的地址。

那&arr数组名到底是啥? 我们看一段代码:

#include int main() {int arr[10] = {0};printf("%p\n", arr);printf("%p\n", &arr);return 0;}

运行结果如下:

000000000061FDF0000000000061FDF0

可见数组名和&数组名打印的地址是一样的。

难道两个是一样的吗?

我们再看一段代码:

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

输出结果如下:

arr = 000000000061FDF0&arr= 000000000061FDF0arr+1 = 000000000061FDF4&arr+1= 000000000061FE18

根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。

实际上: &arr表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)

本例中&arr的类型是: int(*)[10] ,是一种数组指针类型数组的地址+1,跳过整个数组的大小,所以&arr+1相对于&arr的差值是40.

3.3 数组指针的使用

那数组指针是怎么使用的呢?

既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。

实例:

#include int main() {int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};int(*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p//但是我们一般很少这样写代码return 0;}

一个数组指针的使用:

#include void print1(int *arr, int sz) {int i = 0;for (i = 0; i < sz; i++) {//*(arr + i) 表示指针arr偏移i个元素后所指向的值。printf("%d ", *(arr + i));}printf("\n");}void print2(int (*p)[10], int sz) {int i = 0;for (i = 0; i < sz; i++) {//int (*p)[10]是数组指针,(*p)[i] 表示指针p所指向的数组的第i个元素的值。printf("%d ", (*p)[i]); //*p = arr;}printf("\n");}int main() {int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};int sz = sizeof(arr) / sizeof(arr[0]);print1(arr, sz);//1 2 3 4 5 6 7 8 9 10print2(&arr, sz);//1 2 3 4 5 6 7 8 9 10return 0;}

print1 函数的参数是一个整型指针,可以传递任意大小的整型数组给它进行打印。而 print2 函数的参数是一个指向包含10个整数的数组的指针,因此只能传递大小为10的整型数组给它进行打印。

这两种写法都可以用于打印整型数组的元素,选择使用哪种方式取决于你的需求和所操作的数组的类型和大小。

继续:

void print3(int arr[3][5], int r, int c) {int i = 0;for (i = 0; i < r; i++) {int j = 0;for (j = 0; j < c; j++) {//arr[i][j] 表示二维数组arr的第i行、第j列的元素的值。printf("%d ", arr[i][j]);}printf("\n");}}void print4(int (*p)[5], int r, int c) {//函数接受一个指向包含5个整数的数组指针pint i = 0;for (i = 0; i < r; i++) {int j = 0;for (j = 0; j < c; j++) {//*(*(p + i) + j) 表示指针p所指向的二维数组的第i行、第j列的元素的值。//p为指针 p+i表示一维数组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}};print3(arr, 3, 5);print4(arr, 3, 5);return 0;}

解析 *(*(p + i) + j)

  1. (p + i):首先,p 是一个指向包含5个整数的数组的指针。通过 p + i 运算,指针 p 偏移了 i 个整数大小的字节,即指向了二维数组的第 i 行。
  2. *(p + i): 在上一步中,p 偏移了 i 行,所以 *(p + i) 表示指针 p 所指向的二维数组的第 i 行的首个元素。这是一个指向整数的指针。
  3. (*(p + i) + j): 在上一步的基础上,j 是用于偏移列的索引。*(p + i) + j 表示指针 *(p + i) 所指向的一维数组中,偏移了 j 个整数大小的字节,即指向了二维数组中第 i 行、第 j 列的元素。
  4. *(*(p + i) + j): 最后,*(*(p + i) + j) 使用指针解引用操作符 *,获取指针 (*(p + i) + j) 所指向的整数值。这就是二维数组中第 i 行、第 j 列的元素值。

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

int arr[5];int *parr1[10];int (*parr2)[10];int (*parr3[10])[5];
  • int arr[5]; 这是一个包含5个整数的数组,名为arr
  • int *parr1[10]; 这是一个包含10个指向整数的指针的数组,名为parr1。它可以存储10个整数的地址。
  • int (*parr2)[10]; 这是一个指向包含10个整数的数组的指针,名为parr2。它指向一个具有10个整数的数组。
  • int (*parr3[10])[5]; 这是一个包含10个指向包含5个整数数组的指针的数组,名为parr3。它可以存储10个指向具有5个整数的数组的指针。

让我们分解int (*parr3[10])[5]并解释其含义:

  1. 从变量名parr3开始,我们知道这是一个数组。
  2. 继续向右,我们看到[10],表示parr3是一个包含10个元素的数组。
  3. 继续向右,我们看到*,表示数组的元素是指针。
  4. 继续向右,我们看到[5],表示指针指向的是包含5个整数的数组。
  5. 最后,我们有int,表示数组中的整数类型。

因此,int (*parr3[10])[5]可以解读为:parr3是一个包含10个元素的数组,每个元素都是指向包含5个整数的数组的指针。

4.数组参数、指针参数

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

4.1 一维数组传参

#include 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);}

上述代码中都OK。arr[10]等价于*arr,[]中的值可以省略。 *arr[20] 等价于**arr

4.2 二维数组传参

void test(int arr[3][5])//ok?{}void test(int arr[][])//ok?err{}void test(int arr[][5])//ok?{}int main() {int arr[3][5] = {0};test(arr);}

总结:

二维数组传参,函数形参的设计只能省略第一个[]的数字。因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。这样才方便运算。

void test(int *arr)//ok?{}void test(int *arr[5])//ok?err{}void test(int (*arr)[5])//ok?{}void test(int **arr)//ok? err{}int main() {int arr[3][5] = {0};test(arr);}

函数 test(int *arr[5]) 是错误的,因为它声明了一个包含 5 个指向整数的指针的数组,而不是一个指向整数的指针。函数 test(int **arr) 也是错误的,因为它声明了一个指向指针的指针,而不是一个指向整数或整数数组的指针。在主函数中,参数 int *arr 的正确函数声明应该是 test(int (*arr)[5]),因为它是一个包含 5 个整数的数组的指针 。

4.3 一级指针传参

#include void print(int *p, int sz) {int i = 0;for (i = 0; i < sz; i++) {printf("%d ", *(p + i));//1 2 3 4 5 6 7 8 9 10}}int main() {int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};int *p = arr;int sz = sizeof(arr) / sizeof(arr[0]);//一级指针p,传给函数print(p, sz);return 0;}

思考:

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

比如:

void test1(int *p){}//test1函数能接收什么参数?int型一维数组,和int型变量的地址void test2(char* p){}//test2函数能接收什么参数?char型一维数组,和char型变量的地址

4.4 二级指针传参

#include void test(int **ptr) {printf("num = %d\n", **ptr);}int main() {int n = 10;int *p = &n;int **pp = &p;test(pp);//num = 10test(&p);//num = 10return 0;}

思考:

当函数的参数为二级指针的时候,可以接收什么参数?

void test(char **p) {}int main() {char c = 'b';char *pc = &c;char **ppc = &pc;char *arr[10];test(&pc);//pc是一个一级指针,&pc即二级指针test(ppc);//ppc是一个二级指针test(arr);//Ok?return 0;}

数组名本身也可以被解释为指向数组首元素的指针。对于一个char*类型的数组arr[10]arr可以被解释为指向arr[0]的指针,而arr[0]又是一个char*类型的指针。

因此,当调用test(arr)时,实际上将指向arr[0]的指针传递给了test函数。在test函数内部,参数p的类型是char**,它可以接收指向char*类型的指针。因此,test(arr)是合法的。

5.函数指针

首先看一段代码:

#include void test() {printf("hehe\n");}int main() {printf("%p\n", test);printf("%p\n", &test);return 0;}

输出的结果:

00000000004015500000000000401550

输出的是两个地址,这两个地址是 test 函数的地址。

那我们的函数的地址要想保存起来,怎么保存?

下面我们看代码:

void test() {printf("hehe\n");}//下面pfun1和pfun2哪个有能力存放test函数的地址?void (*pfun1)();void *pfun2();

pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。

什么是函数指针?

函数指针是指向函数的指针变量。它可以用于在程序运行时动态地选择调用不同的函数,或者将函数作为参数传递给其他函数。

函数指针的声明:

return_type (*pointer_name)(parameter_list);

其中,return_type是函数的返回类型,pointer_name是指针变量的名称,parameter_list是函数的参数列表。例如,以下是一个指向返回整数类型、接受两个整数参数的函数指针的声明:

int (*sum_ptr)(int, int);

函数指针的赋值:

函数指针可以通过将函数的名称赋值给指针变量来进行初始化。例如,假设有以下函数定义:

int sum(int a, int b) {return a + b;}

可以将该函数赋值给函数指针:

sum_ptr = sum;

使用函数指针调用函数: 使用函数指针调用函数与直接调用函数的语法相似,只需将指针变量后面加上参数列表即可。例如:

int result = sum_ptr(3, 4);

实例1:

#include int Add(int x, int y) {return x + y;}int main() {//pf就是函数指针变量int (*pf)(int x, int y) = Add;//Add和&Add都是函数的地址,没有区别,类似于数组名和&数组名int sum = (*pf)(3, 5);//等价于int sum = Add(3, 5);printf("%d\n", sum);//8return 0;}

实例2:

#include int test(const char *str, double d) {}int main() {int (*pt1)(const char *, double) = &test;int (*pt2)(const char *str, double d) = &test;return 0;}

阅读两段有趣的代码:

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

(*(void (*)())0)();把0直接转换成一个void (*)()的函数指针,然后去调用0地址处的函数,函数的参数为无参

void (*signal(int , void(*)(int)))(int);是一次函数声明

声明的函数叫:signal

signal函数的第一个参数是int类型的

signal函数的第二个参数是一个函数指针类型,该函数指针指向的函数参数是int,返回类型是void

signal函数的返回类型也是一个函数指针类型,该函数指针指向的函数参数是int,返回类型是void

代码2太复杂,如何简化:

typedef void(*pfun_t)(int);//pfun_t 是一个函数指针,参数是int,返回类型是voidpfun_t signal(int, pfun_t);

6.函数指针数组

数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组

比如:

int *arr[10];//数组的每个元素是int*

函数指针数组中的每个元素都是一个函数指针。函数指针是指向函数的指针变量,可以用来调用该函数。

那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

int (*parr1[10])(); //函数指针数组,指向一个无参函数,返回值为intint *parr2[10](); int (*)() parr3[10];

答案是:parr1

parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢?

int (*)() 类型的函数指针

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

例子:(计算器)

#include 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.add2. sub****\n");printf("***** 3.mul4. 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("请输入2个操作数:>");scanf("%d %d", &x, &y);ret = Add(x, y);printf("%d\n", ret);break;case 2:printf("请输入2个操作数:>");scanf("%d %d", &x, &y);ret = Sub(x, y);printf("%d\n", ret);break;case 3:printf("请输入2个操作数:>");scanf("%d %d", &x, &y);ret = Mul(x, y);printf("%d\n", ret);break;case 4:printf("请输入2个操作数:>");scanf("%d %d", &x, &y);ret = Div(x, y);printf("%d\n", ret);break;case 0:printf("退出计算器\n");break;default:printf("选择错误\n");break;}} while (input);}

使用函数指针数组简化代码:

#include 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.add2. sub****\n");printf("***** 3.mul4. 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 == 0) {printf("退出计算器\n");break;}if (input >= 1 && input <= 4) {printf("请输入2个操作数:>");scanf("%d %d", &x, &y);ret = pfArr[input](x, y);printf("%d\n", ret);} else {printf("选择错误\n");}} while (input);}

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

指向函数指针数组的指针是一个指针,它指向函数指针数组的起始地址。可以使用这个指针来访问函数指针数组中的元素,以及通过函数指针调用相应的函数。

下面是一个简单的示例,展示了如何声明和使用指向函数指针数组的指针:

#include void test(const char* str){printf("%s\n", str);}int main(){ //函数指针pfunvoid (*pfun)(const char*) = test; //函数指针的数组pfunArrvoid (*pfunArr[5])(const char* str); pfunArr[0] = test; //指向函数指针数组pfunArr的指针ppfunArr void (*(*ppfunArr)[5])(const char*) = &pfunArr; return 0;}// 函数指针的声明typedef void (*FuncPtr)(int);// 函数定义void func1(int num) {printf("func1: %d\n", num);}void func2(int num) {printf("func2: %d\n", num);}void func3(int num) {printf("func3: %d\n", num);}int main() {// 函数指针数组的定义和初始化FuncPtr funcs[] = { func1, func2, func3 };// 指向函数指针数组的指针FuncPtr (*ptrToFuncArray)[3] = &funcs;// 通过指针访问函数指针数组的元素,并调用相应的函数(*ptrToFuncArray)[0](10);// 调用 func1(*ptrToFuncArray)[1](20);// 调用 func2(*ptrToFuncArray)[2](30);// 调用 func3return 0;}

8.回调函数

回调函数是一种函数指针的使用方式,它允许将一个函数作为参数传递给另一个函数,并在需要的时候调用该函数。回调函数提供了一种灵活的机制,使得代码可以根据特定的条件或事件来执行不同的行为。

#include // 回调函数类型的定义typedef void (*CallbackFunc)(int);// 执行回调函数的函数void performCallback(CallbackFunc callback, int value) {// 调用传递进来的回调函数callback(value);}// 回调函数1void callback1(int value) {printf("Callback 1: %d\n", value);}// 回调函数2void callback2(int value) {printf("Callback 2: %d\n", value);}int main() {int data = 10;// 使用回调函数1进行操作performCallback(callback1, data);// 使用回调函数2进行操作performCallback(callback2, data);return 0;}

输出结果如下:

Callback 1: 10Callback 2: 10

我们首先定义了一个回调函数类型 CallbackFunc,它是一个函数指针类型,可以指向一个带有一个 int 参数和 void 返回类型的函数。

然后,我们定义了一个名为 performCallback 的函数,它接受一个回调函数作为参数,并在需要的时候调用该函数,传递给它一个值。

接下来,我们定义了两个回调函数 callback1callback2,它们都符合 CallbackFunc 类型的函数签名。

main 函数中,我们定义了一个整数变量 data,然后使用 performCallback 函数两次,分别传递不同的回调函数和 data 值。这样就实现了在不同情况下执行不同回调函数的功能。

当程序运行时,会依次调用 performCallback 函数,并根据传递的回调函数打印相应的信息。

qsort函数

C语言中qsort函数也是通过函数指针回调函数实现的

#include //qosrt函数的使用者得实现一个比较函数int int_cmp(const void *p1, const void *p2) {return (*(int *) p1 - *(int *) p2);}int main() {int arr[] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 0};int i = 0;qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {printf("%d ", arr[i]);}printf("\n");return 0;}

用冒泡排序模拟实现下回调函数方式:

#include int cmp_int(const void *e1, const void *e2) {return (*(int *) e1 - *(int *) e2);}//char类型1字节,适用于任何类型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++;}}void bubble_sort(void *base, int sz, int width, int (*cmp)(const void *e1, const void *e2)) {int i = 0;//趟数for (i = 0; i < sz - 1; i++) {//一趟冒泡排序的过程int j = 0;for (j = 0; j < sz - 1 - i; j++) {if (cmp((char *) base + j * width, (char *) base + (j + 1) * width) > 0) {//交换Swap((char *) base + j * width, (char *) base + (j + 1) * width, width);}}}}int main() {int arr[] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 0};//char *arr[] = {"aaaa","dddd","cccc","bbbb"};int i = 0;bubble_sort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), cmp_int);for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {printf("%d ", arr[i]);}printf("\n");return 0;}