目录
1 数组名与指针的关系
1.1 数组名
1.2 对数组名取地址
1.3 数组名与指针的区别
1.3.1 类型不同
1.3.2 sizeof 操作符的行为不同
1.3.3 & 操作符的行为不同
1.3.4 自增自减运算的行为不同
1.3.5 可变性不同
2 使用指针遍历数组
2.1 使用 *(nums + index) 访问数组元素
2.2 使用指针和索引结合
2.3 使用 ptr[index] 访问数组元素
2.4 使用 for 循环与指针递增
2.5 使用 while 循环与条件判断
2.6 使用 do-while 循环
3 指针数组
3.1 定义格式
3.2 存储相同类型的指针数组
3.3 存储不同类型的指针数组
3.4 存储多个字符串的指针数组
3.4.1 字符串常量存储原理
4 数组指针
4.1 定义格式
4.2 数组指针的计算
4.3 数组指针的解引用
4.3.1 标准写法
4.3.2 简化写法
4.4 案例演示
4.5 数组名和数组指针的区别
4.5.1 指向不同
4.5.2 类型不同
4.5.3 可变性不同
4.5.4 初始化不同
4.5.5 访问元素方式不同
4.6 作用一:函数参数传递数组
4.7 作用二: 访问和操作数组的一部分
5 字符指针
5.1 定义格式
5.2 指向字符变量
5.3 指向字符数组
5.4 指向字符串字面量
5.5 字符数组和字符指针表示字符串的区别
5.5.1 对字符数组的操作限制
5.5.2 字符指针的灵活性
5.5.3 修改方式不一样
6 测试题
1 数组名与指针的关系
1.1 数组名
在 C 语言中,数组名在大多数情况下(除了 sizeof 和 & 操作符)会被隐式地转换为指向数组第一个元素的指针。这意味着,当我们在表达式中使用数组名时,它实际上会被解释为一个指向数组首元素的指针。
#include <stdio.h>
int main()
{
// 创建一个包含 5 个整数的数组
int nums[5] = {10, 20, 30, 40, 50};
// 创建一个指针并初始化为数组第一个元素的地址
int *ptr1 = &nums[0];
// 数组名在大多数情况下会被隐式地转换为指向数组第一个元素的指针
// 所以可以像指针一样使用
int *ptr2 = nums;
// 数组名中存储了第一个元素的地址
// 为了代码规范,这里把所有的指针都强制转换为了 (void *) 类型
printf("数组名 nums: 地址 %p,值 %d \n", (void *)nums, *nums);
printf("第一个元素 nums[0]: 地址 %p,值 %d \n", (void *)&nums[0], nums[0]);
printf("指针 ptr1: 地址 %p,值 %d \n", (void *)ptr1, *ptr1);
printf("指针 ptr2: 地址 %p,值 %d \n\n", (void *)ptr2, *ptr2);
// 比较地址
if (ptr1 == ptr2)
{
printf("ptr1 和 ptr2 相等 \n\n"); // 会输出
}
else
{
printf("ptr1 和 ptr2 不相等 \n\n");
}
// 直接比较
if (nums == &nums[0])
{
printf("nums 和 &nums[0] 相等 \n\n"); // 会输出
}
else
{
printf("nums 和 &nums[0] 不相等 \n\n");
}
return 0;
}
输出结果如下所示:
1.2 对数组名取地址
在 C 语言中,对数组名取地址 (&array) 和直接使用数组名 (array) 都会产生一个地址,虽然在数值上他们相同,但它们的类型和行为有所不同。
数组名 array:
- 类型:数组名 array 的类型是 T[N],其中 T 是数组中元素的类型,N 是数组的长度。
- 值:数组名 array 的值是数组的首元素的地址,即 &array[0]。
- 行为:数组名可以像普通指针一样使用,例如 array + 1 会向后移动一个元素的长度。
对数组名取地址 &array:
- 类型:&array 的类型是 T(*)[N],即指向包含 N 个 T 类型元素的数组的指针。
- 值:&array 的值也是数组的首元素的地址,即 &array[0]。
- 行为:&array 是一个指向整个数组的指针,&array + 1 会向后移动整个数组的长度。
第一个元素的地址 &array[0]:
- 类型:&array[0] 的类型是 T*,即指向 T 类型元素的指针。
- 值:&array[0] 的值是数组的首元素的地址。
- 行为:&array[0] 可以像普通指针一样使用,例如 &array[0] + 1 会向后移动一个元素的长度。
#include <stdio.h>
int main()
{
// 创建一个包含 5 个整数的数组
int nums[5] = {10, 20, 30, 40, 50};
// 输出数组名、数组名取地址和第一个元素地址的值(数值上一样)
printf("数组名 nums 的地址: %p\n", (void *)nums);
printf("数组名取地址 &nums 的地址: %p\n", (void *)&nums);
printf("第一个元素地址 &nums[0] 的地址: %p\n\n", (void *)&nums[0]);
// 输出数组名 + 1 和数组名取地址 + 1 的值
// nums + 1 会向后移动 4 个字节(一个 int 元素的长度)
printf("数组名 nums + 1 的地址: %p\n", (void *)(nums + 1));
// &nums + 1 会向后移动 20 个字节(一个包含 5 个 int 元素的数组的长度)
printf("数组名取地址 &nums + 1 的地址: %p\n", (void *)(&nums + 1));
// &nums[0] + 1 会向后移动 4 个字节(一个 int 元素的长度)
printf("第一个元素地址 &nums[0] + 1 的地址: %p\n", (void *)(&nums[0] + 1));
return 0;
}
输出结果如下所示:
1.3 数组名与指针的区别
虽然数组名在大多数情况下(除了 sizeof 和 & 操作符)会被隐式地转换为指向数组第一个元素的指针,但数组名与真正的指针之间仍然存在一些重要的区别。这些区别主要体现在以下几个方面:
1.3.1 类型不同
- 数组名:数组名的类型是 T[N],其中 T 是数组中元素的类型,N 是数组的长度。
- 指针:指针的类型是 T*,即指向 T 类型元素的指针。
int arr[5]; // 数组名 arr 的类型是 int[5]
int *ptr; // 指针 ptr 的类型是 int*
1.3.2 sizeof 操作符的行为不同
- 数组名:sizeof 操作符返回整个数组的大小(以字节为单位)。
- 指针:sizeof 操作符返回指针本身的大小(通常是 4 或 8 个字节,取决于系统架构)。
int arr[5];
int *ptr = arr;
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 输出 20(5 * sizeof(int))
printf("sizeof(ptr) = %zu\n", sizeof(ptr)); // 输出 4 或 8(指针的大小)
1.3.3 & 操作符的行为不同
- 数组名:& 操作符返回指向整个数组的指针(数组指针),类型为 T(*)[N]。
- 指针:& 操作符返回指向指针本身的指针(二级指针),类型为 T**。
int arr[5];
int *ptr = arr;
printf("&arr 的类型: %p\n", (void*)&arr); // 返回指向整个数组的指针,类型为 int(*)[5]
printf("&ptr 的类型: %p\n", (void*)&ptr); // 返回指向指针本身的指针(二级指针),类型为 int**
1.3.4 自增自减运算的行为不同
- 数组名:数组名不能进行自增 (++) 或自减 (--) 运算,因为它是不可变的。
- 指针:指针可以进行自增和自减运算,用于遍历数组或其他操作。
int arr[5];
int *ptr = arr;
// 错误:不能对数组名进行自增运算
// arr++;
// 正确:可以对指针进行自增运算
ptr++;
1.3.5 可变性不同
- 数组名:数组名是不可变的,不能重新赋值,它始终指向数组的首元素地址。
- 指针:指针是可变的,可以重新赋值,指向不同的地址。
int arr[5];
int *ptr = arr;
// 错误:不能对数组名重新赋值
// arr = &arr[1];
// 正确:可以对指针重新赋值
ptr = &arr[1];
以下是一个示例代码,综合演示了数组名与指针之间的这些区别:
#include <stdio.h>
int main()
{
// 创建一个包含 5 个整数的数组
int nums[5] = {10, 20, 30, 40, 50};
// 创建一个指针并初始化为数组第一个元素的地址
int *ptr = nums; // 或者 int *ptr = &nums[0]
// 1. 类型不同
printf("1.类型不同:\n");
printf("数组名 nums 的类型: int[%d]\n", 5); // 数组名的类型是 int[5]
printf("指针 ptr 的类型: int*\n"); // 指针的类型是 int*
// 2. sizeof 操作符的行为不同
printf("\n2. sizeof 操作符的行为不同:\n");
printf("sizeof(nums)返回数组总字节长度:%zu\n", sizeof(nums)); // 输出 20(5 * sizeof(int))
printf("sizeof(ptr)返回指针大小:%zu\n", sizeof(ptr)); // 输出 4 或 8(指针的大小)
// 3. & 操作符的行为不同
printf("\n3. & 操作符的行为不同:\n");
printf("&nums 返回指向整个数组的指针,其类型为: int(*)[%d], 地址: %p\n", 5, (void *)&nums);
// 返回指向整个数组的指针,类型为 int(*)[5]
printf("&ptr 返回指向指针本身的指针(二级指针),其类型为: int**, 地址: %p\n", (void *)&ptr);
// 返回指向指针本身的指针(二级指针),类型为 int**
// 4. 自增自减运算的行为不同
printf("\n4. 自增自减运算的行为不同:\n");
printf("不能对数组名进行自增自减运算,但可以对指针进行自增自减运算\n");
// 错误:不能对数组名进行自增自减运算
// nums++;
// 正确:可以对指针进行自增自减运算
printf("ptr 自增前: %p,值:%d\n", (void *)ptr, *ptr);
ptr++;
printf("ptr 自增后: %p,值:%d\n", (void *)ptr, *ptr);
printf("ptr 自减前: %p,值:%d\n", (void *)ptr, *ptr);
ptr--;
printf("ptr 自减后: %p,值:%d\n", (void *)ptr, *ptr);
// 5. 可变性不同
printf("\n5. 可变性不同:\n");
printf("不能对数组名重新赋值,可以对指针重新赋值\n");
// 错误:不能对数组名重新赋值
// nums = &nums[1];
// 正确:可以对指针重新赋值
printf("ptr 重新赋值前地址: %p,值:%d\n", (void *)ptr, *ptr);
ptr = &nums[2];
printf("ptr 重新赋值后(ptr = &nums[2])地址: %p,值:%d\n", (void *)ptr, *ptr);
printf("ptr 再次重新赋值前地址: %p,值:%d\n", (void *)ptr, *ptr);
ptr = &nums[4];
printf("ptr 再次重新赋值后(ptr = &nums[4])地址: %p,值:%d\n", (void *)ptr, *ptr);
return 0;
}
输出结果如下所示:
2 使用指针遍历数组
2.1 使用 *(nums + index) 访问数组元素
- nums 是数组名,隐式转换为指向数组首元素的指针。
- nums + index 通过指针算术计算出指向数组中第 index 个元素的指针。
- *(nums + index) 通过解引用操作符 * 获取该指针所指向的值。
int arr[] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
printf("%d ", *(arr + i));
}
2.2 使用指针和索引结合
- ptr 是一个指向数组首元素的指针。
- ptr + index 通过指针算术计算出指向数组中第 index 个元素的指针。
- *(ptr + index) 通过解引用操作符 * 获取该指针所指向的值。
int arr[] = {1, 2, 3, 4, 5};
int *ptr = arr;
for (int i = 0; i < 5; i++) {
printf("%d ", *(ptr + i));
}
2.3 使用 ptr[index] 访问数组元素
- ptr[index] 实际上等价于 *(ptr + index)。
int arr[] = {1, 2, 3, 4, 5};
int *ptr = arr;
for (int i = 0; i < 5; i++) {
printf("%d ", ptr[i]);
}
2.4 使用 for 循环与指针递增
通过在每次迭代时增加指针,直到指针达到数组末尾的位置。
int arr[] = {1, 2, 3, 4, 5};
int *ptr = arr; // 或者 int *ptr = &arr[0];
for (int i = 0; i < 5; i++, ptr++) {
printf("%d ", *ptr);
}
2.5 使用 while 循环与条件判断
通过指针递增并在每次迭代前检查指针是否已到达数组末尾。
int arr[] = {1, 2, 3, 4, 5};
int *ptr = arr;
int *end = arr + 5; // 指向数组最后一个元素的下一个位置
while (ptr != end) {
printf("%d ", *ptr);
ptr++;
}
2.6 使用 do-while 循环
确保循环体至少执行一次,适合于那些需要至少处理一个元素的情况。
int arr[] = {1, 2, 3, 4, 5};
int *ptr = arr;
int *end = arr + 5;
do {
printf("%d ", *ptr);
} while (++ptr != end);
3 指针数组
指针数组(Pointer Array)是一个数组,其中的每个元素都是指针。指针数组常用于存储多个字符串(字符数组)的地址,或者存储不同类型数据的地址。指针数组的声明和使用与其他类型的数组类似,但每个元素都是一个指针类型。
3.1 定义格式
数据类型 *指针数组名[长度];
- 数据类型:指针所指向的数据类型,例如 int、char、double、void 等。
- 指针数组名:指针数组的名称。
- 长度:指针数组的长度,即数组中指针的数量。
3.2 存储相同类型的指针数组
#include <stdio.h>
int main()
{
// 创建一个长度为 3 的指针数组,用于存储指向整数的指针
int *ptrArr[3];
// 创建三个整型变量,并分别初始化为 10, 20, 30
int num1 = 10, num2 = 20, num3 = 30;
// 将指针数组的每个元素分别指向之前创建的三个整数变量
ptrArr[0] = &num1; // ptrArr[0] 指向 num1
ptrArr[1] = &num2; // ptrArr[1] 指向 num2
ptrArr[2] = &num3; // ptrArr[2] 指向 num3
// 遍历指针数组,打印每个元素的索引、指针值(即地址)以及指针指向的值
for (int i = 0; i < 3; i++)
{
// %d 用于打印整数(这里是索引 i)
// %p 用于打印指针(即地址,ptrArr[i] 是指向整数的指针)
// *ptrArr[i] 是解引用操作,获取指针指向的值,再用 %d 打印
printf("元素索引:%d, 地址:%p, 元素值:%d \n", i, ptrArr[i], *ptrArr[i]);
}
return 0;
}
输出结果如下所示:
3.3 存储不同类型的指针数组
指针数组不仅可以存储相同类型的指针,还可以存储不同类型的指针。为了安全地使用这些指针,通常需要使用 void 指针,并在使用时进行适当的类型转换。
#include <stdio.h>
int main()
{
// 声明一个 void 指针数组
void *ptrs[3];
// 声明不同类型的变量
int num = 42;
double pi = 3.14159;
char ch = 'A';
// 将不同类型的变量的地址赋值给指针数组
ptrs[0] = #
ptrs[1] = π
ptrs[2] = &ch;
// 遍历指针数组并打印每个变量的值
printf("int: %d\n", *((int *)ptrs[0])); // int: 42
printf("double: %f\n", *((double *)ptrs[1])); // double: 3.141590
printf("char: %c\n", *((char *)ptrs[2])); // char: A
return 0;
}
3.4 存储多个字符串的指针数组
#include <stdio.h>
int main() {
// 声明一个指针数组,每个元素都是一个指向 char 的指针
char *strs[5];
// 初始化指针数组,每个元素指向一个字符串常量
strs[0] = "Hello";
strs[1] = "World";
strs[2] = "C";
strs[3] = "Programming";
strs[4] = "Language";
// 遍历指针数组并打印每个字符串
for (int i = 0; i < 5; i++) {
printf("%s\n", strs[i]);
}
return 0;
}
3.4.1 字符串常量存储原理
#include <stdio.h>
int main()
{
// 创建一个指针数组,每个元素都是指向 char 类型的指针
char *strs[5] = {
"Hello",
"World",
"C",
"Programming",
"Language"};
// 获取数组的长度
// 注意:在 C 语言中,strlen 函数用于计算字符串的长度,
// 但它并不适用于直接获取字符串指针数组的长度。
// strlen 只能用于计算单个以空字符 \0 结尾的字符串的长度。
// int n = strlen(strs);
int n = sizeof(strs) / sizeof(strs[0]);
// 输出指针数组中的每个字符串及其地址
printf("指针数组中的字符串:\n");
for (int i = 0; i < n; i++)
{
printf("strs[%d] = %s, 地址 %p\n", i, strs[i], (void *)strs[i]);
}
// 修改指针数组中的一个元素
// strs[1] 现在存储的是 "C++" 的地址,而不是 "World" 的地址
strs[1] = "C++";
// 再次输出指针数组中的每个字符串及其地址
printf("\n修改后的指针数组中的字符串:\n");
for (int i = 0; i < n; i++)
{
printf("strs[%d] = %s, 地址 %p\n", i, strs[i], (void *)strs[i]);
}
return 0;
}
输出结果如下所示:
字符串常量存储原理:
在 C 语言中,字符串常量(如 "Hello", "World", "C++" 等)通常存储在程序的只读数据段(read-only data segment)中。编译器在编译时将这些字符串常量存储到只读数据段中,并在运行时将这些字符串常量的地址赋值给相应的指针。这些字符串在编译时就已经确定,且它们的地址在程序运行期间是固定的。
指针数组本身是连续的,因为数组的每个元素(即每个指针)在内存中都是连续存储的。但是,这些指针所指向的字符串在内存中并不一定是连续的。它们可以位于内存的任何位置,这取决于编译器和操作系统的内存分配策略。
解释为什么 strs[1] 的元素内容和地址发生了变化:
- 初始状态:
- strs[1] 被初始化为指向字符串常量 "World" 的地址。
- 字符串常量 "World" 存储在只读数据段中,strs[1] 存储的是该字符串常量的地址。
- 修改 strs[1]:
- 当执行 strs[1] = "C++"; 时,strs[1] 不再指向 "World",而是指向新的字符串常量 "C++"。
- 字符串常量 "C++" 也在只读数据段中有一个固定的地址,strs[1] 现在存储的是这个新地址。
- 结果:
- strs[1] 的内容发生了变化,因为它现在指向了一个新的字符串常量 "C++"。
- strs[1] 所指向的地址也发生了变化,因为它现在存储的是 "C++" 的地址,而不是 "World" 的地址。
提示:
在 C 语言中,strlen 函数用于计算字符串的长度,但它并不适用于直接获取字符串指针数组的长度。strlen 只能用于计算单个以空字符 \0 结尾的字符串的长度。
4 数组指针
数组指针(Array Pointer)是一个指针,它指向一个数组。与普通的指针不同,数组指针指向的是整个数组的地址,而不是数组的第一个元素的地址(同上文提到的对数组名取地址一样: &数组名)。虽然数组指针和指向数组第一个元素的指针在数值上可能相同,但在某些运算中会表现出不同的行为。
4.1 定义格式
数据类型 (*数组指针名)[长度];
int arr[5] = {10, 20, 30, 40, 50};
int(*ptr)[5] = &arr; // 定义与初始化
- 数据类型:指针所指向的数组中元素的数据类型,例如 int、char、double 等。
- 数组指针名:数组指针的名称。
- 长度:数组的长度,即数组中元素的数量。
4.2 数组指针的计算
普通指针 + 1 会向后移动一个元素的长度。具体来说,如果指针指向一个 int 类型的元素,每个 int 占用 4 个字节,那么 + 1 会向后移动 4 个字节。
例如,对于 int 类型的普通指针 int *ptr:
- 每个 int 占用 4 个字节。
- 因此,ptr + 1 会向后移动 4 个字节。
数组指针 + 1 会向后移动整个数组的长度。具体来说,如果数组指针指向一个包含 N 个元素的数组,每个元素的大小为 sizeof(元素类型),那么 + 1 会向后移动 N * sizeof(元素类型) 个字节。
例如,对于 int 类型的数组指针 int (*ptr)[5]:
- 每个 int 占用 4 个字节。
- 数组包含 5 个 int,所以整个数组占用 5 * 4 = 20 个字节。
- 因此,ptr + 1 会向后移动 20 个字节。
4.3 数组指针的解引用
数组指针在使用时需要解引用才能访问数组中的元素。这是因为数组指针指向的是整个数组的地址,而不是数组的第一个元素的地址。为了访问数组中的元素,需要先解引用数组指针,然后再使用下标访问元素。
4.3.1 标准写法
(*ptr)[index]
- (*ptr):解引用数组指针,得到整个数组。
- [index]:使用下标访问数组中的元素。
4.3.2 简化写法
虽然标准的解引用写法 (*ptr)[index] 是清晰且正确的,但在某些情况下,C 和 C++ 允许使用更简洁的语法。特别是,当有一个指向数组的指针,并且只是想访问数组中的元素时,可以使用指针算术来简化:
ptr[0][index];
ptr[0] 实际上是对 ptr 的解引用,ptr[0] 等同于 *(ptr + 0),而 *(ptr + 0) 又等同于 *ptr。然后通过 [index] 进行索引,可以访问数组中的第 index 个元素。
然而,这种写法依赖于对指针算术和数组内存布局的理解,可能会让代码的可读性变差。因此,在团队项目中或代码维护时,推荐使用标准的解引用写法 (*ptr)[index] 以保持代码的清晰和一致性。
4.4 案例演示
#include <stdio.h>
int main()
{
// 创建一个包含 5 个整数的数组
int arr[5] = {10, 20, 30, 40, 50};
// 创建一个数组指针,指向整个数组 arr
// &arr 表示整个数组的地址
int(*ptr)[5] = &arr;
// int(*ptr)[5] = arr; // 会有警告,因为 arr 的类型是 int*,而 ptr 的类型是 int(*)[5],虽然它们的值可能相同,但类型不匹配。虽然编译器会进行类型转换,但是做法不推荐!
// 输出数组名和数组指针的地址
// 下面四个数值是相同的,因为它们都指向数组的起始地址
printf("arr起始地址:%p \n", (void *)arr);
printf("&arr的地址:%p \n", (void *)&arr);
printf("arr第一个元素的地址:%p \n", (void *)&arr[0]);
printf("ptr起始地址:%p \n\n", (void *)ptr);
// 数组指针指向的是整个数组的地址,而不是第一个元素的地址
// 数组指针 + 1 会向后移动 4 * 5 = 20 个字节(一个数组的长度)
// 数组 + 1 会向后移动 4 个字节(一个元素的长度)
printf("arr+1后的地址:%p \n", (void *)(arr + 1));
printf("ptr+1后的地址:%p \n\n", (void *)(ptr + 1));
// 使用数组指针遍历数组
// (*ptr) 解引用数组指针,得到整个数组,然后使用 [i] 访问数组中的元素
printf("标准解引用写法:\n");
for (int i = 0; i < 5; i++)
{
printf("(*ptr)[%d] = %d, 地址 %p\n", i, (*ptr)[i], (void *)&(*ptr)[i]);
}
// 简化写法
printf("\n简化解引用写法:\n");
for (int i = 0; i < 5; i++)
{
printf("ptr[0][%d] = %d, 地址 %p\n", i, ptr[0][i], (void *)&(ptr[0][i]));
}
return 0;
}
输出结果如下所示:
4.5 数组名和数组指针的区别
4.5.1 指向不同
- 数组名:
- 指向数组的第一个元素的地址。
- 例如,对于 int arr[5],arr 等同于 &arr[0],指向数组的第一个元素。
- 数组指针:
- 指向整个数组的地址。
- 例如,对于 int (*ptr)[5],ptr 指向整个数组 arr。
4.5.2 类型不同
- 数组名:
- 类型是 T[N],其中 T 是数组中元素的类型,N 是数组的长度。
- 假设数组包含 5 个 int 类型的元素,类型是 int [5]。
- 数组指针:
- 类型是 T(*)[N],其中 T 是数组中元素的类型,N 是数组的长度。
- 假设指向包含 5 个 int 类型元素的数组的指针,类型是 int (*)[5]。
4.5.3 可变性不同
- 数组名:
- 通常是不可变的,不能更改其指向的地址。
- 例如,arr = &another_arr; 是非法的,因为 arr 是一个常量指针。
- 数组指针:
- 是可变的,可以更改其指向的地址,使其指向不同的数组,但数组类型和长度要一致。
- 例如,int(*ptr)[5] = &arr; ptr = &another_arr; 是合法的,只要 another_arr 也是包含 5 个 int 的数组。
4.5.4 初始化不同
- 数组名:
- 不需要显式初始化,它会自动指向数组的首元素。
- 例如,int arr[5] = {1, 2, 3, 4, 5};,arr 自动指向 arr[0]。
- 数组指针:
- 需要显式初始化,指定它将指向的数组。
- 例如,int (*ptr)[5] = &arr; 或 int (*ptr)[5]; ptr = &arr;。
4.5.5 访问元素方式不同
- 数组名:
- 访问数组元素不需要解引用,可以直接使用下标访问。
- 例如,arr[0] 访问数组的第一个元素。
- 数组指针:
- 访问数组元素通常需要解引用,使用 (*ptr)[i] 或 ptr[0][i] 的形式。
- 例如,(*ptr)[0] 或 ptr[0][0] 访问数组的第一个元素。
#include <stdio.h>
int main()
{
// 创建一个包含 5 个整数的数组
int arr[5] = {10, 20, 30, 40, 50};
// 创建另一个包含 5 个整数的数组
int another_arr[5] = {60, 70, 80, 90, 100};
// 1. 指向不同
printf("1. 指向不同:\n");
printf("arr 指向的地址: %p\n", (void *)arr); // 指向数组的第一个元素
printf("&arr 指向的地址: %p\n", (void *)&arr); // 指向整个数组
printf("another_arr 指向的地址: %p\n", (void *)another_arr); // 指向另一个数组的第一个元素
printf("&another_arr 指向的地址: %p\n", (void *)&another_arr); // 指向另一个数组的整个数组
// 2. 类型不同
printf("\n2. 类型不同:\n");
printf("arr 的类型: int [5]\n");
printf("(&arr) 的类型: int (*)[5]\n");
// 3. 可变性不同
printf("\n3. 可变性不同:\n");
printf("数组名是常量指针,不能重新赋值\n数组指针可以重新赋值\n");
// arr = &another_arr; // 错误:数组名是常量指针,不能重新赋值
int(*ptr)[5] = &arr; // 正确:数组指针可以重新赋值
ptr = &another_arr; // 正确:数组指针可以重新赋值
// 4. 初始化不同
printf("\n4. 初始化不同:\n");
printf("arr 自动初始化为指向数组的第一个元素\n");
printf("ptr 需要显式初始化: int (*ptr)[5] = &arr;\n");
// 5. 访问元素方式不同
printf("\n5. 访问元素方式不同:\n");
printf("使用数组名访问元素:\n");
printf("arr[0]: %d\n", arr[0]); // 访问数组的第一个元素
printf("arr[4]: %d\n", arr[4]); // 访问数组的最后一个元素
printf("使用数组指针访问元素:\n");
printf("(*ptr)[0]: %d\n", (*ptr)[0]); // 访问数组的第一个元素
printf("(*ptr)[4]: %d\n", (*ptr)[4]); // 访问数组的最后一个元素
printf("ptr[0][0]: %d\n", ptr[0][0]); // 访问数组的第一个元素
printf("ptr[0][4]: %d\n", ptr[0][4]); // 访问数组的最后一个元素
// 重新赋值后访问元素
ptr = &arr;
printf("重新赋值后使用数组指针访问元素 (指向 arr):\n");
printf("(*ptr)[0]: %d\n", (*ptr)[0]); // 访问 arr 的第一个元素
printf("ptr[0][4]: %d\n", ptr[0][4]); // 访问 arr 的最后一个元素
ptr = &another_arr;
printf("重新赋值后使用数组指针访问元素 (指向 another_arr):\n");
printf("(*ptr)[0]: %d\n", (*ptr)[0]); // 访问 another_arr 的第一个元素
printf("ptr[0][4]: %d\n", ptr[0][4]); // 访问 another_arr 的最后一个元素
return 0;
}
输出结果如下所示:
4.6 作用一:函数参数传递数组
当需要将一个数组作为参数传递给函数时,可以使用数组指针。这样,函数可以访问和操作整个数组。
#include <stdio.h>
// 函数声明:打印一个包含 10 个整数的数组
void printArray(int (*arr)[10], int size)
{
// 遍历数组并打印每个元素
for (int i = 0; i < size; i++)
{
// arr[0][i] 访问数组的第 i 个元素
// 因为 arr 是一个指向包含 10 个 int 的数组的指针,所以 arr[0] 是整个数组
// printf("%d ", arr[0][i]); // 简化解引用
printf("%d ", (*arr)[i]); // 标准解引用
}
// 打印换行符
printf("\n");
}
int main()
{
// 创建一个包含 10 个整数的数组
int myArray[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 调用 printArray 函数,传入数组的地址和数组的大小
// &myArray 是一个指向包含 10 个 int 的数组指针
printArray(&myArray, 10);
return 0;
}
当然,如果只需要遍历和访问数组的元素,也可以直接传递数组首元素的地址:
#include <stdio.h>
// 函数声明:打印一个包含 10 个整数的数组
void printArray(int *ptr, int size)
{
// 遍历数组并打印每个元素
for (int i = 0; i < size; i++)
{
printf("%d ", ptr[i]);
}
// 打印换行符
printf("\n");
}
int main()
{
// 创建一个包含 10 个整数的数组
int myArray[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 直接传递数组首元素地址
printArray(myArray, 10);
return 0;
}
两个程序的区别:
类型安全性:第一个程序在类型上更加明确,它指定了数组的大小。这可以在编译时提供额外的类型检查。如果尝试传递一个大小不是 10 的数组给第一个程序的 printArray 函数,编译器会报错。而第二个程序则更加灵活,它可以接受任何大小的整数数组,但这也意味着它失去了类型安全性。
语义清晰性:第一个程序的函数签名清晰地表明它接受一个指向包含 10 个整数的数组的指针。这有助于阅读代码的人理解函数的预期用途。而第二个程序的函数签名则更加通用,它只表明它接受一个指向整数的指针,这可能不够明确。
性能:在实际运行中,这两个程序的性能几乎没有区别。数组在内存中是连续存储的,所以无论是通过指向数组的指针还是通过指向数组首元素的指针来访问数组元素,都不会对性能产生显著影响。
4.7 作用二: 访问和操作数组的一部分
通过数组指针,可以指向数组的一部分,这在处理数组的子集时非常有用。
#include <stdio.h>
int main()
{
int myArray[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// 指向 myArray 的第 3 个元素开始的 5 个元素
int(*subArray)[5] = (int(*)[5]) & myArray[2];
for (int i = 0; i < 5; i++)
{
// printf("%d ", subArray[0][i]); // 简化解引用
printf("%d ", (*subArray)[i]); // 标准解引用
// 2 3 4 5 6
}
printf("\n");
return 0;
}
扩展:
数组指针除了上述两个作用外,还可以用于动态分配多维数组(后续章节进行讲解)。
数组指针在处理多维数组时特别有用,尤其是当数组的维度在运行时才确定时。例如,可以动态分配一个二维数组。
int rows = 3, cols = 4; int (*arr)[cols] = malloc(rows * sizeof(*arr)); if (arr != NULL) { // 使用arr数组 free(arr); }
5 字符指针
字符指针变量(简称字符指针)是 C 语言中的一种指针类型,它用于指向字符或字符串(字符数组)。字符指针通常用于处理字符串(字符数组),可以方便地进行字符串的读取、修改和传递。
5.1 定义格式
字符指针的定义方式与其它类型的指针类似,只是类型指定为 char。
char *charPtr; // charPtr 是一个指向 char 类型的指针。
5.2 指向字符变量
字符指针可以指向单个字符变量:
#include <stdio.h>
int main()
{
// 定义一个字符变量
char ch = 'A';
// 定义一个字符指针,并让它指向字符变量
char *ptr = &ch;
// 通过指针访问和修改字符变量
printf("Character: %c\n", *ptr); // 输出 A
// 修改字符变量
*ptr = 'B';
printf("Modified character: %c\n", *ptr); // 输出 B
return 0;
}
5.3 指向字符数组
#include <stdio.h>
#include <string.h>
int main()
{
char str[] = "Hello, World!";
char *ptr = str;
while (*ptr != '\0')
{
printf("%c", *ptr);
ptr++;
}
printf("\n");
return 0;
}
5.4 指向字符串字面量
字符串字面量通常存储在只读内存段。因此,直接让字符指针指向字符串字面量通常是不安全的,因为尝试修改这些内存内容可能会导致未定义行为,所以建议在前面加上 const 进行修饰:这表示 pointerStr 所指向的内容是常量,不能通过 pointerStr 修改这些内容,但是可以修改指针的指向。
#include <stdio.h>
#include <string.h>
int main()
{
// 使用字符数组定义字符串
char arrayStr[] = "Hello, Array!";
// 使用字符指针定义字符串
// 推荐在前面加上 const ,这表示 pointerStr 所指向的内容是常量,不能通过 pointerStr 修改这些内容。
// 但是可以修改指针的指向
const char *pointerStr = "Hello, Pointer!";
// 输出字符数组定义的字符串
printf("字符数组定义的字符串: %s\n", arrayStr); // Hello, Array!
// 输出字符指针定义的字符串
printf("字符指针定义的字符串: %s\n\n", pointerStr); // Hello, Pointer!
// 修改字符数组中的内容
// 1. 单个字符修改很麻烦
printf("1. 单个字符修改很麻烦,需要注意字符串的结束符\n");
arrayStr[7] = '1';
arrayStr[8] = '2';
arrayStr[9] = '3';
printf("没添加结束符的情况:%s\n", arrayStr); // Hello, 123ay!
arrayStr[10] = '\0';
printf("添加字符串结束符后:%s\n\n", arrayStr); // Hello, 123
// 2. 使用 strcpy 函数修改字符串
printf("2. 使用 strcpy 函数修改字符串,注意字符数组的长度\n");
strcpy(arrayStr, "new Array!");
printf("使用 strcpy 函数修改后的字符数组: %s\n", arrayStr); // new Array!
// 尝试修改字符指针指向的字符串(不推荐,会导致未定义行为)
// 下面这行代码会被注释掉,以避免编译器警告或运行时错误
// pointerStr[0] = 'M'; // 这行代码会导致未定义行为
printf("\n尝试修改字符指针指向的字符串(不推荐,会导致未定义行为)\n");
// 3. 重新赋值字符指针,使其指向新的字符串
pointerStr = "new Pointer!";
printf("重新赋值(指向其他字符串常量)后的字符指针: %s\n", pointerStr); // new Pointer!
return 0;
}
输出结果如下所示:
5.5 字符数组和字符指针表示字符串的区别
5.5.1 对字符数组的操作限制
字符数组名是一个指向数组首元素的常量指针,这意味着不能将一个新的字符串直接赋值给字符数组名,只能对数组中的各个元素进行赋值,通常使用 strcpy 函数或其他逐个元素赋值的方法来修改字符数组的内容。
#include <stdio.h>
#include <string.h>
int main()
{
// 使用字符数组定义字符串
char arrayStr[50] = "Hello, Array!";
// 使用字符指针定义字符串
const char *pointerStr = "Hello, Pointer!";
// 输出初始的字符串
printf("初始的字符数组: %s\n", arrayStr); // Hello, Array!
printf("初始的字符指针: %s\n", pointerStr); // Hello, Pointer!
// 修改字符数组中的内容
strcpy(arrayStr, "new Array!");
printf("修改后的字符数组: %s\n", arrayStr); // new Array!
// 重新赋值字符指针,使其指向新的字符串
pointerStr = "Modified Pointer!";
printf("重新赋值后的字符指针: %s\n", pointerStr); // Modified Pointer!
// 尝试直接赋值一个新的字符串给字符数组(会导致编译错误)
// arrayStr = "New String!"; // 这行代码会导致编译错误
// 逐个元素赋值修改字符数组的内容
char newString[] = "New String!";
for (int i = 0; i < strlen(newString); i++)
{
arrayStr[i] = newString[i];
}
arrayStr[strlen(newString)] = '\0';// 确保字符串以 null 结尾
printf("逐个元素赋值后的字符数组: %s\n", arrayStr); // New String!
return 0;
}
输出结果如下所示:
5.5.2 字符指针的灵活性
字符指针是一个可变的指针,可以重新赋值,指向新的字符串。
#include <stdio.h>
int main()
{
// 使用字符指针定义字符串
const char *pointerStr1 = "Hello, Pointer 1!";
const char *pointerStr2 = "Hello, Pointer 2!";
// 输出初始的字符串
printf("初始的 pointerStr1: %s\n", pointerStr1); // Hello, Pointer 1!
printf("初始的 pointerStr2: %s\n", pointerStr2); // Hello, Pointer 2!
// 重新赋值字符指针,使其指向新的字符串
pointerStr1 = "Modified Pointer 1!";
pointerStr2 = "Modified Pointer 2!";
// 输出重新赋值后的字符串
printf("重新赋值后的 pointerStr1: %s\n", pointerStr1); // Modified Pointer 1!
printf("重新赋值后的 pointerStr2: %s\n", pointerStr2); // Modified Pointer 2!
// 尝试修改字符指针指向的字符串内容(不推荐,会导致未定义行为)
// 下面这行代码会被注释掉,以避免编译器警告或运行时错误
// pointerStr1[0] = 'M'; // 这行代码会导致未定义行为
return 0;
}
输出结果如下所示:
5.5.3 修改方式不一样
字符数组:
- 数组名是一个指向数组首元素的常量指针,不能重新赋值。
- 数组内容可以被修改,通常使用 strcpy 函数或其他逐个元素赋值的方法。
- 修改数组内容不会改变数组的地址。
字符指针:
- 指针本身是可以重新赋值的,可以指向不同的字符串。
- 指针所指向的内容如果是字符串字面量,则是常量,不能通过指针修改这些内容。
- 重新赋值指针不会修改原内容,只是指针指向了新的字符串。
#include <stdio.h>
#include <string.h>
int main()
{
// 使用字符数组定义字符串
char arrayStr[50] = "Hello, Array!";
// 使用字符指针定义字符串
const char *pointerStr = "Hello, Pointer!";
// 输出初始的字符串和地址
printf("初始的字符数组: %s, 地址: %p\n", arrayStr, (void *)arrayStr);
printf("初始的字符指针: %s, 地址: %p\n", pointerStr, (void *)pointerStr);
// 修改字符数组中的内容
strcpy(arrayStr, "new Array!");
printf("修改后的字符数组: %s, 地址: %p\n", arrayStr, (void *)arrayStr);
// 重新赋值字符指针,使其指向新的字符串
pointerStr = "Modified Pointer!";
printf("重新赋值后的字符指针: %s, 地址: %p\n", pointerStr, (void *)pointerStr);
// 再次输出初始的字符串和地址,以验证是否修改了原始字符串
printf("再次输出初始的字符数组: %s, 地址: %p\n", arrayStr, (void *)arrayStr);
printf("再次输出初始的字符指针: %s, 地址: %p\n", pointerStr, (void *)pointerStr);
return 0;
}
输出结果如下所示:
6 测试题
1. 请写出下面程序的运行结果。
int arr[5] = {10, 20, 30, 40, 50};
int *ptr1 = &arr[0];
int(*ptr2)[5] = &arr;
printf("%s", (arr+1 == ptr1+1) ? "OK " : "NoOK ");
printf("%s", (arr+1 == ptr2+1) ? "Good " : "NoGood ");
【答案】OK NoGood
【解析】
(1)ptr1 指向数组的首元素地址,ptr2 指向整个数组的地址。
(2)arr+1 的结果是数组第二个元素的地址,ptr1+1 得到的是数组第二个元素的地址,所以两个表达式相等,输出 OK。
(3)arr+1 的结果是数组第二个元素的地址,ptr2 + 1 得到的是数组之外的地址,所以两个表达式不相等,输出 NoGood。
2. 指针数组和数组指针有什么不同?
【答案】指针数组是一个数组,其每个元素都是指针。数组指针是一个指针,它指向一个数组。
3. 请写出下面程序的运行结果
char msg[] = "Hello World";
char *ptr = msg;
ptr = "Hello Tom";
msg[1] = 'a';
printf("%s", ptr);
【答案】 Hello Tom
【解析】
(1)创建字符数组 msg 。
(2)字符指针 ptr 指向字符数组 msg 的首元素。
(3)字符指针 ptr 指向了一个新的字符串常量。
(4)修改字符数组 msg 的第二个元素,但此时字符指针 ptr 和 字符数组 msg 已经没有关系了。最终输出字符指针所指向的值,结果是 Hello Tom。