继承的概念
继承机制是面向对象程序设计使代码复用的重要手段,通过继承机制,可以利用已有的数据类型来定义新的数据类型。所定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员。我们称已存在的用来派生新类的类为基类,又称为父类。由已存在的类派生出的新类称为派生类,又称为子类。
语法: class + 派生类名 + : public(继承方式) + 基类名
class Person //父类, 基类
{
protected:
string _name;
int _age;
};
class Student : public Person //子类, 派生类
{
private:
string stu_id;
};
继承方式: public , private, protected
基类和派生类对象赋值转换
class Person
{
protected:
string _name;
int _age;
};
class Student : public Person
{
private:
string _stu_id;
};
void inherit_test1()
{
Student s1;
//子类对象可以赋值给父类对象/指针/引用
Person p1 = s1;
Person* p2 = &s1;
Person& p3 = s1;
//基类对象指针可以强制转换成派生类对象指针
Student* ps = (Student*)p2;
}
注意:基类对象不能赋值给派生类对象,但是基类对象的指针可以强制转换派生类对象的指针,但这样很容易产生指针越界问题,所以不推荐使用
继承作用域
1.在继承体系中基类和派生类都有自己独立的作用域
2.若子类中有和父类同名的成员变量,则构成隐藏或者叫重定义。默认调用子类的成员变量,若想要调用父类的需要在前面加上作用域
3.只要函数名相同就够成隐藏
最好不要在继承体系中定义同名的成员
同名成员变量
class Person
{
protected:
string _name;
int _age = 10;
};
class Student : public Person
{
public:
void Print()
{
cout << _age << endl; //默认调用子类的成员
cout << Person::_age << endl; //加上作用域指定调用父类的成员
}
private:
int _age = 20;
string _stu_id;
};
void inherit_test2()
{
Student s1;
s1.Print();
}
int main()
{
inherit_test2();
return 0;
}
同名函数
class Person
{
public:
void Print() //父类的Print函数
{
cout << "Person Print()" << endl;
}
protected:
string _name = "peter";
};
class Student : public Person
{
public:
void Print(int i) //子类的Print函数
{
cout << "Student Print()" << endl;
}
private:
int _age = 20;
string _stu_id = "123.2.3.2";
};
void inherit_test2()
{
Student s1;
s1.Print(1);
//s1.Print(); //用法错误,无法匹配到父类的Print
s1.Person::Print(); //正确用法
}
int main()
{
inherit_test2();
return 0;
}
虽然父类和子类函数的形参有差别,但是并不构成函数重载,因为两个函数并不在同一个作用域内。它们构成隐藏关系,s1默认只能调用到自己类型内的Print函数.想要调用父类的Print函数也要加上作用域
派生类的默认成员函数
1.派生类的构造函数必须调用基类构造函数来初始化基类的成员。若基类没有默认构造函数,必须在派生类构造函数初始化列表阶段显示调用
2.派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类的拷贝初始化
3.派生类的operator=必须调用基类的operator=来完成基类的赋值
4.派生类的析构函数被调用完成后自动调用基类的析构函数清理基类成员
5.基类先调用构造函数,派生类再调用构造函数。派生类先调用析构函数, 基类再调用析构函数
#include <iostream>
using namespace std;
class Person
{
public:
//基类的四个默认成员函数
Person(const char* name = "peter", int age = 18)
:_name(name)
,_age(age)
{
cout << "Person(const char* name ,int age)" << endl;
}
Person(const Person& p)
:_name(p._name)
,_age(p._age)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
if (this != &p)
{
_name = p._name;
}
cout << "Person& operator=(const Person& p)" << endl;
return *this;
}
~Person()
{
cout << "~Person" << endl;
}
protected:
string _name;
int _age;
};
class Student : public Person
{
public:
//派生类的四个默认成员函数
//当基类没有默认构造函数时,我们必须要在初始化列表处显示调用
//Student(const char* name = "ken", int age = 20, const char* str_id = "000.0.0.0")
//:Person(name, age)
//:_stu_id(str_id)
//{
// cout << "Student(const char* str_id)" << endl;
//}
//若基类有默认构造函数,可以不写,编译器会自动调用
Student(const char* str_id = "000.0.0.0")
:_stu_id(str_id)
{
cout << "Student(const char* str_id)" << endl;
}
//若基类存在默认构造函数,但是我们又在派生类中显示调用,那么先会调用基类的默认构造函数,再进行显示调用
Student(const Student& s)
:Person(s) //这里我们直接将Student类对象的引用传给Person的拷贝构造函数
,_stu_id(s._stu_id) //赋值转换的价值在这里实现
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s)
{
if (&s != this) //首先得避免自己给自己赋值的现象
{
(*this).Person::operator=(s); //显示调用父类的赋值运算符重载(注意要注明作用域,不然会调用自身陷入死循环,造成栈溢出等问题 )
_stu_id = s._stu_id;
}
cout << "Student& operator=(const Student& s)" << endl;
return *this;
}
~Student()
{
cout << "~Student" << endl;
}
void Print()
{
cout << "name " << _name << "age " << _age << "stu_id " << _stu_id << endl;
}
private:
string _stu_id;
};
接下来进行三个小测试来观察一下基类和派生类各自的默认构造函数的调用顺序
void test_inherit1()
{
Student s1;
}
void test_inherit2()
{
Student s1;
Student s2(s1);
}
void test_inherit3()
{
Student s1;
Student s2;
s1 = s2;
}
int main()
{
test_inherit1();
//test_inherit2();
//test_inherit3();
return 0;
}
test1运行结果
Person(const char* name ,int age) //父类构造
Student(const char* name, int age, const char* stu_id) //子类构造
~Student //子类析构
~Person //父类析构
test2运行结果
Person(const char* name ,int age)
Student(const char* name, int age, const char* stu_id)
Person(const Person& p) //父类拷贝构造
Student(const Student& s) //子类拷贝构造
~Student
~Person
~Student
~Person
test3运行结果
Person(const char* name ,int age)
Student(const char* name, int age, const char* stu_id)
Person(const char* name ,int age)
Student(const char* name, int age, const char* stu_id)
Person& operator=(const Person& p) //父类赋值运算符重载
Student& operator=(const Student& s) //子类赋值运算符重载
~Student
~Person
~Student
注意:如果我们想要显示调用基类的析构函数。必须要在前面加上作用域,因为在编译过程中,析构函数的名称会被统一处理成destruct() ,导致基类和派生类的析构函数构成隐藏,以至于不断调用派生类的析构函数导致栈溢出
继承和友元
友元关系不能继承,基类友元不能访问子类私有和保护对象
静态成员变量的继承
在一个继承体系中,静态成员只会有一份,所有子类和父类共用一个静态成员实例
菱形继承和虚拟继承
单继承:一个子类只有一个直接父类时称此继承关系为单继承
多继承:一个子列有多个直接父类时称此关系为多继承
如图:Student和Teacher是Person的派生类,doctor(博士)同时继承了Student和Teacher。此时在doctor中就包含两份Person的成员变量信息。显然产生了数据冗余和二义性。我们可以通过作用域来调用不同父类的同名成员,解决二义性问题 。但是数据冗余的情况任然存在
如图所示:我们发现先继承的类成员地址越小,派生类的成员在基类的上面。并且确实存在两个_a,我们可以通过指定作用域来解决二义性问题。
虚拟继承
为了解决菱形继承中的数据冗余问题,C++提供了虚继承语法。我们可以使用virtual来进行虚继承
class Person
{
public:
int _a = 1;
};
class Student : virtual public Person //虚继承
{
public:
int _b = 2;
};
class Teacher : virtual public Person //虚继承
{
public:
int _c = 3;
};
class doctor : public Student, public Teacher
{
public:
int _d = 4;
};
int main()
{
doctor d;
return 0;
}
在这里插入图片描述
我们发现,变量排列的顺序发生了些许变化,b,c,d的顺序和先前一样,本来的冗余数据_a变成了一份到了最后面。并且d的空间中还有很多非成员变量的数据,这些数据其实是一个个地址
拿出第一个地址输入,我们发现这里有两行数据,第一行数据0,第二行数据为40
我们发现在距离&d四十个字节的地方存贮着_a,所以最上面的指针指向的空间的第二个数值存储着其与_a的相对距离,为了验证我们的猜想,我们进行下一组数据的验证
我们发现第二个值为24
我们发现_a 和 指针的起始位置相差了六行也就是24个字节,和我们的猜想一致
实验总结:使用虚继承后,冗余数据_a变成了一份并且来到结构的地址最高处。其他成员排列顺序不不变。在VS编译器中每一种类的数据以及冗余数据分开存储,中间由0xcc cc cc cc分隔。继承的类开头会有一个指针,指针指向的地方存储了两个数据,第一个两次都为零(暂不讨论),第二个数据存储的是指针和冗余数据_a的相对距离。当使用派生类传递给基类指针或者对象时,编译器可以通过指针指向数据找齐父类的数据,进行切片等操作
其实父类通过开头存储的指针叫做虚机基表指针, 指向的两个表叫做虚基表。虚基表中存储的数据叫做偏移量。通过偏移量可以找到重复继承的部分
多继承是C++语法中较为复杂的,有了多继承就会存在菱形继承,有菱形继承就存在菱形虚拟继承,底层实现非常复杂,不建议使用。像java这些语言就直接不允许多继承
继承和组合
继承:a is b Student is Person 的意思
组合: a has b Student has name 的意思
优先使用 组合
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用
(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。
继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关
系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对
象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),
因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,
耦合度低。优先使用对象组合有助于你保持每个类被封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适
合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就
用组合