【C】指针进阶

简介: 字符指针 数组指针 指针数组 数组传参和指针传参 函数指针 函数指针数组 指向函数指针数组的指针


回调函数和qsort函数的有关内容,将会在下一篇中补充
🎈今日心语:抱怨是一件最没有意义的事,现在的努力是为了以后的不求别人,实力是最强的底气。


目录:


指针的进阶

本章重点

  1. 字符指针
  2. 数组指针
  3. 指针数组
  4. 数组传参和指针传参
  5. 函数指针
  6. 函数指针数组
  7. 指向函数指针数组的指针
  8. 回调函数(下一篇单独介绍)
  9. 指针和数组面试题的解析

指针的主题,我们在初级阶段的《指针》章节已经接触过了,我们知道了指针的概念:

  1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
  2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
  3. 指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。
  4. 指针的运算。

这个章节,我们继续探讨指针的高级主题

1. 字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char* ;

  • 第一种使用方式
int main()
{
  char* ch = 'w';
  char* pc = &ch;
  //可以用*pc(解引用操作)改变ch的值
return 0;
}
  • 第二种使用方式
int main()
{
  const char* ps = "abcdef";//这里的ps指向的是字符串的首字符
  //printf("%s\n", *ps);//%s打印的是字符串的内容
  //*ps = 'w';//常量字符串不可以修改,这里的写法是错误的
  //printf("%c\n", *ps);//这里打印出来的结果是a
  return 0;
}

这里需要注意,常量字符串“abcdef”时存放在只读数据区中的,可以读取,但不能更改,所以我们使用const来修饰,防止通过p修改字符串
const char
ps = “abcdef”;//这里的ps指向的是字符串的首字符

要想修改字符串中的内容需要用到数组:

char arr[]="a b c d e f";
char* p=arr;//arr是首字符地址

代码中的指向关系如图:

有道这样的面试题:

#include <stdio.h>
int main()
{
  char str1[] = "abcdef";//存放的是首元素地址
  char str2[] = "abcdef";//存放的是首元素地址
  //两个数组开辟两个独立的空间,所以首元素的地址不相同
  const char* str3 = "abcdef";//不可改变的常量字符串
  const char* str4 = "abcdef";//不可改变的常量字符串
  //从内存优化的角度看,没必要开辟两个独立的空间
  //所以该字符串在内存中只会保存一份,而str3,str4都是取了首字符的地址,所以是相同的
  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不同。

如果要比较内容,使用:strcmp库函数

2. 指针数组

在《指针》章节我们也学了指针数组,

指针数组是一个存放指针的数组。

这里我们再复习一下,下面指针数组是什么意思?

int* arr1[10]; // 存放整形指针的数组

char *arr2[4]; //存放一级字符指针的数组

char **arr3[5];//存放二级字符指针的数组

举例:

int main()
{
  char* arr[5] = { "abcdef","zhangsan","lisi","hehe","wangcai" };//存放指针的数组
  //把字符串首字符地址存放到了指针数组中
  int i = 0;
  for (i = 0; i < 5; i++)
  {
    printf("%s\n", arr[i]);//字符串的打印用%s
  }
  return 0;
}

这里字符串在内存中的布局可以参考下图

  • 用指针数组模拟二维数组
int main()
{
  int arr1[5] = { 1,2,3,4,5 }; 
  int arr2[5] = { 2,3,4,5,6 };
  int arr3[5] = { 3,4,5,6,7 };
  int* arr[3] = { arr1,arr2,arr3 };
  int i = 0;
  int j = 0;
  for (i = 0; i < 3; i++)
  {
    for (j = 0; j < 5; j++)
    {
      printf("%d ", arr[i][j]);
    }
    printf("\n");
  }
  return 0;
}

运行结果:

图解

3. 数组指针

3.1 数组指针的定义

数组指针是指针?还是数组?

答案是:指针。

我们已经熟悉:

整形指针: int * pint; 能够指向整形数据的指针。

浮点型指针: float * pf; 能够指向浮点型数据的指针。

那数组指针应该是:能够指向数组的指针。

下面代码哪个是数组指针?

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

下面对比了这三种类型的指针

//整型指针 - 指向整形的指针  -存放整型变量的地址
//int* pl;
//字符指针 - 指向字符的指针  -存放字符变量的地址
//char* p2;
//数组指针 - 指向数组的指针  -存放数组的地址
int main()
{
  int a = 10;
  int* pl = &a;
  char ch = 'w';
  char* p2 = &ch;
  int arr[10] = { 1,2,3,4,5 };
  int(*pa)[10] = &arr;//&arr取出的是数组的地址,存放到pa中
  //pa首先和*结合,说明是指针,pa是数组指针变量
  //int(*)[10]   -数组指针类型
  return 0;
}

3.2 &数组名VS数组名

对于下面的数组:

int arr[10];

arr 和 &arr 分别是啥?

我们知道arr是数组名,数组名表示数组首元素的地址。

那&arr数组名到底是啥?

我们看一段代码:

int main()
{
  int arr[10] = { 0 };
  printf("%p\n", arr);//数组首元素地址
  printf("%p\n", &arr[0]);//数组首元素地址
  printf("%p\n", &arr);//数组地址
  return 0;
}

运行结果如下;

可以看到运行结果相同,因为即使是数组的地址也是从首元素的地址开始的;

那么他们有何区别呢?

下面我们对上面的程序略做了修改,并进行了运行对比:

int main()
{
  int arr[10] = { 0 };
  printf("%p\n", arr);
  printf("%p\n", arr+1);
  printf("%p\n", &arr[0]);
  printf("%p\n", &arr[0]+1);
  printf("%p\n", &arr);
  printf("%p\n", &arr+1);
  return 0;
}

得到的运行结果:

根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。

如图,0x28,8代表的是8*(160),2代表的是2*(161),加起来为40

前两个运行结果涨了4,而第三个运行结果涨了40

实际上:arr和&arr[0]是数组首元素地址,类型是int*, &arr 表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)

本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型指针指向的是数组,所以

数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40.

//数组名是数组首元素的地址
//有两个例外
//1.sizeof(数组名)
//2.&数组名

3.3 数组指针的使用

那数组指针是怎么使用的呢?

既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。

看代码:

/之前学习过的几种打印数组内容的方法
//数组指针怎么用?
//void print1(int arr[10], int sz)
//{
//  int i = 0;
//  for (i = 0; i < sz; i++)
//  {
//    printf("%d ", arr[i]);
//  }
//  printf("\n");
//}
//void print1(int *arr, int sz)
//{
//  int i = 0;
//  for (i = 0; i < sz; i++)
//  {
//    printf("%d ", *(arr+i));
//  }
//  printf("\n");
//}
//数组指针在一维数组中用得少
void print2(int(*p)[10], int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%d ", (*p)[i]);//*&arr=arr
  }
  printf("\n");
}
void test1()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  //print1(arr, sz);
  print2(&arr, sz);
}
void print3(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");
  }
}
void print4(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");
  }
}
void test2()
{
  int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
  //print3(arr, 3, 5);
  print4(arr, 3, 5);
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
//可以数组指针来接收
}
int main()
{
  //test1();
  test2();
  return 0;
}

运行结果:

巩固:

学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:

int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];

4. 数组参数、指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?

4.1 一维数组传参

#include <stdio.h>
void test(int arr[])//ok?
{}
void test(int arr[10])//ok?
{}
void test(int *arr)//ok?
{}
void test2(int *arr[20])//ok?
{}
void test2(int **arr)//ok?
{}
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}

4.2 二维数组传参

void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//ok?
{}
void test(int* arr[5])//ok?
{}
void test(int (*arr)[5])//ok?
{}
void test(int **arr)//ok?
{}
int main()
{
int arr[3][5] = {0};
test(arr);
}

4.3 一级指针传参

#include <stdio.h>
void print(int *p, int sz)
{
int i = 0;
for(i=0; i<sz; i++)
{
printf("%d\n", *(p+i));
}
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9};
int *p = arr;
int sz = sizeof(arr)/sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);
return 0;
}

反向思考:

当一个函数的参数部分为一级指针的时候,函数能接收什么参数?

比如:

void test1(int *p)
{}
//test1函数能接收什么参数?
void test2(char* p)
{}
//test2函数能接收什么参数?

解析:

4.4 二级指针传参

#include <stdio.h>
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int*p = &n;
int **pp = &p;
test(pp);
test(&p);
return 0;
}

思考:

当函数的参数为二级指针的时候,可以接收什么参数?

void test(char **p)
{ }
int main()
{
char c = 'b';
char*pc = &c;
char**ppc = &pc;
char* arr[10];
test(&pc);//一级指针变量取地址
test(ppc);//二级指针
test(arr);//Ok?指针数组的数组名
return 0;
}

5. 函数指针

类似于数组指针,指向函数的指针

首先看一段代码

int Add(int x, int y)
{
  return x + y;
}
int main()
{
  //int(*p)[10] = { 0 };//数组指针
  //pf就是函数指针变量
  int (*pf)(int x, int y) = &Add;//函数指针
  int sum = (*pf)(3, 5);//等价 方便理解,解引用
  //int sum = pf(3, 5);//等价
  //int sum = Add(3, 5);//等价
  printf("%d\n", sum);
  //int arr[10] = { 0 };
  //printf("%p\n", &arr);//取出数组的地址
  //printf("%p\n", arr);//这里虽然结果一样,但是本质上是有区别的,前面讲过
  ////Add和&Add都是函数地址,没有区别
  //printf("%p\n", Add);
  //printf("%p\n", &Add);
  return 0;
}

那么,看了上面内容后你是否掌握了函数指针了呢?这里来测试一下

小题测试:

int test(const char* str, double d)

{

}

int main()

{

//这里需要写入函数指针

//提示:

pt= &test;

return 0;

}

答案:

//int (pt)(const char, double) = &test;

//int (pt)(const char str, double d) = &test;


阅读两段有趣的代码:

//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);

解析1:

解析2:

注 :推荐《C陷阱和缺陷》

这本书中提及这两个代码。

代码2太复杂,如何简化:

typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);

6. 函数指针数组

数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组,

比如:

int *arr[10];

//数组的每个元素是int*

那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];

答案是:parr1

parr1先和[]结合,说明 parr1是数组,数组的内容是什么呢?

是int (*)()类型的函数指针。

  • 函数指针数组的一个简单举例:
//函数指针数组
int Add(int x, int y)
{
  return x + y;
}
int Sub(int x, int y)
{
  return x - y;
}
int main()
{
  //函数指针数组
  //可以存放多个【参数相同,返回类型相同】的函数的地址、
  int (*pfArr[2])(int, int) = { Add, Sub };
  int ret = pfArr[0](2, 3);//0是数组下标,这里指的是函数指针数组中的Add
  printf("%d\n", ret);
  ret = pfArr[1](2, 3);//1是数组下标,这里指的是Sub
  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()
{
  int input = 0;
  int x = 0;
  int y = 0;
  int ret = 0;
  do
  {
    menu();
    printf("请选择:>");
    scanf("%d", &input);
    printf("请输入两个操作数:>");
    scanf("%d %d", &x, &y);
    switch (input)
    {
    case 1:
      ret = Add(x, y);
      printf("%d\n", ret);
      break;
    case 2:
      ret = Sub(x, y);
      printf("%d\n", ret);
      break;
    case 3:
      ret = Mul(x, y);
      printf("%d\n", ret);
      break;
    case 4:
      ret = Div(x, y);
      printf("%d\n", ret);
      break;
    case 0:
      printf("退出计算器\n");
      break;
    default:
      printf("选择错误\n");
      break;
    }
  }while (input);
}

观察上述代码,我们发现并没有使用函数指针数组,上述代码有一定的缺陷

  • 代码初测缺陷:

    如图,当我们输入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()
{
  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);
}

观察上述代码,不难发现代码有些冗余,如果我们后续需要加入其他运算,则既需要改动前面自定义的函数,又需要对case语句进行修改,为了更加方便,我们需要运用函数指针数组的知识。

  • 利用函数指针数组改进:
    函数指针数组
    可以存放多个【参数相同,返回类型相同】的函数的地址。

  • 最终代码:
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;
  //函数指针数组   - 转移表
  int (*pfArr[])(int, int) = { 0, Add, Sub, Mul, Div };
  do
  {
    menu();
    printf("请选择:>");
    scanf("%d", &input);
    if (input == 0)
    {
      printf("退出计算器\n");
      break;
    }
    if (input >= 1 && input <= 4)
    {
      printf("请输入2个操作数:>");
      scanf("%d %d", &x, &y);
      ret = pfArr[input](x, y);
      printf("%d\n", ret);
    }
    else
    {
      printf("选择错误\n");
    } 
  } while (input);
}

7. 指向函数指针数组的指针

指向函数指针数组的指针是一个指针

指针指向一个数组,数组的元素都是函数指针;

如何定义?

相关文章
|
18天前
|
存储
指针进阶详解(下)
指针进阶详解(下)
23 0
|
5天前
|
C语言
C语言进阶:进阶指针(下)
C语言进阶:进阶指针(下)
|
5天前
|
C语言
C语言进阶:指针的进阶(上)
C语言进阶:指针的进阶(上)
|
6天前
入门后指针进阶习题深度分析
入门后指针进阶习题深度分析
16 1
|
6天前
进阶指针笔试题
进阶指针笔试题
15 0
|
11天前
|
存储 C语言 C++
C语言进阶——指针
C语言进阶——指针
|
13天前
|
C语言
C语言进阶⑬(字符串函数)+(指针编程题)strlen+strcpy+strcat+strstr+strtok+strerror(下)
C语言进阶⑬(字符串函数)+(指针编程题)strlen+strcpy+strcat+strstr+strtok+strerror
6 0
|
13天前
|
安全 C语言
C语言进阶⑬(字符串函数)+(指针编程题)strlen+strcpy+strcat+strstr+strtok+strerror(中)
C语言进阶⑬(字符串函数)+(指针编程题)strlen+strcpy+strcat+strstr+strtok+strerror
23 0