代码复用是编程语言设计的核心,对于一个函数,其实就是函数级别的代码复用,对于一个类,代码复用的方式有两种:继承和组合,继承是一种is-a的关系,而组合是一种a part of的关系。
继承可以理解为一个类从另一个类获取成员变量和成员函数的过程。
成员变量继承一份给自己,成员函数和父类共用。
被继承的类称为基类或父类,继承的类称为派生类或子类。
继承和派生是一个概念,只是站的角度不同。
派生类除了拥有基类的成员,还可以定义新的成员,以增强其功能。
语法:
class 派生类名:[继承方式]基类名
{
派生类新增加的成员
};
使用继承的场景:
- 如果新创建的类与现有的类相似,只是多出若干成员变量或成员函数时,可以使用继承。
- 当需要创建多个类时,如果它们拥有很多相似的成员变量或成员函数,可以将这些类共同的成员提取出来,定义为基类,然后从基类继承。
类成员的访问权限由高到低依次为:public --> protected -->private,public成员在类外可以访问,private成员只能在类的成员函数中访问。
如果不考虑继承关系,protected成员和private成员一样,类外不能访问。但是,当存在继承关系时,protected和private就不一样了。基类中的protected成员可以在派生类中访问,而基类中的
private成员不能在派生类中访问。
继承方式有三种:public(公有的)、protected(受保护的)和private(私有的)。它是可选的,继承方式如果不写,对于class默认为private,对于struct,默认为public。不同的继承方式决定了在派生类中成员函数中访问基类成员的权限。如图表示了继承到派生类中成员变量的访问权限
继承规则::
例如,当继承方式为protected时,那么基类成员在派生类中的访问权限最高也为protected,高于protected的会降级为protected,但低于protected不会升级。再如,当继承方式为public时,那么基类成员在派生类中的访问权限将保持不变。也就是说,继承方式中的public、protected、private是用来指明基类成员在派生类中的最高访问权限的。
注意:不能使用不代表没有继承,派生类是完完整整的继承了基类的所有成员的,其内存结构如下:
基类是作为一个对象完整继承在派生类中的,即使派生类对其不可访问
3.如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为public 或protected;只有那些不希望在派生类中使用的成员才声明为private。
4.如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。
C++98:把类的构造函数私有化。
C++11:在类定义时在类名后添加关键字:final
如果基类的拷贝构造、赋值重载这些函数特殊(深拷贝),那么派生类也需要写特定的函数对应,而且对于基类的成员部分初始化要显示调用基类的成员函数
#include // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A { // 基类
public:
int m_a;
private:
int m_b;
public:
A() : m_a(0) , m_b(0) // 基类的默认构造函数。
{
cout << "调用了基类的默认构造函数A()。\n";
}
A(int a,int b) : m_a(a) , m_b(b) // 基类有两个参数的构造函数。
{
cout << "调用了基类的构造函数A(int a,int b)。\n";
}
A(const A &a) : m_a(a.m_a+1) , m_b(a.m_b+1) // 基类的拷贝构造函数。
{
cout << "调用了基类的拷贝构造函数A(const A &a)。\n";
}
// 显示基类A全部的成员。
void showA() { cout << "m_a=" << m_a << ",m_b=" << m_b << endl; }
};
class B :public A // 派生类
{
public:
int m_c;
B() : m_c(0) , A() // 派生类的默认构造函数,指明用基类的默认构造函数(不指明也无所谓)。
{
cout << "调用了派生类的默认构造函数B()。\n";
}
B(int a, int b, int c) : A(a, b), m_c(c) // 指明用基类的有两个参数的构造函数。
{
cout << "调用了派生类的构造函数B(int a,int b,int c)。\n";
}
B(const A& a, int c) :A(a), m_c(c) // 指明用基类的拷贝构造函数。
{
cout << "调用了派生类的构造函数B(const A &a,int c) 。\n";
}
// 显示派生类B全部的成员。
void showB() { cout << "m_c=" << m_c << endl << endl; }
};
int main()
{
B b1; // 将调用基类默认的构造函数。
b1.showA(); b1.showB();
B b2(1, 2, 3); // 将调用基类有两个参数的构造函数。
b2.showA(); b2.showB();
A a(10, 20); // 创建基类对象。
B b3(a, 30); // 将调用基类的拷贝造函数。
b3.showA(); b3.showB();
}
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片
或者切割。寓意把派生类中父类那部分切来赋值过去。基类对象不能赋值给派生类对象。(如果赋值了,那么派生类多出来的成员变量怎么办?)
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected :
string _name = "小李子"; // 姓名
int _num = 111; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout<<" 姓名:"<<_name<< endl;
cout<<" 身份证号:"<<Person::_num<< endl;
cout<<" 学号:"<<_num<<endl;
}
protected:
int _num = 999; // 学号
};
void Test()
{
Student s1;
s1.Print();
};
C++中的继承有多种形态,单继承,多继承,菱形继承,其中菱形继承是多继承的特殊形态
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
在Assistant的对象中Person成员会有两份。
针对此问题,C++的设计者通过引入虚继承的概念来解决,如上面的继承关系,在Student和
Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地 方去使用,没有意义
class Person
{
public :
string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
Assistant a ;
a._name = "peter";
}
虚拟继承解决数据冗余和二义性的原理 为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。
class A
{
public:
int _a;
};
// class B : public A
class B : virtual public A
{
public:
int _b;
};
// class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
通过内存窗口和对成员变量的赋值,可以看到各个成员变量和对象的内存位置:
蓝色框的是B类、红色框的是C类,绿色框的是A类;可以看到,通过使用虚继承,会把造成数据冗余的类抽出来,这样就没有了数据冗余和二义性问题了。但是,抽出来单独存放后,怎么找到A类呢?还有B、C类中里面除了成员变量,存的那玩意是啥呢?(e0 7b d1 00 …)
B和C虚继承后,不再对象内存储A,里面反而多了个指针,这个指针指向指向的一张表。这张表记录了虚继承的基类成员的偏移量。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量 可以找到下面的A。
继承:类继承允许你根据其他类的实现来定义一个类的实现。这种通过生成子类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,父类的内部细节对子类可见。继承的操作权限更大,一定程度的破坏了父类的封装性,且父子类之间的耦合度较高。
组合:对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。各个类的封装性得以保持,耦合度低。
继承和组合各有优缺点。类继承是在编译时刻静态定义的,且可直接使用,因为程序设计语言直接支持类继承。类继承可以较方便地改变被复用的实现。当一个子类重定义一些而不是全部操作时,它也能影响它所继承的操作,只要在这些操作中调用了被重定义的操作。
但是类继承也有一些不足之处。首先,因为继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现。更糟的是,父类通常至少定义了部分子类的具体表示。因为继承对子类揭示了其父类的实现细节,所以继承常被认为“破坏了封装性” 。子类中的实现与它的父类有如此紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。当你需要复用子类时,实现上的依赖性就会产生一些问题。如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。一个可用的解决方法就是只继承抽象类,因为抽象类通常提供较少的实现。
对象组合是通过获得对其他对象的引用而在运行时刻动态定义的。组合要求对象遵守彼此的接口约定,进而要求更仔细地定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用。这还会产生良好的结果:因为对象只能通过接口访问,所以我们并不破坏封装性;只要类型一致,运行时刻还可以用一个对象来替代另一个对象;更进一步,因为对象的实现是基于接口写的,所以实现上存在较少的依赖关系。
对象组合对系统设计还有另一个作用,即优先使用对象组合有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。另一方面,基于对象组合的设计会有更多的对象 (而有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中。
这导出了我们的面向对象设计的一个原则:
两者关系在is-a和a part of都相差无几的情况下,优先使用对象组合,而不是类继承。