深入理解C指针 指针和数组
4.指针和数组
一种常见的错误观点是“数组和指针是完全可以互换的”。尽管数组名字有时候可以当作指针来用,但数组的名字不是指针。数组表示法也可以和指针一起使用,但两者明显不同,也不一定能互换。尽管数组使用自身的名字可以返回数组地址,但名字本身不能作为赋值操作的目标。
4.1.1 一维数组
一维数组是线性结构,用一个索引访问成员。数组索引从0开始,到声明的长度减1结束。
对数组做sizeof
操作会得到为该数组分配的字节数,要知道元素的数量,只需将数组长度除以元素长度。
4.1.2 二维数组
二维数组使用行和列来标识数组元素,这类数组需要映射为内存中的一维地址空间。在C中这是通过行-列顺序实现的。先将数组的第一行放进内存,接着是第二行、第三行,直到最后一行。
可以将二维数组当作数组的数组,也就是说,可以只用一个下标访问数组,得到的是对应行的指针。下面片段代码会打印每一行的地址和长度:
int matrix[2][3] = { {1,2,3},{4,5,6} }; for (int i = 0; i < 2; i++) { printf("&matrix[%d]:%p sizeof(matrix[%d]):%d\n", i, &matrix[i], i, sizeof(matrix[i])); } &matrix[0]:100 sizeof(matrix[0]):12 &matrix[1]:112 sizeof(matrix[1]):12 //假设数组位于地址100处
4.2指针表示法和数组
单独使用数组名字时会返回数组地址。可以把地址赋给指针,如下所示:
int vector[5] = {1,2,3,4,5};
int *pv = vector;
pv变量是指向数组第一个元素而不是指向数组本身的指针。给pv赋值是把数组的第一个元素的地址赋给pv。数组的首地址,也就是第一个字符的地址
我们可以只用数组名字,也可以对数组的第一个元素用取地址操作符,这些写法是等价的,都会返回vector
的地址。
printf("%p\n",vector);
printf("%p\n",&vector[0]);
有时候也会使用&vector
这个表达式获取数组地址,不同于其它表示法,这么做返回的是整个数组的指针,其他两种方法得到是整数指针。
我们可以把数组下标用在指针上,实际上pv[i]这种表示法等价于: *(pv +i)
pv指针包含一个内存块的地址,方括号表示法会取出pv中包含的地址,用指针算术运算把索引i加上,然后解引新地址返回其内容。下面两个语句是等价的:
*(pv + i) == *(vector + i)
pv + i实现“基址 + 位移”的运算,其值恰为数组 vector 第i个元素的地址,即&vector[i]。
由于数组元素的下标在内部实现是统一按“基址 + 位移”的方式处理的,这样,一个指向数组的指针也能够以数组名的形式出现 pv[i]等价于 vecotr[i]
vector[i]
生成的代码和*(vector+i)
生成的不一样,vector[i]
表示法生成的机器码从位置vector
开始,移动i
个位置,取出内容。而*(vector+i)
表示法,生成的机器码则是从vector
开始,在地址上增加i
,然后取出这个地址中的内容。尽管结果是一样的,生成的机器码却不一样,对于大部分人来说,这种差别几乎无足轻重。
sizeof
操作符对数组和同一个数组的指针操作也是不同的。对vector
调用sizeof
操作符会返回20,就是这个数组分配的字节数。对pv
调用sizeof
操作符会返回4,就是指针的长度。
pv
是一个坐值,左值表示赋值操作符左边的符号。左值必须能修改。像vector
这样的数组名字不是左值,它不能被修改。
4.3 使用malloc创建一维数组
如果从堆上分配内存并把地址赋给一个指针,那就肯定可以对指针使用数组下标并把这块内存当成一个数组。
int *pv = (int *)malloc(5 * sizeof(int));
for (int i = 0; i < 5; i++) {
pv[i] = i + 1;//*(pv + i) = i + 1 指针表示法
}
用malloc创建的一维数组也可以使用数组表示法,但是用完之后要记得释放内存。
4.4 用realloc调整数组长度
用realloc函数通过一个定长增量来分配额外空间。
char *getLine(void) { const size_t sizeIncrement = 10; //缓冲区的初始大小以及需要增大时的增量 char *buffer = malloc(sizeIncrement); //指向读入字符的指针 char *currentPosition = buffer; //指向缓冲区中下一个空白位置的指针 size_t maximumLength = sizeIncrement; //可以安全地存入缓冲区的最大字符数 size_t length = 0; //读入的字符数 int character; //上次读入的字符数 if (currentPosition == NULL) return NULL; while(1){ character = fgetc(stdin); //从标准输入每次读取一个字符 if (character == '\n') break; //如果是回车符,循环退出 if (++length >= maximumLength) //判断有没有超出缓冲区大小 { char *newBuffer = realloc(buffer, maximumLength += sizeIncrement); if (newBuffer == NULL) { //如果无法分配内存 free(buffer);//释放已分配内存 强制函数返回NULL return NULL; } //新分配的地址可能在原地址也有可能在其它位置 currentPosition = newBuffer + (currentPosition - buffer); buffer = newBuffer; } *currentPosition++ = character; //没有超出字符添加到缓冲区中 } *currentPosition = '\0'; return buffer; }
如果realloc
分配成功,我们不需要释放buffer
,因为realloc
会把原来的缓冲区复制到新的缓冲区,再把旧的释放。如果试图释放buffer
,十有八九程序会终止,因为我们试图重复释放同一块内存。
realloc函数也可以用来减少指针指向的的内存。如下所示的,trim函数会把字符串中开头的空白符和中间的空白符删掉。
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> char *trim(char *phrase) { char *old = phrase; char *new = phrase; while (*old == ' ') old++; //去掉开头空白符 while (*old != '\0') { *(new++) = *(old++); while (*old == ' ') old++;//去除中间空白符 } *new = '\0'; return (char *)realloc(phrase, strlen(phrase) + 1); } void main() { char *buffer = (char *)malloc(strlen(" C at && Tiger ") + 1); strcpy(buffer, " C at && Tiger "); printf("%s\n", trim(buffer)); system("pause"); }
4.5传递一维数组
4.5.1 用数组表示法
void display(int arr[], int size) { for (int i = 0; i < size; i++) printf("%2d", arr[i]); }
4.5.2 用指针表示法
声明函数的数组参数不一定要用方括号表示法,也可以用指针表示法,如下所示:
void display(int *arr, int size) { for (int i = 0; i < size; i++) printf("%2d", arr[i]); }
在函数内部我们仍然使用数组表示法,如果有需要也可以使用指针表示法:
void display(int *arr, int size) { for (int i = 0; i < size; i++) printf("%2d", *(arr + i)); }
如果在声明函数时用了数组表示法,在函数体内还是可以用指针表示法:
void display(int arr[], int size) { for (int i = 0; i < size; i++) printf("%2d", *(arr + i)); }
4.6 使用指针的一维数组
下面的代码片段声明一个整数指针数组,为每个元素分配内存,然后把内存的内容初始化为元素的索引值。
int *arr[5]; for (int i = 0; i < 5; i++) { arr[i] = (int *)malloc(sizeof(int)); *arr[i] = i; }
因为arr声明为一个指针数组,arr[i]返回的是一个地址,用*arr[i]解引指针时,得到的是这个地址的内容。
也可以在循环体中使用下面这种等价的指针表示法:
*(arr + i) = (int *)malloc(sizeof(int)); **(arr + i) = i;
表达式 arr[3][0]引用arr的第4个元素,然后是这个元素所指向的数组的第一个元素。表达式 arr[3][1]有错误,因为第4个元素所指向的数组只有一个元素。
4.7指针和多维数组
可以将多维数组的一部分看作子数组,比如,二维数组的每一行都可以当作一维数组。
int matrix[2][5] = {{1,2,3,4,5},{6,7,8,9,10};
int (*pmatrix)[5] = matrix;
(*pmatrix) 表达式声明了一个数组指针,上面的整条声明语句将pmatrix定义为一个指向二维数组的指针,该二维数组的元素类型是整数,每列有5个元素。
int *pmatrix[5] 如果我们把括号去掉就声明了5个元素的数组,数组元素的类型是整数指针。
如果声明的列数不是5,用该指针访问数组的结果则是不可预期的。
matrix + 1 返回的地址不是从数组开头偏移4,而是偏移了第一行的长度,20字节,得到的是数组的第二行地址。
matrix[0]返回数组第一行第一个元素的地址,这个地址是一个整数数组的地址。
于是,给它加1实际加上的是一个整数的长度,得到的是第二个元素。*(matrix[0] + 1)。
4.8传递多维数组
要传递数组matrix,可以这么写:
void display2DArray(int arr[][5],int rows){
或者这么写:
void display2DArray(int (*arr)[5],int rows){
这两种写法都指明了数组的列数,这很有必要。在第一种写法中 arr[ ]是数组指针的一个隐式声明,第二种写法(*arr)则是指针的一种显示声明。
下面的声明是错误的:
void display2DArray(int *arr[5], int rows){
尽管不会产生语法错误,但是函数会认为传入的数组拥有5个整数指针。
也可能遇到下面这样的函数,接受的参数是一个指针和行列数:
void display2DArrayUnknownSize(int *arr, int rows, int cols) { for (int i = 0; i < rows; i++) for (int j = 0; j < cols; j++) { printf("%d ", *(arr + (i*cols) + j)); } printf("\n"); }
printf语句通过给arr加上前面行的元素数(i * cols)以及当前列的j来计算每个元素的地址。要调用这个函数可以这么写:
display2DArrayUnknownSize(&matrix[0][0], 2, 5);
在函数内我们无法像下面这样使用数组下标:
printf("%d ", arr[i][j]);
原因是没有将指针声明为二维数组。不过倒是可以像下面这样使用数组表示法。
我们可以用一个下标,这样写只是解释为数组内部的偏移量,不能用两个下标是因为编译器不知道一维的长度:
printf("%d ", (arr+i)[j]);
这里传递的是&matrix[0][0]
而不是matrix
,尽管matrix
也能运行,但是会产生编译警告,原因是指针类型不兼容。
&matrix[0][0]
表达式是一个整数指针,而matrix
则是一个整数数组的指针。
在传递二维以上的数组时,除了第一维以外,需要指定其他维度的长度。下面这个函数打印一个三位数组,声明中指定了数组的后二维。
#include<stdio.h> #include<stdlib.h> void display3DArray(int(*arr)[2][4], int rows){ for (int i = 0; i < rows; i++) { for (int j = 0; j < 2; j++) { printf("{"); for (int k = 0; k < 4; k++) { printf("%d ", arr[i][j][k]); } printf("}"); } printf("\n"); } } void main() { int arr3d[3][2][4] = { {{1,2,3,4},{5,6,7,8}}, { { 9,10,11,12 },{ 13,14,15,16 } }, { { 17,18,19,20 },{ 21,22,23,24 } }, }; display3DArray(arr3d, 3); system("pause"); }
arr3d[1] 表达式引数组的第二行,是一个2行4列的二维数组的指针。
arr3d[1][0]引用数组的第二行第一列,是一个长度为4的一维数组的指针。
4.9 动态分配二维数组
为二维数组动态分配内存涉及几个问题: 数组元素是否需要连续; 数组是否需要规则。
一个声明如下的二维数组所分配的内存是连续的:
int matrix[2][5] = {{1,2,3,4,5},{6,7,8,9,0}};
当我们用malloc
这样的函数创建二维数组时,在内存分配上会有几种选择。
由于我们可以将二维数组当作数组的数组,因而“内层”的数组没有理由一定要是连续的。如果对这种数组使用下标,数组的不连续对程序员是透明的。
内存的连续性还会影响复制内存等其他操作,内存不连续就可能需要多次复制。
4.9.1 分配可能不连续的内存
下面代码片段创建一个内存可能不连续的二维数组。首先分配 外层 数组,然后分别用malloc语句为每一行分配。
int rows = 2; int columns = 5; int **matrix = (int **)malloc(rows * sizeof(int *)); for (int i = 0; i < rows; i++) { matrix[i] = (int *)malloc(columns * sizeof(int)); }
因为分别用了malloc,所以内存不一定是连续的。实际分配情况取决于堆管理器和堆的状态,也有可能是连续的。
4.9.2 分配连续内存
第一种:首先分配“外层”数组,然后是各行所需的所有内存。
int rows = 2; int columns = 5; int **matrix = (int **)malloc(rows * sizeof(int *)); matrix[0] = (int *)malloc(rows*columns * sizeof(int)); //按图示来看,这里是否该去掉rows????? for (int i = 1; i < rows; i++) matrix[i] = matrix[0] + i*columns;
第二种:数组所需内存一次性分配。
int *matrix = (int *)malloc(rows * columns * sizeof(int));
后面的代码用到这个数组时不能使用下标,必须手动计算索引。如下代码片段所示,每个元素被初始化为其索引的积:
for (int i = 0; i < rows; i++) { for (int j = 0; j < columns; j++) { *(matrix + (i*columns) + j) = i*j; } }
不能使用数组下标是因为丢失了允许编译器使用下标所需的"形态"信息。4.8节讲过了。
实际项目中很少使用这种方法,但它确实说明了二维数组概念和内存的一维本质的关系。
4.10 不规则数组和指针
不规则数组是每一行的列数不一样的二维数组。