文章目录
一、二维数组
1、二维数组的存储本质与指针关系
- 存储布局:在C语言中,二维数组在内存中是按行存储的。例如,对于二维数组
int a[2][3]
,它的存储顺序是先存储第一行的元素a[0][0]
、a[0][1]
、a[0][2]
,然后再存储第二行的元素a[1][0]
、a[1][1]
、a[1][2]
。从内存角度看,它可以看作是一个连续的内存块,总共有2 * 3 = 6
个int
类型的元素。 - 指针表示:二维数组名本身是一个指向数组的指针。对于
int a[2][3]
,a
是一个指向包含3个int
元素的数组的指针。可以把a
看作是一个指针,它指向a[0]
(a[0]
是一个包含3个int
元素的数组)。如果将a
的值(它是一个地址)赋给一个指针变量,例如int (*p)[3]=a;
,这里p
是一个指向包含3个int
元素的数组的指针,p
和a
在类型上是相同的。
2、通过指针访问二维数组元素
- 行指针方式
- 利用指向数组的指针来访问元素。例如,对于
int a[2][3]
和int (*p)[3]=a;
,可以通过(*(p + i))[j]
来访问a[i][j]
。这里p + i
使指针p
移动i
个“包含3个int
元素的数组”的单位,*(p + i)
得到第i
行数组,然后[j]
操作访问该行数组中的第j
个元素。 - 例如,假设
a[0][0]=1; a[0][1]=2; a[0][2]=3; a[1][0]=4; a[1][1]=5; a[1][2]=6;
,如果int (*p)[3]=a;
,那么(*(p + 1))[2]
的值就是6
(即a[1][2]
)。
- 利用指向数组的指针来访问元素。例如,对于
- 普通指针方式
- 可以将二维数组看作是一个一维数组,其中每个元素又是一个指针(指向每行的数组)。例如,对于
int a[2][3]
,可以定义一个指针int *q=(int *)a;
。此时,访问a[i][j]
可以通过*(q + i * 3 + j)
来实现。 - 解释一下这个公式,
i * 3
是因为每行有3个元素,先跳过i
行,然后再加上j
就可以访问到a[i][j]
这个元素。例如,对于前面定义的数组a
,如果int *q=(int *)a;
,那么*(q + 1 * 3+2)
的值就是6
(即a[1][2]
)。
- 可以将二维数组看作是一个一维数组,其中每个元素又是一个指针(指向每行的数组)。例如,对于
3、二维数组作为函数参数与指针的应用
- 传递二维数组参数
- 当二维数组作为函数参数时,函数形参可以声明为指向数组的指针。例如,一个函数用于打印二维数组的元素:
void printArray(int (*arr)[3], int rows) { for (int i = 0; i < rows; i++) { for (int j = 0; j < 3; j++) { printf("%d ", (*(arr + i))[j]); } printf("\n"); } }
- 可以这样调用这个函数:
int a[2][3] = { {1, 2, 3}, {4, 5, 6} }; printArray(a, 2);
。在函数调用时,二维数组名a
会被隐式转换为指向包含3个int
元素的数组的指针,传递给printArray
函数的arr
参数。
- 当二维数组作为函数参数时,函数形参可以声明为指向数组的指针。例如,一个函数用于打印二维数组的元素:
- 灵活的参数传递方式
- 也可以将二维数组参数转换为指针数组来处理。例如,定义一个函数,它的参数是一个指针数组和行数、列数:
void printArray2(int **arr, int rows, int cols) { for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { printf("%d ", arr[i][j]); } printf("\n"); } }
- 调用这个函数需要先将二维数组转换为指针数组的形式。例如:
int a[2][3] = { {1, 2, 3}, {4, 5, 6} }; int *p[2]; for (int i = 0; i < 2; i++) { p[i]=a[i]; } printArray2(p, 2, 3);
- 这种方式在处理动态分配的二维数组(如使用
malloc
分配的二维数组)或者不规则的二维数组(每行元素个数可能不同)时比较有用。
- 也可以将二维数组参数转换为指针数组来处理。例如,定义一个函数,它的参数是一个指针数组和行数、列数:
通过以上从存储本质、元素访问和函数参数等多个角度对二维数组和指针关系的剖析,可以更深入地理解C语言中二维数组的工作原理,并且能够更加灵活地运用指针来操作二维数组。
二、指针数组
1、概念
- 指针数组是一个数组,其元素是指针类型。也就是说,数组中的每个元素都存储着一个内存地址。在C语言中,它的定义形式为
类型 *数组名[数组大小]
。例如int *arr[5];
,这就定义了一个包含5个元素的指针数组arr
,每个元素都是一个指向int
类型的指针。
2、本质
- 从内存角度看,指针数组在内存中是连续存储各个指针元素的。这些指针元素本身存储的是其他变量或者数组等的地址。它和普通数组一样,有自己的基地址,通过这个基地址和索引可以访问到每个指针元素。
- 本质上,指针数组是一种数据结构,用于集中管理多个指针,这些指针可以指向不同的内存区域,并且可以根据需要灵活地操作这些指针所指向的数据。
3、应用场景和示例
- 字符串数组(字符指针数组)
- 在C语言中,没有专门的字符串类型,字符串通常是以字符数组或者字符指针的形式存在。当处理多个字符串时,使用指针数组是一种很方便的方式。
- 例如:
#include <stdio.h> int main() { char *fruits[] = {"apple", "banana", "cherry"}; int size = sizeof(fruits)/sizeof(fruits[0]); for (int i = 0; i < size; i++) { printf("%s\n", fruits[i]); } return 0; }
- 在这个例子中,
fruits
是一个指针数组,它的每个元素(fruits[0]
、fruits[1]
、fruits[2]
)都是一个字符指针,分别指向存储字符串"apple"
、"banana"
、"cherry"
的内存区域。当使用printf("%s\n", fruits[i]);
时,%s
格式说明符会根据指针fruits[i]
所指向的字符数组(字符串)的起始地址,从这个地址开始逐个字符地打印,直到遇到字符串结束符'\0'
。
- 函数指针数组
- 用于存储多个函数的指针,这样可以根据不同的条件调用不同的函数,增加程序的灵活性。
- 例如,有两个函数用于计算两个数的和与差:
#include <stdio.h> int add(int a, int b) { return a + b; } int subtract(int a, int b) { return a - b; } int main() { int (*funcPtrs[])(int, int) = {add, subtract}; int a = 5, b = 3; printf("加法结果:%d\n", funcPtrs[0](a, b)); printf("减法结果:%d\n", funcPtrs[1](a, b)); return 0; }
- 在这个例子中,
int (*funcPtrs[])(int, int)
定义了一个函数指针数组funcPtrs
,它的元素是指向返回值为int
,参数为两个int
类型的函数的指针。数组funcPtrs
的第一个元素funcPtrs[0]
指向add
函数,第二个元素funcPtrs[1]
指向subtract
函数。通过funcPtrs[0](a, b)
和funcPtrs[1](a, b)
这样的方式可以调用相应的函数。
- 指向动态分配内存的指针数组
- 可以用于管理多个动态分配的内存区域。
- 例如,动态分配多个
int
类型的数组:
#include <stdio.h> #include <stdlib.h> int main() { int *nums[3]; for (int i = 0; i < 3; i++) { nums[i] = (int *)malloc(sizeof(int) * 5); if (nums[i] == NULL) { printf("内存分配失败!\n"); return 1; } } // 对动态分配的数组进行赋值等操作 for (int i = 0; i < 3; i++) { for (int j = 0; j < 5; j++) { nums[i][j] = i * 5 + j; } } // 打印动态分配数组的值 for (int i = 0; i < 3; i++) { for (int j = 0; j < 5; j++) { printf("%d ", nums[i][j]); } printf("\n"); } // 释放动态分配的内存 for (int i = 0; i < 3; i++) { free(nums[i]); } return 0; }
- 在这里,
nums
是一个指针数组,通过循环为每个指针元素分配了一个包含5个int
元素的动态内存空间。然后可以像使用二维数组一样(通过nums[i][j]
)对这些动态分配的内存进行赋值和访问。最后,通过循环释放每个动态分配的内存区域,避免内存泄漏。
4、注意事项
- 内存管理:如果指针数组中的指针指向动态分配的内存,那么需要注意正确地释放这些内存。在上面动态分配内存的例子中,必须逐个释放每个指针所指向的内存区域,否则会导致内存泄漏。
- 指针初始化:在使用指针数组时,要确保数组中的指针被正确初始化。如果指针没有初始化就进行解引用操作,可能会导致程序崩溃或者出现不可预测的行为。例如,在定义了
int *arr[5];
后,如果没有给arr
中的指针元素赋值,直接访问*arr[0]
是错误的。 - 指针类型匹配:指针数组中的指针类型要与所指向的数据类型相匹配。例如,不能将一个指向
double
类型的指针存储在一个定义为int *
类型的指针数组中。
三、二级指针
1、概念
- 在C语言中,指针是用来存储变量地址的变量。二级指针是指向指针的指针,即它存储的是另一个指针变量的地址。如果一级指针(普通指针)可以理解为间接访问一个变量,那么二级指针就是通过两级间接访问来操作变量。
2、本质
- 从内存角度看,二级指针本身也占用内存空间,其内容是另一个指针的内存地址。例如,对于一个
int
类型的变量a
,有一个一级指针p
指向a
(*p = a
),那么二级指针pp
可以指向p
(**pp = a
,通过*pp
可以访问p
,通过**pp
可以访问a
)。 - 二级指针本质上是对指针的更高层次的间接引用,用于在需要操作指针本身的地址或者构建更复杂的数据结构(如指针数组)时使用。
3、应用
- 函数参数传递指针的地址:当需要在函数内部修改指针变量本身时,就需要传递指针的地址,也就是使用二级指针作为函数参数。
- 例如,动态内存分配函数
malloc
返回一个指向所分配内存块的指针。如果要在一个函数中分配内存,并让调用者能够获取这个新分配的指针,就可以使用二级指针。
#include <stdio.h>
#include <stdlib.h>
void allocateMemory(int **ptr) {
*ptr = (int *)malloc(sizeof(int));
if (*ptr == NULL) {
printf("内存分配失败!\n");
return;
}
**ptr = 10;
}
int main() {
int *p = NULL;
allocateMemory(&p);
printf("分配的内存中的值为:%d\n", *p);
free(p);
return 0;
}
- 在这个例子中,
allocateMemory
函数接受一个二级指针int **ptr
。在函数内部,通过*ptr = (int *)malloc(sizeof(int));
为int
类型分配内存,并将分配后的内存地址赋值给*ptr
(也就是main
函数中的p
)。然后通过**ptr = 10;
将分配的内存中的值设置为10。在main
函数中,调用allocateMemory(&p)
,将p
的地址传递给函数,这样函数就可以修改p
本身的值。 - 处理指针数组:二级指针可以方便地用于处理指针数组。
- 例如,假设有一个字符串数组(实际上是一个字符指针数组),可以使用二级指针来遍历和操作这个数组。
#include <stdio.h>
void printStringArray(char **arr, int size) {
for (int i = 0; i < size; i++) {
printf("%s\n", arr[i]);
}
}
int main() {
char *strings[] = {"Hello", "World", "C Language"};
int size = sizeof(strings)/sizeof(strings[0]);
printStringArray(strings, size);
return 0;
}
- 在这个例子中,
printStringArray
函数接受一个二级指针char **arr
和数组大小size
。strings
数组是一个字符指针数组,当传递给printStringArray
函数时,会发生隐式转换,将strings
(类型为char *[]
)转换为char **
。在函数内部,通过arr[i]
(等同于*(arr + i)
)来访问每个字符串指针,然后通过printf("%s\n", arr[i]);
打印出字符串内容。
4、注意事项
- 解引用操作顺序:使用二级指针时,要清楚解引用的顺序。例如,对于二级指针
pp
,*pp
得到它所指向的一级指针,**pp
得到一级指针所指向的变量的值。如果解引用顺序错误,可能会导致程序出错,如段错误(访问非法内存地址)。 - 内存管理:当使用二级指针进行动态内存分配时,要注意内存的释放。如果在一个函数中分配了内存,需要确保在合适的地方(通常是调用者)正确地释放内存。否则可能会导致内存泄漏。在上面的
allocateMemory
例子中,在main
函数中通过free(p)
正确地释放了在allocateMemory
函数中分配的内存。 - 初始化:二级指针在使用前应该进行正确的初始化。如果没有初始化,它可能包含一个随机的内存地址,在解引用时可能会导致不可预测的结果。例如,在定义一个二级指针
int **pp;
后,最好先将其赋值为NULL
或者使其指向一个有效的一级指针。