0
点赞
收藏
分享

微信扫一扫

字节跳动面试CPP基础

Hyggelook 2022-02-10 阅读 100

字节跳动

野(wild)指针与悬空(dangling)指针有什么区别?如何避免?

野指针(wild pointer):就是没有被初始化过的指针。用 gcc -Wall 编译, 会出现 used uninitialized警告。

悬空指针:是指针最初指向的内存已经被释放了的一种指针。

无论是野指针还是悬空指针,都是指向无效内存区域(这里的无效指的是“不安全不可控”)的指针。 访问“不安全可控”(invalid)的内存区域将导致“Undefined Behavior”。

如何避免使用野指针?在平时的编码中,养成在定义指针后且在使用之前完成初始化的习惯或者使用智能指针。

C++ 中内存分配情况

栈:由编译器管理分配和回收,存放局部变量和函数参数。
堆:由程序员管理,需要手动 new malloc delete free 进行分配和回收,空间较大,但可能会出现内存泄漏和空闲碎片的情况。
全局/静态存储区:分为初始化和未初始化两个相邻区域,存储初始化和未初始化的全局变量和静态变量。
常量存储区:存储常量,一般不允许修改。
代码区:存放程序的二进制代码。

什么是类的继承?

  • 类与类之间的关系
    has-A 包含关系,用以描述一个类由多个部件类构成,实现 has-A 关系用类的成员属性表示,即一个类的成员属性是另一个已经定义好的类;
    use-A,一个类使用另一个类,通过类之间的成员函数相互联系,定义友元或者通过传递参数的方式来实现;
    is-A,继承关系,关系具有传递性;

  • 继承的相关概念
    所谓的继承就是一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的属性和方法,被称为子类或者派生类,被继承的类称为父类或者基类;

  • 继承的特点
    子类拥有父类的所有属性和方法,子类可以拥有父类没有的属性和方法,子类对象可以当做父类对象使用;

  • 继承中的访问控制
    public、protected、private

    继承中的构造和析构函数

    继承中的兼容性原则

C++ 中重载和重写,重定义的区别

  • 重载
    翻译自 overload,是指同一可访问区内被声明的几个具有不同参数列表的同名函数,依赖于 C++函数名字的修饰会将参数加在后面,可以是参数类型,个数,顺序的不同。根据参数列表决定调用哪个函数,重载不关心函数的返回类型。

  • 重写
    翻译自 override,派生类中重新定义父类中除了函数体外完全相同的虚函数,注意被重写的函数不能是 static 的,一定要是虚函数,且其他一定要完全相同。要注意,重写和被重写的函数是在不同的类当中的,重写函数的访问修饰符是可以不同的,尽管 virtual 中是 private 的,派生类中重写可以改为 public。

  • 重定义(隐藏)
    派生类重新定义父类中相同名字的非 virtual 函数,参数列表和返回类型都可以不同,即父类中除了定义成 virtual 且完全相同的同名函数才不会被派生类中的同名函数所隐藏(重定义)。

C++ 中的指针参数传递

指针参数传递本质上是值传递,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本(替身)。值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。

引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。

说一下 define、const、typedef、inline 使用方法?

  1. const 与 #define 的区别

    const 定义的常量是变量带类型,而 #define 定义的只是个常数不带类型;

    define 只在预处理阶段起作用,简单的文本替换,而 const 在编译、链接过程中起作用;

    define 只是简单的字符串替换没有类型检查。而const是有数据类型的,是要进行判断的,可以避免一些低级错误;

    define 预处理后,占用代码段空间,const 占用数据段空间;

    const 不能重定义,而 define 可以通过 #undef 取消某个符号的定义,进行重定义;

    define 独特功能,比如可以用来防止文件重复引用。

  2. #define 和别名 typedef 的区别

    执行时间不同,typedef 在编译阶段有效,typedef 有类型检查的功能;#define 是宏定义,发生在预处理阶段,不进行类型检查;

    功能差异,typedef 用来定义类型的别名,定义与平台无关的数据类型,与 struct 的结合使用等。

简单说一下函数指针

从定义和用途两方面来说一下自己的理解:

首先是定义:函数指针是指向函数的指针变量。函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。

在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。

其次是用途:调用函数和做函数的参数,比如回调函数。

说一下 C++ 里是怎么定义常量的?常量存放在内存的哪个位置?

对于局部常量,存放在栈区;

对于全局常量,编译期一般不分配内存,放在符号表中以提高访问效率;

字面值常量,比如字符串,放在常量区。

动态编译与静态编译

静态编译,编译器在编译可执行文件时,把需要用到的对应动态链接库中的部分提取出来,连接到可执行文件中去,使可执行文件在运行时不需要依赖于动态链接库;

动态编译,可执行文件需要附带一个动态链接库,在执行时,需要调用其对应动态链接库的命令。所以其优点一方面是缩小了执行文件本身的体积,另一方面是加快了编译速度,节省了系统资源。缺点是哪怕是很简单的程序,只用到了链接库的一两条命令,也需要附带一个相对庞大的链接库;二是如果其他计算机上没有安装对应的运行库,则用动态编译的可执行文件就不能运行。

虚函数相关(虚函数表,虚函数指针),虚函数的实现原理

首先我们来说一下,C++中多态的表象,在基类的函数前加上 virtual 关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数,如果是基类,就调用基类的函数。

实际上,当一个类中包含虚函数时,编译器会为该类生成一个虚函数表,保存该类中虚函数的地址,同样,派生类继承基类,派生类中自然一定有虚函数,所以编译器也会为派生类生成自己的虚函数表。当我们定义一个派生类对象时,编译器检测该类型有虚函数,所以为这个派生类对象生成一个虚函数指针,指向该类型的虚函数表,这个虚函数指针的初始化是在构造函数中完成的。

后续如果有一个基类类型的指针,指向派生类,那么当调用虚函数时,就会根据所指真正对象的虚函数表指针去寻找虚函数的地址,也就可以调用派生类的虚函数表中的虚函数以此实现多态。

补充:如果基类中没有定义成 virtual,那么进行 Base B; Derived D; Base *p = D; p->function(); 这种情况下调用的则是 Base 中的 function()。因为基类和派生类中都没有虚函数的定义,那么编译器就会认为不用留给动态多态的机会,就事先进行函数地址的绑定(早绑定),详述过程就是,定义了一个派生类对象,首先要构造基类的空间,然后构造派生类的自身内容,形成一个派生类对象,那么在进行类型转换时,直接截取基类的部分的内存,编译器认为类型就是基类,那么(函数符号表[不同于虚函数表的另一个表]中)绑定的函数地址也就是基类中函数的地址,所以执行的是基类的函数。

构造函数析构函数可否抛出异常

C++ 只会析构已经完成的对象,对象只有在其构造函数执行完毕才算是完全构造妥当。在构造函数中发生异常,控制权转出构造函数之外。因此,在对象 b 的构造函数中发生异常,对象b的析构函数不会被调用。因此会造成内存泄漏。

用 autoptr 对象来取代指针类成员,便对构造函数做了强化,免除了抛出异常时发生资源泄漏的危机,不再需要在析构函数中手动释放资源;

如果控制权基于异常的因素离开析构函数,而此时正有另一个异常处于作用状态,C++ 会调用 terminate 函数让程序结束;

如果异常从析构函数抛出,而且没有在当地进行捕捉,那个析构函数便是执行不全的。如果析构函数执行不全,就是没有完成他应该执行的每一件事情。

构造函数为什么一般不定义为虚函数

  • 虚函数调用只需要知道“部分的”信息,即只需要知道函数接口,而不需要知道对象的具体类型。但是,我们要创建一个对象的话,是需要知道对象的完整信息的。特别是,需要知道要创建对象的确切类型,因此,构造函数不应该被定义成虚函数;
  • 而且从目前编译器实现虚函数进行多态的方式来看,虚函数的调用是通过实例化之后对象的虚函数表指针来找到虚函数的地址进行调用的,如果说构造函数是虚的,那么虚函数表指针则是不存在的,无法找到对应的虚函数表来调用虚函数,那么这个调用实际上也是违反了先实例化后调用的准则。

动态联编与静态联编

在 C++ 中,联编是指一个计算机程序的不同部分彼此关联的过程。按照联编所进行的阶段不同,可以分为静态联编和动态联编;

静态联编是指联编工作在编译阶段完成的,这种联编过程是在程序运行之前完成的,又称为早期联编。要实现静态联编,在编译阶段就必须确定程序中的操作调用(如函数调用)与执行该操作代码间的关系,确定这种关系称为束定,在编译时的束定称为静态束定。静态联编对函数的选择是基于指向对象的指针或者引用的类型。其优点是效率高,但灵活性差。

动态联编是指联编在程序运行时动态地进行,根据当时的情况来确定调用哪个同名函数,实际上是在运行时虚函数的实现。这种联编又称为晚期联编,或动态束定。动态联编对成员函数的选择是基于对象的类型,针对不同的对象类型将做出不同的编译结果。

C++中一般情况下的联编是静态联编,但是当涉及到多态性和虚函数时应该使用动态联编。动态联编的优点是灵活性强,但效率低。动态联编规定,只能通过指向基类的指针或基类对象的引用来调用虚函数,其格式为:指向基类的指针变量名->虚函数名(实参表)或基类对象的引用名.虚函数名(实参表)

实现动态联编三个条件:

必须把动态联编的行为定义为类的虚函数;

类之间应满足子类型关系,通常表现为一个类从另一个类公有派生而来;

必须先使用基类指针指向子类型的对象,然后直接或间接使用基类指针调用虚函数;

编译器处理虚函数表应该如何处理

对于派生类来说,编译器建立虚函数表的过程其实一共是三个步骤:

  • 拷贝基类的虚函数表,如果是多继承,就拷贝每个有虚函数基类的虚函数表

  • 当然还有一个基类的虚函数表和派生类自身的虚函数表共用了一个虚函数表,也称为某个基类为派生类的主基类

  • 查看派生类中是否有重写基类中的虚函数, 如果有,就替换成已经重写的虚函数地址;查看派生类是否有自身的虚函数,如果有,就追加自身的虚函数到自身的虚函数表中。

内存泄漏的定义,如何检测与避免?

定义:内存泄漏简单的说就是申请了一块内存空间,使用完毕后没有释放掉。 它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄漏了。

如何检测内存泄漏

  • 首先可以通过观察猜测是否可能发生内存泄漏,Linux 中使用 swap 命令观察还有多少可用的交换空间,在一两分钟内键入该命令三到四次,看看可用的交换区是否在减少。

  • 还可以使用 其他一些 /usr/bin/stat 工具如 netstat、vmstat 等。如发现波段有内存被分配且从不释放,一个可能的解释就是有个进程出现了内存泄漏。

  • 当然也有用于内存调试,内存泄漏检测以及性能分析的软件开发工具 valgrind 这样的工具来进行内存泄漏的检测。

简述C++ 的四种强制转换

  • staticcast:明确指出类型转换,一般建议将隐式转换都替换成显示转换,因为没有动态类型检查,上行转换(派生类->基类)安全,下行转换(基类->派生类) 不安全,所以主要执行非多态的转换操作;
  • dynamiccast:专门用于派生类之间的转换,type-id 必须是类指针,类引用或 void*,对于下行转换是安全的,当类型不一致时,转换过来的是空指针,而staticcast,当类型不一致时,转换过来的事错误意义的指针,可能造成非法访问等问题。
  • constcast:专门用于 const 属性的转换,去除 const 性质,或增加 const 性质, 是四个转换符中唯一一个可以操作常量的转换符。
  • reinterpretcast:不到万不得已,不要使用这个转换符,高危操作。使用特点: 从底层对数据进行重新解释,依赖具体的平台,可移植性差; 可以将整形转 换为指针,也可以把指针转换为数组;可以在指针和引用之间进行肆无忌惮的转换。

什么是组合?

一个类里面的数据成员是另一个类的对象,即内嵌其他类的对象作为自己的成员;创建组合类的对象:首先创建各个内嵌对象,难点在于构造函数的设计。创建对象时既要对基本类型的成员进行初始化,又要对内嵌对象进行初始化。

创建组合类对象,构造函数的执行顺序:先调用内嵌对象的构造函数,然后按照内嵌对象成员在组合类中的定义顺序,与组合类构造函数的初始化列表顺序无关。然后执行组合类构造函数的函数体,析构函数调用顺序相反。

简述C++ 中的引用参数传递

  • 引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用。
  • 从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。

简述weakptr

weakptr 是一种不控制对象生命周期的智能指针,它指向一个 sharedptr 管理的对象。进行该对象的内存管理的是那个强引用的 sharedptr

weakptr 只是提供了对管理对象的一个访问手段。weakptr 设计的目的是为配合 sharedptr 而引入的一种智能指针来协助 sharedptr 工作,它只可以从一个 sharedptr 或另一个 weakptr 对象构造,,它的构造和析构不会引起引用记数的增加或减少。

weakptr 是用来解决 sharedptr 相互引用时的死锁问题,如果说两个 sharedptr 相互引用,那么这两个指针的引用计数永远不可能下降为0,也就是资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和 sharedptr 之间可以相互转化,sharedptr 可以直接赋值给它,它可以通过调用 lock 函数来获得sharedptr。

当两个智能指针都是 sharedptr 类型的时候,析构时两个资源引用计数会减一,但是两者引用计数还是为 1,导致跳出函数时资源没有被释放(的析构函数没有被调用),解决办法:把其中一个改为weakptr就可以。

程序员定义的析构函数被扩展的过程?

析构函数函数体被执行;

如果 class 拥有成员类对象,而后者拥有析构函数,那么它们会以其声明顺序的相反顺序被调用;

如果对象有一个 vptr,现在被重新定义

如果有任何直接的上一层非虚基类拥有析构函数,则它们会以声明顺序被调用;

如果任何虚基类拥有析构函数

介绍 C++ 的构造函数

类的对象被创建时,编译系统为对象分配内存空间,并自动调用构造函数,由构造函数完成成员的初始化工作。

即构造函数的作用:初始化对象的数据成员。

无参数构造函数: 即默认构造函数,如果没有明确写出无参数构造函数,编译器会自动生成默认的无参数构造函数,函数为空,什么也不做,如果不想使用自动生成的无参构造函数,必需要自己显示写出一个无参构造函数。

一般构造函数: 也称重载构造函数,一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同,创建对象时根据传入参数不同调用不同的构造函数。

拷贝构造函数: 拷贝构造函数的函数参数为对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在的对象的数据成员的值一一复制到新创建的对象中。如果没有显示的写拷贝构造函数,则系统会默认创建一个拷贝构造函数,但当类中有指针成员时,最好不要使用编译器提供的默认的拷贝构造函数,最好自己定义并且在函数中执行深拷贝。

类型转换构造函数: 根据一个指定类型的对象创建一个本类的对象,也可以算是一般构造函数的一种,这里提出来,是想说有的时候不允许默认转换的话,要记得将其声明为 explict 的,来阻止一些隐式转换的发生。

赋值运算符的重载 :注意,这个类似拷贝构造函数,将=右边的本类对象的值复制给=左边的对象,它不属于构造函数,=左右两边的对象必需已经被创建。如果没有显示的写赋值运算符的重载,系统也会生成默认的赋值运算符,做一些基本的拷贝工作。

面向对象的三大特性,并举例说明

C++ 面向对象的三大特征是:封装、继承、多态。

  • 所谓封装
    就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让信任的类或者对象操作,对不可信的进行信息隐藏。一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。

  • 所谓继承
    是指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为“子类”或者“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过“继承”和“组合”来实现。

    继承概念的实现方式有两类:

    实现继承:实现继承是指直接使用基类的属性和方法而无需额外编码的能力。

    接口继承:接口继承是指仅使用属性和方法的名称、但是子类必需提供实现的能力。

  • 所谓多态

    就是向不同的对象发送同一个消息,不同对象在接收时会产生不同的行为(即方法)。即一个接口,可以实现多种方法。
    多态与非多态的实质区别就是函数地址是早绑定还是晚绑定的。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并产生代码,则是静态的,即地址早绑定。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。

举报

相关推荐

0 条评论