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

第6章

预处理、const与sizeof

处理问题、const问题和sizeof问题是C++设计语言中的三大难点,也是各大企业面试中反复出现的问题。就sizeof问题而言,我们曾在十几家公司、几十套面试题目中发现它的存在。所以本章把这三大问题单独提出来,并结合详细的分析和解释来阐述各个知识点。

6.1 宏定义

面试例题1: 下面代码输出结果是多少?[美国著名搜索引擎公司G2012年秋季校园招聘题目]

A.array:1 6 3 4 5 6 7 8 9 10

B.array:6 2 3 4 5 6 7 8 9 10

C.程序可以正确编译,但是运行时会崩溃

D.程序语法错误,编译不成功

解析: 宏的那句被预处理器替换成了:*&array[5]-4=6;

由于减号比赋值优先级高,因此先处理减号;由于减号返回一个数而不是合法的值,所以编译报错。

答案 :C

扩展知识 :请思考#define SUB(x,y)(x-y)的情况。

面试例题2: 用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)。[美国某著名计算机嵌入式公司2005年面试题]

解析:

通过这道题面试官想考以下几个知识点:

●#define语法的基本知识(例如,不能以分号结束、括号的使用,等等)。

● 要懂得预处理器将为你计算常数表达式的值,因此,写出你是如何计算一年中有多少秒而不是计算出实际的值,会更有意义。

● 意识到这个表达式将使一个16位机的整型数溢出,因此要用到长整型符号L,告诉编译器这个常数是长整型数。

如果在表达式中用到UL(表示无符号长整型),那么你就有了一个好的起点。记住,第一印象很重要。

答案:

面试例题3: 写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个。[美国某著名计算机嵌入式公司2005年面试题]

解析

这个测试是为下面的目的而设的:

● 标识#define在宏中应用的基本知识。这是很重要的,因为直到嵌入(inline)操作符变为标准C的一部分,宏都是方便地产生嵌入代码的唯一方法。对于嵌入式系统来说,为了能达到要求的性能,嵌入代码经常是必须的方法。

● 三重条件操作符的知识。这个操作符存在C语言中的原因是它使得编译器能产生比if-then-else更优化的代码,了解这个用法是很重要的。

● 懂得在宏中小心地把参数用括号括起来。

答案:

#define MIN(A,B)((A)<=(B)?(A):(B))。

6.2 const

面试例题1: Which "const" modifier should be removed(下面哪个const应该被移除)?[美国某著名软件开发公司2013年面试题]

解析 :关于const修饰指针的情况,一般分为如下4种情况:

如何区别呢?

1)先看情况1。

如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。因此,1和2的情况相同,都是指针所指向的内容为常量(与const放在变量声明符中的位置无关),这种情况下不允许对内容进行更改操作。

换句话来说,如果a是一名仓库管理员的话,他所进入的仓库,里面的货物(*a)是他没权限允许动的,仓库里面的东西原来是什么就是什么;所以

但是也有别的办法去改变*a的值,一个是通过改变b的值:

还有一种改变*a办法就是a指向别处(管理员换个仓库):

对于情况1,可以先不进行初始化。因为虽然指针内容是常量,但指针本身不是常量。

2)情况2与情况1相同。

3)情况3为指针本身是常量,这种情况下不能对指针本身进行更改操作,而指针所指向的内容不是常量。

举例来说:如果a是一名仓库管理员的话,他只能进入指定的某仓库,而不能去别的仓库(所以a++是错误的);但这个仓库里面的货物(*a)是可以随便动的,(*a=600是正确的)。

此外,对于情况3:定义时必须同时初始化。

4)对于情况4为指针本身和指向的内容均为常量。那么这个仓库管理员只能去特定的仓库,并且仓库里面所有的货物他都没有权限去改变。

下面再说一下const成员函数是什么?

我们定义的类的成员函数中,常常有一些成员函数不改变类的数据成员,也就是说,这些函数是“只读”函数,而有一些函数要修改类数据成员的值。如果把不改变数据成员的函数都加上const关键字进行标识,显然,可提高程序的可读性。其实,它还能提高程序的可靠性,已定义成const的成员函数,一旦企图修改数据成员的值,则编译器按错误处理。

一些成员函数改变对象,例如:

一些成员函数不改变对象。

为了使成员函数的意义更加清楚,我们可在不改变对象的成员函数的函数原型中加上const,下面是定义const成员函数的一个实例:

如果GetY()试图用任何方式改变yVal或调用另一个非const成员函数,编译器将给出错误信息。任何不修改成员数据的函数都应该声明为const函数,这样有助于提高程序的可读性和可靠性。

如果把const放在函数声明前呢?因为这样做意味着函数的返回值是常量,意义就完全不同了。

本题中,选项A修饰函数返回值,表示返回的是指针所指向值是常量;选项B的const 这样的函数是常成员函数。常成员函数可以理解为是一个“只读”函数,它既不能更改数据成员的值,也不能调用那些能引起数据成员值变化的成员函数,只能调用const成员函数,把不会修改数据成员的函数GetBuffer声明为const类型。这大大提高了程序的健壮性。

选项D显然不对因为如果存在const BYTE*const m_pBuf;的情况,势必要进行初始化。

答案 :D。

面试例题2: const与#define相比有什么不同?

答案 :C++语言可以用const定义常量,也可以用#define定义常量,但是前者比后者有更多的优点:

● const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换中可能会产生意料不到的错误(边际效应)。

● 有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。在C++程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。

扩展知识

常量的引进是在早期的C++版本中,当时标准C规范正在制订。那时,常量被看作一个好的思想而被包含在C中。但是,C中的const的意思是“一个不能被改变的普通变量”。在C中,它总是占用内存,而且它的名字是全局符。C编译器不能把const看成一个编译期间的常量。在C中,如果写:

尽管看起来好像做了一件合理的事,但这将得到一个错误的结果。因为bufsize占用内存的某个地方,所以C编译器不知道它在编译时的值。在C语言中可以选择这样书写:

这样写在C++中是不对的,而C编译器则把它作为一个声明,这个声明指明在别的地方有内存分配。因为C默认const是外部连接的,C++默认const是内部连接的,这样,如果在C++中想完成与C中同样的事情,必须用extern把内部连接改成外部连接:

这种方法也可用在C语言中。在C语言中使用限定符const不是很有用,即使是在常数表达式里(必须在编译期间被求出)想使用一个已命名的值,使用const也不是很有用的。C迫使程序员在预处理器里使用#define。

面试例题3: 有类如下:

在上面这种情况下,如果要修改类的成员变量,应该怎么办?[美国著名软件企业GS公司2007年12月面试题]

解析 :在C++程序中,类里面的数据成员加上mutable后,修饰为const的成员变量,就可以修改它了,代码如下:

答案 :在const成员函数中,用mutable修饰成员变量名后,就可以修改类的成员变量了。

6.3 sizeof

面试例题1: What is the output of the following code?(下面代码的输出结果是什么?)[美国某著名计算机软硬件公司2005年、2007年面试题]

解析:

ss1是一个字符指针,指针的大小是一个定值,就是4字节,所以sizeof(ss1)是4字节。

ss2是一个字符数组,这个数组最初未定大小,由具体填充值来定。填充值是“0123456789”。1个字符所占空间是1字节,10个就是10字节,再加上隐含的“\0”,所以一共是11字节。

ss3也是一个字符数组,这个数组开始预分配100,所以它的大小一共是100字节。

ss4也是一个整型数组,这个数组开始预分配100,但每个整型变量所占空间是4,所以它的大小一共是400字节。

q1与ss2类似,所以是4字节。

q2里面有一个“\n”,“\n”算作一位,所以它的空间大小是3字节。

q3是一个字符指针,指针的大小是一个定值,就是4,所以sizeof(q3)是4字节。

A和B是两个结构体。在默认情况下,为了方便对结构体内元素的访问和管理,当结构体内的元素的长度都小于处理器的位数的时候,便以结构体里面最长的数据元素为对齐单位,也就是说,结构体的长度一定是最长的数据元素的整数倍。如果结构体内存在长度大于处理器位数的元素,那么就以处理器的位数为对齐单位。但是结构体内类型相同的连续元素和数组一样,将在连续的空间内。

结构体A中有3个short类型变量,各自以2字节对齐,结构体对齐参数按默认的8字节对齐,则a1、a2、a3都取2字节对齐,sizeof(A)为6,其也是2的整数倍。B中a1为4字节对齐,a2为2字节对齐,结构体默认对齐参数为8,则a1取4字节对齐,a2取2字节对齐;结构体大小为6字节,6不为4的整数倍,补空字节,增到8时,符合所有条件,则sizeof(B)为8。

CPU的优化规则大致原则是这样的:对于 n 字节的元素( n =2,4,8,…),它的首地址能被 n 整除,才能获得最好的性能。设计编译器的时候可以遵循这个原则:对于每一个变量,可以从当前位置向后找到第一个满足这个条件的地址作为首地址。例子比较特殊,因为即便采用这个原则,得到的结果也应该为6 字节(long 的首地址偏移量0000,short 首地址偏移量0004,都符合要求)。但是结构体一般会面临数组分配的问题。编译器为了优化这种情况,干脆把它的大小设为8字节,这样就没有麻烦了,否则的话,会出现单个结构体的大小为6字节,而大小为 n 的结构体数组大小却为8×( n -1)+6的尴尬局面。IBM出这道题并不是考查理解语言本身和编译器,而是考查应聘者对计算机底层机制的理解和设计程序的原则。也就是说,如果让你设计编译器,你将怎样解决内存对齐的问题。

答案:

4,11,100,400,4,3,4,6,8,4,4。

扩展知识(内存中的数据对齐)

数据对齐,是指数据所在的内存地址必须是该数据长度的整数倍。DWORD数据的内存起始地址能被4除尽,WORD数据的内存起始地址能被2除尽。x86 CPU能直接访问对齐的数据,当它试图访问一个未对齐的数据时,会在内部进行一系列的调整。这些调整对于程序来说是透明的,但是会降低运行速度,所以编译器在编译程序时会尽量保证数据对齐。同样一段代码,我们来看看用VC、Dev C++和LCC这3个不同的编译器编译出来的程序的执行结果:

这是用VC编译后的执行结果:

变量在内存中的顺序:b(1字节)—a(4字节)—c(4字节)。这是用Dev C++编译后的执行结果:

变量在内存中的顺序:c(4字节)—中间相隔3字节—b(占1字节)—a(4字节)。

这是用LCC编译后的执行结果:

变量在内存中的顺序:同上。

3个编译器都做到了数据对齐,但是后两个编译器显然没VC“聪明”,让一个char占了4字节,浪费内存。

面试例题2: 以下代码为32位机器编译,数据是以4字节为对齐单位,这两个类的输出结果为什么不同?[中国著名软件企业JS公司2008年4月面试题]

解析 :在访问内存时,如果地址按4字节对齐,则访问效率会高很多。这种现象的原因在于访问内存的硬件电路。一般情况下,地址总线总是按照对齐后的地址来访问的。例如你想得到0x00000001开始的4字节内容,系统首先需要以0x00000000读4字节,从中取得3字节,然后再用0x00000004作为开始地址,获得下一个4字节,再从中得到第一个字节,两次组合出你想得到的内容。但是如果地址一开始就是对齐到0x00000000,则系统只要一次读写即可。

考虑到性能方面,编译器会对结构进行对齐处理。考虑下面的结构:

直观地讲,这个结构的尺寸是sizeof(char)+sizeof(int)=6,但是在实际编译下,这个结构尺寸默认是8,因为第二个域iValue会被对齐到第4个字节。

在VC中,我们可以用pack预处理指令来禁止对齐调整。例如,下面的代码将使得结构尺寸更加紧凑,不会出现对齐到4字节问题:

对于这个pack指令的含义,大家可以查询MSDN。请注意,除非你觉得必须,否则不要轻易做这样的调整,因为这将降低程序的性能。目前比较常见的用法有两种,一是这个结构需要被直接写入文件;二是这个结构需要通过网络传给其他程序。

注意:字节对齐是编译时决定的,一旦决定则不会再改变,因此即使有对齐的因素在,也不会出现一个结构在运行时尺寸发生变化的情况。

在本题中,第一种类的数据对齐是下面的情况:

¦bool¦----¦----¦----¦

¦-------int---------¦

¦bool¦----¦----¦----¦

第二种类的数据对齐是下面的情况:

¦-------int---------¦

¦bool¦bool¦----¦----¦

所以类的大小分别是3×4和2×4。

答案 :B类输出12字节,C类输出8字节。

面试例题3 :求解下面程序的结果。[中国著名通信企业H公司面试题]

解析 :因为静态变量是存放在全局数据区的,而sizeof计算栈中分配的大小,是不会计算在内的,所以sizeof(A1)是4。

● 为了照顾数据对齐,int大小为4,char大小为1,所以sizeof(A2)是8。

● 为了照顾数据对齐,float大小为4,char大小为1,所以sizeof(A3)是8。

● 为了照顾数据对齐,float 大小为 4,int 大小为 4,char 大小为 1,所以 sizeof(A4)是12。

● 为了照顾数据对齐,double大小为8,float大小为4,int大小为4,char大小为1,所以sizeof(A5)是24。

答案 :4,8,8,12,24。

面试例题4: 说明sizeof和strlen之间的区别。

解析:

由以下几个例子我们说明sizeof和strlen之间的区别。

第1个例子:

sizeof(ss)结果为4,ss是指向字符串常量的字符指针。

sizeof(*ss)结果为1,*ss是第一个字符。

第2个例子:

sizeof(ss)结果为11,ss是数组,计算到“\0”位置,因此是(10+1)。

sizeof(*ss)结果为1,*ss是第一个字符。

第3个例子:

sizeof(ss)结果为100,ss表示在内存中预分配的大小,100×1。

strlen(ss)结果为10,它的内部实现是用一个循环计算字符串的长度,直到“\0”为止。

第4个例子:

sizeof(ss)结果为400,ss表示在内存中的大小,100×4。

strlen(ss)错误,strlen的参数只能是char*,且必须是以“\0”结尾的。

第5个例子:

cout<<sizeof(X)<<endl;结果为12,内存补齐。

cout<<sizeof(x)<<endl;结果为12,理由同上。

答案:

通过对sizeof与strlen的深入理解,得出两者区别如下:

(1)sizeof操作符的结果类型是size_t,它在头文件中的typedef为unsigned int类型。该类型保证能容纳实现所建立的最大对象的字节大小。

(2)sizeof是运算符,strlen是函数。

(3)sizeof可以用类型做参数,strlen只能用char*做参数,且必须是以“\0”结尾的。sizeof还可以用函数做参数,比如:

输出的结果是sizeof(short),即2。

(4)数组做sizeof的参数不退化,传递给strlen就退化为指针。

(5)大部分编译程序在编译的时候就把sizeof计算过了,是类型或是变量的长度。这就是sizeof(x)可以用来定义数组维数的原因:

(6)strlen的结果要在运行的时候才能计算出来,用来计算字符串的长度,而不是类型占内存的大小。

(7)sizeof后如果是类型必须加括号,如果是变量名可以不加括号。这是因为sizeof是个操作符而不是个函数。

(8)当使用了一个结构类型或变量时,sizeof返回实际的大小。当使用一静态的空间数组时,sizeof返回全部数组的尺寸。sizeof操作符不能返回被动态分配的数组或外部的数组的尺寸。

(9)数组作为参数传给函数时传递的是指针而不是数组,传递的是数组的首地址,如fun(char[8])、fun(char[])都等价于fun(char*)。在C++里传递数组永远都是传递指向数组首元素的指针,编译器不知道数组的大小。如果想在函数内知道数组的大小,需要这样做:进入函数后用memcpy将数组复制出来,长度由另一个形参传进去。代码如下:

(10)计算结构变量的大小就必须讨论数据对齐问题。为了使CPU存取的速度最快(这同CPU取数操作有关,详细的介绍可以参考一些计算机原理方面的书),C++在处理数据时经常把结构变量中的成员的大小按照4或8的倍数计算,这就叫数据对齐(data alignment)。这样做可能会浪费一些内存,但在理论上CPU速度快了。当然,这样的设置会在读写一些别的应用程序生成的数据文件或交换数据时带来不便。MS VC++中的对齐设定,有时候sizeof得到的与实际不等。一般在VC++中加上#pragma pack(n)的设定即可。或者如果要按字节存储,而不进行数据对齐,可以在Options对话框中修改Advanced Compiler选项卡中的“Data Alignment”为按字节对齐。

(11)sizeof操作符不能用于函数类型、不完全类型或位字段。不完全类型指具有未知存储大小数据的数据类型,如未知存储大小的数组类型、未知内容的结构或联合类型、void类型等。

面试例题5: 说明sizeof的使用场合。

答案:

(1)sizeof操作符的一个主要用途是与存储分配和I/O系统那样的例程进行通信。例如:

(2)用它可以看看某种类型的对象在内存中所占的单元字节。例如:

(3)在动态分配一对象时,可以让系统知道要分配多少内存。

(4)便于一些类型的扩充。在Windows中有很多结构类型就有一个专用的字段用来存放该类型的字节大小。

(5)由于操作数的字节数在实现时可能出现变化,建议在涉及操作数字节大小时用sizeof代替常量计算。

(6)如果操作数是函数中的数组形参或函数类型的形参,sizeof给出其指针的大小。

面试例题6: How many bytes will be occupied for the variable(definition:int**a[3][4])?(这个数组占据多大空间?)[中国某著名计算机金融软件公司2005年面试题]

A.64

B.12

C.48

D.128

解析 :sizeof问题,3×4×4=48。

答案 :C

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

解析 :sizeof问题。本题在2005、2007两年的面试题中都出现过。

程序运行后输出:TrendMicroSoftUS。这是因为sizeof(pStrArr1)运算得出的结果是指针pStrArr1的大小,即4。这样就不能正确地输出“USCN”。而字符串strArr1是由3段构成的,所以sizeof(strArr1)大小是12。

首先要明确sizeof不是函数,也不是一元运算符,它是个类似宏定义的特殊关键字,sizeof()。括号内的内容在编译过程中是不被编译的,而是被替代类型,如int a=8;sizeof(a)。在编译过程中,不管a的值是什么,只是被替换成类型sizeof(int),结果为4。如果sizeof(a=6)呢?也是一样地转换成a的类型。但是要注意,因为a=6是不被编译的,所以执行完sizeof(a=6)后,a的值还是8,是不变的。

请记住以下几个结论:

(1)unsigned影响的只是最高位bit的意义(正/负),数据长度是不会被改变的,所以:

(2)自定义类型的sizeof取值等同于它的类型原形。如:

(3)对函数使用sizeof,在编译阶段会被函数返回值的类型取代。如:int f1(){return 0;}。

(4)只要是指针,大小就是4。如:

(5)数组的大小是各维数的乘积×数组元素的大小。如:

数组a的大小在定义时未指定,编译时给它分配的空间是按照初始化的值确定的,也就是7,包括‘\0’。

所以上面的问题就解决得差不多了,sizeof(string)=4。

有的面试者说正确答案是将:

改为:

这样修改的结果能得到正确的输出结果,但却不是正确答案。sizeof(p)))只是指针大小为4,要想求出数组p指向数组的成员个数,应该为sizeof(*p)*2/sizeof(string)。这是因为指针p指向数组,则*p就是指向数组中的成员了,成员的类型是string型,那么sizeof(*p)为4,乘以2才是整个数组的大小。sizeof(p)的大小只是碰巧与sizeof(*p)大小一致罢了。

答案:

正确的程序如下:

面试例题8 :写出下面sizeof的答案。[德国某著名软件咨询企业2005年面试题]

解析 :求类base的大小。因为类base只有一个指针,所以类base的大小是4。Derive大小与base类似,所以也是4。

答案 :A,A。

面试例题9 :以下代码的输出结果是多少?[中国某著名计算机金融软件公司2006年面试题]

A.10

B.9

C.11

D.4

解析 :因为var[]等价于*var,已经退化成一个指针了,所以大小是4。

答案 :D。

面试例题10 :以下代码的输出结果是多少?[美国某著名防毒软件公司2006年面试题]

解析 :float f占4个字节,char p占1字节,int adf[3]占12字节,总共是17字节。根据内存对齐原则,要选择4的倍数,是20字节。

答案 :20

面试例题11 :一个空类占多少空间?多重继承的空类呢?[英国某著名计算机图形图像公司面试题]

解析 :我们用程序来实现一个空类和一个多重继承的空类。看看它们的大小是多少。代码如下:

以上答案分别是:1,1,4,1。这说明空类所占空间为1,单一继承的空类空间也为1,多重继承的空类空间还是1。但是虚继承涉及虚表(虚指针),所以sizeof(C)的大小为4。

答案 :一个空类所占空间为1,多重继承的空类所占空间还是1。

6.4 内联函数和宏定义

面试例题: 内联函数和宏的差别是什么?

答案 :内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中。而宏只是一个简单的替换。

内联函数要做参数类型检查,这是内联函数跟宏相比的优势。

inline是指嵌入代码,就是在调用函数的地方不是跳转,而是把代码直接写到那里去。对于短小的代码来说inline增加空间消耗换来的是效率提高,这方面和宏是一模一样的,但是inline在和宏相比没有付出任何额外代价的情况下更安全。至于是否需要inline函数,就需要根据实际情况来取舍了。

inline一般只用于如下情况:

(1)一个函数不断被重复调用。

(2)函数只有简单的几行,且函数内不包含for、while、switch语句。

一般来说,我们写小程序没有必要定义成inline,但是如果要完成一个工程项目,当一个简单函数被调用多次时,则应该考虑用inline。

宏在C语言里极其重要,而在C++里用得就少多了。关于宏的第一规则是绝不应该去使用它,除非你不得不这样做。几乎每个宏都表明了程序设计语言里、程序里或者程序员的一个缺陷,因为它将在编译器看到程序的正文之前重新摆布这些正文。宏也是许多程序设计工具的主要麻烦。所以,如果你使用了宏,就应该准备只能从各种工具(如排错系统、交叉引用系统、轮廓程序等)中得到较少的服务。

宏是在代码处不加任何验证的简单替代,而内联函数是将代码直接插入调用处,而减少了普通函数调用时的资源消耗。

宏不是函数,只是在编译前(编译预处理阶段)将程序中有关字符串替换成宏体。

关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。如下风格的函数Foo 不能成为内联函数:

而如下风格的函数Foo 则成为内联函数:

所以说,inline 是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。内联能提高函数的执行效率,至于为什么不把所有的函数都定义成内联函数?如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。

以下情况不宜使用内联:1 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。2 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义体放在类声明中。一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了inline不应该出现在函数的声明中)。 nLpvfsmdu+W33KxiSsKT+AKfbC+BjSNcdiMfFxc0cOptNt2cr7aMN30Day8DEF2g

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