第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 一切指针都是纸老虎:彻底理解指针的更多相关文章

  1. 指针的引用(*&)与指针的指针(**)

    指针的引用(*&)与指针的指针(**) 在下列函数声明中,为什么要同时使用*和&符号?以及什么场合使用这种声明方式? void func1( MYCLASS *&pBuildi ...

  2. (转)指针的引用(*&)与指针的指针(**)

    本文转载而来,转载出处:http://www.cppblog.com/doing5552/archive/2010/09/28/127994.html 在下列函数声明中,为什么要同时使用*和& ...

  3. 深入理解C语言-深入理解指针

    关于指针,其是C语言的重点,C语言学的好坏,其实就是指针学的好坏.其实指针并不复杂,学习指针,要正确的理解指针. 指针是一种数据类型 指针也是一种变量,占有内存空间,用来保存内存地址 指针就是告诉编译 ...

  4. 一封来自恶魔的挑战邀请函,那些你见过或者没见过的C语言指针都在这里了

    前言 相信大多数的同学都是第一门能接触到语言是C/C++,其中的指针也是比较让人头疼的部分了,因为光是指针都能专门出一本叫<C和指针>的书籍,足见指针的强大.但如果不慎误用指针,这些指针很 ...

  5. 不可或缺 Windows Native (18) - C++: this 指针, 对象数组, 对象和指针, const 对象, const 指针和指向 const 对象的指针, const 对象的引用

    [源码下载] 不可或缺 Windows Native (18) - C++: this 指针, 对象数组, 对象和指针, const 对象,  const 指针和指向 const 对象的指针, con ...

  6. c++中多态性、dynamic_cast、父类指针、父类对象、子类指针、子类对象

    c++多态性是依靠虚函数和父类指针指向子类对象来实现的.简单来说,父类中定义虚函数,父类指针指向子类对象,父类指针调用函数时调用的就是子类的函数. 父类没有定义虚函数,父类指针指向子类对象时,父类指针 ...

  7. Android系统智能指针的设计思路(轻量级指针、强指针、弱指针)

    本博客为原创,转载请注明出处,谢谢. 参考博文:Android系统的智能指针(轻量级指针.强指针和弱指针)的实现原理分析 C++中最容易出错的地方莫过于指针了,指针问题主要有两类,一是内存泄露,二是无 ...

  8. C++二维数组、指针、对象数组、对象指针

    项目中用到,随手记一下: 1.二维数组.与指针 创建二维数组指针的方式: a.已知一维的大小 1 int **array=new int *[rows]; 2 (for int i=0;i<ro ...

  9. [C++]数组指针,数组引用,函数指针

    数组指针是指一个指向数组的指针,例如有一个数组指针p指向一个数组a[],则 *p是取到这个数组,也就是说 *p=a,因此 **p =a[0], 它的定义为: ]; ]=&a; (*c)表示它是 ...

随机推荐

  1. 详解Spring中的CharacterEncodingFilter

    在项目中有很多让人头疼的问题,其中,编码问题位列其一,那么在Spring框架中是如何解决从页面传来的字符串的编码问题的呢?下面我们来看看Spring框架给我们提供过滤器CharacterEncodin ...

  2. AOP小结

    AOP主要采用代理模式来实现的,静态代理(设计模式中的代理模式),动态代理(反射机制,实现InvocationHandler接口),cglib实现(采用继承方式,针对目标类生成子类,并覆盖方法进行增强 ...

  3. IntegerCache详解

    IntegerCache是Integer的内部类,用来将-128——high之间的对象进行实例化 private static class IntegerCache {        static f ...

  4. IIS的安装与配置

    IIS的安装与配置 5.1.1. IIS安装视频教程 5.1.2. IIS配置与建站设置视频教程 IIS是什么 IIS是Internet Information Services(Internet信息 ...

  5. Delphi NativeXml读取中文乱码问题解决

    NativeXml默认的字符类型为Utf8String,有时在读取中文时还是会出现乱码问题,在329版本中提供一种类型转换函数sdUtf8ToWide(),我们可以这样sdUtf8ToWide(AXm ...

  6. 数学概念——A 几何概型

    You are going from Dhaka to Chittagong by train and you came to know one of your old friends is goin ...

  7. 动态规划——I 记忆化搜索

    Description Michael喜欢滑雪百这并不奇怪, 因为滑雪的确很刺激.可是为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待升降机来载你.Michael想知道 ...

  8. 图解向hadoop分布式文件系统写文件的工作流程

    网上看到一张关于hadoop分布式文件系统(hdfs)的工作原理的图片,其实主要是介绍了向hdfs写一个文件的流程.图中的流程已经非常清晰,直接上图 好吧,博客园告诉我少于200字的文章不允许发布到网 ...

  9. Mysql Binlog日志详解

    一.Mysql Binlog格式介绍       Mysql binlog日志有三种格式,分别为Statement,MiXED,以及ROW! 1.Statement:每一条会修改数据的sql都会记录在 ...

  10. IOS UITextField 设置光标位置

    textField.leftView = [[[UIView alloc] initWithFrame:CGRectMake(, , , )] autorelease]; textField.left ...