社区编辑申请
注册/登录
聊聊C语言中使用频率较高的指针与数组
开发 前端
指针和数组在C语言中使用频率是很高的,在极个别情况下,数组和指针是“通用的”,比如数组名表示这个数组第一个数据的指针。

定义

指针:C语言中某种数据类型的数据存储的内存地址,例如:指向各种整型的指针或者指向某个结构体的指针。

数组:若干个相同C语言数据类型的元素在连续内存中储存的一种形态。

数组在编译时就已经被确定下来,而指针直到运行时才能被真正的确定到底指向何方。所以数组的这些身份(内存)一旦确定下来就不能轻易的改变了,它们(内存)会伴随数组一生。

而指针则有很多的选择,在其一生他可以选择不同的生活方式,比如一个字符指针可以指向单个字符同时也可代表多个字符等。

指针和数组在C语言中使用频率是很高的,在极个别情况下,数组和指针是“通用的”,比如数组名表示这个数组第一个数据的指针。

如下代码:

#include <stdio.h>
char array[4] = {1, 2, 3, 4};
int main(void)
{
char * p;
int i = 0;
p = array;
for (; i < 4; i++)
{
printf("*array = %d\n", *p++);
}
return (0);
}

这里我们将数组名array作为数组第一个数据的指针赋值给p。但是不能写成*array++。准确来说数组名可以作为右值,不能作为左值(左值和右值的概念这里不再展开讲解)。

数组名的值其实是一个指针常量,这样我想你就明白了数组名为什么不能做为左值了。如果想用指针p访问array的下面2的数据,以下写法是合法的。


char data;
/*第一种写法*/
p = array;
data = p[2];
/*第二种写法*/
p = array;
data = *(p+2);
/*第三种写法*/
p = array +

指针与二维数组

先说一下二维数组,二维数组在概念上是二维的,有行和列,但在内存中所有的数组元素都是连续排列的,它们之间没有“缝隙”。以下面的二维数组 a 为例:

int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };

从概念上理解,a 的分布像一个矩阵:

  • 0 1 2 3。
  • 4 5 6 7。
  • 8 9 10 11。

但是内存是连续的,没有这样的“矩阵内存”,所以二维数组a分布是连续的一块内存。

C语言允许把一个二维数组分解成多个一维数组来处理。对于数组 a,它可以分解成三个一维数组,即 a[0]、a[1]、a[2]。每一个一维数组又包含了 4 个元素,例如 a[0] 包含 a[0][0]、a[0][1]、a[0][2]、a[0][3]。那么定义如下指针如何理解呢?

int (*p)[4];

括号中的*表明 p 是一个指针,它指向一个数组,数组的类型为int [4],这正是 a 所包含的每个一维数组的类型。那么和下面定义有什么区别呢?

int *p[4];

这里就要先说明*和[]的优先级了,[]的优先级是高于*的,所以int *p[4];等同于int *(p[4]);。所以它是一个指针数组。这里很绕,总接下:

int (*p)[4];是数组指针,它指向二维数组中每个一维数组的类型,它指向的是一个数组。

int *p[4];是指针数组,它是一个数组,数组中每个数是指向int型的指针。

指针数组与数组指针

对于指针数组,说的已经很明确了,不再详细讲解,下面说一下数组指针。举例看一下:

#include <stdio.h>
int main()
{
int a[3][4] = {{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}};
int(*p)[4];
p = a;
printf("%d\n", sizeof(*(p + 1)));
return (0);
}

对于数组指针p如下:

那么printf("%d\n", sizeof(*(p + 1)));的结果就是16。如果想打印a[1][0]的值,代码如下:

printf("%d\n", *(*(p + 1)));

如果想打印a[1][1]的值,代码如下:

printf("%d\n", *(*(p + 1)+1));

这个代价自行体会,p是数组指针,它指向的是一个数组,所以对获取它指向的值,也就是*p,是指向一个数组还是一个值,指向a[0]。获取获取a[0][0],就需要写成**p。

对指针进行加法(减法)运算时,它前进(后退)的步长与它指向的数据类型有关,p 指向的数据类型是int [4],那么p+1就前进 4×4 = 16 个字节,p-1就后退 16 个字节,这正好是数组 a 所包含的每个一维数组的长度。也就是说,p+1会使得指针指向二维数组的下一行,p-1会使得指针指向数组的上一行。

最后再次捋一下数组指针和指针数组。

int *p1[4];是指针数组。

int (*p2)[4];是数组指针。

“[]”的优先级比“*”要高。

对于指针数组,p1先和“[]”结合,构成一个数组的定义,数组名为p1,int *修饰的是数组的内容,即数组的每个元素。那么它本质是一个数组,这个数组里有4个指向int类型数据的指针。

对于数组指针,“()”的优先级比“[]”高,“*”号和p2 构成一个指针的定义,指针变量名为p2,int 修饰的是数组的内容,即数组的每个元素。数组在这里并没有名字,是个匿名数组。那么它本质是一个指针,它指向一个包含4个int 类型数据的数组。

既然深入谈了指针数组和数组指针,就多聊一下。

#include <stdio.h>
int main()
{
char a[5] = {'A', 'B', 'C', 'D'};
char(*p3)[5] = &a;
char(*p4)[5] = a;
return 0;
}

上面代码是编译编译是报了waring的,报警如下:

注意:不同的编译器编译结果可能不同,我的编译方法请参考《使用vscode编译C语言》。p3 这个定义的“=”号两边的数据类型完全一致,而p4 这个定义的“=”号两边的数据类型就不一致了。左边的类型是指向整个数组的指针,右边的数据类型是指向单个字符的指针。所以才有了上面的警告。

但由于&a 和a 的值一样,而变量作为右值时编译器只是取变量的值,所以运行并没有什么问题。不过编译器仍然警告你别这么用。

再举一个栗子:

int vector[10];
int matrix[3][10];
int *vp,*vm;
vp = vector;
vm = matrix;

上面的代码第5行是错误的,因为vm是指向整型的指针,但是matrix不是指向正向的指针,他是指向整型数组的指针。下面是正确的写法:

int matrix[3][10];
int (*vm)[10];
vm = matrix;

数组指针的应用

上面说了那么多,可能大部分开发者用不到,数组指针在很多时候都是可以代替二维数组的,有些程序员喜欢用指针数组来代替多维数组,一个常见的用法就是处理字符串。

#include <stdio.h>
char *Names[] =
{
"Bill",
"Sam",
"Jim",
"Paul",
"Charles",
0};
void main()
{
char **nm = Names;
while (*nm != 0)
printf("%s \n", *nm++);
}

具体运行我就不讲解了,运行结果如下:

注意数组中的最后一个元素被初始化为0,while循环以次来判断是否到了数组末尾。具有零值的指针常常被用做循环数组的终止符。

这种零值的指针称为为空指针(NULL)。采用空指针作为终止符,在增删元素时就不必改动遍历数组的代码,因为此时数组仍然以空指针作为结束。

操作

写到这里想到一个“操作”,先看下面代码是否正确。

p[-1]=0;

初看这句代码,觉得奇怪,甚至觉得它就是错误,日常C语言开发基本有见到小标是负数的,但是仔细想想没有哪一本书说过下标能为负数的。看下面代码:

void main()
{
int data[4] = {0, 1, 2, 3};
int *p;
p = data +2;
printf("p[-1] is %d\n",p[-1]);
printf("*(p-1) is %d\n",*(p-1));
}

运行结果如下:

不仅可以编译通过,还能正确的输出结果为1。这表明,C的下标引用和间接访问表达式是一样的。当然不鼓励这种操作,代码需要很强的可读性,而不是这样的操作,这里只是演示下标引用和简介表达式的关系。

责任编辑:姜华 来源: 知晓编程
相关推荐

2022-06-29 09:19:09

静态代码C语言c代码

2022-06-14 23:34:10

Linux安全服务器

2022-05-27 10:00:06

C++游戏引擎

2022-06-01 17:47:24

运维监控系统

2022-06-10 07:45:09

CentOS国产操作系统

2022-06-30 11:03:27

DDoS攻击WAF

2022-06-27 09:54:38

编程语言JavaC++

2022-06-30 09:07:52

2022-06-29 10:16:25

数据库SQL

2022-06-17 09:47:04

Linux命令

2022-06-13 07:36:06

2022-06-23 11:42:22

MySQL数据库

2022-05-23 11:03:53

云原生技术DockerIstio

2022-06-27 23:44:37

云原生云存储云计算

2022-06-21 09:26:21

Shell脚本JavaScript

2022-06-10 17:37:37

数据库

2022-06-07 09:59:21

网络安全安全漏洞

2022-04-20 20:28:40

HDF 驱动框架鸿蒙操作系统

2022-06-09 18:04:46

网络攻击网络安全

2022-05-16 07:35:21

Windows远程桌面远程服务器

同话题下的热门内容

哪个版本的JVM最快?无代码软件发展简史及未来趋势携程基于 GraphQL 的前端 BFF 服务开发实践为什么会存在 1px 问题?怎么解决?一文搞定常考Vue-Router知识点EcmaScript 2022 正式发布,有哪些新特性?一文详解|增长那些事儿远程医疗:优势、前景和现有IT解决方案

编辑推荐

太厉害了,终于有人能把TCP/IP协议讲的明明白白了!牛人5次面试腾讯不成功的经验HBase原理–所有Region切分的细节都在这里了Javascript如何监听页面刷新和关闭事件如何搭建一个HTTPS服务端
我收藏的内容
点赞
收藏

51CTO技术栈公众号