考前抱佛脚
完全基于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]。
总结应用场景¶
- 运行时多态:统一接口处理不同派生类对象(如
Shape*数组调用各图形area())[4][11]。 - 安全析构:基类虚析构确保完整销毁派生对象[1][9]。
- 接口设计:抽象类强制派生类实现关键逻辑(如游戏角色基类定义虚函数
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;
}
内联关键点说明:¶
-
类内函数定义 在
Point和Circle类内部定义的函数(getMagnitude()、area())自动成为隐式内联函数。编译器将直接在调用点展开函数体 [1]。 -
高频调用优化
main()中调用c.area()100万次时: - 内联展开消除函数调用开销(压栈/跳转/返回等指令)[1][9]
-
避免重载函数解析成本(普通重载函数需匹配版本)[3][5]
-
嵌入式对象调用
displayCenter()调用center.getMagnitude(): - 直接展开
Point::getMagnitude()逻辑 [1] -
避免额外构造
Point临时对象(对比非内联需构造/析构)[1][12] -
组合类特性
Circle包含Point成员: - 初始化顺序:先构造嵌入式对象
center(即使初始化列表顺序在后)[1] - 依赖内联构造优化嵌入式对象创建流程
输出示例:
Point constructor called Circle constructor called Center magnitude: 3
为何选择内联?¶
- 对比递归函数:避免了栈溢出风险(高频调用深度递归易溢出)[6][12]
- 对比重载函数:绕过重载解析开销(百万次调用节省显著)[3][5]
- 编译器限制:实际是否内联由编译器决定(复杂函数可能被拒绝)[1]
多态(Polymorphism)¶
多态详解¶
多态(Polymorphism)是面向对象编程的核心特性之一,指通过基类接口调用派生类实现的能力,即同一操作作用于不同对象时产生不同行为。其核心机制是通过虚函数实现运行时动态绑定(动态多态),实现方式如下:
-
虚函数(Virtual Function) 基类中用
virtual声明的成员函数。只有类的成员函数可声明为虚函数,普通函数不能定义为虚函数 [1][7]。 -
动态绑定(Dynamic Binding) 当基类指针/引用指向派生类对象时,调用虚函数会执行派生类的覆盖版本(非虚函数则调用基类版本)[1]:
Base* ptr = new Derived(); // 基类指针指向派生类对象 ptr->virtualFunction(); // 调用派生类的实现 -
基类与派生类赋值相容性 [1]:
- 派生类对象可赋值给基类对象
- 派生类对象地址可赋值给基类指针
- 派生类对象可作为基类引用
代码实例¶
#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
关键特性解释:¶
-
虚函数动态绑定
shape->area()在运行时根据实际对象类型调用对应实现(Rectangle::area()或Circle::area())[1]。若无virtual关键字,将始终调用基类函数(静态绑定)。 -
抽象类与纯虚函数
=0语法使Shape成为抽象类,强制派生类实现接口(不可直接实例化)[1][7]。 -
虚析构函数必要性 基类虚析构函数确保
delete shape正确调用派生类析构函数,避免资源泄漏 [1]。 -
对象赋值兼容性
new Rectangle/Circle产生的派生类指针可存入基类指针容器vector<Shape*>,满足派生类到基类的赋值相容性 [1]。
此实例演示了运行时多态的核心思想:通过统一基类接口操作不同派生类对象,提升代码可扩展性和可维护性 [1]。
继承¶
继承知识点详解¶
继承(Inheritance)是面向对象编程的核心概念之一,指派生类(子类)基于基类(父类)创建新类,通过复用基类的属性和行为实现代码扩展。其核心是 "is-a" 关系(例如,"矩形是一种形状")[1]。
核心知识点¶
- 继承类型(引用1):
-
公有继承(public):
- 基类的
public成员在派生类中仍是public。 - 基类的
protected成员在派生类中仍是protected。class Rectangle : public Shape {}; // 公有继承
- 基类的
-
保护继承(protected):
- 基类的
public/protected成员在派生类中变为protected。
- 基类的
-
私有继承(private):
- 基类的
public/protected成员在派生类中变为private。
- 基类的
-
构造与析构顺序(引用1):
- 构造顺序:基类 → 派生类的成员变量 → 派生类构造函数。
-
析构顺序:派生类析构函数 → 派生类成员变量 → 基类析构函数。
-
成员访问权限(引用1):
protected成员:派生类可直接访问,外部不可访问。-
private成员:派生类不可直接访问,需通过基类公有接口。 -
不可继承的内容(引用1):
- 基类的构造函数、析构函数、拷贝构造函数。
-
基类的重载运算符和友元函数。
-
优势(引用1):
- 代码复用:避免重复编写相同逻辑。
- 可扩展性:派生类可新增或重写基类功能。
- 可维护性:修改基类即可影响所有派生类。
代码实例¶
#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;
}
关键解析¶
- 继承关系:
Rectangle通过public Shape继承基类,获得setWidth/setHeight方法(引用1)。- 访问权限:
width/height声明为protected,允许Rectangle::getArea()直接访问(引用1)。- 对象使用:
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 析构
关键点解析:¶
- 构造与析构顺序:
p1构造 →p2构造(拷贝构造)→p3构造 →p3赋值 → 析构顺序反向 [1]。- 深拷贝必要性:
- 默认拷贝构造是浅拷贝(复制指针地址),多个对象指向同一内存会导致重复释放。深拷贝复制数据本身 [6][15]。
- 自我赋值检查:
operator=中if (this != &rhs)避免p3 = p3时先释放自身资源 [6]。- 虚析构函数:
- 若
Person是基类,声明virtual ~Person()确保派生类对象被基类指针删除时调用完整析构链 [7]。
特殊函数是资源安全的基石,尤其在涉及动态内存或继承时需显式定义 [6][7][15]。
封装(Encapsulation)¶
封装(Encapsulation)是面向对象编程(OOP)的核心概念之一,它将数据(属性)和操作数据的方法(行为)捆绑在一个单元(类)中,并控制对内部数据的访问权限,以此实现数据保护与接口隔离。以下是关键知识点及代码实例:
核心知识点¶
- 访问控制权限(引用5):
private:仅类内成员函数可访问,外部和派生类不可直接访问。protected:类内和派生类可访问,外部不可访问。-
public:完全开放访问。class BankAccount { private: // 数据隐藏 double balance; public: // 对外接口 void deposit(double amount) { if (amount > 0) balance += amount; } double getBalance() const { return balance; } }; -
数据隐藏(引用5):
- 私有成员变量(如
balance)只能通过公有方法(如deposit())间接修改,避免直接暴露实现细节。 -
防止外部代码意外修改内部状态(如负存款)。
-
接口与实现分离(引用4 & 8):
- 公有方法提供稳定接口,内部实现可独立修改而不影响调用方。
-
例如修改
balance存储方式(如改用整数存储分),调用方无感知。 -
增强安全性与可维护性(引用4):
- 封装后,数据校验(如
deposit()检查正值)集中在类内完成,避免分散的校验逻辑。 - 修改内部逻辑时无需修改外部调用代码。
代码实例¶
#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;
}
关键解析:¶
- 数据保护:
name、age、gpa声明为private,阻止外部直接修改(如s.age = -5非法)。-
通过
setGpa()方法修改时自动校验有效性(如4.5无效) [4][5]。 -
安全访问:
-
通过
getName()、getAge()等公有方法安全读取数据,getAge()中嵌入逻辑校验(如负年龄返回0) [4]。 -
接口稳定性:
- 若需修改
gpa存储方式(如改为百分制),只需调整setGpa()和display()内部实现,main()无需改动 [8]。
封装的优点¶
- 数据安全:避免非法操作(如负存款) [4]。
- 代码解耦:内部实现可独立优化(如数据存储优化) [5]。
- 易于维护:修改集中在类内,减少错误扩散风险 [8]。
- 接口清晰:使用者只需关注公有方法,无需理解复杂实现 [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]
关键区别与注意事项¶
- 成员函数 vs 全局函数:
=,[],->,()必须为成员函数 [1][9]。- 流运算符
<</>>通常为全局函数(需访问私有成员时用friend)。 - 返回值设计:
- 赋值类运算符(如
+=,=)返回*this的引用以支持链式操作 [1][9]。 - 算术运算符(如
+)返回新对象(避免修改操作数)。 - 参数传递:
- 非修改操作符(如
+)使用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)¶
重载 new 和 delete 与普通运算符不同:
- 它们通常是作为 静态成员(static) 执行的,即使你没有显式写
static。 - 当你重载了
new,通常也必须配套重载delete,以确保内存管理逻辑的一致性。
5. 必须返回引用的特例¶
为了支持“连续赋值”(如 a = b = c;),某些运算符的返回类型有不成文但极其重要的约定:
- 赋值运算符 (
=) 和 复合赋值运算符 (+=,-=):通常应该返回*this的引用(即T&)。 - 如果你返回的是
void或对象副本,虽然有时能编译通过,但会导致无法进行连续操作。
总结表:运算符重载禁区¶
| 类别 | 运算符 | 注意事项 |
|---|---|---|
| 绝对不可重载 | :: , . , .* , ?: , sizeof |
语言基础逻辑,严禁触动 |
| 必须是成员函数 | = , [] , () , -> |
防止语义混乱,不可作为友元 |
| 建议避免重载 | && , ` |
Copy Constructor¶
拷贝构造函数(Copy Constructor)详解与代码实例¶
拷贝构造函数是一种特殊的构造函数,用于通过已有对象创建新对象时执行深拷贝(避免指针等资源的共享)。以下为核心知识点及代码示例,结合引用材料解析。
1. 定义与调用时机¶
- 作用:初始化新对象为同类型另一对象的副本[1][8.1]。
- 触发场景:
注意:C++17起部分场景强制优化(copy elision),如函数返回纯右值时跳过拷贝构造[2][COPY ELISION]。
Person p1("Alice"); Person p2 = p1; // 显式调用拷贝构造 Person p3(p1); // 显式调用拷贝构造 void foo(Person p); foo(p1); // 传参时隐式调用拷贝构造 return p1; // 函数返回时可能调用(C++17前)
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()
*/
p2 的 name 指向独立内存,避免与 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_cast 与 dynamic_cast 详解与代码实例¶
1. static_cast¶
- 作用:编译时类型转换,安全性较高[1]。
- 适用场景:
- 基础类型转换(如
double→int)。 - 类层次转换(向上转型,派生类→基类)。
- 禁止无关指针转换或移除
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 | ✗ 失败 |
关键要点¶
- 必须有虚函数(多态类型)才能使用
dynamic_cast - 指针转换失败返回
nullptr - 引用转换失败抛出
std::bad_cast异常 - 转换是否成功取决于对象的实际运行时类型,而非声明类型
- 使用前最好检查转换结果(指针检查 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_ptr 和 std::weak_ptr区别¶
在 C++ 中,std::shared_ptr 和 std::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); // 推荐
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. 关键注意事项¶
-
循环引用的风险: 当
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 引用计数=2a1和a2计数均保持为 1,内存泄漏[1][3][6]。 解决方案:将其中一个shared_ptr替换为weak_ptr。 -
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_cast、dynamic_cast 和 reinterpret_cast 的详细讲解及代码实例:
1. static_cast¶
核心特性:
- 编译时完成:不进行运行时类型检查,效率高但可能存在不安全转换[1][5]。
- 允许的转换:
- 基本数据类型转换(如 double → int)。
- 类层次中的向上转换(派生类→基类,隐式或显式)。
- 类层次中的向下转换(基类→派生类,但不安全,编译器不检查实际对象类型)[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_cast和dynamic_cast;避免reinterpret_cast,除非处理底层系统编程[1][5]。
Iterator¶
迭代器详解与实例¶
迭代器是 C++ STL 的核心组件,用于 统一访问容器元素,充当容器与算法间的桥梁[1][2][3]。其核心机制是模拟指针行为(如 ++、*、!=),支持遍历容器元素而不暴露底层实现。
1. 迭代器的核心概念¶
- 作用:
- 遍历容器元素(如数组、链表、映射表)[1][3]。
- 作为算法(如
sort、find)的操作范围([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. 注意事项与陷阱¶
- 迭代器失效:
-
修改容器(如
erase)可能使迭代器失效[2][3]。list<int> L = {1, 2, 3}; auto it = L.begin(); L.erase(it); // it 失效 it = L.erase(it); // 正确: 接收 erase 返回的新迭代器[2] -
map的隐式插入: -
用
[]访问不存在的键会插入新元素[3][6]:map<string, int> m; cout << m["new_key"]; // 隐式插入 ("new_key", 0) // 正确做法: 先用 contains() 检查 (C++20) if (m.contains("key")) /*...*/; -
list::size()性能 : - C++11 前可能是 O(n),优先用
empty()[2]。
总结¶
- 迭代器类型:根据容器特性选择(如
vector→ 随机访问;list→ 双向)[1][3]。 - 核心操作:
begin()/end()定义范围,++/*/!=实现遍历[1]。 - 适配器:
back_inserter、ostream_iterator等扩展功能[2]。 - 安全准则:警惕迭代器失效和隐式插入[2][3][6]。
最佳实践:优先用 范围
for循环 和auto推导 简化代码;复杂操作搭配 STL 算法(如sort、copy)[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