哈喽哈喽,同志们,咱们的深入理解指针已经来到了(3),其实指针这一块学起来还是要废些功夫的。但是,希望我们都能坚持下去,不管是指针,还是C语言,还是计算机这条路,只要坚持下去,相信我们都势如破竹!!!
文章目录
1.字符指针变量
- 一般情况下,我们要创建一个字符指针会这样使用:
#include<stdio.h>
int main()
{
char ch = 'b';
char* pc = &ch;
printf("%c\n", *pc);
*pc = 'u';//改变ch的值
printf("%c\n", ch);
return 0;
}
代码输出的结果:
我们还有一种使用方式(拿一个字符指针指向字符串):
int main()
{
char* pc = "hello world";
return 0;
}
- 那么请问,这个是表示把hello world放到p里面去了吗?
- 不是的哈,其实在这里hello world这个字符串就相当于是一个字符数组,我们是把这个字符数组的首元素的地址放进了p里。
//char arr[] = “hello world”;
//char* p = arr;
和这两行代码是非常非常之相似的。
- 我们接下来可以来验证一下:
int main()
{
char* pc = "hello world";//这个是表示把hello world放到p里面去了吗?
printf("%c\n", *pc);
return 0;
}
代码运行结果:
这个运行结果表示,果然和我们所说的一样!
- 我们可以将字符串想象成这样一个字符数组来理解,但是其实它们是有差异的。
- 我们知道字符串是可以改变的,但是字符串是一个常量字符串,而常量字符串是不能修改的.
- 大家可以看到,当我们要对字符串的第一个字符进行修改的时候,我们调试一下就发现编译器报错了(写入访问权限冲突).
- 那么既然这个字符指针变量是不能被修改的,我们何不把它用const修饰一下,对其进行限制呢?
int main() { const char* pc = "hello world";//这个> > 是表示把hello world放到p里面去了吗? printf("%c\n", *pc); *pc = 'b'; return 0; }
- 大家还记得我们在深入理解指针(1)里讲的const修饰指针变量吧!我们来回忆一下,这里我们将const放在( * )前修饰的是 ( *pc ) , 所以当我们想通过p来改变其所指向的内容就不能实现了。
- 这样加了const修饰了之后,我们一旦对其修改,编译器就会报错。
- 我们如果想要使用指针打印这个字符串有两种方式:
#include<stdio.h>
#include<string.h>
int main()
{
const char* pc = "hello world";
//第一种
printf("%s\n", pc);
//第二种(使用循环遍历整个字符串)
int n = strlen(pc);
int i = 0;
for (i = 0; i < n; i++)
{
printf("%c" , * (pc + i));
}
return 0;
}
- 接下来哈,我们来看一道题,这道题是来自《剑指offer》这本书,大家先自己思考一下答案是什么:
#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;
}
我们运行一下,看看代码结果是什么:
看到结果有些同学就开始疑惑了,为甚捏?
别急别急,我们来分析一下,首先,定义了两个数组 str1 和 str2 ,这是我们的两个在数组中的存储:
- 不管这两个数组的内容是不是完全相同,只要我们定义了,那么内存就会开辟一个空间用来存放这个数组。
- 我们看到第一个 if 语句中的判断条件是( str1 == str2 ),我们又知道 str1 和 str2 都是数组名,而数组名就是数组首元素的地址,既然两个数组都在内存中开辟了空间,那么它们的地址当让在内存中是不一样的。
- 第二个 if 语句的判断条件是( str3== str4 ),我们可以看到 str3 和 str4 是指针,指向的是常量字符串,在前面我们有讲到,常量字符串是不能修改的,那么,内容相同的常量字符串有必要在内存中保存两份吗?
- 答案是不会,内存中只会保存一份。所以它们指向的内存中的同一块空间,所以它们两个是相等的。
2.数组指针变量
2.1数组指针变量是什么
- 还记得我们前面讲到过指针数组,通过讲解,我们了解到了,指针数组其实就是存放指针的数组。那么,数组指针是数组呢???还是指针呢???
- 接下来我们还是通过类比来看一下,它到底是数组还是指针?
字符指针----char* p----指向字符的指针,存放的是字符的地址
整型指针----int* p-----指向整型的指针,存放的是整型的地址
数组指针----------------指向数组的指针,存放的是数组的地址???//yes yes yes- 数组指针其实就是一种指针变量,是存放数组地址的指针变量.
2.2数组指针变量怎么初始化
-
不知道大家还记不记得我们当时讲对数组名的理解:
//arr 是整个数组首元素的地址 //&arr[0] 也是数组首元素的地址 //&arr 取出的是数组的地址
-
那我们该怎样去定义一个数组指针呢?
int main()
{
int arr[10] = { 0 };
int(*p)[10] = &arr;
/*
这样定义我们该怎么去理解呢?
1.(*p)代表的是它是一个指针变量
2. [10]代表的是它指向的是一个数组,并且这个数组有10个元素
3. int 代表它指向的数组是int类型的
*/
return 0;
}
- 我们想一下这个指针变量的类型是什么呢?
- 整型指针变量的类型是( int* )
字符指针变量的类型是( char* )
那么我们上面定义的数组指针变量的类型就是( int * [10] )
- 还记得我们前面有讲到一个+1运算吗?
arr
arr+1
二者相差4个字节
&arr[0]
&arr[0]+1
二者相差4个字节
&arr
&arr+1
二者相差40个字节
- 今天我们学了数组指针,就可以通用数组指针类型来解释它为甚是40.
- 原因就是类型决定的,它是int(4个字节)类型的,有10个元素,所以是40个字节.
3.二维数组传参的本质
- 通常情况下,数组指针是在二维数组中使用的.
- 如果我们要打印出一个二维数组,要如何实现呢?
//通常的写法
void print(int arr[3][5], int r, int l)
{
int i = 0;
int j = 0;
for (i = 0; i < r; i++)
{
for (j = 0; j < l; j++)
{
printf("%d ", arr[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} };
print(arr, 3, 5);//打印二维数组
return 0;
}
- 我们已经学过,一维数组名是数组首元素的地址,那么二维数组名是谁的地址呢?
- 我们之前学二维数组的时候,我们说二维数组是一维数组的数组,我们可以把二维数组看成是一个一维数组,而二维数组的每一行元素看成是一个元素,也就是说,我们把一维数组作为数组的元素,这个数组就是二维数组.
- 那么,我们是不是可以说,二维数组的数组名就是二维数组第一行的地址.
- 那接下来我们就可以利用数组指针来实现二维数组的输出.
//数组指针的写法
void print(int (*p)[5], int r, int c)
{
int i = 0;
int j = 0;
for (i = 0; i < r; i++)
{
for (j = 0; j < c; j++)
{
printf("%d ", (*(p + 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} };
print(arr, 3, 5);//打印二维数组
return 0;
}
4.函数指针变量
4.1 函数指针变量的创建
- 要搞明白函数指针变量是什么之前,我们首先要了解什么是函数指针,我们依然采用类比的方式来理解:
1.字符指针 ——指向字符——存放字符的地址
2.整型指针——指向整型——存放整型的地址
3.数组指针——指向数组——存放数组的地址
4.函数指针——指向函数——存放函数的地址???//yes yes yes
- 很好,肯定又有人疑惑了函数这玩意儿的地址是什么,不会是( &函数名 )吧???那么离谱???
- 是的是的,yes yes就是它。
- 来吧,写一个小代码来感受一下吧!
//计算和
int Add(int y, int x)
{
return x + y;
}
int main()
{
int a = 10;
int b = 20;
Add(a, b);
printf("%p\n",&Add);//打印一下,看一下是不是
return 0;
}
- 请看打印结果:
- 那是不是和数组名一样,函数名是不是也是函数首元素的地址呢?
//计算和
int Add(int y, int x)
{
return x + y;
}
int main()
{
int a = 10;
int b = 20;
Add(a, b);
printf("%p\n", &Add);
printf("%p\n", Add);
return 0;
}
看一下结果:
- 呦呵!!!果真是哈!!!两个地址一模一样诶!!!你是不是在想,我果真是拿捏住了 ! ! ! 哈哈哈哈,其实,这是给你挖的坑。
- 数组中数组名是数组首元素的地址,但是函数中( 函数名 )和( &函数名 )都是函数的地址,在函数中不存在什么函数首元素的哈!
- 那我们该怎样去定义一个函数指针变量呢?
int (*pf)(int, int) = Add;
// pf 就是一个函数指针变量。
其实,我们大家仔细观察可以发现,函数指针变量的书写和数组指针变量的书写是十分相似的。
int arr[5] = { 0 };
int(*pa)[8] = &arr;
int (*pf)(int, int) = &Add;
- ( *pf )表示 pf 这个变量是指针变量
- (int, int)中的圆括号表示pf指向的是一个函数,圆括号里面的int,int表示的函数的两个参数类型。
- int 表示这个函数的返回值是 int 类型的。
4.2 函数指针变量的使用
- 我们将函数的地址存起来是为了通过这个地址找到它,然后来调用这个函数。接下来我们就举个简单的例子来使用一下。
int Add(int y, int x)
{
return x + y;
}
int main()
{
int (*pf1)(int, int) = Add;
int (*pf2)(int, int) = &Add;
int r1 = (*pf1)(3, 5);
int r2 = (*pf2)(3, 5);
int r3 = pf1(3, 5);
int r4 = pf2(3, 5);
printf("%d\n", r1);
printf("%d\n", r2);
printf("%d\n", r3);
printf("%d\n", r4);
return 0;
}
- 代码运行结果如下:
- 大家会发现在使用函数指针来调用函数的时候,( *pf )中的星号有一种脱裤子放屁的感觉,在这里不论你加不加星号都是函数的地址,所以这里 的星号是可以省略的。
- 如果大家有兴趣的话就可以试一下加多个星号,最后你会发现,结果都是一样的,这里的星号就像是摆设一样的。
4.3 两段有趣的代码
这两段代码都是出自《C陷阱和缺陷》这本书。
- 代码1:
(*(void (*)())0)();//这是一次函数调用
- 先看 void (*)() 是什么???
//这是一个函数指针类型- 那么再来看 ( void (*)() )0 这个是什么???
//这是一个强制类型转换,我们写成这样( 类型 )0,就比较好看一些。 这个的意思就是,0是一个整型常量,将 0 强制类型转换成一个地址( 函数指针 )- ( * (void (*)())0)() 这个前面的星号就代表我们将0强制类型转换成一个地址之后,我们再将其解引用,后面的原括号就代表,我们解引用完了之后,我们再调用这个函数。
- 代码2:
void (*signal(int , void(*)(int)) )(int);
- 这是一个函数声明,声明的函数的名字叫:signal.signal函数有2个参数,第一个参数的类型是int,第二个参数的类型是 void()(int) 的函数指针类型,该指针可以指向一个函数,指向的参数类型是int,返回值是void。signal 函数的返回类型是void()(int) 的函数指针,该指针可以指向一个函数,指向的参数是int,返回值是void。
4.3.1 typedef 关键字
- 有些时候,我们在创建一个变量时,这个变量的类型可能会比较复杂,这时候我们可以通过使用 typedef关键字来对其简单化。
typedef unsigned int uint;//使用typedf将( unsigned int )简化成了uint
int main()
{
unsigned int num1;//定义一个无符号整型的变量
uint num2;
//此时定义的变量num1和num2的类型是一样的
return 0;
}
- 那么,typedf能不能对指针类型重命名呢?
typedef int* pint;//简化整型指针变量
typedef int(*parr_t)[5];//简化数组指针变量
int main()
{
int* p1 = NULL;
pint p2 = NULL;
int arr[5] = {0};
parr_t p2 = &arr;
return 0;
}
- 重命名函数指针变量
void text(char* s)
{
}
typedef void (*pf_t)(char*);//对函数指针类型重命名产生新的类型
int main()
{
void (*pf1)(char*) = text;
pf_t pf2 = text;
return 0;
}
- 我们学到这里,就可以使用typedef关键字对void (signal(int , void()(int)) )(int);简化一下。
typedef void(*pf_t)(int);
int main()
{
void (*signal(int, void(*)(int)))(int);
pf_t signal2(int, pf_t);
//此时上面两行代码的意思是完全一样的
return 0;
}
- 我们在前面也学过#define
typedef int* ptr_t;
#define PTR_T int*
int main()
{
ptr_t p1;//p1是整型指针
PTR_T p2;//p2是整型指针
//此时二者是没什么差异的
ptr_t p3, p4;//p3 p4是整型指针
PTR_T p5, p6;//p5是整型指针,p6是整型
return 0;
}
- 我们来调试观察一下:
- typedef对类型的重命名会更加彻底一些,如果我们要对一个类型进行重命名时,我们尽量使用typedef.
5.函数指针数组
- 接下来,我们还是通过类比来看看什么是函数指针数组。
- 整型数组: 存放整型的数组
- 字符数组: 存放字符的数组
- 指针数组: 存放指针的数组
- 字符指针数组: 存放字符指针的数组
- 函数指针数组: 存放函数指针的数组
- 我们先来实现一个” + - * / “的函数
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 (*pf1)(int, int) = add;
int (*pf2)(int, int) = sub;
int (*pf3)(int, int) = mul;
int (*pf4)(int, int) = div;
- 那我们大家可以看到这四个函数指针变量的类型都是int ( * )( int, int ),那么,如果要把多个相同类型的函数指针存放在一个数组中,这个数组就是函数指针数组。
int (*pfarr[4])(int, int) = { add, sub, mul, div };
//pfarr就是一个函数指针数组
- 我们来画图理解一下:
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()
{
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++)
{
printf("%d\n", pfarr[i](8, 4));
}
return 0;
}
- 代码运行结果如下:
- 看完之后大家是不是又觉得这个操作就是又脱裤子放屁了,直接调用不就行了吗?搞那么复杂,让我以为那么高级。
- 别急别急,假设我们想要用代码实现一个计算器,我们来看一下这个代码该怎么实现呢?
#include<stio.h>
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()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;//计算出的结果
do
{
menu();
printf("请选择:》");
scanf("%d", &input);
switch(input)
{
case 1:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("%d\n", ret);
break;
case 2:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = sub(x, y);
printf("%d\n", ret);
break;
case 3:
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = mul(x, y);
printf("%d\n", ret);
break;
case 4:
printf("请输入两个操作数:");
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;
}
6.转移表
- 虽然在上面我们已经完成了简易计算机的实现,但是我们发现这个代码里有两个主要的问题:
1.代码大量重复,显得不那么干脆利落。
2.如果我们想要实现更多的运算( >> << & | ^ && || 等 ),代码也会随之大量加长。
- 接下来,我们就对其进行一个优化:
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()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;//计算出的结果
//创建一个函数指针数组
//我们之所以让第一个元素为NULL,是为了确保后面的函数与其下标相对应。
int (*pfarr[5])(int, int) = {NULL, add, sub, mul, div};
do
{
menu();
printf("请选择:》");
scanf("%d", &input);
if (input >=1 && input <= 4)
{
printf("请输入两个操作数:\n");
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;
}
- 其实这里的函数指针数组就是转移表,这个函数指针就像一个跳板一样,我们只要简单地对其进行使用,就可以跳转到我们想要的函数里,完成代码的运算。
- 我们还有另一种优化方式(比较推荐这一种):
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 calc(int (*pf)(int, int))
{
int x = 0;
int y = 0;
int ret = 0;//计算出的结果
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("%d\n", ret);
}
int main()
{
int input = 0;
do
{
menu();
printf("请选择:》");
scanf("%d", &input);
switch(input)
{
case 1:
calc(add);
break;
case 2:
calc(sub);
break;
case 3:
calc(mul);
break;
case 4:
calc(div);
break;
case 0:
printf("退出计算器!\n");
break;
default:
printf("选择错误,重新选择!\n");
break;
}
} while (input);
return 0;
}
哈哈哈哈哈哈好开心啊,深入理解指针(3)终于结束了,家人们,期待深入理解指针(4)叭!!!