二、面向对象
1. 多态
(1)多态的实现有哪几种?
黑马程序员C++核心编程第68页
静态多态和动态多态。
静态多态:是通过重载和模板技术实现的,在编译期间确定函数地址;
动态多态:是通过虚函数和继承关系实现的,执行动态绑定,在运行期间确定函数地址。
(2)动态绑定是如何实现的?
当编译器发现类中有虚函数时,会创建一张虚函数表,把虚函数的函数入口地址放在虚函数表中,并且在对象中增加一个指针vptr,用于指向类的虚函数表。当派生类覆盖基类的虚函数时,会将虚函数表中对应的指针进行替换,从而调用派生类中覆盖后的虚函数,从而实现动态绑定。
(3)动态多态有什么作用?有哪些必要条件?
动态多态的作用:
1. 隐藏实现细节,使代码模块化,提高代码的可复用性。
2. 接口重用,使派生类的功能可以被基类的指针/引用所调用,即向后兼容,提高代码的可扩充性和可维护性。
动态多态的必要条件:
1. 需要有继承关系
2. 需要有子类重写父类的虚函数
3. 需要有基(父)类指针/引用指向子类对象
//类中只有一个纯虚函数就称为抽象类
//抽象类无法实例化对象
//子类必须重写父类中的纯虚函数
class Animal{
public:
virtual void speak() = 0; //纯虚函数
/*
virtual void sepak(){ //虚函数
cout << "动物在说话" << endl;
}
*/
};
class Cat : public Animal{
public:
void speak(){
cout << "小猫在说话" << endl;
}
};
class Dog : public Animal{
public:
void speak(){
cout << "小狗在说话" << endl;
}
};
void DoSpeak(Animal& animal){
animal.speak();
}
int main(){
Cat cat;
DoSpeak(cat);
Dog dog;
DoSpeak(dog);
return 0;
}
(4)多继承存在什么问题?如何消除多继承中的二义性?
在继承时,基类之间发生成员同名时,将出现对成员访问的不确定性,即同名二义性;
消除同名二义性的方法:
1. 利用作用域运算符::,用于限定派生类使用的是哪个基类的成员;
2. 在派生类中定义同名成员,覆盖基类中的相关成员;
当派生类从多个基类派生,而这些基类又从同一个基类派生,则在访问此共同基类的成员时,将产生另一种不确定性,即路径二义性;
消除路径二义性的方法:
1. 消除同名二义性的两种方法都可以;
2. 使用虚继承,使得不同路径继承来的同名成员在内存中只有一份拷贝。
class Animal {
public:
int m_Age;
};
//继承前加virtual关键字后,变为虚继承
//此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};
void test01(){
SheepTuo st;
st.Sheep::m_Age = 100;
st.Tuo::m_Age = 200;
cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl; //200
cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl; //200
cout << "st.m_Age = " << st.m_Age << endl; //200
}
int main() {
test01();
return 0;
}
2. 虚函数
(1)纯虚函数有什么作用?如何实现?
定义纯虚函数是为了实现一个接口,起到规范的作用,想要继承这个类就必须重写该函数。
实现方式是在虚函数声明的结尾加上 = 0;
(2)虚函数表是针对类的还是针对对象的?同一个类的两个对象的虚函数表是怎么维护的?
虚函数表是针对类的,类的所有对象共享这个类的虚函数表,因为每个对象内部都保存了一个指向该虚函数表的指针vptr,每个对象的vptr的存放地址都不同,但都指向同一虚函数表。
(3)为什么基类的构造函数不能定义为虚函数?
虚函数的调用依赖于虚函数表,而指向虚函数表的指针vptr需要在构造函数中进行初始化,所以无法调用定义为虚函数的构造函数。
(4)虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用子类的析构代码。
解决方法:将父类中的析构函数改为虚析构或者纯虚析构
虚析构:virtual ~类名(){}
纯虚析构:virtual ~类名() = 0;
(5)为什么基类的析构函数需要定义为虚函数?
为什么析构函数一般写成虚函数-帅地玩编程 (iamshuaidi.com)
由于类的多态性,基类指针可以指向派生类的对象,如果删除该基类的指针,就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。
如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。
class Parent{
public:
Parent(){
cout << "调用父类构造函数" << endl;
}
~Parent(){
cout << "调用父类析构函数" << endl;
}
};
class Son : public Parent{
public:
Son(){
cout << "调用子类构造函数" << endl;
}
~Son(){
cout << "调用子类析构函数" << endl;
}
};
int main(){
Parent* p = new Son();
delete p;
p = NULL;
system("pause");
return 0;
}
/*
输出:
调用父类构造函数
调用子类构造函数
调用父类析构函数
*/
将父类析构函数声明为虚函数:
class Parent{
public:
Parent(){
cout << "调用父类构造函数" << endl;
}
~Parent(){
cout << "调用父类析构函数" << endl;
}
};
class Son : public Parent{
public:
Son(){
cout << "调用子类构造函数" << endl;
}
virtual ~Son(){
cout << "调用子类析构函数" << endl;
}
};
int main(){
Parent* p = new Son();
delete p;
p = NULL;
system("pause");
return 0;
}
/*
输出:
调用父类构造函数
调用子类构造函数
调用子类析构函数
调用父类析构函数
*/
(6)如何让一个类不能实例化?
将类定义为抽象类(也就是存在纯虚函数),或者将构造函数声明为private
构造函数和析构函数能抛出异常吗?
* 从语法的角度来说,构造函数可以抛出异常,但从逻辑和风险控制的角度来说,尽量不要抛出异常,否则可能导致内存泄漏。
* 析构函数不可以抛出异常,如果析构函数抛出异常,则异常点之后的程序,比如释放内存等操作,就不会被执行,从而造成内存泄漏的问题;而且当异常发生时,C++通常会调用对象的析构函数来释放资源,如果此时析构函数也抛出异常,即前一个异常未处理又出现了新的异常,从而造成程序崩溃的问题。
3. 重写、重载
(1)覆盖和重载之间有什么区别?
覆盖是指派生类中重新定义函数,其函数名、参数列表、返回类型与父类完全相同,只是函数体存在区别;覆盖只发生在类的成员函数中。
重载是指两个函数具有相同的函数名,不同的参数列表,不关心返回值;当调用函数时,根据传递的参数列表来判断调用哪个函数;重载可以是类的成员函数(构造函数重载),也可以是普通函数。
(2)简述类成员函数的重写、重载和隐藏的区别
1. 重写是指子类重写父类的方法,发生在两个类里面;重载发生在一个类里面(如构造函数的重载)。
2. 重写函数的参数列表相同;重载函数的参数列表不同。
3. 虽然重载和重写都是实现多态的基础,但是重载是静态绑定的多态,重写(覆盖)是动态绑定。
4. 其他
(1)面向对象三大特性?
1. 封装:将客观事物封装成抽象的类,而类可以把自己的数据和方法暴露给可信的类或者对象,对不可信的类或对象则进行信息隐藏。
2. 继承:可以使用现有类的所有功能,并且无需重新编写原来的类即可对功能进行扩展
3. 多态:一个类实际的相同方法在不同情形下有不同的表现形式,使不同内部结构的对象可以共享相同的外部接口。
(2)C++中类成员的访问权限
public(共有的):能被类成员函数、子类函数、友元访问,也能被类的对象访问。
protected(受保护的):只能被类成员函数、子类函数及友元访问,不能被其他任何访问,本身的类对象也不行。
private(私有的):只能被类成员函数及友元访问,不能被其他任何访问,本身的类对象也不行。
(3)如果类A是一个空类,那么sizeof(A)的值为多少?
sizeof(A)
的值为1,因为编译器需要区分这个空类的不同实例,分配一个字节,可以使这个空类的不同实例拥有独一无二的地址。
(4)C++的空类有哪些成员函数?
缺省构造函数
缺省拷贝构造函数
省析构函数
赋值运算符
取址运算符
取址运算符const
「注意」:有些书上只是简单的介绍了前四个函数。没有提及后面这两个函数。但后面这两个函数也是空类的默认函数。另外需要注意的是,只有当实际使用这些函数的时候,编译器才会去定义它们。
(5)拷贝构造函数和赋值运算符重载之间有什么区别?
(6)说说C++的四种强制类型转换运算符
1. reinterpret_cast
reinterpret_cast<type>(expression)
type必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以用于类型之间进行强制转换。
2. const_cast
const_cast(expression)
该运算符用来修饰类型的const或volatile属性。除了const或volatile修饰之外, type_id和expression的类型是一样的。用法如下:
* 常量指针被转化成非常量的指针,并且仍然指向原来的对象
* 常量引用被转换成非常量的引用,并且仍然指向原来的对象
* const_cast一般用于修改底层const。如const char *p形式
3. static_cast
static<type>(expression)
该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法: * 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换
1. 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的
2. 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的
* 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
* 把空指针转换成目标类型的空指针
* 把任何类型的表达式转换成void类型
注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性。
4. dynamic_cast
dynamic_cast (expression)
有类型检查,基类向派生类转换比较安全,但是派生类向基类转换则不太安全
说说C++的四种强制类型转换运算符-帅地玩编程 (iamshuaidi.com)
类型转换分为哪几种?各自有什么样的特点?-帅地玩编程 (iamshuaidi.com)
说一说c++中四种cast转换-帅地玩编程 (iamshuaidi.com)
(7)RTTI是什么?其原理是什么?
(8)模板函数和模板类的特例化
模板函数和模板类的特例化-帅地玩编程 (iamshuaidi.com)