深入理解指针——对指针的进阶使用
目录
1.数组名的理解
数组名就是地址,而且还是数组首元素的地址。
我们测试下面这个程序
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];//使用&arr[0]拿到数组首元素地址,并放在指针变量p中。
printf("&arr[0]=%p\n", &arr[0]);
printf(" p =%p\n", p);
printf(" arr =%p\n", arr); //数组名
return 0;
}
我们可以看出数组名和数组首元素的地址打印出的结果是一模一样的,所以,数组名就是数组首元素的地址。
但是也有特殊的情况:
- sizeof(数组名),sizeo中单独放数组名的时候,这里的数组名就表示的是这整个数组,计算的就是整个数组的大小,单位是字节。
- &数组名,这里数组名也是表示的整个数组,取出的就是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的,类型不一样,地址+1跳过的字节就会不一样,在后面会有介绍)
除此之外,其余任何地方使用数组名,数组都表示首元素的地址。
接下来,我们测试这段代码
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("sizeof(arr) = %d \n", sizeof(arr));
printf("&arr = %p \n", &arr);
printf("&arr+1 = %p \n", &arr + 1);
printf("arr = %p \n", arr);
printf("arr+1 = %p \n", arr+1);
printf("&arr[0] = %p \n", &arr[0]);
printf("&arr[0]+1 = %p \n", &arr[0]+1);
return 0;
}
这是它输出的结果,我们怎么去理解这么一堆的数据呢?
我们可以通过自动窗口来查看一些元素的类型,对比一下类型
可以看出整个数组的地址和数组首元素的地址类型是有区别的,&arr是数组的整个地址,类型是int[10]*,这是数组指针变量,我们具体在后面介绍,不同类型+1是跳过一个这个类型占的字节大小空间,所以上面那输出的一堆数据就很好去理解了。
2.使用指针访问数组
根据前面的学习,我们得知:
- 数组在内存中是连续存放的。
- 指针+-整数运算,方便我们获得每一个元素的地址。
所以,可以很方便的使用指针访问数组了。
int main()
{
int arr[10] = { 0 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]); //求数组元素个数
int* p = arr;
//输入
for (i = 0; i < sz; i++)
{
scanf("%d", p + i); //这个等价于scanf("%d", p[i]);
//也可也这样写
//scanf("%d", arr + i) //这个等价于scanf("%d",arr[i]);
}
//输出
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i)); //这个可以写成printf("%d",p[i]); //本质上p[i]==*(p+i)
// []是下标引用操作符
//arr[i]==i[arr] <--->*(arr+i)==*(i+arr)
//arr[i]<==>*(arr+i)<-->*(i+arr)<==>i[arr]
}
}
运行上述程序,结果上图。
其实数组元素的访问在编译器处理的时候,也是转换成首元素的地址+偏移量求出元素的地址,然后再解引用来访问它。(arr[i]<==>*(arr+i))
3.一维数组传参的本质
数组可以传递给函数的,在此之前我们了解了有:
- 数组就是数组,是一块连续的空间(数组大小和数组元素个数、类型都有关系)
- 指针(变量)就是指针(变量),是一个变量,占4/8个字节(32位/64位)
- 数组名是地址,是首元素地址(除了两个特殊例外)
- 可以使用指针来访问数组
我们测试下列这一个代码,让一个数组传给一个函数,让它计算这个数组的元素个数:
//测试一维数组传参(测试把一个数组传给一个函数后,让这个函数求数组的元素个数)
void test(int arr[])
{
int sz2 = sizeof(arr) / sizeof(arr[0]);
printf("sz2 = %d\n", sz2);
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz1 = sizeof(arr) / sizeof(arr[0]);
printf("sz1 = %d\n", sz1);
test(arr);
return 0;
}
输出结果可以看出,明显有问题了,这个问题在哪呢?这就要考虑到一维数组传参的本质了:数组传参本质上传递的是数组首元素的地址(所以形参访问的数组和实参的数组是同一个数组)。由于数组名是数组首元素的地址,且在数组传参时,传递的是数组名。
void test(int arr[]) //(int arr[])<==>(int *arr) 参数写成数组形式,本质上还是指针
//所以函数形参的部分理论上应该使用指针变量接收数组首元素地址。
{
}
所以sizeof(数组名)计算的是一个地址的大小,而不是数组的大小,并且函数参数部分实际上是指针类型;sizeof()函数功能返回一个对象或者类型所占的内存字节数;因为指针就相当于地址,所占内存与指针指向对象没有任何关系(所有的指针变量所占内存大小相等,32位下4个字节、64位下8个字节)。
注:形参的数组是不会单独创建数组空间的,所以形参的数组是可以省略掉数组大小的。
一维数组传参,形参的部分可以携程数组的形式,也可也写成指针的形式。
void Print(int arr[10]);
//可以写成
void Print(int arr[]);
//也可也写成
void Print(int* arr);
4.冒泡排序
冒泡排序是两两相邻的元素进行比较
我们通过对数组名的理解和一维数组传参的本质及指针,就可以实现一个冒泡排序的函数
int arr[] = { 4,1,5,7,2,3,6,9,8,0 }; //创建一个整型数组
//用冒泡排序将它改为升序数组。
接下来给一个图画来大概描述一下冒泡排序原理
//冒泡排序的实现
void bubble_sort(int arr[],size_t sz) 用参数sz接收数组元素的个数
{
int i = 0;
for (i = 0; i < sz-1; i++) //趟数
{
for (int j = 0; j < sz - 1; j++)
//一趟内两两比较
{
if (arr[j] > arr[j+1])
{
int t = arr[j];
arr[j] = arr[j+1];
arr[j+1] = t;
}
}
}
}
void print(int arr[], size_t sz) //打印数组用的
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 4,1,5,7,2,3,6,9,8,0 };
size_t sz = sizeof(arr) / sizeof(arr[0]);
print(arr, sz);
bubble_sort(arr, sz);
print(arr, sz);
return 0;
}
根据这个原理写出了这个函数,确实完成了冒泡排序进行升序,但是这个代码需要的比较太多了,应该需要进行优化一下,若第一趟比较完毕了,那么最大的数肯定被排序到最后的,所以肯定一趟要比一趟的比较次数减少,但是上述函数代码中,却是每一趟都要进行10次比较,所以我们需要优化一下这个代码
//冒泡排序的实现
void bubble_sort(int arr[], size_t sz)
{
int i = 0;
for (i = 0; i < sz-1; i++) //趟数
{
for (int j = 0; j < sz - i - 1; j++) //这个地方被改变了,防止了每趟都要比较10次
//一趟内两两比较
{
if (arr[j] > arr[j+1])
{
int t = arr[j];
arr[j] = arr[j+1];
arr[j+1] = t;
}
}
}
}
void print(int arr[], size_t sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 4,1,5,7,2,3,6,9,8,0 };
size_t sz = sizeof(arr) / sizeof(arr[0]);
print(arr, sz);
bubble_sort(arr, sz);
print(arr, sz);
return 0;
}
输出结果,可以成功排序。
但是这个还是不够优化,当若有一趟排完后彻底有序了,若趟数没走完还需要排序,这有多余了比较次数,所以我们可以假定一个变量赋予一个值,假设这一趟有序了,所以我们的代码可以优化如下:
//冒泡排序的实现
void bubble_sort(int arr[], size_t sz)
{
int i = 0;
for (i = 0; i < sz-1; i++) //趟数
{
int flag = 1; //假设这一趟已经有序了
for (int j = 0; j < sz - i - 1; j++)
//一趟内两两比较
{
if (arr[j] > arr[j+1])
{
flag = 0; //若发生交换,则无序
int t = arr[j];
arr[j] = arr[j+1];
arr[j+1] = t;
}
}
if (flag == 1) //若没发生交换,则说明有序了,后续跳出循环
break;
}
}
void print(int arr[], size_t sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 4,1,5,7,2,3,6,9,8,0 };
size_t sz = sizeof(arr) / sizeof(arr[0]);
print(arr, sz);
bubble_sort(arr, sz);
print(arr, sz);
return 0;
}
检查输出结果,无误,代码优化完毕。
5.二级指针
指针变量也是一个变量,既然它是变量了,那就有地址,二级指针的出现就是为了存放指针变量的地址。
例如:int ** 就是一个二级指针的类型。
我们分析下列代码
int main()
{
int a = 0;
int* pa = &a; //一级指针
int** ppa = &pa; //二级指针 ppa类型是int**
return 0;
}
更通俗的来说可以参考这个表情包
对于二级指针的运算:
1. *ppa通过对ppa中的地址进行解引用,这样找到的是pa,*ppa其实访问的就是pa
int b = 20;
*ppa = &b; //等价于pa=&b;
2. **ppa先通过*ppa找到pa,然后对pa进行解引用的操作:*pa,那找到的是a。
**ppa=30;
//等价于*pa=30;
//等价于a=30;
6.指针数组
指针数组就是存放指针的数组。
指针数组的每个元素都是用来存放地址(指针)的。
指针数组的每个元素是地址,又可以指向一块区域。(从而可以完成下面模拟二维数组)
7.指针数组模拟二维数组
//指针数组模拟二维数组
int main()
{
int arr1[] = { 0,1,2,3,4 };
int arr2[] = { 1,2,3,4,5 };
int arr3[] = { 2,3,4,5,6 };
//数组名是数组首元素的地址,类型是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]); //相当于*(*(parr+i)+j)-->*(parr[i]+j) 本质上都是指针运算
}
printf("\n");
}
return 0;
}
parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型一维数组,parr[i][j]就是整型一维数组中的元素。
注意:上述代码模拟出二维数组的效果,实际并不完全是二维数组,因为每一行地址并非是连续的。
8.数组指针变量
1.理解数组指针变量
根据前面学习的:
字符指针变量:char* pc; 指向字符的指针,存放的是字符型变量的地址。
整型指针变量:int* pi; 指向整型的指针,存放的是整型变量的地址。
浮点型指针变量:float* pf;指向浮点型数据的指针,存放的是浮点型变量的地址。
所以,数组指针也是一个指针变量,存放的是数组的地址,能够指向数组的指针变量。
2.区分数组指针变量和指针数组变量
int *p1[10]; //指针数组————存放指针的数组
int (*p2)[10]; //指针变量————指向的是数组————数组指针
数组指针变量创建就是
int (*p)[10]; //数组指针变量
p先和*结合,说明p是一个指针变量,然后指针指向的是一个大小为10个整型的数组,所以p是一个指针,指向一个数组,叫数组指针。
注意:[]的优先级高于*号的,所以必须加上()让p先和*结合。
3.数组指针变量初始化
数组指针变量是用来存放数组地址的,我们就可以用&数组名,并把这个值存放到数组指针变量中。
int arr[10]={0};
&arr; //取到数组的地址
//存放数组地址,要用数组指针变量
int (*p)[10] = &arr; //注:[10]不可省略
4.数组指针类型解析
9.字符指针变量
字符指针类型为:char*
下面代码将展示使用字符指针变量
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
char c = 'a';
char* pc = &c; //pc是指针变量,类型是char*,指向的是char类型变量c,存储c的地址
printf("c = %c , &c = %p , *pc = %c , pc = %p \n", c, &c, *pc, pc);
*pc = 'w';
printf("c = %c , &c = %p , *pc = %c , pc = %p \n", c, &c, *pc, pc);
return 0;
}
我们来看输出结果
接下来还有一种写法:
int main()
{
const char* ps = "hello world"; //const修饰,防止字符串内容被修改.
printf("%s\n", ps);//%s打印字符串,只需要告诉它地址就行(需提供起始地址)
//这里ps不能解引用
return 0;
}
这里需要注意一点就是在代码const char* ps="hello world";中,这个本质上是把字符串hello world的首字符地址放在了指针变量ps里。所以这个代码意思是把一个常量字符串的首字符h的地址给存到了指针变量ps里。
注:
- 常量字符串的内容不能被修改,字符数组中数组内容是可以改变的
char arr[10]="abcdef"; //相当于———>abcdef\0000 //可以改变的 const char *p2="abcdef"; //相当于-->abcdef\0 一般用const修饰防止后续出错 //不可以改变的
-
相同的两个常量字符串,没必要保存两份,因为常量字符串不能被修改(常量字符串会放在代码段),所以是共用一份且这样也节省了空间。
例如下列一道题
#include <stdio.h>
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
我们运行看到这个结果与我们预想的会有些差异,为什么str1与str2是不等,str3和str4相等。
因为这里str3和str4指向的是同一个常量字符串
C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块,这样就是为什么str1和str2不同了。
10.二维数组传参的本质
我们若有一个二维数组需要传参给一个函数的时候,有一种办法是这样写的:
//二维数组传参(实参是二维数组,形参也写成二维数组形式)
void test(int arr[3][4], int n, int m) //int arr[3][4]可以写成 int arr[][4]
{
int i = 0;
int j = 0;
for (i = 0; i < n; i++)
{
for (j = 0; j < m; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };
test(arr, 3, 4);
return 0;
}
输出结果如下,
我们回顾一下二维数组,二维数组可以看做是每个元素是一维数组的数组,也就是二维数组的每个元素是一个一维数组。二维数组的首元素就是第一行,是一个一维数组。
通过对数组指针的理解,我们可以有其他写法,因为数组名是数组首元素地址,二维数组的数组名表示的就是第一行的地址,第一行的地址是一维数组的地址,二维数组传参本质上也是传递了地址,传递的是第一行这个一维数组的地址。根据上述例子和图片,第一行的一维数组的类型是 int [4],所以第一行的地址的类型就是数组指针类型int(*) [4],所以形参可以写成指针形式的。如下:
//二维数组传参(指针形式)
void test(int(*p)[4], int n, int m)
{
int i = 0;
int j = 0;
for (i = 0; i < n; i++)
{
for (j = 0; j < m; j++)
{
printf("%d ", *(*(p + i) + j)); //-->*(p[i]+j)-->p[i][j]
}
printf("\n");
}
}
int main()
{
int arr[3][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };
test(arr, 3, 4);
return 0;
}
输出结果如下
所以二维数组传参,形参部分可以写成数组或者指针形式(数组指针--指向数组的指针)。
11.函数指针变量以及函数指针数组
1.创建函数指针变量
我们从这个名字和前面学习的整型指针等来看,函数指针应该是一个指向函数的,函数指针变量就是应该用来存放函数地址的,并且可以通过地址能够调用函数。
函数有没有地址?接下来这个代码来展示:
void test()
{
printf("测试函数是否有地址:\n");
}
int main()
{
test();
printf("test = %p \n", test);
printf("&test = %p \n", &test); //&函数名 可以获得函数的地址
return 0;
}
我们可以看出,函数是有地址的,函数名就是函数的地址。
那么函数指针变量就是为了存储函数的地址的,函数指针变量的写法和数组指针较为相似,函数指针写法有:
void test()
{
printf("测试:\n");
}
void(*pf1)() = &test;
void(*pf2)() = test;
// pf1和pf2都存储了test函数的地址
int ADD(int x, int y)
{
return x + y;
}
int (*pf3)(int, int) = ADD;
int (*pf4)(int x, int y) = &ADD; //x,y可以省略的,也可以写上
// pf3和pf4都存储了ADD函数的地址
对函数指针类型的图解:
2.使用函数指针变量
我们有了函数指针变量,可以怎么使用呢?
可以通过函数指针调用指针指向的函数。
下述代码展示了怎么去使用:
//通过函数指针调用指针指向的函数
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int(*pf)(int, int) = Add;
printf("%d\n", (*pf)(2, 3));
printf("%d\n", pf(3, 5));
return 0;
}
输出结果:
可以看出,确实调用了函数。
注意:
printf("%d\n", (*pf)(2, 3));
printf("%d\n", pf(3, 5)); //*可以省略不写,可以不需要解引用
3.typedef关键字
type是用来类型重命名的,可以将复杂的类型简单化。
比如,unsigned int写起来不方便,我们想把它简化写成uint就方便多了,那么就可以
typedef unsigned int uint;
//将unsigned int 重命名为uint
指针类型,也可重命名,比如将int*重命名为ptr_t,
typedef int* ptr_t;
//将int* 重命名为 ptr_t
但是对于数组指针和函数指针有点区别:
例如,有数组指针类型int(*)[5],需要重命名为 parr_t ,则可以这样写,
typedef int(*parr_t)[5]; //新的类型名必须放在*右边
//将数组指针类型int(*)[5] 重命名为 parr_t
函数指针类型的重命名也是一样的,例如将 void(*)(int) 类型重命名为 pf_t ,则可这样写,
typedef void(*pf_t)(int); //新的类型名必须在*右边
//将void(*)(int) 重命名为 pf_t
4.函数指针数组
函数指针数组,从名字来看,这是一个数组,数组是一个存放相同类型数据的存储空间,那么数组里的每个元素,应该是函数的地址,并且是指针类型。
所以,把函数的地址存到一个数组中,那么这个数组就叫函数指针数组。
定义函数指针数组,
int (*parr1[3])();
int *parr2[3]();
int(*)() parr3[3];
//哪个是函数指针数组呢?
parr1先和[]结合,说明parr1是数组,数组的内容是什么呢?是int(*)()类型的函数指针。
所以,
int (*parr1[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(*) (int,int)
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 x, y;
int ret = 0;
int input = 0;
int (*p[5])(int x, int y) = { 0,Add,Sub,Mul,Div }; //转移表
//p是函数指针数组,里面存储的全是函数指针(函数的地址)
//int (*p[5])(int x, int y)中 5可以省略,主要取决于初始化数组。
do
{
menu();
printf("请选择:> ");
scanf("%d", &input);
if (input >= 1 && input <= 4)
{
printf("输入操作数:> ");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出计算器\n");
}
else
{
printf("输入有误\n");
}
} while (input);
return 0;
}
通过这些输出数据,可以看出我们能通过函数指针数组的用途——转移表,模拟计算器的一般实现。
制作不易,求三连qwq。