寒武纪软件开发面试题C++

1 虚函数

  1. 定义一个函数为虚函数,不代表函数为不被实现的函数。(虚函数是可以被实现的
  2. 定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。(主要用于实现运行时多态
  3. 定义一个函数为纯虚函数,才代表函数没有被实现。(纯虚函数是不被实现的
  4. 定义纯虚函数是为了实现一个接口,起到一个规范的作用规范继承这个类的程序员必须实现这个函数。

1、简介

class A
{
   
public:
virtual void foo()
{
   
cout<<"A::foo() is called"<<endl;
}
};
class B:public A
{
   
public:
void foo()
{
   
cout<<"B::foo() is called"<<endl;
}
};
int main(void)
{
   
A *a = new B();
a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
return 0;
}

这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。
虚函数只能借助于指针或者引用来达到多态的效果。

2 C++纯虚函数

一、定义
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”

virtual void funtion1()=0

二、引入原因

  1. 为了方便使用多态特性,我们常常需要在基类中定义虚拟函数
  2. 在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
    2.1 为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类它不能生成对象。这样就很好地解决了上述两个问题。
    2.2 纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。
    2.3 定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口
    2.4 纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

3 抽象类

抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层(基类)。
(1)抽象类的定义:
称带有纯虚函数的类为抽象类。
(2)抽象类的作用:
抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。(其实就是定义接口
(3)使用抽象类时注意:

  1. 抽象类只能作为基类来使用,其纯虚函数实现由派生类给出如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
  2. 抽象类是不能定义对象的。

4 虚函数与纯虚函数总结

  1. 纯虚函数声明如下: virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
  2. 虚函数声明如下:virtual ReturnType FunctionName(Parameter);虚函数必须实现,如果不实现,编译器将报错
  3. 对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定
  4. 实现了纯虚函数的子类,该纯虚函数在子类中就编程了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。
  5. 虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数
  6. 在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。
  7. 析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数

有纯虚函数的类是抽象类,不能生成对象,只能派生。他派生的类的纯虚函数没有被改写,那么,它的派生类还是个抽象类。
定义纯虚函数就是为了让基类不可实例化化
因为实例化这样的抽象数据结构本身并没有意义。
或者给出实现也没有意义
实际上我个人认为纯虚函数的引入,是出于两个目的
1、为了安全,因为避免任何需要明确但是因为不小心而导致的未知的结果,提醒子类去做应做的实现。
2、为了效率,不是程序执行的效率,而是为了编码的效率。

5 C++虚函数表剖析

https://blog.csdn.net/lihao21/article/details/50688337
摘要出的关键信息:

  1. 实现C++的多态,C++使用了一种动态绑定的技术。这个技术的核心是虚函数表
  2. 每个包含了虚函数的类都包含一个虚表
  3. 一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表
  4. 虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。

6 C++三种继承方式下的访问权限控制

1 C++类中的成员(函数/变量)拥有三种访问权限:

  • public: 用该关键字修饰的成员表示公有成员,该成员不仅可以在类内可以被访问,在类外也是可以被访问的,是类对外提供的可访问接口;
  • private: 用该关键字修饰的成员表示私有成员,该成员仅在类内可以被访问,在类体外是隐藏状态;
  • protected: 用该关键字修饰的成员表示保护成员,保护成员在类体外同样是隐藏状态,但是对于该类的派生类来说,相当于公有成员,在派生类中可以被访问。

2 不同的继承方式,基类中的成员访问权限:

  • 若继承方式是public,基类成员在派生类中的访问权限保持不变,也就是说,基类中的成员访问权限,在派生类中仍然保持原来的访问权限;

  • 若继承方式是private,基类所有成员在派生类中的访问权限都会变为私有(private)权限

  • 若继承方式是protected, 基类的共有成员和保护成员在派生类中的访问权限都会变为保护(protected)权限,私有成员在派生类中的访问权限仍然是私有(private)权限。

在这里插入图片描述

7 C++ 宏定义与内嵌函数

用内联函数取代宏的优点:

  1. 内联函数在运行时可调试,而宏定义不可以;
  2. 编译器会对内联函数的参数类型做安全检查自动类型转换(同普通函数),而宏定
    义则不会;
  3. 内联函数可以访问类的成员变量,宏定义则不能
  4. 在类中声明同时定义的成员函数,自动转化为内联函数。

内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中。
内联函数要做参数类型检查,这是内联函数跟宏相比的优势
inline是指嵌入代码,就是在调用函数的地方不是跳转,而是把代码直接写到那里去。对于短小的代码来说,inline可以带来一定的效率提升,而且和C时代的宏函数相比,inline 更安全可靠。可是这个是以增加空间消耗为代价的。至于是否需要inline函数就需要根据你的实际情况取舍了。

  1. 宏是在代码处不加任何验证的简单替代,而内联函数是将代码直接插入调用处,而减少了普通函数调用时的资源消耗。
  2. 宏不是函数,只是在编译前(编译预处理阶段)将程序中有关字符串替换成宏体
  3. inline函数是函数,但在编译中不单独产生代码,而是将有关代码嵌入到调用处

宏定义示例:#define MAX(a,b) ((a)>(b)?(a):(b))
内联函数定义:

inline int MAX(int a,int b)
    {
   
     return a>b?a:b;
    }

8 new、malloc的区别

先对new和delete简单进行一下总结,然后再细说new和malloc的区别
一、new和delete
C语言提供了malloc和free两个系统函数,完成对堆内存的申请和释放。而 C++ 则提供了两个关键字new和delete
1.1 规则

  1. new/delete是关键字,效率高于malloc和free。
  2. 配对使用,避免内存泄漏和多重释放。
  3. 避免交叉使用,比如malloc申请空间delete释放,new出的空间被free。
  4. new/delete 主要是用在类对象的申请和释放申请的时候会调用构造器完成初始化释放的时候,会调用析构器完成内存清理

二、new和malloc的区别
2.1 属性
new和deleteC++关键字,需要编译器支持malloc和free是库函数需要头文件支持
2.2 参数
使用new操作符申请内存分配时无须指定内存块的大小编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸
2.3 返回类型
new操作符内存分配成功时,返回的是 对象类型的指针类型严格与对象匹配无须进行类型转换,故new是符合类型安全性的操作符。 而malloc内存分配成功则是返回void *** ,需要通过强制类型转换将void*指针转换成我们需要的类型**。
2.4 自定义类型
new先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
malloc/free是库函数只能动态的申请和释放内存无法强制要求其做自定义类型对象构造和析构工作
2.5 重载
C++允许重载new/delete操作符,malloc不允许重载。
2.6 内存区域
new做两件事:分配内存和调用类的构造函数,delete是:调用类的析构函数和释放内存。而malloc和free只是分配和释放内存
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。
2.7 分配失败
new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL
2.8 内存泄漏
内存泄漏对于new和malloc都能检测出来,而new可以指明是哪个文件的哪一行,malloc确不可以

9 堆/栈的区别?

https://blog.csdn.net/bolinzhiyi/article/details/104984885
1. 管理方式不同:

  1. 对于来讲,是由编译器自动管理,无需我们手工控制;
  2. 对于来说,申请和释放工作由程序员控制,容易产生memory leak。

2. 空间大小不同:

  1. 对于来讲,一般都是有一定的空间大小的,一般默认的栈空间大小为1M,同时,我们是可以进行修改的;
  2. 对于来说,在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。

3. 能否产生碎片不同:

  1. 对于来讲,不会有碎片,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出(具体操作和数据结构中的栈操作是一致的);
  2. 对于来说,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。

4. 生长方向不同:

  1. 对于来讲,生长方向是向下的,是向着内存地址减小的方向增长;(参考C/C++内存布局图)
  2. 对于来说,生长方向是向上的,也就是向着内存地址增加的方向。

5. 分配方式不同:

  1. 对于来讲,栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。;(参考C/C++内存布局图)
  2. 对于来说,堆都是动态分配的,没有静态分配的堆。

6. 分配效率不同:

  1. 对于来讲,栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
  2. 对于来说,堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。

10 从源文件到可执行文件的过程?

流程概况:将源代码转换成机器可识别代码的过程,编译程序读取源代码,对他进行词法和语法的分析,将高级语言转化为功能等效的汇编代码,然后转化为机器语言,按照操作系统对可执行文件格式的要求连接生成可执行程序
源程序->编译预处理->编译->汇编程序->链接程序->可执行文件
在这里插入图片描述
1)预处理阶段
预处理器(cpp)对源程序中以 #开头的命令进行处理 ,例如将#include命令后面的.h文件内容插入程序文件。输出结果是一个以.i为扩展名的源文件hello.i。
读取源代码,对其中的伪指令(#开头)和特殊符号进行处理.主要包括:
宏定义
#define Name TokenString :将所有Name替换成TokenString
#undef: 取消宏定义
条件编译
#ifdef,#ifudef,#else,#elif,endif等等
这些伪指令让程序员通过不同的宏决定编译程序对那些代码进行处理,从而把不必要的代码过滤掉
头文件包含指令
#include “FileName”,#include
特殊符号
预编译可以处理一些特殊符号,比如LINE是当前的行号(十进制),FILE是当前源程序的名称.预编译会识别这些特殊符号.
2)编译阶段
编译器(ccl)对预处理后的源程序进行编译生成一个汇编语言程序hello.s。汇编语言源程序中的每一条语句都以一种文本格式描述了一条低级指令。
3)汇编阶段
汇编器(as)将hello.s 翻译成机器语言指令,把这些指令打包成一个称为可重定位目标文件的hello.o,一种二进制文件,用文本编辑器打开会乱码。
4)链接阶段
链接器(ld)将多个可重定位目标文件和标准库函数合并为一个可执行目标文件, 或简称可执行文件。

11 静态链接与动态链接的区别

我们大家在编程过程中对“链接”这个词并不陌生,链接所解决的问题即是将我们自己写的代码和别人写的库集成在一起。链接可以分为静态链接动态链接,下文将分别讲解这两种方式的特点与其区别。
1、静态链接
1)特点:在生成可执行文件的时候(链接阶段),把所有需要的函数的二进制代码都包含到可执行文件中去。因此,链接器需要知道参与链接的目标文件需要哪些函数,同时也要知道每个目标文件都能提供什么函数,这样链接器才能知道是不是每个目标文件所需要的函数都能正确地链接。如果某个目标文件需要的函数在参与链接的目标文件中找不到的话,链接器就报错了。目标文件中有两个重要的接口来提供这些信息:一个是符号表,另外一个是重定位表。
2) 优点:在程序发布的时候就不需要的依赖库,也就是不再需要带着库一块发布,程序可以独立执行。
3)缺点: 程序
体积会相对大一些
。 如果静态库有
更新
的话,所有可执行文件都得重新链接才能用上新的静态库。

2 、动态链接
1)特点: 在编译的时候不直接拷贝可执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定的代码时,去共享执行内存中已经加载的动态库可执行代码,最终达到运行时连接的目的
2)优点: 多个程序可以共享同一段代码,而不需要在磁盘上存储多个拷贝。
3)缺点: 由于是运行时加载,可能会影响程序的前期执行性能。

上面的文章多次提到库(lib)这个概念,所谓的库就是一些功能代码经过编译连接后的可执行形式。

12 C++内存机制中内存溢出、内存泄露、内存越界和栈溢出的区别和联系

内存溢出(out of memory)

  • 是指程序在申请内存时,没有足够的内存空间供其使用

内存泄漏(memory leak)

  • 是指程序在申请内存后,无法释放已申请的内存空间,占用有用内存

简单理解,内存溢出就是要求分配的内存超出了系统所给的。内存泄漏是指向系统申请分配内存进行使用(new),但是用完后不归还(delete),导致占用有效内存。(内存泄漏最终会导致内存溢出

内存越界

  • 11
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值