第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. 指针的引用(*&amp;)与指针的指针(**)

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

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

    本文转载而来,转载出处: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. opencv7-ml之svm

    因为<opencv_tutorial>这部分只有两个例子,就先暂时介绍两个例子好了,在refman中ml板块有:统计模型.普通的贝叶斯分类器.KNN.SVM.决策树.boosting.随机 ...

  2. SharePoint 2016 Beta 2 安装体验

    博客地址:http://blog.csdn.net/FoxDave 最近忙碌了一段时间,2016正式版快要发布了,想尽快熟悉熟悉.2016不再提供免费版Foundation的支持,只有Server版本 ...

  3. Java反射学习(java reflect)(二)

    ok之前说了Java的反射和反射分析类,那这些东西有神马作用呢,下面就来说应用: 三.运行时使用反射分析对象 简单写一个Employee类,然后利用JAVA反射去取name域,getDeclareFi ...

  4. saltstack布署实践 【配置文件管理-state模块】

    那如果要批量修改被管机器的某个配置文件怎么做?以下给出一个最简单案例.   先在主管机器master上查看/etc/salt/master配置文件 看到有以下几行配置文件   # file_roots ...

  5. cpu affinity (亲和性)

    来源:http://www.ibm.com/developerworks/cn/linux/l-affinity.html#download 管理处理器的亲和性(affinity) 为什么(3 个原因 ...

  6. cocos2d-lua SDK接入

    1.lua 调用Java函数 1.1 在java中创建一个静态函数(比如在org.cocos2dx.lua.AppActivity.java中)名为Login public static void m ...

  7. asp.net Global.asax 不运行解决

    asp.net application的站点发布后 Global.asax 未运行,搞了好久终于解决, 解决方法如下: publish设置 该设置经测试在win server 2003 和2008 都 ...

  8. String去重方法

    思路:利用集合的contains方法将某个字符串中的集合中没有的单个字符添加到集合中,然后再将集合中每个元素做拼接 @Test public void aa5(){ String aa="a ...

  9. EFCore Owned Entity Types,彩蛋乎?鸡肋乎?之彩蛋篇

    EFCore Owned Entity Types的定义 EFCore Owned Entity Types的文档在这里:https://docs.microsoft.com/zh-cn/ef/cor ...

  10. 我的代码- rf sampling

    # coding: utf-8 # In[6]: import pandas as pdimport numpy as npfrom sklearn import treefrom sklearn.s ...