C语言指针

本文详细介绍了物理内存、虚拟内存的工作原理,以及指针、指针变量、野指针和数组在编程中的应用。通过实例展示了内存寻址、虚拟内存扩展和指针操作的重要性,同时强调了避免野指针和正确使用指针数组的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 物理内存和虚拟内存

1.1 物理内存

实际存在的具体存储器芯片。

  1. 主板上装插的内存条
  2. 显示卡上的显示RAM芯片
  3. 各种适配卡上的RAM芯片和ROM芯片

可供CPU一次性访问的最大的物理内存空间,CPU中集成的地址线可以直接进行寻址的内存空间大小。在没有虚拟内存概念时,程序寻址用的都是物理地址。程序能寻址的范围是有限的,范围取决于CPU的地址线的条数。
例如:32位机器存在32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);那么32根地址线产生的地址就会是:

0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 00001

1111 1111 1111 1111 1111 1111 1111 1111

寻址的范围是2^32也就是4GB。
同样的方法,64位CPU,64根地址线,可自行计算其地址空间大小。

1.2 虚拟内存

虚拟内存,虚拟内存是一种内存管理技术,是虚拟的、逻辑上存在的存储空间。即利用你电脑的硬盘,产生一个比有限的实际内存空间大许多的、逻辑上存在的虚拟空间。以便能够有效地支持多个程序系统的实现和大型程序运行的需要。

在一个32位机器上,一个进程在运行时会默认得到4G的虚拟内存。这个虚拟内存你可以这样认为,每个进程都认为自己得到了或者说拥有了4GB的空间,这只是这个进程自己的想法,实际上,这4G虚拟内存,可能只对应很少一部分的物理内存,其余部分都是由磁盘产生的虚拟内存,实际用了多少内存就会对应分配多少物理内存。
进程得到的这4G虚拟内存是一个连续的地址空间(这是进程所认为的),实际上,这4GB空间,通常是被分隔为多个物理内存碎片,还有一部分是存储在硬盘上的,在需要时进行数据交换。
这些碎片组成的一个空间,被进程认为是连续的,并由CPU地址线进行编址。32位机器一共32根地址线,每根地址线分为高低电平,也就是我们通常所说的0和1,这样也就产生了2^32种不同的情况,对应到内存空间上,就是可以对4GB内存空间进行操作,编址。

地址空间:对存储器编码的范围。我们在软件上常说的内存是指这一层含义。

  1. 编码:对每个物理存储单元(一个字节)分配一个号码
  2. 寻址:可以根据分配的号码找到相应的存储单元,完成数据的读写
  3. 一个32位机器对进程所获得的4GB虚拟内存进行编址,32根地址线根据低高电平可以产生232种不同的编号,这些编号对应从内存低位到高位的一共232个地址,对应4GB的内存空间,每个地址对应1字节的内存单元。

0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 0001

1111 1111 1111 1111 1111 1111 1111 1111

由于二进制编码方式较长且复杂,为了更好的表示地址,计算机通常采用十六进制方式进行地址编码:
在这里插入图片描述
一个最小的内存单元存储空间为一个字节,对应8个比特位,

  • 对于32位系统,对于一个地址,所对应的大小是32位 & 4个字节。
  • 对于64位系统,对于一个地址,所对应的大小是64位 & 8个字节。

2. 指针和指针变量

2.1 指针

1.指针是内存中一个最小单元的编号,也就是地址,一个变量的地址就称为该变量的指针
2.平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
总结:指针就是地址,口语中说的指针通常指的是指针变量。
编码 -> 地址 ->指针

2.2 指针变量

指针变量的类型
变量都有不同的类型:字符型、整形、浮点型等,那指针变量有没有类型呢?看下述代码:

int num = 10;
p = &num

& 为取地址操作符,我们将 int型 变量num的地址保存到变量 p 中,那 p 就是一个指针变量,那指针变量 p 的类型就是 p 所指向的变量的类型,也就是 int型。
创建指针:type *指针变量名;

int *p = NULL;

int *类型的指针表示 该指针变量内存放的是 int型 变量的地址。
我们知道,在创建 int型 变量时,内存会给该变量分配4个字节的内存空间,而一个字节对应一个内存空间,4个字节就对应4个字节空间,如下图所示,
在这里插入图片描述
通常来讲,该变量的寻址地址为,该变量所占内存空间的低地址,也就是分配给该变量的由低到高的地址中的第一个地址。
我们可以通过对一个变量使用取地址操作,取出的就是该变量在虚拟内存空间中的低位地址,将该地址存放到另外的变量中,这个变量就是指针变量。

#include<stdio.h>
int main()
{
	int a = 0; //内存中分配一块4个字节大小的或者说分配四个内存单元给a
	int* pa = &a; // &a —— 取出a所占用的内存空间最低地址
                  // 将取出的地址,赋给 pa ,这个pa就是一个指针变量。
	printf("%p\n", &a);
	printf("%p\n", pa);  // %p 表示打印指针变量/地址

	return 0;
}

指针变量,就是存放变量地址的变量
指针变量的类型为所指向变量的类型
存放在指针变量中的值,都会被当做地址处理
指针变量/地址的大小,在32位系统中为4个字节,64位系统中为8个字节。

为什么会有指针

//对主函数中的a和b的值进行交换

#include<stdio.h>

void Swap(int n, int m)
{
	int tmp = n;
	n = m;
	m = tmp;
}

int main()
{
	int a = 27;
	int b = 35;

	Swap(a, b);

	printf("a = %d\n", a);
	printf("b = %d\n", b);

	return 0;
}

//输出:
//a = 27
//b = 35

通过上述实例的输出结果我们发现,主函数中的a和b的值并没有发生变化。

通过对函数的理解我们知道,函数分为实参和形参,函数参数传递也分为值传递和址传递,当进行值传递时,形参是实参的一份临时拷贝,也就是说,形参会在内存中得到一块内存空间用于存放实参所传递的值,而形参所拥有的内存空间和实参的内存空间是没有联系的,所以对函数内部的形参进行操作时,只是对形参的内存空间里面的值进行了操作没有波及到实参内存空间。上述实例使用的就是值传递,才会输出没有变化。

在当我们函数传参时,如果传输的数据很小,使用值传递,那么在内存中再创建一份临时拷贝,也不会占用多少内存空间,速度也不会很慢,但是当我们需要传输一个具有很多元素的数组、或者结构体等呢?那么就会造成空间上和时间上的极大浪费。所以这时候就用到了址传递。

解引用操作符
解引用操作符又称为间接访问操作符,习惯上称为解引用操作符。

#include<stdio.h>

void swap(int*, int*);
 
int main()
{
	int a = 10;
	int b = 20;
 
	printf("交换前:a=%d b=%d\n", a, b);
	swap(&a, &b);
	printf("交换后:a=%d b=%d\n", a, b);
 
	return 0;
}

void swap(int* a, int* b)
{
	int t = *a; 
	*a = *b; 
	*b = t;
}
//输出结果:
//a = 35
//b = 27

这次函数传参使用的是址传递,&a和&b,取出a和b的地址传给函数。
上文我们讲到,取出地址要用指针变量进行接收存放地址,所以函数创建了两个int*类型的指针变量,进行接收&a和&b所取出的地址。就类似于:

    int a = 27;
	int b = 35;

    int* n = &a;
    int* m = &b;

既然已经知道了变量的地址,那么我们就可以直接对地址所在的内存空间进行操作,而这就需要我们的解引用操作符。

现在指针变量n中存储的是整型a在内存中的地址,*a可以让我们通过存储的地址直接找到地址所对应的那块内存空间。

    int a = 10;
    int* pa = &a;
    *pa = 20;

在这里插入图片描述
pa中存储的是a的地址,*pa就是通过pa中存放的地址,找到内存中对应这个地址编号的一块空间,并直接对空间进行操作。这就是解引用操作符。

所以对于上访的实例,就是直接通过地址对内存空间进行操作,交换主函数中a和b的值。

指针变量的大小
指针变相是用来存放地址的
指针变量的大小取决于一个地址存放的时候需要多大的空间
32位系统上的地址:32bit位 - 4 byte,指针变量的大小是4个字节
64位系统上的地址:64bit位 - 8 byte,指针变量的大小是8个字节

#include <stdio.h>

int main()
{
    printf("%d\n", sizeof(char*));
    printf("%d\n", sizeof(short*));
    printf("%d\n", sizeof(int*));
    printf("%d\n", sizeof(float*));
}
 

野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

  1. 指针未初始化
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
  1. 指针越界访问
    就比如,对于一个数组,只有十个元素,而我非要去访问这个数组的第十一个元素,这就是数组越界访问。
    指针也是,当指针指向的地址不在数组所拥有的内存空间范围时,指针就成为了野指针。
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = arr;
int i = 0;
for(i=0; i<=10; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
  1. 指针指向的空间释放
    局部变量的作用域是有限的,当出了变量所在的局部范围,变量就自动销毁了,其所分配的空间就还给内存了。这个时候,如果主函数内部有指针指向这个局部变量销毁之前所指向的地址,那么局部变量自动销毁之后,这个指针就变成了野指针。
#include<stdio.h>

int* test()
{
	int a = 10;
	return &a;
}
int main()
{
	int *p = test();

	printf("hehe\n");

	printf("%d\n", *p);

	return 0;
}

如何规避野指针

  • 指针初始化
  • 小心指针越界
  • 指针指向空间释放,及时置NULL
  • 避免返回局部变量的地址
  • 指针使用之前检查有效性
#include <stdio.h>
int main()
{
int *p = NULL;
//....
int a = 10;
p = &a;
if(p != NULL)
{
*p = 20;
}
return 0;
}

指针与数组

指针和数组是不同的对象
指针是一种变量,用于存放地址的,大小为4个字节或8个字节
数组是一组相同类型元素的集合,数组的大小取决于元素类型以及元素个数。
数组名和数组首元素的地址是一样的,

#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放的是数组首元素的地址

研究指针与数组之间的关系,代码如下

#include <stdio.h>
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; i++)
{
printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p+i);
}
return 0;
}

/*
&arr[0] = 000000000061FDE0 <====> p+0 = 000000000061FDE0
&arr[1] = 000000000061FDE4 <====> p+1 = 000000000061FDE4
&arr[2] = 000000000061FDE8 <====> p+2 = 000000000061FDE8
&arr[3] = 000000000061FDEC <====> p+3 = 000000000061FDEC
&arr[4] = 000000000061FDF0 <====> p+4 = 000000000061FDF0
&arr[5] = 000000000061FDF4 <====> p+5 = 000000000061FDF4
&arr[6] = 000000000061FDF8 <====> p+6 = 000000000061FDF8
&arr[7] = 000000000061FDFC <====> p+7 = 000000000061FDFC
&arr[8] = 000000000061FE00 <====> p+8 = 000000000061FE00
&arr[9] = 000000000061FE04 <====> p+9 = 000000000061FE04
*/

通过指针访问数组,代码实现如下。

int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i<sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}

二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?这就是二级指针,对指针进行套娃。
一级指针,创建一个指针变量用于存放一个普通类型变量的地址。

int a = 10;
int* pa = &a;  // 其中pa是一级指针变量。

而指针变量也是变量,是变量在创建是就会分配内存空间,所以一个指针变量在内存中也是有一块内存空间的。那么也就存在相应的地址编号。

int a = 10;
int* pa = &a;  // 其中pa是一级指针变量。

int** ppa = &pa;  //ppa是二级指针变量

在这里插入图片描述

a的地址存放在一级指针变量pa中,pa的地址存放在二级指针变量ppa中。
解引用ppa通过存储在ppa内的地址,找到pa,再对pa进行解引用,找到a。

int a; 变量a的类型是int类型
int * pa ,这个* 表示这个pa是一个指针变量,int表示指针pa存储的地址指向的内存空间中int类型的变量
int ** ppa ,第二个* 表示,pa是一个指针变量,int* 表示ppa存储的地址指向的内存空间中int* 类型的指针变量。

指针数组

指针数组是指针还是数组?
答案:是数组。是存放指针的数组。
数组我们已经知道整形数组,字符数组。

int arr1[5];
char arr2[6];

在这里插入图片描述
那指针数组是怎样的?

int* arr3[5];//是什么?

arr3是一个数组,有五个元素,每个元素是一个整形指针。
在这里插入图片描述
引用:https://blog.youkuaiyun.com/2201_75314884/article/details/127992565

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值