1、程序员面试-2 及答案解析(总分:100.00,做题时间:90 分钟)一、论述题(总题数:28,分数:100.00)1.strlen(“/0“)=?sizeof(“/0“)=? (分数:3.00)_2.对于结构体而言,为什么 sizeof 返回的值一般大于期望值 (分数:3.00)_3.指针进行强制类型转换后与地址进行加法运算,结果是什么 (分数:3.00)_4.使用指针有哪些好处 (分数:3.00)_5.引用还是指针 (分数:3.00)_6.指针和数组是否表示同一概念 (分数:3.00)_7.指针是否可进行、=、=、=运算 (分数:3.00)_8.指针与数字相加的结果是什么 (分数:3.00
2、)_9.野指针?空指针 (分数:3.00)_10.C/C+头文件中的 ifndef/define/endif 的作用有哪些 (分数:3.00)_11.#includefilename.h和#include“filename.h“有什么区别 (分数:3.00)_12.#define 有哪些缺陷 (分数:3.00)_13.如何使用 define 声明一个常数,用以表明 1 年中有多少秒(忽略闰年问题) (分数:4.00)_14.含参数的宏与函数有什么区别 (分数:4.00)_15.宏定义平方运算#define SQR(X)X*X 是否正确执行平方运算的宏定义不正确,会造成错误。下面以如下程序代码为
3、例进行分析。 (分数:4.00)_16.不能使用大于、小于、if 语句,如何定义一个宏来比较两个数 a、b 的大小 (分数:4.00)_17.如何判断一个变量是有符号数还是无符号数 (分数:4.00)_18.#define TRACE(S)(printf(“%s/n“,#s),s)是什么意思 (分数:4.00)_19.不使用 sizeof,如何求 int 占用的字节数 (分数:4.00)_20.如何使用宏求结构体的内存偏移地址 (分数:4.00)_21.如何用 sizeof 判断数组中有多少个元素 (分数:4.00)_22.枚举和 define 有什么不同 (分数:4.00)_23.typde
4、f 和 define 有什么区别 (分数:4.00)_24.C+中宏定义与内联函数有什么区别 (分数:4.00)_25.定义常量谁更好?#define 还是 const (分数:4.00)_26.C 语言中 struct 与 union 的区别是什么 (分数:4.00)_27.C 和 C+中 struct 的区别是什么 (分数:4.00)_28.C+中 struct 与 class 的区别是什么 (分数:4.00)_程序员面试-2 答案解析(总分:100.00,做题时间:90 分钟)一、论述题(总题数:28,分数:100.00)1.strlen(“/0“)=?sizeof(“/0“)=? (分
5、数:3.00)_正确答案:()解析:strlen(“/0“)=0,sizeof(“/0“)=2。 strlen 执行的是一个计数器的工作,它从内存的某个位置(可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描,直到碰到第一个字符串结束符“/0“为止,然后返回计数器值。 sizeof 是 C 语言的关键字,它以字节的形式给出了其操作数的存储大小,操作数可以是一个表达式或括在括号内的类型名,操作数的存储大小由操作数的类型决定。 具体而言,strlen 与 sizeof 的差别表现在以下几个方面: 1)sizeof 是关键字,而 strlen 是函数。sizeof 后如果是类型必须
6、加括弧,如果是变量名可以不加括弧。 2)sizeof 操作符的结果类型是 size_t,它在头文件中 typedef 为 unsigned int 类型。该类型保证能够容纳实现所建立的最大对象的字节大小。 3)sizeof 可以用类型作为参数,strlen 只能用 char*做参数,而且必须是以“/0“结尾的。sizeof 还可以以函数作为参数,如 int g(),则 sizeofig()的值等于 sizeof(int)的值,在 32 位计算机下,该值为4。 4)当数组名做 sizeof 的参数时不退化,传递给 strlen 就退化为指针了。以数组 char a10为例,在 32位机器下,si
7、zeof(a)=1*10=10,而传递给 strlen 就不一样了。 5)大部分编译程序的 sizeof 都是在编译的时候计算的,所以可以通过 sizeof(x)来定义数组维数。而strlen 的计算则是在运行期计算的,用来计算字符串的实际长度,不是类型占内存的大小。例如,char str20=“0123456789“,字符数组 str 是编译期大小已经固定的数组,在 32 位机器下,为 1*20=20,而其 strlen 大小则是在运行期确定的,所以其值为字符串的实际长度 10。 6)当用于计算一个结构类型或变量的 sizeof 时,返回实际的大小,当用于计算一个静态变量或数组时,返回整个数
8、组所占用的大小,而 sizeof 不能返回动态数组大小。 7)数组作为参数传给函数时传的是指针而不是数组,传递的是数组的首地址。例如: fun(char8) fun(char) 都等价于 fun(char*)。在 C+里参数传递数组永远都是传递指向数组首元素的指针,编译器不知道数组的大小,如果想在函数内知道数组的大小,需要这样做:进入函数后用 memcpy 复制出来,长度由另一个形参传进去。 fun(unsiged char*p1,int len) unsigned char* buf=new unsigned charlen+1; memcpy(buf,p1,len); 程序示例 1: #i
9、ncludestdio.h #includestring.h int main() char arr10=“Hello“; printf(“%d/n“,strlen(arr); printf(“%d/n“,sizeof(arr); return 0; 程序输出结果: 5 10 sizeof 返回定义的 arr 数组时,编译器为其分配的数组空间大小不关心里面存了多少数据。strlen 只关心存储的数据内容,不关心空间的大小和类型。 程序示例 2: #includestdio.h #includestring.h int main() char* parr=new char10; printf(“
10、%d/n“,strlen(parr); printf(“%d/n“,sizeof(parr); printf(“%d/n“,sizeof(*parr); return 0; 程序输出结果: 14 4 1 在上例中,程序定义了一个字符指针 parr,它指向一个分配了 10 个空间的字符数组,由于没有进行初始化,根据 strlen 的计算原理,所以不能够确定 sterlen(parr)的值,因为无法确定字符串的终止位置,所以该值为一个随机值,本例中输出为 14。在 32 位机器下,parr 为一个指针,所以 sizeof(parr)的值为 4,parr 为指向字符的指针,所以 sizeof(*pa
11、rr)的值为 1。2.对于结构体而言,为什么 sizeof 返回的值一般大于期望值 (分数:3.00)_正确答案:()解析:struct 是一种复合数据类型,其构成元素既可以是基本数据类型,如int、double、float、short、char 等,也可以是复合数据类型,如数组、struct、union 等数据单元。 一般而言,struct 的 sizeof 是所有成员对齐后长度相加,而 union 的 sizeof 是取最大的成员长度。 在结构中,编译器为结构的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址
12、相同。 字节对齐也称为字节填充,它是 C+编译器的一种技术手段,主要是为了在空间与复杂度上达到平衡。简单地讲,是为了在可接受的空间浪费的前提下,尽可能地提高对相同运算过程的最少(快)处理。字节对齐的作用不仅是便于 CPU 的快速访问,使 CPU 的性能达到最佳,而且可以有效地节省存储空间。例如,32位的计算机的数据传输值是 4 字节,64 位计算机数据传输是 8 字节,这样 struct 在默认的情况上,编译器会对 struct 的结构进行(32 位机)4 的倍数或(64 位机)8 的倍数的数据对齐。对于 32 位机来说,4 字节对齐能够使 CPU 访问速度提高,比如说一个 long 类型的变
13、量,如果跨越了 4 字节边界存储,那么 CPU 要读取两次,这样效率就低了,但需要注意的是,如果在 32 位机中使用 1 字节或者 2 字节对齐,不仅不会提高效率,反而会使变量访问速度降低。 在默认情况下,编译器为每一个变量或数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变默认的对界条件: 1)使用伪指令#pragma pack(n),C 编译器将按照 n 个字节对齐。 2)使用伪指令#pragma pack(),取消自定义字节对齐方式。 3)另外,还有如下的一种方式:_attribute(aligned(n),让所作用的结构成员对齐在 n 字节自然边界上。如果结构中有成员的
14、长度大于 n,则按照最大成员的长度来对齐。_attribute_(packed),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。 例如如下数据结构: struct test charx1; short x2; float x3; char x4; ; 由于编译器默认情况下会对 struct 作边界对齐,结构的第一个成员 x1,其偏移地址为 0,占据了第 1 个字节,第二个成员 x2 为 short 类型,其起始地址必须 2 字节对齐,因此编译器在 x2 和 x1 之间填充了一个空字节。结构的第三个成员 x3 和第四个成员 x4 恰好落在其自然边界地址上,在它们前面不需要额外的填充
15、字节。在 test 结构中,成员 x3 要求 4 字节对界,是该结构所有成员中要求的最大边界单元,因而test 结构的自然对界条件为 4 字节,编译器在成员 x4 后面填充了 3 个空字节。整个结构所占据空间为 12字节。 再例如如下数据结构: struct s1 short d; int a; short b; a1; 在 32 位机器下,short 型占两个字节,int 型占 4 个字节,所以为了满足字节对齐,变量 d 除了自身所占用的两个字节外,还需要再填充两个字节,变量 a 占用 4 个字节,变量 b 除了自身占用的两个字节,还需要两个填充字节,所以最终 s1 的 sizeof 为 1
16、2。 字节对齐的细节和编译器实现相关,但一般而言,满足以下 3 个准则: 1)结构体变量的首地址能够被其最宽基本类型成员的大小所整除。 2)结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍。如有需要,编译器会在成员之间加上填充字节。 3)结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。 需要注意的是,基本类型是指前面提到的像 char、short、int、float、double 这样的内置数据类型,这里所说的“数据宽度”就是指其 sizeof 的大小,在 32 位机器上,这些基本数据类型的 sizeof 大小分别
17、为 1、2、4、4、8。由于结构体的成员可以是复合类型,所以在寻找最宽基本类型成员时,应当包括复合类型成员的子成员,而不是把复合成员看成是一个整体。如果一个结构体中包含另外一个结构体成员,那么此时最宽基本类型成员不是该结构体成员,而是取基本类型的最宽值。但在确定复合类型成员的偏移位置时,则是将复合类型作为整体看待,即复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度,达到程序优化的目的。3.指针进行强制类型转换后与地址进行加法运算,结果是什么 (分数:3.00)_正确答案:()解析:假设在 32 位机器上,在对齐为 4 的情况下,sizeof(lo
18、ng)的结果为 4 字节,sizeof(char*)的结果为 4 字节,sizeof(short int)的结果与 sizeoflshort)的结果都为 2 字节,sizeof(char)的结果为 1 字节,sizeof(int)的结果为 4 字节,由于 32 位机器上是 4 字节对齐,以如下结构体为例: struct BBB long num; char *name; short int data; char ha; short ba5; *p; 当 p=0x1000000;则 p+0x200=?(Ulong)p+0x200=?(char*)p+0x200=? 其实,在 32 位机器下,si
19、zeof(struct BBB)=sizeof(*p)=4+4+2+1+1/*补齐*/+2*5+2/*补齐*/=24 字节,而 p=0x1000000,那么 p+0x200=0x1000000+0x200*24 指针加法,加出来的是指针类型的字节长度的整倍数,就是 p 偏移 sizeof(p)*0x200。 (Ulong)p+0x200=0x1000000+0x200 经过 Ulong 后,已经不再是指针加法,而变成一个数值加法了。 (char*)p+0x200=0x1000000+0x200*sizeof(char)结果类型是 char*。4.使用指针有哪些好处 (分数:3.00)_正确答案
20、:()解析:指针与其他类型变量一样,不同之处在于一般的变量包含的是实际的真实数据,而指针包含的是一个指向内存中某个位置的地址。指针好处众多,一般而言,使用指针有以下几个方面的好处: 1)可以动态分配内存。 2)进行多个相似变量的一般访问。 3)为动态数据结构,尤其是树和链表,提供支持。 4)遍历数组,如解析字符串。 5)高效地按引用“复制”数组与结构,特别是作为函数参数的时候,可以按照引用传递函数参数,提高开发效率。5.引用还是指针 (分数:3.00)_正确答案:()解析:程序设计中的引用其实就是别名的意思,它用于定义一个变量来共享另一个变量的内存空间,变量是一个内存空间的名字,如果给内存空间
21、起另外一个名字,那就能够共享这个内存了,进而提高程序的开发效率。指针指向另一个内存空间的变量,可以通过它来索引另一个内存空间的内容,而指针本身也有自己的内存空间。 引用与指针有着相同的地方,即指针指向一块内存,它的内容是所指内存的地址,引用是某块内存的别名。但是,两者并非完全相同,它们之间也存在着差别,具体表现在以下几个方面: 1)从本质上讲,指针是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,即其所指向的地址可以被改变,其指向的地址中所存放的数据也可以被改变。而引用则只是一个别名而已,它在逻辑上不是独立的,它的存在具有依附性,所以弓 l 用必须在一开始就被初始化,而且引用的对象在其
22、整个生命周期中是不能被改变的,即自始至终只能依附于同一个变量,具有“从一而终”的特性。 2)作为参数传递时,两者不同。在 C+语言中,指针与引用都可以用于函数的参数传递,但是指针传递参数和引用传递参数有着本质的不同。指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值。而在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时
23、存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。虽然它们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针的指针,或者指针引用。 3)引用使用时不需要解引用(*),而指针需要解引用。 4)引用只能在定义时被初始化一次,之后不能被
24、改变,即引用具有“从一而终”的特性。而指针却是可变的,指针的初始化不是指指针的定义,而是指针变量存储的数值是个无效的数值。例如,定义 float a,该语句表示 a 会分配一个地址,但初始值是一个随机的值,同样,float *a 也会为 a 分配一个地址,初始值也是随机的值,初始化可以将 a=NULL,这样在以后的程序中可以增加 if(a=NULL)来判断指针是否有效,否则不行,或者为指针分配或者指定空间,如 float *a=new float 或者 float b; float *a= int * int b=8; p= 正确,指针变量的引用 void 不正确,没有变量或对象的类型是 vo
25、id int 不正确,有空指针,无空引用 2)int 不正确,不能用类型来初始化 int *p=new int; int 正确 3)引用不同于一般变量,下面的类型声明是非法的: int 不能建立引用数组 int 不能建立指向引用的指针 int 不能建立引用的引用 4)当使用 char *p=a; 在 32 位机器上,sizeof(a)=12 字节,而 sizeof(p)=4 字节。 但需要注意的是,当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。 void Func(char a100) coutsizeof(a); 此时 sizeof(a)=sizeof(int)=4,而不是
26、sizeof(int)*100=400。7.指针是否可进行、=、=、=运算 (分数:3.00)_正确答案:()解析:不可以。对于指针而言,只能进行=和!=运算。8.指针与数字相加的结果是什么 (分数:3.00)_正确答案:()解析:为了更有说服力地解释本题结果,首先将题目程序完善,在 VC+6.0 的环境下编译运行,程序源代码如下: #includestdio.h int main() unsigned char *p1; unsigned long *p2; p1=(unsigned char*)0x801000; p2=(unsigned long*)0x810000; printf(“%
27、x/n“,p1+5); printf(“%x/n“,p2+5); return 0; 程序输出结果如下: 801005 810014 p1=(unsigned char*)0x801000,是给指针变量赋值,把十六进制 0x801000 放到字符指针变量中,即指针变量 p1 的值就是 0x801000。 p2=(unsigned long*)0x810000,也是给指针变量赋值,同上。 输出结果 p1+5 的值是 801005,因为指针变量指向的值字符加 1 表示指针向后移动 1 个字节,那么加 5 代表向后移动 5 个字节,所以输出 801005。 p2+5 的值是 801016,因为指针变
28、量指向的是长整型,加 1 表示指针向后移动 4 个字节,那么加 5 代表向后移动 54=20 个字节,所以输入 810014(十六进制)。 需要注意的是,内存的基本单位是字节,它以字节为存储单位储存,每个字节是 8 个二进制位,即 8 个bit。9.野指针?空指针 (分数:3.00)_正确答案:()解析:野指针是指指向不可用内存的指针。任何指针变量在被创建时,不会自动成为 NULL 指针(空指针),其默认值是随机的,所以指针变量在创建的同时应当被初始化,或者将指针设置为 NULL,或者让它指向合法的内存,而不应该放之不理,否则就会成为野指针。而同时由于指针被释放(free 或 delete)后
29、,未能将其设置为 NULL,也会导致该指针变为野指针。虽然 free 和 delete 把指针所指的内存给释放掉了,但它们并没有把指针本身释放掉,一般可以采用语句 if(p!=NULL)进行防错处理,但是 if 语句却起不到防错作用,因为即使 p 不是 NULL 指针,它也不指向合法的内存块。第三种造成野指针的原因是指针操作超越了变量的作用范围。 程序示例如下: #includestdio.h #includestdlib.h #includestring.h int main() chal *p=(char*)malloc(100); strcpy(p,“hello“); free(p);
30、if(p!=NULL) printf(“Not NULL/n); return 0; 程序输出: Not NULL 上例中,虽然对 p 执行了 free 操作,p 所指的内存被释放掉了,但是 p 所指的地址仍然不变,在后续的判断 p 是否为 NULL 时,根本没有起到防错的作用,所以程序输出仍然为 Not NULL。 空指针是一个特殊的指针,也是唯一一个对任何指针类型都合法的指针。指针变量具有空指针值,表示它当时处于闲置状态,没有指向有意义的内容。为了提高程序的可读性,标准库定义了一个与 0 等价的符号常量 NULL,程序里可以写 p=0 或者 p=NULL,两种写法都把 p 置为空指针值。C
31、 语言保证这个值不会是任何对象的地址。给指针值赋零则使它不再指向任何有意义的东西。 作为一种风格,很多程序员一般不愿意在程序中到处出现未加修饰的 0,所以习惯定义预处理宏 NULL(在stdio.h和其他几个头文件中)为空指针常数,通常是 0 或者(void*)0)。希望区别整数 0 和空指针 0的程序员可以在需要空指针的地方使用 NULL。 通用指针可以指向任何类型的变量。通用指针的类型用(void*)表示,因此也称为 void 指针。 程序代码如下: #includestdio.h int main() int n=3, *p; void *gp; gp= p=(int *)gp; pri
32、ntf(“%d/n“,*p); return 0; 程序输出结果: 310.C/C+头文件中的 ifndef/define/endif 的作用有哪些 (分数:3.00)_正确答案:()解析:如果一个项目中存在两个 C 文件,而这两个 C 文件都 include(包含)了同一个头文件,当编译时,这两个 C 文件要一同编译成一个可运行文件,可能会产生大量的声明冲突。而解决的办法是把头文件的内容都放在#ifndef 和#endif 中,一般格式如下: #ifndef标识 #define标识 . . #endif 上述代码的作用是当“当标识没有由#define 定义过时,则定义标识。标识在理论上来说可
33、以是自由命名的,但每个头文件的这个“标识”都应该是唯一的。标识的命名规则一般是头文件名全大写,前后加下画线,并把文件名中的“.”也变成下画线,如 stdio.h。 #ifndef_STDIO_H_ #define_STDIO_H_ #endif 在#ifndef 中定义变量出现的问题(一般不定义在#ifndef 中)如下所示: #ifndef AAA #define AAA . int i; . #endif 里面有一个变量定义,在 VC 中链接时就出现了 i 重复定义的错误,而在 C 语言中成功编译。11.#includefilename.h和#include“filename.h“有什么区
34、别 (分数:3.00)_正确答案:()解析:对于#includefilename.h,编译器先从标准库路径开始搜索 filename.h,使得系统文件调用较快。而对于#include “filename.h“,编译器先从用户的工作路径开始搜索 filename.h,然后去寻找系统路径,使得自定义文件较快。 引申:头文件的作用有哪些? 头文件的作用主要表现为以下两个方面: 1)通过头文件来调用库功能。出于对源代码保密的考虑,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口是怎么实现的。编译器会从库中提取相应的代码。
35、 2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,大大减轻程序员调试、改错的负担。12.#define 有哪些缺陷 (分数:3.00)_正确答案:()解析:由于宏定义在预处理阶段进行,主要做的是字符替换工作,所以它存在着一些固有的缺陷: 1)它无法进行类型检查。宏定义是在编译前进行字符的替换,因为还没编译,不能编译前就检查好类型是否匹配,而只能在编译时才知道,所以不具备类型检查功能。 2)由于优先级的不同,使用宏定义时,可能会存在副作用。例如,执行加法操作的宏定义运算#defne ADD(a,b)a+b 在使用的过程中,对于表达式的
36、运算就可能存在潜在的问题,而应该改为#define ADD(a,b)(a)+(b)。 3)无法单步调试。 4)会导致代码膨胀。由于宏定义是文本替换,需要对代码进行展开,相比较函数调用的方式,会存在较多的冗余代码。 5)在 C+中,使用宏无法操作类的私有数据成员。13.如何使用 define 声明一个常数,用以表明 1 年中有多少秒(忽略闰年问题) (分数:4.00)_正确答案:()解析:#define SECOND_PER_YEAR(60*60*24*365)UL 在以上定义中,需要注意以下 3 个方面的内容: 1)由于宏定义是预处理指令,而非语句,所以在进行宏定义时,不能以分号结束。 2)预
37、处理只会执行简单的替换,不会计算表达式的值,所以需要注意括号的使用,直接写出是如何计算出一年中有多少秒而不是计算出实际的值。 例如: #define N4+5 cout2*N; 如果预处理计算表达式的值,那么输出结果应该是 2x(4+5),等于 18,可是实际输出结果却是 2x4+5,等于 13。 3)考虑到可能存在数据溢出问题,更加规范化的写法是使用长整型,即 UL 类型,告诉编译器这个常数是长整型数。14.含参数的宏与函数有什么区别 (分数:4.00)_正确答案:()解析:含参数的宏有时完成的是函数实现的功能,但是并非所有的函数都可以被含参数的宏所替代。具体而言,含参数的宏与函数的特点如下: 1)函数调用时,首先求出实参表达式的值,然后带入形参。而使用带参的宏只是进行简单的字符替换。 2)函数调用是在程序运行时处理的,它需要分配临时的内存单元;而宏展开则是在编译时进行的,在展开时并不分配内存单元,也不进行值