原文地址:http://blog.csdn.net/hehao5/article/details/8686339
这篇总结写到一半时,谷歌一下,发现早有大神总结的宝典中的错误,从他的总结中可以看出来这位大神无论是心思还是技术都比我细腻。 但我找到的一些错误他也没发现,两个人的答案并起来会好一点。强烈推荐:《程序员面试宝典3》大量错误(50+)纠正表 :http://blog.csdn.net/hilyoo/article/details/4466680
最近在找实习,需要补充内力,于是啃起了《程序员面试宝典》第三版。很多人对这本书嗤之以鼻,认为该书只是把网上的题目拼凑起来,很多东西都可以从网上搜到,甚至可以找到更好的解释和更详细的分析。然而,我这本书啃了大半,感觉对于我这样的连笔试都不一定能过的菜狗,还是非常有必要值得认真一读的。遗憾地是,书中的确有些错误,有的是印刷排版错误,有的是技术错误,有的直接贴代码而完全没有解析,例如(P228页的1的个数,完全是来自CSDN的oo大神的源代码,链接如右:http://bbs.csdn.net/topics/70522458)。对这些错误,我有一些自己的答案,但半斤八两的我学识浅薄,不能保证答案正确,只是发布出来希望能和大家探讨一下。下面中的错误但凡来自他人的内容,都会有原帖链接。如果还有其他错误我没有发现,希望各位大神点拨提示一下。
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的格式控制的知识。详见:http://caswycherley.blog.163.com/blog/static/20951605520127159326444/
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,另有网友分析帖:http://www.cnblogs.com/wanghetao/archive/2012/04/04/2431760.html
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。
9. P160页,面试例题3,答案有错,答案说道:“第三个意味着a是一个指向常整数型的指针(也就是说,整型数是不可修改的,但指针可以修改)”。这样的说法不妥当,第三个即const int * a,的确是一个指向const对象的指针,即指针本身不是const的,可以被修改,但它指向的对象,即这个整形数,是可以被修改,只不过是不允许通过这个指针去修改这个对象的值。可能说得有点绕,详见C++primer(中文第四版)P110页。这里举个最简单的例子,a是一个指向const对象的指针,但它指向一个非const的int型对象b。这个b的整型值是可以被修改的,只不过不能通过a这个指针去修改它,否则会报错。
- const int* a;
- int b=30;
- a=&b;
- cout<<*a<<endl; //*a=30
- b=40;
- cout<<*a<<endl; //*a=40
- *a=50;//error C3892: 'a' : you cannot assign to a variable that is const
11. P225页,面试例题1,答案中的程序可以进一步优化,for(int j=i+1;j<len;++j) 优化为j<=(len+i)/2,详见:http://blog.csdn.net/hehao5/article/details/8679347
12. P226页,面试例题2,答案中的程序可以进一步优化。首先,这题的2个for循环中的那个if语句明显多余,这个if语句可以放到j的for循环中去, 即for(int j=0;j<str.length();j++)改为for(int j=0;j<=str.length()-i;j++)。 再者,书上给的程序有一个小bug,例如输入测试例子abba,没有任何输出结果,理论上应该输出a:1或者b:2。当测试例子为abca,也没有任何输出结果,理论上应该输出a:1。 这是因为寻找相同的子串时,程序忽略了长度为1的子串,当然这也可能是题目本意。如果不忽略长度为1的子串,直接修改i变量的下限,令for中的i>=1即可。详见:http://blog.csdn.net/hehao5/article/details/8680413
13. P228页,面试例题5,书上代码完全是来自CSDN的oo大神的源代码,链接如右:http://bbs.csdn.net/topics/70522458。答案中的程序没有任何分析和注释,而且和《编程之美》上P132页的原题目相比,难易度下降了几个数量级,此外还有另外一个大神求解f(n)=n的惊人且高效简单的剪枝方法。详见:http://blog.csdn.net/hehao5/article/details/8686324
14. P230页,面试例题,答案的程序有错,会造成越界错误,原因在于k<=len-1,应该改为k<len-1,而且还有代码冗余加上输出格式错误,for语句之外的if语句完全可以删除。最后简洁版代码如下:
- #include <iostream>
- #include<string>
- using namespace std;
- int main()
- {
- cout<<"Enter the number"<<endl;
- string str;
- char reschar[50];
- reschar[0]='\0';
- getline(cin,str);
- int len=str.length();
- int count=1;
- int k;
- for(k=0;k<len-1;k++)
- {
- if(str[k+1]==str[k])
- {
- count++;
- }else{
- sprintf(reschar+strlen(reschar),"%c%d",str[k],count);
- count=1;
- }
- }
- sprintf(reschar+strlen(reschar),"%c%d",str[k],count);
- cout<<reschar<<endl;
- cout<<endl;
- return 0;
- }
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个节点指针就可以实现了。代码如下:
- void reverse(node* head)
- {
- node* p,q;
- p=head->next;
- head->next=NULL;
- while(p)
- {
- q=p;
- p=p->next;
- q-next=head->next;
- head->next=q;
- }
- }
19. P172页,面试例题7,分析正确,但是算法有错。根源还是在于把头结点当做元素节点了,例如当元素个数为奇数时,比如3个的时候,该算法把第一个元素当做中间结点,其实是第二个元素才是。
20. P182页,面试例题2,题目有错,不应该是全部变量放在(数据段),而是全局变量放在(数据段)。
21. P177页,面试例题2,答案有错,题目解析没错,答案应该选择A,而不是D。书上说,栈的生长方向应该是由上到下。这句话意思不清楚,什么是上,什么是下?可以参考书中P184页所描述部分,“在堆栈中分布变量是从高地址向低地址分布,EBP指向栈底指针,ESP指向栈顶指针”。所以,堆栈的生长方向由高地址向低地址生长,栈底位于高地址。因此,此题答案选择A,D中把栈底和栈顶颠倒了。
22. P208页,面试例题4,分析没错,答案有错。选A,估计为印刷排版错误。
23. P110页,面试例题5,分析不够细致。B temp=Play(5),理论上该有两次拷贝(复制)构造函数,编译器把这两次合为一次,提高效率。所以把此句改为Play(5),会发现结果一样。都是2次析构,只不过区别在于:Play(5)的第一次析构是在函数退出时,对形参的副本进行析构。第二次析构是在函数返回类对象时,再次调用拷贝构造函数来创建返回类对象的副本。所以还需要一次析构函数来析构这个副本。而B temp=Play(5)中的第二次析构是析构B temp。在B temp=Play(5)加一句system('pause');可以验证第二次析构是在析构B temp,而不是析构函数返回值对象的副本,编译器把这两次合为一次,提高效率。更加详细全面的程序:http://blog.sina.com.cn/s/blog_6ab0b9a801019b3h.html
24. P121页,答案有错,模板类中声明友元模板函数时,应该为:详细的例子参见C++ Primer第四版:P555页。
- friend ostream& operator<< <T>(ostream& out, const Test<T> &obj);
25. P128页,扩展知识关于保护继承描述中有错误。在“3. 保护继承方式中”,“基类的公有成员和保护成员都作为派生类的保护成员,并且不能被这个派生类的子类所访问。”这句话有错,应该改为:“基类的公有成员和保护成员都作为派生类的保护成员,并且不能被这个派生类的子类的对象所访问,但可以被派生类的子类所访问”。派生类的对象和派生类本身对基类的访问不能混为一谈,要明细区分。所以,最后书上总结时:“所以,在保护继承时,基类的成员也只能由直接派生类访问,而无法再往下继承”,这句话的意思也是模棱两可,可以忽略。
26. P129页,面试例题2,解析中E的解释有错误。书上说:“m_nPtd是父类Parent的保护变量,可以被公有继承的cd1访问,但不可以修改”,但如果把E句改为 int t= cd1.m_nPtd;会发现依然编译不通过,所以,m_nPtd是父类Parent的保护变量,是不可以被公有继承的cd1访问,更不可以被修改了。虽然m_nPtd是父类Parent的保护变量,经过公有继承后,m_nPtd在子类中依然是Protected,而子类的对象【cd1】是不能访问自身的protected成员,只能访问public成员。
27. P130页,面试例题2。答案和解析都有错,这题的原创帖子:http://blog.csdn.net/rkfang/article/details/131567 包括扩展知识部分的配图和书上的原题目并不搭配,而是摘自这个帖子的图。我在VS2010上跑的结果是:8,20,32。这个结果也符合原帖子中的推理解释,因此书上的答案有错,原因在于少算了一个指针的大小。
相关知识点补充:
关于类对象大小的 sizeof 计算问题 http://blog.csdn.net/heipiaibenben/article/details/7870800
多重继承与虚继承 对象布局 http://www.cnblogs.com/chio/archive/2007/11/24/970983.html
28.P140页,面试例题5,第2问,答案有错。一般在什么时候构造函数被声明为private 呢?书上答案:“比如要阻止编译器生成默认的copy constructor的时候”。其实,把构造函数设为private并不能阻止编译器生成默认的copy constructor,这是毫不相干的两件事情。参见C++ Primer第四版P409页所述:“如果我们没有定义复制构造函数,编译器就会为我们合成一个。与合成默认构造函数不同,即使我们定义了其他构造函数,也会合成复制构造函数”。实践出真知:下面是代码:虽然把构造函数A()设为私有,但编译器依然合成了拷贝构造函数,否则A b(*a);是会出错的。因为这一步用到复制初始化,必须有复制构造函数才能为对象b初始化。
- #include<iostream>
- using namespace std;
- class A{
- public:
- //A(const A& tmp)
- //{
- // this->m=tmp.m;
- // cout<<"copy construct!"<<endl;
- //}
- static A* ini()
- {
- return new A();
- }
- private:
- A(){cout<<"Construct!"<<endl;};
- int m;
- };
- int main(){
- A* a=A::ini();
- A b(*a);
- return 0;
- }
29. P143页,扩展知识部分,建议大家别看这部分了,去翻Primer吧。首先RTTI,C++ Primer第四版P737页的翻译为Run-Time Type Identification,即运行时类型识别。当然这个问题不大。书中第二段中描述:“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
http://www.cnblogs.com/ustc11wj/archive/2012/08/11/2637319.html
30. P147页,面试例题4,解析中有句话:“后缀++如果是成员函数,那么它就是二元操作符”,这颠覆了我C++的世界观,明明是一元操作符怎么变成二元的操作符了,感觉书上有点抠字眼,只是在重载的时候,为了区别前缀++的函数原型,后缀++的重载形式为:void operator++(int);因为多了个int形参,就按步就班地归为二元操作符是不科学的。这个int形参不是后缀操作符的正常工作所需要的,它的唯一作用目的是使后缀函数与前缀函数区别开来。因为不使用这个int形参,所以也没有对这个形参进行命名的必要。使用后缀操作符时,编译器提供0作为这个形参的实参。--摘自C++ Primer第四版P447页。
31. P257页,面试例题3,解析没错,答案有错。按照解析分析,应该选择C,知识扩充参见:http://blog.csdn.net/rivershan/article/details/15779
32. P307页,面试例题1,代码有个明显错误,if(x[i]+y[j]+z[k]==12)改为if(x[i]+y[j]+z[k]==13)
33. P309页,面试例题1,最后明显是比赛9次。
34. P309页,面试例题2,我计算出的答案是1/2。不能肯定哪种答案谁对谁错,望大神指导。甲有(A:2 , B:0)(A:1 , B:1)(A:0 , B:2)3种情况,乙有(A:3 , B:0)(A:2 , B:1)(A:1 , B:2)(A:0 , B:3)共4种情况。所以总共合计有12种情况。其中甲取胜的情况数为6种,分别为:
甲=(A:2 , B:0)乙=(A:1 , B:2)或(A:0 , B:3)
甲=(A:1 , B:1)乙=(A:3 , B:0)或(A:0 , B:3)
甲=(A:0 , B:2)乙=(A:3 , B:0)或(A:2 , B:1)
所以概率为6/12=1/2
35. P318页,扩展知识。关于直线飞行,n架飞机最远能飞多远。表示非常艰难地看不懂分析,答案明显错误。当n=3时,即使允许最后一架飞机坠机,怎么就能飞1+1/3+1/5。3架飞机最远的距离我算到是1+1/4+1/4=1.5。其实不难推断出,n架飞机最远飞1+(n-1/n+1)。也就说,有无限飞机的情况下,最多能飞2。证明过程如下:
先规范题意:假设有n架飞机,前n-1架必须安全返回,第n架飞到坠机为止。每架飞机满油情况下单直线最远距离为1,1架飞机可以瞬间给任意数量飞机加油,但不能超过油箱的油量。现在求n架飞机的最远飞行距离。
解:记第一架为 A1,记第二架飞机为A2,后面命名方式以此类推。
A1返回时,应该是把油量平均分给剩余的n-1架飞机,且使这n-1架飞机邮箱加满。那么A1该在何处分油后返回?设这个距离为x,则A1能分给其他飞机的油量为(1-2x),因为A1返回时也需要x油量。而其他(n-1)架飞机也飞了x,所以它们的油箱必须都加上x的油量后才能恢复满油粮,所需油量为(n-1)x。所以有(1-2x)=(n-1)x,所以x=1/(n+1)
A2返回时,同上所述,只不过A2还额外需要1/(n+1)的油量返回,即A2能分的油量为(1-2x-1/n+1),而另外n-2架飞机所需油量为(n-2)x,所以有(1-2x-1/n+1)=(n-2)x,计算结果依然有x=1/(n+1),所以A2返回时的距离为1/(n+1)+1/(n+1)=2/(n+2)
依次类推计算,An-1返回时飞行的距离为(n-1)*1/(n+1)=(n-1)/(n+1)。
那么第N架坠机身亡时飞行距离为1+(n-1)/(n+1),当n趋于无穷时,n架飞机最多也只能飞到2而已。
所以,非常看不懂扩展部分的解释,也许是我理解题意偏差,如果大神有高见,求指导。
再追加一个严重错误
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同名函数而已。
惊闻这本书已经出到第四版了,第三版这么多错误,这么高效率地放出第四版,而且换了大量题目,本身就是一种不负责的行为。我已经无力吐槽,再追加一个坑爹的错误。
37. P151,面试例题5。答案正确,解析就是瞎扯淡,坑人不浅。
- cout<<p<<endl;
结果是“a”,p是一个char*指针,之所以不输出p这个指针的地址,而是输出p指针指向的字符串“a”,那是因为C++的输出操作符的实现细节是这样实现的。
ostream& operator<< (ostream& os, const char* s);当形参是一个 char*指针时,cout会把这个char*指针所指向的字符串输出来。等效于 printf("%s\n",p);若想打印p这个指针所指向的值的地址,(即指针p自身的值),用printf("%08x\n",d)即可。
- cout<<&p<<endl;
结果是0x22ff7c。这个是存储p这个指针本身的内存地址。所以才会有*(&p)==p;
- int i = (int)"a";
- cout<<i<<endl;
结果是4199056。书上解释说:“4199056是指针P指向的内容,即p的值“a”强制转化为int后的结果。所以(int)"a"的结果也是4199056”。这句解释完全错误。
右边是一个const char*型的字符串,存储在常量区。此处就是把这个字符串首字母在常量区的内存地址转化为int型再赋值给i。举个类似例子就会清晰看到这一点,char* str="a";这里也是把这个字符串首字母在常量区的内存地址直接赋值给str这个指针。因此,(int)"a"等于4199056并不是值“a”强制转化为int后的结果,而是字符串“a”在常量区的内存地址转化为int型的值为4199056。
- int i2 =(int)p;
因为p是个指向常量区字符串"a"的char*指针。所以,这里是把p的值(即“a”的常量区内存地址)转化为int型再赋值给i2,所以i2也是4199056。
- int i5=reinterpret_cast<int>(&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: invalid static_cast from type 'char*' to type 'int'
D选项符合题意,reinterpret_cast<type>(expression)就是从位模式的角度用type型在较低的层次重新解释这个expression。
可以参见C++ Primer第四版P161页所述:"在合法使用static_cast和const_cast的地方,旧式强制转换提供了与各自对应的命名强制转换一样的功能。如果这两种强制转换均不合法,则旧式强制转换执行reinterpret_cast功能。"
因此D选项就是旧式强制转换执行reinterpret_cast功能的最好例子。