现代C++语言核心特性解析part6

文章介绍了C++11标准中关于类的特殊成员函数的控制,包括默认构造函数、删除函数的显式指定,以及如何使用`=default`和`=delete`关键字。同时讨论了C++11后联合类型(union)的使用,特别是含有非平凡类型成员时的注意事项,包括构造和析构函数的处理。
摘要由CSDN通过智能技术生成

第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;
}
相比于 C++98 带来的面向对象的革命性,C++11 带来的 XIII  却并非“翻天覆地”式的改变。很多时候,程序员保持着“C++98 式”的观点来看待 C++11 代码也同样是合理的。因为在编程思想上,C++11 依然遵从了一贯的面向对象的思想,并深 入加强了泛型编程的支持。从我们的观察来看,C++11 更多的是对步入“成熟稳重”的中年 时期的 C++ 的一种改造。比如,像 auto 类型推导这样的新特性,展现出的是语言的亲和力 ; 而右值引用、移动语义的特性,则着重于改变一些使用 C++ 程序库时容易发生的性能不佳的 状况。当然,C++11 中也有局部的创新,比如 lambda 函数的引入,以及原子类型的设计等, 都体现了语言与时俱进的活力。语言的诸多方面都在 C++11 中再次被锤炼,从而变得更加合 理、更加条理清晰、更加易用。C++11 对 C++ 语言改进的每一点,都呈现出了经过长时间技 术沉淀的编程语言的特色与风采。所以从这个角度上看,学习 C++11 与 C++98 在思想上是 一脉相承的,程序员可以用较小的代价对 C++ 的知识进行更新换代。而在现实中,只要修改 少量已有代码(甚至不修改) ,就可以使用 C++11 编译器对旧有代码进行升级编译而获得新 标准带来的好处,这也非常具有实用性。因此,从很多方面来看,C++ 程序员都应该乐于升 级换代已有的知识,而学习及使用 C++11 也正是大势所趋。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值