1.前言
本节我们书接上文,继续进行指针专题的学习,夯实指针的基础,那么废话不多说,我们正式进入今天的学习
2.字符指针变量
我们知道,字符指针的形式为char*,我们可以取出一个字符的地址,并且将它存入到字符指针变量里面去。我们可以通过字符指针变量来打印或者修改字符
int main(void)
{
char ch = 'w';
char* pc = &ch;
printf("%c\n", *pc);
*pc = 'q';
printf("%c\n", *pc);
return 0;
}
这种情况是一般的使用情况,起始字符指针变量还有另外一种使用方式:
当我们以这种形式写代码的时候,并不是表示把字符串--hello world放入指针变量p中。在这种情况下我们可以把hello world理解为一个字符数组,这种写法相当于把这个字符数组的首字符的地址传给了p
char* p = "hello world!";
//等价于:
//char arr[] = "hello world";
//char* p = arr;
我们可以来验证一下,验证代码如下:
int main(void)
{
char* p = "hello world!";
printf("%c\n", *p);
return 0;
}
但是这种特殊的写法只能等价于写成字符数组的形式,并不是真的就是字符数组的形式,它们之间存在着差异:
我们知道字符数里面的元素组是可以被改变的,而我们这种写法下,hello world是一个常量字符串,常量字符串是不能被修改的
因为常量字符串是不能被修改的,我们可以在字符指针变量前面加上一个const来修饰,保证代码的严谨性
虽然常量字符串不能被修改,但是我们还是可以正常的访问常量字符串里面的内容的:
我们有三种方式来打印常量字符串:
int main(void)
{
const char* p = "hello world!";
//第一种
printf("%s\n", p);
//第二种
printf("%s\n", "hello world!");
return 0;
}
第二种方法中其实使用的不是字符串本身,也和第一种方法一样,使用的是首元素h的地址
还有一个较为复杂的方法,我们可以通过遍历来打印常量字符串:
int main(void)
{
const char* p = "hello world!";
//第一种
printf("%s\n", p);
//第二种
printf("%s\n", "hello world!");
//第三种
int len = strlen(p);
for (int i = 0; i < len; i++)
{
printf("%c", *(p + i));
}
return 0;
}
笔试题
(此题目收录于《剑指offer》)
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;
}
这个题目为什么会产生这样的结果呢?我们来分析一下:
我们先分析一下前两句代码,这两句代码的意思是:创建两个字符数组str1和str2,里面都存入hello world. ,因为这是两个不同的数组,它们在内存之中存在于不同的空间,它们之间仅仅是字符数组里面的内容相同,因为数组名是首元素的地址,str1和str2的首元素并不在同一块内存空间中,所以它们两个不相同
我们再分析一下后面的两句代码,后面的hello world. 是一个常量字符串,因为常量字符串是不能被修改的,所以内容相同的常量字符串只会保存一份,不会额外的开辟空间存储两个一样的常量字符串,所以后面的两个字符指针变量都是指向同一个常量字符串hello world. ,两个变量的首地址相同
3.数组指针变量
我们前面学习了指针数组,它的里面既含有指针又含有数组,但是它的本质是一个存放指针的数组
而现在我们要学习的是数组指针变量,它的里面也同时含有数组和指针,而它的本质又是什么呢?我们再次运用类比的方法探究一下:
我们之前学过:
1.字符指针 char*p 它是一个指向字符的指针,存放的是字符的地址
char ch = 'w';
char* pc = &ch;
2.整型指针 int*p 它是一个指向整型的指针,存放的是整型的地址
int n = 0;
int* p = &n;
所以数组指针通过类比可以知道:数组指针是指向数组的指针,存放的是数组的地址
所以说数组指针是一种存放数组地址的指针变量
那么我们该怎么定义数组指针变量呢?
因为数组指针本质上是一个指针,我们参考之前的整型指针和字符指针,所以我们需要用*parr来表示它指针的属性,因为数组里面有多个元素,我们要在其后面加上[n]表示它元素的个数,但是由于[ ]的优先级大于*,所以我们要加上( )来保证parr先与*结合,不加( )就是一个指针数组
int main(void)
{
char ch = 'w';
char* pc = &ch;
int n = 0;
int* p = &n;
int arr[10] = { 0 };
int(*parr)[10] = &arr;
return 0;
}
该定义模式就表示:p是数组指针,p指向的是一个数组,数组里面有10个元素,每个元素的类型都是int
数组指针变量是用来存放数组地址的,那么我们该怎么获得数组的地址呢?我们需要使用之前学习的 &+数组名
数组指针变量的使用:
int main(void)
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int(*p)[10] = &arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
printf("%d ", (*p)[i]);
}
return 0;
}
这种方式下解引用拿到的是整个数组的地址
4.二维数组传参的本质
刚才我们学习了数组指针变量,但是在一维数组中数组指针变量的使用通常都比较牵强,使用会多此一举,我们通常会在二维数组中使用数组指针变量
我们有了对数组指针变量的理解,我们就能更好地探究二维数组传参的本质了
我们先举一个例子,假设我们要对二维数组里面的元素进行打印,我们之前通常会这么写:
void test(int arr[3][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 ", arr[i][j]);
}
printf("\n");
}
}
int main(void)
{
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;
}
我们通过之前的学习知道了一维数组的数组名是数组首元素的地址,那么我们来想一下二维数组的数组名是什么呢?
二维数组的数组名是二维数组中的第一行的地址,二维数组的每一行都是一个元素;
二维数组的每一行都是一个一维数组;
所以此时我们若是要对二维数组里面的元素进行打印,就可以使用数组指针变量:
void test(int(*p)[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 ", (*(p + i))[j]);
}
printf("\n");
}
}
int main(void)
{
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;
}
5.函数指针变量
要想理解函数指针,我们同样的使用类比的思路来帮助我们理解
之前我们学习过:
1.字符指针--指向的是字符类型,用于存放字符的地址
2.整型指针--指向的是整型类型,用于存放整型的地址
3.数组指针--指向的是数组, 用于存放数组的地址
所以我们可以推导出:
函数指针指向的是函数,用于存放函数的地址
因为函数指针存放的是函数的地址,说明函数也是有地址的,我们可以先来观察一下函数的地址:
Add(int a, int b)
{
return a + b;
}
int main(void)
{
printf("%p\n", &Add);
printf("%p\n", Add);
return 0;
}
通过上述的代码我们可以知道,在函数指针里面& + 函数名和函数名都表示的是函数的地址,函数指针里面没有首元素的概念
函数指针变量的写法和数组指针变量的写法很相似,函数指针的写法为Type(*pf)(Type, Type....)
我们来举例说明:
Add(int a, int b)
{
return a + b;
}
int main(void)
{
int arr[8] = { 0 };
int(*pf)(int, int) = &Add;
return 0;
}
我们需要和数组指针一样考虑优先级问题
了解了这些以后,我们可能会产生疑问:函数指针具体要怎么使用呢?
函数指针的使用非常简单,直接用一串代码就可以说明:
Add(int a, int b)
{
return a + b;
}
int main(void)
{
int arr[8] = { 0 };
int(*pf)(int, int) = &Add;
int r = (*pf)(3, 7);
printf("%d\n", r);
return 0;
}
int r = (pf)(3, 7);
在运用函数指针的时候可以不加解引用符号,也可以加多个解引用符号,解引用符号的多少都不会对其产生影响
此时我们可能又会产生疑问:我们觉得函数指针好像并没有什么用处,有点多此一举,不如直接用函数名来调用
函数指针在使用上确实不存在太多的意义,函数指针的存在仅仅是让程序有着更加多元的写法
深入理解函数指针
我们来看两串代码(两串代码均出自《C陷阱和缺陷》):
1:
(*(void (*)())0)();
我们来逐步分析这串代码:
首先,我们不难看出 void (*)( ) 是一个函数指针类型;
我们接着往后看,(函数指针类型)0可以理解为:将0强制类型转换为 void (*)( ) 形式的函数指针类型的地址,此时0自身表示的也是地址,此时代码的意思是在地址为0的地方放着一个返回类型为void,没有参数的一个函数。因为是函数指针,所以前面的*号不会产生任何影响
我们再接着往后看(0强制转换后的函数指针)( ),这串代码表示的是一次函数调用,而且没有往0地址处的函数传入参数
我们通过把代码拆开,实现了对代码理解,所以我们在遇到复杂的函数的时候可以尝试把它拆开来理解
2.
void (*signal(int , void(*)(int)))(int);
因为该代码里面的括号比较多,我们可以试着将括号配对,并且拆开来理解:
void ( *signal ( int , void(*) (int) ) ) (int) ;
1.我们可以知道signal是一个名称,可能是一个函数的名称,也可能是变量的名称。我们通过对题目的分析可以知道:signal是一个函数的名字,它的第一个参数类型是int,它的第二个参数的类型是函数指针类型 void(*)(int)
2.我们令signal ( int , void(*) (int)部分为M,此时我们定义的M是一个函数,现在函数的名称和参数都有了,只差一个返回类型,我们把它带回到原代码中可以得到:void(*M)(int) 这里我们不难看出这是一个返回类型
所以说该代码是一个函数声明,声明的函数叫做signal,signal函数有两个参数;其中第一个参数的类型为int,第二个参数的类型是void(*)(int)的函数指针类型,该指针可以指向一个函数,指向的函数参数是int,返回类型是void;signal函数的返回类型是void(*)(int)的函数指针,该指针可以指向一个函数,指向的函数参数是int,返回类型是void
我们可以把这串代码理解为 void (*)(int) signal(int,void(*)(int)) 但是我们不能这么去书写,只能这样去理解
虽然不能用上述方法去书写,但是我们可以用typedef来进行简化,可以帮助我们理解
6.typedef关键字
typedef 是用来类型重命名的,可以将复杂的类型,简单化
例如:如果觉得unsigned int 写起来很麻烦,不够方便,写成 uint 就方便多了,那么我们可以使用typedef来将类型重命名
typedef unsigned int uint;
我们也可以使用#define PTR_T int* ,这种写法不需要加分号,两种方法都能在一定程度上实现类型重命名的功能,#define方法是替换,typedef是直接改变了类型的名字,二者在本质上存在区别,下面来举一个例子:
typedef int* ptr_t;
#define PTR_T int*
int main(void)
{
ptr_t p1, p2;//p1和p2都是整型指针
PTR_T p3, p4;//被替换成int* p3, p4,此时p3是整型指针,p4旁边没有*,此时p4是整型
return 0;
}
代码ptr_t p1, p2中,p1和p2都是整型指针
而代码PTR_T p3, p4中,因为#define起的是的作用,原代码被替换成int* p3, p4,此时p3旁边加上了*,表示的是整型指针,因为p4旁边没有*,所以p4仅仅是整型
此时我们可能会想:typedef能不能对指针重命名呢?答案是肯定的
如果我们想要把int*重新命名为ptr_t,我们就可以这样书写:
typedef int* ptr_t;
但是使用typedef重新命名数组指针和函数指针类型会与其他的稍微有点区别:
int main(void)
{
int arr[5] = { 0 };
int(*p)[5] = &arr;//p是数组指针变量
return 0;
}
像上面的代码,我们知道上面数组指针变量的类型为 int(*p)[5],这是一种数组指针类型,如果我们要把这个类型重新命名就需要使用不同的方式:
typedef int(*parr_t)[5];
int main(void)
{
int arr[5] = { 0 };
int(*p)[5] = &arr;//p是数组指针变量
return 0;
}
我们此时就不能像之前一样把名称放到最后面,而是需要放到中间,这是一种语法规则
函数指针类型的重命名也是⼀样的,例如:我们要将 void(*)(int) 类型重命名为 pf_t ,就需要这样写:
typedef void(*pfun_t)(int);
新的类型名必须在*的右边
知道了typedef的用法此时我们就可以对前面的第二道题进行优化:
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);
这么看,新的代码就比之前的代码更加容易理解一些
7.函数指针数组
函数指针数组应该如何定义呢?
我们还是来使用类比的方法:
我们之前学习过:
1.整型数组--整型数组是存放整型的数组
2.字符数组--字符数组是存放字符的数组
3.指针数组--指针数组是存放指针的数组
4.字符指针数组: char* arr1[10]
5.整型指针数组: int* arr2[10]
所以我们此时就可以知道函数指针数组就是把函数的地址存到⼀个数组中,
假设我们要完成加减乘除的操作,同时把加减乘除定义为四个函数,此时我们就可以这么写:
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 main(void)
{
int (*pf1)(int, int) = Add;
int (*pf2)(int, int) = Sub;
int (*pf3)(int, int) = Mul;
int (*pf4)(int, int) = Div;
return 0;
}
我们发现者四个函数函数指针变量在去掉名字以后,类型是一模一样的;
我们结合之前所学习过的数组,我们可以发现,数组里面的元素类型也是一模一样的,所以我们可以创建一个数组,并且在数组里面存入函数指针;
如果要把多个相同类型的函数指针存放在一个数组中,那么这个数组就是函数指针数组;
函数指针数组的定义方式为:Type(*pfArr[4])(int, int) ={函数, 函数,函数.... }
所以我们此时就可以修改原代码:
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 main(void)
{
int (*pf1)(int, int) = Add;
int (*pf2)(int, int) = Sub;
int (*pf3)(int, int) = Mul;
int (*pf4)(int, int) = Div;
int (*pfArr[4])(int, int) = { Add,Sub,Mul,Div };
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;
}
int main(void)
{
int (*pf1)(int, int) = Add;
int (*pf2)(int, int) = Sub;
int (*pf3)(int, int) = Mul;
int (*pf4)(int, int) = Div;
int (*pfArr[4])(int, int) = { Add,Sub,Mul,Div };
int i = 0;
for (i = 0; i < 4; i++)
{
int ret = pfArr[i](8, 4);
printf("%d\n", ret);
}
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");
}
int main(void)
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
do
{
menu();
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
ret = Add(x, y);
printf("结果是:%d\n", ret);
break;
case 2:
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("结果是:%d\n", ret);
break;
case 3:
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
ret = Mul(x, y);
printf("结果是:%d\n", ret);
break;
case 4:
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
ret = Div(x, y);
printf("结果是:%d\n", ret);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
当我们这样写代码的时候,我们发现代码有很多重复的地方,显得有点冗余。而且如果我们需要扩展计算器的功能,代码量也会大量增加;
此时我们就可以使用函数指针数组来优化和简化代码:
我们先来创建一个函数指针数组:
int (*pfArr[5])(int, int) = { NULL,Add,Sub,Mul,Div };
我们可以在四个函数的前面加上一个NULL,这样就可以让下标对应,使用起来就会很方便
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");
}
int main(void)
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
//创建函数指针数组
int (*pfArr[5])(int, int) = { NULL,Add,Sub,Mul,Div };
do
{
menu();
printf("请选择:");
scanf("%d", &input);
if (input >= 1 && input <= 4)
{
printf("请输入2个操作数:");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y);
printf("结果是:%d\n", ret);
}
else if (input == 0)
{
printf("退出计算器\n");
break;
}
else
{
printf("选择错误,重新选择\n");
}
} while (input);
return 0;
}
通过这样的优化,代码就会简洁很多;
这样的函数指针数组称为转移表
结尾
本节我们深入学习了指针的相关知识,通过理解和消化我们指针的基础变得更强了,我们也可以更好的运用指针的相关知识了,那么本节的内容也到此结束了,我们下一节还是学习指针的相关内容,谢谢您的浏览