C++的类(从入门到进阶)

一、类的基础

类的定义

在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;
    // ...
};
说明:
  1. 访问修饰符

    • private:默认访问权限,成员只能在类内部访问。
    • public:成员可以在类外部直接访问。
    • protected:成员在派生类中可访问。
  2. 成员函数

    • 可以在类内直接定义,也可以在类外定义(需使用ClassName::作用域解析符)。
  3. 示例

    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;          // 释放内存
关键点:
  1. 对象访问成员

    • 使用.运算符(如rect.area())。
    • 指针使用->运算符(如rectPtr->area())。
  2. 内存管理

    • 栈对象(如rect)自动销毁。
    • 堆对象(如rectPtr)需手动delete

成员变量

成员变量(Member Variables),也称为类的数据成员(Data Members),是定义在类内部的变量。它们用于描述类的属性和状态。

  • 定义位置:在类定义内部声明。
  • 访问权限:可以通过访问修饰符(publicprivateprotected)控制访问权限。
  • 生命周期:与类的对象生命周期一致,对象创建时分配内存,对象销毁时释放内存。
  • 初始化:可以在构造函数中初始化,也可以通过初始化列表或默认值(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;
}

成员变量与成员函数的关系

  • 封装性:成员函数可以访问和修改成员变量,外部代码通过成员函数间接操作数据(遵循封装原则)。
  • 作用域:成员函数可以直接访问类的所有成员(包括 privateprotected),无需通过对象。
  • this 指针:每个非静态成员函数内都有一个隐含的 this 指针,指向调用该函数的对象。
class Car {
public:
    void setSpeed(int s) {
        this->speed = s;  // 使用 this 指针访问成员变量
    }
private:
    int speed;
};

访问控制(public/protected/private)

在C++中,访问控制用于限制类成员的访问权限,主要通过三个关键字实现:publicprotectedprivate。这些关键字决定了类成员在类内、派生类以及类外的可见性。

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;
    }
    
注意事项
  1. 默认情况下,类的成员是private的,而结构体(struct)的成员是public的。
  2. 访问控制是编译时的限制,运行时无法绕过。
  3. 合理使用访问控制可以提高代码的安全性和可维护性。

构造函数

构造函数是一种特殊的成员函数,它在创建类对象时自动调用。主要作用是初始化对象的数据成员。

特点:

  1. 名称与类名完全相同
  2. 没有返回类型(连void都没有)
  3. 可以重载(一个类可以有多个构造函数)
  4. 通常是public的(除非有特殊需求)

示例:

class MyClass {
public:
    MyClass() { // 无参构造函数
        // 初始化代码
    }
    
    MyClass(int x) { // 带参构造函数
        // 使用参数初始化
    }
};

析构函数

析构函数是另一种特殊的成员函数,它在对象销毁时自动调用。主要用于释放对象占用的资源。

特点:

  1. 名称是在类名前加波浪号(~)
  2. 没有返回类型
  3. 没有参数(因此不能重载)
  4. 通常是public的
  5. 如果类中有动态分配的内存,必须定义析构函数

示例:

class MyClass {
public:
    ~MyClass() { // 析构函数
        // 清理代码
    }
};

重要说明:

  1. 如果没有显式定义构造函数/析构函数,编译器会生成默认的
  2. 构造函数可以抛出异常,但析构函数通常不应该抛出异常
  3. 析构函数的调用顺序与构造函数相反(后构造的先析构)

const成员函数

const成员函数是指在成员函数声明和定义时,在参数列表后面加上const关键字的函数。这种函数承诺不会修改调用它的对象的状态(即不会修改对象的成员变量)。

基本语法
class MyClass {
public:
    void nonConstFunc();       // 普通成员函数
    void constFunc() const;    // const成员函数
};
特点
  1. 不修改对象状态:const成员函数不能修改类的任何非静态成员变量(除非成员变量被声明为mutable)。
  2. 调用限制
    • const成员函数可以被const对象和非const对象调用。
    • 非const成员函数只能被非const对象调用。
  3. 重载: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成员函数
}
用途
  1. 增强代码安全性:防止意外修改对象状态。
  2. 支持const对象:const对象只能调用const成员函数。
  3. 明确设计意图:通过函数签名表明该函数是否会修改对象状态。

二、类的特性

封装与信息隐藏

概念

封装(Encapsulation)是面向对象编程(OOP)的核心特性之一,它将数据(成员变量)和操作数据的方法(成员函数)捆绑在一个单元(即类)中。信息隐藏(Information Hiding)是封装的一个关键目标,指的是隐藏对象的内部实现细节,仅对外暴露必要的接口。

实现方式
  1. 访问修饰符

    • private:仅类内部可访问,外部不可见。
    • protected:类内部和派生类可访问。
    • public:完全公开,任何代码均可访问。
  2. 示例代码

    class BankAccount {
    private:
        double balance; // 隐藏数据
    public:
        void deposit(double amount) { // 公开接口
            if (amount > 0) balance += amount;
        }
        double getBalance() { return balance; } // 受控访问
    };
    
核心作用
  1. 安全性:防止外部代码直接修改敏感数据(如通过balance = -1000的非法操作)。
  2. 灵活性:内部实现可自由修改(如改用int存储分币值),不影响调用方代码。
  3. 简化复杂度:用户只需关注接口,无需理解内部逻辑。
设计原则
  • 最小暴露原则:仅公开必要的成员(通常方法公开,数据私有)。
  • 高内聚:类应专注于单一功能,减少不相关数据的暴露。
与信息隐藏的关系

封装是实现信息隐藏的技术手段,而信息隐藏是封装的设计目的。通过合理使用private成员和公共方法,达到"隐藏实现,暴露接口"的效果。


继承与派生类

基本概念

继承是面向对象编程中的一个重要特性,它允许一个类(派生类)基于另一个类(基类)来构建。派生类继承了基类的属性和方法,同时可以添加新的属性和方法,或修改继承来的行为。

语法
class BaseClass {
    // 基类成员
};

class DerivedClass : access-specifier BaseClass {
    // 派生类成员
};

其中:

  • access-specifier 可以是 publicprotectedprivate,用于指定继承方式。
继承方式
  1. 公有继承(public)

    • 基类的 public 成员在派生类中仍然是 public
    • 基类的 protected 成员在派生类中仍然是 protected
    • 基类的 private 成员在派生类中不可访问。
  2. 保护继承(protected)

    • 基类的 publicprotected 成员在派生类中都变为 protected
    • 基类的 private 成员在派生类中不可访问。
  3. 私有继承(private)

    • 基类的 publicprotected 成员在派生类中都变为 private
    • 基类的 private 成员在派生类中不可访问。
派生类的特点
  1. 继承基类的成员

    • 派生类可以访问基类的非私有成员(根据继承方式)。
    • 派生类可以添加新的成员变量和成员函数。
  2. 重写基类方法

    • 派生类可以重新定义基类的方法(函数),以实现不同的行为(多态性)。
  3. 构造和析构顺序

    • 构造顺序:基类构造函数 → 派生类构造函数。
    • 析构顺序:派生类析构函数 → 基类析构函数。
示例代码
#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;
}
注意事项
  1. 基类的私有成员在派生类中不可直接访问,但可以通过基类的公有或保护方法间接访问。
  2. 派生类不能继承基类的构造函数和析构函数,但可以调用基类的构造函数(通过初始化列表)。
  3. 继承关系是单向的,派生类知道基类,但基类不知道派生类。

多态与虚函数

多态

多态(Polymorphism)是面向对象编程的三大特性之一(封装、继承、多态)。它允许不同类的对象对同一消息作出不同的响应。多态分为两种:

  1. 编译时多态(静态多态):通过函数重载和运算符重载实现。
  2. 运行时多态(动态多态):通过虚函数和继承实现。
虚函数

虚函数(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);
特点
  1. 参数:拷贝构造函数的参数是对同类型对象的常量引用(const ClassName&)。
  2. 调用时机
    • 用一个对象初始化另一个对象时(如 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引入的特性,用于优化资源管理,避免不必要的拷贝操作,提升性能。

核心组件
  1. 右值引用(Rvalue Reference)
    使用 && 表示,如 ClassName&&,用于绑定临时对象(右值)。
  2. 移动构造函数(Move Constructor)
    形式为 ClassName(ClassName&& other),通过“窃取”临时对象的资源来初始化新对象。
  3. 移动赋值运算符(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];
    }
};
泛型编程的优势
  1. 代码复用:同一套代码可以用于多种数据类型。
  2. 类型安全:相比C中的void指针,模板提供了更好的类型检查。
  3. 性能:模板是在编译时实例化的,不会带来运行时开销。
模板类的特化

可以为特定类型提供特殊的实现,这称为模板特化。

template <>
class MyContainer<bool> {
private:
    bool element;
public:
    void set(bool value) {
        element = value;
    }
    bool get() const {
        return element;
    }
    // 可以为bool类型提供特殊的方法
};
注意事项
  1. 模板类的定义通常需要放在头文件中,因为编译器需要在实例化时看到完整的定义。
  2. 过度使用模板可能导致代码膨胀(编译生成多个版本的类)。
  3. 模板错误信息通常比较复杂,难以调试。

模板类是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(类型信息运算符)
常用运算符重载示例
  1. 赋值运算符 =
    通常用于深拷贝或资源管理。

    class MyClass {
    public:
        MyClass& operator=(const MyClass& other) {
            if (this != &other) {
                // 实现深拷贝或其他逻辑
            }
            return *this;
        }
    };
    
  2. 输入输出运算符 <<>>
    通常以全局函数形式重载,用于支持自定义类型的输入输出。

    ostream& operator<<(ostream& os, const Complex& c) {
        os << c.real << " + " << c.imag << "i";
        return os;
    }
    
  3. 比较运算符 ==!=
    用于自定义类型的比较逻辑。

    bool operator==(const Complex& a, const Complex& b) {
        return (a.real == b.real) && (a.imag == b.imag);
    }
    
注意事项
  1. 运算符的语义一致性
    重载的运算符应尽量保持与内置类型相似的语义,避免引起混淆。例如,+ 通常用于加法运算,而不是减法或其他操作。

  2. 不能改变运算符的优先级和结合性
    重载运算符的优先级和结合性与内置运算符一致,无法修改。

  3. 谨慎重载 &&||
    由于 &&|| 具有短路求值特性,重载后会失去这一特性,可能引发逻辑问题。

  4. 避免滥用运算符重载
    过度或不合理的运算符重载可能导致代码难以理解和维护。

运算符重载是 C++ 中强大的特性之一,合理使用可以显著提高代码的可读性和简洁性。


智能指针与资源管理

基本概念

智能指针是C++中用于自动管理动态分配内存的类模板,它通过RAII(Resource Acquisition Is Initialization)机制确保资源在适当的时候被释放。智能指针的主要目的是避免内存泄漏和简化资源管理。

主要类型

C++标准库提供了几种智能指针:

  1. std::unique_ptr

    • 独占所有权的智能指针,同一时间只能有一个unique_ptr指向某个对象。
    • unique_ptr被销毁时,它所指向的对象也会被自动删除。
    • 不支持拷贝操作,但支持移动语义(std::move)。
  2. std::shared_ptr

    • 共享所有权的智能指针,多个shared_ptr可以指向同一个对象。
    • 内部使用引用计数机制,当最后一个shared_ptr被销毁时,对象才会被删除。
    • 支持拷贝和移动操作。
  3. 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
    }
}
注意事项
  1. 避免裸指针与智能指针混用:直接使用newdelete可能导致资源管理混乱。
  2. 优先使用make_sharedmake_unique:它们更高效且能避免内存泄漏。
  3. 循环引用问题shared_ptr之间的循环引用会导致内存泄漏,需用weak_ptr解决。

智能指针是C++现代编程中资源管理的核心工具,合理使用可以显著提高代码的安全性和可维护性。


异常处理机制

C++中的异常处理机制是一种用于处理程序运行时错误的结构化方式。它允许程序在遇到错误时,能够优雅地处理错误,而不是直接崩溃或产生未定义行为。

基本组成部分
  1. try 块
    用于包含可能抛出异常的代码。如果在 try 块中的代码抛出了异常,控制权会转移到相应的 catch 块。

    try {
        // 可能抛出异常的代码
    }
    
  2. throw 表达式
    用于抛出异常。可以抛出任何类型的对象(通常是异常类的对象或基本类型)。

    throw SomeException("Error message");
    
  3. catch 块
    用于捕获并处理异常。catch 块必须紧跟在 try 块之后,并且可以捕获特定类型的异常。

    catch (const SomeException& e) {
        // 处理异常
    }
    
异常处理流程
  1. 程序执行到 throw 语句时,会立即停止当前函数的执行,并开始查找匹配的 catch 块。
  2. 查找过程从最近的 try 块开始,逐步向外层作用域查找。
  3. 如果找到匹配的 catch 块,则执行该块中的代码。
  4. 如果没有找到匹配的 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:内存分配失败
注意事项
  1. 异常处理会带来一定的性能开销,因此不应将其用于常规的控制流程。
  2. 确保在析构函数中不要抛出异常,否则可能导致程序终止。
  3. 可以使用 noexcept 关键字标记不会抛出异常的函数。
  4. 可以定义自己的异常类,通常应继承自 std::exception
异常安全

编写异常安全的代码需要注意:

  1. 基本保证:确保对象处于有效状态,即使发生异常。
  2. 强保证:确保操作要么完全成功,要么完全回滚(事务语义)。
  3. 不抛保证:确保操作不会抛出异常。

嵌套类

嵌套类是指在一个类的内部定义的另一个类。嵌套类可以访问外围类的所有成员(包括私有成员),但外围类不能直接访问嵌套类的私有成员,除非通过嵌套类的公有接口。

特点:
  1. 访问权限

    • 嵌套类可以访问外围类的所有成员(包括 privateprotected)。
    • 外围类不能直接访问嵌套类的私有成员。
  2. 作用域

    • 嵌套类的作用域仅限于外围类内部。如果需要在外部使用嵌套类,必须通过外围类的作用域限定符(如 OuterClass::InnerClass)。
  3. 分类

    • 公有嵌套类:使用 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;
}

局部类

局部类是在函数内部定义的类,其作用域仅限于该函数内部。局部类不能定义静态成员,且只能访问函数中的静态变量或枚举常量。

特点:
  1. 作用域限制

    • 局部类的作用域仅限于定义它的函数内部,不能在函数外部使用。
  2. 成员限制

    • 局部类不能定义静态成员变量。
    • 局部类只能访问函数中的静态变量或枚举常量,不能直接访问函数的非静态局部变量。
  3. 实用性

    • 局部类通常用于实现某个函数内部的特定功能,对外部不可见。
示例:
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;
}

总结

  • 嵌套类:用于逻辑分组,可以访问外围类的私有成员,但外围类不能直接访问嵌套类的私有成员。
  • 局部类:仅在函数内部可见,功能受限,通常用于封装函数内部的实现细节。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值