C++从零开始之指针及其语义和运用

本篇是《C++从零开始》系列的附篇。因友人一再认为《C++从零开始》系列中对指针的阐述太过简略,而提出的各个概念又杂七混八,且关于指针这一C++中的重要概念的运用少之又少,故本篇重点说明在《C++从零开始》系列中提出的数字、地址、指针等基础概念,并给出指针的语义,说明指针和数组的关系,阐述多级指针、多维数组、函数指针、数组指针、成员指针的语义及各自的运用。

数字、操作符、类型、类型修饰符

在《C++从零开始(三)》中已经说明,其实CPU连二进制数都不认识,其只能处理状态,而它能处理的状态恰好能用二进制数表示,故称CPU只认识二进制数。应注意由于CPU认识二进制数是认识其所表示的状态,并不是数学意义上的二进制数,因此CPU并不认识十进制数20.不过将20按数学规则转成二进制数10100后,运气极好地CPU的设计人员将加法指令定义成状态10100和状态10100相加将得到状态101000,而这个二进制数按数学规则转成十进制数正好是40,即CPU连加减乘除都不会,只会以不同的方式改变状态,而CPU的设计人员专门将那些状态的改变方式定义成和数学上的加减乘除一样进而使CPU表现得好像会加减乘除。

所以,为了让CPU执行一条指令,则那条指令所涉及的东西只能是二进制数,就必须有规则将给出的数学意义上的数转换成二进制数。如前面的十进制转二进制的规则,在《C++从零开始(二)》中提到的原码、补码、IEEE real*4等。而要编写C++代码,则一定要在代码中能体现上述的转换规则,并且要能在代码上体现欲被转换的数学意义上的数,这样编译器才能根据我们书写的代码来决定CPU要操作的东西的二进制表示。对此,C++中用类型表现前者,用数字体现后者,用操作符表示CPU的指令,即CPU状态的变换方式。

因此,为了让CPU执行加法指令,代码上书写加法指令对应的操作符——“+”,“+”的两侧需要接两个数字,而数字的类型决定了如何将数字所表示的数学上的数转换成二进制数。应注意数字是编译级的概念,不是代码级的概念,即无法在代码上表现数字,而只能通过操作符的计算的返回来获得数字。因为任何操作符都要返回数字(不返回数字的操作符也可以通过返回类型为void的数字来表示以满足这一说法),而最常见的一种得到数字的操作符就是通常被称作常数的东西,如6.3、5.2f、0772等。我在《C++从零开始(二)》中将其称作数字的确引起概念混淆,在此特澄清。

应注意只要是返回数字的东西就是操作符,故前面的常量也是一种操作符。对于变量、成员变量及函数,在《C++从零开始》系列中已多次强调它们都是映射元素,直接书写变量名、成员变量名和函数名将分别返回各自所映射的数字,即变量名函数名等也都是操作符。

数字是具有类型的,C++提供了自定义类型struct、class等来自定义复杂的类型,但不仅如此,C++还提供了更值得称赞的东西——类型修饰符。在《C++从零开始(五)》中已经说明,类型修饰符就是修饰类型用的,即按某种规则改变被修饰类型(称作原类型)所表征的数字转换规则。如猪血羊血和猪肉羊肉,这里的“血”和“肉”都是类型修饰符,改变其各自的原类型——“猪”和“羊”。上面感觉更像后者修饰前者而非前者修饰后者,如猪血中的“血”是主语而“猪”是定语。即类型修饰符其实是以原类型的信息来修改其自身所表征的那个数字转换规则。这就如称“血”、“肉”是一种东西一样,也说某类型是指针类型、引用类型、数组类型、函数类型等。

在《C++从零开始》系列中共提出下面几种类型修饰符——引用“&”、指针“*”、数组“[]”、函数“()”、函数调用规则“__stdcall”、偏移“<自定义类型名>::”、常量“const”和地址类型修饰符。其中的地址类型修饰符是最混乱的。

在《C++从零开始(三)》中已经说明地址在32位操作系统中就是一个数,这个数经常以32位长的二进制数表示,以唯一标识一特定内存单元。而一个数字的类型是地址类型时(因为有地址类型修饰符,就好像一个数字是数组类型时),就将这个数字所代表的数学意义上的数用二进制表示,以标识出一个内存单元,然后按照原类型的规则来解释那块内存单元及其后续单元的内容(类型的长度可能不止一个字节,而地址类型是类型修饰符,故一定有原类型)。由于变量映射的数实际是地址,故变量所映射的数字就是地址类型的。如long a;,假设a映射的是3006,当书写a = 3;时,由于a是变量名,故返回a所映射的数字3006,类型是long类型的地址类型。由于是地址类型,“=”操作符的语法检查成功(这是类型的另一个用处——语法检查,就好像动名形容词一样),执行“=”操作符的计算。

应注意C++并未提出地址类型修饰符这个概念,只是我出于语法上的完备而提出的,否则要涉及更多的无谓概念和规则,如*( p + 1 ) = 20; a[2] = 3;等的解释将复杂化,故在《C++从零开始》系列中提出地址类型的数字这个概念,旨在以尽量少的概念解释尽量多的语法。

最常用的类型修饰符——指针和数组

在《C++从零开始(五)》中已说明指针只是一种类型修饰符。一个数字是指针类型时,将这个数字所代表的数学意义上的数用二进制表示并返回。前面已说过数字的用处就是转换其代表的数为二进制数,其类型仅说明如何转换,而指针类型所代表的规则就是按数学规则变成二进制数,而不管其原类型是何种类型。由于不受原类型的影响,故指针类型总是固定长度(对成员指针则不一定),在32位操作系统上是四个字节。

如long a; long *p = &a;。假设a映射的是3006,p映射的是3010.对于*p = 3;,p这个操作符返回类型为long的指针类型的地址类型的数字3010,即这个数字的类型被两个类型修饰符两次修饰,由于最后是被地址修饰,故3010是地址类型的数字,而其原类型是long的指针类型。故*p返回类型为long类型的地址类型的数字3006,然后执行“=”操作符的计算。

这里请注意两个操作符——取内容操作符“*”和取地址操作符“&”。在《C++从零开始(五)》中也强调过,它们其实名不副实,应该叫类型转换操作符才对。即前者后接指针类型的数字,返回的数字原封不动,仅将其类型变为地址类型;后者后接地址类型的数字,返回的数字原封不动,仅将其类型变为指针类型。这有点耍小聪明的感觉,但请注意:long *p1 = 0; long *p2 = &*p1;如果“*”的操作是取内容,则&*p1将先取地址为0的内存单元的内容,这将引起内存访问违规,但实际并不会,因为“*”仅转换类型而非取内容(取内容是由地址类型的数字的计算来实现的)。

前面已说明,在指针类型的数字返回二进制数时,并不需要原类型的参与,即类型为long*的数字3006和类型为char*的数字3006返回的二进制数都一样,都是3006对应的二进制数。那么为什么要让指针是类型修饰符以带个不用的原类型?根据前面即可看出指针类型的原类型是给取内容操作符“*”用的。但它还有个用处,因为数组类型修饰符的加入,使得指针多了一个所谓的运算功能,在此先看看数组类型。

在《C++从零开始(五)》中已详细说明数组的修饰功能是将原类型的元素重复多个连续存放以此形成一个新的类型。如long a[10];,a的类型就是long[10],长度为10*sizeof(long)=40个字节,而char[7]类型所对应的长度就是7*sizeof(char)=7个字节。一个数字的类型是数组类型时,因这个数字的长度可一个字节,可一万个字节,故这个数字一定被存放在某块内存中,而数组类型的数字返回的二进制数就是其被存放的内存的首地址。所以前面提到的常数就不能返回一个数组类型的数字,因其没有给出一块内存来存放数组类型的数字。

这里有点混乱,注意数字不一定非要被内存所存储。对于long a[3] = { 45, 45, 45 };,假设a映射的数字是3000,则表示以long[3]的规则解释内存单元3000所记录的数字,这个数字的长度是3*sizeof(long)=12个字节,它的值由于数组类型是长度可变的而决定使用3000(记录它的内存的地址)来代表它,实际是3个值为45的数。所以a;将先返回long[3]类型的地址类型的数字3000,然后计算此地址类型的数字而返回其原类型的数字,由于原类型是long[3],而这个数字存放在3000所标识的内存处,故最后返回3000所对应的二进制数。

容易发现指针返回的是一个地址,数组也是一个地址,当它们原类型相同时,后者可以隐式类型转换为前者,但反之不行,因为数组还具备元素个数这个信息,即long[2]和long[3]的原类型相同,但类型不同。因此有:long a[3]; long *p = a;。这里没任何问题,假设a映射的是3000,则p的值就是3000.因此*p = 3;就是将3放到3000处存放的数组类型的数字的第0个元素中。为了放到第1个和第2个元素中,C++提供了一个所谓的指针运算功能,如下:*( p + 1 ) = 4; *( p + 2 ) = 5;。这里就把4放到第1个元素,5放到第2个元素中。对*( p + 1 ) = 4;,p返回一个long*的数字3000,而p + 1返回long*的数字3004,然后继续后续计算。同理,p + 2返回类型为long*的数字3000+2*sizeof(long)=3008.即指针只能进行整数加减,如:char *p1 = 0; p1++; p1 = p1 + 5 * 8 - 1; short *p2 = 0; p2 += 11; p2——;上面p1的值为40,p2的值也为40,因为p1的原类型是char而p2的是short.因此为了获得数组的第2个元素的值,需*( p + 2 );,这很明显地不便于阅读,为此C++专门提供了一个下标操作符“[]”,其前面接指针类型的数字,方括号中放一整型数字,将指针类型的数字换成地址类型,再将值按前面提到的指针运算规则变换,返回。如long a[4]; long *p = a;,假设a映射的是3000.则a[2] = 1;等效于*( p + 2 ) = 1;,a[2]前面接的是long*类型的数字3000(隐式类型转换,从long[4]转成long*),加2*sizeof(long),返回3008,类型则简单地变成long类型的地址类型。由于“[]”仅仅只是前面说的指针运算的简化易读版本,故也可a[-1] = 3;,其等效于*( p - 1 ) = 3;。由于“[]”前接指针,故也可p[-1] = 3;,等效于a[-1] = 3;。

算规则变换,返回。如long a[4]; long *p = a;,假设a映射的是3000.则a[2] = 1;等效于*( p + 2 ) = 1;,a[2]前面接的是long*类型的数字3000(隐式类型转换,从long[4]转成long*),加2*sizeof(long),返回3008,类型则简单地变成long类型的地址类型。由于“[]”仅仅只是前面说的指针运算的简化易读版本,故也可a[-1] = 3;,其等效于*( p - 1 ) = 3;。由于“[]”前接指针,故也可p[-1] = 3;,等效于a[-1] = 3;。

时间: 2024-08-02 23:31:21

C++从零开始之指针及其语义和运用的相关文章

C++指针专题

C++中指针的引用详解 C++中智能指针(smarter pointer)自定义删除器(deleter) 的方法 C++:派生类强制转换为基类 C++二维指针动态分配内存连续问题 C++中的值传递,引用传递及指针传递 C++常量指针和指针常量 C++指针和数组 C++必知必会(五) 引用是别名而非指针 谈函数指针(全局/类成员函数)和函数对象 构造函数中的this指针 Google C++编程风格指南(四):智能指针和其他C++特性 声明函数指针并实现回调 C++指针使用方法解惑 浅析C++中的

Google C++编程风格指南(四):智能指针和其他C++特性

1.对于智能指针,安全第一.方便第二,尽可能局部化(scoped_ptr): 2.引用形参加上const,否则使用指针形参:3.函数重载的使用要清晰.易读:4.鉴于容易误用,禁止使用缺省函数参数(值得商榷):5.禁止使用变长数组:6.合理使用友元-- Google特有的风情 Google有很多自己实现的使C++代码更加健壮的技巧.功能,以及有异于别处的C++的使用方式. 1.智能指针(Smart Pointers) 如果确实需要使用智能指针的话,scoped_ptr完全可以胜任.在非常特殊的情况

C++第11版本中的一些强大的新特性小结_C 语言

Auto Type Deduction 自动类型推导 auto 关键字让用户得以使用 C++ 内置的类型推导特性. std::string something = somethingthatreturnsastring.getString(); auto something = somethingthatreturnsastring.getString(); Auto 关键字会对上述自变量(something)进行自动推导,得出其应该是 string 类型的结论,并在 auto 出现的地方用正确

Emacs之魂(五):变量的“指针”语义

Emacs之魂(一):开篇Emacs之魂(二):一分钟学会人界用法Emacs之魂(三):列表,引用和求值策略Emacs之魂(四):标识符,符号和变量Emacs之魂(五):变量的"指针"语义Emacs之魂(六):宏与元编程Emacs之魂(七):变量捕获与卫生宏Emacs之魂(八):反引用与嵌套反引用Emacs之魂(九):读取器宏 1. 语义学 在计算理论中,形式语义学是关注计算模式和程序设计语言含义的严格的数学研究领域. 语言的形式语义是用数学模型去表达该语言描述的可能计算来给出的. 提

C++学习从零开始(六)

再看main函数,先通过ABC a;定义了一个变量,因为要在栈上分配一块内存,即创建了一个数字(创建装数字的内存也就导致创建了数字,因为内存不能不装数字),进而创建了一个ABC的实例,进而调用ABC的构造函数.由于这里没有给出参数(后面说明), 因此调用了ABC::ABC(),进而a.a为1,a.pF和a.count都为0.接着定义了变量r,但由于它是ABC&,所以并没有在栈上分配内存,进而没有创建实例而没有调用ABC::ABC.接着调用a.Do,分配了一块内存并把首地址放在a.pF中. 注意上

C++学习从零开始(一)

本文的中篇已经介绍了虚的意思,就是要间接获得,并且举例说明电视机的频道就是让人间接获得电视台频率的,因此其从这个意义上说是虚的,因为它可能操作失败--某个频道还未调好而导致一片雪花.并且说明了间接的好处,就是只用编好一段代码(按5频 道),则每次执行它时可能有不同结果(今天5频道被设置成中央5台,明天可以被定成中央2台),进而使得前面编的程序(按5频道)显得很灵活.注意虚之所以能够很灵活是因为它一定通过"一种手段"来间接达到目的,如每个频道记录着一个频率.但这是不够的,一定还有&quo

彻底搞定C语言指针 第四篇

1 int i 说起 你知道我们申明一个变量时象这样int i :这个i是可能在它处重新变赋值的. 如下: int i=0: //- i=20://这里重新赋值了 不过有一天我的程 序可能需要这样一个变量(暂且称它变量),在申明时就赋一个初始值.之后我的程序在其它任何处都 不会再去重新对它赋值.那我又应该怎么办呢?用const . //************** const int ic =20: //- ic=40://这样是不可以的,编译时是无法通过,因为我们不能对 const 修饰的ic

C语言学习教程第六章-指针(5)

使用字符串指针变量与字符数组的区别 用字符数组和字符指针变量都可实现字符串的存储和运算. 但是两者是有区别的.在使用时应注意以下几个问题: 1. 字符串指针变量本身是一个变量,用于存放字符串的首地址.而字符串本身是存放在以该首地址为首的一块连续的内存空间中并以'\0'作为串的结束.字符数组是由于若干个数组元素组成的,它可用来存放整个字符串. 2. 对字符数组作初始化赋值,必须采用外部类型或静态类型,如: static char st[]={"C Language"};而对字符串指针变量

声明函数指针并实现回调

程序员常常需要实现回调.本文将讨论函数指针的基本原则并说明如何使用函数指针实现回调.注意这里针对的是普通的函数,不包括完全依赖于不同语法和语义规则的类成员函数(类成员指针将在另文中讨论). 声明函数指针 回调函数是一个程序员不能显式调用的函数:通过将回调函数的地址传给调用者从而实现调用.要实现回调,必须首先定义函数指针.尽管定义的语法有点不可思议,但如果你熟悉函数声明的一般方法,便会发现函数指针的声明与函数声明非常类似.请看下面的例子: void f():// 函数原型 上面的语句声明了一个函数