TLS(线程本地存储)原理
线程本地存储(Thread Local Storage,TLS)是一种机制,它允许每个线程拥有自己独立的变量实例,这些变量的生命周期与线程相同。也就是说,不同线程对同一个 TLS 变量的访问,实际上是在访问各自独立的副本,彼此之间互不干扰。
实现方式
- 静态 TLS:在编译时就为每个线程分配 TLS 变量的存储空间。编译器会在可执行文件中预留相应的空间,当线程启动时,操作系统会为每个线程初始化这些 TLS 变量。在 C++ 中,可以使用
__declspec(thread)
(Windows)或__thread
(GCC、Clang)关键字来声明静态 TLS 变量。例如:
// 使用 __thread 声明静态 TLS 变量
__thread int tls_variable = 0;
- 动态 TLS:在运行时动态地为线程分配和管理 TLS 变量。操作系统提供了一系列的 API 来创建、访问和销毁动态 TLS 变量。在 C++ 中,可以使用
std::thread_local
关键字来声明动态 TLS 变量。例如:
// 使用 std::thread_local 声明动态 TLS 变量
thread_local int dynamic_tls_variable = 0;
工作原理
- 数据结构:操作系统会为每个线程维护一个 TLS 数据结构,这个数据结构通常是一个数组或链表,用于存储该线程的所有 TLS 变量。
- 索引机制:每个 TLS 变量都有一个唯一的索引,线程通过这个索引来访问自己的 TLS 变量。当线程访问一个 TLS 变量时,操作系统会根据线程 ID 和变量索引,从该线程的 TLS 数据结构中找到对应的变量副本。
- 线程创建和销毁:当一个新线程创建时,操作系统会为该线程分配一个新的 TLS 数据结构,并将所有 TLS 变量初始化为默认值。当线程销毁时,操作系统会释放该线程的 TLS 数据结构。
如何实现一个无锁队列?
无锁队列是一种在多线程环境下不使用锁(如互斥锁)来实现线程安全的队列数据结构。无锁队列通常使用原子操作和内存屏障来保证多线程操作的正确性和一致性。下面将介绍如何使用 C++ 实现一个简单的无锁队列,这里采用单生产者单消费者(SPSC)的无锁队列作为示例。
实现思路
- 使用原子操作来更新队列的头指针和尾指针,避免使用锁带来的性能开销。
- 采用循环数组作为队列的底层存储结构。
- 通过比较和交换(CAS)操作来确保在多线程环境下对头指针和尾指针的更新是原子的。
仿函数的优势
#include <iostream>
#include <vector>
#include <algorithm>
// 定义一个比较仿函数
class Greater {
public:
bool operator()(int a, int b) const {
return a > b;
}
};
int main() {
std::vector<int> numbers = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
// 使用仿函数作为排序规则
std::sort(numbers.begin(), numbers.end(), Greater());
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
- 可状态化:和普通函数不同,仿函数可以拥有自己的状态。也就是说,仿函数类可以有成员变量,这些成员变量能记录仿函数的状态。
- 可作为模板参数:仿函数可以作为模板参数,在模板编程里能发挥很大作用。例如,
std::sort
函数就可以使用仿函数来定义排序规则。
野指针(Wild Pointer)
- 定义:指向无效内存地址的指针,访问会导致未定义行为(崩溃、数据损坏)。
- 常见场景:
- 未初始化的指针:未明确指向有效内存。
- 指向已释放的内存(即悬垂指针)。
- 指向超出作用域的局部变量。
深浅拷贝
c++中引用有没有深浅拷贝的问题
- 浅拷贝是指在拷贝对象时,仅复制对象的成员变量的值,而不复制成员变量所指向的资源。如果成员变量是指针、引用或其他对象的引用,那么浅拷贝后,源对象和目标对象的成员变量将指向相同的资源。
- 深拷贝是指在拷贝对象时,不仅复制对象的成员变量的值,还会复制成员变量所指向的资源。深拷贝后,源对象和目标对象的成员变量将指向不同的资源,互不干扰。
string模拟实现拷贝构造和移动构造(手撕)
#include <iostream>
#include <cstring> // 用于std::strlen和std::strcpy
class MyString {
private:
char* data; // 指向动态分配的字符数组
public:
// 构造函数
MyString(const char* str = "") {
if (str) {
data = new char[std::strlen(str) + 1]; // 分配内存,+1用于存储'\0'
std::strcpy(data, str); // 复制字符串
} else {
data = new char[1];
*data = '\0'; // 空字符串
}
}
// 拷贝构造函数
MyString(const MyString& other) {
data = new char[std::strlen(other.data) + 1];
std::strcpy(data, other.data);
std::cout << "Copy constructor called" << std::endl;
}
// 移动构造函数
MyString(MyString&& other) noexcept {
data = other.data; // 直接接管other的资源
other.data = nullptr; // 将other的指针置为空,避免析构时重复释放
std::cout << "Move constructor called" << std::endl;
}
// 赋值运算符(拷贝赋值)
MyString& operator=(const MyString& other) {
if (this != &other) { // 防止自赋值
delete[] data; // 释放当前对象的资源
data = new char[std::strlen(other.data) + 1];
std::strcpy(data, other.data);
}
return *this;
}
// 赋值运算符(移动赋值)
MyString& operator=(MyString&& other) noexcept {
if (this != &other) { // 防止自赋值
delete[] data; // 释放当前对象的资源
data = other.data; // 接管other的资源
other.data = nullptr; // 将other的指针置为空
}
return *this;
}
// 析构函数
~MyString() {
delete[] data; // 释放动态分配的内存
}
// 获取字符串内容
const char* c_str() const {
return data;
}
// 打印字符串内容
void print() const {
std::cout << data << std::endl;
}
};
int main() {
MyString str1("Hello, World!");
MyString str2(str1); // 调用拷贝构造函数
MyString str3(std::move(str1)); // 调用移动构造函数
std::cout << "str1: ";
str1.print();
std::cout << "str2: ";
str2.print();
std::cout << "str3: ";
str3.print();
return 0;
}
内存泄露的问题、定位内存泄露的问题、处理内存泄露的问题
内存泄露的定义内存泄露(Memory Leak)是指程序在动态分配内存后,由于某种原因未能正确释放,导致这部分内存无法被重新使用。随着时间推移,程序占用的内存不断增加,最终可能导致系统内存耗尽,程序运行缓慢甚至崩溃。2\. 定位内存泄露问题定位内存泄露问题通常需要借助一些工具和方法,以下是一些常见的方法:(1)使用内存分析工具• Valgrind:一款开源的内存调试和性能分析工具,适用于Linux平台。它可以检测内存泄漏、越界访问、非法指针等问题。• 使用方法:在终端运行 valgrind ./your_program ,它会生成详细的报告,指出内存泄露的位置。• AddressSanitizer:Google开源的内存错误检测工具,可以检测内存泄漏、缓冲区溢出等问题。• 使用方法:在编译时添加 -fsanitize=address 选项,运行程序时会自动检测内存问题。• LeakSanitizer:专门用于检测内存泄漏的工具。• mtrace:GNU Glibc自带的内存问题检测工具,通过记录 malloc 和 free 的调用来检测内存泄漏。• 使用方法:在代码中调用 mtrace() 和 muntrace() ,并设置环境变量 MALLOC_TRACE ,运行程序后会生成日志文件,通过分析日志可以定位内存泄漏问题。(2)代码审查定期审查代码,查找未释放的内存分配。重点关注以下几点:• 确保每个 malloc 、 calloc 、 realloc 等动态分配内存的函数都有对应的 free 调用。• 检查指针重新赋值、错误的内存释放以及返回值的不正确处理等情况。(3)日志和调试在程序中添加日志记录内存分配和释放的操作。通过分析日志,可以确定哪些内存分配没有被释放。此外,使用调试器(如GDB)也可以追踪内存泄漏。(4)自定义内存分配函数创建自定义的内存分配和释放函数,记录每次分配和释放的内存信息。通过这种方式可以更直观地检测内存泄漏。3\. 处理内存泄露问题一旦发现内存泄露问题,需要采取以下措施进行处理:(1)手动释放内存在程序中进行内存分配时,确保及时释放不再需要的内存。如果忘记释放内存,就会导致内存泄漏。(2)使用智能指针在C++中,使用智能指针(如 std::unique_ptr 、 std::shared_ptr )可以自动管理内存,避免手动释放内存的繁琐操作,从而减少内存泄漏的风险。(3)修复代码中的错误根据内存分析工具的报告或日志信息,修复代码中的错误。例如:• 确保在指针重新赋值前释放原有内存。• 修复错误的内存释放逻辑。• 正确处理函数返回值,避免内存泄漏。(4)定期进行垃圾回收对于一些支持垃圾回收的语言(如Java、Python),可以定期进行垃圾回收,以释放不再使用的内存。4\. 预防内存泄露预防内存泄露比修复内存泄露更为重要,以下是一些预防内存泄露的方法:• 明确内存所有权:每次分配内存时,明确哪个部分负责释放该内存。• 使用RAII(资源获取即初始化):在C++中,通过RAII机制确保资源在对象生命周期结束时自动释放。• 避免使用裸指针:尽量使用智能指针或容器来管理动态内存。• 编写单元测试:通过单元测试检查内存分配和释放的正确性。
迭代器失效
序列式容器(如vector
、deque
)
插入元素导致迭代器失效
vector
:当向vector
中插入元素时,如果当前容量不足,vector
会重新分配一块更大的内存,并将原有元素复制到新内存中,然后释放旧内存。这会导致所有指向原vector
的迭代器、指针和引用都失效。
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin();
vec.insert(vec.begin(), 0); // 插入元素,可能导致迭代器失效
// *it = 10; // 错误,此时迭代器it可能已失效
return 0;
}
deque
:在deque
的中间插入元素会使所有迭代器、指针和引用失效;在deque
的两端插入元素,只有指向插入点的迭代器会失效。
std::deque 内部实现是一个块序列,每个块存储固定数量的元素。当删除中间元素时,为了保持块的连续性和完整性,可能会导致以下操作:
- 元素移动:删除中间元素后,后面的元素需要向前移动以填补空缺。
- 块调整:可能需要重新分配块,或者调整块的边界。
最终导致迭代器失效:由于元素的物理位置发生了变化,所有迭代器的指向都会变得不确定。
删除元素导致迭代器失效
vector
:删除vector
中的元素时,被删除元素之后的所有迭代器、指针和引用都会失效。
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin() + 1;
vec.erase(vec.begin()); // 删除元素,后续迭代器失效
// *it = 10; // 错误,此时迭代器it已失效
return 0;
}
deque
:删除deque
中的元素时,除了被删除元素的迭代器失效外,若删除的不是两端元素,所有迭代器、指针和引用都会失效。
关联式容器(如set
、map
)
插入元素导致迭代器失效
对于set
和map
等关联式容器,插入元素不会使任何迭代器、指针和引用失效。因为关联式容器使用红黑树等数据结构实现,插入操作只是在树中添加新节点,不会影响原有节点的位置。
#include <iostream>
#include <set>
int main() {
std::set<int> mySet = {1, 2, 3};
auto it = mySet.begin();
mySet.insert(4); // 插入元素,迭代器不会失效
std::cout << *it << std::endl; // 可以正常使用迭代器
return 0;
}
删除元素导致迭代器失效
删除关联式容器中的元素时,被删除元素的迭代器会失效,但其他迭代器不受影响。
#include <iostream>
#include <set>
int main() {
std::set<int> mySet = {1, 2, 3};
auto it = mySet.begin();
auto nextIt = std::next(it);
mySet.erase(it); // 删除元素,当前迭代器失效
// *it = 10; // 错误,此时迭代器it已失效
std::cout << *nextIt << std::endl; // 可以正常使用其他迭代器
return 0;
}
解决迭代器失效问题的方法
- 重新获取迭代器:在进行插入或删除操作后,重新获取迭代器,确保其指向有效的元素。
- 使用返回值更新迭代器:一些容器的插入和删除操作会返回有效的迭代器,可以使用这些返回值更新迭代器。
const的使用
- 1\. 声明常量变量 const 可以用来声明常量变量,确保其值在初始化后不能被修改。cppconst int MAX_SIZE = 100; // MAX_SIZE是一个常量,不能被修改
- 2\. 常量成员函数在类中, const 可以修饰成员函数,表示该成员函数不会修改对象的任何成员变量。这对于保证对象的不可变性非常有用。
- 3\. 常量对象可以使用 const 修饰整个对象,表示该对象的所有成员变量都不能被修改。对于常量对象,只能调用其常量成员函数。
- 4\. 常量引用 const 可以用于声明引用,表示引用所指向的对象不能被修改。常量引用常用于函数参数,以避免不必要的拷贝。
- 5\. 常量指针和指针常量• 常量指针:指针所指向的内容不能被修改。
- 常量指针常量:指针本身和指针所指向的内容都不能被修改。cppconst int* const ptr = &value; //
- 6\. 常量表达式C++11引入了 constexpr ,用于声明编译时常量。 constexpr 比 const 更严格,它要求表达式在编译时必须能够求值。cppconstexpr int MAX_SIZE = 100; // 编译时常量
- 7\. 常量成员变量类中的成员变量可以被声明为 const ,表示该成员变量在对象初始化后不能被修改。常量成员变量必须在构造函数的初始化列表中初始化。
- 8\. 常量与模板 const 可以用于模板参数,表示模板参数是一个常量。
- 9\. 常量与函数返回值函数可以返回 const 类型的值,但通常不推荐,因为返回 const 值会限制函数的使用。
- 10\. 常量与类型别名
dynamic_cast
功能
dynamic_cast
主要用于在继承体系中进行安全的向下转型(从基类指针或引用转换为派生类指针或引用),并且会在运行时检查转型的有效性。如果转型失败,对于指针类型会返回 nullptr
,对于引用类型会抛出 std::bad_cast
异常。
使用场景
常用于多态环境下,当你持有一个基类指针或引用,但需要访问派生类特有的成员时,可以使用 dynamic_cast
进行安全的类型转换。
示例代码
#include <iostream>
class Base {
public:
virtual void print() { std::cout << "Base" << std::endl; }
virtual ~Base() {}
};
class Derived : public Base {
public:
void print() override { std::cout << "Derived" << std::endl; }
void derivedFunction() { std::cout << "Derived function" << std::endl; }
};
int main() {
Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
if (derivedPtr) {
derivedPtr->derivedFunction();
}
delete basePtr;
return 0;
}
注意事项
dynamic_cast
只能用于含有虚函数的类层次结构,因为它依赖于虚函数表来进行运行时类型检查。- 运行时类型检查会带来一定的性能开销。
static_cast
功能
static_cast
是一种编译时的类型转换,它可以用于各种基本类型之间的转换,以及在继承体系中进行向上转型(从派生类指针或引用转换为基类指针或引用)和向下转型(但不进行运行时检查)。
使用场景
- 基本类型的转换,如
int
转double
。 - 继承体系中的向上转型,这是安全的,因为派生类对象包含基类对象的所有成员。
- 显式调用类的转换构造函数或转换运算符。
示例代码
#include <iostream>
class Base {};
class Derived : public Base {};
int main() {
int num = 10;
double d = static_cast<double>(num);
Derived derived;
Base* basePtr = static_cast<Base*>(&derived); // 向上转型
return 0;
}
注意事项
static_cast
不进行运行时类型检查,因此向下转型时如果类型不匹配,可能会导致未定义行为。
const_cast
功能
const_cast
主要用于去除或添加 const
或 volatile
修饰符。它只能用于改变对象的常量性或易变性,不能改变对象的类型。
使用场景
当你需要在某些情况下修改一个原本被声明为 const
的对象时,可以使用 const_cast
去除 const
修饰符,但要确保这种修改是安全的。
示例代码
#include <iostream>
void printNonConst(int& num) {
std::cout << num << std::endl;
}
int main() {
const int num = 10;
int& nonConstNum = const_cast<int&>(num);
// nonConstNum = 20; // 不建议修改,可能导致未定义行为
printNonConst(nonConstNum);
return 0;
}
注意事项
- 去除
const
修饰符后修改原本为const
的对象可能会导致未定义行为,应谨慎使用。
reinterpret_cast
功能
reinterpret_cast
是一种最危险的类型转换,它可以将任意指针类型转换为其他指针类型,甚至可以将指针转换为整数类型或反之。它不进行任何类型检查,只是简单地重新解释二进制位。
使用场景
- 底层编程,如在某些系统编程中需要将指针转换为整数进行地址计算。
- 处理一些特殊的硬件相关操作。