C生万物 | 从浅入深理解指针【第三部分】(转移表的实现)

简介: C生万物 | 从浅入深理解指针【第三部分】(转移表的实现)

一、字符指针变量

  • 在指针的类型中我们知道有一种指针类型为字符指针char* ;
  • 我们这里定义了ch变量,里面存了个字符 w
  • 然后我将这个变量的地址取出来放到pc里,它的类型是char*,pc就是字符指针变量
int main()
{
  char ch = 'w';
  char* pc = &ch;
  return 0;
}
  • 还有一种写法:
  • 这里的指针变量p是要将字符"abcdefghi"放进去吗?
  • 字符指针变量是用来存放地址的
  • 这个代码的意思不是将"abcdefghi\0"字符串放到p中
char* p = "abcdefghi";
  • 这里的表达式都有两个属性:值属性和类型属性
  • 这里的字符串就是一串连续的,和数组一样
  • 这个字符串就是首字符a的地址,也就是说只是把a的地址赋值给了p
  • 我们可以这样验证:
char* p = "abcdefghi";
printf("%c", *p);
  • 可以看到拿出了a

  • 这里的"abcdefghi"是常量字符串,是不能被修改的~~

  • 我们可以给这个指针变量p加上const来修饰
const char* p = "abcdefghi";

那我想要打印一下这个字符串,怎么办?

我们就以%s的方式来打印

const char* p = "abcdefghi";
printf("%s", p);

  • 通过调试我们也可以发现是连续存放的~~

《剑指offer》中收录了一道和字符串相关的笔试题,我们一起来学习一下:

  • 这道题打印的是什么呢?
#include <stdio.h>
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;
}
  • 我们先来看一下结果

  • 那为什么是这样的结果呢,我们来分析一下~~
  • 这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。
  • 但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。

二、数组指针变量

2.1 数组指针变量是什么?

之前我们学习了指针数组,指针数组是一种数组,数组中存放的是地址(指针)。

答案是:指针变量


我们已经熟悉:


整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针。


浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。


数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。

  • 那么我们的数组指针怎么写呢?
int *p1[10];
int (*p2)[10];
  • 这两个是哪个呢?
  • 答案是第二个~~,第一个是指针数组,

数组指针变量

int (*p)[10];
  • 解释: p先和*结合,说明p是一个指针变量变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫 数组指针。
  • 这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。

2.2 数组指针变量怎么初始化

  • 数组指针变量是用来存放数组地址的,那怎么获得数组的地址呢?就是我们之前学习的&数组名
int arr[10] = { 0 };
&arr;//得到的就是数组的地址
  • 如果要存放个数组的地址,就得存放在数组指针变量中,如下:
int(*p)[10] = &arr;
  • 我们调试也能看到&arr 和p 的类型是完全一致的。

  • 数组指针类型的解析:

  • 去掉名字就是这个指针的类型

  • 这就是为什么arr和&arr是不一样的
int arr[10] = { 0 };
arr; //数组首元素的地址 -- int*
&arr;//数组的地址      -- int(*)[10]
  • 指针类型决定了+1加了多少个字节~~

三、二维数组传参的本质

  • 有了数组指针的理解,我们就能够讲一下二维数组传参的本质了。
  • 过去我们有一个二维数组的需要传参给一个函数的时候,我们是这样写的:
#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 [5] ,所以第一行的地址的类型就是数组指针类型int(*)[5] 。那就意味着二维数组传参本质上也是传递了地址,传递的是第一行这个一维数组的地址,那么形参也是可以写成指针形式的。如下:
#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;
}

总结: 二维数组传参,形参的部分可以写成数组,也可以写成指针形式。

四、函数指针变量

4.4 函数指针变量的创建

  • 什么是函数指针变量呢?
  • 数组指针,是指针,指向数组的指针,是存放数组的指针
  • 函数指针,是指针,是指向函数的指针,是存放函数地址的指针~~
  • 那么函数是否有地址呢?
#include <stdio.h>
void test()
{
  printf("hehe\n");
}
int main()
{
  printf("test:  %p\n", test);
  printf("&test: %p\n", &test);
  return 0;
}
  • 我们可以看到是一样的
  • 对于函数来说,&函数名和函数名都是函数的地址~~

  • 我们还可以通过调试来看一下

  • 确实打印出来了地址,所以函数是有地址的,函数名就是函数的地址,当然也可以通过&函数名的方式获得函数的地址。
  • 如果我们要将函数的地址存放起来,就得创建函数指针变量咯,函数指针变量的写法其实和数组指针非常类似。如下:
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写上或者省略都是可以的
  • 那这个函数指针有什么用呢?

4,5 函数指针变量的使用

那我们是不是要进行使用,怎么使用呢?

  • 调用函数指针传参,可以看到是能打印出来的~~
  • 那有的同学会说,我直接调用这个函数不就好了,为什么要多此一举呢?别着急,格局要打开,如果没用的话就不讲了~~
int Add(int x, int y)
{
  return x + y;
}
int main()
{
  int (*pf)(int, int) = &Add;
  int r = (*pf)(3, 5);//调用函数指针
  printf("r = %d\n", r);
  return 0;
}

  • 我们继续来看,那有的同学会说,我pf不加解引用操作符可以吗?答案是可以的~
  • 就算你写多个*也行,但是写上就更容易理解,可读性更高一些~~
int r = pf(3, 5);

函数指针类型解析:

4.6 两段有趣的代码

代码1

(*(void (*)())0)();
  • 调用0地址处的函数,调用的函数,参数是无参,返回类型是void

代码2

void (*signal(int , void(*)(int)))(int);
  • signal是一个函数的函数名,上面的代码是一次函数声明,声明的signal函数有两个参数,第一个参数是int类型的,第二个参数是函数指针类型的,该函数指针指向的函数参数是int类型,返回类型是void
  • signal函数的返回类型也是一个函数指针,该函数指针指向的函数,参数是int,返回类型也是void

两段代码均出自:《C陷阱和缺陷》这本书

4.7 typedef关键字

  • typedef 是用来类型重命名的,可以将复杂的类型,简单化。
  • 比如,你觉得unsigned int写起来不方便,如果能写成uint 就方便多了,那么我们可以使用:
typedef unsigned int uint;
//将unsigned int 重命名为uint
  • 如果是指针类型,能否重命名呢?其实也是可以的,比如,将int*重命名为ptr_t,这样写:
typedef int* ptr_t;
  • 但是对于数组指针和函数指针稍微有点区别:
  • 比如我们有数组指针类型int(*)[5] ,需要重命名为parr_t ,那可以这样写:
typedef int(*parr_t)[5]; //新的类型名必须在*的右边
  • 函数指针类型的重命名也是一样的,比如,将void(*)(int) 类型重命名为pf_t ,就可以这样写:
typedef void(*pfun_t)(int);//新的类型名必须在*的右边
  • 那么要简化代码2,可以这样写:
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);

五、函数指针数组

  • 数组是一个存放相同类型数据的存储空间,我们已经学习了指针数组
  • 整形指针数组:数组,数组中存放的都是整形指针
  • 函数指针数组:数组,数组中存放的都是函数指针

比如:

int *arr[10];
//数组的每个元素是int*
  • 那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[3])();
  • parr1 先和[] 结合,说明 parr1是数组,数组的内容是什么呢?
    int (*)() 类型的函数指针。
  • 函数指针数组就是存放函数指针的数组~~

那么有用吗,有的!!,接下来就来到我们的转移表模块~~

六、转移表

函数指针数组的用途:转移表

  • 举例:计算器的一般实现:
#include <stdio.h>
menu()
{
  printf("*************************\n");
  printf(" 1:add 2:sub \n");
  printf(" 3:mul 4:div \n");
  printf(" 0:exit \n");
  printf("*************************\n");
}
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
  {
    menu();
    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;
}
  • 这个计算器的实现,有一些不好的地方,假设我这个计算器后面要算的功能更多了,随着函数的功能不断的增长,菜单要跟着变,swich里面的也是需要跟着变,代码会越来越长
  • 这个时候有另外一种解决办法,解下来改造我们的版本~~、
#include <stdio.h>
void menu()
{
  printf("*************************\n");
  printf(" 1:add 2:sub \n");
  printf(" 3:mul 4:div \n");
  printf(" 0:exit \n");
  printf("*************************\n");
}
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(*pfArr[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
  do
  {
    menu();
    printf("请选择:");
    scanf("%d", &input);
    if ((input <= 4 && input >= 1))
    {
      printf("输入操作数:");
      scanf("%d %d", &x, &y);
      ret = (*pfArr[input])(x, y);
      printf("ret = %d\n", ret);
    }
    else if (input == 0)
    {
      printf("退出计算器\n");
    }
    else
    {
      printf("输入有误,请重新选择\n");
    }
  } while (input);
  return 0;
}
  • 这样改造我们的代码,代码量大幅度的缩短~~
相关文章
|
7月前
|
C语言
指针(4)---转移表
指针(4)---转移表
35 0
C生万物 | 从浅入深理解指针【最后部分】(二)
C生万物 | 从浅入深理解指针【最后部分】(二)
C生万物 | 从浅入深理解指针【第二部分】(二)
C生万物 | 从浅入深理解指针【第二部分】(二)
|
7月前
|
C语言 C++
C生万物 | 从浅入深理解指针【最后部分】(一)
C生万物 | 从浅入深理解指针【最后部分】(一)
C生万物 | 从浅入深理解指针【第四部分】(qsort的使用和模拟实现)
C生万物 | 从浅入深理解指针【第四部分】(qsort的使用和模拟实现)
|
28天前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
100 13
|
6月前
|
C语言
指针进阶(C语言终)
指针进阶(C语言终)
|
2月前
|
C语言
无头链表二级指针方式实现(C语言描述)
本文介绍了如何在C语言中使用二级指针实现无头链表,并提供了创建节点、插入、删除、查找、销毁链表等操作的函数实现,以及一个示例程序来演示这些操作。
35 0
|
3月前
|
存储 人工智能 C语言
C语言程序设计核心详解 第八章 指针超详细讲解_指针变量_二维数组指针_指向字符串指针
本文详细讲解了C语言中的指针,包括指针变量的定义与引用、指向数组及字符串的指针变量等。首先介绍了指针变量的基本概念和定义格式,随后通过多个示例展示了如何使用指针变量来操作普通变量、数组和字符串。文章还深入探讨了指向函数的指针变量以及指针数组的概念,并解释了空指针的意义和使用场景。通过丰富的代码示例和图形化展示,帮助读者更好地理解和掌握C语言中的指针知识。
122 4
|
4月前
|
C语言
【C初阶——指针5】鹏哥C语言系列文章,基本语法知识全面讲解——指针(5)
【C初阶——指针5】鹏哥C语言系列文章,基本语法知识全面讲解——指针(5)