http://blog.chinaunix.net/uid-22754909-id-3969535.html

今天和几位同仁一起探讨了一下C++的一些基础知识,在座的同仁都是行家了,有的多次当过C++技术面试官。不过我出的题过于刁钻: 不是看起来太难,而是看起来极其容易,但是其实非常难! 结果一圈下来,4道题,平均答对半题。于是只能安慰大家,这几道题,答不对是正常的。
    "你真的清楚构造函数,拷贝构造函数,operator=,析构函数都做了什么吗? 它们什么时候被调用?",这些问题可不是面向初菜的问题,对于老鸟而言,甚至对于许多自诩为老手的人而言,倒在这上面也是很正常的。因为这个问题的答案不但考察我们对于C++语言的理解,而且答案是和编译器的实现有关的!
【第一题】以下代码,main函数中G.i的打印结果是什么? 写在一张纸上,再看答案。我不是在挑战大家的知识,我是在挑战很多人的常识。

点击(此处)折叠或打开

  1. #include<iostream>
  2. using namespace std;
  3. class G
  4. {
  5. public:
  6. static int i;
  7. G() {cout<<"ctor"<<endl;i+=1;}
  8. G(const G& rg){cout<<"copy ctor"<<endl;i+=2;}
  9. G& operator=(const G& rg){cout<<__FUNCTION__<<endl;i+=3;return *this;}
  10. };
  11. int G::i=0;
  12. G Create()
  13. {
  14. cout<<__FUNCTION__<<" starts"<<endl;
  15. G obj;
  16. cout<<__FUNCTION__<<" ends"<<endl;
  17. return obj;
  18. }
  19. int main(int argc, char* argv[])
  20. {
  21. G g1=Create();
  22. cout<<"G.i="<<G::i<<endl;
  23. return 0;
  24. }

"3,2,1,公布答案"。G.i是多少? 回答4及其以上的统统枪毙。回答3及其以下的留下继续讨论。注意,这里根本就没有调用到operator=,因为operator=被调用的前提是一个对象已经存在,我们再次给它赋值,调用的才是operator=。
   那么答案到底是多少呢? VC编译器,用2008或者2012,Debug版都是3,Release版都是1。用GCC4.7,Debug/Release都是1。
   为什么? 因为G g1=Create();这句话,可能会触发C++编译器的一个实现特性,叫做NRVO,命名返回值优化。也就是G函数中的obj并没有被创建在G的调用栈中,而是调用Create()函数的main的栈当中,因此obj不再是一个函数的返回变量,而是用g1给Create()返回的变量命名。
   VC的Debug版没有触发NRVO,因此会多调用一个拷贝构造函数,结果和Release版不一样----能说出这个的C++一定是中级以上水平了。
   这就带了一个问题,如果用VC编程的话,HouseKeep/计数的信息如果在ctor/copy ctor里面,那么不能保证调试版和发布版的行为一致。这个坑太大了。但是GCC没有这个问题!瞬间对理查德-斯托曼无比敬仰。

【第二题】以下程序的运行结果是什么:

点击(此处)折叠或打开

  1. #include <vector>
  2. #include <iostream>
  3. using namespace std;
  4. struct Noisy {
  5. Noisy() {std::cout << "constructed\n"; }
  6. Noisy(const Noisy&) { std::cout << "copied\n"; }
  7. ~Noisy() {std::cout << "destructed\n"; }
  8. };
  9. std::vector<Noisy> f()
  10. {
  11. std::vector<Noisy> v = std::vector<Noisy>(2); // copy elision from temporary to v
  12. return v; // NRVO from v to the nameless temporary that is returned
  13. }
  14. void fn_by_val(std::vector<Noisy> arg) // copied
  15. {
  16. std::cout << "arg.size() = " << arg.size() << '\n';
  17. }
  18. void main()
  19. {
  20. std::vector<Noisy> v = f(); // copy elision from returned temporary to v
  21. cout<<"------------------before"<<endl;
  22. fn_by_val(f());// and from temporary to the argument of fn_by_val()
  23. cout<<"------------------after"<<endl;
  24. }

第一轮没有被枪毙的同学注意了: 这道题目的答案仍然是和编译器有关的,而且和版本还有关系。
(2.1) VC2008 Debug版的运行结果

点击(此处)折叠或打开

  1. constructed
  2. copied
  3. copied
  4. destructed
  5. copied
  6. copied
  7. destructed
  8. destructed
  9. ------------------before
  10. constructed
  11. copied
  12. copied
  13. destructed
  14. copied
  15. copied
  16. destructed
  17. destructed
  18. arg.size() = 2
  19. destructed
  20. destructed
  21. ------------------after
  22. destructed
  23. destructed
  24. Press any key to continue . . .

看到了吗,在"------------before"之前,有一个奇怪的ctor, copy ctor, copy ctor, dtor的调用序列? 这是VC2008当中std::vector<Noisy>(2)做的事情: 先调用一个默认构造函数构造Noisy临时对象,然后把临时对象拷贝给vector的两个程序,再把临时对象析构掉。太傻了吧!Release版的结果稍微好一点,返回的vector不再被拷贝了,就如同第一题所说的:
(2.2) VC2008 Release版的运行结果

点击(此处)折叠或打开

  1. constructed
  2. copied
  3. copied
  4. destructed
  5. ------------------before
  6. constructed
  7. copied
  8. copied
  9. destructed
  10. arg.size() = 2
  11. destructed
  12. destructed
  13. ------------------after
  14. destructed
  15. destructed
  16. Press any key to continue . . .

换个编译器VC2012编译出来的,就聪明多了(Debug/Release运行结果相同):

点击(此处)折叠或打开

  1. constructed
  2. constructed
  3. ------------------before
  4. constructed
  5. constructed
  6. arg.size() = 2
  7. destructed
  8. destructed
  9. ------------------after
  10. destructed
  11. destructed
  12. Press any key to continue . . .

调用了两次ctorl来构造这个vector。性能提高多了。慢点,还有一点不同,因为函数fn_by_val的参数是传值而不是传引用,所以编译器知道在这个函数里面vector没有被修改,因此直接把传值优化成了传const&! VC2012的Debug/Release一致!终于赶上GCC了,不容易。
    问题:到底什么时候一个拷贝构造的操作可以被优化掉呢? C++标准还是有定义的,这个网页说的很清楚(http://en.cppreference.com/w/cpp/language/copy_elision)。其中的Notes一段话非常重要,我贴到这里:
    Notes
    Copy elision is the only allowed form of optimization that can change the observable side-effects. Because some compilers do not perform copy elision in every situation where it is allowed, programs that rely on the side-effects of copy/move constructors and destructors are not portable.
    Even when copy elision takes place and the copy-/move-constructor is not called, it must be present and accessible, otherwise the program is ill-formed.
   也就是说,编译器即使知道ctor/copy ctor/move ctor/dtor有副作用,也会考虑消除拷贝。当然,其他的编译器优化是不能消除副作用的。其他的Copy elision的情况有举例如下。
(2.3)临时变量不需要被copy:

点击(此处)折叠或打开

  1. struct My {
  2. My() {std::cout << "constructed\n"; }
  3. My(const My&) { std::cout << "copied\n"; }
  4. ~My() {std::cout << "destructed\n"; }
  5. };
  6. void f(My m){}
  7. void main()
  8. {
  9. f(My());
  10. }

运行结果是:

点击(此处)折叠或打开

  1. constructed
  2. destructed
  3. Press any key to continue . . .

看起来,临时变量My()被优化成了一个const My&并传递了进去,当作了f的参数。
(2.4)再看一个throw的例子:

点击(此处)折叠或打开

  1. struct My {
  2. My() {std::cout << "constructed\n"; }
  3. My(const My&) { std::cout << "copied\n"; }
  4. ~My() {std::cout << "destructed\n"; }
  5. };
  6. void fm(){throw My();}
  7. void main()
  8. {
  9. try{
  10. cout<<"before throw"<<endl;
  11. fm();
  12. cout<<"after throw"<<endl;
  13. }catch(My& m)
  14. {}
  15. }

这里的throw My()语句构造的My对象,优化后是构造在try的栈上面而非fm的栈上面,因此没有copy ctor的调用。
【第三题】以下程序的运行结果是什么?

点击(此处)折叠或打开

  1. using namespace std;
  2. struct C4
  3. {
  4. void f(){throw 1;}
  5. ~C4(){throw 2;}
  6. };
  7. int main(size_t argc, char* argv[])
  8. {
  9. try
  10. {
  11. try
  12. {
  13. C4 obj;
  14. obj.f();
  15. }catch(int i)
  16. {
  17. cout<<i<<endl;
  18. }
  19. }catch(int i)
  20. {
  21. cout<<i<<endl;
  22. }
  23. return 0;
  24. }

到底是打印1还是打印2还是两个都打印?不要翻书了,这个程序运行起来,什么都不打印,直接崩溃了。用VC2008/VC2012/GCC4.7的Debug/Release都验证过了。原因呢? 和C++编译器的异常传递链条的"实现"有关,展开来解释能有几十页。能答对这道题并说出原因的面试者应该是高级以上水平,可以直接录用,别的都不用看了。
-----------------------------------------------------------------------------------------------------
    以上几个题目真的会成为面试题吗? 基本不会,面试官能答上来的也寥寥。来个测试,
    填空: 用VC2008/VC2012/GCC4.7编译下面的代码Release版:
      那么在main函数中,My的4个函数分别被调用了多少次?
        My::My()调用了___次
        My::My(const My&)调用了___次
        My& My::operator(const My&)调用了___次
        My::~My()调用了___次

点击(此处)折叠或打开

  1. #include<iostream>
  2. using namespace std;
  3. class My{
  4. public:
  5. My() {cout<<"ctor"<<endl;}
  6. My(const My&){cout<<"copy ctor"<<endl;}
  7. My& operator=(const My&){
  8. cout<<"operator="<<endl;
  9. return *this;
  10. }
  11. ~My(){cout<<"dtor"<<endl;}
  12. };
  13. My f1(){
  14. My obj;
  15. return obj;
  16. }
  17. My f2(){return My();}
  18. int main(void){
  19. My obj1;
  20. My obj2=obj1;
  21. My obj3=f1();
  22. My obj4=f2();
  23. return 0;
  24. }

答案是3,1,0,4。你答对了吗?
【第四题】下面这个指针的声明,const的意义是(A)指针指向的内容不能变,还是(B)指针本身不能变

点击(此处)折叠或打开

  1. char const* p="abc";

非常不幸。一群人都选了(B)。用编译器调试,可以发现,p的声明被编译器改成了const char*。网上有很多人说,const修饰谁就看const离谁近,例如char* const q就是说明q本身不能变,const char* r就说明r指向的内容char*不能变。但是char const* p呢? 这个const到底修饰char还是*p? 实际上所谓"离谁近就修饰谁"这个说法不准确,只有const直接跟一个变量名,中间没有其他任何符号(除了空格)的时候,const才是修饰变量名本身的。
   OK,再看下面这两种声明,const修饰谁?

点击(此处)折叠或打开

  1. const (char)* s="abc";
  2. (char) const *t="abc";

不纠结,上面两行在VC/GCC下面都是编译不过的。
    好了,有了前面4道题的讨论基础,做个小测验:构造函数,用初始化列表和不用初始化列表有什么区别? 写出以下代码的输出:

点击(此处)折叠或打开

  1. class My
  2. {
  3. public:
  4. int i;
  5. My(){i=22;}
  6. virtual void f(){printf("f:%d\n",i);}
  7. virtual void g(){printf("g:%d\n",++i);}
  8. virtual void h(){printf("h\n");}
  9. };
  10. typedef void (__thiscall *pMy)(My*);
  11. typedef pMy* VTable;
  12. int main(int argc, char* argv[])
  13. {
  14. My pf;
  15. VTable pVtable=*(VTable*)(&pf);
  16. pVtable[0](&pf);
  17. pVtable[1](&pf);
  18. pVtable[2](&pf);
  19. return 0;
  20. }

考察的要点:初始化列表使用copy ctor,而不用初始化列表,就相当于ctor + operator=。我相信你已经答对了。

上回出了几道有挑战的题,当然那些不会真的做面试题的,让一大半人都挂的题目是没有出的必要的。C++是一个语言标准,不是一个实现标准,语言标准只规定了源代码长什么样合法,没有规定看到想到的和编译出来的东西就一定一样。例如,一个类有virtual关键字修饰的函数,那么就会有这个类就会有虚函数表吗? 不一定啊,因为C++标准压根就没有规定要如何实现虚函数!所谓的虚函数表只是一种流行的,实现虚函数的方式而已。
    C++是马,而某个具体的C++编译器实现是"白马"。白马非马也!有了上一篇文章的基础,我们继续讨论和构造/析构/赋值相关的话题。
【第一题】用VC2008/VC2012/GCC4.7编译下面的代码Release版,输出多少?

点击(此处)折叠或打开

  1. #include<iostream>
  2. using namespace std;
  3. static int i=0;
  4. class My{
  5. public:
  6. My() {i+=1;}
  7. My(const My&){i+=2;}
  8. My& operator=(const My&){
  9. i+=3;
  10. return *this;
  11. }
  12. };
  13. class Derived: public My
  14. {
  15. public:
  16. Derived(){}
  17. Derived(const Derived&d){}
  18. };
  19. int main(int argc, char* argv[])
  20. {
  21. Derived d;
  22. Derived d2(d);
  23. cout<<i<<endl;
  24. return 0;
  25. }

这题的关键是Derived d2(d)这句话,继承类的拷贝构造函数,会调用基类的哪个构造函数呢? 没有显示指定初始化列表,那就是调用基类的默认构造函数,因此本题的答案是2。
【第二题】用VC2008/VC2012/GCC4.7编译下面的代码Release版,输出多少?

点击(此处)折叠或打开

  1. #include<iostream>
  2. using namespace std;
  3. static int i=0;
  4. class My{
  5. public:
  6. My() {f();}
  7. virtual void f(){i+=1;}
  8. };
  9. class Derived: public My
  10. {
  11. public:
  12. Derived(){}
  13. virtual void f(){i+=2;}
  14. };
  15. int main(void)
  16. {
  17. My* pD=new Derived();
  18. cout<<i<<endl;
  19. delete pD;
  20. return 0;
  21. }

这道题的关键是,基类构造函数里面调用了一个虚函数f,那么实际是调的基类的f还是继承累的f呢?<>这本书的条款"Nevel call virtual functions during construction or destruction"有很好的说明,但是书上举例还是不够充分,解释的也不算清楚。因为:
    C++的"类"和"对象"只是语言级的概念,C++标准根本就没有规定编译的结果里面也存在对象,这样就能给编译器和优化器以无穷的空间----反过来说,我们不能假设对象真的有物理存在,因为构造函数有可能被内联,甚至release版连对象都优化得没有了,"多态"这个概念也是可以被编译器优化掉的。因此ctor/dtor要调用类内部的虚函数而根本把所谓多态置之脑后。
    所以,C++在ctor/dtor当中遇到虚函数调用的时候,直接当成非虚函数调用类内部的版本。这道题调用的是My::f(),输出是1。如果允许在基类构造期间调用继承类的函数,那么该函数需要访问继承类的成员例如指针,可此时继承类还没有构造,指针错误,崩溃了。
【第三题】用VC2008/VC2012/GCC4.7编译下面的代码Release版,输出多少?【第二题】用VC2008/VC2012/GCC4.7编译下面的代码Release版,输出多少?【第二题】用VC2008/VC2012/GCC4.7编译下面的代码Release版,输出多少?【第三题】以下程序的运行结果是什么:

点击(此处)折叠或打开

  1. class My
  2. {
  3. My* pSelf;
  4. public:
  5. My(){pSelf=this;}
  6. ~My(){delete pSelf;}
  7. };
  8. int main(void)
  9. {
  10. My m;
  11. return 0;
  12. }

析构函数无限递归,堆栈溢出崩溃。
【第四题】以下代码有什么问题?
用VC2008/VC2012/GCC4.7编译下面的代码,会有什么问题?用VC2008/VC2012/GCC4.7编译下面的代码,会有什么问题?用VC2008/VC2012/GCC4.7编译下面的代码,会有什么问题?用VC2008/VC2012/GCC4.7编译下面的代码,会有什么问题?

点击(此处)折叠或打开

  1. #include<iostream>
  2. using namespace std;
  3. class My{
  4. public:
  5. virtual void f()=0;
  6. void haha(){f();}
  7. virtual ~My(){
  8. cout<<__FUNCTION__<<endl;
  9. f();
  10. }
  11. };
  12. class You:public My{
  13. public:
  14. void f(){cout<<__FUNCTION__<<endl;}
  15. ~You(){
  16. cout<<__FUNCTION__<<endl;
  17. haha();
  18. }
  19. };
  20. int main(){
  21. My *p=new You;
  22. delete p;
  23. return 0;
  24. }

不要着急说,调用一个haha()调用一个不存在的虚函数导致空指针错误。因为上面的代码根本编译不过。因为编译器让析构函数~My()调用本累的f(),而本类的f()是纯虚的,没有实现体,因此提示undefined reference to 'My::f()'。把上面的代码改一改,就能编过了:

点击(此处)折叠或打开

  1. #include<iostream>
  2. using namespace std;
  3. class My
  4. {
  5. public:
  6. virtual void f() = 0;
  7. void haha() { f(); }
  8. virtual ~My(){
  9. cout<<__FUNCTION__<<endl;
  10. haha();
  11. }
  12. };
  13. class You: public My
  14. {
  15. public:
  16. void f(){cout<<__FUNCTION__<<endl;}
  17. ~You(){
  18. cout<<__FUNCTION__<<endl;
  19. haha();
  20. }
  21. };
  22. int main(int argc, char* argv[])
  23. {
  24. My* pm=new You();
  25. delete pm;
  26. return 0;
  27. }

此时~My调用了一个非纯虚的函数haha,没有问题,而haha里面去调用f。运行到delete pm的时候,~My()->haha调用了My::f(),这是虚函数调用,指向一个纯虚(空指针),因此崩溃(pure virtual function call)。如果我把My* pm=new You()改成You* py=new You()会让这个错误错误消失吗? 不会,因为析构函数先~You析构继承类的部分,然后进入~My。这个~My调用的时候,继承类的部分已经不存在了,因此此时虚函数的调用路径回到了基类,程序还是崩溃了。
-----------------------------------------------------------------
    虚拟机语言如C#/Java对象生命周期是GC全局管理,因此不存在这样的陷阱,多态的基类引用在ctor里面调用虚函数,是调进继承类。一下两段代码都是输出两个"Derived"。

点击(此处)折叠或打开

  1. class Base
  2. {
  3. public Base() { f(); }
  4. public virtual void f() { Console.WriteLine("Base"); }
  5. }
  6. class Derived : Base
  7. {
  8. public Derived() { f(); }
  9. public override void f() { Console.WriteLine("Derived"); }
  10. }
  11. [STAThread]
  12. static void Main(string[] args)
  13. {
  14. Base pb = new Derived();
  15. }

点击(此处)折叠或打开

  1. public class JavaApplication1 {
  2. /**
  3. * @param args the command line arguments
  4. */
  5. static public class Base
  6. {
  7. public Base() { f(); }
  8. public void f() { System.out.println("Base"); }
  9. }
  10. static public class Derived extends Base
  11. {
  12. public Derived() { f(); }
  13. public void f() { System.out.println("Derived"); }
  14. }
  15. public static void main(String[] args) {
  16. // TODO code application logic here
  17. JavaApplication1.Base pb = new JavaApplication1.Derived();
  18. }
  19. }

C++陷阱系列:让面试官倒掉的题的更多相关文章

  1. Android开发面试经——6.常见面试官提问Android题②(更新中...)

    版权声明:本文为寻梦-finddreams原创文章,请关注:http://blog.csdn.net/finddreams 关注finddreams博客:http://blog.csdn.net/fi ...

  2. Android开发面试经——5.常见面试官提问Android题①

    版权声明:本文为寻梦-finddreams原创文章,请关注:http://blog.csdn.net/finddreams 关注finddreams博客: http://blog.csdn.net/f ...

  3. 【BAT面试题系列】面试官:你了解乐观锁和悲观锁吗?

    前言 乐观锁和悲观锁问题,是出现频率比较高的面试题.本文将由浅入深,逐步介绍它们的基本概念.实现方式(含实例).适用场景,以及可能遇到的面试官追问,希望能够帮助你打动面试官. 目录 一.基本概念 二. ...

  4. RabbitMQ系列(六)--面试官问为什么要使用MQ,应该怎么回答

    如果简历中有写到使用过RabbitMQ或者其他的消息中间件,可能在MQ方面的第一个问题就是问:为什么要使用MQ 面试官期望的回答 1.项目中有什么业务场景需要用到MQ 2.但是用了MQ,会带来很多问题 ...

  5. 《吊打面试官》系列-HashMap

    你知道的越多,你不知道的越多 点赞再看,养成习惯 本文 GitHub https://github.com/JavaFamily 上已经收录,有一线大厂面试点思维导图,也整理了很多我的文档,欢迎Sta ...

  6. 《吊打面试官》系列-ArrayList

    你知道的越多,你不知道的越多 点赞再看,养成习惯 本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试点思维导图,也整理了很多我的文档,欢迎Star和 ...

  7. 面试高峰期,如何应对面试官的jvm刁难,特写一篇jvm面经(第一部)

    已经进入三月份,正所谓金三银四,正是一年最好的招聘期,想必我的公号粉丝们一定有不少想要跳槽的吧,哈哈,/**偷偷告诉你们其实小编也准备跳槽*/(我要加个注释,被老板知道可就完蛋了),说到面试,想必大家 ...

  8. <转载>面试官,不要再问我三次握手和四次挥手

    版权声明:本文为CSDN博主「夏雪冬日」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明. 原文链接:https://blog.csdn.net/hyg0811/a ...

  9. 《吊打面试官》系列-Redis基础

    你知道的越多,你不知道的越多 点赞再看,养成习惯 前言 Redis在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在Redis的使用和原理方面对小伙伴们进行360°的刁难.作为一个在互联 ...

随机推荐

  1. CentOS 7设置网卡开机自动启用

    一.查看网卡配置 root权限 [root@dbsyn ~]# ip addr 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue ...

  2. Laravel 集合的处理

    其中的方法有: $arrs = collect($arr)->collapse()->collapse() //去除最外一层数组,不论最外层数组时否有值,都会去除掉 collect($ar ...

  3. yaml文件 .yml

    YAML文件简介 我们可能在spring配置文件里见到过.yml格式的东东,配置文件不都是.propertie或者.xml文件吗?.yml是什么鬼,今天我带你们来一探究竟. YAML(Yet Anot ...

  4. XPath知识点简单总结(思维导图)

    XPath是一种用于在XML文档中查找信息的语言,其对HTML也有很好的支持,所以在网络爬虫中可用于解析HTML文档.参考链接. 下图是XPath知识点的简单总结成思维导图:

  5. C#中给WebClient添加代理Proxy

    效果图: using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; ...

  6. scala成长之路(3)隐式转换

    不废话,先上例子:定义一个参数类型为String的函数: scala> def sayHello(name:String) = println("hello " + name ...

  7. 转载:小白使用eclipse提交到GitHub (详细步骤)

    本篇文章只是备忘,以防电脑重装找不到记录 教程:https://blog.csdn.net/bendanany/article/details/78891804

  8. 贪心算法之Huffman

    Huffman编码,权重越大,离根节点越大.所以就是不断的选取两个最小的树,然后组成一颗新树,加入集合,然后去除已选的两棵树.不断的循环,直到最后的树的集合只剩下一棵,则构建完成,最后输出Huffma ...

  9. TRANSLATE(转换大/小写并替换字符)

    可以将字母 转换大/小 写或使用替 换规则. 要转换大/小 写,请使用 TRANSLATE 语句,用法 如下: 语法 TRANSLATE <c> TO UPPER CASE. TRANSL ...

  10. 8.Mongodb备份与恢复

    1.备份 语法 mongodump -h dbhost -d dbname -o dbdirectory -h:服务器地址,也可以指定端口号 -d:需要备份的数据库名称 -o:备份的数据存放位置,此目 ...