文章目录
一、普通数组与指针数组
- 普通数组 :
- 例如
int arr[5];,这是一个简单的一维数组,它包含5个int类型的元素。从类型角度看,arr的类型是int [5],表示一个包含5个int元素的数组。在内存中,这些元素是连续存储的,数组名arr在大多数情况下代表数组的首地址。 - 当我们使用
arr作为表达式时,在很多情况下它会"衰减"为一个指向数组首元素的指针,类型变为int *。例如,当把arr传递给一个函数时,函数接收到的实际是一个int *类型的指针,指向数组的首元素。
- 例如
- 指针数组 :
- 定义形式为
类型 *数组名[数组大小];,例如int *pArr[3];。这里pArr是一个数组,它的元素类型是int *,即每个元素都是一个指向int类型的指针。 - 假设我们有三个
int变量a = 1, b = 2, c = 3,可以这样使用pArr:int *pArr[3] = {&a, &b, &c};,此时pArr[0]指向a,pArr[1]指向b,pArr[2]指向c。这种数组主要用于存储多个指针,方便对这些指针所指向的对象进行操作。
- 定义形式为
二、二维数组与数组指针(指向数组的指针)
- 二维数组 :
- 例如
int twoDArr[3][4];,这是一个二维数组,它的类型是int [3][4],可以看作是一个包含3个元素的数组,每个元素又是一个包含4个int元素的数组。在内存中,它的存储方式是按行存储,即先存储第一行的4个元素,再存储第二行的元素,以此类推。 - 当我们使用
twoDArr作为表达式时,它也会衰减,不过它衰减后的类型是int (*)[4](指向包含4个int元素的数组的指针)。例如,在函数参数传递中,如果有一个函数接收一个二维数组作为参数,形式参数可以写成void func(int (*p)[4]),这里p就是一个数组指针,它可以接收二维数组twoDArr作为实参。
- 例如
- 数组指针(指向数组的指针) :
- 定义形式为
类型 (*指针变量名)[数组大小];,如int (*p)[4];。这种指针是专门用于指向一个包含特定数量元素(这里是4个int元素)的数组。 - 假设我们有一个二维数组
int arr[3][4],可以通过以下方式使用数组指针:
- 定义形式为
c
int (*p)[4];
p = arr;
for(int i = 0; i < 3; i++){
for(int j = 0; j < 4; j++){
// 访问二维数组元素
printf("%d ",(*(p + i))[j]);
}
printf("\n");
}
- 这里`p`指向`arr`的首行(即一个包含4个`int`元素的数组),`p + i`指向第`i`行,`(*(p + i))[j]`就相当于`arr[i][j]`,
- 通过这种方式可以使用数组指针来访问二维数组。
三、字符数组与字符指针
- 字符数组 :
- 例如
char str1[] = "hello";,这是一个字符数组,它的类型是char [6](包括字符串结束符\0)。字符数组在内存中有自己的存储空间,用于存储字符序列。 - 可以通过下标来访问数组中的字符,如
str1[0]表示字符'h'。而且字符数组可以通过strcpy等函数来修改其中的内容。
- 例如
- 字符指针 :
- 定义形式为
char *str2;,如果str2 = "world";,这里str2是一个字符指针,它指向一个字符串常量(在C语言中,字符串常量存储在只读数据区)。其类型是char *。 - 虽然可以通过
str2来访问字符串中的字符,如printf("%c", str2[0]);会输出'w',但是不能通过str2来修改字符串常量的内容(在标准C语言环境下,这样做是未定义行为)。如果想修改字符串内容,需要通过字符数组来实现。例如,char str3[] = "test";,可以通过str3[0]='T';来修改。
- 定义形式为
四、数组名和&数组名的区别
1、数组名
- 代表首元素地址(大部分情况) :在C和C++语言中,数组名在大多数情况下会"衰减"为指向数组首元素的指针。例如,对于
int arr[5];,arr通常等价于&arr[0],它们的类型都是int*。这意味着可以使用数组名来访问数组中的元素,就像使用指针一样。例如,可以通过*(arr + 1)来访问arr[1]。 - 类型信息(数组类型) :尽管数组名在很多操作中表现得像指针,但它本身的类型是"数组类型"。对于
int arr[5];,arr的类型是int [5]。这个类型信息在一些特定的场景下会体现出来,比如在使用sizeof运算符时,sizeof(arr)返回的是整个数组所占用的字节数(在这个例子中是5 * sizeof(int)),而不是指针的大小。 - 作为函数参数 :当数组名作为函数参数传递时,它会被转换为指向数组首元素的指针。例如,有一个函数
void func(int a[]),实际上它等价于void func(int *a)。这是因为在函数调用过程中,传递的是数组的首元素地址,而不是整个数组的副本。
2、取地址数组名(&数组名)
- 代表整个数组的地址 :对于
int arr[5];,&arr返回的是整个数组的地址。它的类型是int (*)[5],这是一个指向包含5个int元素的数组的指针。与arr(大部分情况下等价于&arr[0])不同,&arr的地址值在数值上虽然和&arr[0]相同,但它们的类型和含义是不同的。 - 在指针运算中的差异 :因为
&arr是整个数组的地址,所以在指针运算中会有不同的行为。例如,如果p是一个指向整个数组的指针(类型为int (*)[5]),那么p + 1会跳过整个数组的长度(即5 * sizeof(int)字节),而如果q是一个指向数组首元素的指针(类型为int*),那么q + 1只会跳过一个元素的长度(即sizeof(int)字节)。 - 在函数参数传递中的特殊用途 :当想要传递整个数组(而不是仅仅传递数组的首元素地址)给一个函数时,可以使用指向数组的指针作为参数类型。例如,
void func(int (*p)[5])这个函数可以接收一个指向包含5个int元素的数组的指针。在调用这个函数时,可以使用func(&arr),这里&arr就是整个数组的地址。这种方式在处理多维数组等复杂情况时非常有用,能够更准确地操作数组。
3、举例
- 示例代码
c
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int *p1 = arr;
int (*p2)[5] = &arr;
// 输出数组名和取地址数组名的值(地址值)
printf("arr: %p\n", (void *)arr);
printf("&arr: %p\n", (void *)&arr);
// 指针运算的区别
printf("p1 + 1: %p\n", (void *)(p1 + 1));
printf("p2 + 1: %p\n", (void *)(p2 + 1));
// 通过数组名和取地址数组名访问数组元素
printf("*(arr + 2): %d\n", *(arr + 2));
printf("(*(&arr))[2]: %d\n", (*(&arr))[2]);
return 0;
}
- 运行结果:

- 详细讲解
- 指针定义部分
int *p1 = arr;:这里将p1定义为一个int类型的指针,并将arr赋值给p1。由于数组名arr在这种情况下会衰减为指向首元素的指针,所以p1现在指向arr的第一个元素1。int (*p2)[5] = &arr;:p2被定义为一个指向包含5个int元素的数组的指针,然后将&arr(整个数组的地址)赋值给p2。
- 输出地址值部分
printf("arr: %p\n", (void *)arr);和printf("&arr: %p\n", (void *)&arr);:这两行代码分别输出数组名arr的值(也就是首元素的地址)和取地址数组名&arr(整个数组的地址)。在很多情况下,这两个地址值在数值上是相同的,但它们的类型不同。
- 指针运算部分
printf("p1 + 1: %p\n", (void *)(p1 + 1));:因为p1是指向int类型的指针,所以p1 + 1会使指针向后移动一个int类型元素的大小。在这个例子中,如果int类型占4个字节,那么p1 + 1指向的地址会比p1的地址增加4个字节,也就是指向数组中的第二个元素2。printf("p2 + 1: %p\n", (void *)(p2 + 1));:p2是指向包含5个int元素的数组的指针,所以p2 + 1会使指针向后移动一个包含5个int元素的数组的大小。如果int类型占4个字节,那么p2 + 1指向的地址会比p2的地址增加5 * 4 = 20个字节,这就跳过了整个arr数组。
- 访问数组元素部分
printf("*(arr + 2): %d\n", *(arr + 2));:这里利用数组名arr衰减为指向首元素的指针的特性,arr + 2表示指向数组中第三个元素的指针,然后通过*(arr + 2)来获取这个元素的值,即3。printf("(*(&arr))[2]: %d\n", (*(&arr))[2]);:首先&arr是整个数组的地址,*(&arr)就相当于arr(整个数组),然后(*(&arr))[2]就相当于arr[2],所以输出的值也是3。
- 指针定义部分