1 二维指针(指向指针的指针)
- 指针的本质是变量
- 指针的指针是保存指针变量的地址。如下面的代码:
为什么需要指向指针的存在?还记得之前学习的过程中说的函数传值调用和传址调用么?当要在函数内部修改传进来参数变量的时候,需要传址调用。
同理,如果传进来的本来就是一个指针,想要修改该指针,那么就需要传指向该指针的指针了。道理是一样的。看下面的代码就明白了:
- 代码34-1.c:函数reset为重新为某一段内存分配一段内存空间(可大可小)
#include <stdio.h>
#include <malloc.h>
/* 想要修改p指向的内容,且p的地址也是会变的,就必须使用传址调用。传p的地址。就是双指针 */
int reset(char** p, int old_size, int new_size){
int ret = 1;
int i = 0;
int len = 0;
char* pp = *p;
char* pt = NULL;
char* tmp = NULL;
if((NULL!=p) && (new_size>0)){
pt = (char*)malloc(new_size);
tmp = pt;
len = (old_size < new_size) ? old_size : new_size;
for(i=0; i<len; i++){
*tmp = *pp;
tmp++;
pp++;
}
free(*p);
*p=pt;
}else{
ret = 0;
}
return ret;
}
int main(){
char* p = (char*)malloc(5);
printf("p = %p\n",p);
if(reset(&p, 5, 8)){
printf("reset p = %p\n",p);
}else{
printf("no reset!\n");
}
free(p);
return 0;
}
- 编译运行结果为:
p = 0x8833008
reset p = 0x8833018
分析:
上述代码中reset函数是重新为一块内存分配另一个内存空间。我们知道要分配另一块空间的话,地址肯定是会变的,那么想要最终将原来的地址p改变,就需要进行传址调用。因为p本来就是指针,所以需要传指针的指针进入reset函数。在reset函数中,进行重新分配内存空间并将原有的内存空间中的值拷贝到新的内存地址处。具体自己好好看一下reset函数就可以理解。
由运行可以看出:
- 地址p确实改变了,说明传址调用起了作用
- 注意理解指向指针的指针的意义与用法。需要多琢磨。
2 二维数组
在C语言中,没有二维数组的概念。它只是另一种形式的一维数组。
- 二维数组在内存中是以一维数组的形式排布
- 二维数组的第一维是一维数组(注意,一维数组相当于一个常量指针,也就说二维数组的第一维是存的指针)
- 二维数组的第二维是具体存的数值
- 既然一维数组的数组名可以看成是常量指针,那么二维数组的数组名也同样可以看成是常量指针
- 结合上述四条看看下图中的二维数组在内存中的样式:
结合下面的代码来认识认识二维数组:
- 代码:34-2.c :
#include <stdio.h>
void print_array(int a[], int size){
printf("print_array:sizeof(array) = %d\n",sizeof(a));
int i = 0;
for(i=0; i<size; i++){
printf("%d ",a[i]);
}
printf("\n");
}
int main(){
int a[3][3] = {{0,1,2},{3,4,5},{6,7,8}};
int* p = &a[0][0];
int i=0,j=0;
for(i=0; i<3; i++){
for(j=0; j<3; j++){
printf("%d, ", a[i][j]);
}
printf("\n");
}
printf("\n");
for(i=0; i<3; i++){
for(j=0; j<3; j++){
printf("%d, ", *(*(a+i)+j));
}
printf("\n");
}
printf("a = %p, a+1 = %p\n",a,a+1);
printf("&a = %p, &a+1 = %p\n",&a,&a+1);
printf("p = %p, p+1 = %p\n",p,p+1);
printf("\n");
print_array(p,9);
return 0;
}
- 编译运行结果为:
分析:
- 上述代码并不是很难,所以不做详细分析。只做以下几点说明:
-
可以像这样访问二维数组:
*(*(a+i)+j)
。可以想这样理解: 其中a+i
代表是一维数组的第i个元素(即指针),*(a+i)
代表找到第i个一维数组的起始地址,*(a+i)+j
表示第i个一维数组中的第j个元素的地址。最终*(*(a+i)+j)
表示取出元素。可以参考上面的二维数组的内存图。 -
由:
a = 0xbfb71b00, a+1 = 0xbfb71b0c
知道:二维数组的名字a可以看成是一个常量指针,它的值为二维数组首元素(这个首元素相当于是一个一维数组)的地址值。a+1就直接跨过一个一维数组的长度(这里是12,三个int)。 -
由:
&a = 0xbfb71b00, &a+1 = 0xbfb71b24
知道: &a 代表整个数组的地址(这与一维数组很相似)。&a+1 就直接跨过整个数组的大小到数组末尾 -
由print_array 函数的参数是一维数组知道,二维数组在内存中排布是一维数组的形式。参考上图。
3 二维数组的类型
- 之前学过以为数组的类型如下:
int a[5] ==>>> a的类型为: int*
- 二维数组的类型为:
int a[2][5] ==>>> a的类型为:int(*)[5]
- 二维数组名可以看做是指向数组的常量指针
- 二维数组可以看成是一维数组中存的元素类型是一个同类型的一维数组
3.2 如何动态申请二维数组
从下面的代码来学习如何动态申请二维数组(参考下面的二维数组的内存模型就可以理解下面的代码):
- 代码:34-3.c:
#include <stdio.h>
#include <malloc.h>
int** malloc2d(int row, int col){
int** ret = NULL;
if(row>0 && col>0){
ret = (int**)malloc(row*sizeof(int*)); //相当于申请一个一维数组,存的元素是指针
int* p = (int*)malloc(row*col*sizeof(int));//相当于二维数组在内存中的一维排布,指针p指向这个一维排布
if(NULL!=p && NULL!=ret){
int i = 0;
for(i=0; i<row; i++){
ret[i] = p + i*col; // ret[i]存的是一个个指针指向的数组,每个数组长度是col,可以参考下图的二维数组内存模型
}
}else{
free(ret);
free(p);
ret = NULL;
}
}
return ret;
}
void free2d(int** p){
if(NULL != *p){
free(*p);
}
free(p);
}
int main(){
int** a = malloc2d(3,3);
int i=0,j=0;
for(i=0; i<3; i++){
for(j=0; j<3; j++){
printf("a[%d][%d] = %d,", i,j,a[i][j]);
}
printf("\n");
}
free2d(a);
return 0;
}
- 编译运行代码如下:
虽然上述的数组中的各个值都是0,但是我们要知道malloc申请后的内存中的值是不确定的,并不一定是0
分析:
- 上述代码中的核心代码已经标注,多画图分析即可
- 可以参考下面的二维数组的内存模型图进行分析:
至此,就学会了如何动态的申请二维数组了。一位数组的动态申请比较简单,之前的文章也有学习过。
4 总结
- C语言中只支持一维数组,所谓的二维数组在内存中依然是以一维数组的形式排布
- C语言中的数组大小,必须在编译期就作为常数确定。(毕竟数组的大小是数组类型的一部分,类型都不确定好,如何编译)
- 二维数组就是一个一维数组存的元素是同类型的数组而已