面试题13 main函数执行前还会执行什么代码
【解析】
请看下面的程序代码
#include <iostream>
using namespace std;
class Test
{
public:
Test() //构造函数
{
cout << "constructor of Test====this=" << this << endl;
}
};
Test a; //全局变量
int main()
{
cout << "main() start====" << endl;
Test b;
return 0;
}
运行结果:
显然,这里的执行顺序为:首先午行全局对象a的构造,然后进入main函数中,再进行局部对象b的构造。
【答案】
全局对象的构造函数会在main函数之前执行。
面试题14 C++中的空类默认会产生哪些类成员函数
【解析】
对于一个C++的空类,比如Empty:
class Empty
{
};
虽然Empty类定义中没有任何成员,但为了进行一些默认的操作,编译器会加入以下一些成员函数,
这些成员函数使得类的对象拥有一些通用的功能。
默认构造函数和复制构造函数。它们被用于类的对象的构造函数。
析构函数。它被用于类的对象的析构过程。
赋值函数。它被用于同类的对象间的赋值过程。
取值运算。当对类的对象进行取地址(&)时,此函数被调用。
即虽然程序员没有定义类的任何成员,但是编译器也会插入一些函数,完整的Empty类定义如下。
【答案】
C++的空类中,默认会产生默认构造函数、复制构造函数、析构函数、赋值函数以及取值运算。
面试题15 构造函数和析构函数是否可以被重载
【答案】
构造函数可以被重载,因为构造函数可以有多个,且可以带参数。
析构函数不可以被重载。因为析构函数只能有一个,且不能带参数。
面试题16 关于重载构造函数的调用
下面对程序执行结果的描述中,正确的是()。
A,将会产生运行时错误
B,将会产生编译错误
C,将会执行成功
D,以上说法都不正确
【解析】
Test类定义了两个构造函数。当编译到代码第10行时,由于构造函数的模糊语义,编译器无法决定调用哪一个构造函数,因此会产生编译错误。
另外,如果把第10行注释掉,编译器将不会产生错误。因为C++编译器认为潜在的二义性不是一种错误。
【答案】
B。
面试题17 构造函数的使用
以下代码中的输出语句输出0吗?为什么?
#include <iostream>
using namespace std;
struct CLS
{
int m_i;
CLS(int i): m_i(i) { }
CLS()
{
CLS(0);
}
};
int main()
{
CLS obj;
cout << obj.m_i << endl;
return 0;
}
【解析】
在代码第10行,不带参数的构造函数直接调用了带参数的构造函数。这种调用往往被很多人误解,以为可以通过构造函数的重载和相互调用实现一些类似默认参数的功能,其实是不行的,而且往往会有副作用。下面加几条打印对象地址的语句到原来的程序中。
#include <iostream>
using namespace std;
struct CLS
{
int m_i;
CLS(int i): m_i(i)
{
cout << "CLS(): this = " << this << endl;
}
CLS()
{
CLS(0);
cout << "CLS(int): this = " << this << endl;
}
};
int main()
{
CLS obj;
cout << "&obj = " << &obj << endl;
cout << obj.m_i << endl;
return 0;
}
程序执行结果如下:
可以看到,在带参数构造函数里打印出来的对象地址和对象obj的地址不一致。实际上,代码第13行的调用只是在栈上生成了一个临时对象,对于自己本身毫无影响。还可以发现,构造函数的互相调用引起的后果不是死循环,而是栈溢出。
【答案】
输出不为0,是个随机数。
原因是构造函数内调用构造函数只是在栈上生成了一个临时对象,对于自己本身毫无影响。
面试题18 构造函数explicit与普通构造函数的区别
【解析】
explicit构造函数是用来防止隐式转换的。请看下面代码。
#include <iostream>
using namespace std;
class Test1
{
public:
Test1(int n) {num = n;} //普通构造函数,
private:
int num;
};
class Test2
{
public:
explicit Test2(int n) {num = n;} //explicit(显式)构造函数,
private:
int num;
};
int main()
{
Test1 t1 = 12; //隐式调用其构造函数,成功
Test2 t2 = 12; //编译错误,不能隐式调用其构造函数
Test2 t3(12); //显示调用成功
return 0;
}
Test1的构造函数带一个int型的参数,代码第23行会隐式转换成调用Test1的这个构造函数。而Test2的构造函数被声明为explicit(显式),这表示不能通过隐式转换来调用这个构造函数,因此代码第24行会出现编译错误。
【答案】
普通构造函数能够被隐式调用,而explicit构造函数只能被显示调用。
面试题19 explicit构造函数的作用
下面的程序f()被调用时,输出是什么?
#include <iostream>
#include <string>
using namespace std;
class Number
{
public:
string type;
Number():type("void") {}
explicit Number(short):type("short") {}
Number(int): type("int"){}
};
void Show(const Number& n) {cout << n.type << endl;}
int main()
{
short s = 42;
Show(s);
return 0;
}
A. void
B. short
C. int
D. None of the above
【解析】
Show()函数的参数类型是Number类对象的引用,代码第19行调用Show(s)时采取了以下所示的步骤。
(1)Show(s)中的s为short类型,其值为42,因此首先检查参数为short的构造函数能否被隐式转换。由于代码第10行的构造函数被声明为显式调用(explicit),因此不能隐式转换。于是进行下一步。
(2)42自动转换为int类型。
(3)检查参数为int的构造函数能否被隐式转换。由于代码第11行参数为int的构造函数没有被声明为显式调用,因此调用此构造函数构造出一个临时对象。
(4)打印上一步临时对象的type成员,即“int”。
【答案】
C
面试题20 C++中虚析构函数的作用是什么
【解析】
大家知道,析构函数是为了在对象不被使用之后释放它的资源,虚函数是为了实现多态。那么,
把析构函数声明为virtual有什么作用呢?请看下面的代码。
#include <iostream>
using namespace std;
class Base
{
public:
Base() {} //Base的构造函数,
~Base() //Base的析构函数,
{
cout << "Output from the destructor of class Base!" << endl;
}
virtual void DoSomething()
{
cout << "Do something in class Base!" << endl;
}
};
class Derived: public Base
{
public:
Derived() {} //Derived的构造函数,
~Derived() //Derived的析构函数,
{
cout << "Output from the destructor of class Derived!" << endl;
}
virtual void DoSomething()
{
cout << "Do something in class Derived!" << endl;
}
};
int main()
{
Derived *pTest1 = new Derived();//Derived类的指针,
pTest1->DoSomething();
delete pTest1;
cout << endl;
Base *pTest2 = new Derived(); //Base类的指针,
pTest2->DoSomething();
delete pTest2;
return 0;
}
先看程序输出结果:
代码第36行可以正常释放pTest1的资源,而代码第42行没有正常释放pTest2的资源,因为从结果看,Derived类的析构函数并没有被调用。通常情况下,类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。原因是指针pTest2是Base类型的指针,释放pTest2时只进行Base类的析构函数。在代码第8行前面加上virtual关键字后的运行结果如下。
此时释放指针pTest2时,由于Base的析构函数是virtual的,就会先找到并执行Derived类的析构函数,然后执行Base类的析构函数,资源正常释放,避免了内存泄漏。
因此,只有当一个类被用来作为基类的时候,才会把析构函数写成虚函数。
面试题21 看代码写结果——析构函数的执行顺序
#include <iostream>
using namespace std;
class A
{
private:
int a;
public:
A(int aa) { a = aa; }
~A() {cout << "Destructor A!======" << a << endl;}
};
class B: public A
{
private:
int b;
public:
B(int aa = 0, int bb = 0): A(aa) {b = bb;}
~B() {cout << "Destructor B!======" << b <<endl;}
};
int main()
{
B obj1(5), obj2(6, 7);
return 0;
}
【解析】
本题考查的是析构函数的执行顺序。析构函数的执行顺序与构造函数的执行顺序相反。
main()函数中定义了两个类B的对象,它们的基数是A。由于这两个对象都是栈中分配的,当main()函数退出时会发生析构,又因为obj1比obj2先声明,所以obj2先析构。它们析构的顺序是首先执行B的析构函数,然后执行A的析构函数。
【答案】
程序输出如下。
面试题22 复制构造函数是什么?什么是深复制和浅复制
复制构造函数是什么?什么情况下会用到它?什么是深复制和浅复制?
【解析】
先来说明什么是复制构造函数,以及它被调用的场合。
复制构造函数是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构件及初始化。
如果在类中没有显式地声明一个复制构造函数,那么,编译器会私下里制定一个函灵敏来进行对象之间的位复制(bitwise copy)。
这个隐含的复制构造函数简单地关联了所有的类成员。
在C++中,下面是3种对象需要复制的情况。因此,复制构造函数将会被调用。
(1)一个对象以值传递的方式传入函数体。
(2)一个对象以值传递的方式从函数返回。
(3)一个对象需要通过另外一个对象进行初始化。
下面的程序代码说明了上述三种情况。
#include <iostream>
using namespace std;
class Test
{
public:
int a;
Test(int x)
{
a = x;
}
Test(const Test &test) //复制构造函数,
{
cout << "copy constructor====" << this << " &test==" << &test << endl;
a = test.a;
}
};
void fun1(Test test) //(1)值传递传入函数体,
{
cout << "fun1()...&test==" << &test << endl;
}
Test fun2() //(2)值传递从函数体返回,
{
Test t(2);
cout << "fun2(),,,,&t===" << &t << endl;
return t;
}
int main()
{
Test t1(1);
Test t2 = t1; //(3)用t1对t2做初始化,
cout << "before fun1()...&t1=" << &t1 << " &t2==" << &t2 << endl << endl;
fun1(t1);
cout << endl;
Test t3 = fun2();
cout << "after fun2()..&t3====" << &t3 << endl;
return 0;
}
程序执行结果如下。
fun1(),fun2()以及代码第34行分别对应了上面3种调用复制构造函数的情况。
接下来说明深复制与浅复制。
既然系统会自动提供一个默认的复制构造函数来处理复制,那么,为什么要去自定义复制构造函数呢?下面的程序代码说明了这个问题。
#include <iostream>
using namespace std;
class Test
{
public:
char *buf;
Test(void)//不带参数的构造函数,
{
buf = nullptr;
}
Test(const char *str)//带参数的构造函数,
{
buf = new char[strlen(str) + 1];//分配堆内存
cout << "buf======" << &buf << endl;
strcpy(buf, str);//复制字符串,
}
~Test()
{
cout << "~Test========buf-======" << &buf << endl;
if(nullptr != buf)
{
delete buf;//释放buf指向的堆内存,
buf = nullptr;
}
}
};
int main()
{
Test t1("hello");
Test t2 = t1;//调用默认的复制构造函数
cout << "t1.buf====" << t1.buf << "\t t2.buf====" << t2.buf << endl;
cout << "(t1.buf == t2.buf) ? " << (t1.buf == t2.buf ? "yes" : "no") << endl;
return 0;
}
这里Test类的buf成员是一个字符指针,在带参数的构造函数中为之分配了一块堆内存来存放字符串,然后在析构函数中又将堆内存释放。在main()函数(代码第30行)使用了对象复制,因此会调用默认复制构造函数。程序的执行结果如下。
这里程序崩溃发生在main()函数退出对象析构的时候。由前两行的打印结果可以看出,默认复制构造函数只是简单地把两个对象的指针估赋值运算,它们指向的是同一个地址。当产生两次析构,释放同一块堆内存时发生崩溃。可以在Test类里通过添加一个自定义的复制构造函数解决两次析构的问题。
Test(Test &test)
{
buf = new char[strlen(test.buf) + 1];
strcpy(buf, test.buf);
}
程序执行结果如下。
由于此时buf又分配了一块堆内存来保存字符串,t1的buf和t2的buf分别指向不同的堆内存,析构时就不会发生程序崩溃。
总结:如果复制的对象中引用了某个外部的内容(例如分配在堆上的数据),那么在复制这个对象的时候,让新旧两个对象
指向同一个外部的内容,就是浅复制:如果在复制这个对象的时候为新对象制作了外部对象的独立复制,那么就是深复制。
【答案】
复制构造函数是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的基他对象的构件及初始化。
浅复制指让新旧两个对象指向同一个外部的内容,而深复制是指为新对象制作了外部对象的独立复制。
面试题23 编译器与默认的copyconstructor
什么时候编译器会生成默认的copy constructor呢?如果已经写了一个构造函数,编译器还会生成copy constructor吗?
【答案】
如果用户没有自定义复制构造函数,并且在代码中用到了复制构造函数,那么编译器就会生成默认的复制构造函数;但如果用户定义了复制构造函数,那么编译器就不会再生成复制构造函数。
如果用户定义了一个构造函数,且不是复制构造函数,而此时在代码中用到了复制构造函数,那么编译器也还会生成默认的复制构造函数;如果没有使用,则编译器就不会生成默认的复制构造函数。
面试题24 写一个继承类的复制函数
【解析】
当然,如果基类中没有私有成员,即所有成员都以被派生类访问,则派生类的复制构造函数可以很容易写。但如果基类有私有成员,并且这些私有成员必须在调用派生类的复制构造函数时被初始化,在这种情况下又该怎么做呢?
编写继承类的复制函数有一个原则:使用基类的复制构造函数。这个原则其实就是解问心有愧上述问题的方案。请看下面的程序代码。
#include <iostream>
using namespace std;
class Base
{
public:
Base():i(0){cout << "Base()===i=" << i << endl;}
Base(int n):i(n){cout << "Base(int)==" << endl;}
Base(const Base &b):i(b.i)//复制构造函数,
{
cout << "Base(Base&)==" << endl;
}
private:
int i;//私有成员,
};
class Derived: public Base
{
public:
Derived():Base(0),j(0) {cout << "Derived()=i="/* << this->i */<< " j=" << j << endl;}//默认普通构造函数,
Derived(int m, int n):Base(m), j(n) {cout << "Derived(int)=j=" << j << endl;}//普通构造函数,
Derived(Derived &obj):Base(obj), j(obj.j)//Derived类的复制构造函数,
{
cout << "Derived(Derived&)==i="/* << this->i */<< " j=" << j << endl;
}
private:
int j;
};
int main()
{
Base b(1);
Derived obj(2, 3);
cout << "=================" << endl;
Derived d(obj);
cout << "=================" << endl;
return 0;
}
Derived类继承自Base类,因此在Derived类内不能使用obj.i或Base::i的方式访问Base的私有成员i。很明显,其复制构造函灵敏只有使用Base(obj)(代码第22行)的方式调用其基类的复制构造函数来给基类的私有成员i的初始化。