【面经专栏】独家整理【C++】面经

C++

1、动态库的查找顺序
  1. ELF文件的DT_RPATH段
  2. 环境变量LD_LIBRARY_PATH
  3. 库高速缓存/etc/ld.so.cache文件列表
  4. 默认路径/lib /usr/lib
2、虚继承,多重继承中派生类对象内有谁
3、RTTI,typeid是怎么知道指向对象的类型的

​ typeid函数的主要作用就是让用户知道当前的变量是什么类型的,比如使用typeid(a).name()就能知道变量a是什么类型的。typeid()函数的返回 类型为type_info类型的引用

​ 运行时的多态是通过vtable里面插入的std::type_info来做的,然后在运行时通过取vtable的type_info来达到目的。

​ 如果表达式的类型是类类型且至少包含有一个虚函数,则typeid操作符返回表达式的动态类型,需要在运行时计算;否则,typeid操作符返回表 达式的静态类型,在编译时就可以计算。

​ type_info的name成员函数返回C-style的字符串,用来表示相应的类型名,但务必注意这个返回的类型名与程序中使用的相应类型名并不一定一 致(往往如此,见后面的程序),这具体由编译器的实现所决定的,标准只要求实现为每个类型返回唯一的字符串

4、STL中迭代器继承与其他算法容器的继承思想
  1. 为什么要将迭代器分为这五类,而且为什么要将它们设计为这种继承体系呢?在学习C++继承的时候,我们知道,位置继承体系越后的类,功能越强大,但是考虑的东西也会越多,体型也会越臃肿。为了提供最大化的执行效率,STL在设计算法时,会尽量提供一个最明确最合适的迭代器,在完成任务的同时,也尽量提高算法的效率。
  2. 前向 双向 随机
5、float在内存中的存储
6、const 和 constexpr的作用和区别
  • const 语义是只读,所以可以在运行时进行初始化。const是**变量类型名的一部分,**即 part of type name,一个名字叫“const T”或者“T const”的类型,和T这个类型本身处于一种平级的关系,和T不同的就在于这个类型的对象自产生后就不能再更改了。

  • constexpr 语义是常量,所以编译时就要进行初始化。constexpr是声明的一部分,即 part of a declaration,他不是变量类型的一部分。当他出现在一个变量的声明中时,他要求编译器在编译期间就初始化并确定下来这个变量(否则编译报错);当他出现在一个函数的声明中时,他要求至少有一条return路径可以(但不是必须)在编译中确定下来,即返回的是编译期常量。

  • 二者的联系就在于,在使用constexpr声明一个类型为T的变量时,constexpr会自动把这个变量定义为const T类型。即constexpr在完成它本职工作(告诉编译器这是个编译期常量)的同时,还把原来的T类型改为了const T类型。这就是二者的联系

7、C++中的锁

​ C++11线程之间的锁有:互斥锁、条件锁、自旋锁、读写锁、递归锁

  1. 互斥锁:
    • 头文件:< mutex >
    • 类型: std::mutex
    • 用法:在C++中,通过构造std::mutex的实例创建互斥元,调用成员函数lock()来锁定它,调用unlock()来解锁,不过一般不推荐这种做法,标准C++库提供了std::lock_guard类模板,实现了互斥元的RAII惯用语法。std::mutex和std::lock _ guard。都声明在< mutex >头文件中。
  2. 条件锁
    • 头文件:< condition_variable >
    • 类型:std::condition_variable(只和std::mutex一起工作)
    • wait():检查条件,并在满足时返回。如果条件不满足,wait()解锁互斥元,并将该线程置于阻塞或等待状态。当来自数据准备线程中对notify_one()的调用通知条件变量时,线程从睡眠状态中苏醒(解除其阻塞),重新获得互斥元上的锁,并再次检查条件,如果条件已经满足,就从wait()返回值,互斥元仍被锁定。如果条件不满足,该线程解锁互斥元,并恢复等待
  3. 自旋锁
    • 互斥锁的工作原理,互斥锁是是一种sleep-waiting的锁。假设线程T1获取互斥锁并且正在core1上运行时,此时线程T2也想要获取互斥锁(pthread_mutex_lock),但是由于T1正在使用互斥锁使得T2被阻塞。当T2处于阻塞状态时,T2被放入到等待队列中去,处理器core2会去处理其他任务而不必一直等待(忙等)。也就是说处理器不会因为线程阻塞而空闲着,它去处理其他事务去了
    • 自旋锁是一种busy-waiting的锁。也就是说,如果T1正在使用自旋锁,而T2也去申请这个自旋锁,此时T2肯定得不到这个自旋锁。与互斥锁相反的是,此时运行T2的处理器core2会一直不断地循环检查锁是否可用(自旋锁请求),直到获取到这个自旋锁为止。
    • 当发生阻塞时,互斥锁可以让CPU去处理其他的任务;而自旋锁让CPU一直不断循环请求获取这个锁。通过两个含义的对比可以我们知道“自旋锁”是比较耗费CPU的。
  4. 读写锁
    • 头文件:boost/thread/shared_mutex.cpp
    • 类型:boost::shared_lock
    • 计算机中某些数据被多个进程共享,对数据库的操作有两种:一种是读操作,就是从数据库中读取数据不会修改数据库中内容;另一种就是写操作,写操作会修改数据库中存放的数据。因此可以得到我们允许在数据库上同时执行多个“读”操作,但是某一时刻只能在数据库上有一个“写”操作来更新数据。
8、const修饰成员函数
  • const类对象只能调用const成员函数,不能调用普通成员函数;
  • const成员函数不能修改类的成员变量,若要修改则用mutable修饰该成员变量。
8-2、 const在什么时候初始化

从代码到可执行二进制文件的角度上来说,const生效于编译阶段

从程序执行的角度上说,const变量必须在定义的时候初始化,这也导致如果类的成员变量有const类型的变量,该变量必须在类的初始化列表中进行初始化

8-3、const作用在函数形参上,是传值还是传引用才有用

传引用才有用

传值是形参创建一个和实参相同的临时变量,无论是否对该临时变量使用const,都不会影响实参的值

只有在形参是传引用或者传指针的时候,使用const才能保护实参不被修改

8-4、const修饰的变量存在哪里

const变量的内存位于栈区(局部const)或者静态存储区(全局const)

9、vector的push_back时间复杂度是多少

​ 虽然存在扩容,但是均摊下来的push_back时间复杂度还是O(1)

11、返回值可以作为函数重载的标志么

不能,因为在不需要返回值的场景中,不能得到区分

12、用开链地址法解决hash冲突时,开链法链表太长怎么办

改变解决哈希冲突的规则:线性探测、再哈希(更换哈希函数)

若不改变开链法,就将链表树化。

还可以考虑一致性哈希中的虚拟节点

13、怎样实现红黑树
  • RBTree为二叉搜索树,我们按照二叉搜索树的方法对其进行节点插入
  • RBTree有颜色约束性质,因此我们在插入新节点之后要进行颜色调整

具体步骤如下:

  1. 根节点为NULL,直接插入新节点并将其颜色置为黑色
  2. 根节点不为NULL,找到要插入新节点的位置
  3. 插入新节点
  4. 判断新插入节点对全树颜色的影响,更新调整颜色
14、如何实现线程安全的map

使用读写锁

15、启动多线程的方法

使用 std::thread 来创建一个线程实例,创建完会自动启动,只需要给它传递一个要执行函数的指针即可,这个函数指针可以指向普通函数、仿函数对象、Lambda表达式、非静态成员函数、静态成员函数。

16、回调函数对内存的操作

回调函数在功能上相当于一个中断处理函数,由系统在符合程序员设定的条件时自动调用

指针是一个变量,是用来指向内存地址的。一个程序运行时,所有和运行相关的物件都是需要加载到内存中,这就决定了程序运行时的任何物件都可以用指针来指向它。函数是存放在内存代码区域内的,它们同样有地址,因此同样可以用指针来存取函数,把这种指向函数入口地址的指针称为函数指针。

17、函数返回地址与参数的压栈顺序

C/C++中规定了函数参数的压栈顺序是从右至左,对于含有不定参数的printf函数,其原型是printf(const char* format,…);其中format确定了printf的参数(通过format的%个数判断)。假设是从左至右压栈,那么先入栈的是format(这里我们简化理解为参数个数),然后依次入栈未知参数,此时想要知道参数个数,就必须找到format,而要找到format,就必须知道参数个数,陷入一个逻辑矛盾。因此C/C++中规定参数压栈为从右至左,这样对于不定参数,最后入栈的是参数个数,只需要取栈顶就可以得到。

18、move和forward的区别

move是为了避免对临时变量的拷贝(拷贝临时变量到一个左值变量上,再回收临时变量,这种拷贝是不必要的)。使用move将传入的临时变量(左值)的资源直接转移给右值引用,能够避免不必要的拷贝。

std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义

调用move后,可以销毁一个移后源对象,也可以赋予他新值,但不能使用一个移后源对象的值

终于能处理临时变量了,处理临时变量用右值引用string &&, 处理普通变量用const引用const string &…,每一次必须要重载一个新函数么?为了减少代码重复,引入了forward

forward中包含了move的功能

  1. 如果外面传来了右值,它就转发右值并且启用move语义.
  2. 如果外面传来了左值,它就转发左值并且启用拷贝,然后它也还能保留const

技术上来说, forward确实可以替代所有的move,但还有一些问题:

  1. forward常用于template函数中, 使用的时候必须要多带一个template参数T: forward, 代码略复杂;
  2. 还有, 明确只需要move的情况而用forward, 代码意图不清晰, 其他人看着理解起来比较费劲.

完美转发:https://zhuanlan.zhihu.com/p/161039484

19、静态成员函数怎么访问非静态成员

因为静态函数属于类的公共部分(不属于任何一个对象独有),没有this指针,也就不能在没有外力帮助的情况下访问类的非静态成员(因为非静态成员都是对象独有的)。

静态成员函数要想访问非静态成员,要借助外力:

  • 在静态函数的形参表里加上对象的地址
class A {
public:
    static void test(A *a) {
        a->m_a += 1;
    }
    void hello(){}
private:
    static int m_staticA;
    int m_a
};
  • 使用全局变量(一个全局对象)
A g_a;

class A {
public:
    static void test() {
        g_a.m_a += 1;
    }
    void hello() {}
private:
    static int m_staticA;
    int m_a
};
  • 使用单例:如果我们的这个类是个单例,我们完全可以在创建的时候把this指针赋值给那个静态成员,然后在静态成员函数内部就可以放心的使用了
class A {
public:
    A() {
        m_gA = this;
    }
    static void test() {
        m_gA.m_a += 1;
    }
    void hello() {}
private:
    static int m_staticA;
    static A *m_gA;
    int m_a
};
21、C实现C++虚函数机制
  1. 手动构造父子关系
  2. 手动创建虚函数表
  3. 手动设置vptr并指向虚函数表
  4. 手动填充虚函数表
  5. 若有虚函数覆盖, 还需手动修改函数指针
  6. 若要取得基类指针, 还需手动强制转换
22、类型萃取

https://blog.csdn.net/terence1212/article/details/52287762

24、右值引用与move

右值引用绑定的都是将要销毁的对象

右值引用       不能绑定  左值
左值引用       不能绑定  右值
const 左值引用 可以绑定  右值

虽然不能将一个左值直接赋给一个右值引用,但是可以通过move函数来实现将左值转化为右值,再赋给右值引用

25.模板特化与偏特化

模板为什么要特化,因为编译器认为,对于特定的类型,如果你对某一功能有更好地实现,那么就该听你的

  • 有时为了需要,针对特定的类型,需要对模板进行特化,也就是特殊处理,比如说特例化一个模板,模板参数中有一个T类型,特例化T为int的特殊实现,那么在实际生成模板实例的过程中,如果传入的T就是int类型,就会使用特例化的模板来实现
  • 模板的偏特化是指需要根据模板的某些,但不是全部的参数进行特化,比如说偏特化一个T*类型,或者原来的模板中有T1、T2两个类型,现在偏特化为T1、char这两个类型
26、内存分配的原理(两个系统调用:brk、mmap)

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。

1、brk是将数据段(.data)的最高地址指针_edata往高地址推;

2、mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。

  1. malloc小于128k的内存,使用brk分配内存,将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系)
  2. malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0)

brk分配的内存需要等到高地址内存释放以后才能释放,而mmap分配的内存可以单独释放

https://blog.csdn.net/edonlii/article/details/22601043

27、C++线程是怎么创建的

直接调用std::thread,就创建一个新线程了。该线程拿到任务后立即开始执行。

// createThread.cpp

#include 
#include 

void helloFunction()  {
    std::cout << "Hello C++11 from function." << std::endl;
}

class HelloFunctionObject  {
public:
    void operator()() const {
        std::cout << "Hello C++11 from a function object." << std::endl;
    }
};

int main() {
    std::cout << std::endl;

    // 线程执行函数 helloFunction
    std::thread t1(helloFunction);

    // 线程执行函数对象 helloFunctionObject
    HelloFunctionObject helloFunctionObject;
    std::thread t2(helloFunctionObject);

    // 线程执行 lambda function
    std::thread t3([] {
        std::cout << "Hello C++11 from lambda function." << std::endl; 
    });

    // 确保 t1, t2 and t3 在main函数结束之前结束
    t1.join();
    t2.join();
    t3.join();

    std::cout << std::endl;
};

28、静态多态的形式
  • 函数重载
  • 运算符重载
  • 类模板
  • 函数模板
29、二级指针

假设B是一个指针,里面存着变量C的地址,那么

int* B = nullptr;
int C = 1;
B = &C;	//B中存着变量C的地址
*B 为 C的值;	//通过解引用B得到变量C的内容

若在函数中想更改B的指向,那么需要一个二级指针A,A中保存着B的地址

int** A = nullptr;
A = &B;	//A中保存着指针B的地址
*A 为 B的值;	//通过解引用A得到指针B中存储的内容(也就是变量C的地址)
**A 为 *B 为 C;//双重解引用A能够直接得到变量C的内容

*A = &C1;//可以实现在函数内更改指针B中存储的内容,也就是更改指针B的指向

https://blog.csdn.net/majianfei1023/article/details/46629065

30、单例模式为什么要双重检查

在懒汉单例的线程安全版本中

class lhsingleClass {
public:

    static lhsingleClass* getinstance() {
        if (instance == nullptr) { 			//第一次检查
            i_mutex.lock();
            if (instance == nullptr) { 		//第二次检查
                instance = new lhsingleClass();
            }
            i_mutex.unlock();
        }
        return instance;
    }
private:
    static lhsingleClass* instance;
    static mutex i_mutex; 
    lhsingleClass(){}
};
lhsingleClass* lhsingleClass::instance=nullptr;
mutex lhsingleClass::i_mutex;//类外初始化

若线程A完成第一次检查后,还没有获得锁,发生上下文切换,线程B获得CPU,线程B同样完成了第一次检查,并获得了锁,实例化单例对象后释放锁,此时线程A获得锁,如果不进行第二次检查就会让线程A再次实例化一个对象,不满足单例设计模式的要求

31、void const fun(){} 和 void fun(){}能否编译正确

不能,返回值不能作为函数重载的唯一区分。

另外,const在函数名之前修饰的是返回值为const,void const表示修饰一个空类型为const,可以但没必要

32、程序内存空间中的代码段

代码段(.text段):存储可执行文件的指令;也有可能包含一些只读的常数变量,例如字符串常量等。

代码段这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许自修改程序。 在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等

33、局部静态变量的存储位置

在局部变量前加上“static”关键字,就成了静态局部变量。

  • 静态局部变量存放在内存的全局数据区
  • 函数结束时,静态局部变量不会消失,每次该函数调用时,也不会为其重新分配空间。它始终驻留在全局数据区,直到程序运行结束。
  • 静态局部变量的初始化与全局变量类似.如果不为其显式初始化,则C++自动为其 初始化为0。
  • 静态局部变量与全局变量共享全局数据区,但静态局部变量只在定义它的函数中可见。静态局部变量与局部变量在存储位置上不同,使得其存在的时限也不同,导致对这两者操作 的运行结果也不同。
34、函数地址保存在哪

应该保存在代码段中

如果是动态库中的函数,只有在运行时才能确定函数真正的保存位置

函数调用的时候,会将函数地址压入栈中

35、STL中vector和list的最大容量

应该视(不同版本标准库中容器内存的具体实现算法)和(机器运行时刻的可用内存)而定

调用max_size() 可得

36、C++怎么防止multi-define(同一个头文件被include多次)

同一个文件中只能将一个头文件include一次。记住这个规则很容易,但是很可能在不知情的情况下将头文件包含多次,因为你include的头文件里可能还会include其它的头文件,这样层层嵌套,很容易出现上面的问题。这时就会带来编译的错误。

主要的解决方案有两种

  • #ifndef(if not defined)
#ifndef _COORDIN_H
#define _COORDIN_H
...     // 头文件的内容
#endif

  • #pragma once
#pragma once
... ... // 一些声明语句

(1)#ifndef和#pragma once都发生在预处理阶段,#ifndef的方式依赖于宏名字不能冲突,这不光可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件不会被不小心同时包含。当然,缺点就是不同头文件的宏名不小心“撞车”。

(2)#ifndef是C/C++语言特性,而#pragma once是编译器提供的指令,同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。带来的好处是,你不必再费劲想个宏名了,当然也就不会出现宏名碰撞引发的奇怪问题。对应的缺点就是如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。

(3)#pragma依赖于编译器,所以一些老的编译器不提供(比如说vc6之前),而#ifndef可移植性非常好。

37、dynamic_cast怎么用

static_cast用于将一种数据类型强制转换为另一种数据类型。什么都可以转,**最常用。**如下:

int a = 7;  
int b = 3;  
double result = static_cast<double>(a) / static_cast<double>(b); 

但是在进行下行转换(把基类的指针或引用转换为派生类表示)时,由于没有动态类型检查,是不安全的。

向上转换:指子类向基类转换。

向下转换:指基类向子类转换。

这两种转换,因为子类包含父类,子类转父类是可以任意转的。但是当父类转换成子类时可能出现非法内存访问的问题。

dynamic_cast

正因为static_cast从父类向子类转换时不安全,所以又引入了dynamic_cast。dynamic_cast只能用于含有虚函数的类转换,用于类向上和向下转换。

dynamic_cast通过判断变量运行时类型要转换的类型是否相同来判断是否能够进行向下转换。dynamic_cast可以做类之间上下转换,转换的时候会进行类型检查,类型相等成功转换,类型不等转换失败。

运用RTTI技术,RTTI是“Runtime Type Information”的缩写,意思是运行时类型信息,它提供了运行时确定对象类型的方法。在C++层面主要体现在dynamic_cast和typeid,在虚函数表中存放了指向type_info的指针,对于存在虚函数的类型,dynamic_cast和typeid都会去查询type_info

#include   
#include   
#include   
#include   
using namespace std;  
class Base{  
    public:  
    Base() {};  
    virtual ~Base() {};  
};  
class Inherit :public Base{  
    public:  
    Inherit() {};  
    ~Inherit() {};  
    void show();  
};  
void Inherit::show(){  
    std::cout << "Inherit funtion" << std::endl;  
}  
int main() {  
    Base* pbase = new Inherit();  
    Inherit* pInherit = dynamic_cast<Inherit*>(pbase);  
    pInherit->show();//这样动态转换,我们就可以调用派生类的函数了  
    system("Pause");  
    return 0;  
}

//运行结果为
Inherit funtion
38、shared_ptr,weak_ptr应该什么时候用,两个指针如果相互引用,应该哪个用shared_ptr,哪个用weak_ptr

shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。

共享指针的循环引用计数问题采用weak_ptr指针:

weak_ptr是弱引用,weak_ptr的构造和析构不会引起引用计数的增加或减少。我们可以将其中一个改为weak_ptr指针就可以了。

首先初始化的类采用weak_ptr。

39、怎样定义一个只在堆/栈上生成对象的类
  • 只建立在堆上

    将析构函数设为私有,类对象就无法建立在栈上了

    当对象建立在栈上面时,是由编译器分配内存空间的,调用构造函数来构造栈对象。当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间。编译器管理了对象的整个生命周期。如果编译器无法调用类的析构函数,情况会是怎样的呢?比如,类的析构函数是私有的,编译器无法调用析构函数来释放内存。所以,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。

    为了统一,可以将构造函数设为protected,然后提供一个public的static函数来完成构造,这样不使用new,而是使用一个函数来构造,使用一个函数来析构。

    class  A  {  
    protected :  
        A(){}  
        ~A(){}  
    public :  
        static  A* create()  {  
            return   new  A();  
        }  
        void  destory()  {  
            delete   this ;  
        }  
    }; 
    
  • 只建立在栈上

    只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。将operator new()设为私有即可。

    class  A  {  
    private :  
        void * operator  new ( size_t  t){}      // 注意函数的第一个参数和返回值都是固定的   
        void  operator  delete ( void * ptr){}  // 重载了new就需要重载delete   
    public :  
        A(){}  
        ~A(){}  
    }; 
    
40、内联函数为什么要定义在头文件中
  • 为什么需要内联函数?
    函数调用包含一系列工作,例如保存寄存器,并在返回时恢复,可能需要拷贝实参,程序转向一个新的位置执行等,这些工作会有一定开销,如果把函数代码在调用点上内联地展开,就可以避免这些开销,加快了程序运行速度,代价是程序体积会随着内联的次数增大

  • 简单点来说:内联函数就是把函数直接用函数体里面的代码替换。

  • 内联函数发生替换是在编译期间,在编译期间编译器需要找到内联函数的定义,所以在为了方便编译器找到定义,每个文件引用头文件后,都直接拥有这种定义,而不用再去写。

    而普通函数可以申明和定义分离,主要是编译阶段就不需要函数定义。首先编译阶段找到函数的声明,链接阶段才会去找函数的定义,将之关联起来。

41、shared_ptr是不是线程安全的

https://blog.csdn.net/solstice/article/details/8547547

shared_ptr 的线程安全级别和内建类型、标准库容器、std::string 一样

shared_ptr中有两个成员,这也导致它线程不安全的原因

sp0

不是,shared_ptr的引用计数本身是安全且无锁的,但对象的读写则不是

  • 一个 shared_ptr 对象实体可被多个线程同时读取
  • 两个 shared_ptr 对象实体可以被两个线程同时写入,“析构”算写操作
  • 如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁,但可以通过原子函数完成
41-2 unordered_map是线程安全的么

不是,和shared_ptr一样,多个线程同时读安全,一个线程写安全,多个线程写需要加锁才能安全

42、C++中的lock_guard和unique_lock
  • 为什么用lock_guard?

    • 互斥类的最重要成员函数是lock()和unlock()。在进入临界区时,执行lock()加锁操作,如果这时已经被其它线程锁住,则当前线程在此排队等待。退出临界区时,执行unlock()解锁操作。更好的办法是**采用”资源分配时初始化”(RAII)方法来加锁、解锁,这避免了在临界区中因为抛出异常或return等操作导致没有解锁就退出的问题。**极大地简化了程序员编写mutex相关的异常处理代码。C++11的标准库中提供了std::lock_guard类模板做mutex的RAII。
  • lock_guard的工作过程

    • 在std::lock_guard对象构造时,传入的mutex对象(即它所管理的mutex对象)会被当前线程锁住。在lock_guard对象被析构时,它所管理的mutex对象会自动解锁,不需要程序员手动调用lock和unlock对mutex进行上锁和解锁操作
  • 为什么用unique_lock?

    • unique_lock具有lock_guard的所有功能,而且更为灵活。虽然二者的对象都不能复制,但是unique_lock可以移动(movable),因此用unique_lock管理互斥对象,可以作为函数的返回值,也可以放到STL的容器中。

    • std::unique_lock对象以独占所有权的方式(unique owership)管理mutex对象的上锁和解锁操作,即在unique_lock对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而unique_lock的生命周期结束之后,它所管理的锁对象会被解锁

  • lock_guard和unique_lock的区别

    1. 在创建lock_guard对象时,已经把锁给锁住了,在lock_guard生命周期中不能手动解锁,而unique_lock就可以手动解锁
    2. unique_lock锁的粒度更小
43、static_cast与强制转换的区别

static_cast有相关性检查,不能进行没有相关性的转换

强制类型转换可以进行无关类型的转换,但是转换以后的操作是否安全没有保障

44、单继承与多继承下,虚表指针与虚表的内存模型

在多态的情景下,单继承下会存在一个虚函数表指针,指向成员函数地址;多继承下,会根据继承基类的个数生成相应的多个虚函数表指针 ,从而访问虚函数的地址

https://blog.csdn.net/weixin_43796685/article/details/103742329?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163010956916780255291277%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=163010956916780255291277&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_v2~rank_v29-5-103742329.pc_search_result_cache&utm_term=%E8%8F%B1%E5%BD%A2%E7%BB%A7%E6%89%BF%E7%9A%84%E8%99%9A%E5%87%BD%E6%95%B0%E6%8C%87%E9%92%88&spm=1018.2226.3001.4187

45、内存溢出和内存越界

内存溢出:你要分配的内存超出了系统能给你的,系统不能满足需求,于是产生了溢出

char str[5] = "1234567";

内存越界:你想系统申请一块内存,在使用的这块内存的时候,超过出了你申请的范围

int a[10];
a[12] = 10
46、new出来的对象,使用delete时它怎么知道这个内存的大小
  • new简单类型:直接调用operator new分配内存;
  • new复杂结构:先调用operator new分配内存,然后在分配的内存上调用构造函数;
  • new[]简单类型:计算好大小后调用operator new;
  • new[]复杂结构:先调用operator new[]分配内存。假设指针p指向new[]分配的内存,然后在p的前四个字节写入数组大小n,然后调用n次构造函数,针对复杂类型,new[]会额外存储数组大小;

对应的delete原理:

  • delete简单类型:默认只是调用free函数;
  • delete复杂数据类型:先调用析构函数再调用operator delete;
  • 针对简单类型,delete和delete[]等同。假设指针p指向new[]分配的内存。因为要4字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。delete[]实际释放的就是p-4指向的内存。而delete会直接释放p指向的内存,这个内存根本没有被系统记录,所以会崩溃。
47、宏实现max(a,b)
#define MAX(a,b)  (((a)>(b))?(a):(b))
47-2、宏函数实现两数交换
#define SWAP(x,y)  x=x+y;y=x-y;x=x-y;
48、C++内存管理

栈区、堆区、全局区、常量区、代码区

栈区:由编译器自动分配释放,存放为函数运行的局部变量,函数参数,返回数据,返回地址等。操作方式与数据结构中的类似。

堆区:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表

全局数据区:也叫做静态区,存放全局变量,静态数据。程序结束后由系统释放

文字常量区:可以理解为常量区,常量字符串存放这里。程序结束后由系统释放

程序代码区:存放函数体的二进制代码。但是代码段中也分为代码段和数据段。

关于文字常量区
文字常量区,在大多数解释中,都仅仅说明常量字符串存放这里。但是如果深究字眼,那么其他常量比如整型是否存放这里呢?我查阅了一些资料,是这么解释的:常量之所以称为“文字常量”,其中“文字”是指我们只能以它的值的形式指代它,“常量”是指它的值是不可变的。同时注意一点:文字常量是不可寻址的(即我们的程序中不可能出现获取所谓常量20的存储地址&20这样的表达式),虽然常量也是存储在内存的某个地方,但是我们没有办法访问常量的地址的。

还有就是我们都知道的常量是有类型的。所以总的来说,只要是常量都存放在文字常量区

49、new的操作符重载怎么回事,可以重载内存分配那部分么,可以重载构造函数那部分么

new操作符其实是调用了两个函数

  1. operator new(unsigned int) ------ 分配内存空间
  2. Demo::Demo() -------- 调用构造函数在上面申请的空间上构建对象

当我们在程序中执行new A创建对象时,
编译器会把new操作符拆分成上面的两个操作,然后生成相应的汇编指令。
也就是说operator new是new操作符的子操作。
注:在执行new A时不一定完全生成两个操作,编译器可能会进行优化。比如:使用默认构造函数时,就没有调用构造函数的步骤,只使用operator new分配的内存。

总结:

  1. new, delete都是分两个步骤进行的,分配空间/处理对象。
  2. operator new与new的区别,operator new是new的一个子操作,可以当成函数
  3. 我们重载new时,只能重写operator new, 而不能干预构造函数的调用(由编译器处理)

https://blog.csdn.net/darker0019527/article/details/103411418

50、share_ptr直接类构造和用 makeshared区别
struct A;
std::shared_ptr<A> p1 = std::make_shared<A>();
std::shared_ptr<A> p2(new A);

区别就是std::shared_ptr构造函数会执行两次内存申请,而std::make_shared只执行一次

std::shared_ptr在实现的时候使用的引用计数技术,因此内部会有一个计数器(控制块,用来管理数据)和一个指针,指向数据。因此在执行std::shared_ptr p2(new A)的时候,首先会申请数据的内存,然后申请内控制块,因此是两次内存申请,而std::make_shared()则是只执行一次内存申请,将数据和控制块的申请放到一起。

考虑异常安全场景:

void f(std::shared_ptr<Lhs> &lhs, std::shared_ptr<Rhs> &rhs){...}

f(std::shared_ptr<Lhs>(new Lhs()),
  std::shared_ptr<Rhs>(new Rhs())
);

因为C++允许参数在计算的时候打乱顺序,因此一个可能的顺序如下:

  1. new Lhs()
  2. new Rhs()
  3. std::shared_ptr
  4. std::shared_ptr

此时假设第2步出现异常,则在第一步申请的内存将没处释放了,上面产生内存泄露的本质是当申请数据指针后,没有马上传给std::shared_ptr

https://www.cnblogs.com/shengjianjun/p/3691928.html?utm_source=tuicool&utm_medium=referral

51、构造函数能不能调用成员函数

可以,但不能调用虚函数,以及依赖于类构造完成的函数。

首先在一个类的所有非静态函数中,都隐藏了一个this指针。所以对构造函数这个非静态函数来说,是存在此指针的。(证明了理论上是可以调用的)

至于楼主的意思,是怕在对象未全部构造完时就对其数据成员进行操作,这种考虑是正确的。所以,当你在构造函数中调用member function时,务必不要在member function中使用(注意是使用,不包括赋值)未被构造函数初始化过的数据成员。也就是说,这是在程序员的角度对这个调用进行把关。(证明了实践中的局限性)

https://bbs.csdn.net/topics/50303784

52.判断一个函数是不是虚函数

判断一个成员函数是不是虚函数(重写),有两个三个条件:

  1. 两个成员函数各自在基类和派生类中定义;
  2. 基类中定义的成员函数必须带有关键字virtual,派生类的成员函数可带可不带。
  3. 这两个成员函数原型(函数名,函数参数,函数返回类型)必须相同。

注意:如果这两个函数的返回类型分别为基类和派生类,返回值为指向基类和派生类的指针或引用,则也构成重写。此返回类型称为协变

53、为什么先调用父类构造函数,后调用子类构造函数

因为子类继承父类之后,获取到了父类的内容(属性/字段),而这些内容在使用之前必须先初始化,所以必须先调用父类的构造函数进行内容的初始化.

54、在什么地方调用父类的构造函数

在子类的构造函数中的第一行会隐式的调用 super();即调用了父类的构造函数

如果父类里面没有定义参数为空的构造函数,那么必须在子类的构造函数的第一行显示的调用super(参数);语句调用父类当中其它的构造函数.

55、在父类里的构造、析构函数里调用虚函数,会怎样

https://www.cnblogs.com/redips-l/p/11611240.html

  • 第一个原因,在概念上,构造函数的工作是为对象进行初始化。在构造函数完成之前,被构造的对象被认为“未完全生成”。当创建某个子类的对象时,如果在它的父类的构造函数中调用虚函数,那么此时子类的构造函数并未执行,所调用的函数可能操作还没有被初始化的子类成员,将导致灾难的发生。
  • 析构函数是用来销毁一个对象的,在销毁一个对象时,先调用该对象所属类的析构函数,然后再调用其父类的析构函数,所以,在调用父类的析构函数时,子类对象的析构工作已经完成了,这个时候再调用在子类中定义的函数版本已经没有意义了。
56、如何对字符串string的operator=函数进行重载

https://blog.csdn.net/h674174380/article/details/78037294

返回值string&,参数const string&,函数体判断自赋值情况,不是则深拷贝,返回*this

//声明
MyString& operator=(const char *s);       // 普通字符串赋值
MyString& operator=(const MyString &s);   // 类对象之间赋值

//实现
MyString& MyString::operator=(const char *s) {
	if (m_p != NULL) {
		delete []m_p;
		m_p = NULL;
	}
	m_len = strlen(s);
	m_p = new char[m_len+1];
	strcpy(m_p,s);
	return *this;
}


MyString& MyString::operator=(const MyString &s){
	if(this == &s)
		return *this;
	if (m_p != NULL) {
		delete []m_p;
		m_p = NULL;
	}
	m_len = s.m_len;
	m_p = new char [m_len+1];
	strcpy(m_p,s.m_p);
	return *this;
}
57、深拷贝用什么函数

strcpy:复制char*时会复制’\0’,所有在new 字符数组的时候应该预留’\0’的空间,否则到时候delete的时候会报错

//报错版本
char *arr = new char[10];
strcpy(arr, "wangzhaaaa");
delete []arr;
//正确版本
char *arr = new char[11];
strcpy(arr, "wangzhaaaa");
delete []arr;

改进用memcpy:与strcpy相比,memcpy并不是遇到’\0’就结束,而是一定会拷贝完n个字节

memcpy用来做内存拷贝,你可以拿它拷贝任何数据类型的对象,可以指定拷贝的数据长度

58、STL的仿函数设计原理及其底层实现

让一个不是函数 的东西拥有函数的功能,说的通俗点就是在一个类中利用运算符重载重载"()"让类拥有函数的使用特性和功能

template<class T>
struct A {
	bool operator()(const T& a,const T& b) {
		return (a == b);
	}
};

int main() {
	A<int>a;
	cout << a(1, 2) << endl;
	system("pause");
	return 0;
}
59、C++本身有没有多线程

https://blog.csdn.net/lizun7852/article/details/88753218?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163049977816780269867042%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=163049977816780269867042&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_v2~rank_v29-1-88753218.pc_search_result_hbase_insert&utm_term=C%2B%2B%E6%9C%AC%E8%BA%AB%E6%9C%89%E5%A4%9A%E7%BA%BF%E7%A8%8B%E4%B9%88&spm=1018.2226.3001.4187

60、std::move() 的内部实现
template<typename T>   //在命名空间std中
typename remove_reference<T>::type&& move(T&& param) {
    using ReturnType = typename remove_reference<T>::type&&; //别名声明
 
    return static_cast<ReturnType>(param);
}

std::move无条件地把它的参数转换成一个右值,而std::forward只在特定条件满足的情况下执行这个转换。

https://blog.csdn.net/f110300641/article/details/83477160?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163054139716780357264028%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=163054139716780357264028&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_v2~rank_v29-1-83477160.pc_search_result_hbase_insert&utm_term=std%3A%3Amove%28%29+%E7%9A%84%E5%86%85%E9%83%A8%E5%AE%9E%E7%8E%B0&spm=1018.2226.3001.4187

61、sizeof是关键字还是函数

sizeof既是关键字又是运算符,但不是函数

sizeof是唯一一个以单词形式出现的运算符,它用来计算存放某一个量需要占用多少字节,它的结合性是从右到左。

sizeof不是函数。产生这样的疑问主要是因为有时候sizeof的外在表现确实有点类似函数,比如:i = sizeof(int);这样的式子,就很容易让人误以为sizeof是一个函数呢。但如果sizeof是函数,那么sizeof i;(假设i为int变量)这种式子一定不能成立,因为函数的参数不可能不用括号括起来。事实上这个sizeof i;可以正常运行,这就说明sizeof绝对不是函数。

62、子类中怎样访问父类的私有成员

子类不能直接访问父类私有成员

可以在子类构造函数的成员初始化列表中,通过对父类对象进行初始化来达到访问父类私有成员的目的

也可以通过在子类成员函数中调用父类的protected和public接口实现对父类private成员的访问

63、在头文件中定义一个static变量,两个源文件都包含这个头文件,头文件中的static变量会被共享么

在头文件中定义static变量会造成变量多次定义,造成内存空间的浪费,而且也不是真正的全局变量。应该避免使用这种定义方式。

//Header.h-----------------头文件中定义static变量
#pragma once

static int g_int = 3;

//Source1.cpp
#include 
#include "Header.h"

void TestSource1() {

    wprintf(L"g_int's address in Source1.cpp: %08x\n", &g_int);
    g_int = 5;
    wprintf(L"g_int's value in Source1.cpp: %d\n", g_int);
}

//Source2.cpp
#include 
#include "Header.h"

void TestSource2() {

    wprintf(L"g_int's address in Source2.cpp: %08x\n", &g_int);
    wprintf(L"g_int's value in Source2.cpp: %d\n", g_int);
}
//两个源文件中的g_int变量完全不是同一个

要想实现多个源文件共享头文件中定义的全局变量,就要用extern关键字声明该变量,而不是static

//Header.h
#pragma once

extern int g_int;
64、new异常的时候怎么不抛出异常

用nothrow new,空间分配失败的时候不抛出异常,而是返回NULL

char* p = new(nothrow) char[10];
if (p == NULL) {
	cout << "alloc failed" << endl;
}
65.内存池与内存空闲链表

https://blog.csdn.net/weixin_30806145/article/details/112583567

66、类内成员怎么初始化,包括static、常量和普通成员

https://blog.csdn.net/robinhjwy/article/details/78700492

首先说一条指导规则:通常情况下,不应该在类内部初始化成员!!无论是否为静态、是否为常量、是否为int等!!统统不建议在类内初始化,因为本质上类只是声明,并不分配内存,而初始化会分配内存,类内初始化会将两个过程混在一起!

如果非要在类内初始化以上成员

static成员:

  • c++禁止在类内初始化非静态常量。

    class A {
    public:
        //static int a = 1;  这样报错,静态的非常量必须在类外初始化
        static int a;
        static const int b = 1;	//静态的类内成员必须为常量才能在类内初始化
        
    };
    int A::a = 1;
    

常量:

  • const成员变量,必须在类的构造函数的初始化列表中初始化;
class A {
public:
    //A(): {}  这样报错,常量成员必须在类的初始化列表中初始化
    A(): a(1) {}
    const int a;
};

普通成员:

  • 赋值初始化
class Student  {
public:
	Student(string in_name, int in_age) {
		name = in_name;
		age = in_age;
	}
private :
	string name;
	int    age;
};

  • 初始化列表
class Student {
public:
	Student(string in_name, int in_age):name(in_name),age(in_age) {}
private :
	string name;
	int    age;
};
67、引用的底层实现原理,引用会不会占内存

https://blog.csdn.net/lws123253/article/details/80353197

引用变量内存放的是被引用对象的地址,但是对一个引用取地址 == 对被引用对象取地址。在实际使用时,引用被当作被引用对象的一个别名来使用。但是在底层实现上,通过汇编验证了引用是有自己的内存的

68、函数默认参数

https://www.cnblogs.com/chenke1731/p/9651275.html

**C++规定,默认参数只能放在形参列表的最后,而且一旦为某个形参指定了默认值,那么它后面的所有形参都必须有默认值。**实参和形参的传值是从左到右依次匹配的,默认参数的连续性是保证正确传参的前提。

下面的写法是正确的:

void func(int a, int b=10, int c=20){ }
void func(int a, int b, int c=20){ }

但这样写不可以:

void func(int a, int b=10, int c=20, int d){ }
void func(int a, int b=10, int c, int d=20){ }

除了函数定义,也可以在函数声明处指定默认参数。不过当出现函数声明时情况会变得稍微复杂,有时候可以在声明处和定义处同时指定默认参数,有时候只能在声明处指定:

C++ 规定,在给定的作用域中只能指定一次默认参数,即函数声明与函数实现都有默认参数的话,要放在两个文件中

你可能感兴趣的:(面经专栏,c++,面试,后端)