• 【面试】C/C++面试八股


    编译过程的四个阶段

    一、预处理
    头文件展开,宏替换,去注释,条件编译
    二、编译
    语法词法分析,将源代码转化为汇编指令
    三、汇编
    将汇编指令转换为二进制的机器指令
    四、链接
    用目标文件+库文件生成可执行文件

    C++和C语言的区别

    一.C++解决了很多C语言固有的缺陷:

    1. 针对C语言中存在命名冲突,C++提供了命名空间来解决
      2.C语言不支持函数缺省参数,C++也提供了缺省参数的方法
      3.在C语言中,由于宏函数的有效性无法检查,所以C++提出了内联函数解决

    二.在C语言指针的基础上,C++提出了引用

    三.C++可以实现函数重载,原因是C语言对于函数的区分是用函数名来区分的,而C++是使用函数名+函数参数列表来区分的,因此C语言中,只要函数名称一致,就会导致编译器报错,而C++中,可以设置函数名称一致,而参数列表不同的函数,作为函数的重载形式
    四.C语言采用的是面向过程的编程思想,专注于过程。而C++采用的是面向对象的编程思想,关注的是对象,靠对象之间的交互完成
    五.C语言的结构体中只能定义变量,而C++的类中,既可以定义变量也可以定义函数

    简单介绍一下三大特性

    封装:就是将数据和对数据的操作方式结合起来,隐藏细节,对外暴露一些接口,以供对象之间进行交互,本质上是为了让用户更好的管理类

    继承:是一种代码的复用手段,目的是在原有类的基础上进行扩展,增加新的功能,生成新的类

    多态:就是多种形态,当完成某个动作时,不同的对象去完成,产生不同的状态
    多态可以分为静态多态和动态多态
    静态多态是在程序编译阶段就确定了函数的执行动作,典型例子是:函数重载。
    动态多态是在程序运行时通过传递的对象实际类型来确定函数的执行,典型例子是:函数的重写

    多态的实现原理

    多态的实现条件
    1.需要首先让派生类重写基类的虚函数
    2.通过基类的指针或引用来调用虚函数

    多态的实现原理:
    某个类成员函数被virtual修饰之后,该类的对象模型中就会有一个虚表指针,这个指针指向一个虚表,该虚表中就会存放虚函数地址。
    当某个派生类将该基类继承之后,该派生类的对象模型中也会有一个和基类中一致的虚表指针,如果这个派生类将基类中的虚函数进行了重写,那么自己的虚表中就会将原来基类虚表中对应的虚函数指针替换
    此时如果我们使用基类的指针或引用去调用虚函数,那么如果传递的是基类对象,那么就会通过this指针去调用基类对象的成员函数,如果传递的是子类对象,那么就会通过this指针去调用子类对象的成员函数。

    虚函数的构成原理

    在编译阶段,将虚函数按照在类中的声明次序依次添加到虚表中

    在单继承体系下,子类虚表的构建原理:
    1、原封不动将基类虚表中的内容拷贝一份到子类虚表中
    2、如果子类中哪个函数重写了基类虚表中的虚函数,则将虚表中对应的虚函数指针替换为当前重写后的虚函数指针
    3、将子类新增的虚函数按照在子类中声明的次序进行添加

    虚函数的调用原理

    通过实际传入的对象的this指针,拿到对应的虚表指针,再通过对应的虚表指针,找到对应的虚表,然后通过虚表中存放的虚函数地址,来进行虚函数的调用。

    包含有虚函数的类对象,编译器在编译时,就为该对象创建了一个虚表指针,以便于能够正确的获取该类的虚表

    虚表指针在什么地方进行初始化的?

    在构造函数中,进行虚表的创建和虚表指针的初始化

    构造函数为什么不能是虚函数

    因为虚函数对应一个虚表指针,这个虚表指针是存储在对象的内存空间的,构造函数就是用来去开辟对象的内存空间 的,如果构造函数是虚函数,意味着在对象内存空间还不存在的就要去初始化对象内存空间中的值,是无法实现的

    为什么建议将析构函数设为虚函数

    如果没有将析构函数设为虚函数,那么当传递的是子类对象,而我们是用基类对象的指针或引用去接收时,会导致无法调用子类对象的析构函数,从而导致内存泄漏

    虚函数和纯虚函数的区别

    虚函数目的是建立抽象模型,方便系统扩展
    纯虚函数是不具体实现的函数,一种特殊的函数

    虚函数必须是类的非静态成员

    纯虚函数是虚函数的子集,用于抽象类,包含有纯虚函数的类是抽象类,不能实例化对象
    因为有时候基类实例化对象是不合理的,例如:基类是动物,子类是猫狗,那么基类本身可以创建对象就是不合理的,那么用纯虚函数修饰之后,让基类无法创建对象才是较为合理的

    抽象类

    包含纯虚函数的类就是抽象类
    抽象类不能实例化对象,只能在该类被派生之后,并且所有的纯虚函数都被重写之后,才能被创建对象

    类对象的对象模型

    类对象中只存储了类的成员变量,并且按照成员变量的声明顺序进行存储,遵循内存对齐原则,如果存在虚函数,那么会有虚表指针,如果存在虚拟继承,那么还会有一个虚基表指针。
    在继承体系下,如果是普通的继承,那么子类对象的对象模型是:
    首先是基类继承,然后是子类新增
    在这里插入图片描述

    如果是虚拟继承:
    继承自多个基类,那么按照派生类的声明顺序进行。
    每个来自基类的对象模型中,只存储一个虚基表指针,指向各自基类对应位置
    然后是子类新增,最后是基类成员变量
    在这里插入图片描述

    内存分布方式?

    由高地址到低地址分别为:

    栈区,共享区,堆区,全局区,常量区,代码区

    栈区:由高地址向低地址生长,存储函数调用的临时信息,局部变量等

    共享区:共享内存通过页表映射到不同的进程虚拟地址空间中

    堆区:由低地址向高地址生长,由程序员调用malloc或new申请的空间

    全局区:分为BSS段和数据段,BSS段为未初始化的全局或者静态变量,数据段为已经初始化的全局或者静态变量

    常量区:存放常量字符串,程序结束后由系统释放

    代码段:存放类的成员函数,静态函数和全局函数的二进制代码

    内存对齐是什么?为什么要内存对齐

    1、提高内存访问效率
    2、减小内存占用量

    static关键字的作用

    static关键字的作用: static关键字的作用

    const关键字的作用

    const关键字的作用:const关键字的作用

    堆和栈的区别

    栈也叫栈内存,特点是空间小,读写速度快,数据先进后出,主要存储的是函数内部的临时变量等
    堆也叫堆内存,特点是空间大,读写速度慢,主要存储程序员手动申请的内存空间,在释放之前始终保留直至程序生命周期结束

    内联函数与宏函数区别

    内联函数:在函数定义位置前用inline修饰,在编译阶段会将内联函数进行展开
    宏函数(宏定义):在函数定义前用#define修饰,在预处理阶段就会对宏函数进行展开

    #define fun1() cout << "a" << endl
    
    inline void fun2()
    {
    	cout << "a" << endl;
    }
    int main()
    {
    	//宏函数是在预处理阶段会将fun1() ----> cout << "a" << endl
    	fun1();
    	//在编译阶段会直接将fun2()  --->  cout << "a" << endl;
    	fun2();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    宏函数的优点:
    提高性能,增加代码复用性
    宏函数的缺点:
    不方便调试,可读性差,没有类型安全的检查

    改进方式:常量----->const修饰,函数------>内联函数

    指针和引用的区别

    1.指针存储的是地址,而引用是对变量的一个别名
    2.引用在定义时必须初始化,而指针不用
    3.指针可以通过修改值来改变指针的指向,引用关系一旦确立,引用的实体就不能改变了
    4.sizeof时,传递的是指针,计算的是指针占空间的大小,引用是变量类型的大小
    5.指针需要显式解引用,引用不用
    6.存在空置指针,而不存在空值引用

    什么时候传递指针,什么时候传递引用

    首先传递指针和传递引用都能在函数内对变量进行修改,
    本质上传递指针和传递引用没有区别,但是我们在C语言编程时会用指针传递,C++中会用引用传递
    引用本身就是变量的别名,当作为参数传递,是可以修改传入变量的,但是如果在函数内不想修改变量 ,那么可以使用const修饰该引用。

    介绍一下vector的扩容过程

    vector如何释放内存空间

    map与unordered_map的区别

    1.map的底层是红黑树,unordered_map底层是哈希表
    2.map是按照key有序的,unordered_map无序
    3.map的查询效率是O(logn),unordered_map是O(1)
    4.map更适合需要数据有序,对查询效率不是那么高的场景,unordered_map适合对查询效率极高的场景

    对于插入来说,operator[]与insert的区别

    如果key存在,那么都会插入失败,但是operator[]会将原来的key对应的value替换为新的value,而insert不进行任何操作
    如果key不存在,那么都会插入成功,通过key和value构造键值对,进行插入

    operator[]底层调用的是insert,insert返回的是对应位置的迭代器,operator[]拿到返回的迭代器,返回的是key对应的value

    对于访问来说,operator[]与.at的区别

    如果key存在,operator[]和.at()一样会返回key对应的value
    如果key不存在,operator[]会key和默认的value构成键值对,插入map中

    new和malloc的区别

    1.new是关键字,malloc是库函数
    2.new申请空间失败会抛异常,malloc不会
    3.new只需要传递类型,编译器会根据类型判断申请空间大小,malloc需要手动传递字节数
    4.new申请空间之后不需要进行类型转换,malloc需要
    5.new在申请空间之后还会调用构造函数进行对象的构造,而malloc不会,同理delete会自动调用析构函数,而free不会

    new的实现原理

    1、调用operator new()函数申请空间
    2、在申请的空间上执行构造函数,完成对象的构造

    operator new的原理

    1、调用malloc函数申请空间
    2、如果申请成功,返回空间地址,如果申请失败,会执行用户定义的方法,继续申请空间,如果用户没有定义申请失败的方法,那么就会抛异常

    malloc的实现原理

    1、内存管理:内存管理的数据结构是一个链表, 每个节点表示的是一个内存块,调用malloc时,先遍历该链表找到一块足够大的内存块,标记为已分配状态
    2、内存分配,将已分配的内存块首地址返回给程序

    内存泄露的检测工具

    在linux下进行内存泄漏的检测工具:valgrind
    在win下的内存泄漏检测工具:VLD

    特殊类的设计

    设计类不能被继承:将构造函数私有化
    设计类不能被拷贝:将拷贝构造和赋值运算符重载只声明不定义
    设计类只能从堆上创建对象:
    方法一:将构造函数和拷贝构造函数私有化,设置一个静态成员函数,函数内通过new创建对象,返回对象指针
    设计类只能从栈上创建对象:
    方法一:将构造函数私有化,将operator new()禁掉
    方法二:将构造函数和拷贝构造函数私有化,设置一个静态成员函数,在该函数中调用构造函数,返回对象

    设计一个单例类:
    饿汉模式:将构造拷贝构造赋值运算符都私有化,类内声明一个私有的静态类对象和一个公有的静态成员函数,在函数内对静态类对象进行初始化,返回对象指针

    懒汉模式:将构造函数和拷贝构造函数和赋值运算符重载都私有化,类内声明一个私有的静态类对象指针和一个公有的静态成员函数,在函数中new一个对象的指针赋值给该静态对象指针。需要注意的是double chack

    C++11的锁有哪些

    六大种:
    1、mutex
    2、recursive_mutex
    3、timed_mutex
    4、recursive_timed_mutex
    5、lock_guard:对mutex进行封装,实现了RAII,可以预防死锁
    6、unique_lock:在lock_guard上增加了加锁解锁查看锁状态等功能

    C++11的智能指针

    auto_ptr:已资源管理权限转移的方式解决浅拷贝,弃用,容易造成野指针

    unique_ptr:已资源独占的方式解决浅拷贝,无法拷贝,不方便

    shared_ptr:已资源计数的方式解决浅拷贝,容易造成循环引用的问题,解决方式是weak_ptr,用ListNode的例子解释

    几种排序的思路,代码实现

    排序的思路,代码实现

    深浅拷贝

    在一个类中,如果没有显式定义拷贝构造函数或者赋值运算符重载,编译器自动生成的拷贝构造函数和赋值运算符重载函数就是浅拷贝。

    浅拷贝: 复制对象的过程中,仅复制对象的成员变量的值,不复制指向的动态内存,多个对象可能共用一份数据

    深拷贝:复制对象的过程中,不仅复制对象的成员变量的值,还复制指向的动态内存,创建独立的副本

    静态成员函数的意义

    要求某个函数在对象不被创建的情况下进行调用
    要求某个函数只能去调用该类中的静态成员变量
    没有隐藏的this指针,能够封装线程类(线程的入口函数)
    实现一些特殊类的设计,如设计类不能被继承,不能被拷贝,不能在栈上创建对象,不能在堆上创建对象,单例类等

    struct 和 union的区别

    struct和union都是由多个不同数据类型的变量组成,但是在同一时刻,union只存放了一个被选中的成员变量,而struct中所有的成员都是同时存在的

    struct中每个成员都有各自的内存空间,互不影响,内存大小为所有内存空间的和(遵循内存对齐原则),而union所有成员不能同时占用各自的内存空间,内存大小为union内最大的成员的大小

    对union中的成员进行赋值时,将会对其他成员进行覆盖,所以union变量中起作用的是最后一次存放的成员,而对struct成员赋值,对其他成员是不影响的

    class和struct的区别

    一、在C和C++中struct是有区别的

    1、C语言中,结构体不能为空,而C++中可以
    2、C语言中,结构体内部只能定义成员变量,不能定义函数,而C++可以
    3、C语言中,创建结构体变量时,必须声明struct,C++不用

    二、C++中类和结构体的区别

    1、结构体多用于描述轻量级对象,如Node,类多用于描述数据量大,逻辑复杂的对象
    2、struct的默认继承权限和访问权限是都是public,class的都是private

    define和const的区别

    一、C/C++中const的区别:
    在C语言中,const用来定义不能改变的常量,可以修饰变量,函数参数,指针等,表示该值不能被修改
    在C++中,对const的作用进行了扩充,可以作为宏常量来使用

    二、define和const在C++中的区别

    1、编译阶段不同
    define是在预处理阶段展开的,const是在编译阶段展开的

    2、类型检查和调试不同
    define是没有类型的,无法进行类型检查,也无法进行调试
    const是有类型的,可以进行类型检查,也可以进行调试

    内联函数和宏函数的区别

    1、编译阶段不同
    define可以定义宏函数,其是在预处理阶段展开的,inline修饰的函数是内联函数,其是在编译阶段展开的

    2、类型检查和调试不同
    define是没有类型的,无法进行类型检查,也无法进行调试
    inline内联函数是有类型的,可以进行类型检查,也可以进行调试

    共同点:都减小了参数压栈和函数栈帧的调用,提高了代码运行效率,但是会造成代码膨胀的问题

    动态库和静态库的区别

    1、编译方式不同:
    静态库是在编译阶段将库函数打包到可执行程序中,而动态库是在运行时动态加载到程序中的,因此生成的可执行文件不包含库函数的代码,当程序使用到了哪个库函数,就链接调用哪个库函数,因此动态库生成的可执行文件比静态库生成的可执行文件小很多

    2、内存使用方式不同:
    静态库是将代码全部复制到了程序使用的内存中,不需要额外的内存空间,而动态库是在运行时才被加载到内存中,因此将动态库代码复制到程序使用的内存中,会额外占用内存空间

    多个程序可以共享一个动态库,不需要加载重复的库文件,而静态库只能单个程序使用

    3、更新和维护方式不同:
    静态库作为可执行文件的一部分,更新和维护需要对整个可执行文件进行重新编译和部署,才能让所有该静态库的程序都使用更新的代码

    动态库作为一个单独的文件存于系统中,可以被多个程序共享,因此可以独立于程序进行更新,只需要替换掉旧的动态库文件,不需要重新编译所有使用了该动态库的程序

    因此需要多个程序共享同一个库,或者需要较少的内存占用,动态库更合适
    而需要保持部署和更新的稳定性,静态库更合适

    define和typedef的区别

    请看:define和typedef的区别

  • 相关阅读:
    Redis数据结构之——ziplist
    材料安全数据分析表(一)丨艾美捷NEUROD6单克隆抗体(M17)
    【学习笔记】 - 基础数据结构 :Link-Cut Tree(进阶篇)
    基于 WebWorker 和 indexedDB 的高性能、高容量、高扩展的web端日志系统
    如何不编写 YAML 管理 Kubernetes 应用?
    秋招每日一题T27——日期
    代码随想录Day43 | 1049.最后一块石头的重量II | 494. 目标和 | 474. 一和零
    助力项目快捷实现国际化,造个多语言轮子
    原生Android 以面向对象的方式操作canvas
    10.docker exec -it /bin/bash报错解决、sh与bash区别
  • 原文地址:https://blog.csdn.net/qq_40376612/article/details/133411301