C语言的骨骼与血液:深度剖析指针、数组与多级间接——从一个“迷思”说起

直接上今晚碰到的问题:
到底难在哪里这个指针问题??

这是正确的代码:

为什么**char 用在指针里,会变成这样????

这几张图一定要嵌入在脑子里!!!

非常容易错!

刚开始直接给我整蒙了???????

**p调用的不就是int* ,直接能打印char []啊
 

结果是pple   ple le   e ............


可以说,这个问她我完全可以不管直接继续写代码的,但是好奇心让哥们想起来那句话:

只有在挑错过程中,你的代码能力才能得到提升


于是,我又改了一下:

果然:
 

引言:指针与数组的“罗生门”——一个经典“陷阱”的启示

在C语言的广袤深邃中,指针与数组无疑是其最具魅力、也最令人望而却步的双生子。它们如影随形,时而互为表里,时而又潜藏着令人费解的差异,常常让初学者乃至经验丰富的开发者都为之困惑。今天,我们将从一个看似简单、实则蕴含C语言“精髓”的经典代码片段入手,抽丝剥茧,层层深入,直抵C语言内存管理的神经末梢与处理器指令的脉动,共同揭开指针、数组与多级间接的“罗生门”。

你或许曾不经意间写下这样的代码:

// 代码片段 1:初探指针数组的访问(预期行为)
#include <stdio.h> // 引入标准输入输出库

int main() {
    // 字符指针数组:每个元素都是一个指向字符串字面量的指针
    // 这些字符串字面量存储在程序的只读数据段 (.rodata)
    char * s[] = {"apple ", "banana", "watermelon", "shit", "fuckyou"};
    
    // char** p3:一个指向 char* 类型变量的指针
    // 在这里,p3 被初始化为指向 s 数组的第一个元素(即 s[0] 的地址)
    char** p3 = s; 
    
    // 遍历 s 数组,以两种方式打印其元素
    printf("--- 通过 *(p3+i) 方式访问 ---\n");
    for(int i = 0 ; i < sizeof(s) / sizeof(s[0]); i++){
        // s[i]:直接通过数组下标访问,获取第 i 个字符串指针
        printf("数组下标 s[%d]: %s\n", i, s[i]);
        
        // *(p3+i):p3 是 char**,p3+i 表示向后移动 i 个 char* 的大小
        // 然后解引用 *(p3+i),得到第 i 个 char* 指向的字符串
        printf("指针方式 *(p3+%d): %s\n", i, *(p3+i));
    }
    printf("\n");

    return 0;
}

其输出(与你代码的第一部分类似,但格式稍作调整):

--- 通过 *(p3+i) 方式访问 ---
数组下标 s[0]: apple 
指针方式 *(p3+0): apple 
数组下标 s[1]: banana
指针方式 *(p3+1): banana
数组下标 s[2]: watermelon
指针方式 *(p3+2): watermelon
数组下标 s[3]: shit
指针方式 *(p3+3): shit
数组下标 s[4]: fuckyou
指针方式 *(p3+4): fuckyou

这完全符合我们的直觉和预期:*(p3+i) 正确地访问了数组 s 中的每个字符串。然而,当你不经意间将代码修改为:

// 代码片段 2:引发“迷思”的代码(实际行为)
#include <stdio.h> // 引入标准输入输出库

int main() {
    char * s[] = {"apple ", "banana", "watermelon", "shit", "fuckyou"};
    char** p3 = s;
    
    printf("--- 通过 (*p3)+i 方式访问 ---\n");
    for(int i = 0 ; i < sizeof(s) / sizeof(s[0]); i++){
        printf("数组下标 s[%d]: %s\n", i, s[i]);
        
        // (*p3)+i:先解引用 p3 得到 *p3(即 s[0]),它是一个 char*
        // 然后对这个 char* 进行加 i 操作,意味着在“apple ”字符串内部偏移 i 个字节
        // 打印的结果是从 "apple " 字符串的第 i 个字符开始的子串
        printf("指针方式 (*p3)+%d: %s\n", i, (*p3)+i);
    }
    printf("\n");

    return 0;
}

输出却变得诡异起来:

--- 通过 (*p3)+i 方式访问 ---
数组下标 s[0]: apple 
指针方式 (*p3)+0: apple 
数组下标 s[1]: banana
指针方式 (*p3)+1: pple 
数组下标 s[2]: watermelon
指针方式 (*p3)+2: ple 
数组下标 s[3]: shit
指针方式 (*p3)+3: le 
数组下标 s[4]: fuckyou
指针方式 (*p3)+4: e
```by p3: 1 element is pple`?`by p3: 2 element is ple`?发生了什么?为什么 `*(p3+i)` 和 `(*p3)+i` 的结果天差地别?

这正是C语言的魅力所在——它允许你以极高的自由度直接操作内存,但也要求你对底层机制有颗粒度极细的理解。这种“迷思”并非缺陷,而是C语言精髓的直接体现。接下来,我们将一步步解开这个谜团,让你真正掌握C语言的“骨骼与血液”。

## 第一章:C语言的基石——指针的本质与内存寻址

要理解上述代码的差异,我们必须回到C语言最根本的概念:指针和内存。

### 1.1 指针:地址的抽象与具象

在C语言中,**指针**是一个变量,它存储的不是数据本身,而是**数据所在的内存地址**。你可以把内存想象成一个巨大的格子间,每个格子都有一个唯一的编号(地址)。指针就是记录这些格子编号的“小纸条”。

#### 1.1.1 指针的类型与大小

每一个指针都有其特定的**类型**(如 `int*`, `char*`, `char**`等)。这个类型告诉编译器两件事:
1. **它指向什么类型的数据?** (例如 `int*` 指向一个整数,`char*` 指向一个字符)
2. **在进行指针算术时,步长是多少?** (例如 `int*` 移动一个单位是 `sizeof(int)` 字节,`char*` 移动一个单位是 `sizeof(char)` 字节)

**指针变量本身的大小**(在32位系统上通常是4字节,64位系统上通常是8字节)与它指向的数据类型无关,它只取决于系统的地址总线宽度。

```c
// 代码片段 3:指针类型与大小
#include <stdio.h> // 引入标准输入输出库
#include <stddef.h> // 引入 size_t

int main() {
    int   num    = 100;
    char  ch     = 'A';
    int* ptr_int  = &num;   // ptr_int 存储 num 的地址
    char* ptr_char = &ch;    // ptr_char 存储 ch 的地址

    // 打印不同类型指针变量本身的大小
    printf("--- 指针类型与大小 ---\n");
    printf("sizeof(int)         : %zu bytes\n", sizeof(int));
    printf("sizeof(char)        : %zu bytes\n", sizeof(char));
    printf("sizeof(int*)        : %zu bytes (在当前系统上)\n", sizeof(int*));
    printf("sizeof(char*)       : %zu bytes (在当前系统上)\n", sizeof(char*));
    printf("sizeof(char**)      : %zu bytes (在当前系统上)\n", sizeof(char**));
    printf("sizeof(void*)       : %zu bytes (通用指针,在当前系统上)\n", sizeof(void*));
    printf("\n");

    // 打印变量地址
    printf("变量 num 的地址     : %p\n", (void*)&num);
    printf("变量 ch 的地址      : %p\n", (void*)&ch);
    printf("指针 ptr_int 的值 (num 的地址): %p\n", (void*)ptr_int);
    printf("指针 ptr_char 的值 (ch 的地址): %p\n", (void*)ptr_char);
    printf("\n");

    return 0;
}

输出示例(可能因系统而异):

--- 指针类型与大小 ---
sizeof(int)         : 4 bytes
sizeof(char)        : 1 bytes
sizeof(int*)        : 8 bytes (在当前系统上)
sizeof(char*)       : 8 bytes (在当前系统上)
sizeof(char**)      : 8 bytes (在当前系统上)
sizeof(void*)       : 8 bytes (通用指针,在当前系统上)

变量 num 的地址     : 0x7ffee613a7c8
变量 ch 的地址      : 0x7ffee613a7c7
指针 ptr_int 的值 (num 的地址): 0x7ffee613a7c8
指针 ptr_char 的值 (ch 的地址): 0x7ffee613a7c7

请注意,我的当前环境是64位系统,所以指针大小为8字节。在32位系统上,它们通常是4字节。

1.1.2 解引用:从地址到数据

解引用操作符 * (星号)的意义是“访问指针所指向的内存地址中的数据”。

// 代码片段 4:指针的解引用
#include <stdio.h> // 引入标准输入输出库

int main() {
    int value = 123;
    int *ptr = &value; // ptr 存储 value 的地址

    printf("--- 指针的解引用 ---\n");
    printf("变量 value 的值: %d\n", value);
    printf("指针 ptr 存储的地址: %p\n", (void*)ptr);
    printf("解引用 ptr 得到的值 (*ptr): %d\n", *ptr); // *ptr 访问 value 内存并取出值

    // 修改通过指针解引用的值
    *ptr = 456; // 改变了 value 的值
    printf("通过 *ptr 修改后,变量 value 的值: %d\n", value);
    printf("\n");

    return 0;
}

输出:

--- 指针的解引用 ---
变量 value 的值: 123
指针 ptr 存储的地址: 0x7ffce2e9512c
解引用 ptr 得到的值 (*ptr): 123
通过 *ptr 修改后,变量 value 的值: 456

1.2 指针算术:按类型步进

指针算术(加法、减法)是C语言的一个核心特性,但它并非简单的字节加减。当对指针进行 + n- n 操作时,实际移动的字节数是 n * sizeof(指针所指向的类型)。这就是**“按类型步进”**原则。

// 代码片段 5:指针算术的“按类型步进”
#include <stdio.h> // 引入标准输入输出库

int main() {
    int arr_int[5] = {10, 20, 30, 40, 50}; // 整数数组
    char arr_char[5] = {'a', 'b', 'c', 'd', 'e'}; // 字符数组

    int* p_int = arr_int;   // p_int 指向 arr_int[0]
    char* p_char = arr_char; // p_char 指向 arr_char[0]

    printf("--- 指针算术的按类型步进 ---\n");
    printf("初始 p_int 地址: %p, 指向值: %d\n", (void*)p_int, *p_int);
    printf("p_int + 1 地址: %p, 指向值: %d (移动了 %zu 字节)\n", 
           (void*)(p_int + 1), *(p_int + 1), sizeof(int));
    printf("\n");

    printf("初始 p_char 地址: %p, 指向值: %c\n", (void*)p_char, *p_char);
    printf("p_char + 1 地址: %p, 指向值: %c (移动了 %zu 字节)\n", 
           (void*)(p_char + 1), *(p_char + 1), sizeof(char));
    printf("\n");

    return 0;
}

输出示例:

--- 指针算术的按类型步进 ---
初始 p_int 地址: 0x7ffee21707d0, 指向值: 10
p_int + 1 地址: 0x7ffee21707d4, 指向值: 20 (移动了 4 字节)

初始 p_char 地址: 0x7ffee21707cb, 指向值: a
p_char + 1 地址: 0x7ffee21707cc, 指向值: b (移动了 1 字节)

你会发现 p_int + 1 地址增加了 sizeof(int) 字节(通常是4),而 p_char + 1 地址增加了 sizeof(char) 字节(1)。这就是“按类型步进”的直接体现。

第二章:数组:语法糖下的指针本质

在C语言中,数组和指针有着千丝万缕的联系,它们常常被混淆,但又有着本质的区别。

2.1 数组名:一个常量指针(大多数情况下)

在大多数表达式中,数组名会**“衰变”(decay)**为指向其第一个元素的指针常量。这意味着你可以用数组名来初始化一个同类型的指针,并对其进行指针算术。

// 代码片段 6:数组名的衰变与指针的相似性
#include <stdio.h> // 引入标准输入输出库

int main() {
    int arr[] = {100, 200, 300}; // 整数数组
    int *ptr = arr;             // 数组名 arr 衰变为 &arr[0]

    printf("--- 数组名的衰变与指针的相似性 ---\n");
    printf("arr 的地址 (第一个元素): %p\n", (void*)arr);
    printf("ptr 的值 (arr 的地址): %p\n", (void*)ptr);
    printf("\n");

    // 通过数组下标和指针算术访问元素
    printf("arr[0]: %d, *(arr + 0): %d\n", arr[0], *(arr + 0));
    printf("arr[1]: %d, *(arr + 1): %d\n", arr[1], *(arr + 1));
    printf("arr[2]: %d, *(arr + 2): %d\n", arr[2], *(arr + 2));
    printf("\n");

    printf("ptr[0]: %d, *(ptr + 0): %d\n", ptr[0], *(ptr + 0));
    printf("ptr[1]: %d, *(ptr + 1): %d\n", ptr[1], *(ptr + 1));
    printf("ptr[2]: %d, *(ptr + 2): %d\n", ptr[2], *(ptr + 2));
    printf("\n");

    return 0;
}

输出示例:

--- 数组名的衰变与指针的相似性 ---
arr 的地址 (第一个元素): 0x7ffee27007b0
ptr 的值 (arr 的地址): 0x7ffee27007b0

arr[0]: 100, *(arr + 0): 100
arr[1]: 200, *(arr + 1): 200
arr[2]: 300, *(arr + 2): 300

ptr[0]: 100, *(ptr + 0): 100
ptr[1]: 200, *(ptr + 1): 200
ptr[2]: 300, *(ptr + 2): 300

这表明 arr[i] 实际上是 *(arr + i) 的语法糖。编译器在处理数组下标时,会将其转换为指针算术和解引用操作。

2.2 数组与指针的根本区别:类型与内存分配

尽管数组名在大多数情况下会衰变为指针,但它们并非完全相同:

  1. 类型不同: 数组名是数组类型,而指针是指针类型。sizeof(数组名) 返回整个数组的大小,而 sizeof(指针) 返回指针变量本身的大小。

  2. 内存分配: 数组在定义时就分配了连续的内存空间,而指针只分配了存储地址的内存空间,它指向的内存可能在别处分配。

  3. 可修改性: 数组名是常量,不能被赋值(例如 arr = ptr 是非法的),而指针变量可以被赋值。

第三章:多级指针:间接的层次

现在,让我们把目光投向多级指针,特别是 char**

3.1 char* s[]:一个“字符串数组”的真相

当你声明 char * s[] 时,你创建的并不是一个字符串数组,而是一个字符指针数组。这意味着 s 数组的每个元素都是一个 char* 类型,它存储着一个字符串的起始地址。字符串字面量(如 "apple ")本身是匿名的字符数组,通常存储在程序的只读数据段,并且它们的首地址被赋给了 s 数组中的相应指针。

// 代码片段 7:char* 数组的内存布局
#include <stdio.h> // 引入标准输入输出库

int main() {
    // char* s[]:一个包含 char* 类型元素的数组
    // 每个元素 s[i] 都是一个指针,指向内存中不同的字符串字面量
    char * s[] = {"Apple", "Banana", "Cherry"};

    printf("--- char* s[] 的内存布局 ---\n");
    printf("数组 s 的起始地址 (s[0]的地址): %p\n", (void*)s); // 数组 s 本身的起始地址
    printf("数组 s 的总大小: %zu bytes\n", sizeof(s)); // 数组 s 的总大小

    printf("\n");
    for(int i = 0; i < sizeof(s) / sizeof(s[0]); i++) {
        printf("s[%d] (指向字符串的地址): %p, 字符串内容: \"%s\"\n", i, (void*)s[i], s[i]);
        // s[i] 本身是一个 char* 类型的值,存储着字符串的地址
        // &s[i] 是 s[i] 这个 char* 变量本身的地址,即它在 s 数组中的位置
        printf("  &s[%d] (s数组中元素 s[%d] 的地址): %p\n", i, i, (void*)&s[i]);
    }
    printf("\n");

    return 0;
}

输出示例:

--- char* s[] 的内存布局 ---
数组 s 的起始地址 (s[0]的地址): 0x7ffee81737d0
数组 s 的总大小: 24 bytes

s[0] (指向字符串的地址): 0x100003f58, 字符串内容: "Apple"
  &s[0] (s数组中元素 s[0] 的地址): 0x7ffee81737d0
s[1] (指向字符串的地址): 0x100003f60, 字符串内容: "Banana"
  &s[1] (s数组中元素 s[1] 的地址): 0x7ffee81737d8
s[2] (指向字符串的地址): 0x100003f67, 字符串内容: "Cherry"
  &s[2] (s数组中元素 s[2] 的地址): 0x7ffee81737e0

从输出中可以看出,s 数组本身存储在栈上(或数据段),其元素 (s[0], s[1], s[2]) 紧密排列,每个元素都是一个8字节(在64位系统上)的地址。而这些地址指向的字符串字面量 ("Apple", "Banana") 则位于内存的另一处,通常是只读数据段。

3.2 char** p3 = s;:指针的指针

当你声明 char** p3 = s; 时:

  • p3 是一个指向 char* 类型的指针

  • s (数组名) 衰变为指向其第一个元素 s[0]char* 类型指针。

  • 因此,p3 存储的是 s[0] 的地址。

这形成了一个两级间接:

  1. p3 存储 s[0] 的地址。

  2. s[0] 存储 "apple " 字符串的地址。

  3. "apple " 是实际的字符序列。

3.3 拨开迷雾:*(p3+i) vs (*p3)+i

现在,我们可以清晰地辨析 *(p3+i)(*p3)+i 的差异了。

理解 *(p3+i)

  1. p3 的类型是 char**。它存储的是 s 数组中第一个 char* 元素(即 s[0])的地址。

  2. p3 + i

    • 根据指针算术规则,p3char** 类型,所以 p3 + i 表示向后移动 i * sizeof(char*) 字节。

    • 这正是我们期望的——p3 + i 会跳过 s[0], s[1], ..., s[i-1],从而指向 s 数组中的第 ichar* 元素(即 s[i] 的地址)。

  3. *(p3 + i)

    • (p3 + i) 这个 char** 类型的值进行解引用。

    • 解引用操作 * 会访问 s 数组中第 ichar* 元素所存储的地址。

    • 这个地址正是第 i 个字符串的起始地址。

    • 因此,*(p3 + i) 的结果是一个 char* 类型的值,它指向了数组 s 中的第 i 个字符串字面量。将其作为 printf("%s", ...) 的参数时,printf 会从这个地址开始打印,直到遇到空字符 \0

理解 (*p3)+i

  1. *p3

    • p3 的类型是 char**

    • *p3 表示对 p3 进行解引用。

    • p3 存储的是 s[0] 的地址,所以 *p3 的结果是 s[0] 所存储的那个值,即 "apple " 字符串的起始地址。

    • 此时,*p3 的类型是 char*

  2. (*p3) + i

    • 现在我们对一个 char* 类型的值 (*p3) 进行加 i 操作。

    • 根据指针算术规则,(*p3) + i 表示在 *p3 所指向的内存地址(即 "apple " 字符串的起始地址)上,向后移动 i * sizeof(char) 字节。

    • 由于 sizeof(char) 是1,所以 (*p3) + i 实际上是移动了 i 个字节。

    • 这导致 (*p3) + i 的结果是 "apple " 字符串的第 i 个字符的地址。

    • printf("%s", ...) 接收到 ("apple " + i) 这样的地址时,它会从 "apple " 字符串的第 i 个字符开始打印,直到遇到空字符 \0

      • i=0 时,"apple "+0 得到 "apple "

      • i=1 时,"apple "+1 得到 "pple "

      • i=2 时, "apple "+2 得到 "ple "

      • 以此类推。

内存示意图(简化版):

假设在32位系统上,指针占4字节。

// 内存中 s 数组的实际存储(栈上或数据段)
s (char* s[5])
地址: 0xAAAA0000
+-----------------+
| 0xBBBB1000      |  <-- s[0] 存储 "apple " 的地址
+-----------------+
| 0xBBBB2000      |  <-- s[1] 存储 "banana" 的地址
+-----------------+
| 0xBBBB3000      |  <-- s[2] 存储 "watermelon" 的地址
+-----------------+
| 0xBBBB4000      |  <-- s[3] 存储 "shit" 的地址
+-----------------+
| 0xBBBB5000      |  <-- s[4] 存储 "fuckyou" 的地址
+-----------------+

// 内存中字符串字面量的实际存储(只读数据段)
地址: 0xBBBB1000  -->  'a' 'p' 'p' 'l' 'e' ' ' '\0' ...
地址: 0xBBBB2000  -->  'b' 'a' 'n' 'a' 'n' 'a' '\0' ...
地址: 0xBBBB3000  -->  'w' 'a' 't' 'e' 'r' 'm' 'e' 'l' 'o' 'n' '\0' ...
...

// 指针 p3 的值(栈上)
p3 (char**)
地址: 0xCCCC0000
+-----------------+
| 0xAAAA0000      |  <-- p3 存储的是 s 数组的起始地址(即 &s[0])
+-----------------+

3.4 图解操作:

  1. p3: 0xAAAA0000 (指向 s 数组的开头)

  2. p3 + i:

    • i=0 时, p3 + 0 仍是 0xAAAA0000。

    • i=1 时, p3 + 1 移动 1 * sizeof(char*) 字节 (例如 4 字节),变成 0xAAAA0004 (指向 s[1] 的地址)。

    • i=2 时, p3 + 2 移动 2 * sizeof(char*) 字节 (例如 8 字节),变成 0xAAAA0008 (指向 s[2] 的地址)。

  3. *(p3 + i):

    • i=0 时, *(0xAAAA0000) 解引用得到 0xBBBB1000 (字符串 "apple " 的地址)。

    • i=1 时, *(0xAAAA0004) 解引用得到 0xBBBB2000 (字符串 "banana" 的地址)。

    • i=2 时, *(0xAAAA0008) 解引用得到 0xBBBB3000 (字符串 "watermelon" 的地址)。

  1. *p3: * 解引用 p3 (0xAAAA0000) 得到 0xBBBB1000 (字符串 "apple " 的地址)。

    • 此时,*p3 的类型是 char*

  2. (*p3) + i:

    • i=0 时, 0xBBBB1000 + 0 仍是 0xBBBB1000 (指向 'a')。

    • i=1 时, 0xBBBB1000 + 1 移动 1 * sizeof(char) 字节 (1 字节),变成 0xBBBB1001 (指向 'p')。

    • i=2 时, 0xBBBB1000 + 2 移动 2 * sizeof(char) 字节 (2 字节),变成 0xBBBB1002 (指向 'p')。

至此,相信你已经洞悉了 *(p3+i)(*p3)+i 之间那“毫厘之差,谬以千里”的奥秘。关键在于操作符优先级指针类型在指针算术中的决定性作用。

第四章:const 的哲学:不变性与安全性

在C语言中,const 关键字是实现代码健壮性和安全性的利器。尤其在指针场景下,const 的位置会带来截然不同的含义。理解 const 不仅能避免意外修改,更是掌握C语言内存控制的关键。

4.1 指针与 const 的几种组合

  1. const char* ptr; (指向常量的指针):

    • 指针 ptr 本身可变,可以指向不同的 char

    • 但通过 ptr 不能修改它所指向的 char

    • char *s[] = {"apple", ...} 声明中的 s[i] 实际上就是 const char*,因为字符串字面量是只读的。

    // 代码片段 8.1:const char* (指向常量的指针)
    #include <stdio.h> // 引入标准输入输出库
    
    int main() {
        char buffer[] = "hello"; // 可修改的字符数组
        const char *p_const_char = buffer; // 指向常量的指针
    
        printf("--- const char* (指向常量的指针) ---\n");
        printf("初始值: %s\n", p_const_char);
        // *p_const_char = 'H'; // 错误: 通过指向常量的指针修改其指向的值是非法的
    
        p_const_char = "world"; // 正确: 指针本身可以指向其他常量或变量
        printf("改变指针指向后: %s\n", p_const_char);
    
        // 如果指向非const数据,可以通过原名修改
        buffer[0] = 'H';
        printf("通过原数组修改后: %s\n", buffer); // p_const_char 仍指向 buffer,但不能通过 p_const_char 修改
    
        printf("\n");
        return 0;
    }
    
    

    输出:

    --- const char* (指向常量的指针) ---
    初始值: hello
    改变指针指向后: world
    通过原数组修改后: Hello
    
    
  2. char* const ptr; (常量指针):

    • 指针 ptr 本身是常量,一旦初始化就不能再指向其他地址

    • 但通过 ptr 可以修改它所指向的 char

    // 代码片段 8.2:char* const (常量指针)
    #include <stdio.h> // 引入标准输入输出库
    
    int main() {
        char buffer[] = "hello"; // 可修改的字符数组
        char *const p_char_const = buffer; // 常量指针,必须初始化
    
        printf("--- char* const (常量指针) ---\n");
        printf("初始值: %s\n", p_char_const);
    
        *p_char_const = 'H'; // 正确: 可以通过指针修改其指向的值
        printf("通过 *p_char_const 修改后: %s\n", p_char_const);
    
        // p_char_const = "world"; // 错误: 指针本身是常量,不能重新赋值
        printf("\n");
        return 0;
    }
    
    

    输出:

    --- char* const (常量指针) ---
    初始值: hello
    通过 *p_char_const 修改后: Hello
    
    
  3. const char* const ptr; (指向常量的常量指针):

    • 指针 ptr 本身是常量,不能改变指向。

    • 通过 ptr 也不能修改它所指向的 char 值。

    • 这是一种双重锁定,提供了最高级别的安全性。

    // 代码片段 8.3:const char* const (指向常量的常量指针)
    #include <stdio.h> // 引入标准输入输出库
    
    int main() {
        char buffer[] = "hello"; // 可修改的字符数组
        const char *const p_both_const = buffer; // 指向常量的常量指针
    
        printf("--- const char* const (指向常量的常量指针) ---\n");
        printf("初始值: %s\n", p_both_const);
    
        // *p_both_const = 'H'; // 错误: 不能通过此指针修改其指向的值
        // p_both_const = "world"; // 错误: 指针本身是常量,不能重新赋值
    
        printf("尝试修改后 (无实际修改): %s\n", p_both_const);
        printf("\n");
        return 0;
    }
    
    

    输出:

    --- const char* const (指向常量的常量指针) ---
    初始值: hello
    尝试修改后 (无实际修改): hello
    
    

在你的原始代码中,char *s[] 定义的数组元素 s[i] 其实是 char* 类型。由于字符串字面量是不可修改的,更好的做法是将其声明为 const char *s[],以明确语义并利用编译器进行检查。

// 更好的实践:声明为 const char* 数组
const char * s[] = {"apple ", "banana", "watermelon", "shit", "fuckyou"};

这样,编译器会强制你不能通过 s[i] 来修改字符串字面量的内容,提高了代码的安全性。

第五章:深入底层:内存布局与CPU的视角

C语言之所以被称为“中级语言”,正是因为它能够直接操作内存,并提供接近汇编语言的控制能力。理解上述指针操作在内存中的实际表现,以及CPU是如何执行这些操作的,将让你对C语言的“硬核”程度有更深刻的认识。

5.1 程序的内存分段

一个C程序在内存中通常被划分为几个主要段:

  • 代码段 (.text): 存放编译后的机器指令。

  • 只读数据段 (.rodata): 存放字符串字面量、const 常量等不可修改的数据。

  • 已初始化数据段 (.data): 存放已初始化的全局变量和静态变量。

  • 未初始化数据段 (.bss): 存放未初始化的全局变量和静态变量(在程序启动时被清零)。

  • 堆 (Heap): 动态内存分配区域(malloc/free)。

  • 栈 (Stack): 存放局部变量、函数参数、返回地址等(你的 s 数组和 p3 变量通常在此)。

在你的示例中:

  • "apple ", "banana" 等字符串字面量被编译器放置在只读数据段 (.rodata)

  • char *s[] 这个数组本身(存储了这些字符串的地址)以及 char** p3 变量,通常被放置在栈上(如果它们是局部变量)。

5.2 CPU执行指针操作的微观世界

当CPU执行像 *(p3+i)(*p3)+i 这样的C语言语句时,它实际上执行的是一系列机器指令。

我们以 *(p3+i) 为例,假设在32位系统上,p3 位于栈上,i 也是一个寄存器或栈上的值。

  1. 获取 p3 的值: CPU从栈上加载 p3 变量的值(即 s[0] 的地址)到某个通用寄存器,例如 EAX

  2. 计算 i * sizeof(char*) CPU将 i 的值与 sizeof(char*)(例如4)相乘。这通常通过左移指令(例如 SHL)或乘法指令完成,结果存储在另一个寄存器,例如 EBX

  3. 计算目标地址 (p3 + i) CPU将 EAX (p3的值) 和 EBX (偏移量) 相加,得到 s[i] 在内存中的地址。结果仍然在 EAX 中。

  4. 解引用 *(p3+i) CPU使用 EAX 中存储的地址作为内存操作数,执行一个“内存读取”指令。由于 *(p3+i) 的类型是 char*,CPU会读取该地址处的4个字节(在32位系统上),这4个字节就是某个字符串字面量的起始地址。这个新的地址被加载到 EAX

  5. 传递给 printf EAX 中的值(字符串的起始地址)被作为参数推入栈中,然后调用 printf 函数。

而对于 (*p3)+i

  1. 解引用 *p3 CPU从栈上加载 p3 的值到 EAX。然后使用 EAX 作为内存地址,执行一个“内存读取”指令,读取 s[0] 所存储的字符串字面量的地址(例如 0xBBBB1000)。这个字符串地址被加载到 EAX

  2. 计算 i * sizeof(char) CPU将 i 的值与 sizeof(char)(即1)相乘。结果存储在 EBX

  3. 计算目标地址 (*p3)+i CPU将 EAX (字符串起始地址) 和 EBX (字符偏移量) 相加,得到字符串内部第 i 个字符的地址。结果仍然在 EAX 中。

  4. 传递给 printf EAX 中的值(字符串内部某个字符的地址)被作为参数推入栈中,然后调用 printf 函数。

这种微观层面的拆解,正是C语言魅力与复杂性并存的体现。每当你写下一行C代码,它都在幕后悄然指挥着CPU执行一系列精密的内存操作。

第六章:实践与升华:避免陷阱与最佳实践

理解了原理,现在我们来总结如何在实际开发中避免这类“迷思”,并采用更健壮、更清晰的C语言编程实践。

6.1 数组与指针声明的清晰性

  1. char *s[] 用于存储一系列字符串(每个字符串的地址)。数组的每个元素都是一个 char*

  2. char **p 用于指向一个 char* 类型的变量,或者指向一个 char* 数组的第一个元素。它是一个二级指针。

始终记住它们的含义和它们所指向的数据类型。

// 代码片段 9:不同类型数组和指针的清晰声明与使用
#include <stdio.h> // 引入标准输入输出库
#include <string.h> // 引入字符串处理函数
#include <stdlib.h> // 引入动态内存分配函数

// 定义一些常量
#define MAX_STRINGS 5
#define MAX_LEN     20

int main() {
    printf("--- 不同类型数组和指针的清晰声明与使用 ---\n");

    // 示例 1: char 数组(一维,用于存储一个字符串)
    // 内存中连续的字符序列
    char fixed_str[MAX_LEN] = "Hello, World!"; 
    printf("char 数组 (fixed_str): \"%s\", 地址: %p\n", fixed_str, (void*)fixed_str);
    printf("fixed_str[0]: %c, *(fixed_str + 0): %c\n", fixed_str[0], *(fixed_str + 0));
    printf("fixed_str + 1 (地址偏移1字节): %p, *(fixed_str + 1): %c\n", (void*)(fixed_str + 1), *(fixed_str + 1));
    printf("\n");

    // 示例 2: char* 指针(指向一个字符串)
    // 字符串字面量存储在只读数据区
    const char *ptr_str = "C is powerful.";
    printf("const char* 指针 (ptr_str): \"%s\", 地址: %p\n", ptr_str, (void*)ptr_str);
    printf("ptr_str[0]: %c, *(ptr_str + 0): %c\n", ptr_str[0], *(ptr_str + 0));
    printf("ptr_str + 1 (地址偏移1字节): %p, *(ptr_str + 1): %c\n", (void*)(ptr_str + 1), *(ptr_str + 1));
    printf("\n");

    // 示例 3: char 数组的数组(二维数组,固定行和列)
    // 内存中所有字符连续存储
    char two_d_array[3][MAX_LEN] = {"First", "Second", "Third"};
    printf("char 二维数组 (two_d_array):\n");
    printf("  two_d_array 的地址: %p\n", (void*)two_d_array);
    printf("  two_d_array[0] 的地址: %p\n", (void*)two_d_array[0]);
    printf("  two_d_array[1] 的地址: %p\n", (void*)two_d_array[1]);
    for(int i = 0; i < 3; i++) {
        printf("  two_d_array[%d]: \"%s\"\n", i, two_d_array[i]);
    }
    printf("\n");

    // 示例 4: char* 数组(字符指针数组,动态字符串)
    // 数组元素是 char*,指向不同字符串的地址
    char *string_array[MAX_STRINGS] = {NULL}; // 初始化为 NULL
    printf("char* 数组 (string_array) - 初始状态:\n");
    printf("  string_array 的地址: %p\n", (void*)string_array);
    for(int i = 0; i < MAX_STRINGS; i++) {
        printf("  string_array[%d]: %p\n", i, (void*)string_array[i]);
    }

    // 为 string_array 动态分配内存并赋值
    string_array[0] = (char*)malloc(sizeof(char) * (strlen("Dynamic 1") + 1));
    strcpy(string_array[0], "Dynamic 1");

    string_array[1] = (char*)malloc(sizeof(char) * (strlen("Dynamic 2") + 1));
    strcpy(string_array[1], "Dynamic 2");
    
    printf("\nchar* 数组 (string_array) - 赋值后:\n");
    for(int i = 0; i < 2; i++) { // 只打印已赋值的部分
        printf("  string_array[%d]: %p, 内容: \"%s\"\n", i, (void*)string_array[i], string_array[i]);
    }
    printf("\n");

    // 示例 5: char** (指向 char* 的指针)
    // p_p_char 指向 string_array 的第一个元素
    char **p_p_char = string_array; 
    printf("char** 指针 (p_p_char):\n");
    printf("  p_p_char 的值 (string_array的地址): %p\n", (void*)p_p_char);
    printf("  *p_p_char (string_array[0]的值): %p, 内容: \"%s\"\n", (void*)*p_p_char, *p_p_char);
    printf("  *(p_p_char + 1) (string_array[1]的值): %p, 内容: \"%s\"\n", (void*)*(p_p_char + 1), *(p_p_char + 1));
    printf("\n");

    // 清理动态分配的内存
    for(int i = 0; i < 2; i++) {
        free(string_array[i]);
        string_array[i] = NULL; // 最佳实践:释放后将指针置为 NULL
    }
    printf("动态内存已释放.\n");

    return 0;
}

输出示例:

--- 不同类型数组和指针的清晰声明与使用 ---
char 数组 (fixed_str): "Hello, World!", 地址: 0x7ffee145d7a0
fixed_str[0]: H, *(fixed_str + 0): H
fixed_str + 1 (地址偏移1字节): 0x7ffee145d7a1, *(fixed_str + 1): e

const char* 指针 (ptr_str): "C is powerful.", 地址: 0x102143f60
ptr_str[0]: C, *(ptr_str + 0): C
ptr_str + 1 (地址偏移1字节): 0x102143f61, *(ptr_str + 1):  

char 二维数组 (two_d_array):
  two_d_array 的地址: 0x7ffee145d794
  two_d_array[0] 的地址: 0x7ffee145d794
  two_d_array[1] 的地址: 0x7ffee145d7a8
  two_d_array[0]: "First"
  two_d_array[1]: "Second"
  two_d_array[2]: "Third"

char* 数组 (string_array) - 初始状态:
  string_array 的地址: 0x7ffee145d788
  string_array[0]: 0x0
  string_array[1]: 0x0
  string_array[2]: 0x0
  string_array[3]: 0x0
  string_array[4]: 0x0

char* 数组 (string_array) - 赋值后:
  string_array[0]: 0x100603ca0, 内容: "Dynamic 1"
  string_array[1]: 0x100603cb0, 内容: "Dynamic 2"

char** 指针 (p_p_char):
  p_p_char 的值 (string_array的地址): 0x7ffee145d788
  *p_p_char (string_array[0]的值): 0x100603ca0, 内容: "Dynamic 1"
  *(p_p_char + 1) (string_array[1]的值): 0x100603cb0, 内容: "Dynamic 2"

动态内存已释放.

6.2 括号的力量:明确优先级

使用括号 () 来明确操作符的优先级,是避免这种“迷思”的简单而有效的方法。当你不确定某个表达式的求值顺序时,大胆使用括号。

  • *(p3+i)p3+i 先计算,然后解引用。

  • (*p3)+i*p3 先解引用,然后对结果进行加 i

你的原代码中,正是 (*p3) 这一对括号将 p3 的解引用操作提前,从而完全改变了指针算术的上下文。

6.3 typedef 的妙用:提升代码可读性与健壮性

在涉及多级指针和复杂数据结构时,typedef 可以极大地提升代码的可读性和可维护性。

// 代码片段 10:使用 typedef 提升可读性
#include <stdio.h> // 引入标准输入输出库

// 定义一个指向 char 的指针类型
typedef char* String; 

// 定义一个指向 String (即 char*) 的指针类型
typedef String* StringArrayPtr; 

int main() {
    // 使用 typedef 定义字符指针数组
    String my_strings[] = {"Code", "Is", "Life", "And", "Fun"};
    
    // 使用 typedef 定义二级指针
    StringArrayPtr ptr_to_strings = my_strings;

    printf("--- 使用 typedef 提升可读性 ---\n");
    for(int i = 0; i < sizeof(my_strings) / sizeof(my_strings[0]); i++) {
        // 访问第 i 个字符串
        printf("字符串 %d: %s\n", i, ptr_to_strings[i]); 
        // ptr_to_strings[i] 等价于 *(ptr_to_strings + i)
    }
    printf("\n");

    return 0;
}

输出示例:

--- 使用 typedef 提升可读性 ---
字符串 0: Code
字符串 1: Is
字符串 2: Life
字符串 3: And
字符串 4: Fun

通过 typedefString 更直观地表示“字符串”,StringArrayPtr 则明确表示“字符串数组的指针”,代码意图一目了然。

第七章:超越指针:内存分配与生命周期(扩展话题)

虽然你原问题主要集中在指针算术上,但既然谈到C语言的“精髓”和“底层”,就不能不提内存分配与生命周期管理。在操作系统内核等裸机环境中,你需要手动完成内存的分配与释放,这比在用户态编程中依赖操作系统提供的 malloc/free 更具挑战性。

7.1 栈上、数据段、只读数据段与堆

我们已经提到了这些内存区域。在你的代码中,s 数组本身(存储字符串地址的部分)是在栈上分配的,而字符串字面量 "apple " 等是在只读数据段。如果你需要在运行时创建可修改的字符串,并管理其生命周期,就需要用到堆。

// 代码片段 11:字符串的动态内存分配与管理
#include <stdio.h> // 引入标准输入输出库
#include <stdlib.h> // 引入 malloc, free
#include <string.h> // 引入 strcpy, strlen

#define MAX_DYNAMIC_STRINGS 3

int main() {
    printf("--- 字符串的动态内存分配与管理 ---\n");

    // 声明一个字符指针数组,用于存储动态分配的字符串
    char *dynamic_strings[MAX_DYNAMIC_STRINGS];

    // 动态分配并赋值字符串
    printf("动态分配字符串:\n");
    for (int i = 0; i < MAX_DYNAMIC_STRINGS; i++) {
        char temp_buffer[50];
        sprintf(temp_buffer, "Dynamic String %d", i + 1); // 构建要存储的字符串

        // 分配足够的内存来存储字符串及其结束符 '\0'
        dynamic_strings[i] = (char *)malloc(strlen(temp_buffer) + 1); 
        if (dynamic_strings[i] == NULL) {
            fprintf(stderr, "内存分配失败!\n");
            // 在实际OS中,这里可能需要更复杂的错误处理或OOM处理
            exit(EXIT_FAILURE); 
        }
        strcpy(dynamic_strings[i], temp_buffer); // 复制字符串内容
        printf("  dynamic_strings[%d] 地址: %p, 内容: \"%s\"\n", 
               i, (void*)dynamic_strings[i], dynamic_strings[i]);
    }
    printf("\n");

    // 访问动态分配的字符串 (通过 *(ptr_to_strings + i) 方式)
    char **ptr_to_dynamic_strings = dynamic_strings;
    printf("通过二级指针访问动态字符串:\n");
    for (int i = 0; i < MAX_DYNAMIC_STRINGS; i++) {
        printf("  *(ptr_to_dynamic_strings + %d) 内容: \"%s\"\n", 
               i, *(ptr_to_dynamic_strings + i));
    }
    printf("\n");

    // 释放动态分配的内存
    printf("释放动态分配的内存:\n");
    for (int i = 0; i < MAX_DYNAMIC_STRINGS; i++) {
        printf("  释放地址: %p\n", (void*)dynamic_strings[i]);
        free(dynamic_strings[i]); // 释放每个字符串的内存
        dynamic_strings[i] = NULL; // 最佳实践:释放后将指针置空,避免悬空指针
    }
    printf("所有动态内存已成功释放。\n");

    return 0;
}

输出示例:

--- 字符串的动态内存分配与管理 ---
动态分配字符串:
  dynamic_strings[0] 地址: 0x100603cc0, 内容: "Dynamic String 1"
  dynamic_strings[1] 地址: 0x100603cd0, 内容: "Dynamic String 2"
  dynamic_strings[2] 地址: 0x100603ce0, 内容: "Dynamic String 3"

通过二级指针访问动态字符串:
  *(ptr_to_dynamic_strings + 0) 内容: "Dynamic String 1"
  *(ptr_to_dynamic_strings + 1) 内容: "Dynamic String 2"
  *(ptr_to_dynamic_strings + 2) 内容: "Dynamic String 3"

释放动态分配的内存:
  释放地址: 0x100603cc0
  释放地址: 0x100603cd0
  释放地址: 0x100603ce0
所有动态内存已成功释放。

在操作系统内核中,你需要实现自己的 malloc/free 机制(通常称为“内存分配器”或“堆管理器”),因为它无法依赖外部库。这涉及到管理空闲内存块列表、合并碎片、处理内存对齐等一系列复杂问题,是操作系统内存管理的核心难题之一。

7.2 悬空指针与野指针:内存管理的暗面

  • 悬空指针 (Dangling Pointer): 当一个指针指向的内存已经被释放,但指针本身仍然存在并存储着那个地址时,它就成为了悬空指针。再次解引用悬空指针会导致未定义行为(Undefined Behavior),轻则程序崩溃,重则数据损坏或安全漏洞。

    • 避免: 释放内存后,立即将指针置为 NULL

  • 野指针 (Wild Pointer): 一个未初始化或随机指向的指针。解引用野指针同样会导致未定义行为,因为你无法确定它指向何处。

    • 避免: 所有指针在使用前都必须初始化(指向有效地址或 NULL)。

在裸机编程中,这些错误更加致命,因为没有操作系统来捕获这些非法内存访问。

总结与展望:C语言的硬核之旅

恭喜你,朋友!你不仅成功地发现了C语言指针与数组的经典“迷思”,更借此机会,我们一同深入挖掘了C语言的“骨骼与血液”。从指针的类型步进到数组名的衰变,从多级指针的间接层次到 const 的安全哲学,再到内存分段与CPU执行的微观视角,我们一层层地揭示了C语言底层运行的真谛。

你最初的代码片段,正是C语言对细节精确掌控的直接要求。它不是在刁难你,而是在磨砺你对内存的直觉和对类型系统的敬畏。当你能清晰地在脑海中描绘出 char* s[]char** p3 在内存中的布局,并准确预判 *(p3+i)(*p3)+i 带来的物理内存访问差异时,恭喜你,你已经迈入了C语言“硬核”开发者的行列!

C语言的魅力在于它赋予了你直接与硬件对话的权力。这种权力伴随着巨大的责任——你需要清晰地思考每一个字节的存储位置、每一个地址的含义、每一次操作的物理影响。在操作系统内核开发中,这种对内存的精微掌控更是不可或缺。

这仅仅是C语言硬核之旅的冰山一角。未来,你可以继续探索:

  • 函数指针: 实现回调、状态机等高级设计模式。

  • 结构体与指针: 构建复杂的数据结构,如链表、树等。

  • 位操作: 直接控制硬件寄存器,优化性能。

  • 内存池与自定义分配器: 在OS内核中管理内存的艺术。

  • volatile 关键字: 确保编译器不优化对硬件寄存器的访问。

希望这篇博文能成为你C语言硬核之路上的又一块坚实基石。记住,每一个“迷思”,都是一次深入学习,一次自我超越的机会。

 附录 , 我这里最开头的这些源码:
 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void funcZhizhen14(void)
{
    int a = 10, b = 20, c = 30;
    int *p[3];
    p[0] = &a;
    p[1] = &b;
    p[2] = &c;
    printf("\n\n ----------------in the func14 : \n\n");
    printf("%d %d %d \n", a, b, c);
    printf("%d %d %d \n", *p[0], *p[1], *p[2]);
    printf("%p %p %p \n", p[0], p[1], p[2]);

    // #self !!!vip 指针数组:易错点 si是三个数组的指针
    char *s[] = {"abcdefg ", "hijklmn", "opqrst"};
    for (int i = 0; i < sizeof(s) / sizeof(char *); i++)
    {
        printf("%s \n", s[i]);
    }

    // #self  视频的hqyj官方出的笔试题
    int a1[2][3] = {{1, 2, 3}, {4, 5, 6}};
    int *p1[2];
    p1[0] = a1[0];
    p1[1] = a1[1];
    printf("%d \n", a1[0][0]);
    printf("%d \n", p1[0][1]);
    printf("%d \n", *(p1[0] + 1));

    // #self !!!!!vip 超级重要 : 指针数组 和 数组指针
    int a2[2][3] = {{2, 3, 4}, {5, 6, 8}};
    int (*p2)[3] = a;
    for (int i = 0; i < 2; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            printf("第一种遍历:\n");
            printf("a2[i][j] \n", a2[i][j]);
            printf("第二种遍历\n");
            // printf("%d \n ",*(*(p2+i)+j));
        }
    }
}

void func15(void)
{

    printf("--------------------\n\nin the func15 : \n------------\n");
    int a = 10;
    int *p = &a;
    int **q = &p;
    printf("a=%ld p = %p q=%p \n", a, p, q);
    printf("&a= %p &p=%p  &q=%p \n", &a, &p, &q);
    printf("%ld %ld %ld \n", sizeof(a), sizeof(p), sizeof(q));

    printf("p往后移动:%p %p \n", p, p + 1);
    printf("q往后移动:%p %p \n", q, q + 1);

    int b1[] = {4, 2, 5, 6, 7, 11};
    int *p1[6];
    int n = sizeof(b1) / sizeof(int);
    int **q1;
    for (int i = 0; i < n; i++)
    {
        p1[i] = &b1[i];
    }
    q1 = p1 ;
    for(int i = 0; i < n; i++)
    {
        printf("%d element is %d \n", i, b1[i]);
        printf("    p: %d is %d \n ", i, *p1[i]);
        printf("    q: %d is %d \n", i, **(q1 + i));
    }

    //字符指针数组
    char * s[] = {"apple ","banana","watermelon","shit","fuckyou"};
    char** p3 = s;
    for(int i = 0 ;i<sizeof(s)/sizeof(s[0]);i++){
        printf("\n %d element:%s \n",i,s[i]);
        printf("\nby p3: %d element is %s \n",i, *(p3+i) );
    }

    return;
}

int main(void)
{
    printf("Starting program...\n");
    fflush(stdout);

    // func1 字符数组初始化
    char ch[] = "hello";
    char ch2[] = {'1', '2', '3', '4', '5', '\0'};

    printf("ch: %s - %p - %ld \n", ch, (void *)ch, sizeof(ch));
    printf("ch2: %s - %p-%ld \n", ch2, (void *)ch2, sizeof(ch2));

    // func2 字符数组长度限制
    char a3[10] = "hello";
    printf("a3 is: %s\n", a3);

    // func4 指针基础
    int num1 = 1;
    printf("\n sizeof num1: %zu\n", sizeof(num1));
    printf("address of num1: %p\n", (void *)&num1);

    int num2 = 2;
    int num3 = 3;
    printf("address of num2: %p\n", (void *)&num2);
    printf("address of num3: %p\n", (void *)&num3);

    // func5 指针运算
    int *p6 = &num1;
    printf("\nfunc6: p6 value: %p\n", (void *)p6);
    printf("func6: p6+1: %p\n", (void *)(p6 + 1));
    printf("func6: address of p6: %p\n", (void *)&p6);

    char char6 = 'a';
    printf("---func6: sizeof(char): %zu\n", sizeof(char6));
    printf("---func6: char6 address: %p, char6+1: %p\n",
           (void *)&char6, (void *)(&char6 + 1));

    // func6 二维字符数组
    char a7[][10] = {"apple", "banana", "pineapple", "shit"};
    for (int i = 0; i < sizeof(a7) / sizeof(a7[0]); i++)
    {
        printf("a7[%d]: %s\n", i, a7[i]);
    }

    printf("\n\n ===== 测试1:基本fgets()用法 =====\n\n");

    // // func7 fgets函数(使用安全的输入方式)
    // printf("===== fgets()函数测试 =====\n");
    // char buffer[10];
    // printf("请输入内容(最多9个字符):");
    // fgets(buffer, sizeof(buffer), stdin);
    // printf("输入内容: %s\n", buffer);
    printf("\n\n ===== 测试1:基本fgets()用法结束———————— =====\n\n");

    // func 8 指针运算
    int a8[20] = {1, -2, 3, 44, 5, -14, 26, -34, 6735, -735, 735, 7, 7};
    int *p = a8;
    for (int i = 0; i < sizeof(a8) / sizeof(a8[0]); i++)
    {
        printf("第%d个元素: %d \n", i, a8[i]);
    }
    printf("*p before increment: %d \n", *p);
    *p++;
    printf("*p after increment: %d \n", *p);

    // func9 指针*和++的结合运算
    int a9[5] = {1, 2, 3, 4, 5};
    int *p1 = a9 + 2;
    printf("--------------func9 : \n%d \n", *++p1);
    printf("%d \n", ++*p1);
    for (int i = 0; i < 5; i++)
    {
        printf("a9[%d]: %d \n", i, a9[i]);
    }

    // func10 大小端测试
    unsigned int a10 = 0X12345678;
    printf("--------------------\n\na10: %0#x\n", a10);
    printf("a10地址: %p  \n", &a10);

    // 用char*指针按字节访问
    char *p10 = (char *)&a10;
    for (int i = 0; i < 4; i++)
        printf("按字节访问: %#x \n", *p10++);

    // func 11 数组指针的用法
    printf("\n\n -----------------------------------in func 11 : %#x \n", *p10++);

    int a11[] = {1, 2, 3, 4, 5, 6, 7};
    for (int i = 0; i < sizeof(a11) / sizeof(int); i++)
    {
        printf("\n in the func11 : \n");
        printf("1. a11[%d]: %d \n", i, a11[i]);
        printf("2. *(a11+i): %d \n", *(a11 + i));
        printf("3. *(&a11+i): %#x \n\n", (&a11) + i);
    }

    // 数组指针笔试题
    int ab[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    printf("\nab地址: %p, \n ab+1地址: %p\n", ab, ab + 1);

    // 易错笔试题2 合集#self   !!!vip
    int x[] = {1, 2, 3, 4};
    printf("%ld \n", sizeof(x));
    printf("%ld \n", sizeof(*x));
    printf("%ld \n", sizeof(x + 1));
    printf("%ld \n", sizeof(x[1]));
    printf("%ld \n", sizeof(&x[0]));
    printf("%ld \n", sizeof(&x[0] + 1));

    // func 12 二维数组遍历
    int a12[3][3] = {{1, 2, 3}, {4, 5, 7}, {7, 8, 9}};
    int *p12 = &a12[0][0];
    for (int i = 0; i < 9; i++)
    {
        printf("%d - > %d \n", i, *(p12 + i));
    }
    printf("%p \n", a12);
    printf("%p\n", a12 + 1);
    printf("%p\n", a12 + 2);

    // 数组指针实验
    int (*pSz)[3] = a12;
    printf("\n数组指针实验:%p  -  %p \n", a12, pSz);
    printf("%p - %p \n ", a12 + 1, pSz + 1);

    // a 和a[0]区别
    printf("\na12与a12[0]的地址差异:\n\n");
    printf("%p - %p \n", a12, a12 + 1);
    printf("%p - %p \n", a12[0], a12[0] + 1);

    // 二维数组访问方式 #self !!!!!vip
    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            printf("双层解引用访问: %d \n", *(*(a12 + i) + j));
        }
    }

    // 笔试题解答
    int b12[3][4] = {0};
    printf("%ld \n", sizeof(b12[0]));
    printf("%ld \n", sizeof(*b12));
    printf("%ld \n", sizeof(*(b12 + 1)));
    printf("%ld \n", sizeof(*(&b12[0] + 1)));
    printf("%ld \n", sizeof(b12 + 1));
    printf("%ld \n", sizeof(&b12[0] + 1));
    printf("%ld \n", sizeof(b12[0] + 1));
    printf("%ld \n", sizeof(*(b12[0] + 1)));

    // 第二道题
    int b13[] = {12, 2, 3, 4};
    printf("\n第二大题: \n%ld \n ", sizeof(&b13));
    printf("%ld \n ", sizeof(*&b13));
    printf("%ld \n", sizeof(&b13 + 1));

    // 第三题
    int b14[] = {1, 2, 3, 4, 5};
    int *ptr = (int *)(&b14 + 1);
    printf("%d , %d \n\n\n", *(b14 + 1), *(ptr - 1));

    // 第四题
    int b15[2][5] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int *ptr1 = (int *)(&b15 + 1);
    int *ptr2 = (int *)(*(b15 + 1));
    printf("\n\n 第四大题: %d , %d ", *(ptr1 - 1), *(ptr2 - 1));

    // 第五大题
    // int b16[5][5];
    // int (*p16)[5][5] = &b16;

    // 正确访问方式:直接通过p16解引用
    // printf("\n\n %td \n", (char *)&(*p16)[0][0] - (char *)&b16[0][0]); // 结果应为0

    // func13 字符串指针操作
    // func13 字符串指针操作(修正)
    char s13[] = "Who are you , my friend ? Hello ? Hi? ";
    char *p13 = s13;
    printf("func13: s13内容: %s \n ", p13);
    while (*p13 != '\0')
    {
        if (*p13 >= 'A' && *p13 <= 'Z')
        {                     // 修正:包含边界字符
            *p13 = *p13 + 32; // 转为小写
        }
        else if (*p13 >= 'a' && *p13 <= 'z')
        {                     // 修正:包含边界字符
            *p13 = *p13 - 32; // 转为大写
        }
        p13++;
    }
    printf("after transformation: %s \n", s13);

    // 字符串常量测试(修正)
    char *s14 = "who a/r u ? ";        // 指向字符串常量
    printf("\n字符串常量: %s\n", s14); // 直接打印字符串

    // 字符串连接函数(已修正,无需改动)
    char dest[100] = "abc";
    char *src = "def";
    char *p15 = dest;
    char *q15 = src;
    // printf("in the func strcat : last of dest100 is %c \n",dest[4]);

    // 找到dest末尾
    while (*p15 != '\0')
    {
        p15++;
    }

    // 复制src到dest末尾
    while (*q15 != '\0')
    {
        *p15 = *q15;
        p15++;
        q15++;
    }
    // *p15  ='\0';

    // 输出结果
    printf("\n字符串连接后: %s\n", dest);

    // #self 笔试题
    char str16[] = "Hello";
    char *p16 = "Hello";
    printf("%d %d \n ", sizeof(str16), sizeof(p16));

    // func 14 指针数组

    funcZhizhen14();

    // func15   D15多级指针
    func15();

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值