前言
相信很多小伙伴在日常编程中都会用到指针,所以在这里,我们再来看一看指针到底是怎样的呢,到底是怎样去执行程序的,懂得了底层原理,大家对指针将更加掌握。
1.指针与C++原理
《C++ Primer Plus》中指出“面向对象编程与传统的过程性编程的区别在于,面向对象强调的是在运行阶段进行决策,而不是编译阶段”。也说明运行阶段决策更具有灵活性,而在数组在分配内存时传统方法是需要提前申明数组的大小,int arry[3]={1,2,3},数组的长度是提前设置好的,为3,这就是编译阶段的决策,这样做固然很安全,但是如果我的数组元素多了,几千个甚至更多,我们也用这种方法,或者说我们不确定我们究竟要用多少内存,所以我们会尽可能的把内存扩大,但是这时会有一个新的问题,如果元素没那么多,那么将会浪费很多内存,当然也有人说当下无论手机还是电脑内存都可以很大,不用在乎这么小的内存,但如果两款软件应用实现的功能相同内存占用却不一样,那么用户就会选择内存较小的,这时候,我们就想,怎么样才能让开辟出来的内存恰好合适,那就是我们要推荐的面向对象编程(OPP),它使得程序更加灵活。在程序运行后,可以这次告诉它只需要20个元素,也可以告诉它下次需要多少元素。总之,OPP可以在运行阶段确定数组长度,为了使用这种方法,C++采用的是使用关键词new请求正确数量的内存以及使用我们今天主要讲的指针来跟踪新分配的内存的位置。在运行阶段做决策并不是OPP特有的,但使用C++编写这样的代码比C语言更加就简单。
1.1声明和初始化指针
通常我们会看到 int ∗ * ∗p=nullptr这样的C++语句去描述一个指针,但是你真的懂为什么吗?
这里的p是指针名,表示的是地址, ∗ * ∗运算符被称为间接值或解除引用运算符,将它用于指针p可得到该地址储存的值(C++可根据上下文判断 该符号是乘法还是解除引用)。
int是指针的类型,但是为什么一定要在指针前面指定数据类型呢?可以举例char和double,它们的使用的字节数不同,储存的内部格式也不同,所有,指针必须指定指向的数据的类型。
我们可以说p是指向int类型,还可以说p的类型是指向int的指针,有点绕,但是你仔细品。
一般的,C程序员喜欢这样:int ∗ * ∗p;这里强调 ∗ * ∗p是一个int类型的值。
而C++程序员更喜欢使用:int ∗ * ∗ p;这是强调int ∗ * ∗是一种指向int的指针。
当然在哪里添加本身对程序运行不会出问题,不过可以看出你是什么理解指针的。
这里也说出了指针变量不仅仅是指针,而且是特定类型的指针,也说明了指针都是基于其他类型的。
注意:
如果我们定义不同类型的指针,比如char ∗ * ∗p1和double ∗ * ∗p2,一般char变量是占1个字节,double是占8个字节,p1和p2是指向两个长度不一样的数据类型,但是这两个指针变量本身的长度通常是相同的(为4个字节),也就是说char的地址与double的地址的长度是相同的,这就像1000是超市的地址,1005是学校的地址,地址取决于计算机系统,而不是类型。
而nullptr是c++中常用于指针的初始化,表示空指针,c语言中常用NULL或者0表示,而在C++11中指出其的不严谨,因为0表示的是整数,而不是指针,NULL只是一个代名词,0的代名词,而对于C++,编译器会更加的小心谨慎,故用nullptr表示空指针,当然用NULL与0不会错,但是为了避免歧义,我们在编写C++的时候最好是用前者。
1.2指针的危险
对于指针的危险,它更容易发生在用指针不仔细的人身上。
1.在C++中创建指针时,计算机将分配地址的内存,但是不会分配指针数据类型的内存,为数据类型提供内存是一个独立的事件,如果你这样做:
int *p; *p=8;//错
p确实是指针,但是并没有说它指向哪里,系统随机指向的某个内存空间(野指针),可能这个地址空间上面的内容是另外一个值,这时候,你将8赋值在那个p的地址内容上,将覆盖原来的值,从而造成错误,并且这个错误是隐匿的、最难跟踪的bug,不容易被发现,所有编写程序的人应该避免这种错误。
注意:一定要在解除引用运算符之前将指针初始化一个确定的、适当的地址。
2.计算机会将地址当作整数来处理,当指针不是整形。因此,不能简单地将整数赋给指针,如int *p; p=0xB8000000;//错
0xB8000000我们认为的认为它是一个地址,但是在计算机中,我们并没有告诉系统它是一个地址,系统会将它处理为一个数字,在C99标准发布之前,C语言允许这样的赋值,但C++方面的一致严格要求,编译器会显示的报错,不过我们可以通过强制转换:
int *p; p=(int*)0xB8000000;//对
这样两边都是地址,不会报错。
注意: p是int值的地址并不意味着本身类型是int,就像char ∗ * ∗p,char类型是1个字节,而地址是4个字节。
2. new —— 分配内存
接下来,我们看看程序在运行时是什么分配内存的吧。我们把指针初始化为变量的地址,此时变量在编译时分配有名称的内存,指针为通过名称直接访问内存提供了别名,而指针的可以在运行阶段分配未命名的内存以储存值,这就是我们 最开始说的动态分配内存,只能通过指针来访问内存。在C语言中,可以用库函数malloc()来分配内存,当然C++也可以用,不过,C++中允许使用new来实现动态分配,这将更简单。
2.1 普通变量
//用传统赋值,在编译阶段
int h = 50;
int *p1 = &h;
//在运行阶段赋值
int *p2 = new int;//指明类型
*p2=100;//可以,因为p2指向的内存是已知的
我们需要哪种数据类型就分配哪种,new将找到一个长度正确的内存块,并返回该内存块的地址,此时用指针p2来接收,
∗
*
∗p2是储存在这块内存的值,用传统赋值时可以看出h是一个p指向的一个内存块的名称,而运行阶段的内存块没有名称,专有名词为“数据对象”(变量也是数据对象,但p2指向的内存不是变量)。
注意:new分配的内存与常规变量分配的内存不同,变量h和p1的值都存储在栈的内存区域中,而new分配的内存是从堆或自由存储区的内存区域来的。
不了解栈和堆的小伙伴将在以后的学习中学习到 |
2.2 delete释放内存
程序用new申请一个内存块使用完后我们要用delete手动释放申请的内存,归还的内存才可以供其他程序使用,使用了new一定要使用delete且只能使用delete来释放内存,否则会发生内存泄漏也即是被分配的内存无法使用了,严重的内存泄漏会导致你的计算机没有内存。
int *p = new int;
...
delete p;
delete释放的是p指向的内存,而不会删除p这个指针,也并不意味着使用new的指针,而是用于new的地址
int *p1 = new int;
int *p2 = p1;
delete p2;
这时还可以用p指针指向其他内存地址。不要重复释放同一个内存块,C++标准指出这样做的结果是不确定的;不能用delete来释放声明变量的内存块。
int *p1 = new int;
delete p1;
delete p;//错,重复释放
int h = 5;
int p2 = &h;
delete p2;//错
2.3 动态数组
在程序运行时选择数组的长度成为动态联编,意味着程序是在运行时创建的,这种数组也叫动态数组。
int *p = new int [10];
...
delete [] p;
int类型的指针p指向一个内存块,内存块里事先空出10个元素的内存,new运算符返回首元素的地址,该地址被赋值给指针p,程序执行完后删除这个内存块,new与delete应相对应,否则后果不确定。
总之:
1.不要使用delete来释放不是new分配的内存。
2.不要使用delete释放同一个内存块两次。
3.如果使用new[ ]为数组分配内存,则应使用delete[ ]来释放。
4.如果使用new为一个实体分配内存,则应使用delete(没有括号)来释放。
5.对空指针使用delete是安全的。
如何使用动态数组?
数组与指针基本等价是C和C++的优点之一。
#include<iostream>
using namespace std;
int mian()
{
int *p1 = new int [3];
p1[0] = 1;
p1[1] = 2;
p1[2] = 3;
cout << "p1[1]是:" << p1[1] << endl;
p1 = p1 + 1;
cout << "p1[0]是:" << p1[0] << endl;
cout << "p1[1]是:" << p1[1] << endl;
p1 = p1 - 1;
delete p1;
return 0;
}
输出结果为
p1[1]是:2
p1[0]是:2
p1[1]是:3
这是因为指针p1指向了一个含3个元素的数组的第一个元素地址,即p1[0]的地址,所以p1=p1+1(相当于加了4个字节长度)会使指针向后偏移一个这样数组元素的单位长度,也即会让p1指向第二个元素的地址。
直到恢复为原来的样子再用delete删除,便于delete找到这个动态数组的位置。
3.指针算数原理
可以看看下面这个程序将得出什么样得结论
#include<iostream>
#include<iostream>
using namespace std;
int main()
{
int a[3] = { 3,2,3 };
int* p = nullptr;
p = a;
cout << p << " " << a << " " << *p << " " << p[0] << " " << a[0] << endl;
p = p + 1;
cout << p << " " << a << " " << *p << " " << p[0] << " " << a[0] << endl;
p = &a[0];
cout << p << " " << a << " " << &a[0] << " " << p[0] << " " << a[0] << endl;
cout << "----------------" << endl;
cout << a << " " << &a << " " << &a[1] << " " << (a + 1) << " " << (&a + 1) << endl;
}
结果是
这说明数组名a是第一个元素的地址,&a是整个数组的地址,对于a+1,是第二个元素的地址,&a+1是跳过了整个数组。
不过在sizeof()库函数中却是例外,大家可以自己动手去试一试。
相信大家看完这篇介绍应该更理解指针了吧,希望能帮到你
ok,小编干饭去啦
(还是干饭…)