关于auto推导
- 关于
t最后推导出的类型auto的默认类型推导规则会 忽略引用,直接推导出值的类型- 函数
test()返回的是std::string&(引用),但auto t = test();会 拷贝引用指向的值,生成一个新的std::string对象
|
1 2 3 4 5 |
std::string& test() { std::string a = "999"; std::cout << &a << std::endl; return a; } |
|
1 2 |
auto t = test(); std::cout << &t << std::endl; |
多态相关
概述
C++的多态主要通过 虚函数和 动态绑定实现,其核心依赖于 虚函数表(vtable) 和 虚函数指针(vptr) 的机制
多态基本概念
- 多态允许通过 基类指针或引用 调用不同派生类对象的同名函数,实现 同一接口,多种行为
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Animal { public: virtual void speak() { cout << "Animal sound" << endl; } }; class Dog : public Animal { public: void speak() override { cout << "Woof!" << endl; } }; class Cat : public Animal { public: void speak() override { cout << "Meow!" << endl; } }; // 通过基类指针调用不同对象的 speak() Animal* animals[] = { new Dog(), new Cat() }; for (auto animal : animals) { animal->speak(); // 输出 "Woof!" 和 "Meow!" } |
实现机制:虚函数表
- 虚函数表
vtable的创建- 每个类(包含虚函数或继承虚函数)会生成一个虚函数表(
vtable) vtable结构:一个函数指针数组,存储该类的所有虚函数地址
比如基类Animal的vtable:包含Animal::speak()的地址
派生类Dog的vtable:覆盖为Dog::speak()的地址
- 每个类(包含虚函数或继承虚函数)会生成一个虚函数表(
- 虚函数指针
vptr- 每个对象 内部会隐式包含一个指向其类的
vtable的指针(vptr) - 对象构造时,编译器自动设置
vptr指向当前类的vtable - 派生类对象构造时,先调用基类构造函数(
vptr指向基类vtable),再调用派生类构造函数(vptr更新为派生类vtable)
- 每个对象 内部会隐式包含一个指向其类的
动态绑定过程
- 当通过 基类指针/引用 调用虚函数时,实际执行以下步骤:
- 通过对象的
vptr找到对应的vtable - 从
vtable中查找虚函数的地址(根据函数在表中的偏移量) - 调用该地址对应的函数(可能是基类或派生类的实现)
- 通过对象的
问题
vtable是编译器就确定好的?- 虚函数表(
vtable)的布局和内容是在编译阶段由编译器确定好的,但 虚函数指针(vptr)的指向是在运行时动态绑定 的 - 每个类只有一份虚函数表,所有该类的对象共享同一份
vtable
- 虚函数表(
- 编译器在编译期生成派生类的虚函数表时,是先拷贝的基类的虚函数表,然后,派生类中有和基类同名函数,就把该同名函数的地址覆盖为派生类自己的同名函数的地址,如果有派生类自己的虚函数,就新添加到虚函数表里,这样吗?
- 是这样,构造流程如下:
- 编译器会先拷贝基类的虚函数表,作为派生类虚函数表的初始模板
此时,派生类的虚函数表中所有虚函数地址与基类一致 - 如果派生类重写(
override)了基类的某个虚函数,则将基类虚函数表中对应位置的函数地址替换为派生类的函数地址
覆盖操作保持原函数在虚函数表中的顺序和偏移量不变 - 如果派生类定义了新的虚函数(基类中没有的虚函数),这些函数地址会被追加到虚函数表的末尾
多继承
A类多继承自B类和C类,假如B类和C类有同名函数
- 编译器无法确定调用哪个基类的版本,从而导致编译错误
- 解决方法一:显示指定作用域
|
1 2 |
a.B::func(); // 输出 "B::func" a.C::func(); // 输出 "C::func" |
- 解决方法二:在派生类中重写函数
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class A : public B, public C { public: void func() { B::func(); // 调用 B 的版本 // 或 C::func(); // 或添加新逻辑 } }; int main() { A a; a.func(); // 合法,输出 "B::func" return 0; } |
- 解决方法三:使用虚继承
- 如果
B和C本身继承自同一个基类(菱形继承),可以通过 虚继承 消除歧义,但此方法 不适用于无关的基类
- 如果
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Base { public: virtual void func() { std::cout << "Base::func" << std::endl; } }; class B : virtual public Base {}; class C : virtual public Base {}; class A : public B, public C {}; int main() { A a; a.func(); // 合法,调用 Base::func() return 0; } |
- 解决方法四:虚函数覆盖
- 如果
B和C的同名函数是 虚函数,且A重写(override)了该函数,则调用A的版本:
- 如果
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class B { public: virtual void func() { std::cout << "B::func" << std::endl; } }; class C { public: virtual void func() { std::cout << "C::func" << std::endl; } }; class A : public B, public C { public: void func() override { std::cout << "A::func" << std::endl; } }; int main() { A a; a.func(); // 输出 "A::func" a.B::func(); // 输出 "B::func" a.C::func(); // 输出 "C::func" return 0; } |
编码
编码介绍
ascii- 多字节,如
gb2312,big5等 Unicodeutf-8
API默认编码
- 系统内核和
API层Windows底层(如系统API、内核)统一使用UTF-16 Little Endian编码,通过wchar_t(宽字符)实现
智能指针
lambda相关及场景
-
本质
Lambda表达式本质上是一个匿名函数对象
-
使用场景如下:
-
STL算法的谓词- 简化算法的参数传递,避免定义单独的函数或函数对象:
|
1 2 3 4 |
std::vector<int> nums = {3, 1, 4, 1, 5}; std::sort(nums.begin(), nums.end(), [](int a, int b) { return a > b; // 降序排序 }); |
- 回调函数
callback- 替代函数指针或
std::function,实现简洁的回调逻辑:
- 替代函数指针或
|
1 2 3 4 5 6 7 |
void process_data(const std::function<void(int)>& callback) { callback(42); } process_data([](int value) { std::cout << "Received: " << value << std::endl; }); |
- 线程任务
- 作为线程入口函数,捕获局部变量:
|
1 2 3 4 5 |
int x = 10; std::thread t([x] { // 值捕获 x std::cout << x << std::endl; }); t.join(); |
- 闭包
- 保存上下文状态,实现有状态的函数对象:
|
1 2 3 4 5 6 7 8 9 10 |
auto make_counter = [] { int count = 0; return [count]() mutable { // mutable 允许修改捕获的值 return ++count; }; }; auto counter = make_counter(); std::cout << counter() << std::endl; // 1 std::cout << counter() << std::endl; // 2 |
- 事件处理
- 在
GUI或异步编程中处理事件:
- 在
|
1 2 3 |
button.on_click([](Event e) { std::cout << "Button clicked!" << std::endl; }); |
- 延迟执行
- 将逻辑封装为
lambda,推迟到特定时机执行:
- 将逻辑封装为
|
1 2 3 4 |
std::function<void()> task = [] { std::cout << "Task executed later." << std::endl; }; // ... 后续执行 task(); |
lambda实现机制
- 概述
Lambda表达式在编译器层面会被转换为一个匿名类(闭包类型),其核心机制如下:
- 匿名类的生成
- 编译器为每个
lambda生成一个唯一的类,包含: - 捕获的变量:作为类的成员变量
- 重载的
operator():实现lambda的函数体逻辑
- 编译器为每个
|
1 2 |
int x = 10; auto lambda = [x](int y) { return x + y; }; |
|
1 2 3 4 5 6 7 8 9 10 11 |
// 会被编译器转换为类似: class __AnonymousLambda { private: int x; // 值捕获的变量 public: __AnonymousLambda(int x) : x(x) {} int operator()(int y) const { return x + y; } }; |
- 变量捕获的方式
- 值捕获 (
[x]):捕获变量的副本,生成类成员变量 - 引用捕获 (
[&x]):捕获变量的引用,生成引用类型成员 - 隐式捕获 (
[=]/[&]):自动捕获所有外部变量(值或引用)
- 值捕获 (
mutable- 默认情况下,
operator()是const的。若需修改值捕获的变量,需标记mutable:
- 默认情况下,
|
1 |
auto lambda = [x]() mutable { x++; }; |
|
1 2 3 4 5 |
// 编译器生成的 operator() 将不再有 const 限定: int operator()() { // 非 const x++; } |
- 类型推导
- 若
lambda体为return expr;,返回类型自动推导为expr的类型 - 复杂逻辑需显式指定返回类型:
- 若
|
1 |
auto lambda = []() -> double { /* ... */ }; |
- 闭包对象的生命周期
- 值捕获的变量在闭包对象析构时自动释放
- 引用捕获的变量需确保其生命周期长于闭包对象
语言相关
- 总结就是,
C++可以之间调用C语言API - 但是
C语言代码想调用C++的API,需要C++的API以extern "C"的方式导出
定位内存泄漏
代码审查
工具检测
-
静态分析工具
Clang-Tidy:通过静态分析检查潜在的内存泄漏Cppcheck:
-
动态分析工具
Valgrind(Linux/macOS):AddressSanitizerDr. Memory(跨平台):
Clang-Tidy
|
1 |
clang-tidy --checks=*,-modernize-* your_code.cpp |
cppcheck
|
1 |
cppcheck --enable=all your_code.cpp |
Valgrind(Linux/macOS)
|
1 |
valgrind --leak-check=full --show-leak-kinds=all ./your_program |
|
1 2 3 |
==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==12345== at 0x4C2E0E0: operator new(unsigned long) (vg_replace_malloc.c:342) ==12345== by 0x400A56: main (your_code.cpp:10) |
AddressSanitizer(ASan,跨平台):
- 编译时添加
-fsanitize=address:
|
1 |
g++ -fsanitize=address -g your_code.cpp -o your_program |
- 运行程序后,
ASan会报告泄漏位置和调用栈
Dr. Memory(跨平台)
|
1 |
drmemory -- your_program |
单例相关
传统的双重检查锁定(C++11之前的实现)
- 非线程安全
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
#include <iostream> #include <mutex> class LegacySingleton { private: static LegacySingleton* instance; static std::mutex mtx; LegacySingleton() { std::cout << "LegacySingleton 实例已创建" << std::endl; } LegacySingleton(const LegacySingleton&) = delete; LegacySingleton& operator=(const LegacySingleton&) = delete; public: static LegacySingleton* getInstance() { if (instance == nullptr) { // 第一次检查,避免重复加锁 std::lock_guard<std::mutex> lock(mtx); if (instance == nullptr) { // 第二次检查,确保线程安全 instance = new LegacySingleton(); } } return instance; } void doSomething() { std::cout << "执行传统任务..." << std::endl; } }; LegacySingleton* LegacySingleton::instance = nullptr; std::mutex LegacySingleton::mtx; int main() { LegacySingleton::getInstance()->doSomething(); return 0; } |
- 代码解析
- 在
instance = new Singleton();这一行,编译器和CPU可能重排序以下步骤: - 分配内存
- 调用构造函数
- 将内存地址赋值给
instance - 可能导致不同线程可能看到
instance的不一致状态(由于缓存一致性未强制同步)
- 在
简单的线程安全单例(C++11及以上)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
#include <iostream> class Singleton { private: // 私有构造函数,防止外部实例化 Singleton() { std::cout << "Singleton 实例已创建" << std::endl; } // 删除拷贝构造函数和赋值运算符 Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; public: // 获取单例实例的静态方法 static Singleton& getInstance() { // C++11 起局部静态变量初始化是线程安全的 static Singleton instance; return instance; } // 示例成员函数 void doSomething() { std::cout << "执行任务..." << std::endl; } }; int main() { // 获取单例实例并使用 Singleton::getInstance().doSomething(); // 验证是否为同一实例 Singleton& instance1 = Singleton::getInstance(); Singleton& instance2 = Singleton::getInstance(); std::cout << "实例地址是否相同: " << (&instance1 == &instance2) << std::endl; return 0; } |
- 代码解析
C++11标准规定:局部静态变量的初始化在首次进入声明语句时由编译器保证线程安全- 无需手动加锁,编译器会自动生成线程安全的初始化代码
对于C++11之前的那种传统的双重检查锁定,C++11及以后的线程安全实现
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <atomic> #include <mutex> class Singleton { private: static std::atomic<Singleton*> instance; static std::mutex mtx; Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; public: static Singleton* getInstance() { Singleton* tmp = instance.load(std::memory_order_acquire); if (tmp == nullptr) { // 第一次检查(无锁) std::lock_guard<std::mutex> lock(mtx); tmp = instance.load(std::memory_order_relaxed); if (tmp == nullptr) { // 第二次检查(有锁) tmp = new Singleton(); instance.store(tmp, std::memory_order_release); } } return tmp; } }; std::atomic<Singleton*> Singleton::instance{nullptr}; std::mutex Singleton::mtx; |
- 代码解析
std::atomic<Singleton*>确保对instance的读写是原子的std::memory_order_acquire:确保在读取instance时,后续操作不会被重排序到该读取之前std::memory_order_release:确保在写入instance时,之前的操作不会被重排序到该写入之后
使用std::call_once来实现
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <mutex> class Singleton { private: static Singleton* instance; static std::once_flag onceFlag; Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; public: static Singleton* getInstance() { std::call_once(onceFlag, [] { instance = new Singleton(); }); return instance; } }; Singleton* Singleton::instance = nullptr; std::once_flag Singleton::onceFlag; |
- 代码解析
- 优点:无需手动管理锁和原子操作,代码更简洁且线程安全
- 缺点:依赖标准库实现,但现代编译器中性能与双重检查锁定相当
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ 2023_02_0502/05
- ♥ 2022_03_0203/09
- ♥ 2023_02_2002/20
- ♥ 2022_02_24_0203/01
- ♥ 2023_02_0902/11
- ♥ 2020_11_0511/23
热评文章
- 2022_02_24_02 0
- 2023_02_15 0
- 后端知识点记述 一 0
- 2022_03_01 0
- 2023_02_20 0
- 2020_05_11_02 0