【CPP】CPP经典面试题
文章目录
- 引言
- 1. C++ 基础
- 1.1 C++ 中的 `const` 关键字
- 1.2 C++ 中的 `static` 关键字
- 2. 内存管理
- 2.1 C++ 中的 `new` 和 `delete`
- 2.2 内存泄漏
- 3. 面向对象编程
- 3.1 继承和多态
- 3.2 多重继承
- 4. 模板和泛型编程
- 4.1 函数模板
- 4.2 类模板
- 5. STL 和标准库
- 5.1 容器
- 5.2 迭代器
- 6. 高级特性
- 6.1 移动语义和右值引用
- 6.2 Lambda 表达式
- 7. 设计模式
- 7.1 单例模式
- 7.2 工厂模式
- 8. 性能优化
- 8.1 内联函数
- 8.2 缓存友好性
- 9. 并发编程
- 9.1 线程
- 9.2 条件变量
- 10. 异常处理
- 10.1 异常机制
- 11. C++17 和 C++20 新特性
- 11.1 C++17 新特性
- 11.2 C++20 新特性
- 12. 实际应用
- 12.1 智能指针
- 12.2 RAII 原则
- 13. 调试和测试
- 13.1 调试技巧
- 13.2
引言
C++ 是一门强大且复杂的编程语言,广泛应用于系统编程、游戏开发、嵌入式系统和高性能计算等领域。由于其灵活性和性能优势,C++ 程序员在面试中常常会遇到各种深入的问题。本文将探讨一些经典的 C++ 面试题,涵盖从基础语法到高级特性的多个方面,帮助读者更好地准备面试。
1. C++ 基础
1.1 C++ 中的 const
关键字
const
是 C++ 中用于定义常量的关键字。它可以用于修饰变量、函数参数、函数返回值以及成员函数。
问题: const
和 #define
有什么区别?
答案:
const
是类型安全的,编译器会进行类型检查,而#define
是宏定义,只是简单的文本替换。const
定义的常量在编译时分配内存,而#define
不分配内存。const
可以用于修饰类的成员函数,表示该函数不会修改类的成员变量。
问题: const
成员函数的作用是什么?
答案:
const
成员函数表示该函数不会修改类的成员变量。它可以被 const
对象调用,而非 const
对象既可以调用 const
成员函数,也可以调用非 const
成员函数。
1.2 C++ 中的 static
关键字
static
关键字在 C++ 中有多种用途,包括修饰局部变量、全局变量、类成员变量和类成员函数。
问题: static
局部变量和普通局部变量有什么区别?
答案:
static
局部变量的生命周期贯穿整个程序运行期间,即使函数调用结束,static
局部变量也不会被销毁。- 普通局部变量的生命周期仅限于函数调用期间,函数调用结束后,局部变量会被销毁。
问题: static
成员函数和普通成员函数有什么区别?
答案:
static
成员函数不依赖于类的实例,可以直接通过类名调用。static
成员函数不能访问类的非静态成员变量和非静态成员函数,因为它们没有this
指针。
2. 内存管理
2.1 C++ 中的 new
和 delete
new
和 delete
是 C++ 中用于动态内存分配和释放的操作符。
问题: new
和 malloc
有什么区别?
答案:
new
是 C++ 的操作符,而malloc
是 C 标准库函数。new
会自动调用对象的构造函数,malloc
不会。new
返回的是对象类型的指针,malloc
返回的是void*
,需要显式类型转换。new
分配内存失败时会抛出std::bad_alloc
异常,malloc
失败时返回NULL
。
问题: delete
和 free
有什么区别?
答案:
delete
是 C++ 的操作符,而free
是 C 标准库函数。delete
会自动调用对象的析构函数,free
不会。delete
用于释放new
分配的内存,free
用于释放malloc
分配的内存。
2.2 内存泄漏
内存泄漏是指程序在动态分配内存后,未能正确释放该内存,导致内存占用不断增加。
问题: 如何避免内存泄漏?
答案:
- 使用智能指针(如
std::unique_ptr
和std::shared_ptr
)来管理动态内存。 - 确保每次
new
操作都有对应的delete
操作。 - 使用 RAII(Resource Acquisition Is Initialization)原则,将资源的生命周期与对象的生命周期绑定。
3. 面向对象编程
3.1 继承和多态
继承和多态是面向对象编程中的核心概念。
问题: 什么是虚函数?为什么需要虚函数?
答案:
虚函数是用于实现多态的机制。通过在基类中声明虚函数,派生类可以重写该函数,从而实现运行时多态。当通过基类指针或引用调用虚函数时,实际调用的是派生类的重写函数。
问题: 虚函数表(vtable)是什么?
答案:
虚函数表是编译器为每个包含虚函数的类生成的一个表,表中存储了虚函数的地址。每个对象在内存中都有一个指向虚函数表的指针(vptr),通过这个指针可以在运行时确定调用哪个虚函数。
3.2 多重继承
多重继承是指一个类可以从多个基类继承。
问题: 多重继承会带来什么问题?如何解决?
答案:
多重继承可能导致菱形继承问题(Diamond Problem),即一个类从两个基类继承,而这两个基类又共同继承自同一个基类,导致派生类中包含多个相同的基类子对象。可以通过虚继承(virtual inheritance)来解决这个问题。
4. 模板和泛型编程
4.1 函数模板
函数模板允许编写通用的函数,可以处理不同类型的参数。
问题: 函数模板和函数重载有什么区别?
答案:
- 函数模板通过参数类型推导生成具体的函数实例,适用于不同类型的数据。
- 函数重载是通过定义多个同名函数,每个函数处理不同类型的参数。
问题: 如何特化一个函数模板?
答案:
可以通过显式特化或部分特化来为特定类型提供特殊的实现。显式特化是为特定类型提供完全不同的实现,而部分特化是为特定类型的一部分提供不同的实现。
4.2 类模板
类模板允许编写通用的类,可以处理不同类型的数据。
问题: 类模板和模板类的区别是什么?
答案:
- 类模板是模板的定义,尚未实例化。
- 模板类是类模板实例化后的具体类。
问题: 如何特化一个类模板?
答案:
可以通过显式特化或部分特化来为特定类型提供特殊的实现。显式特化是为特定类型提供完全不同的实现,而部分特化是为特定类型的一部分提供不同的实现。
5. STL 和标准库
5.1 容器
STL 提供了多种容器,如 vector
、list
、map
等。
问题: vector
和 list
有什么区别?
答案:
vector
是动态数组,支持随机访问,插入和删除操作在尾部高效,但在中间或头部效率较低。list
是双向链表,不支持随机访问,插入和删除操作在任何位置都高效。
问题: map
和 unordered_map
有什么区别?
答案:
map
是基于红黑树实现的,元素按键值有序存储,查找、插入和删除操作的时间复杂度为 O(log n)。unordered_map
是基于哈希表实现的,元素无序存储,查找、插入和删除操作的平均时间复杂度为 O(1)。
5.2 迭代器
迭代器是用于遍历容器中元素的对象。
问题: 迭代器的种类有哪些?
答案:
- 输入迭代器:只能读取元素,单向移动。
- 输出迭代器:只能写入元素,单向移动。
- 前向迭代器:可以读取和写入元素,单向移动。
- 双向迭代器:可以读取和写入元素,双向移动。
- 随机访问迭代器:可以读取和写入元素,支持随机访问。
6. 高级特性
6.1 移动语义和右值引用
移动语义和右值引用是 C++11 引入的重要特性,用于提高性能。
问题: 什么是右值引用?为什么需要右值引用?
答案:
右值引用是用于绑定临时对象(右值)的引用类型,通过 &&
表示。右值引用允许将资源(如动态内存)从一个对象“移动”到另一个对象,避免不必要的拷贝操作,从而提高性能。
问题: 什么是移动构造函数和移动赋值运算符?
答案:
移动构造函数和移动赋值运算符是用于实现移动语义的特殊成员函数。移动构造函数接受一个右值引用参数,将资源从源对象移动到目标对象。移动赋值运算符也接受一个右值引用参数,将资源从源对象移动到目标对象,并释放目标对象原有的资源。
6.2 Lambda 表达式
Lambda 表达式是 C++11 引入的匿名函数,可以方便地定义和使用函数对象。
问题: Lambda 表达式的语法是什么?
答案:
Lambda 表达式的语法为:
[捕获列表](参数列表) -> 返回类型 { 函数体 }
捕获列表用于指定 Lambda 表达式可以访问的外部变量,参数列表和返回类型与普通函数类似。
问题: Lambda 表达式的捕获列表有哪些方式?
答案:
[&]
:以引用方式捕获所有外部变量。[=]
:以值方式捕获所有外部变量。[&x, =y]
:以引用方式捕获x
,以值方式捕获y
。[this]
:捕获当前对象的this
指针。
7. 设计模式
7.1 单例模式
单例模式是一种创建型设计模式,确保一个类只有一个实例,并提供全局访问点。
问题: 如何实现线程安全的单例模式?
答案:
可以通过双重检查锁定(Double-Checked Locking)或使用局部静态变量来实现线程安全的单例模式。
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
private:
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
7.2 工厂模式
工厂模式是一种创建型设计模式,用于创建对象而不指定具体的类。
问题: 工厂模式和抽象工厂模式有什么区别?
答案:
- 工厂模式定义一个创建对象的接口,但由子类决定实例化哪个类。
- 抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
8. 性能优化
8.1 内联函数
内联函数是一种优化技术,通过在调用点展开函数体来减少函数调用的开销。
问题: 内联函数有什么优缺点?
答案:
- 优点:减少函数调用的开销,提高执行效率。
- 缺点:增加代码体积,可能导致缓存不命中,影响性能。
问题: 如何定义内联函数?
答案:
可以通过 inline
关键字定义内联函数,或者在类定义中直接定义成员函数。
inline int add(int a, int b) {
return a + b;
}
8.2 缓存友好性
缓存友好性是指程序在访问内存时能够充分利用 CPU 缓存,减少缓存未命中的次数。
问题: 如何编写缓存友好的代码?
答案:
- 尽量使用连续内存访问模式,如数组遍历。
- 避免频繁的内存分配和释放,减少内存碎片。
- 使用适当的数据结构和算法,减少不必要的内存访问。
9. 并发编程
9.1 线程
C++11 引入了多线程支持,提供了 std::thread
类。
问题: 如何创建和启动一个线程?
答案:
可以通过 std::thread
类创建和启动一个线程,线程函数可以是普通函数、Lambda 表达式或成员函数。
#include <iostream>
#include <thread>
void threadFunc() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
std::thread t(threadFunc);
t.join();
return 0;
}
问题: 如何避免数据竞争?
答案:
可以通过互斥锁(std::mutex
)、原子操作(std::atomic
)或其他同步机制来避免数据竞争。
9.2 条件变量
条件变量是用于线程间同步的机制,允许线程等待某个条件成立。
问题: 如何使用条件变量?
答案:
可以通过 std::condition_variable
和 std::mutex
来实现线程间的条件等待和通知。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void waitForReady() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });
std::cout << "Ready!" << std::endl;
}
void setReady() {
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_all();
}
int main() {
std::thread t1(waitForReady);
std::thread t2(setReady);
t1.join();
t2.join();
return 0;
}
10. 异常处理
10.1 异常机制
C++ 提供了异常处理机制,允许程序在运行时处理错误。
问题: try
、catch
和 throw
的作用是什么?
答案:
try
块用于包含可能抛出异常的代码。catch
块用于捕获并处理异常。throw
用于抛出异常。
问题: 如何自定义异常类?
答案:
可以通过继承 std::exception
类来定义自定义异常类,并重写 what()
方法以提供异常描述。
#include <exception>
#include <string>
class MyException : public std::exception {
public:
MyException(const std::string& msg) : msg(msg) {}
const char* what() const noexcept override {
return msg.c_str();
}
private:
std::string msg;
};
11. C++17 和 C++20 新特性
11.1 C++17 新特性
C++17 引入了许多新特性,如结构化绑定、std::optional
、std::variant
等。
问题: 什么是结构化绑定?
答案:
结构化绑定允许将结构体或数组的元素绑定到变量上,简化代码。
#include <iostream>
#include <tuple>
int main() {
std::tuple<int, double, std::string> t(1, 2.0, "hello");
auto [a, b, c] = t;
std::cout << a << ", " << b << ", " << c << std::endl;
return 0;
}
11.2 C++20 新特性
C++20 引入了更多新特性,如概念(Concepts)、范围(Ranges)、协程(Coroutines)等。
问题: 什么是概念(Concepts)?
答案:
概念是用于约束模板参数的机制,可以在编译时检查模板参数是否满足特定要求。
#include <concepts>
#include <iostream>
template<typename T>
requires std::integral<T>
void print(T value) {
std::cout << value << std::endl;
}
int main() {
print(42); // OK
// print(3.14); // Error: does not satisfy std::integral
return 0;
}
12. 实际应用
12.1 智能指针
智能指针是 C++11 引入的用于自动管理动态内存的工具。
问题: std::unique_ptr
和 std::shared_ptr
有什么区别?
答案:
std::unique_ptr
是独占所有权的智能指针,不能复制,只能移动。std::shared_ptr
是共享所有权的智能指针,通过引用计数管理资源,可以复制和移动。
问题: 如何使用 std::make_shared
?
答案:
std::make_shared
是用于创建 std::shared_ptr
的工厂函数,可以一次性分配内存并创建对象。
#include <memory>
#include <iostream>
int main() {
auto ptr = std::make_shared<int>(42);
std::cout << *ptr << std::endl;
return 0;
}
12.2 RAII 原则
RAII(Resource Acquisition Is Initialization)是 C++ 中的重要原则,用于管理资源。
问题: 什么是 RAII 原则?
答案:
RAII 原则是指将资源的生命周期与对象的生命周期绑定,通过对象的构造函数获取资源,通过析构函数释放资源,确保资源在对象销毁时自动释放。
问题: 如何实现 RAII?
答案:
可以通过类的构造函数和析构函数来实现 RAII。例如,使用智能指针管理动态内存,使用 std::fstream
管理文件资源等。
#include <fstream>
#include <iostream>
class FileHandler {
public:
FileHandler(const std::string& filename) : file(filename) {
if (!file.is_open()) {
throw std::runtime_error("Failed to open file");
}
}
~FileHandler() {
file.close();
}
void write(const std::string& data) {
file << data;
}
private:
std::ofstream file;
};
int main() {
try {
FileHandler fh("test.txt");
fh.write("Hello, RAII!");
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
}
return 0;
}
13. 调试和测试
13.1 调试技巧
调试是程序开发中的重要环节,掌握调试技巧可以提高开发效率。
问题: 如何使用 GDB 调试 C++ 程序?
答案:
- 编译时添加
-g
选项生成调试信息。 - 使用
gdb
启动程序,设置断点,单步执行,查看变量值等。
g++ -g -o my_program my_program.cpp
gdb ./my_program
问题: 如何使用 assert
进行调试?
答案:
assert
是用于检查条件的宏,如果条件为假,程序会终止并输出错误信息。
#include <cassert>
#include <iostream>
int main() {
int x = 5;
assert(x == 5);
std::cout << "Assertion passed" << std::endl;
return 0;
}