C++面试模拟01
第一部分:基础知识
-
问:解释
const
关键字的作用,以及在什么场景下你会使用const
? -
问:在 C++ 中,
new
和malloc
的区别是什么? -
问:请解释什么是“深拷贝”和“浅拷贝”?在什么情况下我们需要进行深拷贝?
-
问:C++ 中的引用和指针有什么区别?各自的使用场景是什么?
第二部分:面向对象编程
-
问:请解释 C++ 中的“继承”和“多态”。可以举例说明如何实现多态吗?
-
问:假设你有一个基类
Shape
和派生类Circle
和Rectangle
。如何设计一个类层次结构来支持多态调用?请写出代码示例。 -
问:C++ 支持多重继承(multiple inheritance),但是它可能会带来问题。请解释这些问题并说明如何使用虚继承来解决其中一个问题。
第三部分:STL(标准模板库)
-
问:C++ 标准库中的
std::vector
和std::list
有什么区别?分别适用于什么场景? -
问:
std::map
和std::unordered_map
的区别是什么?它们的时间复杂度分别是多少? -
问:在什么情况下你会使用
std::deque
而不是std::vector
或std::list
?
第四部分:并发与多线程
-
问:请解释 C++ 中的多线程机制。如何使用
std::thread
创建线程?请写出代码示例。 -
问:什么是互斥锁(Mutex)?如何在 C++ 中使用它来保护共享资源?
-
问:什么是条件变量(Condition Variable)?请简要描述它的使用场景。
第五部分:C++ 高级特性
-
问:C++11 引入了移动语义(Move Semantics)。请解释移动语义的作用以及它与复制语义的区别。
-
问:什么是 RAII(资源获取即初始化)?为什么在 C++ 中非常重要?
-
问:C++20 引入了协程(Coroutines)。你能描述一下协程的基本概念和使用场景吗?
第六部分:现场编程
- 问:写一个函数,接收一个整数数组,移除所有重复的元素,并返回不重复的元素个数。请尽可能优化时间复杂度和空间复杂度。
int removeDuplicates(std::vector<int>& nums);
- 问:请写一个单例模式(Singleton Pattern)的实现。保证线程安全。
结束问题
-
问:在你的开发经历中,是否遇到过多线程的竞争条件?你是如何解决的?
-
问:你平时如何优化代码性能?有哪些常用的工具和方法?
new
和 malloc
的区别
-
类型检查:
new
:会调用构造函数,分配内存时会执行类型检查。malloc
:不执行类型检查,返回void*
类型的指针,需要手动转换类型。
-
返回类型:
new
:返回指定类型的指针,不需要类型转换。malloc
:返回void*
,需要显式进行类型转换。
-
异常处理:
new
:如果分配失败,会抛出std::bad_alloc
异常。malloc
:如果分配失败,返回NULL
,需要手动检查。
-
构造/析构函数:
new
:调用构造函数,负责初始化对象。malloc
:只分配内存,不会调用构造函数。
-
释放内存:
new
:使用delete
释放内存,并调用析构函数。malloc
:使用free
释放内存,不会调用析构函数。
引用和指针的区别
-
引用(Reference):
- 引用是某个变量的别名,必须在声明时进行初始化,之后不能更改引用的对象。
- 语法简单、直观,不需要解引用操作符(
*
)。 - 一旦绑定到某个对象,就不能重新绑定。
-
指针(Pointer):
- 指针是存储内存地址的变量,可以指向任意对象或
NULL
,可以在运行时改变所指向的对象。 - 需要使用解引用操作符(
*
)来访问指向的对象。 - 指针可以被重新分配、动态分配内存,使用更加灵活。
- 指针是存储内存地址的变量,可以指向任意对象或
-
区别:
- 引用不能为空,指针可以为空。
- 引用一旦绑定,不能更改;指针可以在运行时改变指向的对象。
- 引用更适合用于参数传递,指针更适合需要动态管理内存的场景。
-
使用场景:
- 引用适用于函数参数传递和返回值,当你想避免拷贝数据时,通常使用引用。
void foo(int& ref); // 引用传递
指针适用于需要动态分配内存的场景,或者需要改变指向的对象。
int* ptr = new int(5); // 动态分配内存
C++ 中的“继承”和“多态”
-
继承:
-
继承是一种面向对象编程的机制,允许从一个现有的类(基类/父类)创建一个新类(派生类/子类),以复用基类的成员变量和成员函数。
-
继承可以让派生类拥有基类的属性和方法,并且可以扩展新的方法或重写基类的方法。
-
多态:
-
多态是指相同的函数或运算符在不同的对象上表现出不同的行为。C++ 支持编译时多态(通过函数重载和运算符重载)和运行时多态(通过虚函数实现)。
-
多态使得程序能够通过基类的指针或引用来调用派生类的实现,从而实现灵活性。
-
示例(虚函数实现多态):
class Base {
public:virtual void show() { std::cout << "Base class" << std::endl; }
};class Derived : public Base {
public:void show() override { std::cout << "Derived class" << std::endl; }
};void func(Base& obj) {obj.show(); // 会根据传入的对象类型调用相应的 show() 方法
}
设计 Shape
、Circle
和 Rectangle
类层次结构
#include <iostream>
#include <cmath>class Shape {
public:virtual double area() const = 0; // 纯虚函数,表示该类是抽象类virtual ~Shape() {} // 虚析构函数
};class Circle : public Shape {
private:double radius;
public:Circle(double r) : radius(r) {}double area() const override { // 实现多态return M_PI * radius * radius;}
};class Rectangle : public Shape {
private:double width, height;
public:Rectangle(double w, double h) : width(w), height(h) {}double area() const override {return width * height;}
};int main() {Shape* shapes[2];shapes[0] = new Circle(5.0);shapes[1] = new Rectangle(4.0, 6.0);for (int i = 0; i < 2; i++) {std::cout << "Area: " << shapes[i]->area() << std::endl;delete shapes[i]; // 注意内存管理}return 0;
}
C++ 中的多重继承问题及虚继承
- 多重继承的问题:
-
二义性问题:当一个类同时继承自两个具有同名成员的基类时,派生类会出现二义性。
-
菱形继承问题:在多重继承中,如果一个类从两个基类派生,而这两个基类又继承自同一个基类,派生类会继承两份相同的基类成员,这就造成了冗余数据和二义性问题。
-
菱形继承示例:
-
class A {
public:void show() { std::cout << "A's show" << std::endl; }
};class B : public A {};
class C : public A {};class D : public B, public C {}; // D 继承了两份 A 类的成员
-
虚继承的解决方案:使用虚继承解决菱形继承问题,确保派生类只继承一份基类的成员。
-
虚继承示例:
class A {
public:void show() { std::cout << "A's show" << std::endl; }
};class B : virtual public A {}; // 虚继承
class C : virtual public A {}; // 虚继承class D : public B, public C {}; // D 只继承一份 A 的成员
解释:通过 virtual
关键字,B 和 C 虚继承自 A,D 类中只有一份 A 类的成员,避免了二义性和数据冗余问题。
std::vector
和 std::list
的区别
-
底层实现:
std::vector
:基于动态数组实现,元素在内存中是连续存储的。std::list
:基于双向链表实现,元素在内存中是非连续存储的。
-
时间复杂度:
- 访问:
vector
支持常数时间的随机访问(O(1)
),因为其元素是连续存储的;而list
访问任意位置的元素都需要线性时间(O(n)
)。 - 插入/删除:
list
在任何位置插入或删除元素的时间复杂度为常数时间(O(1)
),而vector
在尾部插入/删除元素的时间复杂度为O(1)
,但在中间插入或删除元素时需要移动数据,时间复杂度为O(n)
。
- 访问:
-
适用场景:
std::vector
:适用于需要频繁随机访问元素,或在末尾插入、删除元素的场景。std::list
:适用于需要频繁在中间插入或删除元素的场景,但不需要随机访问。
std::map
和 std::unordered_map
的区别
-
底层实现:
std::map
:基于红黑树(自平衡二叉搜索树)实现,键值对按键的顺序存储。std::unordered_map
:基于哈希表实现,键值对无序存储。
-
时间复杂度:
std::map
:插入、删除、查找的平均时间复杂度为O(log n)
。std::unordered_map
:插入、删除、查找的平均时间复杂度为O(1)
,最坏情况下为O(n)
。
-
适用场景:
std::map
:适用于需要按键排序的场景。std::unordered_map
:适用于对性能要求较高、且不需要排序的场景。
何时使用 std::deque
而不是 std::vector
或 std::list
-
std::deque
:双端队列(double-ended queue),在两端插入和删除元素的时间复杂度为O(1)
,并支持常数时间的随机访问。 -
使用场景:
-
当需要在容器的两端频繁进行插入和删除操作,同时还需要随机访问时,使用
std::deque
是最好的选择。 -
与
std::vector
不同,deque
支持在头部进行高效的插入和删除操作。 -
与
std::list
不同,deque
支持随机访问。
-
RAII(Resource Acquisition Is Initialization,资源获取即初始化)是一种重要的 C++ 编程习惯,确保资源的正确管理。RAII 的核心思想是:将资源的生命周期绑定到对象的生命周期。当对象被创建时获取资源,当对象被销毁时释放资源。
-
优点:
- 自动管理资源:资源的分配和释放与对象的生命周期绑定,避免资源泄漏(如内存泄漏、文件句柄泄漏)。
- 异常安全:如果在资源使用过程中发生异常,RAII 确保在栈展开时资源会被正确释放。
- 简化代码:RAII 自动处理资源的释放,减少手动管理资源的复杂性。
-
常见场景:
- 内存管理:智能指针(如
std::unique_ptr
、std::shared_ptr
)通过 RAII 管理动态分配的内存。 - 文件管理:
std::fstream
等标准库类通过 RAII 确保文件在对象销毁时自动关闭。
- 内存管理:智能指针(如
-
示例:
class FileHandler {
private:FILE* file;
public:FileHandler(const char* filename) {file = fopen(filename, "r");if (!file) {throw std::runtime_error("File open failed");}}~FileHandler() {if (file) {fclose(file); // 确保文件关闭}}
};