一、类的基础
类的定义
在C++中,**类(class)**是一种用户自定义的数据类型,用于封装数据(成员变量)和操作这些数据的函数(成员函数)。类的定义使用class
关键字,基本语法如下:
class ClassName {
private:
// 私有成员(仅在类内部可访问)
data_type member_variable1;
data_type member_variable2;
// ...
public:
// 公有成员(类外部可访问)
return_type member_function1(parameters) {
// 函数实现
}
return_type member_function2(parameters);
// ...
protected:
// 受保护成员(派生类可访问)
data_type member_variable3;
// ...
};
说明:
-
访问修饰符:
private
:默认访问权限,成员只能在类内部访问。public
:成员可以在类外部直接访问。protected
:成员在派生类中可访问。
-
成员函数:
- 可以在类内直接定义,也可以在类外定义(需使用
ClassName::
作用域解析符)。
- 可以在类内直接定义,也可以在类外定义(需使用
-
示例:
class Rectangle { private: int width, height; public: void setDimensions(int w, int h) { width = w; height = h; } int area() { return width * height; } };
对象创建
对象(object)是类的实例化结果。通过类创建对象的过程称为实例化,语法如下:
ClassName objectName; // 默认构造
ClassName objectName(args); // 带参数构造
ClassName* ptr = new ClassName(); // 动态分配(堆内存)
示例:
Rectangle rect; // 创建Rectangle类的对象rect
rect.setDimensions(5, 3); // 调用成员函数
cout << rect.area(); // 输出: 15
Rectangle* rectPtr = new Rectangle(); // 动态对象
rectPtr->setDimensions(4, 2);
cout << rectPtr->area(); // 输出: 8
delete rectPtr; // 释放内存
关键点:
-
对象访问成员:
- 使用
.
运算符(如rect.area()
)。 - 指针使用
->
运算符(如rectPtr->area()
)。
- 使用
-
内存管理:
- 栈对象(如
rect
)自动销毁。 - 堆对象(如
rectPtr
)需手动delete
。
- 栈对象(如
成员变量
成员变量(Member Variables),也称为类的数据成员(Data Members),是定义在类内部的变量。它们用于描述类的属性和状态。
- 定义位置:在类定义内部声明。
- 访问权限:可以通过访问修饰符(
public
、private
、protected
)控制访问权限。 - 生命周期:与类的对象生命周期一致,对象创建时分配内存,对象销毁时释放内存。
- 初始化:可以在构造函数中初始化,也可以通过初始化列表或默认值(C++11 起支持)初始化。
class Car {
public:
std::string brand; // 公有成员变量
private:
int speed; // 私有成员变量
protected:
int mileage; // 受保护成员变量
};
成员函数
成员函数(Member Functions),也称为类的方法(Methods),是定义在类内部的函数。它们用于操作类的数据成员或实现类的行为。
- 定义位置:可以在类内部直接定义,也可以在类外部定义(需使用作用域解析运算符
::
)。 - 访问权限:与成员变量一样,受访问修饰符控制。
- 调用方式:通过类的对象或指针调用(静态成员函数除外)。
- 特殊成员函数:如构造函数、析构函数、拷贝构造函数等。
class Car {
public:
void accelerate() { // 类内定义的成员函数
speed += 10;
}
void brake(); // 类内声明,类外定义
private:
int speed;
};
// 类外定义成员函数
void Car::brake() {
speed -= 10;
}
成员变量与成员函数的关系
- 封装性:成员函数可以访问和修改成员变量,外部代码通过成员函数间接操作数据(遵循封装原则)。
- 作用域:成员函数可以直接访问类的所有成员(包括
private
和protected
),无需通过对象。 - this 指针:每个非静态成员函数内都有一个隐含的
this
指针,指向调用该函数的对象。
class Car {
public:
void setSpeed(int s) {
this->speed = s; // 使用 this 指针访问成员变量
}
private:
int speed;
};
访问控制(public/protected/private)
在C++中,访问控制用于限制类成员的访问权限,主要通过三个关键字实现:public
、protected
和private
。这些关键字决定了类成员在类内、派生类以及类外的可见性。
1. public(公有)
- 定义:
public
成员在类的内部、派生类以及类的外部都可以直接访问。 - 用途:通常用于定义类的接口,即提供给外部使用的成员函数或数据成员。
- 示例:
class MyClass { public: int publicVar; // 公有成员变量 void publicFunc() { // 公有成员函数 cout << "Public function called." << endl; } }; int main() { MyClass obj; obj.publicVar = 10; // 外部直接访问 obj.publicFunc(); // 外部直接调用 return 0; }
2. protected(保护)
- 定义:
protected
成员在类的内部和派生类中可以访问,但在类的外部不可访问。 - 用途:通常用于派生类需要访问的成员,但对类的外部隐藏。
- 示例:
class Base { protected: int protectedVar; // 保护成员变量 }; class Derived : public Base { public: void accessProtected() { protectedVar = 20; // 派生类中可以访问 } }; int main() { Derived obj; // obj.protectedVar = 30; // 错误:外部无法访问 obj.accessProtected(); // 通过公有接口间接访问 return 0; }
3. private(私有)
- 定义:
private
成员仅在类的内部可以访问,派生类和类的外部均不可访问。 - 用途:通常用于隐藏类的实现细节,确保数据封装。
- 示例:
class MyClass { private: int privateVar; // 私有成员变量 void privateFunc() { // 私有成员函数 cout << "Private function called." << endl; } public: void setPrivate(int val) { privateVar = val; // 类内部可以访问 privateFunc(); // 类内部可以调用 } }; int main() { MyClass obj; // obj.privateVar = 40; // 错误:外部无法访问 // obj.privateFunc(); // 错误:外部无法调用 obj.setPrivate(40); // 通过公有接口间接访问 return 0; }
注意事项
- 默认情况下,类的成员是
private
的,而结构体(struct
)的成员是public
的。 - 访问控制是编译时的限制,运行时无法绕过。
- 合理使用访问控制可以提高代码的安全性和可维护性。
构造函数
构造函数是一种特殊的成员函数,它在创建类对象时自动调用。主要作用是初始化对象的数据成员。
特点:
- 名称与类名完全相同
- 没有返回类型(连void都没有)
- 可以重载(一个类可以有多个构造函数)
- 通常是public的(除非有特殊需求)
示例:
class MyClass {
public:
MyClass() { // 无参构造函数
// 初始化代码
}
MyClass(int x) { // 带参构造函数
// 使用参数初始化
}
};
析构函数
析构函数是另一种特殊的成员函数,它在对象销毁时自动调用。主要用于释放对象占用的资源。
特点:
- 名称是在类名前加波浪号(~)
- 没有返回类型
- 没有参数(因此不能重载)
- 通常是public的
- 如果类中有动态分配的内存,必须定义析构函数
示例:
class MyClass {
public:
~MyClass() { // 析构函数
// 清理代码
}
};
重要说明:
- 如果没有显式定义构造函数/析构函数,编译器会生成默认的
- 构造函数可以抛出异常,但析构函数通常不应该抛出异常
- 析构函数的调用顺序与构造函数相反(后构造的先析构)
const成员函数
const成员函数是指在成员函数声明和定义时,在参数列表后面加上const
关键字的函数。这种函数承诺不会修改调用它的对象的状态(即不会修改对象的成员变量)。
基本语法
class MyClass {
public:
void nonConstFunc(); // 普通成员函数
void constFunc() const; // const成员函数
};
特点
- 不修改对象状态:const成员函数不能修改类的任何非静态成员变量(除非成员变量被声明为
mutable
)。 - 调用限制:
- const成员函数可以被const对象和非const对象调用。
- 非const成员函数只能被非const对象调用。
- 重载:const和非const版本的成员函数可以构成重载。编译器会根据调用对象的const性质选择合适的版本。
示例
class Example {
int value;
public:
Example(int v) : value(v) {}
// const成员函数
int getValue() const {
return value; // 允许读取成员变量
// value = 10; // 错误:不能修改成员变量
}
// 非const成员函数
void setValue(int v) {
value = v; // 允许修改成员变量
}
};
int main() {
const Example obj1(5);
Example obj2(10);
cout << obj1.getValue(); // 正确:const对象调用const成员函数
// obj1.setValue(20); // 错误:const对象不能调用非const成员函数
cout << obj2.getValue(); // 正确:非const对象可以调用const成员函数
obj2.setValue(20); // 正确:非const对象调用非const成员函数
}
用途
- 增强代码安全性:防止意外修改对象状态。
- 支持const对象:const对象只能调用const成员函数。
- 明确设计意图:通过函数签名表明该函数是否会修改对象状态。
二、类的特性
封装与信息隐藏
概念
封装(Encapsulation)是面向对象编程(OOP)的核心特性之一,它将数据(成员变量)和操作数据的方法(成员函数)捆绑在一个单元(即类)中。信息隐藏(Information Hiding)是封装的一个关键目标,指的是隐藏对象的内部实现细节,仅对外暴露必要的接口。
实现方式
-
访问修饰符:
private
:仅类内部可访问,外部不可见。protected
:类内部和派生类可访问。public
:完全公开,任何代码均可访问。
-
示例代码:
class BankAccount { private: double balance; // 隐藏数据 public: void deposit(double amount) { // 公开接口 if (amount > 0) balance += amount; } double getBalance() { return balance; } // 受控访问 };
核心作用
- 安全性:防止外部代码直接修改敏感数据(如通过
balance = -1000
的非法操作)。 - 灵活性:内部实现可自由修改(如改用
int
存储分币值),不影响调用方代码。 - 简化复杂度:用户只需关注接口,无需理解内部逻辑。
设计原则
- 最小暴露原则:仅公开必要的成员(通常方法公开,数据私有)。
- 高内聚:类应专注于单一功能,减少不相关数据的暴露。
与信息隐藏的关系
封装是实现信息隐藏的技术手段,而信息隐藏是封装的设计目的。通过合理使用private
成员和公共方法,达到"隐藏实现,暴露接口"的效果。
继承与派生类
基本概念
继承是面向对象编程中的一个重要特性,它允许一个类(派生类)基于另一个类(基类)来构建。派生类继承了基类的属性和方法,同时可以添加新的属性和方法,或修改继承来的行为。
语法
class BaseClass {
// 基类成员
};
class DerivedClass : access-specifier BaseClass {
// 派生类成员
};
其中:
access-specifier
可以是public
、protected
或private
,用于指定继承方式。
继承方式
-
公有继承(public)
- 基类的
public
成员在派生类中仍然是public
。 - 基类的
protected
成员在派生类中仍然是protected
。 - 基类的
private
成员在派生类中不可访问。
- 基类的
-
保护继承(protected)
- 基类的
public
和protected
成员在派生类中都变为protected
。 - 基类的
private
成员在派生类中不可访问。
- 基类的
-
私有继承(private)
- 基类的
public
和protected
成员在派生类中都变为private
。 - 基类的
private
成员在派生类中不可访问。
- 基类的
派生类的特点
-
继承基类的成员
- 派生类可以访问基类的非私有成员(根据继承方式)。
- 派生类可以添加新的成员变量和成员函数。
-
重写基类方法
- 派生类可以重新定义基类的方法(函数),以实现不同的行为(多态性)。
-
构造和析构顺序
- 构造顺序:基类构造函数 → 派生类构造函数。
- 析构顺序:派生类析构函数 → 基类析构函数。
示例代码
#include <iostream>
using namespace std;
// 基类
class Animal {
public:
void eat() {
cout << "Animal is eating." << endl;
}
};
// 派生类(公有继承)
class Dog : public Animal {
public:
void bark() {
cout << "Dog is barking." << endl;
}
};
int main() {
Dog myDog;
myDog.eat(); // 继承自基类
myDog.bark(); // 派生类新增方法
return 0;
}
注意事项
- 基类的私有成员在派生类中不可直接访问,但可以通过基类的公有或保护方法间接访问。
- 派生类不能继承基类的构造函数和析构函数,但可以调用基类的构造函数(通过初始化列表)。
- 继承关系是单向的,派生类知道基类,但基类不知道派生类。
多态与虚函数
多态
多态(Polymorphism)是面向对象编程的三大特性之一(封装、继承、多态)。它允许不同类的对象对同一消息作出不同的响应。多态分为两种:
- 编译时多态(静态多态):通过函数重载和运算符重载实现。
- 运行时多态(动态多态):通过虚函数和继承实现。
虚函数
虚函数(Virtual Function)是实现运行时多态的关键。通过在基类中声明虚函数,派生类可以重写(override)该函数,从而实现不同的行为。
虚函数的声明
在基类中使用 virtual
关键字声明虚函数:
class Base {
public:
virtual void show() {
cout << "Base class show()" << endl;
}
};
虚函数的重写
派生类可以重写基类的虚函数,以实现特定的功能:
class Derived : public Base {
public:
void show() override { // override 关键字(C++11引入)确保重写的是虚函数
cout << "Derived class show()" << endl;
}
};
虚函数的使用
通过基类指针或引用调用虚函数时,实际调用的是对象的动态类型(派生类)的函数:
Base* basePtr = new Derived();
basePtr->show(); // 输出 "Derived class show()"
纯虚函数与抽象类
纯虚函数(Pure Virtual Function)是在基类中声明但没有实现的虚函数,派生类必须实现它。包含纯虚函数的类称为抽象类,不能实例化。
class AbstractBase {
public:
virtual void pureVirtualFunc() = 0; // 纯虚函数
};
虚析构函数
如果基类的析构函数不是虚函数,通过基类指针删除派生类对象时,只会调用基类的析构函数,可能导致内存泄漏。因此,基类的析构函数通常应声明为虚函数:
class Base {
public:
virtual ~Base() {
cout << "Base destructor" << endl;
}
};
静态成员
静态成员是类中声明为static
的成员,包括静态数据成员和静态成员函数。
静态数据成员
- 属于类本身,而不是类的某个对象
- 所有对象共享同一个静态数据成员
- 必须在类外进行定义和初始化(使用类名和作用域解析运算符
::
) - 生命周期与程序相同
class MyClass {
public:
static int count; // 声明静态数据成员
};
int MyClass::count = 0; // 定义并初始化静态数据成员
静态成员函数
- 只能访问静态成员(包括静态数据成员和其他静态成员函数)
- 没有
this
指针 - 可以直接通过类名调用,不需要通过对象
class MyClass {
public:
static void showCount() {
cout << "Count: " << count << endl;
}
static int count;
};
MyClass::showCount(); // 直接通过类名调用
友元
友元是一种破坏封装性的机制,允许非成员函数或其他类访问类的私有成员。
友元函数
- 在类中声明为
friend
的非成员函数 - 可以访问类的所有成员(包括私有成员)
- 不是类的成员函数,没有
this
指针
class MyClass {
private:
int secret;
public:
friend void showSecret(MyClass obj); // 声明友元函数
};
void showSecret(MyClass obj) {
cout << obj.secret; // 可以访问私有成员
}
友元类
- 在类A中声明类B为
friend
,则类B的所有成员函数都可以访问类A的私有成员 - 友元关系是单向的(A声明B为友元,不代表A可以访问B的私有成员)
- 友元关系不能继承
class A {
private:
int secret;
public:
friend class B; // 声明B为友元类
};
class B {
public:
void showAsecret(A obj) {
cout << obj.secret; // 可以访问A的私有成员
}
};
注意:虽然友元提供了灵活性,但过度使用会破坏封装性,应谨慎使用。
拷贝构造
定义
拷贝构造函数(Copy Constructor)是C++中的一种特殊的构造函数,用于创建一个新对象作为已有对象的副本。它的形式通常为:
ClassName(const ClassName& other);
特点
- 参数:拷贝构造函数的参数是对同类型对象的常量引用(
const ClassName&
)。 - 调用时机:
- 用一个对象初始化另一个对象时(如
ClassName obj2 = obj1;
)。 - 函数参数按值传递对象时。
- 函数返回对象时(某些情况下可能会被优化掉)。
- 用一个对象初始化另一个对象时(如
默认行为
如果用户没有显式定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数,执行浅拷贝(即逐成员复制)。对于包含指针或动态分配资源的类,通常需要自定义拷贝构造函数以实现深拷贝。
示例
class MyString {
public:
char* data;
// 自定义拷贝构造函数(深拷贝)
MyString(const MyString& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
};
移动语义
背景
移动语义(Move Semantics)是C++11引入的特性,用于优化资源管理,避免不必要的拷贝操作,提升性能。
核心组件
- 右值引用(Rvalue Reference):
使用&&
表示,如ClassName&&
,用于绑定临时对象(右值)。 - 移动构造函数(Move Constructor):
形式为ClassName(ClassName&& other)
,通过“窃取”临时对象的资源来初始化新对象。 - 移动赋值运算符(Move Assignment Operator):
形式为ClassName& operator=(ClassName&& other)
。
特点
- 资源转移:移动操作将资源(如指针)从源对象转移到目标对象,避免深拷贝。
- 源对象状态:移动后,源对象应处于有效但未定义的状态(通常为空或默认状态)。
示例
class MyString {
public:
char* data;
// 移动构造函数
MyString(MyString&& other) noexcept {
data = other.data; // 直接接管资源
other.data = nullptr; // 置空源对象
}
};
使用场景
- 临时对象传递时(如函数返回局部对象)。
- 显式使用
std::move
将左值转为右值以触发移动操作。
注意
- 移动操作应标记为
noexcept
,以确保与标准库容器兼容(如std::vector
的扩容操作)。 - 如果未定义移动操作,编译器可能回退到拷贝操作。
三、高级特性
模板类与泛型编程
模板类的定义
模板类(Template Class)是C++中实现泛型编程的重要工具。它允许在定义类时使用一个或多个类型参数,这些参数可以在实例化时被具体类型替换。通过模板类,可以编写与类型无关的代码,提高代码的复用性。
template <typename T>
class MyContainer {
private:
T element;
public:
void set(const T& value) {
element = value;
}
T get() const {
return element;
}
};
模板类的实例化
模板类本身并不是一个完整的类,它只是一个“蓝图”。只有在实例化时,编译器才会根据提供的具体类型生成对应的类代码。
MyContainer<int> intContainer; // 实例化为int类型
MyContainer<std::string> strContainer; // 实例化为string类型
模板参数
模板类可以接受多个类型参数,也可以有非类型参数(如整数、指针等)。
template <typename T, int size>
class FixedArray {
private:
T arr[size];
public:
T& operator[](int index) {
return arr[index];
}
};
泛型编程的优势
- 代码复用:同一套代码可以用于多种数据类型。
- 类型安全:相比C中的void指针,模板提供了更好的类型检查。
- 性能:模板是在编译时实例化的,不会带来运行时开销。
模板类的特化
可以为特定类型提供特殊的实现,这称为模板特化。
template <>
class MyContainer<bool> {
private:
bool element;
public:
void set(bool value) {
element = value;
}
bool get() const {
return element;
}
// 可以为bool类型提供特殊的方法
};
注意事项
- 模板类的定义通常需要放在头文件中,因为编译器需要在实例化时看到完整的定义。
- 过度使用模板可能导致代码膨胀(编译生成多个版本的类)。
- 模板错误信息通常比较复杂,难以调试。
模板类是C++强大抽象能力的体现,它使得开发者可以编写高度通用且高效的代码,是标准库(如vector、map等)实现的基础。
运算符重载
运算符重载(Operator Overloading)是 C++ 中允许程序员重新定义已有运算符(如 +
、-
、*
、/
等)的行为的一种特性。通过运算符重载,可以使自定义的类对象也能像内置类型一样使用这些运算符。
基本语法
运算符重载通常以成员函数或全局函数的形式实现。语法如下:
返回类型 operator 运算符(参数列表) {
// 实现运算符的功能
}
成员函数形式的运算符重载
当运算符重载作为类的成员函数时,左侧的操作数必须是该类的对象。例如:
class Complex {
private:
double real;
double imag;
public:
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}
// 重载 + 运算符(成员函数形式)
Complex operator+(const Complex& other) {
return Complex(real + other.real, imag + other.imag);
}
};
全局函数形式的运算符重载
全局函数形式的运算符重载可以处理左侧操作数不是类对象的情况。例如:
Complex operator+(const Complex& a, const Complex& b) {
return Complex(a.real + b.real, a.imag + b.imag);
}
可重载的运算符
C++ 中大多数运算符都可以重载,但以下运算符不能重载:
.
(成员访问运算符).*
(成员指针访问运算符)::
(作用域解析运算符)?:
(条件运算符)sizeof
(大小运算符)typeid
(类型信息运算符)
常用运算符重载示例
-
赋值运算符
=
通常用于深拷贝或资源管理。class MyClass { public: MyClass& operator=(const MyClass& other) { if (this != &other) { // 实现深拷贝或其他逻辑 } return *this; } };
-
输入输出运算符
<<
和>>
通常以全局函数形式重载,用于支持自定义类型的输入输出。ostream& operator<<(ostream& os, const Complex& c) { os << c.real << " + " << c.imag << "i"; return os; }
-
比较运算符
==
、!=
等
用于自定义类型的比较逻辑。bool operator==(const Complex& a, const Complex& b) { return (a.real == b.real) && (a.imag == b.imag); }
注意事项
-
运算符的语义一致性
重载的运算符应尽量保持与内置类型相似的语义,避免引起混淆。例如,+
通常用于加法运算,而不是减法或其他操作。 -
不能改变运算符的优先级和结合性
重载运算符的优先级和结合性与内置运算符一致,无法修改。 -
谨慎重载
&&
和||
由于&&
和||
具有短路求值特性,重载后会失去这一特性,可能引发逻辑问题。 -
避免滥用运算符重载
过度或不合理的运算符重载可能导致代码难以理解和维护。
运算符重载是 C++ 中强大的特性之一,合理使用可以显著提高代码的可读性和简洁性。
智能指针与资源管理
基本概念
智能指针是C++中用于自动管理动态分配内存的类模板,它通过RAII(Resource Acquisition Is Initialization)机制确保资源在适当的时候被释放。智能指针的主要目的是避免内存泄漏和简化资源管理。
主要类型
C++标准库提供了几种智能指针:
-
std::unique_ptr
- 独占所有权的智能指针,同一时间只能有一个
unique_ptr
指向某个对象。 - 当
unique_ptr
被销毁时,它所指向的对象也会被自动删除。 - 不支持拷贝操作,但支持移动语义(
std::move
)。
- 独占所有权的智能指针,同一时间只能有一个
-
std::shared_ptr
- 共享所有权的智能指针,多个
shared_ptr
可以指向同一个对象。 - 内部使用引用计数机制,当最后一个
shared_ptr
被销毁时,对象才会被删除。 - 支持拷贝和移动操作。
- 共享所有权的智能指针,多个
-
std::weak_ptr
- 弱引用智能指针,通常与
shared_ptr
配合使用,解决循环引用问题。 - 不增加引用计数,不能直接访问资源,需通过
lock()
方法转换为shared_ptr
。
- 弱引用智能指针,通常与
使用场景
unique_ptr
:适用于独占资源所有权的场景,如工厂模式返回的对象。shared_ptr
:适用于需要共享资源所有权的场景,如多线程环境下的共享数据。weak_ptr
:适用于需要观察资源但不影响其生命周期的场景,如缓存或观察者模式。
示例代码
#include <memory>
#include <iostream>
// unique_ptr 示例
void uniquePtrExample() {
std::unique_ptr<int> ptr(new int(42));
std::cout << *ptr << std::endl; // 输出: 42
// ptr 离开作用域时自动释放内存
}
// shared_ptr 示例
void sharedPtrExample() {
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = ptr1; // 共享所有权
std::cout << *ptr1 << " " << *ptr2 << std::endl; // 输出: 42 42
// ptr1 和 ptr2 离开作用域时,引用计数为0,内存被释放
}
// weak_ptr 示例
void weakPtrExample() {
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::weak_ptr<int> weakPtr = sharedPtr;
if (auto tempPtr = weakPtr.lock()) { // 转换为 shared_ptr
std::cout << *tempPtr << std::endl; // 输出: 42
}
}
注意事项
- 避免裸指针与智能指针混用:直接使用
new
和delete
可能导致资源管理混乱。 - 优先使用
make_shared
和make_unique
:它们更高效且能避免内存泄漏。 - 循环引用问题:
shared_ptr
之间的循环引用会导致内存泄漏,需用weak_ptr
解决。
智能指针是C++现代编程中资源管理的核心工具,合理使用可以显著提高代码的安全性和可维护性。
异常处理机制
C++中的异常处理机制是一种用于处理程序运行时错误的结构化方式。它允许程序在遇到错误时,能够优雅地处理错误,而不是直接崩溃或产生未定义行为。
基本组成部分
-
try 块
用于包含可能抛出异常的代码。如果在 try 块中的代码抛出了异常,控制权会转移到相应的 catch 块。try { // 可能抛出异常的代码 }
-
throw 表达式
用于抛出异常。可以抛出任何类型的对象(通常是异常类的对象或基本类型)。throw SomeException("Error message");
-
catch 块
用于捕获并处理异常。catch 块必须紧跟在 try 块之后,并且可以捕获特定类型的异常。catch (const SomeException& e) { // 处理异常 }
异常处理流程
- 程序执行到
throw
语句时,会立即停止当前函数的执行,并开始查找匹配的catch
块。 - 查找过程从最近的
try
块开始,逐步向外层作用域查找。 - 如果找到匹配的
catch
块,则执行该块中的代码。 - 如果没有找到匹配的
catch
块,程序会调用std::terminate
终止。
示例代码
#include <iostream>
#include <stdexcept>
void riskyFunction(int value) {
if (value < 0) {
throw std::runtime_error("Negative value not allowed");
}
std::cout << "Value is acceptable: " << value << std::endl;
}
int main() {
try {
riskyFunction(-5); // 这会抛出异常
} catch (const std::runtime_error& e) {
std::cerr << "Caught an exception: " << e.what() << std::endl;
}
return 0;
}
标准异常类
C++标准库提供了一些常用的异常类,都继承自 std::exception
:
std::runtime_error
:运行时错误std::logic_error
:逻辑错误std::out_of_range
:下标越界std::bad_alloc
:内存分配失败
注意事项
- 异常处理会带来一定的性能开销,因此不应将其用于常规的控制流程。
- 确保在析构函数中不要抛出异常,否则可能导致程序终止。
- 可以使用
noexcept
关键字标记不会抛出异常的函数。 - 可以定义自己的异常类,通常应继承自
std::exception
。
异常安全
编写异常安全的代码需要注意:
- 基本保证:确保对象处于有效状态,即使发生异常。
- 强保证:确保操作要么完全成功,要么完全回滚(事务语义)。
- 不抛保证:确保操作不会抛出异常。
嵌套类
嵌套类是指在一个类的内部定义的另一个类。嵌套类可以访问外围类的所有成员(包括私有成员),但外围类不能直接访问嵌套类的私有成员,除非通过嵌套类的公有接口。
特点:
-
访问权限:
- 嵌套类可以访问外围类的所有成员(包括
private
和protected
)。 - 外围类不能直接访问嵌套类的私有成员。
- 嵌套类可以访问外围类的所有成员(包括
-
作用域:
- 嵌套类的作用域仅限于外围类内部。如果需要在外部使用嵌套类,必须通过外围类的作用域限定符(如
OuterClass::InnerClass
)。
- 嵌套类的作用域仅限于外围类内部。如果需要在外部使用嵌套类,必须通过外围类的作用域限定符(如
-
分类:
- 公有嵌套类:使用
public
修饰,可以在外部通过外围类访问。 - 私有嵌套类:使用
private
修饰,仅限外围类内部使用。
- 公有嵌套类:使用
示例:
class Outer {
private:
int outer_data;
public:
class Inner { // 公有嵌套类
public:
void display(Outer& outer) {
std::cout << "Outer data: " << outer.outer_data << std::endl;
}
};
void useInner() {
Inner inner;
inner.display(*this);
}
};
int main() {
Outer outer;
Outer::Inner inner; // 外部访问嵌套类
inner.display(outer);
return 0;
}
局部类
局部类是在函数内部定义的类,其作用域仅限于该函数内部。局部类不能定义静态成员,且只能访问函数中的静态变量或枚举常量。
特点:
-
作用域限制:
- 局部类的作用域仅限于定义它的函数内部,不能在函数外部使用。
-
成员限制:
- 局部类不能定义静态成员变量。
- 局部类只能访问函数中的静态变量或枚举常量,不能直接访问函数的非静态局部变量。
-
实用性:
- 局部类通常用于实现某个函数内部的特定功能,对外部不可见。
示例:
void exampleFunction() {
static int static_var = 10; // 静态变量
int local_var = 20; // 非静态局部变量(局部类无法访问)
class LocalClass { // 局部类
public:
void display() {
std::cout << "Static var: " << static_var << std::endl;
// 错误:无法访问 local_var
// std::cout << "Local var: " << local_var << std::endl;
}
};
LocalClass obj;
obj.display();
}
int main() {
exampleFunction();
return 0;
}
总结
- 嵌套类:用于逻辑分组,可以访问外围类的私有成员,但外围类不能直接访问嵌套类的私有成员。
- 局部类:仅在函数内部可见,功能受限,通常用于封装函数内部的实现细节。