面试题目梳理

前言

正值秋招面试季,梳理了一些常见的面试题。

C++知识点

  • 解释下封装、继承和多态 这里主要说下虚函数表和虚函数指针的使用特点,了解后基本能够掌握多态的原理

关于虚函数表和虚函数指针

  • 静态绑定:指在程序编译过程中,把函数调用与响应调用所需的代码结合的过程,称为静态绑定,发生在编译期 非虚成员函数属于静态绑定:编译器在编译期间,根据指针(或对象)的类型完成了绑定

  • 动态绑定:指在执行期间判断所引用对象的实际类型,根据实际的类型调用其相应的方法。程序运行过程中,把函数调用与响应调用所需的代码相结合的过程称为动态绑定,发生于运行期

  • C++标准规格说明书中说到,编译器必须要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)

  • 虚函数是通过虚函数表实现,这个表中记录虚函数的地址 编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类所有对象共享 虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址

  • 类的每个虚成员占据虚函数表中的一行,如果类中有N个虚函数,那么其虚函数表将有N*4字节(32位)的大小

  • 在单继承中,Child类覆盖Base类中的同名虚函数,在Child类的虚函数表中体现为对应位置被Child类中的新函数替换,而没有被覆盖的Base类的虚函数则顺次继承不发生变化 对于派生类Child自己的虚函数,则顺次添加到Child类虚函数表后面

  • 类型转换

    • 向上类型转换:将派生类指针或引用转换为基类的指针或引用,安全、隐式的
    • 向下类型转换:将基类指针或引用转换为派生类指针或引用,非安全需要程序员保证
    • 向上转型后通过基类的指针、引用本质类型是属于基类(虚函数表指针却指向转换前类的虚函数表地址),只能访问基类成员和成员函数,如果是基类存在虚函数则会触发多态:根据转型前的对象类的虚函数表指针决定
    • 如果是对象类型(非指针、引用)向上转型,则是完全转型,虚函数表指针指向转换后类的虚函数表地址
  • 对类进行实例化时,在构造函数执行时会对虚表指针初始化,并且存在对象内存布局的最前面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    class Base {
    public:
    virtual void func() {
    cout << "Base" << endl;
    }
    int b = 2;
    };

    class Child : public Base {
    public:
    virtual void func() {
    cout << "Child" << endl;
    }

    void test() {
    cout << "Child test" << endl;
    }
    int c = 1;
    };

    int main(int argc, char* argv[]) {
    Base *bp = new Child();
    Base b1;
    Child c1, c2;
    cout << "Child vfptr = " << *((int *)&c1) << endl; // 取虚函数表地址值*((int *)&c1)
    cout << "Child vfptr = " << *((int *)&c2) << endl;
    cout << "Child vfptr = " << *((int *)bp) << endl; // 触发多态bp的虚函数表 = c1的虚函数表 = Child类的虚函数表
    Base b2 = static_cast<Base>(c1);
    cout << "Base vfptr = " << *((int *)&b2) << endl; // 静态对象类型转换不触发多态,b2的虚函数表 = Base类的虚函数表
    cout << "Base vfptr = " << *((int *)&b1) << endl;
    bp->func(); // 触发多态
    b2.func(); // 不触发多态
    // bp->test(); // error: ‘class Base’ has no member named ‘test’

    Base *bp1;
    Child *cp1 = new Child();
    bp1 = reinterpret_cast<Base *>(cp1);
    cout << "Child vfptr = " << *((int *)bp1) << endl; // 触发多态bp1的虚函数表 = cp1的虚函数表 = Child类的虚函数表
    cout << "Child vfptr = " << *((int *)cp1) << endl;

    return 0;
    }

    $ ./test
    Child vfptr = 432614704
    Child vfptr = 432614704
    Child vfptr = 432614704
    Base vfptr = 432614728
    Base vfptr = 432614728
    Child
    Base
    Child vfptr = 432614704
    Child vfptr = 432614704

  • 是否了解设计模式 对设计模式的掌握是判断候选人能否快速切入项目的一个重要标准 一般应届生候选人接触具备良好中间件的大型工程项目的机会并不会太多,因此如果能够写出几个常见的设计模式基本会让面试官眼前一亮 务必掌握:单例设计模式(饿汉式的线程安全double check)、工厂设计模式、观察者设计模式、模板方法设计模式等 推荐设计模式网站:https://refactoringguru.cn/design-patterns/cpp

  • 单例设计模式 — C11之后线程安全,饿汉式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Singleton {
    public:
    static Singleton& GetInstance() {
    static Singleton instance; // C++11之后保证了线程安全
    return instance;
    }

    void Func() { cout << "in singleton" << endl; }

    private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator= (const Singleton&) = delete;
    };

  • 单例设计模式 — 饿汉式,主要考察的是锁的double check的内涵和static的使用细节

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    class Singleton {
    public:
    static Singleton *GetInstance() {
    if (instance_ == nullptr) { // 为了效率,避免每个线程进入都要加锁
    std::unique_lock<std::mutex> lock(mutex_);
    if (instance_ == nullptr) { // 为了配合第一个if,如果不加这行,多个线程同时阻塞lock处,获取锁后会重复申请
    instance_ = new Singleton();
    }
    }

    return instance_;
    }

    void Func() { cout << "in singleton" << endl; }

    private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* instance_;
    static std::mutex mutex_;
    };
    Singleton* Singleton::instance_ = nullptr; // 注意static修饰的类成员变量的类外定义
    std::mutex Singleton::mutex_;

  • 单例设计模式 — 懒汉式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Singleton {
    public:
    static Singleton* GetInstance() { return instance_; }

    void Func() { cout << "in singleton" << endl; }

    private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* instance_;
    };
    Singleton* Singleton::instance_ = new Singleton();

  • 模板工厂设计模式,项目中参考使用的,该模板基本可以覆盖大部分工厂模式的需求

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    class Product {
    public:
    Product() { cout << "base default construct" << endl; }
    Product(int in) { cout << "base input construct " << in << endl; }
    virtual ~Product() {}
    virtual void Operation() = 0;
    };

    class ProductA : public Product {
    public:
    void Operation() { cout << "A" << endl; }
    };

    class ProductB : public Product {
    public:
    void Operation() { cout << "B" << endl; }
    };

    class ProductC : public Product {
    public:
    ProductC(int in) : Product(in) { cout << "construct C " << in << endl; }
    void Operation() { cout << "C" << endl; }
    };

    // 如果ClassType是带入参的构造函数,可采用自定义的ClassCreatorFunc
    template <typename IdType, typename ClassType,
    typename ClassCreatorFunc = ClassType* (*)()>
    class Factory {
    public:
    // 当Lambda向函数指针的转换时,编译器为Lambda的匿名类实现函数指针类型转换运算符
    bool Register(const IdType& id, const ClassCreatorFunc creator) {
    return mp_.insert(std::make_pair(id, creator)).second;
    }

    bool Unregister(const IdType& id) { return mp_.erase(id) == 1; }

    template <typename... ArgsType>
    std::unique_ptr<ClassType> CreateObject(const IdType& id,
    ArgsType&&... args) {
    auto it = mp_.find(id);
    if (it != mp_.end()) {
    return std::unique_ptr<ClassType>(
    (it->second)(std::forward<ArgsType>(args)...));
    }
    return nullptr;
    }

    private:
    std::unordered_map<IdType, ClassCreatorFunc> mp_;
    };

    // 使用方式
    int main() {
    Factory<std::string, Product> factory;
    factory.Register("A", []() -> Product* { return new ProductA(); }); // 注意-> Product*强调返回类型
    factory.Register("B", []() -> Product* { return new ProductB(); });
    auto ptr = factory.CreateObject("B");
    ptr->Operation();

    Factory<std::string, Product, Product* (*)(int in)> factory1;
    // 自定义的ClassCreatorFunc
    factory1.Register("C", [](int in) -> Product* { return new ProductC(in); });
    auto ptr1 = factory1.CreateObject("C", 123); // 123为C构造函数入参
    ptr1->Operation();

    return 0;
    }

  • 观察者设计模式,经常使用的、很常见的设计模式,在异步通知中比较常见

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    class ObserverBase {
    public:
    virtual ~ObserverBase() {}
    virtual void Update() = 0;
    };

    class A : public ObserverBase {
    public:
    void Update() { cout << "A" << endl; }
    };

    class B : public ObserverBase {
    public:
    void Update() { cout << "B" << endl; }
    };

    class NotifyBase {
    public:
    void Attach(ObserverBase* ob) { list_ob_.emplace_back(ob); }

    void Detach(ObserverBase* ob) { list_ob_.remove(ob); }

    void Notify() {
    for (auto i : list_ob_) {
    i->Update();
    }
    }

    private:
    std::list<ObserverBase*> list_ob_;
    };

    // 使用方式
    int main() {
    ObserverBase* a = new A();
    ObserverBase* b = new B();
    NotifyBase n;
    n.Attach(a);
    n.Notify();
    n.Attach(b);
    n.Notify();
    n.Detach(a);
    n.Notify();

    return 0;
    }

  • 模板方法设计模式,也很常见,例如线程池库中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Base {
    public:
    void Run() {
    this->BaseFunc();
    this->Func1();
    this->Func2();
    }

    protected:
    virtual void Func1() = 0;
    virtual void Func2() = 0;

    private:
    void BaseFunc() { cout << "BaseFunc" << endl; }
    };

    class Concrete : public Base {
    protected:
    void Func1() { cout << "Func1" << endl; }

    void Func2() { cout << "Func2" << endl; }
    };

  • static关键字使用特点

  • 类外
    • 静态全局变量
    1. 存储在全局数据区,默认初始化为0
    2. 整个文件内可见,文件外不可见
    • 静态局部变量
    1. 同样存储在全局数据区,默认初始化为0
    2. 在程序执行到该变量的声明处时被首次初始化,以后的函数调用不再进行初始化
    3. 始终驻留在全局数据区,直到程序运行结束,作用域为局部作用域
  • 类内
    • 静态成员变量为类的所有对象共享,在内存中只有一个副本
    • 静态成员变量使用前必须类外初始化,否则会报“undefined reference to”
    • 静态成员变量依然遵循public,protected,private访问规则
    • 非静态成员函数可以访问静态成员变量和非静态成员变量
    • 静态成员函数可以访问静态成员变量,不可以访问非静态成员变量和非静态成员函数,静态成员函数没有this指针
    • 除了对象调用,还可以类名::静态成员函数名(参数表)调用类的静态成员函数
  • 堆栈区别(堆和栈的生命周期)

分配/回收方式 new或者malloc,程序员自主管理 编译器管理
大小 取决于内存大小 有限制,不同系统不同,一般是MB级别
生长方向 向着内存地址增加的方向增长 向着内存地址减小的方向增长
分配效率 效率较低 效率较高
  • 如何解决栈溢出
  1. 减少局部变量的使用,必要时在堆上分配空间
  2. 函数入参尽量使用引用或者指针类型
  3. 非必要不使用函数递归
  • new和malloc的区别 这个题重点考察的是new的原理步骤
  1. operator new函数申请空间,内部调用malloc
  2. placement new在该空间上调用构造函数
  3. 返回指针 另:malloc不保证类型安全,new可以保证类型安全
  • vector迭代器什么时候会失效 vector a{2, 1, 4, 4, 3, 4, 5, 6}; // 剔除4

1
2
3
4
5
6
7
8
for (auto it = a.begin(); it != a.end();) {
if (*it == 4) {
it = a.erase(it); // 关联型容器注意迭代器失效
} else {
++it;
}
}
// C++20 才支持 std::erase(a, '4');
另:vector扩容时也会导致原迭代器失效

  • 为什么基类析构函数一般写成虚函数
  1. 不触发多态时,派生类的构造和析构顺序如下
  • 构造顺序:
    • 基类成员对象的构造函数
    • 基类的构造函数
    • 派生类成员对象的构造函数
    • 派生类的构造函数
  • 析构顺序:
    • 派生类的析构函数
    • 派生类成员的析构函数
    • 基类的析构函数
    • 基类成员的析构函数
  1. 触发多态时(释放基类指针或引用指向的派生类对象时),如果基类的析构函数非虚函数 该情况下,基类析构函数则为静态绑定,因此派生类析构不会被调用,如果派生类存在内存申请或者有构造类对象则会造成内存泄漏 另:如果派生类并无额外资源需要释放或析构(情况少见),则不要无脑声明基类虚析构,以防编译器生成虚函数表,无意义的扩大类的存储空间

  2. 触发多态时(释放基类指针或引用指向的派生类对象时),如果基类的析构函数为虚函数 该情况下,基类析构函数属于动态绑定,在释放派生类会查询虚函数表指针vfptr指向的是派生类的虚函数表, 则先调用派生类的析构函数,同时编译器保证每个派生类的析构函数结束时会自动(隐含地)调上基类的析构函数,而普通虚函数并不会

  • 解释下std::move 只是进行了左右值类型转换,一般配合移动构造使用 可移动对象在需要拷贝且被拷贝者之后不再被需要的场景,建议使用std::move触发移动语义,提升性能

  • 指针和引用的区别

指针 引用
指针变量需要占用内存空间 不占用空间
定义和声明可以分开,初始化可以为 nullptr 只有声明,没有定义,声明时必须初始化,不能为空
可以递增,递减 不支持加减
可以实现多级(例如:指针的指针) 只支持一级
sizeof指针,为指针变量内的地址值的长度,32位 = 4字节,64位 = 8字节 sizeof引用,为引用变量类型的长度
初始化后可以改变指向 不能改变引用指向
  • const int *p和int *const p区别 const int p:const修饰的是*p,即指针变量指向的内存单元的值,即内存值是常量无法改变; int const p: const修饰的是p,即指针变量的值,即指向的内存单元的地址无法改变,即无法改变指针的指向

  • 手写string类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    class String {
    public:
    String(const char* cstr) { // notice const
    if (cstr == nullptr) {
    data_ = new char[1];
    data_[0] = '\0';
    } else {
    data_ = new char[strlen(cstr) + 1];
    strcpy(data_, cstr);
    }
    }

    ~String() {
    if (data_ != nullptr) {
    delete[] data_;
    data_ = nullptr;
    }
    }

    String(const String& str) {
    data_ = new char[str.Size()];
    strcpy(data_, str.data_);
    }

    String& operator=(const String& str) {
    if (this == &str) {
    return *this;
    }

    delete[] data_; // notice delete original
    data_ = new char[str.Size()];
    strcpy(data_, str.data_);
    return *this;
    }

    String(String&& str) noexcept { // notice noexcept
    data_ = str.data_;
    str.data_ = nullptr;
    }

    String& operator=(String&& str) noexcept { // notice noexcept
    if (this == &str) {
    return *this;
    }

    delete[] data_; // notice delete original
    data_ = str.data_;
    str.data_ = nullptr;
    return *this;
    }

    uint32_t Size() const { return strlen(data_) + 1; } // notice const

    private:
    char* data_;
    };

  • 用栈实现队列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    template <typename T>
    class Queue {
    public:
    void push(const T& element) {
    back_element = element;
    sta_.push(element);
    }

    void pop() {
    if (!stb_.empty()) {
    stb_.pop();
    } else {
    if (!sta_.empty()) {
    while (!sta_.empty()) {
    stb_.push(sta_.top());
    sta_.pop();
    }
    stb_.pop();
    } else {
    throw std::runtime_error("queue is empty");
    }
    }
    }

    T front() {
    if (!stb_.empty()) {
    return stb_.top();
    } else {
    if (!sta_.empty()) {
    while (!sta_.empty()) {
    stb_.push(sta_.top());
    sta_.pop();
    }
    return stb_.top();
    } else {
    throw std::runtime_error("queue is empty");
    }
    }
    }

    T back() {
    if (!empty()) {
    return back_element;
    } else {
    throw std::runtime_error("queue is empty");
    }
    }

    int size() const { // notice const
    return sta_.size() + stb_.size();
    }

    bool empty() const { // notice const
    return sta_.empty() && stb_.empty();
    }

    private:
    std::stack<T> sta_;
    std::stack<T> stb_;
    T back_element;
    };

  • 手写智能指针shared_ptr

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    template <typename T>
    class SharedPtr {
    public:
    SharedPtr() : ref_count_(nullptr), ptr_(nullptr) {}

    explicit SharedPtr(T* p) : ref_count_(new uint32_t(1)), ptr_(p) {}

    SharedPtr(const SharedPtr& sh_p)
    : ref_count_(sh_p.ref_count_), ptr_(sh_p.ptr_) {
    if (ref_count_) {
    ++(*ref_count_);
    }
    }

    SharedPtr& operator=(const SharedPtr& sh_p) {
    if (this != &sh_p) {
    if (ref_count_) {
    if ((--(*ref_count_)) == 0) {
    Release();
    }
    }

    ref_count_ = sh_p.ref_count_;
    ptr_ = sh_p.ptr_;
    ++(*ref_count_);
    }

    return *this;
    }

    SharedPtr(SharedPtr&& sh_p) noexcept
    : ref_count_(sh_p.ref_count_), ptr_(sh_p.ptr_) {
    sh_p.ref_count_ = nullptr;
    sh_p.ptr_ = nullptr;
    }

    SharedPtr& operator=(SharedPtr&& sh_p) noexcept {
    if (this != &sh_p) {
    if (ref_count_) {
    if ((--(*ref_count_)) == 0) {
    Release();
    }
    }

    ref_count_ = sh_p.ref_count_;
    ptr_ = sh_p.ptr_;
    }

    return *this;
    }

    T* operator->() { return ptr_; }

    T& operator*() { return *ptr_; }

    T* get() { return ptr_; }

    uint32_t use_count() const noexcept { return ref_count_ ? *ref_count_ : 0; }

    ~SharedPtr() {
    if (ref_count_) {
    if ((--(*ref_count_)) == 0) {
    Release();
    }
    }
    }

    private:
    void Release() {
    if (ptr_) {
    delete ptr_;
    ptr_ = nullptr;
    }

    if (ref_count_) {
    delete ref_count_;
    ref_count_ = nullptr;
    }
    }

    uint32_t* ref_count_;
    T* ptr_;
    };

Linux系统知识点

  • Linux线程、进程区别,进程间通信用过哪些 线程间共享虚拟内存空间,是CPU的最小调度单元 进程各自独占虚拟内存空间,是CPU的最小资源分配单元,一个进程可以拥有多个线程 另:线程和进程在内核中统一使用struct task_struct描述符管理,从内核角度进程和线程统称为task,均使用_do_fork带入不同的flag创建
图1

图1 task创建图

  • Linux进程间通信用过哪些
    • unix域套接字socket,相对网络套接字性能更高
    • 网络套接字
    • message queue
    • share mem
    • pipe
    • fifo
  • Linux查看当前系统运行的进程信息的命令 可以使用top或者ps命令查看 top指令的表头含义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    PID — 进程id
    USER — 进程所有者
    PR — 进程优先级
    NI — nice值,CFS调度器使用,值越高优先级越低
    VIRT — 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES
    RES — 进程使用的、未被换出的物理内存大小,单位kb,RES=CODE+DATA
    SHR — 共享内存大小,单位kb
    S —进程状态。D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程
    %CPU — 上次更新到现在的CPU时间占用百分比
    %MEM — 进程使用的物理内存百分比
    TIME+ — 进程使用的CPU时间总计,单位1/100秒
    COMMAND — 进程名称(命令名/命令行)

  • Linux如何用命令判断文件1.dat中是否含有”abc”字符串 这里主要是为了考察grep指令,在linux环境下开发,grep指令太重要了,必须掌握

    1
    grep -nr "abc" 1.dat

  • Linux硬盘挂载指令

    1
    2
    3
    4
    挂载指令:mount
    查看系统挂载点:cat /proc/mounts
    查看挂载点的使用情况:df -h
    查看快设备节点的信息:blkid, lsblk

  • Linux如何查看网络连接状况,如何查看系统都开启了哪些端口

    1
    2
    ifconfig, ip
    netstat

开发经验

推荐:如何优雅的调试段错误

https://cloud.tencent.com/developer/article/1629269

面试沟通技巧

  • 不要轻易放弃,抗住压力,有时候一些偏难的题目可能只是面试官在探查你的技能掌握深度
  • 白板题时,积极主动表达自己的思路,方便面试官给予思路的修正和提示
-------------The End-------------
🙈坚持原创技术分享,感谢支持🙈
0%