面试题:对于一个类,C++默认会生成哪些函数呢?
在C++中一共有8个默认构造函数:
- 默认构造函数、默认析构函数:
- 默认拷贝构造函数、默认拷贝运算符(即重载赋值运算符)
- 默认移动构造函数、默认移动运算符(重载移动赋值操作符函数)
- 默认重载取地址运算符const函数、默认重载取地址运算符非const函数
什么时候空类不再是个空类呢?当C++处理过它之后。定义为空类之后,默认会生成如上8个函数
- 所有这些函数都是public而且inline,只有当这些函数被调用,它们才会被编译器创建出来。
- 编译器产生出的析构函数是non-virtual的,除非这个类的基类自身声明由virtual析构函数
- 如果没有显式定义,编译器会自动生成默认的重载取地址运算符函数,函数内部直接 ruturn this;
- 默认构造函数和析构函数主要是给编译器一个地方用来放置"藏身幕后"的代码,像是调用基类和non-static成员变量的构造函数和析构函数。
- 对于拷贝函数和拷贝运算符,编译器创建的版本只是单纯的将来自元对象的每一个non-static成员变量拷贝到模板对象。
也就是说:
class A{};
相当于:
class A
{
public:
A(); // 默认构造函数
~A(); // 默认析构函数
A(const A&); // 默认拷贝构造函数
A& operator = (const A&); // 默认重载赋值运算符函数
A* operator & (); // 默认取地址运算符函数
const A* operator & () const; // 默认重载取地址运算符const函数
A(A&&); // 默认移动构造函数
A& operator = (const A&&); // 默认重载移动赋值操作符函数
};
编程范式:如果不想使用编译器自动生成的函数,就应该明确拒绝
一般来讲,如果你不希望类支持某一特定功能,只要不声明对应函数就可以了。但是这个策略对拷贝构造函数和拷贝运算符这样的不起作用,因为当某些人尝试调用它们,编译器就会自动生成这些函数并且是public的
解决方法:
- 第一种方法:可以将相应的成员函数声明为private并且不实现(不推荐)
- 为什么要声明?阻止编译器创建拷贝函数/拷贝运算符等函数。
- 为什么不要实现?因为成员函数和友元还是可以调用private函数,而我们只声明不定义,这时当有人调用就会获得一个链接错误
- 为什么private权限:阻止其他人调用
- 第二种方法:也可以使用类似Uncopyable 这样的基类(推荐)
第一种方法怎么写?
怎么解决呢? 可以
class HomeForSale{
private:
HomeForSale(const HomeForSale&); //只有声明
HomeForSale& operator=(const HomeForSale&)
}
第二种方法怎么写?
// Uncopyable是一个一个专门为了阻止拷贝动作而设计的基类,它可以将将链接期错误移动到编译期
class Uncopyable{
protectde: //允许派生类对象构造和析构
Uncopyable(){}
~Uncopyable(){}
private:
Uncopyable(); //但是阻止拷贝
Uncopyable operator=(const Uncopyable&);
};
class HomeForSale : private Uncopyable { //类中不再声明拷贝构造函数或者拷贝运算符
}
比起第一种方法的优点是:
- 第一种方法 如果友元调用拷贝函数/拷贝运算符等函数时,会出现未定义错误
- 而第二种方法,比如上面,只要任何地方尝试拷贝HomeForSale 对象,编译期就会试着生成一个拷贝构造函数或者拷贝运算符,这些函数的"编译期生成版本"会尝试调用其基类的对应兄弟,那些调用会被编译期拒绝,因为其基类的拷贝函数是private。
详解
默认生成的函数到底是干了些啥?
看个例子:
template<typename T>
class NamedObject{
public:
NamedObject(const char* name, const T& value);
NamedObject(const std::string& name, const T& value);
private:
std::string nameValue;
T objectValue;
};
- 上面声明了一个构造函数,编译器就不会再为这个类创建默认构造函数
- NameObject既没有声明拷贝构造函数,也没有声明拷贝运算符,所以编译器会为它创建那些函数(如果它们被调用的话)
NamedObject<int>no1("smaller", 2);
NamedObject<int>no2(no1); // 调用拷贝构造函数
编译器生成的拷贝函数必须以no1.nameValue和no1.objectValue为初值设置no2.nameValue和no2.objectValue:
- nameValue的类型是string,而string有一个拷贝构造函数,所以no2.nameValue的初始化方式是调用string的拷贝构造函数并以no1.nameValue为实参。
- objectValue的类型是int,所以no2.objectValue会以"拷贝no1.objectValue内的每一个bit"来完成初始化。
一般来说,只有当生成的代码合法而且由适当机会证明它有意义,才会生成operator=,否则编译器不会为类声明operator=。举个例子:
template<typename T>
class NamedObject{
public:
//name不能是const,因为nameValue只接受reference-to-non-const string
NamedObject(std::string& name, const T& value);
private:
std::string &nameValue; //是个引用
const T objectValue; //是个const
};
那么:
std::string newDog("pade");
std::string oldDog("lpds");
NamedObject<int> p (newDog, 2);
NamedObject<int> s (oldDog, 21);
p = s; // p的成员变量将会发生什么事?
赋值之前,p.nameValue和s.nameValue指向两个不同的string对象。赋值之后呢?p.nameValue会指向s.nameValue吗?也就是说引用自身可被改动吗?
C++并不允许"让引用指向不同的对象",因此,C++会拒绝编译哪一行的赋值动作。也就是说不会生成拷贝运算符
如果你打算在一个"内含引用成员"的类支持拷贝操作,你必须自己定义拷贝运算符。
面对"内含引用成员"的类,编译器的反应也一样。更改const成员是不合法的,
最后,如果某个基类将拷贝运算符声明为private,编译器将拒绝为其派生类生成一个拷贝运算符。因为编译器为派生类所生成的拷贝运算符想象中可以处理基类成分,但是它们无法调用派生类无权调用的成员函数。