今天和几位同仁一起探讨了一下C++的一些基础知识,在座的同仁都是行家了,有的多次当过C++技术面试官。不过我出的题过于刁钻: 不是看起来太难,而是看起来极其容易,但是其实非常难! 结果一圈下来,4道题,平均答对半题。于是只能安慰大家,这几道题,答不对是正常的。
"你真的清楚构造函数,拷贝构造函数,operator=,析构函数都做了什么吗? 它们什么时候被调用?",这些问题可不是面向初菜的问题,对于老鸟而言,甚至对于许多自诩为老手的人而言,倒在这上面也是很正常的。因为这个问题的答案不但考察我们对于C++语言的理解,而且答案是和编译器的实现有关的!
【第一题】以下代码,main函数中G.i的打印结果是什么? 写在一张纸上,再看答案。我不是在挑战大家的知识,我是在挑战很多人的常识。
-
#include<iostream>
-
using namespace std;
-
class G
-
{
-
public:
-
static int i;
-
G() {cout<<"ctor"<<endl;i+=1;}
-
G(const G& rg){cout<<"copy ctor"<<endl;i+=2;}
-
G& operator=(const G& rg){cout<<__FUNCTION__<<endl;i+=3;return *this;}
-
};
-
int G::i=0;
-
G Create()
-
{
-
cout<<__FUNCTION__<<" starts"<<endl;
-
G obj;
-
cout<<__FUNCTION__<<" ends"<<endl;
-
return obj;
-
}
-
int main(int argc, char* argv[])
-
{
-
G g1=Create();
-
cout<<"G.i="<<G::i<<endl;
-
return 0;
-
}
"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没有这个问题!瞬间对
理查德-斯托曼无比敬仰。
【第二题】以下程序的运行结果是什么:
-
#include <vector>
-
#include <iostream>
-
using namespace std;
-
struct Noisy {
-
Noisy() {std::cout << "constructed\n"; }
-
Noisy(const Noisy&) { std::cout << "copied\n"; }
-
~Noisy() {std::cout << "destructed\n"; }
-
};
-
-
std::vector<Noisy> f()
-
{
-
std::vector<Noisy> v = std::vector<Noisy>(2); // copy elision from temporary to v
-
return v; // NRVO from v to the nameless temporary that is returned
-
}
-
-
void fn_by_val(std::vector<Noisy> arg) // copied
-
{
-
std::cout << "arg.size() = " << arg.size() << '\n';
-
}
-
-
void main()
-
{
-
std::vector<Noisy> v = f(); // copy elision from returned temporary to v
-
cout<<"------------------before"<<endl;
-
fn_by_val(f());// and from temporary to the argument of fn_by_val()
-
cout<<"------------------after"<<endl;
-
}
第一轮没有被枪毙的同学注意了: 这道题目的答案仍然是和编译器有关的,而且和版本还有关系。
(2.1) VC2008 Debug
版的运行结果
-
constructed
-
copied
-
copied
-
destructed
-
copied
-
copied
-
destructed
-
destructed
-
------------------before
-
constructed
-
copied
-
copied
-
destructed
-
copied
-
copied
-
destructed
-
destructed
-
arg.size() = 2
-
destructed
-
destructed
-
------------------after
-
destructed
-
destructed
-
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版的运行结果
-
constructed
-
copied
-
copied
-
destructed
-
------------------before
-
constructed
-
copied
-
copied
-
destructed
-
arg.size() = 2
-
destructed
-
destructed
-
------------------after
-
destructed
-
destructed
-
Press any key to continue . . .
换个编译器VC2012编译出来的,就聪明多了(Debug/Release运行结果相同):
-
constructed
-
constructed
-
------------------before
-
constructed
-
constructed
-
arg.size() = 2
-
destructed
-
destructed
-
------------------after
-
destructed
-
destructed
-
Press any key to continue . . .
调用了两次ctorl来构造这个vector。性能提高多了。慢点,还有一点不同,因为函数fn_by_val的参数是传值而不是传引用,所以编译器知道在这个函数里面vector没有被修改,因此直接把传值优化成了传const&! VC2012的Debug/Release一致!终于赶上GCC了,不容易。
问题:到底什么时候一个拷贝构造的操作可以被优化掉呢? C++标准还是有定义的,这个网页说的很清楚()。其中的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:
-
struct My {
-
My() {std::cout << "constructed\n"; }
-
My(const My&) { std::cout << "copied\n"; }
-
~My() {std::cout << "destructed\n"; }
-
};
-
void f(My m){}
-
void main()
-
{
-
f(My());
-
}
运行结果是:
-
constructed
-
destructed
-
Press any key to continue . . .
看起来,临时变量My()被优化成了一个const My&并传递了进去,当作了f的参数。
(2.4)再看一个throw的例子:
-
struct My {
-
My() {std::cout << "constructed\n"; }
-
My(const My&) { std::cout << "copied\n"; }
-
~My() {std::cout << "destructed\n"; }
-
};
-
void fm(){throw My();}
-
void main()
-
{
-
try{
-
cout<<"before throw"<<endl;
-
fm();
-
cout<<"after throw"<<endl;
-
}catch(My& m)
-
{}
-
}
这里的throw My()语句构造的My对象,优化后是构造在try的栈上面而非fm的栈上面,因此没有copy ctor的调用。
【第三题】以下程序的运行结果是什么?
-
using namespace std;
-
struct C4
-
{
-
void f(){throw 1;}
-
~C4(){throw 2;}
-
};
-
int main(size_t argc, char* argv[])
-
{
-
try
-
{
-
try
-
{
-
C4 obj;
-
obj.f();
-
}catch(int i)
-
{
-
cout<<i<<endl;
-
}
-
}catch(int i)
-
{
-
cout<<i<<endl;
-
}
-
return 0;
-
}
到底是打印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()调用了___次
-
#include<iostream>
-
using namespace std;
-
class My{
-
public:
-
My() {cout<<"ctor"<<endl;}
-
My(const My&){cout<<"copy ctor"<<endl;}
-
My& operator=(const My&){
-
cout<<"operator="<<endl;
-
return *this;
-
}
-
~My(){cout<<"dtor"<<endl;}
-
};
-
My f1(){
-
My obj;
-
return obj;
-
}
-
My f2(){return My();}
-
int main(void){
-
My obj1;
-
My obj2=obj1;
-
My obj3=f1();
-
My obj4=f2();
-
return 0;
-
}
答案是3,1,0,4。你答对了吗?
【第四题】下面这个指针的声明,const的意义是(A)指针指向的内容不能变,还是(B)指针本身不能变
非常不幸。一群人都选了(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修饰谁?
-
const (char)* s="abc";
-
(char) const *t="abc";
不纠结,上面两行在VC/GCC下面都是编译不过的。
好了,有了前面4道题的讨论基础,做个小测验:构造函数,用初始化列表和不用初始化列表有什么区别? 写出以下代码的输出:
-
class My
-
{
-
public:
-
int i;
-
My(){i=22;}
-
virtual void f(){printf("f:%d\n",i);}
-
virtual void g(){printf("g:%d\n",++i);}
-
virtual void h(){printf("h\n");}
-
};
-
typedef void (__thiscall *pMy)(My*);
-
typedef pMy* VTable;
-
int main(int argc, char* argv[])
-
{
-
My pf;
-
VTable pVtable=*(VTable*)(&pf);
-
pVtable[0](&pf);
-
pVtable[1](&pf);
-
pVtable[2](&pf);
-
return 0;
-
}
考察的要点:初始化列表使用copy ctor,而不用初始化列表,就相当于ctor + operator=。我相信你已经答对了。
阅读(411) | 评论(0) | 转发(0) |