C++常规面试题库
1. C++11新特性
1.1 类型推导
- auto:用于推导变量类型;
- decltype:用于推导表达式类型;
// 两者均在编译器执行类型推导
auto a = 1;
const int& b = 2;
decltype(b) c = 3;
1.2 左值-右值
- 左值:可以取地址,放在等式左边的;
- 右值:不能取地址,不能放在等号左边的;
- 左值引用:对左值进行引用类型
- 右值引用:对右值进行引用类型
- 移动语义:主要用于转移变量的资源所有权,将原始的资源归为己用,原来的变量被销毁
- 完美转发:std::forward,目标函数和转发函数类型一致
int a = 1; // a是左值
int b = a + 1; // b+1的返回值以及1字符型常量,均为右值
int& c = a; // 左值引用
int&& d = std::move(a); // 通过std:move将左值a转换为右值,从而使用右值引用
std::forward<int&>(a); // 完美转发,a是左值引用类型
1.3 列表初始化
直接对变量使用列表初始化值,包括基本数据类型、数组、STL
float a{10.2};
int b[2] = {1, 2};
std::vector<int> c{1, 3 ,6};
1.4 lambda表达式 && std::function && std::bind
- lanbda表达式:匿名函数
auto func = [capture] (params) -> ret { func_body; }; - std::function:函数对象
- std::bind:绑定可调用对象和参数
// 1.lamdba表达式,
// []不捕获任何变量;
// [&]引用捕获,捕获外部作用域所有变量,在函数体内当作引用使用;
// [=]值捕获,捕获外部作用域所有变量,在函数内内有个副本使用
// [a]只值捕获a变量,不捕获其它变量
// [this]捕获当前类中的this指针
vector<int> nums{2, 6, 3, 8};
auto sort_nums = [](int a, int b) -> bool {return a > b;}
std::sort(nums.begin(), nums.end(), sort_nums);
// 2.function函数
void print_nums(int a) {
std::cout << "a: " << a << std::endl;
}
std::function<void(int)> fun = print_nums; // function调用函数对象
fun(12);
// 3.std::bind函数
double callableFunc (double x, double y) {return x/y;}
auto NewCallable = std::bind (callableFunc, std::placeholders::_1,2);
std::cout << NewCallable (10) << '\n'; // 相当于调用NewCallable(10, 2),因为其中2被绑定
1.5 范围for
- 可遍历数组、容器、字符串以及可迭代对象
for (const auto& ele : elems) {
std::cout << "ele: " << ele << std::endl;
}
1.6 智能指针
- shared_ptr:共享式指针,多个指针可同时共享对同一对象的拥有权;
- unique_ptr:独占式指针,管理唯一对象,仅有一个指针可访问该对象;
- weak_ptr:弱引用指针,可解决shared_ptr的循环引用问题;
std::shared_ptr<int> a_ptr = std::make_shared<int>(2);
std::weak_ptr<int> b_ptr = a_ptr;
std::unique_ptr<int> c_ptr = std::make_unqiue<int>(3);
1.7 final && override
- final:修饰类的关键字,表示禁止该类派生和虚函数的重写;
- override:修饰派生类成员函数,表名基类对应的函数被重写;
class base {
public:
base() = default;
virtual void function() {
}
};
class derived : base {
public:
dervied() = default;
void function() override {
std::cout << "override" << std::endl;
}
};
1.8 线程与锁
- std::thread:可直接定义创建子线程,可使用join和detch;
- std::mutex:用于线程同步,配合std::lock_guard或std::unique_lock使用;
- std::condition_variable:条件变量,配合std::unique_lock使用,可阻塞线程;
- std::atomic:原子操作,可以不需要std::mutex保护;
互斥锁:用于多线程编程的同步机制,保证同一时刻只能有一个线程访问共享资源。
条件变量:用于多线程编程的同步机制,可以让一个线程在某些条件不满足时等待,并在条件满足时被唤醒;通常与互斥锁一起使用,以保证唤醒等待的线程能获得锁。
互斥锁用于保护共享资源;条件变量用于多线程协作运行。
void run(int nums) {
nums++;
}
std::thread thread_process = std::thread(run, 10);
std::mutex lock_mutex;
std::lock_guard<std::mutex>(lock_mutex);
std::atomic<bool> process_single;
2. 多线程读写锁与普通锁的区别
普通锁仅保证多线程访问共享数据时互斥,同一时刻只能有一个线程访问共享数据;
读写锁允许多个线程同时读取共享数据,但有线程对数据写入时,其他线程不能对数据执行读写操作。
3. C++多态机制
C++多态机制是通过虚函数实现,允许程序运行时根据对象类型动态调用函数。
类中声明函数为虚函数,则C++会在类中增加一个虚函数表,该表中存储所有的虚函数地址,每个对象都有一个指向虚函数表的指针。
多态机制可增加代码的灵活性和可扩展性,减少代码的冗余性。
4. vector避免内存重新分配
vector可通过reverse预留足够多的内存空间,从而避免内存重新分配。
5. 指针与引用的区别
- 指针:存储变量的地址,指向内存单元;
- 引用:引用只是一个别名;
两者区别:可以有const指针,但没有const引用;指针可以为空,但引用必须被初始化;指针可以有多级,但引用只有一级。
6. 虚函数
构造函数不能是虚函数,因为构造函数是用来产生对象的,但虚函数是基于对象;
基类的析构函数可以是虚函数,可保证通过基类指针删除派生类对象时的正确资源回收,避免内存泄漏。
7. 进程和线程
进程是计算机中的一个独立的执行单元,它是操作系统进行资源分配和调度的基本单位;线程是进程的一个执行流,是 CPU 调度和分配的基本单位。
8. STL容器
STL容器分为两大类:序列式容器和关联式容器
序列式容器:vector(动态数组),deque(双端队列),list(双向链表),array(固定长度数组);
关联式容器:map(键值对升序集合),set(不重复元素升序集合),unordered_map(键值对无序集合),unordered_set(不重复元素无序集合),multiset(可重复元素升序集合),multimap(可重复键值对升序集合);
9. C和C++的区别
- C++是面向对象的语言,而C是面向过程的结构化编程语言;
- C++具有封装、多态和继承三大特性;
- 相比于C,C++增加类型安全功能,如:强制类型转换、智能指针;
- C++支持泛型编程,比如:模板类、函数模板等;
封装: 允许通过类隐藏实现细节,提供公共接口供其他函数使用,其数据封装和访问控制有助于防止不正确的访问。
多态: 包括静态多态(函数重载和操作符重载)和动态多态(虚函数运行时绑定);
继承: 允许一个类从另一个类继承数据成员和成员函数,并且可以扩展或修改其行为,提供了代码复用和层次化的结构。
10. 程序内存分配方式
- 栈区(stack):编译器在需要时分配,不需要时自动清除的存储区,通常是局部变量、函数参数等;
- 堆区(heap):由程序员分配、释放,程序结束时可能由系统回收,通常是由 new、malloc分配内存,由delete、free释放内存;
- 全局区(静态区):全局变量和静态变量被分配到同一块内存,程序结束后由系统释放;
- 常量存储区:存储常量字符串、字面值常量,不允许修改,由系统释放;
- 程序代码区:存放函数体的二进制代码。
11. 如何防止内存泄露
内存泄漏:程序运行过程中,由于某种原因导致程序无法释放已经不再使用的内存,导致系统内存资源浪费。
防止内存泄漏:C++11提供智能指针,可以有效确保不使用的对象可释放(shared_ptr的引用计数为0即释放资源);针对new的对象,不使用时需要使用delete释放。
12. vetcor的实现方式
vector是动态数组,属于STL(标准模板库) 的一种模板类型,内部通过动态数组存储元素,可在运行时动态改变底层数组容量,增加的元素超过当前最大容量,可自动分配内存(分配方式是在原来基础之上扩充两倍),一般提前预留容量(reserve)可避免二次内存分配,提高效率。
13. 虚函数概念
虚函数是指在另一个类中希望重写的成员函数,使用基类指针指向派生类对象时,调用虚函数时,实际调用的是派生类的函数。
类中一旦存在虚函数,变量便会多出一个虚函数表指针,空间会增大4字节(32位4字节,64位8字节)。
虚函数表数量:是一个类中虚函数的数量;
14. inline与宏的区别
- inline内联函数:编译时展开,直接嵌入到目标代码,可以进行类型检查、语句是否正确等;
- 宏:预编译时展开,简单的文本替换,无类型检查,存在一定的局限性和隐患。
15. 单例模式
单例模式(Singleton Pattern),设计模式之一,保证一个类仅有一个实例,并提供全局访问入口函数,该实例被所有程序模块共享。
16. 静态成员变量和函数
- 静态成员变量:存储在全局区,必须要初始化(类外初始化,初始化时不带static);
- 静态成员函数:声明和实现分离时,实现不带static,静态成员函数内部不能访问非静态成员变量,且不能为虚函数;
class base {
static int base_variable;
};
int base::base_variable = 10;
17. C++类型转换
- static_cast:隐式转换。常用于基本数据类型的强制类型转换;
- dynamic_cast:基类指向派生类的转换;
- const_cast:用于去掉变量的const;
- reinterpret_cast:不安全的底层类型转换,可以将指针或者引用转换为其他类型的指针和引用;
18. new/delete和malloc/free的区别
- new/delete:属于C++的操作符,其底层是malloc和free实现;new自动分配内存,无需指定大小;
- malloc/free:属于标准库函数,由头文件stdlib.h引入;malloc需要自行指定动态分配内存的大小
19. vector和list区别
- vector底层实现是数组;list是双向链表;
- vector支持随机访问;list不支持;
- vector是按顺序分配内存;list不是;
- vector一次性分配内存,不够时才进行2倍扩充;list每次插入节点都会进行内存扩充;
- vector插入删除性能差;list插入删除性能好;
20. 虚函数和纯虚函数的区别
- 纯虚函数:类中只要存在纯虚函数,就是抽象类,抽象类无法实例化对象;且子类必须重写纯虚函数,否则子类也是纯虚函数;
- 虚函数:基类的函数前加virtual,子类可重写该函数;
class base {
public:
base() = default;
virtual void function() = 0;
};
class base_a : public base {
public:
base_a() = default;
void function() override {
std::cout << "function" << std::endl;
}
};
21. 虚函数和重载的区别
- 虚函数:虚函数是程序运行时根据对象的具体类型调用不同的函数,其在派生类中可被覆盖重写;
- 重载:同一命名域下,同一函数名存在不同的形参列表(包括类型和数量),在编译时匹配;
虚函数用于实现多态;重载用于提供函数的不同形式。