新晋码农一枚,小编会定期整理一些写的比较好的代码和知识点,作为自己的学习笔记,试着做一下批注和补充,转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!本章内容较多,可点击文章目录进行跳转!
小编整理和学习了C语言的相关知识,可作为扫盲使用,后续也会更新一些技术类的文章,大家共同交流学习!
您的点赞、关注、收藏就是对小编最大的动力!
目录
十一、指针的大小与系统相关,而指针的类型决定了如何解释它指向的内存内容
一、指针的本质与内存模型
1.1 内存的物理与逻辑结构
计算机内存由物理存储单元组成,每个单元有唯一地址(通常以十六进制表示,如0x7ffd3a4c)。操作系统通过虚拟内存机制将物理地址映射为逻辑地址,为每个进程提供独立的地址空间。C语言指针直接操作这些逻辑地址,实现数据的间接访问。
1.2 指针的定义与二重性
- 定义:指针是存储内存地址的变量,其类型决定了访问内存的“步长”和解释方式。
#include <stdio.h> int main() { int num = 43; /*野指针风险:若省略& num(如int* p; 后直接解引用* p = 43; ),p成为未初始化的野指针,解引用将引发段错误(Segmentation Fault)*/ int* p = # // p存储num的地址 // 指针声明:int *p定义一个指向整型的指针变量p。 // *表示p是指针类型,int限定其指向的数据类型为整型。 /* 指针大小:32位系统占4字节,64位系统占8字节,与int大小无关。 取地址操作:&num 获取num的内存地址(如0x7ffd3a4c),并赋值给p。 此时p存储该地址,p与& num等价。*/ /*空指针保护:可初始化为NULL(如int* p = NULL; ),使用前需检查:*/ if (p == NULL) { *p = 100; // 安全操作 } printf("num的值: %d\n", num); // 输出43 printf("num的地址: %p\n", &num); // 输出地址(如0x7ffd3a4c) printf("p的值: %p\n", p); // 输出与&num相同 printf("*p的值: %d\n", *p); // 输出43 *p = 100; // 修改p指向的值 printf("修改后的num: %d\n", num); // 输出100 /* 解引用操作:通过* p访问p指向的内存数据。 * p等价于num,值为43。 修改* p(如* p = 100; )会同步改变num的值 类型安全: int *p确保解引用时按int类型解释内存数据(4字节对齐,小端序/大端序依赖系统)。 错误类型匹配(如char *p = #)会导致数据解析错误 */ return 0; } - 二重性:
- 地址属性:指针本身是变量,存储地址值。
- 类型属性:指针类型(如
int*、char*)决定解引用时的行为(如读取4字节或1字节)。

1.3 指针的大小与系统架构
- 32位系统:指针占4字节(地址范围
0x00000000~0xFFFFFFFF)。 - 64位系统:指针占8字节(地址范围
0x0000000000000000~0xFFFFFFFFFFFFFFFF)。 - 验证方法:
#include <stdio.h> //这是一个预处理指令,用于引入标准输入输出库(stdio.h) //因为程序中使用了printf函数(用于输出内容到控制台),而printf的声明就包含在stdio.h中,所以必须通过该指令引入这个库才能正常使用printf int main() { printf("Pointer size: %zu bytes\n", sizeof(int*)); return 0; } //main函数是 C 程序的入口点,程序从main函数开始执行。 //int表示main函数的返回值类型为整数,通常用于表示程序的执行状态(return 0表示程序正常结束) //sizeof(int*):sizeof是 C 语言的一个运算符,用于计算括号中数据类型或变量所占用的字节数。这里int*表示 “指向 int 类型的指针”,sizeof(int*)即计算这种指针在当前系统中占用的内存字节数 //printf输出:printf函数用于将内容打印到控制台。格式字符串"Pointer size: %zu bytes\n"中,%zu是格式化占位符,用于匹配sizeof的返回值(size_t类型,无符号整数),最终会被替换为int*指针的字节数。 //例如,在 32 位系统中,指针通常占用 4 字节;在 64 位系统中,指针通常占用 8 字节,因此程序输出可能是Pointer size: 8 bytes(取决于运行环境)

这里补充解释一下范围0-F:
快速查阅表
| 十六进制 | 十进制 | 二进制(4位表示) |
|---|---|---|
| 0 | 0 | 0000 |
| 1 | 1 | 0001 |
| 2 | 2 | 0010 |
| 3 | 3 | 0011 |
| 4 | 4 | 0100 |
| 5 | 5 | 0101 |
| 6 | 6 | 0110 |
| 7 | 7 | 0111 |
| 8 | 8 | 1000 |
| 9 | 9 | 1001 |
| A | 10 | 1010 |
| B | 11 | 1011 |
| C | 12 | 1100 |
| D | 13 | 1101 |
| E | 14 | 1110 |
| F | 15 | 1111 |
二、指针的核心操作与类型系统
2.1 指针的声明与初始化
- 声明语法:
int *p; // 声明指向int的指针 char *c; // 声明指向char的指针 - 初始化规则:
- 直接初始化:
int num = 10; int *p = # // 合法:p指向num - 动态初始化:
#include <stdio.h> #include <stdlib.h> // 包含 malloc 和 exit int main() { int* p = malloc(sizeof(int)); if (p == NULL) { fprintf(stderr, "错误:内存分配失败!\n"); return 1; } *p = 42; // 写入数据 printf("分配的内存地址: %p\n", (void*)p); printf("存储的值: %d\n", *p); free(p); // 释放内存 p = NULL; // 避免悬垂指针 return 0; }
禁止行为: -
int *p; // 未初始化 *p = 42; // 错误:p是野指针,解引用导致未定义行为
- 直接初始化:
2.2 指针的类型系统
- 类型决定步长:
int arr[3] = {1, 2, 3}; int *p = arr; printf("%d\n", *(p + 1)); // 输出2(p+1移动4字节) - 类型匹配原则:
- 指针类型必须与指向数据类型一致,否则解引用可能读取错误数据。
- 显式类型转换(谨慎使用):
double d = 3.14; int *p = (int*)&d; // 危险:按int解释double的内存 //指针类型决定了指针在被解引用的时候访问几个字节 //如果是int* 的指针,解引用访问4个字节 //如果是char* 的指针,解引用访问1个字节 //推广到其他类型
| 数据类型 | 32位系统 | 64位系统 | 说明 |
|---|---|---|---|
char | 1 | 1 | 存储单个字符(如'A') |
short | 2 | 2 | 短整型(范围:-32768~32767) |
int | 4 | 4 | 整型(常用,范围约±21亿) |
long | 4 | 8(Linux)/4(Windows) | 长整型 |
long long | 8 | 8 | 超长整型(范围极大) |
float | 4 | 4 | 单精度浮点数(约7位有效数字) |
double | 8 | 8 | 双精度浮点数(约15位有效数字) |
pointer(指针) | 4 | 8 | 存储内存地址(32位/64位系统差异大) |
2.3 空指针与野指针
- 空指针(NULL):
- 定义为
(void*)0,表示不指向任何有效内存。 - 使用前需检查:
if (p != NULL) { *p = 42; }
- 定义为
- 野指针:
- 产生原因:未初始化、越界访问、释放后继续使用。
- 示例:
int *p; free(p); // p成为野指针 *p = 10; // 未定义行为
三、指针与数组的深度关联
3.1 数组名与指针的等价性
- 数组名退化:在多数表达式中,数组名等价于首元素地址。
int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; // 等价于 p = &arr[0] - 例外情况:
sizeof(arr)返回整个数组大小(如5 * sizeof(int))。&arr返回指向数组的指针(类型为int (*)[5])。
3.2 指针遍历数组
- 下标法:
arr[i]等价于*(arr + i)。 - 指针法:
for (int *p = arr; p < arr + 5; p++) { printf("%d ", *p); // 输出1 2 3 4 5 } - 性能对比:指针遍历通常比下标法更快(避免重复计算地址)。
3.3 多维数组与指针
- 二维数组的指针表示:
int matrix[3][3] = {{1,2,3}, {4,5,6}, {7,8,9}}; int (*p)[3] = matrix; // p指向包含3个int的数组 printf("%d\n", p[1][2]); // 输出6(等价于matrix[1][2]) - 指针数组:
int *rows[3]; // 存储3个int指针的数组 for (int i = 0; i < 3; i++) { rows[i] = matrix[i]; // 每个rows[i]指向一行 }
四、指针在函数中的高级应用
4.1 地址传递与参数修改
- 基础示例:
void swap(int *a, int *b) { int temp = *a; *a = *b; *b = temp; } int x = 1, y = 2; swap(&x, &y); // x和y的值被交换 - 数组参数传递:
void print_array(int *arr, int size) { for (int i = 0; i < size; i++) { printf("%d ", arr[i]); // arr退化为指针 } }
4.2 函数指针与回调机制
- 函数指针声明:
int add(int a, int b) { return a + b; } int (*func_ptr)(int, int) = add; // func_ptr指向add函数 - 回调应用:
#include <stdio.h> void process(int (*operation)(int, int), int x, int y) { printf("Result: %d\n", operation(x, y)); } int main() { process(add, 3, 4); // 输出7 return 0; }
4.3 返回指针的函数
- 安全实践:
- 避免返回局部变量地址(局部变量在函数返回后失效)。
- 返回动态分配内存或全局变量地址:
int *create_array(int size) { int *arr = malloc(size * sizeof(int)); if (arr == NULL) return NULL; for (int i = 0; i < size; i++) arr[i] = i; return arr; // 调用者需负责释放 }
五、动态内存管理:malloc与free
5.1 动态分配函数对比
| 函数 | 原型 | 行为 |
|---|---|---|
malloc | void* malloc(size_t size) | 分配未初始化内存 |
calloc | void* calloc(size_t num, size_t size) | 分配并初始化为0 |
realloc | void* realloc(void* ptr, size_t size) | 调整已分配内存大小 |
5.2 内存泄漏的常见场景
- 场景1:分配后未释放
void leak() { int *p = malloc(sizeof(int)); // 忘记free(p) } - 场景2:异常路径导致泄漏
void risky() { int *p = malloc(sizeof(int)); if (error_condition) return; // 直接返回导致泄漏 free(p); }
5.3 内存管理最佳实践
- 成对使用:每个
malloc必须有对应的free。 - 释放后置NULL:
free(p); p = NULL; // 避免悬垂指针 - 使用工具检测:
Valgrind:检测内存泄漏和非法访问。AddressSanitizer(GCC/Clang):编译时插入内存检查代码。
六、结构体与指针的深度结合
6.1 结构体指针与成员访问
- 箭头运算符:
struct Student { int age; char name[20]; }; struct Student s = {20, "Alice"}; struct Student *p = &s; printf("%s\n", p->name); // 等价于 (*p).name
6.2 自引用结构与链表
- 链表节点定义:
struct Node { int data; struct Node *next; // 自引用指针 }; - 链表遍历示例:
void print_list(struct Node *head) { for (struct Node *p = head; p != NULL; p = p->next) { printf("%d ", p->data); } }
6.3 动态结构体数组
- 分配与释放:
struct Student *students = malloc(3 * sizeof(struct Student)); if (students == NULL) { /* 处理错误 */ } students[0].age = 20; // 通过数组下标访问 free(students); // 释放整个数组
七、const限定符与指针的复杂交互
7.1 const指针的分类
| 语法 | 含义 | 示例 |
|---|---|---|
const int *p | 指向常量,不可通过p修改数据 | const int *p = # |
int *const p | 指针常量,不可修改指向地址 | int num = 10; int *const p = # |
const int *const p | 双重常量,地址和数据均不可变 | const int num = 10; const int *const p = # |
7.2 const指针的应用场景
- 保护函数参数:
void print_string(const char *s) { // 防止函数内部修改s指向的字符串 while (*s) putchar(*s++); } - 常量字符串字面量:
const char *msg = "Hello"; // msg指向只读内存 // msg[0] = 'h'; // 错误:试图修改只读内存
八、指针的常见问题与调试技巧
8.1 野指针与悬垂指针
- 野指针:未初始化的指针,解引用导致段错误(Segmentation Fault)。
- 悬垂指针:释放内存后未置NULL,继续解引用。
- 调试方法:
- 使用
gdb定位崩溃点:gcc -g program.c -o program gdb ./program (gdb) run (gdb) backtrace # 查看调用栈
- 使用
8.2 内存越界访问
- 数组越界:
int arr[3] = {1, 2, 3}; int *p = arr; printf("%d\n", p[3]); // 越界访问,行为未定义 - 缓冲区溢出:
char buf[10]; strcpy(buf, "This string is too long!"); // 溢出
8.3 类型不匹配问题
- 错误示例:
double d = 3.14; int *p = &d; // 警告:类型不匹配 *p = 42; // 可能覆盖相邻内存 - 解决方案:显式类型转换(需确保逻辑正确):
int *p = (int*)&d; // 仅在明确知道内存布局时使用
这里补充一点:指针的类型决定了指针+/-操作的时候跳过几个字节,决定了指针的步长
int main() {
int a = 0x11223344;
int* pa = &a;
char* pc = (char*)&a;
printf("pa=%p\n", pa);
printf("pa+1 =%p\n", pa + 1);
printf("pc=%p\n", pc);
printf("pc+1 =%p\n", pc + 1);
//指针的类型决定了指针+/-操作的时候跳过几个字节,决定了指针的步长
}

这里的int跳过了四个字节,char跳过了1个字节,跟int和char本身所占字节有关。
接着这个问题,继续提出猜想:int和float的字节数一样,那这两者可以通用吗?
答案是 不能
当存储的数值改为int型的100时:
int main() {
int a = 0;
int* pi = &a;
float* pf = &a;
*pi = 100;
return 0;
}
可以看到输入a的地址时,内存中存储的数为64 00 00 00 cc........
字节序(Endianness)的影响
- 小端序(Little-Endian):低位字节存储在低地址。
- 例如,
100(0x0064)存储为64 00。
- 例如,
- 大端序(Big-Endian):高位字节存储在低地址。
- 例如,
100存储为00 64。
- 例如,
- 图中内存从
0x0000063FD6FFCB4开始显示64 00...,符合小端序下100的存储方式

当存储的数值改为float型的100.0时:变化为00 00 c8 42 cc......

| 特性 | 整数 100(int) | 浮点数 100.0(float) |
|---|---|---|
| 数据类型 | 整型(直接二进制表示) | 浮点型(IEEE 754编码) |
| 存储示例 | 64 00 00 00(小端序) | 00 00 C8 42(小端序) |
| 解析方式 | 直接按权展开 | 符号位 + 指数位 + 尾数位 |
根本原因:整数和浮点数的二进制编码规则完全不同,即使数值相同(如 100 和 100.0),内存中的字节表示也会不同。
所以,不能int 和float不通用,即使字节数一样!
九、指针的高级主题与扩展应用
9.1 柔性数组(C99特性)
- 定义:结构体末尾的未指定大小数组,用于动态扩展。
struct FlexArray { int size; int data[]; // 柔性数组 }; - 使用示例:
struct FlexArray *fa = malloc(sizeof(struct FlexArray) + 5 * sizeof(int)); fa->size = 5; for (int i = 0; i < 5; i++) fa->data[i] = i; free(fa);
9.2 指针与位操作
- 直接操作内存:
int num = 0x12345678; char *p = (char*)# // 按字节访问 printf("%02x\n", *p); // 输出78(小端序) - 应用场景:协议解析、硬件寄存器操作。
9.3 指针与多线程
- 共享指针的同步:
#include <pthread.h> int shared_data = 0; int *p = &shared_data; void* thread_func(void* arg) { *p = 42; // 需要互斥锁保护 return NULL; } - 线程安全实践:使用互斥锁(
pthread_mutex_t)保护指针访问。
十、门牌号一样大,但门牌号代表的面积不一样大
我们可以通过一个门牌号与房屋面积的类比,来理解指针地址和存储面积的关系:
1. 门牌号(指针地址)
- 作用:门牌号是房屋的唯一标识,用于定位具体位置(如“XX路123号”)。
- 特点:
- 每个门牌号是唯一的,但不直接反映房屋大小。
- 例如,“XX路100号”可能是一个小公寓,也可能是一个大别墅。
- 对应到内存:
- 指针地址是内存中某个位置的唯一标识(如
0x0000063FD6FFCB4)。 - 地址本身不决定存储数据的大小,仅用于定位。
- 指针地址是内存中某个位置的唯一标识(如
2. 房屋面积(存储面积)
- 作用:房屋面积决定实际空间大小(如50㎡或200㎡)。
- 特点:
- 面积由房屋设计决定,与门牌号无关。
- 同一个门牌号(地址)可能指向不同大小的房屋(数据类型不同)。
- 对应到内存:
- 存储面积由数据类型决定:
char:1字节(小公寓)。int:4字节(普通住宅)。double:8字节(大别墅)。
- 例如,地址
0x1000可能存储:- 一个
char(1字节),或 - 一个
int(4字节),或 - 一个
double(8字节)。
- 一个
- 存储面积由数据类型决定:
3. 类比解释
场景1:门牌号相同,面积不同
- 现实例子:
- “XX路100号”可能是一个:
- 10㎡ 的快递柜(类似
char类型),或 - 100㎡ 的办公室(类似
int类型),或 - 500㎡ 的仓库(类似
double数组)。
- 10㎡ 的快递柜(类似
- “XX路100号”可能是一个:
- 内存例子:
- 地址
0x1000可能存储:- 一个字符
'A'(1字节),或 - 一个整数
100(4字节),或 - 一个浮点数
3.14(4/8字节)。
- 一个字符
- 地址
场景2:门牌号不同,面积相同
- 现实例子:
- “XX路100号”和“YY路200号”可能都是50㎡的公寓。
- 内存例子:
- 地址
0x1000和0x2000可能都存储int类型的100(各占4字节)。
- 地址
4. 指针与数据类型的关联
- 指针的本质:
- 指针是一个变量,存储的是另一个变量的地址(门牌号)。
- 指针的类型决定了如何解释该地址后的数据(即“房屋面积”)。
- 示例:
int num = 100; int *p = # // p指向一个4字节的int char c = 'A'; char *q = &c; // q指向一个1字节的charp和q都是地址(门牌号),但:*p会读取4字节(int类型)。*q会读取1字节(char类型)。
5. 为什么需要这种设计?
- 灵活性:
- 同一个地址可以按需解释为不同类型(如通过类型转换)。
- 效率:
- 指针仅存储地址(通常4/8字节),不占用额外空间。
- 安全性:
- 程序员需明确指针类型,避免误读数据(如将
char*当int*使用会导致错误)。
- 程序员需明确指针类型,避免误读数据(如将
6. 总结类比表
| 概念 | 门牌号类比 | 内存类比 |
|---|---|---|
| 地址 | 门牌号(唯一标识) | 指针值(如 0x1000) |
| 数据大小 | 房屋面积 | 数据类型占用的字节数 |
| 指针类型 | 房屋用途说明 | 告诉编译器如何解释地址后的数据 |
通过这个类比,可以直观理解:指针地址是定位符,而存储面积由数据类型决定。就像门牌号不决定房屋大小一样,地址本身也不决定数据占用的内存空间。
十一、指针的大小与系统相关,而指针的类型决定了如何解释它指向的内存内容
实际上,指针 pa 的大小(占用的内存字节数)与它指向的变量 a 的大小无关,而是由系统的地址空间大小决定。而 pa 的类型决定了如何解释它指向的内存内容(即 a 的大小)。
1. 指针 pa 的大小(存储地址所需的字节数)
- 指针的本质:指针是一个变量,用于存储另一个变量的地址。
- 指针的大小:
- 在 32位系统 中,地址空间是 32 位,因此指针占 4 字节。
- 在 64位系统 中,地址空间是 64 位,因此指针占 8 字节。
- 与
a的大小无关:- 无论
a是char(1字节)、int(4字节)还是double(8字节),指针pa的大小只取决于系统是 32 位还是 64 位。
- 无论
2. 指针 pa 的类型(决定如何解释 a 的大小)
- 指针的类型:
- 指针的类型(如
char*、int*、double*)告诉编译器如何解释指针指向的内存内容。 - 例如:
int* pa = &a;:pa指向一个int,编译器知道从pa开始的 4 字节 是a的值。char* pa = &c;:pa指向一个char,编译器知道从pa开始的 1 字节 是c的值。
- 指针的类型(如
- 类型的作用:
- 决定指针算术运算的步长(如
pa++移动多少字节)。 - 决定解引用(
*pa)时读取多少字节。
- 决定指针算术运算的步长(如
3. 类比解释
- 门牌号(指针地址):
- 门牌号本身的大小(如写在纸上的数字长度)与它指向的房屋大小无关。
- 无论门牌号指向的是小仓库还是大别墅,门牌号本身的存储空间是固定的(如用A4纸打印,总是占一张纸)。
- 内存中的指针:
- 指针
pa就像一张纸条,上面写着a的地址(如0x1000)。 - 在 32 位系统中,纸条上写 8 位十六进制数(4 字节);在 64 位系统中,写 16 位十六进制数(8 字节)。
- 纸条的大小(指针的大小)与
a的大小无关,只与系统有关。
- 指针
4. 示例代码验证
#include <stdio.h>
int main() {
char c = 'A';
int a = 100;
double d = 3.14;
char* pc = &c;
int* pa = &a;
double* pd = &d;
printf("指针 pc 的大小: %zu 字节\n", sizeof(pc));
printf("指针 pa 的大小: %zu 字节\n", sizeof(pa));
printf("指针 pd 的大小: %zu 字节\n", sizeof(pd));
return 0;
}
输出结果(64位系统):

- 结论:
- 无论
pc、pa还是pd,它们的大小都是 8 字节(64 位系统)。 - 它们的大小与指向的变量
c、a、d的大小无关。
- 无论
5. 关键总结
| 特性 | 解释 |
|---|---|
| 指针的大小 | 由系统的地址空间大小决定(32位系统:4字节,64位系统:8字节)。 |
| 指针的类型 | 决定如何解释指针指向的内存内容(即变量 a 的大小和类型)。 |
| 指针的用途 | 类型用于指针算术和解引用,大小用于存储地址本身。 |
6. 为什么容易混淆?
- 混淆点1:误以为指针的大小与指向的变量大小有关。
- 实际:指针的大小是固定的,与系统相关。
- 混淆点2:误以为
int*和char*的指针大小不同。- 实际:所有指针类型的大小相同(在同一个系统中)。
的大小与系统相关,而指针的类型决定了如何解释它指向的内存内容。
十二、总结与学习建议
12.1 指针的核心价值
- 直接内存操作:绕过命名变量,直接访问任意地址。
- 高效数据传递:函数间共享大数据无需复制。
- 动态数据结构:支持链表、树、图等复杂结构。
12.2 学习路径建议
- 基础阶段:掌握指针声明、初始化、算术运算。
- 进阶阶段:理解指针与数组、结构体的关系,动态内存管理。
- 实战阶段:通过项目(如实现链表、解析二进制文件)巩固知识。
- 调试阶段:熟练使用
gdb、Valgrind等工具排查问题。
12.3 经典书籍推荐
- 《C程序设计语言》(K&R):指针章节为经典范本。
- 《C和指针》:系统讲解指针的方方面面。
- 《深度探索C++对象模型》:理解C++中指针的底层行为(进阶)。
通过系统学习与实践,指针将成为你驾驭C语言的利器,开启高效、灵活的系统编程之旅。

687

被折叠的 条评论
为什么被折叠?



