C++面试_1

1、内敛函数如何提高效率?底层如何实现?
一般而言,一个函数的调用大概要经过将指令压入栈中,将类型、变量压入栈中,将类型、变量出栈,将指令出栈,大概四个步骤,那么在每次调用此函数时总会浪费很多时间,所有对于少于十行且频繁调用的函数我们使用关键字inline将其定义为内敛函数,从而减小时间开销,但是这样做的同时也增加了内存空间,即所谓的牺牲空间换取时间。
关键字inline即告诉编译器,在看见此函数时直接将此函数复制嵌套到主调函数中,就不需要上述所说的四个步骤,节省了程序运行时间。
注:inline的使用是有所限制的,inline只适合函数体内代码简单的函数使用,不能包含复杂的结构控制语句例如while、switch。
关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。
如果内联函数不能增强性能,就避免使用它。

2、内敛函数与宏有什么区别?
宏是在预处理阶段,内敛函数是在编译期间;
宏只是简单的字符替换,内联函数直接被嵌入到目标代码中去;
宏会产生二义性,比如说定义个x * x的,假如x=2; 那么结果就是4,但如果我传入x=2++,我期望得到(2+2)(2+2)=16,但编译器执行的却是2+22+2=8;
内联函数可以进行诸如类型安全检查、语句是否正确等编译功能,宏不具有这样的功能,无法调试;
宏不是函数,而inline是函数;
所以我们会用内敛函数代替宏;const常量代替宏定义。

那么,const与宏有什么不同呢?
define 在预处理阶段进行替换,const 在编译时确定其值;
define 无类型,不进行类型安全检查,可能会产生意想不到的错误,const 有数据类型,编译时会进行类型检查;
define 不分配内存,给出的是立即数,有多少次使用就进行多少次替换,在内存中会有多个拷贝,消耗内存大,const 在静态存储区中分配空间,在程序运行过程中内存中只有一个拷贝;

指针常量与常量指针有什么区别?
指针常量(int * const a = 10; ),是个指针,所指向变量的地址不可更改,但指向的值可以更改;
常量指针(const int * a = 10; ),是个常量,所指向的变量可以更改,但是变量的地址不可更改。
常指针常量:指针不能改变,指针指向的值也不能改变。

3、深拷贝?浅拷贝?
基本类型赋值时,赋的是数据,不存在深拷贝和浅拷贝的问题;但当引用类型赋值时,赋的值地址,此时就涉及到深浅拷贝。
浅拷贝就是两个指针指向同一个资源,但是万一有一个指的释放掉了(析构)那么另外一个就是野指针了,所以就有深拷贝,指向的是两个资源,但资源的内容是一样的。

4、函数高级(函数默认参数、函数占位参数、函数重载)
函数默认参数,函数的行参列表中的行参是可以有默认值的。
返回值类型 函数名 (参数 = 默认值) { }
如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值。
如果函数的声明有默认参数,函数实现就不能有默认参数。(声明和实现只能用一个默认参数)

函数占位参数,函数的行参列表里可以有占位参数,调用函数时必须填补该位置。
返回值类型 函数名 (数据类型){ }
占位参数还可以有默认值。

函数重载,函数名相同,提高复用性。
函数重载满足条件:同一个作用域下,函数名相同,函数参数类型不同或者个数不同或者顺序不同。
函数的返回值不可以作为函数重载的条件。
函数重载注意事项:
1、引用作为重载条件。
2、函数重载碰到函数默认参数,导致二义性报错。

5、多态?(静态多态、动态多态)
多态:为不同数据类型的实体提供统一接口。
静态多态:也成为编译时的多态;在编译时期就已经确定要执行了的函数地址了;主要有函数重载、运算符重载和函数模板(这里只写函数重载)。
【简单解释一些运算符重载:运算符重载 比如说1+1.1 这两个数其实是 int+ float 类型不一样,实际是不能直接加的 底层有给你重载+这个符号 然后你就可以加了。】
C++中当我们一个类继承于一个另一个类时,我们在子类中不想使用父类中的函数,想重新写一个同名函数,这是被允许的;因此我们的父类与子类中可以有同名的函数;例如我们现在有一个类中已经有了一个函数名为show无参的函数,我们在子类中再写一个名为show函数,这叫函数重载也称为覆盖和重写;

此时子类中重写了父类中的同名函数,因此在son类中看到的show的函数是子类中的重写后的show函数;其实father类中的show函数还是存在的,并不是说重写了函数后,它就不存在了因此我们还是可以通过(a.father::show(); )去调用father类中的函数。

动态多态:即动态绑定,在运行时才去确定对象类型和正确选择需要调用的函数,一般用于解决父类指针或引用子类对象调用类中重写的方法(函数)时出现的问题,此时我们实现这种动态多态就需要通过使用关键字virtual来实现。(可串联8、继承时,父类的析构可为虚函数吗?构造呢?为什么?)
参考链接:动态多态和静态多态(C++

6、虚函数、虚表?
虚函数:利用virtual关键字,多态实现的关键,(在一个类中,用这个关键字定义一个父类的成员函数,通过指向子类的父类指针或引用,来访问子类中同名覆盖成员函数。)
虚表:虚函数是通过虚表来实现的,虚表存放的是这个类所有虚函数的地址,编译器会为包含虚函数的类加上一个成员变量,该成员变量是一个指向虚函数表的指针,因此虚表指针是一个成员属性,也就是说,如果一个类含有虚表,那么类的每个对象都含有虚表指针。

(1)virtual 修饰的函数称为虚函数,其是通过使用virtual关键字后产生一个虚表(虚函数表),然后每一个对象会创建一个虚表,虚表存放当前对象拥有的虚函数。
(2)当虚函数被创建后就会被放入虚表,当派生类重写虚函数后,创建派生类对象时会把虚表内的对应函数地址换成派生类中重写的函数地址。
注意:
(1)每次执行虚函数都会去重新去替换掉原来虚表的位置;
(2)当使用虚函数后原来函数的存放位置会分配一个指针用于指向虚表中函数的地址;
(3)一个对象可以拥有多个虚函数,但只能有一个虚表。

虚函数调用关系:this -> vptr -> vtable -> virtual func() {…}

7、虚函数如何实现动态多态?虚函数与纯虚函数的区别?
包含纯虚函数的类,被称为是“抽象类”。抽象类不能使用new出对象,只有实现了这个纯虚函数的子类才能new出对象。比如说我定义一个动物类,里面有小狗、小猫、但是动物本身生成动物是不合理的,所以需要将动物类定义为抽象类;
纯虚函数更像是“只提供申明,没有实现”,是对子类的约束,是“接口继承”。
纯虚函数是在父类中声明的虚函数,它在父类中没有定义,但要求任何子类都要定义自己的实现方法。在子类中实现纯虚函数的方法是在函数原型后加“=0”,virtual void funtion1()=0

8、继承时,父类的析构可为虚函数吗?构造呢?为什么?
析构可以,构造不可以。
如果一个类的析构函数是虚函数,那么由它继承而来的所有子类的析构函数也是虚函数。析构函数被定义为虚函数之后,在使用指针引用时可以动态绑定,实现运行时的多态,保证使用父类指针就能够调用适当的虚构函数针对不同的对象进行清理工作。因此,对于有可能会被继承的父类的析构函数应设置为虚函数,我们使用父类指针指向该子类对象,释放父类指针时可以释放掉子类的空间,防止内存泄漏。
参考:析构函数和虚析构函数
调用构造函数后, 才能生成一个对象。 假设构造函数是虚函数, 虚函数存在于虚函数表中, 而去找虚函数表又需要虚函数表指针, 而虚函数表指针又存在于对象中, 这样就矛盾了: 都没有生成对象, 哪有什么虚函数表指针呢?类似讨论是蛋生鸡还是鸡生蛋。

析构函数与构造函数:构造函数可以有多个,但是析构函数只有一个。构造函数:进行初始化操作,可以有参数,可以发生重载,创建对象的时候,构造函数会自动调用,而且只调用一次;析构函数:进行清理操作,不可以有参数,不可以发生重载,对象在销毁前会自动调用析构函数,而且只会调用一次;构造和析构但是必须有的实现,如果我们自己不提供,编译器会提供一个空实现的构造和析构。

9、进程、线程
进程包含了正在运行的一个程序的所有状态信息。
线程是进程的一部分,描述指令流执行状态。它是进程中的指令执行流的最小单位,是CPU调度的基本单位。
主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。

11、什么是C++引用?引用和指针有什么区别?
引用就是一个别名;当声明一个引用时应该把它初始化为另一个对象名,也就是目标。从这时起,引用就成为目标的替代名,所有对引用的操作实际都是对目标的操作。
(1)、指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名。
(2)、引用不可以为空,当被创建的时候,必须初始化,而指针可以是空值,可以在任何时候被初始化。
(3)、可以有const指针,但是没有const引用;
(4)、指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)
(5)、指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;
(6)、指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。
(7)、”sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;
(8)、指针和引用的自增(++)运算意义不一样;
(9)、如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄漏;why?
举例:int *p = new int[10]; delete p; int & q = * new int[10]; 因为是引用int&q,误以为引用一个变量,如int x; int&q=x; 从而未释放内存而造成泄露。

12、野指针
野指针是指向不可用内存的指针。
如何产生:
①、指针创建时未初始化:任何指针变量在创建时,不会自动初始化为NULL指针(空指针),其默认值是随机的。所以,指针变量在创建的同时应该被初始化,或者将指针置为空指针或者指向合法的地址,而不应该放置不理。
②、指针free/delete后未置空:因为free和delete只是把指针指向的内存释放掉了,但是并没有把指针本身释放掉。
一般可以采用语句if(NULL != ptr)来进行防错处理,但是if语句起不到防错处理,因为即使ptr不为空,也不指向合法的内存块,该指针也是野指针。
③、 数组越界:指针操作了超越数组范围外的数据

13、请讲述堆和栈的区别?
栈区(stack),由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
堆区(heap),一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。

13、友元的目的?友元如何实现?
就是让一个函数或者类 访问另一个类中的私有成员。
参考链接:C++:友元

14、几个常问的排序算法
冒泡:数组中相邻的两个元素比较大小,如果前者大于后者则交换位置。
快排:通常拿第一个数为基准、从后往前比较、比它小的就放在前面。然后从前往后比较,比它大的放在后面。一直找到中间的位置,就把基准数放在中间的位置。这样基准数左边就是小的数、右边就是比基准数大的数。
选择:选择出未排序部分的最小值,进行排序操作
插入:构建有序序列,对于未排序数据 在已经排序序列中从后往前扫描,找到相应的位置进行插入,在从后向前的扫描过程中需要反复把已经排序的元素逐步向后挪位,为最新元素插入提供空间。

15、智能指针
智能指针就是帮我们C++程序员管理动态分配的内存的,它会帮助我们自动释放new出来的内存,从而避免内存泄漏
参考链接:C++ 智能指针 - 全部用法详解

16、什么是内存对齐?为什么要内存对齐?
在用sizeof运算符求算某结构体所占空间时,并不是简单地将结构体中所有元素各自的空间相加,这里涉及到内存对齐的问题。访问未对齐的内存,处理器需要访问两次(数据先读高位再读低位然后进行拼接),而访问对齐的内存,只需要一次。为了提高效率,所以进行内存对齐。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值