一、内存和地址
1.1 内存的基本概念
生活中,一栋宿舍楼的每个房间都有唯一编号(如101、202),通过编号能快速找到目标房间。计算机的内存管理与之类似:
- 内存被划分为一个个1字节的内存单元,每个单元都有唯一编号(即地址)
- CPU通过地址快速读写内存中的数据,避免逐个查找的低效操作
1.2 计算机存储单位
内存单位的换算关系是理解内存的基础:
单位 | 说明 | 换算关系 |
---|---|---|
bit(比特位) | 最小存储单位,存1或0 | - |
byte(字节) | 基础存储单位 | 1byte = 8bit |
KB | 千字节 | 1KB = 1024byte |
MB | 兆字节 | 1MB = 1024KB |
GB | 吉字节 | 1GB = 1024MB |
TB | 太字节 | 1TB = 1024GB |
1.3 地址的本质:地址总线
内存单元的地址不是“记录”出来的,而是由硬件设计决定的,核心是地址总线:
- 32位机器有32根地址总线,每根线表示0或1(电脉冲有无)
- 32根地址线可表示 (2^{32}) 个地址(约4GB内存空间)
- 地址通过地址总线传递给内存,内存根据地址返回数据(通过数据总线传给CPU)
类比:钢琴的琴键位置是制造商设计好的“硬件约定”,演奏者按约定即可找到对应音符;地址总线的设计也是一种硬件约定,确保CPU能准确访问内存。
二、指针变量和地址
2.1 取地址操作符(&)
C语言中,创建变量就是向内存申请空间。用&
可获取变量的地址(即内存单元的编号):
#include <stdio.h>
int main() {
int a = 10; // 申请4字节内存,存放10
printf("a的地址:%p\n", &a); // %p用于打印地址,输出类似0x006FFD70
return 0;
}
注意:
&a
获取的是a
占用的4个字节中地址最小的那个字节的地址。
2.2 指针变量:存储地址的变量
地址是数值,需要专门的变量存储,这类变量称为指针变量:
#include <stdio.h>
int main() {
int a = 10;
int* pa = &a; // pa是指针变量,存储a的地址
// int* 表示pa是指向int类型变量的指针
return 0;
}
- 指针变量的类型格式:
类型*
(如int*
、char*
) *
说明该变量是指针,前面的int
/char
表示指针指向的变量类型
2.3 解引用操作符(*)
通过指针变量的地址,可使用*
(解引用操作符)访问指向的变量:
#include <stdio.h>
int main() {
int a = 100;
int* pa = &a;
*pa = 0; // 通过pa的地址找到a,将a改为0
printf("a = %d\n", a); // 输出a = 0
return 0;
}
*pa
等价于a
,通过指针间接修改变量的值,增加了操作灵活性。
2.4 指针变量的大小
指针变量的大小取决于地址的位数,与指向的类型无关:
#include <stdio.h>
int main() {
printf("char* 大小:%zd\n", sizeof(char*)); // 4字节(32位)/8字节(64位)
printf("int* 大小:%zd\n", sizeof(int*)); // 同上
printf("double* 大小:%zd\n", sizeof(double*)); // 同上
return 0;
}
- 32位平台:地址是32位(4字节),所有指针变量都是4字节
- 64位平台:地址是64位(8字节),所有指针变量都是8字节
三、指针变量类型的意义
指针类型不影响大小,但决定了操作权限和步长:
3.1 解引用的权限
指针类型决定解引用时访问的字节数:
#include <stdio.h>
int main() {
int n = 0x11223344; // 假设内存中存储为0x44,0x33,0x22,0x11(小端)
int* pi = &n;
*pi = 0; // int*解引用访问4字节,n变为0x00000000
char* pc = (char*)&n;
*pc = 0; // char*解引用访问1字节,n变为0x11223300
return 0;
}
int*
解引用:操作4字节char*
解引用:操作1字节
3.2 指针±整数的步长
指针±整数时,步长由指针类型决定(即跳过的字节数):
#include <stdio.h>
int main() {
int n = 10;
char* pc = (char*)&n;
int* pi = &n;
printf("&n = %p\n", &n); // 假设输出0x00AFF974
printf("pc + 1 = %p\n", pc + 1); // 0x00AFF975(+1字节)
printf("pi + 1 = %p\n", pi + 1); // 0x00AFF978(+4字节)
return 0;
}
char*
±1:跳过1字节int*
±1:跳过4字节double*
±1:跳过8字节
3.3 void* 指针:泛型指针
void*
可接收任意类型的地址,但不能直接解引用或±整数:
#include <stdio.h>
int main() {
int a = 10;
void* pv = &a; // 合法:void*接收int*地址
// *pv = 20; // 错误:void*不能直接解引用
// pv + 1; // 错误:void*不能±整数
return 0;
}
- 用途:函数参数中接收任意类型地址(如
memcpy
函数),实现泛型编程。
四、const修饰指针
const
可限制指针或其指向的内容是否可修改,位置不同效果不同:
4.1 const在*左边:限制指向的内容
const int* p; // 等价于int const* p;
- 指针
p
指向的内容(*p
)不能通过p
修改 - 指针
p
本身可以指向其他地址
4.2 const在*右边:限制指针本身
int* const p;
- 指针
p
本身不能修改(不能指向其他地址) - 指向的内容(
*p
)可以修改
4.3 左右都有const:两者都限制
const int* const p;
- 指针
p
本身不能修改 - 指向的内容(
*p
)也不能修改
示例验证:
#include <stdio.h>
void test() {
int a = 10, b = 20;
const int* p1 = &a;
// *p1 = 30; // 错误:不能修改指向的内容
p1 = &b; // 合法:指针本身可改
int* const p2 = &a;
*p2 = 30; // 合法:指向的内容可改
// p2 = &b; // 错误:指针本身不可改
const int* const p3 = &a;
// *p3 = 30; // 错误
// p3 = &b; // 错误
}
int main() { test(); return 0; }
五、指针运算
指针有三种基本运算:±整数、指针-指针、关系运算。
5.1 指针±整数
结合数组使用,通过指针遍历数组:
#include <stdio.h>
int main() {
int arr[5] = {1,2,3,4,5};
int* p = arr; // 数组名是首元素地址,等价于&arr[0]
for (int i = 0; i < 5; i++) {
printf("%d ", *(p + i)); // 等价于arr[i],输出1 2 3 4 5
}
return 0;
}
5.2 指针-指针
两个指针相减的结果是之间的元素个数(需指向同一数组):
#include <stdio.h>
// 模拟strlen:计算字符串长度(\0之前的字符数)
int my_strlen(char* s) {
char* start = s;
while (*s != '\0') { // 遍历到\0停止
s++;
}
return s - start; // 指针相减得长度
}
int main() {
printf("abc的长度:%d\n", my_strlen("abc")); // 输出3
return 0;
}
5.3 指针的关系运算
指针可比较大小(地址高低),常用于数组遍历:
#include <stdio.h>
int main() {
int arr[5] = {1,2,3,4,5};
int* p = arr;
while (p < arr + 5) { // 指针比较:p未超过数组末尾
printf("%d ", *p);
p++;
}
return 0;
}
六、野指针
野指针:指向位置未知(随机、不正确)的指针,操作野指针可能导致程序崩溃。
6.1 野指针成因
- 指针未初始化:局部指针默认值随机
int main() {
int* p; // 未初始化,值随机
*p = 10; // 危险:操作野指针
return 0;
}
- 指针越界访问:超出数组等申请的空间
int main() {
int arr[10] = {0};
int* p = arr;
for (int i = 0; i <= 10; i++) { // i=10时越界
*(p++) = i;
}
return 0;
}
- 返回局部变量的地址:局部变量销毁后地址无效
int* test() {
int n = 10;
return &n; // n是局部变量,函数结束后销毁
}
int main() {
int* p = test();
*p = 20; // 危险:访问已销毁的变量
return 0;
}
6.2 规避野指针
- 初始化指针:未知指向时赋
NULL
(NULL
是0地址,不可访问)
int main() {
int a = 10;
int* p1 = &a; // 明确指向时初始化
int* p2 = NULL; // 未知指向时赋NULL
return 0;
}
避免越界访问:确保指针操作在申请的空间内
指针不用时置为NULL,使用前检查
int main() {
int arr[10] = {0};
int* p = arr;
// 使用后置NULL
p = NULL;
// 使用前检查
if (p != NULL) {
*p = 10;
}
return 0;
}
- 不返回局部变量的地址
七、assert断言
assert
宏用于调试时验证条件,若不满足则报错终止程序,需包含<assert.h>
。
7.1 基本使用
#include <stdio.h>
#include <assert.h>
int main() {
int* p = NULL;
assert(p != NULL); // 条件为假,程序终止并报错
return 0;
}
- 报错信息包含文件名、行号和失败的条件,便于调试。
7.2 禁用assert
定义NDEBUG
可禁用所有assert
(Release版本常用,减少性能消耗):
#define NDEBUG // 放在#include <assert.h>前
#include <assert.h>
// 此时assert无效
八、指针的使用和传址调用
8.1 传值调用 vs 传址调用
- 传值调用:函数接收变量的副本,修改副本不影响原变量
// 失败的交换函数(传值调用)
void Swap1(int x, int y) {
int tmp = x;
x = y;
y = tmp; // 只修改形参x、y,不影响实参
}
int main() {
int a = 10, b = 20;
Swap1(a, b); // 交换后a、b仍为10、20
return 0;
}
- 传址调用:函数接收变量的地址,通过指针修改原变量
// 成功的交换函数(传址调用)
void Swap2(int* px, int* py) {
int tmp = *px;
*px = *py;
*py = tmp; // 通过地址修改原变量
}
int main() {
int a = 10, b = 20;
Swap2(&a, &b); // 交换后a=20,b=10
return 0;
}
8.2 模拟实现strlen(传址调用应用)
#include <stdio.h>
#include <assert.h>
int my_strlen(const char* str) {
assert(str != NULL); // 确保str不是NULL
int count = 0;
while (*str != '\0') { // 遍历到\0停止
count++;
str++;
}
return count;
}
int main() {
printf("长度:%d\n", my_strlen("abcdef")); // 输出6
return 0;
}
总结
指针是C语言的核心难点,也是强大之处:
- 内存单元的地址即指针,指针变量用于存储地址
- 指针类型决定操作权限和步长
- 合理使用
const
和assert
可提高代码安全性 - 传址调用通过指针实现函数对外部变量的修改
掌握指针需要多写代码练习,理解内存和地址的底层逻辑是关键!