跳转至

考前抱佛脚

完全基于DeepSeek/R1以及OOP期末资料搭建RAG整理得到.

虚函数

1. 基本概念

  • 定义:虚函数是用 virtual 关键字修饰的成员函数,支持动态绑定(运行时多态)
  • 作用:允许通过基类指针或引用调用派生类重写的函数版本
    class Shape {
    public:
        virtual double area() const = 0; // 声明为虚函数
    };
    class Circle : public Shape {
    public:
        double area() const override { 
            return 3.14159 * radius * radius; 
        } // 派生类重写
    };
    

2. 关键特性

  • 动态绑定:通过基类指针/引用调用虚函数时,实际执行派生类的版本(需满足继承关系)
    Shape* s = new Circle(5);
    s->area(); // 调用Circle::area(), 而非Shape::area()
    
  • Override 关键字
  • 显式标记派生类重写基类虚函数[1]。
  • 编译器检查:确保签名严格匹配(参数类型、const限定符等),否则报错[1][4]。
    double area() const override { ... } // 正确重写
    
  • Final 关键字
  • 修饰虚函数:禁止派生类进一步重写[1]。
  • 修饰类:禁止被继承(如 class Base final {};)[1]。

3. 协变返回类型(Covariant Return)

  • 允许派生类重写时返回基类函数返回类型的派生类型的指针或引用,但不能是值类型[1]。
    class Base {
    public:
        virtual Base* clone() const;
    };
    class Derived : public Base {
    public:
        Derived* clone() const override; // 合法协变
    };
    

4. 限制条件

虚函数不可用于:普通函数(非成员函数),静态函数static),构造函数友元函数(即使声明在类内),模板函数(与动态绑定机制冲突)

5. 虚析构函数

  • 必要性:若基类指针指向派生类对象,基类析构函数必须是虚函数,否则派生类析构不会被调用,导致资源泄漏[1][7][9]。
    class Base {
    public:
        virtual ~Base() {} // 虚析构
    };
    class Derived : public Base {
    public:
        ~Derived() { /* 清理资源 */ }
    };
    Base* p = new Derived();
    delete p; // 正确调用Derived::~Derived()
    

6. 纯虚函数与抽象类

  • 纯虚函数
  • 声明方式:virtual ret_type func() = 0;
  • 无实现,强制派生类重写[1][4]。
  • 抽象类
  • 包含至少一个纯虚函数的类。
  • 不可实例化(如 Shape s; 错误)[1][4]。
  • 作用:定义接口规范,可包含普通成员和构造函数[1][4]。
    class Shape {
    public:
        virtual double area() const = 0; // 纯虚函数
        virtual ~Shape() {} 
    };
    

7. 虚函数表(vTable)机制

  • 实现原理:每个多态类含一个虚函数表指针(vptr),指向存储虚函数地址的vTable[1][6]。
  • 运行时开销:动态绑定通过vptr间接调用函数,略慢于静态绑定[6]。

总结应用场景

  1. 运行时多态:统一接口处理不同派生类对象(如 Shape* 数组调用各图形area())[4][11]。
  2. 安全析构:基类虚析构确保完整销毁派生对象[1][9]。
  3. 接口设计:抽象类强制派生类实现关键逻辑(如游戏角色基类定义虚函数 attack())[1][4]。

内联函数(Inline function)

以下是一个内联函数调用的实例(基于参考材料中组合类内的嵌入式函数特性):

#include <iostream>
using namespace std;

class Point {
private:
    int x, y;
public:
    // 类内定义构造函数 = 隐式内联函数 [1]
    Point(int x, int y) : x(x), y(y) {
        cout << "Point constructor called" << endl; 
    }

    // 类内定义成员函数 = 隐式内联函数 [1]
    double getMagnitude() const {
        return sqrt(x*x + y*y); // 短小计算函数适合内联
    }
};

class Circle {
private:
    Point center;    // 嵌入式对象 [1]
    int radius;
public:
    // 组合类构造函数,隐式调用Point的隐式内联构造函数
    Circle(int x, int y, int r) : center(x, y), radius(r) {
        cout << "Circle constructor called" << endl;
    }

    // 内联成员函数:高频调用且逻辑简单 [1]
    double area() const { 
        return 3.14159 * radius * radius; 
    }

    void displayCenter() const {
        // 内联函数调用点:
        // center.getMagnitude() 被展开为内联代码 [1]
        cout << "Center magnitude: " << center.getMagnitude();
    }
};

int main() {
    Circle c(0, 3, 10);
    // 高频调用的内联函数 (编译器会展开代码)
    for (int i = 0; i < 1000000; i++) {
        c.area();     // 虚函数调用被优化:消除压栈/跳转开销 [1][9]
    }
    c.displayCenter(); // 调用嵌套内联函数
    return 0;
}

内联关键点说明:

  1. 类内函数定义PointCircle 类内部定义的函数(getMagnitude()area())自动成为隐式内联函数。编译器将直接在调用点展开函数体 [1]。

  2. 高频调用优化 main() 中调用 c.area() 100万次时:

  3. 内联展开消除函数调用开销(压栈/跳转/返回等指令)[1][9]
  4. 避免重载函数解析成本(普通重载函数需匹配版本)[3][5]

  5. 嵌入式对象调用 displayCenter() 调用 center.getMagnitude()

  6. 直接展开 Point::getMagnitude() 逻辑 [1]
  7. 避免额外构造 Point 临时对象(对比非内联需构造/析构)[1][12]

  8. 组合类特性 Circle 包含 Point 成员:

  9. 初始化顺序:先构造嵌入式对象 center(即使初始化列表顺序在后)[1]
  10. 依赖内联构造优化嵌入式对象创建流程

输出示例:

Point constructor called
Circle constructor called
Center magnitude: 3


为何选择内联?

  • 对比递归函数:避免了栈溢出风险(高频调用深度递归易溢出)[6][12]
  • 对比重载函数:绕过重载解析开销(百万次调用节省显著)[3][5]
  • 编译器限制:实际是否内联由编译器决定(复杂函数可能被拒绝)[1]

多态(Polymorphism)

多态详解

多态(Polymorphism)是面向对象编程的核心特性之一,指通过基类接口调用派生类实现的能力,即同一操作作用于不同对象时产生不同行为。其核心机制是通过虚函数实现运行时动态绑定(动态多态),实现方式如下:

  1. 虚函数(Virtual Function) 基类中用 virtual 声明的成员函数。只有类的成员函数可声明为虚函数,普通函数不能定义为虚函数 [1][7]。

  2. 动态绑定(Dynamic Binding) 当基类指针/引用指向派生类对象时,调用虚函数会执行派生类的覆盖版本(非虚函数则调用基类版本)[1]:

    Base* ptr = new Derived();  // 基类指针指向派生类对象
    ptr->virtualFunction();     // 调用派生类的实现
    

  3. 基类与派生类赋值相容性 [1]:

  4. 派生类对象可赋值给基类对象
  5. 派生类对象地址可赋值给基类指针
  6. 派生类对象可作为基类引用

代码实例

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

// 基类(抽象类含纯虚函数)
class Shape {
public:
    virtual double area() const = 0;  // 纯虚函数 [1][7]
    virtual ~Shape() {}                // 虚析构函数(确保正确释放资源)[1]
};

// 派生类1:矩形
class Rectangle : public Shape {
private:
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    double area() const override {  // override确保正确重写虚函数 [1]
        return width * height;
    }
};

// 派生类2:圆形
class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() const override {
        return 3.14159 * radius * radius;
    }
};

int main() {
    vector<Shape*> shapes;      // 基类指针容器
    shapes.push_back(new Rectangle(4, 5));
    shapes.push_back(new Circle(3));

    // 多态调用:同一接口对不同对象产生不同行为
    for (const auto& shape : shapes) {
        cout << "Area: " << shape->area() << endl;  // 动态绑定 [1]
    }

    // 释放资源
    for (auto& shape : shapes) delete shape;
    return 0;
}

输出结果:

Area: 20
Area: 28.3433

关键特性解释:

  1. 虚函数动态绑定 shape->area() 在运行时根据实际对象类型调用对应实现(Rectangle::area()Circle::area())[1]。若无 virtual 关键字,将始终调用基类函数(静态绑定)。

  2. 抽象类与纯虚函数 =0 语法使 Shape 成为抽象类,强制派生类实现接口(不可直接实例化)[1][7]。

  3. 虚析构函数必要性 基类虚析构函数确保 delete shape 正确调用派生类析构函数,避免资源泄漏 [1]。

  4. 对象赋值兼容性 new Rectangle/Circle 产生的派生类指针可存入基类指针容器 vector<Shape*>,满足派生类到基类的赋值相容性 [1]。

此实例演示了运行时多态的核心思想:通过统一基类接口操作不同派生类对象,提升代码可扩展性和可维护性 [1]。

继承

继承知识点详解

继承(Inheritance)是面向对象编程的核心概念之一,指派生类(子类)基于基类(父类)创建新类,通过复用基类的属性和行为实现代码扩展。其核心是 "is-a" 关系(例如,"矩形是一种形状")[1]。


核心知识点

  1. 继承类型(引用1):
  2. 公有继承(public)

    • 基类的 public 成员在派生类中仍是 public
    • 基类的 protected 成员在派生类中仍是 protected
      class Rectangle : public Shape {}; // 公有继承
      
  3. 保护继承(protected)

    • 基类的 public/protected 成员在派生类中变为 protected
  4. 私有继承(private)

    • 基类的 public/protected 成员在派生类中变为 private
  5. 构造与析构顺序(引用1):

  6. 构造顺序:基类 → 派生类的成员变量 → 派生类构造函数。
  7. 析构顺序:派生类析构函数 → 派生类成员变量 → 基类析构函数。

  8. 成员访问权限(引用1):

  9. protected 成员:派生类可直接访问,外部不可访问。
  10. private 成员:派生类不可直接访问,需通过基类公有接口。

  11. 不可继承的内容(引用1):

  12. 基类的构造函数、析构函数、拷贝构造函数。
  13. 基类的重载运算符和友元函数。

  14. 优势(引用1):

  15. 代码复用:避免重复编写相同逻辑。
  16. 可扩展性:派生类可新增或重写基类功能。
  17. 可维护性:修改基类即可影响所有派生类。

代码实例

#include <iostream>
using namespace std;

// 基类:Shape
class Shape {
public:
    void setWidth(int w) { 
        width = w; 
    }
    void setHeight(int h) { 
        height = h; 
    }
protected:  // 派生类可直接访问
    int width;
    int height;
};

// 派生类:Rectangle(公有继承Shape)
class Rectangle : public Shape {
public:
    int getArea() { 
        return width * height;  // 直接访问基类protected成员 [1]
    }
};

int main() {
    Rectangle rect;
    rect.setWidth(5);       // 调用基类公有方法
    rect.setHeight(7);      // 调用基类公有方法
    cout << "Area: " << rect.getArea() << endl; // 输出:35
    return 0;
}

关键解析

  1. 继承关系
  2. Rectangle 通过 public Shape 继承基类,获得 setWidth/setHeight 方法(引用1)。
  3. 访问权限
  4. width/height 声明为 protected,允许 Rectangle::getArea() 直接访问(引用1)。
  5. 对象使用
  6. rect 对象调用基类方法设置尺寸,自身方法计算面积,体现代码复用。

注意事项

  • 基类初始化:若基类无默认构造函数,需在派生类初始化列表中显式调用基类构造函数(引用1):
    class Base { public: Base(int) {} };
    class Derived : public Base {
    public:
        Derived() : Base(42) {} // 显式调用基类构造函数
    };
    
  • 优先使用公有继承:确保派生类满足 "is-a" 关系,避免设计歧义。
  • 慎用多继承:易引发命名冲突和复杂度(引用4)。

继承是代码层次化的基石,通过合理设计可显著提升程序结构的清晰度和可扩展性 [1][2]。

特殊函数

在 C++ 面向对象编程中,特殊函数是编译器隐式声明或用户可重写的关键函数,用于控制对象的生命周期、拷贝行为和资源管理。主要包括以下函数:

1. 构造函数(Constructor)

  • 作用:初始化对象成员。
  • 调用时机:对象创建时自动调用。
  • 重要规则
  • 构造顺序:先基类 → 成员变量 → 派生类 [1]。
  • 如果基类无默认构造函数,派生类必须在初始化列表中显式调用基类构造函数 [1]。

2. 析构函数(Destructor)

  • 作用:释放对象资源(如堆内存)。
  • 调用时机:对象销毁时自动调用。
  • 重要规则
  • 析构顺序:派生类 → 成员变量 → 基类(与构造相反)[1]。
  • 基类析构函数应声明为 virtual,确保通过基类指针删除派生类对象时调用正确的析构函数,否则可能导致资源泄漏 [1][7]。

3. 拷贝构造函数(Copy Constructor)

  • 作用:用同类型对象初始化新对象(深拷贝)。
  • 签名T(const T& other)
  • 调用时机
  • 对象初始化:T obj2 = obj1;
  • 函数传参:void f(T obj)
  • 函数返回对象 [7]。
  • 重要规则
  • 默认生成浅拷贝,含指针成员时必须自定义深拷贝 [6]。

4. 拷贝赋值运算符(Copy Assignment Operator)

  • 作用:将一个对象的值赋给另一个已存在的对象。
  • 签名T& operator=(const T& rhs)
  • 重要规则
  • 需检查自我赋值:if (this != &rhs) [6]。
  • 返回 *this 以支持链式赋值(如 a = b = c)[19]。

5. 移动构造函数与移动赋值运算符(C++11 新增)

  • 作用:高效转移资源(避免深拷贝开销)。
  • 签名T(T&& other)T& operator=(T&& rhs)

代码实例

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

class Person {
private:
    char* name;  // 指针成员需深拷贝 [6]
public:
    // 构造函数
    Person(const char* s) {
        name = new char[strlen(s) + 1];
        strcpy(name, s);
        cout << "Constructor: " << name << endl;
    }

    // 拷贝构造函数(深拷贝)
    Person(const Person& other) {
        name = new char[strlen(other.name) + 1];
        strcpy(name, other.name);
        cout << "Copy Constructor: " << name << endl;
    }

    // 拷贝赋值运算符
    Person& operator=(const Person& rhs) {
        if (this != &rhs) {  // 防止自我赋值 [6]
            delete[] name;  // 释放旧资源
            name = new char[strlen(rhs.name) + 1];
            strcpy(name, rhs.name);
        }
        cout << "Copy Assignment: " << name << endl;
        return *this;
    }

    // 析构函数
    virtual ~Person() {  // 虚析构确保正确释放派生类资源 [7]
        cout << "Destructor: " << name << endl;
        delete[] name;
    }
};

int main() {
    Person p1("Alice");    // 调用构造函数
    Person p2 = p1;        // 调用拷贝构造函数
    Person p3("Bob");
    p3 = p1;               // 调用拷贝赋值运算符
    return 0;
}

输出

Constructor: Alice
Copy Constructor: Alice   // p2 深拷贝 p1
Constructor: Bob
Copy Assignment: Alice    // p3 赋值 p1
Destructor: Alice         // p3 析构(先释放旧资源 "Bob")
Destructor: Alice         // p2 析构
Destructor: Alice         // p1 析构

关键点解析

  1. 构造与析构顺序
  2. p1 构造 → p2 构造(拷贝构造)→ p3 构造 → p3 赋值 → 析构顺序反向 [1]。
  3. 深拷贝必要性
  4. 默认拷贝构造是浅拷贝(复制指针地址),多个对象指向同一内存会导致重复释放。深拷贝复制数据本身 [6][15]。
  5. 自我赋值检查
  6. operator=if (this != &rhs) 避免 p3 = p3 时先释放自身资源 [6]。
  7. 虚析构函数
  8. Person 是基类,声明 virtual ~Person() 确保派生类对象被基类指针删除时调用完整析构链 [7]。

特殊函数是资源安全的基石,尤其在涉及动态内存或继承时需显式定义 [6][7][15]。

封装(Encapsulation)

封装(Encapsulation)是面向对象编程(OOP)的核心概念之一,它将数据(属性)和操作数据的方法(行为)捆绑在一个单元(类)中,并控制对内部数据的访问权限,以此实现数据保护与接口隔离。以下是关键知识点及代码实例:


核心知识点

  1. 访问控制权限(引用5):
  2. private:仅类内成员函数可访问,外部和派生类不可直接访问。
  3. protected:类内和派生类可访问,外部不可访问。
  4. public:完全开放访问。

    class BankAccount {
    private:  // 数据隐藏
       double balance;
    public:   // 对外接口
       void deposit(double amount) { 
           if (amount > 0) balance += amount; 
       }
       double getBalance() const { return balance; } 
    };
    

  5. 数据隐藏(引用5):

  6. 私有成员变量(如balance)只能通过公有方法(如deposit())间接修改,避免直接暴露实现细节。
  7. 防止外部代码意外修改内部状态(如负存款)。

  8. 接口与实现分离(引用4 & 8):

  9. 公有方法提供稳定接口,内部实现可独立修改而不影响调用方。
  10. 例如修改balance存储方式(如改用整数存储分),调用方无感知。

  11. 增强安全性与可维护性(引用4):

  12. 封装后,数据校验(如deposit()检查正值)集中在类内完成,避免分散的校验逻辑。
  13. 修改内部逻辑时无需修改外部调用代码。

代码实例

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

// 封装示例:学生类
class Student {
private:
    string name;         // 私有属性:外部不可直接访问
    int age;           
    double gpa;

public:    // 公有接口:控制数据访问
    // 构造方法:初始化数据
    Student(string n, int a, double g) : name(n), age(a), gpa(g) {}

    // Getter方法:提供安全读取
    string getName() const { return name; }
    int getAge() const { 
        if (age < 0) return 0; // 数据校验
        return age; 
    }

    // Setter方法:控制修改逻辑
    void setGpa(double newGpa) { 
        if (newGpa >= 0 && newGpa <= 4.0) { // 有效性检查
            gpa = newGpa; 
        } else {
            cerr << "Invalid GPA!" << endl;
        }
    }

    void display() const {
        cout << name << ", Age: " << age << ", GPA: " << gpa << endl;
    }
};

int main() {
    Student s("Alice", 20, 3.8);
    s.display();         // 输出:Alice, Age: 20, GPA: 3.8

    // s.age = -5;       // 错误!私有成员不可外部访问
    s.setGpa(4.5);       // 输出:"Invalid GPA!"(数据保护生效)
    cout << s.getName(); // 安全读取:输出 "Alice"
    return 0;
}

关键解析

  1. 数据保护
  2. nameagegpa声明为private,阻止外部直接修改(如s.age = -5非法)。
  3. 通过setGpa()方法修改时自动校验有效性(如4.5无效) [4][5]。

  4. 安全访问

  5. 通过getName()getAge()等公有方法安全读取数据,getAge()中嵌入逻辑校验(如负年龄返回0) [4]。

  6. 接口稳定性

  7. 若需修改gpa存储方式(如改为百分制),只需调整setGpa()display()内部实现,main()无需改动 [8]。

封装的优点

  1. 数据安全:避免非法操作(如负存款) [4]。
  2. 代码解耦:内部实现可独立优化(如数据存储优化) [5]。
  3. 易于维护:修改集中在类内,减少错误扩散风险 [8]。
  4. 接口清晰:使用者只需关注公有方法,无需理解复杂实现 [5]。

总结:封装通过访问控制接口抽象实现数据保护与模块化设计,是构建健壮、可维护OOP系统的基石 [4][5][8]。

重载

重载(Overloading)知识点详解与代码实例

重载允许同一作用域内定义多个同名函数/运算符,通过参数类型/数量的差异实现不同功能。以下是核心知识点及代码示例:


1. 函数重载(Function Overloading)

  • 规则
  • 同名函数需参数列表不同(类型/数量/顺序) [1][9]。
  • 返回类型不能用于区分重载函数 [1]。
    // 参数数量不同
    void print(int a) { cout << a; }
    void print(int a, int b) { cout << a << b; }
    
    // 参数类型不同
    void print(double a) { cout << a; }
    

2. 运算符重载(Operator Overloading)

(1) 基本规则
  • 目的:为用户自定义类型提供运算符语义(如 +, <<, [])。
  • 限制
  • 不能创建新运算符(如 **)或改变优先级/结合性 [1][9]。
  • =, (), [], -> 必须定义为成员函数 [1][9]。
  • 实现方式
  • 成员函数:隐含 this 指针作为左操作数。
  • 全局函数:需显式声明所有参数,常配合 friend 访问私有成员 [1][9]。
(2) 常见运算符重载示例:

① 算术运算符(+, +=

class Vector {
public:
    int x, y;
    Vector(int x, int y) : x(x), y(y) {}

    // 成员函数:v1 + v2
    Vector operator+(const Vector& rhs) const {
        return Vector(x + rhs.x, y + rhs.y);
    }

    // 成员函数:v1 += v2
    Vector& operator+=(const Vector& rhs) {
        x += rhs.x;
        y += rhs.y;
        return *this;  // 支持链式调用(如 v1 += v2 += v3)[1][9]
    }
};
// 使用
Vector v1(1,2), v2(3,4);
Vector v3 = v1 + v2;  // 调用 operator+
v1 += v2;             // 调用 operator+=

② 下标运算符([]

class Array {
private:
    int data[10];
public:
    // 返回引用以允许修改元素
    int& operator[](int index) {
        if (index < 0 || index >= 10) throw out_of_range("Index error");
        return data[index];
    }

    // const 版本(只读)
    const int& operator[](int index) const {
        return data[index];
    }
};
// 使用
Array arr;
arr[2] = 42;        // 调用非 const 版本
cout << arr[2];     // 调用 const 版本

③ 自增运算符(++

class Counter {
private:
    int count;
public:
    // 前缀 ++(返回引用)
    Counter& operator++() {
        ++count;
        return *this;
    }

    // 后缀 ++(int 占位符区分重载,返回旧值副本)
    Counter operator++(int) {
        Counter temp(*this);
        ++count;
        return temp;  // 返回临时对象 [1][9]
    }
};
// 使用
Counter c;
++c;    // 前缀:c.operator++()
c++;    // 后缀:c.operator++(0)

④ 流运算符(<<, >>

class Point {
private:
    int x, y;
public:
    friend ostream& operator<<(ostream& os, const Point& p); // 声明友元
    friend istream& operator>>(istream& is, Point& p);
};

// 全局函数重载 <<
ostream& operator<<(ostream& os, const Point& p) {
    return os << "(" << p.x << "," << p.y << ")";  // 返回 os 支持链式调用 [4][9]
}

// 全局函数重载 >>
istream& operator>>(istream& is, Point& p) {
    char comma;
    return is >> p.x >> comma >> p.y;  // 格式:x, y
}
// 使用
Point p;
cin >> p;
cout << "Point: " << p;  // 输出 "(x,y)"

⑤ 函数调用运算符(()

class Multiplier {
private:
    int factor;
public:
    Multiplier(int f) : factor(f) {}
    // 重载 () 使对象可像函数一样调用
    int operator()(int x) const {
        return x * factor;
    }
};
// 使用
Multiplier mulBy5(5);
cout << mulBy5(10);  // 输出 50(等价于 mulBy5.operator()(10))[1][9]


关键区别与注意事项

  1. 成员函数 vs 全局函数
  2. =, [], ->, () 必须为成员函数 [1][9]。
  3. 流运算符 <</>> 通常为全局函数(需访问私有成员时用 friend)。
  4. 返回值设计
  5. 赋值类运算符(如 +=, =)返回 *this 的引用以支持链式操作 [1][9]。
  6. 算术运算符(如 +)返回新对象(避免修改操作数)。
  7. 参数传递
  8. 非修改操作符(如 +)使用 const 引用提升效率 [1][9]。

总结:重载通过扩展运算符/函数的语义增强代码可读性,但需严守语法规则并保证逻辑一致性 [1][2][9]。

特例与规则

1. 不能改变的“三大属性”

重载运算符时,你只能改变运算符的行为(具体实现),但以下三个物理属性是绝对不能改变的:

  • 优先级 (Precedence):例如,即使你重载了 +*,在表达式中 * 依然会先于 + 执行。
  • 结合性 (Associativity):运算符是从左往右还是从右往左计算是固定的。
  • 操作数个数 (Arity):你不能把单目运算符(如 !)重载成双目运算符。

2. 逻辑运算符的“短路效应”丢失

这是一个非常隐蔽的坑。对于原生运算符:

  • &&|| 具有短路求值(Short-circuit evaluation)特性(如果左侧表达式已经能决定结果,右侧就不再计算)。
  • 特例:一旦你重载&&||,它们就变成了普通的函数调用。在函数调用中,所有参数都会在进入函数前被计算,因此短路效应会完全消失。这可能导致程序逻辑错误或性能下降。

3. 递增/递减运算符(++ / --)的区分

由于前置(++i)和后置(i++)的符号完全一样,C++ 使用了一个“语法糖”来区分它们:

  • 前置重载T& operator++();(不带参数)。
  • 后置重载T operator++(int);(带一个虚设的 int 参数)。这个 int 没有任何实际用途,仅仅是为了让编译器区分这两个函数。

4. 内存管理运算符(new / delete)

重载 newdelete 与普通运算符不同:

  • 它们通常是作为 静态成员(static) 执行的,即使你没有显式写 static
  • 当你重载了 new,通常也必须配套重载 delete,以确保内存管理逻辑的一致性。

5. 必须返回引用的特例

为了支持“连续赋值”(如 a = b = c;),某些运算符的返回类型有不成文但极其重要的约定:

  • 赋值运算符 (=)复合赋值运算符 (+=, -=):通常应该返回 *this 的引用(即 T&)。
  • 如果你返回的是 void 或对象副本,虽然有时能编译通过,但会导致无法进行连续操作。

总结表:运算符重载禁区

类别 运算符 注意事项
绝对不可重载 :: , . , .* , ?: , sizeof 语言基础逻辑,严禁触动
必须是成员函数 = , [] , () , -> 防止语义混乱,不可作为友元
建议避免重载 && , `

Copy Constructor

拷贝构造函数(Copy Constructor)详解与代码实例

拷贝构造函数是一种特殊的构造函数,用于通过已有对象创建新对象时执行深拷贝(避免指针等资源的共享)。以下为核心知识点及代码示例,结合引用材料解析。


1. 定义与调用时机

  • 作用:初始化新对象为同类型另一对象的副本[1][8.1]。
  • 触发场景
    Person p1("Alice");
    Person p2 = p1;   // 显式调用拷贝构造
    Person p3(p1);    // 显式调用拷贝构造
    void foo(Person p);
    foo(p1);          // 传参时隐式调用拷贝构造
    return p1;        // 函数返回时可能调用(C++17前)
    
    注意:C++17起部分场景强制优化(copy elision),如函数返回纯右值时跳过拷贝构造[2][COPY ELISION]。

2. 语法与实现规则

  • 声明格式ClassName(const ClassName& other)
  • 默认行为
  • 若未显式定义,编译器生成浅拷贝版本(逐成员复制)[2]。
  • 对指针成员仅复制地址(多个对象共享同一内存),导致双重释放风险 [2][8.3]。
  • 自定义必要性
  • 类含指针/资源时需深拷贝(独立分配内存)[2][8.3]。
  • Rule of Three
  • 若自定义析构函数、拷贝构造或拷贝赋值运算符三者之一,通常需同时定义另外两个[2][8.2]。

3. 代码示例与解析

示例1:基础深拷贝实现

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

class Person {
private:
    char* name;  // 指针成员需深拷贝
public:
    // 构造函数
    Person(const char* s) {
        name = new char[strlen(s) + 1];
        strcpy(name, s);
        cout << "Person()" << endl;
    }

    // 自定义拷贝构造函数(深拷贝)
    Person(const Person& other) {
        name = new char[strlen(other.name) + 1];  // 新分配内存
        strcpy(name, other.name);                 // 复制内容
        cout << "Person(&)" << endl;              // 标识调用
    }

    // 析构函数释放资源
    ~Person() {
        delete[] name;
        cout << "~Person()" << endl;
    }
};

int main() {
    Person p1("Alice");
    Person p2 = p1;  // 调用拷贝构造函数(深拷贝)
    return 0;
}
/* 输出:
Person()
Person(&)
~Person()
~Person()
*/
解析: - p2name 指向独立内存,避免与 p1 共享[2][8.3]。 - 未自定义会引发问题(默认浅拷贝导致两次析构释放同一内存)。


示例2:与容器交互的情况

#include <vector>
class MyClass {
public:
    MyClass() { cout << "Default\n"; }
    MyClass(const MyClass&) { cout << "Copy\n"; }
};

int main() {
    vector<MyClass> vec;
    vec.push_back(MyClass());  // C++17前:可能调用拷贝构造
    vec.emplace_back();        // 直接构造(无拷贝)
}
优化: - 使用 emplace_back 直接构造元素,避免额外拷贝[2][8.4]。 - vector 扩容时触发拷贝构造(优化:预分配空间 reserve())[2][8.4]。


示例3:拷贝赋值运算符关联实现(Rule of Three)

class Person {
    // ...(构造函数、拷贝构造、析构如前)

    // 自定义拷贝赋值运算符(Rule of Three)
    Person& operator=(const Person& other) {
        if (this != &other) {        // 防止自赋值
            delete[] name;           // 释放旧资源
            name = new char[strlen(other.name) + 1];
            strcpy(name, other.name);
            cout << "Operator=()" << endl;
        }
        return *this;                // 支持链式赋值
    }
};

int main() {
    Person p1("Alice"), p2("Bob");
    p2 = p1;  // 调用拷贝赋值运算符(深拷贝)
}
关键点: - 赋值运算符需检查自赋值if (this != &other))[2][8.3]。 - 先释放旧资源再分配新内存,避免内存泄漏[2][8.3]。


常见陷阱与解决方案

问题 原因 解决方案
双重释放 默认浅拷贝使多个对象共享指针 自定义拷贝构造(深拷贝)
自赋值错误 p = p 未处理导致资源释放 赋值运算符中检查 this != &other
容器扩容性能损失 vector 扩容触发多次拷贝 预分配空间或使用 emplace_back

总结:拷贝构造确保对象副本的独立性,尤其对含资源的类需自定义深拷贝版本并遵守 Rule of Three[2][8.2][8.3]。

dynamic_cast, static_cast

static_castdynamic_cast 详解与代码实例


1. static_cast

  • 作用:编译时类型转换,安全性较高[1]。
  • 适用场景
  • 基础类型转换(如 doubleint)。
  • 类层次转换(向上转型,派生类→基类)。
  • 禁止无关指针转换或移除 const
  • 示例代码
    double d = 7.1;
    int a = static_cast<int>(d);  // 基础类型转换,输出7
    
    struct Base { virtual void f() {} };
    struct Derived : Base {}; 
    
    Derived* d_ptr = new Derived();
    Base* b_ptr = static_cast<Base*>(d_ptr);      // 向上转型(安全)
    Derived* d2_ptr = static_cast<Derived*>(b_ptr); // 向下转型(不安全!运行时可能出错)[1]
    

2. dynamic_cast

  • 作用:运行时检查的多态类型转换[1][11]。
  • 适用场景
  • 多态类型(基类需有虚函数)的向下转型
  • 成功返回目标指针/引用,失败返回 nullptr(指针)或抛出异常(引用)。
  • 示例代码
    struct A { virtual ~A() {} };  // 必须含虚函数
    struct B : A {};
    
    A* a_ptr1 = new B();  // 实际指向B对象
    A* a_ptr2 = new A();  // 实际指向A对象
    
    // 指针转换
    B* b_ptr1 = dynamic_cast<B*>(a_ptr1); // 成功:实际是B对象
    B* b_ptr2 = dynamic_cast<B*>(a_ptr2); // 失败:返回nullptr
    if (b_ptr1) cout << "Dynamic_cast (1) OK!\n";
    if (!b_ptr2) cout << "Dynamic_cast (2) Fail!\n";  // [2][11]
    
    // 引用转换(失败抛异常)
    try {
        B& b_ref = dynamic_cast<B&>(*a_ptr2);  // 引用实际指向A对象
    } catch (...) {
        cout << "Dynamic_cast (3) Fail!";  // 异常捕获[1]
    }
    

关键区别与注意事项

特性 static_cast dynamic_cast
执行时机 编译时 运行时
安全性 低(部分转换不检查) 高(运行时类型检查)
多态要求 不需要基类虚函数 必须基类有虚函数
失败处理(指针) 产生未定义行为 返回 nullptr
失败处理(引用) 未定义行为(危险) 抛出 std::bad_cast 异常
适用方向 向上/向下转型(无运行时检查) 向下转型(动态检查)

总结: - static_cast:用于显式类型转换(非多态场景),编译期完成,效率高但需谨慎[1]。 - dynamic_cast:用于多态类型安全向下转型,运行时检查确保安全性,需基类有虚函数[1][11]。

dynamic_cast 成功与失败的情况

让我详细解释 dynamic_cast 在什么情况下会成功或失败。

基本概念

dynamic_cast 是 C++ 中用于安全的多态类型转换的运算符,主要用于继承体系中的向下转换(downcast)和交叉转换(cross cast)。

成功的情况

1. 向上转换(Upcast) - 总是成功
class Base {
    virtual void foo() {}  // 必须有虚函数
};

class Derived : public Base {
    void bar() {}
};

int main() {
    Derived* d = new Derived();
    Base* b = dynamic_cast<Base*>(d);  // ✓ 成功(但通常用隐式转换即可)

    delete d;
}
2. 向下转换(Downcast) - 当实际类型匹配时成功
class Base {
    virtual void foo() {}
};

class Derived : public Base {
    void bar() {}
};

int main() {
    // 情况1:实际对象确实是 Derived
    Base* b = new Derived();  // 实际指向 Derived
    Derived* d = dynamic_cast<Derived*>(b);  // ✓ 成功,d != nullptr

    if (d) {
        cout << "转换成功!" << endl;
        d->bar();
    }

    delete b;
}
3. 交叉转换(Cross Cast) - 在菱形继承中转换兄弟类
class Base {
    virtual void foo() {}
};

class Derived1 : public Base {
    void bar1() {}
};

class Derived2 : public Base {
    void bar2() {}
};

class Final : public Derived1, public Derived2 {
};

int main() {
    Final* f = new Final();
    Derived1* d1 = f;

    // 交叉转换:从 Derived1* 转换到 Derived2*
    Derived2* d2 = dynamic_cast<Derived2*>(d1);  // ✓ 成功,因为实际对象是 Final

    if (d2) {
        cout << "交叉转换成功!" << endl;
    }

    delete f;
}

失败的情况

1. 没有虚函数 - 编译错误
class Base {
    // 没有虚函数!
};

class Derived : public Base {
};

int main() {
    Base* b = new Derived();
    Derived* d = dynamic_cast<Derived*>(b);  // ❌ 编译错误!
    // 错误:'Base' 不是多态类型

    delete b;
}

原因dynamic_cast 需要 RTTI(运行时类型信息),而 RTTI 依赖虚函数表。

2. 向下转换类型不匹配 - 返回 nullptr(指针)或抛异常(引用)
class Base {
    virtual void foo() {}
};

class Derived1 : public Base {
    void bar1() {}
};

class Derived2 : public Base {
    void bar2() {}
};

int main() {
    Base* b = new Derived1();  // 实际指向 Derived1

    // 尝试转换为 Derived2(类型不匹配)
    Derived2* d2 = dynamic_cast<Derived2*>(b);  // ❌ 失败,返回 nullptr

    if (d2 == nullptr) {
        cout << "转换失败!" << endl;  // 会执行这里
    }

    delete b;
}
3. 引用转换失败 - 抛出 bad_cast 异常
#include <typeinfo>

class Base {
    virtual void foo() {}
};

class Derived : public Base {
};

class Other : public Base {
};

int main() {
    Derived d;
    Base& b = d;

    try {
        Other& o = dynamic_cast<Other&>(b);  // ❌ 失败,抛出异常
    }
    catch (std::bad_cast& e) {
        cout << "转换失败:" << e.what() << endl;
    }
}
4. 私有或保护继承 - 编译错误或运行时失败
class Base {
    virtual void foo() {}
};

class Derived : private Base {  // 私有继承
};

int main() {
    Derived* d = new Derived();
    Base* b = dynamic_cast<Base*>(d);  // ❌ 编译错误或失败

    delete d;
}

完整示例代码

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

class Animal {
public:
    virtual ~Animal() {}  // 虚析构函数
    virtual void speak() { cout << "Animal speaks" << endl; }
};

class Dog : public Animal {
public:
    void speak() override { cout << "Woof!" << endl; }
    void wagTail() { cout << "Wagging tail" << endl; }
};

class Cat : public Animal {
public:
    void speak() override { cout << "Meow!" << endl; }
    void purr() { cout << "Purring" << endl; }
};

int main() {
    // ========== 成功案例 ==========

    // 1. 正确的向下转换
    Animal* animal1 = new Dog();
    Dog* dog = dynamic_cast<Dog*>(animal1);
    if (dog) {
        cout << "✓ 转换成功!这是一只狗:";
        dog->wagTail();
    }

    // ========== 失败案例 ==========

    // 2. 错误的向下转换(指针)
    Animal* animal2 = new Dog();
    Cat* cat = dynamic_cast<Cat*>(animal2);  // 失败:Dog 不是 Cat
    if (cat == nullptr) {
        cout << "✗ 转换失败!这不是一只猫" << endl;
    }

    // 3. 错误的向下转换(引用)
    Dog dog_obj;
    Animal& animal_ref = dog_obj;
    try {
        Cat& cat_ref = dynamic_cast<Cat&>(animal_ref);  // 抛出异常
    }
    catch (bad_cast& e) {
        cout << "✗ 引用转换失败:" << e.what() << endl;
    }

    // 4. 使用 typeid 验证实际类型
    Animal* animal3 = new Cat();
    cout << "实际类型:" << typeid(*animal3).name() << endl;

    // 清理
    delete animal1;
    delete animal2;
    delete animal3;

    return 0;
}
输出结果
✓ 转换成功!这是一只狗:Wagging tail
✗ 转换失败!这不是一只猫
✗ 引用转换失败:std::bad_cast
实际类型:3Cat

总结对比表

情况 指针转换 引用转换 是否成功
向上转换 返回有效指针 成功 ✓ 总是成功
向下转换(类型匹配) 返回有效指针 成功 ✓ 成功
向下转换(类型不匹配) 返回 nullptr 抛出 bad_cast ✗ 失败
没有虚函数 编译错误 编译错误 ✗ 编译失败
交叉转换(类型匹配) 返回有效指针 成功 ✓ 成功
交叉转换(类型不匹配) 返回 nullptr 抛出 bad_cast ✗ 失败

关键要点

  1. 必须有虚函数(多态类型)才能使用 dynamic_cast
  2. 指针转换失败返回 nullptr
  3. 引用转换失败抛出 std::bad_cast 异常
  4. 转换是否成功取决于对象的实际运行时类型,而非声明类型
  5. 使用前最好检查转换结果(指针检查 nullptr,引用用 try-catch)

this指针

this指针在 C++ 中是一个隐式调用的机制,在类的非静态成员函数中自动生成并使用。它在以下场景被调用:


1. 成员函数内访问成员变量时

当成员函数内访问当前对象的成员变量时,编译器自动插入 this-> 前缀以明确成员归属[5][10]:

class Student {
public:
    int id;
    void setID(int id) {
        this->id = id; // 实际等价于 this->id = id
    }
};
原理:编译器将 id = id; 隐式解释为 this->id = id;


2. 成员函数返回当前对象时

若需链式调用(如连续赋值或连续操作),成员函数需返回对象的引用(*this):

class Counter {
    int count;
public:
    Counter& increment() {
        count++;
        return *this; // 返回当前对象引用以支持链式操作[16]
    }
};

// 链式调用
Counter c;
c.increment().increment(); 


3. 在拷贝赋值运算符中避免自赋值

在重载 operator= 时,通过 this 检查自赋值:

Person& operator=(const Person& other) {
    if (this != &other) { // 检查是否自赋值[16]
        delete[] name;      // 释放旧资源
        name = new char[strlen(other.name) + 1]; 
        strcpy(name, other.name);
    }
    return *this; // 返回当前对象引用
}


4. 区分局部变量和成员变量

当成员变量名与函数参数/局部变量名冲突时,必须显式使用 this 消除歧义:

class Point {
    int x, y;
public:
    Point(int x, int y) : x(x), y(y) {} 
    // 等价的构造函数:Point(int x, int y) { this->x = x; this->y = y; }
};


5. 触发机制总结

场景 this 的作用 是否显式使用
成员函数访问成员变量 指明成员属于当前对象 隐式(自动插入)
链式调用(如 obj.f1().f2() 返回对象自身引用(*this 必须显式
赋值运算符避免自赋值 检查地址是否一致(this == &other 必须显式
成员变量与局部变量同名 强制指定访问成员(this->member 必须显式

关键点: - this编译期行为,成员函数调用时自动传递当前对象地址[5][16]。 - 静态成员函数无 this 指针(属于类而非对象)。 - 使用 this 可提升代码清晰度,尤其在变量名冲突时必备。

Smart Pointers

智能指针详解与代码实例

智能指针是 C++ 中用于自动化管理动态内存的 RAII(资源获取即初始化)工具,通过自动释放内存消除内存泄漏和悬空指针风险[1][3]。以下是核心知识点及代码示例:


1. 核心类型与特性

类型 所有权机制 适用场景 关键限制
std::unique_ptr 独占所有权(不可复制) 单一所有者场景(如工厂模式) 只能通过 std::move 转移
std::shared_ptr 共享所有权(引用计数) 多个对象共享同一资源 循环引用导致内存泄漏
std::weak_ptr 观察者(不增加引用计数) 打破 shared_ptr 循环引用 需转换为 shared_ptr 使用

设计原则:替代裸指针,自动管理生命周期[3][12]。


2. 代码实例分析

(1) unique_ptr:独占所有权

#include <memory>
#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource Created\n"; }
    ~Resource() { std::cout << "Resource Destroyed\n"; }
};

int main() {
    std::unique_ptr<Resource> p1(new Resource());  // 创建独占指针
    // auto p2 = p1; // 错误!不可复制 [1]
    std::unique_ptr<Resource> p2 = std::move(p1);   // 所有权转移
    if (!p1) std::cout << "p1 is now null\n";      // 输出: p1 is now null
} // p2 自动析构,释放内存
输出
Resource Created
p1 is now null
Resource Destroyed
解析: - p1 转移所有权后变为 nullptr,避免双重释放。 - 离开作用域时 p2 自动调用析构函数释放资源[3][12]。


(2) shared_ptr:共享所有权与引用计数

#include <memory>
#include <iostream>

class Node {
public:
    std::shared_ptr<Node> next;  // 共享指针成员
    ~Node() { std::cout << "Node Destroyed\n"; }
};

int main() {
    auto node1 = std::make_shared<Node>(); // 引用计数=1
    auto node2 = std::make_shared<Node>(); // 引用计数=1
    node1->next = node2;  // node2 引用计数=2
    node2->next = node1;  // 循环引用!内存泄漏 [1][3]
} // 引用计数未归零,资源未释放!
问题:循环引用导致引用计数永不归零,内存泄漏。


(3) weak_ptr:打破循环引用

class SafeNode {
public:
    std::shared_ptr<SafeNode> next;
    std::weak_ptr<SafeNode> prev;  // 弱引用观察者
    ~SafeNode() { std::cout << "SafeNode Destroyed\n"; }
};

int main() {
    auto node1 = std::make_shared<SafeNode>();
    auto node2 = std::make_shared<SafeNode>();
    node1->next = node2;
    node2->prev = node1;  // 弱引用不增加计数

    if (auto locked = node2->prev.lock()) {  // 转为 shared_ptr
        std::cout << "Access node1 via weak_ptr\n";
    } // locked 析构,计数恢复
} // 无泄漏!自动释放资源
输出
Access node1 via weak_ptr
SafeNode Destroyed
SafeNode Destroyed
关键点: - weak_ptr::lock() 返回临时 shared_ptr 安全访问资源。 - 离开作用域时引用计数正确归零[3][12]。


3. 最佳实践与陷阱

场景 解决方案 避免的错误
循环引用 weak_ptr 替代 shared_ptr shared_ptr 双向引用导致泄漏
返回动态分配的对象 使用 make_unique/make_shared 裸指针管理失控(内存泄漏)
多线程资源访问 shared_ptr 引用计数原子操作 竞态条件导致重复释放

优先使用 make_shared:减少内存分配次数,异常安全[3]。

auto ptr = std::make_shared<int>(42);  // 优于 shared_ptr<int>(new int(42))

智能指针通过自动化内存管理显著提升代码健壮性,结合 RAII 原则可彻底避免资源泄漏[1][3][12]。


std::shared_ptrstd::weak_ptr区别

在 C++ 中,std::shared_ptrstd::weak_ptr 是用于管理动态内存的智能指针,核心目标是解决资源自动释放和循环引用问题。其工作机制和区别如下:

1. std::shared_ptr:共享所有权

核心机制:通过引用计数管理资源的所有权。 - 多个 shared_ptr 可指向同一对象。 - 每新增一个 shared_ptr,引用计数 +1;每销毁一个,计数 -1。 - 当引用计数归零时,自动释放资源[1][3]。

#include <memory>
#include <iostream>
class Resource {
public:
    Resource() { std::cout << "Resource Created\n"; }
    ~Resource() { std::cout << "Resource Destroyed\n"; }
};

int main() {
    std::shared_ptr<Resource> p1 = std::make_shared<Resource>(); // 引用计数=1
    {
        std::shared_ptr<Resource> p2 = p1; // 引用计数=2
        std::cout << "Use count: " << p1.use_count() << "\n"; // 输出 2
    } // p2 析构,引用计数降为 1
    std::cout << "Use count: " << p1.use_count() << "\n"; // 输出 1
} // p1 析构,计数归零,释放资源
输出
Resource Created
Use count: 2
Use count: 1
Resource Destroyed

关键特性: 1. 线程安全:引用计数的增减是原子操作,线程安全[3]。 2. 性能std::make_shared 合并内存分配,效率更高(优于 new)[3]:

auto p = std::make_shared<int>(42); // 推荐
3. 循环引用问题:若对象相互持有 shared_ptr,引用计数永不归零,导致内存泄漏[1][3]: - 优先使用 std::make_shared:减少内存分配次数,提升性能[3]。


2. std::weak_ptr:弱引用观察者

核心机制不控制对象生命周期,仅观察 shared_ptr 管理的资源。 - 不增加引用计数,无法直接访问资源。 - 需通过 lock() 转换为 shared_ptr 后使用。 - 若资源已被释放,lock() 返回 nullptr[1][3][6]。

#include <memory>
#include <iostream>
class Node {
public:
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;    // 弱引用打破循环
    ~Node() { std::cout << "Node Destroyed\n"; }
};

int main() {
    auto node1 = std::make_shared<Node>(); // node1 引用计数=1
    auto node2 = std::make_shared<Node>(); // node2 引用计数=1

    node1->next = node2;     // node2 引用计数变为 2
    node2->prev = node1;     // node1 引用计数仍为 1(weak_ptr 不增加计数)

    if (auto locked = node2->prev.lock()) { // 转为 shared_ptr,临时计数+1
        std::cout << "Access node1 via weak_ptr\n";
    } // locked 析构,计数恢复
} // node1/node2 的引用计数均归零,资源释放
输出
Access node1 via weak_ptr
Node Destroyed
Node Destroyed

关键特性: 1. 解决循环引用: 若两个对象互相持有 shared_ptr,引用计数永不归零,导致内存泄漏。 使用 weak_ptr 断开循环(如上例中的 prev)[3][6]。 2. 资源安全检查: 通过 lock() 检查资源有效性,避免悬空指针。


3. 二者核心区别

特性 shared_ptr weak_ptr
所有权 共享所有权,影响引用计数 仅观察资源,不参与引用计数
直接访问资源 支持(通过 operator*/-> 不支持,需用 lock() 转换
内存开销 控制块(含引用计数) 依赖 shared_ptr 的控制块
典型场景 多个对象共享资源 解决循环引用或缓存观察

4. 关键注意事项

  1. 循环引用的风险: 当 shared_ptr 形成闭环时,资源无法释放。例如:

    struct A {
        std::shared_ptr<A> ptr;
    };
    auto a1 = std::make_shared<A>();
    auto a2 = std::make_shared<A>();
    a1->ptr = a2;  // a2 引用计数=2
    a2->ptr = a1;  // a1 引用计数=2
    
    此时离开作用域后,a1a2 计数均保持为 1,内存泄漏[1][3][6]。 解决方案:将其中一个 shared_ptr 替换为 weak_ptr

  2. weak_ptr::lock() 的安全使用

    std::shared_ptr<Resource> p_shared = std::make_shared<Resource>();
    std::weak_ptr<Resource> p_weak = p_shared;
    if (auto p = p_weak.lock()) { // 安全访问
        p->do_something();
    }
    


总结

  • shared_ptr:管理共享资源,通过引用计数自动释放内存。
  • weak_ptr:观察者模式,解决循环引用问题,需通过 lock() 安全访问资源[1][3][6]。 实际开发中,优先使用 std::make_shared 创建智能指针,并在可能产生循环引用的场景主动使用 weak_ptr 避免内存泄漏。

cast (类型转换)

以下是针对 static_castdynamic_castreinterpret_cast 的详细讲解及代码实例:


1. static_cast

核心特性: - 编译时完成:不进行运行时类型检查,效率高但可能存在不安全转换[1][5]。 - 允许的转换: - 基本数据类型转换(如 doubleint)。 - 类层次中的向上转换(派生类→基类,隐式或显式)。 - 类层次中的向下转换(基类→派生类,但不安全,编译器不检查实际对象类型)[1]。 - 禁止的转换: - 无关类型指针(如 int*double* 会编译错误)。 - 移除 const 限定[1][5]。

代码实例

// 基本类型转换
double d = 7.1;
int a = static_cast<int>(d);  // OK: 7[1]

// 类层次向上转换(安全)
struct Base {};
struct Derived : Base {};
Derived d;
Base* pb = static_cast<Base*>(&d);  // 隐式转换也可,显式更清晰[1]

// 类层次向下转换(不安全!)
Base* pb = new Derived;
Derived* pd = static_cast<Derived*>(pb);  // 编译通过,但若 pb 实际指向其他派生类,行为未定义[1]


2. dynamic_cast

核心特性: - 运行时检查:依赖 RTTI(运行时类型信息),检查转换是否合法[1][2]。 - 适用场景: - 向下转换:多态类(基类需有虚函数)中,将基类指针/引用转为派生类指针/引用。 - 转换失败时:指针返回 nullptr,引用抛出 std::bad_cast 异常[1][2][3]。 - 限制:基类必须有虚函数(否则编译错误)[1]。

代码实例

struct A { virtual void f() {} };  // 必须有虚函数
struct B : A {};
struct C : A {};

int main() {
    A* pa = new B;  // 实际指向 B 对象

    // 向下转换(成功)
    B* pb = dynamic_cast<B*>(pa);  // OK: 返回 B*[1]

    // 向下转换(失败)
    C* pc = dynamic_cast<C*>(pa);  // 失败,返回 nullptr[1]
    if (pc == nullptr) std::cout << "Cast failed!\n";

    // 引用转换(失败时抛异常)
    try {
        C& rc = dynamic_cast<C&>(*pa);  // 抛出 std::bad_cast[2][3]
    } catch (std::bad_cast& e) {
        std::cerr << e.what() << std::endl;
    }
    delete pa;
}

输出示例

Cast failed!
std::bad_cast


3. reinterpret_cast

核心特性: - 底层二进制重解释:直接按比特位重新解释指针类型,无任何类型检查[1][5]。 - 适用场景: - 无关类型指针的转换(如 int*double*)。 - 函数指针与数据指针的转换。 - 风险:易引发未定义行为,应谨慎使用[1]。

代码实例

int main() {
    int a = 0x12345678;
    // int* 转 double*(语法允许,但值无意义)
    double* pd = reinterpret_cast<double*>(&a); 
    std::cout << *pd << "\n";  // 输出无意义的浮点数

    // 检查转换后的指针地址是否相同
    std::cout << "Address: " << &a << " vs " << pd << std::endl;  // 地址相同
}


关键对比总结

特性 static_cast dynamic_cast reinterpret_cast
检查时机 编译时 运行时(依赖 RTTI) 无检查
安全性 部分安全(向下转换不安全) 安全(失败返回 nullptr 或抛异常) 极不安全
转换类型 相关类型(数值、类层次) 多态类的向下转换 任意指针/整数类型
性能开销 中(运行时类型查询)
适用场景 显式数值转换、类向上转换 多态类向下转换 底层硬件操作、序列化

补充:const_cast

虽未在用户问题中提及,但为完整性补充: - 唯一功能:添加或移除 const/volatile 限定[1][5]。

const int x = 10;
int* px = const_cast<int*>(&x);  // 移除 const
*px = 20;  // 未定义行为!x 原是常量

建议:优先使用 static_castdynamic_cast;避免 reinterpret_cast,除非处理底层系统编程[1][5]。

Iterator

迭代器详解与实例

迭代器是 C++ STL 的核心组件,用于 统一访问容器元素,充当容器与算法间的桥梁[1][2][3]。其核心机制是模拟指针行为(如 ++*!=),支持遍历容器元素而不暴露底层实现。


1. 迭代器的核心概念

  • 作用:
  • 遍历容器元素(如数组、链表、映射表)[1][3]。
  • 作为算法(如 sortfind)的操作范围([begin(), end()))[2][3]。
  • 实现要求
  • 容器需提供 begin()end() 成员函数,返回迭代器[1][3]。
  • 迭代器类型需支持 ++(后移)、*(解引用)、!=(比较)操作[1][3]。

2. 迭代器分类(能力从低到高)

类型 支持操作 代表容器
Input Iterator 单向遍历、读取元素(单次算法) istream_iterator
Forward Iterator 多次单向遍历 forward_list
Bidirectional Iterator 双向遍历(++-- list, set
RandomAccess Iterator 常数时间跳跃(+n-n vector, deque, array
Contiguous Iterator (C++17) 物理内存连续 array, vector 的数据指针

关键差异: - vector 迭代器支持 v.begin() + 5(随机访问)[1][3]; - list 迭代器仅支持 ++--(双向)[3][6]。


3. 迭代器使用实例

示例1:容器遍历的4种方式(vector[3]
#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<int> nums = {2, 4, 6, 8};

    // 1. 传统索引 (仅支持连续容器)
    for (int i = 0; i < nums.size(); i++) 
        cout << nums[i] << " ";  // 输出: 2 4 6 8

    // 2. 显式迭代器
    for (vector<int>::iterator it = nums.begin(); it != nums.end(); ++it)
        cout << *it << " ";

    // 3. 自动类型推导 (推荐)
    for (auto it = nums.begin(); it != nums.end(); ++it)
        cout << *it << " ";

    // 4. 范围for循环 (底层依赖迭代器)
    for (int n : nums) 
        cout << n << " ";
}
示例2:list 的双向迭代器[3]
#include <list>
#include <iostream>
using namespace std;

int main() {
    list<string> words = {"hello", "world", "C++"};

    // 正向遍历
    for (auto it = words.begin(); it != words.end(); ++it)
        cout << *it << " ";  // 输出: hello world C++

    // 反向遍历
    auto rit = words.rbegin();
    while (rit != words.rend()) {
        cout << *rit << " ";  // 输出: C++ world hello
        ++rit;
    }
}
示例3:迭代器适配器(back_inserter[2]
#include <vector>
#include <algorithm>
#include <iterator>
using namespace std;

int main() {
    vector<int> src = {1, 2, 3};
    vector<int> dest;

    // 使用 back_inserter 避免预分配空间
    copy(src.begin(), src.end(), back_inserter(dest)); 
    // dest 变为 {1, 2, 3}
}
示例4:map 的迭代器(键值对访问)[3][6]
#include <map>
#include <iostream>
using namespace std;

int main() {
    map<string, int> price = {{"apple", 3}, {"banana", 1}};

    // 遍历键值对
    for (auto it = price.begin(); it != price.end(); ++it)
        cout << it->first << ":" << it->second << " "; 
        // 输出: apple:3 banana:1

    // C++17 结构化绑定
    for (const auto& [fruit, cost] : price)
        cout << fruit << " costs $" << cost << endl;
}

4. 迭代器适配器应用

  • ostream_iterator: 将输出流变为迭代器[2]:
    vector<int> v = {1, 2, 3};
    copy(v.begin(), v.end(), ostream_iterator<int>(cout, ", ")); 
    // 输出: 1, 2, 3,
    
  • 自定义输出(避免末尾逗号)
    // 自定义 infix_ostream_iterator [2]
    copy(v.begin(), v.end(), infix_ostream_iterator<int>(cout, ", ")); 
    // 输出: 1, 2, 3
    

5. 注意事项与陷阱

  1. 迭代器失效
  2. 修改容器(如 erase)可能使迭代器失效[2][3]。

    list<int> L = {1, 2, 3};
    auto it = L.begin();
    L.erase(it);       // it 失效
    it = L.erase(it);  // 正确: 接收 erase 返回的新迭代器[2]
    

  3. map 的隐式插入

  4. [] 访问不存在的键会插入新元素[3][6]:

    map<string, int> m;
    cout << m["new_key"];  // 隐式插入 ("new_key", 0)
    // 正确做法: 先用 contains() 检查 (C++20)
    if (m.contains("key")) /*...*/;
    

  5. list::size() 性能 :

  6. C++11 前可能是 O(n),优先用 empty()[2]。

总结

  • 迭代器类型:根据容器特性选择(如 vector → 随机访问;list → 双向)[1][3]。
  • 核心操作begin()/end() 定义范围,++/*/!= 实现遍历[1]。
  • 适配器back_inserterostream_iterator 等扩展功能[2]。
  • 安全准则:警惕迭代器失效和隐式插入[2][3][6]。

最佳实践:优先用 范围for循环auto推导 简化代码;复杂操作搭配 STL 算法(如 sortcopy)[2][3]。

静态数据访问

省流:3种方法(假设MyClass类下static int count;且有类实例MyClass obj1;.

  • 方式1:使用类名访问(推荐,标准方式) MyClass::count
  • 方式2:通过对象访问(不推荐,但可以) obj1.count
  • 方式3:通过静态成员函数访问 MyClass::printCount();
#include <iostream>
using namespace std;

class MyClass {
public:
    static int count;  // 静态数据成员声明
    int id;

    MyClass() {
        id = ++count;  // 在成员函数内直接访问
    }

    static void printCount() {  // 静态成员函数
        cout << "Count: " << count << endl;  // 直接访问
    }
};

// 静态数据成员必须在类外定义
int MyClass::count = 0;

int main() {
    // 方式1:使用类名访问(推荐,标准方式)
    cout << "Initial count: " << MyClass::count << endl;

    MyClass obj1, obj2, obj3;

    // 方式2:通过对象访问(不推荐,但可以)
    cout << "Count via object: " << obj1.count << endl;

    // 方式3:使用类名访问(标准方式)
    cout << "Final count: " << MyClass::count << endl;

    // 方式4:通过静态成员函数访问
    MyClass::printCount();

    return 0;
}

输出结果:

Initial count: 0
Count via object: 3
Final count: 3
Count: 3