C++面试总结

面试总结

C++三大特性

封装:

把一类事物的属性和行为提取出来,组合在一起,用类这种自定义的数据类型把他们包起来隐藏对象的属性和实现细节,仅对外公开接口和对象进行交互 。

使用的类时候进行实例化。实例化对象分配内存的方式是字节对齐,可以实例化多个对象,且每个对象都有一份自己的数据成员,但是成员函数在内存中只占一块空间,它们属于这个类,不属于某一个对象,所有对象都可以使用的,是共享的。

继承:

在原有类的基础上进行扩展,也可以将一些事物的相同属相或方法封装为一个类,不同的部分通过继承进行扩充实现。

多态:

静态多态是指在编译阶段就能知道执行哪一个函数体 运算符重载与函数重载

动态多态是在运行阶段才能知道执行哪一个函数体 。通过虚函数表实现,虚函数表的实现原理是, 函数指针数组 ,保存的类中虚函数的地址,使用时保存数组的起始地址 。

解决了继承中父类指针或者引用操作子类对象,只能操作子类从父类继承过来成员。

在继承关系中,子类重写父类虚函数,通过父类的引用或者是指针去操作子类对象,去调用虚函数的时候才会触发多态

访问权限

  1. public:公有成员,可以在类外部被访问。
  2. private:私有成员,只能在类内部被访问。
  3. protected:保护成员,可以在类内部和派生类中被访问。

什么是类内与类外{}

多态

静态多态:

通过运算符重载与函数重载实现:

运算符重载:有两种实现方式,

成员函数重载:

数据类型 operator 运算符(操作数){}

友元函数重载:

函数类型 operator 运算符(操作数){}

函数重载:

函数名相同,功能相似,参数不同(参数的个数不同、参数类型不用、参数类型的顺序不用),与返回值类型无关的一组函数构成重载!

动态多态:

在C++中,如果一个类中有虚函数,那么编译器就会为该类生成一个虚表(virtual table),虚表中存储了所有虚函数的地址。当对象被创建时,会在对象中分配一个指针指向该类的虚表。

当调用该类的虚函数时,通过对象指针或引用找到该对象所属的虚表,并根据虚函数的索引在虚表中查找对应的函数地址,然后进行函数调用。因为虚表中存储的是函数地址,所以可以在运行时动态地决定调用哪个函数,从而实现动态多态性。

动态多态的实现条件:

必须有继承,一个父类,多个子类

父类中要有虚函数,子类要重写父类的虚函数

必须通过父类的引用或者是指针去操作子类对象,去调用虚函数的时候才会触发多态,实现动态的多态!

动态多态的实现原理

虚函数表, 也就是一个表里面可以保存多个虚函数的地址!

映射到我们的代码中: 函数指针数组。一个类中一旦有了虚函数,这个类就会有一个函数指针数组存在!函数指针数组中存放的就是这个类中虚函数的地址!

创建对象的时候,就会给函数指针数组分配空间,我们只需要保存数组名即可!也就是只要分配4字节(32位)的内存空间就可以了!保存数组的起始地址。

调用虚函数,就是去当前这个类的虚函数表中去找对应的函数,函数地址不同,执行的就是不同的函数体!

C++为什么不执行子类的析构函数,就会造成内存泄漏?

当通过父类指针去释放子类对象,发现析构函数只执行了父类的析构,并没有执行子类的析构,导致创建子类对象的时候,有在子类的构造中采用new的方式去动态分配空间,不执行析构,就没有办法释放对应的堆区空间

如何用list或者vector实现一个map

  1. 嵌套容器:可以将 list 或者 vector 作为 map 的值的类型,实现键值对的映射。例如,可以定义一个 vector> 或者 list>,其中 Key 是键的类型,Value 是值的类型,pair 表示一个键值对。这种方式的优点是简单易懂,缺点是查询效率较低,因为需要遍历整个容器来查找元素。
  2. 自定义哈希表:可以使用自定义的哈希函数和哈希冲突解决方法,将键映射到数组的下标上,从而实现 O(1) 的查询。实现一个哈希表比较复杂,需要解决哈希冲突、自动扩容等问题,因此不是很简单。

纯虚函数与虚函数有什么区别

  1. 虚函数是有实现的函数,它可以在基类中实现,也可以在派生类中重载。如果在基类中实现,则可以使用基类指针或引用来调用派生类的函数。虚函数可以被继承,并且在派生类中可以选择是否重载虚函数。
  2. 纯虚函数是没有实现的函数,它只有声明,没有定义。在基类中声明纯虚函数后,派生类必须实现它,否则派生类也将变成抽象类。纯虚函数可以用来定义接口,强制派生类实现某些函数,从而实现多态。

C++中常用的查找方法有:

  1. 线性查找:从数组的第一个元素开始逐个比较,直到找到目标元素或遍历完整个数组。

    int linearSearch(int arr[], int n, int x) {
        for (int i = 0; i < n; i++) {
            if (arr[i] == x) {
                return i;   // 找到目标元素,返回下标
            }
        }
        return -1;  // 遍历完整个数组,未找到目标元素,返回-1
    }
    
  2. 二分查找:针对有序数组,将数组分成两部分,每次取中间位置的元素与目标元素比较,缩小查找范围,直到找到目标元素或确定不存在。

    int binarySearch(int arr[], int n, int x) {
        int left = 0, right = n - 1;
        while (left <= right) {
            int mid = (left + right) / 2;
            if (arr[mid] == x) {
                return mid;   // 找到目标元素,返回下标
            } else if (arr[mid] < x) {
                left = mid + 1;   // 目标元素在右半部分
            } else {
                right = mid - 1;  // 目标元素在左半部分
            }
        }
        return -1;  // 未找到目标元素,返回-1
    }
    
  3. 哈希查找:通过哈希函数将目标元素映射到数组的某个位置,查找时直接访问该位置,通常用于快速查找大量数据。 C++中,可以使用STL库中的unordered_map来实现哈希查找。unordered_map是一个哈希表容器,可以存储键值对,并且支持快速查找、插入和删除操作。

    #include <iostream>
    #include <unordered_map>
    
    using namespace std;
    
    int main() {
        // 创建一个哈希表
        unordered_map<int, string> mymap;
    
        // 插入键值对
        mymap.insert(make_pair(1, "apple"));
        mymap.insert(make_pair(2, "banana"));
        mymap.insert(make_pair(3, "orange"));
    
        // 查找键值对
        int key = 2;
        unordered_map<int, string>::iterator it = mymap.find(key);
        if (it != mymap.end()) {
            cout << "key " << key << " found, value is " << it->second << endl;
        } else {
            cout << "key " << key << " not found" << endl;
        }
    
        // 删除键值对
        key = 3;
        mymap.erase(key);
        cout << "key " << key << " deleted" << endl;
    
        // 遍历哈希表
        for (auto it = mymap.begin(); it != mymap.end(); ++it) {
            cout << "key: " << it->first << " value: " << it->second << endl;
        }
    
        return 0;
    }
    
  4. 二叉查找树:通过将元素存储在二叉树中,并按照一定规则进行排序,从树的根节点开始查找,每次比较节点的值,缩小查找范围。

    • 定义二叉查找树节点结构体:结构体中包含节点的值、左右子节点指针等成员变量。

    • 实现二叉查找树的插入操作:首先判断二叉查找树是否为空,如果为空直接创建一个新节点作为根节点。如果不为空,则遍历二叉查找树,根据节点值与插入值的大小关系,判断将插入值插入到左子树还是右子树中。

    • 实现二叉查找树的查找操作:从根节点开始遍历二叉查找树,根据节点值与查找值的大小关系,判断继续遍历左子树还是右子树。如果遍历到空节点仍未找到,则返回空。

    • 实现二叉查找树的删除操作:先找到要删除的节点,然后根据节点的子节点情况,分别处理删除操作。

  5. AVL树:一种自平衡二叉查找树,通过旋转操作保持树的平衡,提高查找效率。

  6. 红黑树:一种自平衡二叉查找树,通过对节点的颜色进行调整,保持树的平衡,提高查找效率。

  7. B树:一种多路平衡查找树,每个节点可以存储多个元素,提高内存利用率,适合存储大量数据。

  8. 布隆过滤器:一种快速查找算法,通过哈希函数将元素映射到多个位置,将这些位置标记为存在,查找时通过哈希函数判断元素可能存在的位置,如果这些位置均被标记,则认为元素存在,否则认为元素不存在。

C++常用的排序算法

  1. 冒泡排序:每次将相邻的两个元素比较,如果顺序不对就交换它们的位置,直到整个数组有序。

    void bubbleSort(int arr[], int n) {
        for (int i = 0; i < n - 1; i++) {
            for (int j = 0; j < n - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    swap(arr[j], arr[j + 1]);
                }
            }
        }
    }
    
    
  2. 选择排序:每次从未排序的部分中选出最小的元素,与未排序部分的第一个元素交换位置,直到整个数组有序。

    void selectionSort(int arr[], int n) {
        for (int i = 0; i < n - 1; i++) {
            int minIdx = i;
            for (int j = i + 1; j < n; j++) {
                if (arr[j] < arr[minIdx]) {
                    minIdx = j;
                }
            }
            swap(arr[i], arr[minIdx]);
        }
    }
    
  3. 插入排序:将数组分为已排序部分和未排序部分,每次从未排序部分选出一个元素,插入到已排序部分的正确位置,直到整个数组有序。

    void insertionSort(int arr[], int n) {
        for (int i = 1; i < n; i++) {
            int j = i - 1, temp = arr[i];
            while (j >= 0 && arr[j] > temp) {
                arr[j + 1] = arr[j];
                j--;
            }
            arr[j + 1] = temp;
        }
    }
    
  4. 快速排序:选择一个基准元素,将数组分为左右两部分,将小于基准元素的元素放在左侧,大于等于基准元素的元素放在右侧,然后递归地对左右两部分进行快速排序,直到整个数组有序。

    int partition(int arr[], int left, int right) {
        int pivot = arr[right];
        int i = left - 1;
        for (int j = left; j < right; j++) {
            if (arr[j] < pivot) {
                i++;
                swap(arr[i], arr[j]);
            }
        }
        swap(arr[i + 1], arr[right]);
        return i + 1;
    }
    
    void quickSort(int arr[], int left, int right) {
        if (left < right) {
            int p = partition(arr, left, right);
            quickSort(arr, left, p - 1);
            quickSort(arr, p + 1, right);
        }
    }
    
    
  5. 归并排序:将数组分为两部分,递归地对左右两部分进行归并排序,然后将排好序的左右两部分合并起来,得到完整的有序数组。

    void merge(int arr[], int left, int mid, int right) {
        int len1 = mid - left + 1, len2 = right - mid;
        int L[len1], R[len2];
        for (int i = 0; i < len1; i++) {
            L[i] = arr[left + i];
        }
        for (int j = 0; j < len2; j++) {
            R[j] = arr[mid + j + 1];
        }
    
        int i = 0, j = 0, k = left;
        while (i < len1 && j < len2) {
            if (L[i] <= R[j]) {
                arr[k++] = L[i++];
            } else {
                arr[k++] = R[j++];
            }
        }
        while (i < len1) {
            arr[k++] = L[i++];
        }
        while (j < len2) {
            arr[k++] = R[j++];
        }
    }
    
    void mergeSort(int arr[], int left, int right) {
        if (left < right) {
            int mid = (left + right) / 2;
            mergeSort(arr, left, mid);
            mergeSort(arr, mid + 1, right);
            merge(arr, left, mid, right);
        }
    }
    
    
  6. 堆排序:将数组建立成一个堆,每次从堆中选出最大(或最小)的元素,放入已排序部分的末尾,直到整个数组有序。

    void heapify(int arr[], int n, int i) {
        int largest = i;
        int l = 2 * i + 1, r = 2 * i + 2;
        if (l < n && arr[l] > arr[largest]) {
            largest = l;
        }
        if (r < n && arr[r] > arr[]argest]) {
            largest = r;
        }
        if (largest != i) {
            swap(arr[i], arr[largest]);
            heapify(arr, n, largest);
        }
    }
    
    void heapSort(int arr[], int n) {
        for (int i = n / 2 - 1; i >= 0; i--) {
            heapify(arr, n, i);
        }
        for (int i = n - 1; i >= 0; i--) {
            swap(arr[0], arr[i]);
            heapify(arr, i, 0);
        }
    }
    
    

4种类型转换函数

const_cast 只有一种用途,去掉类型的const或volatile属性
static_cast 无条件转换,静态类型转换(类似强转)
dynamic_cast 子类转父类。有条件转换,动态类型转换,运行时检查类型安全(转换失败返回NULL)
reinterpret_cast 仅重新解释类型,但没有进行二进制的转换

QT中如何自定义一个控件

  1. 创建一个新类,继承QWidget或其子类,例如QLabel、QPushButton等。
  2. 在新类的头文件中声明需要的成员变量、函数和信号槽等。
  3. 实现构造函数和析构函数,在构造函数中初始化控件的属性和样式等。
  4. 在新类中重写需要的绘图函数(如paintEvent),实现自定义绘制。
  5. 在新类中处理鼠标、键盘等事件,实现响应用户输入的功能。
  6. 可以添加需要的属性、方法和信号槽等,方便其他代码调用。
  7. 编译项目,使用新类创建实例并显示在界面上。

学习QT时使用过的控件

  1. QLabel:显示文本、图片等。
  2. QPushButton:按钮控件。
  3. QCheckBox:复选框控件。
  4. QRadioButton:单选按钮控件。
  5. QLineEdit:单行文本输入框。
  6. QTextEdit:多行文本输入框。
  7. QComboBox:下拉框控件。
  8. QSpinBox:数字选择框控件。
  9. QSlider:滑动条控件。
  10. QProgressBar:进度条控件。
  11. QTabWidget:选项卡控件。
  12. QTreeWidget:树形控件。
  13. QListWidget:列表控件。
  14. QCalendarWidget:日历控件。
  15. QDockWidget:浮动窗口控件。
  16. QMenu/QMenuBar/QAction:菜单和菜单项控件。
  17. QGraphicsView/QGraphicsScene/QGraphicsItem:图形控件。

string类的构造函数、析构函数、拷贝构造和赋值运算符重载

  1. 构造函数
  • 默认构造函数:std::string(),创建一个空字符串对象。
  • 复制构造函数:std::string(const std::string& str),创建一个与给定字符串str相同的新字符串对象。
  • 字符串构造函数:std::string(const char* s),从C风格字符串s中创建一个新字符串对象。
  • 子串构造函数:std::string(const std::string& str, size_t pos, size_t len),从给定字符串str的位置pos开始,长度为len创建一个新字符串对象。
  • 重复构造函数:std::string(size_t n, char c),创建一个由n个字符c组成的新字符串对象。
  • 范围构造函数:std::string(InputIt first, InputIt last),从迭代器区间[first, last)中创建一个新字符串对象。
  • 移动构造函数:std::string(std::string&& str),从一个右值引用str创建一个新字符串对象。
  1. 析构函数

std::string::~string(),释放字符串对象占用的内存。

  1. 拷贝构造函数

std::string(const std::string& other),从另一个字符串对象other中创建一个新字符串对象。

  1. 赋值运算符重载
  • 拷贝赋值运算符:std::string& operator=(const std::string& other),将另一个字符串对象other的内容拷贝到当前字符串对象中。
  • 移动赋值运算符:std::string& operator=(std::string&& other),将一个右值引用other的内容移动到当前字符串对象中。
  • 字符串赋值运算符:std::string& operator=(const char* s),将C风格字符串s的内容赋值给当前字符串对象。
  • 字符赋值运算符:std::string& operator=(char c),将字符c赋值给当前字符串对象。

模板函数

C++模板函数是一种通用函数,可以处理多种不同的数据类型,而不需要为每种数据类型编写不同的函数。

模板函数是模板,不占用内存空间,在使用的时候把真正的数据类型传进去的时候,才会变成一个真正的函数!这个真正的函数是系统根据我们给的具体的类型自动帮我们生成!

类模板

C++模板类是一种通用类,可以处理多种不同类型的数据,而不需要为每种数据类型编写不同的类。

static

  1. 静态变量:在函数内部使用static修饰的变量称为静态变量,它们的生命周期从程序开始运行到程序结束,不会因为函数的调用而被创建或销毁。静态变量在第一次被初始化时分配内存,在程序结束时释放内存。静态变量的作用域仅限于定义它的函数内部,但是它们的值在函数调用之间保持不变。
  2. 静态函数:在函数声明时使用static修饰的函数称为静态函数,它们的作用域仅限于定义它们的文件内部,不能被其他文件访问。静态函数的一个主要作用是隐藏函数的实现细节,防止其他文件直接访问。
  3. 静态成员变量和函数:在类内部使用static修饰的成员变量和函数称为静态成员变量和函数,它们属于整个类而不是类的某个实例,因此它们不依赖于类的任何实例而存在。静态成员变量和函数在类定义时被声明,并在类外部进行初始化。静态成员变量和函数可以被类的所有实例和类名直接访问。

需要注意的是,静态变量和静态成员变量的初始化必须在定义时进行,而不能在构造函数中进行。此外,静态成员变量和函数可以通过类名和作用域解析运算符::来访问,而普通成员变量和函数只能通过实例名和点运算符.来访问。

static修饰类的数据成员

类的静态的数据成员属于类,并不属于具体的某一个对象;但是类的所有对象都可以访问!

类的静态的数据成员要在类内做声明,类外做定义以及初始化!

类的静态的数据成员初始化的格式: 数据类型 类名::成员变量名=初始值;

建议在对应的cpp的开头去做!

类的静态的数据成员访问的方法:

通过对象的方式去访问: 对象名.变量名;

通过类的方式去访问: 类名::变量名;

static修饰类的成员函数

类的静态成员函数可以通过对象的方式去调用,也可以通过类名::的方式去调用

类的静态的成员函数是没有this指针的

类的静态成员函数是不能访问类的非静态的成员;

类的静态成员函数只能访问类的静态的成员

类的非静态成员能访问类的静态成员。

什么时候会把类的数据成员和成员函数用static修饰呢?

想要实现数据共享的,就可以把这个数据用static去修饰,要记得做初始化!

成员函数当要在多个类中都使用到这个函数,就可以把它用static修饰一下,变成静态的成员函数,这样在其他类中可以通过类名::成员函数();

const

  1. 定义常量:使用const修饰的变量是只读的,它们不能被修改。定义常量时,需要在类型前加上const关键字来指示编译器该变量是常量。例如:const int a = 10;
  2. 保护函数返回值:使用const修饰函数返回值时,可以确保函数返回的值不会被修改。例如:const int getValue() { ... }
  3. 保护函数参数:使用const修饰函数参数时,可以确保函数在使用参数时不会修改它们的值。同时,使用const修饰的函数参数可以接受常量对象和非常量对象,从而提高函数的通用性。例如:void printValue(const int a) { ... }
  4. 防止指针操作:使用const修饰指针时,可以确保指针指向的值不会被修改。同时,使用const修饰的指针只能指向const对象或非const对象的const属性,而不能指向非const对象。例如:const int* p = &a;

const修饰类的成员

const修饰的数据成员不能在构造函数里面进行初始化;要在构造函数之前去做

如何初始化const修饰的数据成员:采用初始化列表的方式

总结:

类的非静态数据成员都可以使用初始化列表进行初始化。

类的const修饰的数据成员必须使用初始化列表进行初始化。

初始化列表给数据成员初始化的顺序先后是不会影响结果的!

const修饰类的成员函数

const修饰成员函数,本质上修饰的是函数中隐藏的那个this指针。

const修饰的成员函数可以有一个非const修饰的同名函数和他互为重载

const修饰的成员函数只能访问数据成员,并不能修改数据成员的值

const修饰的成员函数是不可以调用非const修饰的成员函数

非const修饰的成员函数能调用const修饰的成员函数。

const修饰的成员函数叫做常函数

const修饰类对象

const修饰的类对象是不可以调用非const修饰的成员函数;可以调用const修饰的成员函数的!

const修饰的对象是常对象,只能调用常函数!

define与const区别

  1. define是预处理指令,编译器在编译前会将define定义的常量替换为具体的值,而const是变量修饰符,编译器在编译时会为const定义的常量分配内存空间。
  2. define定义的常量没有类型,而const定义的常量有类型,因此在使用时需要注意数据类型的匹配。定义常量时,使用const关键字可以提高代码的可读性和可维护性。
  3. define定义的常量可以用于预处理指令(如#if和#ifdef),而const定义的常量则不能。
  4. define定义的常量没有作用域限制,而const定义的常量有作用域限制。使用const关键字定义的常量只在定义所在的作用域内有效,而使用define定义的常量则在整个程序中都有效。
  5. 在使用define定义常量时,需要注意宏定义中参数的类型和作用域。在使用const定义常量时,需要注意const变量的初始化和修改,以避免由于const变量的特殊性质导致的错误。

C++工厂模式代码实现

#include <iostream>
#include <string>

// 抽象产品类
class Product {
public:
    virtual ~Product() {}
    virtual std::string name() const = 0;  
};

// 具体产品类 A
class ProductA : public Product {
public:
    std::string name() const override {
        return "Product A";
    }  
};

// 具体产品类 B
class ProductB : public Product {
public:
    std::string name() const override {
        return "Product B";
    }  
};

// 抽象工厂类
class Factory {
public:
    virtual ~Factory() {}
    virtual Product* create() const = 0;  
};

// 具体工厂类 A
class FactoryA : public Factory {
public:
    Product* create() const override {
        return new ProductA;
    }  
};

// 具体工厂类 B
class FactoryB : public Factory {
public:
    Product* create() const override {
        return new ProductB;
    }  
};

int main() {
    // 使用具体工厂类 A 创建产品对象 A
    Factory* factoryA = new FactoryA;
    Product* productA = factoryA->create();
    std::cout << "Product name: " << productA->name() << std::endl;
    delete factoryA;
    delete productA;

    // 使用具体工厂类 B 创建产品对象 B
    Factory* factoryB = new FactoryB;
    Product* productB = factoryB->create();
    std::cout << "Product name: " << productB->name() << std::endl;
    delete factoryB;
    delete productB;

    return 0;
}

这段代码定义了一个抽象产品类 Product,以及两个具体产品类 ProductAProductB,它们都继承自 Product 并实现了 name 方法。

同时,这段代码还定义了一个抽象工厂类 Factory,以及两个具体工厂类 FactoryAFactoryB,它们都继承自 Factory 并实现了 create 方法,用于创建对应的产品对象。

在主函数中,先使用具体工厂类 A 创建产品对象 A,然后使用具体工厂类 B 创建产品对象 B。通过这种方式,客户端代码可以在不了解具体产品类及其创建方式的情况下创建对应的产品对象。

C++单例模式

需要注意的是,单例模式虽然可以确保一个类只有一个实例,但也有可能带来以下问题:

  1. 破坏单一职责原则:单例模式一般将对象的创建和获取功能合并在一个类中,这可能导致该类的职责过重。
  2. 破坏开闭原则:如果需要扩展单例类的功能,可能需要修改该类的实现代码,这与开闭原则不符。
  3. 多线程下的安全问题:如果多个线程同时调用 getInstance() 函数,可能导致创建多个实例的问题。可以采用线程安全的写法来解决此问题。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值