文章目录
- C++面试:封装、继承、多态的具象示例
- 1. 封装 (Encapsulation)
- 2. 继承 (Inheritance)
- 3. 多态 (Polymorphism)
- 综合示例:封装、继承、多态
- C++模板编程的理解与应用场景
- 我对模板编程的理解
- C++中最常用的模板编程场景
- 1. STL (标准模板库)
- 2. 通用容器实现
- 3. 通用算法实现
- 4. 类型安全的接口
- 5. 策略模式实现
- 6. 元编程和编译期计算
- 7. CRTP (奇异递归模板模式)
- 8. 类型萃取(Type Traits)
- 模板编程的优势
- 总结
- 链表适用的场景
- 1. 频繁的插入和删除操作
- 2. 不确定数据量的动态存储
- 3. 不需要随机访问的场景
- 4. 内存碎片化严重的环境
- 5. 需要高效合并/拆分的场景
- 6. 实现高级数据结构
- 链表 vs 数组的对比
- 实际案例
- 何时选择链表
- 1. **Linux 管理进程队列**
- 2. **内存页管理(如 Buddy System 的链表)**
- 3. **堆内存管理(如 malloc 的 free list)**
- 4. **文件系统管理文件信息(如 inode 链表)**
- 5. **MySQL 索引的叶子节点链表(B+Tree 特性)**
- **链表的优势总结**
- **为什么不选数组或其他结构?**
C++面试:封装、继承、多态的具象示例
1. 封装 (Encapsulation)
封装是将数据和操作数据的方法绑定在一起,并对外隐藏实现细节的过程。
示例:
class BankAccount {
private: // 数据隐藏
std::string accountNumber;
double balance;
public: // 公开接口
BankAccount(const std::string& accNum, double initialBalance)
: accountNumber(accNum), balance(initialBalance) {}
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
bool withdraw(double amount) {
if (amount > 0 && balance >= amount) {
balance -= amount;
return true;
}
return false;
}
double getBalance() const {
return balance;
}
};
int main() {
BankAccount account("123456789", 1000.0);
account.deposit(500.0);
account.withdraw(200.0);
std::cout << "Current balance: " << account.getBalance() << std::endl;
// account.balance = 1000000.0; // 错误!balance是私有的
}
封装的好处:
- 保护数据不被外部随意修改
- 可以修改内部实现而不影响外部代码
- 提供清晰的接口供外部使用
2. 继承 (Inheritance)
继承允许我们基于已有的类创建新类,新类继承原有类的特性并可以添加新特性。
示例:
// 基类
class Animal {
protected:
std::string name;
int age;
public:
Animal(const std::string& name, int age) : name(name), age(age) {}
void eat() {
std::cout << name << " is eating." << std::endl;
}
void sleep() {
std::cout << name << " is sleeping." << std::endl;
}
};
// 派生类
class Dog : public Animal {
private:
std::string breed;
public:
Dog(const std::string& name, int age, const std::string& breed)
: Animal(name, age), breed(breed) {}
void bark() {
std::cout << name << " (a " << breed << ") is barking: Woof! Woof!" << std::endl;
}
void fetch() {
std::cout << name << " is fetching the ball." << std::endl;
}
};
int main() {
Dog myDog("Buddy", 3, "Golden Retriever");
myDog.eat(); // 继承自Animal
myDog.sleep(); // 继承自Animal
myDog.bark(); // Dog特有的方法
myDog.fetch(); // Dog特有的方法
}
继承的好处:
- 代码复用:派生类可以重用基类的代码
- 层次化组织:可以创建类的层次结构
- 可扩展性:可以在不修改基类的情况下添加新功能
3. 多态 (Polymorphism)
多态允许我们使用统一的接口处理不同类型的对象。
示例:
class Shape {
public:
virtual void draw() const = 0; // 纯虚函数
virtual ~Shape() {} // 虚析构函数
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
void draw() const override {
std::cout << "Drawing a circle with radius " << radius << std::endl;
}
};
class Square : public Shape {
private:
double side;
public:
Square(double s) : side(s) {}
void draw() const override {
std::cout << "Drawing a square with side " << side << std::endl;
}
};
void drawAllShapes(const std::vector<Shape*>& shapes) {
for (const auto& shape : shapes) {
shape->draw(); // 多态调用
}
}
int main() {
std::vector<Shape*> shapes;
shapes.push_back(new Circle(5.0));
shapes.push_back(new Square(4.0));
shapes.push_back(new Circle(3.0));
drawAllShapes(shapes);
// 清理内存
for (auto shape : shapes) {
delete shape;
}
}
多态的好处:
- 接口统一:可以用相同的方式处理不同的对象
- 可扩展性:可以添加新类而不需要修改现有代码
- 灵活性:运行时决定调用哪个方法
综合示例:封装、继承、多态
#include <iostream>
#include <vector>
#include <memory>
// 封装
class Employee {
private:
std::string name;
int id;
double salary;
public:
Employee(const std::string& n, int i, double s)
: name(n), id(i), salary(s) {}
virtual ~Employee() {}
virtual void work() const {
std::cout << name << " (ID: " << id << ") is working." << std::endl;
}
virtual double calculateBonus() const {
return salary * 0.1; // 默认10%的奖金
}
std::string getName() const { return name; }
int getId() const { return id; }
double getSalary() const { return salary; }
};
// 继承
class Manager : public Employee {
private:
std::string department;
public:
Manager(const std::string& n, int i, double s, const std::string& d)
: Employee(n, i, s), department(d) {}
void work() const override {
std::cout << getName() << " (Manager of " << department
<< ") is managing the team." << std::endl;
}
double calculateBonus() const override {
return getSalary() * 0.2; // 经理有20%的奖金
}
};
class Developer : public Employee {
private:
std::string programmingLanguage;
public:
Developer(const std::string& n, int i, double s, const std::string& lang)
: Employee(n, i, s), programmingLanguage(lang) {}
void work() const override {
std::cout << getName() << " is coding in "
<< programmingLanguage << "." << std::endl;
}
void debug() const {
std::cout << getName() << " is debugging "
<< programmingLanguage << " code." << std::endl;
}
};
// 多态
void processEmployees(const std::vector<std::unique_ptr<Employee>>& employees) {
for (const auto& emp : employees) {
emp->work(); // 多态调用
std::cout << emp->getName() << "'s bonus: $"
<< emp->calculateBonus() << std::endl;
// 尝试向下转型
if (auto dev = dynamic_cast<Developer*>(emp.get())) {
dev->debug();
}
}
}
int main() {
std::vector<std::unique_ptr<Employee>> employees;
employees.emplace_back(std::make_unique<Manager>("Alice", 101, 80000, "Engineering"));
employees.emplace_back(std::make_unique<Developer>("Bob", 102, 70000, "C++"));
employees.emplace_back(std::make_unique<Developer>("Charlie", 103, 75000, "Python"));
processEmployees(employees);
return 0;
}
这个综合示例展示了:
- 封装:Employee类隐藏了内部数据,提供公共接口
- 继承:Manager和Developer继承自Employee,扩展了功能
- 多态:processEmployees函数可以处理不同类型的Employee对象
C++模板编程的理解与应用场景
我对模板编程的理解
模板编程(Template Programming)是C++中一种强大的泛型编程技术,它允许我们编写与数据类型无关的代码。通过模板,我们可以创建能够处理多种数据类型的函数或类,而无需为每种类型重复编写代码。
模板编程的核心思想是**“参数化类型”**,即在编写代码时不指定具体的数据类型,而是使用类型参数,在实际使用时再指定具体类型。
C++中最常用的模板编程场景
1. STL (标准模板库)
STL是C++模板编程最经典、最广泛的应用场景:
#include <vector>
#include <list>
#include <map>
#include <algorithm>
// 这些容器和算法都是模板
std::vector<int> intVec; // 整数向量
std::list<std::string> strList; // 字符串链表
std::map<int, double> idMap; // 整数到浮点数的映射
// 模板算法
std::sort(intVec.begin(), intVec.end());
auto it = std::find(strList.begin(), strList.end(), "template");
2. 通用容器实现
当需要实现可以存储任意类型数据的容器时:
template <typename T>
class MyArray {
private:
T* data;
size_t size;
public:
MyArray(size_t s) : size(s), data(new T[s]) {}
~MyArray() { delete[] data; }
T& operator[](size_t index) {
return data[index];
}
// ... 其他成员函数
};
// 使用
MyArray<int> intArray(10);
MyArray<std::string> strArray(5);
3. 通用算法实现
编写适用于多种数据类型的算法:
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
// 使用
int m1 = max(3, 5); // int
double m2 = max(3.14, 2.71); // double
std::string m3 = max("apple", "zoo"); // std::string
4. 类型安全的接口
创建类型安全且灵活的接口:
template <typename T>
class Singleton {
private:
static T* instance;
Singleton() = default;
public:
static T& getInstance() {
if (!instance) {
instance = new T();
}
return *instance;
}
};
template <typename T>
T* Singleton<T>::instance = nullptr;
// 使用
class Logger : public Singleton<Logger> {
friend class Singleton<Logger>;
private:
Logger() = default;
public:
void log(const std::string& message) {
// 实现日志功能
}
};
Logger::getInstance().log("Template example");
5. 策略模式实现
通过模板实现编译期策略选择:
// 排序策略
struct BubbleSort {
template <typename Iter>
static void sort(Iter begin, Iter end) {
// 冒泡排序实现
}
};
struct QuickSort {
template <typename Iter>
static void sort(Iter begin, Iter end) {
// 快速排序实现
}
};
// 排序器模板
template <typename SortStrategy>
class Sorter {
public:
template <typename Iter>
void operator()(Iter begin, Iter end) {
SortStrategy::sort(begin, end);
}
};
// 使用
std::vector<int> data = {5, 2, 9, 1, 5};
Sorter<QuickSort>()(data.begin(), data.end());
6. 元编程和编译期计算
利用模板进行编译期计算和类型操作:
// 编译期阶乘计算
template <unsigned n>
struct Factorial {
static const unsigned value = n * Factorial<n-1>::value;
};
template <>
struct Factorial<0> {
static const unsigned value = 1;
};
// 使用
constexpr unsigned fact5 = Factorial<5>::value; // 120
7. CRTP (奇异递归模板模式)
用于静态多态和代码复用:
template <typename Derived>
class Base {
public:
void interface() {
static_cast<Derived*>(this)->implementation();
}
};
class Derived : public Base<Derived> {
public:
void implementation() {
std::cout << "Derived implementation" << std::endl;
}
};
// 使用
Derived d;
d.interface(); // 输出 "Derived implementation"
8. 类型萃取(Type Traits)
在模板中检查和操作类型信息:
#include <type_traits>
template <typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
std::cout << "Processing integer: " << value << std::endl;
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "Processing float: " << value << std::endl;
} else {
std::cout << "Processing unknown type" << std::endl;
}
}
// 使用
process(42); // 处理整数
process(3.14); // 处理浮点数
process("hello"); // 处理未知类型
模板编程的优势
- 代码复用:编写一次,适用于多种类型
- 类型安全:编译时类型检查,避免运行时错误
- 性能:没有运行时开销,所有工作在编译期完成
- 灵活性:可以创建高度通用的库和组件
- 可扩展性:易于添加对新类型的支持
总结
C++中模板编程最常见的应用场景包括STL容器和算法、通用数据结构和算法实现、类型安全接口设计、策略模式实现、编译期计算、静态多态以及类型萃取等。模板编程是C++强大表达能力的核心之一,也是现代C++编程不可或缺的部分。
链表适用的场景
链表是一种基础的数据结构,在以下场景中特别适用:
- 频繁的中间插入和删除操作
- linux管理进程各种队列,内存页,堆内存链表,文件信息链表,mysql的索引叶子结点链表
- 实现特定的数据结构:lru,哈希表链地址法,图邻接表,跳表,
1. 频繁的插入和删除操作
当需要频繁在数据集合中间进行插入或删除操作时,链表比数组更有优势。
典型场景:
- 实现撤销(Undo)功能(每次操作都插入链表头部)
- 浏览器历史记录(前进/后退)
- 进程调度队列(经常需要添加和移除进程)
// 例如实现撤销功能
struct Command {
virtual void execute() = 0;
virtual void undo() = 0;
};
std::list<std::shared_ptr<Command>> undoStack;
// 执行新命令时
auto cmd = std::make_shared<SomeCommand>();
cmd->execute();
undoStack.push_front(cmd); // O(1)时间复杂度
// 撤销时
if (!undoStack.empty()) {
undoStack.front()->undo();
undoStack.pop_front(); // O(1)时间复杂度
}
2. 不确定数据量的动态存储
当数据量未知或变化很大时,链表可以动态增长而不需要预先分配空间。
典型场景:
- 读取未知长度的数据流
- 内存受限环境中的动态分配
- 实现其他数据结构(如栈、队列、图等)
// 读取未知长度的输入
std::list<std::string> inputLines;
std::string line;
while (std::getline(std::cin, line)) {
inputLines.push_back(line);
}
3. 不需要随机访问的场景
当主要操作是顺序访问而非随机访问时,链表很合适。
典型场景:
- 音乐播放器的播放列表
- 文件系统的目录结构
- 多道程序环境下的I/O缓冲区
// 音乐播放列表
class Song {
std::string title;
std::string artist;
// ...
};
std::list<Song> playlist;
// 顺序播放
for (const auto& song : playlist) {
playSong(song);
}
4. 内存碎片化严重的环境
在内存受限或碎片化严重的环境中,链表比数组更有优势,因为它不需要连续内存空间。
典型场景:
- 嵌入式系统
- 操作系统内核数据结构
- 内存分配器实现
5. 需要高效合并/拆分的场景
当需要频繁合并两个集合或拆分集合时,链表可以在O(1)时间内完成。
典型场景:
- 归并排序的实现
- 数据库中的多表连接操作
- 消息队列的合并
// 合并两个链表
std::list<int> list1 = {1, 2, 3};
std::list<int> list2 = {4, 5, 6};
list1.splice(list1.end(), list2); // O(1)操作
// list1现在包含1,2,3,4,5,6
6. 实现高级数据结构
许多高级数据结构基于链表实现:
典型场景:
- 哈希表的链地址法解决冲突
- 图的邻接表表示
- 跳表的基础结构
- 二叉树的某些表示方法
// 哈希表的链地址法实现
template <typename K, typename V>
class HashMap {
private:
std::vector<std::list<std::pair<K, V>>> buckets;
// ...
public:
void insert(const K& key, const V& value) {
size_t index = hash(key) % buckets.size();
for (auto& pair : buckets[index]) {
if (pair.first == key) {
pair.second = value;
return;
}
}
buckets[index].emplace_back(key, value);
}
// ...
};
链表 vs 数组的对比
场景/操作 | 链表优势 | 数组优势 |
---|---|---|
频繁插入删除 | ✅ O(1) | ❌ O(n) |
随机访问 | ❌ O(n) | ✅ O(1) |
内存使用 | 动态分配 | 局部性好 |
缓存友好度 | ❌ 差 | ✅ 好 |
内存碎片化环境 | ✅ 适应 | ❌ 不适应 |
预知大小、少变动的数据 | ❌ | ✅ |
实际案例
- Linux内核:使用链表管理进程、内存页等
- Redis:使用双向链表实现List数据类型
- Nginx:使用链表管理连接、请求等
- 游戏开发:场景中的动态对象管理
- 区块链:区块链本身就是一种链表结构
何时选择链表
选择链表而非数组的主要考虑因素是:
- 插入/删除频率高于随机访问频率
- 数据量变化大或不可预测
- 内存连续性无法保证
- 需要频繁合并/拆分数据结构
- 实现特定的算法需求(如LRU缓存)
在C++中,std::list
(双向链表)和std::forward_list
(单向链表)是标准库提供的链表实现,应根据具体需求选择合适的类型。
这些场景选择链表(尤其是双向链表)作为数据结构,主要基于链表的以下特性与场景需求的匹配:
1. Linux 管理进程队列
- 动态增删频繁:进程的创建、终止、调度等操作需要频繁插入/删除节点。链表在 O ( 1 ) O(1) O(1) 时间内即可完成这些操作,而数组需要移动元素。
- 不确定性数量:进程数量动态变化,链表无需预分配固定空间,避免内存浪费或扩容开销。
- 双向遍历需求:Linux 使用双向链表(如
struct list_head
),便于从任意节点向前或向后遍历(例如查找相邻进程或优先级调整)。
2. 内存页管理(如 Buddy System 的链表)
- 碎片化处理:内存页需按不同大小(如 4KB、8KB)组织成多个链表。链表可灵活管理非连续的空闲页块。
- 快速分配/释放:分配内存时从对应大小的链表中移除节点;释放时快速插入,均无需移动其他元素。
- 合并操作:双向链表便于合并相邻空闲页块(通过前后指针检查相邻页状态)。
3. 堆内存管理(如 malloc 的 free list)
- 变长内存块:堆内存块大小不一,链表可串联不同大小的空闲块,通过指针连接,无需连续内存。
- 高效合并:释放内存时,双向链表能快速检查前后块是否空闲,合并减少碎片。
4. 文件系统管理文件信息(如 inode 链表)
- 动态文件数量:文件创建/删除导致 inode 数量变化,链表支持动态扩展。
- 快速查找与遍历:结合哈希表或树,链表用于维护同一哈希桶或目录下的文件序列,便于线性访问。
5. MySQL 索引的叶子节点链表(B+Tree 特性)
- 范围查询优化:B+Tree 的叶子节点通过链表串联,使范围查询(如
WHERE id BETWEEN 10 AND 100
)只需定位起始节点后顺链遍历,无需回溯父节点。 - 磁盘 I/O 优化:链表结构适合磁盘预读(顺序访问性能接近数组),而插入/删除节点仅需修改相邻节点的指针,避免大量数据移动。
链表的优势总结
- 插入/删除高效:尤其适合频繁动态修改的场景。
- 无预分配限制:适应数据规模不确定的情况。
- 双向链表支持逆向操作:如 Linux 进程回滚、内存块合并等。
- 天然支持非连续存储:适合内存碎片化或磁盘分散存储的场景。
为什么不选数组或其他结构?
- 数组:插入/删除需移动元素,扩容成本高;静态大小不灵活。
- 哈希表:无法高效支持范围查询(如 MySQL 需要链表辅助)。
- 普通树结构:维护复杂,且范围查询不如链表直接。
在这些场景中,链表(尤其是双向链表)在动态性、操作效率和空间利用率上达到了最佳平衡。