云计算百科
云计算领域专业知识百科平台

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

一、内存和地址

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可提高代码安全性
    • 传址调用通过指针实现函数对外部变量的修改

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

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 深入理解C语言指针(上)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!