一、什么是继承
概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类(或子类),被继承的类称基类(或父类)。
概念当然是苦涩难懂的,形式还是要走一下的,其实继承顾名思义就是传t承上一个类的成员变量和成员函数,既然类的成员会被访问限定符限制,继承也有继承方式:pubilc 、private 、protect
继承方式:
而继承下来的并不是只有单一的继承方式约束,还有成员的访问限定符约束
认真观察,不需要花时间记,其实访问限定符和继承方式取那个最小的
为继承而生的访问级别:protect
public公有级别能被外界直接访问,private只能被在类内部和类成员函数访问,子类继承父类后,便拥有了父类的所有属性,那么这个时候,子类能直接访问父类的私有成员吗?答案是不能,用protect修饰的成员,跟私有成员一样,无法被外界直接访问,但是能被子类直接访问。也可以说,protect就是专门为继承而生的。
二、子类和父类(基类和派生类)
class Person
{
public:
int _age;
string _name;
}
class Student
{
public:
int _stu_id;
}
创建两个类: Person p,Student s
可以p = s, 却 s = p这样的行为编译不给过,会报错
为什么子类能轻松的赋给父类呢?
这里我们要了解一个东西叫做 切片或者 切割
就是派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。寓意把派生类中父类那部分切来赋值过去
通俗的理解,因为父类有的,子类一定有,而子类有的,父类不一定有,要是父类对象传给子类对象时,值有可能少传,所以当然是不可以这样的
三种赋值
1.赋值
student s;//子类
Person p;//父类
p = s;
2.引用
Student s;
Person& p = s;
3. 指针
Student s;
Person* pPtr = &s;
这里的知识会在多态体现重要性
继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 注意在实际中在继承体系里面最好不要定义同名的成员
class Person
{
protected :
string _name = "小吴"; // 姓名
int _num = 123456; // 身份证号
};
class Student : public Person
{
protected:
int _num = 999; // 学号
};
此时Student::_num与Person::_num构成隐藏
class AA
{
public:
void func(int i)
{
cout << "AA:func" << endl;
}
};
class BB : public AA
{
public:
void func()
{
cout << "BB:func" << endl;
}
};
只要函数名相同,也会构成隐藏,对参数列表没有要求。
三、子类的默认成员函数
1.构造函数
编译器会默认先调用父类的构造函数,再调用子类的构造函数,如下
class Person {
public:
Person(string name = "小明")
:_name(name)
{
cout << "Person()" << endl;
}
protected:
string _name;
};
class Student :public Person {
public:
Student(string name, int age)
:_age(age)
{
cout << "Student()" << endl;
}
protected:
int _age;
};
编译器先调用了父类的构造,再调用了子类的构造
Person的构造是给了缺省值的,若是没给,此时
可是创建s给了值,却没用呢
其实要对Student的构造函数修改下
Student(string name, int age)
:Person(name)
,_age(age)
{
cout << "Student()" << endl;
}
最好在子类的初始化列表中主动调用父类的构造函数
2.析构函数
析构函数和构造函数相反,编译器默认先调用子类的析构函数,再调用父类的析构函数。
再对上面的原有代码上去多写两个析构函数
class Person {
public:
Person(string name)
:_name(name)
{
cout << "Person()" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student :public Person {
public:
Student(string name, int age)
:_age(age)
, Person(name)
{
cout << "Student()" << endl;
}
~Student()
{
cout << "~Student()" << endl;
}
protected:
int _age;
};
不需要在Student的析构函数里去主动调用Person的析构函数,编译器会自动给我们去调,这样的行为能确保一块区域不会被析构两次,出现造成野指针的问题。
3.拷贝构造
子类中调用父类的拷贝构造时,直接传入子类对象即可,父类的拷贝构造会通过“切片”拿到父类的那一部分。
class Person {
public:
Person(string name)
:_name(name)
{}
Person(const Person& p)
{
_name = p._name;
}
protected:
string _name;
};
class Student :public Person {
public:
Student(string name, int age)
:_age(age)
, Person(name)
{}
Student(const Student& s)
: Person(s)
{
_age = s._age;
}
protected:
int _age;
};
4. 赋值运算符重载
子类的operator=必须要显式调用父类的operator=完成父类的赋值。
来先来单独看下子类的赋值重载:
Student& operator=(const Student& s)
{
if (this != &s)
{
operator=(s);
_age = s._age;
}
return *this;
}
其他的不管,先来运行一下
因为子类和父类的运算符,编译器默认给与了同一个名字,所以构成了隐藏,所以每次调用=这个赋值运算符都会一直调用子类,会造成循环,导致了栈溢出,所以这里的赋值要直接修饰限定父类。
class Person
{
public:
Person(string name)
:_name(name)
{}
Person& operator=(const Person& p)
{
if (this != &p)
{
_name = p._name;
}
return *this;
}
Person(const Person& p)
{
_name = p._name;
}
protected:
string _name;
};
class Student :public Person {
public:
Student(string name, int age)
:_age(age)
, Person(name)
{}
Student(const Student& s)
: Person(s)
{
_age = s._age;
}
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_age = s._age;
}
return *this;
}
protected:
int _age;
};
四.单继承和多继承
单继承: 一个子类只有一个直接父类的继承关系。
多继承: 一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况
本贾尼大佬刚开始发明了多继承沾沾自喜,可却没想菱形继承的出现给他带来了更多烦恼
让我们来看看菱形继承
class A
{
public:
int a;
};
class B : public A
{
public:
int b;
};
class C : public A
{
public:
int c;
};
class D : public B, public C
{
public:
int d;
};
这样带来了行为突显出菱形继承有数据冗余和二义性的问题。在d的对象中a成员会有两份。
当去访问d中访问a变量时,偏移器会报错
我们可以通过虚继承来解决二义性问题
class B : public A -> class B : virtual public A
class C : public A -> class C : virtual public A
再次运行代码,去观察内存窗口
没有两份a成员了,多了两个指针,而这两个指针是什么呢?
指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量
可以找到下面的A。
能力有限,反正多继承是C++复杂的一个体现。有了多继承,就存在菱形继承,为了解决菱形继承,又出现了菱形虚拟继承,其底层实现又很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。