今天和几位同仁一起探讨了一下C++的一些基础知识,在座的同仁都是行家了,有的多次当过C++技术面试官。不过我出的题过于刁钻: 不是看起来太难,而是看起来极其容易,但是其实非常难! 结果一圈下来,3道题,竟然无一人答对,于是只能安慰大家,这几道题,答不对是正常的。
    "你真的清楚构造函数,拷贝构造函数,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<Noisy>的两个程序,再把临时对象析构掉。太傻了吧!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编译下面的代码,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 f(){
  14. My obj;
  15. return obj;
  16. }
  17. int main(void){
  18. My obj1;
  19. My obj2=obj1;
  20. My obj3=f();
  21. return 0;
  22. }

C++ 坑人系列(1): 让面试官晕倒的题目的更多相关文章

  1. C++陷阱系列:让面试官倒掉的题

    http://blog.chinaunix.net/uid-22754909-id-3969535.html 今天和几位同仁一起探讨了一下C++的一些基础知识,在座的同仁都是行家了,有的多次当过C++ ...

  2. 感受一把面试官通过一道题目引出的关于 TCP 的 5 个连环炮!

    面试现场:从 URL 在浏览器被被输入到页面展现的过程中发生了什么? 相信大多数准备过的同学都能回答出来,但是如果继续问:收到的 HTML 如果包含几十个图片标签,这些图片是以什么方式什么顺序下载?建 ...

  3. 飞越面试官(一)--Java基础

    大家好!我是本公众号唯一官方指定没头屑的小便--怕屁林. 众所周知,现场面试(包括视频面试)多数时候是没有白板,也就是说,对于你的知识点.项目经验.过往经历和个人介绍等等,都是靠一张嘴.所以考虑到这个 ...

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

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

  5. 《吊打面试官》系列-Redis哨兵、持久化、主从、手撕LRU

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

  6. 《吊打面试官》系列-Redis终章_凛冬将至、FPX_新王登基

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

  7. 《吊打面试官》系列-Redis常见面试题(带答案)

    你知道的越多,你不知道的越多 点赞再看,养成习惯 GitHub上已经开源,有面试点思维导图,欢迎[Star]和[完善] 前言 Redis在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在 ...

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

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

  9. 《吊打面试官》系列-ConcurrentHashMap & HashTable

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

随机推荐

  1. python自动化测试报告(excel篇)

    转:https://www.jianshu.com/p/6c5d328bf390 # -*- coding: utf-8 -*- import xlsxwriter def get_format(wd ...

  2. 单线程实现并发——协程,gevent模块

    一 并发的本质 1 切换 2 保存状态 二 协程的概念 协程,又称微线程,纤程.英文名Coroutine.单线程下实现并发,用户从应用程序级别控制单线程下任务的切换,注意一定是遇到I/O才切. 协程的 ...

  3. vue.js源码学习分享(一)

    今天看了vue.js源码  发现非常不错,想一边看一遍写博客和大家分享 /** * Convert a value to a string that is actually rendered. *转换 ...

  4. jsp文件放在WebRoot下还是WebInfo下

    观点一:(较为赞同) 安全性不是真正的原因,因为jsp是要解析后才显示到浏览器的,即使用户知道你jsp的路径,也不可能通过浏览器看到jsp源码的,而如果是通过其它手段入侵服务器的话,放在WEB-INF ...

  5. 【Visual Studio】The project appears to be under source control, but the associated source control plug-in is not installed on this computer

    [问题描述]用 Visual Studio 2013打开一个项目时,出现下面错误: [问题原因]参考 http://codeverge.com/asp.net.web-forms/the-projec ...

  6. linux环境内存分配原理 mallocinfo【转】

    转自:http://www.cnblogs.com/dongzhiquan/p/5621906.html Linux的虚拟内存管理有几个关键概念: Linux 虚拟地址空间如何分布?malloc和fr ...

  7. libstdc++.so.6: version `GLIBCXX_3.4.20' not found

    libstdc++.so.6: version `GLIBCXX_3.4.20' not found 参考链接: (1)解决/usr/lib/libstdc++.so.6: version `GLIB ...

  8. 富文本ZSSRichTextEditor之趟坑集锦

    富文本ZSSRichTextEditor是iOS原生与网页交互的集大成者,各种交互.自然问题也是多多,这篇文文章陆续更新遇到的奇葩问题. 1.问题1:从头条这种文章里头复制粘贴的文章,里边有图片,我们 ...

  9. Vue.js实战:初识Vue.js

    一.Vue.js是什么 简单小巧的核心,渐进式技术栈,足以应付任何规模的应用. 简单小巧指的是Vue.js 压缩后大小仅有17KB 所谓渐进式(Progressive)就是你一步一步,有阶段性地来使用 ...

  10. Widows下利用OpenSSL生成证书

    1.下载OpenSSL的windows版本 32位:openssl-1.0.2a-i386-win32.zip 64位:openssl-1.0.2a-x64_86-win64.zip 下载之后解压即可 ...