Skip to content

Commit

Permalink
Merge pull request #45 from alu234/master
Browse files Browse the repository at this point in the history
update: 6 articles
  • Loading branch information
youngyangyang04 authored Feb 6, 2024
2 parents a6050cc + 26dbbc1 commit 745d69d
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 0 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@
* [虚函数是否可以声明为static?](./problems/虚函数是否可以声明为static?.md)
* [如何使用gdb来定位C++程序中的死锁?](./problems/如何使用gdb来定位C++程序中的死锁?.md)
* [C++中常用的类优化技术有那些?](./problems/C++中常用的类优化技术有那些?.md)
* [C++的atomic<bool>代码底层是如何实现的?](./problems/C++的atomic-bool代码底层是如何实现的?.md)
* [原子变量的内存序是什么?](./problems/原子变量的内存序是什么?.md)
* [什么是左值?什么是右值?有什么不同?](./problems/什么是左值?什么是右值?有什么不同?.md)
* [什么是完美转发?](./problems/什么是完美转发?.md)
* [C++中四种cast的转换?](./problems/C++中四种cast的转换?.md)
* [内存池是什么?在C++中如何设计一个简单的内存池?](./problems/内存池是什么?在C++中如何设计一个简单的内存池?.md)

# 数据结构与算法

Expand Down
48 changes: 48 additions & 0 deletions problems/C++中四种cast的转换?.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@


1. **static_cast**`static_cast` 是用于编译时检测到的相关类型之间的转换,比如整型和浮点数、派生类和基类之间的指针或引用转换。它不能用于含有虚函数的多态基类指针或引用到派生类的转换,因为这需要运行时信息。

示例:

```
int i = 10;
float f = static_cast<float>(i); // 整型到浮点数的转换
```

2. **dynamic_cast**`dynamic_cast` 主要用于处理多态性。当涉及到继承体系中的向下转换(将基类的指针或引用转换为派生类类型)时,这个转换会检查转换的安全性。如果转换无效,对于指针,它会返回nullptr;对于引用,则抛出一个`std::bad_cast`异常。使用`dynamic_cast`需要运行时类型信息(RTTI),因此它有一定的性能代价。

示例:

```
Base* b = new Derived(); // 基类指针指向派生类对象
Derived* d = dynamic_cast<Derived*>(b); // 向下转型成功
if (d) {
// 转型有效,'d' 不是 nullptr
}
```

3. **const_cast**`const_cast` 用于移除或添加`const``volatile`属性。通常情况下,它被用于移除对象的常量性,允许修改原本被声明为`const`的变量。需要注意的是,去除一个本质上确实是常量的对象的`const`标记并进行修改可能会导致未定义行为。

示例:

```
const int ci = 10;
int& modifiable = const_cast<int&>(ci); // 移除常量性以便修改
modifiable = 20; // 注意:如果原对象真的是const,这里可能是未定义行为
```

4. **reinterpret_cast**`reinterpret_cast` 是最危险的cast,它能够执行低级的强制类型转换。尽管几乎没有任何语义检查,但它能够在几乎任意两种类型之间转换,例如整数与指针之间的转换。由于它的不安全性,应该尽可能避免使用`reinterpret_cast`,除非你完全理解所进行的转换。

示例:

```
int* p = new int(65);
char* ch = reinterpret_cast<char*>(p); // 强制指针类型转换
```

总结:

- `static_cast`在相关类型间做安全转换。
- `dynamic_cast`在类层次结构中转换,并支持运行时检查。
- `const_cast`改变类型的`const``volatile`限定。
- `reinterpret_cast`进行低级别、不安全的强制类型转换。
20 changes: 20 additions & 0 deletions problems/C++的atomic-bool代码底层是如何实现的?.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
在C++中,`std::atomic` 是一个模板类,用于提供对基础类型的原子操作。`std::atomic<bool>` 是该模板类针对布尔类型的特化。原子操作保证了即使在多线程环境中,每个操作也是不可分割的,从而避免了竞态条件。

`std::atomic<bool>` 底层实现通常依赖于硬件和编译器的支持来提供原子性保证。具体实现可能涉及以下几个方面:

1. **内存屏障**(Memory Barriers/Fences): 内存屏障用于确保指令的执行顺序,阻止编译器或者处理器重排操作顺序。
2. **锁前缀指令**(Lock Prefix): 在x86架构中,处理器提供了带有`LOCK`前缀的指令,比如`LOCK XCHG`,它可以将操作变为原子性的。当CPU执行带有`LOCK`前缀的指令时,会确保指令完整执行,期间不会被其他处理器打断。
3. **特殊的原子指令**: 现代处理器提供了一系列原子指令,比如`XADD`(交换并加),`CMPXCHG`(比较并交换)等,可以用来实现原子变量。对于布尔变量,可能会使用这些指令来实现无锁的原子读写操作。
4. **编译器内建函数**(Compiler Intrinsics): 编译器可能提供特殊的内建函数来映射到底层的原子指令。

例如,在GCC和Clang上,通常会使用GCC的内建函数来实现原子操作。例如:

```
bool old_value = __atomic_fetch_and(&my_atomic_bool, true, __ATOMIC_SEQ_CST);
```

这里的 `__atomic_fetch_and` 函数是GCC提供的内建函数,用于执行原子AND操作,并返回变量的旧值。第三个参数 `__ATOMIC_SEQ_CST` 表示使用最严格的内存顺序:Sequentially Consistent。

对于不支持原子指令的数据类型或复杂操作,可能需要使用锁(比如互斥锁)来保证操作的原子性。但由于 `bool` 类型非常简单,大多数平台都能够提供无锁的原子操作支持。

不同的平台和编译器可能有不同的实现方式,因此没有一个统一的实现细节。如果你想知道具体的实现,可以查看特定编译器的源代码或者汇编输出。
31 changes: 31 additions & 0 deletions problems/什么是完美转发?.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
完美转发是一个与模板和函数重载相关的概念,它允许一个函数将其接收到的参数以原始的值类别(左值或右值)传递给另一个函数。这意味着**如果你传递了一个左值给包装函数,那么被调用的函数也会接收到一个左值;如果传递的是一个右值,则同样地,被调用的函数会接收到一个右值。**



```
#include <utility>
// 这个函数负责“转发”它的参数到另一个函数
template<typename T>
void wrapper(T&& arg) {
// 使用 std::forward 来确保 arg 的值类别得以保持不变
target(std::forward<T>(arg));
}
// 一个可能接受左值或右值参数的目标函数
void target(int& x) {
// 处理左值
}
void target(int&& x) {
// 处理右值
}
int main() {
int lv = 5; // 左值
wrapper(lv); // 应该调用 void target(int& x)
wrapper(10); // 应该调用 void target(int&& x)
}
```

14 changes: 14 additions & 0 deletions problems/什么是左值?什么是右值?有什么不同?.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
### 左值 (Lvalue):

一个**左值**表示表达式结束后依然存在的对象。它指向一个具体的内存位置,并且你可以取得其地址。通常情况下,左值表达式可能出现在赋值操作的左侧。

### 右值 (Rvalue):

一个**右值**通常是暂时的并且不会长时间存在,它不能被赋予另一个值。右值通常是直接的数据值或者无法通过标识符直接访问的临时对象。

**不同点:**

- **身份**: 左值具有明确的内存地址,而右值通常没有固定的内存地址。
- **持久性**: 左值代表长期存在的对象,右值代表临时或即将销毁的对象。
- **可移动性**: 右值可以被移动,而左值通常不能,除非显式地转换成右值引用。
- **引用类型**: 可以声明左值引用指向左值(`T&`),而右值引用(`T&&`)可以绑定到右值上,优化资源使用。
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
内存池是一种内存分配方式,它预先在内存中分配一定数量的块或对象,形成一个“池”。当程序需要分配内存时,它从这个池中分配一个块;当内存被释放时,这个块返回到池中以供再次使用。内存池可以显著减少频繁分配和释放内存所带来的开销,并且有助于避免内存碎片化,提高内存使用效率。

下面展示了如何设计一个简单的内存池,这个简单的内存池设计包括以下几个关键特性:

- **预分配**:在构造函数中预先分配了一定数量的固定大小内存块。
- **分配与释放**`allocate` 方法从池中分配一个内存块,而 `deallocate` 方法则将不再使用的内存块返还给池。
- **管理策略**:本例中使用 `std::list` 来管理空闲内存块,但实际应用中可能需考虑更高效的数据结构。

```
#include <iostream>
#include <list>
class MemoryPool {
public:
MemoryPool(size_t size, unsigned int count) {
for (unsigned int i = 0; i < count; ++i) {
freeBlocks.push_back(new char[size]);
}
blockSize = size;
}
~MemoryPool() {
for (auto block : freeBlocks) {
delete[] block;
}
}
void* allocate() {
if (freeBlocks.empty()) {
throw std::bad_alloc();
}
char* block = freeBlocks.front();
freeBlocks.pop_front();
return block;
}
void deallocate(void* block) {
freeBlocks.push_back(static_cast<char*>(block));
}
private:
std::list<char*> freeBlocks;
size_t blockSize;
};
// 使用示例
int main() {
const int blockSize = 32; // 块大小
const int blockCount = 10; // 块数量
MemoryPool pool(blockSize, blockCount);
// 分配内存
void* ptr1 = pool.allocate();
void* ptr2 = pool.allocate();
// 使用ptr1和ptr2...
// 释放内存
pool.deallocate(ptr1);
pool.deallocate(ptr2);
return 0;
}
```

10 changes: 10 additions & 0 deletions problems/原子变量的内存序是什么?.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
在C++11及之后的标准中,为了给开发者提供更细粒度的控制以及可能的性能优化空间,引入了多种内存顺序选项:

1. **memory_order_relaxed**:放松的内存顺序。不对执行顺序做任何保证,除了原子操作本身的原子性。这意味着,在没有其他同步操作手段的情况下,读写操作的顺序可能与程序代码中的顺序不一致。
2. **memory_order_consume**:较轻量级的保序需求,用于指定操作依赖于先前的某些操作结果。这在实际实现中通常被视为与`memory_order_acquire`相同。
3. **memory_order_acquire**:获取操作,禁止后续的读或写被重排到当前操作之前。用于读取操作。
4. **memory_order_release**:释放操作,防止之前的读或写操作被重排到当前操作之后。用于写入操作。
5. **memory_order_acq_rel**:同时包含获取和释放语义。适用于同时具有读取和写入特性的操作。
6. **memory_order_seq_cst**:顺序一致性内存顺序。所有线程看到的操作顺序一致。这是默认的内存顺序,并且提供了最强的顺序保证。

内存顺序的选择影响着程序的正确性和性能。较弱的内存顺序(例如`memory_order_relaxed`)可能带来更好的性能,因为它们允许更多的指令重排序;但是,使用它们也需要更小心地设计程序,以避免数据竞争和其他并发相关的错误。相反,`memory_order_seq_cst`提供了最简单和最直观的并发模型,但可能因为额外的同步代价而影响性能。

0 comments on commit 745d69d

Please sign in to comment.