第10章 默认和删除函数(C++11)
10.1 类的特殊成员函数
在没有自定义构造函数的情况下,编译器会为类添加默认的构造函数。像这样有特殊待遇的成员函数一共有6个(C++11以前是4个),具体如下。
1.默认构造函数。
2.析构函数。
3.复制构造函数。
4.复制赋值运算符函数。
5.移动构造函数(C++11新增)。
6.移动赋值运算符函数(C++11新增)。
程序员可以有更多精力关注类本身的功能而不必为了某些语法特性而分心,同时也避免了让程序员编写重复的代码
#include <string>
#include <vector>
class City {
std::string name;
std::vector<std::string> street_name;
};
int main()
{
City a, b;
a = b;
}
我们虽然没有为City类添加复制赋值运算符函数City:: operator= (const City &),但是编译器仍然可以成功编译代码。
该特性的存在也给我们带来了一些麻烦。
1.声明任何构造函数都会抑制默认构造函数的添加。
2.一旦用自定义构造函数代替默认构造函数,类就将转变为非平凡类型。
3.没有明确的办法彻底禁止特殊成员函数的生成(C++11之前)
#include <string>
#include <vector>
class City {
std::string name;
std::vector<std::string> street_name;
public:
City(const char *n) : name(n) {}
};
int main()
{
City a("wuhan");
City b; // 编译失败,自定义构造函数抑制了默认构造函数
b = a;
}
为了解决这个问题我们不得不添加一个无参数的构造函数
class Trivial
{
int i;
public:
Trivial(int n) : i(n), j(n) {}
Trivial() {}
int j;
};
int main()
{
Trivial a(5);
Trivial b;
b = a;
std::cout << "std::is_trivial_v<Trivial> : "
<< std::is_trivial_v<Trivial> << std::endl;
}
上面的代码中有两个动作会将Trivial类的类型从一个平凡类型转变为非平凡类型。第一是定义了一个构造函数Trivial(int n),它导致编译器抑制添加默认构造函数,于是Trivial类转变为非平凡类型。第二是定义了一个无参数的构造函数,同样可以让Trivial类转变为非平凡类型。
还有一个典型的例子,禁止重载函数的某些版本
class Base {
void foo(long &);
public:
void foo(int) {}
};
int main()
{
Base b;
long l = 5;
b.foo(8);
b.foo(l); // 编译错误
}
假设现在我们需要继承Base类,并且实现子类的foo函数;另外,还想沿用基类
Base的foo函数,于是这里使用using说明符将Base的foo成员函数引入子类,代码如下:
class Base {
void foo(long &);
public:
void foo(int) {}
};
class Derived : public Base {
public:
using Base::foo;
void foo(const char *) {}
};
int main()
{
Derived d;
d.foo("hello");
d.foo(5);
}
上面这段代码看上去合情合理,而实际上却无法通过编译。
10.2 显式默认和显式删除
C++11标准提供了一种方法能够简单有效又精确地控制默认特殊成员函数的添加和删除,我们将这种方法叫作显式默认和显式删除。显式默认和显式删除的语法非常简单,只需要在声明函数的尾部添加=default和=delete
struct type
{
type() = default;
virtual ~type() = delete;
type(const type &);
};
type::type(const type &) = default;
=default可以添加到类内部函数声明,也可以添加到类外部。
delete与=default不同,它必须添加在类内部的函数声明中,如果将其添加到类外部,那么会引发编译错误。
class NonTrivial
{
int i;
public:
NonTrivial(int n) : i(n), j(n) {}
NonTrivial() {}
int j;
};
class Trivial
{
int i;
public:
Trivial(int n) : i(n), j(n) {}
Trivial() = default;
int j;
};
int main()
{
Trivial a(5);
Trivial b;
b = a;
std::cout << "std::is_trivial_v<Trivial> : " <<
std::is_trivial_v<Trivial> << std::endl;
std::cout << "std::is_trivial_v<NonTrivial> : " <<
std::is_trivial_v<NonTrivial> << std::endl;
}
相对于使用private限制函数访问,使用=delete更加彻底,它从编译层面上抑制了函数的生成
int a;
struct S2 {
int y : (true ? 8 : a = 42);
int z : (1 || new int { 0 });
};
所以我们可以通过使用括号明确代码被解析的优先级来解决这个问题:
class NonCopyable
{
public:
NonCopyable() = default; // 显式添加默认构造函数
NonCopyable(const NonCopyable&) = delete; // 显式删除复制构造函数
NonCopyable& operator=(const NonCopyable&) = delete; // 显式删除复制赋值运算符函数
};
int main()
{
NonCopyable a, b;
a = b; //编译失败,复制赋值运算符已被删除
}
用= delete来解决禁止重载函数的继承问题
class Base {
// void foo(long &);
public:
void foo(long &) = delete; // 删除foo(long &)函数
void foo(int) {}
};
class Derived : public Base {
public:
using Base::foo;
void foo(const char *) {}
};
int main()
{
Derived d;
d.foo("hello");
d.foo(5);
}
10.3 显式删除的其他用法
显式删除不仅适用于类的成员函数,对于普通函数同样有效。
void foo() = delete;
static void bar() = delete;
int main()
{
bar(); // 编译失败,函数已经被显式删除
foo(); // 编译失败,函数已经被显式删除
}
显式删除还可以用于类的new运算符和类析构函数。显式删除特定类的new运算符可以阻止该类在堆上动态创建对象
struct type
{
void * operator new(std::size_t) = delete;
};
type global_var;
int main()
{
static type static_var;
type auto_var;
type *var_ptr = new type; // 编译失败,该类的new已被删除
}
显式删除类的析构函数在某种程度上和删除new运算符的目的正好相反,它阻止类通过自动变量、静态变量或者全局变量的方式创建对象,但是却可以通过new运算符创建对象。
struct type
{
~type() = delete;
};
type global_var; // 编译失败,析构函数被删除无法隐式调用
int main()
{
static type static_var; // 编译失败,析构函数被删除无法隐式调用
type auto_var; // 编译失败,析构函数被删除无法隐式调用
type *var_ptr = new type;
delete var_ptr; // 编译失败,析构函数被删除无法显式调用
}
10.4 explicit和=delete
在类的构造函数上同时使用explicit和=delete是一个不明智的做法
struct type
{
type(long long) {}
explicit type(long) = delete;
};
void foo(type) {}
int main()
{
foo(type(58));
foo(58);
}
foo(type(58))会造成编译失败,原因是type(58)显式调用了构造函数,但是explicit type(long)却被删除了。foo(58)可以通过编译,因为编译器会选择type(long long)来构造对象。
第11章 非受限联合类型(C++11)
11.1 联合类型在C++中的局限性
C++中的联合类型(union)可以说是节约内存的一个典型代表。因为在联合类型中多个对象可以共享一片内存,相应的这片内存也只能由一个对象使用
#include <iostream>
union U
{
int x1;
float x2;
};
int main()
{
U u;
u.x1 = 5;
std::cout << u.x1 << std::endl;
std::cout << u.x2 << std::endl;
u.x2 = 5.0;
std::cout << u.x1 << std::endl;
std::cout << u.x2 << std::endl;
}
输出结果:
5
7.00649e-45
1084227584
5
过去的C++标准规定,联合类型的成员变量的类型不能是一个非平凡类型,也就是说它的成员类型不能有自定义构造函数
union U
{
int x1;
float x2;
std::string x3;
};
上面的代码是无法通过编译的,因为x3存在自定义的构造函数,所以它是一个非平凡类型。
11.2 使用非受限联合类型
在C++11标准中解除了大部分限制,联合类型的成员可以是除了引用类型外的所有类型。
在C++11中如果有联合类型中存在非平凡类型,那么这个联合类型的特殊成员函数将被隐式删除,也就是说我们必须自己至少提供联合类型的构造和析构函数,比如:
#include <iostream>
#include <string>
#include <vector>
union U
{
U() {} // 存在非平凡类型成员,必须提供构造函数
~U() {} // 存在非平凡类型成员,必须提供析构函数
int x1;
float x2;
std::string x3;
std::vector<int> x4;
};
int main()
{
U u;
u.x3 = "hello world";
std::cout << u.x3;
}
实际上这段代码会运行出错,因为非平凡类型x3并没有被构造,所以在赋值操作的时候必然会出错。
#include <iostream>
#include <string>
#include <vector>
union U
{
U() : x3() {}
~U() { x3.~basic_string(); }
int x1;
float x2;
std::string x3;
std::vector<int> x4;
};
int main()
{
U u;
u.x3 = "hello world";
std::cout << u.x3;
}
如果在main函数内使用U的成员x4,由于x4并没有经过初始化,因此会导致程序出错:
#include <iostream>
#include <string>
#include <vector>
union U
{
U() : x3() {}
~U() { x3.~basic_string(); }
int x1;
float x2;
std::string x3;
std::vector<int> x4;
};
int main()
{
U u;
u.x4.push_back(58);
}
基于这些考虑,较推荐让联合类型的构造和析构函数为空,也就是什么也不做,并且将其成员的构造和析构函数放在需要使用联合类型的地方。
#include <iostream>
#include <string>
#include <vector>
union U
{
U() {}
~U() {}
int x1;
float x2;
std::string x3;
std::vector<int> x4;
};
int main()
{
U u;
new(&u.x3) std::string("hello world");
std::cout << u.x3 << std::endl;
u.x3.~basic_string();
new(&u.x4) std::vector<int>;
u.x4.push_back(58);
std::cout << u.x4[0] << std::endl;
u.x4.~vector();
}
上面的代码用了placement new的技巧来初始化构造x3和x4对象,在使用完对象后手动调用对象的析构函数。
联合类型的静态成员不属于联合类型的任何对象,所以并不是对象构造时被定义的,不能在联合类型内部初始化。
#include <iostream>
union U
{
static int x1;
};
int U::x1 = 42;
int main()
{
std::cout << U::x1 << std::endl;
}