《程序员面试宝典第四版》笔记1

时间:2022-11-21 15:04:41

该篇文章主要记录一下《程序员面试宝典第四版》典型题目。

5.1 赋值语句

面试例题1 下列C++代码的输出结果是什么?

#include<iostream>
using namespace std;
int i = 1;
int main()
{
int i = i;
cout << i << endl;
}
A.The i within main will have an undefined value.(main()里的i是一个未定义值)
B.The i within main will have a value of 1.(main()里的i值为1)
C.The compiler will not allow this statement.(编译器不允许这种写法)
D.The i within main will have a value of 0.(main()里的i值为0)
解析:答案A。当面试者看到int i=i;时,也许第一反应就是怎么有这么诡异的代码?但是在C++中这样做是完全合法的(但显然不合理)。
 int i=i,i变量从声明的那一刻开始就是可见的了,main()里的i不是1,因为它和main()外的i无关,而是一个未定义值。
5.2 i++
面试例题2 以下代码的输出结果是什么?

#include<iostream>
using namespace std;
int main()
{
int arr[] = { 6, 7, 8, 9, 10 };
int *ptr = arr;
*(ptr++) += 123;
printf("%d,%d\n", *ptr, *(++ptr));
}
解析:C中printf计算参数时是从右到左压栈的。
printf("%d\n ",*ptr);此时ptr应指向第一个元素6。
*(ptr++)+=123应为*ptr=*ptr+123;ptr++,此时ptr应指向第二个元素7。
printf("%d\n ",*(ptr-1));此时输出第一个元素129,注意此时是经过计算的。
printf("%d\n ",*ptr);此时输出第二个元素7,此时ptr还是指向第二个元素7。

printf("%d,%d\n ",*ptr,*(++ptr));从右到左运算,第一个是(++ptr),也就是ptr++,*ptr=8,此时ptr指向第三个元素8,所以全部为8。

5.4 类型转换
面试例题1:下面程序的结果是多少?

#include<iostream>
using namespace std;
int main()
{
float a = 1.0f;
cout << (int)a << endl;
cout << &a << endl;
cout << (int&)a << endl;
cout << boolalpha << ((int)a == (int&)a) << endl;//false

float b = 0.0f;
cout << (int)b << endl;
cout << &b << endl;
cout << (int&)b << endl;
cout << boolalpha << ((int)b == (int&)b) << endl;//true
}
解析:
说明要点:
1、float b=1.0f;int&a=b;这里b=1,a=1065353216,为什么呢,因为b是浮点型变量,浮点数1在内存中的存储格式为
符号位(1位) 阶(8位) 尾数(23位)
0           01111111  00000000000000000000000
二进制00111111100000000000000000000000转换为十进制整数为1065353216,所以a=1065353216。
2、(int)&a;这里&a指的是a的地址,前面的int是将十六进制表示的地址强制转换为int类型,这与int&a是不一样的。
3、inta[3]={3,5,6};
这里&a[0]加1并不是17300052+1,因为&a[0]是指针类型,指针类型加n表示加上指针所指类型所占内存乘以n。
a+2和&a[0]+2都是数组首地址+2*4,因为a和&a[0]都是指向数组首地址的指针,int类型占用4位,所以就是加上4*2。
而为什么&a+2是17300076呢,因为&a和a不一样,&a是整个数组的地址,a的类型是int*,&a的类型是int(*)[5],整个数组占用12位,故&a+2=17300052+2*12=17300076。
因此,其实,(int&)a就是*(int*)(&a)
首先对float型变量取地址
强制类型转换为整型变量的地址(地址的值并没有变)
将该地址指向的变量输出(但是由于整型和浮点型数据存储方式的不同,输出结果是不同的)

面试例题2:下面程序的结果是多少?

#include<iostream>
using namespace std;
int main()
{
unsigned int a = 0xFFFFFFF7;
unsigned char i = (unsigned char)a;
char *b = (char*)&a;
printf("%08x,%08x", i, *b);
}
解析:答案:000000f7,fffffff7。unsigned int变量赋值给unsigned char变量时会发生字节截断。那么第二个数,也就是char* b=(char*)&a中a本身为一个uint类型的值,把它的地址赋给一个执行char类型数据的指针。 char类型的长度只有一个字节,打印char类型的指针指向的值会是多少?
&a的结果是一个指针,它的类型取决于a的类型,此处&a的类型应该是:
unsigned int *;
char *b=(char*)&a;
上面等价于:
unsigned int *p=&a;
char *b=(char*)p;
上面的步骤就是将一个unsigned int型的指针强制转换成一个char型的指针。所以请注意:这里是char类型的指针转换,而不是char类型的转换。

这样转换后,假设a的地址是x:p+1=x+1*sizeof(int)=x+4;b+1=x+1*sizeof(char)=x+1;影响的是指针的寻址。

5.5 运算符问题
面试例题1:下面程序的结果是多少?

#include<iostream>
using namespace std;
int main()
{
unsigned char a = 0xA5;
unsigned char b = ~a >> 4 + 1;
printf("b=%d\n", b);
}
因为“~”的优先级高于“>>”和“+”,本题的过程是这样的:先对于10100101取反0101 1010;再右移,这里有一个问题,是先右移4位再加1呢,还是直接右移5(4+1)位。 因为“+”的优先级高于“>>”,所以直接右移5位。 结果是0000 0010。最后的结果应该是2才对,但把如上的指令放到vs2008中运行,答案居然是250。那么到底是什么地方出了问题?在调试的过程中进入汇编指令。 可以看到高级语句转换为汇编语言以后,是先执行取反再位移的。 我们看到eax是16位的寄存器,于是在机器中0xA5的寄存中表达是0000 0000 1010 0101 ,取反是1111 1111 0101 1010,那么右移5位是0000 0111 1111 1010,由于是unsigned char型的只能表示低8位的数值,即250。

面试例题4:利用位运算实现两个整数的加法运算,请用代码实现。

#include<iostream>
using namespace std;
int AddTwo(int a, int b)
{
if (b == 0)
return a;
if (a == 0)
return b;
int t1 = (a&b) << 1;
int t2 = a^b;
return AddTwo(t1, t2);
}
int main()
{
int a = 134;
int b = 456;
int res = AddTwo(a, b);
cout << res << endl;
}

5.6 a、 b交换与比较
面试例题1:有两个变量a和b,不用“if”、 “?:”、“switch”或其他判断语句,找出两个数中间比较大的。

#include<iostream>
using namespace std;
#include<cmath>
int main()
{
int a = 134;
int b = 456;
int max = (a + b + abs(a - b)) / 2;
cout << max << endl;
}

5.7 C和C++的关系
面试例题1:在C++程序中调用被C编译器编译后的函数,为什么要加extern "C"?

答案:C++语言支持函数重载,C语言不支持函数重载。 函数被C++编译后在库中的名字与C语言的不同。 假设某个函数的原型为void foo(int x, int y)。 该函数被C编译器编译后在库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。C++提供了C连接交换指定符号extern "C"解决名字匹配问题。

面试例题3:评价一下C与C++的各自特点。 如果一个程序既需要大量运算,又要有一个好的用户界面,还需要与其他软件大量交流,应该怎样选择合适的语言?
答案:C是一种结构化语言,重点在于算法和数据结构。 C程序的设计首先考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现过程(事务)控
制)。 而对于C++,首先考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现过程(事务)控制。
对于大规模数值运算,C/C++和Java/.NET之间没有明显的性能差异。 不过,如果运算设计向量计算、 矩阵运算,可以使用FORTRAN或者MATLAB编写计算组件(如COM)。大规模用户界面相关的软件可以考虑使用.NET进行开发(Windows环境下),而且.NET同COM之间的互操作十分容易,同时.NET对数据库访问的支持也相当好。

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

解析:由以下几个例子我们说明sizeof和strlen之间的区别。
第1个例子:char *ss="0123456789"
sizeof(ss)结果为4,ss是指向字符串常量的字符指针。
sizeof(*ss)结果为1,*ss是第一个字符。
第2个例子:char ss[]="0123456789"
sizeof(ss)结果为11,ss是数组,计算到“\0”位置,因此是(10+1)。
sizeof(*ss)结果为1,*ss是第一个字符。
第3个例子:char ss[100]="0123456789"
sizeof(ss)结果为100,ss表示在内存中预分配的大小,100×1。
strlen(ss)结果为10,它的内部实现是用一个循环计算字符串的长度,直到“\0”为止。
第4个例子:int ss[100]="0123456789"
sizeof(ss)结果为400,ss表示在内存中的大小,100×4。
strlen(ss)错误,strlen的参数只能是char*,且必须是以“\0”结尾的。
第5个例子:
class X{
int i;
int j;
char k;
}
X x;
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还可以用函数做参数,比如:short f();cout<<sizeof(f());输出的结果是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++里传递数组永远都是传递指向数组首
元素的指针,编译器不知道数组的大小。 
(10)计算结构变量的大小就必须讨论数据对齐问题。 为了使CPU存取的速度最快(这同CPU取数操作有关,详细的介绍可以参考一些计算机原理方面的书),C++在处理数据时
经常把结构变量中的成员的大小按照4或8的倍数计算,这就叫数据对齐(data alignment)。这样做可能会浪费一些内存,但在理论上CPU速度快了。 当然,这样的设置会在读写一些别
的应用程序生成的数据文件或交换数据时带来不便。 MS VC++中的对齐设定,有时候sizeof得到的与实际不等。 一般在VC++中加上#pragma pack(n)的设定即可。 或者如果要按字节存
储,而不进行数据对齐,可以在Options对话框中修改Advanced Compiler选项卡中的“DataAlignment”为按字节对齐。
(11)sizeof操作符不能用于函数类型、 不完全类型或位字段。 不完全类型指具有未知存储大小数据的数据类型,如未知存储大小的数组类型、 未知内容的结构或联合类型、 void类型等。

面试例题5:说明sizeof的使用场合。
答案:
(1)sizeof操作符的一个主要用途是与存储分配和I/O系统那样的例程进行通信。 
(2)用它可以看看某种类型的对象在内存中所占的单元字节。 例如:void memset(void *s,int c,sizeof(s));
(3)在动态分配一对象时,可以让系统知道要分配多少内存。
(4)便于一些类型的扩充。 在Windows中有很多结构类型就有一个专用的字段用来存放该类型的字节大小。
(5)由于操作数的字节数在实现时可能出现变化,建议在涉及操作数字节大小时用sizeof代替常量计算。
(6)如果操作数是函数中的数组形参或函数类型的形参,sizeof给出其指针的大小。

面试例题11:一个空类占多少空间?多重继承的空类呢?
答案:空类所占空间为1,单一继承的空类空间也为1,多重继承的空类空间还是1。 但是虚继承涉及虚表(虚指针),所以大小为4。
一个空类所占空间为1,多重继承的空类所占空间还是1。

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

答案:内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编
译的时候内联函数可以直接被镶嵌到目标代码中。 而宏只是一个简单的替换。
内联函数要做参数类型检查,这是内联函数跟宏相比的优势。
inline是指嵌入代码,就是在调用函数的地方不是跳转,而是把代码直接写到那里去。对于短小的代码来说inline增加空间消耗换来的是效率提高,这方面和宏是一模一样的,但
是inline在和宏相比没有付出任何额外代价的情况下更安全。 至于是否需要inline函数,就需要根据实际情况来取舍了。
inline一般只用于如下情况:
(1)一个函数不断被重复调用。
(2)函数只有简单的几行,且函数内不包含for、 while、 switch语句。
一般来说,我们写小程序没有必要定义成inline,但是如果要完成一个工程项目,当一个简单函数被调用多次时,则应该考虑用inline。
宏在C语言里极其重要,而在C++里用得就少多了。 关于宏的第一规则是绝不应该去使用它,除非你不得不这样做。 几乎每个宏都表明了程序设计语言里、 程序里或者程序员的一个缺陷,因为它将在编译器看到程序的正文之前重新摆布这些正文。 宏也是许多程序设计工具的主要麻烦。 所以,如果你使用了宏,就应该准备只能从各种工具(如排错系统、 交叉引用系统、 轮廓程序等)中得到较少的服务。
宏是在代码处不加任何验证的简单替代,而内联函数是将代码直接插入调用处,而减少了普通函数调用时的资源消耗。
宏不是函数,只是在编译前(编译预处理阶段)将程序中有关字符串替换成宏体。
关键字inline必须与函数定义体放在一起才能使函数成为内联,仅将inline放在函数声明前面不起任何作用。 
所以说,inline是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。 内联能提高函数的执行效率,至于为什么不把所有的函数都定义成内联函数?如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。 如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。 另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
以下情况不宜使用内联:
1 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。 
2 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。 类的构造函数和析构函数容易让人误解成使用内联更有效。 要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。 所以不要随便地将构造函数和析构函数的定义体放在类声明中。 一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了inline不应该出现在函数的声明中)。