学习指针之前我们先来了解一下内存的基本概念
1.内存
(1).概念:
内存(Memory)是计算机的重要部件,也称内存储器和主储存器,它用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。它是外存与CPU进行沟通的桥梁,计算机中所有程序的运行都在内存中进行,内存性能的强弱影响计算机整体发挥的水平。只要计算机开始运行,操作系统就会把需要运算的数据从内存调到CPU中进行运算,当运算完成,CPU将结果传送出来。(来自百度)
简单来说:内存是一块存储空间,用于暂时存放数据的。
(2).计算机如何对内存做管理
那么计算机如何对内存做管理的呢?
答:首先将一块内存分成一个个小的内存单元,一个内存单元的空间大小是一个字节,也就是8个比特位,将每一个内存单元都编上号,就称为地址,每个内存单元都有一个地址,而CPU就是通过地址去找到内存去做数据处理的。
CPU通过三种主要的总线对内存做处理
- 控制总线:对内存进行控制,比如说要进行读写内存中数据的操作
- 数据总线:进行数据传输,比如说对内存数据进行读写
- 地址总线:通过地址来找到内存单元,比如说要在哪里读写数据
这里只要弄懂地址总线。地址总线的数目是地址线之和,那么一共有多少地址线呢?
对于x86的环境下,一个地址是用8个16进制位数字来表示的,而8个16进制位就是4*8=32个二进制位,每一个二进制位都有0/1两种,那就有2^(32)个地址,所以需要访问这2^(32)个地址,其实只需要32根地址线,每根是地址线上都有0/1(低电平/高电平)两种方式,也就是2^(32),就可以访问到任何一个地址。
而对于x64的环境下,地址就需要16个16进制数来表示,就是64个二进制位,同理就需要64根地址线。
2.指针的概念
概念:
地址就是指针,就是内存单元的编号,变量的指针就是地址,而指针变量就是用来存地址的。
打个比方:
3.指针的创建
(1).创建方式:
type_t * Pointer_variable_name;
type_t * 指针变量类型(就是普通变量类型后面加个*)
Pointer_variable_name 指针变量名字
打个比方:
//创建整形指针变量
int* p;
//创建整形变量
int a;
//其实很简单你想要创建什么类型的指针变量就跟创建普通变量差不多,就是在类型后面多加了个*号。
4.指针有关的操作符
(1).取地址操作符' & '
//取地址操作符是对普通变量进行取地址
int a = 9;//创建一个变量
int* p = &a;//取出a的地址放入指针变量p中
(2).解引用操作符' * '
//解引用操作符是对指针变量来使用,进而得到指针所指向的变量
int a = 9;//创建一个变量
int* p = &a;//取出a的地址放入指针变量p中
*p = 10;//解引用p指针,对p指向的变量做修改,*p=a=10;
5.指针的类型
(1).指针的常见类型
指针变量的类型其实和普通变量的类型差不多,就是多了个*,其实把*号遮住不看,剩下的就是指针变量的类型,比如:
int* p1;//整形指针变量,指针类型是int*
char* p2;//字符指针变量,指针类型是char*
float* p3;//浮点型指针变量,指针类型是float*
void* p4;//空指针,没有类型。
......还有函数指针,数组指针等等。
(2).常见类型的指针
int类型 4字节
int a = 1;
int* p = &a;
二进制:0000 0000 0000 0000 0000 0000 0000 0001
16进制:0 0 0 0 0 0 0 1
内存单元空间大小为1个字节,8个比特位,8个二进制数,两个16进制数。
那么,第一个00对应1个地址,第二个00对应1个地址,第三个00对应1个地址,第四个01对应1个地址。
p存的地址为第四个01对应的地址。
char类型 1字节
char b = 'a';
int* p = &a;
只有一个字节,所以只有一个地址,p存的地址为地址最低的内存单元的地址。
指针永远都是地址最低的内存单元的地址,意味着指针大小永远为存储一个内存单元的编号的大小,大小与所存变量的类型无关 。
(3).指针类型的意义
- 指针类型决定指针进行解引用操作时每一次能读取字节的个数
举例:int*和char*存相同地址,打印整形数。
可见,int*和char*不同类型一次读取的字节数不同。
4.指针的空间大小
前面我们讨论过,在x86的环境下,地址需要用32为二进制数来表示,那么把这个32位二进制数的指针变量存储起来就需要32个二进制位,就是4个字节。
注意别混淆:内存单元的空间大小是1个字节,内存单元的地址需要32位二进制才能表示完,内存单元的地址存储需要4个字节。
**********************************
不同类型的指针变量为啥空间大小相同?
int类型是4个字节 --> 需要4个内存单元 --> 4个内存单元对应4个地址
那为啥int*的指针只有1个地址呢?
*****实际上指针变量只拿取对应变量的最低位的一个内存单元的地址,剩余的3个地址编译器可以通过最低位的地址访问到*****
这就是为啥不同变量的指针变量都是4个字节,因为拿取的是最低位内存单元的地址。
int* p1 的地址是4字节中最低位1个字节的地址。
char* p2 的地址是最低位1个字节的地址。
(1).x64的环境下:
任何类型指针变量的空间大小都为8个字节
(2).x86的环境下:
任何类型指针变量的空间大小都为4个字节
5.指针的运算
(1).指针的加减运算
指针 +(-) 整数
指针的加减==指针加减sizeof(指针所指向的数据的类型)==地址加减sizeof(指针所指向的数据的类型)==地址加减指针所指向的变量类型的字节数
注意:加的是字节数,不是加的字节,int*指向变量4个字节,加的是数字4,不是4字节。
指针 +(-) 指针
指针加减指针==地址加减地址,通常进行运算的指针是同类型数组里面的元素地址,来计算两指针所指向的元素之间还差了多少个元素,比如:指针-指针
指针+指针的地址通常没什么用,根本不知道加得到的那个指针只向哪里。
(2).指针的大小比较
指针大小的比较就是地址大小的比较
常见的几种:
- 指针1>指针2,如:用于数组的比较,指针1指向的元素在前。
- 指针1==指针2,如:两个指针所指向的空间是同一块空间。
- 指针 != 0,指针 != NULL,如:判断指针是不是空指针。
6.const修饰指针
下面以int*类型指针为例:
(1)const int* p
(2)int* const p
(3)const int* const
总结:
const修饰的变量变成“ 常量 ”,这个不是真正意义上的常量,只是具有常属性的常量。
- 如果const在*p的左边修饰,那么*p(解引用p==变量)(*p具有常属性) 不能被修改,p(p==地址)的值能修改。
- 如果const在p的左边修饰,那么p(p==地址,具有常属性)的值不能被修改,*p(*p==变量)的值可以被修改
- 如果const在*p和p的左边修饰,那么*p和p的值都不能被修改
7.野指针
(1).概念:
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
(2)常见的野指针:
a.未初始化的指针:
int* p;//未初始化的指针 *p = 10;//未初始化的指针不知道指向那段空间,就不能把10存储起来,没空间可存。
b.指针越界访问:
int arr[10]={1,2,3,4,5,6,7,8,9,10}; int* p = &arr[0]; p = p + 10;//指针越界,此时*p==arr[10],超出数组,越界。
c.指针指向已经被销毁的空间:
#include<stdio.h> int* test() { int a = 666; return &a; //当函数调用完为a开辟的空间就会被销毁,那么函数返回的就是一个已经被销毁的空间的地址 int main() { int* p = test();//p是只向为一段被销毁的空间,无法使用,野指针 return 0; }
(3).规避野指针
a.指针进行初始化
//1.明确的空间 int a = 1; int* p = &a;//p明确指向a的空间 *p = 10;//对a的值进行更改
//2.不明确的空间 int* p = NULL;//当你不知道指针要指到哪里时,此时就给它置为NULL(空指针) //放任不管,那他就是个随机值,就像一条野狗,会乱咬人,更加危险
b.避免指针越界
int arr[10]={1,2,3,4,5,6,7,8,9,10}; int* p = &arr[0]; p = p + 9;//不让指针越界,此时*p==arr[9],未超出,未越界
c.避免使用指向已销毁的空间的指针
如:返回的是调用函数时创建的变量。局部变量在栈区开辟空间,当函数调用结束后 ,栈区上局部变量的内存空间就会被释放。
8.assert断言
既然存在野指针,那么我们在写程序时就需要规避野指针,这里就提供一个用于在调试过程中捕捉程序错误的函数,assert()断言函数。
(1).函数的介绍
函数原型:
函数参数:int expression //要检测的表达式
返回值:void //空,无返回值
assert断言:断定条件为真,则不需要做任何操作,断定为假,则报错,同时表明出项错误的地方。
函数所在头文件:assert.h
(2).assert断言的使用
举个简单例子:
#include<stdio.h>
#include<assert.h>
void test(int* p)
{
assert(p!=NULL);//判断传进来的是不是有效指针
...
}
int main()
{
int a = 8;
int* p = &a;
test(p);
return 0;
}
(3).NDEBUG
补充:
NDEBUG 是”No Debug“的意思,也即“非调试”。有的编译器(例如 Visual Studio)在发布(Release)模式下会定义 NDEBUG 宏,在调试(Debug)模式下不会定义定义这个宏;有的编译器(例如 Xcode)在发布模式和调试模式下都不会定义 NDEBUG 宏,这样当我们以发布模式编译程序时,就必须自己在编译参数中增加 NDEBUG 宏,或者在包含 <assert.h> 头文件之前定义 NDEBUG 宏。
调试模式是程序员在测试代码期间使用的编译模式,发布模式是将程序提供给用户时使用的编译模式。在发布模式下,我们不应该再依赖 assert() 宏,因为程序一旦出错,assert() 会抛出一段用户看不懂的提示信息,并毫无预警地终止程序执行,这样会严重影响软件的用户体验,所以在发布模式下应该让 assert() 失效。
原文链接:https://blog.csdn.net/weixin_52377137/article/details/119864975
(4).assert断言与if判断
除了使用assert断言之外,还可以写个if判断语句进行判断,两者区别就是assert更加方便,
当不想使用assert断言时用NDEBUG就可以禁用,而当不想使用if判断时还需要找if去进行删除。
此外assert断言会在断言处结束程序报错,而if判断在执行完后报错,增加了运行时间。
还有就是assert会提示报错的地方,更加清晰。
9.void*型指针
(1).概念:
void型指针是一种特殊类型的指针,它没有具体指针类型,也就意味着它可以存储任何类型变量的地址,同时也就意味着这种指针没有具体的指向,不能直接进行解引用,解引用void指针时需要将其转换为具体的类型指针,因为void指针是一种通用的指针类型,它可以指向任意类型的数据,但是在解引用时需要知道具体的数据类型才能正确地访问和操作数据。
例如:如果你有一个void指针ptr,你可以通过将其转换为int类型指针来解引用:
int *p = (int *)ptr;
int value = *p;
本期内容结束,再见!!!