一、数组 和 指针 的微妙关系

数组 是指将固定个数、相同类型的变量排列起来的对象。

正如之前说明的那样,给指针加 N,指针前进“当前指针指向的变量类型的长度 X N”。

因此,给指向数组的某个元素的指针加 N 后,指针会指向 N 个之后的元素。

#include <stdio.h>

int main(void)
{
int array[5];
int *p;
int i; /* 给数组 array 的各元素设定值 */
for (i = 0; i < 5; i++)
{
array[i] = i;
} /* 输入数组各元素的值(指针版) */
for (p = &array[0]; p != &array[5]; p++)
{
printf("%d\n", *p);
} return 0;
}

运行结果如下:

0
1
2
3
4

从第 16 行开始一个 for 循环,最初指针 p 指向 array[0],通过 p++ 顺序地移动指针,引导指针指向 array[5](尽管它不存在)

使用 ++运算符给指针加 1,指针前进 sizeof(int) 个字节。

此外,第 16~18 行的代码也可以换一种写法(我们可以称之为“改写版”)。

p = &array[0];
for (i = 0, i < 5; i++)
{
printf("%d\n", *(p + i));
}

这种写法,指针并没有一步步前进,而是固定的,只是在打印的时候加 i。

话说回来,你觉得这种写法容易阅读吗?

至少在我看来,无论写成 p++,还是 *(p + i),都不容易读。还是最初的例子中 a[i] 这样的方式更容易理解。

实际上,本书主张的是“因为利用指针运算的写法不容易阅读,所以让我们 抛弃这种写法吧”。

二、表达式中,下标运算符[] 和 数组 是没有关系的

在前一小节的“改写版”例程中,像下面这样将指针指向数组的初始元素。

p = &array[0];

其实也可以写成下面这样

p = array;

对于这种写法,很多 C 语言的入门书籍是这样说明的:

在 C 中,如果在数组名后不加[],单独地只写数组名,那么此名称就表示“指向数组初始化元素的指针”。

在这里,我可以负责地告诉你,上面的说明是错误的。

在 C 的世界里,事到如今你再去否定“数组名后不加[],就代表指向初始元素的指针”这个“强大的”误解显得有点无奈。对于这种已经深入人心的观点,你突然放言它其实是个误解,可能很多人无法接受。下面让我们依法来证明。

将 &array[0] 改写成 array,“改写版”的程序甚至可以写成下面这样:

p = array;	// 只是改写了这里,可以 ...
for (i = 0; i < 5; i++)
{
printf("%d\n", *(p + i));
}

另外,程序中 *(p + i) 也可以写成 p[i]。

p = array;
for (i = 0; i < 5; i++)
{
printf("%d\n", p[i]);
}

也就是说,*(p + i)p[i] 是同样的意思。可以认为后面的写法是前面的简便写法。

在这个例子中,最初通过 p = array; 完成了向 p 的赋值,但之后 P 一直没有发生更改。所以,早知道如此,何必当初偏要多声明一个 p,还不如一开始就写成 array 呢。

for (i = 0; i < 5; i++)
{
printf("%d\n", array[i]);
}

呀,好像又回去了呢。

结论就是,

p[i] 这种写法只不过是 *(p + i) 这种写法的简便写法,除此之外,它毫无意义array[i]p[i] 有什么不一样吗?array[i] 也可以像 p[i] 一样,将 array 解读成“指向数组的初始元素的指针”

也就是说,存在

int array[5];

这样的声明的时候,“一旦后面不追加[],只写 array”并不代表要使 array 具有指向数组第 1 个元素的指针的含义,无论加不加 [],

在表达式中,数组名都可以被解读成指针。

顺便说一下,对于这个规则来说,有三个小的例外,我们会在第 3 章作详细说明。

你可以认为这是一个哗众取宠的异端邪说,但至少在语法上,数组 下标运算符[] 和 数组无关

这里也是 C 的数组下标从 0 开始的理由之一。

要点

非常重要!!

在表达式中,数组可以解读成“指向它的初始元素的指针”。尽管有 3 个小例外,但是这和在后面加不加[]没有关系。

要点

p[i] 和 *(p + i) 的简便写法。

下标运算符[]原本只有这种用法,它 和 数组无关。

需要强调的是,认为 [] 和 数组 没有关系,这里的 [] 是指在表达式中出现的下标运算符[]。

声明中的[],还是表达式数组的意思。也就是说,声明中的 [] 和 表达式 中的 [] 意义完全不同。表达式中的 * 和 声明中的 * 意义也是完全不同的。这些现象使得 C 语言的声明在理解上变得更加扑朔迷离...,第 3 章将会进行详细说明。

此外,如果将 a + b 改写成 b + a,表达式的意义没有发生改变,所以你可以将 *(p + i) 写成 *(i + p)。其次,因为 p[i] 是 *(p + i) 的简便写法,实际上它也可以写成 i[p]。

引用数组元素的时候,通常我们使用 array[5] 这样的写法。其实,就算你写成 5[array],还是可以正确地引用到你想要的元素。可是,这种写法实在太另类了,它不能给我们带来任何好处。

要点

p[i] 可以写成 i[p],但不推荐这样写。

三、什么是指向数组的指针

“数组”和“指针”都是派生类型。它们都是由基本类型开始重复派生生成的。

也就是说,派生出“数组”后,再派生出“指针”,就可以生成“指向数组的指针”。

一听到“指向数组的指针”,有人也许要说:

这不是很简单嘛,数组名后不加[],不就是“指向数组的指针”吗?

的确,在表达式中,数组名可以解读成指针。但是,这不是“指向数组的指针”,而是“指向数组初始元素的指针”。

实际地声明一个“指向数组的指针”,

int (*array_p)[3]; /* array_p 是指向 int 数组(元素个数3)的指针 */

根据 ANSI C 的定义,在数组前加上&,可以取得“指向数组的指针”。(注意:这里是“数组可以解读成指向它初始元素的指针”这个规则的一个例外(参照 3.3.3 节))。

因此,

int array[3];
int (*array_p)[3]; array_p = &array; // 数组添加 &,取得“指向数组的指针”

这样的赋值是没有问题,因为类型相同。

可是,如果进行

array_p = array;

这样的赋值,编译器就会报出警告。

“指向 int 的指针”和“指向 int 的数组(元素个数 3)的指针”是完全不同的数据类型。

但是,从地址的角度来看,array 和 &array 也许就是指向同一地址。但要说起它们的不同之处,那就是它们在做指针运算时结果不同。

在我的机器上,因为 int 类型的长度是 4 个字节,所以给“指向 int 的指针”加 1,指针前进 4 个字节。但对于“指向 int 的数组(元素个数 3)的指针”,这个指针指向的类型为“int 的数组(元素个数 3)”,当前数组的尺寸为 12 个字节(如果 int 的长度为 4 个字节),因此给这个指针加 1,指针就前进 12 个字节。

四、C语言中,不存在多维数组

在 C 语言中,可以通过下面的方式声明一个多维数组:

int hoge[3][2];

我想企图这么干的人应该很多。请大家回忆一下 C 的声明的解读方法,上面的声明应该怎样解读呢?

是“int 类型的多维数组”吗?

这是不对的。应该是“int 的数组(元素个数 2)的数组(元素个数 3)”。

也就是说,即使 C 中存在“数组的数组”,也不存在多维数组*。

* 在 C 标准中,“多维数组”这个词最初出现在脚注中,之后这个词也会不时地出现在各个角落。尽管“不存在多维数组”这个观点会让人感觉有些极端,但如果你不接受这个观点,对于C 的类型模型的理解,可能就会比较困难。

“数组”就是将一定个数的类型进行排列而得到的类型。“数组的数组”也只不过是派生源的类型恰好为数组。图 3-7 是“int 的数组(元素个数 2)的数组(元素个数 3)”。

要 点

C 语言中不存在多维数组。

看上去像多维数组, 其实是“数组的数组”。

对于下面的这个声明:

int hoge[3][2];

可以通过 hoge[i][j]的方式去访问,此时,hoge[i]是指“int 的数组(元素个数 2)的数组(元素个数 3)”中的第 i 个元素,其类型为“int 数组(元素个数 2)”。当然,因为是在表达式中,所以在此时此刻,hoge[i]也可以被解读成“指向 int 的指针”。

关于这一点,3.3.5 节中会有更详细的说明。

那么,如果将这个“伪多维数组”作为函数的参数进行传递,会发生什么呢?

试图将“int 的数组”作为参数传递给函数,其实可以直接传递“指向 int 的指针”。这是因为在表达式中,数组可以解释成指针

因此,在将“int 的数组”作为参数传递的时候,对应的函数的原型如下:

void func(int *hoge);

在“int 的数组(元素个数 2)的数组(元素个数 3)”的情况下,假设使用同样的方式来考虑,

int 的数组( 元素个数 2) 的数组(元素个数 3)

其中下划线部分,在表达式中可以解释成指针,所以可以向函数传递

指向 int 的数组( 元素个数 2) 的指针

这样的参数,说白了它就是“指向数组的指针”。

也就是说,接收这个参数的函数的原型为:

void func(int (*hoge)[2]);

直到现在,有很多人将这个函数原型写成下面这样:

void func(int hoge[3][2]);

或者这样:

void func(int hoge[][2]);

其实,

void func(int (*hoge)[2]);

就是以上两种写法的语法糖,它和上面两种写法完全相同。

关于将数组作为参数进行传递这种的情况下的语法糖,在 3.5.1 节中会再一次进行说明。

延伸阅读:

《征服 C 指针》摘录1:什么是空指针?区分 NULL、0 和 '\0'

《征服 C 指针》摘录2:C变量的 作用域 和 生命周期(存储期)

《征服 C 指针》摘录3:数组 与 指针

《征服 C 指针》摘录4:函数 与 指针

《征服 C 指针》摘录5:函数形参 和 空的下标运算符[]

《征服 C 指针》摘录6:解读 C 的声明

《征服 C 指针》摘录7:练习——挑战那些复杂的声明

《征服 C 指针》摘录3:数组 与 指针的更多相关文章

  1. C++笔记-数组指针/二维数组转换指针

    参考资料: 1. 作者 BensonLaur  :https://www.cnblogs.com/BensonLaur/p/6367077.html 2. https://blog.csdn.net/ ...

  2. C基础知识(3):指针--概念、数组中指针的递增/递减、指针数组&数组指针、指向指针的指针

    指针是一个变量,其值为另一个变量的地址. 所有指针的值的实际数据类型,不管是整型.浮点型.字符型,还是其他的数据类型,都是一样的,都是一个代表内存地址的长的十六进制数. 下面从4个代码例子分别讲述以下 ...

  3. [C++]指针和指向数组的指针[一维数组与指针]

     1.一维数组与指针      形如:int型 数组 a[10]                1)&a[0]  地址常量;地址类型:int *型   ; 存储数组a的首地址          ...

  4. #运算符、不同的指针类型、数组和指针、指针运算、堆、栈、静态区、只读区、下标VS指针

    #运算符:用于在预编译期将宏参数转换为字符串 #define CONVERS(x)  #x   //注:没用双引号包括. 不同类型的指针占用的内存空间大小相同. 局部变量 定义: a[5]; 打印a[ ...

  5. C语言数组指针(指向数组的指针)

    注意:数组指针的定义,与指针数组的区别 转载:http://c.biancheng.net/cpp/biancheng/view/162.html 指向多维数组元素的指针变量 ① 指向数组元素的指针变 ...

  6. C++——指针2-指向数组的指针和指针数组

    7.4 指向数组元素的指针 声明与赋值 例:int a[10], *pa; pa=&a[0]; 或 pa=a[p1] ; 通过指针引用数组元素,经过上述声明及赋值后: *pa就是a[0],*( ...

  7. int (*p)[4] p 是二级指针 二维数组 二级指针 .xml

    pre{ line-height:1; color:#2f88e4; background-color:#e9ffff; font-size:16px;}.sysFunc{color:#3d7477; ...

  8. 26深入理解C指针之---不规则数组与指针

    一.不规则数组:每一行的列数不相等 1.复合字面量: 1).复合字面量是一种C构造 2).外形和数组声明差不多,写法与类型转换一样,(int[3]){10, 20, 30,} 3).将多个复合字面量可 ...

  9. [C++]数组与指针[二维数组与指针]

  10. C语言学习笔记之成员数组和指针

    成员数组和指针是我们c语言中一个非常重要的知识点,记得以前在大学时老师一直要我们做这类的练习了,但是最的还是忘记了,今天来恶补一下.     单看这文章的标题,你可能会觉得好像没什么意思.你先别下这个 ...

随机推荐

  1. Linux 下系统调用的三种方法

    系统调用(System Call)是操作系统为在用户态运行的进程与硬件设备(如CPU.磁盘.打印机等)进行交互提供的一组接口.当用户进程需要发生系统调用时,CPU 通过软中断切换到内核态开始执行内核系 ...

  2. 三维等值面提取算法(Dual Contouring)

    上一篇介绍了Marching Cubes算法,Marching Cubes算法是三维重建算法中的经典算法,算法主要思想是检测与等值面相交的体素单元并计算交点的坐标,然后对不同的相交情况利用查找表在体素 ...

  3. NOIP2012同余方程[exgcd]

    题目描述 求关于 x 的同余方程 ax ≡ 1 (mod b)的最小正整数解. 输入输出格式 输入格式: 输入只有一行,包含两个正整数 a, b,用一个空格隔开 输出格式: 输出只有一行,包含一个正整 ...

  4. Linux环境导入*.sql文件出现数据库为空

    登录mysql命令: 导入.sql文件: 或者: mysql -h127.0.0.1 -uroot -p userDb < /home/user.sql  按回车键后输数据库的密码 导入成功后, ...

  5. 直线的参数方程ABC

    直线的参数方程的来源 如图所示, 直线\(l\)的倾斜角为\(\theta\),经过定点\(P_0(x_0,y_0)\),在直线上有一动点\(P(x,y)\),如果我们取直线的单位方向向量\(\vec ...

  6. angular学习笔记(二十九)-$q服务

    angular中的$q是用来处理异步的(主要当然是http交互啦~). $q采用的是promise式的异步编程.什么是promise异步编程呢? 异步编程最重要的核心就是回调,因为有回调函数,所以才构 ...

  7. js 检测页面刷新或关闭

    window.onbeforeunload=function(){ //要提交的内容 return "随意写";//必须有return ,不然只有ie有效,chrome无效 }

  8. Error: Could not find the required version of the Java(TM) 2 Runtime Environment in'(null)'.

    今天拿到一台新机器,搭一下开发环境,安装个JDK是个很基本的事情,从Orale的网站上下了个安装,但是一直出下面的错: 我信了你的邪,Google了一圈,有人说是可能文件下载有问题,重新下载安装就可以 ...

  9. QString, string, int, char* 之间相互转换

    这三种数据类型在实际运用中经常需要互相转换,那么这里小结下它们之间的转换方法: - Qstring & string Qt中封装的类十分强大,其成员函数数量之多比STD有过之而无不及,许多程序 ...

  10. 【跟着子迟品 underscore】for ... in 存在的浏览器兼容问题你造吗

    Why underscore 最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中. 阅读一些著名框架类库的源码,就好像和一个个大师对 ...