本专栏目的
- 更新C/C++的基础语法,包括C++的一些新特性
前言
- 指针是C/C++的灵魂,和内存地址相关联,运行的时候速度快,但是同时也有很多细节和规范要注意的,毕竟内存泄漏是很恐怖的
- 指针打算分三篇文章进行讲解,基本涵盖了常见的用法和注意事项
- 制作不易,欢迎收藏+点赞+关注,本人会持续更新
文章目录
初识指针
地址/存储单元
字节(Byte):计算机将内存分成一格一格,每一格用来存储一个数字,每一格对应的专业术语叫字节。
地址(Address):计算机给内存中的每个字节都指定一个唯一的编号,编号从0开始,后续字节编号依次加1.
存储区(Buffer):计算机将1字节或多个连续的字节形成一个存储单元,简称存储区,又称缓存区。
首地址(Base Address):又称起始地址,存储区中第一个字节的地址用来当存储区的首地址,又称基地址。
原则:任何程序访问内存前先分配。(操作系统负责分配)
我们原先学过的变量、数组、函数等都是放在内存中的,我们可以通过名字去使用变量、数组、函数等,但实际运行时,系统使用得是内存地址,而不是变量名,变量名只是方便我们程序员使用的。
怎么获得变量的地址呢?
-
&符号,这个符号就是获取变量的地址的符号。
int age = 18; //输出age变量的地址 printf("addr:%p\n",&age);
注意:
- **连续性:**每个内存单元之间地址是连续的
- 唯一性:在同一台机器上每个内存单元的地址是唯一的
- **随机性:**每次运行程序,变量的地址不一定一样,这是由操作系统随机分配的
指针
定义
指针描述了数据在内存中的位置,标示了一个占据存储空间的实体,在 C/C++语言中,指针一般被认为是指针变量,指针变量的内容存储的是其指向的对象的首地址,指向的对象可以是变量,数组,函数等占据存储空间的实体。
总的来说:
- 指针实际上是一种特殊的数据类型,用来存储的首地址
- 一种特殊的数据类型,用来描述数据在内存中的位置
指针定义如下:
int *p;
int *p1,*p2,*p3;
同时定义多个指针变量时,每个标识符前面都要加*号
,否则后面的会被定义成int型变量。
指针大小
指针作为一个变量是有大小的,其大小在32位平台是4个字节,64位平台上是8个字节,大小与指针的类型无关。
案例如下:
printf("<char*> size is %llu byte\n", sizeof(char*));
printf("<int*> size is %llu byte\n", sizeof(int*));
printf("<double*> size is %llu byte\n", sizeof(double*));
printf("<char**> size is %llu byte\n", sizeof(char**));
printf("<int**> size is %llu byte\n", sizeof(int**));
结果如下:
<char*> size is 8 byte
<int*> size is 8 byte
<double*> size is 8 byte
<char**> size is 8 byte
<int**> size is 8 byte
通过分析发现:
- 指针大小都是 8 byte,与指针类型无关(在x64平台是8字节)
指针使用
既然指针保存的是对象的地址,那么如何通过指针操作对象呐?
&:
取地址符。用于获取变量所在的首地址*:
间接访问运算符,也叫作解引用运算符。用于获取地址对应的值。
这两个运算符的优先级一样,结合起来使用也非常简单!(如果不理解,最好加上括号;如:*(&p)),我们来看一下这个案例:
int a = 520;
int* p = &a;
printf("%d, %p, %p, %p\n", *&a, &*p, *&p, &a);
结果:
520, 0000001B416FFAB4, 0000001B416FFAB4, 0000001B416FFAB4
分析:
- &a:变量a的地址,*:解应用,解(&a),所以输出还是a
- *p:解引用,输出变量a,&:取地址,即输出a的地址
- &p:指针p的地址,*:解引用,即输出指针变量p,也就是a的地址
- &a:很明显,就是输出a的地址
使用指针的好处:
- 直接访问硬件
- 快速传递数据(指针表示地址)
- 返回一个以上的值,返回一个(数组或者结构体的指针)
- 方便处理字符串
万能指针
万能指针:即void* 类型的指针,因为它既可以保存任意
类型的地址,还可以再任意
类型的指针
类型之间进行转换
-
可以指向任何类型地址
int maye = 20; void* p = &maye;
-
可以隐式自动转换为其他类型指针
int* pi = p;
-
**不能对void*取值操作,因为它没有类型,**或者说不能判断存储的是什么类型,需要强转指定一个确定的类型才能使用
printf("%d\n",*p); //error printf("%d\n",*(int*)p);//right
指针的特殊状态
我们在使用指针的时候,总是会遇到各种稀奇古怪的问题,但万变不离其宗,下面是一些指针常见的问题:
野指针
野指针(wild pointer)就是没有被初始化过的指针。
【示例:】
#include<stdio.h>
int main()
{
int *p;
printf("%d\n",*p); // 没有初始化,那他就是随机的
return 0;
}
如果用Vs编译,会直接报错error C4700: 使用了未初始化的局部变量“p”
,还是比较人性的,从根本上避免了野指针。没有初始化,那他就是随机的,又因为指针和内存相挂钩,所以随机内存是很危险的。
空指针
空指针就是被赋值为NULL的指针,它不指向任何的对象或者函数。(坚决不能使用空指针,否则程序就会崩)
空指针的出现是为了避免错误的引用指针而导致的难以排查的问题,不过空指针也不能直接访问,但是可以用来判断。
【示例:】
#include<stdio.h>
int main()
{
int* p = NULL;
//判断指针是否为NULL
if (p != NULL)
{
printf("%d\n", *p);
}
return 0;
}
如果把指针值为空,则可以进行判断,就算没有判断,直接对空指针进行引用,产生的报错也非常好理解。
悬空指针
悬空指针是指针**最初指向的内存已经被释放了的一种指针。 **
【示例:】从函数中返回临时变量的地址
#include<stdio.h>
int* foo()
{
int age = 18; //函数执行完毕,age的内存会被自动释放
return &age;
}
int main()
{
int* p = foo();
//getchar();
printf("%d\n", *p);
return 0;
}
运行上面的代码,貌似没有任何问题,的确如此,但不代表这个代码是正确的。那是因为释放内存需要时间,现在我们把main函数中的getchar的注释放开
,然后重新运行程序,等待几秒之后按下任意键,发现输出的结果已经不对了,因为这个时候内存已经释放了
**注意:**悬空指针是编码过程中最容易出现问题的,切记,认真检查!
指针的运算
算数运算:
-
指针是存储的地址,地址本质就是一个整数,因此,我们可以对指针执行四种算术运算:++、–、+、-(其他运算没有意义),代表内存位置的移动。
char* pc = NULL; printf("%p %p\n",pc,pc+1); //0 1 int* pi = NULL; printf("%p %p\n",pi,pi+1); //0 4 double* pd = NULL; printf("%p %p\n",pd,pd+1); //0 8
-
总结:
- 指针的每一次递增,它其实会指向下一个元素的存储单元。
- 指针的每一次递减,它都会指向前一个元素的存储单元。
- 指针在递增和递减时跳跃的字节数(步长)取决于指针所指向变量数据类型长度,比如 int 就是 4 个字节
- 不同类型的指针所占内存大小都是一样的(32位计算机4个字节,64位8个字节)
关系运算:
- 指针可以用关系运算符进行比较,如 ==、< 和 >。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。
- 总结:
- 对相关变量的指针进行比较,才有意义
- 大于小于常用在数组中,全等一般是判断指针是否为NULL
指针的类型
指针使用来间接访问内存的,那么就需要知道指针指向的是什么样的数据类型,应该如何解析它。
所以我们需要一个特定类型的指针变量来存放特定类型变量的地址。
- int* -> int
- char* - > char
- double* -> double
- int* - >char 错误!错误!错误!
为什么这么麻烦呢?
因为,我们不仅仅使用指针来存储内存地址;同时也是用它来解引用那些地址的内容,这样我们就可以访问和修改这些地址对应的值了,故需要指定指针的类型。
地址分析
不同的数据类型有不同的大小,char 1个字节,int 4个字节,double 8个字节。不仅仅是大小方面的差异,这些变量或数据类型在存储信息的方式上也有所不同。
上图所示,如果是int类型,那么最高位表示符号位
,剩下的31个位用来存储值,接下来让我们探讨一下几种情况:
-
现在如果声明一个指针p指向整型变量,然后用取地址符把a的地址存放在p中,然后打印p,p的值会是多少呢?0x200,也就是byte0的地址,也就是说整型变量a的起始地址是0x200;
-
如果我们想知道那个地址的内容(值),就使用*p去解引用这个地址。然后编译器看到之后就会觉得没有问题,p是一个指向整型的指针,因此我们需要看4个字节。从地址0x200开始,编译器就知道如何去提取一个整型数据。所以,它从这四个字节中得到了1025这个值。
-
如果p是一个字符类型的指针,那么在解引用的时候编译器只会看一个字节,因为一个字符类型只有一个字节。如果p是一个浮点型指针,尽管浮点型也是4个字节,但是浮点数4字节的信息表示缺与整型不一样。打印出来的值也会是不一样的。
const 与 指针
const是constant的简写,只要一个变量前面用const来修饰,就意味着该变量里的数据可以被访问,不能被修改。也就是说const意味着“只读”。任何修改该变量的尝试都会导致编译错误。const是通过编译器在编译的时候执行检查来确保实现的,也就是说const类型的变量不能改是编译错误,不是运行时错误。
所以我们只要想办法骗过编译器,就可以修改const定义的常量,而运行时不会报错。
const与指针可以搭配出三种不同的含义:
-
指针指向的内存不可修改,但指针的指向可以修改
int age = 18; const int* ptr = &age; //常量指针 int const* ptr = &age; //和上面一句是等价的 *ptr = 20; //err,不能修改 ptr = NULL; //ok,可以修改指向
-
指针的指向不可以修改,但指向的内存可以修改
int* const ptr = &age; *ptr = 20; //ok ptr = NULL; //err
-
指针的指向不可以修改,指向的内存也不可以修改
const int* const ptr = &age; *ptr = 20; //err ptr = NULL; //err
常量指针(指向常量的指针
)是指指向常量的指针,顾名思义,就是指针指向的是常量,即,它不能指向变量,它指向的内容不能被改变,不能通过指针来修改它指向的内容,但是指针自身不是常量,它自身的值可以改变,从而指向另一个常量。
指针常量(指针是常量
)是指指针本身是常量。**它指向的地址是不可改变的,但地址里的内容可以通过指针改变。**有一点需要注意的是,指针常量在定义时必须同时赋初值。
typedef陷阱(易错)
示例:
typedef char* IntPtr;
const IntPtr p;
const IntPtr
相当于 const char*
” 呢?还是char* const
呢?
- 答案是相当于
char* const
,原因很简单,typedef 是用来定义一种类型的新别名的,它不同于宏,不是简单的字符串替换。 - 因此,
const IntPtr
中的 const 给予了整个指针本身常量性,也就是形成了常量指针char* const
(一个指向char的常量指针),而不是const char*
(指向常量 char 的指针)。 - 当然,要想让 const IntPtr相当于 const char* 也很容易。如下面的代码所示:
typedef const int* const_IntPtr;
const_IntPtr p;
还值得注意的是,虽然 typedef 并不真正影响对象的存储特性,但在语法上它还是一个存储类的关键字,就像 auto、extern、static 和 register 等关键字一样。因此,像下面这种声明方式是不可行的:
typedef static int Static_Int; //错误
不可行的**原因是不能声明多个存储类关键字,由于 typedef 已经占据了存储类关键字的位置,**因此,在 typedef 声明中就不能够再使用 static 或任何其他存储类关键字了。
指针做函数参数
学习函数的时候,讲了函数的参数都是值拷贝,在函数里面改变形参的值,实参并不会发生改变。
如果想要通过形参改变实参的值,就需要传入指针了。
注意: 虽然指针能在函数里面改变实参的值,但是函数传参还是值拷贝。**不过指针虽然是值拷贝,但是却指向的同一片内存空间,**即拷贝指针
指针做函数返回值
返回指针的函数,也叫作指针函数。
和普通函数一样,只是返回值类型不同而已,先看一下下面这个函数:
int fun(int x,int y);
接下来看另外一个函数声明
int* fun(int x,int y);
这样一对比,发现所谓的指针函数也没什么特别的。
注意:
- 不要返回临时变量的地址
- 可以返回动态申请的空间的地址
- 可以返回静态变量和全局变量的地址
大、小端模式
- 大端(Big-endian)和小端(Little-endian)是什么?
- 在计算机业界,Endian表示数据在存储器中的存放顺序
- 大端模式:数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中
- 这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这种存放方式符合人类的正常思维
- 小端模式:数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中
- 这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。
- 总结:采用大小模式对数据进行存放的主要区别在于在存放的字节顺序,大端方式将高位存放在低地址,小端方式将高位存放在高地址。采用大端方式进行数据存放符合人类的正常思维,而采用小端方式进行数据存放利于计算机处理。