41、类什么时候会析构?
- 对象生命周期结束,被销毁时;
- 对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。
42、构造函数的几种关键字
default
default关键字可以显式要求编译器生成合成构造函数,防止在调用时相关构造函数类型没有定义而报错。
#include <iostream>
using namespace std;
class CString
{
public:
CString() = default; //语句1
//构造函数
CString(const char* pstr) : _str(pstr){}
//析构函数
~CString(){}
public:
string _str;
};
int main()
{
auto a = new CString(); //语句2
cout << "Hello World" <<endl;
return 0;
}
//运行结果
//Hello World
如果没有加语句1,语句2会报错,表示找不到参数为空的构造函数,将其设置为default可以解决这个问题。
delete
delete关键字可以删除构造函数、赋值运算符函数等,这样在使用的时候会得到友善的提示。
#include <iostream>
using namespace std;
class CString
{
public:
void* operator new() = delete;//这样不允许使用new关键字
//析构函数
~CString(){}
};
int main()
{
auto a = new CString(); //语句1
cout << "Hello World" <<endl;
return 0;
}
在执行语句1时,会提示new方法已经被删除,如果将new设置为私有方法,则会报惨不忍睹的错误,因此使用delete关键字可以更加人性化的删除一些默认方法。
43、构造函数、拷贝构造函数和赋值操作符的区别
构造函数
- 对象不存在,没用别的对象初始化,在创建一个新的对象时调用构造函数。
拷贝构造函数
- 对象不存在,但是使用别的已经存在的对象来进行初始化。
赋值运算符
- 对象存在,用别的对象给它赋值,这属于重载“=”号运算符的范畴,“=”号两侧的对象都是已存在的。
#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout << "我是构造函数" << endl;
}
A(const A& a)
{
cout << "我是拷贝构造函数" << endl;
}
A& operator = (A& a)
{
cout << "我是赋值操作符" << endl;
return *this;
}
~A() {};
};
int main()
{
A a1; //调用构造函数
A a2 = a1; //调用拷贝构造函数
a2 = a1; //调用赋值操作符
return 0;
}
//输出结果
//我是构造函数
//我是拷贝构造函数
//我是赋值操作符
44、拷贝构造函数和赋值运算符重载的区别?
- 拷贝构造函数是函数,赋值运算符是运算符重载。
- 拷贝构造函数会生成新的类对象,赋值运算符不能。
- 拷贝构造函数是直接构造一个新的类对象,所以在初始化对象前不需要检查源对象和新建对象是否相同;
- 赋值运算符需要上述操作并提供两套不同的复制策略,另外赋值运算符中如果原来的对象有内存分配则需要先把内存释放掉。
- 形参传递是调用拷贝构造函数(调用的被赋值对象的拷贝构造函数),但并不是所有出现"="的地方都是使用赋值运算符,如下:
Student s;
Student s1 = s; // 调用拷贝构造函数
Student s2;
s2 = s; // 赋值运算符操作
注: 类中有指针变量时要重写析构函数、拷贝构造函数和赋值运算符。
45、什么是虚拟继承?
由于C++支持多继承,除了public、protected和private三种继承方式外,还支持虚拟(virtual)继承,举个例子:
#include <iostream>
using namespace std;
class A{}
class B : virtual public A{};
class C : virtual public A{};
class D : public B, public C{};
int main()
{
cout << "sizeof(A):" << sizeof A <<endl; // 1,空对象,只有一个占位
cout << "sizeof(B):" << sizeof B <<endl; // 4,一个bptr指针,省去占位,不需要对齐
cout << "sizeof(C):" << sizeof C <<endl; // 4,一个bptr指针,省去占位,不需要对齐
cout << "sizeof(D):" << sizeof D <<endl; // 8,两个bptr,省去占位,不需要对齐
}
上述代码所体现的关系是,B和C虚拟继承A,D又公有继承B和C,这种方式是一种菱形继承或者钻石继承,可以用如下图来表示:
虚拟继承的情况下,无论基类被继承多少次,只会存在一个实体。
虚拟继承基类的子类中,子类会增加某种形式的指针,或者指向虚基类子对象,或者指向一个相关的表格;
**表格中存放的不是虚基类子对象的地址,就是其偏移量,此类指针被称为bptr,**如上图所示。
如果既存在vptr又存在bptr,某些编译器会将其优化,合并为一个指针。
46、什么情况会自动生成默认构造函数?
-
带有默认构造函数的类成员对象,如果一个类没有任何构造函数,但它含有一个成员对象,而后者有默认构造函数,那么编译器就为该类合成出一个默认构造函数。
不过这个合成操作只有在构造函数真正被需要的时候才会发生;
如果一个类A含有多个成员类对象的话,那么类A的每一个构造函数必须调用每一个成员对象的默认构造函数而且必须按照类对象在类A中的声明顺序进行; -
带有默认构造函数的基类,如果一个没有任何构造函数的派生类派生自一个带有默认构造函数基类,那么该派生类会合成一个构造函数调用上一层基类的默认构造函数;
-
带有一个虚函数的类;
-
带有一个虚基类的类;
47、抽象基类为什么不能创建对象?
抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。
48、模板类和模板函数的区别是什么?
- 函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定。
- 即函数模板允许隐式调用和显式调用而类模板只能显示调用。在使用时类模板必须加,而 函数模板不必。
49、多继承的优缺点,作为一个开发者怎么看待多继承?
- C++允许为一个派生类指定多个基类,这样的继承结构被称做多重继承;
- 多重继承的优点很明显,就是对象可以调用多个基类中的接口;
- 如果派生类所继承的多个基类有相同的基类,而派生类对象需要调用这个祖先类的接口方法,就会容易出现二义性;
- 加上全局符确定调用哪个基类的方法。比如pa.Author::eat()调用属于Author的方法。
- 使用虚拟继承,使得多重继承类Programmer_Author只拥有Person类的一份拷贝。
50、模板和实现可不可以不写在一个文件里面?为什么?
不可以!
因为 在编译时 模板并不能生成真正的二进制代码,而是在编译调用模板类或函数的CPP文件时才会去找对应的模板声明和实现,在这种情况下编译器是不知道实现模板类或函数的CPP文件的存在,所以它只能找到模板类或函数的声明而找不到实现,而只好创建一个符号寄希望于链接程序找地址。
但模板类或函数的实现并不能被编译成二进制代码,结果链接程序找不到地址只好报错了。
《C++编程思想》第15章(第300页)说明了原因:模板定义很特殊。由template<…>处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。
所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。