33 指针与数组:数组名与指针的关系、使用指针遍历数组、数组指针、指针数组、字符指针

目录​​​​​​​

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, 30int num1 = 10, num2 = 20, num3 = 30;// 将指针数组的每个元素分别指向之前创建的三个整数变量ptrArr[0] = &num1; // ptrArr[0] 指向 num1ptrArr[1] = &num2; // ptrArr[1] 指向 num2ptrArr[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] = &num;ptrs[1] = &pi;ptrs[2] = &ch;// 遍历指针数组并打印每个变量的值printf("int: %d\n", *((int *)ptrs[0]));       // int: 42printf("double: %f\n", *((double *)ptrs[1])); // double: 3.141590printf("char: %c\n", *((char *)ptrs[2]));     // char: Areturn 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); // 输出 Breturn 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。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1551629.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

硬布线控制器与微程序控制器

硬布线控制器和微程序控制器都是控制单元&#xff08;CU&#xff09;的实现方式&#xff0c;用于协调和控制计算机系统的操作。它们的主要区别在于控制信号的生成方式&#xff1a; 硬布线控制器 (Hardwired Controller): 概念: 硬布线控制器使用组合逻辑电路直接生成控制信号…

自己做个国庆75周年头像生成器

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 下载相关代码&#xff1a;【免费】《自己做个国庆75周年头像生成器》代码资源-CSDN文库 又是一年国庆节&#xff0c;今年使用国旗做…

《情书》你的名字,是最美的情书

《情书》你的名字&#xff0c;是最美的情书 岩井俊二&#xff0c;日本电影导演&#xff0c;作家及记录片导演。被誉为日本最有潜质的新近“映像作家”&#xff0c;也有中国影迷称他为“日本王家卫”。影像清新独特、感情细腻丰富。&#xff08;来自豆瓣&#xff09; 穆晓芳 译 …

总结C/C++中内存区域划分

目录 1.C/C程序内存分配主要的几个区域&#xff1a; 2.内存分布图 1.C/C程序内存分配主要的几个区域&#xff1a; 1、栈区 2、堆区 3、数据段&#xff08;静态区&#xff09; 4.代码段 2.内存分布图 如图&#xff1a; static修饰静态变量成员——放在静态区 int globalVar 是…

【学习笔记】 陈强-机器学习-Python-Ch15 人工神经网络(2)Keras

文章目录 前言一、Keras二、使用Kears 估计回归问题的神经网络1. 载入、处理数据2. 数据预处理&#xff1a;归一化3. 设定一系列随机数种子4. 定义了一个简单的深度神经网络5. 训练模型6. 查看训练结果7. 使用最优轮数&#xff08;index1&#xff09;重新估计 此神经网络模型8.…

小程序原生-列表渲染

1. 列表渲染的基础用法 <!--渲染数组列表--> <view wx:for"{{numList}}" wx:key"*this" > 序号&#xff1a;{{index}} - 元素&#xff1a;{{item}}</view> <!--渲染对象属性--> <view wx:for"{{userInfo}}" wx:key&q…

springboot实战学习(10)(ThreadLoacl优化获取用户详细信息接口)(重写拦截器afterCompletion()方法)

接着学习。之前的博客的进度&#xff1a;完成用户模块的注册接口的开发以及注册时的参数合法性校验、也基本完成用户模块的登录接口的主逻辑的基础上、JWT令牌"的组成与使用、完成了"登录认证"&#xff08;生成与验证JWT令牌&#xff09;以及完成获取用户详细信…

Java中异常的认识和创建

文章目录 前言一、异常的概念与体系结构 1.1 异常的概念1.2 异常的体系结构1.3 异常的分类二、异常的处理 2.1.防御式编程2.2 异常的抛出2.3 异常的捕获2.4 异常的处理流程三、自定义异常类 一、异常的概念与体系结构 1.1 异常的概念 在生活中&#xff0c;一个人表情痛苦&…

51单片机的智能垃圾桶【proteus仿真+程序+报告+原理图+演示视频】

1、主要功能 该系统由AT89C51/STC89C52单片机LCD1602显示模块超声波传感器红外传感器步进电机按键、蜂鸣器、LED等模块构成。适用于智能自动感应垃圾桶、超声波智能垃圾桶等相似项目。 可实现基本功能: 1、LCD1602实时显示桶内垃圾高度 2、超声波传感器采集桶顶到垃圾的距离…

Temporal Dynamic Quantization for Diffusion Models阅读

文章目录 AbstractIntroductionBackgrounds and Related Works2.1 扩散模型2.2 量化2.3 量化感知训练和训练后量化 TemporalDynamic Quantization3.1 量化方法3.2 扩散模型量化的挑战3.3 TDQ模块的实现3.4 工程细节时间步的频率编码TDQ模块的初始化 Experimental SetupResults5…

C99中的变长数组

在C99标准之前&#xff0c;从语言在创建数组的时候&#xff0c;指定数组的大小只能使用常量和表达式&#xff0c;或者数据初始化的时候&#xff0c;可以省略数组大小。 1.int arr[5]{1,2,3,4,4}; 2.int arr[24]{1,2,3,4,5,6}; 3.int arr[]{1,2,3,3,4,5,6}; 这样的语法限制不够灵…

使用 ModelScope Studio 实现可滚动 ChatBot

前面的文章提到过 Gradio 的 Chatbot 不能自动滚动的问题&#xff0c;最近看到了 ModelScope Studio 扩展的一些组件&#xff0c;其中 Chatbot 就完美解决了自动滚动的问题&#xff0c;同时还增加了很多更细化的功能&#xff0c;例如可以设置用户和 AI 的头像。官方文档&#x…

QCamera6.7笔记

1.QCamera​ .h文件 #include <QtWidgets/QMainWindow> #include "ui_QCamera_test1.h" #include <QCamera> #include <QtMultimedia> #include <QtMultimediaWidgets> #include<QMediaCaptureSession> #include <QMediaDevices&…

渗透测试之密码暴力破解工具medusa美杜莎

《网安面试指南》http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247484339&idx1&sn356300f169de74e7a778b04bfbbbd0ab&chksmc0e47aeff793f3f9a5f7abcfa57695e8944e52bca2de2c7a3eb1aecb3c1e6b9cb6abe509d51f&scene21#wechat_redirect 《Java代码审…

C语言指针详解与应用(不断更新)

指针简介 指针(Pointer)是C语言的一个重要知识点&#xff0c;其使用灵活、功能强大&#xff0c;是C语言的灵魂 指针与底层硬件联系紧密&#xff0c;使用指针可操作数据的地址&#xff0c;实现数据的间接访问 指针生活实例化 指针的本质是地址&#xff0c;在生活中比如你取快…

C++快速入门

文章目录 C快速入门一、命名空间1.初始C2.概念3.命名空间的定义1.普通的命名空间2.命名空间的嵌套3.命名空间的重名问题3.命名空间的展开 二、C的输入&输出三、缺省参数1.全缺省参数2.半缺省参数3.缺省参数的用途4.缺省参数的注意点 四、函数重载1.函数重载的原则2.以下的函…

【RocketMQ】RocketMQ应用难点

&#x1f3af; 导读&#xff1a;本文探讨了RocketMQ中消息重复消费的问题及其解决方案&#xff0c;尤其是在CLUSTERING模式下的扩容影响。文章分析了重复消费的原因&#xff0c;如广播模式、负载均衡模式下的多consumerGroup消费、消费者组内的动态变化及网络延迟等&#xff0c…

婚恋交友系统该如何做才能做到日进斗金?

要使婚恋交友系统实现盈利并做到日进斗金&#xff0c;需要综合考虑市场需求、用户体验、商业模式和营销策略等多个方面。以下是一些建议&#xff0c;旨在帮助构建一个成功且盈利的婚恋交友系统&#xff1a; 深入了解目标市场&#xff1a; 研究目标用户群体的需求、偏好和行为模…

这 5 个自动化运维场景,可能用 Python 更香?

许多运维工程师会使用 Python 脚本来自动化运维任务。Python 是一种流行的编程语言&#xff0c;具有丰富的第三方库和强大的自动化能力&#xff0c;适用于许多不同的领域。 这里插播一条粉丝福利&#xff0c;如果你正在学习Python或者有计划学习Python&#xff0c;想要突破自我…

10款好用的开源 HarmonyOS 工具库

大家好&#xff0c;我是 V 哥&#xff0c;今天给大家分享10款好用的 HarmonyOS的工具库&#xff0c;在开发鸿蒙应用时可以用下&#xff0c;好用的工具可以简化代码&#xff0c;让你写出优雅的应用来。废话不多说&#xff0c;马上开整。 1. efTool efTool是一个功能丰富且易用…