1. 字符指针变量
在指针的类型中我们知道有⼀种指针类型为字符指针 char* ;
一般使用:
int main() {
char i = 'a';
char* p = &i;
*p = 'q';
printf("%c", i);
return 0;
}
-
然后我们看这个例子,这是把⼀个字符串放到pstr指针变量里了吗?
-
事实上不是,他只是将这个字符常量的首字符的地址放进指针里
-
常量字符串不能被修改
int main()
{
const char* pstr = "hello bit.";//这⾥是把⼀个字符串放到pstr指针变量⾥了吗?
printf("%s\n", pstr);
return 0;
}
-
那我们怎么才可以输出这个字符常量呢?
-
%s 用p 就能输出所字符串
int main() {
char* p = "abcdef";
printf("%c", *p);
printf("%s",p);//用%s 用p 就能输出所字符串
return 0;
}
-
常量字符串不能被修改,如果我们直接这样写char* p = "abcdef"; 其实是错的,但是编译器没有报错;有时候往往找到不到bug在哪里
-
我们最好加一个const 到开头,直接限制他的错误
int main() {
char* p = "abcdef"; //const char* p = "abcdef";
printf("%c\n", *p);
printf("%s",p);//用%s 用p 就能输出所字符串
*p = 'b';
return 0;
}
《剑指offer》中收录了⼀道和字符串相关的笔试题,我们⼀起来学习⼀下:
-
内容相同的字符串,只需要保存一个就行 因为常量字符串不能修改
-
常量字符串(放在只读数据区)
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 and str2 are not same
//str3 and str4 are same
2.数组指针
-
什么是数组指针?我们来类比一下
-
数组指针,就是存放数组地址的指针
-
我们要对比记忆,数组指针和指针数组的区别
-
int (*) [10] 是p的类型 (数组指针的类型) arr -- int * arr+1 跳过四个字节 arr -- int * arr+1 也跳过四个字节 &arr -- int(*)[10] 跳过40个字节
int main() {
int arr[10] = {0};
int (*p) [10] = &arr; //p是数组指针,p存放的是数组的地址
//int (*) [10] 是p的类型 (数组指针的类型)
//arr -- int * arr+1 跳过四个字节
//arr -- int * arr+1 也跳过四个字节
// &arr -- int(*)[10] 跳过40个字节
return 0;
}
int (*p) [10] = &arr; //数组指针初始化
| | |
| | |
| | p指向数组的元素个数
| p是数组指针变量名
p指向的数组的元素类型
2.1 数组指针和指针数组的区别
-
我们要对比记忆,数组指针和指针数组的区别
-
指针数组,-->数组,存放指针的数组
-
数组指针-->指针,存放数组地址的指针
2.2访问数组指针
-
我们可以用(*p)[i] 访问数组的每一个元素
-
(*p)[i] 怎么理解?---->p相当于一个指针 指向arr的地址,然后我们解引用 *p 得到的的是arr ,然后通过下标[i]遍历数组元素
-
这种方法一般不会使用,复杂化了问题
#include <stdio.h>
int main() {
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int(*p)[10] = &arr;
int i = 0;
for (i = 0; i < 10; i++) {
printf("%d ", (*p)[i]);
}
return 0;
}
3. ⼆维数组传参的本质
有了数组指针的理解,我们就能够讲⼀下⼆维数组传参的本质了。
过去我们有⼀个⼆维数组的需要传参给⼀个函数的时候,我们是这样写的:
#include <stdio.h>
void test(int a[3][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 ", a[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;
}
-
二维数组的数组名如何理解呢?
-
也是数组首元素的地址,二维数组其实是一个一维数组的数组,二维数组的首元素就是他的第一行的地址
-
我们换成数组指针来遍历该二维数组
-
实参传过来的,我们可以知道二维数组的数组名是第一行的地址
-
第一行是一个一维数组,传过去的就是第一行的这个一维数组的地址(整个数组的地址)
-
然后我们用 int(*arr)[5] 数组指针接收 [5]表示一行有几个元素
void test(int(*arr)[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 ", 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} };
test(arr, 3, 5);
return 0;
}
-
或者用另外一种方式
#include <stdio.h>
void test(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} };
test(arr, 3, 5);
return 0;
}
-
怎么理解 * (*(p+i)+j)
-
我们可以看到下图,二维数组是一维数组的数组 ,*(p+i)访问第一个数组(首元素地址)-->
相当于 p[0] 再解引用访问第二个数组即可
-
p[i] 等于 *(p+i)
-
p[i] [j] 等于 * (*(p+i)+j)
4. 函数指针变量
什么是函数指针变量呢?
根据前⾯学习整型指针,数组指针的时候,我们的类⽐关系,我们不难得出结论:
函数指针变量应该是⽤来存放函数地址的,未来通过地址能够调⽤函数的。
那么函数是否有地址呢?
我们做个测试:
void test()
{
printf("hehe\n");
}
int main()
{
printf("test: %p\n", test);
printf("&test: %p\n", &test);
return 0;
-
我们来思考一下,&test 和 test
-
在数组中,&test 是表示整个数组元素的地址, arr 表示首元素地址
-
但是在函数指针中, &test 和 test是一个东西
如果我们要将函数的地址存放起来,就得创建函数指针变量咯,函数指针变量的写法其实和数组指针非常类似。如下:
-
int ( * ) ( int , int ) 函数指针的类型
void*test()
{
printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)()= test;
int Add(int x, int y)
{
return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;*//x和y写上或者省略都是可以的
-
调用的时候 (*pf) 和 pf 用法一样
-
注意 我们带*的时候 一定要带上( )---> ( *p) 不然会报错
printf("%d\n", (*pf3)(2, 3));
printf("%d\n", pf3(3, 5));
4.1两段有趣的代码
这两段代码均来自《C陷阱和缺陷》这本书
一个程序员与我交谈一个问题。他当时正在编写一个独立运行于某种微处理器上的 C程序。当计算机启动时,硬件将调用首地址为0位置的子例程为了模拟开机启动时的情形,我们必须设计出一个 C 语句,以显式调用该子例程。经过一段时间的思考,我们最后得到的语句如下:
//代码1
(*(void (*)( ) )0 ) ();
-
(void) (*) ( )--->表示函数指针的类型,将0(整形)强制转换成函数指针(没有返回值,没有参数)
-
外面的 * 表示解引用( *号可以省略 )这个函数指针(函数调用,0地址处放的那个函数) 右边的 ( )表示在函数调用的时候无参数调用
//代码2
void (*signal(int , void(*)(int) ) )(int);
-
我们看图解析
-
该代码是一次函数声明 类似于int add(int , int ) 不需要传入参数 只需要标明 返回类型和参数数据类型
4.2 typedef 关键字
typedef 是用来类型重命名的,可以将复杂的类型,简单化。
-
比如,你觉得 unsigned int
写起来不方便,如果能写成 uint 就方便多了,那么我们可以使用:
typedef unsigned int unit;//起别名
int main() {
unit i = 0;
}
-
如果是指针类型,能否重命名呢?其实也是可以的,比如,将 int* 重命名为 ptr_t ,这样写:
-
int* p = &a; ptr_t p = &a; 这两句代码一样
typedef int* ptr_t;
int main() {
int a = 3;
int* p = &a;
ptr_t p = &a;
}
-
数组指针和上述的命名方式不太一样, 将重命名放进括号里面
typedef(*parr_t)[10];
//数组指针类型
int main() {
int arr[10] = { 0 };
int(*p)[10] = &arr;
parr_t p2;
return 0;
}
-
函数指针也一样,重命名要放到*右边
typedef void(*pfun_t)(int);
-
那我们来简化一下上面代码二的形式
typedef void (*pfun_t) (int) // void (*) (int)
void (*signal(int , void(*)(int) ) )(int);
//然后我们可以简化成下面的形式
pfun_t signal(int,pfun_t)
5.函数指针数组
-
怎么用函数指针数组呢?
-
函数指针的类型假设为-->int (*) (int,int) 那函数指针数组的形式为----> int( * p[10]) (int ,int) = { }
-
要注意 数组里面存放的指针的类型要一致,返回类型也要一致
-
那么我们什么场景用函数指针数组呢?
下列四个函数返回类型和参数类型都是一样的,那么我们分别放到一个指针是不是很麻烦?
#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(*p)(int, int) = add;
int(*p1)(int, int) = sub;
int(*p2)(int, int) = mul;
int(*p3)(int, int) = div;
}
我们可以将函数指针放进数组里面,通过循环 数组遍历下标即可
#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(*p[4])(int, int) = { add,sub,mul,div };
int i = 0;
for (i = 0; i < 4; i++) {
int ret = p[i](2, 6);
printf("%d\n", ret);
}
}
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;
}
-
我们如果往下加入新的运算,这个case越加越多,代码就会很长
-
用函数指针数组来实现,这个代码就更灵活了
#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;
}
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 (*pfArr[5])(int, int) = { 0,add ,sub,mul,div };//我们前面加一个0,数组下标恰好对应菜单
do {
menu();
printf("请选择你想要的运算:");
scanf("%d", &input);
if (input >= 1 && input <= 4) {
printf("请输入两个操作数:");
scanf("%d %d", &x, &y);
int ret = pfArr[input](x, y);
printf("%d\n", ret);
}
else if(input==0)
{
printf("退出计算器");
break;
}
else
{
printf("输入错误请输入(1-4):\n");
}
}while(input);
return 0;
}