《深入理解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中创建不规则数组。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Diode丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值