多态
在C++中,多态通常是通过基类和派生类的关系以及虚函数来实现的。多态性允许派生类的对象被视作其基类的实例,让基类的指针或引用可以指向派生类的对象,并且通过基类接口调用派生类的方法。这种特性让代码更加灵活和可扩展。
C++中的多态分为两类:
静态多态(编译时多态):通过函数重载和模板(泛型编程)实现。这是在编译时解析的,不涉及运行时类型信息。
动态多态(运行时多态):主要通过虚函数实现,涉及运行时类型识别和动态绑定。
虚函数
红黑树
红黑树是一种自平衡的二叉搜索树(BST),它在插入和删除操作中保持大致的平衡,以确保最坏情况下基本操作(如查找、插入和删除)的时间复杂度保持在O(log n)。在许多语言的标准库中(如C++的STL中的`map`、`multimap`、`set`和`multiset`),红黑树提供了底层的数据结构实现。
红黑树通过对任何一条从根到叶子的路径上各个节点的颜色进行约束来确保平衡。每个节点都被染成红色或黑色,并遵循以下红黑属性:
1. 节点颜色:每个节点要么是红的,要么是黑的。
2. 根节点:根节点总是黑色的。
3. 红色节点规则:红色节点的两个子节点都是黑色的(也就是说,两个红色节点不能连续出现)。
4. 每条路径上黑色节点的数目相同:从任一节点到其每个叶子的所有简单路径上,黑色节点的数量都相同。
5. 新插入节点为红色:新插入的节点为红色(在多数情况下,插入的红色节点不会违反红黑树的性质,但是在某些情况下,可能需要对树进行一系列颜色改变和树结构调整)。
6. 叶子节点:所有叶子节点(NIL节点、空节点)都是黑色的。
为了维护这些属性,红黑树在插入和删除节点时可能需要通过以下一种或多种操作来调整:
- 颜色改变:改变某个节点的颜色。
- 左旋转:对某个节点进行左旋转,即该节点成为其右子节点的左子节点,而原右子节点的左子树成为该节点的右子树。
- 右旋转:对某个节点进行右旋转,即该节点成为其左子节点的右子节点,而原左子节点的右子树成为该节点的左子树。
通过这些操作,红黑树在每次插入或删除后都能迅速恢复其平衡性质,从而保证了操作的高效性。
图片的话,这个博主图片很清晰,7张图带你了解红黑树变色、左旋和右旋 - 知乎 (zhihu.com)
vector/map 底层
在 C++ 标准模板库(STL)中,`vector` 和 `map` 是两种非常基础且常用的容器类型,它们的底层实现和特性各不相同。
vector 的底层实现:
`vector` 是一个动态数组,它能够存储连续的元素。`vector` 的底层实质是一个能够自动扩展大小的数组。当新的元素被添加到 `vector` 并且当前的存储空间不足以容纳时,`vector` 会:
1. 分配一个更大的内存块。
2. 将所有现有的元素从当前内存块复制到新的内存块。
3. 释放旧的内存块。
4. 插入新的元素。
这允许 `vector` 提供快速的随机访问(即通过索引快速访问任何元素),但插入和删除元素(特别是在 `vector` 的开始或中间)可能较慢,因为这可能涉及到复制整个数组到新的内存位置。
map 的底层实现:
`map` 是一个基于键值对的关联容器,它提供了基于键的快速检索能力。在 C++ STL 中,`map` 通常是使用红黑树实现的。红黑树是一种自平衡的二叉搜索树,它保证了树的高度大约是 log(n),从而确保操作(如查找、插入、删除)的时间复杂度为 O(log(n))。
`map` 中的每个元素都是一个包含键和值的对(`pair`),并且所有的键都是唯一的。红黑树的特性保证了元素按照键的排序顺序存储,使得基于范围的迭代变得高效。
在 C++11 之后,引入了 `unordered_map`,它通常使用哈希表实现,提供了平均情况下 O(1) 的访问时间复杂度。然而,由于哈希冲突的存在,最坏情况下的时间复杂度可能退化到 O(n)。`unordered_map` 中的元素不是有序的。
总结一下:
- `vector` 提供了快速的随机访问和尾部插入/删除操作,但可能在大量元素的中间或开始插入/删除时效率低下。
- `map` 提供了基于键的快速查找,并保持元素有序,但在插入和删除方面通常比 `vector` 慢,因为它需要维护树的平衡。
智能指针
在 C++ 中,智能指针是一类模板类,它们提供了自动的对象生命周期管理,帮助防止内存泄漏和悬挂指针问题。C++11 标准引入了几种智能指针,主要包括 `std::unique_ptr`、`std::shared_ptr` 和 `std::weak_ptr`。
std::unique_ptr
`unique_ptr` 是一种独占的智能指针,它保证同一时间内只有一个智能指针实例可以指向一个给定的对象。当 `unique_ptr` 被销毁(例如,离开其作用域)时,它所指向的对象也会被自动销毁。`unique_ptr` 不能被复制,确保其独占性,但它可以被移动,以转移其所有权。它通常用于表示对对象的独占拥有权。
std::unique_ptr<int> ptr(new int(10)); // ptr 现在拥有一个 int 实例
// std::unique_ptr<int> ptr2 = ptr; // 错误:不能复制 unique_ptr
std::unique_ptr<int> ptr2 = std::move(ptr); // 正确:ptr2 现在拥有该 int 实例,ptr 变为空
这里 `std::move`的作用如下:
`std::move` 是 C++11 引入的一个函数模板,它可以将其参数转换为右值引用,从而允许资源的移动语义而非复制。当你对一个对象使用 `std::move`,你实际上是告诉编译器你打算将这个对象的资源转移给另一个对象。这个转换并不是类型转换,而是将对象的状态从潜在的左值(具有持久状态)转换为右值(临时或可移动状态)。
对于智能指针而言,当你使用 `std::move`,你确实是在将智能指针转换为右值引用。这使得智能指针的所有权可以从一个对象转移到另一个对象,这就是所谓的"移动语义"。
std::unique_ptr<int> ptr1(new int(10));
// std::unique_ptr<int> ptr2 = ptr1; // 这是错误的,因为 unique_ptr 不能被复制
std::unique_ptr<int> ptr2 = std::move(ptr1); // 这是正确的,ptr1 的所有权被移动到 ptr2
在上面的代码中,`std::move(ptr1)` 将 `ptr1` 转换为 `std::unique_ptr<int>&&` 类型(右值引用),这允许 `ptr2` 的构造函数接管 `ptr1` 指向的内存的所有权,随后 `ptr1` 将为空。这里 `std::move` 的作用在于启用移动构造或移动赋值,而不是进行任何实际的内存移动操作。
std::shared_ptr
`shared_ptr` 是一个引用计数的智能指针,它允许多个 `shared_ptr` 实例指向同一个对象。内部的引用计数会跟踪有多少个 `shared_ptr` 拥有同一个对象,一旦最后一个这样的 `shared_ptr` 被销毁,对象就会被自动删除。`shared_ptr` 适用于表示对象的共享所有权。
std::shared_ptr<int> ptr1(new int(10)); // 引用计数现在是 1
std::shared_ptr<int> ptr2 = ptr1; // 引用计数增加到 2
// ptr1 和 ptr2 都销毁后,对象会被自动删除
std::weak_ptr
`weak_ptr` 与 `shared_ptr` 配合使用,提供对 `shared_ptr` 所管理对象的非拥有(弱)引用。`weak_ptr` 不增加对象的引用计数,因此它不会阻止其所指向的对象被销毁。当你需要一个指向 `shared_ptr` 管理的对象的指针,但不需要拥有该对象时,`weak_ptr` 是一个很好的选择。这可以避免 `shared_ptr` 之间的循环引用,这些循环引用可能导致内存泄漏。
std::shared_ptr<int> sharedPtr(new int(10));
std::weak_ptr<int> weakPtr(sharedPtr); // weakPtr 指向 sharedPtr 所管理的对象
// 在你需要使用对象时,从 weakPtr 创建一个 shared_ptr
std::shared_ptr<int> tempSharedPtr = weakPtr.lock();
if (tempSharedPtr) {
// 使用 tempSharedPtr 访问对象
}
内存泄漏
中序遍历二叉树
#include <iostream>
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr)};
void inorderTraversal(TreeNode* root) {
if (root == nullptr) {
return;
}
inorderTraversal(root->left); // 遍历左子树
std::cout << root->val << " "; // 访问当前节点
inorderTraversal(root->right); // 遍历右子树
}
int main() {
// 创建一个简单的树来测试
// 1
// / \
// 2 3
// / \ /
// 4 5 6
TreeNode *root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
root->left->left = new TreeNode(4);
root->left->right = new TreeNode(5);
root->right->left = new TreeNode(6);
inorderTraversal(root); // 输出应该是 4 2 5 1 6 3
return 0;
}
反转单链表
#include <iostream>
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr){}
};
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* current = head;
ListNode* next = nullptr;
while (current != nullptr) {
next = current->next; // 保存下一个节点
current->next = prev; // 反转当前节点的指针方向
prev = current; // 移动prev指针
current = next; // 移动current指针
}
return prev; // prev将会是反转后的头节点
}
// 打印链表
void printList(ListNode* head) {
ListNode* current = head;
while (current != nullptr) {
std::cout << current->val << " ";
current = current->next;
}
std::cout << std::endl;
}
int main() {
// 创建链表 1 -> 2 -> 3 -> 4 -> 5
ListNode* head = new ListNode(1);
head->next = new ListNode(2);
head->next->next = new ListNode(3);
head->next->next->next = new ListNode(4);
head->next->next->next->next = new ListNode(5);
std::cout << "Original List: ";
printList(head);
// 反转链表
head = reverseList(head);
std::cout << "Reversed List: ";
printList(head);
// 清理动态分配的内存
ListNode* current = head;
while (current != nullptr) {
ListNode* next = current->next;
delete current;
current = next;
}
return 0;
}
这个函数通过迭代方式实现了单链表的反转。它使用了三个指针:
- `prev` 指向已经反转部分的最后一个节点,初始时指向`nullptr`,
- `current` 指向还未反转部分的第一个节点,初始时指向`head`,
- `next` 用来临时存储`current`的下一个节点。
在每次循环中,当前节点的`next`指针被更新为指向`prev`,然后三个指针都向前移动一位。当`current`变为`nullptr`时,就意味着链表已经完全反转,此时`prev`即为新的头节点。
指针和引用的区别
在 C++ 中,指针(Pointer)和引用(Reference)都是间接访问其他变量的方式,但它们之间有几个关键的区别:
区别分类 | 指针 | 引用 |
基本定义 | 是一个变量,其值为另一个变量的地址。指针需要被声明为某种特定类型的指针,并且可以指向那种类型的数据。 | 是某个已存在变量的另一个名字(别名),用于对该变量进行操作。 |
语法 | 使用`*`标记,如`int* ptr;`,使用`&`取得变量地址赋值给指针,如`ptr = &var;`,使用`*`来访问指针指向的值,如`*ptr = 5;`。 | 使用`&`标记,如`int& ref = var;`,引用必须在声明时就初始化,并且不能更改为引用其他变量。 |
空值 | 可以指向`nullptr`(即空指针,不指向任何对象)。 | 必须连接到一块合法的内存,一旦引用被初始化为某个变量,就不能再改变为引用其他变量,因此不存在“空引用”。 |
内存地址 | 持有一个内存地址,这个地址是指向另一个变量的。 | 不持有内存地址,它是别名,但在实现层面可能会使用指针。 |
可变性 | 可以改变所指向的变量,也可以改变指向另一个变量。 | 不能重新赋值引用其他的变量。 |
自身地址 | 作为一个实体,它自身也有内存地址。 | 没有自己的内存地址,因为它只是一个已经存在的变量的别名。 |
操作符重载和参数传递 | 可以进行更多的操作,比如自增,自减,加法,减法等。 | 通常用于参数传递和操作符重载,它们使得函数调用和操作符看起来像是对普通变量的操作一样。 |
重新赋值 | 可以在指针声明后重新指向另一个地址。 | 一旦初始化之后就无法改变引用的对象。 |
数组 | 可以指向数组的开头,可以通过递增指针来遍历数组。 | 不能指向数组,但可以引用数组的元素。 |