五、面向对象
5.1 面向对象的三大特性
三大特性:封装,继承,多态
- 封装:封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(我们称之为类)。封装的意义在于保护或者防止代码(数据)被我们无意中破坏。
- 继承:继承主要实现重用代码,节省开发时间。子类可以继承父类的一些东西。
- 多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。分为编译时多态和运行时多态。
5.2 函数重载和运算符重载
问:函数重载的依据?
答:
- 参数个数
- 参数类型
- const方法与非const方法构成重载
问:运算符重载的限制?
答:
- 被重载的运算符,至少有一个操作数是用户自定义类型,也就是说不能重载C++语言的标准运算
- 重载的运算符的句法规则不可以改变,操作数、结合性和优先级无法更改。以前是几元现在就是几元;该是左结合还是左结合;优先级无法更改。
- 不能自定义运算符,不能创建新的运算符。
- 不能重载的运算符有:
- 成员访问运算符 .
- 成员指针运算符 .*
- 作用域解析运算符 ::
- 条件运算符 ?:
- sizeof
- typeid
- 四个类型转换运算符
- const_cast
- static_cast
- dynamic_cast
- reinterpret_cast
- 只能通过成员函数重载,而不能通过友元重载的运算符:
- 赋值运算符 =
- 函数调用运算符 ()
- 下标运算符 []
- 通过指针访问成员运算符 ->
- 只能通过友元重载,不能通过成员函数重载的情况:
- 双目运算符最好用友元重载,单目运算符最好用成员函数重载
- 若运算符所需的操作数(尤其是第一个操作数)希望有隐式类型转换,则只能选用友元函数
- 左操作数是不同类的对象或者内部类型,比如ostream, istream, int, float等
- 当需要重载运算符具有可交换性时,选择重载为友元函数
- 对返回类型没有限制,可以是void或者其他类型
- 重载一元运算符需要注意,由于一元运算符没有参数,前缀和后缀无法区分,所以需要加一个哑元(dummy),哑元永远用不上,如果有哑元,则是后缀形式,否则,就是前缀。
5.3 哪些成员无法被继承?
- 无法被继承的有
- 构造函数
- 析构函数
- 赋值运算符
- 友元函数
- 可以被继承的有
- 静态成员
- 静态方法
- 非静态成员
- 非静态方法(无论是private\public\protected,只是private的继承了也无法访问)
- 虚表指针
5.4 定义默认构造函数的两种方法?
- 给已有的构造函数中的一个的所有参数加上默认值
- 通过方法重载定义一个无参数构造函数
注意:
- 隐式调用默认构造函数不要加括号(), 会被编译器解释为函数声明。
5.5 调用非默认构造函数的三种方法?
- Foo f(...); // 隐式调用
- Foo f = Foo(...) ;// 显式调用
- Foo* f = new Foo(); // 显式调用
5.6 由编译器生成的6个成员函数?
注意:对于空类,不会生成任何成员函数,只会生成一个字节的占位符。
- 默认构造函数
- 析构函数
- 复制构造函数
- 赋值运算符
- 取地址运算符
- 取地址运算符 const版本
5.7 友元的三种实现方式
- 友元函数
- 友元类
- 友元成员函数
5.8 为什么基类的析构函数为什么要声明为虚函数?
为了能在多态情况下准确调用派生类的析构函数。如果基类的析构函数非虚函数,则用基类指针或引用引用派生类进行析构时,只会调用基类的析构函数;如果是虚析构函数,则会依次调用派生类的析构和基类的析构。(基类的析构是一定会调用的,无论是否为虚)。
5.9 为什么构造函数不可以是虚函数?
- 虚函数在运行期决定函数调用,而在构造一个对象时,由于对象还未构造成功,编译器无法确定对象的实际类型,继而无法决定调用哪一个构造函数。
- 虚函数的执行依赖于虚函数表,而虚函数表在构造函数中进行初始化工作,即初始化 vptr,让它指向正确的虚函数表,而在构造期间,虚函数表还没有初始化,所以无法决定调用哪个构造函数。
5.10 析构函数什么时候声明为私有?什么时候不能声明为私有?
- 私有析构函数可以使得对象只在堆上构造。在栈上创建的对象要求构造函数和析构函数必须都是公有的,否则编译器报错“析构函数不可访问”;而堆对象由程序员创建和删除,可以把析构函数声明为私有的。由于delete会调用析构函数,而私有的析构无法被访问,编译器报错,此时通过增加一个destroy()方法,在方法内调用析构函数来释放对象:
- void destroy()
- {
- delete this;
- }
- 析构函数不能声明为私有的情况:基类的析构函数不能声明为私有,因为要在派生类的析构函数中被隐式调用。
5.11 构造函数什么时候声明为私有?什么时候不能声明为私有?
- 单例模式。
- 基类的构造函数不能声明为私有,因为要在派生类的构造函数中被隐式调用。如果在派生类的构造函数中没有显式调用基类的构造,则会调用基类的默认构造函数。
5.12 不能声明为虚函数的成员函数
构造函数:
首先明确一点,在编译期间编译器完成了虚表的创建,而虚指针在构造函数期间被初始化。
如果构造函数是虚函数,那必然需要通过虚指针来找到虚构造函数的入口地址,但是这个时候我们还没有把虚指针初始化。因此,构造函数不能是虚函数。
內联函数:
编译期內联函数在调用处被展开,而虚函数在运行时才能被确定具体调用哪个类的虚函数。內联函数体现的是编译期机制,而虚函数体现的是运行期机制。
静态成员函数:
静态成员函数和类有关,即使没有生成一个实例对象,也可以调用类的静态成员函数。而虚函数的调用和虚指针有关,虚指针存在于一个类的实例对象中,如果静态成员函数被声明成虚函数,那么调用成员静态函数时又如何访问虚指针呢。总之可以这么理解,静态成员函数与类有关,而虚函数与类的实例对象有关。
非成员函数:
虚函数的目的是为了实现多态,多态和继承有关。所以声明一个非成员函数为虚函数没有任何意义。
5.13 虚函数机制以及内存分布
http://www.cnblogs.com/freeopen/p/5482965.html 重点看多继承的内存分布。
虚函数机制涉及的指针和表有:
- 虚函数表指针 vfptr和虚函数表 vftable
- 虚继承下还涉及 虚基类表指针 vbptr和虚基类表 vbtable
虚函数的实现过程:
1.编译器为每个含有虚函数的类或者从此类派生的类创建一个虚函数表vftable, 保存此类所有虚函数的地址,并增加一个隐藏成员虚函数表指针vfptr放在所有数据成员之前。在创建类的对象时,在构造函数内部对虚函数表指针进行初始化,指向之前创建的虚函数表。
2. 单继承情况下,派生类会继承基类所有的数据成员和虚函数表指针,并由编译器生成虚函数表,在创建派生类实例时,将虚函数表指针指向新的,属于派生类的虚函数表。
3. 多重继承情况下,会有多个虚函数表,几重继承,就会有几个虚函数表。这些表按照派生的顺序依次排列,如果派生类改写了基类的虚函数,那么就会用派生类自己的虚函数覆盖虚函数表的相应的位置,如果派生类有新的虚函数,那么就添加到第一个虚函数表的末尾。
4. 虚继承情况下,会再创建一个虚基类表和一个虚基类表指针,也就是说,编译器会增加两个指针,一个是虚基类表指针,指向虚基类表,保存了所有继承过来的虚基类在内存中的地址(偏移量);另一个是继承过来的虚函数表指针,保存了虚函数的地址。如果派生类有新的虚函数,那么就再增加一个虚函数表指针,指向一个新的虚函数表,保存了派生类新的虚函数的地址。
5. 虚基类部分会在C++继承层次中只有一份。所有由虚基类派生的类都持有一个虚基类表指针,指向一个虚基类表,表里面保存了所有它继承的虚基类部分的地址。虚基类部分有一个虚函数表指针,指向虚函数表。
5.14 class 与 struct的区别
- class默认的继承方式为private, struct 默认继承方式为public
- class的成员访问默认为private, struct默认为public
5.15 重载、重写(覆盖)与隐藏(重定义)的关系
重载 override
重写(覆盖)override
隐藏 hide
- 重载。函数名相同,参数个数、类型不同,或者用const重载。是同一个类中方法之间的关系,是水平关系。
- 重写。派生类重新定义基类中有相同名称和参数的虚函数,要求参数列表必须相同。方法在基类和派生中的访问限制可以不同。
- 隐藏。派生类重新定义基类中有相同名称的函数(参数列表可以不同)会把其他基类的同名方法隐藏起来,无法被派生类调用。
5.16 哪些情况下方法可以不写定义?
- 纯虚方法
- 非虚方法
所以,非纯虚的虚方法也就是普通的虚方法必须写定义,哪怕是空的,因为要生成虚函数表,没有方法定义就没有方法地址。
5.17 派生类可以不实现虚基类的纯虚方法,派生类也成了抽象类。
5.18 三种继承方式(public, private, protected)的区别?
- 公有继承(public): 基类成员对其对象的可见性与一般类及其对象的可见性相同,public成员可见,protected和private成员不可见,基类成员对派生类的可见性对派生类来说,基类的public和protected成员可见:基类的public成员和protected成员作为派生类的成员时,它们都保持原有状态;基类的private成员依旧是private,派生类不可访问基类中的private成员。 基类成员对派生类对象的可见性对派生类对象来说,基类的public成员是可见的,其他成员是不可见的。 所以,在公有继承时,派生类的对象可以访问基类中的public成员,派生类的成员方法可以访问基类中的public成员和protected成员。
- 私有继承(private) 基类成员对其对象的可见性与一般类及其对象的可见性相同,public成员可见,其他成员不可见,基类成员对派生类的可见性对派生类来说,基类的public成员和protected成员是可见的:基类的public成员和protected成员都作为派生类的private成员,并且不能被这个派生类的子类所访问;基类的私有成员是不可见的:派生类不可访问基类中的private成员,基类成员对派生类对象的可见性对派生类对象来说,基类的所有成员都是不可见的,所以在私有继承时,基类的成员只能由直接派生类访问,无法再往下继承。
- 保护继承(protected) 保护继承与私有继承相似,基类成员对其对象的可见性与一般类及其对象的可见性相同,public成员可见,其他成员不可见,基类成员对派生类的可见性,对派生类来说,基类的public和protected成员是可见的:基类的public成员和protected成员都作为派生类的protected成员,并且不能被这个派生类的子类所访问;基类的private成员是不可见的:派生类不可访问基类中的private成员。基类成员对派生类对象的可见性对派生类对象来说,基类的所有成员都是不可见的。所以,在保护继承时,基类的成员也只能由直接派生类访问,而无法再向下继承。C++支持多重继承。多重继承是一个类从多个基类派生而来的能力。派生类实际上获取了所有基类的特性。当一个类 是两个或多个基类的派生类时,派生类的构造函数必须激活所有基类的构造函数,并把相应的参数传递给它们 。
5.19 如果赋值构造函数参数不是传引用而是传值会有什么问题?
如果不是传引用,会造成栈溢出。因为如果是Foo(Foo f)的形式,实参初始化形参的时候也会调用复制构造函数,造成死循环。所以,复制构造函数一定要传引用:
Foo(Foo& f);
5.20 如何实现只能动态分配类对象,不能定义类对象?
即只能将对象创建于堆上,不能创建于栈上。需要把构造函数和析构函数设为protected,派生类可以访问,外部无法访问。同时创建create和destroy函数,在内部调用构造和析构,用于创建和删除对象。其中create设为static,使用类名访问。
class A{ protected: A(){}; ~A(){}; public: static A* creat(){ return new A(); } void destroy(){ delete this; } }; int main() { A* a = A::creat(); a->destroy(); }
5.21 如何实现只能在栈上创建对象?不能在堆上创建对象?
在堆上创建对象的唯一方法是使用new关键字,所以,只需要禁用new关键字就可以了。将operator new 设为私有的, 外部不可访问。
class A { private: void* operator new(size_t t){} // 注意函数的第一个参数和返回值都是固定的 void operator delete(void* ptr){} // 重载了new就需要重载delete public: A(){} ~A(){} };
5.22 必须在构造函数初始化式里进行初始化的数据成员有哪些?
- 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
- 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
- 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化
5.23 抽象类和接口的区别?
抽象类是包含纯虚函数的类 C++中的接口是指只包含纯虚函数的抽象类,不能被实例化。 一个类可以实现多个接口(多重继承)
5.24 虚基类和虚继承,虚基指针和虚基表
虚基类是使用virtual继承的公共基类。虚继承使得在内存中只有基类成员的一份拷贝。虚继承消除了歧义,如果B,C,继承于A,A中有一个公有成员 i,D继承于B,C,此时D无法访问 i,因为会有歧义,不知道是B还是C的,此时使用虚继承可以解决,让B,C以虚继承方式继承A,这样就消除了歧义。底层实现原理:底层实现原理与编译器相关,一般通过虚基类指针实现,即各对象中只保存一份父类的对象,多继承时通过虚基类指针引用该公共对象,从而避免菱形继承中的二义性问题。
虚基类的初始化与一般多继承的初始化在语法上是一样的,但构造函数的调用次序不同。派生类构造函数的调用次序有三个原则:
(1)虚基类的构造函数在非虚基类之前调用;
(2)若同一层次中包含多个虚基类,这些虚基类的构造函数按它们说明的次序调用;
(3)若虚基类由非虚基类派生而来,则仍先调用基类构造函数,再调用派生类的构造函数。
虚继承的派生类会增加一个隐藏成员虚基指针vbPtr指向虚基表vbTable。
5.25 构造函数和析构函数中可以调用调用虚函数吗?
可以,虚函数底层实现原理(但是最好不要在构造和析构函数中调用) 可以,但是没有动态绑定的效果,父类构造函数中调用的仍然是父类版本的函数,子类中调用的仍然是子类版本的函数。 effictive c++第九条,绝不在构造和析构过程中调用virtual,因为构造函数中的base的虚函数不会下降到derived上。而是直接调用base类的虚函数。
5.26 构造函数和析构函数调用顺序?
- 先调用基类构造函数
- 在调用成员类构造函数
- 最后调用本身的构造函数
- 析构顺序相反
5.27 动态绑定如何实现?
C++ 中,通过基类的引用或指针调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。
5.28 多态性有哪些?
多态指当不同的对象收到相同的消息时,产生不同的动作
- 编译时多态(静态绑定),函数重载,运算符重载,模板。
- 运行时多态(动态绑定),虚函数机制。
5.29 构造函数可不可以抛出异常?析构函数呢?
1. 构造函数中尽量不要抛出异常,能避免的就避免,如果必须,要考虑不要内存泄露!
2. 不要在析构函数中抛出异常!
理论上都可以抛出异常。 但析构函数最好不要抛出异常,将会导致析构不完全,从而有内存泄露。
为什么不应该在析构函数中抛出异常?
1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如内存泄漏的问题。
2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
3)当在某一个析构函数中会有一些可能(哪怕是一点点可能)发生异常时,那么就必须要把这种可能发生的异常完全封装在析构函数内部,决不能让它抛出函数之外(这招简直是绝杀!呵呵!
5.30 成员函数调用底层机制?
例如我们要调用Point的实例 p 的 vec3 normalize() 方法,即 p.normalize();编译器会做下面的转变:
1. 改写函数的原型,增加一个额外的参数 this 指针到参数列表的最前面:
// 如果成员函数是非const函数,则this指针是指针常量
vec3 Point :: normalize( Point* const this);
// 如果成员函数是const函数,则this指针是指向常量的指针常量
vec3 Point :: normalize( const Point* const this);
2. 将函数内部对“非静态成员”的访问,改写为通过this指针访问
{
return sqrt(
this->x * this->x +
this->y * this->y +
this->z * this->z
);
}
3. 将成员函数重写写为一个外部函数,并修改函数名,避免名称和其他函数名冲突:
extern normalize__3PointFv(register const Point* const this);