15.1、友元
在 C++ 中,友元是一种特殊的关系,它使得一个函数或者类能够访问另一个类的私有成员变量或者成员函数。友元可以被定义为一个函数、类、成员函数、或者整个命名空间。
在使用友元时,需要在被访问的类中声明友元,可以在类定义中使用 friend
关键字来声明一个友元函数或者友元类,如下所示:
class MyOtherClass {
friend class MyClass;
friend void myFriendFunction(MyClass& obj);
private:
int privateData;
};
在上面的代码中,我们通过使用 friend
关键字声明了一个友元类 MyClass
和一个友元函数 myFriendFunction
,它们都能够访问 MyOtherClass
类的私有成员变量 privateData
。
使用友元可以提高代码的灵活性和可维护性,但也会降低封装性,增大了代码的复杂度,因此应该谨慎使用。
需要注意的是,友元关系是单向的,即如果类 A 是类 B 的友元,那么只有类 A 可以访问类 B 的私有成员,类 B 不能访问类 A 的私有成员。
此外,C++ 中还有一种叫做成员友元的概念,它是指在一个类的成员函数中可以访问该类的私有成员变量,如下所示:
class MyClass {
public:
MyClass(int data) : mData(data) {}
void myMemberFunction(MyClass& obj) {
obj.mData = 42;
}
private:
int mData;
};
在上面的代码中,我们在成员函数 myMemberFunction
中访问了该类的私有成员变量 mData
,这里的 myMemberFunction
就是成员友元函数。
总之,友元是一种特殊的 C++ 特性,它可以使得一个函数或者类能够访问另一个类的私有成员变量或者成员函数,使用友元可以提高代码的灵活性和可维护性,但也会降低封装性,应该谨慎使用。
15.1.1、友元类
友元类是一种特殊的 C++ 特性,允许在一个类中声明另一个类为友元,从而让其能够访问该类的私有成员。在使用友元类时,需要在被访问的类中通过 friend
关键字将另一个类声明为友元,如下所示:
class MyClass {
friend class MyFriendClass; // MyFriendClass 是 MyClass 的友元类
public:
// ...
private:
int m_data;
// ...
};
class MyFriendClass {
public:
void someFunction(MyClass& obj) {
obj.m_data = 42; // 可以访问私有成员 m_data
}
};
在上面的代码中,我们声明了一个 MyClass
类,用 friend
关键字将另一个类 MyFriendClass
声明为其友元类。在 MyFriendClass
类中的 someFunction
函数中,我们可以访问 MyClass
类的私有成员变量 m_data
。
需要注意的是,友元类是单向的,即如果类 B 是类 A 的友元类,那么只有类 B 可以访问类 A 的私有成员,类 A 不能访问类 B 的私有成员。
使用友元类可以给程序带来更大的灵活性和可维护性,但作为一种特殊的关系,要慎重使用,否则可能会降低代码的可读性和封装性。
15.1.2、友元成员函数
友元成员函数是指在一个类中声明另一个类的成员函数为友元,从而让其能够访问该类的私有成员。在使用友元成员函数时,需要在被访问的类中通过 friend
关键字将另一个类的成员函数声明为友元,如下所示:
class MyClass {
public:
friend void MyFriendClass::someFunction(MyClass& obj); // someFunction 是 MyFriendClass 的成员函数,且是 MyClass 的友元
private:
int m_data;
};
class MyFriendClass {
public:
void someFunction(MyClass& obj) {
obj.m_data = 42; // 可以访问私有成员 m_data
}
};
在上面的代码中,我们声明了一个 MyClass
类和一个 MyFriendClass
类,在 MyClass
中使用 friend
关键字将 MyFriendClass
的成员函数 someFunction
声明为其友元成员函数。在 MyFriendClass
的 someFunction
函数中,我们可以访问 MyClass
类的私有成员变量 m_data
。
需要注意的是,友元成员函数是单向的,即如果类 A 的某个成员函数是类 B 的友元成员函数,那么只有该成员函数可以访问类 B 的私有成员,而类 A 中的其他函数及其对象不能访问类 B 的私有成员。
使用友元成员函数可以给程序带来更大的灵活性和可维护性,但作为一种特殊的关系,要慎重使用,否则可能会降低代码的可读性和封装性。
15.1.3.、其他友元关系
除了友元类和友元成员函数之外,C++ 还支持其他类型的友元关系,如友元函数和全局友元。下面我们分别介绍一下这两种友元关系。
1、友元函数
友元函数是指在一个类中声明一个非成员函数为友元,从而让其能够访问该类的私有成员。在使用友元函数时,需要在被访问的类中通过 friend
关键字将另一个非成员函数声明为友元,如下所示:
class MyClass {
public:
friend void myFriendFunction(MyClass& obj); // myFriendFunction 是 MyClass 的友元函数
private:
int m_data;
};
void myFriendFunction(MyClass& obj) {
obj.m_data = 42; // 可以访问私有成员 m_data
}
在上面的代码中,我们声明了一个 MyClass
类和一个非成员函数 myFriendFunction
,在 MyClass
中使用 friend
关键字将 myFriendFunction
声明为其友元函数。在 myFriendFunction
函数中,我们可以访问 MyClass
类的私有成员变量 m_data
。
需要注意的是,友元函数是单向的,即如果函数 A 是类 B 的友元函数,那么只有函数 A 可以访问类 B 的私有成员,类 B 不能访问函数 A 的私有成员。
使用友元函数可以给程序带来更大的灵活性和可维护性,但作为一种特殊的关系,要慎重使用,否则可能会降低代码的可读性和封装性。
2、全局友元
全局友元是指使用 friend
关键字在全局命名空间下定义一个类的友元,从而让所有函数和类都能够访问该类的私有成员。在使用全局友元时,需要在被访问的类中通过 friend
关键字将全局友元声明为其友元,如下所示:
class MyClass {
friend void friendFunction(MyClass& obj); // friendFunction 是 MyClass 的全局友元
private:
int m_data;
};
void friendFunction(MyClass& obj) {
obj.m_data = 42; // 可以访问私有成员 m_data
}
在上面的代码中,我们声明了一个 MyClass
类和一个全局函数 friendFunction
,在 MyClass
中通过使用 friend
关键字将 friendFunction
定义为其全局友元。在 friendFunction
函数中,我们可以访问 MyClass
类的私有成员变量 m_data
。
需要注意的是,全局友元是双向的,即如果类 A 是函数 B 的友元,那么函数 B 也是类 A 的友元。
使用全局友元可以给程序带来更大的灵活性和可维护性,但作为一种特殊的关系,要慎重使用,否则可能会降低代码的可读性和封装性。
15.1.4、共同的友元
共同的友元是指一个类和多个其他类之间共享同一个友元的关系。在使用共同的友元时,可以将一个友元声明为多个类的友元,从而让多个类都能够访问该友元。下面我们来看一个示例代码:
class ClassA;
class ClassB;
class CommonFriendClass {
public:
void someFunctionA(ClassA& obj);
void someFunctionB(ClassB& obj);
};
class ClassA {
friend class CommonFriendClass;
private:
int privateDataA;
};
class ClassB {
friend class CommonFriendClass;
private:
int privateDataB;
};
void CommonFriendClass::someFunctionA(ClassA& obj) {
obj.privateDataA = 42; // 可以访问 ClassA 的私有成员
}
void CommonFriendClass::someFunctionB(ClassB& obj) {
obj.privateDataB = 88; // 可以访问 ClassB 的私有成员
}
在上面的代码中,我们定义了三个类 CommonFriendClass
、ClassA
和 ClassB
,其中 CommonFriendClass
是 ClassA
和 ClassB
的共同友元类,即 CommonFriendClass
中的成员函数 someFunctionA
和 someFunctionB
都可以访问 ClassA
和 ClassB
类的私有成员变量。在 ClassA
和 ClassB
中使用 friend
关键字将 CommonFriendClass
声明为其友元类,使得 CommonFriendClass
可以访问 ClassA
和 ClassB
的私有成员变量。
需要注意的是,共同的友元是多向的,即多个类都可以访问共同的友元的私有成员,但它并不表示多个友元之间也存在关系。
使用共同的友元可以给程序带来更大的灵活性和可维护性,但作为一种特殊的关系,要慎重使用,否则可能会降低代码的可读性和封装性。
15.2、嵌套类
嵌套类是指一个类中嵌套定义的另一个类。嵌套类在语法上与普通的类并没有什么区别,只是在定义的时候需要将其放在另一个类的内部。
嵌套类可以访问包含它的外部类的私有成员,但外部类不能直接访问嵌套类的私有成员。需要通过嵌套类的公共接口进行访问。
下面我们用一个示例来演示如何定义和使用嵌套类:
class OuterClass {
public:
// 声明嵌套类
class InnerClass {
public:
void innerFunction() {
std::cout << "InnerClass::innerFunction()" << std::endl;
}
};
// 外部类成员函数可以创建嵌套类对象并访问其成员函数
void outerFunction() {
InnerClass obj;
obj.innerFunction(); // 输出 "InnerClass::innerFunction()"
}
private:
int privateData;
};
在上面的代码中,我们定义了一个外部类 OuterClass
和一个嵌套类 InnerClass
。在 OuterClass
的公有成员函数 outerFunction
中创建了 InnerClass
的对象 obj
,并通过调用其成员函数 innerFunction()
输出了一条信息。
需要注意的是,嵌套类的作用域是在包含它的外部类中。如果要在外部类以外的地方使用嵌套类,需要使用冗长的作用域解析运算符 ::
,如 OuterClass::InnerClass
。
嵌套类可以增强代码的可读性和可维护性,但也会增加代码的复杂度和难度,需要根据具体的情况来决定是否使用嵌套类。
15.2.1、嵌套类和访问权限
嵌套类和访问权限之间有一定的关联。在一个类中定义的嵌套类的访问权限,可以被看做是该嵌套类成员函数的默认访问权限。
例如,如果在一个类中定义的嵌套类是私有的,那么该嵌套类的成员函数也默认是私有的;如果嵌套类是公共的,那么该嵌套类的成员函数也默认是公共的。
下面我们用一个示例来演示这个概念:
class OuterClass {
public:
// 声明嵌套类,默认为私有
class InnerClass {
public:
void innerFunction() {
std::cout << "InnerClass::innerFunction()" << std::endl;
}
};
// 声明公共成员函数
void outerFunction() {
InnerClass obj;
obj.innerFunction(); // 可以调用 InnerClass 的成员函数,因为 InnerClass 是 OuterClass 的嵌套类
}
private:
int privateData;
// 声明私有嵌套类
class PrivateInnerClass {
public:
void innerFunction() {
std::cout << "PrivateInnerClass::innerFunction()" << std::endl;
}
};
};
int main() {
OuterClass obj;
obj.outerFunction(); // 可以调用 OuterClass 的公共成员函数
// 以下代码不能通过编译,因为 PrivateInnerClass 是私有的,外部代码不能访问它
// OuterClass::PrivateInnerClass obj2;
// obj2.innerFunction();
return 0;
}
在上面的示例中,我们定义了一个外部类 OuterClass
,其中包含一个公有的成员函数 outerFunction
,以及两个嵌套类 InnerClass
和 PrivateInnerClass
,分别是公有和私有的。
在 outerFunction
中,我们创建了一个 InnerClass
的对象并调用其公共成员函数 innerFunction
。由于 InnerClass
是 OuterClass
的公有嵌套类,所以可以从 outerFunction
中访问。
在 main
函数中,我们创建了一个 OuterClass
的对象并调用其公共成员函数 outerFunction
。由于 outerFunction
是公共的,所以可以从外部代码中调用它。
需要注意的是,在 OuterClass
中定义的私有嵌套类 PrivateInnerClass
,它对外部代码是不可见的,因此外部代码不能访问它或者创建它的对象。
15.2.2、模板中的嵌套
模板中也可以定义嵌套类,使用方法和非模板类中定义嵌套类的方法类似。例如:
template <typename T>
class OuterClass {
public:
class InnerClass {
public:
void innerFunction() {
std::cout << "InnerClass::innerFunction()" << std::endl;
}
};
void outerFunction() {
InnerClass obj;
obj.innerFunction();
}
};
int main() {
OuterClass<int>::InnerClass obj;
obj.innerFunction();
return 0;
}
在这个示例中,我们定义了一个模板类 OuterClass
,其中包含一个嵌套类 InnerClass
和一个公共成员函数 outerFunction
。InnerClass
中定义了一个公共成员函数 innerFunction
,并在 outerFunction
中创建了一个 InnerClass
的对象并调用了其 innerFunction
函数。
在 main
函数中,我们先使用 OuterClass<int>
实例化一个 OuterClass
类,然后通过 OuterClass<int>::InnerClass
的方式创建了一个 InnerClass
的对象并调用了其 innerFunction
函数。
需要注意的是,模板中的嵌套类和外部类一样,访问权限默认为私有。如果需要将嵌套类的访问权限设置为公共或受保护的,可以使用 public
或 protected
关键字。例如:
template <typename T>
class OuterClass {
public:
// 将嵌套类的访问权限设置为公共
class PublicInnerClass {
public:
void innerFunction() {
std::cout << "PublicInnerClass::innerFunction()" << std::endl;
}
};
// 将嵌套类的访问权限设置为受保护
class ProtectedInnerClass {
protected:
void innerFunction() {
std::cout << "ProtectedInnerClass::innerFunction()" << std::endl;
}
};
};
15.3、异常
异常是一种在程序运行过程中遇到外部错误或内部错误时,通过抛出异常对象来转移程序控制权的机制。异常处理可以诊断程序中的错误,并采取合适的措施来解决它们。
在 C++ 中,异常处理是通过 try
、throw
和 catch
语句来实现的。try
块用于包含可能抛出异常的代码,throw
语句用于抛出一个异常对象,而 catch
块用于捕获并处理抛出的异常对象。
下面我们来看一个示例来进一步了解异常处理机制:
#include <iostream>
#include <string>
void processInput(std::string input) {
try {
// 尝试转换输入为整数
int num = std::stoi(input);
std::cout << "The converted integer is: " << num << std::endl;
} catch (std::invalid_argument& e) {
// 处理无效参数异常
std::cerr << "Caught an invalid_argument exception: " << e.what() << std::endl;
} catch (std::out_of_range& e) {
// 处理超出范围异常
std::cerr << "Caught an out_of_range exception: " << e.what() << std::endl;
} catch (...) {
// 处理其他异常
std::cerr << "Caught an unknown exception" << std::endl;
}
}
int main() {
std::string input;
std::cout << "Please enter an integer: ";
std::getline(std::cin, input);
// 调用 processInput 函数,处理可能抛出异常的代码块
processInput(input);
return 0;
}
在这个示例中,我们定义了一个 processInput
函数,该函数的作用是将输入的字符串转换为整数并输出转换结果。为了处理可能抛出异常的代码块,我们将整个代码块放在一个 try
块中。如果转换过程中抛出异常,将被 catch
块捕获并处理。
在 catch
块中,我们定义了三个异常处理分支,分别处理 std::invalid_argument
异常和 std::out_of_range
异常。如果抛出的异常不属于这两种异常,它将被 '...'
捕获,即最后一个 catch
块。在这个块中,我们输出一条未知异常的错误信息。
需要注意的是,C++ 中的异常处理机制会带来一些性能损失。因此,它不应该被滥用,只应该在特定的情况下使用。通常情况下,只有在处理外部输入、文件、网络通信等错误情况时,才需要使用异常处理机制。
15.3.1、调用abort()
在 C++ 中,abort()
是一个用于异常情况下终止程序的库函数。它会在当前程序的执行点处向操作系统发起一个异常终止信号,操作系统受到信号后会将程序终止,同时调用 abort()
的程序上层调用者也会被终止,具体呈现为 std::abort 调用异常处理程序来终止程序。同时,abort()
函数还会生成一个 core dump 文件,用于调试程序。
需要注意的是,abort()
函数是一个非常暴力的程序终止方法,它会跳过程序的清理工作,导致可能发生内存泄漏或文件未关闭等问题。因此,只有在程序遇到无法恢复的、需要立即终止程序的严重异常情况下,才应该使用 abort()
函数。
下面是一个使用 abort()
函数的例子:
#include <iostream>
#include <cstdlib>
void processInput(int num) {
if (num <= 0) {
std::cerr << "Invalid input: " << num << std::endl;
std::abort(); // 调用 abort 终止程序
}
std::cout << "The input is valid: " << num << std::endl;
}
int main() {
int num;
std::cout << "Please enter a positive integer: ";
std::cin >> num;
// 调用 processInput 函数,如果输入无效,将会调用 abort 终止程序
processInput(num);
return 0;
}
在这个示例中,我们定义了一个 processInput
函数,该函数的作用是检查输入是否为正整数。如果输入不是正整数,将打印一条错误信息,并调用 abort()
函数终止程序。
在 main
函数中,我们调用 processInput
函数,如果输入无效,程序将被 abort()
函数终止。需要注意的是,abort()
函数并不会清理程序的状态,因此,这种终止方式应该仅在必要时使用。
15.3.2、返回错误码
在 C++ 中,函数可以通过返回一个错误码的方式来指示函数执行成功或失败。通常,函数执行成功时返回 0,否则返回一个非零的错误码。返回的错误码可以根据程序的需求而定,通常可以使用枚举类型或预定义的常量表示不同的错误类型。在函数执行失败的情况下,通常需要输出错误信息来提示用户或开发人员发生了什么错误。
下面是一个使用返回错误码的例子:
enum ErrorCode {
NO_ERROR = 0,
INVALID_INPUT = 1,
FILE_NOT_FOUND = 2,
FILE_READ_ERROR = 3,
FILE_WRITE_ERROR = 4
};
ErrorCode readFile(const std::string& filename, std::string& content) {
std::ifstream file(filename);
if (!file) {
std::cerr << "Error: file " << filename << " not found" << std::endl;
return FILE_NOT_FOUND;
}
std::stringstream buffer;
buffer << file.rdbuf();
content = buffer.str();
if (file.bad()) {
std::cerr << "Error: failed to read " << filename << std::endl;
return FILE_READ_ERROR;
}
return NO_ERROR;
}
int main() {
std::string content;
ErrorCode result = readFile("example.txt", content);
if (result != NO_ERROR) {
std::cerr << "Failed to read file: error code " << result << std::endl;
return result;
}
std::cout << "File content: " << content << std::endl;
return 0;
}
在这个示例中,我们定义了一个 readFile
函数,该函数的作用是从文件中读取内容并返回错误码。如果文件不存在,函数会返回 FILE_NOT_FOUND
错误码,如果文件读取失败,函数会返回 FILE_READ_ERROR
错误码。如果函数执行成功,返回 NO_ERROR
错误码,同时将读取的内容写入到 content
参数中。
在 main
函数中,我们调用 readFile
函数来读取 example.txt
文件,并通过返回的错误码来判断函数是否执行成功。如果函数执行失败,我们将会输出相应的错误信息,并返回相应的错误码。如果函数执行成功,我们将会输出读取到的文件内容。
需要注意的是,使用返回错误码的方式来处理函数执行失败时需要开发人员自己来管理错误码和错误信息,需要较大的开发量,并且容易出错。因此,现代 C++ 中推荐使用异常处理机制来代替返回错误码。
15.3.3、异常机制
异常机制是一种在程序运行时可能会抛出(throw)的异常对象,并在程序的控制流中查找可以处理该异常的异常处理程序(exception handler)的机制。在 C++ 中,异常可以是任何类型的值,通常使用具有拷贝和析构函数的类来表示异常。当代码抛出异常时,程序会跳到查找到的异常处理程序处,执行异常处理程序并继续执行程序的剩余部分。
异常机制可以方便地处理函数执行期间可能出现的错误,使代码更加健壮和可维护。具体而言,异常机制可以:
- 在函数执行失败时快速控制程序流程,避免错误传播和导致程序崩溃。
- 简化代码逻辑,避免在每次调用函数时都必须检查返回值。
- 提供了一种通用的方法来处理程序的错误和异常情况,并可以很好地处理在函数和模块之间传递的错误信息。
下面是一个使用异常处理机制的例子:
#include <iostream>
#include <fstream>
#include <string>
#include <stdexcept>
std::string readFile(const std::string& filename) {
std::ifstream file(filename);
if (!file) {
throw std::runtime_error("File not found: " + filename);
}
std::stringstream buffer;
buffer << file.rdbuf();
if (file.bad()) {
throw std::runtime_error("Failed to read file: " + filename);
}
return buffer.str();
}
int main() {
std::string filename = "example.txt";
try {
std::string content = readFile(filename);
std::cout << "File content: " << content << std::endl;
} catch (std::exception& e) {
std::cerr << "Failed to read file " << filename << ": " << e.what() << std::endl;
return 1;
}
return 0;
}
在这个示例中,我们定义了一个 readFile
函数来读取文件的内容。当文件不存在时,函数抛出一个 std::runtime_error
异常并传递相应的错误信息。如果文件读取失败,函数也会抛出一个 std::runtime_error
异常并传递相应的错误信息。
在 main
函数中,我们调用 readFile
函数来读取 example.txt
文件。如果函数执行失败,我们将会捕获抛出的异常,并输出相应的错误信息。如果函数执行成功,我们将输出读取到的文件内容。
需要注意的是,在执行可能抛出异常的代码时应该使用 try
块,并在 catch
块中捕获相应的异常。在捕获异常时应该使用引用类型的异常对象,以避免对象复制的开销。此外,在捕获异常时应该使用较具体的异常类型来捕获异常,以确保只捕获到期望的异常并避免不必要的异常捕获和处理。
总而言之,异常机制是 C++ 中一个非常有用和广泛使用的特性,它可以使代码更加可靠和健壮,并简化程序的错误和异常处理。
15.3.4、将对象用作异常类型
在 C++ 中,可以将任何对象用作异常类型,只要对象可以拷贝并提供异常处理所需的必要信息。
当一个对象被抛出作为异常时,它的类型和值都会被捕获并传递给异常处理程序,这使得程序能够在运行时获得关于错误的详细信息。异常对象通常由自定义异常类的实例或标准库提供的异常类的实例表示,可以在异常处理程序中将其用于错误和异常检测、记录和处理。
考虑以下自定义异常类 my_exception
的示例:
#include <exception>
#include <string>
class my_exception : public std::exception {
public:
my_exception(const std::string& message) : message_(message) {}
virtual const char* what() const noexcept override {
return message_.c_str();
}
private:
std::string message_;
};
int main() {
try {
throw my_exception("Something went wrong!");
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return 1;
}
return 0;
}
在这个示例中,我们定义了一个继承自 std::exception
类的自定义异常类 my_exception
,用于表示程序中可能发生的错误和异常。在构造函数中,我们传递一个字符串参数来指定异常的相关信息。在 what
函数中,我们以 C 字符串的形式返回该信息,并确保函数可以安全调用并不抛出异常。
然后,我们在 main
函数中抛出一个 my_exception
异常,并在 catch
块中捕获和处理该异常。在捕获异常时,我们使用常量引用来捕获相应的异常对象,并调用 what
函数来获取该异常的相关信息。如果异常被抛出,我们将输出异常信息,并返回非零值以指示程序出现了错误。
需要注意的是,当自定义异常类派生自 std::exception
时,应该实现 what
函数并以 C 字符串的形式返回异常信息。这是因为标准库中的异常类std::exception
也继承了 what
函数并提供了默认的实现,它返回了一个表示未知异常的字符串。
总而言之,使用对象作为异常类型是 C++ 中一种非常有用和灵活的方法,它允许程序在运行时捕获和处理所需的异常和错误信息。为了实现对象的异常处理,我们需要保证对象可拷贝并提供必要的异常信息,并根据需要为异常类实现异常处理所需的接口。
15.3.5、异常规范和C++11
异常规范是 C++ 中用于指定函数可能抛出异常的一种机制,它的用法在 C++11 标准中已被弃用。
在旧版本的 C++ 中,可以使用异常规范来指定函数可能抛出哪些类型的异常。异常规范使用 throw
关键字后面跟随 ()
中列出的异常类型来指定。例如,以下函数声明指定该函数可能抛出 std::bad_alloc
异常:
void* operator new(std::size_t size) throw(std::bad_alloc);
这意味着函数在执行过程中可能分配内存失败,抛出 std::bad_alloc
异常。如果函数抛出了一个未指定的异常类型或未列出的异常类型,那么程序将会调用 std::unexpected()
函数并默认调用 std::terminate()
函数。
然而,在 C++11 标准中,异常规范已经被弃用并被移除。这是因为异常规范的使用本质上是一种约束,它与许多现代 C++ 的编程范式(如 RAII 和异常安全代码)不兼容,会产生一些问题:
- 异常规范不允许函数抛出没有列出的异常,这意味着函数必须知道它可能抛出什么异常,这可能会导致一些不必要的限制和额外的复杂性。
- 使用异常规范可能导致二进制兼容性问题。如果一个函数在未来的版本中抛出了一个新的异常,那么它可能无法与旧版本的客户端代码兼容,并且需要重新编译和链接。
在 C++11 中,异常规范被移除并被 noexcept
关键字替代。noexcept
用于指示表达式是否抛出异常。例如,以下函数声明指定该函数不会抛出任何异常:
void do_something() noexcept;
此外,使用 noexcept
是一种提高代码性能的技巧,因为编译器可以利用 noexcept
关键字的信息进行一些优化,例如使用更快的代码路径等。
总之,C++11 标准中已经弃用了异常规范,使用 noexcept
关键字来指示函数是否抛出异常。使用 noexcept
有助于编写更加安全和现代的 C++ 代码,并提高代码性能。
15.3.6、栈解退
栈解退(stack unwinding)是指在 C++ 中,当发生异常时,程序会自动销毁对象,关闭函数,并从当前作用域返回到下一个外层作用域。
当 C++程序抛出异常时,程序会在堆栈上顺序执行所有函数的销毁操作,直到找到一个异常处理程序(try-catch块)。这样做是为了保证程序状态的一致性,避免内存泄漏和资源的浪费。
例如,考虑以下代码片段:
void foo() {
std::string s1("hello");
std::string s2("world");
throw std::runtime_error("exception occurred");
}
int main() {
try {
foo();
} catch (std::exception& e) {
std::cout << "Exception caught: " << e.what() << std::endl;
}
return 0;
}
当 foo()
函数抛出异常时,程序会先执行 s2
的析构函数,然后再执行 s1
的析构函数,最后将控制流传回 main()
函数中的异常处理块。
栈解退是 C++ 中异常处理机制的关键部分。它确保任何在异常抛出前已经创建但尚未释放的对象全部可以正确地销毁,并且程序控制流正确地传递到正确的异常处理程序。栈解退也是确保程序能够保持一致性的重要机制。除非在非常特殊的情况下,否则不应在 C++ 程序中手动执行栈解退。
15.3.7、其他异常特性
除了栈解退以外,C++ 还提供了一些其他的异常特性,具体如下:
-
异常说明(exception specification):可以通过在函数声明中指定异常说明来告诉调用方,该函数会抛出哪些类型的异常,或者不抛出任何异常。异常说明采用
throw
关键字和一个括号括起来的异常类型列表来定义。例如:void foo() throw(std::runtime_error) { throw std::runtime_error("exception occurred"); }
上述
foo()
函数使用throw
关键字指定了它可能抛出std::runtime_error
异常。异常说明可以帮助程序员更好地设计程序,并提高程序的可读性和健壮性。不过需要注意的是,在 C++11 之后,异常说明已经不再被推荐使用,因为它们很难被正确使用,并且很容易引入难以调试的问题。
-
std::exception
类:这是 C++ 标准库中的异常类。其他标准库异常类都派生自它。它包括一个虚函数what()
,可用于获取关于异常的信息。通常情况下,程序员需要从std::exception
继承并重新实现what()
函数,以提供有关特定异常的详细信息。class MyException : public std::exception { public: const char* what() const noexcept override { return "My Exception occurred"; } };
上述代码实现了一个
MyException
异常类,它继承自std::exception
并重新实现了what()
函数。可以通过
catch
块捕获std::exception
或其派生类的任何异常。这样做可以确保代码适合各种异常,即使在未知异常的情况下也可以保持安全。例如:catch (const std::exception& e) { std::cerr << "Exception caught: " << e.what() << '\n'; }
-
std::nested_exception
类:这是一个 C++11 引入的新类,用于捕获并再次抛出异常。使用std::nested_exception
可以将当前异常嵌套在另一个异常中,并在需要时再次抛出。这样可以方便地将异常传递给其他代码,同时保留原始异常的信息。例如:try { // some code that throws an exception } catch (const std::exception& e) { std::throw_with_nested(MyException("caught in outer block")); }
上述代码在
catch
块中将当前异常嵌套在MyException
异常中,并抛出新异常。这样,如果在后续代码中发生其他异常,嵌套异常信息仍然可以保留。std::nested_exception
还提供了std::rethrow_if_nested()
函数,该函数在存在嵌套异常时重新抛出它。
以上是 C++ 中一些重要的异常特性,程序员需要正确理解并使用它们,以确保程序在出现异常时表现良好,并保持一致性和健壮性。
15.3.8、exception类
std::exception
是 C++ 标准库中定义的异常类,它是所有标准库异常类的基类,设计为抽象类,由程序员继承并重新实现其虚函数 what()
以提供异常信息。
std::exception
类包含了如下的主要成员函数:
what()
:由程序员重载,返回一个以 null 结尾的字符序列,指出异常的种类和详细信息。
what()
函数通常被程序员重载以提供自定义的异常信息,如果未重载,则异常信息为默认的 "std::exception"
。例如:
#include <iostream>
#include <exception>
class MyException : public std::exception {
public:
const char* what() const noexcept override {
return "My Exception occurred";
}
};
int main() {
try {
throw MyException();
} catch(const std::exception& e) {
std::cout << "Caught exception: " << e.what() << std::endl;
}
return 0;
}
运行上述程序将输出:
Caught exception: My Exception occurred
除了 std::exception
之外,C++ 也提供了许多其他的标准库异常类,它们都继承自 std::exception
。以下是一些常用的标准库异常类:
std::logic_error
:逻辑错误的异常基类,如std::invalid_argument
和std::out_of_range
。std::runtime_error
:运行时错误的异常基类,如std::range_error
和std::overflow_error
。std::bad_alloc
:动态申请内存失败时抛出的异常。std::bad_cast
:类型转换失败时抛出的异常。
程序员可以通过捕获这些标准库异常类或它们的派生类来处理异常。例如:
try {
// some code that may throw an exception
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
上述代码中,catch
块捕获所有派生自 std::exception
类的异常,并输出异常信息。
15.3.9、异常、类和继承
在 C++ 中,异常和类之间有着密切的关系,因为在面向对象程序设计中,异常通常被用来表示类中的错误。此外,C++ 中的继承机制也能够在异常处理中发挥作用。
异常属于类型安全的程序设计的一部分,从某种意义上来说,异常也是类的一个实例。通常情况下,异常类派生自标准库中的 std::exception
类,也可以定义自己的异常类型,例如:
class MyException : public std::exception {
public:
MyException(const std::string& message)
: message_(message) {}
virtual const char* what() const noexcept override {
return message_.c_str();
}
private:
std::string message_;
};
void foo() {
// some operation that may result in an exception
throw MyException("An error occurred in foo()");
}
int main() {
try {
foo();
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
return 0;
}
在上述示例代码中,MyException
类继承自 std::exception
类,并定义了自己的异常信息。在 foo()
函数中,如果某些操作失败,将抛出 MyException
类的异常。在 main()
函数中,try-catch
块将捕获并处理这个异常。
另外,在面向对象程序设计中,继承也经常用于异常的处理。通常情况下,派生类的异常表示基类异常的某种特殊情况。例如:
class BaseException : public std::exception {
public:
BaseException(const std::string& message)
: message_(message) {}
virtual const char* what() const noexcept override {
return message_.c_str();
}
private:
std::string message_;
};
class DerivedException : public BaseException {
public:
DerivedException(const std::string& message)
: BaseException(message) {}
// additional functions and members
};
void foo() {
// some operation that may result in an exception
throw DerivedException("An error occurred in foo()");
}
int main() {
try {
foo();
} catch (const BaseException& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
return 0;
}
在上述示例中,DerivedException
类继承自 BaseException
类,并添加了一些额外的函数和成员。在 foo()
函数中,如果某些操作失败,将抛出 DerivedException
类的异常。在 main()
函数中,try-catch
块将捕获所有派生自 BaseException
类的异常,并输出异常信息。
需要注意的是,在使用继承时,派生类的 what()
函数应该调用其基类的 what()
函数来提供全面的异常信息。例如:
class DerivedException : public BaseException {
public:
DerivedException(const std::string& message)
: BaseException(message) {}
const char* what() const noexcept override {
std::stringstream ss;
ss << "DerivedException: " << BaseException::what();
return ss.str().c_str();
}
};
在上述示例代码中,DerivedException
类的 what()
函数调用了其基类 BaseException
的 what()
函数,以提供更加全面的异常信息。
15.3.10、异常何何时迷失方向
在使用异常的过程中,可能会出现异常迷失方向的情况。具体来说,这种情况通常发生在以下几种情况下:
1、异常处理不够周详
对于某些代码段,可能会抛出多种类型的异常,但是在异常处理中只捕获了部分异常,对于未处理的异常没有作出任何处理。这种情况下,当未处理的异常发生时,程序可能会产生奇怪的行为或崩溃。
解决方法:在异常处理中尽量考虑到可能发生的所有异常类型,并在 catch
块中进行合理的处理。同时,使用基类异常 std::exception
来覆盖未知类型的异常。
例如:
try {
// some operation that may throw exception
} catch (const std::domain_error& e) {
// handle domain_error
} catch (const std::invalid_argument& e) {
// handle invalid_argument
} catch (const std::exception& e) {
// handle unknown exception
}
2、在异常处理中引发新的异常
当在 catch
块中抛出异常时,在程序的异常处理机制中可能会产生复杂的行为。例如,如果抛出异常的代码段也在 try-catch
块中,可能会导致原始异常多次抛出,从而导致程序的崩溃或意外行为。
解决方法:尽量避免在异常处理中抛出新的异常。如果必须引发异常,请使用 std::rethrow_exception
来处理异常,避免将异常错误地传递给应用程序的其他部分。
例如:
try {
// some operation that may throw exception
} catch (const SomeException& e) {
try {
// do something to handle SomeException
} catch (const OtherException& ex) {
// handle OtherException
std::rethrow_exception(std::current_exception());
}
}
在上述代码中,std::rethrow_exception
函数可用于将异常重新抛出,避免引发新的异常。
3、不正确的异常处理顺序
当异常处理的顺序不正确时,可能会导致异常忽略或捕获不到的情况。例如,如果将基础异常与派生异常混合使用,并且先捕获派生异常,则基础异常可能会被忽略,导致程序的崩溃或产生意外行为。
解决方法:在异常处理中应该按照异常的继承关系顺序进行处理,先捕获基础异常,再捕获派生异常。如果需要捕获特定类型的异常,请使用动态类型的 dynamic_cast
运算符来判断捕获的异常类型。
例如:
try {
// some operation that may throw exception
} catch (const DerivedException& e) {
// handle DerivedException
} catch (const BaseException& e) {
// handle BaseException
} catch (...) {
// handle unknown exception
}
在上述代码中,先捕获 DerivedException
异常,再捕获 BaseException
异常。如果仍然存在未处理的异常,则使用 catch (...)
来处理未知异常。
15.3.11、有关异常的注意事项
在使用异常时,需要注意以下几点:
1、只在必要时使用异常
异常机制虽然可以提高代码的健壮性和可靠性,但是仅在某些不可预见和无法处理的情况下才应该使用异常。否则,过多的异常可能会降低代码的性能和可读性。
2、使用标准异常类型
C++ 中提供了一系列标准异常类型,如 std::logic_error
、std::runtime_error
、std::invalid_argument
、std::out_of_range
等,可以使用这些标准异常类型来标记代码中的异常情况,提高代码的可维护性。
3、非成员函数中抛出异常时,应使用 noexcept
关键字
如果在非成员函数中抛出异常,在函数声明中建议使用 noexcept
关键字来表明函数不会抛出异常,从而提高代码的可读性和可维护性。
4、负责资源释放的异常安全函数
在异常抛出时,所有未释放的资源应正确释放,避免资源泄漏。因此,应设计异常安全函数,确保在函数执行过程中无论是否发生异常都能够正确释放资源。
5、在使用异常的过程中,需要注意异常的性能代价。
异常处理可能会增加代码的性能开销。因此,在一些高性能代码的场景中,应避免在循环和频繁调用的函数中使用异常机制。
6、在异常处理时,尽量避免使用 goto 语句。
使用 goto 语句可以在处理异常时跳过一些代码段,但是会使代码难以理解和调试,因此应尽量避免。
最后,需要注意,异常处理虽然可以提高代码的可靠性,但并不是解决所有问题的完美方法,仍需要根据实际应用场景进行选择和使用。
15.4、RTTI
RTTI(Runtime Type Information)是C++语言提供的一种机制,用于在运行时获取对象的类型信息。RTTI可以支持类型安全的动态类型转换、对象识别和类型信息查询等操作。
C++中的RTTI主要通过两个运算符实现:
1、typeid
运算符
typeid
运算符用于获取一个表达式的类型信息,返回一个 std::type_info
对象的引用。例如:
class Base {};
class Derived : public Base {};
Base* ptr = new Derived;
if(typeid(*ptr) == typeid(Derived)) {
std::cout << "ptr points to a Derived object\n";
}
2、dynamic_cast
运算符
dynamic_cast
运算符用于将一个指针或引用转换为另一个指定类型的指针或引用,如果类型转换失败则返回 nullptr
。在转换之前,运算符会检查是否存在继承关系,从而保证转换的类型安全性。例如:
Base* ptr = new Derived;
Derived* dptr = dynamic_cast<Derived*>(ptr);
if(dptr != nullptr) {
std::cout << "ptr successfully cast to Derived*\n";
}
需要注意的是,如果基类中没有虚函数,使用 typeid
和 dynamic_cast
可能会出现不正常的结果。因此,在使用RTTI时应注意类的继承关系和虚函数的使用。
RTTI通常被认为是C++中不必要的特性,因为使用RTTI可能会增加代码的复杂性和运行时开销。在设计C++类的时候,应该尽量避免使用RTTI,而是通过良好的面向对象设计和多态机制来实现类型安全的操作。
15.4.1、RTTI的用途
RTTI主要用于在运行时获取对象的类型信息,C++标准库中的一些类(例如std::type_info
,std::exception
等)也使用了RTTI。具体来说,RTTI可以有以下用途:
1、动态类型转换
通过 dynamic_cast
运算符可以在运行时将指针或引用转换为另一个类型的指针或引用,从而实现多态类型的操作。例如在父类指针或引用指向子类对象时,可以使用 dynamic_cast
转换为子类指针或引用,以实现对子类特有的操作。
2、对象识别
通过 typeid
运算符可以在运行时获取对象的类型信息,从而识别对象的类型。例如在处理异常时,可以使用 typeid
判断异常的类型,以选择相应的处理方法。
3、类型信息查询
通过获取 std::type_info
对象可以查询类型相关信息,例如类型名称、是否为基本类型等。这在某些场景下(例如类型序列化、类型判断)可以很有用。
需要注意的是,RTTI应该尽量避免滥用,因为使用RTTI会增加代码的复杂性和运行时开销。在设计C++类的时候,应该尽量避免使用RTTI,而是通过良好的面向对象设计和多态机制来实现类型安全的操作。
15.4.2、RTTI的工作原理
RTTI的工作原理是通过将类型信息存储在虚函数表(VTable)中,并使用一种特殊的 type_info
类型来表示类的类型。C++编译器会为每个包含虚函数的类创建一个虚函数表,其中包含指向虚函数的指针和指向 type_info
对象的指针。当类型信息被请求时,RTTI会检查对象的虚函数表中存储的信息,并返回相应的 type_info
对象。
具体来说,假设存在如下代码:
class Base {
public:
virtual void foo() {}
};
class Derived : public Base {
public:
virtual void bar() {}
};
Base* ptr = new Derived;
在 Derived
类中覆盖了 Base
中的 foo
函数,因此 Derived
类的VTable与 Base
类的VTable不同。当使用 typeid
或 dynamic_cast
运算符时,编译器会检查对象的VTable,并找到其中存储的 type_info
对象,返回相应的类类型信息。例如:
if(typeid(*ptr) == typeid(Derived)) {
std::cout << "ptr points to a Derived object\n";
}
在上述代码中,运行时会检查 ptr
指针指向的对象的类型信息,并与 Derived
类型进行匹配。如果匹配成功,则输出相应的信息。
需要注意的是,由于RTTI需要访问对象的VTable,因此只有包含虚函数的类才能使用RTTI。否则,RTTI可能无法找到 type_info
对象并返回正确的类型信息。