指针
内存
物理内存与虚拟内存
物理内存,CPU中集成的地址线可以直接进行寻址的内存空间大小。在没有虚拟内存概念时,程序寻址用的都是物理地址。程序能寻址的范围是有限的,范围取决于CPU的地址线的条数。
例如:32位机器存在32根地址线,寻址的范围是2^32也就是4GB。
虚拟内存,虚拟内存是一种内存管理技术,是虚拟的、逻辑上存在的存储空间。即利用你电脑的硬盘,产生一个比有限的实际内存空间大许多的、逻辑上存在的虚拟空间。以便能够有效地支持多个程序系统的实现和大型程序运行的需要。
为什么会需要虚拟内存?
在一个32位机器上,一个进程在运行时会默认得到4G的虚拟内存。这个虚拟内存你可以这样认为,每个进程都认为自己得到了或者说拥有了4GB的空间,这只是这个进程自己的想法,实际上,这4G虚拟内存,可能只对应很少一部分的物理内存,其余部分都是由磁盘产生的虚拟内存,实际用了多少内存就会对应分配多少物理内存。
进程得到的这4G虚拟内存是一个连续的地址空间(这是进程所认为的),实际上,这4GB空间,通常是被分隔为多个物理内存碎片,还有一部分是存储在硬盘上的,在需要是进行数据交换。
这些碎片组成的一个空间,被进程认为是连续的,并由CPU地址线进行编址。32位机器一共32根地址线,每根地址线分为高低电平,也就是我们通常所说的0和1,这样也就产生了2^32种不同的情况,对应到内存空间上,就是可以对4GB内存空间进行操作,编址。
地址
一个32位机器对进程所获得的4GB虚拟内存进行编址,32根地址线根据低高电平可以产生232种不同的编号,这些编号对应从内存低位到高位的一共232个地址,对应4GB的内存空间,每个地址对应1字节的内存单元。
00000000000000000000000000000000
00000000000000000000000000000001
00000000000000000000000000000010
...
00001111111111111111111111111111
00010000000000000000000000000000
00010000000000000000000000000001
...
11111111111111111111111111111111
看到上面的二进制编号,我想你应该不会喜欢这种方式,所以为了更好的观感,以及更好的编写,通常采用十六进制方式,进行显示。例如下图中的显示编号(地址)。
并且对于32位机器,32根地址线对应着32个比特位,一个字节对应8个比特位,而对于一个地址(编号),所对应的就是4个字节8个比特位。
指针是什么?
对于指针我们可以通过两个方面进行理解:
- 指针是内存中的一个最小单元的编号,通过上节,我们可以得知其实就是指地址
- 对于我们平时口中所讲述的指针,通常指的是指针变量,指针变量是用来存放内存地址的变量。
地址
在第一节中的内存中,我们得知一个32位机器在一个进程中可以一次操控4G的内存空间,并对这4GB的连续内存空间进行编码,得到2^32种编号,这个编号就是内存地址,一个编号(地址)对应一个内存单元。
指针,是内存中最小单元的编号,而内存中最小单元指的就是内存单元,内存单元的编号指的就是上述的2^32种编号,所以我们可以这样说:编号-地址-指针,这三种其实所指相同。
指针变量
在前面,我们学过许多种操作符,其中有个取地址操作符(&)以及解引用操作符(*)(又称为间接访问操作符)。
我们知道在创建一个变量时,当程序执行到创建变量,那么相应的会在内存空间内分配一个内存空间给这个变量,用于存储这个变量的值。
如果对一个变量使用取地址操作符,也那么就是取出这个变量的地址,通过之前的章节我们知道,例如一个int类型的变量,在创建变量时,内存会分给这个变量4个字节的空间大小,而一个字节对应着一个内存空间,对应一个内存地址,而四个字节就对应四个内存地址,而通常来说,我口中的变量的地址指的是这个变量所占内存空间的低地址,也就是分配给这个变量的由低到高的地址中的第一个地址。
而当我们对一个变量使用取地址操作符时,取出的就是这个对应内存空间的第一个内存单元的地址。
如果我们取出这个变量的地址之后把它存放在另一个变量中,那么这个变量就是指针变量。
#include<stdio.h>
int main()
{
int a = 0; //内存中分配一块4个字节大小的或者说分配四个内存单元给a
int* pa = &a; // &a —— 取出a所占用的内存空间最低地址
// 将取出的地址,赋给 pa ,这个pa就是一个指针变量。
printf("%p\n", pa);
return 0;
}
总结
指针变量,就是用来存放地址的变量。存放在指针中的值都会被当成地址处理。
指针变量是用来存放地址的,一个地址编号是唯一标识一个内存单元的。
指针的大小在32位平台上是4个字节,在64位平台上是8个字节。
指针和指针类型
指针类型
我们在之前都有或多或少的了解过变量,我们应该知道,变量具有类型,在创建变量时,我们有许多种变量类型选择,例如int(整型)、char(字符型)、float(单精度浮点型)、double(双精度浮点型)等。
我们上面说到,指针变量,它也是一种变量,那么指针变量也是有类型的。
我们在上文有看到这样的一段代码。
#include<stdio.h>
int main()
{
int a = 0;
int* pa = &a;
return 0;
}
在代码中取出a的地址存放在pa中,pa是一个指针变量,这个指针变量的类型如普通变量一样,变量名的前面就是其类型,所以指针变量pa的类型是int*
。
相对于普通的变量类型,指针变量的类型只是在类型后加上了*
号,这个星号可和我们之前了解的解引用操作符不是一个作用(含义),这个星做作用是,与星号前面的类型进行结合,表明这个类型是一个指针类型,这个指针类型所定义的变量是指针变量。*
的个数表示这个指针的级数。
举一反三我们可以得知:
char* pc = NULL; //char*类型的指针用于存放char类型变量的地址
short* ps = NULL; //short*类型的指针用于存放short类型变量的地址
int* pi = NULL; //int*类型的指针用于存放int类型变量的地址
long* pl = NULL; //long*类型的指针用于存放long类型变量的地址
float* pf = NULL; //float*类型的指针用于存放float类型变量的地址
double* pd = NULL; //double*类型的指针用于存放double类型变量的地址
定义指针
而定义一个指针变量的语法格式是:
type* name = NULL;
其中type位类型,name是你要创建的指针变量的变量名。
要注意的是,当你创建一个指针变量时,你需要给其进行初始化,如果你不知道或者说暂时不需要赋值,你可以先赋值为NULL。绝对不允许不进行初始化创建指针变量,不初始化会出现野指针,这是很危险的行为。
NULL在C语言中你可以理解为空指针,是计算机内存中保留的值。虽然C语言标准没有明确指出NULL空指针与指向内存地址为
0x00000000
的指针相同,但是在实际情况中,基本就是这样。
另外虽然在初始化时可以赋给指针变量NULL,但是解引用空指针,这种操作是不允许存在的,是C语言为定义的行为。
指针类型的意义
- 指针类型决定了,在对指针进行解引用时,可以访问的内存大小,可以访问多少个字节大小的内存空间。或者说可以对多大的内存空间进行操作。
- 指针类型决定了指针的步长。
为什么会有指针?
通过上面的学习我们知道,指针其实就是地址,指针变量就是存放地址的一种变量类型。那么指针有什么用呢?明明我们可以直接对普通变量类型进行操作,为什么还要通过指针进行操作呢?
下面我们通过一个简单例子来讲述一个浅显的作用:
//对主函数中的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* 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 = 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的值。
解引用可以访问多大内存空间?
我们知道对于一个int类型的变量,它所占的内存空间的大小为4个字节,而一个char类型,所占内存空间为1
个字节。
如果我们用一个char*
类型的指针变量对一个int类型的变量进行操作会发生什么呢?
我们看到下面一段代码,在不使用编译器进行打印时,你觉得输出结果是什么呢?
#include <stdio.h>
int main()
{
int a = 0x11223344;
char* pc = (char*)&a;
*pc = 0;
printf("%x\n", a);
return 0;
}
代码中的0x11223344
是一个十六进制数字,因为方便书写,并且编译器内显示的就是十六进制,也更好理解。将这么一个数存储在内存中是怎么存储的呢?用一个char*
类型的指针对其赋值,最后又是什么结果呢?
通过在vs编译器内存监视窗口,我们可以发现0x11223344
在内存中是这样存储的,当然这只是为了方便才显示为16进制,实际上内存中仍然是二进制。
我们可以看到内存中最低位存储为44,最高位存储为11,这是因为大小端字节序存储方式,至于什么是大小端下面会详细说明。
这是整型类型a在内存中的存储情况,一个地址对应一个字节的内存空间,而pc
是char*
类型的指针变量,我们知道char类型是一个字节,那么对应在指针变量中,它可以操作的内存空间也应该是一个字节,而&a
,取出的是a的低地址,所以*pc=0;
只能把整型所对应的4个字节的内存空间中的一个进行操作。
输出结果:11223300
char* 类型的指针,解引用访问1个字节
short* 类型的指针,解引用访问2个字节
int* 类型的指针,解引用访问4个字节
long* 类型的指针,解引用访问8个字节
float* 类型的指针,解引用访问4个字节
double* 类型的指针,解引用访问8个字节
总结:
指针类型决定了,在对指针进行解引用时,可以访问的内存大小,可以访问多少个字节大小的内存空间。或者说可以对多大的内存空间进行操作。
大小端字节序
字节序
字节序又称端序或尾序,在计算机领域中,指电脑内存中或在数字通信链路中,占用多个字节的数据的字节排列顺序。
字节的排列方式有两个通用规则:
大端序(Big-Endian)将数据的低位字节存放在内存的高位地址,高位字节存放在低位地址。这种排列方式与数据用字节表示时的书写顺序一致,符合人类的阅读习惯。
小端序(Little-Endian),将一个多位数的低位放在较小的地址处,高位放在较大的地址处,则称小端序。小端序与人类的阅读习惯相反,但更符合计算机读取内存的方式,因为CPU读取内存中的数据时,是从低地址向高地址方向进行读取的。
在内存中存放整型数值168496141 需要4个字节,这个数值的对应的16进制表示是0X0A0B0C0D,这个数值在用大端序和小端序排列时的在内存中的示意图如下
为什么会出现大小端呢?
当往内存中存放数据的时候,我们有很多种存放的顺序,正着放,反着放,随机存放等等等等,例如我们往内存中存放0x11223344
,这只是演示。
如果是你让你从上述片段中快速的按顺序取出数据,你会选择哪种方法呢?所以就出现了大小端排序。
过这个大小端,为什么叫大小端而不是叫别的,据说是因为发明者当时在看格列夫游记,看到其中两个国家因为争论吃鸡蛋应该先从大头剥还是小投剥,发动了战争,而获得启发,起名大小端。
本小段大小端引用文章:https://zhuanlan.zhihu.com/p/352145413
步长
指针类型决定了指针的步长。
#include <stdio.h>
int main()
{
int n = 10;
int* pi = &n;
char* pc = (char*)&n;
printf("%p\n", &n); //010FF8F4
printf("%p\n", pi); //010FF8F4
printf("%p\n", pi+1); //010FF8F8 +4
printf("%p\n", pc); //010FF8F4
printf("%p\n", pc + 1); //010FF8F5 +1
return 0;
}
仔细看上述代码,我们取出了整型变量n
的地址分别存放在int*
和char*
类型的指针变量里面,然后在之后的程序中,我们都对指针变量pi
和pc
进行了加一操作之后,然后再打印,结果却大不相同。
经过计算我们的知,int*
类型的指针变量中存储的地址偏移了4个字节,而char*
类型的指针变量中存储的地址偏移了一个字节,而这就是步长。
同时所便宜的字节大小正对应,普通类型大小,所以我们的知:
char* 类型的指针,步长1个字节
short* 类型的指针,步长2个字节
int* 类型的指针,步长4个字节
…
总结:指针的不同类型其实就是提供了不同的视角去观看和访问内存。
野指针
野指针就是指针指向的位置是不可知的、随机的、甚至没有访问权限的。
为什么会存在野指针?
- 指针未初始化
#include <stdio.h>
int main()
{
int* p; //局部变量指针未初始化。默认为随机值
*p = 10;
return 0;
}
- 指针越界访问
就比如,对于一个数组,只有十个元素,而我非要去访问这个数组的第十一个元素,这就是数组越界访问。
指针也是,当指针指向的地址不在数组所拥有的内存空间范围时,指针就成为了野指针。
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int i = 0;
for (i = 0; i <= 10; i++)
{
printf("%d ", *(p+i));
}
return 0;
}
//输出为:
//1 2 3 4 5 6 7 8 9 10 -858993460
- 指针指向的空间释放
局部变量的作用域是有限的,当出了变量所在的局部范围,变量就自动销毁了,其所分配的空间就还给内存了。这个时候,如果主函数内部有指针指向这个局部变量销毁之前所指向的地址,那么局部变量自动销毁之后,这个指针就变成了野指针。
例如以下程序:
#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
- 避免返回局部变量的地址
- 指针使用之前检查有效性
指针运算
指针 +/- 整数
通过一个小例题来理解
使用指针,将一个数组内的所有元素赋值为0
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
int* pa;
for (pa = &arr[0] ; pa < &arr[sz] ; )
{
*pa++ = 0;
}
return 0;
}
指针 - 指针
使用指针-指针,运算时,前提是两个指针需要指向同一块内存空间。例如同时指向一个数组中的不同元素。
指针-指针得到的值的绝对值是两个指针之间的元素的个数。
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%d\n", &arr[9] - &arr[0]); // 9
printf("%d\n", &arr[0] - &arr[9]); // -9
return 0;
}
指针运算关系
对于指向地址的指针,可以进行下列的运算:
(1) 对一个指针执行整数加法和减法操作。
(2) 两个指针相减。
(3) 比较两个指针。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针进行比较,但是不允许与指向数组第一个元素之前的那个内存位置的指针进行比较。
指针与数组
指针和数组是不同的对象
指针是一种变量,用于存放地址的,大小为4个字节或8个字节
数组是一组相同类型元素的集合,是可以存放错了元素的,数组的大小取决于元素类型以及元素个数。
数组名与数组首元素相同,数组名表示的是数组首元素的地址(除了两种特殊的情况)。这是我们之前所讲过的。如果还有疑惑,可以阅读我之前的一篇关于数组的文章 C浅解-数组。
那么既然数组名是数组首元素的地址,把数组首元素地址存放在一个指针变量里面是可行的,那么就可以这样写。
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *pa = arr;
pa中存放的是数组首元素的地址,通过指针运算访问整个数组,通过上面知识的学习,就变得非常简单了。
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
int i = 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] = 012FF81C <====> p+0 = 012FF81C
&arr[1] = 012FF820 <====> p+1 = 012FF820
&arr[2] = 012FF824 <====> p+2 = 012FF824
&arr[3] = 012FF828 <====> p+3 = 012FF828
&arr[4] = 012FF82C <====> p+4 = 012FF82C
&arr[5] = 012FF830 <====> p+5 = 012FF830
&arr[6] = 012FF834 <====> p+6 = 012FF834
&arr[7] = 012FF838 <====> p+7 = 012FF838
&arr[8] = 012FF83C <====> p+8 = 012FF83C
&arr[9] = 012FF840 <====> p+9 = 012FF840
我们把上述代码稍微改动一下下,就可以直接打印数组每个元素。
实现直接通过指针来访问数组
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
int i = 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);
printf("&arr[%d] = %p <====> p+%d = %d\n", i, &arr[i], i, *(p + i));
}
return 0;
}
输出结果:
&arr[0] = 012FF81C <====> p+0 = 1
&arr[1] = 012FF820 <====> p+1 = 2
&arr[2] = 012FF824 <====> p+2 = 3
&arr[3] = 012FF828 <====> p+3 = 4
&arr[4] = 012FF82C <====> p+4 = 5
&arr[5] = 012FF830 <====> p+5 = 6
&arr[6] = 012FF834 <====> p+6 = 7
&arr[7] = 012FF838 <====> p+7 = 8
&arr[8] = 012FF83C <====> p+8 = 9
&arr[9] = 012FF840 <====> p+9 = 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* *pa
,第二个*
表示,pa是一个指针变量,int*
表示ppa存储的地址指向的内存空间中存储的是int*
类型的。
指针数组
指针数组,从语文的角度分析,数组是主语,指针是修饰词。解释为:存放数组的指针。
回顾一下:数组是一组相同类型的元素的集合。那么这个类型可不可以是指针类型呢?可以。
就比如:
int* arr[3];
arr是一个数组,有三个元素,每个元素的类型是整型指针。