- 1. 虚析构函数的作用
- 2. 智能指针种类以及使用场景
- 3. 动态库与静态库的区别?
- 4. 简述多态实现原理
- 5. 关键字override_final的作用
- 6. 怎么解决菱形继承
- 7. 左值引用与右值引用的区别?右值引用的意义?
- 8. 虚函数表和虚函数表指针的创建时机
- 9. c++ 类型推导用法
- 10. 继承下的构造函数和析构函数执行顺序
- 11. c++11用过哪些特性?
- 12. 面对对象的三大特征
- 13. function_lambda_bind之间的关系
- 17. 函数重载和重写的区别?
- 18. 虚函数的调用过程?
- 19. 运行期多态的实现原理?
1. 虚析构函数的作用
确保正确调用派生类的析构函数:
当一个基类指针指向一个派生类对象,并且通过这个基类指针删除对象时,
如果基类的析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数。
这可能导致派生类中分配的资源没有被正确释放,从而引发内存泄漏或其他资源管理问题。
通过将基类的析构函数声明为虚函数,可以确保在删除对象时,先调用派生类的析构函数,然后再调用基类的析构函数,从而正确释放所有资源。支持多态性:
虚析构函数是多态性的一种体现。通过使用虚析构函数,
可以确保在运行时正确调用适当的析构函数,从而支持动态绑定和多态行为。
2. 智能指针种类以及使用场景
1. std::unique_ptr
特点:独占所有权,不能被复制,只能被移动。
使用场景:适用于管理那些不需要共享所有权的资源。例如,当一个对象的生命周期与某个作用域紧密相关时,可以使用std::unique_ptr。
2. std::shared_ptr
特点:共享所有权,可以被复制。内部使用引用计数来管理对象的生命周期。
使用场景:适用于需要共享所有权的场景。例如,多个对象需要共享同一个资源,直到最后一个对象被销毁时,资源才会被释放。
3. std::weak_ptr
特点:不拥有对象的所有权,只是对std::shared_ptr的弱引用。不会增加引用计数。
使用场景:适用于需要观察对象而不影响其生命周期的场景。例如,避免循环引用导致的内存泄漏。std::enable_shared_from_this<> 是 C++11 引入的一个模板类,用于在一个类中安全地创建 std::shared_ptr 实例。
std::enable_shared_from_this<> 的实现原理是通过一个私有成员变量(通常是一个 std::weak_ptr)来存储当前对象的控制块。
3. 动态库与静态库的区别?
静态库在编译时被链接到可执行文件中,代码被复制到最终的可执行文件中,适用于简单部署和快速启动的场景;动态库在运行时被加载到内存中,代码不复制到可执行文件中,适用于共享代码和节省内存的场景。
1. 编译和链接方式
静态库:静态库在编译时被链接到可执行文件中。编译器会将静态库中的代码复制到最终的可执行文件中。
静态库通常以.a(在Unix/Linux系统中)或.lib(在Windows系统中)为扩展名。
动态库:动态库在运行时被加载到内存中。编译器在链接时只记录动态库的引用,而不是将代码复制到可执行文件中。
动态库通常以.so(在Unix/Linux系统中)或.dll(在Windows系统中)为扩展名。
2. 可执行文件大小
静态库:由于静态库的代码被复制到可执行文件中,因此生成的可执行文件通常较大。
动态库:动态库的代码不会被复制到可执行文件中,因此生成的可执行文件通常较小。
3. 更新和维护
静态库:如果静态库需要更新,所有依赖该静态库的可执行文件都需要重新编译和链接。
动态库:动态库可以在运行时被更新,而不需要重新编译和链接可执行文件。只需要替换动态库文件即可。
4. 内存使用
静态库:每个使用静态库的可执行文件都会在内存中加载一份静态库的代码,可能导致内存占用较高。
动态库:多个可执行文件可以共享同一个动态库的实例,从而节省内存。
5. 加载时间
静态库:静态库的代码在可执行文件启动时就已经加载到内存中,因此启动时间较快。
动态库:动态库在运行时加载,可能会增加启动时间,但可以通过延迟加载(Lazy Loading)来优化。
6. 跨平台兼容性
静态库:静态库通常具有较好的跨平台兼容性,因为它们不依赖于特定的运行时环境。
动态库:动态库的兼容性依赖于操作系统和运行时环境,可能需要针对不同的平台进行适配。
4. 简述多态实现原理
1. 虚函数表(Virtual Table, vtable)
虚函数表:每个包含虚函数的类都有一个虚函数表。虚函数表是一个函数指针数组,存储了该类及其基类的虚函数地址。
虚函数表指针:每个对象实例中都有一个指向该类虚函数表的指针(通常称为vptr)。
2. 虚函数的工作机制
定义虚函数:在基类中使用virtual关键字定义虚函数。派生类可以重写(override)这些虚函数。
动态绑定:当通过基类指针或引用调用虚函数时,编译器会在运行时根据对象的实际类型(即vptr指向的虚函数表)来决定调用哪个版本的虚函数。
4. 运行时多态的实现
编译时:编译器为每个类生成虚函数表,并在对象实例中插入虚函数表指针。
运行时:当调用虚函数时,程序通过对象的vptr找到对应的虚函数表,然后调用表中存储的函数地址。
5. 多态的优势
灵活性:允许在运行时根据对象的实际类型调用不同的函数实现。
代码复用:通过基类指针或引用可以统一管理不同派生类的对象。
5. 关键字override_final的作用
1. override 关键字
override关键字用于显式地表明一个函数是重写(override)基类的虚函数。它的主要作用是:提高代码可读性:明确表示该函数是重写基类的虚函数。
防止错误:如果函数签名与基类的虚函数不匹配,编译器会报错,从而避免潜在的错误。2. final 关键字
final关键字用于防止类被继承或函数被重写。它的主要作用是:防止类被继承:在类定义中使用final关键字,使得该类不能被其他类继承。
防止函数被重写:在虚函数定义中使用final关键字,使得该函数不能被派生类重写。
6. 怎么解决菱形继承
菱形继承(Diamond Inheritance)是指在多重继承中,一个派生类同时继承了两个或多个基类,而这些基类又都继承自同一个基类。
这种情况下,派生类会包含基类的多个实例,导致数据冗余和二义性问题。1. 使用虚继承(Virtual Inheritance)
虚继承是解决菱形继承问题的主要方法。通过在继承时使用virtual关键字,可以确保派生类只包含基类的一个实例,从而避免数据冗余和二义性。2. 使用作用域解析运算符(::)
如果不使用虚继承,可以通过作用域解析运算符来明确指定访问哪个基类的成员,从而避免二义性。3. 使用组合替代继承
在某些情况下,可以通过组合(Composition)来替代继承,从而避免菱形继承问题。
组合是指将一个类的对象作为另一个类的成员,而不是通过继承来使用其功能。
7. 左值引用与右值引用的区别?右值引用的意义?
1. 左值引用(Lvalue Reference)
左值引用是最常见的引用类型,通常用于将一个变量绑定到一个对象上。左值引用使用单个&符号来声明。特点
绑定到左值:左值引用只能绑定到左值(可以取地址的表达式)。
生命周期:左值引用的生命周期与绑定的对象相同。2. 右值引用(Rvalue Reference)
右值引用是C++11引入的新特性,用于绑定到右值(临时对象或即将销毁的对象)。右值引用使用双&&符号来声明。特点
绑定到右值:右值引用只能绑定到右值(临时对象或即将销毁的对象)。
生命周期:右值引用的生命周期通常与绑定的临时对象相同。3. 右值引用的意义
右值引用的引入主要是为了支持移动语义(Move Semantics)和完美转发(Perfect Forwarding),从而提高代码的性能和灵活性。3.1 移动语义(Move Semantics)
移动语义允许将资源(如动态分配的内存)从一个对象“移动”到另一个对象,而不是进行深拷贝。这可以显著提高性能,特别是在处理大型对象或资源密集型操作时。3.2 完美转发(Perfect Forwarding)
完美转发允许函数将其参数按原样传递给另一个函数,而不改变其左值或右值属性。这在实现通用代码(如模板函数)时非常有用。总结
左值引用:绑定到左值,通常用于修改对象或传递参数。
右值引用:绑定到右值,主要用于支持移动语义和完美转发,提高性能和代码灵活性。
8. 虚函数表和虚函数表指针的创建时机
1. 虚函数表(vtable)的创建时机
虚函数表是在编译时由编译器生成的。每个包含虚函数的类都有一个对应的虚函数表,该表是一个函数指针数组,存储了该类及其基类的虚函数地址。
创建时机
编译时:编译器为每个包含虚函数的类生成一个虚函数表。虚函数表的内容包括该类及其基类的虚函数地址。
链接时:虚函数表在链接阶段被整合到最终的可执行文件中。2. 虚函数表指针(vptr)的创建时机
虚函数表指针是在对象实例化时由编译器插入的。每个包含虚函数的类的对象实例中都有一个指向该类虚函数表的指针。
创建时机
对象实例化时:当创建一个包含虚函数的类的对象时,编译器会在对象的内存布局中插入一个虚函数表指针(vptr),
并将其初始化为指向该类的虚函数表。总结
虚函数表(vtable):在编译时由编译器生成,存储在可执行文件中。
虚函数表指针(vptr):在对象实例化时由编译器插入,并初始化为指向该类的虚函数表。
9. c++ 类型推导用法
1. auto 关键字
auto 是C++11引入的关键字,用于自动推导变量的类型。编译器会根据变量的初始化表达式来推导出变量的类型。2. decltype 关键字
decltype 是C++11引入的关键字,用于获取表达式的类型。decltype 不会对表达式求值,只是获取其类型。3. decltype(auto) 关键字
decltype(auto) 是C++14引入的关键字,结合了 decltype 和 auto 的功能。
它用于推导表达式的类型,并且保留表达式的引用和 const 限定符。
int x = 42;
int& rx = x;decltype(auto) y = rx; // y 被推导为 int&
decltype(auto) z = (rx); // z 被推导为 int&4. 函数模板中的类型推导
在函数模板中,编译器会根据传入的参数类型来推导模板参数的类型。template<typename T>
void print(T value) {std::cout << value << std::endl;
}print(42); // T 被推导为 int
print("hello"); // T 被推导为 const char*5. 返回类型推导
C++14引入了返回类型推导,允许函数模板的返回类型由编译器自动推导。
template<typename T, typename U>
auto add(T t, U u) {return t + u;
}auto result = add(3, 4.5); // result 被推导为 double6. 结构化绑定(C++17)
结构化绑定允许你将一个复杂类型的成员直接绑定到变量上,并且编译器会自动推导这些变量的类型。std::pair<int, double> p(42, 3.14);
auto [i, d] = p; // i 被推导为 int, d 被推导为 double7. constexpr if 中的类型推导(C++17)
在 constexpr if 语句中,编译器会根据条件推导出不同的类型。template<typename T>
void print_type(T value) {if constexpr (std::is_same_v<T, int>) {std::cout << "int: " << value << std::endl;} else if constexpr (std::is_same_v<T, double>) {std::cout << "double: " << value << std::endl;} else {std::cout << "unknown type: " << value << std::endl;}
}print_type(42); // 输出 "int: 42"
print_type(3.14); // 输出 "double: 3.14"
10. 继承下的构造函数和析构函数执行顺序
构造函数的执行顺序
当创建一个派生类对象时,构造函数的执行顺序如下:基类构造函数:首先调用基类的构造函数。如果有多个基类,按照它们在类定义中出现的顺序调用。成员对象构造函数:然后调用成员对象的构造函数。如果有多个成员对象,按照它们在类定义中声明的顺序调用。派生类构造函数:最后调用派生类的构造函数。析构函数的执行顺序
当销毁一个派生类对象时,析构函数的执行顺序如下:派生类析构函数:首先调用派生类的析构函数。成员对象析构函数:然后调用成员对象的析构函数。如果有多个成员对象,按照它们在类定义中声明的顺序的逆序调用。基类析构函数:最后调用基类的析构函数。如果有多个基类,按照它们在类定义中出现的顺序的逆序调用。
11. c++11用过哪些特性?
1. 自动类型推导 (auto 和 decltype)
auto:用于自动推导变量的类型。decltype:用于获取表达式的类型。auto i = 42; // i 被推导为 int
decltype(i) j = i; // j 被推导为 int2. 范围 for 循环
范围 for 循环简化了遍历容器或数组的操作。std::vector<int> v = {1, 2, 3, 4, 5};
for (auto& elem : v) {std::cout << elem << std::endl;
}3. 初始化列表
C++11 引入了统一的初始化语法,可以使用大括号 {} 进行初始化。int arr[] = {1, 2, 3, 4, 5};
std::vector<int> v = {1, 2, 3, 4, 5};4. 右值引用和移动语义
右值引用 (&&) 和移动语义提高了资源管理的效率。std::vector<int> createVector() {return std::vector<int>{1, 2, 3, 4, 5};
}std::vector<int> v = createVector(); // 使用移动语义5. 智能指针
C++11 引入了 std::unique_ptr、std::shared_ptr 和 std::weak_ptr,用于更安全地管理动态内存。std::unique_ptr<int> p1(new int(42));
std::shared_ptr<int> p2 = std::make_shared<int>(42);6. Lambda 表达式
Lambda 表达式允许在代码中内联定义匿名函数。auto add = [](int a, int b) { return a + b; };
std::cout << add(3, 4) << std::endl; // 输出 77. 可变参数模板
可变参数模板允许模板接受任意数量的参数。template<typename... Args>
void print(Args... args) {(std::cout << ... << args) << std::endl;
}print(1, 2, 3, "hello"); // 输出 123hello8. constexpr
constexpr 关键字用于编译时常量表达式。constexpr int factorial(int n) {return n <= 1 ? 1 : n * factorial(n - 1);
}constexpr int f = factorial(5); // f 在编译时计算9. nullptr
nullptr 是一个空指针常量,用于替代 NULL 或 0。int* p = nullptr;10. 强类型枚举
强类型枚举 (enum class) 避免了传统枚举类型的命名冲突问题。enum class Color { Red, Green, Blue };
Color c = Color::Red;11. 委托构造函数
委托构造函数允许一个构造函数调用同一个类的另一个构造函数。class MyClass {
public:MyClass(int x, int y) : x_(x), y_(y) {}MyClass() : MyClass(0, 0) {}
private:int x_, y_;
};12. 默认和删除函数
可以显式地指定默认构造函数、析构函数等,或者删除某些函数。复制
class MyClass {
public:MyClass() = default;MyClass(const MyClass&) = delete;
};13. std::thread 和并发支持
C++11 引入了 std::thread 和相关的并发库,支持多线程编程。#include <thread>
#include <iostream>void hello() {std::cout << "Hello, Concurrent World!" << std::endl;
}int main() {std::thread t(hello);t.join();return 0;
}14. std::chrono 时间库
std::chrono 提供了高精度的时间操作。#include <chrono>
#include <iostream>int main() {auto start = std::chrono::high_resolution_clock::now();// 一些操作auto end = std::chrono::high_resolution_clock::now();std::chrono::duration<double> elapsed = end - start;std::cout << "Elapsed time: " << elapsed.count() << " s\n";
}15. std::tuple
std::tuple 是一个固定大小的异构值集合。#include <tuple>
#include <iostream>int main() {std::tuple<int, double, std::string> t(42, 3.14, "hello");std::cout << std::get<0>(t) << ", " << std::get<1>(t) << ", " << std::get<2>(t) << std::endl;
}总结
C++11 引入了许多强大的新特性,极大地增强了语言的功能和表达能力。
这些特性包括自动类型推导、范围 for 循环、初始化列表、
右值引用和移动语义、智能指针、Lambda 表达式、
可变参数模板、constexpr、nullptr、强类型枚举、
委托构造函数、默认和删除函数、并发支持、时间库和 std::tuple 等。
这些特性使得 C++ 更加现代化、高效和易用。
12. 面对对象的三大特征
封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)。
面向对象编程的三大特征——封装、继承和多态,是构建复杂软件系统的基石。
封装提供了数据的安全性和代码的模块化;继承实现了代码的重用和扩展;多态则提高了代码的灵活性和可扩展性。
1. 封装(Encapsulation)
封装是指将数据和操作数据的方法绑定在一起,并对外隐藏对象的内部实现细节。
通过封装,可以控制数据的访问权限,防止外部代码直接操作对象的内部数据,从而提高代码的安全性和可维护性。
2. 继承(Inheritance)
继承是指一个类(派生类)可以继承另一个类(基类)的属性和方法。
通过继承,可以实现代码的重用,减少代码的冗余。派生类可以扩展或修改基类的行为。
3. 多态(Polymorphism)
多态是指同一个方法在不同的类中有不同的实现。
多态允许使用基类的指针或引用来调用派生类的方法,从而实现动态绑定(运行时多态)。多态提高了代码的灵活性和可扩展性。
13. function_lambda_bind之间的关系
1. std::function
std::function 是 C++11 引入的一个模板类,用于存储、复制和调用任何可调用对象(如函数、函数指针、成员函数指针、lambda 表达式等)。
它提供了一种统一的方式来处理不同类型的可调用对象。
#include <iostream>
#include <functional>void printHello() {std::cout << "Hello, World!" << std::endl;
}int add(int a, int b) {return a + b;
}int main() {std::function<void()> func1 = printHello;func1(); // 输出 "Hello, World!"std::function<int(int, int)> func2 = add;std::cout << func2(3, 4) << std::endl; // 输出 7return 0;
}2. Lambda 表达式
Lambda 表达式是 C++11 引入的一种匿名函数,可以在代码中内联定义。
Lambda 表达式可以捕获外部变量,并且可以作为函数对象传递。3. std::bind
std::bind 是 C++11 引入的一个函数模板,
用于将函数与参数绑定在一起,生成一个新的可调用对象。
它可以用于部分应用函数(即固定部分参数),或者重新排列参数顺序。std::function:用于存储和调用任何可调用对象。提供了一种统一的方式来处理不同类型的可调用对象。可以存储 lambda 表达式、函数指针、成员函数指针等。Lambda 表达式:用于内联定义匿名函数。可以捕获外部变量。可以作为函数对象传递。通常比 std::bind 更简洁和直观。std::bind:用于将函数与参数绑定在一起,生成一个新的可调用对象。可以用于部分应用函数或重新排列参数顺序。比 lambda 表达式更灵活,但通常更复杂。
17. 函数重载和重写的区别?
1. 函数重载(Function Overloading)
定义:函数重载是指在同一个作用域内,定义多个同名函数,但它们的参数列表(参数的类型、数量或顺序)不同。
函数重载的目的是为了提供多个版本的函数,以便根据不同的参数类型或数量来执行不同的操作。特点:
函数名相同。
参数列表不同(参数的类型、数量或顺序不同)。
返回类型可以相同也可以不同。
发生在同一个类中。2. 函数重写(Function Overriding)
定义:函数重写是指在派生类中重新定义(覆盖)基类中已有的虚函数。函数重写的目的是为了实现多态,即在运行时根据对象的实际类型来调用相应的函数。特点:
函数名相同。
参数列表相同。
返回类型相同或兼容(C++11 及以后)
基类中的函数必须是虚函数(virtual)。
发生在基类和派生类之间。
18. 虚函数的调用过程?
虚函数的工作原理
虚函数的工作原理主要依赖于虚函数表(Virtual Table, vtable)和虚函数指针(Virtual Table Pointer, vptr)。每个包含虚函数的类都有一个虚函数表,而每个对象都有一个指向该虚函数表的指针。虚函数表(vtable)
虚函数表是一个存储虚函数地址的数组。每个包含虚函数的类都有一个虚函数表。虚函数表中的条目按虚函数在类中声明的顺序排列。虚函数指针(vptr)
虚函数指针是一个指向虚函数表的指针。每个对象都有一个虚函数指针,通常位于对象内存布局的顶部。虚函数的调用过程
创建对象:当创建一个包含虚函数的类的对象时,编译器会在对象的内存布局中插入一个虚函数指针(vptr),并将其初始化为指向该类的虚函数表。调用虚函数:当通过基类指针或引用调用虚函数时,编译器会生成代码,通过虚函数指针(vptr)查找虚函数表(vtable),并调用表中对应的函数地址。动态绑定:由于虚函数指针(vptr)指向的是对象实际类型的虚函数表,因此调用的函数是对象实际类型的函数,而不是指针或引用类型的函数。
19. 运行期多态的实现原理?
运行期多态的实现主要依赖于以下两个机制:虚函数表(Virtual Table, vtable):每个包含虚函数的类都有一个虚函数表。虚函数表是一个存储虚函数地址的数组。虚函数表中的条目按虚函数在类中声明的顺序排列。虚函数指针(Virtual Table Pointer, vptr):每个对象都有一个虚函数指针,通常位于对象内存布局的顶部。虚函数指针指向该对象所属类的虚函数表。具体实现步骤 定义基类和派生类:在基类中定义虚函数。在派生类中重写(override)虚函数。创建对象:创建派生类对象时,编译器会在对象的内存布局中插入一个虚函数指针(vptr),并将其初始化为指向派生类的虚函数表。调用虚函数:当通过基类指针或引用调用虚函数时,编译器会生成代码,通过虚函数指针(vptr)查找虚函数表(vtable),并调用表中对应的函数地址。动态绑定:由于虚函数指针(vptr)指向的是对象实际类型的虚函数表,因此调用的函数是对象实际类型的函数,而不是指针或引用类型的函数。