购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

>>> 第7章 <<<

指针与引用

指针是C系语言的特色。指针是C++提供的一种颇具特色的数据类型,允许直接获取和操纵数据地址,实现动态存储分配。

指针是C和C++的精华所在,也是C和C++中一个十分重要的概念。一个数据对象的内存地址称为该数据对象的指针。指针可以表示各种数据对象,如简单变量、数组、数组元素、结构体,甚至函数。换句话说,指针具有不同的类型,可以指向不同的数据存储体。

指针问题,包括常量指针、数组指针、函数指针、this指针、指针传值、指向指针的指针等,这些问题也是各大公司的常备考点。本章不对指针基本知识做回顾和分析(请参考C++其他经典著作),而是通过对各公司面试题目进行全面、仔细的解析帮助读者解决其中的难点。

7.1 指针基本问题

面试例题1: 指针和引用的差别?

答案:

(1)非空区别。在任何情况下都不能使用指向空值的引用。一个引用必须总是指向某些对象。因此如果你使用一个变量并让它指向一个对象,但是该变量在某些时候也可能不指向任何对象,这时你应该把变量声明为指针,因为这样你可以赋空值给该变量。相反,如果变量肯定指向一个对象,例如你的设计不允许变量为空,这时你就可以把变量声明为引用。不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针要高。

(2)合法性区别。在使用引用之前不需要测试它的合法性。相反,指针则应该总是被测试,防止其为空。

(3)可修改区别。指针与引用的另一个重要的区别是指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变,但是指定的对象其内容可以改变。

(4)应用区别。总的来说,在以下情况下应该使用指针:一是考虑到存在不指向任何对象的可能(在这种情况下,能够设置指针为空),二是需要能够在不同的时刻指向不同的对象(在这种情况下,你能改变指针的指向)。如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么应该使用引用。

面试例题2: Please check out which of the following statements are wrong?(看下面的程序哪里有错?)[中国台湾某著名计算机硬件公司2005年12月面试题]

img

答案:

● 1正确,很正常地声明了一个整型变量。

● 2正确,很正常地声明了一个整型变量,同时初始化这个变量。

● 3正确,理由同上。

● 4错误,声明了一个引用,但引用不能为空,必须同时初始化。

● 5正确,声明了一个引用reiv2,同时初始化了,也就是reiv2是iv的别名。

● 6正确,理由同上。

● 7正确,声明了一个整数指针,但是并没有定义这个指针所指向的地址。

● 8错误,整数指针pi并没有指向实际的地址。在这种情况下就给它赋值是错误的,因为赋的值不知道该放到哪里去,从而造成错误。

● 9正确,整数指针pi指向iv3实际的地址。

● 10错误,const常量赋值时,必须同时初始化。

● 11正确,const常量赋值并同时初始化。

● 12正确,const常量指针赋值并同时初始化。

7.2 传递动态内存

面试例题1: 下面5个函数哪个能够成功进行两个数的交换?[中国某互联网公司2009年12月笔试题]

img

解析: 这道题考察函数参数传递、值传递、指针传递(地址传递)、引用传递。

swap1传的是值的副本,在函数体内被修改了形参p、q(实际参数a、b的一个拷贝),p、q的值确实交换了,但是它们是局部变量,不会影响到主函数中的a和b。当函数swap1生命周期结束时,p、q所在栈也就被删除了。如下图所示。

img

swap2传的是一个地址进去,在函数体内的形参*p、*q是指向实际参数a、b地址的两个指针。

这里要注意:

img

是不符合逻辑的一段代码,int *temp新建了一个指针(但没有分配内存)。*temp=*p不是指向而是拷贝。把*p所指向的内存里的值(也就是实参a的值)拷贝到*temp所指向内存里了。但是int *temp不是不分配内存吗?的确不分配,于是系统在拷贝时临时给了一个随机地址,让它存值。分配的随机地址是个“意外”,且函数结束后不收回,造成内存泄露,如下图所示。

img

那么swap2到底能否实现两数交换吗?这要视编译器而定,笔者在Dev-C++可以通过测试,但是在更加“严格”的编译器如vs2008,这段代码会报错。

swap3传的是一个地址进去,在函数体内的形参*p、*q是指向实际参数a、b地址的两个指针。这里要注意:

img

int *temp新建了一个指针(但没有分配内存)。temp=p是指向而不是拷贝。temp指向了*p所指向的地址(也就是a)。而代码:

img

意思是p指向了*q所指向的地址(也就是b)。q指向了*t所指向的地址(也就是a),如下图所示。

img

但是函数swap3不能实现两数的交换,这是因为函数体内只是指针的变化,而对地址中的值却没有改变。举个简单的例子,a、b两个仓库的两把备用钥匙p、q,p钥匙用来开a仓库,q钥匙用来开b仓库。现在进入函数体,p、q钥匙功能发生了改变:p钥匙用来开b仓库,q钥匙用来开a仓库;但是仓库本身的货物没有变化(a仓库原来是韭菜现在还是韭菜,b仓库原来是番薯现在还是番薯)。当函数结束,p、q两把备用钥匙自动销毁。主函数里用主钥匙打开a、b两个仓库,发现值还是没有变化。

函数swap4可以实现两数的交换,因为它修改的是指针所指向地址中的值,如下图所示。

img

swap5函数与swap4类似,是一个引用传递,修改的结果直接影响实参。

答案: swap4函数和swap5函数。

面试例题2: What will happen after running the “Test”?(这个程序测试后会有什么结果?)[美国某著名计算机嵌入式公司2005年9月面试题]

img

解析: 毛病出在函数GetMemory中。void GetMemory(char *p, int num)中的*p实际上是主函数中str的一个副本,编译器总是要为函数的每个参数制作临时副本。在本例中,p申请了新的内存,只是把p所指的内存地址改变了,但是str丝毫未变。因为函数GetMemory没有返回值,因此str并不指向p所申请的那段内存,所以函数GetMemory并不能输出任何东西,如下图所示。事实上,每执行一次GetMemory就会申请一块内存,但申请的内存却不能有效释放,结果是内存一直被独占,最终造成内存泄露。

img

如果一定要用指针参数去申请内存,那么应该采用指向指针的指针,传str的地址给函数GetMemory。代码如下:

img
img

这样的话,程序就可以运行成功。字符串是一个比较特殊的例子。我们分别打印*str、str、&str可以发现,结果分别是h、hello、0*22f7c。str就是字符串的值;*str是字符串中某一字符的值,默认的是首字符,所以是h;&str是字符串的地址值。

由于“指向指针的指针”这个概念不容易理解,我们可以用函数返回值来传递动态内存。这种方法更加简单,代码如下:

img

我们可以对这道题推而广之,看一下整型变量是如何传值的,代码如下:

img

GetMemory2 把v的地址传了进来,*z是地址里的值,是v的副本。通过直接修改地址里的值,不需要有返回值,也把v给修改了,因为v所指向地址的值发生了改变。

答案: 程序崩溃。因为GetMemory 并不能传递动态内存,Test 函数中的str一直都是NULL。

面试例题3: 这个函数有什么问题?该如何修改?[美国著名硬盘公司S 2008年4月面试题]

img

解析: 这个str里存的地址是函数strA栈帧里“hello word ”的首地址。函数调用完成,栈帧恢复到调用strA之前的状态,临时空间被重置,堆栈“回缩”,strA栈帧不再属于应该访问的范围。存于strA栈帧里的“hello word”当然也不应该访问了。这段程序可以正确输出结果,但是这种访问方法违背了函数的栈帧机制。

分配内存时有一句老话:First time you do it,then something change it(一旦使用,它即改变)。也许是一个妨碍其他函数调用的内存块,这些情况都是无法预知的。如果运行一段函数,不会改变其他函数所调用的内存,在这种情况下,你运行多少次都不是问题。

但是只要另外一个函数调用的话,你就会发现,这种方式的不合理及危险性。我们面对的是一个有操作系统覆盖的计算机,而一个不再访问的内存块,随时都有被收回或作为他用的可能。

如果想获得正确的函数,改成下面这样就可以:

img

首先要搞清楚char *str和char str[]:

img

是分配一个局部数组:

img

是分配一个全局数组:

局部数组是局部变量,它所对应的是内存中的栈。全局数组是全局变量,它所对应的是内存中的全局区域。字符串常量保存在只读的数据段,而不是像全局变量那样保存在普通数据段(静态存储区),如:

img

c占用一个存储区域,但是局部区的数据是可以修改的:

img

这里c不占存储空间。

另外要想修改,也可以这样:

img

通过static开辟一段静态存储空间。

答案:因为这个函数返回的是局部变量的地址,当调用这个函数后,这个局部变量str就释放了,所以返回的结果是不确定的且不安全,随时都有被收回的可能。

面试例题4: 写出下面程序的运行结果。[美国著名硬盘公司S 2008年4月面试题]

img

解析: 本题考的是指针与地址的关系问题。

本程序结构如下:

(1)先声明了一个整型数组a[3],然后分别给数组赋值。

(2)又声明了两个整数指针p、q,但是并没有定义这两个指针所指向的地址。

(3)使整数指针p的地址指向a(注意a就是a[0]),使整数指针q的地址指向a[2]。可实际验证程序如下:

img

上面的输出结果分别是:

img

q的实际地址是0x22ff70,p的实际地址是0x22ff68。0x22ff70-0x22ff68=0x08(十六进制减法),相差是8。

q-p的实际运算是(q的地址值(0x22ff70)-p的地址值(0x22ff68))/sizeof(int),即8/sizeof(int)=2。

答案: 运行结果是2。

面试例题5: 请问下面代码的输出结果是多少?[中国某互联网公司2009年12月笔试题]

img

解析: 首先可以肯定的是上面这段代码是非常糟糕的,无论是可读性还是安全性都很差。写这种代码的人,按照Bjarne Stroustrup(C++标志化制定者)的说法,应该“斩立决”。

这道题出的目的就是考察你对内存偏移的理解:

img

这是一个野蛮的转化,强制把a地址内容看成是一个B类对象,pb指向的是a类的内存空间:

img

正常情况下,B类只有一个元素是int m_c,但是a类的内存空间中存放第一个元素的位置是m_a,pb 指向的是对象的内存首地址,比如0x22ff58, 当pb->func()调用B::func( )来打印m_c时,编译器对m_c对它的认识就是m_c距离对象的偏移量0,于是打印了对象a首地址的编译量0x22ff58+0 变量值。所以打印的是m_a的值1。以下代码来证明如下:

img

答案: 1

面试例题6 :What results after run the following code?(下列代码的运行结果是什么?)[中国台湾某著名CPU生产公司2005年面试题]

img

解析: 指针问题。

答案: 这样做会导致运行时错误,因为这种做法会给一个指针分配一个随意的地址,这是非常危险的。不管这个指针有没有被使用过,这么做都是不允许的。

面试例题7: 下列程序的输出结果是什么?[中国著名网络企业XL公司2007年12月面试题]

img

A.22

B.11

C.12

D.21

解析: B类中的_a把A类中_a的覆盖掉了。在构造B类时,先调用A类的构造函数。所以A类的_a是1,而B类的_a是2。

答案: C

面试例题8: 以下描述正确的是( )。[中国著名网络企业XL公司2010年7月面试题]

A.函数的形参在函数未调用时预分配存储空间

B.若函数的定义出现在主函数之前,则可以不必再说明

C.若一个函数没有return语句,则什么值都不返回

D.一般来说,函数的形参和实参的类型应该一致

解析

A:错误的,调用到实参才会分配空间。

B:函数需要在它被调用之前被声明,这个跟main()函数无关。

C:错误的,在主函数main中可以不写return语句,因为编译器会隐式返回0;但是在一般函数中没return语句是不行的。

D:正确的。

答案: D

面试例题9: 下列程序会在哪一行崩溃?[美国著名软件企业M公司2007年11月面试题]

img

解析:

int *p = &s.i;相当于int *p; p = &si;。当执行 p[0]=4; p[1]=3;的时候,p始终等于&si。s.p = p相当于建立了如下关系:

s.p 存了p的值,也就是&s.i;s.p[1]相当于*(&s.i + 1),即s.i的地址加1,也就是s.p。 s.p[1] 跟s.p其实是同一个地方,所以到s.p[1]=1,那么s.p[0]将指向内存地址为1的地方。

s.p[0] = 2;并不是给s.i赋值,而是相当于 *((int *)1) = 2;。

也就是要访问0x00000001空间——对于一个未做声明的地址直接进行访问,所以访问出错。

编写程序如下:

img

可以看到输出结果如下:

img

答案 :s.p[0] = 2;行程序会崩溃。

7.3 函数指针

面试例题1: 写出函数指针、函数返回指针、const指针、指向const的指针、指向const的const指针。

答案:

img

面试例题2: Find the defects in each of the following programs, and explain why it is incorrect.(找出下面程序的错误,并解释它为什么是错的。)[中国台湾某著名杀毒软件公司2005年10月面试题]

img

解析: 这道程序体存在着函数指针的错误使用问题。

答案:

正确的程序如下:

img

面试例题3: Write in words the data type of the identifier involved in the following definitions.(下面的数据声明都代表什么?)[美国某著名计算机嵌入式公司2005年9月面试题]

(1)float(**def)[10];

(2)double*(*gh)[10];

(3)double(*f[10])();

(4)int*((*b)[10]);

(5)Long (* fun)(int)

(6)Int (*(*F)(int,int))(int)

解析: 函数指针的问题。

就像数组名是指向数组第一个元素的常指针一样,函数名也是指向函数的常指针。可以声明一个指向函数的指针变量,并且用这个指针来调用其他函数——只要这个函数和你的函数指针在签名、返回、参数值方面一致即可。

img

上面就是一个函数指针——指向函数的指针,这个指针返回值是long,所带的参数是int。如果去掉(* fun)的“()”它就是指针函数,是一个带有整数参量并返回一个长整型变量的指针的函数。

img

如上所示,F是一个指向函数的指针,它指向一种函数(该函数参数为int,int返回值为一个指针),返回的这个指针指向的是另外一个函数(参数类型为int,返回值为int类型的函数)。

答案:

(1)float(**def)[10];

def是一个二级指针,它指向的是一个一维数组的指针,数组的元素都是float。

(2)double*(*gh)[10];

gh是一个指针,它指向一个一维数组,数组元素都是double*。

(3)double(*f[10])();

f是一个数组,f有10个元素,元素都是函数的指针,指向的函数类型是没有参数且返回double的函数。

(4)int*((*b)[10]);

就跟“int* (*b)[10]”是一样的,是一维数组的指针。

(5)Long (* fun)(int)

函数指针。

(6)Int (*(*F)(int,int))(int)

F是一个函数的指针,指向的函数的类型是有两个int参数并且返回一个函数指针的函数,返回的函数指针指向有一个int参数且返回int的函数。

7.4 指针数组和数组指针

面试例题1: 以下程序的输出是( )[美国某软件公司2009年12月面试题目]

img

解析: 本题定义一个指针指向一个10个int元素的数组。a+1表明a指针向后移动1*sizeof(数组大小);a+1后共向后移动40个字节。*a+1仅针对这一行向后移动4个字节,如下图所示。

img

答案: 输出如下:

1 11 2 2 11

面试例题2: 一个指向整型数组的指针的定义为( )。

A.int (*ptr)[]

B.int *ptr[]

C.int *(ptr[])

D.int ptr[]

解析:

int (*ptr)[]是一个指向整型数组的指针。

int *ptr[]是指针数组,ptr[]里面存的是地址。它指向位置的值就是*ptr[0]、*ptr[1]、*ptr[2]、*ptr[3]。不要存*ptr[0]=5;、*ptr[1]=6;,因为这里面没有相应的地址。

int *(ptr[])与B相同。

int ptr[]是一个普通的数组。

答案: A

扩展知识

a是指针数组,是指一个数组里面装着指针。

b是指向数组的指针,代表它是指针,指向整个数组。

以下是指针数组a:

img

下面是数组指针b:

img

面试例题3: 用变量a给出下面的定义。[中国台湾某著名CPU生产公司2005年面试题]

(1)一个整型数(An integer)

(2)一个指向整型数的指针(A pointer to an integer)

(3)一个指向指针的指针,它指向的指针是指向一个整型数(A pointer to a pointer to an integer)

(4)一个有10个整型数的数组(An array of 10 integers)

(5)一个有10个指针的数组,该指针是指向一个整型数的(An array of 10 pointers to integers)

(6)一个指向有10个整型数数组的指针(A pointer to an array of 10 integers)

(7)一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and returns an integer)

(8)一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数(An array of 10 pointers to functions that take an integer argument and return an integer)

解析: 这道面试例题是嵌入式编程和指针运用中经常考到的问题。是那种要翻一下书才能回答的问题。当我写这本书时,为了确定语法的正确性,我的确查了一下书。但是当我被面试的时候,我期望被问到这个问题(或者相近的问题)。因为在被面试的这段时间里,我确定我知道这个问题的答案。应试者如果不知道所有的答案(或至少大部分答案),那么也就没有为这次面试做好准备。如果该面试者没有为这次面试做好准备,那么他又能为什么做好准备呢?

答案:

(1)int a; // An integer

(2)int *a; // A pointer to an integer

(3)int **a; // A pointer to a pointer to an integer

(4)int a[10]; // An array of 10 integers

(5)int *a[10]; // An array of 10 pointers to integers

(6)int (*a)[10]; // A pointer to an array of 10 integers

(7)int (*a)(int); // A pointer to a function that takes an integer argument and returns an integer

(8)int (*a[10])(int); //An array of 10 pointers to functions that take an integer argument and return an integer

面试例题4: 写出如下程序片段的输出。[美国某著名CPU生产公司面试题]

img

解析: 第一个结果好理解,是正常的指针运算。但是第二个却有点难以理解。

第二个的确是5。首先a表示一个1行5列数组,在内存中表示为一个5个元素的序列。int *ptr = (int*)(&a + 1)的意思是,指向a数组的第6个元素(尽管这个元素不存在)。那么显然,(ptr - 1)所指向的数据就是a数组的第5个元素——5。

如果存在这样的数组:

img

那么显然:

img

实际上,b的数据分布还是按照1、2、3、4、5、6、7、8、9、10分布的,所谓b[0]和b[1]实际上只是指向其中一个元素的指针。

时刻牢记这样的观点:数组名本身就是指针,再加个&,就变成了双指针,这里的双指针就是指二维数组,加1,就是数组整体加一行,ptr指向a的第6个元素。

答案: 2,5。

扩展知识(火烧赤壁的故事)

0X0000(a)里含有至少两个信息,第一就是地址本身,第二个是隐藏的所指向的数组的大小。1F000地址直接和0X0000进行通信并为之提供服务,而不和1、2、3、4、5等直接通信,服务的内容为0X0000的需求,而和数组每个元素本身的大小没有直接关系(只是间接)。1F000里也至少含有两个信息,即一是地址本身,二是所服务的对象的容量。就像一艘船按序排列有1、2、3、4、5个座位,&a+1的意思是我要坐下一艘船的1号座位,而不是这艘船本身的座位。

话说曹操听了别人的计策,把800艘战船用铁链首尾相接(两船间稍有空隙)连成一条龙,准备攻打东吴。每个船上顺序排列有5个位子,分别坐着船长、舵手、枪兵、弓兵、刀兵,每个船及座位编号规律为boat1~boat800_1~5,其中boat1~800代表本船在船队中的序号,1~5代表本船上的位子。周瑜说:“把所有位子的人员按顺序逐个消灭。”诸葛亮说:“公瑾此言差矣,我用火攻,&a+1的方法岂不是比逐个遍历a[]更快捷?即所谓倾巢之下,安有完卵?”周瑜听后道:“既生瑜,何生亮!”

7.5 迷途指针

面试例题1: 以下代码有什么错误?会造成什么问题?[美国某著名CPU生产公司面试题]

img

解析: 编程中一种很难发现的错误是迷途指针。迷途指针也叫悬浮指针、失控指针,是当对一个指针进行delete操作后——这样会释放它所指向的内存——并没有把它设置为空时产生的。而后,如果你没有重新赋值就试图再次使用该指针,引起的结果是不可预料的。如果程序崩溃算都算走运了。

这就如同一家水果公司搬家了,但你使用的仍然是它原来的电话号码。这可能不会导致什么严重的后果——也许这个电话号码是放在一个无人居住的房子里面。另一方面,这个号码也可能被重新分配给一个军工厂,你的电话可能引起爆炸,把整个城市炸毁。

总之,在删除指针后小心不要再使用它。虽然这个指针仍然指向原来的内存区域,但是编译器已经把这块内存区域分配给了其他的数据。再次使用这个指针会导致你的程序崩溃。更糟糕的情况是,程序可能表面上运行得很好,过不了几分钟就崩溃了。这被称为定时炸弹,可不是开玩笑。为了安全起见,在删除一个指针后,把它设置为空指针(0)。这样就可以消除它的危害。

在本题中,首先声明了一个指针pInt,然后打印。打印后使用delete将其删除。那么现在pInt就是一个迷途指针,或者说是悬浮指针。

第2步,声明了一个新的指针pLong,把90 000赋值给它,然后打印。

第3步,把值20赋给pInt所指向的内存区域,但此时pInt不指向任何有效的空间。因为它原来所指向的内存空间已经被释放了,所以这样做会给内存区域带来很坏的结果。

第4步,打印pInt的值,结果是20。再打印pLong的值,发现它变成了65556而不是90 000了。这是因为把90 000赋给*pLong后,它实际存储为5F 90 00 01。把20(也就是十六进制的00 14)赋给指针pInt,因为指针pInt仍然指向相同的地址,因此pLong的前两个字节被覆盖了,变成了00 14 00 01,所以打印的结果变成了65 556。

答案: 以上程序使用了迷途指针并重新赋值,会造成系统崩溃。

面试例题2: 空指针和迷途指针的区别是什么?

答案: 当delete一个指针的时候,实际上仅是让编译器释放内存,但指针本身依然存在。这时它就是一个迷途指针。

当使用以下语句时,可以把迷途指针改为空指针:

img

通常,如果在删除一个指针后又把它删除了一次,程序就会变得非常不稳定,任何情况都有可能发生。但是如果你只是删除了一个空指针,则什么事都不会发生,这样做非常安全。

使用迷途指针或空指针(如MyPtr=0)是非法的,而且有可能造成程序崩溃。如果指针是空指针,尽管同样是崩溃,但它同迷途指针造成的崩溃相比是一种可预料的崩溃。这样调试起来会方便得多。

面试例题3: C++中有了malloc/free,为什么还需要new/delete?

答案: malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。

对于非内部数据类型的对象而言,只用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。

因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。new/delete不是库函数,而是运算符。

面试例题4: 下列程序的输出结果是什么?[中国著名网络企业XL公司2007年10月面试题]

img

A.theworld

B.the

C.ello

D.ellotheworld

解析: 指针的指针问题。

答案: B。

7.6 指针和句柄

面试例题1: 句柄和指针的区别和联系是什么?[英国某著名计算机图形图像公司面试题]

解析: 句柄是一个32位的整数,实际上是Windows在内存中维护的一个对象(窗口等)内存物理地址列表的整数索引。因为Windows的内存管理经常会将当前空闲对象的内存释放掉,当需要时访问再重新提交到物理内存,所以对象的物理地址是变化的,不允许程序直接通过物理地址来访问对象。程序将想访问的对象的句柄传递给系统,系统根据句柄检索自己维护的对象列表就能知道程序想访问的对象及其物理地址了。

句柄是一种指向指针的指针。我们知道,所谓指针是一种内存地址。应用程序启动后,组成这个程序的各对象是驻留在内存的。如果简单地理解,似乎我们只要获知这个内存的首地址,那么就可以随时用这个地址访问对象。但是,如果真的这样认为,那么就大错特错了。我们知道,Windows是一个以虚拟内存为基础的操作系统。在这种系统环境下,Windows内存管理器经常在内存中来回移动对象,以此来满足各种应用程序的内存需要。对象被移动意味着它的地址变化了。如果地址总是如此变化,我们该到哪里去找该对象呢?为了解决这个问题,Windows操作系统为各应用程序腾出一些内存地址,用来专门登记各应用对象在内存中的地址变化,而这个地址(存储单元的位置)本身是不变的。Windows内存管理器移动对象在内存中的位置后,把对象新的地址告知这个句柄地址来保存。这样我们只需记住这个句柄地址就可以间接地知道对象具体在内存中的哪个位置。这个地址是在对象装载(Load)时由系统分配的,当系统卸载时(Unload)又释放给系统。句柄地址(稳定)→记载着对象在内存中的地址→对象在内存中的地址(不稳定)→实际对象。但是,必须注意的是,程序每次重新启动,系统不能保证分配给这个程序的句柄还是原来的那个句柄,而且绝大多数情况下的确不一样。假如我们把进入电影院看电影看成是一个应用程序的启动运行,那么系统给应用程序分配的句柄总是不一样,这和每次电影院售给我们的门票总是不同的座位是一样的道理。

HDC是设备描述表句柄。CDC是设备描述表类。用GetSafeHwnd和FromHandle可以互相转换。

答案: 句柄和指针其实是两个截然不同的概念。Windows系统用句柄标记系统资源,隐藏系统的信息。你只要知道有这个东西,然后去调用就行了,它是个32bit的uint。指针则标记某个物理内存地址,两者是不同的概念。

面试例题2: In C++, which of the following are valid uses of the std::auto_ptr template considering the class definition below? (下面关于智能指针auto_ptr用法正确的是哪项?)[美国软件公司M2009年12月笔试题]

img

选项如下所示:

img

解析:auto_ptr是安全指针。最初动机是使得下面的代码更安全:

img

如果f()从没有执行delete语句(因为过早的return或者是在函数体内部抛出了异常),动态分配的对象将没有被delete,一个典型的内存泄漏。使其安全的一个简单方法是用一个“灵巧”的类指针对象包容这个指针,在其析构时自动删除此指针:

img

现在,这个代码将不再泄漏T对象,无论是函数正常结束还是因为异常,因为pt的析构函数总在退栈过程中被调用。类似地,auto_ptr可以被用来安全地包容指针:

img

现在,析构函数不再需要删除pimpl_指针,因为auto_ptr将自动处理它。

如果了解auto_ptr格式,就知道A是对的,C是错的。B、D也是错的,因为auto_ptr放在vector之中是不合理的。 因为auto_ptr的拷贝并不等价。 当auto_ptr被拷贝时,原来的那一份会被删除。在《Exceptional C++》特别提到:“尽管编译器不会对此给出任何警告,把auto_ptr放入container仍是不安全的。这是因为我们无法告知这个container关于auto_ptr具有特殊的语义的情况。不错,如今我所知的大多数auto_ptr实现都会让你侥幸摆脱这个麻烦;而且在某些流行的编译器所提供的文档中,与此几乎相同的代码甚至还被作为优良的代码而给出。然而实际上它是不安全的(现在成为非法的了)。”auto_ptr并不满足对能够放入container的类别之需求,因为auto_ptr之间的拷贝是不等价的。创建额外的拷贝实在是不必要和低效的;出于商业竞争的考虑,一个厂商当然不可能会发售一个本来可以很高效的低效程序库。

E也是正确的,从new Object构造出一个 auto_ptr <Object>。

答案: A,E。

7.7 this指针

面试例题1: Please choose the right statement of "this" pointer:(下面关于this指针哪个描述是正确的)

A."this" pointer cannot be used in static functions

B."this" pointer could not be store in Register.

C."this" pointer is constructed before member function.

D."this" pointer is not counted for calculating the size of the object.

E."this" pointer is read only.?[英国某著名计算机图形图像公司面试题]

解析: 解 析:关于This指针,有这样一段描述:当你进入一个房子后,你可以看见桌子、椅子、地板等,但是房子你是看不到全貌了。

对于一个类的实例来说, 你可以看到它的成员函数、成员变量, 但是实例本身呢? this指针是这样一个指针,它时时刻刻指向这个实例本身。

this指针易混的几个问题如下。

(1)This指针本质是一个函数参数,只是编译器隐藏起形式的,语法层面上的参数。

this只能在成员函数中使用,全局函数、静态函数都不能使用this。

实际上,成员函数默认第一个参数为T* const this。

如:

img

其中,func的原型在编译器看来应该是:

img

(2)this在成员函数的开始前构造,在成员的结束后清除。这个生命周期同任何一个函数的参数是一样的,没有任何区别。当调用一个类的成员函数时,编译器将类的指针作为函数的this参数传递进去。如:

img

此处,编译器将会编译成:

img

看起来和静态函数没差别,不过,区别还是有的。编译器通常会对this指针做一些优化,因此,this指针的传递效率比较高,如VC通常是通过ecx寄存器传递this参数的。

(3)this指针并不占用对象的空间。

this相当于非静态成员函数的一个隐函的参数,不占对象的空间。它跟对象之间没有包含关系,只是当前调用函数的对象被它指向而已。

所有成员函数的参数,不管是不是隐含的,都不会占用对象的空间,只会占用参数传递时的栈空间,或者直接占用一个寄存器。

(4)this指针是什么时候创建的?

this在成员函数的开始执行前构造,在成员的执行结束后清除。

但是如果class或者struct里面没有方法的话,它们是没有构造函数的,只能当做C的struct使用。采用TYPE xx的方式定义的话,在栈里分配内存,这时候this指针的值就是这块内存的地址。采用new方式创建对象的话,在堆里分配内存,new操作符通过eax返回分配的地址,然后设置给指针变量。之后去调用构造函数(如果有构造函数的话),这时将这个内存块的地址传给ecx。

(5)this指针存放在何处?堆、栈、还是其他?

this指针会因编译器不同而有不同的放置位置。可能是堆、栈,也可能是寄存器。

C++是一种静态的语言,那么对C++的分析应该从语法层面和实现层面两个方面进行。

语法上,this是个指向对象的“常指针”,因此无法改变。它是一个指向相应对象的指针。所有对象共用的成员函数利用这个指针区别不同变量,也就是说,this是“不同对象共享相同成员函数”的保证。

而在实际应用的时候,this应该是个寄存器参数。这个不是语言规定的,而是“调用约定”,C++的默认调用约定是__cdecl,也就是C风格的调用约定。该约定规定参数自右向左入栈,由调用方负责平衡堆栈。对于成员函数,将对象的指针(即this指针)存入ecx中(有的书将这一点单独分开,叫做thiscall,但是这的确是cdecl的一部分)。因为这只是一个调用约定,不是语言的组成部分,不同编译器自然可以自由发挥。但是现在的主流编译器都是这么做的。

(6)this指针是如何传递给类中的函数的?绑定?还是在函数参数的首参数就是this指针?那么,this指针又是如何找到“类实例后函数”的?

大多数编译器通过ecx寄存器传递this指针。事实上,这也是一个潜规则。一般来说,不同编译器都会遵从一致的传参规则,否则不同编译器产生的obj就无法匹配了。

(7)我们只有获得一个对象后,才能通过对象使用this指针。如果我们知道一个对象this指针的位置,可以直接使用吗?

this指针只有在成员函数中才有定义。因此,你获得一个对象后,也不能通过对象使用this指针。所以,我们无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以通过&this获得),也可以直接使用它。

答案: E。 I8cl8jvSBKHiYhtxnqrhKbIheRFH66r7OFqFe5jNqroarkWXkQnT/F3CZNGFt6Ut

点击中间区域
呼出菜单
上一章
目录
下一章
×

打开