当前位置: 首页 > ds >正文

【C语言】指针深度剖析(一)

文章目录

  • 一、内存和地址
    • 1.1 内存的基本概念
    • 1.2 编址的原理
  • 二、指针变量和地址
    • 2.1 取地址操作符(&)
    • 2.2 指针变量和解引用操作符(*)
      • 2.2.1 指针变量
      • 2.2.2 指针类型的解读
      • 2.2.3 解引用操作符
    • 2.3 指针变量的大小
  • 三、指针变量类型的意义
    • 3.1 影响解引用的权限
    • 3.2 影响指针 ± 整数的步长
    • 3.3 void* 指针
  • 四、const修饰指针
    • 4.1 const修饰变量
    • 4.2 const修饰指针变量
  • 五、指针运算
    • 5.1 指针±整数
    • 5.2 指针-指针
    • 5.3 指针的关系运算
  • 六、野指针
    • 6.1 野指针成因
    • 6.2 如何规避野指针
  • 七、assert断言
  • 八、指针的使用和传址调用
    • 8.1 strlen的模拟实现
    • 8.2 传值调用和传址调用

一、内存和地址

1.1 内存的基本概念

在计算机中,CPU处理数据时需要从内存中读取数据,处理后的数据也会放回内存。为了高效管理内存,内存被划分为一个个大小为1字节的内存单元,每个内存单元都有唯一的编号,这个编号就是我们所说的地址,在C语言中也被称为指针。

举个例子:一栋宿舍楼有100个房间,给每个房间编上号(如101、102等),根据房间号就能快速找到房间。内存就像这栋宿舍楼,每个内存单元就像一个房间,地址则是房间号,有了地址,CPU就能快速找到对应的内存单元。

计算机中常见的存储单位及换算关系如下:

  • 1byte(字节)= 8bit(比特位)
  • 1KB = 1024byte
  • 1MB = 1024KB
  • 1GB = 1024MB
  • 1TB = 1024GB
  • 1PB = 1024TB

1.2 编址的原理

CPU访问内存中的某个字节空间,必须知道该字节空间的地址。计算机的编址是通过硬件设计实现的,就像钢琴、吉他等乐器,制造商在硬件层面设计好,演奏者就能准确找到相应位置。

CPU和内存之间通过大量的线连接,其中一组重要的线是地址总线。32位机器有32根地址总线,每根线有0和1两种状态(表示电脉冲有无),32根地址线能表示2^32种不同的地址。地址信息通过地址总线传给内存,内存根据地址找到对应数据,再通过数据总线传入CPU寄存器。

在这里插入图片描述

二、指针变量和地址

2.1 取地址操作符(&)

在C语言中,创建变量的本质是向内存申请空间

#include <stdio.h>
int main()
{int a = 10;return 0;
}

变量a占用4个字节的内存空间,每个字节都有自己的地址。我们可以使用取地址操作符&来获取变量的地址,如&a得到的是a所占4个字节中地址较小的那个字节的地址。
在这里插入图片描述

2.2 指针变量和解引用操作符(*)

2.2.1 指针变量

通过取地址操作符&得到的地址是一个数值,我们可以将其存储在指针变量中。指针变量就是专门用来存放地址的变量

#include <stdio.h>
int main()
{int a = 10;int* pa = &a; // 取出a的地址并存储到指针变量pa中return 0;
}

2.2.2 指针类型的解读

指针变量的类型由*和前面的类型组成,如int*表示该指针变量指向的是整型(int)类型的对象。对于char类型的变量ch,其地址应存放在char*类型的指针变量中。

2.2.3 解引用操作符

有了指针变量存储地址后,我们可以使用解引用操作符*通过地址找到对应的变量并进行操作。

#include <stdio.h>
int main()
{int a = 100;int* pa = &a;*pa = 0; // 通过pa中存放的地址找到a,并将a的值改为0return 0;
}

这里*pa就相当于变量a,通过*pa可以对a进行修改,这为操作变量提供了另一种途径。

2.3 指针变量的大小

指针变量的大小取决于地址的大小:

  • 在32位平台下,地址是32个比特位,指针变量大小为4个字节。
  • 在64位平台下,地址是64个比特位,指针变量大小为8个字节。

需要注意的是,指针变量的大小和其类型无关,在相同平台下,所有指针类型的变量大小都是相同的。例如:

#include <stdio.h>
int main()
{printf("%zd\n", sizeof(char*));printf("%zd\n", sizeof(short*));printf("%zd\n", sizeof(int*));printf("%zd\n", sizeof(double*));return 0;
}

在32位环境下输出结果均为4,在64位环境下输出结果均为8。

三、指针变量类型的意义

虽然指针变量的大小和类型无关,但指针类型有着重要的意义。

3.1 影响解引用的权限

指针的类型决定了对指针解引用时的操作权限,即一次能操作的字节数。例如:

  • char*类型的指针解引用只能访问1个字节。
  • int*类型的指针解引用能访问4个字节。

看下面两段代码:

// 代码1
int main() {int a = 0x11223344;int* p = &a;*p = 0;return 0;
}

逐语句调试,代码运行到15行时。int*可以访问四个字节,将四个字节都改为0
在这里插入图片描述

// 代码2
int main() {int a = 0x11223344;char* p = &a;*p = 0;return 0;
}

逐语句调试,代码运行到22行时。char*只访问访问一个字节,将第一个字节改为0
在这里插入图片描述

3.2 影响指针 ± 整数的步长

指针的类型决定了指针向前或者向后走一步的距离。例如:

int* + 1 跳过四个字节(int大小),char* + 1 跳过一个字节(char大小)
在这里插入图片描述

3.3 void* 指针

void*类型的指针可以接受任意类型的地址,可以理解为无具体类型指针(或者叫泛型指针)但它不能直接进行指针的±整数和解引用运算。通常用于函数参数部分,实现泛型编程的效果,以处理多种类型的数据。

四、const修饰指针

4.1 const修饰变量

const修饰的变量不能直接被修改。

#include <stdio.h>
int main()
{const int n = 0;n = 20; // 报错,n不能被直接修改return 0;
}

此时变量具有常属性,称为常变量,但本质依旧是变量而不是常量。

在C++中被const修饰则为常量。

但如果通过指针获取其地址,还是可以修改该变量的值,这显然打破了const的限制,所以需要用const修饰指针变量。

在这里插入图片描述

4.2 const修饰指针变量

  • const放在*的右边:修饰的是指针变量本身,指针变量不可以再指向其他变量。但可以通过指针修改指向的内容。
int main()
{int a = 10;int b = 20;int * const p = &a;p = &b;//err*p = 100;//可以通过编译return 0;
}
  • const放在*的左边:限制指向的内容,不可以通过指针来修改,但可以修改指针指向的变量。
int main()
{int a = 10;int b = 20;int const* p = &a;p = &b;//可以通过编译*p = 100;//errreturn 0;
}

在这里插入图片描述

五、指针运算

指针的基本运算有三种:

5.1 指针±整数

原理同本文3.2部分。

由于数组在内存中是连续存放的,知道第一个元素的地址后,通过指针±整数可以访问数组中的其他元素。

int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};int* p = &arr[0];int i = 0;int sz = sizeof(arr) / sizeof(arr[0]);for (i = 0; i < sz; i++){printf("%d ", *(p + i)); // 通过指针+整数访问数组元素}return 0;
}

在这里插入图片描述
注意:

  • *(p+1)不要写成*p+1,前者表示指针变量+1,后者表示p指向的内容+1
  • sizeof()中,输入数组名arr,计算整个数组的大小。

5.2 指针-指针

通过上述,我们可以明确:

指针1 + 整数 = 指针2

以此推理出

整数 = 指针2 - 指针1

类比“日期 - 日期”,得到之间的天数。两个指针相减的结果是它们之间的元素个数,常用于计算字符串长度等场景

  • strlen()求字符串长度,统计字符串\0之前字符个数
  • 数组名arr是数组首元素的地址。arr等价于&arr[0]

模拟实现strlen()函数:

  • 方法1 计数器
int my_strlen(char* s)
{int cnt = 0; //计数器while(*s != '\0'){cnt++;str++}return cnt; // 计算两个指针之间的元素个数,即字符串长度
}
int main()
{printf("%d\n", my_strlen("abc")); //输出3return 0;
}
  • 方法2 指针 - 指针
int my_strlen(char* s)
{char* p = s;while (*p != '\0')p++;return p - s; // 计算两个指针之间的元素个数,即字符串长度
}
int main()
{printf("%d\n", my_strlen("abc")); //输出3return 0;
}

注意:

  • 指针 - 指针 的前提时两个指针指向同一块空间!

例如:

int main()
{int arr[10] = {0};char ch[10] = {'0'};printf("%d\n",&ch[0] - &arr[0]);//errreturn 0;
}
  • "日期 + 日期"没有意义,同样的,“指针 + 指针”也没有任何意义。

5.3 指针的关系运算

指针与指针比较大小,其实就是地址与地质比较大小。

数组随下标变大,地址由低变高。

int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};int* p = &arr[0];int sz = sizeof(arr) / sizeof(arr[0]);while (p < arr + sz) // 指针的大小比较,当p指向的地址小于arr+sz(相当于数组最后一个元素的地址)进入循环{printf("%d ", *p);p++;}//输出 1 2 3 4 5 6 7 8 9 10return 0;
}

六、野指针

野指针就是指针指向内容是不可知的(不正确、随机、没有明确限制)

6.1 野指针成因

  • 指针未初始化:局部变量指针未初始化时,其值是随机的。
int main()
{ int *p;//此时p是局部变量,指针未初始化,默认为随机值 *p = 20;return 0;
}
  • 指针越界访问:指针指向的范围超出数组等申请的内存空间。
int main()
{int arr[10] = {0};int *p = &arr[0];int i = 0;for(i=0; i<=11; i++){//当指针指向的范围超出数组arr的范围时,p就是野指针*(p++) = i;}return 0;
}
  • 指针指向的空间释放:返回局部变量的地址,该局部变量的空间在函数调用结束后会被释放。
int* test()
{int n = 100;return &n;
}int main()
{int*p = test();printf("%d\n", *p);//此时test()调用完成,栈帧被销毁, 内存被释放return 0;
}

6.2 如何规避野指针

  • 指针初始化:明确指向时直接赋值地址,否则赋值NULLNULL是值为0的标识符常量,该地址无法使用,读写地址也会报错)。
int main()
{int* p = NULL;*p = 20//err
}
  • 小心指针越界:不访问超出申请内存范围的空间。
  • 及时置NULL并检查:指针变量不再使用时置为NULL,使用前判断是否为NULL
  • 避免返回局部变量的地址。

七、assert断言

assert.h头文件中的assert()宏用于在运行时确保程序符合指定条件,如果不符合就报错终止运行。其表达式为真时程序继续运行,为假时报错并显示相关信息。

assert(p != NULL;//确保 p为有效指针
  • assert()断言相对if语句的优点:
    • 出现错误会直接报错,指明在什么文件,哪一行
    • 无需修改代码就可以禁用assert().可以通过在#include <assert.h>前定义NDEBUG宏来禁用assert()语句.
      在这里插入图片描述
  • 缺点:
    • 引入了额外的检查,增加了程序运行时间

通常在Debug版本中使用,Release版本中禁用,以不影响程序效率。VS2022中release版会直接禁用assert

八、指针的使用和传址调用

8.1 strlen的模拟实现

strlen函数用于求字符串长度,统计的是字符串中\0之前的字符个数。模拟实现如下:

#include <stdio.h>
#include <assert.h>
size_t my_strlen(const char* str)//限制内容,不能被修改
{int count = 0;assert(str); // 确保 str不为NULLwhile (*str){count++;str++;}return count;
}
int main()
{int len = my_strlen("abcdef");printf("%zd\n", len);return 0;
}

求出的长度不可能是负数,因此返回值类型使用size_t(无符号整型)更合适,打印应使用zd%作为占位符。

8.2 传值调用和传址调用

  • 传值调用:实参传递给形参时,形参创建临时空间接收实参。形参和实参是独立的两个空间。形参只是实参的一份临时拷贝,对形参的修改不影响实参。

  • 传址调用:将变量的地址传递给函数,函数内部通过地址间接操作主调函数中的变量,可实现对变量的修改。

例如交换两个整型变量的值,使用传址调用:

#include <stdio.h>
void Swap(int* px, int* py)
{int tmp = 0;tmp = *px;*px = *py;*py = tmp;
}
int main()
{int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交换前:a=%d b=%d\n", a, b);Swap(&a, &b);printf("交换后:a=%d b=%d\n", a, b);return 0;
}

运行结果:
在这里插入图片描述

指针是C语言的精华,掌握好指针能让我们在编程中更加得心应手。希望本文能帮助大家更好地理解指针的相关知识,后续还会有更深入的探讨。

http://www.xdnf.cn/news/16758.html

相关文章:

  • 集成电路学习:什么是Wi-Fi无线保真度
  • Java优雅使用Spring Boot+MQTT推送与订阅
  • 使用LangChain构建法庭预定智能体:结合vLLM部署的Qwen3-32B模型
  • Accessibility Insights for Windows 使用教程
  • dubbo应用之3.0新特性(响应式编程)(2)
  • JVM 崩溃(Fatal Error)解决方法
  • C++与C#实战:FFmpeg屏幕录制开发指南
  • Rust基础-part8-模式匹配、常见集合
  • 前端学习日记(十五)
  • 利用对称算法及非对称算法实现安全启动
  • 《剑指offer》-算法篇-位运算
  • 【术语扫盲】MCU与MPU
  • [CSP-J 2022] 逻辑表达式
  • 【C++算法】76.优先级队列_前 K 个高频单词
  • 【VOS虚拟操作系统】未来之窗打包工具在前端资源优化中的应用与优势分析——仙盟创梦IDE
  • Java奖客富翁系统:注册登录抽奖全实现
  • 小程序视频播放,与父视图一致等样式设置
  • Python爬虫01_Requests第一血获取响应数据
  • 【Python】数据可视化之聚类图
  • logtrick 按位或最大的最小子数组长度
  • Apache Ignite 的对等类加载(Peer Class Loading, P2P Class Loading)机制
  • 快速了解逻辑回归
  • 6、微服务架构常用十种设计模式
  • PLC如何进行远程维护远程上下载程序?
  • QT项目 -仿QQ音乐的音乐播放器(第三节)
  • 基于dcmtk的dicom工具 第九章 以json文件或sqlite为数据源的worklist服务(附工程源码)
  • Qt 移动应用性能优化策略
  • 复现cacti的RCE(CVE-2022-46169)
  • TDengine 中 TDgpt 异常检测的机器学习算法
  • Leetcode——41. 缺失的第一个正数