当前位置: 首页 > news >正文

C语言第13节:指针(3)

1. 回调函数

回调函数的基本思想是,将函数指针作为参数传递给另一个函数,并在需要时通过这个函数指针调用对应的函数。这种方式允许一个函数对执行的内容进行控制,而不需要知道具体的实现细节。

回调函数在以下场景中尤为有用:

  1. 事件驱动编程:例如处理按键事件或鼠标点击。
  2. 排序或过滤:可以将比较函数作为参数传递给排序函数。
  3. 异步操作:例如定时器到时后调用某个处理函数。

1.1 实现回调函数的步骤

实现回调函数主要有以下几个步骤:

  1. 定义一个回调函数:编写一个可以被回调的函数,这个函数的签名需要与回调机制所期望的签名一致。
  2. 定义函数指针:定义一个可以指向回调函数的函数指针。
  3. 调用函数并传递回调函数指针:在需要执行回调函数的地方,通过传递回调函数的地址来调用它。

1.2 回调函数实例

上一节我们讲解了转移表来改善我们的简易计算器,那我们能不能通过回调函数来实现呢?

1.2.1 改造前的代码

//使用回调函数改造前
#include <stdio.h>
int add(int a, int b)
{return a + b;
}
int sub(int a, int b)
{return a - b;
}
int mul(int a, int b)
{return a * b;
}
int div(int a, int b)
{return a / b;
}
int main()
{int x, y;int input = 1;int ret = 0;do{printf("*************************\n");printf(" 1:add2:sub \n");printf(" 3:mul4:div \n");printf("*************************\n");printf("请选择:");scanf("%d", &input);switch (input){case 1:printf("输入操作数:");scanf("%d %d", &x, &y);ret = add(x, y);printf("ret = %d\n", ret);break;case 2:printf("输入操作数:");scanf("%d %d", &x, &y);ret = sub(x, y);printf("ret = %d\n", ret);break;case 3:printf("输入操作数:");scanf("%d %d", &x, &y);ret = mul(x, y);printf("ret = %d\n", ret);break;case 4:printf("输入操作数:");scanf("%d %d", &x, &y);ret = div(x, y);printf("ret = %d\n", ret);break;case 0:printf("退出程序\n");break;default:printf("选择错误\n");break;}} while (input);return 0;
}

我们发现每一个case中的代码有很大一部分都是重复的,那么这一部分可以用回调函数来改造一下

1.2.2 改造后的代码

//使用回到函数改造后
#include <stdio.h>
int add(int a, int b)
{return a + b;
}
int sub(int a, int b)
{return a - b;
}
int mul(int a, int b)
{return a * b;
}
int div(int a, int b)
{return a / b;
}
void calc(int(*pf)(int, int))
{int ret = 0;int x, y;printf("输入操作数:");scanf("%d %d", &x, &y);ret = pf(x, y);printf("ret = %d\n", ret);
}
int main()
{int input = 1;do{printf("*************************\n");printf(" 1:add2:sub \n");printf(" 3:mul4:div \n");printf("*************************\n");printf("请选择:");scanf("%d", &input);switch (input){case 1:calc(add);break;case 2:calc(sub);break;case 3:calc(mul);break;case 4:calc(div);break;case 0:printf("退出程序\n");break;default:printf("选择错误\n");break;}} while (input);return 0;
}

1.2.3 代码解析

这段代码的核心在于使用回调函数 calc 统一处理不同的运算操作,从而简化了 main 函数中的逻辑。

1.2.3.1 定义 calc 回调函数

calc 函数使用了函数指针 pf,可以接收任意符合 int (int, int) 签名的函数。calc 函数的作用是通用地处理不同的运算逻辑,而不直接关心运算的具体实现,这使得代码更加灵活。

void calc(int (*pf)(int, int)) {int ret = 0;int x, y;printf("输入操作数:");scanf("%d %d", &x, &y);ret = pf(x, y);  // 调用传入的回调函数printf("ret = %d\n", ret);
}
  • 输入操作数:提示用户输入两个操作数 xy
  • 调用回调函数:通过 pf(x, y) 调用传入的运算函数。
  • 输出结果:将结果 ret 输出给用户。
1.2.3.2 主函数 main

main 函数的作用是提供用户交互界面,根据用户的选择调用对应的运算函数。

int main() {int input = 1;do {printf("*************************\n");printf(" 1:add2:sub \n");printf(" 3:mul4:div \n");printf("*************************\n");printf("请选择:");scanf("%d", &input);switch (input) {case 1:calc(add);  // 将加法函数指针传递给 calcbreak;case 2:calc(sub);  // 将减法函数指针传递给 calcbreak;case 3:calc(mul);  // 将乘法函数指针传递给 calcbreak;case 4:calc(div);  // 将除法函数指针传递给 calcbreak;case 0:printf("退出程序\n");break;default:printf("选择错误\n");break;}} while (input);return 0;
}
  • 菜单显示:每次循环都会显示运算选项菜单,用户可以选择对应的运算。
  • 用户输入:获取用户的选择并存储在 input 中。
  • 调用 calc 函数:根据 input 的值,通过 calc 调用对应的运算函数。
  • 退出程序:当 input0 时,程序输出退出提示并结束循环。

1.3 回调函数的优点

  1. 代码复用:通过传递不同的回调函数,calc 函数可以实现不同的功能。
  2. 灵活性:可以在程序运行时动态地选择调用哪个函数。
  3. 模块化设计:使代码逻辑更加清晰、可维护。

1.4 回调函数的应用场景

  1. 排序算法:在 qsort 等函数中,用户可以自定义比较函数,实现各种排序逻辑。(下面会讲)
  2. 事件驱动的GUI程序:响应用户的点击、输入等操作。
  3. 异步任务处理:在异步操作完成时,通过回调函数通知调用者任务完成。

2. qsort 使用

qsort是C语言标准库<stdlib.h>中的一个通用排序函数,它使用快速排序算法来对数组进行排序。qsort的强大之处在于它可以用于各种数据类型的排序,包括基本数据类型和复杂的自定义数据结构。这里将探讨qsort的使用方法,并通过详细的实例演示如何对不同类型的数据进行排序。

2.1 qsort函数原型详解

void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))

参数说明:

  • base:指向要排序的数组的第一个元素的指针。它是一个void指针,可以接受任何类型的数组。
  • nitems:数组中元素的个数。通常使用sizeof(array) / sizeof(array[0])来计算。
  • size:数组中每个元素的大小,以字节为单位。通常使用sizeof(array[0])来获取。
  • compar:用于比较两个元素的函数指针。这个函数决定了排序的顺序和标准。

2.2 比较函数的设计

比较函数是qsort的核心,它决定了排序的方式。比较函数的原型如下:

int compare(const void *a, const void *b)

比较函数应该遵循以下规则:

  • 如果 *a < *b,返回负数(通常是-1)
  • 如果 *a == *b,返回0
  • 如果 *a > *b,返回正数(通常是1)

注意:比较函数接收的是void指针,需要在函数内部进行适当的类型转换。

2.3 整数排序详解

下面是一个使用qsort对整数数组进行排序的详细例子:

#include <stdio.h>
#include <stdlib.h>// 升序排序的比较函数
int compare_ints_asc(const void* a, const void* b) {return (*(int*)a - *(int*)b);
}// 降序排序的比较函数
int compare_ints_desc(const void* a, const void* b) {return (*(int*)b - *(int*)a);
}int main() {int arr[] = {64, 34, 25, 12, 22, 11, 90};int n = sizeof(arr) / sizeof(arr[0]);// 升序排序qsort(arr, n, sizeof(int), compare_ints_asc);printf("升序排序后的数组:\n");for (int i = 0; i < n; i++)printf("%d ", arr[i]);printf("\n");// 降序排序qsort(arr, n, sizeof(int), compare_ints_desc);printf("降序排序后的数组:\n");for (int i = 0; i < n; i++)printf("%d ", arr[i]);printf("\n");return 0;
}

在这个例子中,我们定义了两个比较函数:一个用于升序排序,另一个用于降序排序。通过更改传递给qsort的比较函数,我们可以轻松地改变排序的顺序。

2.4 字符串排序详解

对字符串数组进行排序需要特别注意,因为我们处理的是指向字符串的指针数组。以下是一个详细的例子:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>// 升序排序的比较函数
int compare_strings_asc(const void* a, const void* b) {return strcmp(*(const char**)a, *(const char**)b);
}// 降序排序的比较函数
int compare_strings_desc(const void* a, const void* b) {return strcmp(*(const char**)b, *(const char**)a);
}int main() {const char* arr[] = {"banana", "apple", "cherry", "date", "elderberry"};int n = sizeof(arr) / sizeof(arr[0]);// 升序排序qsort(arr, n, sizeof(const char*), compare_strings_asc);printf("升序排序后的字符串数组:\n");for (int i = 0; i < n; i++)printf("%s\n", arr[i]);// 降序排序qsort(arr, n, sizeof(const char*), compare_strings_desc);printf("\n降序排序后的字符串数组:\n");for (int i = 0; i < n; i++)printf("%s\n", arr[i]);return 0;
}

在这个例子中,我们使用strcmp函数来比较字符串。注意比较函数中的双重指针:*(const char**)a 用于获取实际的字符串指针。

2.5 结构体排序详解

对结构体数组进行排序是qsort最强大的应用之一。我们可以根据结构体的不同成员进行排序。以下是一个详细的例子:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>typedef struct {char name[50];int age;float height;
} Person;// 按年龄升序排序
int compare_age_asc(const void* a, const void* b) {Person* personA = (Person*)a;Person* personB = (Person*)b;return personA->age - personB->age;
}// 按身高降序排序
int compare_height_desc(const void* a, const void* b) {Person* personA = (Person*)a;Person* personB = (Person*)b;if (personA->height < personB->height) return 1;if (personA->height > personB->height) return -1;return 0;
}// 按姓名字母顺序排序
int compare_name(const void* a, const void* b) {Person* personA = (Person*)a;Person* personB = (Person*)b;return strcmp(personA->name, personB->name);
}int main() {Person people[] = {{"Alice", 30, 165.5},{"Bob", 25, 180.0},{"Charlie", 35, 175.5},{"David", 28, 170.0},{"Eve", 22, 160.0}};int n = sizeof(people) / sizeof(people[0]);// 按年龄升序排序qsort(people, n, sizeof(Person), compare_age_asc);printf("按年龄升序排序:\n");for (int i = 0; i < n; i++)printf("%s: %d岁, %.1fcm\n", people[i].name, people[i].age, people[i].height);// 按身高降序排序qsort(people, n, sizeof(Person), compare_height_desc);printf("\n按身高降序排序:\n");for (int i = 0; i < n; i++)printf("%s: %d岁, %.1fcm\n", people[i].name, people[i].age, people[i].height);// 按姓名字母顺序排序qsort(people, n, sizeof(Person), compare_name);printf("\n按姓名字母顺序排序:\n");for (int i = 0; i < n; i++)printf("%s: %d岁, %.1fcm\n", people[i].name, people[i].age, people[i].height);return 0;
}

在这个例子中,我们定义了三个不同的比较函数,分别按年龄、身高和姓名进行排序。这展示了qsort在处理复杂数据结构时的灵活性。

2.6 qsort的高级应用

  1. 多级排序:当主要排序标准相同时,可以在比较函数中添加次要排序标准。

    int compare_multi(const void* a, const void* b) {Person* personA = (Person*)a;Person* personB = (Person*)b;if (personA->age != personB->age)return personA->age - personB->age;  // 主要标准:年龄return strcmp(personA->name, personB->name);  // 次要标准:姓名
    }
    
  2. 稳定性处理qsort本身不保证稳定性(相等元素的相对顺序可能改变)。如果需要稳定排序,可以在比较函数中加入原始索引比较。

  3. 自定义排序规则:比较函数可以实现复杂的自定义排序逻辑,如按字符串长度排序、按特定规则排序等。

2.7 qsort的性能考虑

  1. qsort的时间复杂度平均为O(n log n),但在最坏情况下可能达到O(n^2)
  2. 对于小数组(通常少于10-20个元素),插入排序等简单算法可能更快。
  3. 比较函数的效率直接影响排序的整体性能,应尽可能简化比较函数。

2.8 结论

qsort函数是C语言中一个强大而灵活的排序工具。通过合理设计比较函数,它可以适应各种复杂的排序需求。掌握qsort的使用不仅可以提高编程效率,还能帮助深入理解指针、函数指针和回调函数等C语言的重要概念。在实际应用中,合理使用qsort可以大大简化代码结构,提高程序的可读性和可维护性。

3. qsort函数的模拟实现

我们采用 冒泡排序 来实现,并用回调函数来定义比较规则,这样我们的排序函数可以对不同数据类型进行排序。

3.1 基本思路

  1. 冒泡排序:冒泡排序是一种简单的排序算法,它通过重复地交换相邻的元素来将最大或最小的元素逐步移动到数组的末尾或开头。
  2. 回调函数qsort 使用一个回调函数来比较元素的大小。这个回调函数返回负值、零或正值,分别表示第一个元素小于、等于或大于第二个元素。
  3. 通用性:为了实现一个通用的排序函数,函数接收一个 void* 类型的数组指针,并通过额外的参数指定元素的大小和数组的长度。通过这样的设置,可以排序任意类型的数组。

3.2 模拟实现 qsort 的代码

以下代码展示了如何使用回调函数模拟 qsort 功能,采用冒泡排序的方式来实现排序。

#include <stdio.h>
int int_cmp(const void * p1, const void * p2)
{return (*( int *)p1 - *(int *) p2);
}
void _swap(void *p1, void * p2, int size)
{int i = 0;for (i = 0; i< size; i++){char tmp = *((char *)p1 + i);*(( char *)p1 + i) = *((char *) p2 + i);*(( char *)p2 + i) = tmp;}
}
void bubble(void *base, int count , int size, int(*cmp )(void *, void *))
{int i = 0;int j = 0;for (i = 0; i< count - 1; i++){for (j = 0; j<count-i-1; j++){if (cmp ((char *) base + j*size , (char *)base + (j + 1)*size) > 0){_swap(( char *)base + j*size, (char *)base + (j + 1)*size,size);}}}
}
int main()
{int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };int i = 0;bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++){printf( "%d ", arr[i]);}printf("\n");return 0;
}

3.3 代码解析

3.3.1 整数比较函数 int_cmp

int int_cmp(const void * p1, const void * p2) {return (*(int *)p1 - *(int *)p2);
}
  • 功能int_cmp 是一个比较函数,用于比较两个整数的大小。它接受两个 const void* 类型的指针 p1p2
  • 实现:将 p1p2 转换为 int*,解引用后得到两个整数,并返回它们的差值。差值的正负代表 p1p2 的相对大小(大于零表示 p1 > p2,小于零表示 p1 < p2)。

3.3.2 交换函数 _swap

void _swap(void *p1, void * p2, int size) {int i = 0;for (i = 0; i < size; i++) {char tmp = *((char *)p1 + i);*((char *)p1 + i) = *((char *)p2 + i);*((char *)p2 + i) = tmp;}
}
  • 功能_swap 函数用于交换两个内存位置的数据。这在排序中非常重要,因为我们需要不断交换位置来排列数组元素。
  • 实现:它通过字节级别的拷贝实现交换操作。函数参数 size 决定了每次交换的数据块大小,这样可以适用于任意数据类型。
  • 细节:通过 char* 类型指针对每个字节进行交换,使用循环将 p1p2 对应位置的数据逐字节交换。

3.3.3 冒泡排序函数 bubble

void bubble(void *base, int count , int size, int(*cmp)(void *, void *)) {int i = 0;int j = 0;for (i = 0; i < count - 1; i++) {for (j = 0; j < count - i - 1; j++) {if (cmp((char *)base + j * size, (char *)base + (j + 1) * size) > 0) {_swap((char *)base + j * size, (char *)base + (j + 1) * size, size);}}}
}
  • 功能bubble 函数是一个通用的冒泡排序函数,可以排序任何类型的数据。
  • 参数:
    • base:待排序数组的起始地址。
    • count:数组中的元素个数。
    • size:每个元素的大小(字节数)。
    • cmp:用于比较两个元素的函数指针。
  • 实现:
    • 使用两层嵌套循环实现冒泡排序。外层循环控制排序轮数,内层循环每次将最大(或最小)的元素移动到数组的末尾。
    • cmp 函数用于比较相邻两个元素的大小。
    • 如果 cmp 返回值大于 0,表示前一个元素大于后一个元素,则调用 _swap 函数交换它们的位置。

3.3.4 主函数 main

int main() {int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };int i = 0;bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {printf("%d ", arr[i]);}printf("\n");return 0;
}
  • 功能:定义一个整型数组 arr,调用 bubble 函数对其进行排序,然后输出排序后的结果。
  • 实现:
    • sizeof(arr) / sizeof(arr[0]) 计算数组中元素的个数。
    • sizeof(int) 表示每个元素的大小(字节数)。
    • int_cmp 作为参数传递给 bubble 函数,指定排序时使用的比较逻辑。

3.4 总结

  1. 通用性bubble 函数使用了 void* 指针和 cmp 比较函数指针,因此可以排序任意类型的数据,只需传入适当的比较函数。
  2. 灵活性:通过 int_cmp 函数,我们定义了整数的比较方式,可以方便地替换为其他比较方式,比如降序排序或不同数据类型的排序。
  3. 冒泡排序:排序逻辑基于冒泡排序,复杂度为 O(n^2),适用于小规模数据。

这段代码展示了使用回调函数实现通用排序的思想,是 C 语言中实现灵活算法的重要方法。

4. sizeofstrlen的对比

4.1 sizeof()的作用和用法

  1. 功能sizeof() 是一个编译时运算符,用于返回变量或数据类型的大小(以字节为单位)。

  2. 返回值:返回值的类型为 size_t,它是一个无符号整数,表示所占的字节数。

  3. 用法

    • sizeof(变量名):返回变量所占的内存大小。
    • sizeof(数据类型):返回指定数据类型所占的内存大小。
  4. 示例:

    int a = 10;
    printf("sizeof(int): %zu\n", sizeof(int));        // 输出4(假设int为4字节)
    printf("sizeof(a): %zu\n", sizeof(a));             // 输出4
    
  5. 使用场景:

  • 计算基本数据类型或结构体的大小,例如 sizeof(int)sizeof(double) 等。
  • 确定数组大小,特别是当数组的大小在编译时已知时很有用。

4.2 strlen()的作用和用法

  1. 功能strlen() 是一个库函数,定义在 <string.h> 中,用于计算字符串的长度(即字符数,不包括字符串末尾的 \0 终止符)。

  2. 返回值:返回值的类型为 size_t,表示字符串中字符的数量。

  3. 用法

    • strlen(字符串):计算字符串的长度,字符串必须以 \0 结尾。
  4. 示例:

    char str[] = "Hello";
    printf("strlen(str): %zu\n", strlen(str));       // 输出5,不包括'\0'
    
  5. 使用场景:

  • 常用于操作字符串时获取其长度,通常配合 char 数组或 char 指针。
  • 适合在运行时处理动态长度的字符串。

4.3 sizeof()strlen() 的主要区别

特性sizeof()strlen()
定义位置编译时运算符运行时库函数
功能计算变量或数据类型的内存大小计算字符串的字符长度
适用类型任意数据类型仅适用于字符串
返回值是否包含 \0包含(对于 char 数组)不包含字符串末尾的 \0
使用限制适用于任何类型,编译时计算仅限以 \0 结尾的字符串,运行时计算

4.4 sizeof()strlen() 的常见应用与误区

  1. 数组和字符串的区别

    • 对于字符数组,sizeof 会返回数组总大小(包括 \0 ),而 strlen 仅返回字符串长度(不包含 \0)。
    char str[10] = "Hello";
    printf("sizeof(str): %zu\n", sizeof(str));    // 输出10,包含未使用的字符空间
    printf("strlen(str): %zu\n", strlen(str));    // 输出5,只计算到'Hello'的长度
    
  2. 指针与数组的区别

    • 对于字符指针,sizeof 返回指针的大小(通常是 4 或 8 字节,取决于系统),而 strlen 返回字符串的实际长度。
    char *str_ptr = "Hello";
    printf("sizeof(str_ptr): %zu\n", sizeof(str_ptr));    // 输出指针大小(4或8字节)
    printf("strlen(str_ptr): %zu\n", strlen(str_ptr));    // 输出5,字符串的长度
    
  3. 字符串常量

    • 对于字符串常量,sizeof 会计算字符串的字节数并包含 \0,而 strlen 则不包括。
    printf("sizeof("Hello"): %zu\n", sizeof("Hello"));    // 输出6,包含'\0'
    printf("strlen("Hello"): %zu\n", strlen("Hello"));    // 输出5,不包含'\0'
    

4.5 总结

  • sizeof() 是一个运算符,用于在编译时确定数据的总内存大小,适用于任意类型。对数组而言,它返回整个数组的大小,对指针而言,它返回指针本身的大小。
  • strlen() 适用于字符串类型,只在运行时计算字符数,不包含终止符 \0

掌握 sizeof()strlen() 的差异,可以有效避免数组和字符串处理中的常见错误。在字符串操作时,记得区分 sizeofstrlen 的用途,以免出现意外的结果。

5. 数组和指针笔试题解析

5.1 一维数组

int a[] = {1,2,3,4};
printf("%zd\n",sizeof(a));
printf("%zd\n",sizeof(a+0));
printf("%zd\n",sizeof(*a));
printf("%zd\n",sizeof(a+1));
printf("%zd\n",sizeof(a[1]));
printf("%zd\n",sizeof(&a));
printf("%zd\n",sizeof(*&a));
printf("%zd\n",sizeof(&a+1));
printf("%zd\n",sizeof(&a[0]));
printf("%zd\n",sizeof(&a[0]+1));

5.1.1 代码及解释

int a[] = {1,2,3,4};

声明了一个整型数组 a,包含 4 个元素。数组 a 的内存大小为 4 * sizeof(int) = 16 字节。


5.1.1.1 sizeof(a)
printf("%zd\n", sizeof(a));
  • 解释a 是一个数组名,作为 sizeof 的操作数时,表示整个数组的大小。
  • 结果:返回整个数组的大小,即 4 * sizeof(int) = 16 字节。

5.1.1.2 sizeof(a+0)
printf("%zd\n", sizeof(a+0));
  • 解释a+0 表示将数组 a 转换为指向第一个元素的指针,然后再加上偏移量 0。因此 a+0 实际上是一个指向 int 的指针。
  • 结果:返回指针的大小。在 32 位系统上通常是 4 字节,在 64 位系统上通常是 8 字节。

5.1.1.3 sizeof(*a)
printf("%zd\n", sizeof(*a));
  • 解释*a 表示解引用 a,即获取数组第一个元素的值。第一个元素是一个 int 类型,所以 sizeof(*a) 实际上是 sizeof(int)
  • 结果:返回 int 的大小,即 4 字节。

5.1.1.4 sizeof(a+1)
printf("%zd\n", sizeof(a+1));
  • 解释a+1 是数组名 a 加上偏移量 1,表示指向数组第二个元素的指针。a+1 的类型是指针类型。
  • 结果:返回指针的大小,在 32 位系统上通常是 4 字节,在 64 位系统上通常是 8 字节。

5.1.1.5 sizeof(a[1])
printf("%zd\n", sizeof(a[1]));
  • 解释a[1] 表示数组的第二个元素,类型为 int
  • 结果:返回 int 的大小,即 4 字节。

5.1.1.6 sizeof(&a)
printf("%zd\n", sizeof(&a));
  • 解释&a 是整个数组 a 的地址,它的类型是 int (*)[4],即指向一个包含 4 个 int 的数组的指针。
  • 结果:返回一个指针的大小,在 32 位系统上通常是 4 字节,在 64 位系统上通常是 8 字节。

5.1.1.7 sizeof(*&a)
printf("%zd\n", sizeof(*&a));
  • 解释*&a 先取 a 的地址,然后再解引用,回到原来的数组 a。因此,*&a 的类型是 int[4],代表整个数组。
  • 结果:返回数组 a 的大小,即 16 字节。

5.1.1.8 sizeof(&a+1)
printf("%zd\n", sizeof(&a+1));
  • 解释&a+1 表示数组 a 的地址加上 1。由于 &a 是一个 int (*)[4] 类型指针(指向包含 4 个 int 的数组),&a+1 指向的是下一个同类型数组的地址。
  • 结果:返回一个指针的大小,在 32 位系统上通常是 4 字节,在 64 位系统上通常是 8 字节。

5.1.1.9 sizeof(&a[0])
printf("%zd\n", sizeof(&a[0]));
  • 解释&a[0] 是第一个元素的地址,即一个指向 int 的指针。
  • 结果:返回一个指针的大小,在 32 位系统上通常是 4 字节,在 64 位系统上通常是 8 字节。

5.1.1.10 sizeof(&a[0]+1)
printf("%zd\n", sizeof(&a[0]+1));
  • 解释&a[0]+1 是第一个元素的地址加上 1,结果是一个指向 a[1] 的指针,即一个指向 int 的指针。
  • 结果:返回指针的大小,在 32 位系统上通常是 4 字节,在 64 位系统上通常是 8 字节。

5.1.2 总结

表达式解释返回值(字节)
sizeof(a)数组 a 的大小16
sizeof(a+0)指向 a 的第一个元素的指针的大小4/8
sizeof(*a)a[0] 的大小,即 int 的大小4
sizeof(a+1)指向 a 的第二个元素的指针的大小4/8
sizeof(a[1])数组第二个元素的大小,即 int 的大小4
sizeof(&a)数组 a 的地址的大小4/8
sizeof(*&a)数组 a 的大小16
sizeof(&a+1)指向下一个数组的地址的大小4/8
sizeof(&a[0])a[0] 的地址的大小4/8
sizeof(&a[0]+1)a[1] 的地址的大小4/8

在这里,sizeof 操作数是数组时返回整个数组的大小,而是指针时则返回指针的大小。这些差异在理解 C 语言中的指针和数组时非常重要。

5.2 字符数组

5.2.1 代码1:

char arr[] = {'a','b','c','d','e','f'};
printf("%zd\n", sizeof(arr));
printf("%zd\n", sizeof(arr+0));
printf("%zd\n", sizeof(*arr));
printf("%zd\n", sizeof(arr[1]));
printf("%zd\n", sizeof(&arr));
printf("%zd\n", sizeof(&arr+1));
printf("%zd\n", sizeof(&arr[0]+1));
5.2.1.1 sizeof(arr)
printf("%zd\n", sizeof(arr));
  • 解释arr 是一个数组名,在 sizeof 中使用时,它表示整个数组的大小。
  • 结果:数组 arr 的总大小为 6 个字节,因为它包含 6 个 char 元素,每个 char 是 1 字节。
  • 输出6

5.2.1.2 sizeof(arr+0)
printf("%zd\n", sizeof(arr+0));
  • 解释arr+0 表示将数组名 arr 转换为指向数组第一个元素的指针,然后再加上偏移量 0。结果是一个 char* 指针。
  • 结果:返回指针的大小。在 32 位系统上通常是 4 字节,在 64 位系统上通常是 8 字节。
  • 输出4(32 位系统)或 8(64 位系统)

5.2.1.3 sizeof(*arr)
printf("%zd\n", sizeof(*arr));
  • 解释*arr 表示解引用数组 arr,即获取第一个元素的值。第一个元素是一个 char 类型,所以 sizeof(*arr) 实际上是 sizeof(char)
  • 结果:返回 char 的大小,即 1 字节。
  • 输出1

5.2.1.4 sizeof(arr[1])
printf("%zd\n", sizeof(arr[1]));
  • 解释arr[1] 表示数组中的第二个元素(即字符 'b'),类型为 char
  • 结果:返回 char 的大小,即 1 字节。
  • 输出1

5.2.1.5 sizeof(&arr)
printf("%zd\n", sizeof(&arr));
  • 解释&arr 是整个数组 arr 的地址,它的类型是 char (*)[6],即指向包含 6 个 char 的数组的指针。
  • 结果:返回指针的大小,在 32 位系统上通常是 4 字节,在 64 位系统上通常是 8 字节。
  • 输出4(32 位系统)或 8(64 位系统)

5.2.1.6 sizeof(&arr+1)
printf("%zd\n", sizeof(&arr+1));
  • 解释&arr+1 表示数组 arr 的地址加上 1。由于 &arr 是一个 char (*)[6] 类型指针(指向包含 6 个 char 的数组),所以 &arr+1 指向的是下一个同类型数组的地址。
  • 结果:返回指针的大小,在 32 位系统上通常是 4 字节,在 64 位系统上通常是 8 字节。
  • 输出4(32 位系统)或 8(64 位系统)

5.2.1.7 sizeof(&arr[0]+1)
printf("%zd\n", sizeof(&arr[0]+1));
  • 解释&arr[0] 是第一个元素的地址,即一个指向 char 的指针。&arr[0]+1 表示指针加一,指向数组的第二个元素。
  • 结果:返回指针的大小,在 32 位系统上通常是 4 字节,在 64 位系统上通常是 8 字节。
  • 输出4(32 位系统)或 8(64 位系统)
5.2.1.8 总结
表达式解释返回值(字节)输出(32 位)输出(64 位)
sizeof(arr)数组 arr 的大小666
sizeof(arr+0)arr 的第一个元素的指针的大小4(或 8)48
sizeof(*arr)arr[0] 的大小,即 char 的大小111
sizeof(arr[1])数组第二个元素的大小,即 char 的大小111
sizeof(&arr)数组 arr 的地址的大小4(或 8)48
sizeof(&arr+1)下一个数组的地址的大小4(或 8)48
sizeof(&arr[0]+1)arr[1] 的地址的大小4(或 8)48

这道题展示了 sizeof 的不同应用。使用 sizeof 对数组名、指针和指向数组的指针时会有不同的结果。在数组名作为 sizeof 操作数时,返回整个数组的大小,而在指针表达式时返回的是指针的大小。

5.2.2 代码2:

char arr[] = {'a','b','c','d','e','f'};
printf("%zd\n", strlen(arr));
printf("%zd\n", strlen(arr+0));
printf("%zd\n", strlen(*arr));
printf("%zd\n", strlen(arr[1]));
printf("%zd\n", strlen(&arr));
printf("%zd\n", strlen(&arr+1));
printf("%zd\n", strlen(&arr[0]+1));
5.2.2.1 strlen(arr)
printf("%zd\n", strlen(arr));
  • 解释arr 是一个字符数组的首地址,strlen(arr) 试图从 arr 开始计算字符长度,直到找到 \0
  • 问题:由于 arr 没有 \0 终止符,因此 strlen 将继续访问内存,直到遇到偶然的 \0,这会导致未定义的行为,可能返回一个不确定的长度,甚至引发程序崩溃。
  • 建议:如果想要使用 strlen,应当定义数组时添加一个 \0 终止符,如 char arr[] = "abcdef";

5.2.2.2 strlen(arr+0)
printf("%zd\n", strlen(arr+0));
  • 解释arr+0 是一个指向 arr 第一个元素的指针,因此 strlen(arr+0) 等价于 strlen(arr)
  • 问题:与 strlen(arr) 相同,arr 没有以 \0 结尾,因此 strlen(arr+0) 会导致未定义行为。
  • 建议:应确保 arr 是一个以 \0 结尾的字符串,才能使用 strlen

5.2.2.3 strlen(*arr)
printf("%zd\n", strlen(*arr));
  • 解释*arr 表示解引用 arr,即 arr[0],它的值是字符 'a'
  • 问题strlen 的参数应为指向 char 的指针,而 *arr 是一个 char'a' 的 ASCII 值是 97)。传递非指针类型给 strlen 会导致编译错误,因为 strlen 期望一个 char* 指针。
  • 错误信息:编译器通常会提示类型不兼容错误。

5.2.2.4 strlen(arr[1])
printf("%zd\n", strlen(arr[1]));
  • 解释arr[1] 是数组的第二个元素,值为字符 'b'
  • 问题:与上一行类似,strlen 需要一个 char* 类型的参数,而 arr[1] 是一个 char,会导致类型不兼容错误。
  • 错误信息:编译器通常会提示类型不兼容错误。

5.2.2.5 strlen(&arr)
printf("%zd\n", strlen(&arr));
  • 解释&arr 是整个数组的地址,类型是 char (*)[6],指向包含 6 个字符的数组。
  • 问题strlen 期望一个 char* 参数,而 &arr 是一个 char (*)[6] 类型的指针,这与 char* 类型不兼容。虽然 &arrarr 的值相同,但类型不同,会导致未定义行为。
  • 错误信息:编译器可能会报类型不兼容的警告或错误,运行时可能导致错误。

5.2.2.6 strlen(&arr+1)
printf("%zd\n", strlen(&arr+1));
  • 解释&arr+1arr 数组的地址加上 1,跳过整个 arr 数组的大小,因此指向数组 arr 之后的内存。
  • 问题strlen 期望一个指向 char 的指针,而 &arr+1char (*)[6] 类型的指针,类型不兼容。此外,它指向 arr 之后的内存区域,而不是有效的字符串起始地址,访问会导致未定义行为。
  • 错误信息:编译器可能会报类型不兼容的警告,运行时可能导致程序崩溃。

5.2.2.7 strlen(&arr[0]+1)
printf("%zd\n", strlen(&arr[0]+1));
  • 解释&arr[0]arr 的第一个元素的地址,即一个 char* 指针。&arr[0] + 1 指向数组的第二个元素,即 arr[1]
  • 问题:由于 arr 不是以 \0 结束的字符串,strlen(&arr[0] + 1) 仍会继续读取直到遇到一个偶然的 \0,导致未定义行为。
  • 建议:如果想要计算从 arr[1] 开始的有效字符串长度,应确保 arr 是以 \0 结尾的字符串。
5.2.2.8 总结

由于 arr 不是一个以 \0 结尾的字符串,因此不能直接使用 strlen 计算其长度。在 C 语言中,strlen 只能用来处理以 \0 结束的字符串,否则会出现未定义行为。正确的方式是确保数组以 \0 结束,或者使用 sizeof(arr) 来计算数组的总大小。

5.2.3 代码3:

char arr[] = "abcdef";
printf("%zd\n", sizeof(arr));
printf("%zd\n", sizeof(arr+0));
printf("%zd\n", sizeof(*arr));
printf("%zd\n", sizeof(arr[1]));
printf("%zd\n", sizeof(&arr));
printf("%zd\n", sizeof(&arr+1));
printf("%zd\n", sizeof(&arr[0]+1));
5.2.3.1 sizeof(arr)
printf("%zd\n", sizeof(arr));
  • 解释arr 是一个字符数组的名字,在 sizeof 中使用时,它表示整个数组的大小。
  • 结果:数组 arr 的总大小为 7 个字节,因为它包含 "abcdef" 和一个 \0 终止符。
  • 输出7

5.2.3.2 sizeof(arr+0)
printf("%zd\n", sizeof(arr+0));
  • 解释arr+0 是一个指向数组第一个元素的指针(即 char* 指针),因为数组名 arr 在表达式中会衰减为指针。sizeof(arr+0) 返回指针的大小,而不是数组的大小。
  • 结果:返回指针的大小。在 32 位系统上通常是 4 字节,在 64 位系统上通常是 8 字节。
  • 输出4(32 位系统)或 8(64 位系统)

5.2.3.3 sizeof(*arr)
printf("%zd\n", sizeof(*arr));
  • 解释*arr 是解引用 arr,即获取数组的第一个元素的值,类型为 char
  • 结果:返回 char 的大小,即 1 字节。
  • 输出1

5.2.3.4 sizeof(arr[1])
printf("%zd\n", sizeof(arr[1]));
  • 解释arr[1] 是数组的第二个元素,类型为 char
  • 结果:返回 char 的大小,即 1 字节。
  • 输出1

5.2.3.5 sizeof(&arr)
printf("%zd\n", sizeof(&arr));
  • 解释&arr 是数组 arr 的地址,它的类型是 char (*)[7],即指向包含 7 个字符的数组的指针。
  • 结果:返回指向数组的指针的大小,而不是数组的大小。在 32 位系统上通常是 4 字节,在 64 位系统上通常是 8 字节。
  • 输出4(32 位系统)或 8(64 位系统)

5.2.3.6 sizeof(&arr+1)
printf("%zd\n", sizeof(&arr+1));
  • 解释&arr+1arr 数组的地址加上 1。这会跳过整个数组的大小,指向下一个 char[7] 类型的数组的地址。&arr+1 的类型仍然是 char (*)[7]
  • 结果:返回指向数组的指针的大小。在 32 位系统上通常是 4 字节,在 64 位系统上通常是 8 字节。
  • 输出4(32 位系统)或 8(64 位系统)

5.2.3.7 sizeof(&arr[0]+1)
printf("%zd\n", sizeof(&arr[0]+1));
  • 解释&arr[0] 是数组第一个元素的地址,即一个 char* 指针。&arr[0] + 1 指向数组的第二个元素,即 arr[1]
  • 结果sizeof(&arr[0]+1) 返回 char* 指针的大小,因为 &arr[0]+1 是一个指向 char 的指针。
  • 输出4(32 位系统)或 8(64 位系统)

5.2.4 代码4:

char arr[] = "abcdef";
printf("%zd\n", strlen(arr));
printf("%zd\n", strlen(arr+0));
printf("%zd\n", strlen(*arr));
printf("%zd\n", strlen(arr[1]));
printf("%zd\n", strlen(&arr));
printf("%zd\n", strlen(&arr+1));
printf("%zd\n", strlen(&arr[0]+1));
5.2.4.1 strlen(arr)
printf("%zd\n", strlen(arr));
  • 解释arr 是字符数组的名字,它在表达式中会退化为指向第一个字符的指针。
  • 结果strlenarr 开始,逐字符计算,直到遇到 \0 终止符。因此,strlen(arr) 返回字符串 "abcdef" 的长度,即 6。
  • 输出6

5.2.4.2 strlen(arr+0)
printf("%zd\n", strlen(arr+0));
  • 解释arr+0 是数组名 arr 加上偏移量 0,相当于指向第一个字符的指针。因此 strlen(arr+0)strlen(arr) 的效果相同。
  • 结果:返回字符串 "abcdef" 的长度,即 6。
  • 输出6

5.2.4.3 strlen(*arr)
printf("%zd\n", strlen(*arr));
  • 解释*arr 是解引用 arr,即获取 arr 的第一个字符 arr[0] 的值,也就是字符 'a'strlen 的参数必须是一个 char* 类型的指针,而 *arr 是一个 char 类型(字符 'a' 的 ASCII 值)。
  • 问题:将 char 类型作为 strlen 的参数会导致编译错误,因为 strlen 需要一个指向字符数组的指针,而不是一个单个字符。
  • 错误信息:编译器会提示类型不兼容错误。

5.2.4.4 strlen(arr[1])
printf("%zd\n", strlen(arr[1]));
  • 解释arr[1] 是数组的第二个元素,其值为字符 'b'。与 *arr 类似,这里传递了一个 char 类型的参数(即 'b'),而不是指向字符数组的指针。
  • 问题:同样地,将 char 类型作为 strlen 的参数会导致编译错误,因为 strlen 需要的是一个 char* 类型的指针,而不是单个字符。
  • 错误信息:编译器会提示类型不兼容错误。

5.2.4.5 strlen(&arr)
printf("%zd\n", strlen(&arr));
  • 解释&arr 是整个数组 arr 的地址,其类型是 char (*)[7](指向一个包含 7 个字符的数组)。虽然 &arrarr 的值相同(都是数组的起始地址),但是类型不同。
  • 问题strlen 需要的是一个 char*,而 &arr 的类型是 char (*)[7]。虽然它指向同一块内存,但类型不同,可能会导致未定义行为。
  • 运行结果:编译器可能会警告类型不兼容。运行时可能输出正确的字符串长度(6),但不推荐使用这种写法。

5.2.4.6 strlen(&arr+1)
printf("%zd\n", strlen(&arr+1));
  • 解释&arr+1 是数组 arr 的地址加 1,它跳过整个数组的大小,因此指向数组 arr 之后的内存区域。
  • 问题&arr+1 指向的并不是有效的字符串地址,传递它给 strlen 会导致未定义行为,因为它指向的是 arr 数组末尾之后的位置,访问将导致未定义的结果。
  • 运行结果:可能会导致程序崩溃或产生不确定的结果。编译器也可能会警告类型不兼容。

5.2.4.7 strlen(&arr[0]+1)
printf("%zd\n", strlen(&arr[0]+1));
  • 解释&arr[0] 是数组第一个元素的地址,即一个 char* 指针,指向字符 'a'&arr[0] + 1 指向数组的第二个元素,即字符 'b'
  • 结果strlen(&arr[0] + 1)arr[1] 开始计算长度,返回从字符 'b' 到字符串结尾的长度,即 5。
  • 输出5
5.2.4.8 总结
表达式解释返回值(字符数)输出
strlen(arr)arr 开始计算字符串长度66
strlen(arr+0)等价于 strlen(arr),从 arr 开始计算长度66
strlen(*arr)*arr 是第一个字符 'a',类型错误错误编译错误
strlen(arr[1])arr[1] 是第二个字符 'b',类型错误错误编译错误
strlen(&arr)&arr 是数组地址,类型不兼容,可能导致错误未定义未定义(可能是 6
strlen(&arr+1)指向数组结束后的位置,未定义行为未定义未定义(可能崩溃)
strlen(&arr[0]+1)arr[1] 开始计算字符串长度55

注意:strlen 只能用于以 \0 结尾的字符串,对于其他情况或不适当的类型传递会导致编译错误或未定义行为。

5.2.5 代码5:

char *p = "abcdef";
printf("%zd\n", sizeof(p));
printf("%zd\n", sizeof(p+1));
printf("%zd\n", sizeof(*p));
printf("%zd\n", sizeof(p[0]));
printf("%zd\n", sizeof(&p));
printf("%zd\n", sizeof(&p+1));
printf("%zd\n", sizeof(&p[0]+1));
5.2.5.1 sizeof(p)
printf("%zd\n", sizeof(p));
  • 解释p 是一个指向字符的指针,类型是 char*
  • 结果sizeof(p) 返回指针的大小。在 32 位系统上通常是 4 字节,在 64 位系统上通常是 8 字节。
  • 输出4(32 位系统)或 8(64 位系统)

5.2.5.2 sizeof(p+1)
printf("%zd\n", sizeof(p+1));
  • 解释p+1 表示指针 p 向后移动一个位置,指向字符串的第二个字符(即 'b')。p+1 的类型仍然是 char*,只是指向的内存位置发生了变化。
  • 结果sizeof(p+1) 返回指针的大小,与 sizeof(p) 相同。
  • 输出4(32 位系统)或 8(64 位系统)

5.2.5.3 sizeof(*p)
printf("%zd\n", sizeof(*p));
  • 解释*p 是解引用指针 p,即获取指针所指向的第一个字符的值。*p 的类型是 char
  • 结果sizeof(*p) 返回 char 的大小,通常为 1 字节。
  • 输出1

5.2.5.4 sizeof(p[0])
printf("%zd\n", sizeof(p[0]));
  • 解释p[0] 等价于 *p,表示指针 p 所指向的第一个字符,类型为 char
  • 结果sizeof(p[0]) 返回 char 的大小,通常为 1 字节。
  • 输出1

5.2.5.5 sizeof(&p)
printf("%zd\n", sizeof(&p));
  • 解释&p 是指针 p 的地址,类型是 char**(即指向 char* 的指针)。
  • 结果sizeof(&p) 返回指向指针的指针的大小。在 32 位系统上通常是 4 字节,在 64 位系统上通常是 8 字节。
  • 输出4(32 位系统)或 8(64 位系统)

5.2.5.6 sizeof(&p+1)
printf("%zd\n", sizeof(&p+1));
  • 解释&p+1 是指针 p 的地址加上 1。&p 的类型是 char**,因此 &p+1 指向的是下一个 char* 指针的地址。
  • 结果sizeof(&p+1) 返回指向指针的指针的大小,与 sizeof(&p) 相同。
  • 输出4(32 位系统)或 8(64 位系统)

5.2.5.7 sizeof(&p[0]+1)
printf("%zd\n", sizeof(&p[0]+1));
  • 解释&p[0]p 所指向的第一个字符的地址,即字符串 "abcdef" 的起始地址。&p[0] 的类型是 char*,所以 &p[0] + 1 表示这个地址向后移动一个字符,指向字符串的第二个字符(即 'b')。
  • 结果sizeof(&p[0]+1) 返回指针的大小,与 sizeof(p) 相同。
  • 输出4(32 位系统)或 8(64 位系统)

5.2.5.8 总结
表达式解释返回值(字节)输出(32 位)输出(64 位)
sizeof(p)p 是一个 char* 指针,返回指针大小4(或 8)48
sizeof(p+1)p+1 仍是 char* 指针,返回指针大小4(或 8)48
sizeof(*p)*p 是指向的第一个字符,类型为 char111
sizeof(p[0])等同于 *p,返回第一个字符的大小111
sizeof(&p)&pchar** 类型,返回指向指针的指针的大小4(或 8)48
sizeof(&p+1)&p+1char** 类型,返回指向指针的指针的大小4(或 8)48
sizeof(&p[0]+1)&p[0]+1char* 类型,返回指针大小4(或 8)48

这段代码展示了 sizeof 运算符在处理指针和指针的地址时的不同效果。主要区别在于 sizeof(p) 返回 p 本身的大小,而 sizeof(*p) 返回 p 所指向的数据的大小。对于 &p&p+1,它们的类型是 char**,因为它们指向的是指针 p 的地址。

5.2.6 代码6:

char *p = "abcdef";
printf("%zd\n", strlen(p));
printf("%zd\n", strlen(p+1));
printf("%zd\n", strlen(*p));
printf("%zd\n", strlen(p[0]));
printf("%zd\n", strlen(&p));
printf("%zd\n", strlen(&p+1));
printf("%zd\n", strlen(&p[0]+1));
5.2.6.1 strlen(p)
printf("%zd\n", strlen(p));
  • 解释p 是一个指向字符串 "abcdef" 的指针。
  • 结果strlenp 开始计算字符数,直到遇到 \0。因此,strlen(p) 返回字符串 "abcdef" 的长度,即 6。
  • 输出6

5.2.6.2 strlen(p+1)
printf("%zd\n", strlen(p+1));
  • 解释p+1 表示指针 p 向后移动一个字符的位置,因此指向字符串的第二个字符 'b'
  • 结果strlen(p+1)p+1 开始计算,返回从字符 'b' 到字符串结尾的长度,即 5。
  • 输出5

5.2.6.3 strlen(*p)
printf("%zd\n", strlen(*p));
  • 解释*p 是对指针 p 的解引用,得到 p 所指向的第一个字符的值 'a'*p 的类型是 char,而不是 char*
  • 问题strlen 需要一个 char* 参数,但 *p 是一个 char,会导致编译错误,因为类型不匹配。
  • 错误信息:编译器会提示类型不兼容的错误。

5.2.6.4 strlen(p[0])
printf("%zd\n", strlen(p[0]));
  • 解释p[0] 等价于 *p,表示 p 所指向的第一个字符 p[0] 的值 'a',类型为 char
  • 问题:与上面相同,strlen 需要 char* 参数,而 p[0]char 类型,导致类型不兼容错误。
  • 错误信息:编译器会提示类型不兼容的错误。

5.2.6.5 strlen(&p)
printf("%zd\n", strlen(&p));
  • 解释&p 是指针 p 的地址,类型是 char**(即指向 char* 的指针)。
  • 问题strlen 需要一个 char* 类型的参数,而 &pchar**,类型不匹配,可能导致未定义行为。
  • 运行结果:编译器可能会发出类型不兼容的警告。运行时可能产生不可预期的结果或导致崩溃。

5.2.6.6 strlen(&p+1)
printf("%zd\n", strlen(&p+1));
  • 解释&p+1 是指向指针 p 的地址再加 1,因此它指向 p 之后的内存地址,类型为 char**
  • 问题strlen 期望一个 char* 参数,而 &p+1char**,类型不匹配,可能导致未定义行为。
  • 运行结果:编译器可能会发出类型不兼容的警告,运行时可能会导致崩溃。

5.2.6.7 strlen(&p[0]+1)
printf("%zd\n", strlen(&p[0]+1));
  • 解释&p[0] 是字符串第一个字符的地址,即 p 本身。&p[0] + 1 表示从 p 向后移动一个字符的位置,指向字符串的第二个字符 'b'
  • 结果strlen(&p[0] + 1)p[1](即字符 'b')开始计算字符串长度,返回 5。
  • 输出5

5.2.6.8 总结
表达式解释返回值(字符数)输出
strlen(p)p 开始计算字符串长度66
strlen(p+1)p+1(即字符 'b')开始计算长度55
strlen(*p)*p 是字符 'a',类型错误错误编译错误
strlen(p[0])p[0] 是字符 'a',类型错误错误编译错误
strlen(&p)&pchar**,类型不匹配,未定义行为未定义可能崩溃或错误
strlen(&p+1)&p+1char**,类型不匹配,未定义行为未定义可能崩溃或错误
strlen(&p[0]+1)p[1](即字符 'b')开始计算长度55

注意:strlen 只能用于以 \0 结尾的字符串,对于其他数据类型或错误类型传递会导致编译错误或未定义行为。正确使用时,应确保传递的是指向以 \0 结束的 char 字符串的指针。

5.3 二维数组

int a[3][4] = {0};
printf("%zd\n",sizeof(a));
printf("%zd\n",sizeof(a[0][0]));
printf("%zd\n",sizeof(a[0]));
printf("%zd\n",sizeof(a[0]+1));
printf("%zd\n",sizeof(*(a[0]+1)));
printf("%zd\n",sizeof(a+1));
printf("%zd\n",sizeof(*(a+1)));
printf("%zd\n",sizeof(&a[0]+1));
printf("%zd\n",sizeof(*(&a[0]+1)));
printf("%zd\n",sizeof(*a));
printf("%zd\n",sizeof(a[3]));

5.3.1. sizeof(a)

printf("%zd\n", sizeof(a));
  • 解释a 是一个二维数组,sizeof(a) 返回整个数组的大小。
  • 结果:整个数组有 3 行 4 列,每个元素是 4 字节,因此 sizeof(a) = 3 * 4 * 4 = 48 字节。
  • 输出48

5.3.2 sizeof(a[0][0])

printf("%zd\n", sizeof(a[0][0]));
  • 解释a[0][0] 是数组中的第一个元素,类型是 int
  • 结果sizeof(a[0][0]) 返回 int 的大小,即 4 字节。
  • 输出4

5.3.3 sizeof(a[0])

printf("%zd\n", sizeof(a[0]));
  • 解释a[0] 表示二维数组的第一行,类型是 int[4],即一个包含 4 个整数的一维数组。
  • 结果sizeof(a[0]) 返回第一行的大小,即 4 * sizeof(int) = 16 字节。
  • 输出16

5.3.4 sizeof(a[0]+1)

printf("%zd\n", sizeof(a[0]+1));
  • 解释a[0] 是第一行的起始地址,即一个 int* 指针,a[0] + 1 表示将该指针偏移到 a[0][1] 的位置,类型仍为 int*
  • 结果sizeof(a[0]+1) 返回指针的大小。在 32 位系统上通常为 4 字节,在 64 位系统上通常为 8 字节。
  • 输出4(32 位系统)或 8(64 位系统)

5.3.5 sizeof(*(a[0]+1))

printf("%zd\n", sizeof(*(a[0]+1)));
  • 解释a[0] + 1 是指向 a[0][1] 的指针,*(a[0] + 1) 解引用该指针,得到 a[0][1] 的值,类型为 int
  • 结果sizeof(*(a[0]+1)) 返回 int 的大小,即 4 字节。
  • 输出4

5.3.6 sizeof(a+1)

printf("%zd\n", sizeof(a+1));
  • 解释a 是一个二维数组,表达式 a+1 会将 a 视为指向 int[4] 的指针(即指向第一行),a+1 表示指向下一行的地址,类型为 int (*)[4]
  • 结果sizeof(a+1) 返回指针的大小。在 32 位系统上通常为 4 字节,在 64 位系统上通常为 8 字节。
  • 输出4(32 位系统)或 8(64 位系统)

5.3.7 sizeof(*(a+1))

printf("%zd\n", sizeof(*(a+1)));
  • 解释a+1 是指向第二行的指针,类型为 int (*)[4]*(a+1) 解引用得到 a[1],类型是 int[4],即一个包含 4 个 int 的一维数组。
  • 结果sizeof(*(a+1)) 返回第二行的大小,即 4 * sizeof(int) = 16 字节。
  • 输出16

5.3.8 sizeof(&a[0]+1)

printf("%zd\n", sizeof(&a[0]+1));
  • 解释&a[0] 是第一行的地址,类型为 int (*)[4]&a[0] + 1 指向第二行的地址,类型仍为 int (*)[4]
  • 结果sizeof(&a[0]+1) 返回指向数组一行的指针的大小。在 32 位系统上通常为 4 字节,在 64 位系统上通常为 8 字节。
  • 输出4(32 位系统)或 8(64 位系统)

5.3.9 sizeof(*(&a[0]+1))

printf("%zd\n", sizeof(*(&a[0]+1)));
  • 解释&a[0] + 1 是指向第二行的地址,类型为 int (*)[4]*(&a[0]+1) 解引用得到第二行,即 a[1],类型为 int[4]
  • 结果sizeof(*(&a[0]+1)) 返回 a[1] 的大小,即 4 * sizeof(int) = 16 字节。
  • 输出16

5.3.10 sizeof(*a)

printf("%zd\n", sizeof(*a));
  • 解释a 是一个指向第一行的指针,类型为 int (*)[4]*a 解引用得到第一行的内容,即 a[0],类型为 int[4]
  • 结果sizeof(*a) 返回第一行的大小,即 4 * sizeof(int) = 16 字节。
  • 输出16

5.3.11 sizeof(a[3])

printf("%zd\n", sizeof(a[3]));
  • 解释:虽然 a[3] 超出了数组的范围,但在 sizeof 中不会引发越界错误。a[3] 类型被视为 int[4],与 a[0] 相同。
  • 结果sizeof(a[3]) 返回 int[4] 的大小,即 4 * sizeof(int) = 16 字节。
  • 输出16

5.3.12 总结

表达式解释返回值(字节)输出(假设 int 为 4 字节)
sizeof(a)返回整个二维数组的大小4848
sizeof(a[0][0])返回第一个元素(int)的大小44
sizeof(a[0])返回第一行的大小1616
sizeof(a[0]+1)返回指向 a[0][1] 的指针的大小4(或 8)4 或 8
sizeof(*(a[0]+1))返回 a[0][1]int)的大小44
sizeof(a+1)返回指向 a[1] 的指针的大小4(或 8)4 或 8
sizeof(*(a+1))返回第二行的大小1616
sizeof(&a[0]+1)返回指向 a[1] 的指针的大小4(或 8)4 或 8
sizeof(*(&a[0]+1))返回 a[1](一行)的大小1616
sizeof(*a)返回 a[0](第一行)的大小1616
sizeof(a[3])返回 int[4] 的大小1616

这些表达式展示了 sizeof 在二维数组和指针上下文中的应用。对于数组,sizeof 返回总大小;对于指针,返回指针的大小。

数组名的意义:

  1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
  2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
  3. 除此之外所有的数组名都表示首元素的地址。

6. 指针运算笔试题解析

6.1 题目1:

#include <stdio.h>
int main()
{int a[5] = { 1, 2, 3, 4, 5 };int *ptr = (int *)(&a + 1);printf( "%d,%d", *(a + 1), *(ptr - 1));return 0;
}
//程序的结果是什么?

6.1.1 代码分析

6.1.1.1 初始化数组和指针
int a[5] = { 1, 2, 3, 4, 5 };

定义了一个包含 5 个元素的数组 a

  • 数组 a 的元素依次是:1, 2, 3, 4, 5
  • 数组在内存中是连续存储的。

假设数组 a 的起始地址为 0x100,则其布局如下(每个元素占用 4 个字节):

地址
0x1001
0x1042
0x1083
0x10C4
0x1105

指针的声明:

int *ptr = (int *)(&a + 1);

这里分两步解析:

  1. &a 表示 数组 a 的地址,其类型是 int (*)[5],即指向一个包含 5 个 int 的数组。
    • 假设 &a 的值是 0x100,则 &a + 1 跳过整个数组,占用的内存大小是 5 * sizeof(int),即 20 字节。
    • 所以,&a + 1 的值是 0x114
  2. (int *)(&a + 1)&a + 1 强制转换为 int * 类型。
    • ptr 最终指向地址 0x114

6.1.1.2 输出语句解析
printf("%d,%d", *(a + 1), *(ptr - 1));

这段代码输出两个值,分别是:

  1. *(a + 1)
    • a 是一个数组名,等价于指向数组首元素的指针,其类型是 int *
    • a + 1 是指向数组 a 的第二个元素(地址 0x104)。
    • *(a + 1) 的值是该地址存储的值,即 2
  2. *(ptr - 1)
    • ptr 当前指向地址 0x114
    • ptr - 1 是向后移动一个 int 类型的地址(4 字节),即 0x110
    • *(ptr - 1) 的值是 0x110 地址存储的值,即 5

在这里插入图片描述


6.1.2 程序输出

printf("%d,%d", *(a + 1), *(ptr - 1));

根据上面的分析:

  • *(a + 1) 的值是 2
  • *(ptr - 1) 的值是 5

所以,程序的输出是:

2,5

6.1.3 关键点总结

  1. 数组名的行为
    • 数组名 a 可以被视为指向数组首元素的指针。
    • 通过 a + i 可以访问数组第 i 个元素。
  2. &aa 的区别
    • a 是指向数组首元素的指针,类型是 int *
    • &a 是整个数组的地址,类型是 int (*)[5]
  3. 指针运算和强制类型转换
    • &a + 1 会跳过整个数组。
    • 将其转换为 int * 类型后,可以访问数组后的内存区域。
  4. 内存布局
    • 理解数组和指针的内存地址是关键,程序中利用了数组边界之后的地址。

6.2 题目2

//在X86环境下
//假设结构体的大小是20个字节
//程序输出的结果是啥?
struct Test
{int Num;char *pcName;short sDate;char cha[2];short sBa[4];
}*p = (struct Test*)0x100000;
int main()
{printf("%p\n", p + 0x1);printf("%p\n", (unsigned long)p + 0x1);printf("%p\n", (unsigned int*)p + 0x1);return 0;
}

6.2.1 代码分析

以下代码涉及指针运算、类型转换以及结构体的内存对齐规则。在 X86(32 位)环境下,指针大小为 4 字节,指针运算会依据指针的类型进行偏移。

6.2.1.1 结构体对齐与大小
struct Test
{int Num;            // 4 字节char *pcName;       // 4 字节short sDate;        // 2 字节char cha[2];        // 2 字节short sBa[4];       // 8 字节
} *p = (struct Test*)0x100000; // 假设结构体起始地址为 0x100000
// 假设结构体的总大小为 20 字节
  • 成员对齐
    • 成员 int Num 占 4 字节,地址偏移为 0
    • 成员 char *pcName 占 4 字节,地址偏移为 4
    • 成员 short sDate 占 2 字节,地址偏移为 8
    • 成员 char cha[2] 占 2 字节,地址偏移为 10
    • 成员 short sBa[4] 占 8 字节,地址偏移为 1220
  • 结构体对齐
    • 按最大成员对齐(4 字节),总大小为 20 字节。

6.2.1.2 指针初始化
struct Test *p = (struct Test*)0x100000;
  • 指针 p 指向地址 0x100000,表示该地址处存储一个 struct Test 结构体。

6.2.1.3 指针运算与输出分析
第一条语句:p + 0x1
printf("%p\n", p + 0x1);
  • p 是一个指向 struct Test 的指针。
  • p + 1的含义是指向下一个 struct Test,根据结构体大小跳过 20 字节:
    • 新地址 = 当前地址 + sizeof(struct Test) = 0x100000 + 20 = 0x100014

输出

0x100014

第二条语句:(unsigned long)p + 0x1
printf("%p\n", (unsigned long)p + 0x1);
  • (unsigned long)pp强制转换为无符号长整型:
    • 指针地址本身不变,转换后为整数类型 0x100000
  • 加法运算是直接对整数进行运算:
    • 结果 = 0x100000 + 0x1 = 0x100001

输出

0x100001

第三条语句:(unsigned int*)p + 0x1
printf("%p\n", (unsigned int*)p + 0x1);
  • (unsigned int*)pp 强制转换为 unsigned int* 类型。
  • unsigned int占用 4 字节,因此指针加法跳过 4 字节:
    • 新地址 = 当前地址 + sizeof(unsigned int) = 0x100000 + 4 = 0x100004

输出

0x100004

程序输出

综合上述分析,程序依次输出:

0x100014
0x100001
0x100004

关键点总结

  1. 指针类型与运算规则
    • p + 1 的偏移取决于指针类型指向的数据大小(sizeof(数据类型))。
  2. 强制类型转换
    • (unsigned long)p:将指针当作整数进行运算,结果直接加上偏移值。
    • (unsigned int*)p:改变指针类型,指针运算规则改为基于新的类型大小。
  3. 结构体对齐和大小
    • 结构体大小受内存对齐规则影响,成员偏移和对齐方式决定结构体的总大小。
  4. 内存模型与架构
    • X86 架构下,指针和 int 类型为 4 字节。

这段代码巧妙利用了指针运算和类型转换的特性,使输出产生不同的结果。

6.3 题目3

#include <stdio.h>
int main()
{int a[3][2] = { (0, 1), (2, 3), (4, 5) };int *p;p = a[0];printf( "%d", p[0]);return 0;
}

6.3.1 代码分析

这段代码考察了数组的初始化方式、二维数组的指针使用、以及逗号表达式的行为。


6.3.1.1 二维数组初始化
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
  • a 是一个 3×2 的二维数组。

  • 初始化使用了 逗号表达式 (x, y)

    • 在 C 中,逗号表达式 (x, y) 的值是表达式 y 的值。
    • 例如,(0, 1) 的结果是 1(2, 3) 的结果是 3(4, 5) 的结果是 5
  • 因此,数组 a 被初始化为:

    a[0][0] = 1, a[0][1] = 3
    a[1][0] = 5, a[1][1] = 未定义值
    

    (因为初始化的逗号表达式个数不足填满数组,所以未完全指定的元素值是未定义行为)


6.3.1.2 指针赋值
int *p;
p = a[0];
  • a[0]a 的第一行的地址,其类型是 int*,表示指向第一行的首元素。
  • 赋值后,指针 p 指向 a[0][0]

假设 a 在内存中的布局如下(地址以 0x1000 为例):

地址
0x10001
0x10043
0x10085
0x100C未定义值
0x1010未定义值
0x1014未定义值
6.3.1.3 输出操作
printf("%d", p[0]);
  • p[0] 等价于 *(p + 0),指向 a[0][0] 的值。
  • 根据初始化的结果,a[0][0] = 1,因此输出 1

在这里插入图片描述


6.3.2 程序输出

1

6.3.3 关键点总结

  1. 逗号表达式
    • (x, y) 返回 y 的值,这会影响数组的初始化。
  2. 二维数组与指针
    • a[0] 是第一行的地址,类型为 int*
  3. 指针与数组关系
    • 一维数组的指针可以直接用于元素访问,例如 p[0]
  4. 未完全初始化的数组
    • 如果初始化的值不足,剩余的元素未定义(如果全初始化了会被填充为 0)。在本例中可能存在未定义值的行为。

6.4 题目4

//假设环境是x86环境,程序输出的结果是啥?
#include <stdio.h>
int main()
{int a[5][5];int(*p)[4];p = a;printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);return 0;
}

6.4.1 代码分析

6.4.1.1 定义二维数组 a
int a[5][5];
  • a 是一个 5x5 的二维数组,包含 5 行,每行 5 个 int
  • x86 环境下,每个 int 占用 4 字节,因此整个数组 a 的总大小是 5 * 5 * 4 = 100 字节。

6.4.1.2 定义指针 p
int (*p)[4];
  • p 是一个指向 int[4] 类型数组的指针。
  • 换句话说,p 是一个指向长度为 4 的 int 数组的指针。
  • 这意味着在通过 p 进行指针运算时,它会认为每行是 4 个整数,而不是 a 实际的每行 5 个整数。

6.4.1.3 指针赋值
p = a;
  • a 是一个 int[5][5] 类型的数组,可以隐式转换为 int (*)[5] 类型(即指向 int[5] 的指针)。
  • 尽管 p 的类型是 int (*)[4],编译器在这里允许将 a 赋值给 p,但可能会给出警告,因为类型并不完全匹配。
  • 赋值完成后,p 指向数组 a 的起始地址,即 a[0][0] 的地址。

6.4.1.4 计算并输出地址差
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);

这行代码中的表达式 &p[4][2] - &a[4][2] 用于计算 &p[4][2]&a[4][2] 之间的差异。我们逐个解析:

  1. &p[4][2] 的含义
    • p 被解释为一个指向 int[4] 的指针。
    • p[4] 表示 p 所指向的“数组”的第五行。
    • &p[4][2] 指的是 p[4] 的第三个元素(即第 2 个索引位置)的地址。
    • 因为 p 被解释为一个每行包含 4 个整数的指针,p[4] 实际上指向的是数组 a 中的第 4 * 4 = 16 个整数。
    • 所以 &p[4][2] 实际上是 a[16 + 2],即 a[18] 的地址。
  2. &a[4][2] 的含义
    • a 是一个 5x5 的二维数组。
    • &a[4][2] 指的是 a 的第五行的第三个元素的地址。
    • 在数组 a 中,a[4][2] 实际上是 a 中的第 4 * 5 + 2 = 22 个元素的地址。
  3. 指针差值计算
    • &p[4][2] - &a[4][2] 计算的是 a 中第 18 个元素地址和第 22 个元素地址的差值。
    • 结果是 18 - 22 = -4

在这里插入图片描述


程序输出

printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);

根据上述分析,程序的输出将是:

-4, -4

关键点总结

  1. 二维数组的指针类型
    • aint[5][5] 类型,实际上每行有 5 个整数。
    • p 的类型是 int (*)[4],它将每行视为 4 个整数的数组指针。
  2. 指针差值计算
    • &p[4][2] - &a[4][2] 计算了两个不同类型指针之间的地址差,这个差值是以 int 为单位的。
  3. 输出的结果
    • 由于 pa 的类型差异导致了计算结果的偏移,输出为 -4

6.5 题目5

#include <stdio.h>
int main()
{int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };int *ptr1 = (int *)(&aa + 1);int *ptr2 = (int *)(*(aa + 1));printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));return 0;
}

6.5.1 代码逐行分析

int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
6.5.1.1 定义二维数组 aa
  • aa 是一个 2x5 的二维数组,即包含 2 行,每行有 5 个整数。
  • 数组 aa 被初始化为 { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 },所以 aa 的内容如下:
aa[0][0] = 1,  aa[0][1] = 2,  aa[0][2] = 3,  aa[0][3] = 4,  aa[0][4] = 5
aa[1][0] = 6,  aa[1][1] = 7,  aa[1][2] = 8,  aa[1][3] = 9,  aa[1][4] = 10

假设数组 aa 的起始地址为 0x1000,则内存布局为(每个整数占 4 字节):

地址
0x10001
0x10042
0x10083
0x100C4
0x10105
0x10146
0x10187
0x101C8
0x10209
0x102410

int *ptr1 = (int *)(&aa + 1);
6.5.1.2 指针 ptr1 的计算
  • &aa 是数组 aa 的地址,它的类型是 int (*)[2][5],即指向一个 2x5 整数数组的指针。
  • &aa + 1 使指针跳过整个 aa 数组。因为 aa 包含 2 行,每行 5 个 int,所以 aa 的大小是 2 * 5 * sizeof(int) = 40 字节。
  • 因此,&aa + 1 指向 aa 之后的内存位置,即 0x1000 + 40 = 0x1028
  • (int *)(&aa + 1)&aa + 1 强制转换为 int* 类型,所以 ptr1 最终指向地址 0x1028

int *ptr2 = (int *)(*(aa + 1));
6.5.1.3 指针 ptr2 的计算
  • aa 是一个指向 int[5] 的数组指针,即 aa 是一个指向第 0 行的指针。
  • aa + 1 表示数组的第 1 行的首地址(即 aa[1] 的地址)。
  • *(aa + 1) 解引用得到 aa[1] 的首地址,即 &aa[1][0],也就是 6 的地址(假设为 0x1014)。
  • *(aa + 1) 强制转换为 int*,所以 ptr2 最终指向 aa[1][0] 的地址(即 0x1014)。

printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
6.5.1.4 输出操作
  • *(ptr1 - 1)
    • ptr1 指向地址 0x1028ptr1 - 1 指向 0x1024
    • 0x1024aa[1][4] 的地址,对应的值是 10
    • 因此,*(ptr1 - 1) 的值是 10
  • *(ptr2 - 1)
    • ptr2 指向地址 0x1014ptr2 - 1 指向 0x1010
    • 0x1010aa[0][4] 的地址,对应的值是 5
    • 因此,*(ptr2 - 1) 的值是 5

在这里插入图片描述


6.5.2 程序输出

根据上述分析,程序的输出为:

10,5

6.5.3 关键点总结

  1. 二维数组的地址计算
    • &aa 是整个数组的地址,跳过数组时需要考虑整个数组的大小。
    • aa + 1 指向数组的下一行。
  2. 指针运算与类型转换
    • &aa + 1 转换为 int * 使得指针可以按单个 int 进行减法操作。
    • ptr1 - 1ptr2 - 1 的结果取决于二维数组内存布局。
  3. 内存布局理解
    • 通过理解二维数组在内存中的排布,能够正确解析指针的指向位置。

6.6 题目6

#include <stdio.h>
int main()
{char *a[] = {"work","at","alibaba"};char**pa = a;pa++;printf("%s\n", *pa);return 0;
}

6.6.1 代码分析

这段代码涉及到字符指针数组和指针运算。让我们逐行分析代码的工作原理和输出。


char *a[] = {"work", "at", "alibaba"};
6.6.1.1 定义字符指针数组 a
  • a 是一个字符指针数组,包含 3 个字符串:"work""at""alibaba"

  • 每个元素 a[i] 都是一个指向字符串的指针,因此 a 的类型是 char *[3]

    内存布局如下(假设字符串地址为 0x10000x1005、和 0x1008,具体地址只是举例):

    a[0]指向字符串 "work"地址假设为 0x1000
    a[1]指向字符串 "at"地址假设为 0x1005
    a[2]指向字符串 "alibaba"地址假设为 0x1008

char **pa = a;
6.6.1.2 定义字符指针的指针 pa 并初始化
  • pa 是一个指向指针的指针,类型是 char **

  • pa 被初始化为 a,即指向 a 数组的第一个元素 a[0] 的地址。

    此时,pa 的指向关系如下:

    pa -> a[0] -> "work"
    

pa++;
6.6.1.3 指针运算
  • pa++ 使 pa 移动到数组 a 的下一个元素。

  • 由于 pa 是一个 char** 类型的指针,pa++ 会使 pa 指向 a[1](即 a 中的第二个元素,指向字符串 "at" 的指针)。

    现在,pa 的指向关系如下:

    pa -> a[1] -> "at"
    

printf("%s\n", *pa);
6.6.1.4 打印字符串
  • *pa 解引用 pa,即得到 a[1],指向字符串 "at" 的指针。
  • printf("%s\n", *pa); 打印 *pa 指向的字符串,即 "at"

在这里插入图片描述


6.6.2 程序输出

根据以上分析,程序的输出为:

at

6.6.3 关键点总结

  1. 字符指针数组
    • a 是一个字符指针数组,每个元素都指向一个字符串常量。
    • a 的类型是 char *[3],包含 3 个指针,分别指向 "work""at""alibaba"
  2. 指针的偏移
    • pa 是一个 char** 类型的指针,最初指向 a[0]
    • pa++ 使 pa 指向 a[1],即第二个字符串 "at" 的地址。
  3. 指针解引用
    • *pa 解引用得到 a[1],指向字符串 "at" 的指针。
    • 打印 *pa 输出 "at"

通过这些分析,程序最终输出 "at"

6.7 题目7

#include <stdio.h>
int main()
{char *c[] = {"ENTER","NEW","POINT","FIRST"};char**cp[] = {c+3,c+2,c+1,c};char***cpp = cp;printf("%s\n", **++cpp);printf("%s\n", *--*++cpp+3);printf("%s\n", *cpp[-2]+3);printf("%s\n", cpp[-1][-1]+1);return 0;
}

6.7.1 代码分析

char *c[] = {"ENTER","NEW","POINT","FIRST"};
6.7.1.1 定义字符指针数组 c
  • c 是一个字符指针数组,包含 4 个字符串:"ENTER""NEW""POINT""FIRST"

  • 每个元素 c[i] 都是一个指向字符串的指针。

  • 假设 c 数组的内存布局如下(具体地址只是示例):

    c[0]指向字符串 "ENTER"
    c[1]指向字符串 "NEW"
    c[2]指向字符串 "POINT"
    c[3]指向字符串 "FIRST"

char** cp[] = {c+3, c+2, c+1, c};
6.7.1.2 定义字符指针的指针数组 cp
  • cp 是一个数组,包含 4 个 char** 类型的元素。

  • 每个元素 cp[i] 是一个指向 c 数组中元素的指针。

  • cp 的内容为:{ c+3, c+2, c+1, c }

    • c+3 是指向 c[3] 的指针,指向 "FIRST";
    • c+2 是指向 c[2] 的指针,指向 "POINT";
    • c+1 是指向 c[1] 的指针,指向 "NEW";
    • c 是指向 c[0] 的指针,指向 "ENTER"

    所以,cp 数组的内容布局如下:

    cp[0]指向 c[3],即指向 "FIRST"
    cp[1]指向 c[2],即指向 "POINT"
    cp[2]指向 c[1],即指向 "NEW"
    cp[3]指向 c[0],即指向 "ENTER"

char*** cpp = cp;
6.7.1.3 定义三重指针 cpp
  • cpp 是一个 char*** 类型的指针,初始化为 cp
  • 因此,cpp 最初指向 cp[0],即 c+3,也就是指向字符串 "FIRST" 的指针。

输出语句逐行分析
printf("%s\n", **++cpp);
6.7.1.4 第一个输出:**++cpp
  • ++cpp:将 cpp向前移动一个位置,从 cp[0]移动到 cp[1]
    • 此时,cpp 指向 cp[1],即 c+2
  • **cpp:解引用两次,即 *(*cpp)
    • *cppc+2,即指向 "POINT"
    • **cpp 解引用 c+2,得到字符串 "POINT"

输出

POINT

在这里插入图片描述


printf("%s\n", *--*++cpp+3);
6.7.1.5 第二个输出:*--*++cpp+3
  • ++cpp:将 cpp向前移动一个位置,从cp[1]移动到cp[2]
    • 此时,cpp 指向 cp[2],即 c+1
  • *cpp:解引用一次,得到 c+1,即指向 "NEW" 的指针。
  • --*cpp:对 *cpp 进行递减操作,即从 c+1 移动到 c,现在指向 "ENTER"
  • *--*cpp:现在 *cpp 指向 "ENTER",解引用得到字符串 "ENTER"
  • *--*cpp + 3:对字符串 "ENTER" 进行偏移 3 个字符,得到 "ER"

输出

ER

在这里插入图片描述


printf("%s\n", *cpp[-2]+3);
6.7.1.6 第三个输出:*cpp[-2]+3
  • cpp[-2]cpp 当前指向 cp[2],因此 cpp[-2] 指向 cp[0],即 c+3
  • *cpp[-2]:解引用 cpp[-2],即 c+3,指向字符串 "FIRST"
  • *cpp[-2] + 3:对字符串 "FIRST" 偏移 3 个字符,得到 "ST"

输出

ST

在这里插入图片描述


printf("%s\n", cpp[-1][-1]+1);
6.7.1.7 第四个输出:cpp[-1][-1]+1
  • cpp[-1]cpp 当前指向 cp[2],所以 cpp[-1] 指向 cp[1],即 c+2
  • cpp[-1][-1]
    • cpp[-1]c+2,即指向 "POINT"
    • cpp[-1][-1] 相当于 c[1],即 "NEW"
  • cpp[-1][-1] + 1:对字符串 "NEW" 偏移 1 个字符,得到 "EW"

输出

EW

在这里插入图片描述


6.7.2 程序的最终输出

POINT
ER
ST
EW

6.7.3 关键点总结

  1. 多重指针解引用
    • 代码通过多层指针操作和数组下标引用来访问字符数组。
    • 解引用的顺序和操作顺序(如 ++--)会影响最终的指向结果。
  2. 字符数组偏移
    • 偏移操作(如 +3)用于获取字符串的子串。
  3. 指针算术运算
    • cpp[-2]cpp[-1][-1] 等复杂的指针运算是代码的核心,需要清晰理解多级指针之间的关系。

—完—

相关文章:

C语言第13节:指针(3)

1. 回调函数 回调函数的基本思想是&#xff0c;将函数指针作为参数传递给另一个函数&#xff0c;并在需要时通过这个函数指针调用对应的函数。这种方式允许一个函数对执行的内容进行控制&#xff0c;而不需要知道具体的实现细节。 回调函数在以下场景中尤为有用&#xff1a; …...

java:简单小练习,面积

面积&#xff1a;圆和长方形 接口&#xff1a;实现面积 test:调用 一、interface: 对于接口&#xff0c;它是Java中一个新增的知识点&#xff0c;而C中没有&#xff0c;因为Java有一个缺陷就是不可以实现多继承&#xff0c;只可以单继承&#xff0c;这就限制了有些功能的使…...

@Autowired 和 @Resource思考(注入redisTemplate时发现一些奇怪的现象)

1. 前置知识 Configuration public class RedisConfig {Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> template new RedisTemplate<>();template.setConnectionFactory(facto…...

PostgreSQL提取JSON格式的数据(包含提取list指定索引数据)

PostgreSQL提取JSON格式的数据&#xff08;包含提取list指定索引数据&#xff09; ->>, ->, #>, #>> 在PostgreSQL中&#xff0c;处理json或jsonb类型数据时&#xff0c;->>, ->, #> 和 #>> 是非常有用的操作符&#xff0c;它们允许你以…...

如何利用谷歌浏览器提高网络安全

在当今数字化时代&#xff0c;网络安全已成为我们不可忽视的重要议题。作为全球最受欢迎的网络浏览器之一&#xff0c;谷歌浏览器不仅提供了快速、便捷的浏览体验&#xff0c;还内置了多种安全功能来保护用户的在线安全。本文将详细介绍如何通过谷歌浏览器提高您的网络安全&…...

go-zero(四) 错误处理(统一响应信息)

go-zero 错误处理&#xff08;统一响应信息&#xff09; 在实现注册逻辑时&#xff0c;尝试重复注册可能会返回 400 状态码&#xff0c;显然不符合正常设计思维。我们希望状态码为 200&#xff0c;并在响应中返回错误信息。 一、使用第三方库 1.下载库 目前 go-zero官方的…...

1.1 爬虫的一些知识(大模型提供语料)

1.1 爬虫的一些知识&#xff08;大模型提供语料&#xff09; 网页资源&#xff1a; 资源组织方式&#xff1a;列表分页,搜索引擎&#xff0c;推荐 发送请求的文档类型&#xff1a;html ,js 响应请求的文档类型&#xff1a;html,js,json 请求方式&#xff1a;同步和异步 页面形式…...

Linux开发工具:Vim 与 gcc,打造高效编程的魔法双剑

文章目录 一、初识Vim模式 二、vim基本操作2.1基础操作2.2命令模式/正常模式2.2.1光标定位2.2.2复制粘贴、删除2.2.3撤销2.2.4替换字符2.2.5替换模式 2.3底行模式2.3.1退出vim和**保存文件**2.3.2定位文本字符串2.3.3命令2.3.4实现分屏2.3.5替换指定字符串 2.4补充指令2.4.1视图…...

cesium for unity的使用

先聊聊导入 看到这里的因该能够知道&#xff0c;官网以及网上绝大多数的方法都导入不进来&#xff0c;那么解决方法如下: 两个链接&#xff1a;按照顺序依次下载这两个tgz和zip&#xff0c;其中tgz为主要部分&#xff0c;zip为示例工程项目 如果您要查看示例工程项目的话&am…...

Android AOSP 架构和各层次开发内容介绍

一、系统架构总况​​​​ 官方文档:架构概览 | Android Open Source Project (google.cn)https://source.android.google.cn/docs/core/architecture?hl=zh-cn 下面是Google Android 提供的最新架构层次图: 图. AOSP 的软件堆栈层次 System API 表示仅供合作伙伴和 OEM…...

Kafka 到 Kafka 数据同步

简述 Kafka 为处理实时数据提供了一个统一、高吞吐、低延迟的平台&#xff0c;其持久化层本质上是一个“按照分布式事务日志架构的大规模发布/订阅消息队列”&#xff0c;这使它作为企业级基础设施来处理流式数据非常有价值。因此实现 Kafka 到 Kafka 的数据同步也成了一项重要…...

华为刷题笔记--题目索引

文章目录 更多关于刷题的内容欢迎订阅我的专栏华为刷题笔记简单题目 更多关于刷题的内容欢迎订阅我的专栏华为刷题笔记 该专栏题目包含两部分&#xff1a; 100 分值部分题目 200 分值部分题目 所有题目都会陆续更新&#xff0c;订阅防丢失 简单题目 –题目分值试卷1华为OD机…...

osgEarth加载倾斜摄影测量数据

一、代码 // .cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include <osgViewer/Viewer> #include <osgEarth/Notify> #include <osgEarth/EarthManipulator>...

消息推送问题梳理-团队管理

管理用户界面&#xff1a; 新增加用户列表&#xff1a;这些用有资格收到推送消户息 当删除一个医生的时候&#xff0c;重新添加这个人的时候 发现团队中没有这个人了 &#xff0c;这个时候 需要重新添加这个人。 处理这个问题遵循的原则&#xff1a; 删除这个用户的时候&…...

如何在 Ubuntu 上使用 Docker 部署 LibreOffice Online

简介 LibreOffice Online&#xff08;也称为Collabora Online&#xff09;是一个开源的在线办公套件&#xff0c;它提供了与LibreOffice桌面版相似的功能&#xff0c;但完全在云端运行。这意味着用户可以通过浏览器访问和编辑文档&#xff0c;而无需在本地计算机上安装任何软件…...

MongoDB数据备份与恢复(内含工具下载、数据处理以及常见问题解决方法)

一、工具准备 对MongoDB进行导入导出、备份恢复等操作时需要用到命令工具&#xff0c;我们要先检查一下MongoDB安装目录下是否有这些工具&#xff0c;正常情况下是没有的:)&#xff0c;因为新版本的MongoDB安装时不包含这些工具&#xff0c;需要我们手动下载安装。下载成功之后…...

代码随想录第三十一天| 56. 合并区间 738.单调递增的数字

56. 合并区间 题目描述 给定一个区间的集合 intervals&#xff0c;请合并所有重叠的区间。 解题思路 排序区间 按照每个区间的起点 start 升序排序&#xff0c;便于后续合并。 合并区间 使用两个变量 start 和 right 分别记录当前区间的起点和终点。遍历排序后的区间&#x…...

C语言基本知识 2.2void 函数

在C语言中&#xff0c; void 是一个重要的关键字&#xff0c;具有多种用途&#xff0c;以下是详细介绍&#xff1a; 函数返回值类型声明 - 当函数不需要返回任何值时&#xff0c;可以将函数的返回值类型声明为 void 。例如&#xff1a; void printMessage() { printf(…...

Spring 框架中哪些接口可以创建对象

Spring 框架中哪些接口可以创建对象 在 Spring 框架中&#xff0c;向 IOC 容器中添加 Bean 主要有以下几种接口和方式。Spring 提供了不同的手段来实现对象的创建和管理&#xff0c;涵盖了不同的需求和场景。以下是几种常用的接口和方式&#xff1a; 1. BeanFactory 接口 Be…...

豆瓣书摘 | 爬虫 | Python

获取豆瓣书摘&#xff0c;存入MongoDB中。 import logging import timeimport requests from bs4 import BeautifulSoup from pymongo import MongoClientheaders {accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,image/apng,*/*;q0.8,…...

Oracle数据库物理存储结构管理

​ 大家好&#xff0c;我是程序员小羊&#xff01; 前言&#xff1a; 安装和配置Oracle数据库是一个涉及多个步骤的任务&#xff0c;通常包括环境准备、软件安装、数据库创建和基本配置等。以下是详细的安装与配置流程。 一、环境准备 1. 系统要求 操作系统&#xff1a;Wind…...

java——Map接口

Java的Map接口是一种键值对的数据结构&#xff0c;用于存储和操作键值对的集合。它是Java集合框架中的一部分&#xff0c;提供了一种以键值对形式组织数据的方法。 Map接口有多个实现类&#xff0c;常用的有HashMap、TreeMap和LinkedHashMap。每种实现类都有不同的特点和适用场…...

量子计算机全面解析:技术、应用与未来

标题&#xff1a;量子计算机全面解析&#xff1a;技术、应用与未来 一、什么是量子计算机&#xff1f; 量子计算机是一种利用量子力学原理&#xff08;如叠加、纠缠和干涉&#xff09;进行计算的新型计算设备。与传统计算机基于比特&#xff08;0 和 1&#xff09;的运算方式不…...

IDEA相关(包括但不限于快捷键,使用技巧)成长笔记

1.IDEA创建及命名规范 IDEA创建是&#xff1a;项目、模块、包、类 命名规范&#xff1a; 1.项目名全部小写 2.包名全部小写 3.类名首字母大写&#xff0c;一般都是使用驼峰式命名。 如&#xff1a;public class MyFirstClass{} 4.同上&#xff1a; 变量名、方法名首字母小…...

【再谈设计模式】适配器模式 ~接口兼容的桥梁

一、引言 在软件开发的复杂世界里&#xff0c;不同的组件、类或者系统往往有着各自独立的设计和接口定义。当需要将这些原本不兼容的部分整合在一起协同工作时&#xff0c;就像尝试将方形的榫头插入圆形的卯眼一样困难。适配器设计模式就如同一位神奇的工匠&#xff0c;能够巧妙…...

使用Cursor和Claude AI打造你的第一个App

大家好&#xff0c;使用Cursor和Claude AI打造应用程序是一个结合智能代码辅助和人工智能对话的创新过程。Cursor是一个编程辅助工具&#xff0c;它通过智能代码补全、聊天式AI对话和代码生成等功能&#xff0c;帮助开发者提高编程效率。Claude AI则是一个强大的人工智能平台&a…...

粗读Apache Paimon 的基本概念及其组成结构

文章目录 一、Paimon的基本概念&#xff08;一&#xff09;快照&#xff08;二&#xff09;分区&#xff08;三&#xff09;分桶&#xff08;四&#xff09;一致性保证 二、Paimon的组成结构&#xff08;一&#xff09;文件布局&#xff08;二&#xff09;存储结构&#xff08;…...

c++调用 c# dll 通过 P/Invoke (详细避坑)

项目场景&#xff1a; VS2022 .NET8 &#xff0c; 项目应用的库需要支持AOT&#xff0c;不支持AOT的库看这里 我的c项目很奇怪&#xff0c;如果使用 clr 调用c# dll,会有很多报错&#xff0c;所以使用 P/Invoke方法&#xff0c;这个方法不会有任何奇怪的报错 解决方案 示例…...

李春葆《数据结构》——图相关代码

邻接矩阵结构体&#xff1a; #define MAX<最大结点个数> #define INF 32765 //定义无穷 typedef struct{int no;//顶点的编号&#xff1b;InfoType info;//顶点的其他信息 }vertexType;//顶点的类型 typedef struct{int edges[MAX][Max];//邻接矩阵数组 int vertexTy…...

Linux驱动开发第2步_“物理内存”和“虚拟内存”的映射

“新字符设备的GPIO驱动”和“设备树下的GPIO驱动”都要用到寄存器地址&#xff0c;使用“物理内存”和“虚拟内存”映射时&#xff0c;非常不方便&#xff0c;而pinctrl和gpio子系统的GPIO驱动&#xff0c;非常简化。因此&#xff0c;要重点学习pinctrl和gpio子系统下的GPIO驱…...

减少网站跳出率/站长统计官网

一、为什么要挂载windows分区 随着linux越来越普及&#xff0c;很多linux FANS正在或者已经从windows系统过度到linux系统上了。在这个过度时期&#xff0c;大部分FANS在自己的机器上都是至少装了两套系统——windows和linux。这样&#xff0c;问题就出现了&#xff0c;当FANS们…...

网站建设上qq图标去除/建站快车

快速导读&#xff1a;Q1&#xff1a;vbs怎么调用子程序?你可以这样操作比如你要调用C:\m.exe Set ws CreateObject("Wscript.Shell") Set fs CreateObject("Scripting.FileSystemObject") ws.Run "c:\m.exe" 希望能帮助你&#xff01;&#x…...

南京哪家网络公司做网站优化好/关键词优化排名平台

C中的IO类(iostream, fstream, stringstream)小结 参考网址&#xff1a;https://blog.csdn.net/stpeace/article/details/44763009 以前学习C的时候&#xff0c; 总是囫囵吞枣地理解cin, cout等东东&#xff0c; 最近又在复习C, 复习到IO类这一章节的时候&#xff0c; 有点感…...

京东淘宝网站是怎么做的/网络推广公司哪里好

消防应急照明和疏散指示系统是由各类消防应急灯具及相关装组成&#xff0c;为人员疏散、消防作业提供照明和疏散指示的系统。民用建筑中应设疏散照明的部位在GB50016-2014《建筑设计防火规范》第10.3.1条做了相应规定。原先国家规范要求&#xff1a;JGJ333-2014《会展建筑电气设…...

潍坊的网站开发公司/长沙网站推广和优化

jQuery.extend 对jQuery对象的扩展&#xff0c;可以理解为静态方法&#xff0c;不需要实例jQuery就可以使用。 <script type"text/javascript"> <!-- jQuery.extend({ add: function(a, b) { return a b; …...

wordpress 拼车/深圳网络营销推广培训

【学习笔记】Linux操作系统bond配置主/备网卡绑定与测试案例时间:2016-10-17 15:35 来源:Oracle研究中心 作者:网络 点击:次天萃荷净分享一篇关于Linux操作系统bond的7种模式中最常用的主/备模式配置方法案例&#xff0c;是测试rhel 5这个版本下配置网卡的多个bond。linu…...