数组的基本概念
数组(Array)也是一种复合数据类型,它由一系列相同类型的元素(Element)组成。和结构体成员类似,数组元素的存储空间也是相邻的。
数组类型的长度应该用一个整数常量表达式来指定,常量表达式可以是一个数字、#define
定义的常量标识符、或者 sizeof()
表达式。注意,const int
并不是常量表达式,不能用于初始化数组(但在 C++ 中是可以的)
int arr[4];
#define MAX 4
int arr[MAX];
char a[sizeof(Object)];
我们可以在定义时对数组进行初始化,比如:
int arr[2] = {1,2};
int arr[] = {1,2};
上面两种写法是一致的。当不指定长度时,会根据初始元素的长度来设定数组长度。另外,我们也可以只对数组中的某些元素赋初值:
int arr[4] = {1,2}; //后面的元素为0
int arr[4] = {[0]=1, [1]=2}; //其余元素为0
数组中的元素通过下标(或者叫索引,Index)来访问。下标从 0 开始,一直到 length-1。下面是一个正常访问的例子:
#include <stdio.h>
int main(void)
{
int count[4] = { 3, 2, }, i;
for (i = 0; i < 4; i++)
printf("count[%d]=%d\n", i, count[i]);
return 0;
}
通过下标访问的数组可以当作左值来使用(就是可以当作一个变量)。
count[0]=7;
count[1]=count[2];
++count[0];
值得注意的是,c 并不检查下标是否在数组外,所以 count[-1]
是可以编译通过的,但这样可能会造成某些隐藏的bug,所以不能这样做。
另外,数组不能相互赋值或初始化。例如这样是错的:
int a[4] = {4,3,2,1};
int a = b; //wrong
a = b; //wrong
严格来说,既然不能相互赋值,也就不能用数组类型作为函数的参数或返回值。但实际上,这时只是传递指针而已,所以可以这样用。(见 1.5 函数)
除了下标外,可以用另一种方法访问数组元素:指针
1
2
3
4
for(int* p=arr;p<&a[MAX]; ++p)
{
x=*p;
}
使用指针更快一点,因为使用下标的话还需要计算地址。
字符串数组
在 c 中,字符串就是 char 类型的数组,但在 c++ 中,这两个不等价。
因此,字符串可以当成数组用,比如:
char c = "Hello, world.\n"[0];
字符串数组初始化时,可以直接用字符串初始化:
char str[] = "Hello";
//等价于
char str[6] = { 'H', 'e', 'l', 'l', 'o', '\0' };
注意到最后一个 \0
在字符串中并没有,这是 C 自动增加的,以标识字符串结尾。所以,数组长度比字符串长度多 1。
多维数组
数组可以嵌套,比如:
int a[3][2] = { 1, 2, 3, 4, 5 };
//等价于
int a[][2] = { {1,2},{3,4},{5,} };
也可以对特定元素赋初值:
int a[3][2]={[0][1]=2, [2][1]=0};
注意,除了第一维的长度可以由编译器自动计算而不需要指定,其余各维都必须明确指定长度。
指针
指针可以帮助我们很方便地访问内存,但一旦用不好,就会出现内存空洞、多次释放一个指针、野指针、越界下标等问题。并且这类 bug 也很难找。
指针本身是变量,存的是某个地址,这个地址可以是其他变量的地址,也可以是操作系统分配的内存空间。
指针的声明如下:
int *ptr;
char *ptr;
int **ptr;
int (*ptr)[3];
int *(*ptr)[4];
我们怎么判断指针指向什么呢?首先,先去掉指针名和左边的一个 *
,然后剩下的就是指针指向的对象。比如:
int *ptr; //int 整型
char *ptr; //char 字符型
int **ptr; //int* 指向整型的指针
int (*ptr)[3]; //int [3] 指向数组
int *(*ptr)[4]; //int* [4] 指向指针的数组
一种特殊情况是 int *ptr[3]
,这是要看作 int *(ptr[3])
,这是个指针数组,有三个指针,每个指针指向 int
指针的赋值与取值
可以用 &
取地址,然后把地址赋给指针:
int i;
int *ptr = &i;
指针间也可以相互赋值:
int *ptr;
int *p = ptr;
不过注意的是,指针间赋值要求指针是同一类型的,如果类型不同,需要进行类型转换:
char *ptr;
int *p = (int *)ptr;
有一类特殊的指针类型叫通用指针 void *
,它可以转化成其他类型的指针,其他类型的指针也可以转化成通用指针(无需进行类型转换)。比如:
void *ptr;
int *p;
ptr=p;
p=ptr;
可以用 *
取指针指向地址存储的值。比如:
int i=1;
int *p = &i;
*p++; //i++
指针的运算
“指针±整数”的效果是指针向前/后移动整数个元素大小的地址。比如,如果指针指向浮点数,那么就移动整数×4bytes个地址。我们可以利用这点在数组中移动指针,注意不要越界。
“指针-指针” 的结果的类型是 ptrdiff_t
,是一个有符号整数,表示两个指针在内存中的距离(以元素长度为单位,而不是字节)
指针之间也可以进行比较大小(本质是比较地址),在数组中用得比较多。
空指针
我们用 p=NULL
表示指针指向一个空地址。 NULL
可以看作是 0,操作系统不会把任何数据保存在地址 0 及其附近。
正常情况下,一个未初始化的指针的值就是 NULL
,但为了保险起见,最好手动赋 NULL
。
#include<stdio.h>
int main(void)
{
int *p;
if(p==NULL)
{
printf("p is NULL");
}
else
{
printf("p is %d", *p);
}
return 0;
}
如果一个指针不用了,也应该赋 NULL
,避免出现野指针。
常量指针与指针常量
常量指针是“指向常量的指针”,而指针常量是“指向变量的常指针”。前者指向的地址可以变,但指向地址内存储的值不能变;后者则相反。
区分这两者的关键就是看 const 关键字修饰什么:
const int *p; //常量指针
int * const p; //指针常量