数组与指针

数组的基本概念

数组(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; //指针常量