C++面经

C++面经

1.C和C++的区别

面向对象和面向过程的区别

  • C语言是面向过程的编程语言。面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较**消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。面向过程没有面向对象易维护、易复用、易扩展

  • C++是面向对象的编程语言,面向对象易拓展,易复用,易维护。引入了,有封装,继承,多态三大特性,可以设计出低耦合的系统,但性能比面向过程低。C++模板含有STL库,可复用性高

面向过程就是按流程办事不管谁在办,
面向对象是关注某个对象在做什么事。
比如吃饭,面向过程关注的是开口,咀嚼,吞咽这个流程;
面向对象的关注 我,这个个体 在吃饭的时候的行为。

2.C++三大特性

C++11中引入了nullptr、auto变量、Lambda匿名函数、右值引用、智能指针。

  • 封装:就是类的抽象,将具体的事物对象进行抽象化,封装其属性和行为,隐藏不必要的代码和实现细节,留下给我们用户调用的接口

  • 继承:(代码重用和接口继承
    实现了代码的重用,即派生类要使用基类的属性和方法,就不用再重新编写代码,这种可以算是实现继承。还有一种就是继承了某样东西,但是派生类需要重新实现一下,也就是接口(重用)继承
    继承缺点:子类会继承父类的部分行为,父类的任何改变都可能影响子类的行为

  • 多态:基类的指针指向不同的派生类其行为不同。实现了接口的重用,同样的接口,派生类与基类不同的实现。多态性是一个接口多种实现,是面向对象的核心。分为编译多态性和运行多态性(静态多态和动态多态)。

    编译时多态:比如函数重载,还有运算符重载
    运行时多态:**利用虚函数,**借助虚函数表,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。

  • 多态衍生:虚函数作用以及底层原理

    虚函数是用来实现动态绑定对象的。

    C++中虚函数使用虚函数表和虚表指针实现,虚函数表是一个类的虚函数的地址表,用于索引类本身以及父类的虚函数的地址,假如子类重写了父类的虚函数,则对应在虚函数表中会把对应的虚函数替换为子类的函数的地址(子类中可以不是函数,但是必须同名);虚函数表指针存在于每个对象中(通常出于效率考虑,会放在对象的开始地址处),它指向对象所在类的虚函数表的地址;在多继承环境下,会存在多个虚函数表指针,分别指向对应不同基类的虚函数表。

    虚函数表和类是对应的,虚表指针是和对象对应的

    前置知识

    虚函数

    1. 用virtual关键字声明的函数叫做虚函数,虚函数肯定是类的成员函数
    2. 存在虚函数的类都有一个一维的虚函数表叫做虚表。每一个类的对象都有一个指向虚表开始的指针。虚表和类是对应的,虚表指针是和对象对应的。
    3. 运行多态是虚函数实现,结合动态绑定
    4. 抽象类是指包括至少一个纯虚函数的类(虚函数再加上=0,该函数只有声明,没有实现)

    纯虚函数:父类中如果都是纯虚函数,那么此父类可以作为接口;
    子类可以直接重写(覆盖)父类的纯虚函数,实现多态。

    纯虚函数是抽象类,抽象类定义:

    1. 抽象类不能定义对象,只描绘这组子类共同的操作接口,而完整的实现留给子类去完成

    2. 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出

    注意:如果子类中没有堆区数据,可以不写虚析构或纯虚析构

    • 创建时机

    (1)虚函数表指针(vptr)创建时机
    vptr跟着对象走,所以对象什么时候创建出来,vptr就什么时候创建出来,也就是运行的时候。当程序在编译期间,编译器会为构造函数中增加为vptr赋值的代码(这是编译器的行为),当程序在运行时,遇到创建对象的代码,执行对象的构造函数,那么这个构造函数里有为这个对象的vptr赋值的语句。

    (2)虚函数表创建时机
    虚函数表创建时机是在编译期间。编译期间编译器就为每个类确定好了对应的虚函数表里的内容。所以在程序运行时,编译器会把虚函数表的首地址赋值给虚函数表指针,所以,这个虚函数表指针就有值了。

    • 为什么需要虚析构函数?(什么情况下要用虚析构函数?)
      在存在类继承并且析构函数中需要析构某些资源时,析构函数需要是虚函数。否则若使用父类指针指向子类对象,在delete时只会调用父类的析构函数,而不能调用子类的析构函数,造成内存泄露。

    • 析构函数一定是虚函数吗?
      不一定。1. 虚函数效率相对要低一些;2. 有些类并没有子类,没必要用虚析构函数。

    • 为什么构造函数不能为虚函数
      因为在创建对象时会调用构造函数,构造函数在这时初始化对象的虚表指针。如果构造函数是虚函数,那么意味着对象必须要通过虚表指针去调用构造函数,但是在调用构造函数之前,虚表指针还没被赋值,这就出现了矛盾。(先有鸡还是先有蛋的问题)

    • 构造函数中可以调用虚函数吗?
      可以,但是没有意义,起不到动态绑定的效果。父类构造函数中调用的仍然是父类版本的函数,子类中调用的仍然是子类版本的函数。

    • 构造函数什么时候必须初始化列表

      委任构造函数:默认构造函数内部调用带参的构造函数

      构造函数初始化列表和赋值的区别

      • 初始化列表只会调用一次构造函数,其实就是变量声明时初始化;
      • 赋值会先调用构造函数,再调用一次赋值函数,它相当于在声明后,又进行了赋值。
      • const声明的变量,必须要有初值;
      • reference引用声明的变量,必须要有初值;
      • 没有默认构造函数但存在有参构造函数的类,它必须初始化的时候给一个入参。
    • 简述C++中虚继承的作用及底层实现原理?

      虚继承用于解决多继承条件下的菱形继承问题,底层原理的实现与编译器相关,一般通过虚基类指针实现,即各对象中只保存一份父类的对象,多继承时通过虚基类指针引用该公共对象,从而避免菱形继承中的二义性问题。链接阶段冲突。

    • 内联函数、构造函数、静态成员函数不能是虚函数
      都不可以。
      内联函数(inline)需要在编译阶段展开(在编译时就已经确定了),而虚函数是运行时动态绑定的,编译时无法展开,因此是矛盾的;
      构造函数在进行调用时还不存在父类和子类的概念,父类只会调用父类的构造函数,子类调用子类的,因此不存在动态绑定的概念(先有父类才能有子类,构造父类的时候子类还不存在,子类都还没有怎么可能在父类里动态调用子类);
      静态成员函数(static)是以为单位的函数,与具体对象无关,虚函数是与对象动态绑定的,因此是两个矛盾的概念;
      友元函数:C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数这一说法。友元函数不属于类的成员函数,不能被继承

3.重载和重写的区别

  • 重载(函数重载):同一作用域,函数名相同,参数个数或类型不同,返回类型不限制。
  • 覆盖(重写):不同作用域(父子类),函数名,参数和返回类型都完全一样,且基类方法必须带virtual关键字。子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是重写。

4.C++关键字

static

  • 修饰函数的局部变量,改变其变量的生命周期。
  • 修饰全局变量,改变变量的可见性,只在本文件内可见。
  • 修饰类成员变量,会使类成员变量变成类的全局变量,会被类的所有对象共享,不属于某个对象。
  • 修饰类成员函数,修饰后该函数不含有this指针,不能访问非静态成员。

extern C

  • C++编译器用C规则编译指定的代码(除函数重载外,**extern “C”**不影响C++其他特性)
  • 因为C和C++的编译规则不一样,主要区别体现在编译期间生成函数符号的规则不一致

C++比C出道晚,但增加了很多优秀的功能,函数重载就是其中之一。由于C++需要支持重载,单纯的函数名无法区分出具体的函数,

所以在编译阶段就需要将形参列表作为附加项增加到函数符号中

explicit:防止类构造函数隐式自动转换

const

  • 修饰变量后该变量就不能被修改;

  • 修饰成员函数后,该函数不能修改任何数据成员

  • const在修饰指针时,放在 *号前和 *号后分别表示指针常量和常量指针,前者表示指向的内容不能改变,后者表示指向不能改变。

    • const和define的区别
      • #define是宏定义,是在预编译阶段处理,进行宏替换;const是在编译阶段起作用;
      • #define是宏替换,不会检查类型,而const会进行类型判断,能够在编译阶段检查一些错误;
      • #define替换的地方都会在内存中存储,有n个备份,n=替换数,而const只有一个备份,节省内存空间;
      • #define无法调试,const可以调试。

5.空类大小为1个字节

这就是实例化的原因(空类同样可以被实例化),保证内存地址的唯一性,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,

这样空类在实例化后在内存得到了独一无二的地址,所以空类所占的内存大小是1个字节

6.引用和指针的区别与联系

  • 引用是变量的别名,操作引用就是操作变量本身;
  • 声明和使用引用时必须初始化,一旦初始化就不可改变,而指针可以不初始化;
  • 引用没有数据类型,不占据存储空间,而指针有数据类型,占据存储空间;
  • 不能返回局部变量的引用,不能返回new分配的引用
  • 联系:都和地址有关系的,指针指向一块内存,而引用是一块内存的别名。引用的内部使用指针实现的,是受了限制的指针

7.友元函数和友元类

  • 友元函数:在函数前面加上friend,这个函数就变成了友元函数,它代表这个函数与某个类成为朋友了,此时访问类的私有成员也是不受限制的。

    注意:友元函数违反了封装的原则,可以不受访问权限的限制而访问类的任何成员,也就是它可以直接接触类的实现。只是有时基于我们自身的某些使用场景,不得不使用友元。

  • 友元类:与友元函数类似,在一个类A中声明另外一个类B为friend类型,那么这个类B就是友元类,它访问类A的私有成员和保护成员都不受限制。

8.Python的运行效率为啥比其他语言慢

python和C++都属于强类型语言,无法隐式转换

  • python是动态语言
    一个变量所指向对象的类型在运行时才确定,编译器做不了任何预测,也就无从优化。举一个简单的例子:r = a + b。a和b相加,但a和b的类型在运行时才知道,对于加法操作,不同的类型有不同的处理,所以每次运行的时候都会去判断a和b的类型,然后执行对应的操作。

  • python GIL
    Python中的多线程并不能实现真正的并发,一个进程产生多个线程,以此来争取抢占资源命中的概率,进程线程死锁调度。

  • python是解释执行,但是不支持JIT(just in time compiler)

  • python中的一切都是对象,每个对象都需要维护引用计数,增加了额外的工作。

9.内存泄漏

在这里插入图片描述

10.内存中的堆与栈

  • 栈内存:由操作系统自动分配释放 ,用于存放函数的参数值、局部变量等

  • 堆内存:由开发人员分配和释放, 若开发人员不释放,程序结束时由可能 OS 回收,分配方式类似于链表,new/malloc

  • 区别

    • 管理方式不同。栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;
    • 空间大小不同。每个进程拥有的栈大小要远远小于堆大小
    • 内存地址生长方向不同。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。
    • 分配方式不同。堆都是动态分配的,没有静态分配的堆。栈有 2 种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由alloca()函数分配,但是栈的动态分配和堆是不同的,它的动态分配是由操作系统进行释放,无需我们手工实现。
    • 分配效率不同。栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。

虚拟内存空间

11.深浅拷贝

系统默认的拷贝构造为浅拷贝,对于指针型数据,会存在共同指向的一个地址空间

  • 浅拷贝:创建一个对象用一个现成的对象初始化它时,只复制了成员(简单拷贝),而没有拷贝分配给成员的资源(如给指针变量成员分配动态内存)。
  • 深拷贝:当一个对象创建时,如果分配了资源,就需要定义它的拷贝构造函数。不但拷贝成员而且拷贝分配给它的资源。

12.原码,反码与补码

在这里插入图片描述

13.malloc/free和new/delete的区别

  1. malloc/free是C语言中的标准库,new/delete是C++中的运算符,都可用于申请动态内存和释放内存

  2. 非内部数据对象struct(公有),class(私有),union,只用malloc/free无法满足动态对象的要求。 这是因为对象在创建的同时需要自动执行构造函数,对象在消亡之前要自动执行析构函数,而由于malloc/free是库函数而不是运算符,不在编译器的控制权限内,也就不能自动执行构造函数和析构函数。所以C++需要能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理和释放内存工作的运算符delete。

  3. 创建类型不同,new建立的是一个对象,malloc分配的是一块内存区域,用指针来访问,并且可以在区域里面移动指针。

14.右值引用&&,移动语义,完美转发

左值引用是对左值的引用,避免对象拷贝,比如函数传参和函数返回值

右值引用是对右值的引用

引用指向改变

  1. 右值可通过std::move指向左值,比如将亡值
  2. const左值引用能指向右值;局限不能修改这个值
  3. 声明出来的左值引用和右值引用都是左值

右值分为三类:

  1. 只能在等号右边,不能取地址,不具名
  2. 纯右值:非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等
  3. 将亡值:与右值引用相关的表达式,比如,T&& 类型函数的返回值、 std::move 的返回值等。

功能

  • 实现移动语义

    对象赋值时,避免资源的重新分配。解决对象赋值问题,类会触发拷贝构造(深拷贝)和赋值构造(浅拷贝);

    A a;   //拷贝构造
    A b(std::move(a));
    

    移动构造和移动拷贝构造,智能指针unique_ptr;

    STL应用,提高容器的效率;

  • 实现完美转发

    函数模板可以完美地转发给内部调用的其他函数

    完美:准确的转发参数的值,并保证被抓发的参数的左右值属性不变

    template<typename T>
    void revoke(T&&t)
    {
    	fun(std::forward<T>(t));
    }
    
    //引用折叠
    revoke(static_cast<int&>n)
        int & && => int &
    

15.智能指针

在这里插入图片描述

智能指针其作⽤是管理⼀个指针,避免程序员申请的空间在函数结束时忘记释放,造成内存泄漏这种情况滴发⽣。

使⽤智能指针可以很⼤程度上的避免这个问题,因为智能指针就是⼀个类,当超出了类的作⽤域是,类会⾃动调⽤析构函数,

析构函数会⾃动释放资源。所以智能指针的作⽤原理就是在函数结束时⾃动释放内存空间,不需要⼿动释放内存空间

  • 智能指针可分为两部分,一个是原指针,一个是引用计数(关联的计数器)。创建一个智能指针的时候引用计数为1,当引用计数为0的时候,智能指针本身会自动释放。当该智能指针被其他指针所用,引用计数就会相应的叠加,可以这么理解:引用计数的大小就是当前管理该内存的指针的数量。

常⽤接⼝

T* get();
T& operator*();
T* operator->();
T& operator=(const T& val);
T* release();
void reset (T* ptr = nullptr);
  • T 是模板参数, 也就是传⼊的类型;

  • get() ⽤来获取 auto_ptr 封装在内部的指针, 也就是获取原⽣指针;

  • operator*()* 重载 , operator->() 重载了->, operator=()᯿载了=;

  • realease() 将 auto_ptr 封装在内部的指针置为 nullptr, 但并不会破坏指针所指向的内容, 函

    数返回的是内部指针置空之前的值;

  • 直接释放封装的内部指针所指向的内存, 如果指定了 ptr 的值, 则将内部指针初始化为该值 (否则将其设置为nullptr;

智能指针种类

15.1 unique_ptr 独占有智能指针

唯一的指向一个对象,该对象不能被共享(指针和资源一对一),unique_ptr向比于原始指针,使得在出现异常的情况下动态资源得以释放,unique_ptr的释放规则是:unique_ptr从指针开始,到离开作用域时,释放其指向的对象资源。

15.2 shared_ptr 共享智能指针

shared_ptr 实现共享式拥有概念,多个智能指针可以指向相同对象,该对象和其相关资源会在“最后⼀个引⽤被销毁”时候释放。从名字 share 就可以看出了资源可以被多个指针共享,它使⽤计数机制来表明资源被⼏个指针共享。

为什么不能检测shared_ptr是否为NULL?

因为这就是错的。shared_ptrnullptr相等,并不代表所引用的对象不存在了。

shared_ptr允许有一个“empty”状态,代表shared_ptr<T>对象的实例本身存在,但并不指向一个有效的Tshared_ptr重载了===来表示以下意义:

  • shared_ptrnullptr判等,代表判断是否处在empty状态;
  • shared_ptr被赋值为nullptr,不代表shared_ptr实例本身没了,而是把这个shared_ptr实例的状态改为empty。
  • 一个已经处于empty状态的shared_ptr仍可以被继续赋值成为一个有效、有值的shared_ptr
15.3 weak_ptr 弱引用智能指针

弱引用智能指针 std::weak_ptr 可以看做是 shared_ptr 的助手,它不管理 shared_ptr 内部的指针。std::weak_ptr 没有重载操作符 * 和 ->,因为它不共享指针,不能操作资源,所以它的构造不会增加引用计数,析构也不会减少引用计数,它的主要作用就是作为一个旁观者监视 shared_ptr 中管理的资源是否存在。

weak_ptr不会控制所指对象的生命周期。

15.3.1 用weak_ptr解决共享指针循环引用,内存泄漏问题

首先析构 shared_ptr child 智能指针,_Usrs 强引用计数自减后为0,继而将其所指的Child对象进行析构shared_ptr parent智能指针,_Weaks 弱引用计数随着上一步孩子对象析构,引用计数变为1。接着析构shared_ptr parent,_Usrs自减后为0,将其所指Parent对象进行析构,并且由于_Weaks 弱引用计数也自减得0,将其 RefCnt 进行析构。随着上一步Parent对象析构,原本shared_ptr child所指 RefCnt 中的_Weaks 也变为0,将child智能指针的 RefCnt 也进行析构共享智能指针中会对weaks首先赋值为1,并且弱引用指针没有权限去释放计数器所指的资源。

15.3.2 weak_ptr方法
  • std::weak_ptr 类提供的 expired() 方法来判断观测的资源是否已经被释放

    // 返回true表示资源已经被释放, 返回false表示资源没有被释放
    bool expired() const noexcept;

  • // use_count()函数返回所监测的资源的引用计数
    long int use_count() const noexcept;

  • std::weak_ptr 类提供的 lock() 方法来获取管理所监测资源的 shared_ptr 对象

  • reset()清空对象

16.Lambda表达式

Lambda表达式的话是我们C++11的新特性,定义并创建匿名的函数对象

捕获列表的参数可以为空,表示不捕获外部变量;取与的话表示引用传值;然后取等的话表示用值传入。

在这里插入图片描述

17.静态变量、全局变量、局部变量的特点

  1. 位置
    局部变量:在函数中或者方法中,函数的参数,局部代码块中。
    全局变量:在文件中,函数外。
    静态变量:使用 static 修饰,可以是局部、全局或者修饰类成员。

  2. 作用域

    局部变量:作用域为局部,也就是函数或方法中,出了作用域就不能访问,同一作用域不能有同名的变量,如果全局变量和局部变量同名,则访问时采用"就近原则"。

    全局变量:作用域为全局,在本文件或者其它文件中都可以访问,在其它文件中访问可以通过 extern 进行声明,表示使用外部的全部变量。

    静态变量:静态局部变量作用域为局部,静态全局变量作用域为所在文件中,其它文件中访问不了。

  3. 内存位置

    局部变量:存储在栈内存中。

    全局变量:存储在静态存储区中,如果未初始化或者初始化为0,在BSS段,初始化了在DATA段。

    静态变量:存储在静态存储区中,如果未初始化或者初始化为0,在BSS段,初始化了在DATA段。

  4. 生命周期
    局部变量:出了作用域销毁。
    全局变量:程序结束销毁。
    静态变量:程序结束销毁。

18 C++的四种类型转换

dynamic_cast: 将一个基类对象指针(或引用)转换到继承类指针,根据基类指针是否真正指向继承类指针来做相应处理。dynamic_cast运算符涉及到编译器的属性设置,而且牵扯到的面向对象的多态性跟程序运行时的状态也有关系,所以不能完全的使用传统的替换方式来代替。但是也因此它最常用,是最不可缺少的一个运算符。

static_cast: 把一个表达式转换为某种类型,但没有运行时类型检查来保证传唤的安全性。

reinterpret_cast:强制类型转换符。

const_cast: 是基于C语言编程开发的运算方法,其主要作用是:修改类型的const或volatile属性。使用该运算方法可以返回一个指向非常量的指针(或引用),就可以通过该指针(或引用)对他的数据成功元任意改变。

dynamic_cast和static_cast的区别

1、static_cast:向上转换,例如:基类向派生类转换
2、dynamic_cast:向下转换,例如:派生类向基类转换

fork()的作用

fork函数是在已经存在的进程中创建一个子进程,其中这个已经存在的这个进程被称为父进程

19GDB常用指令

常用命令
(gdb)help:查看命令帮助,具体命令查询在gdb中输入help + 命令,简写h
(gdb)run:重新开始运行文件(run-text:加载文本文件,run-bin:加载二进制文件),简写r
(gdb)start:单步执行,运行程序,停在第一执行语句
(gdb)list:查看原代码(list-n,从第n行开始查看代码。list+ 函数名:查看具体函数),简写l
(gdb)set:设置变量的值
(gdb)next:单步调试(逐过程,函数直接执行),简写n
(gdb)step:单步调试(逐语句:跳入自定义函数内部执行),简写s
(gdb)backtrace:查看函数的调用的栈帧和层级关系,简写bt
(gdb)frame:切换函数的栈帧,简写f
(gdb)info:查看函数内部局部变量的数值,简写i
(gdb)finish:结束当前函数,返回到函数调用点
(gdb)continue:继续运行,简写c
(gdb)print:打印值及地址,简写p
(gdb)quit:退出gdb,简写q
(gdb)break+num:在第num行设置断点,简写b
(gdb)info breakpoints:查看当前设置的所有断点
(gdb)delete breakpoints num:删除第num个断点,简写d
(gdb)display:追踪查看具体变量值
(gdb)undisplay:取消追踪观察变量
(gdb)watch:被设置观察点的变量发生修改时,打印显示
(gdb)i watch:显示观察点
(gdb)enable breakpoints:启用断点
(gdb)disable breakpoints:禁用断点
(gdb)x:查看内存x/20xw 显示20个单元,16进制,4字节每单元
(gdb)run argv[1] argv[2]:调试时命令行传参
(gdb)set follow-fork-mode child#Makefile项目管理:选择跟踪父子进程(fork())

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Q_Outsider

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值