C++ 面试八股文
1、C++ 中的内存分配情况
栈: 由编译器管理分配和回收,存放局部变量和函数参数。
堆: 由程序员管理,需要手动 new malloc delete free
进行分配和回收,空间较大,但可能会出现内存泄漏和空闲碎片的情况。
全局/静态存储区: 分为初始化和未初始化两个相邻区域,存储初始化和未初始化的全局变量和静态变量。
常量存储区: 存储常量,一般不允许修改。
代码区: 存放函数体的二进制代码。
2、C++ 中的指针参数传递和引用参数传递
指针参数传递本质上是值传递:它说传递是第一个地址值,值在传递的过程中被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本 (替身)。值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量处理的,不会影响到主调函数的实参变量值(形参指针变了,实参指针不会变)
引用参数传递的过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数,放进来的实参变量地址。被调函数对形参的任何操作都被处理成为间接寻址,即通过栈中存放的地方访问,主调函数中的实参变量(根据别名找到主调函数中的本体)。因此,被调函数对形成的任何操作都会影响主调函数中的实参变量。
引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理,都会通过一个间接寻址的方式操作到主调函数中的相关变量。 而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量,那就使用指向指针的指针或者指针引用。
3、C++ 中的 const 和 static 关键字
static
1、修饰局部变量:一般来说局部变量是存放在栈区的,但是如果用 static 修饰,这个局部变量就会被放到静态区,生命周期也会变长,直到程序结束才会被释放。 但是其作用域并没有改变。
2、修饰全局变量:对于一个全局变量,他既可以被本文件访问到,也可以被其他文件访问到(使用extern)。用 static 可以将全局变量的作用域限制在该文件内
3、修饰函数:用 static 修饰函数,情况和全局变量类似,也是改变了函数的作用域,只能在本文件内访问到。
4、类成员/类函数声明 static:
- 函数体内 static 变量的作用范围为该函数体,不同于 auto 变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
- 在模块内的 static 全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;
- 类中的 static 成员变量属与整个类所拥有
const
1、修饰基本类型数据类型:基础数据类型,修饰符 const 可以用于类型说明符前,也可以用于类型说明符后,其结果是一样的。在使用这些常量的时候,只要不改变这些常量的值就可以了。
2、const 修饰指针变量:如果const是位于 左侧,则 const 是用来修饰指针所指向的变量,即指针指向常量;如果 const 位于 的右侧,则 const 是修饰指针本事,指针本身是常量。
3、const 应用到函数中:作为参数的 const 修饰符,表示函数内部不会修改该参数的值,因此可以避免不必要的内存拷贝,提高效率。 比如:void func(const int a)
中的 a 就是一个常量,不能被修改。
4、const 在类中的用法:
const 成员变量:只在某个对象生命周期内是常量,而对于整个类而言是可以改变的。因为类可以创建多个对象,不同的对象其 const 数据成员值可以不同。所以不能在类的声明中初始化 const 数据成员, 因为类的对象在没有创建时候,编译器不知道 const 数据成员的值是什么。const 数据成员的初始化只能在类的构造函数的初始化列表中进行。
const 成员函数: const 成员函数的主要目的是防止成员函数修改对象的内容。 要注意,const 关键字和 static 关键字对于成员函数来说是不能同时使用的,因为 static 关键字修饰静态成员函数不含 有 this 指针,即不能实例化,const 成员函数又必须具体到某一个函数。
4、C 和 C++ 的区别 (函数/类/struct/class)
1、函数方面 C++ 中有重载和虚函数的概念
2、类方面,C 的 struct 和 C++ 的类也有很大不同
3、C++ 中增加了模板还用代码,提供了更加强大的 STL 标准库
5、C++ 中的重载和重写
重载
同一个区域内被声明多次的函数,他们的参数列表不同,这些函数就是重载函数。重载函数的特点是:函数名相同,参数列表不同,返回值可以相同也可以不同。
重写
重写是子类对父类的虚函数的重新实现。重写的特点是:函数名、参数列表、返回值类型都相同,但是函数体不同。
5、介绍 C++ 中所有的构造函数
无参数构造函数: 即默认构造函数,如果没有明确写出无参数构造函数,编译器会自动生成默认的无参数构造函数,函数为空,什么也不做,如果不想使用自动生成的无参构造函数,必需要自己显示写出一个无参构造函数。
一般构造函数:也称重载构造函数,一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是 参数的个数或者类型不同,创建对象时根据传入参数不同调用不同的构造函数。
拷⻉构造函数:拷⻉构造函数的函数参数为对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对 象,一般在函数中会将已存在的对象的数据成员的值一一复制到新创建的对象中。如果没有显示的写拷⻉构造函数,则系统会默认创建一个拷⻉构造函数,但当类中有指针成员时,最好不要使用编译器提供的默认的拷⻉构造函数,最好自己定义并且在函数中执行深拷⻉。
6、堆和栈的区别
栈
由编译器进行管理,在需要的时候由编译器自动分配空间,不需要的时候自动回事,一般保存的是局部变量和函数参数等
连续的内存空间,在函数调用的时候,首先入栈的主函数的下一条可执行指令的地址,然后是各个函数的参数
大多编译器中,参数是从右往左入栈,本次函数调用结束的时候,局部变量先出栈,然后是参数,最后是栈顶指针最开始存放的地址,程序由该点继续运行,不会产生碎片
堆
堆是由程序员管理,需要手动 new malloc delete free 进行分配和回收,如果不进行回收的话,可能会造成内存泄露的问题。
不连续的空间,实际上系统中有一个空闲链表,当有程序申请的时候,系统遍历空闲链表找到第一个大于等于申请大小的空间分配给程序,一般在分配程序的时候,也会在空间头部写入内存大小,方便delete回收空间大小,当然如果还有剩余,也会将剩余的插入到空闲链表中,这也是内存碎片产生的原因。
7、面向对象的三大特性,并举例说明
C++ 面向对象的三大特性是封装继承和多态
封装
封装就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让信任的类或者对象操作,对不可信的进行信息隐藏。一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部 分意外的改变或错误的使用了对象的私有部分。
继承
继承是指这样一种能 力:它可以使用现有类的所有功能,并在无需新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新 类称为“子类”或者“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要 实现继承,可以通过“继承”和“组合”来实现。
多态
多态与非多态的实质区别就是函数地址是早绑定还是晚绑定的。如果函数的调用,在编译器编译期间就可以确定函 数的调用地址,并产生代码,则是静态的,即地址早绑定。而如果函数调用的地址不能在编译器期间确定,需要在 运行时才确定,这就属于晚绑定。
多态其实一般就是指继承加虚函数实现的多态,对于重载来说,实际上基于的原理是,编译器为函数生成符号表时的不同规则,重载只是一种语言特性,与多态无关,与面向对象也无关,但这又是 C++中增加的新规则,所以也算 属于 C++,所以如果非要说重载算是多态的一种,那就可以说:多态可以分为静态多态和动态多态。
动态多态是指通过子类重写父类的虚函数来实现的,因为是在运行期间决定调用的函数,所以称为动态多态,一般情况下我们不区分这两个时所说的多态就是指动态多态。
8、虚函数相关
析构函数一般写成虚函数的原因
直观的讲: 是为了降低内存泄漏的可能性。举例来说就是,一个基类的指针指向一个派生类的对象,在使用完毕准备销毁时,如果基类的析构函数没有定义成虚函数,那么编译器根据指针类型就会认为当前对象的类型是基类,调用基类的析构函数 (该对象的析构函数的函数地址早就被绑定为基类的析构函数),仅执行基类的析构,派生类的自身内容将无法被析构,造成内存泄漏。
如果基类的析构函数定义成虚函数,那么编译器就可以根据实际对象,执行派生类的析构函数,再执行基类的析构函数,成功释放内存。
构造函数为什么一般不定义为虚函数?
虚函数调用只需要知道“部分的”信息,即只需要知道函数接口,而不需要知道对象的具体类型。但是,我们要 创建一个对象的话,是需要知道对象的完整信息的。特别是,需要知道要创建对象的确切类型,因此,构造函数不应该被定义成虚函数;
析构函数的作用,如何起作用?
构造函数只是起初始化值的作用,但实例化一个对象的时候,可以通过实例去传递参数,从主函数传递到其他的函数里面,这样就使其他的函数里面有值了。规则,只要你一实例化对象,系统自动回调用一个构造函数,就是你不写,编译器也自动调用一次。
析构函数与构造函数的作用相反,用于撤销对象的一些特殊任务处理,可以是释放对象分配的内存空间;特点:析构函数与构造函数同名,但该函数前面加~。
析构函数没有参数,也没有返回值,而且不能重载,在一个类中只能有一个析构函数。当撤销对象时,编译器也会自动调用析构函数。每一个类必须有一个析构函数,用户可以自定义析构函数,也可以是编译器自动生成默认的析构函数。一般析构函数定义为类的公有成员。
纯虚函数是什么
实际上,纯虚函数的出现就是为了让继承可以出现多种情况:
- 有时我们希望派生类只继承成员函数的接口
- 有时我们又希望派生类既继承成员函数的接口,又继承成员函数的实现,而且可以在派生类中可以重写成员函数以实现多态
- 有的时候我们又希望派生类在继承成员函数接口和实现的情况下,不能重写缺省的实现。
其实,声明一个纯虚函数的目的就是为了让派生类只继承函数的接口,而且派生类中必需提供一个这个纯虚函数的实现,否则含有纯虚函数的类将是抽象类,不能进行实例化。
对于纯虚函数来说,我们其实是可以给它提供实现代码的,但是由于抽象类不能实例化,调用这个实现的唯一方式 是在派生类对象中指出其 class 名称来调用。
9、深拷⻉和浅拷⻉的区别
当出现类的等号赋值时,会调用拷⻉函数,在未定义显示拷⻉构造函数的情况下, 系统会调用默认的拷⻉函数-即浅拷⻉,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷⻉是可行的。
但当数据成员中有指针时,如果采用简单的浅拷⻉,则两类中的两个指针指向同一个地址,当对象快要结束时,会调用两次析构函数,而导致指野指针的问题。
所以,这时必需采用深拷⻉。深拷⻉与浅拷⻉之间的区别就在于深拷⻉会在堆内存中另外申请空间来存储数据,从 而也就解决来野指针的问题。简而言之,当数据成员中有指针时,必需要用深拷⻉更加安全。
10、什么情况下会调用拷⻉构造函数
类的对象需要拷⻉时,拷⻉构造函数将会被调用,以下的情况都会调用拷⻉构造函数:
- 一个对象以值传递的方式传入函数体,需要拷⻉构造函数创建一个临时对象压入到栈空间中。
- 一个对象以值传递的方式从函数返回,需要执行拷⻉构造函数创建一个临时对象作为返回值。
- 一个对象需要通过另外一个对象进行初始化。
11、内存泄漏的定义,如何检测与避免?
定义: 内存泄漏简单的说就是申请了一块内存空间,使用完毕后没有释放掉。 它的一般表现方式是程序运行时间越⻓,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄漏了。
如何检测内存泄漏:
1、首先可以通过观察猜测是否可能发生内存泄漏,Linux 中使用 swap 命令观察还有多少可用的交换空间,在一两分钟内键入该命令三到四次,看看可用的交换区是否在减少。 2、还可以使用 其他一些 /usr/bin/stat 工具如 netstat、vmstat 等。如发现波段有内存被分配且从不释放,一个可能的解释就是有个进程出现了内存泄漏。 3、当然也有用于内存调试,内存泄漏检测以及性能分析的软件开发工具 valgrind 这样的工具来进行内存泄漏的检测。
12、动态编译与静态编译
静态编译,编译器在编译可执行文件时,把需要用到的对应动态链接库中的部分提取出来,连接到可执行文件中去,使可执行文件在运行时不需要依赖于动态链接库;
动态编译,可执行文件需要附带一个动态链接库,在执行时,需要调用其对应动态链接库的命令。所以其优点一方面是缩小了执行文件本身的体积,另一方面是加快了编译速度,节省了系统资源。缺点是哪怕是很简单的程序,只用到了链接库的一两条命令,也需要附带一个相对庞大的链接库; 二是如果其他计算机上没有安装对应的运行库, 则用动态编译的可执行文件就不能运行。
13、请你来说一下 fork 函数
成功调用 fork()
会创建一个新的进程,它几乎与调用 fork()
的进程一模一样,这两个进程都会继续运行。在子进程 中,成功的 fork()
调用会返回 。在父进程中 fork()
返回子进程的 pid。