C生万物 | 指针进阶 · 提升篇-2

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: C生万物 | 指针进阶 · 提升篇

【数组指针】

讲完指针数组后,我们就来讲讲它的双胞胎兄弟 —— 【数组指针】

💬首先还是这个问题,数组指针是指针?还是数组?

1、数组指针的定义

  • 我们通过指针初阶中所学习的整型指针和字符指针来做一个对比


int a = 10;
char ch = 'x';
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* pa = &a;     —— 整型指针 - 存放整型地址的指针
char* pc = &ch;     —— 字符指针 - 存放字符地址的指针
int(*parr)[10] = &arr;  —— 数组指针 - 存放数组地址的指针
  • 也是一样来分析一下这三个指针
  • 对于pa,它是一个整型指针,里面存放的是一个整型的地址
  • 对于pc,它是一个字符型指针,里面存放的是一个字符的地址
  • 对于parr,它是一个数组指针,里面存放的是一个数组的地址
  • 通过这么的对比相信你对【数组指针】有了一初步的概念,它也是一个指针,它所指向的是一个数组的地址

然后就来仔细介绍一下数组指针

  • 下面有一个arr数组,数组里面有5个元素,每个元素都是一个int类型。那现在我要将这个数组的地址存起来,那肯定需要一个指针来接收,那既然是一个指针的话我们肯定会想要用*做修饰,不过这还不够,因为接收的是一个数组的地址,所以我们还会想要再加上[10],而且这个10还不能像我们定义数组的可以省略调用,一定要加上
  • 但是像下面这样真的可以吗?或许你应该去了解一下运算符优先级,因为[]的优先级是最高的,所以这个【pa】会首先和[]结合,而不是先和*,那么它就是一个数组,而不是指针了!


int arr[5] = { 1,2,3,4,5 };
int* pa[10] = &arr;
  • 若是想要【pa】和这个*先结合的话,在它们的外面加上一个()即可,如下所示👇


int (*pa)[10] = &arr;

==这才是一个完整又正确的【数组指针】==

2、&数组名VS数组名

对于数组名是首元素地址这个说法我们已经是耳熟于心了,不过上面看到了一个新的写法&数组名,这和数组名存在着什么关联呢?本模块我们就来探讨一下这个

  • 可以看到,在下面我分别打印了三种情形,那可以预测第一种和第二种是一样的,而第三种可能就不一样


int arr[5] = { 1,2,3,4,5 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
printf("%p\n", &arr);

但是从运行结果可以看到它们都是一样的,这是为什么呢?

image.png

  • 数组章节我就有讲到过&数组名值得是取出整个数组的地址,而&arr[0]则是数组首元素的地址。不过从下图可以看,它们的位置是一样的,所以打印出来的地址就是一样的

image.png💬那有同学说:难道它们就完全相同吗,那&数组名还有什么意义呢?

  • 但此时我将当前取到的地址再去 + 1的话,会有什么变化呢?


printf("%p\n", arr);
printf("%p\n", arr + 1);
puts("---------------");
printf("%p\n", &arr[0]);
printf("%p\n", &arr[0] + 1);
puts("---------------");
printf("%p\n", &arr);
printf("%p\n", &arr + 1);
puts("---------------");

可以看到,最后一个&数组名和上面两个的结果不同

image.png

  • 对于arr&arr[0]都一样,取到的是首元素的地址,这是一个整型数组,首元素是一个int类型的数据,那么其地址就是int*类型,那在【指针初阶部分】我有讲到过一个int*的指针一次可以访问4个字节的大小,那在这数组中每个元素都占4个字节,所以 + 1就会跳过一个元素也就是4个字节
  • 对于&arr来说,取出的是整个数组的大小,虽然它的位置和首元素地址是一样的,但是它 + 1跳过的确是整个数组的大小,上面说到过一个数组的地址给到【数组指针】来接收int (*parr)[5] = &arr;,此时去掉它的变量名后这个指针的类型就是int(*)[10],上面我们也有讲过一个指针一次可以访问的字节取决于它的类型

具体可以看看这张图👇

image.png💬在知晓了这一点后许多同学就明白了这个地址的偏移为何是这样,但是仔细一算好像也不对呀,整个数组所占的字节数不是20吗,这里是14呀?

  • 要知道,编译器对于一块地址的表示形式是以十六进制的形式,所以我们计算出的差值应该再转换为十进制才对,那么14转换为十进制后刚好就是20,不清楚规则的同学可以去了解一下十六进制转十进制

3、数组指针的使用【⭐】

讲了这么多后,这个数组指针到底有什么用呢?

1.数组指针在一维数组的使用场景

  • 之前我们在使用函数封装一个打印数组时有着下面两种写法,一个就是使用数组做接收,一个则是使用指针做接收。因为外界所传入的都是数组名,数组名就是首元素地址


void print1(int arr[], int n)
{
  int i = 0;
  for (i = 0; i < n; ++i)
  {
    printf("%d ", arr[i]);
  }
  printf("\n");
}
void print2(int* arr, int n)
{
  int i = 0;
  for (i = 0; i < n; ++i)
  {
    printf("%d ", arr[i]);
  }
  printf("\n");
}
print1(a, sz);
print2(a, sz);
  • 那在学习了【数组指针】后,我们还可以把形参写成下面这种样子


void print3(int (*p)[5], int n)
{
  int i = 0;
  for (i = 0; i < n; ++i)
  {
    printf("%d ", (*p)[i]); //a[i]
  }
}
  • 实参就要以下面这种形式进行传递,那此时形参p接收到的就是整个数组的地址,那么此时*p也就取到了这个一维数组的数组名,那我们平常用数组名来访问数组中的每个元素时,都是用的arr[i]这样的形式,那么用解引用后的数组指针来访问就可以写成(*p)[i]


print3(&a, sz);

💬但这样不是很别扭吗?传进来数组的地址,然后再解引用获取到数组名,还不如直接传递数组名呢🤨

  • 是的,一般数组指针我们不会用在一维数组的情况下,但是我们一般直接会用数组名或者指针来接收。但数组指针在二维数组中使用的还是比较的多的

2.数组指针在二维数组的使用场景

  • 下面是我们之前在使用函数封装二维数组打印的时候所需要的传参


void print4(int arr[3][5], int row, int col)
{
  int i = 0;
  for (i = 0; i < row; i++)
  {
    int j = 0;
    for (j = 0; j < col; j++)
    {
      printf("%d ", arr[i][j]);
    }
    printf("\n");
  }
}
int a[3][5] = { {1,2,3,4,5}, {2,3,4,5,6}, {3,4,5,6,7} };
print4(a, 3, 5);
  • 那采用【数组指针】的写法也是像上面这样,但是有同学却疑惑说:传进来的不是一个二维数组吗?


void print5(int (*p)[5], int row, int col)
{
  int i = 0;
  for (i = 0; i < row; i++)
  {
    int j = 0;
    for (j = 0; j < col; j++)
    {
      printf("%d ", *(*(p + i) + j));
    }
    printf("\n");
  }
}
  • 这一块的话我就来重点分析一下了:首先你要知道知道对于一维数组而言,它的首元素地址即为数组中第一个元素的地址,那么二维数组的首元素地址相当于什么呢?如果你仔细看过数组章节的话就可以知道为第一行的地址,此时形参p接收到的即为第一行的地址。对于二维数组把每一行看做是一个元素,那么对于这个数组来说三行就有三个元素,那么要如何访问到每一行呢?那就是使用p + i,随着【i】的不断变化就可以取到每一行的地址
  • 但是我们要访问的是二维数组中的每一个元素,那取到这一行的地址后还不够,因为我们访问数组中元素时使用的都是数组名,此时*(p + i)也就拿到了当前的这一行的数组名,假设现在要访问第一行,那它的数组名那就是a[0],或者是*(a + 0),以此类推后面的几行数组名就是a[1]、a[2]。那数组名我们知道,意味着首元素地址,现在先访问第一行中的每个元素,那么首先拿到的就是【1】的地址,那要访问到后面的每一个元素首先要对地址进行一个偏移,*(p + i) + j就可以拿到每个元素的地址,那此时就简单了,再解引用*(*(p + i) + j)也就取到了当前行中的每个元素,根据数组名和指针的转换规则,即为p[i][j]

image.png来看一下运行结果

image.png


在学习了【指针数组】和【数组指针】后,来看一下这四个指针 or 数组?


int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
  1. 第一个【arr】首先和[]结合,表明它是是一个数组,数组有五个元素,每个元素都是int类型的,说明这是一个一维数组
  2. 第二个【parr】首先和[]结合,表明它是一个数组,数组的每个元素都是一个int类型的指针,说明这是一个指针数组
  3. 第三个【parr2】首先和*结合,表明它是一个指针,然后往后一看,它指向一个数组,该数组有10个元素,每个元素都是int类型,说明这是一个数组指针
  4. 第四个【parr3】首先和[]结合,表明它是一个数组,数组有十个元素,把parr3[10]去掉后就可以看出它的类型,是int(*)[5],说明数组中存放着的都是数组指针,每个数组指针都指向一个存有5个元素,每个元素都是int类型的数组。最后我们判定其为==数组指针数组==

第四个的图示如下:image.png

【数组传参与指针传参】

相信有很多同学对于数组传参、指针传参都是搞的稀里糊涂的

1、 一维数组传参

代码:


/*一维数组传参*/
void test(int arr[]) //ok?
{}
void test(int arr[10]) //ok?
{}
void test(int* arr) //ok?
{}
int main()
{
  int arr[10] = { 0 };
  test(arr);
}

解析:

  • 首先来看一维数组的传参,test传进来一个arr数组名,那第一个利用arr[]接收这是我们最常见的,没有问题✔
  • 第二个和第一个类似,只是在[]里加上了一个10,不过我们知道对于一维数组里面的数组大小声明是可以省略的,所以没有关系
  • 第三个是采用*arr的方式进行接收,那传递进来的arr为数组名,数组名是首元素地址,那给到一个指针作为接收也没什么问题

代码:


void test2(int* arr[20]) //ok?
{}
void test2(int** arr) //ok?
{}
int main()
{
  int* arr2[20] = { 0 };
  test2(arr2);
}

解析:

  • 接下去看到我向test2传递了一个指针数组,那使用* arr[20]合情合理 ✔
  • 那么第二个** arr是都可以呢?这点我们可以通过画图来分析,因为arr2是一个指针数组,而且里面存放的每个元素都是int类型的, 那我们传递【指针数组】的数组名过去的话,那其实就是首元素地址,即这个一级指针int*的地址,那么形参部分使用二级指针来接收也是正确的 ✔

image.png

总结:

最后总结一下一维数组传参形参可以是哪些内容

  1. 形参可以是数组
  2. 形参可以是指针
  3. 形参可以是一个二级指针,指针数组的地址可以给到二级指针做接收,==因为指针数组里面存放的都是一级指针==

2、 二维数组传参

代码:


/*二维数组传参*/
void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
int main()
{
  int arr[3][5] = { 0 };
  test(arr);
}

解析:

  • 接下去我们再来看看二维数组的传参,第一个无需多说。第二个的话形参这种写法是不可以的,因为二维数组必须确定它的列,也就是每行有多少个元素,但是有多少行可以不用知道❌

image.png

  • 那对于第三个来说就是正确的,虽然省略了第一个[]的数组,但是指明了列的个数,就没有关系 ✔

代码:


void test2(int* arr)//ok?
{}
void test2(int* arr[5])//ok?
{}
void test2(int(*arr)[5])//ok?
{}
void test2(int** arr)//ok?
{}
int main()
{
  int arr[3][5] = { 0 };
  test2(arr);
}

解析:

  • 上面的代码是采取形参部分指针进行接收,上面我们有分析到,二维数组的数组名是首行的地址,那可以使用一个一级指针来接收吗?很显然是不可以的❌
  • 第二个int* arr[5]可以吗?首先你要分析看它是个什么,我们传递过来的是一个地址,那地址就要使用指针来进行接收,但是可以看到这很明显是一个指针数组,因为arr和[]先结合了,所以也是错误的❌
  • 那么第三个呢?通过观察可以判断出它是一个数组指针, 接收一个二维数组第一行的地址,那肯定是不会有问题的 ✔
  • 最后是一个二级指针,但是二级指针只能接收一个一级指针的地址,不过我们传递过来的是一个二维数组中某一行的地址,根本牛头不对马嘴❌

总结:

最后总结一下二维数组传参形参可以是哪些内容

  1. 直接用二维数组做接收
  2. 二维数组的数组名是首行的地址,是一个一维数组的地址,要使用数组指针来接收

3、 一级指针传参

代码:


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

解析:

  • 接下去我们来看看一级指针的传参,那其实这很明确,在main函数中指针指向arr数组的首元素地址,传递过去后形参部分的p也指向这个地址,那么通过解引用就访问到了数组中的每一个元素

思考:

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

  1. 可以直接是一个变量的地址
  2. 可以是一级指针
  3. 一维数组的数组名(数组名是首元素地址,数组中的每一个元素都是一个变量)

4、 二级指针传参

代码:


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

解析:

  • 接下去我们来看看一级指针的传参,那其实这很明确,在main函数中指针指向arr数组的首元素地址,传递过去后形参部分的p也指向这个地址,那么通过解引用就访问到了数组中的每一个元素

思考:

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

  1. 可以直接是一个一级指针的地址
  2. 可以是二级指针
  3. 指针数组的数组名(数组名是首元素地址,数组中的每一个元素都是一个一级指针)

四、指针函数与函数指针

【指针函数】

1、定义

指针函数,简单的来说,就是一个返回指针的函数,其本质是一个函数,而该函数的返回值是一个指针

格式】:返回类型* 函数名(参数表)

  • 指针函数还是很好理解的,通过基本的函数来做个对比


int func(int x, int y)


int* func(int x, int y)
  • 很清楚地可以看出,【指针函数】就是普通的一个函数,只是它的返回值类型为一个指针罢了

2、示例

下面展示一个指针函数的相关案例

  • Open()函数从外界接收一个值,用于在函数内部开辟出一块大小为n的空间,然后return返回,返回类型为int*,此时外界使用int*来进行接收,就获取到了函数内部开辟出这个数组的首元素地址,然后通过循环为数组中n个元素初始化
  • 这里无需担心在函数内部开辟的这块空间的地址,因为它存放在堆上,而不是在栈上,所以不会随着函数栈帧的销毁而消亡,所以这里在举例的时候我专门去堆上面申请空间然后返回,若是返回函数中局部变量的地址,就会有很大的风险!


int* Open(int n)
{
  int* a = (int*)malloc(sizeof(int) * n);
  if (NULL == a)
  {
    perror("fail malloc");
    exit(-1);
  }
  return a;
}
int main(void)
{
  int n = 10;
  int* arr = Open(n);
  memset(arr, 0, sizeof(int) * n);
  for (int i = 0; i < n; ++i)
  {
    *(arr + i) = i + 1;
  }
  printf("Initialized Successfully\n");
  return 0;
}

通过运行结果可以看出确实可以起到初始化数组的效果

image.png

【函数指针】

讲完指针函数,我们也来说说它的双胞胎兄弟 —— 函数指针

1、概念理清

经过上面所讲的字符指针、数组指针,相信你马上就能类比出函数指针:没错,它就是一个指针,所指向的就是一个函数

  • 在【数组指针】中我有讲到过数组名&数组名的区别,虽然它们都指向数组的首元素地址,但是在它们往后偏移时,访问的字节数却不同;既然一个数组可以取出它的地址,那么函数是否可以取出它的地址呢?一起来看看

image.png

  • 从打印结果可以看出无论是函数名还是&函数名,它们的地址都是相同的,这是为什么呢?这就是语法规定的,一个函数名取不取地址都是这个函数的地址,因为对于函数来说也没有什么首函数的地址,是吧

对于数组的地址,我们可以用数组指针保存起来,那函数可以吗?当然可以,使用到的就是【函数指针】

  • 那我现在想问,下面那种形式可以将函数的地址存放起来呢


//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();

💡答案揭晓,就是第二个,解析如下

  • 回忆我们数组指针的写法,为了不让指针变量和[]先结合,所以在*和指针变量外加了一(),其实对于函数指针也是一样的, 若是不加这个括号的话,就会变成* pf(),pf就会优先和后面的()结合,那么这会被编译器当成是一个函数的声明
  • 加上括号后,(*pf)就会是一个指针,向外一看有个(),说明它指向一个函数,这个函数的参数就是Add形参部分两个参数的类型
  • 最后是它的返回类型,也就是这个函数的返回类型int

所以Add函数的函数指针应该写成下面这种形式


int (*pf)(int, int) = &Add;

2、如何调用函数指针?

清楚了函数该如何去声明后,那既然有了这个指针,而且它指向一个函数,是否可以通过这个指针去调用这个函数呢?

  • 调用函数肯定得传参,那我们为刚才声明的形参部分传入两个参数试试,然后再拿返回值接收一下
  • 可以看到确实可以调用Add函数进行求和计算

image.png

  • 不过这个编译器到底是怎么根据这个函数指针来判断去调用的Add函数,我们来对比一下


int ret = (*pf)(3, 4);
printf("ret = %d\n", ret);
int ret2 = Add(1, 2);
printf("ret = %d\n", ret2);

通过调试来观察可以发现,编译器很智能,确实是通过函数指针的指向去找到函数的地址

也可以通过汇编来看,很清晰地看出它们都去call了这个函数的地址

image.png


  • 上面说到无论是函数名还是&函数名,它们所取到的地址都是一样的,所以我们可以将函数指针的声明写成下面这种形式,读者可以自己去试一下,效果也是一样的


int (*pf)(int, int) = Add;
  • 那观察上面这样的声明形式,把指针变量单独抽离出来其实就是把Add赋给了pf,然后调用的时候在前面加上一个*作为解引用,取到这个函数,那其实Add和pf就是一样的,所以我们可以像pf(1, 2)这样去调用函数,具体如下


//int ret = (*pf)(3, 4);
int ret = pf(3, 4);
int ret2 = Add(1, 2);

通过运行可以发现效果也是一样的,所以前面的*其实是可以省略的,甚至你多加几个像(****pf)(3, 4)都是可以的

image.png

相关文章
|
6月前
|
C语言
指针进阶(C语言终)
指针进阶(C语言终)
|
6月前
|
C语言
指针进阶(回调函数)(C语言)
指针进阶(回调函数)(C语言)
|
6月前
|
存储 C语言 C++
指针进阶(函数指针)(C语言)
指针进阶(函数指针)(C语言)
|
6月前
|
编译器 C语言
指针进阶(数组指针 )(C语言)
指针进阶(数组指针 )(C语言)
|
6月前
|
搜索推荐
指针进阶(2)
指针进阶(2)
55 4
|
6月前
指针进阶(3)
指针进阶(3)
48 1
|
6月前
|
C++
指针进阶(1)
指针进阶(1)
49 1
|
6月前
|
存储 安全 编译器
C++进阶之路:何为引用、内联函数、auto与指针空值nullptr关键字
C++进阶之路:何为引用、内联函数、auto与指针空值nullptr关键字
53 2
|
6月前
|
Java 程序员 Linux
探索C语言宝库:从基础到进阶的干货知识(类型变量+条件循环+函数模块+指针+内存+文件)
探索C语言宝库:从基础到进阶的干货知识(类型变量+条件循环+函数模块+指针+内存+文件)
58 0
|
6月前
|
存储 安全 编译器
C++进阶之路:探索访问限定符、封装与this指针的奥秘(类与对象_上篇)
C++进阶之路:探索访问限定符、封装与this指针的奥秘(类与对象_上篇)
52 0