c++--补充

发布时间:2026/7/24 20:13:26
c++--补充 基础部分1.普通函数指针成员函数指针类型声明调用普通函数指针ReturnType (*ptr)(Args...)ptr(args...)成员函数指针ReturnType (Class::*ptr)(Args...)(obj.*ptr)(args...)#includeiostreamclassEntity{public:voidsay(conststd::stringmsg){std::coutEntity says: msgstd::endl;}voidshout(conststd::stringmsg){std::coutEntity SHOUTS: msg!!!std::endl;}};// 定义成员函数指针类型建议用 using/typedef 简化usingEntityMethodvoid(Entity::*)(conststd::string);intmain(){Entity e;EntityMethod methodEntity::say;// 调用语法必须使用 .* 或 -*(e.*method)(Hello);// 输出: Entity says: HellomethodEntity::shout;(e.*method)(Hello);// 输出: Entity SHOUTS: Hello!!!// 通过指针调用Entity*pe;(p-*method)(World);// 输出: Entity SHOUTS: World!!!return0;}成员函数指针语法繁琐现代 C 更推荐使用 std::function std::bind 或 Lambda#includeiostream#includefunctionalclassWorker{public:voidprocess(intx){std::coutProcessing x\n;}};intmain(){Worker w;// 方法 1std::bind需配合 placeholdersautoboundstd::bind(Worker::process,w,std::placeholders::_1);bound(42);// 调用 w.process(42)// 方法 2Lambda最推荐类型安全且灵活autolambda[w](intx){w.process(x);};lambda(42);// 方法 3std::mem_fn专门用于成员函数automemfnstd::mem_fn(Worker::process);memfn(w,42);// 第一个参数传对象实例return0;}2.野指针悬空指针3.指针与引用A.指针所指向的内存空间在程序运行过程中可以改变而引用所绑定的对象一旦绑定就不能改变。是否可变B.指针本身在内存中占有内存空间引用相当于变量的别名在内存中不占内存空间。是否占内存C.指针可以为空但是引用必须绑定对象。是否可为空D.指针可以有多级但是引用只能一级。是否能为多级4.C全局变量、局部变量、静态全局变量、静态局部变量的区别从作用域看全局变量具有全局作用域。全局变量只需在一个源文件中定义就可以作用于所有的源文件。当然其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。静态全局变量具有文件作用域。它作用于定义它的文件里不能作用到其它文件里这样即使两个不同的源文件都定义了相同名字的静态全局变量它们也是不同的变量。局部变量具有局部作用域。它是自动对象auto只在函数执行期间存在函数的一次调用执行结束后变量被撤销其所占用的内存也被收回。静态局部变量具有局部作用域。它只被初始化一次从第一次被初始化直到程序运行结束都一直存在它和全局变量的区别在于全局变量对所有的函数都是可见的而静态局部变量只对定义自己的函数体始终可见。从分配内存空间看静态存储区全局变量静态局部变量静态全局变量。栈局部变量。5.extern C 的作用是什么当 C 程序 需要调用 C 语言编写的函数C 使用链接指示即 extern “C” 指出任意非 C 函数所用的语言。举例**6.sizeof(11) 在 C 和 C 中分别是什么结果**c 语言关系运算返回int类型所以结果是4c 语言关系运算返回bool所以结果是1这个差异源于 C 语言没有内置 bool 类型C99 引入的 _Bool 是后来添加的且关系运算符仍返回 int而 C 从设计之初就内置了 bool 类型并且关系运算符返回 bool。7.C 中 struct和Class区别是什么默认访问权限不同struct 默认访问权限为 publicclass 默认访问权限为 private。默认继承方式不同当使用继承时两者的默认继承权限也不同struct 默认继承方式为 public 继承class 默认继承方式为 private 继承。补充当 class 继承 struct 或者 struct 继承 class 时默认的继承级别取决于 class 或 struct 本身classprivate 继承structpublic 继承即取决于派生类的默认继承级别。class 可以用于定义模板参数struct 不能用于定义模板参数。8.struct 和 union 的区别是什么区别联合体和结构体都是由若干个数据类型不同的数据成员组成。使用时联合体只有一个有效的成员而结构体所有的成员都有效。对联合体的不同成员赋值将会对覆盖其他成员的值而对于结构体的对不同成员赋值时相互不影响。联合体的大小为其内部所有变量的最大值按照最大类型的倍数进行分配大小结构体分配内存的大小遵循内存对齐原则。关于结构体s3结果为24的解释结构体总大小必须是最大对齐数8的整数倍。17 不是 8 的倍数最近的是 24所以填充 7 字节补充可以用 #pragma pack 强制紧凑布局#pragmapack(push,1)// 按 1 字节对齐structA_packed{chara;doubleb;charc;};#pragmapack(pop)// sizeof(A_packed) 10无填充但可能降低访问效率甚至导致崩溃⚠️ 强制 1 字节对齐后double 可能跨越缓存行边界严重降低性能且某些架构如 ARM可能直接崩溃。9.c的静态局部变量10.c中staticstatic作用于全局变量和函数改变了全局变量和函数的作用域使得全局变量和函数只能在定义它的文件中使用在源文件中不具有全局可见性。static 作用于类的成员变量和类的成员函数使得类变量或者类成员函数和类有关也就是说可以不定义类的对象就可以通过类访问这些静态成员。注意类的静态成员函数中只能访问静态成员变量或者静态成员函数不能将静态成员函数定义成虚函数。static 静态成员变量静态成员变量是在类内进行声明在类外进行定义和初始化在类外进行定义和初始化的时候不要出现static关键字和private、public、protected 访问规则。静态成员变量相当于类域中的全局变量被类的所有对象所共享包括派生类的对象。静态成员变量可以作为成员函数的参数而普通成员变量不可以。静态数据成员的类型可以是所属类的类型而普通数据成员的类型只能是该类类型的指针或引用。static 静态成员函数静态成员函数不能调用非静态成员变量或者非静态成员函数因为静态成员函数没有 this 指针。静态成员函数不能声明成虚函数virtual、const 函数和 volatile 函数。11.介绍const 作用及用法const 修饰成员变量定义成 const 常量const 修饰函数参数使得传递过来的函数参数的值不能改变。const 修饰成员函数使得成员函数不能修改任何类型的成员变量mutable 修饰的变量除外也不能调用非 const 成员函数因为非 const 成员函数可能会修改成员变量。在类中的用法const 成员变量const 成员变量只能在类内声明、定义在构造函数初始化列表中初始化。const 成员变量只在某个对象的生存周期内是常量对于整个类而言却是可变的因为类可以创建多个对象不同类的 const 成员变量的值是不同的。const 成员函数不能修改成员变量的值除非有 mutable 修饰只能访问成员变量。不能调用非常量成员函数以防修改成员变量的值。12.define 和 const 的区别是什么编译阶段define 是在编译预处理阶段进行替换const 是在编译阶段确定其值。安全性define 定义的宏常量没有数据类型只是进行简单的替换不会进行类型安全的检查const 定义的常量是有类型的是要进行判断的可以避免一些低级的错误。内存占用define 定义的宏常量在程序中使用多少次就会进行多少次替换内存中有多个备份占用的是代码段的空间const 定义的常量占用静态存储区的空间程序运行过程中只有一份。调试define 定义的宏常量不能调试因为在预编译阶段就已经进行替换了cons定义的常量可以进行调试。13.define 和 typedef 的区别是什么14.volatile 的作用是否具有原子性对编译器有什么影响❌ 没有 volatile 时编译器会自作聪明intflag0;voidwait(){while(flag0){// 编译器认为 flag 不会被修改// 空循环}}编译器优化后可能变成;只读一次 flag存入寄存器然后死循环 mov eax,[flag]loop:test eax,eax je loop;永远跳因为 eax 没变✅ 加上 volatile 后volatileintflag0;voidwait(){while(flag0){// 每次循环都从内存重新读取// ...}}编译器必须每次使用都从内存重新读取禁止寄存器缓存每次修改都立即写回内存禁止延迟写禁止指令重排序volatile 操作之间的相对顺序不能变原子性不属于volatile范畴volatile 有明确的适用场景但核心原则不变只用于编译器优化会误判的外部可见变化不用于线程同步。适用场景内存映射 I/OMMIO—— 最典型的场景硬件寄存器在程序看来就是普通内存地址但读写会触发硬件行为且值随时可能被硬件改变。// 串口数据寄存器volatileuint8_t*constUART_DATA(volatileuint8_t*)0x40001000;// 发送一个字节voiduart_send(uint8_tdata){while(*UART_STATUSTX_BUSY);// 等待硬件就绪必须每次读内存*UART_DATAdata;// 写入触发硬件发送}不用 volatile 会怎样 编译器可能把 *UART_STATUS 缓存到寄存器永远读不到硬件状态变化。中断/信号处理程序修改的变量volatilebooldata_readyfalse;// ISR 会修改它volatileintsensor_value0;// 中断服务程序ISRvoidISR_Timer(){sensor_valueADC_Read();// 读取传感器data_readytrue;// 通知主循环}// 主循环intmain(){while(true){if(data_ready){// 必须每次从内存读不能优化掉process(sensor_value);data_readyfalse;}}}注意这里假设是裸机/单核环境没有多核缓存一致性问题。多核环境仍需内存屏障。解释编译器对不存在依赖的指令可能会调整其顺序cpu可能乱序执行指令内存屏障的作用图解ISR(CPU0)主循环(CPU1)───────────────────────────────────────────────────────────── sensor_value42;──────┐smp_wmb();───┼──→ 屏障确保前面的写操作先完成 data_readytrue;─────┘ │ ├──→ 如果看到 data_readytrue│ 则一定能看到 sensor_value42↓if(data_ready){smp_rmb();// 一定能读到 42valsensor_value;}setjmp/longjmp 相关的局部变量volatileintx0;// 必须 volatile否则 longjmp 回来后可能读到旧值if(setjmp(env)0){x42;longjmp(env,1);// 跳转回来}else{// x 如果不是 volatile这里可能不是 42printf(%d\n,x);}与汇编代码交互的变量volatileintresult;__asm__volatile(movl $100, %0:r(result)// 汇编修改了 result);// 编译器知道 result 被外部修改过不会优化掉后续读取printf(%d\n,result);15.为什么一般将析构函数设置为虚函数如果我们有一个基类指针它实际上指向一个派生类对象当我们删除这个基类指针时如果析构函数不是虚函数那么就只会调用基类的析构函数而不会调用派生类的析构函数。这可能会导致派生类对象的一些资源没有被正确释放从而引发内存泄漏等问题。如果我们将析构函数设置为虚函数那么在删除基类指针时会首先调用派生类的析构函数然后再调用基类的析构函数从而确保所有的资源都能被正确释放。16.为什么构造函数不写为虚函数从存储空间角度虚函数对应一个vtable可是这个vtable其实是存储在对象的内存空间的。问题出来了如果构造函数是虚的就需要通过 vtable来调用可是对象还没有实例化也就是内存空间还没有无法找到vtable所以构造函数不能是虚函数。从使用角度虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的不可能通过父类的指针或者引用去调用因此也就规定构造函数不能是虚函数。17.什么是内联函数在C中使用关键字inline可以声明一个内联函数。声明为内联函数的函数会在编译时被视为候选项编译器会尝试将其展开将函数体直接插入到调用点处。这样可以避免函数调用的开销18.include “ “ 和 的区别是什么查找文件的位置include文件名在标准库头文件所在的目录中查找如果没有再到当前源文件所在目录下查找#include”文件名” 在当前源文件所在目录中进行查找如果没有再到系统目录中查找。使用习惯对于标准库中的头文件常用 include文件名对于自己定义的头文件常用 #include”文件名”19.静态链接库和动态链接库有什么区别面向对象1.重载、重写、隐藏的区别是什么重载是指同一可访问区内被声明几个具有不同参数列参数的类型、个数、顺序的同名函数根据参数列表确定调用哪个函数重载不关心函数返回类型。隐藏是指派生类的函数屏蔽了与其同名的基类函数主要只要同名函数不管参数列表是否相同基类函数都会被隐藏。重写的函数的函数名、参数列表和返回值类型都需要和原函数相同父类中被重写的函数需要有 virtual 修饰。说明上述代码中 ex.fun(1, 0.01); 出现错误说明派生类中将基类的同名函数隐藏了。若是想调用基类中的同名函数可以加上类型名指明 ex.Base::fun(1, 0.01);这样就可以调用基类中的同名函数。2.多态原理实现原理是每个包含虚函数的类都有一个虚函数表这个表记录了该类对象可以调用的虚函数指针每个对象也有一个隐式的虚函数指针指向其类的虚函数表。在运行时调用虚函数时程序通过虚函数指针查找正确的函数。c类相关1.什么是纯虚函数有哪些应用场景2.虚函数的实现机制是什么实现机制虚函数通过虚函数表来实现。虚函数的地址保存在虚函数表中在类的对象所在的内存空间中保存了指向虚函数表的指针称为“虚表指针”通过虚表指针可以找到类对应的虚函数表。当用基类的指针或引用来操作一个派生类的时候这张虚函数表就指明了实际应该调用的函数虚函数表相关知识点虚函数表存放的内容类的虚函数的地址。虚函数表建立的时间编译阶段即程序的编译过程中会将虚函数的地址放在虚函数表中。虚表指针保存的位置虚表指针存放在对象的内存空间中最前面的位置这是为了保证正确取到虚函数的偏移量。3.单继承和多继承的虚函数表结构是怎样的上述Base*画的和文字不一致虚表分析单继承无重写略单继承有重写略多继承无重写派生类的虚函数表基类的顺序和声明的顺序一致多继承有虚函数覆盖的情况派生类的虚函数表