在讲数组和指针之前,我想先讲讲关于变量的内存分配。
int a = 10;
变量在分配内存时发生了什么。
1.分配4个连续的内存地址(一个字节一个地址),
2.将10的二进制(32位由0或1写成的数字)
-
分成 4 个字节:00000000 | 00000000 | 00000000 | 00001010
-
每个字节占 8 位,存储在连续的 4 个地址中
但是C语言里:int 的地址是它的起始地址,通过这个地址可以访问到完整的 4 个字节。
所以对于地址为起始地址的标识符,利用指针变量,存储的都是其起始地址。
一、指针和数组的官方定义
一、什么是指针
指针也就是内存地址,指针变量是用来存放内存地址的变量。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:
type *var_name;
在这里,type 是指针的基类型,它必须是一个有效的 C 数据类型,var_name 是指针变量的名称。用来声明指针的星号 * 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。
c
int a = 10;
int *p = &a; // 把 a 的地址赋值给 p
p存的是a变量的地址,*p 是“解引用”——通过 p 里存的地址,去找到那个地址上存的值10;
那么用%p打印p会得到变量a的起始地址,一个十六位进制的数。
二、指针的运算
1.核心运算:加减法和自增自减
这是最常见的运算,步长完全取决于指针类型。
-
int *p; p + 1;:偏移 sizeof(int) 字节(通常4字节),指向下一个整数。
-
char *p; p + 1;:偏移 sizeof(char) 字节(固定1字节),指向下一个字符。
-
double *p; p + 1;:偏移 sizeof(double) 字节(通常8字节)。
代码直观对比(假设地址从 1000 开始):
int arr[5] = {10, 20, 30, 40, 50};
int *p_int = arr; // p_int = 1000
char *p_char = (char*)arr; // p_char = 1000
printf("%p\\n", p_int + 1); // 输出 1004 (跳过了4个字节)
printf("%p\\n", p_char + 1); // 输出 1001 (跳过了1个字节)
这完美解释了你上问中 &array + 1 为何跳过整个数组——因为 &array 的类型是“整个数组的指针”,sizeof(整个数组) 就是步长。
2. 指针相减(求间隔)
当两个指针指向同一块连续内存(比如同一个数组)时,它们可以相减,结果是一个 ptrdiff_t 类型的整数,代表中间隔了多少个元素,而不是字节数。
int arr[10];
int *p1 = &arr[0]; // 指向第0个
int *p2 = &arr[5]; // 指向第5个
printf("%ld\\n", p2 – p1); // 输出 5 (隔了5个元素)
printf("%ld\\n", p1 – p2); // 输出 -5 (顺序相反)
注意:如果两个指针指向毫无关联的独立变量(如 &a – &b),这种运算是未定义行为,虽然能编译但结果毫无意义,绝不要这样写。
3. 指针比较(判断先后)
指向同一数组的两个指针,可以使用关系运算符(>, <, >=, <=, ==)进行比较。
for (int *p = arr; p < arr + 5; p++) { // 合法且优雅
printf("%d ", *p); // 遍历数组
}
比较结果代表地址在内存空间中的先后次序。指向高地址的指针更大。
二、数组
一、数组的定义
定义: 数组是一种由相同类型元素构成的、非空的、在内存中连续存储的数据类型。
语法: int arr[n]; 定义了一个名为 arr 的数组,其包含 n个类型为 int类型 的元素。
特点:
-
它的类型由“元素类型”和“元素个数”共同决定。例如 int[10] 和 float[10] 是不同类型;int[5] 和 int[10] 也是不同类型。
-
它的总大小(字节数)被严格锁定为 元素个数 × 单个元素大小(即 sizeof(数组) = 元素个数 * sizeof(元素类型))。
-
它的内存地址是连续的。所有元素在内存中连续排列,地址依次递增。
二、数组名的定义(细讲)
在 C 语言中,数组名表示数组的地址,即数组首元素的地址。当我们在声明和定义一个数组时,该数组名就代表着该数组的地址。
例如,在以下代码中:
int myArray[5] = {10, 20, 30, 40, 50};
在这里,myArray 是数组名,它表示整数类型的数组,包含 5 个元素。myArray 也代表着数组的地址,即第一个元素的地址。
三、&数组名和&数组名[下标]
数组名本身是一个常量指针,意味着它的值是不能被改变的,一旦确定,就不能再指向其他地方。
我们可以使用&运算符来获取数组的地址,如下所示:
int myArray[5] = {10, 20, 30, 40, 50};
int *ptr = &myArray[0]; // 或者直接写作 int *ptr = myArray;
在上面的例子中,ptr 指针变量被初始化为 myArray 的地址,即数组的第一个元素的地址。
int arr[5]={1,2,3,4,5};
printf("%p",&arr);
&是取地址符,&arr,代表整个数组的起始内存地址。当数组名作为 & 运算符的操作数时,它不会退化为指向首元素的指针。这是C标准(C11)中一个明确的“豁免”规则。
其结果是一个指向整个数组的指针。
但是根据数组的定义,数组的元素地址分配是一个连续的,而计算机中规定,每一块连续区域的“起始地址”,指的就是它的第一个字节物理编号,即第一个元素的地址。
所以&arr和&arr[0]打印的值相同。
但是二者的内存长度不相同,sizeof &arr是整个内存的长度20个字节,而sizeof &arr[0]是4个字节(int型是4个字节)。
四、数组名的退化
在绝大多数表达式中,数组名会隐式转换为一个指向其首元素的指针,类型为 int*,且值为 &arr[0]。这个过程称为“数组退化”。
规则例外: 数组名不发生退化的两种特殊情况:
作为 sizeof 的操作数(sizeof(arr) 计算的是整个数组的大小)。
作为取地址运算符 & 的操作数(&arr 的类型是 int (*)[n])。
原因:& 的作用是“取地址”,它的产出是一个“指针”。如果arr进行退化,那么&arr相当于时**arr,进行两次“解引”。
数组名的退化现象
int arr[5] = {1, 2, 3, 4, 5};
// 情况1:表达式中的 arr 退化为了 int*
int *p = arr; // 编译通过,arr 退化为 &arr[0]
// 情况2:sizeof(arr) 中 arr 未退化,保留完整数组类型
printf("sizeof(arr) = %zu\\n", sizeof(arr)); // 输出 20
// 情况3:&arr 中 arr 未退化,得到的是整个数组的地址,类型 int(*)[5]
int (*q)[5] = &arr; // 编译通过,&arr 的类型为 int(*)[5]
五、arr 与 &arr 的深度对比
1. 值相同,类型不同
虽然 arr(退化为指针)和 &arr 打印出的地址数值相同,但它们属于完全不同的类型,这直接导致指针运算的步长截然不同。
-
arr 的类型:int*(指向单个 int 的指针)。
-
&arr 的类型:int (*)[N](指向“包含 N 个 int 的数组”的指针)。
int arr[5] = {1, 2, 3, 4, 5};
printf("arr = %p\\n", arr); // 假设输出 0x1000
printf("&arr = %p\\n", &arr); // 输出 0x1000 (值相同)
printf("arr + 1 = %p\\n", arr + 1); // 输出 0x1004 (跳过 1 个 int,4 字节)
printf("&arr + 1 = %p\\n", &arr + 1); // 输出 0x1014 (跳过整个数组,20 字节)
分析:
-
arr + 1:因为类型为 int*,指针加 1 增加 sizeof(int) 即 4 字节。
-
&arr + 1:因为类型为 int (*)[5],指针加 1 增加 5 * sizeof(int) 即 20 字节,越过整个数组。&会产生一个指针,所以&arr此刻是指针类型,sizeof &arr即8字节。
三、指针和数组之间的联系
一、相同点
相同点1:在绝大多数表达式中,数组名退化为首元素地址,其数值与指针变量保存的数值相同。
int arr[5] = {0};
int *p = arr;
printf("%p\\n", arr); // 输出:0x7ffc12345670
printf("%p\\n", &arr[0]); // 输出:0x7ffc12345670
printf("%p\\n", p); // 输出:0x7ffc12345670
//arr、&arr[0]、p 三个打印值完全相同。
相同点 2:都支持下标访问 []、解引用 *、与整数加减 +/-,且底层统一转换为地址运算。
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
// 下标访问
printf("%d\\n", arr[2]); // 输出:30
printf("%d\\n", p[2]); // 输出:30
// 解引用
printf("%d\\n", *arr); // 输出:10
printf("%d\\n", *p); // 输出:10
// 与整数加减
printf("%d\\n", *(arr + 3)); // 输出:40
printf("%d\\n", *(p + 3)); // 输出:40
// 事实:arr 和 p 在 []、*、+/- 三种运算上行为完全一致。
相同点 3:都支持关系比较运算符(==、!=、<、>、<=、>=),用于判断地址先后或是否相等。
int arr[5] = {0};
int *p = arr;
// 比较地址是否相等
if (arr == p) {
printf("arr == p 成立\\n"); // 输出:arr == p 成立
}
// 比较地址先后(指向同一数组时有效)
if (p < arr + 5) {
printf("p 在数组范围内\\n"); // 输出:p 在数组范围内
}
if (arr + 5 > p) {
printf("arr+5 在 p 之后\\n"); // 输出:arr+5 在 p 之后
}
// arr(退化后)和 p 均可以作为关系比较的操作数。
相同点 4:都可以作为逻辑条件(如 if、while)直接判断,非空(非NULL)为真,空(NULL)为假。
int arr[5] = {0};
int *p = NULL;
if (arr) {
printf("arr 为真\\n"); // 输出:arr 为真(数组地址恒非0)
}
if (p) {
printf("p 为真\\n"); // 不输出,因为 p 为 NULL
}
p = arr;
if (p) {
printf("p 重新赋值后为真\\n"); // 输出:p 重新赋值后为真
}
// 数组名退化的地址和指针变量都可以直接参与布尔判断。
相同点 5:作为函数实参传递时,传递的都是地址值(按值传递地址)。
void func(int *ptr) {
printf("接收到的地址:%p\\n", (void*)ptr);
}
int main() {
int arr[5] = {0};
int *p = arr;
func(arr); // 传递数组名,实际传入首元素地址
func(p); // 传递指针变量,传入其保存的地址值
// 输出两次地址完全相同。
// 无论传 arr 还是传 p,函数接收到的都是同一个地址数值。
二、不同点
不同点 1:sizeof 的结果不同。对数组名返回整个数组字节数;对指针变量返回指针自身字节数(64位系统为8)。
int arr[5] = {0};
int *p = arr;
printf("%zu\\n", sizeof(arr)); // 输出:20 (5个int × 4字节)
printf("%zu\\n", sizeof(p)); // 输出:8 (64位系统指针大小为8)
//若两者等价,sizeof 应该相同,但实际 20 != 8。
不同点 2:数组名不可作为左值被赋值;指针变量可作为左值被修改。
int arr[5] = {0};
int *p = arr;
int other[5] = {0};
p = other; // 合法,编译通过
// arr = other; // 编译报错(error: assignment to expression with array type)
// p 可以指向新地址,arr 不能指向新地址。
不同点 3:取地址 & 的结果类型不同。&arr 类型是 int (*)[5](数组指针);&p 类型是 int **(二级指针)。类型不同导致 +1 的跳跃步长不同。
int arr[5] = {0};
int *p = arr;
// &arr 的类型是 int (*)[5],+1 跳跃整个数组(20字节)
printf("%p\\n", &arr); // 假设输出:0x1000
printf("%p\\n", &arr + 1); // 输出:0x1014 (0x1000 + 20 字节)
// &p 的类型是 int **,+1 跳跃一个指针变量的大小(8字节)
printf("%p\\n", &p); // 假设输出:0x2000
printf("%p\\n", &p + 1); // 输出:0x2008 (0x2000 + 8 字节)
// &arr 和 &arr+1 相差20字节;&p 和 &p+1 相差8字节。数值变化证明类型不同。
不同点 4:作为函数形参时,数组声明会退化为指针声明。函数内部对形参使用 sizeof 得到的是指针大小,而非数组大小。
// 形参写为 int arr[5] 或 int arr[],编译器强制改写为 int *arr
void test(int arr[5]) {
printf("%zu\\n", sizeof(arr)); // 输出:8 (指针大小)
}
int main() {
int arr[5] = {0};
test(arr);
return 0;
}
// 若形参真的是数组,sizeof 应为20,但实际输出8,证明形参已退化成了指针变量。
四、arr、&arr、&arr[0]、指针p一张表带你总结
| arr | 0x1000 | int* | 4(1 个 int) | ❌ 不能(arr++ 非法) |
| &arr[0] | 0x1000 | int* | 4 | ❌ 不能(&arr[0]++ 非法) |
| &arr | 0x1000 | int (*)[5] | 20(整个数组) | ❌ 不能(&arr++ 非法) |
| p = arr | 0x1000 | int* | 4 | ✅ 能(p++ 合法) |
五、指针数组和数组指针
一、什么是指针数组
int *arr[n]
定义: 一个数组,其元素均为指向int 类型的指针。
运算优先级: [] 的优先级高于 *,因此 int *arr[n] 被解析为 (int *) arr[n],即 arr 是一个数组,元素类型为 int*。
内存占用: sizeof(arr) = n * sizeof(int*)。
int*是8个字节,因为是指针类型。
指针数组的创建与使用
// 指针数组
int num1=100,num2=200,num3=300;
printf("看一下变量地址%p\\n", &num1);
printf("看一下变量地址%p\\n",&num2);
printf("看一下变量地址%p\\n", &num3);
// 定义指针数组:
// 指针数组:p 是数组,里面有 3 个int*类型元素(3个地址)
// 一个存储三个地址的指针数组;
int *prt[3] = {&num1, &num2, &num3};
printf("那么prt占多少字节:%zu\\n",sizeof prt); // 3 * 8 = 24 (64位系统)
for (int i = 0; i < 3; i++)
{//尝试打印出里面的元素
printf("里面存储的是:%p\\n", prt[i]);//打印的一串16进制数,地址。
printf("数组的地址是:%p\\n", &prt[i]);//所以数组prt元素的地址是8个字节一个
printf("里面指针解引的值是:%d\\n", *prt[i]);//解引用—获取地址存储的值
printf("里面类型是:%zu\\n", sizeof prt[i]); // 8 指针类型
printf("里面类型是:%zu\\n", sizeof &prt); // 此时&prt是指针
//所以prt是数组,里面存储的是指针,各变量的地址
}
总结: 指针数组的每个元素都是一个独立的指针变量,可以指向不同的变量或内存。
二、什么是数组指针:int (*p)[n]
定义: 一个指针,指向一个包含 n 个 int类型元素的数组。
运算优先级: 使用括号 () 强制 * 先与变量名结合,即 (*p) 是指针,指向的类型是 int[n]。
内存占用: sizeof(p) 为一个指针的大小(64位系统下为 8 字节),无论数组元素多少。
案例6:数组指针的赋值与解引用
//创建数组指针
int arr[5] = {1, 2, 3, 4, 5};
// p 是一个指针,指向一个包含 5 个 int 的数组
int (*p)[5] = &arr;
// 解引用 p,得到整个数组实体(类型退化为 int*)
//&arr表示整个数组的地址,&运算符会产生一个指针。
// 注意:这里的解引用是将 p 指向的“整体数组”还原,而非访问元素。
printf("p 指向的数组首元素: %d\\n", **p);
// *p 得到数组首地址,再 * 得到第一个元素
// 使用下标访问数组指针指向的元素
for (int i = 0; i < 5; i++) {
printf("(*p)[%d] = %d\\n", i, (*p)[i]); // (*p)[i] 等价于 arr[i]
}
// 验证 p 的移动步长
printf("p 的值: %p\\n", p);
printf("p + 1 的值: %p\\n", p + 1); // 地址增加 20 字节,跳过整个数组
关于数组指针的一些知识点
1.p*的一些特性
int (*p)[5] = &arr;
*p 的类型是数组,但数组在 C 语言中有一个特性:在大多数表达式中,它会“退化”为指向其首元素的指针。在 C 语言的类型系统中,*p 此刻(作为表达式的结果)的类型是 int[5](数组类型),它不是一个指针。
-
当你写 *p 时,它代表那块 20 字节的内存,整个数组实体。
-
当你写 *p + 1 时,*p 遇到了“表达式运算”,它临时退化为 int*,指向第一个元素,然后加 1 指向第二个元素。
-
利用下标访问元素(语法糖 ——指针[下标],为*(指针+下标)),所以(*p)[n]===*((*p)+n),那么n=0时,**p是表示访问首元素。
2.*p和p打印地址值相同,但类型不同
-
p 是一个指针变量(有存储空间,存地址)。
-
*p 是一个数组实体(没有独立的存储空间,它是 p 指向的那块内存的别名)。
*p 代表的是 p 所指向的那个完整的数组对象。
3.sizeof *p和sizeof arr是相同的;sizeof p和sizeof &arr是相同的;
总结: 数组指针 p 存储的是数组整体的起始地址,*p 代表该数组实体,(*p)[i] 用于访问第 i 个元素。




