指针的进阶【详解】

简介: 指针的进阶【详解】

指针是什么

在计算机中,数据通常储存在内存中,不同类型的数据需要开辟不同大小的空间进行数据存储,

而在计算机中,每块内存都有着相应的编号,即相应的地址;当我们需要将一个数据的地址进行保存

时,则需要一个变量来指向这个地址,而在c语言中,指向一个地址的变量,即为指针变量。


基础指针类型

在c语言中,有许多比较基本的指针类型

int* - 指向整形地址的指针

char* - 指向字符地址的指针

float* - 指向浮点数地址的指针


举个例子

int main()
{
  int a = 10;/*整型变量*/
  char b = 'a';/*字符型变量*/
  float c = 1.5f;/*浮点型变量*/
  //--------------
  int*pa = &a;//pa即为整形指针,指向整形变量a的地址
  char*pb = &b;//pb即为字符指针,指向字符变量b的地址
  float*pc = &c;//pc即为浮点型指针,指向浮点型变量c的地址
  return 0;
}

进阶指针类型

除了以上的较为简单的指针以外,还存在着一些比较复杂的指针。

  1. 数组指针
  2. 函数指针

根据类比法我们可知

整形指针即为指向整形数据地址的指针

字符指针即为指向字符数据地址的指针

同理可得

数组指针即为指向数组地址的指针

函数指针即为指向函数地址的指针


数组指针

从上面的类比法可以得到结论 - 数组指针即为指向数组的指针 但是数组指针应该如何表示?

假设存在一个数组 - int arr[10] = {0} ;

并要求利用一个变量名为pr的指针变量接收,应该怎么写?

int main()
{ //错误
  int arr[10] = {0};
  int *p[10] = &arr;
  return 0;
}

可以利用上面的方式写吗? 当仔细分析,我们可知,这里的变量p先与[]进行结合,说明p其实为数组,

而我们需要表达的意思为采用一个名为p的指针来指向数组地址。 故上面的代码不合题意,故 ×


那如何去对该题进行解释呢? 根据操作符优先级我们不难发现,如果以上面的代码形式来创建数组指针

p都会先于[]进行结合成为一个数组,能不能在此使用一个操作符来改变顺序使p先与*结合

int main()
{ //正确
  int arr[10] = {0};
  int (*p)[10] = &arr;
  return 0;
}

当我们使用()使强行先与p进行结合时,p即为指针 该段代码的理解则是: 根据操作符优先级,()内与p先进行结合,p即为指针

指针指向的为[]数组,数组内共有10个元素,每个元素的类型为int型 而该指针指向的即为数组arr的地址。

&数组名与数组名的区别

从上面的代码中,可以知道数组指针的表示形式为

  • int (* )[ ] - 且其功能为指向数组地址
    但其中有个疑问,指针中存在着这样的表达形式
    假设存在一个数组 int arr[10] = {0}; int* p= arr;
    即数组名为arr,数组元素个数为10,且每个元素的类型为int型
    既然&数组名才是数组地址的话,数组名又是什么?

当把arr与&arr同时以地址的形式打印出时,可以观察到,其实两者的地址相同,

或许有些人就会理所应当认为&arr与arr等价,但若是同时再将两个地址进行+1, 将会打印什么?

从这里不难发现 当以地址打印arr与arr + 1时,两者相差了4

而以地址打印&arr与&arr + 1时,两者却相差了2 8

(因为这里的表示形式为地址,而为了更好的表现,地址以16进制进行展示)

十六进制的2 8换做十进制即为40

即为40 又因为数组的元素个数为10,可以发现当&arr + 1时,越过了整个数组的地址

再将arr[0]的地址与sizeof(arr),sizeof(arr[0])分别进行打印时

可以得出结论:

在大部分情况中,数组名即为首元素地址

其中有两个例外:

1、sizeof(数组名) 2、&arr

int main()
{
  int arr[10] = { 0 };
  printf("arr = %p\n",arr);
  //数组名为首元素地址,故这里打印的时候为数组首元素的地址
  printf("arr + 1 = %p\n\n", arr+1);  //  int *
  //数组首元素地址+1,即数组首元素后一个元素的地址
  //因为类型为int型,故地址中相差4个字节
  printf("&arr = %p\n", &arr);  //int(* )[10]
  //数组名为首元素地址,但是有两个例外
  //一个是sizeof()数组名,一个是&数组名
  printf("&arr + 1 = %p\n\n", &arr+1);
  //当&数组名+1时,共相差 "数组元素个数 * 每个元素字节"
  //从这里可知,在内存中,地址相差40个字节
  //故 &数组名 为整个数组的地址
  printf("&arr[0] = %p\n", &arr[0]);  //int *
  //该处对数组单个元素的地址进行打印
  printf("&arr[0] + 1 = %p\n\n", &arr[0]+1);
  //当对单个数组地址+1时,只越过了4个字节,即一个整形
  printf("sizeof(arr) = %d\n", sizeof(arr));
  //该处是对整个数组的元素大小进行计算
  //故结果为 "数组元素个数 * 元素大小"
  printf("sizeof(arr[0]) = %d\n\n", sizeof(arr[0]));
  //该处只是对数组内单个元素进行计算
  //故打印结果即为单个元素的大小
      /*数组名为首元素地址是大部分的情况,除了两个特殊例外
        分别为 &数组名 与 sizeof(数组名) 
        除此之外,数组指针是一个指向整个数组的指针
        而普通指针只能存放数组单个元素的地址*/
  return 0;
}

数组指针该如何使用

假设存在一个数组 - int arr[10] = {1,2,3,4,5,6,7,8,9,10};

该如何访问数组中的每个元素?


1.for循环,利用下标访问操作符[]来访问每个元素

int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    for (int i = 0; i < sz; i++) {
    printf("%d ", arr[i]);
    }*/
  return 0;
}

2.for循环,利用*解引用操作每个元素的地址进行访问

int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
  //使用指针来访问数组元素   ②
  int sz = sizeof(arr) / sizeof(arr[0]);
  int* p = arr;
  for (int i = 0; i < sz; i++) {
      //printf("%d ", *(arr + i));
  printf("%d ", *(p + i));
  }
  return 0;
}

3.for循环利用数组指针访问数组内元素

int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
    使用数组指针对数组元素进行访问   
    int sz = sizeof(arr) / sizeof(arr[0]);
    int(*p)[10] = &arr;//p即为整个数组的地址
    for (int i = 0; i < sz; i++) {
      printf("%d ", *((*p) + i));
      printf("%d ", (*p)[i]);
      // - 对指针p进行解引用得到数组名
      //数组名没有跟着sizeof或者&故为首元素地址
      //对首元素地址进行加i进行解引用 
      // 冗余  - 不建议这样使用
      //数组指针在使用时一般在逻辑比较复杂的情况
  return 0;
}

该方法为创建一个数组指针来指向该数组,再将数组指针解引用得到数组

再利用for循环访问各个元素,过于冗余,不建议使用。


数据传参

在了解数组指针后,我们来了解了解数据传参。


一维数组传参

存在以下代码

int main() {
  int arr[10] = {0};
  int* arr2[20] = {0};
  test(arr);//传入数组首元素地址
  test2(arr2);//传入指针数组的数组首元素
  return 0;

判断以下函数接收数据参数是否匹配:

void test(int arr[]) { }
//传入的为数组,所以可以用数组进行接收
//该形参正确 ✔
void test(int arr[10]) { }
//同理上面那段代码,在以一维数组形式的形参接收数组参数时
//可以采用一维数组进行接收,且数组内元素个数可有可无
//该形参正确 ✔
void test(int* arr) { }
//数组传参时本质上传的为数组的首元素地址
//既然是地址那就说明可以用指针进行接收
//所以该函数的形参为指针 ✔
void test2(int* arr[20]) { }
//test2在传参过程中传入的为指针数组
//当使用相应的指针数组进行接收时
//可以利用相应的指针数组作为形参 ✔
void test2(int** arr) { }
//在传参过程中,传入的是一个一维指针的地址
//故可以利用二维指针的形参进行接收 **arr  - arr即为二维指针
//  ✔

可得出结论:

一维数组传参,形参可以是数组,也可以是指针

当形参为指针时,要注意类型


二维数组传参

存在以下代码

int main()
{
  int arr[3][5] = { 0 };
  test(arr);
  return 0;
}

判断以下函数接收数据参数是否匹配:

void test(int arr[3][5]) {}
//传入一个二维数组时
//可以利用二维数组作为形参来接收参数
//但是接收二维数组的形参在二维数组的规定一致
//只可忽略行,不可忽略列 ✔
void test(int arr[][]) {}
//根据上一行代码的结论
//在利用二维数组作为形参来接收二维数组的实参时
//行可省略列不可省略,所以该行代码将列也一并省略了
//故该行代码 ❌
void test(int arr[][5]) {}
//根据上两行代码可知该行代码 ✔
void test(int* arr) {}
//传入一个二维数组,说明传入的值为二维数组的数组首元素地址
//二维数组的数组首元素地址即为数组第一行的元素地址
//故需要一个数组指针进行接收
//而该行代码形参只能接收普通类型的int*指针
//不能接收整个二维数组首行元素的地址
//代码能运行是因为在运行时编译器进行了处理
//本质上是不行的,故该段代码 ❌
void test(int* arr[5]) {}
//传入的为一个二维数组,需要用数组指针进行接收
//改行代码形参为指针数组,故 ❌
void test(int(*arr)[5]) {}
//传入的为一个二维数组,需要用数组指针接收
//该行形参即为数组指针,且对应的元素个数与传入参数相同  ✔
void test(int** arr) {}
//该行形参为二阶指针
//二阶指针适合用来作为指针数组的形参
//但传入参数为二维数组,类型不匹配,故  ❌

可得出结论: 二维数组传参,函数形参的设计只能省略第一个[ ]的数字

因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。

这样才方便运算。


一级指针传参

我们已经知道了,当为一个函数传入一维数组或二维数组为参数时应该怎么接收参数,

那我们可以反着来一次,

若是函数内能以一级指针作为参数时可以传入什么参数 存在以下函数,

函数的参数为一级指针,判断可以传入什么实参:

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 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;
}

由此可推断出 当函数的参数为二级指针时,可以传入:

二级指针、一级指针的地址 指针数组 -

数组名为首元素地址,而指针数组内元素也为指针


函数指针

从以上的内容我们认识到,任何数据都有相应的地址,即也就拥有相应的指针,

既然如此, 我们可以来讨论讨论,函数是否存在地址,是否存在指针。

通过类比法可得知

整型指针 - 指向整型的地址

字符指针 - 指向字符的地址

浮点数指针 - 指向浮点数的地址

数组指针 - 指向数组的地址

函数地址 - 指向函数的地址?


在了解函数指针前,我们先来了解函数是否有对应的地址

存在下列代码,代码的功能为打印函数的地址

int Add(int x, int y) 
{
  return x + y;
}
int main()
{
  printf("%p\n", &Add);
  printf("%p\n", Add);
  //打印函数的地址
  return 0;
}

从打印结果得知,函数也存在地址

&函数名与函数名的地址相同,也由此得知,函数名与&函数名等价


既然函数也存在地址,那么函数指针应该如何表达

存在数组int arr[10] = {0}; 若需要创建数组指针p

该数组的数组指针即为

  • int(*p)[10] = &arr;
    类比数组指针,可得知:若存在函数int Add(int x,int y){;}
    且需要创建函数指针变量pp指向该函数
    函数指针即为 - int (*pp)(int , int) = &Add;
int Add(int x, int y) 
{
  return x + y;
}
int main()
{
  printf("%p\n", &Add);
  printf("%p\n", Add);
  //打印函数的地址
  int(*add)(int, int) = &Add;
  //*add说明add是一个指针
  //指针指向( )说明指向函数
  //函数参数的类型为两个int类型
  //函数的返回值也为int类型
  int(*adc)(int, int) = Add;
  int(*ad)(int, int) = &Add;
  //在创建函数指针时,&函数名与函数名为一致
  //所以函数名本质上就是函数地址
  //同时在创建函数指针时,不需要变量名,只需要类型名
  /*int ret = (*ad)(3, 5);*/
  //函数指针的调用,在创建函数指针时
  //已经交代了函数返回值的类型以及传值的类型
  //故函数调用时只需要解引用并进行传参即可
  /*int ret = ad(3, 5);*/
  int ret = (******ad)(3, 5);
  //并且函数指针在调用时,其*可省略,只是为了方便代码更加容易解读
  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;
}
int main()
{
  //函数指针数组的创建与使用
  int(*cul[])(int,int)= { Add,Sub,Mul,Div };
  //cal[10]说明cal为函数指针数组的数组名
  //每个元素的类型为int(* )(int,int)
  for (int i = 0; i < 4; i++) {
    printf("%d\n",cul[i](8, 4));
    //根据[]下标访问操作符访问每个数组元素
    //即访问每个函数的函数名
    //函数名无论有无&都等价
    //故有无解引用也一样
  }
  return 0;
}

函数指针数组的使用

分享一个函数指针数组的使用:

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) = { NULL,Add,Sub,Mul,Div };
  //创建一个函数指针数组
  //数组名为pfArr - 元素个数为5个
  //每个元素类型为int (* )(int, int)
  menu();
  do {
    printf("请选择:>");
    scanf("%d", &input);
    if (input == 0) {
      printf("退出计算器\n");
      break;
    }
    else if (input > 0 && input <= 4) {
      printf("请输入两个整形参数>");
      scanf("%d%d", &x, &y);
      printf("%d\n",pfArr[input](x, y));
    }
    else {
      printf("输入错误\n");
    }
  }
  while (input);
  return 0;
}

本章完

分享不易,感谢支持。

相关文章
|
9天前
|
C语言
指针进阶(C语言终)
指针进阶(C语言终)
|
1月前
|
机器学习/深度学习 搜索推荐 算法
【再识C进阶2(下)】详细介绍指针的进阶——利用冒泡排序算法模拟实现qsort函数,以及一下习题和指针笔试题
【再识C进阶2(下)】详细介绍指针的进阶——利用冒泡排序算法模拟实现qsort函数,以及一下习题和指针笔试题
|
9天前
|
C语言
指针进阶(回调函数)(C语言)
指针进阶(回调函数)(C语言)
|
9天前
|
存储 C语言 C++
指针进阶(函数指针)(C语言)
指针进阶(函数指针)(C语言)
|
9天前
|
编译器 C语言
指针进阶(数组指针 )(C语言)
指针进阶(数组指针 )(C语言)
|
14天前
|
搜索推荐
指针进阶(2)
指针进阶(2)
28 4
|
14天前
指针进阶(3)
指针进阶(3)
22 1
|
14天前
|
C++
指针进阶(1)
指针进阶(1)
29 1
|
7天前
|
Java 程序员 Linux
探索C语言宝库:从基础到进阶的干货知识(类型变量+条件循环+函数模块+指针+内存+文件)
探索C语言宝库:从基础到进阶的干货知识(类型变量+条件循环+函数模块+指针+内存+文件)
13 0
|
1月前
|
存储 安全 编译器
C++进阶之路:何为引用、内联函数、auto与指针空值nullptr关键字
C++进阶之路:何为引用、内联函数、auto与指针空值nullptr关键字
15 2