C语言——关于指针(逐渐清晰版)

为了更好地理解本篇文章的知识内容,读者可以将以下文章作为补充知识进行阅读 :
C语言------------原码 补码 反码 (超绝详细解释)-CSDN博客

C语言------------二、八、十、十六进制的相互转换-CSDN博客

C语言------------斐波那契数列的理解和运用-CSDN博客

目录

[1. 内存和指针(地址)](#1. 内存和指针(地址))

[1.1 内存的介绍](#1.1 内存的介绍)

1.1.1内存的划分

[1.2 指针和地址](#1.2 指针和地址)

[2. 指针变量和地址](#2. 指针变量和地址)

[2.1 取地址操作符&](#2.1 取地址操作符&)

[2.2 指针变量](#2.2 指针变量)

[2.2.1 指针变量的定义](#2.2.1 指针变量的定义)

[2.2.2 指针变量的类型](#2.2.2 指针变量的类型)

[2.2.3 指针变量的大小](#2.2.3 指针变量的大小)

[2.3 解引用操作符*](#2.3 解引用操作符*)

[​2.4 指针变量类型的意义](#2.4 指针变量类型的意义)

[3. 指针计算](#3. 指针计算)

[3.1 指针+-整数](#3.1 指针+-整数)

[3.2 指针-指针](#3.2 指针-指针)

[3.3 指针的关系运算](#3.3 指针的关系运算)

4.野指针

[4.1 野指针的成因和解决方法](#4.1 野指针的成因和解决方法)

[4.1.1 指针未初始化](#4.1.1 指针未初始化)

[4.1.2 指针越界访问](#4.1.2 指针越界访问)

[4.1.3 指针指向的空间释放](#4.1.3 指针指向的空间释放)

[5. void指针和assert断言](#5. void指针和assert断言)

[5.1 void指针](#5.1 void指针)

[5.2 assert断言](#5.2 assert断言)


1. 内存和指针(地址)

1.1 内存的介绍

在计算机中,有各种各样的数据,他们的存储需要在内存中划分空间,计算机中的内存空间大小是有限的。如果把数据比作水,内存就是用以承载水的容器,而我们知道在生活中容器的大小都是有限的。因此我们可以 更好地理解内存之于数据的意义。

1.1.1内存的划分

一个整型变量a= 10存储在程序中需要占据4个字节的内存空间大小,而数据的单位是多种多样的,那我们在内存中应该按照何种单位进行空间划分呢?

为了内存空间的高效管理,内存被划分为一个个的内存单元,而每个内存单元的大小为1字节。

其中,**一个bit位可以存储一个二进制的0或1,一个字节可以存储8个bit位,即一个内存单元能存储8个bit位。**​

在内存中存储的数据需要通过CPU的处理,那么CPU又是如何读取这些数据的呢?

1.2 指针和地址

我们打个比方,当我们在入住一个酒店时,服务员会给我们对应的 房号和房卡,这样我们就能快速找到对应的房间。CPU和内存中的数据传输也是同样的道理,他们之间通过很多的地址总线进行连接,每根线只有两态,表示0或1(联想到二进制),那么通过地址总线不同的脉冲组合形成的这样一个二进制数字,就是对应数据的地址编码,即地址。

​在C语言中,我们将这样的地址起名为指针。

所以我们可以理解为:

内存单元的编号 == 地址 == 指针

2. 指针变量和地址

2.1 取地址操作符&

我们在学习scanf函数时知道,scanf函数除格式化字符串以外的参数代表的都是地址。

当我们在创建变量的时候,他会向内存申请空间,我们想知道他具体的地址编号时就需要用到操作符**&**,示例如下:

如图创建的整型变量a,通过查看内存,我们知道他的地址即指针为0x00000099588FFB14-0x00000099588FFB17(x64 环境下),共四个字节;但如果我们对a的地址进行打印的话(x86环境下,更加便于查看),结果又是怎样的呢?

我们会发现,他只打印了一个地址编号,这是因为一个数据进行存储时,他的内存空间都是连续的,打印的往往是最低 的那个地址编号,进而根据数据的内存大小,从低往高访问对应数据。

经过多次尝试我们会发现,每一次变量的地址都是在发生变化的,这是因为在每次运行程序时,操作系统的内存分配情况存在差异,所以分配给变量的具体内存地址是不同的。

2.2 指针变量

2.2.1 指针变量的定义

那么通过取地址操作符&得到的地址我们又该将他存储在哪呢?为了方便提取这些指针的数据,C语言中用指针变量作为他的容器。如:

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

此时的pa就是一个指针变量,而他的类型为int *

指针变量也是⼀种变量 ,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址

在C语言中,地址就是指针,指针就是地址。

2.2.2 指针变量的类型

由上我们知道的一种指针变量类型为int *,我们应该怎么去理解他呢?

我们单独看int * pa = &a这段语句可以知道,a为整型变量,pa存储的是a的地址。由此知道:

int代表pa存储的指针所指向的数据a的类型(整型),* 表明pa为指针变量。

a和pa分别都在内存中划分了属于他们自己的空间。

那么字符类型的变量a,他的地址又该放在上面类型的指针变量中呢?

我们可以进一步推导如下:

cpp 复制代码
int main()
{
	char a = '2';
	char* pc = &a;//字符指针pc,类型为char *
	return 0;
}
2.2.3 指针变量的大小

在介绍内存中,我们知道地址的编号是由地址总线输出的信号转换得到的,32位机器假设有32根地址总线,他们产生的二进制序列作为一个地址,那么一个地址就是32个bit位,需要4个字节的存储空间,指针变量的大小就是 4个字节。
同理64位机器,假设有64根地址线,一个地址就是64个二进制位组成的二进制序列,存储起来就需要 8个字节的空间,指针变量的大小就是8个字节。

我们可以输出如下的代码进行测试:

cpp 复制代码
#include <stdio.h>
//指针变量的⼤⼩取决于地址的⼤⼩
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)
int main()
{
 printf("%zd\n", sizeof(char *));
 printf("%zd\n", sizeof(short *));
 printf("%zd\n", sizeof(int *));
 printf("%zd\n", sizeof(double *));
 return 0;
}

在x86环境下,即32位操作系统

在x64环境下,即64位操作系统

结论:

**1.**32位平台下地址是32个bit位,指针变量大小是4个字节

2. 64位平台下地址是64个bit位,指针变量大小是8个字节

**注:**指针变量的大小和类型是无关的,同样指针类型的变量,在相同平台下,大小都是相同的

2.3 解引用操作符*

那么对于指针变量,他们应该如何使用呢?这里我们将介绍一个关键的操作符------解引用操作符*

他相当于是一把钥匙,指针变量是对应的地址,指针变量指向的数据相当于被存储在对应的地址,但我们无法直接操作他,因此需要通过钥匙打开这道壁垒,这样我们在不直接使用数据变量时,也能对数据进行相应的操作。示例如下:

cpp 复制代码
#include <stdio.h>
int main()
{
 int a = 100;
 int* pa = &a;
 *pa = 0;//找到变量a,并通过*打开操作他的权限
 return 0;
}

我们会发现,通过解引用,*pa就相当于变量a ,我们能够对他进行重新赋值

​2.4 指针变量类型的意义

由2.1中我们知道,指针变量会存储数据空间中最小的地址编号,整型指针变量解引用时,他会向上访问四个字节的内存空间,我们思考一下,如果我们使用字符指针变量对&a进行访问,能得到正确的数据么?

在**int ***整型指针下,我们打印读取的整型变量数据是正确的(十六进制11223344转为十进制为287454020‬)

当我们使用char *字符指针对整型变量n进行读取时,我们发现,他仅读取了n内存空间中的一个内存单元,数据为十六进制的44,转为十进制为68。

在这里我们可以发现,不同的指针变量所访问的内存空间大小 也是不一样的,因此学习指针变量的类型也是十分关键的。

注:在进行地址存储时,指针变量的类型应该和地址的类型相对应,在代码中我们可以看到**(char*)&n** ,由于&n的类型为int * ,我们用char * 的指针变量接收他,两者类型不同 ,为了使指针变量pc能够顺利存储n的地址,我们需要对&n进行强制转换 ,如果不进行强制转换,编译器会发出警告。(编译器会进行隐式转换类型

结论:指针的类型决定了对指针解引⽤的时候有多大的权限(⼀次能操作几个字节)。

如:char* 的指针解引用就只能访问⼀个字节,而int* 的指针的解引用就能访问四个字节。

3. 指针计算

3.1 指针+-整数

我们观察如下代码的运行结果:

我们可以发现,整型指针变量+1,他的地址跳过了4个字节;字符指针变量+1,他的地址跳过了1个字节。

指针+1,就是跳过1个指针指向的元素。指针可以+1,那也可以-1。

跳过一个指针的空间大小就取决于指针的类型

3.2 指针-指针

此运算的前提条件

  • 参与减法运算的两个指针必须指向同一数组中的元素(或数组最后一个元素的下一个位置)
  • 两个指针必须指向相同类型的数据(即指针变量类型一致)

运算结果

结果是一个ptrdiff_t类型(在<stddef.h>中定义的有符号整数类型),表示两个指针所指向元素之间的元素个数差,而不是字节数差。

我们观察如下代码:

cpp 复制代码
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int* p1 = &arr[9];
	printf("%d\n", (int)(p1 - p));//p1 - p为ptrdiff_t类型,%d无法读取,需要强制转换
	return 0;
}

指针求差他们的结果就是两个指针之间内存的元素个数,如下图,两个箭头之间的元素有1,2,3,4,5,6,7,8,9。共9个整型元素,故输出9。

3.3 指针的关系运算

我们知道内存的地址编码是从低到高依次排布的,因为指针是可以用来比较大小的。

常见的关系运算符包括:== 、!= 、< 、> 、<= 、>= 。

这些关系符构建的指针关系运算可以作为语句的判断条件

指针的关系运算常用于数组遍历内存区间判断。

cpp 复制代码
int arr[5] = {1,2,3,4,5};
int *p;

// 遍历数组:当p未超过数组最后一个元素时继续循环
for (p = &arr[0]; p < &arr[5]; p++) {
    printf("%d ", *p);
}

4.野指针

概念: 野指针就是指针指向的位置是不可知的( 随机的不正确的没有明确限制的
当一个程序存在野指针时,野指针的行为具有不确定性

  1. 可能立即触发程序崩溃(如段错误)
  2. 可能暂时正常运行,但在后续操作中引发错误
  3. 可能修改无关内存区域,导致数据损坏或程序逻辑错误
  4. 可能触发安全漏洞,被用于缓冲区溢出等攻击

4.1 野指针的成因和解决方法

4.1.1 指针未初始化
cpp 复制代码
#include <stdio.h>
int main()
{ 
 int *p;//局部变量指针未初始化,默认为随机值
 *p = 20;
 return 0;
}

局部变量p未进行初始化,此时的p就是野指针。

解决方法
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL。NULL相当于给指针变量了一个固定的地方,不再"野"。
NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。

4.1.2 指针越界访问
cpp 复制代码
#include <stdio.h>
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;
}

当指针指向的范围超出数组arr的范围时,p就是野指针 。
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。

4.1.3 指针指向的空间释放
cpp 复制代码
#include <stdio.h>
int* test()
{
 int n = 100;
 return &n;
}
int main()
{
 int*p = test();
printf("%d\n", *p);
 return 0;
}

当程序运行函数test()结束后,由于n为局部变量,他在内存中所占的空间就会被销毁,导致指针p无法指向具体的变量,成为了野指针。

解决方法

  1. 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性

  2. 如造成野指针的第3个例⼦,不要返回局部变量的地址。


持续更新中

5. void指针和assert断言

5.1 void指针

5.2 assert断言

打怪升级中.........................................................................................................................................

相关推荐
草莓熊Lotso31 分钟前
【数据结构初阶】--二叉树(二)
c语言·数据结构·经验分享·其他
Villiam_AY2 小时前
Redis 缓存机制详解:原理、问题与最佳实践
开发语言·redis·后端
UQWRJ2 小时前
菜鸟教程R语言一二章阅读笔记
开发语言·笔记·r语言
岁忧4 小时前
macOS配置 GO语言环境
开发语言·macos·golang
朝朝又沐沐5 小时前
算法竞赛阶段二-数据结构(36)数据结构双向链表模拟实现
开发语言·数据结构·c++·算法·链表
魔尔助理顾问5 小时前
系统整理Python的循环语句和常用方法
开发语言·后端·python
Ares-Wang5 小时前
JavaScript》》JS》 Var、Let、Const 大总结
开发语言·前端·javascript
遇见尚硅谷6 小时前
C语言:*p++与p++有何区别
c语言·开发语言·笔记·学习·算法
SkyrimCitadelValinor6 小时前
c#中让图片显示清晰
开发语言·c#
艾莉丝努力练剑6 小时前
【数据结构与算法】数据结构初阶:详解排序(二)——交换排序中的快速排序
c语言·开发语言·数据结构·学习·算法·链表·排序算法