7.1 一切指针都是纸老虎:彻底理解指针

简介:

7.1.1  指针的运算

从本质上讲,指针也是一种数据,只不过这种数据有点特殊而已。我们通常所见的数据就是各种数值数据文字数据等,而指针所表示的是内存地址数据。既然是数据,那么自然就涉及到了数据的运算。像普通数据一样,指针也可以参与部分运算,包括算术运算、关系运算和赋值运算,而我们最常用的就是指针的算术加减运算。

如果指针的值是某个内存位置的地址值,那么我们就说指针指向这个内存位置。而指针的加减运算,实际上是让指针的指向发生偏转,指向另外的内存位置。通过这种指针的偏转,可以灵活地访问到该指针起始位置附近的内存。如果这种偏移是在某个范围内连续发生的话,则可以通过指针访问到某一连续内存区域的数据。例如,在3.6节中介绍过数组,数组名实际上就是数组数据所在内存区域的首地址,表示数组在内存中的起始位置。可以通过把首地址赋值给指针,然后对该指针进行加减运算,使指针发生偏转指向数组中的其他元素,从而遍历整个数组。例如:

复制代码
int nArray[3] = { 1, 2, 3 };   // 定义一个数组
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[3] = { 1, 2, 3 };    // 定义一个数组
int* pIndex = nArray;           // 将数组的起始地址赋值给指针pIndex
int* pEnd = nArray + 3;         // 计算数组的结束地址并赋值给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[10] = {0};
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 = 2;
int nVal2 = 3;
double fRes = ((double)nVal1)/nVal2;   

 如果用static_cast进行类型转换,则应该这样写:  

double fRes = static_cast<double>(nVal1)/nVal2; 

使用C++风格的类型转换,不论是对代码阅读者还是对程序都很容易识别。我们应该在代码中尽量避免进行类型转换,但如果类型转换无可避免,那么使用C++风格的类型转换在一定程度上既可增加代码的可读性,也是对类型转换损失的一种补偿。

  原文文章出自博悦平台 http://www.hongshulin001.com   原创发布转载请注明出处

目录
相关文章
|
人工智能 测试技术 Go
如何使用 Google 的 Gemini
Google Gemini是谷歌发布的人工智能大模型,能够在从数据中心到移动设备等不同平台上运行。本文将介绍Gemini以及如何使用Gemini。
2037 0
protobuf实战-----之Mac 安装篇
因为项目的需要,所以,要在mac上安装配置protobuf。其中走了不少弯路。
1984 0
|
Linux iOS开发 MacOS
PowerShell命令行输出和添加系统环境变量
主要介绍使用PowerShell命令如何查看、修改和删除系统环境变量,对于需要操作添加PATH环境变量非常实用 。由于 Powershell 的跨平台,其环境变量修改可以在linux、macos...
4871 0
PowerShell命令行输出和添加系统环境变量
|
监控 数据挖掘 数据安全/隐私保护
ERP系统中的税务管理与优化
【7月更文挑战第25天】 ERP系统中的税务管理与优化
1024 2
|
数据管理 API 调度
阿里云百炼平台知识检索应用评测:搭建之旅与一点建议
阿里云百炼平台成为企业智能化转型的重要工具之一。
|
安全 测试技术 Swift
Llama 3开源,魔搭社区手把手带你推理,部署,微调和评估
Meta发布了 Meta Llama 3系列,是LLama系列开源大型语言模型的下一代。在接下来的几个月,Meta预计将推出新功能、更长的上下文窗口、额外的模型大小和增强的性能,并会分享 Llama 3 研究论文。
Llama 3开源,魔搭社区手把手带你推理,部署,微调和评估
|
Web App开发 缓存 搜索推荐
实用的Chrome浏览器命令
【5月更文挑战第6天】探索Chrome的隐藏命令行工具,提升浏览效率和解决问题。如`chrome://flags/`启用实验性功能,`chrome://net-internals/`进行网络诊断,`chrome://settings/content/`管理内容设置等。了解这些工具,可解决浏览器问题,优化隐私和性能,实现个性化设置。成为Chrome专家,让浏览体验更上一层楼。
568 0
阿里云公网IP地址多少钱一个?
阿里云公网IP价格因地域而异,如华北1(青岛)包年包月约20.70元/月,华北2(北京)及其他地区23元/月,香港30元/月,新加坡23元/月。按量付费模式下,保有费0.020元/小时,流量额外计费。
3963 0
阿里云公网IP地址多少钱一个?
|
缓存 小程序 安全
微信PC端小程序无法加载数据
某些小程序在PC端小程序打开会出现白屏或无法加载数据的情况,本篇文章整理了几种常见情况的解决方案。
1936 1
|
Java C# 开发工具
9个最好用的在线编译/调试工具
电脑没有C/C++的开发环境了,只能找找在线的编译器。。IDEone不错。。。 本文要推荐9个最好用的在线编译器,以下顺序不按排名先后: 1、ideone 可以在线编译、调试C/C++,JAVA,PHP,Python,Perl,以及其他40多种编程语言。
3716 0

热门文章

最新文章