1、继承
所谓继承,指的是在已有类的基础上创建新的类型,新的类型会继承(获取)原有类所有的特征(扩展已有类型)
继承能够实现代码复用、减少代码冗余、提高开发效率。
继承主要用于描述那些非常相似 但有细微差别的类型之间的关系(is-a的关系)。
例:
计算机
台式机、笔记本、平板、手机
学生
大学生、中学生、小学生
车
公交车、洒水车、小汽车、电动车
用于描述共性的类型,称为基础类(Base Class),简称基类,也叫父类,在基类的基础上创建出来的新类型,称为派生类(Derived Class),也叫子类
C++中继承语法如下:
class 派生类名: 继承方式 基类名1, ..., 继承方式 基类名n
{
派生类新增加的成员
};
根据基类的个数,继承可分为:
单(一)继承:只有一个基类
多(重)继承:两个或两个以上的基类
继承方式:决定了基类中的成员,在派生类内部和外部的访问权限,分三种:
public: 共有继承
private: 私有继承
protected: 保护继承
说明:
继承方式可省略,如果省略则:
class默认的继承方式为private
struct的默认继承方式为public
继承方式说明:
public公有继承
基类的公有成员,通过公有继承,成为派生类的公有成员(即在派生类内部和外部都可直接访问)
基类的私有成员,通过公有继承,成为派生类的不可访问的成员(即在派生类内部和外部都不可直接访问)
基类的保护成员,通过公有继承,成为派生类的保护成员(即在派生类内部可直接访问,外部不能直接访问,友元可直接访问、后代可直接访问)
private私有继承
基类的公有成员,通过私有继承,成为派生类的私有成员(即在派生类内部可直接访问,外部不能直接访问,友元可直接访问)
基类的私有成员,通过私有继承,成为派生类的不可访问的成员(即在派生类内部和外部都不可直接访问)
基类的保护成员,通过私有继承,成为派生类的私有成员(即在派生类内部可直接访问,外部不能直接访问,友元可直接访问)
protected保护继承
基类的公有成员,通过保护继承,成为派生类的保护成员(即在派生类内部可直接访问,外部不能直接访问,友元可直接访问、后代可直接访问)
基类的私有成员,通过保护继承,成为派生类的不可访问的成员(即在派生类内部和外部都不可直接访问)
基类的保护成员,通过保护继承,成为派生类的保护成员(即在派生类内部可直接访问,外部不能直接访问,友元可直接访问、后代可直接访问)
2、继承中的构造与析构
构造函数与析构函数 是不能继承的
如果基类有默认构造函数,当实例化派生类对象时,会先自动调用基类的构造函数,在调用派生类自己的构造函数
如果基类没有默认构造函数,或需要调用基类其他带参数的构造函数,则:
派生类必须定义构造函数,且在构造函数的初始化列表中显示的调用基类的构造函数才能初始化从基类继承来的成员
例:
class Derived: public Base
{
public:
Derived(int a, int b, int c): Base(a,b,c)
{
cout << "Derived(int,int,int)" << endl;
}
};
问:派生类如果增加了成员对象,则成员对象的构造函数与析构函数在什么时候调用?
说明:
当实例化一个派生类对象时,可能会涉及三种构造函数与析构函数:
基类的构造函数、成员函数的构造函数、派生类的构造函数
析构函数的调用与此相反
与对象的资源控制相关的函数,都不继承
3、继承中的同名成员
派生类可以在基类的基础上增加新的成员,这些新的成员可以与基类的成员同名(因为作用域不同)。
如果派生类中定义的函数与基类中的某个函数同名,会出现函数隐藏的效果。
函数隐藏:
当派生类对象调用这种同名函数时,默认调用的是派生类新增加的那个函数
而基类的同名函数被隐藏了。
函数隐藏只与函数名相关,与返回类型及参数列表无关。
如果需要显示的调用那些被隐藏的成员,只需要指名作用域既可。
例:
class Base
{
public:
void print() const
{
cout << "x = " << this->_x << endl;
}
};
class Derived: public Base
{
public:
int print(int y=1) const // 与基类函数同名
{
cout << "y = " << _y << endl;
return 0;
}
};
Derived d;
d.print(); // 调用的派生类新增加的print函数
d.Base::print();// 通过指定作用域调用基类的同名函数
4.is-a关系具体表现
公有派生类与他的基类,存在着“是一个”(is-a)的关系
在程序中,具体表现如下:
1、可以用公有派生类对象初始化基类对象
Derived d1;
Base b1=d1;
2、可以用公有派生类对象给基类对象赋值
b1=d1l
3、基类指针可以指向任意的公有派生类对象
Base *p=&d1;
4、基类引用可以绑定到任意的公有派生类对象
Base &r=d1;
问:
基类指针可以指向公有派生类对象,能不能通过指针与访问派生类新增加的成员?
答:
通过基类指针, 无法直接访问派生类新增加的成员
如果确实需要访问派生类增加的成员,那么使用static_cast做类型转换即可(把基类指针转成派生类指针)
5、多重继承
所谓多重继承,只一个派生类有两个或两个以上基类的情况,派生类会继承他所有基类的特征。
例:
class Base1 {};
class Base2 {};
class Derived: public Base1, public Base2 {}
多重继承中的二义性问题:
例:
class Base1 {
public:
void foo() {}
};
class Base2 {
void foo() {}
};
class Derived: public Base1, public Base2 {};
Derived d;
d.foo(); // 编译错误
解决方案:
1、指定作用域
d.Base1::foo();
2、隐藏基类的同名函数
多重继承中“菱形继承”(“钻石继承”)
例:
class A {};
class B: public A {};
class C: public A {};
class D: public B, public C {};
菱形继承中存在的问题:
最顶层A类中的成员,通过多条继承路径,达到最底层的D类中时,会出现多分拷贝且被初始化多次的情况
解决方案:使用虚继承
在菱形继承中,B类和C类在继承A类时,在继承方式的前面(或后面)加上一个关键字virtual,这种继承方式就被称为虚继承,能保证虚继承A中的成员在派生类D中只保留一份(B和C共享A的成员),且只初始化一次(由D类调用A的构造函数)
例:
class A {}; // 虚基类
class B: virtual public A {}; // 虚继承
class C: virtual public A {}; // 虚继承
class D: public B, public C {};
说明:
虚继承会付出一定的代价:只要使用了虚继承,该类的对象就会多出一个指针(虚基类指针)
在计算D类对象大小时,需要考虑这些自动生成的虚指针及字节对齐。
6、多态
多态:指不同对象,收到相同的消息,产生不同的行为。
说明:
不同的对象,一般是只同一类族中的不同类型的对象
相同的消息,指调用这些不同对象的相同的成员函数
不同的行为,指这些相同的函数定义不同。
例:
class A {
virtual void foo();
};
class B : public A {};
class C : public A {};
void bar(A* p)
{
p->foo(); // 如果p指向不同的对象,就输出不同的数据,这种效果就是多态
}
bar(new B);
bar(new C);
多态能实现接口的重用,能通过统一的接口处理不同类型的对象
多态能在一定程度上忽略那些相似类型之间的差异,已同意的方式进行处理
C++中的多态一般分两种:
编译时多态(静多态、静态联编、静态绑定)
当调用某个函数时,在编译阶段就能确定具体要调用的是哪个函数
实现方式:函数重载、运算符重载
运行时多态(动态多态、动态联编、动态绑定)
当调用某个函数时,在编译阶段无法确定具体要调用的是哪个函数,只有在程序运行时,才能确定下来;
实现方式:虚函数
7、虚函数
用关键字virtual说明的函数,称为虚函数
一般格式:
class 类名
{
public:
virtual 返回类型 函数名(参数列表); // virtual 只需要在声明时添加即可。
};
如果一个函数被说明为虚函数,则表示允许(希望)他的派生类重新定义该函数。
如果派生类需要重新定义基类的某个虚函数,则要求这个重新定义的函数原型(返回类型、函数名、参数列表)与基类的虚函数原型完全一致。(注:只读成员函数后面的那个const也是参数列表的一部分)
这种派生类重新定义的函数,即使不加关键字virtual,他自动的就是虚函数。
这种在派生类中重新定义基类某个虚函数的行为,称为函数重写(或函数覆盖)
当基类指针或基类引用调用这种虚函数时,就会发生动态绑定
例:
class A {
virtual void foo(){}
};
class B : public A {
void foo(){}
};
class C : public A {
void foo(){}
};
void bar(A* p)
{
p->foo(); // 如果p指向B类对象,则调用B中的foo函数,如果p指向C类对象,则调用C中的foo函数,这就是动态绑定
}
bar(new B);
bar(new C);
8、纯虚函数与抽象类
在设计基类时,有些虚函数仅表示该类型应该具有某种功能,但该功能的具体实现应该由不同的派生类来完。
这种函数,一般会设计成纯虚函数。
例:virtual 返回类型 函数名(参数列表)=0;
纯虚函数用于规范接口,而不是提供具体实现。
如果一个类中包含纯虚函数,则这种类型,称为抽象类
抽象类无法实例化对象。
9、关键字overried与final(c++11)
overried用于说明派生类中的某个函数是虚函数,且时重写(或覆盖)了基类的纯虚函数
同时,编译器会对这种函数进行语法检查,如果不满足重写的规则,会给出错误提示
例:
class Base
{
public:
virtual void foo() {}
};
class Derived: public Base
{
public:
void foo() overried {}
};
final可以修饰虚函数,也可以修饰类型
修饰虚函数时,表示该函数不能被重写
修饰类型时,表示该类型不能继承
10、虚函数中的默认值
一般的虚函数可以重载,可以设置默认值。
如果基类的虚函数设置了默认值,派生类重写的虚函数可以不设置默认值,也可以设置不一样的默认值。
当基类指针或引用调用这种虚函数时,发生动态绑定,使用的是基类设置的默认值。
例:
class Base
{
public:
virtual void foo(int x = 1)
{
cout << "Base.x = " << x << endl;
}
};
class Derived: public Base
{
public:
void foo(int x=2) override // 派生类重写的虚函数设置了不一样的默认值
{
cout << "Derived.x = " << x << endl;
}
};
void bar(Base* p)
{
p->foo(); // 不管p指向的是基类对象还是派生类对象,默认值在编译器阶段确定,都是1
}
11、虚析构函数
有些函数是不能用virtual说明的,比如,全局函数、友元函数、静态函数、构造函数。
但析构函数可以。
正常情况下,派生类对象销毁时,会依次调用派生类的析构函数、成员对象的析构函数、基类的析构函数。
但,如果是用基类指针指向new出来的派生类对象,当用delete释放这种对象时
默认情况下,只会调用基类的析构函数,而不会调用其他的析构函数,从而可能造成资源泄漏。
把基类的析构函数说明为虚函数,然后他所有派生类的析构函数自动称为虚函数。
当用delete释放这种由基类指针指向的动态对象时,就不会再造成资源泄漏。
说明:
如果是基类,则起析构函数应该说明为虚函数。
问题:
在构造函数与析构函数中,如果调用虚函数,会不会发生动态绑定?
在构造函数与析构函数中调用 虚函数,不会发生动态绑定。
class Base
{
public:
Base()
{
foo();
}
virtual void foo() {}
};
class Derived: public Base
{
public:
Derived()
{
foo();
}
void foo() override {}
};
Base* p = new Derived;
12、动态类型转换dynamic_cast
dynamic_cast用于多态类型之间的向下类型转换
说明:
多态类型:包含有虚函数的类型
向下类型转换:把基类指针或引用转换成派生类指针或引用
dynamic_cast在类型转换时,会进行运行时类型识别,如果类型一致,则返回对象的地址,如果类型不一致,则转换失败
如果是指针,失败时,返回空指针nullptr
如果是引用,失败时,抛出异常。
所以在使用dynamic_cast时,需要判断其是否转换成功。
例:
class Base {};
class Derived1: public Base {};
class Derived2: public Base {};
Base *p = new Derived1;
Derived1 * p1 = dynamic_cast<Derived1*>(p);
if (p1 != nullptr)
{
// 转换成功,就可以通过p1访问派生类新增加的成员
}
Derived2 * p2 = dynamic_cast<Derived2*>(p); // 转换失败,返回nullptr
static_cast和dynamic_cast的主要区别:
static_cast在编译时进行类型转换,而dynamic_cast在运行时进行类型转换,并提供了类型安全性检查。
这两种转换操作符各有其特定的用途和限制,选择使用哪一种取决于转换的需求和类型安全性要求。
static_cast
定义与用途:static_cast主要用于 基本数据类型之间的转换、隐式转换的显式化、以及 向上转型 (子类指针或引用转为父类指针或引用)。
它进行的是编译时的类型转换, 只能用于已知的类型之间的转换 ,且不能转换掉const、volatile等属性。
限制:static_cast 不能 进行运行时类型检查,因此在使用时需要特别注意类型安全和转换结果的判断。
示例:将整数转换为浮点数、将派生类对象指针转换为基类指针等。
dynamic_cast
定义与用途:dynamic_cast用于基类和派生类之间的类型转换,特别是 向下转型 (父类指针或引用转为子类指针或引用)。
它进行的是运行时的类型转换,可以在 基类和派生类之间 进行类型转换,并且能够检查类型是否符合转换。
限制:dynamic_cast 只能在 多态类型中使用 ,即类至少具有一个虚拟方法。如果转换失败,则返回空指针或抛出std::bad_cast异常。
示例:将基类指针转换为派生类指针,如果转换失败则返回空指针。
选择使用哪种转换
静态转换(static_cast):适用于已知类型的静态转换,当类型转换是安全的且不需要运行时检查时使用。
动态转换(dynamic_cast):适用于未知类型的动态转换,特别是当需要进行类型安全检查时,如基类和派生类之间的转换。
通过上述分析,可以看出static_cast和dynamic_cast各有其适用场景和限制。选择使用哪一种转换取决于具体的转换需求和类型安全性要求。
在编写涉及多态类型的代码时,应优先考虑使用dynamic_cast以确保类型安全;而在进行基本数据类型之间的简单转换时,static_cast则更为合适。
13、虚函数的调用过程
如果一个类中包含有虚函数,编译器在编译时,会为该类型维护一个虚函数表(vtable,可以理解为一个数组)。
虚函数表中记录了该类中所有虚函数的地址
在编译基础指针或引用调用虚函数的代码时,会把虚函数的调用转换成在虚函数表中该虚函数所对应的索引(下标)。
例:
class Base
{
public:
virtual void foo() {}
virtual void bar() {}
};
编译器自动创建一个虚函数表:
该表中第一个元素就是foo函数的地址,索引下标为0
该表中第二个元素就是bar函数的地址,索引下标为1
void f(Base* p)
{
p->foo(); // 编译时,此处记录的不是foo函数的地址,而是它在虚函数表中的索引下标
}
当实例化一个对象时,编译器会自动把该类型的虚函数表中的地址(vptr)插入到对象的储存空间中(一般插入到最前面)
当基类指针或引用,执行虚函数的调用时:
先从对象空间中找到虚指针(虚函数表的地址)
然后根据编译时记录下来的索引下标,从虚函数表中获取虚函数的地址(即函数指针)
最后通过函数指针调用虚函数。
当一个类继承这种带有虚函数的类时,派生类也会生成一个虚函数表,该表中一次存储的是:
1、从基类继承来的虚函数的地址
2.派生类增加的虚函数的地址
注:如果派生类重写了基类的某个虚函数,则会覆盖虚函数表中基类虚函数的地址
例:
class Derived: public Base
{
public:
void bar() override {}
virtual void show() {}
};
派生类虚函数表中存储的是:
&Base::foo
&Derived::bar
&Derived::show