《深入理解C指针》精华摘录与解读(三)指针&数组
解读《深入理解C指针》,以及附上一些书中未提到的知识点,结合来理解。
关于指针与字符串、结构体的共同应用会在解读(四)指针&结构体、字符串中。
很多内容可以参考同专栏的其他文章,像指针&函数、结构体、字符串,指针基础概念与内存分配,什么时候要用指针等。
本篇的重点在指针与数组的共同应用。
.
.
前言
数组和指针的关联十分紧密,在合适的上下文中可以互换,因为数组名字有时候可以当指针来用,但是要注意二者不是一定可以互换的,数组名字也不是指针,虽然数组名字可以返回数组地址,但是却不能被赋值。
本章着重点在于深入理解数组、使用指针操作数组的各种方法、数组表示法和指针表示法的异同点、以及传递和返回数组时可能发生的问题。以及引入向量和矩阵来说明一维数组和多维数组。
四、指针&数组
4.1 数组概述
①、数组是能用索引访问的同质元素连续集合。同质:相同类型;连续:数组元素在内存中相邻连续,无空隙。
4.1.1 一维数组
②、一维数组是线性结构,其内部不包含元素数量信息,因此一般数组做参数传递时,还要传入元素数量。
注:要注意声明时[ ]内为数量,其他操作时[ ]内为索引。
int arr[5]; //索引0开始,4结束
int eleNum = sizeof(arr)/sizeof(int); //数组元素数量
4.1.2 二维数组
③、二维数组使用行和列来标识数组元素,可以将一行理解为一个一维数组,顺序也是按整“行”进入内存,只用一个[索引]来访问时,得到的就是对应行的指针,例如:
int main()
{
int matrix[2][3] = {{1,2,3}, {4,5,6}};
printf("&matrix[0]: %p sizeof(matrix[0]): %d\n",
&matrix[0],sizeof(matrix[0]));
return 0;
}
/*编译结果:每行三个元素,数组长度12
&matrix[0]: 100 sizeof(matrix[0]): 12
*/
4.1.3 多维数组
④、以三维为例:
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}}
};
/*在内存中就以顺序连续分配。
arr3d[0][0][0]=1; // 地址 =100
arr3d[0][0][1]=2; // 地址 =104
arr3d[0][0][2]=3; // 地址 =108
...
arr3d[0][1][0]=5; // 地址 =116
arr3d[0][1][1]=6; // 地址 =120
...
arr3d[2][1][2]=23; // 地址 =188
arr3d[2][1][3]=24; // 地址 =192
*/
4.2 指针表示法和数组
①、单独使用数组名字时会返回数组地址,也可以对数组的第一个元素取地址,两种方法等价。
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; //将数组的第一个元素地址赋值给指针
int *p_ = &arr[0]; //两种方法等价
②、表示数组中元素的几种方法:
/* 以下三种都可以表示数组arr第i+1个元素 */
p[i]; //数组表示法可以理解为“偏移并解引”操作。即向右偏移2个int位置,再解引取值。
*(p+i);
*(arr+i); //+1实际是地址+4(int)
注:要注意 i 的范围不能导致数组越界,(0~4)。
下面例子和注释的几种写法是等价的:
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
for(int i=0;i<5;i++)
{
printf("arr[%d]=%d\n",i,arr[i]); //arr[i]也可以写成*(arr+i)
}
for(int j=0;j<5;j++)
{
arr[j] *= 3; //*(arr+j)可以
//*p++ *= 3; //p可以,*arr++不可以,③会讲
printf("arr[%d]=%d\n",j,arr[j]);
}
return 0;
}
③、数组和指针表示法也不是可以完全等价的,以上例代码为例,区别如下:
arr[i]
和*(arr+i)
生成的机器码是不一样的。sizeof(arr)
=20。sizeof(p)
=4。前者是数组字节数,后者是指针长度(int)。sizeof(arr[0])
也是4。- 下面这点十分重要
p
是一个左值(赋值操作符左边的符号),左值必须能修改。而arr
是一个数组名,它不是左值,不能被修改。也就是说p++
是合法的,但是arr++
是不合法的。但是arr+1
表达式本身没有问题,因为没有对`arr``进行赋值操作。
p=p+1; //正确
arr=arr+1;//语法错误
p=arr+1;//正确
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;
}
...
free(pv); //记得用完后释放
注:一般*(pv+i)
这样的操作都要加上括号,因为解引操作符*
的优先级要比+
高。
4.4 用realloc调整数组长度
①、用malloc创建的已有数组的长度可以通过realloc函数来调整,原理同指针。realloc
函数的使用参考同专栏的解读(一)的2.2.3。
void *realloc(void* p,size_t size);
第一个参数是指向原内存块的指针,第二个是请求的大小。通过第二个参数可以增加和减少指针(数组)指向的内存。
4.5 传递一维数组
①、在传递数组时也需要传递长度信息。
比如:void displayArray(int arr[], int size)
注意:对数组使用sizeof(数组名)来获取元素数量是错误的。正确的应该是sizeof(数组名)/sizeof(数组元素类型)
②、作为参数传递数组时,指针表示法和数组表示法基本相同,例:
void displayArray(int* arr, int size) {
/* void displayArray(int arr[], int size) { */
for (int i = 0; i < size; i++) {
printf("%d\n",arr[i]);
/* printf("%d\n",*(arr+i)); */
}
}
4.6 指针一维数组
①、指针数组就是元素是指针的数组:
int* arr[5];
for (int i = 0; i < 5; i++)
{
arr[i]=(int *)malloc(sizeof(int));
*arr[i]=i;
}
以上例子的arr[i]
用*(arr+i)
替换也是等价的,如下:
int* arr[5];
for (int i = 0; i < 5; i++)
{
*(arr+i)=(int *)malloc(sizeof(int));
**(arr+i)=i;
}
/*
arr+i:第i个元素的地址
*(arr+i):第i个元素的值(指针数组所以值是个指针)
**(arr+i):解引上面这个指针获得该指针指向的值
*/
也可以把指针数组的每个元素理解成指向一个长度为1的数组的指针,即int* arr[5]
的元素指向的值的集合为int arr[5][1]={{0},{1},{2},{3},{4}};
,那么第4个元素指向的值就可以这么表示:arr[3][0]
。
4.7 数组指针和多维数组
①、可以将多维数组的一部分看做子数组。比如说,二维数组的每一行都可以当做一维数组。
/* 3个四个元素的一维数组 */
int arr2D[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
②、数组指针:指向数组的指针,本质是指针,与指针数组做好区分(参考同专栏解读(一)的1.2.2)。
int main(){
int arr2D[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
int (*arr2Dp)[4]=arr2D; //指向arr2D的第一行的第一个元素
printf("%p\n",arr2D); // 0061FEEC
printf("%p\n",arr2D[0]); // 0061FEEC
printf("%d\n",arr2D[0][1]); // 2
printf("%p\n",arr2D[0]+1); // 0061FEF0(0061FEEC+4)
printf("%d\n",*(arr2D[0]+1)); // 2 :第一行第二个元素
printf("%p\n",arr2D+1); // 0061FEFC
printf("%p\n",arr2D[1]); // 0061FEFC(0061FEEC+16):指向第二行第一个元素
printf("%d\n",sizeof(arr2D[0])); // 16
return 0;
}
注:
(1)、上面的arr2D+1
返回的地址(注释的是编译结果)不是偏移4字节(int),而是16字节(二维数组的一行,4*int)。
(2)、指向多维数组的数组指针,一般都是把行(最大维度)去掉,把指针名加上除行外的几个维度,即至少要传入一个维度的大小。例如上面的二维数组:int (*arr2Dp)[4]
->arr2D[][4]
。如果是三维:int (*arr3Dp)[3][4]
->arr3D[][3][4]
。
(3)、*(arr2D[0]+1)
等同于arr2D[0][1]
,就像*(arr+1)
等同于arr[1]
。
(4)、arr[i][j]
:有i个{ j个元素的数组 }
arr:数组地址;
i:数组行数;
j:一维数组元素数;
4.8 传递多维数组
①、传递多维数组时,在函数签名(函数原型声明)中使用数组表示法还是指针表示法都可以。
②、打印已知次维元素数(列数)的多维数组的函数可以用以下两种方式声明:
/* void print2Darr(int (*arr2D)[4],int rows) */
void print2Darr(int arr2D[][4],int rows){ //几行的四元素数组
for(int i=0;i<rows;i++)
{
printf("row%d: ",i);
for(int j=0;j<4;j++)
{
printf(" %d;",arr2D[i][j]);
}
printf("\n\r");
}
}
/*注意不要出现这种错误: void print2Darr(int *arr2D[4],int rows) */
/* 传递时依旧传递名字(地址)即可 */
int main(){
int arr2D[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
print2Darr(arr2D,3);
}
注:调用函数时,函数不会给这个数组分配内存,传递的只是地址。
③、打印不知行列数的多维数组的函数:
void print2Darr(int *arr2D,int rows,int cols) //int类型指针+1时,地址+4(要和数组指针区分,不是加一行)
{
for(int i=0;i<rows;i++)
{
printf("row%d: ",i);
for(int j=0;j<cols;j++)
{
//printf(" %d;",arr2D[i][j]); 这样不行,因为没有将指针声明为二维数组
//printf(" %d;",(arr2D+i)[j]);不会出语法错误,偏移是错的,因为声明的是int类型指针,这样不是按行偏移是按元素偏移
printf(" %d;",*(arr2D+(i*cols)+j));
}
printf("\n\r");
}
}
/* 调用时 */
int main(){
int arr2D[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
print2Darr(&arr2D[0][0],3,4);
//print2Darr(arr2D,3,4);也行 但是会有警告指针类型不兼容,因为是int数组指针,而上面是int指针
return 0;
}
/*printf(" %d;",*(arr2D+(i*cols)+j))的编译结果:
row0: 1; 2; 3; 4;
row1: 5; 6; 7; 8;
row2: 9; 10; 11; 12;
printf(" %d;",(arr2D+i)[j])的编译结果:
row0: 1; 2; 3; 4;
row1: 2; 3; 4; 5;
row2: 3; 4; 5; 6;
*/
④、打印二维以上数组的函数:
在传递二维以上的数组时,除了第一维以外,需要指定其他维度的长度。
void print3Darr(int (*arr3D)[2][4],int rows)
{
for(int i=0;i<rows;i++)
{
printf("row%d: ",i);
for(int j=0;j<2;j++)
{
printf("{");
for(int k=0;k<4;k++)
{
printf("%d,",arr3D[i][j][k]);
}
printf("}");
}
printf("\n\r");
}
}
int 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}} };
print3Darr(arr3D,3);
return 0;
}
4.9 动态分配二维数组
①、二维数组是数组的数组,因此用malloc创建二维数组时。两个“数组”不一定要连续。而连续性会影响复制内存等操作,例如不连续就要多次复制。
4.9.1 分配不连续内存的二维数组
②、首先分配“外层”数组,然后分别用malloc语句为每一行分配。因为分别用了malloc,所以内存不一定是连续的,但也有可能是连续的(取决于堆管理器和堆的状态)。
int rows = 2;
int columns = 5;
int **matrix = (int **) malloc(rows * sizeof(int *)); //分配行的地址(可看做外层数组数量为2,是2个指针大小)
for (int i = 0; i < rows; i++) {
matrix[i] = (int *) malloc(columns * sizeof(int)); //分配第i行的内存
}
4.9.2 分配连续内存的二维数组
③、第一种方法:先分配行指针(“外层”数组),然后再分配该二维数组每行所有元素的内存,如下:第一个malloc分配了行指针的内存,第二个malloc则分配了10个int元素的内存,然后再将“第2行”的地址计算出赋值给第一个malloc分配的第二个元素(即第2行指针)。
int rows = 2;
int columns = 5;
int **matrix = (int **) malloc(rows * sizeof(int *));
matrix[0] = (int *) malloc(rows * columns * sizeof(int));
for (int i = 1; i < rows; i++)
matrix[i] = matrix[0] + i * columns;
该例和不连续分配的区别就在于所有元素一起malloc,因此是否在一个malloc里分配元素内存即意味着元素内存是否连续。
④、第二种方法:将所有内存一次性分配,即不分配行指针。
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.10 不规则数组和指针
①、复合字面量:( 类型名 ) { 初始化列表 }
,它定义了一个匿名“对象”,该“对象”的值由初始化列表里的值指定,如果是一个匿名数组,那么数组长度也由初始化列表的元素个数决定。
复合字面量可以作为左值。
②、不规则数组:不规则数组是每一行的列数不一样的二维数组。如:第一行4个元素,第二行2个元素,第三行3个元素。即每行不对称。
③、用复合字面量创建普通二维数组:该数组的地址连续,且按顺序初始化。
int (*(arr1[])) = {
(int[]) {0, 1, 2},
(int[]) {3, 4, 5},
(int[]) {6, 7, 8}};
/*
该数组的内存布局如下:
元素下标 地址 值
arr2[0][0] 0x100 0
arr2[0][1] 0x104 1
arr2[0][2] 0x108 2
arr2[1][0] 0x112 3
arr2[1][1] 0x116 4
arr2[1][2] 0x120 5
arr2[2][0] 0x124 6
arr2[2][1] 0x128 7
arr2[2][2] 0x132 8
*/
④、用复合字面量创建不规则数组:
int (*(arr2[])) = {
(int[]) {0, 1, 2, 3},
(int[]) {4, 5},
(int[]) {6, 7, 8}};
/*
该数组的内存布局如下:
元素下标 地址 值
arr2[0][0] 0x100 0
arr2[0][1] 0x104 1
arr2[0][2] 0x108 2
arr2[0][3] 0x112 3
arr2[1][0] 0x116 4
arr2[1][1] 0x120 5
arr2[2][0] 0x124 6
arr2[2][1] 0x128 7
arr2[2][2] 0x132 8
*/
通过复合字面量创建不规则数组很简单,但是访问不规则数组的时候就比较麻烦,比如上面例子就要用3个for循环来访问。访问数组内容时数组表示法和指针表示法都可以。
结
本文简述了数组以及数组表示法和指针表示法的异同。探讨了除了标准的创建数组方法,还可以用malloc函数创建数组,对于多维数组,得尽量确保数组分配在连续的内存上。还有在传递和返回数组时可能产生的问题,通常需要给函数传递数组长度以便函数能正确处理数组。以及如何在C中创建不规则数组。