1.继承的概念以及定义
1.1继承的基础概念
继承是类设计层次代码复用的重要手段。使得我们能够在原有类的基础上进行扩展、增加功能,从而产生一个新类,并且这两个类还有一定的联系。原有类通常称为基类(父类),新类通常称为派生类(子类)。
1.2继承的定义
class Person //此类作为基类
{
public:
void print()
{
cout << "name:" << _name << endl;
}
protected:
string _name;
};
class Student : public Person //Student类由Person类继承而来
{
public:
void print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
int _age = 0;
};
派生列表可以有多个基类,中间用逗号分隔。
1.3继承方式和访问限定符
派生类可以继承基类的所有成员,但派生类不能访问基类的private成员。如果我们想使基类的某些成员不能在类外被访问,但是可以在派生类内被访问,那么可以使用protected访问限定符修饰这些成员。
继承方式将会影响基类的成员在派生类中的访问限定符:
- 如果是public继承,那么基类的成员在派生类中保持原有的访问限定符。
- 如果是protected继承,那么基类的public成员在派生类将会变成被protected访问限定符修饰,protected和private成员在派生类中的访问限定符不变。
- 如果是private继承,那么基类的所有成员在派生类中的访问限定符都是private。
在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强。
继承方式不一定非要显示地写出来。如果派生类的关键字为class,那么其继承方式默认为private;关键字为struct时,其继承方式为public。
2.派生类向基类的直接赋值
在继承关系中,派生类的对象可以直接赋值给基类的对象、指针或引用。更确切的说,派生类的对象可以直接赋值给基类的指针或引用,可以直接赋值给基类的对象的原因在于:调用了基类的拷贝构造或赋值运算符重载(这些函数的参数都是基类的引用或指针)。
但是基类对象不能赋值给派生类对象。
这种转换方式我们称之为切片。如下图所示:
class Person //此类作为基类
{
public:
void print()
{
cout << "name:" << _name << endl;
}
protected:
string _name;
};
class Student : public Person //Student类由Person类继承而来
{
public:
void print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
int _age = 0;
};
int main()
{
Student s;
Person p = s; //拷贝构造
Person p2;
p2 = s; //赋值运算符重载
Person* ptr = &s;
Person& rp = s;
Student* pptr = &p2; //错误,基类对象不能赋值给派生类对象
return 0;
}
3.继承中的作用域
在继承体系中基类和派生类都是互不干扰的独立作用域(一个类构成一个类域)。
如果基类和派生类中有同名成员,派生类将屏蔽对基类成员的直接访问(可以通过指定作用域访问基类成员),这些成员的相互关系为隐藏(重定义)。如果成员是成员函数,函数名相同就构成隐藏。
注意在实际中在继承体系里面最好不要定义同名的成员,容易混淆。
// 基类的_num和派生类的_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; //直接访问的是派生类的_num
}
protected:
int _num = 999; // 学号
};
void Test()
{
Student s1;
s1.Print();
};
// 基类的fun函数和派生类的fun函数构成隐藏关系
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)->" << i << endl;
}
};
int main()
{
B b;
b.fun(1); //直接访问的是派生类的fun函数
b.A::fun();
return 0;
}
4.派生类的默认成员函数
对于普通类来说,默认成员函数无非就是处理内置类型和自定义类型。
但是对于派生类来说,其成员就增加了基类的成员。所以对域派生类的默认成员函数来说,他们需要处理属于基类成员的部分、内置类型和自定义类型。而处理基类成员的方式就是调用基类的成员函数。
那么在设计类时,我们需要遵守以下规则:
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果是显示地定义构造函数,那么就必须在初始化列表显示地调用基类的构造函数。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的那一部分成员拷贝初始化。
- 派生类的operator=必须要调用基类的operator=完成基类的那一部分成员的复制。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。也就是在设计析构函数时,不需要显式调用基类的析构函数,我们只需要处理派生类的成员即可。
- 派生类对象初始化先调用基类构造再调派生类构造。
- 派生类对象析构先调用派生类析构再调基类的析构(所以我们不能在派生类的析构中调用基类的析构含函数,因为有可能我们会使清理成员的顺序发生改变)。
- 由于多态的原因,基类和派生类的析构函数互为隐藏关系。其原因在于编译器会将基类和派生类的析构函数处理成同名的destructor()函数。
class Person
{
public:
Person(const string& name = "")
:_name(name)
{}
Person(const Person& p)
{
_name = p._name;
}
Person& operator=(const Person& p)
{
_name = p._name;
return *this;
}
~Person()
{
cout << "Person::~Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student(const string& name = "",int age = 0)
:Person(name),_age(age) //基类成员通过调用基类的构造函数完成初始化
{}
Student(const Student& s)
:Person(s) //基类的引用仅能看到派生类对象的基类部分
{
_age = s._age;
}
Student& operator=(const Student& s)
{
Person::operator=(s);
_age = s._age;
return *this;
}
~Student()
{
//只需要清理派生类的成员即可
cout << "Student::~Student" << endl;
}//派生类的析构函数调用结束后会自动调用基类的析构函数
protected:
int _age = 0;
};
5.继承与友元
友元关系不能被继承。
class Student;
class Person
{
public:
//基类的友元
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;//友元关系不能被继承,所以不能访问派生类的protected/private成员
}
6.继承与静态成员
我们知道静态成员不属于具体的某个对象,而是属于整个类。因此,在整个继承体系中,静态成员永远只有一个。
class Person
{
public:
static int _count;
};
int Person::_count = 0;
class Student : public Person
{
public:
int _num;
};
int main()
{
// 静态成员在整个继承体系中只有一个
Person::_count = 3;
cout << Student::_count << endl;
return 0;
}
7.多继承与菱形继承
一个派生类有且仅有一个直接基类,这样的方式称为单继承。
一个派生类有两个或两个以上的直接基类,这样的方式称为多继承。
菱形继承是多继承的一种特殊情况:
菱形继承存在的问题在于:数据冗余和二义性。如上述的模型中,最后的Assistant对象中将会有两份Person成员。
下面以一份简化的代码来研究菱形继承。
class A
{
public:
int _a = 1;
};
class B : public A
{
public:
int _b = 2;
};
class C : public A
{
public:
int _c = 3;
};
class D : public B,public C
{
public:
int _d = 4;
};
int main()
{
D d;
return 0;
}
通过调试窗口可以发现,d对象中已经包含两份A类中的成员。
如果d对象直接访问A类的成员,将会引发二义性:
int main()
{
D d;
d._a = 3; //直接访问将会引发二义性
d.B::_a = 4;
d.C::_a = 6;
return 0;
}
解决方式为在菱形继承的腰部使用virtual关键字进行虚继承。
class A
{
public:
int _a = 1;
};
class B : virtual public A
{
public:
int _b = 2;
};
class C : virtual public A
{
public:
int _c = 3;
};
class D : public B,public C
{
public:
int _d = 4;
};
7.1虚继承剖析
先来观察未使用虚继承之前的内存情况(32位):
再观察使用虚继承后的内存情况(32位):
可以看到,使用虚继承后,A类的成员在D类对象中只存在一份了。这就是虚继承为什么能够解决数据冗余与二义性的原因。
B对象(蓝色框)和C对象(绿色框)的首地址是一个指针,它指向了虚基表,虚基表存放的是此指针距A类成员的距离(偏移量)。
使用虚基表的作用在于:当使用B类或C类的指针、引用时,绑定的对象可能是D类对象,但是其指针或引用只能看到D类对象的B类或C类部分,所以需要一个虚基表存储A类成员的偏移量,以供指针计算。
使用虚继承后,B类和C类的对象模型如下图:
所以,对于B类或C类的指针、引用指向自己的对象时,同样适应上述规则。
那么D类对象模型可以是这样的:
8.继承和组合
继承的语法上面介绍过了,组合思想我们早已使用过:
class X
{
public:
};
class Y
{
public:
X _x; //在Y类中定义X类的对象
};
这两种四想都是类设计层次代码复用的手段,他们两个各有千秋。继承是一种is-a的关系,就比如学生是人,老师是人;组合是一种has-a的关系,就比如头有眼睛,车有轮子。
在面向对象程序设计时,能使用组合思想设计类,就使用组合思想设计。其原因在于:使用组合的设计类的耦合度更低,因为使用的是对象作为成员,那么此种情况只关心结果(不需要知道此对象里面究竟有什么成员,即使成员发生变动,只要结果不变,照样正常使用);而继承的耦合度就更高了,因为派生类直接继承了基类的成员,而基类的成员一旦发生变动,派生类也必须做出相应的修改。