关于指针的初步理解可以浏览之前写的博客C语言——指针_学习笔记
本文目录
借助一些经典例题来深入理解C语言指针
一、指针运算
对于指针运算,在前面C语言——指针_学习笔记一文中,关于指针加减法和指针的自增自减运算已经做过详细介绍,
本文将结合数组相关知识,对指针运算进行深入剖析。(主要以题目为主)
1.1题目一
#include<stdio.h>
int main()
{
char* a[] = {
"work","at","alibaba" };
char** pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
输出结果为:
at
如下图:
题目意思是,有一个一维数组,有3个元素,数组中每个元素的类型都是 char* 类型,存的是三个字符常量首字母的地址(a[]为指针数组,数组的每个元素是指针类型);又接着把指针数组a[]的首元素地址存在pa 中,然后进行pa++操作,而后以 %s的形式打印 *pa 。
1.2 题目二
#include <stdio.h>
int main()
{
char* c[] = {
"ENTER","NEW","POINT","FIRST" };
char** cp[] = {
c + 3,c + 2,c + 1,c };
char*** cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *-- * ++cpp + 3);
printf("%s\n", *cpp[-2] + 3);
printf("%s\n", cpp[-1][-1] + 1);
return 0;
}
输出结果为:
首先分析一下这段代码
通过前三行代码,可以大致画出一个指向关系的图,如下:
-
下面分析 printf(“%s\n”, **++cpp); ++cpp使得cpp指向c+2元素,*++cpp 得到的是数组c中的第三个元素,也就是首元素P的地址,所以 **++cpp就是对P的地址进行解引用,以%s 的形式打印出来,所以结果为 POINT。
-
接着,指向关系图发生变化,如下
-
因为上一行代码又进行了++cpp 的操作,所以指向关系图又变了,如下图:
-
指向关系图没变
二、常量指针
常量指针(const pointer)是一种指针,它指向的内容不可以被修改,但指针本身可以移动。常量指针的使用场景包括但不限于以下几种情况:
2.1 保护数据
有时,我们希望指针指向的数据是常量,即不能被修改。这通常出现在需要保护数据的完整性或者安全性的时候。例如,在函数中传递指向敏感数据的指针时,可以使用常量指针来防止数据被修改。
const int *ptr = &secret_data;
2.2 函数参数传递
在函数参数传递中,常量指针可以用于防止函数修改传入的数据。例如,在实现一个排序函数时,我们可能不希望该函数修改传入的数据,可以使用常量指针。
void sort(const int* arr, int n) {
// ...
}
2.3 字符串处理
在C语言,字符串通常被表示为字符数组或字符指针。当处理字符串时,常量指针可以确保字符串的内容不会被修改,这有助于防止潜在的错误
const char* str = "Hello, World!";
2.4 优化性能
常量指针和普通指针在底层硬件上可能有不同的行为。在一些情况下,使用常量指针可能会提高性能,特别是在涉及位操作和高精度计算时。然而,这种优化通常需要对底层硬件和编译器有深入的理解,因此,如果没有充分的理由,一般不推荐进行这种优化。
2.5 数据结构的const成员
在某些数据结构中,常量指针可以用于表示该数据结构是常量,例如const STL容器。
2.6 避免类型转换
常量指针也可以用于避免不必要的类型转换。例如,如果你有一个函数返回一个const double*,那么你就不需要将返回值转换为其他类型。
三、变量指针
变量指针的使用场景举例包括但不限于以下几种情况:
3.1 写一个函数交换两个变量的值
通过使用指针,可以在函数中交换两个变量的值。
#include <stdio.h>
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 10;
int y = 20;
printf("Before swap: x=%d, y=%d\n", x, y);
swap(&x, &y);
printf("After swap: x=%d, y=%d\n", x, y);
return 0;
}
3.2 函数返回运算的状态,但结果通过指针返回
例如,让函数返回一个特殊的不属于有效范围内的值来表示出错。
#include <stdio.h>
int calculate(int *result) {
// ... some calculation ...
if (error) {
*result = -1; // set result to -1 if there is an error
return 0;
} else {
*result = 123; // set result to some value if no error
return 1;
}
}
int main() {
int result = -1; // Initialize result to -1
calculate(&result);
if (result == -1) {
printf("Error occurred!\n");
} else {
printf("Result: %d\n", result);
}
return 0;
}
四、数组指针和指针数组
4.1 概念
- 数组指针是指针,是指向数组的指针。
例如:
int arr[] = {
1, 2, 3, 4, 5};
int *ptr = arr; // ptr是指向数组的指针,指向数组首元素的地址,ptr+1是跨过4个字节
int *ptra = &a;//ptr是指向整个数组的指针,也是数组首元素的地址,但是ptra+1跨过20个字节
- 指针数组是数组,数组中的每个元素存的是一个地址。
char* a[] = {
"work","at","alibaba" };//这里的数组a就是一个指针数组,数组中的每个元素都是char*类型的指针
指针数组的每个元素是地址,⼜可以指向⼀块区域。
注意:前文指针运算的时候有提到。
4.2 指针数组模拟实现二维数组
既然指针数组的每个元素是地址,而这个地址还可以接着指向一块区域,那么就可以利用这一点,用指针数组来模拟实现二维数组
代码如下:
#include <stdio.h>
int main()
{
int arr1[] = {
1,2,3,4,5 };
int arr2[] = {
2,3,4,5,6 };
int arr3[] = {
3,4,5,6,7 };
//数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中
int* parr[3] = {
arr1, arr2, arr3 };
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 5; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
通过指针数组来访问并打印arr1,arr2和arr3三个数组中的内容,运行结果如下:
他们之间的关系画图表示如下:
parr是一个指针数组,有三个元素,都是地址,第一个元素存的是数组 arr1首元素的地;第二个元素存的是数组arr2首元素的地址;第三个元素存的是数组arr3 首元素的地址。要访问arr[1]的话,可以写成parr[0][1] ,神似二维数组。
4.3 数组传参的本质
在C语言中,数组的传递通常是通过指针进行的。当你将数组作为参数传递给函数时,实际上传递的是数组的地址。这是因为在C语言中,数组名实际上就是一个指向数组第一个元素的指针。
例如:
#include <stdio.h>
void printArray(int arr[], int size) {
//其实这里函数写成 void printArray(int* , int size)这种形式也是可以的
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int arr[] = {
1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
printArray(arr, size);
return 0;
}
补充:一维数组传参的本质是数组首元素的地址,二维数组传参的本质是数组第一行的地址
五、函数指针
函数指针是指针,是指向函数的指针,存放的是函数的地址,可以通过函数指针变量调用函数。
5.1 函数的地址:
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("test: %p\n", test);
printf("&test: %p\n", &test);
return 0;
}
运行结果如下:
说明函数也是有地址的,函数名,&(函数名)都可以拿到函数的地址。
函数指针类型解析
int (*pf3) (int x, int y)
| | ------------
| | |
| | pf3指向函数的参数类型和个数的交代
| 函数指针变量名
pf3指向函数的返回类型
int (*) (int x, int y) //pf3函数指针变量的类型
5.2 回调函数
谈到函数指针就不得不提到回调函数,回调函数是什么?
回调函数就是一个通过函数指针调用的函数。
如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
举个例子来理解:
现在要写一个计算器程序,实现两个整形数字的加减乘除运算。
利用函数指针,把调用的函数的地址以参数的形式传递过去,使用函数指针接收,函数指针指向什么函数就调用什么函数,这里其实使用的就是回调函数的功能。
5.3 转移表
函数指针数组的实现;转移表
前文已经介绍了指针数组的相关内容,那么这里也就很好理解了,函数指针数组是一个数组,数组中每个元素都是函数指针,数组中每个元素存储的是一个函数的地址。
上面计算器的程序可以利用函数指针数组进一步优化:
#include <stdio.h>
int Add(int a, int b)
{
return a + b;
}
int Sub(int a, int b)
{
return a - b;
}
int Mul(int a, int b)
{
return a * b;
}
int Div(int a, int b)
{
return a / b;
}
int main()
{
int input = 0;
int ret = 0;
int a = 0;
int b = 0;
//初始化一个函数指针数组,5个元素
int (*p[5])(int, int) = {
0,Add,Sub,Mul,Div}; //这里加入一个元素0,是为了使数组下标和选择匹配
do
{
printf("*****************************\n");
printf("******* 1. Add 2.Sub ******\n");
printf("******* 3.Mul 4.Div ******\n");
printf("******* 0.Exit (退出) ******\n");
printf("*****************************\n");
printf("请选择你要使用的运算法则:");
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("输入操作数:");
scanf("%d %d", &a, &b);
ret = (*p[input])(a, b);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出计算器\n");
}
else
{
printf("输入有误\n");
}
} while (input);
return 0;
}
六、二级指针和多级指针
二级指针是指向指针的指针,它通常用于处理指向指针的指针变量。例如,如果你有一个指向整数的指针,并且你想通过一个指针来存储该指针的地址,那么你需要使用二级指针。
多级指针是指向指针的指针,可以有多级,通常用于处理指向指针的指针变量。例如,如果你有一个指向整数的一级指针,并且你想通过一个指针来存储该指针的地址,那么你需要使用二级指针。同理,如果你有一个指向二级指针的一级指针,并且你想通过一个指针来存储该一级指针的地址,那么你需要使用三级指针。以此类推,可以有多级指针。
多级指针解题建议多画内存布局图,搞清楚指针到底指向什么位置,是什么内容!!!
多级指针一般不常用,后面如果接触到了实例再补充。