程序员面试宝典 第三版 错误.docx
《程序员面试宝典 第三版 错误.docx》由会员分享,可在线阅读,更多相关《程序员面试宝典 第三版 错误.docx(11页珍藏版)》请在冰豆网上搜索。
![程序员面试宝典 第三版 错误.docx](https://file1.bdocx.com/fileroot1/2023-1/10/ed90a75a-d427-452a-96fb-7de5169a6616/ed90a75a-d427-452a-96fb-7de5169a66161.gif)
程序员面试宝典第三版错误
程序员面试宝典第三版错误
分类:
笔经面经2013-03-1904:
441341人阅读评论(0)收藏举报
第三版错误程序员面试宝典
这篇总结写到一半时,谷歌一下,发现早有大神总结的宝典中的错误,从他的总结中可以看出来这位大神无论是心思还是技术都比我细腻。
但我找到的一些错误他也没发现,两个人的答案并起来会好一点。
强烈推荐:
《程序员面试宝典3》大量错误(50+)纠正表:
最近在找实习,需要补充内力,于是啃起了《程序员面试宝典》第三版。
很多人对这本书嗤之以鼻,认为该书只是把网上的题目拼凑起来,很多东西都可以从网上搜到,甚至可以找到更好的解释和更详细的分析。
然而,我这本书啃了大半,感觉对于我这样的连笔试都不一定能过的菜狗,还是非常有必要值得认真一读的。
遗憾地是,书中的确有些错误,有的是印刷排版错误,有的是技术错误,有的直接贴代码而完全没有解析,例如(P228页的1的个数,完全是来自CSDN的oo大神的源代码,链接如右:
1.P30页,面试例题2,解析没错,答案:
10,10,1,3,1,7,应改为10,10,1,3,1,7,1。
估计为印刷排版错误。
2.P32页,面试例题2,题目有错,题目中sint*ptr=arr,应改为int*ptr=arr。
估计为印刷排版错误。
3.P34页,面试例题2,答案没错,但分析不妥。
需要用到“小端存储”的概念和printf的格式控制的知识。
详见:
4.P40页,面试例题2,解析中的例子有错,最底下一行:
00000101^00000101得到00001001,应改为00001100^00000101得到00001001。
意思是12^5=9。
估计为印刷排版错误。
5.P52页,面试例题3,解析有错,sizeof(A3)是12,应改为,sizeof(A4)是12。
sizeof(A3)是24,应改为,sizeof(A5)是24。
估计为印刷排版错误。
6.P56页,面试例题7,解析中的第(5)点有错,文中写到sizeof(string)=4,笔者在自己64为系统上的VS2010上测试为sizeof(string)=32,另有网友分析帖:
7.P83页,面试例题1,解析题目都有错,题目中,即输出125,146,145,146,应改为,即输出125,126,145,146。
解析中的错误也如此。
估计为印刷排版错误。
8.P96页,面试例题3,答案修改方法2有错,array.erase(itor2);,应改为itor=array.erase(itor2);。
原因来自C++primer(中文第四版)P282页的“小心地雷”所述:
“erase,pop_front和pop_back函数使指向被删除元素的所有迭代器失效“。
所以执行了array.erase(itor2)语句后,itor也会失效,因此需要把对itor重新赋值,即把erase操作后的返回值赋给itor。
23.P110页,面试例题5,分析不够细致。
Btemp=Play(5),理论上该有两次拷贝(复制)构造函数,编译器把这两次合为一次,提高效率。
所以把此句改为Play(5),会发现结果一样。
都是2次析构,只不过区别在于:
Play(5)的第一次析构是在函数退出时,对形参的副本进行析构。
第二次析构是在函数返回类对象时,再次调用拷贝构造函数来创建返回类对象的副本。
所以还需要一次析构函数来析构这个副本。
而Btemp=Play(5)中的第二次析构是析构Btemp。
在Btemp=Play(5)加一句system('pause');可以验证第二次析构是在析构Btemp,而不是析构函数返回值对象的副本,编译器把这两次合为一次,提高效率。
更加详细全面的程序:
36.P115页,面试例题4。
答案错误,解析也错误。
正确答案应该为B和C。
C++Primer第四版P502页习题15.25和此题类似。
参见C++Primer第四版P477页所述:
“派生类中虚函数的声明必须与基类中定义方式完全匹配,但是有一个例外:
返回对基类型的引用或指针的虚函数。
派生类中的虚函数可以返回基类函数 所返回类型 的 派生类的引用或指针"
同样的逻辑见C++Primer第四版P507页所述:
“如果虚函数的基类实例返回类类型的引用或指针,则该虚函数的派生类实例可以返回基类实例返回的类型的派生类(或者是类类型的指针或引用)”
A选项错误,因为虚函数的声明必须与基类中定义方式完全匹配。
而子类的虚函数的形参为Derived*,与父类的虚函数形参不同。
因此,子类这个不是虚函数的声明。
但是书上解释A是函数重载,这个说法是错的。
函数重载的定义参见C++Primer第四版P228页所述:
“出现在相同作用域中的两个函数,如果具有相同的名字而形参表不同,则称为函数重载”。
因此函数重载的前提是两个函数必须在同一个作用域。
这一个前提往往被初学者给忽略。
同样的逻辑在C++Primer第四版P500页的小心地雷所述:
“回忆一下,局部作用于中的声明的函数不会重载全局作用域中定义的函数”。
A选项子类只是重新定义了一个具有不同形参的同名函数而已,并且这个同名函数会屏蔽父类的同名函数。
因为派生类的作用域嵌套在基类的作用域中。
B选项正确,因此B选项就是P477页所述的例外,基类虚函数的返回类型是Base*,而子类虚函数的返回类型是Derived*,且Derived是Base的派生类。
所以,B的虚函数声明是正确的。
C选项正确,虽然基类的虚函数声明中多了一个默认实参,但是依然和子类的虚函数属于同一个函数声明。
D选项错误,因为D的子类的虚函数不是一个const函数,和基类的虚函数声明不一致。
D选项也不是函数重载,只是子类重新定义了一个非const同名函数而已。
惊闻这本书已经出到第四版了,第三版这么多错误,这么高效率地放出第四版,而且换了大量题目,本身就是一种不负责的行为。
我已经无力吐槽,再追加一个坑爹的错误。
24.P121页,答案有错,模板类中声明友元模板函数时,应该为:
详细的例子参见C++Primer第四版:
P555页。
[cpp]viewplaincopy
1.friend ostream& operator<< (ostream& out, const Test &obj);
31.P257页,面试例题3,解析没错,答案有错。
按照解析分析,应该选择C,知识扩充参见:
25.P128页,扩展知识关于保护继承描述中有错误。
在“3.保护继承方式中”,“基类的公有成员和保护成员都作为派生类的保护成员,并且不能被这个派生类的子类所访问。
”这句话有错,应该改为:
“基类的公有成员和保护成员都作为派生类的保护成员,并且不能被这个派生类的子类的对象所访问,但可以被派生类的子类所访问”。
派生类的对象和派生类本身对基类的访问不能混为一谈,要明细区分。
所以,最后书上总结时:
“所以,在保护继承时,基类的成员也只能由直接派生类访问,而无法再往下继承”,这句话的意思也是模棱两可,可以忽略。
26.P129页,面试例题2,解析中E的解释有错误。
书上说:
“m_nPtd是父类Parent的保护变量,可以被公有继承的cd1访问,但不可以修改”,但如果把E句改为 intt=cd1.m_nPtd;会发现依然编译不通过,所以,m_nPtd是父类Parent的保护变量,是不可以被公有继承的cd1访问,更不可以被修改了。
虽然m_nPtd是父类Parent的保护变量,经过公有继承后,m_nPtd在子类中依然是Protected,而子类的对象【cd1】是不能访问自身的protected成员,只能访问public成员。
27.P130页,面试例题2。
答案和解析都有错,这题的原创帖子:
包括扩展知识部分的配图和书上的原题目并不搭配,而是摘自这个帖子的图。
我在VS2010上跑的结果是:
8,20,32。
这个结果也符合原帖子中的推理解释,因此书上的答案有错,原因在于少算了一个指针的大小。
相关知识点补充:
关于类对象大小的sizeof计算问题
多重继承与虚继承对象布局
28.P140页,面试例题5,第2问,答案有错。
一般在什么时候构造函数被声明为private呢?
书上答案:
“比如要阻止编译器生成默认的copyconstructor的时候”。
其实,把构造函数设为private并不能阻止编译器生成默认的copyconstructor,这是毫不相干的两件事情。
参见C++Primer第四版P409页所述:
“如果我们没有定义复制构造函数,编译器就会为我们合成一个。
与合成默认构造函数不同,即使我们定义了其他构造函数,也会合成复制构造函数”。
实践出真知:
下面是代码:
虽然把构造函数A()设为私有,但编译器依然合成了拷贝构造函数,否则Ab(*a);是会出错的。
因为这一步用到复制初始化,必须有复制构造函数才能为对象b初始化。
[cpp]viewplaincopy
1.#include
2.using namespace std;
3.
4.class A{
5.public:
6.
7. //A(const A& tmp)
8. //{
9. // this->m=tmp.m;
10. // cout<<"copy construct!
"<11. //}
12. static A* ini()
13. {
14. return new A();
15. }
16.private:
17. A(){cout<<"Construct!
"<18. int m;
19.};
20.
21.int main(){
22. A* a=A:
:
ini();
23. A b(*a);
24. return 0;
25.}
那么,私有的构造函数的实际用途何在呢?
我也不太清楚,请大神指导,只知道作用是不能直接通过构造函数来定义对象,因为构造函数是私有的,类外的用户代码是不能访问类中私有的成员,但可以在类中间接地调用构造函数来构造对象,例如上面代码中的staticA*ini(),也可以通过友元来间接调用私有的构造函数来构造对象。
29.P143页,扩展知识部分,建议大家别看这部分了,去翻Primer吧。
首先RTTI,C++Primer第四版P737页的翻译为Run-TimeTypeIdentification,即运行时类型识别。
当然这个问题不大。
书中第二段中描述:
“dynamic_cast必须要在有虚函数的hierarchy里进行,如果成功则返回1”,这句话描述有错。
参见C++Primer第四版P647页所述:
“可以使用dynamic_cast操作符将基类类型对象的引用或指针转化为同一继承层次中的其他类型的引用或指针。
.......如果绑定到引用或指针的对象不是目标类型的对象,则dynamic_cast失败。
如果转换到指针类型的dynamic_cast失败,则dynamic_cast的结果是0值;如果转换到引用类型的dynamic_cast失败,则抛出一个bad_cast类型的异常”。
因此可见,如果成功返回1这句话描述有错。
另外,在“运行时类型识别RTTI使用时要注意一下几点”中的描述,原书断章取义:
“如果P是指针,typeid(*p)返回p所指向的派生类类型,typeid(p)返回基类类型;如果r是引用,typeid(r)返回p所指向的派生类类型,typeid(&r)返回基类类型。
” 不知这本书从哪里抄了这么一段话,这两句如果缺乏上下文环境,所以描述错误。
还原上下文环境:
如果p是基类指针,并且指向一个派生类型的对象,并且基类中有虚函数,那么typeid(*p)返回p所指向的派生类类型,typeid(p)返回基类类型。
建议看下面的链接关于C++关键字typeid。
知识点补充:
C++关键字typeid
30.P147页,面试例题4,解析中有句话:
“后缀++如果是成员函数,那么它就是二元操作符”,这颠覆了我C++的世界观,明明是一元操作符怎么变成二元的操作符了,感觉书上有点抠字眼,只是在重载的时候,为了区别前缀++的函数原型,后缀++的重载形式为:
voidoperator++(int);因为多了个int形参,就按步就班地归为二元操作符是不科学的。
这个int形参不是后缀操作符的正常工作所需要的,它的唯一作用目的是使后缀函数与前缀函数区别开来。
因为不使用这个int形参,所以也没有对这个形参进行命名的必要。
使用后缀操作符时,编译器提供0作为这个形参的实参。
--摘自C++Primer第四版P447页。
再追加一个严重错误
37.P151,面试例题5。
答案正确,解析就是瞎扯淡,坑人不浅。
[cpp]viewplaincopy
1.cout<
结果是“a”,p是一个char*指针,之所以不输出p这个指针的地址,而是输出p指针指向的字符串“a”,那是因为C++的输出操作符的实现细节是这样实现的。
ostream&operator<<(ostream&os,constchar*s);
当形参是一个char*指针时,cout会把这个char*指针所指向的字符串输出来。
等效于printf("%s\n",p);若想打印p这个指针所指向的值的地址,(即指针p自身的值),用printf("%08x\n",d)即可。
[cpp]viewplaincopy
1.cout<<&p<结果是0x22ff7c。
这个是存储p这个指针本身的内存地址。
所以才会有*(&p)==p;
[cpp]viewplaincopy
1.int i = (int)"a";
2.cout<
结果是4199056。
书上解释说:
“4199056是指针P指向的内容,即p的值“a”强制转化为int后的结果。
所以(int)"a"的结果也是4199056”。
这句解释完全错误。
右边是一个constchar*型的字符串,存储在常量区。
此处就是把这个字符串首字母在常量区的内存地址转化为int型再赋值给i。
举个类似例子就会清晰看到这一点,char*str="a";这里也是把这个字符串首字母在常量区的内存地址直接赋值给str这个指针。
因此,(int)"a"等于4199056并不是值“a”强制转化为int后的结果,而是字符串“a”在常量区的内存地址转化为int型的值为4199056。
[cpp]viewplaincopy
1.int i2 =(int)p;
因为p是个指向常量区字符串"a"的char*指针。
所以,这里是把p的值(即“a”的常量区内存地址)转化为int型再赋值给i2,所以i2也是4199056。
[cpp]viewplaincopy
1.int i5=reinterpret_cast(&p);
这里是把指针p的地址的位模式用int型去重新解释。
而指针p的地址,即&p=0x22ff7c。
这是前面输出的结果,这里就相当于把这个0x22ff7c内存地址用int型去解释。
因此输出的就是十进制的指针p的地址。
所以结果是2293628。
因为2293628转化为16进制就是0x22ff7c。
此题A,C选项可以迅速排除。
A选项,dynamic_cast顾名思义是支持动态的类型转换,即支持运行时识别指针或引用所指向的对象。
原题明显是静态的类型转换。
C选项,const_cast是转换掉表达式的const性质。
因此C也不符合。
B选项错在不存在char*到int型的隐式转换,编译器会报错:
error:
invalidstatic_castfromtype'char*'totype'int'
D选项符合题意,reinterpret_cast(expression)就是从位模式的角度用type型在较低的层次重新解释这个expression。
可以参见C++Primer第四版P161页所述:
"在合法使用static_cast和const_cast的地方,旧式强制转换提供了与各自对应的命名强制转换一样的功能。
如果这两种强制转换均不合法,则旧式强制转换执行reinterpret_cast功能。
"
因此D选项就是旧式强制转换执行reinterpret_cast功能的最好例子。
9.P160页,面试例题3,答案有错,答案说道:
“第三个意味着a是一个指向常整数型的指针(也就是说,整型数是不可修改的,但指针可以修改)”。
这样的说法不妥当,第三个即constint*a,的确是一个指向const对象的指针,即指针本身不是const的,可以被修改,但它指向的对象,即这个整形数,是可以被修改,只不过是不允许通过这个指针去修改这个对象的值。
可能说得有点绕,详见C++primer(中文第四版)P110页。
这里举个最简单的例子,a是一个指向const对象的指针,但它指向一个非const的int型对象b。
这个b的整型值是可以被修改的,只不过不能通过a这个指针去修改它,否则会报错。
[cpp]viewplaincopy
1.const int* a;
2.int b=30;
3.a=&b;
4.cout<<*a<5.b=40;
6.cout<<*a<7.*a=50;//error C3892:
'a' :
you cannot assign to a variable that is const
15.P167页,面试例题1,程序错漏百出,首先这里使用的链表的头结点head的数据域data是为空的,因此测长度的时候,不应该从头结点数起,否则测量长度时多测了一个不存储任何数据的头结点。
再者,在链表打印函数里,不能打印头结点的data域,print函数中的if语句是多余的,直接让p=head->next;即可。
关键在于混淆了头结点,把它当做存储元素的节点使用去了,包括接下来的面试例题2,面试例题3都犯了这个错误。
16.P168页,面试例题2,解析中的图画的不错,但程序依然错漏百出。
和面试例题1一样犯了同样的错误。
为什么链表的数据结构要选择头结点head的data域为空,为什么不让head->data去存储数据呢?
不这样设计的话,删除元素时就要检测删除的元素是头结点,还是非头结点,然后做出不同的操作。
这样设计的原因是因为删除元素时,即使是删除第一个节点元素,相当于删除头结点的下一个元素。
因此就不用检测删除的元素是不是头结点了,因为第一个元素没存放在头结点,存放在头结点的下一个节点。
所以,程序中的第二个if语句是多余的,因为如果第一个if语句成立,即满足num==p1->data,那么必定有P1!
=head,因为P1=head的话,head->data域为空,不存放数据,那么怎么可能会有num==p1->data。
所以,第二个if居于肯定为假。
另外,第二个if语句为假时的分支没有free掉删除元素所占的内存。
17.P169页,面试例题3,同样如此,建议这一块大家抱着面试找错题的心态去看这里的代码吧。
单链表的插入用图示画了3种情况,图画的不错,代码依然有错,而且还分三种情况去插入元素,其实三种情况可以归为一种,正确规范代码建议看严蔚敏版的《数据结构》的单链表那节。
18.P170页,面试例题5,单链表逆置,用了3个节点指针,其实2个节点指针就可以实现了。
代码如下:
[cpp]viewplaincopy
1.void reverse(node* head)
2.{
3. node* p,q;
4. p=head->next;
5. head->next=NULL;
6. while(p)
7. {
8. q=p;
9. p=p->next;
10. q-next=head->next;
11. head->next=q;
12. }
13.}
19.P172页,面试例题7,分析正确,但是算法有错。
根源还是在于把头结点当做元素节点了,例如当元素个数为奇数时,比如3个的时候,该算法把第一个元素当做中间结点,其实是第二个元素才是。
21.P177页,面试例题2,答案有错,题目解析没错,答案应该选择A,而不是D。
书上说,栈的生长方向应该是由上到下。
这句话意思不清楚,什么是上,什么是下?
可以参考书中P184页所描述部分,“在堆栈中分布变量是从高地址向低地址分布,EBP指向栈底指针,ESP指向栈顶指针”。
所以,堆栈的生长方向由高地址向低地址生长,栈底位于高地址。
因此,此题答案选择A,D中把栈底和栈顶颠倒了。