详解C语言指针

一、字符指针

定义:指向字符的指针变量。

语法格式如下:

// 指向单个字符的指针变量
char ch0 = 'a';
char* pc = &ch0;
// 指向字符串的指针变量
char* pc1 = "abcdef";
// 指向字符串的指针变量的另一种写法
char str[] = "abcdef";
char* pstr = str;

实际上,字符指针变量中存储的是指向的字符串中的首字符地址,我们可以用代码验证:

image-20230311162730608

字符指针可以像数组那样通过索引,访问对应的字符,如下:

注意:在使用字符指针访问字符串时,字符串必须是以空字符’\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 &数组名

在绝大多数情况下,数组名就 是数组首元素地址,但有两个例外。

  1. sizeof(数组名)。运算符sizeof内部单独放置一个数组名时,此时的数组名表示整个数组的大小,此时sizeof计算的是整个数组在内存中的总大小,运算结果的单位是字节。
  2. &数组名。此时的数组名表示整个数组的大小。虽然&数组名和数组名的地址值是一样的,但是它们所代表的意义不一样。
#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环境下编译运行,运行结果如下:

数组名VS&数组名

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

源代码运行结果,如下:

image-20230309221203083

解读:

​ 根据程序的运行结果,函数名和&函数名的地址值是一样的,而且函数名+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初入门,上述讲解如有错误,请多多包涵。

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值