C++开发实习面试高频考点(国家一级假勤奋大学生整理)

C++开发实习面试高频考点


内容持续更新,收藏+点赞,留着慢慢消化,希望大家都能斩获好的实习offer!

1. inline

C++关键字,把函数指定为内联函数,解决一些频繁调用的函数大量消耗栈空间的问题。需要与函数定义放在一起,不能和函数声明放在一起。

为了消除函数调用的时空开销,C++ 提供一种提高效率的方法,即在编译时将函数调用处用函数体替换,类似于C语言中的宏展开。这种在函数调用处直接嵌入函数体的函数称为内联函数(Inline Function)

内联函数将函数体直接扩展到调用内联函数的地方,减少参数压栈、跳转、返回的过程,和宏相比,是有参数检查和返回值检查的,使用起来更安全(内联发生在编译阶段)

2. 哈希冲突

通过拉链进行冲突的解决(开址)

闭址法:往后找空位

3. n个初始数字 vector两倍扩容,问插入n个数字的平均复杂度

平摊是O(1),扩容的时间复杂度为2N,拷贝时间复杂度为N,总的时间复杂度为3N

vector要扩容需要完全开辟一个新的空间,再将旧空间中的数据复制

4. 构造函数一般不定义为虚函数?而析构函数一般写成虚函数的原因?

构造函数不能声明为虚函数

  • 创建对象需要确定对象的类型,而虚函数在运行时才确定其类型
  • 虚函数的调用需要虚函数表指针,而该指针存放于对象的内存空间;若构造函数声明为虚函数,则对象未创建,没有内存空间,更没有虚函数表地址来调用虚函数即构造函数

析构函数最好声明为虚函数

  • 为了避免内存泄露的风险
  • 当析构一个指向派生类的基类指针时,最好将基类的析构函数声明为虚函数,否则可能出现内存泄露的问题
  • 如果析构函数不被声明成虚函数,则编译器实施静态绑定时,在删除指向派生类的基类指针时,会只调用基类的析构函数而不调用派生类析构函数,使得派生类对象析构不完全

5. 虚函数的作用和实现

首先:强调一个概念

定义一个函数为虚函数,不代表函数为不被实现的函数。

定义它为虚函数是为了允许用基类的指针来调用子类的这个函数

定义一个函数为纯虚函数,才代表函数没有被实现

实现了多态的机制,基类定义虚函数,子类可以重写该函数。

定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承该类的程序员必须实现这个函数

纯虚函数的定义:

virtual void funtion1()=0

纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。

定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。

虚函数的作用是可以使得函数调用发生在运行阶段而不是编译阶段

如何实现虚函数:

创建一个虚函数表,利用虚指针指向虚函数表

虚表指针存哪(类开头)

如果父类指针指向了一个子类对象,那么父类指针会获取子类对象的隐藏的虚函数表指针。当调用一个虚函数后,会通过虚函数表指针找到虚函数,再通过相对偏移,找到对应的虚函数指针,进而找到虚函数。

虚析构函数是为了避免内存泄露,当子类中会有指针成员变量时才会使用得到的

虚函数使用的核心目的是通过基类访问派生类定义的函数。

所谓虚函数就是在基类定义一个未实现的函数名,其需要加上virtual关键字

常见用法:声明基类指针,利用指针指向任意一个子类对象,调用相关虚函数,动态绑定

例子:

#include<iostream>  
using namespace std;  
  
class A  
{  
public:  
    void foo()  
    {  
        printf("1\n");  
    }  
    virtual void fun()  
    {  
        printf("2\n");  
    }  
};  
class B : public A  
{  
public:  
    void foo()  //隐藏:派生类的函数屏蔽了与其同名的基类函数
    {  
        printf("3\n");  
    }  
    void fun()  //多态、覆盖
    {  
        printf("4\n");  
    }  
};  
int main(void)  
{  
    A a;  
    B b;  
    A *p = &a;  
    p->foo();  //输出1
    p->fun();  //输出2
    p = &b;  
    p->foo();  //取决于指针类型,输出1
    p->fun();  //取决于对象类型,输出4,体现了多态
    return 0;  
}

常见的错误分为两种:

无意的重写:在派生类中声明一个与基类的某个虚函数具有相同签名的成员函数

虚函数签名不匹配:函数名、参数列表、const属性不一致,导致创建一个新虚函数,而不是重写已存在的虚函数。

为了避免上述错误,C++11增加关键字override和final

override:保证派生类中声明的重载函数与基类的虚函数有相同的签名

final:阻止类的进一步派生和虚函数的进一步重写

虚函数的寻址过程:

1、获取类型名和函数名

2、从符号表中获得当前虚函数的偏移量

3、利用偏移量得到虚函数的访问地址,并调用虚函数

6. 传参方式

按值:

形参和实参各占一个独立的储存空间,形参的储存空间是函数被调用才分配的,调用时,系统为形参开辟一个临时的存储区然后将各实参传递给形参,这时形参就得到了各实参的值。

地址:

形参得到实参的储存地址,使得形参指针和实参指针指向同一块地址,因此函数中对形参的造成的任何变化都能影响到实参。

引用:

以引用为别名,对形参的任何操作都会对实参进行相应改变。

7. 自旋锁、互斥锁、信号量

自旋锁是一种互斥锁的实现方式而已,相比一般的互斥锁会在等待期间放弃cpu,自旋锁(spinlock)则是不断循环并测试锁的状态,这样就一直占着cpu。

互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。

临界区:每个进程中访问临界资源的那段程序称为临界区,每次只允许一个进程进入临界区,进入后不允许其他进程进入。

自旋锁:与互斥量类似,它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。用在以下情况:锁持有的时间短,而且线程并不希望在重新调度上花太多的成本。“原地打转”。

自旋锁与互斥锁的区别:线程在申请自旋锁的时候,线程不会被挂起,而是处于忙等的状态。

信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

8. 重载函数的命名查找规则

  1. 成员函数名查找,当使用 . 或者→ 进行成员函数调用时候,名称查找位于该成员类中的同名函数。

  2. 限定名称查找,当使用限定符 :: 进行函数调用时,例如std::sort,则查找位于 :: 左侧的名称空间中的同名函数。

  3. 未限定名称查找,除了上面两种,编译器还可以根据参数依赖查找规则(ADL)进行查找。

9. 遇到多个匹配参数的函数,编译器会调用哪个

最佳匹配选择

如果调用重载函数传入的实参有多个可行函数完全匹配,编译器会在两种情况下有个最佳匹配选择,从而完成重载解析:

函数匹配的第三步是从可行函数中选择与本次调用最匹配的函数。在这一过程,逐一检查函数调用提供的实参,寻找形参类型与实参类型最匹配的那个可行函数。为了确定最佳匹配,编译器将实参类型到形参类型的转化划分成几个等级(从 1 到 5 匹配度逐步降低),具体排序如下所示:

1.精确匹配,包括以下情况(它们具有相同且最高的匹配度):

实参类型和形参类型相同。
实参从数组类型或函数类型转换成对应的指针类型
向实参添加顶层const或者从实参中删除顶层const(形参的顶层cosnt是可以直接忽略掉的)
2.通过const转化实现匹配(底层cosnt的转换)

3.通过类型提升实现的匹配

4.通过算术类型转换实现的匹配

5.通过类类型转换实现的匹配

**C++如何跟踪重载函数?**有一个名词叫做名称修饰。编译器会根据函数名和每一个形参类型对一个函数名进行修饰。注意修饰的时候,是根据函数名和形参类型为根据的,这也是为什么我们不能以函数返回值类型作为函数重载依据的原因。

10. 静态函数存在的意义?

静态私有成员在类外不能被访问,可通过类的静态成员函数来访问;

当类的构造函数是私有的时,不像普通类那样实例化自己,只能通过静态成员函数来调用构造函数

11. 多态

多态的实现主要分为静态多态和动态多态,静态多态主要是重载和模板,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。

12. 函数重载

如何实现?

编译器将重载的函数映射为不同的名字:函数名+参数列表

重载与重写

重载:允许存在多个同名函数,单参数表不同

重写:子类重写定义父类虚函数的方法

13. static

强调唯一的拷贝

全局变量定义在函数体外部,在全局数据区分配存储空间,且编译器会自动对其初始化。

普通全局变量对整个工程可见,其他文件可以使用extern外部声明后直接使用。也就是说其他文件不能再定义一个与其相同名字的变量了(否则编译器会认为它们是同一个变量)。

静态全局变量仅对当前文件可见,其他文件不可访问,其他文件可以定义与其同名的变量,两者互不影响。

在函数的返回类型前加上static,就是静态函数。其特性如下:

  • 静态函数只能在声明它的文件中可见,其他文件不能引用该函数
  • 不同的文件可以使用相同名字的静态函数,互不影响

静态数据成员
在类内数据成员的声明前加上static关键字,该数据成员就是类内的静态数据成员。其特点如下:

静态数据成员存储在全局数据区,静态数据成员在定义时分配存储空间,所以不能在类声明中定义
静态数据成员是类的成员,无论定义了多少个类的对象,静态数据成员的拷贝只有一个,且对该类的所有对象可见。也就是说任一对象都可以对静态数据成员进行操作。而对于非静态数据成员,每个对象都有自己的一份拷贝。
由于上面的原因,静态数据成员不属于任何对象,在没有类的实例时其作用域就可见,在没有任何对象时,就可以进行操作。

和普通数据成员一样,静态数据成员也遵从public, protected, private访问规则
静态数据成员的初始化格式:<数据类型><类名>::<静态数据成员名>=<值>
类的静态数据成员有两种访问方式:<类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>

同全局变量相比,使用静态数据成员有两个优势:

  • 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性

  • 可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能

    静态成员函数

    与静态数据成员类似,静态成员函数属于整个类,而不是某一个对象,其特性如下:

静态成员函数没有this指针,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数
出现在类体外的函数定义不能指定关键字static
非静态成员函数可以任意地访问静态成员函数和静态数据成员

另外简洁一点的说法:

(1)函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;

(2)在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;

(3)在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;

(4)在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;

(5)在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。

14. 二分查找和二叉树查找的区别

关于二分查找和二叉树的理解:
(1)二分查找即折半查找,优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难

(2)二叉查找树,它或者是一棵空树,或者若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。

两者明显的区别是二分查找速度快删除和插入困难,而对于建立的二叉树索引来说,他的插入和删除是相对较快的。

为什么会出现这两者的差别其实底层更多的考虑的是数据的存储结构:

顺序存储和链式存储的概念
(1)从空间性能,顺序存储会对空间资源做到百分之百的利用,而链式存储对对空间的利用不是百分之百,因为存储了指针,不是真正的数据
(2)从时间性能上来讲读取速度的话顺序存储更优,插入和删除操作链式存储更优,链式存储只需要移动指针,不需要移动元素。

什么时候采用二分什么时候采用二叉索引
(1)如果我们的数据是不进行频繁变化且是有序,而且查询相对较多的情况下采用二分查找
(2)我们的数据是频繁变化的考虑到后面的数据扩容的情况下,我们考虑采用二叉索引的方式,但是这种会有一点空间资源的牺牲。

至于二叉查找的算法,因为从时间复杂度的算法上面考虑。两者的算法如下:

二分查找时间复杂度算法:
第一次 N/2

第k次 N/2^k
最坏的情况下第k次才找到,此时只剩一个数据,长度为1。
即 N/2^k = 1
查找次数 k=log(N)。

对于二叉查找的时间复杂度算法理想状态下,他的时间复杂度和二分查找是相同的都是log(N)。
但是, 最差情况为所有数据全部在一端时(链表无索引,需要一个一个搜索),这个时候的时间复杂度就是相当于一个一个去匹配,然后找到我们想要的那个数。这个时候的时间复杂度就变成了O(n),不在是理想状态下的O(logn)

15. vector扩容机制

vector扩容规则: 当数组大小不够容纳新增元素时,开辟更大的内存空间,把旧空间上的数据复制过来,然后在新空间中继续增加。 新的更大的内存空间,一般是当前空间的1.5倍或者2倍,这个1.5或者2被称为扩容因子,不同系统实现扩容因子也不同。

16. malloc、new、delete、free

malloc分配的空间一定是连续的,多次new分配的空间不一定连续。物理空间上不一定连续。

new/delete是C++的运算符,用于申请动态内存和释放内存,而malloc和free是库函数

new和delete可以满足对象创建时自动调用构造函数完成对象的初始化,以及对象销毁时自动调用析构函数完成内存释放。

delete和delete[]的区别;

delete[]会调用每一个成员的析构函数

17. C++ vector(STL vector)底层实现机制

使用 3 个迭代器(可以理解成指针)来表示 ,一个指向起始字节位置,一个指向当前最后一个元素的末尾字节;一个指向容器所占内存空间的末尾字节

18. map与unordered_map

map和set的底层都是通过红黑树来实现的,但并不是原生态的红黑树,而是经过改造后的红黑树。

🌱 unordered_map和map的功能类似,都是KV模型。
🌱 底层不同,map的底层是红黑树,unordered_map的底层是哈希表。
🌱 map有排序的功能,而unordered_map是无序的。
🌱 从效率上看,unordered_map增删查改的时间复杂度为O(1),而map增删查改的时间复杂度为O(logN)。
🌱 从空间上看,unordered_map消耗的空间比map空间更大。
🌱 使用场景:对数据有排序或者空间要求时,选择map;对效率有要求时,选择unordered_map。

19. 链地址法的优化

链表节点数目大于8时改用红黑树进行优化(Java)

20. 左值右值

实际上左值可以称为“单元”,右值称为“值”,一切就更加清晰了。值是数据本身。单元是储存数据的一块内存空间。

然后再来个具名与非具名的划分,就OK了。变量是具名的单元,字面量是非具名的“值”,数组元素是非具名的单元,多数表达式是非具名的值,常量是具名的值(C++的常量不纯,只能算是只读变量,真正的常量是值类型)。值一般不需要分配内存存储,有些值也会被安排一定的内存来存放(如字符串)但这对程序是透明的。不需要内存的值,往往被直接内嵌到代码中。但如果需要的话,编译器也可以临时分配内存给值,以便转化为“只读引用”类型参数使用。

**认为右值不在内存中具有地址,暂时储存在寄存器中 **

左值引用与右值引用:

①右值引用做参数和做返回值时可减少拷贝次数,本质上利用了移动构造和移动赋值;
②左值引用和右值引用的作用都是减少拷贝,右值引用本质可以认为是弥补左值引用的不足的地方;
③左值引用作用:解决传参过程中,减少返回值的拷贝
做参数时:解决传参时的拷贝;
做返回值:解决返回值的拷贝;
注意:若是返回对象出了作用域就不存在了,则不能返回引用,左值引用就无法解决,故用C++11中右值引用。
④右值引用:解决传参后,内部的存储拷贝问题
做参数:解决内部拷贝次数,不再使用拷贝构造次数,而是移动构造
做返回值:解决外部调用接收返回对象的拷贝,右值引用的移动赋值,减少了拷贝次数。

21. C++从代码到可执行文件的过程

主要经过四个阶段:预编译阶段、编译阶段、汇编阶段和链接阶段

预编译阶段:
1.将所有的#define删除,并且展开相关的宏定义
2.处理相应的预编译条件指令,如#if、#ifdef
3.处理#include预编译指令,将被包含的文件插入到相应位置
4.删掉所有注释部分
5.添加行号的文件标识符

编译阶段
1.词法、语法、语义分析,将源代码的字符序列分成一系列的片段,然后片段进行语法分析,生成语法树,判断表达式是否正确有意义。
2.优化生成的代码
3.将生成的代码转换成汇编代码
4.优化汇编代码
这个过程对编译器的要求很高,也体现了不同编译器的效率问题,编译过程不仅与编译有关系,还与机器的硬件条件有关系,优化过程一方面实对中间代码的优化,这部分不依赖于机器,比如删除公共表达式、循环优化、复写传播、删除无用的复制等等。还有一部分是针对所处的硬件平台进行的优化,这部分要充分考虑到硬件特性,相关和指令集的特点、相关寄存器的使用来提高效率,也是非常考验相关工程师的水平的。

汇编过程
汇编过程是把编译后的汇编语言转换成目标机器指令而生成目标文件的过程。
目标文件最少包含代码段和数据段两个段。代码段可读可执行一般不可写,数据段一般可读可写可执行。
再win平台上一般生成.obj文件

链接
就是将不同的目标文件链接成一个可执行文件的过程。
汇编完毕的代码不能直接执行,还有许多问题没有解决:
比如源文件调用了另一个源文件中的变量或者函数;程序中可能调用了某个库中的库函数等等。这些问题都有链接来解决。
链接过程的目的就是将目标文件彼此相连接,使得他们可以连成一个整体装入操作系统执行。

根据链接方式的不同,可以分为静态链接和动态链接两种:

1.静态链接:
静态链接的过程中,函数代码会从静态库中拷贝到最终可执行文件中,这样程序在执行的时候就会把文件中的代码拷贝到该进程的虚拟地址空间中。这样可执行文件与静态库的文件不再联系了,删除静态库或者移到另一个环境也可以使用。静态库的后缀为win下 .lib ;linux下.a
2.动态链接
在动态链接过程中,函数的代码并不会拷贝到可执行文件中,只会在可执行文件中记录必要的定位信息,可以找到相应动态库代码的位置即可。在运行程序时,需要把动态库映射到进程的虚拟地址空间中,所以动态库和文件是相关联的,如果删掉动态库可执行文件就无法正常运行了。 文件后辍为:win下为.dll,linux下为.so。

1.预处理,产生.ii文件

2.编译,产生汇编文件(.s文件)

3.汇编,产生目标文件(.o或.obj文件)

4.链接,产生可执行文件(.out或.exe文件)

22. C/C++内存分布

栈区:存储非静态局部变量/函数参数/返回值等,栈向下增长

自由存储区:由malloc分配的内存,由free释放

堆区:程序运行时动态内存分配,向上增长

全局/静态存储区:存储全局数据和静态数据

常量区:可执行的代码和只读常量

1.c++中如果是申请内置类型对象或者数组,malloc和new没有什么区别。
2.如果是自定义类型,那么区别很大,new和delete是开空间 + 初始化,析构清理 + 释放空间,malloc和free仅仅是开空间 + 释放空间。
3.建议在c++中,无论是自定义类型还是内置类型的申请和释放,尽量使用new和delete。

malloc/free和new/delete的区别
1.相同点:它们都是堆上申请空间并且手动释放。
2.malloc和free是函数,new和delete是操作符。
3.malloc申请的空间不初始化,new申请的空间可以初始化。
4.malloc申请空间时需要计算所需空间的大小并且传递,new申请空间时只需在其后加上空间类型即可。
5.malloc的返回值为void*,在使用时必须强转,new不需要,因为new后跟的是空间的类型。
6.malloc申请空间失败时返回NULL,使用时需要判空,而new不需要判空,但是new需要捕获异常。
7.申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数和析构函数处理空间,但new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前后调用析构函数完成空间中资源的清理。

23. 内存泄露

1️⃣ 堆内存泄漏(Heap Leak)
堆内存指程序执行中需要通过malloc、realloc、realloc、new等从堆中分配内存,用完后需通过调用free或者delete释放。若程序的设计错误导致这一部分内存没有被释放掉,那么之后这块空间将无法继续使用,就会发生堆内存泄漏。

2️⃣ 系统资源泄漏
指程序使用系统分配的资源,比如套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重则可导致系统的效能减少,系统执行不稳等。

如何避免?

  • 工程前期涉及规范,养成良好的编码习惯
  • 提前预防,使用智能指针
  • 内存泄露工具的使用

24. 智能指针

智能指针(smart pointer)的一种通用实现技术是使用引用计数(reference count)。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象的指针指向同一对象 。

智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。

24. 抽象类

包含纯虚函数的类成为抽象类

为了抽象和设计的目的而建立

抽象类的作用:
将有关操作作为结构接口组成在一个继承层次结构中,由它来为派生类提供公共的根,派生类将具体实现其基类中作为接口的操作。

抽象类只能作为基类使用,其纯虚函数的实现由派生类给出,若派生类中没有重新定义纯虚函数,只是继承基类的纯虚函数,则派生类仍然是一个抽象类。

抽象类是不能定义对象的

25. 拷贝构造函数

只有一个参数,参数类型是本类的引用

如果类的设计者不写拷贝构造函数,编译器会自动生成,大多数情况下是实现源对象到目标对象逐个字节的复制。

在通过其他同类对象构造新对象时,会调用拷贝构造函数进行初始化

#include<iostream>
using namespace std;
class Complex{
public:
    double real, imag;
    Complex(double r,double i){
        real = r; imag = i;
    }
    Complex(const Complex & c){
        real = c.real; imag = c.imag;
        cout<<"Copy Constructor called"<<endl ;
    }
};

int main(){
    Complex cl(1, 2);
    Complex c2 (cl);  //调用复制构造函数
    cout<<c2.real<<","<<c2.imag;
    return 0;
}

拷贝构造函数被调用的三种情况:

  • 当用一个对象去初始化同类的另一个对象时,引发拷贝构造函数的调用
Complex c2(c1);
Complex c2 = c1;
//注意,第二条语句是初始化语句,不是赋值语句。
  • 如果函数F的参数是类A的对象,那么当F被调用时,类A的复制构造函数将被调用

也就是说作为形参的对象,是用拷贝构造函数初始化的

#include<iostream>
using namespace std;
class A{
public:
    A(){};
    A(A & a){
        cout<<"Copy constructor called"<<endl;
    }
};
void Func(A a){ }
int main(){
    A a;
    Func(a);
    return 0;
}

前面说过,函数的形参的值等于函数调用时对应的实参,现在可以知道这不一定是正确的。如果形参是一个对象,那么形参的值是否等于实参,取决于该对象所属的类的复制构造函数是如何实现的。例如上面的例子,Func 函数的形参 a 的值在进入函数时是随机的,未必等于实参,因为复制构造函数没有做复制的工作。

以对象作为函数的形参,在函数被调用时,生成的形参要用复制构造函数初始化,这会带来时间上的开销。如果用对象的引用而不是对象作为形参,就没有这个问题了。但是以引用作为形参有一定的风险,因为这种情况下如果形参的值发生改变,实参的值也会跟着改变。

如果要确保实参的值不会改变,又希望避免复制构造函数带来的开销,解决办法就是将形参声明为对象的 const 引用。例如:

void Function(const Complex & c)
{
    ...
}
  • 函数的返回值是类A的对象,则函数返回时,类A的复制构造函数将被调用
#include<iostream>
using namespace std;
class A {
public:
    int v;
    A(int n) { v = n; };
    A(const A & a) {
        v = a.v;
        cout << "Copy constructor called" << endl;
    }
};
A Func() {
    A a(4);
    return a;
}
int main() {
    cout << Func().v << endl;
    return 0;
}

什么时候必须重写拷贝构造函数?

当构造函数涉及到动态存储分配空间时,要自己写拷贝构造函数,并且要深拷贝。

26. 引用&

引用是某个目标变量的别名,对其进行操作与对变量直接进行操作的效果相同。

声明一个引用不是定义一个变量,因此它本身不是一种数据类型,不占存储单元,不能建立数组引用

引用作为函数参数的特点:

  • 传递引用给函数与传递指针的效果相同。
  • 使用引用传递函数参数时,内存中不会参数实参的副本。而一般函数的参数在函数调用时,需要给形参分配存储单元,形参是实参的副本;此时改变形参不会改变实参的值,因为函数调用后形参会销毁。

const &何时使用?

既要提高程序的效率,又要保护传递给函数的数据不在函数中被修改,则使用常引用

将引用作为函数的返回值类型需要注意:

  • 不能返回局部变量的引用,因为局部变量会在函数返回后被销毁,此时返回的引用成为无所指的引用,程序进入未知状态
  • 不能返回函数内部new分配内存的引用
  • 可以返回类成员的引用

27. main函数执行前,还会执行什么代码?

全局对象的构造函数会在main函数之前执行

28. const与宏定义(#define)相比的优点?

  • const定义变量,修饰函数参数,修饰函数返回值,使得被修饰的东西收到强制保护,避免意外变动,提高程序的健壮性
  • const常量有数据类型,而宏没有,编译器会对前者做类型安全检查,而后者仅做字符替换不做检查,可能产生错误

29. 数组和指针的区别?

  • 数组要么在静态存储区被创建(全局数组),要么在栈上被创建,而指针可以随时指向任意类型的内存块

30. 引用与指针的区别

  • 引用必须初始化,指针不必
  • 引用初始化后不能改变,指针可以改变所指
  • 不存在指向空值的引用,存在指向NULL的指针

31. #include<file.h> #include “file.h” 的区别

前者从标准库路径寻找

后者从当前工作路径寻找

32. 声明和定义

声明式告诉编译器变量的类型和名字,不会为变量分配空间

定义需要分配空间,同一个变量可以被多次声明,但是只能定义一次

33. 友元函数和友元类

友元(friend)提供了不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。

通过友元,另一个类中的成员函数可以访问类中的私有成员和保护成员

  1. 友元函数

可以访问类的私有成员的非成员函数。定义在类外的普通函数,不属于任何类,但是需要在类的定义中加以声明

一个函数可以是多个类的友元函数,需要在各个类中分别说明

  1. 友元类

所有成员函数都是另一个类的友元函数,都可以访问另一个类的隐藏信息(包括私有成员和保护成员)

使用友元类需要注意:

  • 有缘关系不能被继承
  • 友元关系是单向的,不具有交换性
  • 友元关系不具有传递性

34. 头文件中的 ifndef/define/endif 是干什么用的? 该用法和 program once 的区别?

作用是防止头文件被重复包含

  • ifndef由语言本身支持提供,program once由编译器提供支持
  • 运行速度上ifndef会慢于program once

35. 数组指针和指针数组

数组指针,是指向数组的指针,而指针数组则是指该数组的元素均为指针。

数组指针,是指向数组的指针,其本质为指针,形式如下。如 int (*p)[10],p即为指向数组的指针,()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。数组指针是指向数组首元素的地址的指针,其本质为指针,可以看成是二级指针。

类型名 (*数组标识符)[数组长度]

指针数组,在C语言和C++中,数组元素全为指针的数组称为指针数组,其中一维指针数组的定义形式如下。指针数组中每一个元素均为指针,其本质为数组。如 int p[n], []优先级高,先与p结合成为一个数组,再由int说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]…p[n-1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 p=a; 这里p表示指针数组第一个元素的值,a的首地址的值。

类型名 *数组标识符[数组长度]

36. typedef和define的区别

(1) 用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义常量,以及书写复杂使用频繁的宏。
(2) 执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。
(3) 作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在 define 声明后的引用都是正确的。
(4) 对指针的操作不同:typedef 和 define 定义的指针时有很大的区别。
注意:typedef 定义是语句,因为句尾要加上分号。而 define 不是语句,千万不能在句尾加分号。

37. 指针常量和常量指针的区别

指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。常量指针是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。
指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。
注意:无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变特性。

38. extern“C”的作用

Extern “C”是由C++提供的一个连接交换指定符号,用于告诉C++这段代码是C函数。这是因为C++编译后库中函数名会变得很长,与C生成的不一致,造成C++不能直接调用C函数,加上extren “c”后,C++就能直接调用C函数了。

Extern “C”主要使用正规DLL函数的引用和导出 和 在C++包含C函数或C头文件时使用。使用时在前面加上extern “c” 关键字即可。可以用一句话概括extern “C”这个声明的真实目的:实现C++与C及其它语言的混合编程。

39. 定义一个宏需要注意什么?

定义部分的每个形参和整个表达式都必须用括号括起来,以避免不可预料的错误发生

40. 在类的内部定义成员函数的函数体,这种函数会具备那种属性?

这种函数会自动为内联函数,这种函数在函数调用的地方在编译阶段都会进行代码替换。

41. C++中四种cast的使用场景

constcast:去掉常量属性以及volatile,但是如果原来他就是常量去掉之后千万不要修改;比如你手里有一个常量指针引用,但是函数接口是非常量指针,可能需要转换一下;成员函数声明为const,你想用this去执行一个函数,也需要用constcast

staticcast:基本类型转换到void,转换父类指针到子类不安全

dynamiccast:判断基类指针或引用是不是我要的子类类型,不是强转结果就返回null,用于多态中的类型转换

reintercast:可以完成一些跨类型的转换,如int到void*,用于序列化网络包数据

42. 成员函数调用delete this会发生什么?之后再进行读写会怎么样?

在类的成员函数中能不能调用delete this?答案是肯定的,能调用,而且很多老一点的库都有这种代码。假设这个成员函数名字叫release,而delete this就在这个release方法中被调用,那么这个对象在调用release方法后,还能进行其他操作,如调用该对象的其他方法么?答案仍然是肯定的,调用release之后还能调用其他的方法,但是有个前提:被调用的方法不涉及这个对象的数据成员和虚函数。

如果这时候调用普通的成员函数应该没有问题,因为这些成员函数与普通函数区别不大也在代码段,也需要走函数栈的逻辑。

如果调用虚函数,那就需要获取类内存的虚函数指针,这就涉及到堆内存的操作了,因为这时候虚函数指针也会被设置成未初始化的值,会有问题。

如果操作非指针成员变量,可能读和写都没有问题。

如果操作指针成员变量,指针可能设置成未初始化的值,很可能指向不合法的地方,强制赋值可能会导致崩溃

简单来说就是不要再去操作他的内存数据,否则很有可能崩溃,因为释放后,这个内存不确定系统如何处理。

另外,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。

43. 单例模式

将构造函数、析构函数、复制构造函数、赋值操作符声明为私有,即可实现单例模式

class Singleton
{
public:
    static Singleton* Instance();
protected:
    Singleton();
private:
    static Singleton* _instance;
};
Singleton::Singleton(){}
Singleton* Singleton::_instance = nullptr;
Singleton* Singleton::Instance()
{
    if(_instance == nullptr)
        _instance = new Singleton;
    return _instance;
}

(2)、避免用户的复制行为,可以将复制构造函数声明为private或者使用C++11中的delete语法。
(3)、实现线程安全的单例模式:上面实现中的GetInstance()不是线程安全的,因为在单例的静态初始化中存在竞争条件。如果碰巧有多个线程在同时调用该方法,那么就有可能被构造多次。
比较简单的做法是在存在竞争条件的地方加上互斥锁。这样做的代价是开销比较高。因为每次方法调用时都需要加锁。
比较常用的做法是使用双重检查锁定模式(DCLP)。但是DCLP并不能保证在所有编译器和处理器内存模型下都能正常工作。如,共享内存的对称多处理器通常突发式提交内存写操作,这会造成不同线程的写操作重新排序。这种情况通常可以采用volatile解决,他能将读写操作同步到易变数据中,但这样在多线程环境下仍旧存在问题。

44. C++11特性

了解C++新特性吗

1.关键字及新语法:auto、nullptr、for

2.STL容器:std::array、std::forward_list、std::unordered_map、std::unordered_set

3.多线程:std::thread、std::atomic、std::condition_variable

4.智能指针内存管理:std::shared_ptr、std::weak_ptr

5.其他:std::function、std::bind和lamda表达式

45. 堆区与栈区详解

申请方式
stack:
由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:
需要程序员自己申请,并指明大小,在c中malloc函数
如p1 = (char *)malloc(10);
在C++中用new运算符
如p2 = new char[10];
但是注意p1、p2本身是在栈中的。

申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间,另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意 思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储
的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小
受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

申请效率的比较
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是
直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。

堆和栈中的存储内容
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可
执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈
的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地
址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。

管理方式不同
栈是编译器管理,堆的占用和释放都是由程序员进行控制的;

空间大小不同
在32位系统下,一般堆的内存可以达到4G的空间,可以说堆内存几乎是没有限制的。但是对于栈,一般都有一定空间大小(跟编译器有关),比如在VC6下默认的栈空间大小是1M

能否产生碎片不同
对于堆来说,频繁的new/delete操作会造成内存空间的不连续,从而造成大量碎片,使程序效率降低;
但是对于栈来说,因为总是先进后出不存在内存块不连续的问题。

生长方向不同
堆的生长方向是向上的,即向着内存地址增加的方向;
栈的生长方向是向下的,即向着内存地址减小的方向增长。

分配方式不同
堆总是动态分配的,需要程序员手动释放;
栈存在静态分配和动态分配的:
其中静态分配是由编译器完成的(比如局部变量的分配);
动态分配是由alloca函数进行分配的(这个函数会在栈帧的调用处上分配一定空间,当调用alloca的函数返回到调用位置时,这些临时空间会被自动释放),栈的动态分配是由编译器自己进行释放的。

分配效率不同
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持,包括:分配专门的寄存器来存放栈的地址、入出栈都有专门指令,因此栈的效率会比较高。
堆是C/C++函数库提供的,其机制非常复杂,比如为了分配一块内存,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果找不到(可能是因为内存碎片过多),就可能调用系统功能区(用户模式和内核模式的切换)增加程序数据段的内存空间,如此便有机会分到足够大小的内存,然后进行返回。

46. 内存越界

StackOverflow和BufferOverflow

缓冲区溢出strcpy会一直复制直到碰到\0,很多平台的栈变量是按照地址顺序倒着分配的(高地址向低地址),所以destination溢出后会先修改先前定义的变量,这样黑客就可以把is_administrator改为true,从而造成缓冲区溢出攻击,当然数组越界也可以造成类似的效果,不过现在C++都提供了越界检查的版本

// 缓冲区溢出攻击
const int MAX_LENGTH = 16;
bool is_administrator = false;
char destination[MAX_LENGTH];
std::string source = read_string_from_client(); //内容存储在缓冲区
strcpy(destination,source.c_str());

栈溢出攻击在栈上分配length字节的空间,再往栈顶放上一个data。当Length十分大,会把data挤到栈空间之外,此时如果编译器不做越界检查的话,那么黑客只要用客户端送特定的length和data,就能改写服务器的任意内存(比如黑客可以修改服务器代码的机器码,注入一些JMP指令跳转到黑客想执行的函数)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

国家一级假勤奋研究牲

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

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

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

打赏作者

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

抵扣说明:

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

余额充值