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. }

最新文章

  1. Git私钥openssh格式转ppk
  2. Windows系统bug
  3. js友好提示是否继续,post提交
  4. HNOI2008玩具装箱 (斜率优化)
  5. 第8章BOM笔记
  6. ural1682 Crazy Professor
  7. Android----基于多触控的图片缩放和拖动代码实现
  8. 自建Nuget服务器
  9. 序列化Serializable和Parcelable
  10. Qt websocket
  11. React16的interactiveUpdates
  12. Human Motion Analysis with Wearable Inertial Sensors——阅读3
  13. docker 私有仓库简易搭建
  14. 天哪又要搬家啦qvq
  15. extern C小结
  16. 初识go的tomb包
  17. Win10 x64 + CUDA 10.0 + cuDNN v7.5 + TensorFlow GPU 1.13 安装指南
  18. SQL Server 常见数据类型介绍
  19. scrapy中deferred的回调
  20. error LNK2001: 无法解析的外部符号 _H5T_NATIVE_DOUBLE_g

热门文章

  1. 搭建基本的React Native开发环境
  2. Java常用的正则校验
  3. Java中抽象类与接口的比较
  4. github 常用
  5. thinkphp发送邮箱(以thinkphp5作为示例)。
  6. Apache Maven(六):存储库
  7. Source Insight的使用
  8. Java学习笔记四:Java的八种基本数据类型
  9. [转载]三小时学会Kubernetes:容器编排详细指南
  10. R语言绘图:时间序列分析