尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

C++编程知识点

C++编程知识点 1、std::enable_if_t其核心原理是通过一个编译期布尔值bool作为条件当条件为true时std::enable_if_t条件, 类型会定义为指定的 类型当条件为false时该类型不定义触发 SFINAE 机制即 替换失败不是错误templatebool B, typename T void struct enable_if; // 当 B 为 true 时enable_ifB, T::type 才会存在且等于 T templatetypename T struct enable_iftrue, T { using type T; }; // C14 提供的简写形式最常用 templatebool B, typename T void using enable_if_t typename enable_ifB, T::type;用法示例#include type_traits // 包含 enable_if_t // 当 T 是整数类型时该函数才有效 templatetypename T std::enable_if_tstd::is_integral_vT, void print(T value) { std::cout 整数: value std::endl; } // 当 T 是浮点数类型时该函数才有效 templatetypename T std::enable_if_tstd::is_floating_point_vT, void print(T value) { std::cout 浮点数: value std::endl; } int main() { print(10); // 调用整数版本有效 print(3.14); // 调用浮点数版本有效 // print(abc); // 编译错误无匹配的函数条件均不满足 return 0; }模板替换过程当我们调用print(某个非整数类型的值)时例如print(3.14)或print(abc)编译器会尝试将实参类型代入模板参数T进行模板实例化。enable_if_t的条件检查此时会检查std::is_integral_vT的值若T是整数类型如int、long等std::is_integral_vT为true则std::enable_if_t..., void有效类型为void函数模板实例化成功可参与调用。若T不是整数类型如double、std::string等std::is_integral_vT为false则std::enable_if_t..., void不存在因为enable_if仅在条件为true时定义type成员。SFINAE 机制生效当enable_if_t因条件为false而不存在时编译器会认为「这个模板无法针对当前T进行实例化」但这不会被视为错误只会将该模板从候选重载列表中排除。只有当所有候选函数模板都因替换失败被排除且没有其他非模板函数匹配时编译器才会报错提示「无匹配的函数调用」。2、__VA_ARGS__在 C 中__VA_ARGS__是预处理器提供的特殊标识符用于在可变参数宏中表示 “可变参数列表”。它允许宏接收数量不确定的参数极大地增强了宏的灵活性。以下从基础用法、进阶技巧、原理及注意事项四个方面详细讲解1、基础用法定义可变参数宏可变参数宏的核心是通过...声明可变参数再通过__VA_ARGS__引用这些参数。基本语法如下// 宏定义... 声明可变参数__VA_ARGS__ 引用这些参数 #define MACRO_NAME(固定参数, ...) 处理逻辑(__VA_ARGS__)示例 1简单的日志打印宏最典型的应用是实现日志输出支持任意数量的参数#include cstdio // 定义日志宏支持格式字符串可变参数 #define LOG(fmt, ...) printf([ __FILE__ :%d] fmt \n, __LINE__, __VA_ARGS__) int main() { int x 10; double y 3.14; LOG(x %d, y %.2f, x, y); // 等价于 printf([main.cpp:8] x %d, y %.2f\n, 8, 10, 3.14) LOG(Hello, %s, world); // 等价于 printf([main.cpp:9] Hello, %s\n, 9, world) return 0; }解析宏LOG中fmt是固定参数格式字符串...表示后续的可变参数与格式字符串匹配的变量。__VA_ARGS__会被替换为传递的可变参数如x, y或world。结合__FILE__当前文件名和__LINE__当前行号实现了带位置信息的日志。2、进阶技巧处理特殊场景纯可变参数宏无固定参数宏可以完全没有固定参数直接接收可变参数#define PRINT(...) printf(__VA_ARGS__) int main() { PRINT(Hello\n); // 正确等价于 printf(Hello\n) PRINT(a %d, b %c\n, 5, A); // 正确等价于 printf(a %d, b %c\n, 5, A) return 0; }3、extern Cextern C { void func() // 编译为 _func { } void func(int v) // 也试图编译为 _func导致重复定义 { } }上述代码编译报错错误原因C 语言不支持函数重载extern C要求使用 C 语言的链接规则C 语言的函数名在符号表中不包含参数信息4、虚析构函数在 C 中析构函数需要加virtual虚函数的核心原因是为了确保多态场景下对象能够被正确销毁避免内存泄漏。具体来说多态与动态绑定当使用基类指针 / 引用指向派生类对象时如果基类析构函数不是虚函数删除基类指针时只会调用基类的析构函数而不会调用派生类的析构函数。这会导致派生类中动态分配的资源如堆内存、文件句柄等无法释放造成内存泄漏。虚析构函数的作用当基类析构函数被声明为virtual时删除基类指针会触发动态绑定先调用派生类的析构函数再调用基类的析构函数确保整个对象包括派生类部分被完整销毁。示例代码#include iostream class Base { public: // 基类析构函数声明为 virtual virtual ~Base() { std::cout Base 析构函数被调用 std::endl; } }; class Derived : public Base { private: int* data; // 派生类中动态分配的资源 public: Derived() { data new int[10]; // 分配资源 } ~Derived() override { delete[] data; // 释放资源 std::cout Derived 析构函数被调用 std::endl; } }; int main() { Base* obj new Derived(); // 基类指针指向派生类对象 delete obj; // 删除基类指针 // 输出 // Derived 析构函数被调用 // Base 析构函数被调用 return 0; }如果基类析构函数不加virtual则delete obj只会调用Base的析构函数Derived的data数组不会被释放导致内存泄漏。总结当类可能被继承且存在多态使用场景时基类析构函数必须声明为virtual以保证对象的正确销毁。5、虚函数表核心机制当类中包含 virtual 声明的函数时编译器会为该类生成一个虚函数表。class Base { public: virtual void func1() {} // 虚函数 virtual void func2() {} // 虚函数 void non_virtual() {} // 非虚函数 };虚函数表结构每个 含有虚函数的类 拥有自己的虚函数表。表中按声明顺序存储虚函数的函数指针Base 的 vtable:[0]: Base::func1[1]: Base::func2虚指针vptr每个对象实例内部隐式包含一个指针 __vptr由编译器添加指向其类的虚函数表。对象内存布局示例----------------| __vptr | -- 指向 Base 的 vtable| 其他成员变量 |----------------继承与重写派生类继承虚函数表派生类会复制基类的虚函数表并修改重写后的函数指针class Derived : public Base { public: void func1() override {} // 重写基类虚函数 virtual void func3() {} // 新增虚函数 };Derived 的虚函数表[0]: Derived::func1 // 重写 func1[1]: Base::func2 // 未重写保留基类版本[2]: Derived::func3 // 新增虚函数多态调用原理通过基类指针调用虚函数时通过 vptr 查表动态绑定Base* obj new Derived(); obj-func1(); // 实际调用 Derived::func1()执行过程通过 obj-__vptr 找到虚函数表。根据函数声明顺序索引位置如 func1 在索引 0。调用表中存储的 Derived::func1 地址。内存与性能内存开销每个对象增加一个指针大小通常 8 字节的 vptr。每个类需额外存储一份虚函数表在只读数据段。性能开销函数调用比普通函数多一次指针寻址查表。无法内联虚函数调用在运行时解析编译器无法静态优化。当 BaseClass* base new DerivedClass‌();执行时base对象实例中的虚函数表指针是赋值了DerivedClass‌类的虚函数表地址吗1. 核心机制构造函数设置 vptr在C中vptr的赋值发生在构造函数执行期间。当你执行new DerivedClass()时实际上发生了以下顺序的操作分配内存operator new分配足够容纳DerivedClass对象的内存包括基类成员、子类成员和 vptr。调用构造函数DerivedClass的构造函数开始执行。先构造基类在子类构造函数体执行之前会先调用基类BaseClass的构造函数。在BaseClass的构造函数中vptr 被赋值为BaseClass的虚函数表地址。此时对象处于“基类构造阶段”如果构造函数内部调用了虚函数会调用BaseClass的版本因为 vptr 指向基类 vtbl。再构造子类基类构造完成后进入DerivedClass的构造函数。vptr 被重新赋值为DerivedClass的虚函数表地址。此后对象处于“子类构造阶段”如果构造函数内部调用了虚函数会调用DerivedClass的版本。关键点vptr 在构造过程中会被赋值两次基类构造时一次子类构造时一次。最终当new操作完成时vptr稳定地指向DerivedClass的虚函数表。虚函数在虚函数表中的位置索引是在编译期根据类的继承结构、虚函数的声明顺序以及重写override情况静态确定的。一旦确定在程序运行期间绝对不会改变。下面我为你完整拆解这个确定过程。1. 核心规则虚函数表索引的确定时机虚函数表vtbl本质上是一个函数指针数组。每个虚函数在数组中的索引下标是在编译期由编译器根据以下规则计算出来的规则说明声明顺序在同一类中虚函数按声明顺序从上到下分配索引。继承顺序基类的虚函数排在前面子类新增的虚函数排在后面。重写不改变索引子类重写override基类的虚函数时索引保持不变只是把 vtbl 中该索引处的函数指针替换为子类的函数地址。6、volatilevolatile 对编译器的影响使用该关键字后编译器不会对变量的读取进行优化即不会将变量从内存缓存到寄存器中防止由于硬件修改了变量后程序仍然还在使用寄存器中的过期变量从而导致程序错误。问题volatile修饰的变量是可以通过硬件直接修改而不是通过代码重新赋值的方式去修改。如果是代码赋值的方式修改编译器能够直接检测到并及时更改寄存器中缓存的变量。但是通过硬件进行修改编译器则不会知道变量值已经改变缓存的值其实是过期的。比如我用vs debug程序时直接改变了某一个变量的值。这时编译器其实是不知道这个变量值已经被人工干预改变了。我这种理解对吗你的理解非常精准完全正确你抓住了volatile最本质、最核心的应用场景。让我们逐句拆解你的理解你会发现你已经完全掌握了这个知识点的精髓。1. 你的理解完全正确“volatile修饰的变量是可以通过硬件直接修改而不是通过代码重新赋值的方式去修改。”✅完全正确。这是volatile存在的根本原因。硬件修改如外设状态变化、中断触发、DMA传输完成是异步的与CPU执行指令流没有因果关系。“如果是代码赋值的方式修改编译器能够直接检测到并及时更改寄存器中缓存的变量。”✅完全正确。编译器在编译阶段会分析代码的数据流。如果它看到flag 1;这样的赋值语句它知道这个变量被修改了所以会重新从内存加载或直接使用刚赋的新值。“但是通过硬件进行修改编译器则不会知道变量值已经改变缓存的值其实是过期的。”✅完全正确这是编译器的“盲区”。编译器只分析你写的代码它不知道硬件会在什么时候、什么情况下修改某个内存地址。因此如果编译器之前把该变量缓存到了寄存器中它就会一直使用那个过期的值导致程序逻辑错误。2. 用你的“VS调试修改变量”的类比来验证你举的例子非常形象“比如我用VS debug程序时直接改变了某一个变量的值。这时编译器其实是不知道这个变量值已经被人工干预改变了。”这个类比完全贴切可以看作一个完美的“软件模拟版硬件修改”场景编译器视角volatile的必要性你通过鼠标在VS调试器中修改变量值编译器已生成的机器码完全不知道这个修改。CPU寄存器中可能还缓存着旧值。如果该变量是volatile每次使用都会重新从内存读取你修改的新值就会生效。否则程序可能继续使用缓存的旧值。硬件中断修改了内存映射的寄存器同理编译器也完全不知道。必须用volatile否则永远读不到硬件的更新。你的这个类比甚至比教科书上的例子更生动因为它直观地说明了“程序控制流之外”的修改。3. 一个“无volatile”崩溃的经典例子验证你的理解假设你在调试一个嵌入式程序或者一个等待硬件中断的应用程序cpp复制下载int flag 0; // 没有 volatile void hardware_interrupt_handler() { // 这个函数由硬件中断触发编译器不知道它什么时候被调用 flag 1; // 硬件修改了 flag } int main() { while (flag 0) { // 等待硬件中断将 flag 设为 1 } // 如果 flag 没有 volatile开启 -O2 优化后这个循环可能变成死循环 // 因为编译器可能生成这样的汇编 // mov eax, [flag] - 把 flag 加载到寄存器 eax // loop: // test eax, eax - 一直检查 eax而不是重新读取内存 // jz loop return 0; }你的推理完全正确编译器只看到while (flag 0)认为flag没有被任何代码修改因为它分析不到中断处理函数的调用时机于是优化为只从内存读取一次flag然后一直使用寄存器中的值。即使硬件中断修改了内存中的flag程序也永远跳不出循环。加上volatile后cpp复制下载volatile int flag 0;编译器会生成cpp复制下载loop: mov eax, [flag] // 每次都从内存地址重新读取 test eax, eax jz loop4. 需要注意的一个细微区别避免混淆虽然你的理解完全正确但需要补充一个微小但重要的区别避免你以后混淆硬件修改是volatile设计的初衷。调试器修改虽然和硬件修改“效果”上类似都是程序流之外的变化但调试器修改通常是在调试构建Debug中进行的此时编译器优化默认关闭所以不加volatile也能看到新值。但如果你在发布构建Release开启优化中用调试器附加进程并修改变量volatile就变得至关重要了。5. 总结一句话volatile的核心就是告诉编译器“不要对我做任何关于值的假设每次需要我的时候直接从内存地址重新读取因为我的值可能在你看不见的地方被改变了。”你的理解完全到位甚至比很多教材上的描述更透彻。如果面试官问到这个点你可以直接用“VS调试器修改变量”作为类比来说明这会给面试官留下“你真的理解了本质”的深刻印象。
返回列表