C语言指针详解(三)目录版
- 1、字符指针变量
- 1.1、字符指针变量的一般应用
- 1.2、常量字符串
- 1.3、常量字符串与普通字符串的区别
- 1.3.1 常量字符串的不可修改性
- 1.3.2 常量字符串的存储
- 2、数组指针变量
- 2.1、数组指针变量定义
- 2.2、数组指针变量的初始化
- 3、二维数组传参本质
- 4、函数指针变量
- 4.1、函数指针变量的创建
- 4.1.1、验证函数地址的存在
- 4.1.2、函数指针变量表达式
- 4.2、函数指针变量的使用
- 4.3、深入理解函数指针
- 4.3.1、例题一
- 4.3.2、例题二
- 4.3.3、例题一解析
- 4.3.4、例题二解析
- 4.4、关键字 typedef 介绍
- 4.4.1、typedef 表达式与实例演示
- 4.4.2、define 与 typedef 的区别
- 5、函数指针数组
- 6、转移表
1、字符指针变量
我们知道若指针所指向的内容是字符,那么指针的类型就是字符指针类型 char* ,这个指针变量就是字符指针变量。
1.1、字符指针变量的一般应用
int main()
{
char ch = 'a';
char* pc = &ch;
*pc = 'w';
printf("%c", ch);
return 0;
}
1.2、常量字符串
#include<stdio.h>
int main()
{
const char* pt = "hello world.";//请思考这里是把一个字符串放到了 pt 指针当中了吗?
printf("%s\n", pt);
return 0;
}
如上面代码所示的 char* name = “字符串” 就是常量字符串的表达方式。那么我们思考一下注释中的问题:这里是把一个字符串放到了 pt 指针当中了吗?
答案是:否。毕竟前面是一个指针变量,其存储的应当为地址,故放入的是首字符的地址。因为字符是连续存放的,输出时系统会自动向后遍历,直至遇到 “/0”。
运行效果图:
1.3、常量字符串与普通字符串的区别
1.3.1 常量字符串的不可修改性
在 1.1 的代码和运行图中我们可见一般情况下的字符(字符串)是可以通过指针解引用来进行修改的。而常量字符串则不可修改,如下面代码示例演示
int main()
{
char* pt = "hello world.";
printf("%s\n", pt);
*pt = "Hello World";
return 0;
}
原因请见下文 1.3.2
1.3.2 常量字符串的存储
我们知道在存储位置中有栈区,堆区,静态区。其实在此之外还有一部分称为代码段。一般字符(字符串)存储在栈区中,而常量字符串则存储在代码段中。所以即使通过指针解引用也不能更改字符串内容。正因如此,当多个指针变量指向的常量字符串内容一致时,系统不会存储多份相同的常量字符串,而是会只存储一份,各个变量共用这一份常量字符串(即字符串地址相同)。为使各位对此更加清晰,我们通过以下代码和图解来进行进一步解析。
int main()
{
char str1[] = "You are handsome !";
char str2[] = "YOu are handsome !";
char* str3 = "You are so beautiful";
char* str4 = "You are so beautiful";
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;
}
2、数组指针变量
2.1、数组指针变量定义
数组指针,顾名思义它是一种指针。我们已经熟知:
整型指针变量:int * p
;存放的是整型变量的地址,指向整型数据的指针。
浮点型指针变量:float * p
;存放的是浮点型变量的地址,指向浮点型数据的指针。
由上推知:数组指针变量应当存放的是数组的地址,指向数组的指针。
数组指针变量表达式:int (*p) [20]
;
在这个表达式中,p会先和*结合,表明 p 是一个指针变量,然后指针指向的是一个大小为20个整型变量的数组。
注意:因为 [ ] 的优先级要高于 * 号,所以必须加上 ()来保证 p 先和 * 结合
2.2、数组指针变量的初始化
既然数组指针变量存放的是数组的地址,那么我们就要将数组的地址给指针变量。数组地址:&数组名
int arr[10] = {0};//定义数组并初始化
int (*p) [10] = &arr;//将数组的地址赋值给指针变量
3、二维数组传参本质
在数组章节中曾提及二维数组传参的如下写法:
#include <stdio.h>
void print_arr(int arr[4][5], int a, int b)
{
int i = 0;
for (i = 0; i < a; i++)
{
int j = 0;
for (j = 0; j < b; j++)
{
printf("%d", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[4][5] = { {1,2,3,4,5},{2.3,4,5,6},{3,4,5,6,7},{4,5,6,7,8}};
print_arr(arr, 4, 5);
return 0;
}
在上述代码中,实参为二维数组,形参也为二维数组。那么下面来介绍另一种传参方式,使用数组指针进行传参。
二维数组在之前数组章节曾讲过,其可以看成每个元素是一维数组的数组,即二维数组的每个元素是一个一维数组,二维数组首元素就是第一行,是一个一维数组。
二维数组的数组名就是第一行的地址,是一个一维数组的地址,第一行一维数组类型为int [5]
,故其地址类型为int (*) [5];
。如此也就证明二维数组传参实际上是传递了第一行一维数组的地址。由此可知,函数形参部分也可以写成指针形式。如下所示:
#include <stdio.h>
void print_arr(int (*p) [5], int a, int b)//数组形参设置为数组指针
{
int i = 0;
for (i = 0; i < a; i++)
{
int j = 0;
for (j = 0; j < b; j++)
{
printf("%d", *( * ( p + i ) + j ) );//深入理解系统对数组arr[ i ]的编译。
// arr[ i ] = *(arr + i)
}
printf("\n");
}
}
int main()
{
int arr[4][5] = { {1,2,3,4,5},{2.3,4,5,6},{3,4,5,6,7},{4,5,6,7,8} };
print_arr(arr, 4, 5);
return 0;
}
4、函数指针变量
4.1、函数指针变量的创建
根据上文内容,我们可以推断出函数指针变量是存储函数地址的变量,同时我们可以通过函数的地址来调用函数。
4.1.1、验证函数地址的存在
下面我们来验证函数地址的存在:
#include<stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("test = %p\n", test);//函数名
printf("&test = %p\n", &test);//取函数地址
return 0;
}
由此说明函数存在地址,函数名与 &函数名 都可以得到函数的地址。
4.1.2、函数指针变量表达式
表达式如下:
int (*pf)(int x , int y);
//或 int (*pf)(int , int)
去掉变量名即为其变量类型:
int (*)(int x , int )
4.2、函数指针变量的使用
在使用函数指针变量调用函数时,(*pf)()与 pf()两种方法都是可行的,因为二者表示的意思都一致,都是对应函数的地址。
代码演示:
#include<stdio.h>
int div(int a , int b)
{
return a / b;
}
int main()
{
int (*pf) (int, int) = div;
printf("%d\n", (*pf)(4 , 2) );//调用方式一
printf("%d\n", pf(4, 2));//调用方式二
return 0;
}
4.3、深入理解函数指针
4.3.1、例题一
(*( void (*)() ) 0)();//请思考此行代码的意义
请思考上面代码的意义,解析见 4.3.3
4.3.2、例题二
void (*signal(int, void(*)(int)))(int);//请思考此行代码的意义
请思考上面代码得意义,解析见 4.3.4
4.3.3、例题一解析
入手关键点在 0 处,其前方应当为0的类型,( void (*) () ) 0
典型的强制类型转换,将0从整型 int 强制转换为函数指针类型 void ( * ) () ,从而 0 就成为一个函数指针变量,整体就是通过函数指针调用函数。
4.3.4、例题二解析
入手关键点在中间的函数名及其参数部分 signal(int, void(*)(int))
我们可见在这一小部分中已经包含了signal函数的函数名和该函数内的两个参数的类型,但无参数名,根据C语言标准可知这应当是一个函数声明。故剩余部分为函数的返回类型,为 void (*) (int)
函数指针类型。因为解引用操作符 * 后要跟名称,所以signal(int, void(*)(int))
放在了 * 之后。综上,整体为一个函数声明
4.4、关键字 typedef 介绍
4.4.1、typedef 表达式与实例演示
typedef是用来类型的重命名的,可以简化复杂的类型,如 4.3.4 例题二中的函数指针类型。其重定义表达式如下:
typedef name1 name2;
name1 表示需要重定义类型名
name2 表示 name1 重定义后的类型名
具体演示如下:
#include<stdio.h>
int div(int a , int b)
{
return a / b;
}
int main()
{
typedef int(*son)(int, int);
// * 后要跟名称,所以新的类型名要放在 * 后
son pf = div;
printf("%d\n", (*pf)(18 , 2) );
printf("%d\n", pf(10, 2));
return 0;
}
4.4.2、define 与 typedef 的区别
1、 本质区别:
#define
是预处理指令,用于文本替换。在编译之前,预处理器会直接将 #define
定义的宏替换成指定的文本。
typedef
是类型定义命令,用于为已存在的数据类型创建一个新的名字。
2.、作用范围:
#define
定义的宏没有作用域限制,一旦定义,就会一直有效,除非被 #undef
取消定义。
typedef
定义的类型有作用域限制,它遵循C语言的变量作用域规则。
3、 类型检查:
#define
不进行类型检查。它仅仅是在预处理阶段进行文本替换,所以不会检查替换后的类型是否正确。
typedef
会进行类型检查。当你使用 typedef
定义的新类型时,编译器会检查类型是否匹配。
4、 使用方式:
#define
可以用于定义常量、宏函数等,不仅仅限于类型。
typedef
仅用于定义类型的别名。
5、 内存分配:
#define
不会分配内存,因为它只是在预处理阶段进行文本替换。
typedef
本身也不分配内存,但它定义的类型在创建变量时会分配内存。
5、函数指针数组
我们知道数组是一个存储相同类型数据的存储空间。故函数指针数组就是一块连续的存储函数指针的空间。
表达式如下:
type (* name[number])();
name 会先和 [ ] 结合,表明 name 是数组,数组的内容是 type (*) ()
类型的函数指针。(因为 *
后面要跟名称,所以 *
后面跟上数组名)
6、转移表
转移表是函数指针数组实例化的体现。
下面以简易计算器的改造为例:
//改造前的计算器
#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 x, y;
int input = 1;
int ret = 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);
switch (input)
{
case 1:
printf("请输入操作数:");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("请输入操作数:");
scanf("%d %d", &x, &y);
ret = sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("请输入操作数:");
scanf("%d %d", &x, &y);
ret = mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("请输入操作数:");
scanf("%d %d", &x, &y);
ret = div(x, y);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
上述代码的条件分支中我们可以看到有许多重复的部分
这样就出现两个问题:
1、将这样重复的代码实现成函数。 2、这个函数又能完成不同的任务
如此我们有两种思路:
1、设计回调函数
2、引入函数指针数组来设计转移表。
在此我们以思路二进行改造(思路一请见《C语言指针详解(四)》)。首先我们要思考,设计转移表的话函数指针数组中的元素应当如何设置。我们一共自定义了 4 个函数,所以我们就把这四个函数的地址放入数组。具体演示如下:
#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 x, y;
int input = 1;
int ret = 0;
int (*parr[5])(int , int) = { 0 , add , sub , mul , div };//转移表
//之所以首元素为 0 是为了便于选择,使1-4都对应具体函数
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", &x, &y);
ret = (parr[input])(x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出计算器\n");
}
else
{
printf("输入错误");
}
} while (input);
return 0;
}
全文至此结束!!!
写作不易,不知各位老板能否给个一键三连或是一个免费的赞呢(▽)(▽),这将是对我最大的肯定与支持!!!谢谢!!!(▽)(▽)