深入理解C语言指针(上)

发布于:2025-07-30 ⋅ 阅读:(17) ⋅ 点赞:(0)

一、内存和地址

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 野指针成因

  1. 指针未初始化:局部指针默认值随机
int main() {
    int* p;  // 未初始化,值随机
    *p = 10;  // 危险:操作野指针
    return 0;
}
  1. 指针越界访问:超出数组等申请的空间
int main() {
    int arr[10] = {0};
    int* p = arr;
    for (int i = 0; i <= 10; i++) {  // i=10时越界
        *(p++) = i;
    }
    return 0;
}
  1. 返回局部变量的地址:局部变量销毁后地址无效
int* test() {
    int n = 10;
    return &n;  // n是局部变量,函数结束后销毁
}
int main() {
    int* p = test();
    *p = 20;  // 危险:访问已销毁的变量
    return 0;
}

6.2 规避野指针

  1. 初始化指针:未知指向时赋NULLNULL是0地址,不可访问)
int main() {
    int a = 10;
    int* p1 = &a;  // 明确指向时初始化
    int* p2 = NULL;  // 未知指向时赋NULL
    return 0;
}
  1. 避免越界访问:确保指针操作在申请的空间内

  2. 指针不用时置为NULL,使用前检查

int main() {
    int arr[10] = {0};
    int* p = arr;
    // 使用后置NULL
    p = NULL;
    
    // 使用前检查
    if (p != NULL) {
        *p = 10;
    }
    return 0;
}
  1. 不返回局部变量的地址

七、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语言的核心难点,也是强大之处:

  • 内存单元的地址即指针,指针变量用于存储地址
  • 指针类型决定操作权限和步长
  • 合理使用constassert可提高代码安全性
  • 传址调用通过指针实现函数对外部变量的修改

掌握指针需要多写代码练习,理解内存和地址的底层逻辑是关键!


网站公告

今日签到

点亮在社区的每一天
去签到