C++知识点复习(面向面试6)
今天是第六天了也是终于快复习一半了,为自己鼓鼓掌。今天依旧是五条知识点,一起来查漏补缺吧。
26.类和结构体什么区别?
27.被 delete或free释放的内存会直接返还给操作系统吗?
28.什么时候需要析构函数?
29.常函数内能否修改静态成员?
30.初始化参数列表什么特点
好的,我们从第二十六条开始。
26.类和结构体什么区别?
语法层面:只有两个默认权限不同
在 C++ 中,class和struct在功能上几乎是 100% 相同的(都可以包含成员变量、成员函数、构造/析构函数,也都支持继承和多态)。它们唯二的硬性语法区别在于:
- 默认访问权限不同:
class的默认访问权限是private(私有)。struct的默认访问权限是public(公开)。
- 默认继承权限不同:
class继承默认是private继承。struct继承默认是public继承。
27.被 delete或free释放的内存会直接返还给操作系统吗?
如何主动将内存还给操作系统?
如果面试官继续深挖:“如果程序长时间运行,产生了大量空闲内存但都没还给系统,导致进程占用内存(RSS)居高不下,该怎么办?”
- 解决方案:可以调用 glibc 提供的非标准函数
malloc_trim(0)。 - 作用:它会尝试强制将堆顶(top chunk)连续的空闲内存通过
brk(-size)系统调用收缩并归还给操作系统。但需要注意,如果堆顶内存不连续(比如被一些未释放的小对象挡住),这个操作可能会失败或只能归还部分内存。、
28.什么时候需要析构函数?
需要手写析构函数的根本原因是“类内部申请了需要手动释放的资源(堆内存、文件句柄、网络连接等)”。并且,一旦手写了析构函数,一定要警惕默认的浅拷贝带来的双重释放风险,遵循“三/五法则”来完善类的拷贝和移动语义。
1. 动态分配的内存(最典型)
当类的成员变量中,有通过new、new[]或 C 语言的malloc等函数在堆区申请的内存时,必须在析构函数中使用delete、delete[]或free来释放。
- 举例:你提到的指针成员
char* mName = new char[100];,如果不在析构函数中delete[] mName;,就会导致内存泄漏。
2. 系统资源句柄(极易被忽略)
除了内存,程序在运行中打开的各类系统资源,如果不关闭,会导致资源泄漏,甚至影响整个系统的正常运行。这些资源通常不是通过new分配的,但也必须在析构函数中手动关闭。
- 打开的文件:通过
fopen打开的文件指针(FILE*),需要在析构中调用fclose关闭。 - 网络连接/数据库连接:比如 Socket 套接字、数据库连接句柄等,需要在析构中调用
close或专门的断开连接函数。 - 互斥锁/信号量:在多线程编程中,如果类持有锁,需要在析构中确保锁被正确释放,防止死锁。
3. 自定义类型成员需要特殊清理时
如果类中包含其他自定义类型的成员,且这些成员本身管理着上述资源,通常这些成员自己的析构函数会处理清理工作。但如果你需要控制它们的销毁顺序,或者进行一些额外的联动清理操作,也需要手写当前类的析构函数。
- 补充机制:如果类中全是
int、double或std::string这种自带完善析构机制的成员,编译器生成的默认析构函数就完全够用了,此时不需要手写。
💡 面试核心加分项:析构函数与“三/五法则”
在面试中,当你回答了“什么时候需要析构函数”后,面试官紧接着大概率会追问:“如果你手写了析构函数,还需要注意什么?”
这时你可以抛出 C++ 著名的“三法则(Rule of Three)”或 C++11 后的“五法则(Rule of Five)”:
- 核心逻辑:如果你需要手写析构函数,说明你的类在手动管理资源。那么,编译器自动生成的拷贝构造函数和拷贝赋值运算符(浅拷贝)大概率是错误的。
- 潜在风险:默认的浅拷贝会导致两个对象的指针成员指向同一块堆内存。当其中一个对象被销毁(调用析构函数释放内存)后,另一个对象的指针就会变成“野指针”,再次析构时会引发重复释放(Double Free),导致程序崩溃。
- 正确做法:一旦手写了析构函数,通常也必须显式手写(或禁用)拷贝构造函数和拷贝赋值运算符,来实现深拷贝或禁止拷贝。在 C++11 以后,还需要考虑移动构造函数和移动赋值运算符。
29.常函数内能否修改静态成员?
●构造函数和类名相同,没有返回值也不写 void
●如果没有实现构造函数编译器会提供默认的无参构造函数
●构造函数是给成员变量赋值的不是初始化(引出初始化参数列表)
●移动构造,继承构造,委托构造(C++11后的写法,更加简洁高效)
#include <iostream> #include <string> // 1. 模拟一个没有默认构造函数的类(作为成员对象) class Engine { public: Engine(int horsepower) : m_horsepower(horsepower) { std::cout << "Engine 构造函数被调用,马力: " << m_horsepower << std::endl; } private: int m_horsepower; }; // 2. 父类(没有默认构造函数,强制子类在初始化列表中调用) class Vehicle { public: Vehicle(std::string brand) : m_brand(brand) { std::cout << "Vehicle 父类构造函数被调用,品牌: " << m_brand << std::endl; } private: std::string m_brand; }; // 3. 子类 Car,包含 const、引用、无默认构造成员 class Car : public Vehicle { public: // 构造函数:演示初始化列表的各种用法 Car(std::string brand, int hp, int& yearRef) : Vehicle(brand), // 特点5:调用父类的带参构造函数 m_engine(hp), // 特点4:调用成员对象 Engine 的带参构造函数 m_maxSpeed(220), // 特点3:const 常量必须在初始化列表中初始化 m_yearRef(yearRef) // 特点3:引用成员必须在初始化列表中绑定 { std::cout << "Car 子类构造函数体执行" << std::endl; // 注意:如果在这里写 m_maxSpeed = 220; 或 m_yearRef = yearRef; 编译器会直接报错! } void display() { std::cout << "当前引用的年份: " << m_yearRef << std::endl; } private: int m_maxSpeed; // const 常量成员 int& m_yearRef; // 引用成员 Engine m_engine; // 没有默认构造函数的成员对象 // 演示初始化顺序的坑: int m_a; int m_b; }; int main() { int currentYear = 2026; std::cout << "--- 开始创建 Car 对象 ---" << std::endl; // 传入品牌、马力、以及一个外部变量的引用 Car myCar("Tesla", 500, currentYear); myCar.display(); return 0; }