你好,C++(40)7.1 一切指针都是纸老虎:彻底理解指针
第7章 C++世界的奇人异事
在武侠小说中,初入武林的毛头小子总是要遇到几位奇人,发生几件异事,经过高人的指点,经历一番磨炼,方能武功精进,从新手成长为高手。在C++世界,同样有诸多的奇人异事。在C++世界中游历学习的我们,是否也同样期望着遇到几位奇人,经历几件异事,而后从一个C++新手成长为C++高手呢?
武林中的奇人异事可遇而不可求,但是C++世界中的奇人异事却可以为你一一引见。
7.1 一切指针都是纸老虎:彻底理解指针
C++世界中什么最难?指针!C++世界中什么最强?指针!
指针作为C++世界中一种特殊的访问数据的方式,因为其使用方式的灵活而使得它在C++世界中显得威力无比;然而,也正是因为它的灵活,使得它成为初学者最难掌握的C++技能。它就像一只吊睛白额“大老虎”,虽然威力无比但是难以掌握控制,用好了可以方便高效地解决问题,但如果使用不当却又很可能给程序带来灾难性的后果。今天就来打倒指针这只“纸老虎”,彻底掌握控制指针。
7.1.1 指针的运算
从本质上讲,指针也是一种数据,只不过这种数据有点特殊而已。我们通常所见的数据就是各种数值数据文字数据等,而指针所表示的是内存地址数据。既然是数据,那么自然就涉及到了数据的运算。像普通数据一样,指针也可以参与部分运算,包括算术运算、关系运算和赋值运算,而我们最常用的就是指针的算术加减运算。
如果指针的值是某个内存位置的地址值,那么我们就说指针指向这个内存位置。而指针的加减运算,实际上是让指针的指向发生偏转,指向另外的内存位置。通过这种指针的偏转,可以灵活地访问到该指针起始位置附近的内存。如果这种偏移是在某个范围内连续发生的话,则可以通过指针访问到某一连续内存区域的数据。例如,在3.6节中介绍过数组,数组名实际上就是数组数据所在内存区域的首地址,表示数组在内存中的起始位置。可以通过把首地址赋值给指针,然后对该指针进行加减运算,使指针发生偏转指向数组中的其他元素,从而遍历整个数组。例如:
int nArray[] = { , , }; // 定义一个数组
int* pIndex = nArray; // 将数组的起始地址赋值给指针pIndex
cout<<"指针指向的地址是:"<<pIndex<<endl; // 输出指针指向的地址
cout<<"指针所指向的数据的值是:"<<*pIndex<<endl; // 输出这个位置上的数据 pIndex++; // 对指针进行加运算,使其指向数组中的下一个值
cout<<"指针指向的地址是:"<<pIndex<<endl; // 输出指针指向的地址
cout<<"指针所指向的数据的值是:"<<*pIndex<<endl; // 输出数据
这段程序执行后,可以得到这样的输出:
指针指向的地址是:0016FA38
指针所指向的数据的值是:1
指针指向的地址是:0016FA3C
指针所指向的数据的值是:2
从输出结果中可以看到,pIndex指针初始指向的地址是0016FA38,也就是nArray这个数组的首地址。换句话说,也就是pIndex指向的是数组中的第一个数据,所以输出“*pIndex”的值是1。而在对指针进行加1运算后,指针指向的地址变为0016FA3C,它向地址增大的方向偏移了4个字节,指向了数组中的第二个数据,输出“*pIndex”的值自然也就变成了2。
这里大家肯定会奇怪,对指针进行的是加1的运算,怎么指针指向的地址却增加了4个单位?这是因为指针的加减运算跟它所指向的数据的真正数据类型相关,指针加1或者减1,会使指针指向的地址增加或者减少一个对应的数据类型的字节数。比如以上代码中的pIndex指针,它可以指向的是int类型的数据,所以它的加1运算就使地址增加了4个字节,也就是一个int类型数的字节数。同样的道理,对于可以指向char类型数据的char*类型指针,加1会使指针偏移1个字节;而对于可以指向double类型数据的double*类型指针,加2会使指针偏移16(8*2)个字节。指针偏转流程如图7-1所示。
图7-1 指针运算引起的指针10:43:4010:43:41偏转
除了指针的加减算术运算之外,常用到的还有指针的关系运算。指针的关系运算通常用“==”或“!=”来判断两个相同类型的指针是否相等,也就是判断它们是否指向同一地址上的同一数据,以此作为条件或循环结构中的条件判断语句。例如:
int nArray[] = { , , }; // 定义一个数组
int* pIndex = nArray; // 将数组的起始地址赋值给指针pIndex
int* pEnd = nArray + ; // 计算数组的结束地址并赋值给pEnd
while( pIndex != pEnd ) // 在while的条件语句中判断两个指针是否相等,
// 也就是判断当前指针是否已经偏转到结束地址
{
cout<<*pIndex<<endl; // 输出当前指针指向的数据
// 对指针进行加1 运算,
// 使其偏移到下一个内存位置,指向数组中的下一个数据
++pIndex;
}
在以上这段代码中,利用表示数组当前位置的指针pIndex跟表示结束位置的指针pEnd进行相等与否的比较,如果不相等,则意味着pIndex尚未偏移到数组的结束位置,循环可以继续对pIndex进行加1运算,使其偏移至下一个位置指向数组中的下一个元素;如果相等,则意味着pIndex正好偏移到数组的结束位置,while循环已经遍历了整个数组,循环可以结束。
另外,指针变量也常和nullptr关键字进行相等比较,来判断指针是否已经被初识化而指向正确的内存位置,也就是判断这个指针是否有效。虽然我们提倡在定义指针的同时就完成对它的初始化,可有时在定义指针的时候,并没有合适的初始值可以赋给它,但如果让它保持最开始的随机值,又会产生不可预见的结果。在这种情况下,我们会在定义这个指针的同时将这个指针赋值为nullptr,表示这个指针还没有被初始化,处于不可用的状态。而等到合适的时候,再将真正有意义的值赋值给它来完成这个指针的初始化,这时指针的值将不再是nullptr,也就意味着这个指针处于可用的状态。所以,将nullptr跟某个指针进行相等比较,是判断这个指针是否可用的常用手段。下面是一个典型的例子:
int* pInt; // 定义一个指针,这时的指针是一个随机值,指向随机的一个内存地址
// 将指针赋值为nullptr,表示指针还没有合适的值,处于不可用的状态
pInt = nullptr; //… int nArray[] = {};
pInt = nArray; // 将数组首地址赋值给指针
if( nullptr != pInt ) // 判断指针是否已经完成初始化处于可用状态
{
// 指针可用,开始使用指针访问它指向的数据
}
因为通过指针可以直接访问它所指向的内存,所以对尚未初始化的指针的访问,有可能带带来非常严重的后果。而将指针与nullptr进行相等比较,可以有效地避免指针的非法访问。虽然在业务逻辑上这不是必须的,但这样做可以让我们的程序更加健壮,所以这也是一条非常好的编程经验。
7.1.2 灵活的void类型和void类型指针
C++是一种强类型的语言,其中的变量都有自己的数据类型,保存着与之相应类型的数据。比如,一个int类型的变量可以保存数值1,而不能保存数值1.1,它需要一个与之相应的double类型的变量来保存。相应数据类型的变量保存相应的数据,本来相安无事过的好好的。但是,在C++世界中却出现了一个异类,那就是void类型。从本质上讲,void类型并不是一个真正的数据类型,我们并不能定义一个void类型的变量。void更多的是体现一种抽象,在程序中,void类型更多的是用于“修饰”和“限制”一个函数。例如,如果一个函数没有返回值,则可用void作为这个函数的返回值类型,代替具体的返回值数据类型;如果一个函数没有形式参数列表,也可用void作为其形式参数,表示这个函数不需要任何参数。
跟void类型对函数的“修饰”作用不同,void类型指针作为指向抽象数据的指针,它可以成为两个具有特定类型的指针之间相互转换的桥梁。众所周知,在用一个指针对另一个指针进行赋值时,如果两个指针的类型相同,那么可以直接在这两个指针之间进行赋值;如果两个指针的类型不同,则必须使用强制类型转换,把赋值操作符右边的指针类型转换为左边的指针类型,然后才能进行赋值。例如:
int* pInt; // 指向整型数的指针
float* pFloat; // 指向浮点数的指针
pInt = pFloat; // 直接赋值会产生编译错误
pInt = (int*)pFloat; // 强制类型转换后进行赋值
但是,当使用void类型指针时,就没有类型转换的麻烦。void类型指针显得八面玲珑,任何其他类型的指针都可以直接赋值给void类型指针,例如:
void* pVoid; // void类型指针
pVoid = pInt; // 任何其他类型的指针都可以直接赋值给void类型指针
pVoid = pFloat;
虽然任何类型的指针都可以直接赋值给void类型指针,但这并不意味着void类型指针也可以直接赋值给其他类型的指针。要完成这个赋值,必须经过强制类型转换,让“无类型”变成“有类型”。例如:
pInt = (int*)pVoid; // 通过强制类型转换,将void类型指针转换成int类型指针
pFloat = (float*)pVoid; // 通过强制类型转换,将void类型指针转换成float类型指针
虽然通过强制类型转换,void类型指针可以在其他类型指针之间自由转换,但是,这种转换应当遵循一定的规则,void类型指针所转换成的其他类型,必须与它所指向的数据的真实类型相符。比如把int类型指针赋值给void类型指针,那么这个void类型指针指向的就是int类型数据,这时如果再把这个void类型指针强制转换成double类型指针并通过它访问它所指向的数据,那么很可能得到错误的结果。因为void类型指针对它所指向的内存数据类型并没有要求,所以它可以用来代表任何类型的指针,如果函数可以接受任何类型的指针,那么应该将其参数声明为void类型指针。例如内存复制函数:
void* memcpy(void* dest, const void* src, size_t len);
在这里,任何类型的指针都可以作为参数传入memcpy()函数中,这也真实地体现了内存操作函数的意义,因为它操作的对象仅仅是一片内存,而不论这片内存上的数据是什么数据类型。如果memcpy()函数的参数类型不是void类型指针,而是char类型指针或者其他类型指针,那么在使用其他类型的指针作为参数调用memcpy()函数时,就需要进行指针类型的转换以适应它对参数类型的要求,纠缠于具体的数据类型,这样的memcpy()函数明显不是一个“纯粹的、脱离低级趣味的”内存复制函数。
最佳实践:11:06:42指针类型的转换
虽然指针类型的转换可能会带来一些不可预料的麻烦,但在某些特殊情况下,例如,需要将某个指针转换成函数参数所要求的指针类型,以达到调用这个函数的目的时,指针类型的转换就成为一种必需。
在C++中,可以使用C风格的强制类型转换进行指针类型的转换。其形式非常简单,只需要在指针前的小括号内指明新的指针类型,就可以将指针转换成新的类型。例如:
int* pInt; // int*类型指针
float* pFloat = (float*)pInt; // 强制类型转换成float*类型指针
在这里,我们通过在int类型指针pInt之前加上“(float*)”而将其强制转换成了一个float类型指针。虽然这种强制类型转换的方式比较直接,但是却显得非常粗鲁。因为它允许我们在任何类型之间进行转换,而不管这种转换是否合理。另外,这种方式在程序语句中很难识别,代码阅读者可能会忽略类型转换的语句。
为了克服C风格类型转换的这些缺点,C++引进了新的类型转换操作符static_cast。在C风格类型转换中,我们使用如下的方式进行类型转换:
(类型说明符)表达式
现在,使用static_cast应该写成这样:
static_cast<类型说明符>(表达式)
其中,表达式是已有的旧数据类型的数据,而类型说明符就是要转换成的新数据类型。在使用上,static_cast的用法与C风格的类型转换的用法相似。例如,两个int类型的变量相除时,为了让结果是比较精确的小数形式,我们需要用类型转换将其中一个变量转换为double类型。如果用C风格的类型转换,可以这样写:
int nVal1 = ;
int nVal2 = ;
double fRes = ((double)nVal1)/nVal2;
如果用static_cast进行类型转换,则应该这样写:
double fRes = static_cast<double>(nVal1)/nVal2;
使用C++风格的类型转换,不论是对代码阅读者还是对程序都很容易识别。我们应该在代码中尽量避免进行类型转换,但如果类型转换无可避免,那么使用C++风格的类型转换在一定程度上既可增加代码的可读性,也是对类型转换损失的一种补偿。
你好,C++(40)7.1 一切指针都是纸老虎:彻底理解指针的更多相关文章
- 指针的引用(*&)与指针的指针(**)
指针的引用(*&)与指针的指针(**) 在下列函数声明中,为什么要同时使用*和&符号?以及什么场合使用这种声明方式? void func1( MYCLASS *&pBuildi ...
- (转)指针的引用(*&)与指针的指针(**)
本文转载而来,转载出处:http://www.cppblog.com/doing5552/archive/2010/09/28/127994.html 在下列函数声明中,为什么要同时使用*和& ...
- 深入理解C语言-深入理解指针
关于指针,其是C语言的重点,C语言学的好坏,其实就是指针学的好坏.其实指针并不复杂,学习指针,要正确的理解指针. 指针是一种数据类型 指针也是一种变量,占有内存空间,用来保存内存地址 指针就是告诉编译 ...
- 一封来自恶魔的挑战邀请函,那些你见过或者没见过的C语言指针都在这里了
前言 相信大多数的同学都是第一门能接触到语言是C/C++,其中的指针也是比较让人头疼的部分了,因为光是指针都能专门出一本叫<C和指针>的书籍,足见指针的强大.但如果不慎误用指针,这些指针很 ...
- 不可或缺 Windows Native (18) - C++: this 指针, 对象数组, 对象和指针, const 对象, const 指针和指向 const 对象的指针, const 对象的引用
[源码下载] 不可或缺 Windows Native (18) - C++: this 指针, 对象数组, 对象和指针, const 对象, const 指针和指向 const 对象的指针, con ...
- c++中多态性、dynamic_cast、父类指针、父类对象、子类指针、子类对象
c++多态性是依靠虚函数和父类指针指向子类对象来实现的.简单来说,父类中定义虚函数,父类指针指向子类对象,父类指针调用函数时调用的就是子类的函数. 父类没有定义虚函数,父类指针指向子类对象时,父类指针 ...
- Android系统智能指针的设计思路(轻量级指针、强指针、弱指针)
本博客为原创,转载请注明出处,谢谢. 参考博文:Android系统的智能指针(轻量级指针.强指针和弱指针)的实现原理分析 C++中最容易出错的地方莫过于指针了,指针问题主要有两类,一是内存泄露,二是无 ...
- C++二维数组、指针、对象数组、对象指针
项目中用到,随手记一下: 1.二维数组.与指针 创建二维数组指针的方式: a.已知一维的大小 1 int **array=new int *[rows]; 2 (for int i=0;i<ro ...
- [C++]数组指针,数组引用,函数指针
数组指针是指一个指向数组的指针,例如有一个数组指针p指向一个数组a[],则 *p是取到这个数组,也就是说 *p=a,因此 **p =a[0], 它的定义为: ]; ]=&a; (*c)表示它是 ...
随机推荐
- Android中SharedPreferences使用方法介绍
一.Android SharedPreferences的简介 SharedPreferences是一种轻型的Android数据存储方式,它的本质是基于XML文件存储key-value键值对数据,通常用 ...
- Linux2.6内核--内存管理(2)--区
由于硬件的限制,内核不能对所有的页一视同仁.有些页位于内存中的特定物理地址上,所以,不能将其用于一些特别的任务.(关于内存分页机制可以查看:http://blog.csdn.net/dlutbruce ...
- Gvim一些基本配置
介绍一些关于Gvim(windows 7 32位 Vim 7.4)的基本配置,除了特别说明,代码一律添加在安装目录下的_vimrc文件中. 1.取消自动备份,这行代码需要添加在 _vimrc文件中的b ...
- Oracle存储过程 --3
Oracle存储过程包含三部分:过程声明,执行过程部分,存储过程异常. Oracle存储过程可以有无参数存储过程和带参数存储过程. 一.无参程序过程语法 1 create or replace pro ...
- 服务端调用js:javax.script
谈起js在服务端的应用,大部分人的第一反应都是node.js.node.js作为一套服务器端的 JavaScript 运行环境,有自己的独到之处,但不是所有的地方都需要使用它. 例如在已有的服务端代码 ...
- kvm编译安装及常见问题解决
一.KVM的编译安装 1.安装基本系统和开发工具 1.1 编译内核 mkdir /root/kvm cd /root/kvm wget http://www.kernel.org/pub/linux/ ...
- 关于node升级到7.0,无法gulp alljs的问题
http://stackoverflow.com/questions/40308623/cannot-find-module-internal-fs-afer-upgrading-to-node-7 ...
- JavaBean的boolean isXXX反序列化问题
JavaBean规范中规定boolean的getter/setter 为isXXX/setXXX,包装类Boolean的getter/setter 为getXXX/setXXX,其中XXX为变量名(I ...
- Android RadioGroup Fragment Viewpager FragmentPagerAdapter 去哪网Fragment嵌套
RadioGroup中的各个选择器 <selector xmlns:android="http://schemas.android.com/apk/res/android"& ...
- [转] 学习HTML/JavaScript/PHP 三者的关系以及各自的作用
1.What is HTML? When you write a normal document using a word processor like Microsoft Word/Office, ...