指针篇(2)- 指针与数组

前言

上篇博文讲到 C 语言指针的基本使用(声明和初始化以及解引用), 对指针有了基本的了解之后,就来说说指针的一个暧昧对象——数组。

数组是什么?

数组是几乎所有编程语言都有的一种类型, 实际上它也是最简单的一种数据结构。 在 C 语言当中, 数组由一组数目固定的、数据类型相同的元素构成。 当用户定义一个数组, 系统就会在内存中自动分配一块连续的大小固定的栈内存供其使用。 数组的定义和使用想必读者都很清楚, 这里就不再赘述了。 下面我们先来说说指针和数组为什么暧昧。

指针和数组名

说指针和数组暧昧, 暧昧在哪里? 其实就暧昧在指针和数组名。 经常可以看到类似于如下的一段程序:

int a[10] = {0,};
int *pa = NULL;
pa = a;

为了更加方便地说明数组和指针的关系, 可以看一下一维数组在内存中的排列方式:

图中开辟了两块内存,分别用于存储一个长度为 10 的数组 a (其数据类型为 int)和一个指针 pa(其数据类型为 int)。 并且指针指向了数组 a:「pa = a」。可能对于 C 初学者而言,「 pa = a 」实际上并不是个容易理解的表达式, 更加一目了然的语句应该是「 pa = &a[0] 」。在上篇博文中说过指针的初始化是通过对目标对象进行取址运算完成, 而这里却使用了数组名 a 来代替 &a[0], 这说明数组名 a 出现在赋值运算符「=」右边时和 &a[0] 是等效的, 都代表了数组首元素的地址。 但是不能将数组名等价于数组首元素的地址, 实际上数组名的类型就是是数组类型——TYPE [], 数组名只是一个符号, 在内存中并没有实际的空间用于存储数组名 a 。但是在大多数情况下数组名会隐式转换为指针类型。

从上面的图看到访问数组成员可以使用两种方式。

一种是直接访问, 譬如访问数组的第一个成员: 第一个成员的地址是 &a[0], 对其解引用, 即 *&a[0](在 C 语言中, 取址运算符和解引用运算符是相反的运算符, 类比于数学中的平方和开平方), 与 a[0] 等价。 这种直接访问方式称为下标访问, 改变下标的数字即可直接访问数组的其他元素; 另外一种则是间接访问,譬如访问数组的第一个成员: 如果指针指向数组的首元素, 这时候可以直接对指针进行解引用, 即 *pa。 假如想要访问其他元素, 可以在指针原有的地址(基地址)上增加一个偏移量, 譬如 pa + 2, 对其解引用得到 *(pa + 2)。 在习惯上我们喜欢把指针指向数组的首元素, 实际上可以指向任何位置, 甚至数组外, 譬如 「pa = a - 1」。 不过需要注意的是在访问的时候千万不要越出数组边界, C 语言编译器是不会帮我们作数组边界检查的。 除了上述的两种方式, C 语言还提供了一种有趣的访问数组的方式:指针 + 下标。 譬如访问数组的第三个成员: pa[2]。 这实际上是 C 语言给程序员提供的「语法糖」, 它在本质上与 *(pa+2) 并无区别, 不过由于与数组的下标访问方式相同, 使得程序的可读性增加了。

关于数组的概念, 需要明确以下两个结论:

1. 数组名的类型就是数组类型——Type[], 它不是常量, 不是指针, 也不是常量指针。

2. 数组名在大多数情况下会转换为一般指针类型, 除了以下三种情况(C99 标准):

  • 使用 sizeof 运算符计算数组所占内存空间大小的时候 
  • 使用字符串字面量初始化数组的时候(即初始化一个字符串数组)
  • 对数组名进行取址运算 「&」的时候 ,得到类型是数组指针类型——Type(*) [], 关于这一点可以看到一个有趣的现象, 我们来看以下这段代码。
/* 
    GCC 5.4 环境编译 
     
*/ 

#include <stdio.h>

int main(void)
{
	int a[10] = {0};
	
	printf("a = %p\n",a);
	printf("&a[0] = %p\n",&a[0]);
	printf("&a = %p\n",&a);	
	
	return 0;	
}

输出结果如下:

a = 0x7ffce513bb70
&a[0] = 0x7ffce513bb70
&a = 0x7ffce513bb70

可以看到三个地址值完全一样, 前面我们说过了 a 和 &a[0] 在某些情况下是等效的, 都代表了数组首元素的首地址, 而 &a  则代表的是整个数组的首地址。 虽然两者的值相同, 但含义却完全不同。

左值和右值

这里提及一个 C 初学者不是很熟悉的一个概念:「左值」(lvalue)和「右值」(rvalue)。这个概念来自于赋值语句: 「X = Y;」。 很多人认为「lvalue」的「l」是 left 的意思,「rvalue」的「r」是 right 的意思, 所以赋值运算符「=」左边的就称为左值, 右边的就称为右值。 但是这其实算是比较古老的一种说法,不过现在也习惯这么称呼。 实际上「l」应该解读为「location」, 表示可寻址(即可使用「&」运算符取址); 「r」可以解读为「read」, 表示可读。 一般来说, 可以这样定义「左值」(lvalue)和「右值」(rvalue): 可以使用取址运算符「&」获取地址的是左值, 否则为右值。 大多数的左值都可以放在赋值运算的左侧或者右侧, 除了使用 const 限定符的变量和数组以外。 这两者(比较常见)被称为不可修改的左值。 这和我们平时所说的「const 修饰的变量无法修改」,「数组无法整体赋值」相符合。 所以以下的赋值语句都是错误的:

const int a = 10;
a  = 12; //错误

int b[10] = {0};
int c[10] = {0, 1, 2 ,3 ,4, 5, 6, 7, 8, 9,}
b = c ; //错误


关于左值和右值我们可以得出以下几个结论:

  • 可修改的左值是可设定地址的、可赋值的。
  • 不可修改的左值是可设定地址、不可赋值的。
  • 右值不可设定地址,也不可赋值。
关于数组和指针的关系大致讲到这里, 其实这篇的目的就是为了让读者重新认识一下数组。 关于数组和指针的内容在之后还会提到。 下一篇博文我们来聊聊一些复杂的指针, 这些指针在一些公司的笔试题中经常出现。 
2017 年 7 月 7 日
Kilento

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值