C++ 面试题集
一、基础概念题
1. 请简述C++中struct和class的主要区别。
在C++中,struct和class的主要区别在于默认的访问权限和继承方式。struct的成员默认是public的,继承默认是public继承;而class的成员默认是private的,继承默认是private继承。除此之外,两者在功能上完全相同,都支持封装、继承、多态等面向对象特性。
2. 解释C++中的四种类型转换:static_cast、dynamic_cast、const_cast和reinterpret_cast。
static_cast用于非多态类型的转换,如数值类型转换、指针向上转型等,编译时检查;dynamic_cast用于多态类型的向下转型,安全但有运行时开销;const_cast用于添加或移除const/volatile限定;reinterpret_cast进行低层次的重新解释转换,不安全但功能强大。四种_cast各有适用场景,应根据具体需求选择。
3. 什么是虚函数?它的作用是什么?请举例说明。
虚函数是用virtual关键字声明的成员函数,通过虚函数表(vtable)实现运行时多态。当基类指针或引用指向派生类对象时,调用虚函数会动态绑定到实际对象的类型上。例如:基类Animal定义虚函数makeSound(),派生类Dog和Cat重写该函数,通过Animal*指针调用时,会根据实际对象类型调用相应的函数。
4. C++中的const关键字有哪些用法?请列举至少三种。
const可以修饰变量使其成为常量;修饰指针时区分指向常量的指针和常量指针;修饰成员函数表示该函数不能修改类的成员变量;修饰函数参数表示参数在函数内部不可被修改;修饰类的静态成员变量表示全局唯一且不可修改。
5. 解释C++的三大特性:封装、继承、多态,并分别举例说明。
封装是将数据和操作数据的方法绑定在一起,通过访问控制(public/protected/private)隐藏实现细节,如类的定义;继承是类之间is-a的关系,子类继承父类的属性和行为,如class Dog : public Animal;多态是同一接口不同实现,通过虚函数和继承实现,如不同形状的draw()方法。
6. 什么是RAII(Resource Acquisition Is Initialization)?它有什么优点?
RAII是C++的核心编程技巧,将资源的获取与对象的生命周期绑定。构造函数中获取资源,析构函数中释放资源。优点是异常安全——即使发生异常,局部对象的析构函数也会被调用,确保资源被正确释放,避免内存泄漏和资源泄漏。
7. 请解释C++中new和malloc的主要区别。
new是C++运算符,调用构造函数分配内存,返回正确类型指针,支持重载;malloc是C函数,只分配原始内存,返回void*,需要手动类型转换。new分配的内存用delete释放,malloc用free释放。new支持异常机制,malloc失败返回NULL。
8. 什么是函数重载?函数重载的原理是什么?
函数重载是在同一作用域内定义多个同名函数,但参数列表(参数个数、类型或顺序)不同。原理是编译器通过名称修饰(name mangling),根据函数名和参数列表生成唯一的内部标识符,从而实现同名函数的区分。
9. 解释C++中的命名空间(namespace)及其作用。
命名空间用于解决命名冲突问题,将标识符(如变量、函数、类)组织在逻辑分组中。使用namespace关键字定义,通过::运算符访问其中的成员。标准库使用std命名空间,有效避免不同库之间的命名冲突。
10. 请简述C++程序的编译过程。
C++编译过程包括预处理(处理头文件包含、宏定义、条件编译)、编译(将源代码翻译成汇编代码)、汇编(将汇编代码转为机器码的目标文件)、链接(将多个目标文件和库文件合并成可执行文件)四个阶段。
二、指针与引用
11. 请解释指针和引用的区别。
指针是一个变量,存储另一个变量的地址,可以为空(nullptr),可以重新赋值,可以进行指针运算,有多级指针;引用是变量的别名,必须在定义时初始化,不能为空,不能重新绑定,不支持指针运算,没有多级引用。指针更灵活但风险更高,引用更安全且语义更清晰。
12. 什么是指针的空指针(nullptr)和野指针?如何避免野指针?
空指针是指向nullptr的指针,表示"不指向任何有效对象";野指针是指向未定义或已释放内存的指针,可能导致程序崩溃或未定义行为。避免野指针的方法包括:初始化指针为nullptr、及时释放内存后置为nullptr、使用智能指针管理内存生命周期。
13. 解释const指针和指向const的指针的区别。
const int* p是指向const的指针,不能通过p修改指向的值,但p可以指向其他地址;int* const p是const指针,p的地址不能改变,但可以通过p修改指向的值;const int* const p两者都不能改变。
14. 请解释以下声明的含义:
int* ptr[10]:指针数组,数组包含10个指向int的指针int (*ptr)[10]:数组指针,ptr指向一个包含10个int的数组int** ptr:指向指针的指针,ptr指向一个int指针int* func():返回指针的函数,func返回一个指向int的指针
15. 引用作为函数参数和指针作为函数参数有什么优缺点?
引用作为参数语法更简洁,不需要检查nullptr,语义清晰(表示别名);指针作为参数可以表示可选参数(nullptr),可以修改指针本身(交换指针)。引用无法重新绑定,指针可以指向不同对象。引用避免了空指针检查,更安全;指针更灵活但需要额外的空值检查。
16. 解释C++11中的右值引用(&&)和移动语义。
右值引用绑定到临时对象(右值),延长临时对象的生命周期。移动语义通过移动构造函数和移动赋值运算符,将资源(如动态内存)从源对象"转移"到目标对象,而非复制,提高性能。std::move()将左值转换为右值引用,触发移动语义。
三、内存管理
17. 请解释C++中的栈(stack)和堆(heap)的区别。
栈内存由编译器自动分配和释放,存储局部变量、函数参数等,内存连续分配,分配效率高,容量有限(通常几MB),自动管理;堆内存由程序员手动管理(new/delete或malloc/free),存储动态分配的对象,内存分散分配,需要手动释放,容易产生内存泄漏,容量受系统限制。
18. 什么是内存泄漏?如何检测和防止内存泄漏?
内存泄漏是程序分配的内存未被释放,导致内存使用量持续增长。检测方法包括使用Valgrind、AddressSanitizer等工具,检查new/delete配对,在关键位置打印内存使用量。防止方法包括使用RAII、智能指针,遵循谁分配谁释放原则,定期审查代码。
19. 解释C++中的智能指针(shared_ptr、unique_ptr、weak_ptr)及其用法。
unique_ptr独占所有权,不可复制但可移动,作用域结束时自动释放,适合唯一所有权场景;shared_ptr共享所有权,使用引用计数,当计数为0时释放,适合共享所有权场景;weak_ptr弱引用,不增加引用计数,用于打破shared_ptr的循环引用,需要lock()转换为shared_ptr使用。
20. 请解释C++中的浅拷贝和深拷贝。
浅拷贝只复制对象的成员变量值,对于指针类型只复制指针地址,多个对象共享同一块内存,可能导致悬挂指针和重复释放;深拷贝不仅复制指针地址,还复制指针指向的内容,使每个对象有独立的内存副本,更安全但开销更大。默认的拷贝构造函数是浅拷贝,需要自定义实现深拷贝。
21. 什么是内存对齐?为什么要进行内存对齐?
内存对齐是数据在内存中的起始地址必须是其大小的整数倍。原因是:1)CPU访问未对齐内存需要多次访问,降低效率;2)某些CPU架构不支持未对齐访问,会导致程序崩溃;3)对齐可以提高缓存命中率。通常编译器会自动处理对齐,但可以用#pragma pack或__attribute__((aligned))指定对齐方式。
22. 解释C++中的placement new及其用途。
placement new是在已分配的内存上构造对象,不分配内存。语法为new (ptr) Type(args)。用途包括:1)在内存池中分配对象;2)在特定内存地址构造对象;3)构造数组后重新初始化。使用后需要手动调用析构函数ptr->~Type()。
四、面向对象设计
23. 请解释C++中的构造函数和析构函数的调用顺序。
构造顺序:先基类构造函数(按继承列表顺序),再成员变量构造函数(按声明顺序),最后派生类构造函数。析构顺序与构造相反:先派生类析构函数,再成员变量析构函数(按声明逆序),最后基类析构函数(按继承逆序)。
24. 什么是拷贝构造函数?什么情况下会调用拷贝构造函数?
拷贝构造函数是用同类对象初始化新对象的构造函数,签名形式为ClassName(const ClassName&)。调用场景包括:用已有对象初始化新对象、函数参数按值传递、函数返回对象、容器插入元素时。编译器会生成默认的拷贝构造函数(浅拷贝)。
25. 解释C++中的虚析构函数及其作用。
当基类指针指向派生类对象并通过基类指针删除时,如果基类析构函数不是虚函数,只会调用基类析构函数,导致派生类部分未释放(内存泄漏)。将基类析构函数声明为虚函数,可以确保正确调用派生类析构函数和基类析构函数,释放所有资源。
26. 什么是纯虚函数?包含纯虚函数的类有什么特点?
纯虚函数是在基类中声明但没有定义的虚函数,语法为virtual void func() = 0;。包含纯虚函数的类称为抽象类,不能实例化对象,只能作为基类被继承。派生类必须实现所有纯虚函数才能实例化。抽象类用于定义接口,强制派生类实现特定功能。
27. 请解释C++中的多重继承和虚继承。
多重继承是一个类继承多个基类,语法class D : public B1, public B2。可能带来问题如菱形继承。虚继承是在继承时使用virtual关键字,如class D : virtual public B,确保虚基类只有一个共享实例,解决多重继承中的数据冗余和二义性问题。
28. 什么是菱形继承问题?如何解决?
菱形继承是指类D继承自类B1和类B2,而B1和B2都继承自类A,导致D有两份A的成员,造成数据冗余和二义性。解决方法是在B1和B2继承A时使用虚继承(virtual public),这样A只会有一个共享实例。
29. 解释C++中的抽象类和接口类的区别。
抽象类可以包含实现(普通成员函数、成员变量),也可以包含纯虚函数;接口类(通常只用纯虚函数)只定义行为,不包含实现和数据。C++没有专门的接口关键字,通常用抽象类模拟接口。接口强调"能做什么",抽象类强调"是什么"。
30. 请解释C++中的运算符重载及其规则。
运算符重载是为已有运算符赋予新的含义,使其可以用于自定义类型。规则包括:1)不能创建新运算符;2)不能改变运算符的优先级和结合性;3)运算符重载不能改变操作数的个数;4)某些运算符不能重载(:: . .* ?:);5)通常重载为成员函数或全局函数。一元运算符重载为成员时无参数,二元运算符有一个参数。
五、STL与模板
31. 请解释C++ STL的三大核心组件。
STL三大核心组件是容器(Container)、迭代器(Iterator)、算法(Algorithm)。容器存储数据,如vector、list、map;迭代器提供遍历容器的方式,是算法与容器之间的桥梁;算法对容器中的元素进行操作,如sort、find、transform。三者配合实现数据存储、访问和处理的分离。
32. vector和list有什么区别?分别在什么情况下使用?
vector是动态数组,内存连续,支持随机访问,尾部插入删除效率高,中间插入删除需要移动元素;list是双向链表,内存不连续,不支持随机访问,任何位置插入删除都是O(1)。vector适合频繁随机访问和尾部操作,list适合频繁中间插入删除和不需要随机访问的场景。
33. 解释C++模板的函数模板和类模板。
函数模板是通用的函数定义,使用模板参数表示类型或值,实例化时根据实参类型生成具体函数,如template<typename T> T max(T a, T b);类模板是通用的类定义,实例化时指定模板参数生成具体类,如template<typename T> class Vector。模板在编译时实例化。
34. 什么是模板特化?什么是偏特化?
模板特化是为特定类型提供特殊实现,分为全特化和偏特化。全特化是指定所有模板参数的具体类型,如template<> class vector<bool>;偏特化(部分特化)是指定部分模板参数或对模板参数添加约束,如template<typename T> class vector<T*>。特化允许针对特定类型优化或处理特殊逻辑。
35. 请解释C++中的迭代器失效问题。
迭代器失效是指迭代器指向的元素被删除或容器结构改变后,迭代器仍然使用旧地址,可能导致未定义行为。vector扩容或中间插入删除会导致所有迭代器失效;list删除元素只会使指向该元素的迭代器失效;map插入不会使已有迭代器失效。解决方法是在修改容器后重新获取迭代器。
36. map和unordered_map有什么区别?
map是红黑树实现的有序关联容器,元素按键排序,支持范围查询,查找、插入、删除复杂度为O(log n);unordered_map是哈希表实现的无序关联容器,不排序,平均复杂度O(1),最坏O(n)。map的键需要支持<比较,unordered_map需要哈希函数和相等比较。
37. 解释C++中的函数对象(Function Object)及其用途。
函数对象是重载了()运算符的类对象,可以像函数一样调用(obj(args))。相比普通函数,函数对象可以保存状态(成员变量),可以内联调用效率更高,常用于STL算法的定制操作(如sort的自定义比较),如std::sort(v.begin(), v.end(), greater<int>())。
六、C++11/14/17/20新特性
38. 请列举C++11中至少五个重要的新特性。
C++11重要新特性包括:1)右值引用和移动语义;2)智能指针(shared_ptr, unique_ptr, weak_ptr);3)lambda表达式;4)auto和decltype关键字;5)范围for循环;6)强类型枚举(enum class);7)可变参数模板;8)线程支持(std::thread, std::mutex);9)STL新增容器(array, forward_list);10) initializer_list初始化。
39. 解释C++11中的lambda表达式及其用法。
Lambda是匿名函数对象,语法为[capture](params) -> return_type { body }。capture列表捕获外部变量(值捕获[=]、引用捕获[&]、特定变量捕获[x])。可用于STL算法定制、事件回调、线程任务等场景,极大简化代码并支持内联函数对象。
40. 什么是右值引用和移动构造函数?它们有什么关系?
右值引用(T&&)绑定到临时对象(右值),延长临时对象生命周期。移动构造函数(ClassName(ClassName&&))将资源从源对象"转移"到目标对象,而非复制,提高性能。右值引用是实现移动构造函数的基础,通过std::move()将左值转换为右值触发移动构造。
41. 解释C++中的auto和decltype关键字。
auto根据初始化值自动推导变量类型,简化冗长类型声明,如auto i = 10;推导为int;decltype根据表达式推导类型但不计算表达式,如decltype(x+y) sum;。两者结合可用于复杂类型声明,如容器遍历for(auto& elem : container)。
42. 请解释C++中的范围for循环。
范围for循环简化容器遍历,语法for(auto& elem : container)。auto可以替换为具体类型,&表示引用避免拷贝。底层使用容器的begin()和end()迭代器,实现简单直观的遍历方式,适用于所有支持begin/end或可Range-based for的范围对象。
43. 什么是智能指针?请解释shared_ptr的工作原理。
智能指针是RAII风格的动态内存管理类。shared_ptr通过引用计数实现共享所有权:每创建一个副本计数加1,销毁一个计数减1,计数为0时释放内存。使用make_shared<T>()可一次性分配控制块和对象内存,减少内存碎片。注意循环引用问题(用weak_ptr解决)。
44. 解释C++中的std::move和std::forward。
std::move将左值无条件转换为右值引用,触发移动语义,但不移动任何内容,只是类型转换;std::forward用于模板参数转发,保持参数的左值/右值属性(完美转发)。move用于资源转移场景,forward用于泛型代码中保持参数原始类型。
45. 请解释C++20中的概念(Concepts)及其作用。
Concepts是编译时约束模板参数的类型属性,语法template<typename T> concept Addable = requires(T a, T b) { a + b; }。用于限制模板参数类型,提供更好的错误信息,替代SFINAE使代码更清晰。可以用requires子句或concept约束模板,实现编译期类型检查。
七、设计模式与实践
46. 请解释单例模式(Singleton)及其实现方式。
单例模式确保类只有一个实例,并提供全局访问点。实现要点:构造函数私有,拷贝和移动操作删除,提供静态方法获取唯一实例。实现方式包括饿汉模式(静态局部变量或静态成员)、懒汉模式(双重检查锁定的静态指针)。C++11以后推荐使用局部静态变量(线程安全)。
47. 什么是RAII模式?请举例说明其在文件操作中的应用。
RAII将资源获取与对象构造绑定,析构函数释放资源。文件操作示例:创建文件句柄类,构造函数打开文件,析构函数关闭文件。当对象超出作用域,自动关闭文件,无需手动调用fclose,即使发生异常也能正确关闭文件。
48. 解释观察者模式(Observer Pattern)及其在C++中的实现。
观察者模式定义一对多依赖,当一个对象状态改变时,所有依赖它的对象都收到通知并更新。实现包括Subject(主题)维护观察者列表并通知;Observer(观察者)定义更新接口;ConcreteSubject和ConcreteObserver实现具体逻辑。C++中可用虚函数或std::function/std::bind实现。
49. 请解释工厂模式(Factory Pattern)的两种形式。
工厂方法模式:定义创建对象的接口,让子类决定实例化哪个类,延迟到子类实现。抽象工厂模式:提供创建一系列相关对象的接口,而无需指定具体类。简单工厂(非设计模式)用一个工厂类根据参数创建不同产品。
50. 在C++中如何实现线程安全?请列举至少两种方法。
线程安全实现方法:1)互斥锁(std::mutex),保护共享数据,lock/unlock或RAII的lock_guard;2)原子操作(std::atomic),用于简单计数器等;3)线程局部存储(thread_local);4)无锁数据结构;5)不可变数据,共享只读数据无需同步。
八、编程实践题
51. 请实现一个简单的单例模式类。
classSingleton{private:Singleton(){}// 私有构造函数~Singleton(){}// 删除拷贝和移动Singleton(constSingleton&)=delete;Singleton&operator=(constSingleton&)=delete;Singleton(Singleton&&)=delete;Singleton&operator=(Singleton&&)=delete;public:staticSingleton&getInstance(){staticSingleton instance;returninstance;}voiddoSomething(){// 业务逻辑}};52. 请实现一个线程安全的单例模式(使用双重检查锁定)。
classSingleton{private:staticstd::atomic<Singleton*>instance;staticstd::mutex mtx;Singleton(){}~Singleton(){}public:staticSingleton*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=newSingleton;instance.store(tmp,std::memory_order_release);}}returntmp;}};std::atomic<Singleton*>Singleton::instance{nullptr};std::mutex Singleton::mtx;53. 请用智能指针实现一个简单的观察者模式。
#include<iostream>#include<vector>#include<memory>classObserver{public:virtual~Observer()=default;virtualvoidupdate(conststd::string&msg)=0;};classSubject{private:std::vector<std::weak_ptr<Observer>>observers;public:voidattach(std::shared_ptr<Observer>obs){observers.push_back(obs);}voidnotify(conststd::string&msg){for(autoit=observers.begin();it!=observers.end();){if(autoobs=it->lock()){obs->update(msg);++it;}else{it=observers.erase(it);}}}};54. 请实现一个简单的内存池分配器。
classMemoryPool{private:structBlock{Block*next;chardata[1];// 柔性数组};Block*freeList;size_t blockSize;size_t poolSize;public:MemoryPool(size_t itemSize,size_t count):blockSize(itemSize),poolSize(count){size_t totalSize=sizeof(Block)+itemSize;freeList=nullptr;for(inti=0;i<count;++i){Block*block=(Block*)malloc(totalSize);block->next=freeList;freeList=block;}}~MemoryPool(){while(freeList){Block*next=freeList->next;free(freeList);freeList=next;}}void*allocate(){if(!freeList)returnnullptr;Block*block=freeList;freeList=freeList->next;returnblock->data;}voiddeallocate(void*ptr){Block*block=(Block*)((char*)ptr-offsetof(Block,data));block->next=freeList;freeList=block;}};55. 请实现一个不可拷贝的类。
classNonCopyable{protected:NonCopyable()=default;~NonCopyable()=default;public:NonCopyable(constNonCopyable&)=delete;NonCopyable&operator=(constNonCopyable&)=delete;// 可选:也删除移动操作NonCopyable(NonCopyable&&)=delete;NonCopyable&operator=(NonCopyable&&)=delete;};// 使用示例classMyClass:publicNonCopyable{// 类的实现};提示:请先独立思考和练习题目,再对照答案检查理解。理解原理比死记硬背更重要。