【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也要放在[]旁边


总结

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

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

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

相关文章
|
26天前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
45 0
|
25天前
|
C语言
【c语言】指针就该这么学(3)
本文介绍了C语言中的函数指针、typedef关键字及函数指针数组的概念与应用。首先讲解了函数指针的创建与使用,接着通过typedef简化复杂类型定义,最后探讨了函数指针数组及其在转移表中的应用,通过实例展示了如何利用这些特性实现更简洁高效的代码。
15 2
|
25天前
|
C语言
如何避免 C 语言中的野指针问题?
在C语言中,野指针是指向未知内存地址的指针,可能引发程序崩溃或数据损坏。避免野指针的方法包括:初始化指针为NULL、使用完毕后将指针置为NULL、检查指针是否为空以及合理管理动态分配的内存。
|
25天前
|
C语言
C语言:哪些情况下会出现野指针
C语言中,野指针是指指向未知地址的指针,通常由以下情况产生:1) 指针被声明但未初始化;2) 指针指向的内存已被释放或重新分配;3) 指针指向局部变量,而该变量已超出作用域。使用野指针可能导致程序崩溃或不可预测的行为。
|
1月前
|
存储 C语言
C语言32位或64位平台下指针的大小
在32位平台上,C语言中指针的大小通常为4字节;而在64位平台上,指针的大小通常为8字节。这反映了不同平台对内存地址空间的不同处理方式。
|
1月前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。
|
1月前
|
存储 C语言
C语言指针与指针变量的区别指针
指针是C语言中的重要概念,用于存储内存地址。指针变量是一种特殊的变量,用于存放其他变量的内存地址,通过指针可以间接访问和修改该变量的值。指针与指针变量的主要区别在于:指针是一个泛指的概念,而指针变量是具体的实现形式。
|
1月前
|
C语言
C语言指针(3)
C语言指针(3)
11 1
|
1月前
|
C语言
C语言指针(2)
C语言指针(2)
13 1
|
25天前
|
编译器 C语言
【c语言】指针就该这么学(2)
本文详细介绍了指针与数组的关系,包括指针访问数组、一维数组传参、二级指针、指针数组和数组指针等内容。通过具体代码示例,解释了数组名作为首元素地址的用法,以及如何使用指针数组模拟二维数组和传递二维数组。文章还强调了数组指针与指针数组的区别,并通过调试窗口展示了不同类型指针的差异。最后,总结了指针在数组操作中的重要性和应用场景。
17 0