C++ 面试准备【一】
目录
-
关键字与操作符
static
const
#define
typedef
using -
指针与引用
引用与指针的区别和联系
为什么传引用比传指针安全?
野指针
智能指针 -
类
空类默认成员函数
友元函数和友元类 -
多态与虚函数
C++多态性实现原理
动态绑定
虚函数表和虚函数指针
纯虚函数
override和final
回避虚函数机制
不能声明为虚函数的成员函数
override和overwrite的区别
关键字与操作符
该部分内容包括:
- static
- const
- #define
- typedef
- using
指针与引用
该部分内容包括:
- 引用与指针的区别和联系
- 为什么传引用比传指针安全?
- 野指针
- 智能指针
引用与指针的区别和联系
- 指针是实体,会为其分配内存,且可以允许多级指针
- 引用创建时必须初始化且不可变(只能初始化一次故不用const),指针创建时无须初始化但最好初始化以防止NULL
- 二者自增(++)结果不同,引用是值进行自增,而指针是地址进行自增;
- sizeof 结果不同,sizeof 引用得到的是所指向的变量(对象)的大小,而sizeof 指针得到的是指针本身的大小
- 引用访问是直接访问原对象,指针则是间接访问
- 作为参数给函数传参不同,引用传参会比指针传参更安全,原因见下一问
- 联系
- 引用的内部使用指针实现的
- 引用是受了限制的指针
为什么传引用比传指针安全?
- 引用在创建的同时必须初始化,保证引用的对象是有效的,所以不存在NULL引用;而指针在定义的时候不必初始化,所以,指针则可以是NULL,可以在定义后面的任何地方重新赋值
- 引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用;而指针在任何时候都可以改变为指向另一个对象
- 引用的创建和销毁并不会调用类的拷贝构造函数
因为不存在空引用,并且引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用,所以比指针安全。
由于 const 指针仍然存在空指针,并且有可能产生野指针,所以还是不安全。解决方案是智能指针。
野指针
野指针不是NULL指针,是未初始化或者未清零的指针,它指向的内存地址不是程序员所期望的,可能指向了受限的内存。
成因:
1)指针变量没有被初始化
2)指针指向的内存被释放了,但是指针没有置NULL
3)指针超过了变量的作用范围,比如b[10],指针b+11
介绍一下智能指针
智能指针,将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。
智能指针就是一种栈上创建的对象,函数退出时会调用其析构函数,这个析构函数里面往往就是一堆计数之类的条件判断,如果达到某个条件,就把真正指针指向的空间给释放了。
使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,野指针,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
注意:不能将指针直接赋值给一个智能指针,一个是类,一个是指针。
常用的智能指针
智能指针在C++11版本之后提供,包含在头文件
在C++98中,有std::auto_ptr
,但它有很多问题。 不支持复制(拷贝构造函数)和赋值(operator =),但复制或赋值的时候不会提示出错。所以可能会造成程序崩溃。
auto_ptr<string> p1(new string ("auto") );//#1
auto_ptr<string> p2; //#2
p2 = p1; //#3
在语句#3中,p2接管string对象的所有权后,p1的所有权将被剥夺,可防止p1和p2的析构函数试图刪同—个对象;
如果再访问p1指向的内容则会导致程序崩溃,因为p1不再指向有效的数据。std::auto_ptr
被unique_ptr代替。
- unique_ptr
某个时刻只能有一个unique_ptr指向一个给定的对象。
不支持复制和赋值,但比auto_ptr好,直接赋值会编译出错。实在想赋值的话,需要使用std::move
。 - shared_ptr
基于引用计数的智能指针,提供所有权共享的智能指针,在最后一个指向共享对象的shared_ptr被销毁时释放共享对象。 - weak_ptr
用来解决shared_ptr循环引用的问题,见参考4。
弱智能指针对象,它不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的智能指针。
将一个weak_ptr绑定到一个shared_ptr对象,不会改变shared_ptr的引用计数。
一旦最后一个所指向对象的shared_ptr被销毁,所指向的对象就会被释放,即使此时有weak_ptr指向该对象,所指向的对象依然被释放。
类
该部分内容包括:
- 空类默认成员函数
- 友元函数和友元类
空类缺省有哪些成员函数
- 构造函数
- 析构函数
- 拷贝构造函数
- 赋值函数(operator=)
友元函数和友元类
友元提供了不同类的成员函数之间、类的成员函数和一般函数之间进行数据共享的机制。
通过友元,一个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员。
友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。
详情见参考5
- 友元函数
友元函数是可以访问类的私有成员的非成员函数。它是定义在类外的普通函数,不属于任何类,但是需要在类的定义中加以声明。
friend 类型 函数名(形式参数);
一个函数可以是多个类的友元函数,只需要在各个类中分别声明。
- 友元类
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
friend class 类名;
使用友元类时注意:
(1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明
多态与虚函数
多态是“一个接口,多种实现”,通过派生类重写父类的虚函数,实现了接口的重用。
多态包括编译时多态和运行时多态,编译时多态体现在运算符重载上,运行时多态是通过继承和虚函数来体现的。
类的继承中使用虚函数来重写(override)基类中的函数。
在实际开发时,一般面向接口开发,操作父类指针即可。运行时父类指针指向子类对象并可访问子类同名函数,这时父类的虚函数就提供了接口。
该部分内容包括:
- C++多态性实现原理
- 动态绑定
- 纯虚函数
- override和final
- 回避虚函数机制
- 不能声明为虚函数的成员函数
- override和overwrite的区别
C++多态性实现原理
在继承体系下,将父类的某个函数给成虚函数(即加上virtual关键字),在派生类中对这个虚函数进行重写,利用父类的指针或引用调用虚函数。通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。对于虚函数调用来说,每一个对象内部都有一个虚表指针,在构造子类对象时,执行构造函数中进行虚表的创建和虚表指针的初始化,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用,这就是C++多态性实现的原理。
普通函数是静态编译的,没有运行时多态。
动态绑定与静态绑定
静态绑定和动态绑定是C++多态性的一种特性。
1)对象的静态类型和动态类型
静态类型:对象在声明时采用的类型,在编译时确定
动态类型:当前对象所指的类型,在运行期决定,对象的动态类型可变,静态类型无法更改
2)静态绑定和动态绑定
静态绑定:绑定的是对象的静态类型,函数依赖于对象的静态类型,在编译期确定
动态绑定:绑定的是对象的动态类型,函数依赖于对象的动态类型,在运行期确定
只有虚函数才使用的是动态绑定,其他的全部是静态绑定。
何时发生动态绑定
只有通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数,才能发生动态绑定。若使用静态对象调用虚函数,是无法发生动态绑定,这时只能调用基类的成员函数。
详情可见参考3。
纯虚函数
基类中为其派生类保留一个名字,以便派生类根据需要进行定义。
包含一个纯虚函数的类被称为抽象类,比如说“动物”这样一个类不便于定义具体成员函数,其子类“长颈鹿”、“大象”等才应该定义具体的成员函数,这时候“动物”类适合定义定义为抽象类,成员函数创建为纯虚函数以提供接口。
纯虚函数的形式如下:
virtual returnType function() = 0;
抽象类不可以实例化,但可以定义指针。
虚函数的使用场景
函数传参时,直接传基类指针,在函数中即可直接对基类指针操作,而不用考虑具体是哪个派生类,这样便于函数的封装。
void func(Base *base){...}
虚函数表和虚函数指针
虚函数vtable表属于类(有点像一个类里面的staic成员变量);不同编译器的vtable存放位置不同,如gcc编译器的实现中虚函数表vtable存放在可执行文件的只读数据段。
虚函数指针属于类的实例化对象,大小固定;vptr存放在栈中。
override和final说明符
C++11中可以使用override关键字来说明派生类中的虚函数以方便查错。
void f1() const override; // 子类虚函数重写f1
为了拒绝在子类中重写父类的函数,可以使用final,方便查错。
void f1() const final; // 父类函数不允许子类重写f1
如何回避虚函数机制
某些情况希望对虚函数的调用不要进行动态绑定,而是强迫执行虚函数的某个特定版本。
这时候可以使用作用域运算符::
。
DerivedClass child;
double x = child -> BaseClass::function();
哪些类中的成员函数不能声明为虚函数
- 静态成员函数
- 内联函数
- 构造函数
- 静态成员函数不能定义为虚函数
因为静态成员函数没有this指针,并且静态成员函数可以通过类名来访问。
又因为虚函数是放在对象的虚表里面的,同一个类中的所有对象虽然共用同一张虚表,但是类名无法找到虚表。 - 内联函数不能定义为虚函数
因为内联函数没有地址,而虚表里面存放的就是虚函数的地址。 - 构造函数不能定义为虚函数
因为虚函数是存放在对象的虚表里面,如果将构造函数定义为虚函数,则构造函数也必须存放在虚表里面,但是此时对象都还没有创建也就没有所谓的虚表。
override和overwrite的区别
1)override,派生类覆盖基类的虚函数,实现接口的重用,返回值类型必须相同
特征:不同范围(基类和派生类)、函数名字相同、参数相同、基类中必须有virtual关键字(必须是虚函数)
2)overwrite,派生类屏蔽了其同名的基类函数,返回值类型可以不同
特征:不同范围(基类和派生类)、函数名字相同、参数不同或者参数相同且无virtual关键字
在子类中overwrite直接覆盖掉同名成员函数,对于子类对象来说,父类的同名函数是不可见的,就无法实现父类指针或引用调用子类同名函数了。
值得注意的是,C++中是没有overwrite这个术语的。
参考内容
- 常见C++笔试面试题整理. https://mp.weixin.qq.com/s/hONnHSkR8Qk4doIK0aYHiQ
- C++面试基础题汇总. https://www.cnblogs.com/277223178dudu/p/10750434.html
- 虚函数调用的几种方式. https://www.cnblogs.com/lsgxeva/p/7692567.html
- weak_ptr这个智能指针有什么用. https://segmentfault.com/q/1010000004078858
- 【C++基础之十】友元函数和友元类. https://blog.csdn.net/m0_38126105/article/details/78757872
- 图说C++对象模型:对象内存布局详解. https://www.cnblogs.com/QG-whz/p/4909359.html#_label1