数组并非指针
一、什么是声明,什么是定义
首先我们回顾下C
语言中的声明和定义概念。
- 定义
只能出现在一个地方。定义它确定对象的类型并分配内存,用于创建新的对象。例如:int a[100];
- 声明
可以出现多次。声明它描述了对象的类型,用于指代其他地方定义的对象(例如在其它文件里)。例如:extern int a[];
记住C语言中的对象(比如函数和变量)必须有且只有一个定义,但它可以有多个extern声明。
extern对象的声明告诉编译器对象的类型和名字,对象的内存分配则在别处进行。由于并未在声明中为数组分配内存,所以并不需要提供关于数组长度的信息。对于多维数组,需要提供除左边一维之外其它维的长度。
指针的外部声明与数组定义不匹配会导致出错,例如下面的示例:
文件 a.c
int a[100];
文件 b.c
#include <stdio.h>
#include "a.c"
int main(int argc, char **argv)
{
extern int *a;
printf("%d\n", a[0]);
return 0;
}
编译并运行:
[dendi875@localhost Temp]$ gcc -g b.c -o b
b.c: 在函数‘main’中:
b.c:6: 错误:与‘a’类型冲突
a.c:1: 附注:‘a’的上一个定义在此
其实要修正这个错误很容易,只要使声明与定义保持一致就行。
修改后的 b.c
#include <stdio.h>
#include "a.c"
int main(int argc, char **argv)
{
extern int a[];
printf("%d\n", a[0]);
return 0;
}
文件 a.c 中 int a[100]
表示定义了 a 数组并分配了100个int
的空间,而 int *p
则表示申请一个地址来装该指针。指针的名字是 p,它可以指向任何一个 int
变量(或int型数组)。指针变量 p 本身始终位于同一个地址,但它的内容在任何时候都可以不同,指向不同地址的 int 变量。这些不同的 int 变量可以有不同的值。int a[100]
中数组a的地址并不能改变,在不同的时候它的内容可以不同,但它总是表示100个连续的内存空间。
二、数组并非指针
我们经常听到过数组和指针是相同的这种说法,但其实这种说明并非完全正确,确实存在一种指针和数组的定义完全相同的上下文环境,但这只是数组的一种极为普通的用法,并非所有情况下都是如此。但是,人们却自然而然地归纳并假定在所有的情况下数组和指针都是等同的。我们分析下面两个小例子
a.c
#include <stdio.h>
int main(int argc, char **argv)
{
char p[] = "abcd";
printf("%c\n", p[0]);
return 0;
}
编译并运行
[dendi875@localhost Temp]$ gcc -g a.c -o a
[dendi875@localhost Temp]$ ./a
a
p.c
#include <stdio.h>
int main(int argc, char **argv)
{
char *p = "abcd";
printf("%c\n", p[0]);
return 0;
}
编译并运行
[dendi875@localhost Temp]$ gcc -g p.c -o p
[dendi875@localhost Temp]$ ./p
a
上面两个例子分别对数组和指针使用了下标的访问,它们都能正常运行并结果一致。但就是这种情况让我们错误的认为数组和指针完全相同。
下面我们分析下数组和指针是如何访问的
对数组下标的引用
char p[] = "abcd"; p[i];
编译器编译时为 p 变量分配了一个地址假设为 7321,上面的代码运行时分为两个步骤:
运行时步骤1:取 i 的值,将它与 7321 相加
运行时步骤2:取地址(7321 + 1)的内容
数组的这种引用方式是对内存的直接引用
对指针进行下标的引用
char *p = "abcd"; p[i];
编译器编译时为p变量分配了一个地址假设为 1237,上面的代码运行时分为三个步骤:
运行时步骤1:取地址 1237 的内容,假设为 8352
运行时步骤2:取得i的值,并将它与 8352 相加
运行时步骤3:取地址(8352 + 1)的内容
指针的这种引用方式是对内存间接的引用
三、数组和指针的其它区别
指针 | 数组 |
---|---|
保存数据的地址 | 保存数据 |
间接访问数据,首先取得指针的内容,把它作为地址,然后从这个地址提取数据。 如果指针有一个下标 p[i] ,就把指针的内容加上 i 作为地址,从中提取数据 | 直接访问数据,a[i] 只是简单地以 a + i 为地址取得数据 |
通常用于动态数据结构 | 通常用于存储固定数目且数据类型相同的元素 |
相关的函数 malloc,free等 | 隐式分配和删除 |
通常指向匿名数据 | 自身即为数据名 |
数组和指针都可以在它们的定义中用字符串常量进行初始化。尽管看上去一样,底层机制却不相同。
定义指针
char *p;
定义数组
char buff[4096];
定义指针时,编译器并不为指针所指向的对象分配空间,它只是分配指针本身的空间,除非在定义的同时赋给指针一个字符串常量进行初始化。例如,下面的定义创建了一个字符串常量(为其分配了内存)
char *p = "abcd";
注意只有对字符串常量才能这样,如浮点数就不行
float *pi = 3.14; /* 错误,无法通过编译 */
- 在ANSI C中,初始化指针时所创建的字符串常量被定义为只读。如果试图通过指针修改这个字符串的值,程序就会出现未定义的行为
例如文件 p.c
int main(int argc, char **argv)
{
char *p = "abcd";
p[0] = 'x'; // 运行时会导致段错误 (core dumped)
*p = 'y'; // 同样的运行时会导致段错误 (core dumped)
return 0;
}
- 与指针相反,由字符串初始化的数组是可以修改的
例如文件 a.c
#include <stdio.h>
int main(int argc, char **argv)
{
char s[] = "abcd";
s[0] = 'x';
printf("s = %s\n", s);
return 0;
}
编译并运行
[dendi875@localhost Temp]$ gcc -g a.c -o a
[dendi875@localhost Temp]$ ./a
s = xbcd