C++ 基础面试题总结(一)

C++ 基础面试题总结(一)

C++的特性 √

  • 面向对象

    • 面向对象程序设计(Object-oriented programming,OOP)是种具有对象概念的程序编程典范,同时也是一种程序开发的抽象方针。

    • 面向对象三大特征 —— 封装、继承、多态

  • 封装

    • 把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。关键字:public, protected, private。不写默认为 private。

      • public 成员:可以被任意实体访问
      • protected 成员:只允许被子类及本类的成员函数访问
      • private 成员:只允许被本类的成员函数、友元类或友元函数访问
  • 继承

    • 基类(父类)——> 派生类(子类)
  • 多态

两种多态 √

  • 静态多态(编译时多态性、早绑定)

    • 静态多态是通过函数重载和模板实现的。

    • 在编译时,根据函数的参数类型或模板的实例化类型,选择适当的函数或模板进行调用。

    • 静态多态的解析发生在编译阶段。

// 静态多态通过函数重载实现
void print(int num) {
    std::cout << "Integer: " << num << std::endl;
}

void print(double num) {
    std::cout << "Double: " << num << std::endl;
}

// 静态多态通过模板实现
template <typename T>
void print(T value) {
    std::cout << "Value: " << value << std::endl;
}

int main() {
    print(10);      // Integer: 10
    print(3.14);    // Double: 3.14
    print("Hello"); // Value: Hello

    return 0;
}
  • 动态多态(运行时多态,晚绑定)

    • 动态多态是通过继承和虚函数实现的。

    • 通过基类指针或引用调用虚函数时,根据对象的实际类型,动态选择调用相应的派生类函数。

    • 动态多态的解析发生在运行时。

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

class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Dog is barking" << std::endl;
    }
};

class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "Cat is meowing" << std::endl;
    }
};

int main() {
    Animal* animalPtr = new Dog();
    animalPtr->makeSound();  // Dog is barking

    animalPtr = new Cat();
    animalPtr->makeSound();  // Cat is meowing

    delete animalPtr;

    return 0;
}

C++11的特性 √

  1. 自动类型推断(Type Inference):auto关键字可以用于自动推断变量的类型,根据变量的初始化值进行类型推导。

  2. 列表初始化(List Initialization):可以使用花括号{}进行初始化,可以用于各种类型的初始化,包括数组、容器、结构体等。

    // 初始化变量
    int y{ 42 }; 
    
    // 初始化数组
    int arr[] = { 1, 2, 3, 4, 5 }; 
    
    // 初始化结构体
    struct Point {
        int x;
        int y;
    };
    
    Point p1 = { 10, 20 }; // 列表初始化
    Point p2{ 30, 40 }; // 列表初始化 
    
    // 初始化类对象
    class Person {
    public:
        std::string name;
        int age;
    
        Person(const std::string& n, int a) : name(n), age(a) {
            // 构造函数使用初始化列表进行成员变量的初始化
        }
    };
    
    int main() {
        Person p1("Alice", 25); // 使用构造函数进行列表初始化
        Person p2{"Bob", 30}; // 直接使用花括号进行列表初始化
        return 0;
    }
    
    // 初始化容器
    std::vector<int> numbers = { 1, 2, 3, 4, 5 }; // 列表初始化 
    
    // 初始化嵌套结构体或类对象
    struct Rectangle {
        Point topLeft;
        Point bottomRight;
    };
    
    Rectangle rect = { { 0, 0 }, { 100, 100 } }; // 列表初始化
    
- 需要注意的是,列表初始化对于类型转换要求更严格,不允许进行窄化转换,例如将浮点数赋值给整数类型。如果需要进行窄化转换,可以使用传统的括号初始化或显式类型转换。

  ```cpp
  int x = 3.14; // 普通初始化,允许窄化转换
  int y{ 3.14 }; // 列表初始化,禁止窄化转换,编译错误

  int z = static_cast(3.14); // 显式类型转换,允许窄化转换
  1. 增强的for循环:引入了新的for循环语法,用于遍历容器、数组以及其他支持迭代的对象。

  2. 右值引用(Rvalue References)和移动语义(Move Semantics):引入了新的引用类型&&,用于支持移动语义,提高对临时对象的效率,并支持移动构造函数和移动赋值运算符。

  • 右值引用

    • 右值引用是一种新的引用类型,使用双引号&&表示。它主要用于绑定到右值(临时对象)上,例如临时对象、表达式结果或将要销毁的对象。与传统的左值引用不同,右值引用允许对其绑定的对象进行移动操作。

      int x = 42;
      int& lvalueRef = x; // 左值引用绑定到左值
      int&& rvalueRef = 42; // 右值引用绑定到右值
      
  1. Lambda表达式:Lambda表达式是C++11引入的一种便捷的函数对象表示法,它允许在代码中内联定义匿名函数。Lambda表达式的原理基于闭包(Closure)的概念。
[capture-list](parameter-list) -> return-type { 
    // 函数体
}

其中,capture-list用于捕获外部变量,parameter-list定义函数参数,return-type指定返回类型,而函数体则包含具体的操作逻辑。

  • 闭包

    • Lambda表达式可以捕获其所在作用域中的变量,并在函数体内部使用这些变量。捕获的变量可以是值、引用或通过引用捕获(mutable)进行修改。这些捕获的变量的生命周期会延长到Lambda表达式的调用结束。通过闭包,Lambda表达式可以访问和操作外部作用域的变量,即使这些变量在Lambda表达式定义之后发生了变化。
  • 函数对象

    • Lambda表达式实际上生成了一个匿名的函数对象,也称为闭包对象。这个函数对象可以像普通函数一样进行调用,可以传递给其他函数、存储在容器中等。
  1. 空指针常量nullptr:引入了新的空指针常量nullptr,用于表示空指针,避免了传统C++中空指针的二义性。

  2. 强类型枚举(Strongly Typed Enumerations):枚举类型可以指定底层数据类型,并且不会自动隐式转换为整数类型。

  3. 智能指针(Smart Pointers):引入了shared_ptrunique_ptrweak_ptr等智能指针,用于管理动态分配的内存,提供自动内存管理和避免内存泄漏。

  4. 新的标准库组件:引入了许多新的标准库组件,如等,提供更多的数据结构和算法支持。

  5. 并发编程支持:引入了线程库、互斥量、条件变量等,支持多线程和并发编程。

智能指针 √

智能指针是C++中的一种特殊类型的指针,它提供了自动化的内存管理和资源释放,帮助避免常见的内存泄漏和资源泄漏问题。

  1. std::unique_ptr:一种独占指针,它提供对动态分配对象的唯一所有权。它保证在其拥有的对象不再需要时,会自动释放所分配的内存。它不能被复制,但可以被移动。使用std::move函数可以将所有权从一个std::unique_ptr转移到另一个。

  2. std::shared_ptr:一种共享指针,它允许多个指针共享对同一对象的所有权。它使用引用计数技术来跟踪共享对象的引用计数,并在引用计数变为零时自动释放所分配的内存。std::shared_ptr可以被复制和移动,当最后一个引用被销毁时,它会自动释放所拥有的资源。

  3. std::weak_ptr:一种弱指针,它用于解决std::shared_ptr可能导致的循环引用问题。std::weak_ptr允许观察一个对象,但不拥有该对象。它不会增加引用计数,也不会阻止对象的销毁。通过调用std::weak_ptrlock函数,可以获得一个有效的std::shared_ptr,用于访问所观察的对象。

    • 循环引用

      • 循环引用指的是对象之间相互引用形成的循环链表,导致无法自动回收对象并引发内存泄漏问题。循环引用的解决方法之一是使用弱引用,如C++中的std::weak_ptr,它允许观察对象而不增加引用计数,从而打破循环引用关系。

        #include 
        #include 
        
        class Node {
        public:
            std::weak_ptr<Node> next;
        };
        
        int main() {
            std::shared_ptr<Node> node1 = std::make_shared<Node>();
            std::shared_ptr<Node> node2 = std::make_shared<Node>();
        
            node1->next = node2;
            node2->next = node1;  // 使用 std::weak_ptr,避免循环引用
        
            return 0;
        }
        

命名规范 √

  1. 标识符命名风格:

    • 使用驼峰命名法(Camel Case):将单词连接在一起,每个单词的首字母大写,除了第一个单词的首字母小写。例如:myVariable, myFunction()
    • 避免使用下划线作为标识符的开头或结尾,除非是成员变量(例如:_privateVariable
    • 类名应该以大写字母开头,每个单词首字母大写(例如:ClassName
  2. 命名约定:

    • 使用有意义的名字:选择能够清晰表达变量、函数或类的用途和含义的名字。
    • 避免使用单个字符的名字,除非是循环索引或者其他具有明确用途的情况。
    • 使用英文单词而不是缩写,以增加代码的可读性。
    • 使用一致的命名约定,以便在整个代码库中保持一致性。
  3. 命名空间(Namespace):

    • 命名空间应该使用全小写字母,并使用下划线分隔单词(例如:my_namespace)。
  4. 常量命名:

    • 常量应该使用全大写字母,单词之间使用下划线分隔(例如:MY_CONSTANT)。
  5. 类成员变量:

    • 成员变量应该以小写字母开头,使用驼峰命名法(例如:myVariable_)。
    • 使用下划线作为成员变量的后缀,以便与局部变量和参数进行区分。
  6. 函数和方法:

    • 函数和方法的命名应该以动词或动词短语开头,使用驼峰命名法(例如:calculateValue())。
    • 构造函数和析构函数应该与类名相同。
  7. 文件命名:

    • 头文件应该使用.h.hpp作为文件扩展名。
    • 源文件应该使用.cpp作为文件扩展名。

Linux基本操作 √

  1. 文件和目录操作:

    • ls:列出当前目录下的文件和目录。
    • cd:切换到指定目录。
    • pwd:显示当前所在的目录。
    • mkdir:创建新目录。
    • rm:删除文件或目录。
    • cp:复制文件或目录。
    • mv:移动文件或目录。
  2. 文件查看和编辑:

    • cat:显示文件内容。
    • less:逐页显示文件内容。
    • head:显示文件的前几行。
    • tail:显示文件的后几行。
    • vivim:文本编辑器,用于编辑文件。
  3. 文件权限和用户管理:

    • chmod:修改文件权限。
    • chown:修改文件的所有者。
    • chgrp:修改文件的所属组。
    • useradd:添加新用户。
    • passwd:修改用户密码。
  4. 进程管理:

    • ps:显示当前运行的进程。
    • top:实时显示系统资源使用情况和进程状态。
    • kill:终止指定进程。
    • bg:将进程置于后台运行。
    • fg:将后台进程切换到前台运行。
  5. 网络操作:

    • ping:测试与指定主机的连通性。
    • ifconfig:显示和配置网络接口信息。
    • ssh:通过SSH协议连接到远程主机。
    • scp:在本地主机和远程主机之间复制文件。

GDB常用命令 √

  1. 启动和运行程序:

    • gdb :启动GDB并加载可执行文件。
    • runr:运行程序。
    • break b :在指定行设置断点。
    • break :在指定函数设置断点。
    • continuec:继续执行程序。
  2. 断点和调试:

    • info breakpointsi b:显示当前设置的断点信息。
    • delete breakpoints :删除指定编号的断点。
    • nextn:执行下一行代码。
    • steps:进入函数调用或跳转到下一行代码。
    • finish:运行到当前函数的结束,并返回到调用者。
    • backtracebt:显示当前的函数调用栈。
  3. 变量和表达式:

    • print p :打印变量的值。
    • display :在每次停止时自动显示变量的值。
    • set = :设置变量的值。
    • whatis :显示表达式的类型。
  4. 内存和地址:

    • x/
      :显示指定地址的内存内容。
    • info registersi r:显示当前的寄存器值。
    • up:向上切换到调用者的栈帧。
    • down:向下切换到当前栈帧的调用者。
  5. 程序状态和环境:

    • info program:显示程序的状态和环境信息。
    • info threads:显示当前线程的信息。
    • thread :切换到指定编号的线程。
    • set environment :设置环境变量的值。
  6. 调试器控制:

    • quitq:退出GDB。
    • helph:显示帮助信息。
    • run :重新运行程序,并传递命令行参数。

内存分析工具 √

  1. Valgrind
    Valgrind是一个强大的开源工具套件,包含多个工具,其中最常用的是Memcheck。Memcheck可以检测内存泄漏、非法内存访问、使用未初始化的变量等问题。使用Valgrind运行程序时,它会在运行过程中进行内存访问的跟踪和检查。

  2. AddressSanitizer(ASan):
    AddressSanitizer是GCC和Clang编译器的一项内置工具,用于检测内存错误。它通过在程序运行时注入额外的代码来捕获内存访问错误,例如缓冲区溢出、使用已释放的内存等。ASan提供了更快的检测速度和较低的内存开销。

并发的方法 √

  1. 多线程:使用多个线程并发执行任务。每个线程都是独立的执行单元,可以同时执行不同的代码路径。线程之间可以共享数据,但需要注意线程安全性和同步问题。

    • 比如说slam中的,跟踪线程、建图线程、回环线程
  2. 锁机制:使用锁来控制对共享资源的访问。常见的锁包括互斥锁(mutex)、读写锁(read-write lock)和条件变量(condition variable)。在访问共享资源之前,线程需要获得相应的锁,以确保只有一个线程能够访问资源,从而避免竞态条件和数据不一致的问题。

  3. 信号量:使用信号量来控制对资源的访问。信号量是一个计数器,用于表示可用资源的数量。线程在访问资源之前需要获取信号量,如果信号量计数为0,则线程会阻塞等待,直到有可用资源。

  4. 条件变量:用于线程之间的通信和同步。条件变量允许线程等待某个特定条件的发生,并在条件满足时唤醒等待的线程。条件变量通常与锁结合使用,以确保线程在等待条件时能够正确同步。

  5. 原子操作:使用原子操作来保证对共享数据的原子性操作。原子操作是不可中断的操作,可以保证在多线程环境中对共享数据的操作是线程安全的。

  6. 并发数据结构:使用特定的并发数据结构,如并发队列、并发哈希表等,来实现多线程环境下的高效并发访问。

  7. 任务并行:将任务分解为多个独立的子任务,并使用多线程或任务调度框架来并发执行这些子任务。任务并行可以提高程序的执行效率,并充分利用多核处理器的计算能力。

    • 比如说slam中的,双目可以同时提取特征点

拷贝构造函数 √

拷贝构造函数是一种特殊的构造函数,用于创建一个新对象,其内容与另一个同类型对象相同。

#include 

class MyClass {
public:
    int value;

    MyClass(int val) : value(val) {}
    MyClass(const MyClass& other) : value(other.value) {}
};

int main() {
    MyClass obj1(10);
    MyClass obj2(obj1);  // 使用拷贝构造函数创建 obj2 的副本

    std::cout << obj2.value << std::endl;  // 输出: 10

    return 0;
}

左值引用与右值引用 √

左值引用(Lvalue) √
  • 左值引用使用&符号表示,可以绑定到一个左值(可寻址的表达式,如变量、对象成员、表达式中的左值等)。它可以用于修改绑定对象的值和传递对象的引用。

    int x = 5;
    int& ref = x;  // 左值引用绑定到左值 x
    ref = 10;  // 修改 x 的值
    
右值引用(Rvalue)√
  • 右值引用使用&&符号表示,可以绑定到一个右值(临时对象、表达式返回的临时结果等)。它通常用于实现移动语义完美转发,并支持对临时对象的有效操作。

    int&& rref = 5;  // 右值引用绑定到右值 5
    int x = std::move(rref);  
    
  • 临时对象的延长生命周期:std::move

    • 功能将一个左值/右值, 转换为右值引用。 主要是将左值强制转为右值引用,因为右值引用无法直接绑定到左值上, 为了能让右值引用绑定到左值上, 必须将左值转为右值引用,std::move提供做的就是这个。 对于传入右值, 那么std::move将什么都不做, 直接返回对应的右值引用。

    • 右值引用可以将临时对象的生命周期延长,使其可以在函数调用之后继续存在。这在某些情况下非常有用,例如在函数返回右值引用时,能够避免临时对象的销毁。

      std::string&& createTemporaryObject() {
          std::string temp = "Hello";
          return std::move(temp); // 将临时对象转为右值引用返回
      }
      
完美转发 √

完美转发(Perfect Forwarding)是指在函数模板中将参数保持其原始值类别(左值或右值),并将其转发给其他函数。它的目的是在不丢失值类别的情况下,将参数传递给其他函数,实现参数的透明传递。

在C++中,完美转发通常使用两个关键字来实现:

  1. std::forwardstd::forward是一个模板函数,用于在函数模板中进行完美转发。它接受一个参数,并根据参数的值类别进行转发。

  2. 双引用(Forwarding Reference):双引用是指使用&&来声明的函数模板参数。当一个函数模板的参数是双引用时,它可以接受任意类型的参数,并保持原始值类别。

#include 
#include 

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

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

template <typename T>
void wrapper(T&& arg) {
    process(std::forward<T>(arg));
}

int main() {
    int x = 5;

    wrapper(x);          // 传递左值
    wrapper(10);         // 传递右值

    return 0;
}
移动语义 √
  • 移动语义是利用右值引用实现的一种机制,用于高效地转移资源的所有权,而不是进行昂贵的复制操作。通过移动而不是复制对象,可以避免资源的不必要复制和释放,从而提高性能。它们将资源的所有权从一个对象转移到另一个对象,通常通过指针的简单交换来实现。

    class MyString {
    public:
        char* data;
    
        MyString(const char* str) {
            // 构造函数,分配内存并复制字符串
            int size = strlen(str) + 1;
            data = new char[size];
            strcpy(data, str);
        }
    
        // 移动构造函数
        MyString(MyString&& other) noexcept : data(other.data) {
            other.data = nullptr;
        }
    
        // 移动赋值运算符
        MyString& operator=(MyString&& other) noexcept {
            if (this != &other) {
                delete[] data;
                data = other.data;
                other.data = nullptr;
            }
            return *this;
        }
    
        ~MyString() {
            // 析构函数,释放内存
            delete[] data;
        }
    };
    
    int main() {
        MyString str1("Hello");
        MyString str2(std::move(str1)); // 使用移动构造函数,转移str1的资源给str2
    
        str1 = MyString("World"); // 使用移动赋值运算符,转移临时对象的资源给str1
    
        return 0;
    }
    

数组与指针 √

  1. 类型关系:

    • 数组是一组相同类型的元素的集合,可以在内存中连续存储。
    • 指针是一个变量,存储了内存地址。
  2. 内存表示:

    • 数组以连续的内存块存储元素,可以通过索引访问数组中的元素。
    • 指针存储了一个内存地址,可以通过解引用操作符(*)访问指针所指向的内存位置的值。
  3. 隐式转换:

    • 数组名可以被隐式转换为指向第一个元素的指针。例如,对于数组 int arr[5],可以使用 int* ptr = arr 将数组转换为指针。
    • 指针可以通过加减运算符来移动指向的内存位置。 例如,ptr++ 将指针向后移动一个位置。
  4. 大小和长度:

    • 数组的大小是在编译时确定的,可以使用 sizeof 运算符获取数组的大小(以字节为单位)。
    • 指针的大小是固定的,与系统架构相关(通常为4字节或8字节)。
    • 数组的长度可以通过除以每个元素的大小来计算。例如,sizeof(arr) / sizeof(arr[0]) 可以得到数组 arr 的长度。
    • 指针没有直接的方式来获取所指向的内存块的长度,因此需要其他方式来记录长度信息。

extern 关键字 √

在C++中,extern是一个关键字,用于声明一个全局变量或函数的存在,而不进行定义。它告诉编译器该标识符是在其他地方定义的,并且在链接时会找到其定义。

  • 声明全局变量

    • 使用extern关键字可以在一个文件中声明一个全局变量,并在其他文件中定义。这样可以在多个文件之间共享同一个全局变量
  • 声明外部函数

    • 使用extern关键字可以在一个文件中声明一个外部函数,并在其他文件中定义该函数。这样可以在多个文件之间共享同一个函数

extern "C"

extern "C" 的作用是让 C++ 编译器将 extern "C" 声明的代码当作 C 语言代码处理,可以避免 C++ 因符号修饰导致代码不能和C语言库中的符号进行链接的问题。

  • 被 extern 限定的函数或变量是 extern 类型的
  • extern "C" 修饰的变量和函数是按照 C 语言方式编译和链接的
#ifdef __cplusplus
extern "C" {
#endif

void *memset(void *, int, size_t);

#ifdef __cplusplus
}
#endif

static 关键字 √

在函数内部定义的变量,当程序执行到它的定义处时,编译器为它在栈上分配空间,函数在栈上分配的空间在此函数执行结束时会释放掉。

在 C++ 中,需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见时,可将其定义为静态数据

  • 修饰变量

    • 修改变量的存储区域和生命周期,变量存于静态存储区,在main运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
  • 修饰普通函数

    • 表明函数作用范围仅在定义的文件中使用
  • 修饰成员变量

    • 静态成员变量属于类而不是类的实例,它在整个类的所有实例之间共享,并且可以在没有实例的情况下访问
  • 修饰成员函数

    • 使用static修饰符声明的静态成员函数可以在没有类的实例对象的情况下被调用,因为它们不访问实例特定的数据。

    • 静态成员函数不能访问非静态成员变量,只能访问其他静态成员变量或者其他静态成员函数。

指针与引用 √

指针和引用之间的主要区别如下:

  1. 初始化要求:指针可以被初始化为nullptr或一个合法的对象地址,也可以进行重新赋值。引用必须在声明时进行初始化,并且不能重新绑定到其他对象。

  2. 空值(Null Value):指针可以具有空值(即nullptr),表示它不指向任何有效的对象。引用必须始终引用一个有效的对象。

  3. 内存管理:指针可以进行动态内存分配和释放,可以指向堆上的对象。引用只是一个别名,无法进行动态内存分配和释放。

  4. 操作符:指针使用解引用运算符*和成员访问运算符->来访问和操作指针所指向的对象。引用直接使用变量名来访问和操作所引用的对象。

  5. 参数传递:指针可以作为函数参数进行传递,可以传递nullptr表示空指针。引用也可以作为函数参数进行传递,可以传递对象的别名。

#defineconst

宏定义 #define const 常量
宏定义,相当于字符替换 常量声明
预处理器处理 编译器处理
无类型安全检查 有类型安全检查
不分配内存 要分配内存
存储在代码段 存储在数据段
可通过 #undef 取消 不可取消

const 关键字 √

  • 修饰变量,变量当做常量

  • 修饰指针,分为指向常量的指针(指向的地址的内容不可修改)、指针本身是常量(指向的地址不可修改)

  • 修饰引用const int &var 用于形参类型,避免拷贝,避免修改

  • 修饰成员函数,则该成员函数内不能修改成员变量

    classstruct的区别 √

  • 默认访问权限

    • class中,默认的成员访问权限是private

    • struct中,默认的成员访问权限是public

  • 继承方式

    • 使用class进行继承时,默认的继承方式是private继承;

    • 使用struct进行继承时,默认的继承方式是public继承。

  • 使用习惯

    • class被用于封装复杂的数据结构和行为,强调数据的封装和隐藏;

    • struct通常用于简单的数据聚合,强调数据的公开访问。

this指针 √

  • this指针是C++中的一个特殊指针,它是指向当前对象的指针。每个非静态成员函数(包括构造函数和析构函数)都有一个隐含的this指针,指向调用该函数的对象。

  • this指针的主要作用是在成员函数中访问当前对象的成员变量和成员函数。通过this指针,可以明确地指向当前对象,以便在函数内部访问和操作对象的成员。

  • this指针是一个常量指针,指向的对象是不可修改的。在成员函数中,不能修改this指针的值。

菱形继承 √

菱形继承(Diamond Inheritance)是指在类继承关系中存在一个菱形形状的结构,其中一个派生类同时继承自两个基类,并且这两个基类又共同继承自同一个基类。

    A
   / \
  B   C
   \ /
    D
  • 菱形继承可能导致一些问题,主要包括以下两个方面:

    • 冗余数据:由于类D同时继承自BC,而BC又共同继承自A,因此在类D中会有两份来自A的数据成员,导致数据的冗余。这种冗余可能会浪费内存空间。

    • 命名冲突:菱形继承可能导致成员函数或数据成员在派生类中发生命名冲突。当派生类D同时继承自BC时,如果BC中都有同名的成员函数或数据成员,那么在派生类D中就会发生命名冲突,编译器无法确定应该调用哪个成员。

  • 为了解决菱形继承带来的问题,C++提供了以下两种机制:

    • 虚继承(Virtual Inheritance):通过在继承关系中使用virtual关键字,可以声明虚继承。当类BC虚继承自类A时,派生类D就只会包含一份来自A的数据成员,解决了数据冗余的问题。虚继承还会影响构造函数和析构函数的调用顺序,需要注意正确使用。

    • 虚函数重写:当在菱形继承中发生了成员函数的命名冲突时,派生类可以通过重写(override)的方式解决冲突。通过在派生类中重新定义相应的成员函数来明确指定要使用的版本。这样就可以消除命名冲突,并根据需要调用特定的成员函数实现。

虚函数与纯虚函数 √

  • 虚函数

    • 在C++中,虚函数(Virtual Function)是一种允许派生类重写基类函数的机制。通过在基类中声明虚函数,并在派生类中进行重写,可以实现多态性,即在运行时根据对象的实际类型调用适当的函数实现。

    • 构造函数不能是虚函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针)

    • 内联函数不能是表现多态性时的虚函数

  • 虚函数表

    • 虚函数表(Virtual Function Table)是用于实现C++中虚函数机制的一种数据结构。每个包含虚函数的类都会生成一个虚函数表,用于存储该类的虚函数地址。虚函数表是一个类的静态数据成员,对于每个类来说,只有一个虚函数表。

    • 虚函数表是一个由函数指针构成的数组,每个函数指针对应类中的一个虚函数。在对象的内存布局中,通常会有一个指向虚函数表的指针,称为虚函数表指针(vptr)。这个指针在对象的内存中通常是一个隐藏的成员。

    • 此外,虚函数表只适用于包含虚函数的类,对于没有虚函数的类,不会生成虚函数表。

  • 纯虚函数

    • 纯虚函数(Pure Virtual Function)是一种特殊的虚函数,它在基类中被声明为纯虚函数,没有提供实现。纯虚函数用= 0来表示,并且基类被称为抽象类(Abstract Class)。抽象类不能被实例化,只能用作派生类的基类,派生类必须实现纯虚函数。

虚析构函数 √

  • 虚析构函数是在C++中用于处理多态对象析构的特殊函数。当基类指针指向派生类对象,并且通过基类指针删除对象时,如果基类的析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数。这可能导致资源泄漏或未定义的行为。

  • 为了解决这个问题,可以在基类的析构函数前面加上关键字virtual,将其声明为虚析构函数。这样,当通过基类指针删除派生类对象时,会首先调用派生类的析构函数,然后再调用基类的析构函数,确保正确释放派生类所占用的资源。

new/delete/malloc/free的用法 √

  1. newdelete

    • new 用于在堆上动态分配单个对象的内存,并返回指向该对象的指针。
    • new 在分配内存后会调用对象的构造函数进行初始化。
    • delete 用于释放通过 new 分配的单个对象的内存。
    • delete 在释放内存前会调用对象的析构函数进行清理。

    示例代码:

    int* p = new int;   // 动态分配一个int类型的对象
    *p = 10;            // 对对象进行操作
    delete p;           // 释放内存
    
  2. new[]delete[]

    • new[] 用于在堆上动态分配一个数组的内存,并返回指向数组的指针。
    • new[] 在分配内存后会调用每个对象的默认构造函数进行初始化。
    • delete[] 用于释放通过 new[] 分配的数组的内存。
    • delete[] 在释放内存前会调用每个对象的析构函数进行清理。

    示例代码:

    int* arr = new int[5];   // 动态分配一个包含5个int类型元素的数组
    arr[0] = 1;              // 对数组进行操作
    delete[] arr;            // 释放内存
    
  3. mallocfree —— C语言

    • malloc 是C语言中的函数,用于在堆上分配指定大小的内存块,并返回指向该内存块的指针。
    • malloc 分配的内存块大小通过参数指定,不会自动调用构造函数进行初始化。
    • free 用于释放通过 malloc 分配的内存块的内存。

    示例代码:

    int* p = (int*)malloc(sizeof(int));   // 动态分配一个int类型的内存块
    *p = 10;                              // 对内存块进行操作
    free(p);                              // 释放内存
    

stl迭代器的失效情况 √

  1. 添加或删除元素:在对容器进行添加或删除元素的操作后,迭代器可能会失效。这是因为添加或删除元素可能导致容器重新分配内存或改变容器的结构,从而使得之前的迭代器无法正确地指向容器的元素。

  2. 容器扩容:当容器需要扩容时(例如,vector中的元素数量超过当前容量),容器可能会重新分配内存,并将元素移动到新的内存位置。这将导致之前的迭代器无效,因为它们仍然指向旧的内存位置。

  3. 删除当前迭代器指向的元素:如果在迭代器指向的元素被删除之后,尝试继续使用该迭代器,将会导致未定义行为。因为删除一个元素后,迭代器将不再指向有效的元素。

  4. 使用被销毁的容器:如果迭代器试图访问一个已经被销毁的容器中的元素,那么迭代器将失效,因为容器对象已经不存在。

堆和栈 √

堆(Heap):

  • 堆是动态分配内存的区域,用于存储程序运行时动态创建的对象和数据结构。
  • 堆是一块较大的内存区域,用于存储动态分配的内存块。
  • 堆的内存分配和释放是通过程序员手动控制的,需要显式地调用newdelete(或对应的运算符new[]delete[])来申请和释放内存。
  • 堆内存的分配和释放可以在任何时候进行,并且大小可以动态调整。
  • 堆中的内存分配和释放可能会导致内存碎片化问题。

栈(Stack):

  • 栈是一种自动分配内存的区域,用于存储程序运行时的局部变量和函数调用的上下文信息。
  • 栈是一种后进先出(LIFO)的数据结构,栈顶的元素最后被添加,也最先被移除。
  • 栈的内存分配和释放是由编译器自动进行的,无需程序员显式地操作。
  • 栈的内存分配和释放是基于函数的调用和返回,当函数被调用时,会在栈上分配一段内存作为函数的堆栈帧,当函数返回时,会释放该段内存。
  • 栈内存的大小是有限的,通常较小,由操作系统或编译器定义。

多线程

线程的创建与终止 √

  • 创建线程的方式

    • 通过函数指针创建

      #include 
      #include 
      
      // 线程函数
      void threadFunction() {
          // 线程执行的代码
          std::cout << "Thread is running" << std::endl;
      }
      
      int main() {
          // 创建线程对象,并指定线程函数
          std::thread t(threadFunction);
      
          // 等待线程执行完毕
          t.join();
      
          std::cout << "Thread has completed." << std::endl;
      
          return 0;
      }
      
    • 通过Lambda表达式创建

      #include 
      #include 
      
      int main() {
          // 创建线程对象,并指定lambda表达式
          std::thread t([](){
              // 线程执行的代码
              std::cout << "Thread is running" << std::endl;
          });
      
          // 等待线程执行完毕
          t.join();
      
          std::cout << "Thread has completed." << std::endl;
      
          return 0;
      }
      
    • 使用可调用对象创建

      #include 
      #include 
      
      // 可调用对象类
      struct Callable {
          void operator()() {
              // 线程执行的代码
              std::cout << "Thread is running" << std::endl;
          }
      };
      
      int main() {
          // 创建线程对象,并指定可调用对象
          Callable callable;
          std::thread t(callable);
      
          // 等待线程执行完毕
          t.join();
      
          std::cout << "Thread has completed." << std::endl;
      
          return 0;
      }
      
  • 结束线程的方式

    如果需要等待线程执行完成并获取其结果,(阻塞)应使用join()。如果希望将线程独立执行,不需要等待其完成,(非阻塞)可以使用detach()

线程间参数传递 √

  1. 传递参数给线程函数:可以通过线程的构造函数将参数传递给线程函数。这种方法适用于使用函数指针或lambda表达式创建线程的情况。

    #include 
    #include 
    
    // 线程函数
    void threadFunction(int value) {
       // 线程执行的代码,使用传递的参数
       std::cout << "Thread value: " << value << std::endl;
    }
    
    int main() {
       int parameter = 42;
    
       // 创建线程对象,并传递参数
       std::thread t(threadFunction, parameter);
    
       // 等待线程执行完毕
       t.join();
    
       return 0;
    }
    
  2. 使用引用传递参数:可以使用引用来传递参数,使得线程函数能够直接修改传递的参数。

    #include 
    #include 
    
    // 线程函数
    void threadFunction(int& value) {
       // 线程执行的代码,修改传递的参数
       value = 100;
    }
    
    int main() {
       int parameter = 42;
    
       // 创建线程对象,并传递参数引用
       std::thread t(threadFunction, std::ref(parameter));
    
       // 等待线程执行完毕
       t.join();
    
       std::cout << "Modified value: " << parameter << std::endl;
    
       return 0;
    }
    
  3. 使用共享数据结构:可以使用共享的数据结构(例如std::atomic或std::mutex)来在线程之间传递参数或共享数据。这些数据结构提供了线程安全的访问机制,可以确保多个线程之间对数据的正确访问。

    #include 
    #include 
    #include 
    
    std::atomic<int> sharedValue(0);
    
    // 线程函数
    void threadFunction() {
       // 使用共享的数据结构,修改或读取数据
       sharedValue = 100;
    }
    
    int main() {
       // 创建线程对象
       std::thread t(threadFunction);
    
       // 等待线程执行完毕
       t.join();
    
       std::cout << "Shared value: " << sharedValue << std::endl;
    
       return 0;
    }
    

进程间通信 √

管道(Pipe)√

管道是一种半双工的通信方式,用于在具有父子关系的进程之间进行通信。它可以是匿名管道(在内存中创建)或命名管道(基于文件系统)。管道只能在具有共同祖先的进程之间使用。

  • 创建管道

    • 使用pipe系统调用创建管道,它会返回两个文件描述符:fd[0]用于读取数据,fd[1]用于写入数据。可以使用如下的C语言代码创建管道:

      #include 
      int pipe(int fd[2]);
      
  • 父子进程通信

    • 管道通常用于具有父子关系的进程之间进行通信。在创建子进程之前,可以调用pipe来创建管道。子进程继承了父进程的文件描述符,因此可以通过管道进行通信。

      #include 
      #include 
      #include 
      
      int main() {
          int fd[2];
          if (pipe(fd) == -1) {
              perror("pipe");
              exit(EXIT_FAILURE);
          }
      
          pid_t pid = fork();
          if (pid == -1) {
              perror("fork");
              exit(EXIT_FAILURE);
          }
      
          if (pid == 0) {
              // 子进程
              close(fd[1]);  // 关闭写入端
              char buffer[256];
              read(fd[0], buffer, sizeof(buffer));
              printf("子进程接收到的数据:%s\n", buffer);
              close(fd[0]);  // 关闭读取端
          } else {
              // 父进程
              close(fd[0]);  // 关闭读取端
              char message[] = "Hello, child process!";
              write(fd[1], message, sizeof(message));
              close(fd[1]);  // 关闭写入端
          }
      
          return 0;
      }
      
  • 关闭文件描述符

    • 在使用管道进行通信后,需要及时关闭相关的文件描述符。关闭不需要的文件描述符可以确保资源的正确释放。

      close(fd[0]);  // 关闭读取端
      close(fd[1]);  // 关闭写入端
      
FIFO 命名管道(Named Pipe)√

命名管道也称为FIFO(First-In, First-Out),它是一种基于文件系统的有名管道。多个进程可以通过打开同一个命名管道进行通信。命名管道允许无关进程之间的通信。

#include 
#include 
#include 
#include 

int main() {
    const char* fifoFile = "/tmp/myfifo";

    // 创建FIFO
    mkfifo(fifoFile, 0666);

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // 子进程
        int fd = open(fifoFile, O_RDONLY);
        if (fd == -1) {
            perror("open");
            exit(EXIT_FAILURE);
        }

        char buffer[256];
        read(fd, buffer, sizeof(buffer));
        std::cout << "子进程接收到的数据:" << buffer << std::endl;
        close(fd);
    } else {
        // 父进程
        int fd = open(fifoFile, O_WRONLY);
        if (fd == -1) {
            perror("open");
            exit(EXIT_FAILURE);
        }

        std::string message = "Hello, child process!";
        write(fd, message.c_str(), message.length() + 1);
        close(fd);
    }

    // 删除FIFO
    unlink(fifoFile);

    return 0;
}
  • 使用FIFO进行进程间通信可以实现不相关进程之间的通信,但需要确保FIFO的路径在通信的两个进程中是可见的。此外,需要注意的是FIFO是阻塞的,即读取和写入操作会阻塞进程,直到有数据可读或有空间可写。如果需要进行非阻塞的通信,可以使用open函数时指定O_NONBLOCK标志。
共享内存(Shared Memory)√

共享内存是一种高效的进程间通信方式,通过在不同进程之间共享同一块内存区域来实现。多个进程可以同时读写共享内存,从而实现数据的快速交换。需要注意的是,共享内存本身不提供同步机制,因此需要结合其他同步方式来确保数据的一致性和互斥访问。

#include 
#include 
#include 
#include 

int main() {
    const int sharedMemorySize = 1024;
    key_t key = ftok("/tmp/mem", 'R');
    int shmid = shmget(key, sharedMemorySize, IPC_CREAT | 0666);

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // 子进程
        char* sharedMemory = static_cast<char*>(shmat(shmid, nullptr, 0));
        std::cout << "子进程接收到的数据:" << sharedMemory << std::endl;
        shmdt(sharedMemory);
    } else {
        // 父进程
        char* sharedMemory = static_cast<char*>(shmat(shmid, nullptr, 0));
        std::string message = "Hello, child process!";
        std::copy(message.begin(), message.end(), sharedMemory);
        sharedMemory[message.length()] = '\0';
        shmdt(sharedMemory);
    }

    // 删除共享内存
    shmctl(shmid, IPC_RMID, nullptr);

    return 0;
}
  • 在这个示例中,我们使用了ftok函数生成一个唯一的键值,然后使用shmget函数创建共享内存段。子进程和父进程通过调用shmat函数来附加共享内存段,并将其转换为适当的指针类型,以进行读取或写入操作。

  • 需要注意的是,共享内存需要一个唯一的键值来标识共享内存段。在示例中,我们使用了ftok函数生成一个键值。共享内存段的大小由shmget函数的第二个参数指定。

  • 在示例中,我们使用了std::copy函数将数据从字符串复制到共享内存中,然后在共享内存的末尾添加了一个空字符以表示字符串的结束。

  • 在使用完共享内存后,可以使用shmdt函数将共享内存从进程中分离,并使用shmctl函数删除共享内存段。

  • 使用共享内存进行进程间通信可以实现高效的数据共享,但需要注意同步和互斥问题,以确保多个进程对共享内存的访问是安全的。

信号量(Semaphore) √

信号量是一种用于进程间同步的工具。它可以用来实现互斥访问共享资源或者进行进程间的同步操作。信号量维护一个计数器,并提供了原子的增加和减少操作。

#include 
#include 
#include 
#include 
#include 

int main() {
    key_t key = ftok("/tmp/semaphore", 'R');
    int semid = semget(key, 1, IPC_CREAT | 0666);

    // 设置信号量的值
    union semun {
        int val;
        struct semid_ds *buf;
        unsigned short *array;
    } semarg;
    semarg.val = 1;
    semctl(semid, 0, SETVAL, semarg);

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // 子进程
        struct sembuf sops;
        sops.sem_num = 0;  // 信号量索引
        sops.sem_op = -1;  // P操作,获取信号量
        sops.sem_flg = 0;  // 默认标志
        semop(semid, &sops, 1);

        std::cout << "子进程在临界区内" << std::endl;
        sleep(2);

        sops.sem_op = 1;  // V操作,释放信号量
        semop(semid, &sops, 1);
    } else {
        // 父进程
        struct sembuf sops;
        sops.sem_num = 0;  // 信号量索引
        sops.sem_op = -1;  // P操作,获取信号量
        sops.sem_flg = 0;  // 默认标志
        semop(semid, &sops, 1);

        std::cout << "父进程在临界区内" << std::endl;
        sleep(2);

        sops.sem_op = 1;  // V操作,释放信号量
        semop(semid, &sops, 1);
    }

    // 删除信号量
    semctl(semid, 0, IPC_RMID);

    return 0;
} 
消息队列(Message Queue) √

消息队列是一种进程间通信的方式,进程可以通过将消息发送到队列中,然后其他进程从队列中接收消息。消息队列可以提供异步通信和实现进程间的解耦

#include 
#include 
#include 
#include 
#include 

// 定义消息结构体
struct Message {
    long mtype;  // 消息类型
    char mtext[1024];  // 消息内容
};

int main() {
    key_t key = ftok("/tmp/message_queue", 'R');
    int msgid = msgget(key, IPC_CREAT | 0666);

    Message message;

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // 子进程
        message.mtype = 1;  // 子进程发送的消息类型
        strcpy(message.mtext, "Hello from child process!");

        // 发送消息
        msgsnd(msgid, &message, sizeof(message.mtext), 0);
    } else {
        // 父进程
        // 接收消息
        msgrcv(msgid, &message, sizeof(message.mtext), 1, 0);

        std::cout << "父进程接收到的消息:" << message.mtext << std::endl;
    }

    // 删除消息队列
    msgctl(msgid, IPC_RMID, nullptr);

    return 0;
}
套接字(Socket)√

套接字是一种网络编程中常用的进程间通信方式,可以在不同主机之间进行通信。套接字提供了一种通过网络传输数据的机制,可以实现客户端和服务器之间的通信。

#include 
#include 
#include 
#include 
#include 
#include 

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 设置服务器地址信息
    struct sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(8080);
    serverAddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定套接字到指定地址和端口
    if (bind(sockfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    // 监听连接请求
    if (listen(sockfd, 10) == -1) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    // 接受客户端连接请求
    struct sockaddr_in clientAddr;
    socklen_t clientAddrLen = sizeof(clientAddr);
    int clientSockfd = accept(sockfd, (struct sockaddr*)&clientAddr, &clientAddrLen);
    if (clientSockfd == -1) {
        perror("accept");
        exit(EXIT_FAILURE);
    }

    // 从客户端接收数据
    char buffer[1024];
    memset(buffer, 0, sizeof(buffer));
    if (recv(clientSockfd, buffer, sizeof(buffer), 0) == -1) {
        perror("recv");
        exit(EXIT_FAILURE);
    }

    std::cout << "接收到的消息:" << buffer << std::endl;

    // 发送数据到客户端
    const char* response = "Hello from server!";
    if (send(clientSockfd, response, strlen(response), 0) == -1) {
        perror("send");
        exit(EXIT_FAILURE);
    }

    close(clientSockfd);
    close(sockfd);

    return 0;
} 

你可能感兴趣的:(C++,c++,面试)