IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 《More Effictive C++》学习笔记 — 基础议题 操作符 异常 -> 正文阅读

[C++知识库]《More Effictive C++》学习笔记 — 基础议题 操作符 异常

条款3 — 绝对不要以多态方式处理数组

好吧,这个问题在《深度探索C++对象模型》也提到了。当时,我只在msvc上验证了下,发现并不是这样。现在又看到这个提议,我认真地研究了下。

1、C++标准中的规定

首先,我们来看看C++中是怎样规定的(摘自C++20草案):

In an array delete expression, if the dynamic type of the object to be deleted differs from its static type, the behavior is undefined

这和C++98中的规定保持一致,对于动态类型和静态类型不一致的对象(也就是我们尝试以多态方式处理的对象),我们尝试使用delete[] 的方式处理它们时,行为是未定义的。

2、编译器实现

msvcmingwgcc11.1.0上验证后,我发现并没有这个问题。它们都可以正确地释放派生类对象。

3、问题分析

这个问题说明C++标准对于**delete[]**中如何进行析构函数的绑定并不做限制。如果只调用了基类的析构函数,说明编译器肯定是通过静态调用析构函数实现的:

for (int i = 0; i < size; i++)
{
	arr[i].~CLS_Base();
}

现在,编译器都能正常处理该情况,说明它们都意识到通过动态类型调用是更好的选择。这也正是基于C++对于RTTI的支持,他们才能检测到数组真正的类型及应该调用的析构函数版本。目前,只有 delete[] 被用于非多态形式中的动静态类型不一致才会导致析构调用错误:

#include <iostream>
using namespace std;

class CLS_Base
{
public:
	~CLS_Base()
	{
		cout << "~CLS_Base()" << endl;
	}

	void test()
	{
		cout << "CLS_Base::test" << endl;
	}
};
class CLS_Derived : public CLS_Base
{
public:
	~CLS_Derived()
	{
		cout << "~CLS_Derived()" << endl;
	}

	void test()
	{
		cout << "CLS_Derived::test" << endl;
	}
};

void testArr(CLS_Base arr[], size_t size)
{
	for (int i = 0; i < size; i++)
	{
		arr[i].test();
	}
}

int main()
{
	CLS_Base* pBaseArr = new CLS_Derived[10];
	testArr(pBaseArr, 10);
	delete[] pBaseArr;
}

在这里插入图片描述
我本以为这种情况下,在test中打印i会出现神奇的数字,没想到结果是正确的。这说明即使对于非多态类型,现在编译器仍然会检测其数据类型,根据其真正的size决定 arr[i] 在实际内存中的偏移量。我认为这对编译器来说是一个合理的选择。

条款5 — 对定制的类型转换函数保持警觉

这里面需要我们关注的就是C++的转换准则之一:没有任何一个转换程序可以内含一个以上的用户定制转换行为。这也引出了我们如何不使用explicit关键字阻止隐式类型转换:

class CLS_Test
{
public:
	class CLS_Size
	{
	public:
		CLS_Size(int size) {};
	};
	CLS_Test(CLS_Size) {};
	// CLS_Test(int) {};
};

int main()
{
	CLS_Test obj = 10; // invalid
}

这里我们使用了一个内部类使得从intCLS_Test从1步变为2步,导致编译器识别出此错误。

条款6 — 区别递增递减操作符的前置和后置形式

这个条款提到了一个很细节的问题:后置递增操作符的返回值应该是const。为什么呢?仔细观察该函数的实现:

class CLS_Test
{
public:
	...
	const CLS_Test operator++(int)
	{
		CLS_Test old = *this;
		++(*this);
		return old;
	}
	...
};

如果我们不将返回类型声明为const的,则下面的代码将是合理的:

CLS_Test obj;
obj++++;

有两个理由促使我们拒绝这种行为。第一个理由是:我们应该让重载的操作符保持和内置类型一样的功能和特性。而内置类型的后置递增操作符是不支持这种操作。第二个理由是:即使我们连续使用递增操作符,其结果也只是修改第一次递增返回的过时的旧值,而不能改变obj本身。以vector的迭代器为例(其后置递增运算符并没有返回const类型):

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> vec = {1, 2, 3, 4};
	auto iter = vec.begin();
	iter++++;
	cout << *iter << endl;
	++iter++;
	cout << *iter << endl;
}

在这里插入图片描述
看着花里胡哨的代码,实际上除了递增,其他什么都没发生。哦不,他还创建了一个根本用不上的临时对象。这也是为什么许多书中都会建议:尽量使用前置递增/递减运算符

条款11 — 禁止异常流出析构函数之外

1、uncaught_exceptions

这个条款类似《Effective C++》中的条款08。不过这个条款里提到了一个很有趣的函数uncaught_exceptions。这个函数可以用于判断当前是否尤为捕获的异常。由于异常发生之后,在未捕获的情况下,程序只会执行临时对象的析构函数。因此大部分情况下它是在析构函数中使用的。

#include <iostream>
#include <stdexcept>
using namespace std;

class Foo 
{
public:
	int count = std::uncaught_exceptions();
	~Foo() 
	{
		count == std::uncaught_exceptions() ? cout << "~Foo() called normally" << endl : cout << "~Foo() called during stack unwinding" << endl;
	}
};
int main()
{
	Foo obj;
	try 
	{
		Foo obj;
		cout << "Exception thrown" << endl;
		throw runtime_error("test");
	}
	catch (const exception& e)
	{
		cout << "Exception: " << e.what() << endl;
	}
}

在这里插入图片描述
在C++的参考手册中提到,uncaught_exceptions函数的一个用处是用于监控一个或几个函数是否抛出异常。如在boostlog模块中,支持这样一种写法:

BOOST_LOG(logger) << foo();

logger作为一个哨兵对象,其实现大致如我们前面的例子中的Foo类。在构造和析构时分别记录异常数量。这样就可以得到foo函数中是否产生异常了。

2、C++11中的异常处理

C++11中提供了一些用于处理异常的函数:

#include <iostream>
#include <stdexcept>
using namespace std;

class CLS_MyException : public exception
{
public:
	CLS_MyException(const char* pc):
		exception(pc)
	{
		cout << "create" << endl;
	}

	CLS_MyException(const CLS_MyException& other) :
		exception(other)
	{
		cout << "copy" << endl;
	}
	
	~CLS_MyException()
	{
		cout << "destroy" << endl;
	}
};

int main()
{
	exception_ptr eptr;
	try
	{
		throw CLS_MyException("test error");
	}
	catch (const CLS_MyException& e)
	{
		cout << "before" << endl;
		eptr = current_exception();
		cout << "after" << endl;
	}

	try
	{
		if (eptr)
		{
			rethrow_exception(eptr);
		}
	}
	catch (...)
	{
		cout << "Caught exception " << endl;
	}
}

在这里插入图片描述
我们使用current_exception实际上是对当前异常对象进行了一次拷贝。其返回的是一个exception_ptr对象。此对象类似一个智能指针,只有当没有异常指针对象指向该拷贝,其才会被释放。我们也可以选择make_exception_ptr方法获取一个异常指针:

eptr = make_exception_ptr(e);

在这里插入图片描述
对比来看,make_exception_ptr会多次执行一次拷贝和析构。这是因为其执行过程相当于:

try {
    throw e;
} catch(...) {
    return std::current_exception();
}

条款12 — 了解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”之间的差异

是的,仔细观察不难发现,抛出异常的过程与参数调用很相似。它们都有三种参数方式:值传递、引用传递、指针传递。然而,其最大的区别在于当调用函数结束后,函数会回到调用端;而抛出异常后,控制权不会再回到抛出端。

1、区别1 — 对象复制和拷贝类型

(1)静态类型拷贝

C++标准中提到:

Throwing an exception copy-initializes (9.4, 11.4.4.2) a temporary object, called the exception object

抛出异常会拷贝初始化一个临时对象。因此,在异常抛出的过程中,无论使用值传递还是引用传递方式,都一定会进行对象拷贝。无论该对象的生命周期时局部的还是静态的,不影响此行为。有一点需要我们记住:当拷贝发生时,实际上调用的拷贝函数取决于异常的静态类型而非动态类型,实际上能捕获该异常的catch类型也是其静态类型

#include <iostream>
#include <stdexcept>
using namespace std;

class CLS_MyException : public exception
{
public:
	CLS_MyException(const char* pc) :
		exception(pc)
	{
		cout << "create CLS_MyException" << endl;
	}

	CLS_MyException(const CLS_MyException& other) :
		exception(other)
	{
		cout << "copy CLS_MyException" << endl;
	}

	~CLS_MyException()
	{
		cout << "destroy CLS_MyException" << endl;
	}
};

class CLS_MyExceptionDervied : public CLS_MyException
{
public:
	CLS_MyExceptionDervied(const char* pc) :
		CLS_MyException(pc)
	{
		cout << "create CLS_MyExceptionDervied" << endl;
	}

	CLS_MyExceptionDervied(const CLS_MyExceptionDervied& other) :
		CLS_MyException(other)
	{
		cout << "copy CLS_MyExceptionDervied" << endl;
	}

	~CLS_MyExceptionDervied()
	{
		cout << "destroy CLS_MyExceptionDervied" << endl;
	}
};

int main()
{
	try
	{
		CLS_MyExceptionDervied e("test error");
		cout << endl;
		CLS_MyException& re = e;
		throw re;
	}
	catch (const CLS_MyExceptionDervied& e)
	{
		cout << endl << "catch CLS_MyExceptionDervied" << endl;
	}
	catch (const CLS_MyException& e)
	{
		cout << endl << "catch CLS_MyException" << endl;
	}

在这里插入图片描述

(2)throw的区别

这也就是说明如果我们捕获了一个派生类异常,然后使用抛出了其基类引用对象,那么就会发生切割行为。如果我们不想拷贝异常,也不想改变其类型,我们可以使用throw语句。以下的代码使用的类结构和上面一样:

void testExcep()
{
	try
	{
		throw CLS_MyExceptionDervied("test error");
	}
	catch (const CLS_MyException& e)
	{
		throw e;
	}
}

int main()
{
	try
	{
		testExcep();
	}
	catch (const CLS_MyExceptionDervied& e)
	{
		cout << endl << "catch CLS_MyExceptionDervied" << endl;
	}
	catch (const CLS_MyException& e)
	{
		cout << endl << "catch CLS_MyException" << endl;
	}
}

在这里插入图片描述
如果我们使用

throw;

在这里插入图片描述
这里不仅少发生了一次拷贝,而且在捕获阶段可以捕获到正确类型的对象,而非切割后的基类。

(3)异常对象与const引用

如果我们抛出的是一个exception对象,那么下面三种catch捕获方式是没有区别的:

catch(exception e)
catch(exception &e)
catch(const exception &e)

这让我们可以注意到异常捕获和函数调用的第二个区别:函数调用无法将临时对象传递给const引用,而在异常捕获中可以。我们思考下其原理。前者被拒绝可以认为是因为临时对象的生命周期在进入函数作用域之前就结束了;而异常对象保存在异常堆栈中,有编译器维护,其在当前异常处理块中都有效。
当然这里我们需要注意如果我们尝试按值传递的方式,与函数一样,会进行一次拷贝。也就是说,值传递方式的异常捕获会导致共计两次的对象创建(拷贝)。这显然是冗余的。

因此,总的来说,如果我们希望捕获的异常对象和希望抛出的异常对象完全一致,我们需要:
在抛出异常时抛出异常对象引用、指针或对象,而不要抛出其基类引用或指针
过程中如果使用基类catch语句捕获到了异常,需要使用空的throw语句再次抛出该异常

2、区别2 — 隐式类型转换

函数参数传递和异常捕获都支持隐式类型转换,然而异常捕获所支持的转换范围更小。严格来讲,其所支持的类型转换仅包括:
类继承体系中的对象转换 这种转换可以通过值传递、引用传递、指针传递的方式进行。
从有形指针向无形指针的转换 从某个类型指针转换到void*。
事实上,《C++ Primer》中指出,转换类型还包括:
非常量向常量转换 如上所述的const引用。
数组和指针的转换,函数及函数指针的转换

int main()
{
	try
	{
		throw new runtime_error("test error");
	}
	catch (exception* e)
	{
		cout << "catch (exception* e)" << endl;
	}

	try
	{
		throw new runtime_error("test error");
	}
	catch (void* e)
	{
		cout << "catch (void* e)" << endl;
	}

	int iArr[10];
	try
	{
		throw iArr;
	}
	catch (int*)
	{
		cout << "catch (int*)" << endl;
	}
}

在这里插入图片描述

3、区别3 — 最佳匹配和优先匹配

函数匹配的规则很复杂,但它总是倾向于寻找所有待匹配项中最优的那个。异常和函数不同,它总是匹配异常列表中第一个匹配的项。也就是说,如果我们抛出派生类异常对象,其是有可能被捕获列表中靠前的基类catch块捕获到的。

条款13 — 以引用方式抛出异常

异常传递的方式有三种:指针、值、引用。让我们来分别分析下它们。

1、按指针传递

与其它方式相比,按指针传递应该是效率最高的一种了。既然拷贝一定发生,那还有什么比拷贝地址更快的呢?然而,这样做的风险是:程序员常常忘了使用全局或静态对象来保存异常对象。这就导致传递的指针实际上是个野指针(这也是C++11中提供的异常指针想要解决的问题)。
此外,标准的异常抛出(包括bad_allocbad_cast等)都是以对象的形式抛出的。

2、按值传递

按值传递的问题很明显,它会引起切割以及重复的拷贝操作;它也拒绝了多态的使用。

3、按引用传递

按引用传递避免了上述所有问题。因此其是更好的一个选择。

条款14 — 明智运用异常特化

虽然这项功能已经被deprecated,但是其中的一些想法仍旧适用与现在的nothrow操作。我们主要学习下如何防止 nothrow(true) 函数中抛出异常。

1、原则1 — 避免将其放在需要类型自变量的模板身上

考虑下面的模板函数:

template<class T>
bool operator==(const T& lhs, const T&rhs) nothrow()
{
	return &lhs == &rhs;
}

试想如果某个类型重载了取地址操作符,而该操作中又会抛出异常。那么当使用该类实例化此模板函数时,就会导致程序终止。因此,避免在模板中声明nothrow。因为其是否抛出异常和类型参数息息相关。

2、原则2 — 如果嵌套调用的函数没有异常声明,则外部函数也不应该有

这项原则很好理解,但是我们往往会忽略一种情况:回调函数。回调函数参数的类型往往是由typedef定义的。作者在书中提到typedef中不支持nothrow,这导致我们没法确认传入的回调函数是否会被抛出异常。然而在C++17中,typedef中已经支持nothrow声明。其使用类似:

typedef void (*pfn)(int&) noexcept;

void test(int& para, pfn f) noexcept
{
	f(para);
}

void testIntNoexcept(int& para) noexcept
{
	++para;
}

void testInt(int& para)
{
	++para;
}

int main()
{
	int a = 0;
	test(a, testInt); // invalid
	test(a, testIntNoexcept);
}
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-07-24 11:15:53  更:2021-07-24 11:17:13 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/8 1:58:05-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码