《C++ Primer》学习笔记 —用于大型程序的工具

一、异常处理

1、异常与移动构造

我们在抛出异常时,可以使用类类型的对象。这要求类必须含有一个可访问的析构函数和一个可访问的拷贝或移动构造函数。最后这项要求很趣,这就是说我们可以没有拷贝构造函数而只有移动构造函数。

#include <iostream>
using namespace std;

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

	CLS_Exception(const CLS_Exception&) = delete;

	CLS_Exception(CLS_Exception&&)
	{
		cout << "CLS_Exception(CLS_Exception&&)" << endl;
	}

	~CLS_Exception()
	{
		cout << "~CLS_Exception" << endl;
	}
};

int main()
{
	try
	{
		CLS_Exception e;
		throw e;
	}
	catch (const CLS_Exception& e)
	{
	}
}

在这里插入图片描述
看似异常捕获阶段优先选择调用移动构造函数。然而,这是有条件的(自己总结的未必全):

(1)条件1 — 非值传递

catch (const CLS_Exception e)
{
}

把捕获代码改成如此便无法编译通过(当然,这是因为值传递时在抛出时会多进行一次拷贝)。

(2)条件2 — 异常不能再次被抛出

catch (const CLS_Exception& e)
{
	throw e;
}

如果我们把捕获到的异常再次抛出也将导致无法编译通过(然而如果我们只是抛出而不引用具体对象则无问题)。

(3)条件3 — 异常对象的创建需要在当前异常块内

CLS_Exception e;
try
{
	throw e;
}

这样的代码也无法编译通过。因为编译器认为这样的异常对象并不能作为右值。

2、try语句与构造函数

异常可能会发生在任何过程中。如果异常并不是发生在函数内部,而是在构造函数的初始化列表中,我们该如何捕获呢?这种情况下,我们必须把构造函数写成函数try语句块(function try block)。

#include <iostream>
using namespace std;

class CLS_Test
{
	string m_str;
public:
	CLS_Test() try : m_str(initStr()) 
	{
	}
	catch (exception& e)
	{
		cout << "caught exception " << e.what() << endl;
	}

	string initStr()
	{
		throw runtime_error("initStr err");
	}
};

int main()
{
	CLS_Test test;
}

在这里插入图片描述
这里的异常捕获技能捕获到初始化里列表中的异常,也能捕获函数体中的异常。需要注意的是,如果在构造函数参数初始化时发生的异常,则只能由调用方捕获。

3、noexcept异常说明

(1)noexcept的位置

对于一个函数来说,noexcept说明要么出现在该函数的所有声明和定义语句中,要么一次也不出现。该说明应该出现在位置返回类型、finaloverride、纯虚函数的 =0 之前,跟在const& 限定符之后:

class CLS_Test
{
public:
	virtual void testNoexcept() const noexcept final = 0;
};

如果我们尝试将其放在final后面,会导致编译报错。
除此之外,有一点需要我们注意,noexcept说明不允许出现在 typedef(截止C++17)。这是因为在C++17之前,noexcept并不被认为是函数声明的一部分。这也就意味这我们没法在定义回调函数类型时指明其异常类型。

(2)使用noexcept的条件

很显然,我们在认定函数不会抛出异常时可以使用noexcept说明。除此之外,如果我们根本不知道如何处理某个函数中可能抛出的异常,也可以使用该说明符。

(3)异常说明与函数指针、虚函数和拷贝控制

函数指针及其指向的函数必须有一致的异常说明。也就是说,我们只能将声明了不抛出异常的函数赋值给声明为不抛出异常的函数指针;但是可以将任何类型匹配的函数赋值给声明为可能抛出异常的函数指针,因为没有异常声明的函数可以抛出异常也可以不抛出异常:

void testNoexcept() noexcept {}
void test() {}

int main()
{
	void(*pfn)() noexcept;
	void(*pf)();

	pfn = &testNoexcept;
	pf = &testNoexcept;
	pfn = &test; // invalid
	pf = &test;
}

类似地,派生类虚函数的异常声明必须与其基类的虚函数保持一致。而当编译器为我们合成拷贝控制成员时,当且仅当其合成成员调用的所有函数和基类方法都承诺不抛出异常,才会生成noexcept(false)

二、命名空间

1、命名空间可以是不连续的

命名空间可以定义在几个不同的部分。因此我们将几个对立的接口和实现文件组成一个命名空间。此时,命名空间的组织方式类似我们管理自定义类及函数的方式:
(1)命名空间的一部分成员的作用是定义类,以及声明作为类接口的函数及对象,则这些成员应该置于头文件中,这些头文件将被包含在使用了这些成员的文件中。
(2)命名空间成员的定一部分应该置于源文件中。

// CLS_TestSpace.h
namespace mySpace
{
	class CLS_TestSpace
	{
	public:
		CLS_TestSpace();
		...
	}
}
// CLS_TestSpace.cpp
#include "CLS_TestSpace"
namespace mySpace
{
	class CLS_TestSpace::CLS_TestSpace()
	{
		...
	}
	// 其余成员定义
}

在程序中某些实体定义一次:如非内联函数、静态数据成员、变量等,命名空间中定义的名字需要满足这一要求。有一点需要注意,我们不把#include放在命名空间内部。因为这意味把头文件中所有的名字定义成该命名空间的成员。其实包含这些名字本身并不会导致编译有问题。但是有些头文件的组织方式可能会导致编译出错(如包含string):

namespace MySpace
{
#include <string>
}

这是因为string中定义了:

_NODISCARD inline string to_string(double _Val) { // convert double to string
    const auto _Len = static_cast<size_t>(_CSTD _scprintf("%f", _Val));
    string _Str(_Len, '\0');
    _CSTD sprintf_s(&_Str[0], _Len + 1, "%f", _Val);
    return _Str;
}

_NODISCARD inline string to_string(float _Val) { // convert float to string
    return _STD to_string(static_cast<double>(_Val));
}

而这里重载第二个调用第一个重载使用的 _STD

#define _STD       ::std::

这就导致其会去全局std名称空间中查找该重载,而不是在当前名称空间的内层空间中查找。

2、命名空间定义

命名空间中的成员可以定义在命名空间外部(通过限定命名空间名称),但是不能定义在不相关的作用域中。

namespace MySpace
{
	namespace InnerSpace1
	{
		std::istream& operator>>(std::istream& in, string s);
	}

	namespace InnerSpace2
	{
		std::istream& MySpace::InnerSpace1::operator>>(std::istream& in, string s) {} // invalid
	}

	std::istream& InnerSpace1::operator>>(std::istream& in, string s) {}
}

3、内联名称空间

这是C++11中引入的新特性。其与普通嵌套名称空间的区别是:声明为内联的名称空间可以直接在外部名称空间中被使用。这相当于将内联名称空间中的所有名字插入到当前名称空间中。我们需要注意,如果想要使用内联名称空间,必须在第一次定义它的时候就加上inline关键字:

#include <iostream>
using namespace std;

namespace MySpace
{
	inline namespace InnerSpace1
	{
		void test()
		{
			cout << "InnerSpace1::test()" << endl;
		}
	}
}

int main()
{
	MySpace::test();
}

在这里插入图片描述
当然,这也会导致name lookup时可以看到此函数。如果外层空间中有同名函数,将会导致重载解析错误。

4、匿名名称空间

匿名名称空间的主要是用于设置静态生命周期。在匿名空间中定义的数据仅在当前文件中有效。这意味着,虽然匿名名称空间也支持不连续定义,但是不同文件中的匿名名称空间是不同的名称空间。定义在匿名名称空间中的数据在当前文件范围都可见,无需通过名称空间作用域访问。(作者提到使用static进行静态声明的做法已经被取消了,但是我测试时貌似可以使用,而且我在C++标准中也没看到类似说明。)

5、using declaration 和 using directive

一条using声明语句一次只引入命名空间的一个成员。它的有效范围从using声明开始,一直到其所在的作用域结束为止。其引入的成员将隐藏外部同名成员(如果我们不加作用域地访问)。
using指示一次引入一个命名空间中的所有名字。其作用域范围较为复杂。因为它具有将命名空间成员提升到包含命名空间本身和using指示的最近作用域的能力。也就是说如果我们在一个函数中使用using声明引入了一个名称空间,其作用域为包含此函数的作用域。这并不是说在该函数外部也可以直接使用该空间的成员,而是说名称空间的名字将和此作用域内的名字相互冲突;而函数内部定义的名称将具有更高的优先级:

#include <iostream>
using namespace std;

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

void test()
{
	cout << "global test" << endl;
}

void myTest()
{
	using namespace MySpace;
	test(); // multiple overload match
}

如果我们使用局部变量:

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

void myTest()
{
	using namespace MySpace;
	auto test = []() {cout << "myTest::test" << endl; };
	test();
}

int main()
{
	myTest();
}

在这里插入图片描述
using指示在头文件中使用的风险:一次性注入某个命名空间的所有名字。当程序升级或引入新版本的库失败后可能导致名称冲突,编译失败;同时,这种二义性错误只有在调用了冲突的名字时才会被检测到,此时我们可能很难再对代码重构了。因此,我们最好在实现文件或局部作用域内使用using指示。

6、名称空间名称查找

对名称空间内名字的查找规则:由内向外依次查找每个外层作用域;除了类中的成员函数定义外,总是向上查找作用域

namespace MySpace
{
	int i = 100;
	namespace MySpaceInner
	{
		int i = 200;

		void test()
		{
			cout << i << endl;
			// j = 10; // not declared
		}
		int j = 10;
	}
	int j = 10;
}

int main()
{
	MySpace::MySpaceInner::test();
}

在这里插入图片描述

7、名称空间与函数重载解析

当我们调用函数,进行重载解析时,如果参数类型为类类型,函数名称查找除了发生在当前作用域外,还发生在类所属的名称空间。因此下述代码是合理的:

#include <iostream>

int main()
{
	operator<<(std::cout, "test");
}

与此类似,对于友元函数,如果其仅在类中被声明,则其属于类所在名称空间。这也就导致:

namespace MySpace
{
	class CLS_Test
	{
		friend void test(){}
		friend void testObj(CLS_Test& obj){}
	};
}

int main()
{
	MySpace::CLS_Test obj;
	test(); // invalid
	testObj(obj);
}

三、多重继承

1、类型转换与多个基类

在函数参数重载解析时,编译器不会在派生类向基类的几种转换中进行比较和选择,因为在它看来转换到任意一种基类都一样好。这种一样好是有条件的,下面我们来测试一下:

class CLS_Base1 {};
class CLS_Base2 {};
class CLS_BaseForVirtual {};
class CLS_DerivedVirtual1st : virtual public CLS_BaseForVirtual {};
class CLS_DerivedVirtual2nd : virtual public CLS_BaseForVirtual {};
class CLS_Derived1 : public CLS_Base1 {};
class CLS_Derived2 : public CLS_Base2 {};
class CLS_DerivedMost : public CLS_DerivedVirtual1st, public CLS_DerivedVirtual2nd, public CLS_Derived1, public CLS_Derived2 {};

void test1(const CLS_Derived1&) {}
void test1(const CLS_Derived2&) {}

void test2(const CLS_Derived1&) {}
void test2(const CLS_DerivedVirtual1st&) {}

void test3(const CLS_Base2&) {}
void test3(const CLS_Derived1&) {}

void test4(const CLS_Base1&) {}
void test4(const CLS_Derived1&) {}

void test5(const CLS_Base1&) {}
void test5(const CLS_DerivedVirtual1st&) {}

void test6(const CLS_BaseForVirtual&) {}
void test6(const CLS_DerivedVirtual1st&) {}

void test7(const CLS_Base1&) {}
void test7(const CLS_BaseForVirtual&) {}

int main()
{
	CLS_DerivedMost obj;
	test1(obj); // ambiguous
	test2(obj); // ambiguous
	test3(obj); // ambiguous
	test4(obj); 
	test5(obj); // ambiguous
	test6(obj); 
	test7(obj); // ambiguous
}

从这我们可以总结出:
(1)纵向来看,在继承体系中从某一基类派生出来的类,其层次越接近派生类,转换时的优先级越高。
(2)其余情况下,无论是同一继承深度的不同类,还是位于不同深度派生于不同基类的类,转换时的优先级都相同。
(3)虚继承并不会改变转换的优先级。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值