前言:对于指针我们已经有了初步的了解,并已能够简单使用。今天我们来深入理解指针。让我们的指针功力更上一层楼。
1 使用指针访问数组
有了指针的知识,再结合数组的特点,我们就可以使用指针来访问数组了。
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
int i = 0;
for (i = 0; i < sz; i++)
{
//这几种方式都是可以的
//scanf("%d", &arr[i]);
//scanf("%d",(arr+i));
scanf("%d",(p+i));
}
for (i = 0; i < sz; i++)
{
//任选一种方式
//printf("%d ", arr[i]);
printf("%d ",p[i]);
//printf("%d ", *(arr + i));
//printf("%d ", *(p + i));
}
return 0;
}
数组名是数组首元素的地址
。将数组名arr赋值给指针变量p,那么数组名arr和p在这里是等价的。我们可以使用arr[i]来访问数组元素,那么也可以使用p[i]来访问数组元素。
数组元素的访问在编译器处理的时候,也是转换成数组首元素的地址+偏移量求出元素的地址,然后解引用访问的。
2 一维数组传参的本质
数组是可以传递给函数的,这一次我们来讨论一下数组传参的本质。之前我们都是在函数外部计算数组元素个数,那我们把数组传递给函数后,能否在函数内部求得数组元素的个数呢?
#include<stdio.h>
//参数部分写成数组形式,本质上还是指针。也可以写成指针形式
int test(int arr[])
{
int sz = sizeof(arr) / sizeof(arr[0]);
return sz;
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz1 = sizeof(arr) / sizeof(arr[0]);
int sz2 = test(arr);
printf("sz1=%d\n", sz1);
printf("sz2=%d\n",sz2);
return 0;
}
可以看到,在函数内部并没有求得正确的结果。这是为什么呢?这就不得不搞清楚一维数组传参的本质了。
前面我们提到过数组名代表的是数组首元素的地址
,那么在test函数里的形参理论上应该使用指针变量来接收地址。因此在函数内部里的sizeof(arr)求的是一个地址的大小(单位是字节),而不是数组的大小(单位是字节)。正是因为函数的形参部分本质上是指针,所以函数内部无法求出数组元素个数的。
3 冒泡排序
核心思想就是:两两相邻的元素进行比较
。
#include<stdio.h>
void bubble_sort(int arr[], int sz)
{
int i = 0;
// 趟数
for (i = 0; i < sz - 1; i++)
{
int flag = 1;//假设这一趟是有序的
int j = 0;
// 一趟需要比较的次数
for (j = 0; j < sz - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag = 0;//发生交换,说明无序
}
}
//第一趟有序的话,后面就无需排序了
if (flag)
{
break;
}
}
}
int main()
{
int arr[10] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
//标准输入设备输入数据
for (i = 0; i < sz; i++)
{
scanf("%d", &arr[i]);
}
//按升序进行排序
bubble_sort(arr, sz);
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
4 二级指针
指针变量也是变量,是变量就有地址,那么指针变量的地址存放在哪里?答案是:二级指针
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;
int** ppa = &pa;//二级指针
return 0;
}
指针pa里存放的是a的地址,指针ppa里存放的是pa的地址。
5 指针数组
什么是指针数组呢?我们学习过整形数组,是存放整型的数组;字符数组,是存放字符的数组。那么,指针数组当然也是数组了。只是指针数组里存放的是指针(地址)
。
为了能够更加清楚的感受到什么是指针数组。我们来上实战。用指针数组来模拟实现二维数组。
#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*的,就可以存放到指针数组中
int* parr[] = { arr1,arr2,arr3 };
//指针数组中的元素个数
int sz = sizeof(parr) / sizeof(parr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
printf("%d ", *((*(parr + i)) + j));
// printf("%d ", *(parr[i] + j));
// printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
parr[i]访问的是parr数组中的元素,parr[i]找到的元素指向了整型的一维数组,parr[i][j]访问的是一维数组里的元素
。
6 字符指针变量
在指针的类型中有一种指针类型是字符指针char*
。
一般使用
#include<stdio.h>
int main()
{
char c = 'w';
//字符指针
char* pc = &c;
printf("%c\n", *pc);
return 0;
}
还有一种使用方式
#include<stdio.h>
int main()
{
//这里并不是把字符串放在字符指针里了,而是把字符串首字符的地址放在了pc里
char* pc = "hello world";//常量字符串
printf("%s\n", pc);
return 0;
}
接下来让我们上实战,检验一下小伙伴的功力如何。
#include<stdio.h>
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");
}
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;
}
不妨猜猜结果如何?
结果是否和小伙伴们一致呢?现在就让我们一起来深入解析这道题吧。
在main函数中创建了两个字符数组str1和str2,系统会为这两个数组分别申请一块内存空间
,前面我们学习到数组名就是数组首元素的地址
。也就是说str1与str2数组首元素的地址是不一样的。所以str1与str2不同。str1与str2两个数组的内容是一样的。
str3与str4都是字符指针,存储的是字符串首字符的地址
。这里的str3与str4指向的是同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,它们会指向同一块内存
。所以str3与str4相同。
7 数组指针变量
之前我们学习了指针数组,指针数组是数组,存储的是指针(地址)。那么数组指针又是什么呢?答案是:指针变量。存储的是数组的地址。
int* parr[10]; //指针数组
//parr先和[]结合,说明parr是一个数组,10代表元素个数,int*是数组元素的类型
int (*p)[10]; //数组指针
//p先和*结合,说明p是一个指针变量,[10]说明p指向了一个大小为10的数组
//int说明数组元素的类型
这里要注意:[]的优先级是要高于*的,所以必须加上()来保证p先与*结合
。
数组指针变量初始化
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5 };
int(*p)[10] = &arr;
}
8 二维数组传参的本质
有了数组指针的理解,我们就可以进一步探讨二维数组传参的本质了。
二维数组可以看做是一维数组的数组。也就是说二维数组的每个元素是一个一维数组。那么二维数组的首元素就是第一行,是个一维数组。根据数组名就是数组首元素地址的这个规则,也就是说二维数组的数组名表示的是第一行的地址,是一个一维数组的地址。所以二维数组传参的本质也是传递了地址,传递的是第一行这个一维数组的地址
。
#include<stdio.h>
// 数组指针就是用来接收数组的地址的
void test(int(*parr)[5], int r, int c)
{
int i = 0;
for (i = 0; i < r; i++)
{
int j = 0;
for (j = 0; j < c; j++)
{
//printf("%d ", *(*(parr + i) + j));
printf("%d ", parr[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 };
// 数组名代表的是第一行的地址,也就是一个一维数组的地址
test(arr, 3, 5);
return 0;
}
9 函数指针变量
根据前面学习的整型指针,数组指针,进行类比,就不难得出函数指针是什么了。函数指针本质上是一个指针,指向的是一个函数,函数指针变量是专门用来存放函数的地址的
。
那我们要如何获取函数的地址呢?类比数组,就可以得到结论。&数组名取出的是数组的地址
,那么&函数名自然也是函数的地址
了。那可能就会有很多小伙伴有疑问了。数组名代表的是数组首元素的地址,函数名呢?其实函数名和&函数名是一样的,都代表的是函数的地址
。
#include<stdio.h>
void Add()
{
return;
}
int main()
{
printf(" Add :%p\n", Add);
printf("&Add :%p\n", &Add);
return 0;
}
可以看到,函数也是有地址的。那函数的地址又该存放在哪里呢?当然是函数指针变量里了。
上实战,计算一个字符串的长度。
#include<stdio.h>
int my_strlen(char* ch)
{
if (*ch == '\0')
{
return 0;
}
else
{
return (1 + my_strlen(ch+1));
}
}
int main()
{
char ch[20] = "abcdefg";
//函数指针,pch里存放的是函数my_strlen的地址
int (*pch)(char* ch) =&my_strlen;
int ret = pch(ch);
printf("%d\n", ret);
return 0;
}
int (*pch) (char* ch)
| | |
| | |
| | |
| 函数指针变量名 pch指向函数参数的类型及个数
pch指向函数的
返回类型
int (*)(char* ch)是pch函数指针变量的类型
10 typedef关键字
typedef关键字可以将复杂的类型简单化,是用来类型重命名的。
比如:
typedef unsigned int uint;
#include<stdio.h>
int main()
{
unsigned int a = 10;
uint b = 6;
return 0;
}
通过调试可以看到a和b的类型是一样的,都是unsigned int类型。这就是typedef的作用,它可以将复杂的类型简单化。
除此之外,它还有其他方面的好处。请看接下来的代码。
#include<stdio.h>
int main()
{
int a = 2;
int* p1, p2;
p1 = &a;
p2 = 10;
return 0;
}
在main函数里创建了p1和p2两个变量,p1是int*类型,p1是一个指针变量,可以
用来存放变量a的地址。p2是int类型,是一个整型变量。
用typedef类型重命名,再来看看会有什么不同呢?
typedef int* pint;
#include<stdio.h>
int main()
{
int n = 20;
int m = 10;
pint pn, pm;
pn = &n;
pm = &m;
return 0;
}
int*类型用typedef重命名后,创建出的pn,pm都是int*类型的。这就是
typedef重命名后无形中带来的好处,不至于出现变量与变量之间类型的
混淆。
当然了,这些只是typedef的简单应用。还有更加惊奇的事情在后面呢。
#include<stdio.h>
int main()
{
( * (void (*)()) 0) ();
//这段代码什么意思呢?
//void(*)()这是一个函数指针,那么将这样的一个函数指针放在()里是什么意思呢?
//我们学习过类型的强制转换 ------- (类型)
//那么应该很清楚了,这里的函数指针是一个类型,是一个函数指针类型,将整型数据0作为一个函数的地址
//0地址处的这个函数是一个无参,返回类型是void类型的一个函数
//*是去调用0地址处的这个函数,不需要进行传参
return 0;
}
那么,能否使用typedef对函数指针类型进行重命名呢?当然是可以的了。
typedef void(*pfun_t)();
//新的类型名必须在*的右边
那么,上面的一段代码就可以这样去写。
typedef void(*pfun_t)();
#include<stdio.h>
int main()
{
( * (void (*)()) 0) ();
(*(pfun_t)0)();
return 0;
}
再来看一段有难度的代码,巩固一下我们理解。
#include<stdio.h>
int main()
{
void (* signal (int, void(*)(int)) ) (int);
//signal(int,(void(*)(int))里面的int,void(*)(int)都是类型,作为signal函数参数的类型
//signal是一个函数,返回类型是void(*)(int)的一个函数指针类型
return 0;
}
同样的,我们用typedef重命名简化上面的代码。
typedef void(*pfun_t)(int);
#include<stdio.h>
int main()
{
void (* signal (int, void(*)(int)) ) (int);
pfun_t siganl(int, pfun_t);
return 0;
}
11 函数指针数组
数组是一组相同元素类型的集合
。像指针数组是每个元素类型是指针类型的数组
。函数指针数组本质上是每个元素类型是函数指针类型的数组,
。
那么函数指针数组该如何定义呢?
#include<stdio.h>
int main()
{
int(*parr[5])(int, int);//函数指针数组
//parr先和[]结合,说明parr是一个数组
//5说明数组的大小
//数组的类型是int(*)(int,int)
return 0;
}
函数指针数组的用途:转移表。
上实战,计算器的实现
#include<stdio.h>
#include<stdlib.h>
void menu()
{
printf("****************************\n");
printf("***** 1.add 2.sub *****\n");
printf("***** 3.mul 4.div *****\n");
printf("***** 5.mol 0.exit *****\n");
printf("****************************\n");
}
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);
}
int Mol(int x, int y)
{
return (x % y);
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
//函数指针数组
int(*pfun[6])(int, int) = { 0,Add,Sub,Mul,Div,Mol };//转移表
do
{
menu();
printf("请输入选择:>>");
scanf("%d", &input);
if (input >= 1 && input <= 5)
{
printf("请输入两个操作数:>>");
scanf("%d %d", &x, &y);
int ret = pfun[input](x, y);
printf("%d\n", ret);
}
else if (input == 0)
{
exit(0);
}
else
{
printf("输入错误,请重新输入\n");
}
} while (input);
return 0;
}
结语:今天的指针内容到此为止。相信会有许多小伙伴们收获满满。看到这里就为小编点点赞吧。