详解C语言的指针
一、字符指针
定义:指向字符的指针变量。
语法格式如下:
// 指向单个字符的指针变量
char ch0 = 'a';
char* pc = &ch0;
// 指向字符串的指针变量
char* pc1 = "abcdef";
// 指向字符串的指针变量的另一种写法
char str[] = "abcdef";
char* pstr = str;
实际上,字符指针变量中存储的是指向的字符串中的首字符地址,我们可以用代码验证:
字符指针可以像数组那样通过索引,访问对应的字符,如下:
注意:在使用字符指针访问字符串时,字符串必须是以空字符’\0’结尾的,否则可能会导致访问越界。
二、指针数组
2.1定义
定义:是一个数组,数组元素的类型是指针变量。
在C语言中,指针数组可以定义为type *array[size]
的形式,其中type
表示指针数组中每个元素的数据类型,array
表示指针数组的名称,size
表示数组元素的个数。例如,int *ptr_arr[10]
表示一个包含10个整型指针的指针数组。
// 定义三个整型数组
int arr1[3] = {1, 2, 3};
int arr2[3] = {4, 5, 6};
int arr3[3] = {7, 8, 9};
// 创建一个指向整型数组的指针变量
int *arr[3] = {arr1, arr2, arr3};
解读:标识符arr先与数组下标运算符[]结合,表明arr是一个数组。arr继续与int*结合,说明该指针变量指向的数组的元素是整型。因此,arr是一个指针数组,每个指针变量都是整型指针。
注意数组元素的类型不一定都是相同的,但是在使用这些数组元素时需要强制类型转换。举个例子:
int a = 2;
int* pa = &a;// 整型指针
double d = 2.2;
double* pd = &d;// 双精度浮点型指针
int arr[] = {2, 3, 4, 5, 6};
char* str = "abcde";// 字符指针
// 使用void*可以指向任意类型的数据,因此ptrf可以存储不同类型的指针
void* ptrf[] = {pa, pd, arr, str};
//使用ptrf访问数组元素
printf("%s\n", (char*)ptrf[3]);
2.2 指针数组的用法
指针数组的用法有很多,小编在此就挑选两个进行详细讲解。
使用指针数组访问数组元素,说明指针数组的用法:
#include <stdio.h>
int main()
{
int arr1[3] = {1, 2, 3};
int arr2[3] = {4, 5, 6};
int arr3[3] = {7, 8, 9};
// 创建一个存储整型指针的数组
int *arr[3] = {arr1, arr2, arr3};
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
printf("%d ", *(*(arr + i) + j));
}
printf("\n");
}
return 0;
}
运行结果如下:
1 2 3
4 5 6
7 8 9
可以使用指针数组存储字符串的地址,方便访问和遍历多个字符串。举个例子说明
#include <stdio.h>
int main()
{
// 该数组的元素是字符指针,每个字符指针存储的都是对应字符串中首字符的地址
char* str[] = {"abc", "def", "ghi", "jkl"};
for (int i = 0; i < 4; i++)
{
// str[i]表示数组中字符串的起始地址
printf("%s\n", str[i]);
}
return 0;
}
当然了,指针数组的用法不止上述这些,还有很多,比如说:使用函数指针数组存储多个函数的指针,可以动态地调用函数;在需要动态分配内存的场景,需要使用指针数组存储指向不同的内存块的指针,方便释放和管理等等,在此就不一一举例。
三、数组指针
3.1、数组指针的定义
定义:数组指针是一种指针变量,指向的是一个数组。
预备知识:二维数组其实是一维数组的数组。二维数组的数组名就是数组首元素的地址。对二维数组的数组名解引用获取的值就是二维数组的首元素,二维数组的首元素相当于一维数组的数组名。
数组指针的语法格式如下:
type *var_name[number];
// type表示指针指向的数组的元素的数据类型
// var_name表示数组指针变量名
// number表示指向的数组有多少个元素
// 举个例子:
int arr[3] = {1, 2, 3};
// 创建一个数组指针
int (*parr)[3] = &arr;
解读数组指针:标识符parr先与解引用符号*结合,说明变量parr是一个指针变量。然后parr再与数组下标运算符[]结合,说明指针指向的是一个数组。最后parr再与int结合,说明指向的数组是一个整型数组。
3.2、数组名 VS &数组名
在绝大多数情况下,数组名就 是数组首元素地址,但有两个例外。
- sizeof(数组名)。运算符sizeof内部单独放置一个数组名时,此时的数组名表示整个数组的大小,此时sizeof计算的是整个数组在内存中的总大小,运算结果的单位是字节。
- &数组名。此时的数组名表示整个数组的大小。虽然&数组名和数组名的地址值是一样的,但是它们所代表的意义不一样。
#include <stdio.h>
int main()
{
int arr[3] = {1, 2, 3};
printf("sizeof(*arr) = %d\n", sizeof(*arr));// 4
// sizeof(arr)的计算结果代表整个数组的大小
// 整型数组arr由三个元素组成,
//每个整型元素在内存中占4个字节(默认为x-86环境),所以数组在内存中共占12个字节
printf("sizeof(arr) = %d\n", sizeof(arr));// 12个字节
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;
}
源代码在VS的x-86环境下编译运行,运行结果如下:
arr的地址值与arr + 1的地址值相减的结果为:0xc - 0x8 = 0x4;
&arr的地址值与&arr + 1的地址值相减的结果为:0xd4 - 0xC8 = 0xc = (12)10
解读:
规定:给指针(地址)加1,其值会增加对应类型大小的数值。arr+1和&arr+1的结果不一样说明arr对应的数据类型和&arr对应的数据类型不一样。一个地址标识一个字节。由arr到arr+1,相应的地址值增加4个字节,所以arr对应的数据类型是整型指针类型。由&arr到&arr+1, 相应的地址值增加12个字节, 所以&arr对应的数据类型是整型数组指针类型。
另外要注意的是:对空指针进行加1等操作是违法操作的,会导致程序崩溃。
3.3、数组指针的应用
两个用途:访问数组中的元素;可以把数组的地址传递给函数, 从而实现在函数内部访问数组元素,减少了函数调用时内存的开销。
例子一:在函数之间传递数组的地址。
#include <stdio.h>
// 数组指针作函数形参,接收的是数组的地址
void test(int (*parr)[3], int row, int col); // 函数声明
int main()
{
int arr[][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int len = sizeof(arr) / sizeof(*arr);
// 数组名arr是数组首元素地址,也是内含3个整型元素的数组的地址
// 当传递的实参为arr时,其实传递的是一维数组arr[0]的地址
test(arr, 3, 3);
return 0;
}
// 当传递的实参是数组的地址时,必须要用数组指针接收数组的地址
void test(int (*parr)[3], int row, int col) // 函数定义
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
printf("%d ", *(*(parr + i) + j));
}
printf("\n");
}
}
运行结果如下:
1 2 3
4 5 6
7 8 9
例子二:使用数组指针访问数组中的元素
#include <stdio.h>
int main()
{
int arr[3] = {1, 2, 3};
int (*parr)[3] = &arr;
for (int i = 0; i < 3; i++)
{
//先解引用指针parr得到数组的首元素,再通过下标访问数组元素。
printf("%d ", (*parr)[i]);
}
return 0;
}
运行结果:
1 2 3
综上,数组指针是C语言常用的数据类型,可以方便地操作数组,节省内存空间,提高运行效率。
四、函数指针
4.1、函数指针的定义
可以类比:整型指针,整型指针就是指向整型变量的指针变量;字符指针就是指向字符的指针变量;
因此,函数指针就是指向函数的的指针变量。那问题来了,函数有地址吗?当然,函数在内存中也有地址。在C语言中,函数名被视为函数的地址。
注意:函数名与&函数名表示的意义是一样的,举个例子:
#include <stdio.h>
int Add(int x, int y)
{
return (x + y);
}
int main()
{
printf(" Add = %p\n", Add);
printf("&Add = %p\n", &Add);
printf(" Add + 1 = %p\n", Add + 1);
printf("&Add + 1 = %p\n", &Add + 1);
return 0;
}
源代码运行结果,如下:
解读:
根据程序的运行结果,函数名和&函数名的地址值是一样的,而且函数名+1的地址值与&函数名+1的地址值都是相同的,说明函数名对应的数据类型和&函数名对应的数据类型都是相同的。因此,在一般情况下函数名和&函数名没有区别,具有相同的意义。
函数指针的语法格式如下:
return_type (* arr_name[size]) (parameter_list);
返回值类型 (*指针变量名)(参数列表)
返回值类型:指针指向的函数的返回值类型
指针变量名:函数指针变量名
参数列表:被指向的函数的参数列表
// 举例说明函数指针的语法
#include <stdio.h>
int Add(int x, int y)
{
return (x + y);
}
int main()
{
// 创建一个函数指针,该指针变量指向一个函数
int (*pf)(int, int) = Add;
// 另一种写法,如下
//int (*pf)(int, int) = &Add;
// 使用函数指针
int ret = (*pf)(3, 5);
printf("ret = %d\n", ret);
return 0;
}
运行结果:
ret = 3
解读
标识符pf先解引用符号* 说明pf是一个指针变量,那么该指针是什么类型变量呢?去除*pf,剩下的符号就是对指针变量的的类型说明。因此,该指针指向的是一个函数,是一个函数指针。你可以理解为type (*)(type, type)表示的一种函数指针类型,其中type表示基本的数据类型比如int,float, char* 等等。
也许你会问,函数指针的这种用法与直接调用函数有什么区别?没错,函数指针的用法的确不是这样的。在此仅仅是为了说明有一种语法现象即函数指针。
4.2、函数指针数组
首先,函数指针数组是一个数组,然后该数组的元素是函数指针。举个例子:
#include <stdio.h>
int Add(int x,int y)
{
return (x + y);
}
int main()
{
// 创建一个函数指针
int (*pf)(int, int) = Add;
// 创建一个函数指针数组
int (*pfArr[3])(int, int) = {Add};
return 0;
}
解读:标识符先与数组下标运算符,说明pf是一个数组,但是数组的元素类型是什么呢?pf与数组下标运算符[]结合后,再与int (*)(int, int)结合,说明数组pf的元素类型是函数指针。
函数指针数组的应用:转移表
// 函数指针数组的用法:转移表
#include <stdio.h>
// 声明函数
int Add(int x, int y);
int Sub(int x, int y);
int Mul(int x, int y);
int Div(int x, int y);
void Menu();
int main()
{
int input = 0;
// 创建一个函数指针数组
int (*pfArr[])(int, int) = {NULL, Add, Sub, Mul, Div};
do
{
Menu();//显示菜单
printf("请输入选项:");
scanf("%d", &input);
if (input >= 1 && input <= sizeof(pfArr) / sizeof(*pfArr) - 1)
{
int num1 = 0;
int num2 = 0;
printf("请输入两个操作数:");
scanf("%d %d", &num1, &num2);
printf("%d\n", (*pfArr[input])(num1, num2));
}
else if (0 == input)
{
printf("已退出计算器!\n");
break;
}
else
{
printf("输入错误,请重新输入\n");
}
} while (input);
return 0;
}
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");
}
解读:
在转移表中,我们将所有可能用到的函数都放在一个函数指针数组中,然后通过输入数值来索引这个数组里的元素,最后解引用函数指针调用相应的函数。转移表是一种静态的数据结构,它将输入值映射到相应的处理函数中。它的优点是将大量的分支语句转化为一个表格,从而提高代码的可读性与可维护性。
当然,函数指针数组的应用很多比如多态的实现,即通过不同的函数指针调用不同的函数,从而实现同一接口的不同实现。总之,函数指针数组是非常有用的数据类型,可以实现非常多的功能,非常有利于提高程序的灵活性和拓展性。
五、回调函数
在阐述回调函数之前,先讲一个简单易懂的例子。
某一天,你要去商店里买东西,但是商店里没有货,于是你留下了你的电话号码。过几天,店员通过你留下的手机号码打电话告知你店里有你需要的商品。于是你去店里购买商品。在这个例子中,你留下的电话号码就是回调函数。店员告知你店里有货就是出现触发了回调函数的事件。店员给你打电话就是调用回调函数。你去商店购买商品就是响应回调函数。
回调函数的定义:函数指针(地址)可以作为函数的参数,而回调函数就是通过函数指针调用的函数。如果把函数指针传递给另一个函数,当通过解引用函数指针来调用该指针指向的函数时,那么就说该指针指向的函数就是回调函数。
回调函数的意义是什么呢?回调函数的是为了实现灵活地设计程序和功能拓展而设计的。通过回调函数,可以在程序运行时动态地调用相应的函数。听到这,你也许还是不太懂,请看下面的例子:
//通过回调函数编写一个实现加减乘除功能的计算器
#include <stdio.h>
// 函数声明
int Add(int x, int y);
int Sub(int x, int y);
int Mul(int x, int y);
int Div(int x, int y);
void Menu();
void Cal(int (*pf)(int, int));
int main()
{
// 创建一个接收用户输入的变量
int input = 0;
do
{
Menu();
printf("请输入选项:");
scanf("%d", &input);
switch (input)
{
case 1:
// 参数是函数地址
Cal(Add);
break;
case 2:
Cal(Sub);
break;
case 3:
Cal(Mul);
break;
case 4:
Cal(Div);
break;
case 0:
printf("退出计算器!\n");
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (input);
return 0;
}
// 函数定义
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 Cal(int (*pf)(int, int))
{
int num1 = 0;
int num2 = 0;
printf("请输入两个操作数:\n");
scanf("%d %d", &num1, &num2);
// 在另一个函数的内部调用回调函数
printf("%d\n", (*pf)(num1, num2));
}
当我们想在该程序拓展新功能比如左移或右移时,只需编写实现左移或右移功能的函数和更改菜单函数即可,其他模块不需要任何的改动。通过回调函数,极大地增加了程序设计的灵活性。
小编是IT初入门,上述讲解如有错误,请多多包涵。
y);
}
void Menu()
{
printf(“\n");
printf(" 1.add 2.sub \n");
printf(" 3.mul 4.div \n");
printf(" 0.exit \n");
printf("\n”);
}
// 实参必须是函数指针,才能接收函数地址
void Cal(int (*pf)(int, int))
{
int num1 = 0;
int num2 = 0;
printf(“请输入两个操作数:\n”);
scanf(“%d %d”, &num1, &num2);
// 在另一个函数的内部调用回调函数
printf(“%d\n”, (*pf)(num1, num2));
}
当我们想在该程序拓展新功能比如左移或右移时,只需编写实现左移或右移功能的函数和更改菜单函数即可,其他模块不需要任何的改动。通过回调函数,极大地增加了程序设计的灵活性。
***
小编是IT初入门,上述讲解如有错误,请多多包涵。