直接上今晚碰到的问题:
到底难在哪里这个指针问题??
这是正确的代码:
为什么**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 = # // 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 数组与指针的根本区别:类型与内存分配
尽管数组名在大多数情况下会衰变为指针,但它们并非完全相同:
-
类型不同: 数组名是数组类型,而指针是指针类型。
sizeof(数组名)
返回整个数组的大小,而sizeof(指针)
返回指针变量本身的大小。 -
内存分配: 数组在定义时就分配了连续的内存空间,而指针只分配了存储地址的内存空间,它指向的内存可能在别处分配。
-
可修改性: 数组名是常量,不能被赋值(例如
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]
的地址。
这形成了一个两级间接:
-
p3
存储s[0]
的地址。 -
s[0]
存储"apple "
字符串的地址。 -
"apple "
是实际的字符序列。
3.3 拨开迷雾:*(p3+i)
vs (*p3)+i
现在,我们可以清晰地辨析 *(p3+i)
和 (*p3)+i
的差异了。
理解 *(p3+i)
:
-
p3
的类型是char**
。它存储的是s
数组中第一个char*
元素(即s[0]
)的地址。 -
p3 + i
:-
根据指针算术规则,
p3
是char**
类型,所以p3 + i
表示向后移动i * sizeof(char*)
字节。 -
这正是我们期望的——
p3 + i
会跳过s[0]
,s[1]
, ...,s[i-1]
,从而指向s
数组中的第i
个char*
元素(即s[i]
的地址)。
-
-
*(p3 + i)
:-
对
(p3 + i)
这个char**
类型的值进行解引用。 -
解引用操作
*
会访问s
数组中第i
个char*
元素所存储的地址。 -
这个地址正是第
i
个字符串的起始地址。 -
因此,
*(p3 + i)
的结果是一个char*
类型的值,它指向了数组s
中的第i
个字符串字面量。将其作为printf("%s", ...)
的参数时,printf
会从这个地址开始打印,直到遇到空字符\0
。
-
理解 (*p3)+i
:
-
*p3
:-
p3
的类型是char**
。 -
*p3
表示对p3
进行解引用。 -
p3
存储的是s[0]
的地址,所以*p3
的结果是s[0]
所存储的那个值,即"apple "
字符串的起始地址。 -
此时,
*p3
的类型是char*
。
-
-
(*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 图解操作:
-
p3
: 0xAAAA0000 (指向s
数组的开头) -
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]
的地址)。
-
-
*(p3 + i)
:-
当
i=0
时,*(0xAAAA0000)
解引用得到 0xBBBB1000 (字符串 "apple " 的地址)。 -
当
i=1
时,*(0xAAAA0004)
解引用得到 0xBBBB2000 (字符串 "banana" 的地址)。 -
当
i=2
时,*(0xAAAA0008)
解引用得到 0xBBBB3000 (字符串 "watermelon" 的地址)。
-
-
*p3
: * 解引用p3
(0xAAAA0000) 得到 0xBBBB1000 (字符串 "apple " 的地址)。-
此时,
*p3
的类型是char*
。
-
-
(*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
的几种组合
-
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
-
-
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
-
-
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
也是一个寄存器或栈上的值。
-
获取
p3
的值: CPU从栈上加载p3
变量的值(即s[0]
的地址)到某个通用寄存器,例如EAX
。 -
计算
i * sizeof(char*)
: CPU将i
的值与sizeof(char*)
(例如4)相乘。这通常通过左移指令(例如SHL
)或乘法指令完成,结果存储在另一个寄存器,例如EBX
。 -
计算目标地址
(p3 + i)
: CPU将EAX
(p3的值) 和EBX
(偏移量) 相加,得到s[i]
在内存中的地址。结果仍然在EAX
中。 -
解引用
*(p3+i)
: CPU使用EAX
中存储的地址作为内存操作数,执行一个“内存读取”指令。由于*(p3+i)
的类型是char*
,CPU会读取该地址处的4个字节(在32位系统上),这4个字节就是某个字符串字面量的起始地址。这个新的地址被加载到EAX
。 -
传递给
printf
:EAX
中的值(字符串的起始地址)被作为参数推入栈中,然后调用printf
函数。
而对于 (*p3)+i
:
-
解引用
*p3
: CPU从栈上加载p3
的值到EAX
。然后使用EAX
作为内存地址,执行一个“内存读取”指令,读取s[0]
所存储的字符串字面量的地址(例如 0xBBBB1000)。这个字符串地址被加载到EAX
。 -
计算
i * sizeof(char)
: CPU将i
的值与sizeof(char)
(即1)相乘。结果存储在EBX
。 -
计算目标地址
(*p3)+i
: CPU将EAX
(字符串起始地址) 和EBX
(字符偏移量) 相加,得到字符串内部第i
个字符的地址。结果仍然在EAX
中。 -
传递给
printf
:EAX
中的值(字符串内部某个字符的地址)被作为参数推入栈中,然后调用printf
函数。
这种微观层面的拆解,正是C语言魅力与复杂性并存的体现。每当你写下一行C代码,它都在幕后悄然指挥着CPU执行一系列精密的内存操作。
第六章:实践与升华:避免陷阱与最佳实践
理解了原理,现在我们来总结如何在实际开发中避免这类“迷思”,并采用更健壮、更清晰的C语言编程实践。
6.1 数组与指针声明的清晰性
-
char *s[]
: 用于存储一系列字符串(每个字符串的地址)。数组的每个元素都是一个char*
。 -
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
通过 typedef
,String
更直观地表示“字符串”,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;
}