2023最新C++面经(一):vector内存预分配,左值引用和右值引用,move语义

文章目录

  • 零、前言
  • 一、在C++中,往vector插入1000个数字,怎么做能保证性能最高
  • 二、在vector中对10000个数字删除偶数位置的数,怎么做保证性能较高
  • 三、malloc用delete会出现什么问题
  • 四、weak_ptr解决的是什么问题,lock返回的对象可以直接使用吗
  • 五、C++中的左值引用和右值引用是什么? 如果不写移动构造函数,那调用的会是什么?
  • 六、移动构造函数实例
  • 七、std::move
  • 八、对于map和unordered_map容器怎么分别做到对字母的大小写不敏感


零、前言

今天在朋友圈看到别人发的一套C++面经,特意思考了一下

一、在C++中,往vector插入1000个数字,怎么做能保证性能最高

以下是一些可以提高在C++中向vector中插入大量数字性能的技巧:

  1. 预先分配内存空间:使用vector::reserve函数预先分配足够的内存空间,以避免在插入元素时进行重新分配内存的开销。例如,如果你需要插入1000个数字,可以使用vector::reserve(1000)在插入前先为向量分配1000个元素的空间。
  2. 使用移动语义:使用std::move将元素插入vector,而不是使用拷贝构造函数。因为拷贝构造函数会导致新的内存分配和数据复制,而使用移动语义可以避免这些开销。例如,可以使用以下代码向vector中插入一个元素:
vector<int> v;
int x = 123;
v.push_back(std::move(x));
  1. 批量插入元素: 使用std::vector::insert函数批量插入元素,而不是使用单个插入函数。因为单个插入函数可能会多次进行内存分配和内存复制,而批量插入函数可以减少这些开销:
vector<int> v;
vector<int> data(1000);
v.insert(v.end(),data.begin(),data.end());
  1. 使用移动迭代器: 使用std::make_move_iterator将元素转换为移动迭代器以避免拷贝构造函数的开销。
std::vector<std::string> v;
std::string str = "hello";
v.insert(v.end(), std::make_move_iterator(std::begin(str)), std::make_move_iterator(std::end(str)));

二、在vector中对10000个数字删除偶数位置的数,怎么做保证性能较高

以下是一些可以提高在C++中从vector中删除偶数位置的数字的性能的技巧:

  1. 使用erase-remove惯用法:使用erase-remove惯用法从vector中删除偶数位置的数字。该惯用法的思想是先使用std::remove_if算法将所有需要删除的元素移动到vector的末尾,然后再使用std::vector::erase函数删除这些元素。例如,可以使用以下代码从vector中删除偶数位置的数字:
std::vector<int> v{...}; // 假设有10000个数字
v.erase(std::remove_if(v.begin(), v.end(), [](const auto& x){ return &x - &v[0] % 2 == 0; }), v.end());
  1. 使用reserve预分配内存:在删除元素之前使用std::vector::reserve函数预分配足够的内存,以避免在移动元素时进行重新分配内存的开销。例如,可以使用以下代码预分配内存:
std::vector<int> v{...}; // 假设有10000个数字
v.reserve(v.size() / 2);
  1. 使用迭代器递增器:在使用std::remove_if算法时,使用迭代器递增器std::next来遍历vector中的元素,而不是使用索引操作符。例如,可以使用以下代码遍历vector中的元素
std::vector<int> v{...}; // 假设有10000个数字
v.erase(std::remove_if(v.begin(), v.end(), [i = 0](const auto&){ return i++ % 2 == 0; }), v.end());

该代码使用了lambda表达式作为std::remove_if算法的谓词,其中[i = 0]表示定义了一个名为i的变量,并初始化为0。i++ % 2 == 0表示先取i的值,然后将i加1,再判断该值是否为偶数。使用迭代器递增器可以避免使用索引操作符的性能开销。

在这句代码中,const auto&是一个lambda表达式的参数,它代表了一个引用类型的元素。lambda表达式是一个匿名函数,它可以接受一个或多个参数,这些参数可以是值类型、引用类型或者指针类型。

在这个lambda表达式中,使用const auto&作为参数,表示接受一个任意类型的常量引用。在std::remove_if算法中,该lambda表达式作为谓词被调用,每次调用时会传入vector中的一个元素,并对该元素进行判断。由于lambda表达式中的参数是一个常量引用,所以在对该元素进行判断时不会改变原始vector中的元素。

在该lambda表达式中,i++ % 2 == 0表示先取i的值,然后将i加1,再判断该值是否为偶数。由于在lambda表达式中定义了一个变量i,所以可以通过该变量来判断元素在vector中的位置是否为偶数位置。

三、malloc用delete会出现什么问题

malloc()和delete是两个不同的内存分配和释放函数,它们具有不同的行为和约定。使用malloc()分配的内存需要使用free()函数进行释放,而使用new运算符分配的内存需要使用delete运算符进行释放。因此,如果使用delete释放使用malloc()分配的内存,会导致内存错误和未定义的行为。

使用delete释放使用malloc()分配的内存,可能会导致以下问题:

  1. 内存泄漏:如果使用delete释放使用malloc()分配的内存,将无法正确释放内存,导致内存泄漏。

  2. 崩溃或异常:使用delete释放使用malloc()分配的内存,可能会导致程序崩溃或抛出异常,因为delete假设它的参数是通过new分配的内存。

  3. 不正确的内存释放:使用delete释放使用malloc()分配的内存,可能会释放掉不属于该内存块的内存,或者释放掉一个已经被释放的内存块,导致不正确的内存释放。

四、weak_ptr解决的是什么问题,lock返回的对象可以直接使用吗

weak_ptr是C++11中引入的一种智能指针类型,用于解决shared_ptr循环引用导致的内存泄漏问题。在使用shared_ptr进行循环引用时,如果存在两个或多个shared_ptr相互引用,那么它们之间的引用计数永远不会达到0,导致分配的内存永远无法被释放。而weak_ptr则可以作为一个非拥有者指针,指向shared_ptr所管理的对象,但不会增加引用计数,因此不会导致循环引用和内存泄漏问题。

lock()函数是weak_ptr提供的一个成员函数,用于返回一个指向weak_ptr所管理对象的shared_ptr对象。lock()函数首先检查weak_ptr所指向的对象是否还存在,如果存在,则返回一个指向该对象的shared_ptr对象;否则,返回一个空的shared_ptr对象。使用lock()函数获得shared_ptr对象后,可以使用该对象来访问weak_ptr所管理的对象。需要注意的是,在使用lock()函数获得shared_ptr对象之前,需要检查lock()返回的shared_ptr对象是否为空,如果为空则表示weak_ptr所指向的对象已经被销毁了。

返回的shared_ptr对象可以直接使用,但需要注意其生命周期。shared_ptr对象在所有指向该对象的shared_ptr对象都被销毁后才会自动销毁,因此如果需要使用shared_ptr对象,需要确保该对象的生命周期符合预期,以避免内存泄漏等问题。另外,由于weak_ptr对象只是一个非拥有者指针,它并不拥有shared_ptr所管理的对象,因此需要确保在使用lock()函数返回的shared_ptr对象之前,shared_ptr所管理的对象仍然存在,否则会出现未定义的行为。

五、C++中的左值引用和右值引用是什么? 如果不写移动构造函数,那调用的会是什么?

左值引用和右值引用都是C++中的引用类型,用于表示一个对象的别名。其中左值引用表示对一个左值对象的引用,而右值引用表示对一个右值对象(包括临时对象和将要销毁的对象)的引用。

左值引用的声明形式为T&,表示对类型为T的左值对象的引用;而右值引用的声明形式为T&&,表示对类型为T的右值对象的引用。例如,以下代码声明了一个左值引用和一个右值引用:

int x = 123;
int& lref = x;  // 左值引用
int&& rref = 123;  // 右值引用

在C++11之前,如果没有编写移动构造函数,当一个对象被移动构造时,实际上是调用了该对象的拷贝构造函数。因此,如果一个类没有显式定义移动构造函数,但是有拷贝构造函数,当使用右值引用移动构造该对象时,实际上是调用该类的拷贝构造函数。

在C++11及之后的版本中,如果一个类定义了移动构造函数,那么当使用右值引用移动构造该对象时,实际上是调用该类的移动构造函数。移动构造函数是一种特殊的构造函数,用于将一个对象的资源(如堆内存、文件句柄等)从一个对象转移到另一个对象,而不进行数据的复制。使用移动构造函数可以避免拷贝大量数据的开销,提高程序的性能。

需要注意的是,如果一个类既定义了拷贝构造函数,又定义了移动构造函数,那么在使用右值引用移动构造该对象时,会优先调用移动构造函数。如果没有定义移动构造函数,但是定义了移动赋值运算符,那么在使用右值引用进行赋值操作时,会调用移动赋值运算符。如果同时没有定义移动构造函数和移动赋值运算符,但是定义了析构函数,那么在对象销毁时,会使用拷贝构造函数进行拷贝构造。如果以上所有函数都没有定义,那么编译器会生成默认的函数实现。

六、移动构造函数实例

class MyClass {
public:
  // 移动构造函数
  MyClass(MyClass&& other) noexcept {
    // 将other对象的资源移动到当前对象
    data_ = other.data_;
    size_ = other.size_;
    // 将other对象的资源释放
    other.data_ = nullptr;
    other.size_ = 0;
  }
  
  // 其他成员函数
  // ...
  
private:
  int* data_;
  size_t size_;
};

该移动构造函数使用右值引用作为参数类型,并且使用了noexcept关键字,表示该函数不会抛出异常。在移动构造函数中,首先将另一个对象的资源移动到当前对象,然后将另一个对象的资源释放,以避免资源的重复释放。

在该例子中,假设data_是一个指向堆内存的指针,size_表示堆内存的大小。在移动构造函数中,将另一个对象的data_指针和size_成员移动到当前对象,然后将另一个对象的data_指针置为nullptr,size_成员置为0,以表示另一个对象的资源已经被移动到当前对象。

移动构造函数可以在类中定义,也可以在类外定义。如果在类外定义,需要在函数名前添加类名和作用域解析符::。需要注意的是,移动构造函数的参数应该是一个非const右值引用,并且应该使用noexcept关键字标记,以便在移动操作时可以获得更好的性能和安全性。

移动构造函数的参数为什么要是一个非const右值引用呢? 这是因为移动构造函数的参数为非const右值引用是为了表示该参数是一个将要销毁的对象(右值对象),并且该对象的资源可以被移动到另一个对象中。非const右值引用参数的特点是不能被赋值、取地址和修改值,因此移动构造函数可以对该参数进行资源的“窃取”,而不必担心该对象被修改或复制。此外,由于移动构造函数可以修改参数对象的状态,因此该参数不能是const类型的,否则会导致编译错误。

七、std::move

std::move是C++11引入的一个函数模板,用于将一个左值转换为右值引用,以便在移动构造函数和移动赋值运算符中使用。std::move本质上只是将一个左值强制转换为右值引用,它并不会实际地移动对象或改变对象的状态。

在正常情况下,使用std::move可以将一个左值对象的资源(如堆内存、文件句柄等)移动到一个新的对象中,从而避免了拷贝大量数据的开销,提高了程序的性能。使用std::move时需要注意以下几点:

  1. std::move只能用于可移动的对象:使用std::move时,需要确保对象的类型支持移动语义,即有移动构造函数和移动赋值运算符。如果对象没有移动构造函数和移动赋值运算符,则无法使用std::move进行移动操作。
  2. 移动后对象的状态:使用std::move将一个对象的资源移动到另一个对象中后,原始对象的状态可能会发生变化,例如指针会被置为空、计数器会被减少等。需要确保移动后对象的状态符合预期,以避免错误的操作。
  3. 临时对象的生命周期:在某些情况下,使用std::move将一个左值对象转换为右值引用后,会生成一个临时对象。例如,将一个std::vector对象传递给函数时,如果使用std::move将该对象转换为右值引用,则会生成一个临时对象。需要确保临时对象的生命周期符合预期,以避免临时对象在不适当的时候被销毁或者访问已经失效的对象。

八、对于map和unordered_map容器怎么分别做到对字母的大小写不敏感

在map和unordered_map容器中实现大小写不敏感,可以通过自定义比较函数来实现。具体来说,可以重载std::map和std::unordered_map容器中的Compare类型,定义一个自己的比较函数对象,并将其作为容器的第三个模板参数传递进去。比较函数对象需要重载operator()运算符,对于字符串类型的键,可以在比较之前将所有字符转换成小写或大写字母,然后再进行比较。
以下是一个示例代码:

#include 
#include 
#include 
#include 
#include 

struct CaseInsensitiveLess
{
    bool operator()(const std::string& lhs, const std::string& rhs) const
    {
        std::string lstr(lhs);
        std::string rstr(rhs);
        std::transform(lstr.begin(), lstr.end(), lstr.begin(), ::tolower);
        //transform函数: transform是一个通用算法,用于对指定区间内的元素进行转换,并将结果存储在另一个区间中。transform算法接受两个输入迭代器和一个输出迭代器作为参数,以及一个一元或二元操作函数对象(可通过函数指针、函数对象、Lambda表达式等形式指定),将指定区间内的元素依次传入操作函数进行转换,然后将转换后的结果存储到输出迭代器指向的位置。
        std::transform(rstr.begin(), rstr.end(), rstr.begin(), ::tolower);
        return lstr < rstr;
    }
};

int main()
{
    std::map<std::string, int, CaseInsensitiveLess> m;
    m["a"] = 1;
    m["B"] = 2;
    m["c"] = 3;
    std::cout << m["A"] << std::endl; // 输出 1
    std::cout << m["b"] << std::endl; // 输出 2

    std::unordered_map<std::string, int, std::hash<std::string>, CaseInsensitiveLess> um;
    um["a"] = 1;
    um["B"] = 2;
    um["c"] = 3;
    std::cout << um["A"] << std::endl; // 输出 1
    std::cout << um["b"] << std::endl; // 输出 2

    return 0;
}

你可能感兴趣的:(C++,c++,算法,jvm)