【C语言航路】第十站:指针进阶(一)

简介: 【C语言航路】第十站:指针进阶(一)



一、字符指针

我们知道指针有一种类型叫做字符指针char*,他一般是下面这种使用的

#include<stdio.h>
int main()
{
  char ch = 'w';
  char* pc = &ch;
  *pc = 'a';
  printf("%c", ch);
  return 0;
}

当然这是一种最基本的使用方法,其实还有一种使用方法是这样的,如下图所示,看上去好像是将一个字符串直接赋值给p这个指针了?其实这段代码不是这个意思,这个代码的意思是将abcdef这个字符串的首元素地址赋值给p,让p指向这个字符串,而且是将字符串放入p这个指针也放不下去啊,因为p这个指针在32位环境下只有4个字节。而这个字符串已经八个字符了,需要八个字节,所以其实是将这个字符串的的地址赋值给p的。

注意,字符串常量是放在只读常量区的,是不能被修改的。我们之前所说的内存分为栈区,堆区,静态区只是最常用的几个分区,他还有很多更细的分区,如字符串常量就放在只读常量区。既然只读,那么我们也就便明白了,这个字符串是不可以被修改的。所以我们严格来说要加上一个const修饰,如果不加的话,在vs2022上会报警告,甚至在未来我们不小心修改的话,则直接程序崩溃

而我们加上const以后,程序直接报错误,直接编译不过去。有利于我们排查错误

而我们需要打印这个字符串的话,也就直接将字符串的地址放上去就可以打印了

#include<stdio.h>
int main()
{
  const char* p = "abcdef";
  printf("%s", p);
  return 0;
}

我们来看下面一个例子

#include <stdio.h>
int main()
{
  char str1[] = "hello world.";
  char str2[] = "hello world.";
  const char* str3 = "hello world.";
  const char* str4 = "hello world.";
  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;
}

对于这段代码,他最终输出的结果是,一和二不一样,三和四一样。那么这是为什么呢?其实这是因为hello world这个字符串,在一和二中是先开辟了这两个数组空间,这两个数组空间的地址是不一样的,然后才将这个字符串放到里面去,我们使用的是==,也就是比较一和二的地址,两个数组空间的地址当然是不一样的了,而三和四是直接将只读数据区的字符串常量的首元素地址直接赋值给这个指针,那么当然是一样的了。

二、指针数组

我们知道整型数组是一个数组,用来存放整型,字符数组是一个数组,用来存放字符,那么同理,指针数组也是一个数组,用来存放指针,每一个变量都是一个指针

比如说下面这段代码中。

#include<stdio.h>
int main()
{
  const char* arr[4] = { "abcdef","qsja","hello world","hehe" };
  int i = 0;
  for (i = 0; i < 4; i++)
  {
    printf("%s\n", arr[i]);
  }
  return 0;
}

我们定义了一个字符指针数组,他是一个数组,数组中的每一个元素都是一个指针,虽然看上去是一个字符串,但是我们这是将一些字符串放入每一个元素中的,这每一个元素都是一个指针。就相当于将一个字符串赋值给一个指针,那我们就知道,这就是是相当于将字符串的首元素的地址赋值给指针了。而这些字符串都是只读的,所以要加上const。然后直接打印即可

当然还有下面这个整型指针数组的例子

#include<stdio.h>
int main()
{
  int arr1[4] = { 0,1,2,3 };
  int arr2[4] = { 1,2,3,4 };
  int arr3[4] = { 2,3,4,5 };
  int arr4[4] = { 3,4,5,6 };
  int* arr[4] = { arr1,arr2,arr3,arr4 };
  int i = 0;
  for (i = 0; i < 4; i++)
  {
    int j = 0;
    for (j = 0; j < 4; j++)
    {
      printf("%d ", arr[i][j]);
    }
    printf("\n");
  }
  return 0;
}

这段代码就是先创建了四个整型数组,然后再创建一个整型指针数组,由于数组名就是首元素的地址,所以直接将数组名放进去可以了。此时这个指针数组里面存放的四个指针就是这四个数组的首元素的地址,然后我们进行打印,首先是arr[i]找到了这四个数组的首元素的地址,然后在通过一个下标i来继续访问后面的元素即可。

当然我们这种打印的写法还可以转化成指针形式的写法,而且还有我们之前的操作符转化的方式。可以拓展出下面几种写法

除过整型指针数组和字符指针数组以外,还有二级指针数组等等

int* arr1[10]         //整型指针数组

char* arr2[10]      //字符指针数组

char** arr3[10]     //二级字符指针数组

...............

三、数组指针

1.数组指针的定义

我们知道

字符指针 :存放字符地址的指针 ,指向字符的指针,char*

整型指针 :存放整型地址的指针 ,指向整型的指针,int*

浮点型的指针:存放浮点型地址的指针,指向浮点型的指针,float*,double*

那么我们可以推出:数组指针就是一个存放数组地址的指针,指向数组的指针

那么数组指针该如何写出来呢?应该这样写

#include<stdio.h>
int main()
{
  int arr[10] = { 0 };
  int(*pa)[10] = &arr;
  return 0;
}

这样写的话我们也不难看出,arr这个数组的类型是int [10],pa是一个指针,这颗*说明pa是一个指针,由于[]的优先级高于*,所以要将*pa给括号括起来。pa指向的arr这个数组的地址,pa的所指向的类型是int [10],pa他本身的类型就是int (*)[10],由于数组名就是在中类型的中间夹着的,导致了这个数组指针的类型中,指针名就夹在了类型的中间。

2.数组名和&数组名

我们看下面的这段代码

#include<stdio.h>
int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  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+1相差四个字节的地址,&arr[0]和&arr[0]+1的地址相差四个字节,而&arr和&arr+1却相差40个字节,这是因为,数组名代表首元素的地址。&arr是整个数组的地址,数组首元素的地址和数组的地址从值的角度来看是一样的,但是意义是不一样的,他们的步长是有区别的。数组首元素的地址,他+1后,移动一个元素的地址。而整个数组取地址后+1,移动整个数组的地址,从指针的角度来看的话,数组名代表首元素的地址,他的类型就是int*,而取地址数组名的话,他代表整个数组的地址,他的类型是int(*)[10],那么步长当然就有区别了。

关于这两个的类型,我们还要注意不要用混了,比如说把一个数组首元素的地址传给了数组指针,这样的话就是不合理的,除非进行强制类型转化,当然那样做虽然不合理,但是vs2022编译器却能通过

比如下面这样就可以进行强制类型转化。

当然还有字符数组指针的声明,我们也可以类比的写出来

3.数组指针的使用

数组指针是如何使用的呢?我们知道如何使用数组的首元素地址去打印整个数组,那么能不能用整个数组的地址去打印整个数组呢?我们可以这样做

#include<stdio.h>
int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  int(*pa)[10] = &arr;
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    printf("%d ", (*pa)[i]);
  }
  return 0;
}

当然我们这样显得有点大材小用了,其实数组指针一般不是用于一维数组的,而是用在二维数组中的。我们看下面的代码

这是一种二维数组传参的方式,目的是打印这个二维数组,这个我们都很容易能理解的。但是我们要思考一下,如果是一维数组传参的话,可以写一个数组的模样,也可以写一个指针,因为数组传参传的是首元素的地址。那么二维数组传参可以使用一个指针来接收吗

当然是可以的,其实二维数组传参传的也是首元素的地址,只不过这个首元素有一点不同,我们之前说过二维数组的首元素是第一行一维数组。所以我们传参传的直接就是一行一维数组的地址,要接收这个一维的数组的地址,我们就只能使用一个数组指针来进行接受。于是我们的代码可以写成如下所示

根据这段代码,我们也瞬间理解了,为什么二维数组必须要有列,但是行是不必要的。这是因为我们二维数组名可以看成一个个指向一行数组的指针。先通过一次解引用,访问到这一行数组,然后再通过一个解引用,就可以很自然的访问到每一个元素了

#include<stdio.h>
void print1(int arr[3][4], 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 print2(int(*arr)[4], 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("%d ",arr[i][j]);
    }
    printf("\n");
  }
}
int main()
{
  int arr[3][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };
  //print1(arr, 3, 4);
  print2(arr, 3, 4);
  return 0;
}

有了上面的理解,我们来看一下下面这几个代码的意思

int arr[5];

int *parr1[10];

int (*parr2)[10];

int (*parr3[10])[5];

解析

int arr[5];                //一个整型数组,数组有五个元素,每个元素都是int类型

int *parr1[10];        //一个指针数组,数组有十个元素,每个元素都是int*类型

int (*parr2)[10];       //这是一个数组指针,这个指针指向一个有十个元素的,每个元素都是int类型的数组

int (*parr3[10])[5];//首先parr3先跟[]结合,所以他是一个数组,这个数组有十个元素,每个元素的类型我们只需要将parr3[10]抽出来就知道了,所以他的每个元素的类型是int (*)[5],也就是每个元素都是一个数组指针,而这每一个数组指针都指向一个数组,这个数组是由五个元素的,每个元素都是int类型的

四、数组参数、指针参数

1.一维数组传参

我们看如下所示的一些代码

首先这是一些伪代码,但是我们要能够知道,上面这些传参是否都是合理的呢?

一维数组传参,要么直接照着数组本来的样子直接模仿着写过去,要么就要写成指针的方式,就这两种方式

显然第一种,第二种属于直接模仿这数组的模样写过去的,所以当然是没有任何问题的,第三种是写成了指针的方式,数组名就是首元素地址,所以当然也是没有任何问题的。

第四种,他是传一个指针数组,他直接写了模仿者写了一个数组,当然是没有任何问题的,而且这个数组甚至可以不用写具体的个数

第五种,他是使用了一个二级指针,二级指针的意思是,一颗*代表着arr是一个指针,int*代表着,arr指向一个整形指针。而我们传参的时候,arr2就是一个指针数组,数组的每一个元素都是一个指针,我们传参传的是数组名,数组名就是首元素的地址,也就是相当于我们将一个指针的地址传了过去,所以当然是用一个二级指针来接受了,如果用一级指针来接受,那就报错了

2.二维数组传参

我们来分析下面的代码

显然这也是一段伪代码,但是我们是可以进行分析的。

第一种方式直接模仿数组的样子写出来的形参,是没有任何问题的

第二种方式模仿但没模仿完,因为他缺少了行标和列标,我们知道,行标可以省略,列标是万万不可以省略的。我们也可以从指针的角度进行分析,因为数组名就是首元素地址,二维数组的首元素地址就是第一个一维数组的地址,那么如果缺少了列标,那就相当于不知道首元素地址的类型了,这就出现问题了

第三种方式没有省略列标,这是正确的

第四种方式、第五种方式、第六种方式、第七种方式中,只有第六种方式是正确的,因为二维数组传参传的是一个一维数组的地址,要使用数组指针来接收,只有第六种是数组指针类型的,其他的指针类型都不匹配,所以只有第六个正确。

3.一级指针传参

我们来看下面这个代码

这个方式是当然可行的,实际上一级指针传参也只能放一个一级指针的形参过去。否则类型也不匹配

那么我们思考一下形参是一级指针,那么实参可以是什么呢?

其实形参是一级指针情况下

比如说int* p是形参

那么我们可以传一个整型变量的地址

比如

int a;

test(&a);

我们也可以传一个一级指针过去

int* p=&a;

test(p);

我们也可以传一个数组名过去

int arr[5];

test(arr);

总的来说就三种方式:变量的地址,一级指针,数组名

4.二级指针传参

我们来看下面的代码

这段代吗就是将一个二级指针传过去的,那么我们想要接收一个二级指针,只能使用一个二级指针来接收

那么我们反过来思考一下:如果形参是二级指针,那么实参可以有哪些呢?

实际上,如果形参是二级指针

那么实参可以是一级指针的地址,也可以是二级指针,也可以是一个指针数组

五、函数指针

我们已经知道了数组指针、就是指向整个数组的地址

那么函数指针呢,其实就是指向函数的指针,我们可以类比的写出他的类型

#include<stdio.h>
int Add(int x, int y)
{
  return x + y;
}
int main()
{
  int (*pA)(int, int) = &Add;
  return 0;
}

因为函数也是我们自定义出来的类型,而决定一个函数的主要因素就是返回类型,参数类型,和函数名。去掉函数名,就是函数的类型,然后使用一颗*代表着这个变量是一个指针。而这就是一个函数指针,他指向一个函数,他指向的函数的类型就是去掉他后的类型。这与数组指针是极其类似的。

但是也与数组指针不同的地方,数组名是数组首元素的地址,那么函数名是否又代表了某种含义呢?其实是的,函数名和&函数名都是函数的地址,这是函数与数组类型的一个区别。所以我们也可以这样定义函数指针,这两种方式都是可行的

那么我们知道了函数指针的定义,该如何调用它呢?其实也是一样的道理,先解引用它,然后直接输入参数即可

其实,函数指针调用中的那颗*是一个摆设,我们可以不用写它的,也可以写很多个,都是没有任何问题的

这一点也是函数指针与其他指针的一个区别,还有一个区别是函数指针是无法进行加减运算的,因为也没有任何意义

我们最后来看两个代码,分析他们

#include<stdio.h>
int main()
{
  //代码1
  (*(void (*)())0)();
  //代码2
  void (*signal(int, void(*)(int)))(int);
  return 0;
}

首先是代码1:

这段代码可以一看的话,就懵逼了,其实大可不必,这段代码的意思是是将0强制类型转化为void (*)()类型的函数指针,然后再去调用0地址处的这个函数

我们在来分析代码2:

这是一次函数声明

声明的函数名字是signal

signal有两个参数,一个是int,一个是函数指针类型:void(*)(int),

signal的返回类型是也是一个函数指针:类型是void(*)(int)

当然这样我们会发现还是比较难以理解,我们可以使用一个typedef来简化代码

如下图所示:

要注意的这个重定义类型可能看起来比较奇怪,这是对指针的typedef时要放在*旁边

同样的,对于数组使用typedef也要放在[]旁边


总结

本小节讲解了字符指针、指针数组、数组指针、函数指针,以及数组和指针传参的问题

如果对你有帮助,不要忘记点赞加收藏哦!!!

想获得更多优质内容,一定要关注我哦!!!

相关文章
|
1天前
|
C语言
c语言指针总结
c语言指针总结
15 1
|
1天前
|
C语言
C语言(指针详解)重点笔记:指针易错点,都是精华
C语言(指针详解)重点笔记:指针易错点,都是精华
3 0
|
1天前
|
存储 C语言
C语言指针讲解(适用于初学者)
C语言指针讲解(适用于初学者)
6 0
|
1天前
|
搜索推荐 C语言
详解指针进阶2
详解指针进阶2
|
1天前
|
存储 程序员 C语言
【C 言专栏】C 语言指针的深度解析
【4月更文挑战第30天】C 语言中的指针是程序设计的关键,它如同一把钥匙,提供直接内存操作的途径。指针是存储其他变量地址的变量,通过声明如`int *ptr`来使用。它们在动态内存分配、函数参数传递及数组操作中发挥重要作用。然而,误用指针可能导致错误,如空指针引用和内存泄漏。理解指针的运算、与数组和函数的关系,以及在结构体中的应用,是成为熟练 C 语言程序员的必经之路。虽然挑战重重,但掌握指针将增强编程效率和灵活性。不断实践和学习,我们将驾驭指针,探索更广阔的编程世界。
|
1天前
|
存储 C语言
C语言进阶---------作业复习
C语言进阶---------作业复习
|
1天前
|
存储 Linux C语言
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)-2
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)
|
1天前
|
自然语言处理 Linux 编译器
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)-1
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)
|
1天前
|
存储 编译器 C语言
C语言进阶第十课 --------文件的操作-1
C语言进阶第十课 --------文件的操作

热门文章

最新文章