C++面试

目录

1.线程池的数量一般怎么设置?IO密集和CPU密集

2.来了一个新的任务,线程池是怎么工作的?

3.RAII指?在C++11中有什么用了RAII

4.完美转发介绍一下 去掉std::forward会怎样?

5.介绍一下unique_lock和lock_guard区别?

6.shared_lock

7.条件变量的wait

8.多态介绍一下

9.虚函数原理 虚表是什么时候建立的

10.为什么要把析构函数设置成虚函数?

11.动态多态和静态多态有什么?

12.C代码中引用C++代码有时候会报错为什么?


1.线程池的数量一般怎么设置?IO密集和CPU密集

线程池的数量设置对系统性能和资源占用有很大的影响。一般来说,线程池的数量要根据系统的特点、任务的类型、系统负载和可用资源等因素来考虑,具体设置方法如下:

对于I/O密集型任务,线程的主要时间都用在等待I/O操作的完成上,CPU的利用率很低,可以设置较多的线程数,并且线程数可大于物理CPU总数。根据经验,线程数可设置为2 * CPU核数 + 1左右。

对于CPU密集型任务,线程的时间大部分用在CPU计算上,线程数不能设置过多,否则系统会出现过多的线程切换和竞争,导致CPU利用率下降。一般来说,线程数应该小于等于CPU核心数。

但是,线程池的数量也不是越大越好,因为线程池数量过多也会占用大量的系统资源,甚至影响系统的稳定性。所以,在设置线程池数量时,还需结合具体应用场景进行优化。

2.来了一个新的任务,线程池是怎么工作的?

线程池是由一组多个线程组成的资源池,它可以用来执行并发的任务,从而提高程序的运行效率。具体地说,线程池会在程序启动时预先创建一定数量的线程,并将这些线程保存在一个池中;当需要执行任务时,线程池会从池中取出一个空闲的线程,并将任务分配给其执行;当任务执行完成后,线程将会返回池中等待下一个任务的到来。

使用线程池可以避免反复地创建和销毁线程带来的开销,节约系统资源,并且可以提高系统的并发性能。但是线程池的数量设置需要根据任务类型、系统负载和可用资源等因素来考虑,过多或过少的线程池都会影响系统的执行效率。

3.RAII指?在C++11中有什么用了RAII

RAII是Resource Acquisition Is Initialization的缩写,指的是资源的获取与释放应该在对象的构造和析构时完成。在C++11中,RAII被广泛使用,特别是在管理动态内存和文件句柄等资源方面。使用RAII可以避免内存泄漏和资源泄漏的问题,使得代码更加可靠和安全。通过使用标准库中的智能指针、容器等封装类,可以更加方便地使用RAII。同时,C++11还引入了移动语义,进一步简化了RAII的使用。

4.完美转发介绍一下 去掉std::forward会怎样?

完美转发是在 C++11 中引入的一种技术,用于将函数传递的参数(包括左值和右值)转发到另一个函数中,同时保持参数的原始类型和值类别。

在使用完美转发时,通常需要使用 std::forward 函数模板来实现参数转发。该函数模板具有以下特点:

  • 对于右值参数,std::forward 返回右值引用。
  • 对于左值参数,std::forward 返回左值引用。

如果不使用 std::forward 来实现参数转发,而是直接使用对应的引用类型来传递参数,则会出现参数类型和值类别被截断的情况,导致代码出现编译错误或者运行时错误。

例如,考虑以下示例代码:

void foo(int& x)
{
    std::cout << "Lvalue: " << x << std::endl;
}

void foo(int&& x)
{
    std::cout << "Rvalue: " << x << std::endl;
}

template 
void bar(T&& x)
{
    foo(x);
}

int main()
{
    int i = 5;
    bar(i);
    bar(10);
    return 0;
}

在 bar 函数内部,我们试图将传递给 bar 的参数 x 转发到 foo 函数中。由于 x 的类型为 T&&,可以接受任意类型的参数,包括左值和右值。但是,如果我们在调用 foo 函数时,使用 x 作为参数,则会出现编译错误,因为 x 可能是一个左值引用,但是 foo 函数却只接受右值引用或者左值引用。因此,我们需要使用 std::forward 来实现参数的完美转发,即:

template 
void bar(T&& x)
{
    foo(std::forward(x));
}

这样就可以实现完美转发,保持参数类型和值类别的一致性了。

5.介绍一下unique_lock和lock_guard区别?

unique_locklock_guard都是C++标准库提供的互斥锁的RAII封装,用来自动管理锁的获取和释放。

它们的主要区别是unique_lock提供了更多的灵活性和可定制性,但是相应地也会带来更大的开销。

具体来说,unique_lock的特点是:

  • 可以手动控制互斥锁的加锁和解锁操作,即可以在任何时候释放锁以及在加锁时传入不同的锁模式(如普通锁、递归锁等)。
  • 支持defer_lock模式,即创建一个没有锁定的unique_lock对象,并在以后的某个时刻通过调用lock方法来手动获得锁。
  • 支持try_lock模式,即尝试去获取锁并返回成功或失败的结果,避免了程序因无法获取锁而一直阻塞。
  • 由于具有以上更灵活的特性,因此unique_lock通常比lock_guard更适用于需要更复杂的锁控制的场景。

lock_guard的特点是:

  • 只能在构造时加锁,在析构时解锁,无法手动控制加锁和解锁操作。
  • 由于只是简单的加锁和解锁操作,因此执行效率比unique_lock更高。

综上所述,对于需求简单的锁控制场景,使用lock_guard更为合适。而在需要更复杂的锁控制场景中,或者需要在多个代码路径中控制锁的加锁和解锁时,使用unique_lock会更好。

6.shared_lock

shared_lock 是 C++11 中标准库提供的一个读取锁,支持多个线程同时读取一个共享的资源,但只有一个线程能够写入。这种锁可以用来优化读操作远小于写操作的情况,以避免因为读写互斥锁而导致的性能瓶颈。

使用shared_lock 需要引入  头文件,它与 std::mutex 类似,支持 lock 和 unlock 操作,但也有 lock_shared 和 unlock_shared 操作用于读取锁。当 shared_lock 析构时,自动调用解锁操作。

以下是一个简单的示例:

#include 
#include 
#include 
#include 

std::shared_mutex my_mutex;

void write_operation() {
    std::unique_lock writer_lock(my_mutex);
    std::cout << "Writing to shared resource" << std::endl;
    // 写操作 ...
}

void read_operation() {
    std::shared_lock reader_lock(my_mutex);
    std::cout << "Reading from shared resource" << std::endl;
    // 读操作 ...
}

int main() {
    std::thread th1(write_operation);
    std::thread th2(read_operation);
    std::thread th3(read_operation);
    th1.join();
    th2.join();
    th3.join();
    return 0;
}

在这个示例中,我们使用了一个 std::shared_mutex 作为共享锁,三个线程分别进行写和读操作,由于写操作使用了 unique_lock 而读操作使用了 shared_lock,因此多个线程可同时读取但不能同时写入。

7.条件变量的wait

条件变量的 `wait` 函数可以让线程在满足某个特定条件之前处于等待状态。当 `wait` 函数被调用时,它会先释放互斥锁,并阻塞当前线程,直到其他线程调用了相关的条件变量的 `notify` 函数来通知该线程特定的条件已经满足,然后线程重新获得互斥锁,并从 `wait` 函数中返回,可以继续正常执行。如果线程在 `wait` 期间遇到了 `spurious wakeup`,也就是在没有收到 `notify` 的情况下被唤醒,那么它会重新测试条件,如果条件仍然不满足,那么它会继续等待下去。

需要注意的是,在使用条件变量时,我们需要先拥有某个互斥锁的所有权,以避免数据的竞争和并发问题。严格地说,条件变量仅描述了等待和通知的机制,并不能提供保护数据不被并发访问的能力。

8.多态介绍一下

多态是面向对象编程中的概念,指的是不同的对象对同一个消息做出不同的响应的能力。其核心思想是同一类型的对象在运行时可以表现出不同的行为。在 C++ 中,通常通过虚函数和指针或引用来实现多态性。

举个例子,假如有一个 “动物” 类,它有一个虚函数 eat(),而其子类 “狗” 和 “猫” 分别继承了 “动物” 类,并且均重载了 eat() 函数。如果我们用一个父类指针指向一个子类对象,并调用 eat() 函数,那么在运行时会根据指针的实际类型,调用相应子类的 eat() 函数。

利用多态能够增加代码的复用性和灵活性,这也是面向对象编程的一大优势。

9.虚函数原理 虚表是什么时候建立的

在 C++ 中,虚函数通过虚函数表来实现。虚函数表是一个包含虚函数指针的表格,对应着每一个有虚函数的类,由编译器在编译时生成。当一个类被实例化时,这个类的对象上会存在一个指向它的虚函数表的指针(通常是首个指针)。虚函数则通过在虚函数表中的索引调用对应的函数,这样可以实现运行时多态的特性。

虚表是在编译期间由编译器生成的,它记录了类中所有的虚函数的地址。当类的对象被创建时,虚函数表的指针被放在这个对象的头部,从而使得这个对象能够在运行时准确地找到它的虚函数地址。虚函数表的地址是在编译期就确定的,而不是在运行时动态生成的。因此,如果子类中覆盖了父类的虚函数,那么子类的虚表指针会指向子类的虚表,从而调用到子类中的实现。

10.为什么要把析构函数设置成虚函数?

把析构函数设置成虚函数,是为了解决多态的问题。

当子类继承自父类,并且父类的指针指向子类对象时,如果父类的析构函数不是虚函数,则在删除指向子类对象的父类指针时只会调用到父类的析构函数,而子类的析构函数不会被调用,就会造成内存泄漏。而如果将父类的析构函数设置成虚函数,在删除指向子类对象的父类指针时,会先调用子类的析构函数,然后再调用父类的析构函数,就能够正确地释放掉动态分配的子类对象,解决了多态下的内存泄漏问题。

因此,C++11规定,对于一个基类,如果它有虚函数,那么它的析构函数必须也是虚函数。在实际编程中,可以避免多态导致的内存问题,保证程序的正确性

11.动态多态和静态多态有什么?

多态性是面向对象编程中的一个重要概念,允许同一个方法名在不同的对象类型中执行不同的操作。其中静态多态和动态多态是两种不同的多态形式。

静态多态,也称编译期多态,是在编译时就确定方法的调用。这种多态性是通过函数重载、运算符重载和模板来实现的。因为在编译时就已经知道了方法的实际调用,所以它的执行速度相对较快。

动态多态,也称运行期多态,是在运行时根据实际对象的类型来确定方法的调用。这种多态性是通过一个基类的指针或引用来访问派生类的对象,从而让同一个方法名可以在不同的对象类型中执行不同的操作。在运行时需要进行一些额外的开销,但这种方式更加灵活,可以实现更强大的程序逻辑。

总的来说,静态多态是在编译时解析方法调用,而动态多态是在运行时解析方法调用,两者的实现方式不同,并且各有优缺点,需要根据实际情况选择。

12.C代码中引用C++代码有时候会报错为什么?

在C语言中使用C++代码可能会出现报错的原因有很多,其中最常见的是编译器不支持C++特定的编译方式或语法。由于C++语言比C语言更加复杂,带有更多的特性和语法,它需要更高级的编译器来进行编译。因此,如果您在C代码中包含了C++代码,您需要确保使用的是C++编译器,并将源文件的后缀从“.c”更改为“.cpp”,这样编译器就会使用正确的语法和编译选项来编译代码。

此外,还有一些C++的特性,如类、模板、命名空间等,在C语言中并不支持。如果您在C代码中使用这些特性,会导致编译器报告错误。为了解决这个问题,您可以将C++代码封装到一个库中,然后在C代码中使用该库。

总之,在C代码中使用C++代码需要注意编译器的支持以及源文件的文件后缀需要更改。如果您需要使用C++特性,也需要将C++代码封装成库以供C代码使用。

你可能感兴趣的:(面经,面试,java,jvm)