linux C/C++后端八股文

1 C/C++


常见问题:智能指针、多态、虚函数、stl原理。
1. 智能指针实现原理
2. 智能指针,里面的计数器何时会改变
3. 智能指针和管理的对象分别在哪个区(智能指针本身在栈区,托管的资源在堆区,利用了栈对象超出生命周期后自动析构的特征,所以无需手动delete释放资源。
4. 面向对象的特性:多态原理
5. 介绍一下虚函数,虚函数怎么实现的

虚函数是面向对象编程中的概念,用于实现运行时多态性。在C++中,虚函数通过虚函数表(vtable)来实现。虚函数是在基类中声明的,可以在派生类中进行重写。它的特点是在运行时动态地决定调用哪个函数,而不是在编译时静态地决定。这允许我们以统一的方式处理不同派生类的对象,从而实现多态性。当一个类中有至少一个虚函数时,编译器会自动生成一个虚函数表。虚函数表是一个存储虚函数指针的数组,在对象的内存中有一个指向虚函数表的指针。每个虚函数在虚函数表中占据一个位置,具体位置取决于它在类中声明的顺序。当通过基类指针或引用调用虚函数时,程序会根据对象的实际类型(而不仅是指针或引用的类型)查找虚函数表,并根据表中相应的函数指针调用适当的函数。这意味着即使基类指针或引用指向派生类对象,也能正确调用派生类中重写的虚函数。虚函数的实现涉及到虚函数表的创建和使用,以及对函数调用的动态绑定。通过使用虚函数,我们可以实现更灵活和可扩展的代码结构,并提高代码的可读性和可维护性。
6. 多态和继承在什么情况下使用

继承通常在以下情况下使用:
1. 实现类之间的"is-a"关系:当一个类可以被另一个类合理地看作其基类的一种特殊类型时,可以使用继承来建立两者之间的关系。例如,一个"狗"类可以继承自"动物"类,因为狗是动物的一种。
2. 代码重用:通过继承,派生类可以获得基类的成员变量和成员函数,从而避免重复编写相同的代码。
3. 实现多层次的继承结构:通过多级继承,可以建立更复杂的类层次结构,使代码更加模块化和可维护。


多态通常在以下情况下使用:
1. 处理不同类型对象的集合:当需要处理多个不同类型的对象,并且希望以一种统一的方式进行操作和处理时,可以使用多态。通过将对象存储在基类的指针或引用中,并使用虚函数调用,可以根据实际对象类型的差异来执行适当的操作。
2. 实现接口和抽象类:多态是实现接口和抽象类的重要手段。接口和抽象类定义了一组共享的行为和方法,但不提供具体的实现。通过派生具体的类并实现这些接口或抽象类,可以实现多态性,并根据需要进行定制化的实现。
总的来说,继承用于建立类之间的关系和代码重用,而多态用于处理不同类型对象的统一操作,并实现接口和抽象类。这些概念有助于编写更具灵活性、可维护性和可扩展性的代码。

7. 除了多态和继承还有什么面向对象方法

1. 封装:封装是面向对象编程的基本原则之一,它将相关的数据和方法封装在一个单独的实体中,以限制对其内部的直接访问,并提供公共接口以实现访问和操作。封装有助于隐藏实现细节,提供更好的代码组织和维护性。
2. 抽象:抽象是将实体的共同特征提取出来,形成一个抽象类或接口。抽象类定义了一组相关的方法和属性,并可以包含一些已实现的方法。接口定义了一组方法的签名,但没有实现。通过抽象类和接口,可以实现多态和遵循面向对象的设计原则。
3. 组合:组合是将多个对象组合在一起,形成更大的对象或实体。通过将对象作为成员变量添加到其他类中,可以实现更复杂的关系和行为。
4. 接口实现:接口实现是一种将接口的方法实现添加到类中的方式。一个类可以实现一个或多个接口,并提供接口定义的方法的具体实现。接口实现允许不同类之间共享一些公共的行为,并增加代码的可扩展性和灵活性。
5. 单一职责原则:单一职责原则是指一个类应该只有一个引起它变化的原因。它促使开发者将不同的功能分解到不同的类中,以减少耦合性,并使代码更易于理解和维护。

这些方法在面向对象编程中都扮演着重要的角色,它们有助于提高代码的可重用性、可扩展性和可维护性,同时也符合面向对象编程的核心原则
8. C++内存分布。什么样的数据在栈区,什么样的在堆区

通常情况下,以下数据存储在栈区:
- 函数的参数和局部变量
- 函数的返回地址
- 函数调用时的上下文信息

而以下数据通常存储在堆区:
- 动态分配的对象和数组
- 全局变量和静态变量
需要注意的是,这只是一般情况下的规则,实际的内存分布还会受到编译器、操作系统和程序设计的影响。因此,在编写C++程序时,确保正确地使用栈区和堆区,合理管理内存是非常重要的。

9. C++内存管理(RAII啥的)

C++中的内存管理通过RAII(Resource Acquisition Is Initialization)机制来实现,它是一种重要的编程范式,用于确保资源的正确获取和释放。
RAII的基本原则是:在资源获取时进行初始化,在资源释放时进行反初始化。通过在对象的构造函数中获取资源,并在析构函数中释放资源,可以确保在对象生命周期结束时,资源会被自动释放。
以下是RAII在内存管理中的一些常见应用:
1. 对象的生命周期控制:使用堆上的对象时,可以通过使用智能指针(如std::unique_ptr、std::shared_ptr)来管理对象的生命周期。智能指针会在不再使用对象时自动释放内存,避免内存泄漏。
2. 文件和资源管理:RAII可以用来管理文件、网络连接、锁等资源。可以使用RAII类封装这些资源,通过在构造函数中打开资源,在析构函数中关闭资源,确保资源的正确释放。
3. 异常安全性:RAII可以用于确保在异常发生时也能正确释放资源。将资源的释放操作放在对象的析构函数中,无论代码执行路径如何,都能保证资源的正确释放。这样可以避免资源泄漏和数据不一致的问题。
通过使用RAII,可以避免手动管理内存和资源的复杂性,减少内存泄漏和资源泄漏的风险,并提高代码的可靠性和可维护性。

10. C++从源程序到可执行程序的过程

1. 编写源代码:首先,程序员使用文本编辑器编写C++源代码,源代码是以`.cpp`或`.cc`扩展名保存的文本文件,包含程序的逻辑和功能。
2. 预处理:在编译之前,源代码会经过预处理器的处理。预处理器会根据预处理指令(以`#`开头的指令,如`#include`和`#define`)对源码进行处理,例如展开宏定义、包含头文件等。预处理器的输出是一个被预处理后的源文件。
3. 编译:经过预处理后,编译器将预处理后的源文件翻译成机器可执行的中间代码。编译器会进行词法分析、语法分析、语义分析等步骤来检查代码的正确性,并生成中间代码(通常是一种称为目标代码的形式)。中间代码是与特定硬件平台无关的表示形式。
4. 汇编:生成的中间代码需要进一步转换为与特定硬件平台相关的机器码。这个过程由汇编器完成,将中间代码转换为目标机器的机器指令。生成的机器指令以二进制的形式表示。
5. 链接:在编译和汇编阶段生成的机器码可能引用了其他源文件中定义的函数和变量。链接器的任务是将这些引用关联起来,创建一个单独的可执行文件。链接器会解析函数和变量的引用,将它们的地址解析为正确的地址,并将所有必要的代码和数据组合到一个可执行文件中。
6. 可执行程序:经过链接后,最终生成的文件就是可执行程序。它是一个包含已编译和链接的代码的二进制文件,可以在操作系统中运行。
需要注意的是,这个过程可能会因编译器和操作系统的不同而有所差异。此外,编译过程中可能还包括优化步骤,以提高程序的性能和执行效率。

11. 一个对象=另一个对象会发生什么(赋值构造函数)


1. 分配新的内存空间:赋值构造函数首先会为目标对象分配新的内存空间,以存储要复制的数据。
2. 复制数据:接下来,赋值构造函数会将源对象的数据复制到目标对象中。这通常涉及逐个复制成员变量的值。
3. 处理指针和资源:如果类中包含指针成员变量或动态分配的资源(如堆内存或文件句柄),赋值构造函数可能需要进行特殊处理。常见的做法是执行深拷贝,即复制指针所指向的内容,而不仅仅是复制指针本身。这样可以避免多个对象共享相同的资源,造成资源释放的问题。
4. 销毁原有数据:最后,赋值构造函数会销毁源对象中的数据,以确保在完成赋值后不会出现内存泄漏或资源泄漏的问题。这可能涉及释放任何动态分配的内存或其他资源。
需要注意的是,如果没有定义赋值构造函数,编译器将生成一个默认的浅拷贝的赋值构造函数,它只会逐个复制成员变量的值,而不会处理指针或资源。这可能导致多个对象共享相同的资源,造成潜在的问题,如资源的重复释放或悬挂指针。因此,如果类中包含指针或动态分配的资源,需要自定义赋值构造函数来执行正确的深拷贝操作。

12. 如果new了之后出了问题直接return。会导致内存泄漏。怎么办(智能指针,raii)

可以使用智能指针和RAII(Resource Acquisition Is Initialization)的原则。

13. c++11的智能指针有哪些。weak_ptr的使用场景。什么情况下会产生循环引用


C++11引入了三种主要的智能指针:`std::shared_ptr`、`std::unique_ptr`和`std::weak_ptr`。
1. `std::shared_ptr`:允许多个指针共享相同的对象,使用引用计数来跟踪对象的引用数量。当最后一个`std::shared_ptr`超出范围时,或者通过调用`std::shared_ptr`的`reset()`方法显式释放资源时,引用计数为零,对应的对象会被自动销毁。
2. `std::unique_ptr`:是独占所有权的智能指针,同一时间只能有一个`std::unique_ptr`拥有资源。当`std::unique_ptr`超出范围时或被赋予新的资源时,先前的资源会被自动释放。
3. `std::weak_ptr`:是一种弱引用,它指向由`std::shared_ptr`管理的对象。与`std::shared_ptr`不同,`std::weak_ptr`并不增加对象的引用计数。它用于解决潜在的循环引用问题,并允许安全地访问由`std::shared_ptr`管理的对象。
`std::weak_ptr`的使用场景:
- 缓存:当需要缓存一个对象,但又不希望该对象的生命周期仅由缓存决定时,可以使用`std::weak_ptr`。这样可以避免对象因缓存而一直存在,占用不必要的内存空间。
- 解决循环引用:循环引用是指两个或多个对象相互持有对方的`std::shared_ptr`,导致资源无法释放。可以使用`std::weak_ptr`来打破循环引用。其中一个对象持有`std::shared_ptr`,而另一个对象持有对应的`std::weak_ptr`。这样,即使两个对象互相引用,但它们之间的引用并不会使引用计数增加,从而避免了内存泄漏问题。
循环引用的产生情况:
- 一个对象持有`std::shared_ptr`指向另一个对象,而另一个对象也持有`std::shared_ptr`指向第一个对象,形成相互引用。
- 典型的例子是父子关系,父对象持有指向子对象的`std::shared_ptr`,而子对象也持有指向父对象的`std::shared_ptr`。除非中断这个循环关系,否则两个对象将永远无法释放。
为了避免循环引用,可以使用`std::weak_ptr`在其中一个
对象中持有对另一个对象的弱引用。这样可以打破循环,以便在不再有其他`std::shared_ptr`引用对象时正确释放资源。

14. 多进程fork后不同进程会共享哪些资源

1. 程序代码段:子进程会完全复制父进程的代码段。
2. 进程上下文:包括进程ID(PID)、父进程ID(PPID)、用户ID(UID)、组ID(GID)等。
3. 打开的文件描述符:子进程会继承父进程的打开文件描述符,包括标准输入、标准输出和标准错误输出。
4. 文件锁:子进程会继承父进程的文件锁。
5. 信号处理程序:子进程会继承父进程的信号处理程序。
6. 内存映射区域:如果父进程在`mmap()`函数中创建了内存映射区域,子进程也会继承这些映射区域。
需要注意的是,尽管子进程继承了父进程的资源,但它们拥有独立的地址空间。这意味着对于数据段、堆和栈等可写的内存区域,子进程将获得一个副本,而不是与父进程共享。
为了确保共享资源的正确使用,以及避免竞争条件和数据一致性问题,通常需要使用进程间通信(Inter-Process Communication,IPC)机制,如管道、共享内存、消息队列等来实现进程间的数据交换。

15. 多线程里线程的同步方式有哪些


1. 互斥锁(Mutex):使用互斥锁可以保护共享资源,确保同一时间只有一个线程可以访问该资源。
```cpp
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
// 线程函数
void thread_function()
{
    std::lock_guard<std::mutex> lock(mtx);
    // 访问共享资源
    // ...
}
int main()
{
    // 创建多个线程并启动
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i)
    {
        threads.push_back(std::thread(thread_function));
    }
    // 等待所有线程结束
    for (auto& thread : threads)
    {
        thread.join();
    }

    return 0;
}
```

2. 条件变量(Condition Variable):使用条件变量可以让线程等待某个条件满足后再继续执行。

```cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool condition = false;
// 线程函数
void thread_function()
{
    std::unique_lock<std::mutex> lock(mtx);
    // 检查条件,如果不满足,则等待
    cv.wait(lock, []{ return condition; });
    // 执行条件满足后的操作
    // ...
}

int main()
{
    // 创建多个线程并启动
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i)
    {
        threads.push_back(std::thread(thread_function));
    }

    // 触发条件满足时,通知等待的线程
    {
        std::lock_guard<std::mutex> lock(mtx);
        condition = true;
        cv.notify_all();
    }

    // 等待所有线程结束
    for (auto& thread : threads)
    {
        thread.join();
    }

    return 0;
}
```

3. 原子操作(Atomic Operations):原子操作可以确保对共享变量的操作是原子的,即不会被中断或交错。

```cpp
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> counter(0);
// 线程函数
void thread_function()
{
    for (int i = 0; i < 10000; ++i)
    {
        counter.fetch_add(1);
    }
}
int main()
{
    // 创建多个线程并启动
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i)
    {
        threads.push_back(std::thread(thread_function));
    }
    // 等待所有线程结束
    for (auto& thread : threads)
    {
        thread.join();
    }
    std::cout << "Counter: " << counter << std::endl;

    return 0;
}


4. 信号量(Semaphore):信号量是一种用于控制并发访问的同步原语,可以限制同时访问某个资源的线程数量。

```cpp
#include <iostream>
#include <thread>
#include <semaphore>
// 信号量对象
std::binary_semaphore sem(1);
// 线程函数
void thread_function()
{
    // 等待信号量
    sem.acquire();
  
    // 访问共享资源
    // ...
    // 释放信号量
    sem.release();
}

int main()
{
    // 创建多个线程并启动
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i)
    {
        threads.push_back(std::thread(thread_function));
    }
    // 等待所有线程结束
    for (auto& thread : threads)
    {
        thread.join();
    }
    return 0;
}
```

5. 读写锁(Reader-Writer Lock):读写锁允许多个线程同时读取共享资源,但只允许一个线程进行写操作。

```cpp
#include <iostream>
#include <thread>
#include <shared_mutex>
std::shared_mutex mtx;
int shared_data = 0;
// 读线程函数
void reader_thread()
{
    std::shared_lock<std::shared_mutex> lock(mtx);
    // 读取共享资源
    std::cout << "Reader thread: " << shared_data << std::endl;
}
// 写线程函数
void writer_thread()
{
    std::unique_lock<std::shared_mutex> lock(mtx);
    // 修改共享资源
    shared_data++;
}

int main()
{
    // 创建多个读线程并启动
    std::vector<std::thread> readers;
    for (int i = 0; i < 3; ++i)
    {
        readers.push_back(std::thread(reader_thread));
   
    // 创建多个写线程并启动
    std::vector<std::thread> writers;
    for (int i = 0; i < 2; ++i)
    {
        writers.push_back(std::thread(writer_thread));
    }
    // 等待所有读线程结束
    for (auto& reader : readers)
    {
        reader.join();
    }
    // 等待所有写线程结束
    for (auto& writer : writers)
    {
        writer.join();
    }
    return 0;
}
```
16. size_of是在编译期还是在运行期确定

size_of是一个在编译期确定的操作符,用于获取数据类型或变量的大小。它通常用于计算内存分配或数据处理的需要。在不同的编程语言中,size_of的用法可能有所不同。
17. 函数重载的机制。重载是在编译期还是在运行期确定

函数重载是一种允许在同一个作用域中定义具有相同名称但参数列表不同的多个函数的机制。重载函数的选择是在编译期确定的。


18. 指针常量和常量指针

int num = 10;
const int* ptr = &num;
在上述示例中,`ptr` 是一个指向常量的指针,它指向变量 `num`。通过 `ptr` 可以读取 `num` 的值,但不能修改 `num` 的值。

int num = 10;
int* const ptr = &num;
在上述示例中,`ptr` 是一个常量指针,它指向变量 `num`。通过 `ptr` 可以修改 `num` 的值,但不能改变 `ptr` 指向的地址。

19. vector的原理,怎么扩容

1. 原理:
   - std::vector 是一个使用动态内存分配的连续存储空间,可以在运行时进行动态调整大小。
   - 在创建 vector 时,分配一定大小的内部数组作为初始存储空间。
   - 连续元素的索引值可以用于直接访问数组中的元素。
   - 当 vector 的元素数量超过当前内部数组的容量时,需要进行扩容。

2. 扩容方式:
   - 当元素数量超过当前内部数组的容量,vector 将重新分配更大的内存空间。
   - 通常会分配比当前容量更大一些的新数组,以便在未来有足够的空间来存储更多的元素。
   - 扩容一般会导致内存重新分配和数据拷贝,因此,扩容操作可能会有一些性能开销。
   - 扩容时,vector 一般会按照一种策略增加内存容量,例如每次扩容时将容量增加一倍。
   - 重新分配内存后,vector 将会将现有元素复制到新分配的内存空间中。
   - 旧的内存空间将被释放以用于其他目的。

20. 介绍一下const

1. 常量的概念:
   - 常量是一个在程序执行过程中不能被修改的值。
   - 常量可以是字面量(例如数字、字符、字符串等)或者变量。
   - 常量的值在定义后不能被改变,这样可以防止无意间修改了应该保持不变的值。
2. const 关键字的应用:
   - 在变量声明时,使用 const 关键字可以将其定义为常量。例如:const int PI = 3.14159;
   - const 可以应用于各种数据类型,包括整型、浮点型、字符型、指针等。
3. const 的特点:
   - const 声明的常量在程序执行期间保持不变。
   - const 常量必须在声明时进行初始化,且初始化后不能再进行修改。
   - const 变量的值可以被编译器用于进行编译时优化,提高代码效率。
   - const 变量的作用域与普通变量相同,可以是全局的或局部的。
   - 对于 const 指针或引用,指针本身或引用本身是不可修改的,但可以通过它们访问和修改指向的对象。

例如:
```cpp
const int MAX_VALUE = 100;
const float PI = 3.14159;

void func() {
    const int num = 42;
    // num = 10;     // 错误,无法修改常量 num
}
int main() {
    const int* ptr = &MAX_VALUE;
    // *ptr = 50;     // 错误,不可修改通过指针访问的常量

    const int& ref = MAX_VALUE;
    // ref = 50;      // 错误,不可修改通过引用访问的常量
    
    return 0;
}
总结:const 关键字被用于定义常量,防止其值被修改。它可以应用于各种数据类型,并提供了编译时的类型检查及代码优化的机会。
21. 引用和指针的区别


1. 定义和使用:
   - 指针通过使用 * 符号来声明和操作。例如:int *ptr; *ptr = 10;
   - 引用通过使用 & 符号来声明,但在使用时无需加上 * 符号进行解引用。例如:int &ref = num; ref = 10;
2. 初始化和赋值:
   - 指针可以在任何时候被初始化,并且可以被重新赋值为其他对象的地址。例如:int *ptr = &num; ptr = &otherNum;
   - 引用必须在声明时进行初始化,并且一旦引用被初始化后,它将一直引用同一个对象,无法改变。例如:int &ref = num;
3. 空值:
   - 指针可以为空,即指向空地址或者空指针常量(nullptr)。例如:int *ptr = nullptr;
   - 引用必须始终引用一个有效的对象,不能为空。
4. 操作:
   - 使用指针可以对指针变量进行算术运算(例如指针加法)和逻辑运算(例如比较两个指针的大小)。
   - 引用本身并不是一个独立的对象,而是原对象的一个别名,不能对引用进行运算。
5. 内存管理和安全性:
   - 指针可以指向任意类型的对象,并且可以手动管理内存,包括动态分配和释放内存。但这也可能导致悬空指针和内存泄漏等问题。
   - 引用必须在初始化时指定所引用的对象,无法更改对象的引用目标,因此更容易避免悬空引用和内存泄漏等问题。
总结:引用和指针都是用于访问和操作对象的工具,但它们在定义、初始化、赋值、操作和内存管理等方面有一些区别。引用提供了更直接的访问方式,并具有更高的安全性,而指针提供了更多的操作和灵活性。选择使用哪种方式应该根据具体的需求和情况来决定。

22. Cpp新特性知道哪些

1. C++11:
   - 自动类型推导(auto和decltype关键字)

  - `auto` 关键字允许编译器根据变量的初始化值自动推导出变量的类型。例如:
     ```cpp
     auto x = 42; // 推导出x的类型为int
     auto name = "John"; // 推导出name的类型为const char*
     auto result = someFunction(); // 推导出result的类型为函数返回值的类型

   - `decltype` 关键字用于推导表达式的类型。例如:
     ```cpp
     decltype(x) y; // 推导出y的类型与x相同
     decltype(x + y) z; // 推导出z的类型为x和y相加的结果类型
   - 基于范围的for循环

   基于范围的for循环允许遍历容器(如数组、向量、列表等)中的元素,而无需使用迭代器或下标。例如:
   ```cpp
   std::vector<int> numbers = {1, 2, 3, 4, 5};
   for (auto num : numbers) {
       std::cout << num << " "; // 依次打印出1 2 3 4 5
   }
   循环中的 `num` 是自动推导的类型,它将依次取出容器中的每个元素。
   - 列表初始化(braced initialization)

 列表初始化提供了一种简洁的方式来初始化变量,可以使用大括号 `{}` 来括起初始化值。有三种主要的列表初始化方法:
   - 拷贝列表初始化:
     ```cpp
     int x{10}; // 初始化一个整数变量x为10
     std::string name{"John"}; // 初始化一个字符串变量name为"John"

   - 直接列表初始化:
     ```cpp
     std::vector<int> numbers{1, 2, 3, 4, 5}; // 初始化一个整型向量numbers为包含数字1到5的元素
     ```
   - 聚合类型初始化:
     ```cpp
     struct Point {
         int x;
         int y;
     };
     Point p{10, 20}; // 初始化一个Point结构体变量p的成员x为10,成员y为20
   - Lambda表达式

   Lambda表达式是一种方便的匿名函数表示方式,可以在需要函数对象的地方使用。Lambda表达式的语法如下:
   ```
   [捕获列表](参数列表) mutable(optional) 异常属性 -> 返回类型 { 函数体 }
   ```
   示例:
   ```cpp
   auto sum = [](int a, int b) { return a + b; }; // 定义一个接收两个整数参数并返回它们的和的Lambda函数
   int result = sum(3, 4); // 调用Lambda函数,result的值为7
   ```
   在Lambda表达式中,可以使用捕获列表 `[捕获列表]` 捕获外部变量,也可以指定返回类型、声明异常规范等
   - 右值引用(move语义)

   右值引用(也称为移动语义)提供了一种高效的资源管理方式,通过将资源的所有权从一个对象转移到另一个对象,避免了不必要的拷贝操作。右值引用使用双引号 `&&` 表示。示例:
   ```cpp
   std::string str1 = "Hello";
   std::string str2 = std::move(str1); // 使用std::move将str1的值转移给str2
   在移动资源拥有者时,不会进行内存分配和拷贝,而只是修改指针或引用的状态。
   - 智能指针(shared_ptr和unique_ptr)


   - 并发编程库(std::thread和std::mutex等)

2. C++14:
   - 泛型Lambda表达式

泛型Lambda表达式是一个能够在编译时根据参数类型自动推导出返回类型的Lambda表达式。通过使用auto关键字作为参数类型,可以实现泛型函数对象的定义。
   例如,下面是一个简单的泛型Lambda表达式的示例:
   ```cpp
   auto add = [](auto a, auto b) { return a + b; };
   在这个示例中,Lambda表达式使用了auto作为参数类型,因此可以接受任意类型的参数,并根据参数类型自动推导出返回类型。
   - 二进制字面量

   二进制字面量是指以二进制形式表示的整数字面量。C++14引入了这个特性,可以通过在数字前面加上0b或0B前缀来表示二进制字面量。
   例如,下面是一个使用二进制字面量的示例:
   ```cpp
   int binaryValue = 0b1010; // 二进制字面量,表示十进制的10
   - 运行时的常量表达式

   运行时的常量表达式是指在编译时已知值并在运行时保持不变的表达式。C++11引入了constexpr关键字,用于声明运行时常量表达式。
   ```cpp
   constexpr int square(int x) { return x * x; }
   int result = square(5); // result在编译时计算为25
   - 返回类型推导(auto和decltype关键字)

 返回类型推导是通过使用auto或decltype关键字来让编译器自动推导函数的返回类型。
   - auto关键字用于推导函数返回类型为编译时表达式的值的类型。
   ```cpp
   auto add(int a, int b) {
       return a + b;
   }
   ```
   
   - decltype关键字用于根据表达式的类型推导函数的返回类型。
   ```cpp
   int x = 5;
   decltype(x) y = x; // y的类型为int,和x相同的类型
   - 通用的函数调用语法

当我们讨论通用函数调用语法时,我们通常指的是使用类似函数调用的语法来调用自定义类型的对象。下面是一个具体的示例:
```cpp
#include <iostream>
struct MyFunctor {
    void operator()(const std::string& message) {
        std::cout << "MyFunctor: " << message << std::endl;
    }
};
int main() {
    MyFunctor myFunctor;
    myFunctor("Hello, world!"); // 使用通用函数调用语法调用myFunctor对象
    return 0;
}

3. C++17:
   - 结构化绑定(structured bindings)

它允许将复杂的数据结构(例如std::tuple、std::pair、数组等)解构为独立的变量。下面是一个C++的结构化绑定的示例:

```cpp
#include <iostream>
#include <tuple>
int main() {
    // 定义一个tuple
    std::tuple<int, std::string, double> person = std::make_tuple(30, "John", 1.80);
    // 使用结构化绑定解构tuple
    auto [age, name, height] = person;
    // 打印解构后的变量
    std::cout << "Age: " << age << std::endl;      // 输出: Age: 30
    std::cout << "Name: " << name << std::endl;    // 输出: Name: John
    std::cout << "Height: " << height << std::endl; // 输出: Height: 1.8
    return 0;
}
```
在上述示例中,我们首先创建了一个tuple `person`,其中包含一个整数、一个字符串和一个浮点数。然后,我们使用结构化绑定将tuple解构为三个独立的变量`age`、`name`和`height`。这样我们就可以方便地访问和操作这些变量。
除了tuple,结构化绑定也可以用于其他复杂数据结构,例如std::pair、数组等。它的作用是使代码更加简洁和可读性更强。结构化绑定还可以用于迭代器,例如在循环中一次性访问和处理容器中的元素。需要注意的是,结构化绑定只能应用于具有固定大小的数据结构,无法应用于动态大小的容器(例如std::vector、std::list等)。
总之,C++的结构化绑定特性可以方便地将复杂的数据结构解构为独立的变量,提高了代码的可读性和可维护性。
   - if和switch语句中的初始化语句

```cpp
#include <iostream>
int main() {
    if (int x = 42; x > 0) {
        std::cout << "x is positive." << std::endl;
    } else {
        std::cout << "x is non-positive." << std::endl;
    }
    return 0;
}
```
这个示例展示了在if语句中使用初始化语句。在`if (int x = 42; x > 0)`中,我们在if条件中声明并初始化了变量`x`。这样,我们可以在if语句块中使用该变量`x`。这种方式可以使得代码更加简洁和可读。
   - 内联变量(inline variables)
```cpp
#include <iostream>
inline int getNumber() {
    return 42;
}
int main() {
    int myNumber = getNumber();
    std::cout << "My number: " << myNumber << std::endl;
    return 0;
}
```
这个示例展示了如何使用内联变量。在`inline int getNumber() { return 42; }`这行代码中,函数`getNumber()`被声明为内联函数,可以在函数调用处直接进行展开,而不需要进行函数调用的开销。这样可以提高代码的执行效率。
   - 并行算法库(std::execution::par)
```cpp
#include <algorithm>
#include <execution>
#include <iostream>
#include <vector>
int main() {
    std::vector<int> vec = {4, 2, 6, 8, 1, 3,
 5, 7};
    std::sort(std::execution::par, vec.begin(), vec.end());
    for (const auto& num : vec) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}
```
这个示例展示了如何使用并行算法库来并行地对容器进行排序。在`std::sort(std::execution::par, vec.begin(), vec.end())`这行代码中,我们使用了`std::execution::par`作为排序算法的执行策略,表示使用并行方式进行排序。这样可以在支持并行执行的系统上提高算法的执行速度。
   - 文件系统库(std::filesystem)
```cpp
#include <filesystem>
#include <iostream>
int main() {
    std::filesystem::path path("/home/user/example.txt");

    std::cout << "File name: " << path.filename() << std::endl;
    std::cout << "Parent path: " << path.parent_path() << std::endl;
    std::cout << "File extension: " << path.extension() << std::endl;

    return 0;
}
这个示例展示了如何使用文件系统库来处理文件路径。在`std::filesystem::path`这行代码中,我们创建了一个表示文件路径的对象。然后,我们可以使用该对象的成员函数来获取文件名、父路径和文件扩展名等信息。这样可以方便地对文件系统进行操作和管理。
4. C++20:
   - 概念(concepts)

  ```cpp
   template <typename T>
   concept Arithmetic = std::is_arithmetic<T>::value;
   template <Arithmetic T>
   void print(T value) {
       std::cout << value << std::endl;
   }
   int main() {
       print(42);  // 满足概念Arithmetic的类型可以使用print函数
       print("Hello");  // 编译时错误,字符串类型不满足Arithmetic概念
       return 0;
   }
   - 三路比较运算符(spaceship operator)

 三路比较运算符(<=>)是C++20中引入的一种比较运算符,用于比较两个对象的大小关系。它返回一个表示比较结果的值,可以是小于、等于或大于零的整数,这样可以方便地进行对象的排序和比较。

  ```cpp
   struct Point {
       int x;
       int y;
       auto operator<=>(const Point& other) const {
           return std::tie(x, y) <=> std::tie(other.x, other.y);
       }
   };
   int main() {
       Point p1{2, 3};
       Point p2{2, 5};
       if (p1 < p2) {
           std::cout << "p1 is less than p2" << std::endl;
       } else if (p1 > p2) {
           std::cout << "p1 is greater than p2" << std::endl;
       } else {
           std::cout << "p1 and p2 are equal" << std::endl;
       }
       return 0;
   }
   - 初始化器列表中的直接初始化(direct initialization in initializer lists)


   - 协程(coroutines)

协程是一种支持异步编程的语言特性,可以在函数中暂停执行并在稍后的时间点继续执行。C++20引入了协程支持,通过关键字co_await和co_yield,可以实现基于生成器的异步编程模型,简化了异步代码的编写和理解。
   - 模块(modules)

模块是C++20中引入的一种组织和管理代码的机制。它可以将代码划分为逻辑上独立的模块,并提供了更好的封装和可组合性。通过使用模块,可以避免头文件的冗余和依赖问题,提高编译速度和模块化开发的效率。
   - 区间(ranges

 区间是C++20中引入的一种抽象,用于处理和操作序列。区间提供了一个统一的接口来处理容器和迭代器,并提供了丰富的操作符和函数来进行元素的筛选、变换和组合等操作。区间的使用示例包括使用范围for循环、使用标准库算法和自定义操作符等。通过使用区间,可以简化代码并提高可读性和可维护性。
23. 类型转换

1. 隐式类型转换(Implicit type conversion): 有时编译器会自动执行类型转换,将一个类型的值转换为另一个类型的值。这种转换是基于编译器的类型转换规则完成的,例如将整数转换为浮点数,或将较小的数据类型转换为较大的数据类型。
   例如:
   int num = 10;
   double result = num; // 隐式将整数转换为浮点数
   ```
2. 显式类型转换(Explicit type conversion): 在某些情况下,程序员需要显式地指定要进行的类型转换。这可以通过使用类型转换操作符或函数来完成。

   - 静态转换(static_cast): 在编译时执行的一种类型转换,用于执行较为常见的转换操作,例如数值类型之间的转换、基类指针向派生类指针的转换等。

     例如:
     ```
     int num = 10;
     double result = static_cast<double>(num);
     ```
   - 动态转换(dynamic_cast): 在运行时执行的一种类型转换,用于处理多态类型的对象指针或引用的转换。
     例如:
     Base* basePtr = new Derived();
     Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
   - 重新解释转换(reinterpret_cast): 执行较为底层的转换操作,例如将一个指针转换为整数、将一个整数转换为指针等。这种转换可能会违反类型系统的规则,需要慎重使用。
     例如:
     int num = 42;
     char* charPtr = reinterpret_cast<char*>(&num);
   - 常量转换(const_cast): 用于去除指针或引用的常量属性,允许对常量对象进行一定程度的修改。
     例如:
     const int num = 10;
     int* ptr = const_cast<int*>(&num);
     *ptr = 20; // 修改被const修饰的常量对象
24. RAII基于什么实现的(生命周期、作用域、构造析构

1. 生命周期(Lifetime):每个对象都有自己的生命周期,即对象从创建到销毁的过程。RAII利用了对象生命周期的概念,通过在对象的构造函数中获取资源,在析构函数中释放资源,从而保证了资源的正确管理。
2. 作用域(Scope):作用域是指程序中变量的可见性和有效性的范围。RAII利用了作用域的概念,将资源管理的责任委托给对象的作用域。当对象超出其作用域时,其析构函数会自动被调用,从而实现

资源的自动释放。
3. 构造和析构(Construction and Destruction):对象的构造函数在对象创建时被调用,用于初始化对象的状态和获取资源。析构函数在对象销毁时被调用,用于清理对象的状态和释放资源。RAII依赖于构造函数和析构函数的自动调用机制,确保资源的正确获取和释放。
通过将资源的获取和释放与对象的构造和析构联系起来,RAII可以有效地避免资源泄漏和错误的资源使用。它是C++语言中常用的编程范式,特别适用于管理动态分配的内存、文件句柄、网络连接等需要手动释放的资源。
RAII的优点包括简化资源管理、提高代码可读性、降低出错概率和方便异常处理。它是C++中实现资源管理的一种强大范例,也可以在其他编程语言中采用类似的思想实现。
25. 手撕:Unique_ptr,控制权转移(移动语义)

Unique_ptr是C++标准库中的一个智能指针,用于管理动态分配的对象。它通过控制资源的所有权,实现了资源的自动释放功能。下面是一个手撕Unique_ptr的示例代码:

``cpp
template <typename T>
class Unique_ptr {
private:
    T* ptr;
public:
    Unique_ptr(T* p = nullptr) : ptr(p) {}
    ~Unique_ptr() {
        delete ptr;
    }
    // 禁止复制构造函数和复制赋值运算符
    Unique_ptr(const Unique_ptr&) = delete;
    Unique_ptr& operator=(const Unique_ptr&) = delete;
    // 移动构造函数
    Unique_ptr(Unique_ptr&& other) noexcept {
        ptr = other.ptr;
        other.ptr = nullptr;
    }
    // 移动赋值运算符
    Unique_ptr& operator=(Unique_ptr&& other) noexcept {
        if (this != &other) {
            delete ptr;
            ptr = other.ptr;
            other.ptr = nullptr;
        }
        return *this;
    }
    T* operator->() const {
        return ptr;
    }
    T& operator*() const {
        return *ptr;
    }
};
```
上述代码实现了一个简化版的Unique_ptr类。它在析构函数中释放资源,禁止了复制构造函数和复制赋值运算符的使用,通过移动构造函数和移动赋值运算符实现了控制权的转移。
 



手撕:类继承,堆栈上分别代码实现多态

在类继承方面,多态是一种重要的特性,它允许通过基类指针或引用来调用派生类对象的成员函数。下面是一个手撕类继承的示例代码,展示了如何在堆栈上分别实现多态:

```cpp
#include <iostream>

class Animal {
public:
    virtual void makeSound() const {
        std::cout << "Animal makes a sound" << std::endl;
    }
};

class Cat : public Animal {
public:
    void makeSound() const override {
        std::cout << "Cat says meow" << std::endl;
    }
};

class Dog : public Animal {
public:
    void makeSound() const override {
        std::cout << "Dog says woof" << std::endl;
    }
};

int main() {
    Animal* animal1 = new Cat();
    Animal* animal2 = new Dog();
    animal1->makeSound();  // 输出: Cat says meow
    animal2->makeSound();  // 输出: Dog says woof
    delete animal1;
    delete animal2;
    return 0;
}
```
上述代码定义了一个基类Animal和两个派生类Cat和Dog,它们都重写了makeSound()函数。在main()函数中,通过基类指针分别创建了Cat和Dog对象,并调用它们的makeSound()函数,实现了在堆栈上的多态调用。
输出结果:
```
Cat says meow
Dog says woof
```
通过使用虚函数和基类指针,可以实现运行时多态性,允许根据实际的派生类类型来调用相应的函数,在面向对象的程序设计中非常常见和有用。
26. unique_ptr和shared_ptr区别

unique_ptr和shared_ptr是C++标准库中的两种智能指针类型,用于管理动态分配的内存。
unique_ptr是一种独占式智能指针,它确保只有一个unique_ptr可以拥有指向内存的所有权。当unique_ptr被销毁或重置时,它将自动释放所拥有的内存。unique_ptr不能被复制,但可以通过移动语义进行转移所有权。这使得unique_ptr非常适用于避免内存泄漏并确保使用动态分配的对象的唯一所有者。
示例用法:
```cpp
std::unique_ptr<int> ptr(new int(10));
``
shared_ptr是一种共享式智能指针,它可以被多个shared_ptr实例共享相同的指向内存的所有权。它使用引用计数来跟踪有多少个shared_ptr指向相同的内存。只有当最后一个shared_ptr析构或重置时,才会释放所拥有的内存。shared_ptr可以被复制,并在每个复制体之间共享所有权。
示例用法:
```cpp
std::shared_ptr<int> ptr1(new int(10));
std::shared_ptr<int> ptr2 = ptr1; // 共享所有权
```
在选择unique_ptr和shared_ptr之间时,可以根据具体的需求来决定。如果您需要独占访问一个对象并且不希望与其他指针共享所有权,可以选择unique_ptr。如果您需要多个指针来共享访问同一对象,可以选择shared_ptr。
27. 右值引用

右值引用是C++11引入的一种新的引用类型,用于表示可以被移动或转移所有权的临时对象或即将过期的对象。
在C++中,左值(lvalue)是可以被引用的表达式,而右值(rvalue)是临时创建的、没有引用的表达式。右值引用允许我们绑定到右值,并使用移动语义来转移资源的所有权,而不是进行昂贵的复制操作。
右值引用的声明使用双引号&&符号:
```cpp
T&& a = b;
```
其中,T表示类型,a是一个右值引用,b是一个右值。右值引用可以绑定到临时对象、将亡值(即将被销毁的对象)或通过std::move转换得到的对象。
右值引用主要用于实现移动语义,用于提高代码的效率和性能。通过移动语义,可以在不进行深拷贝的情况下将资源从一个对象转移到另

  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值