继承
继承概念
继承是面向对象程序设计使代码可以复用的最重要的手段,允许程序员在保持原有类特性的基础上进行扩展,增加功能,从而产生新的类,这样的类称为派生类。继承呈现了面向对象程序设计的层次结构,由简到繁。继承是类设计层次的复用。
举例来看:
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "Bob"; //姓名
int _age = 10; //年龄
};
//S->P 学生继承了人
class Student : public Person
{
protected:
int _stuId; //学号
};
//T->P 老师继承了人
class Teacher : public Person
{
protected:
int _jobId; //工号
};
int main()
{
//通过监视窗口来看,对象s和t都包含老师的对象后
Student s;
Teacher t;
s.Print();
t.Print();
}
上面可以看出,继承的基本语法。并且继承后,**每个派生类对象都有基类的私有成员变量!**如果不加限制,子类可以“使用”父类的成员变量和成员函数。
继承语法
格式定义
继承基类成员的访问方式
总结:
- 基类的private成员在派生类中,无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是在语法上限制派生类的对象不管在类里面还是类外面都不能去访问它。
- 基类的private成员在派生类中是不能被访问的。如果基类成员不想在类外直接被访问,但需要在派生类中访问,就定义为protected。保护成员限定符是因为继承才出现。
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
private:
string _name = "Bob"; //姓名
protected:
int _age = 10; //年龄
};
class Student : public Person
{
public:
void Print()
{
cout << "name:" << _name << endl; //报错,因为_name为父类的private成员
cout << "age:" << _age << endl; // 编译通过,因为_age为父类的protected成员,可以在派生类中中中访问
}
protected:
int _stuId; //学号
};
- 继承方式总是public>protected>private取最小的方式。(继承的方式,相当于对派生类来说的,比如说如果以private方式来说,派生类只能在类中访问它,但在类外不能访问,并且别人继承该派生类,由于是private,孙子类不可以访问)
- 使用class时,默认的继承方式是private;使用struct默认是public,但最好显示写出继承方式!
- **实际应用中一般使用都是使用public继承,很少使用protected/private继承。**因为protected/private继承只能在派生类的类里面使用,实际维护扩展性不强。
基类和派生类对象赋值转换
- 派生类对象 可以赋值给基类的对象/指针/基类的引用,这类操作叫切片。意思是把派生类中父类那部分切来赋值过去。
- 基类对象不能赋值给派生类对象。
如下示例:
class Person
{
protected:
string _name;
string _sex;
int _age;
};
class Student : public Person
{
public:
int _NO;
};
void Test()
{
//1.子类对象可以赋值给父类对象/指针/引用
Student s;
Person p = s;
Person* ptrp = &s;
Person& rp = s;
//2.基类对象不能赋值给派生类对象
s = p; //报错
}
父类引用/指向的仅仅是自己部分。
继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用基类::基类成员 来显示访问)
成员变量发生隐藏:
class Person
{
protected:
string _name = "张三";
string _sex;
int _age = 10;
};
class Student : public Person
{
public:
void Print()
{
cout << "姓名:" << _name << endl;
cout << "年龄:" << _age << endl;
cout << "学号:" << _NO << endl;
}
protected:
int _NO = 1;
int _age = 20;
};
void Test()
{
Student s;
s.Print();
}
可见,_age由于同名,产生了隐藏,(根据就近原则)打印子类的_age。
如果想访问父类的_age,需要指定类域。如下修改:
成员函数发生隐藏:
class Person
{
public:
void Print()
{
cout << "我是父类的成员函数" << endl;
//cout << "姓名:" << _name << endl;
//cout << "年龄:" << _age << endl;
}
protected:
string _name = "张三";
string _sex;
int _age = 10;
};
class Student : public Person
{
public:
void Print()
{
cout << "我是子类的成员函数" << endl;
//cout << "姓名:" << _name << endl;
//cout << "年龄:" << _age << endl;
//cout << "学号:" << _NO << endl;
}
protected:
int _NO = 1;
int _age = 20;
};
void Test()
{
Student s;
s.Print();
}
运行结果:
可见父类的Print()被隐藏,使用子类调用的是子类的Print()。
如果想调用父类的Print(),需要调用时显示指定父类域。
如下:
派生类的默认成员函数
"默认"的意思是如果我们不写,编译器会帮我们自动生成一个。
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显式调用。
class Person
{
public:
Person(const string name = "张三", const string sex = "男")
:_name(name)
,_sex(sex)
,_age(10)
{
cout << "Person()" << endl;
}
protected:
string _name;
string _sex;
int _age;
};
class Student : public Person
{
public:
Student(int NO = 199)
:_NO(NO)
{
cout << "Student()" << endl;
}
protected:
int _NO;
};
void Test()
{
Student s;
}
如果我们基类有默认构造,当我们调用派生类的构造函数时,编译器会自动调用基类的默认构造函数(我们没写的话,会自动生成一个)。
如果我们基类没有默认构造,我们就需要在派生类的初始化列表显式调用!!!否则就会报错
需要显式调用:
显式初始化后,派生类就会根据我们指定的内容去调用基类的构造函数。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成过基类的拷贝初始化。(对于内置类型,如果我们不写拷贝构造,编译器同样也会给我们生成一个默认的拷贝构造)因此这里用内置类型举例不合适,但是和序号1一样,如果我们没有写编译器会给我默认生成,一旦我们写了,我们就要在派生类显式调用!!!
如下例子:
class Person
{
public:
Person(const string name, const string sex = "男")
:_name(name)
,_sex(sex)
,_age(10)
{
cout << "Person()" << endl;
}
Person(const Person& p)
:_name(p._name)
,_sex(p._sex)
,_age(p._age)
{
cout << "Person(const Person& p)" << endl;
}
protected:
string _name;
string _sex;
int _age;
};
class Student : public Person
{
public:
Student(const string name,int NO = 199)
:Person(name)
,_NO(NO)
{
cout << "Student()" << endl;
}
Student(const Student& s)
:Person(s)
,_NO(s._NO)
{
cout << "Student(const Student& s)" << endl;
}
protected:
int _NO;
};
void Test()
{
Student s("李四", 22222222);
Student v(s);
}
- 派生类的operator=必须调用基类的operator=完成基类的赋值。(不写会调用默认的)(为了防止深拷贝的场景我们也要学着写赋值重载)
class Person
{
public:
Person(const string name, const string sex = "男")
:_name(name)
,_sex(sex)
,_age(10)
{
cout << "Person()" << endl;
}
Person(const Person& p)
:_name(p._name)
,_sex(p._sex)
,_age(p._age)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
if (this!= &p)
{
_name = p._name;
_sex = p._sex;
_age = p._age;
}
cout << "Person& operator=(const Person& p)" << endl;
return *this;
}
protected:
string _name;
string _sex;
int _age;
};
class Student : public Person
{
public:
Student(const string name,int NO = 199)
:Person(name)
,_NO(NO)
{
cout << "Student()" << endl;
}
Student(const Student& s)
:Person(s)
,_NO(s._NO)
{
cout << "Student(const Student& s)" << endl;
}
//返回值这样设置是为了防止连续赋值
Student& operator=(const Student& s)
{
if (this!= &s)
{
operator=(s);
_NO = s._NO;
}
cout << "Student& operator=(const Student& s)" << endl;
return *this;
}
protected:
int _NO;
};
void Test()
{
Student s("李四", 22222222);
Student v("王五",1111111);
v = s;
}
我们运行这段代码报错了:
很显然是栈溢出,重复调用子类的赋值重载导致的,我们又忽视了一个问题,当父类和子类的函数名相同时,会造成隐藏!!!
我们需要显式调用父类的赋值重载函数即可。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。保证派生类对象先清理派生类成员再清理基类成员的顺序。
class Person
{
public:
Person(const string name, const string sex = "男")
:_name(name)
,_sex(sex)
,_age(10)
{
cout << "Person()" << endl;
}
Person(const Person& p)
:_name(p._name)
,_sex(p._sex)
,_age(p._age)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
if (this!= &p)
{
_name = p._name;
_sex = p._sex;
_age = p._age;
}
cout << "Person& operator=(const Person& p)" << endl;
return *this;
}
//析构函数会被处理成destructor
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;
string _sex;
int _age;
};
class Student : public Person
{
public:
Student(const string name,int NO = 199)
:Person(name)
,_NO(NO)
{
cout << "Student()" << endl;
}
Student(const Student& s)
:Person(s)
,_NO(s._NO)
{
cout << "Student(const Student& s)" << endl;
}
//返回值这样设置是为了防止连续赋值
Student& operator=(const Student& s)
{
if (this!= &s)
{
Person::operator=(s);
_NO = s._NO;
}
cout << "Student& operator=(const Student& s)" << endl;
return *this;
}
//析构函数会被处理成destructor
~Student()
{
Person::~Person();
cout << "~Student()" << endl;
}
protected:
int _NO;
};
void Test()
{
Student s("李四", 22222222);
//Student v("王五",1111111);
//v = s;
}
执行结果后,我们发现~Person()多执行了一次,也就是父类多执行一次!
查阅了资料发现派生类调用析构函数时,为了保证先处理子成员,再处理父成员。编译器会默认等派生类的析构函数完成后,再调用父类的析构函数。因此我们不需要显式调用父类的析构函数,编译器会帮助我们!!
- 关于调用顺序:
- 派生类对象初始化先调用基类构造再调用派生类构造。
- 派生类对象析构先调用派生类析构再调用基类析构。
继承和友元
友元关系不能继承,也就是基类的友元不能访问子类私有和保护成员!!
该例子可知,友元关系不能继承!
继承和静态成员
基类定义了一个static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。
从下面的例子中可以看出:
class Person
{
public:
Person() { _count++; }
protected:
string _name; //姓名
public:
static int _count; //统计次数
};
class Student : public Person
{
protected:
int _ID; //学号
};
class Teacher : public Person
{
protected:
int _jobId; //工号
};
//类外初始化
int Person::_count = 0;
void Test()
{
Student s1;
Student s2;
Student s3;
Teacher t1;
cout << "人数:" << Person::_count << endl; //4
Student::_count = 0;
cout << "人数:" << Person::_count << endl; // 0
}
给我实现一个不能被继承的类
只需要将基类的构造函数私有化或者将析构函数私有化,都可以。
class Person
{
private:
Person()
{}
};
class Student :public Person
{
public:
Student(){}
};
void Test()
{
Student s1;
}
但是如果这样的话,Person自己就不能定义对象了。我们应该再显式写一个想办法构造Person的成员函数。
class Person
{
public:
static Person CreateObj()
{
return Person();
}
private:
Person()
{}
};
void Test()
{
//Student s1;
Person s1 = Person::CreateObj();
}
为什么要加static呢?询问chatgpt可知
简单来说,由于构造函数私有化,不能直接定义。非静态成员函数需要对象去调用,但是我们又没有对象(因为私有化了)。所以我们可以定义成静态成员变量,这样就可以使用类名调用啦!
菱形继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。
菱形继承:多继承的一种特殊情况
菱形继承有什么问题呢?
菱形继承会有数据冗余和二义性的问题。
我们有如下菱形继承关系:
class Person
{
public:
string _name; //名字
};
class Student :public Person
{
protected:
int _num; //学号
};
class Teacher :public Person
{
protected:
int _id;//工号
};
class Assistant :public Student, public Teacher
{
protected:
string _major; //专业
};
- 二义性问题
这样虽然能解决二义性的问题,但是很麻烦,并且一个人有两个名字多少有一点不合理。
- 数据冗余问题
数据真的冗余了吗,我们换一套代码,来说明这个问题。
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;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
监视窗口看出:B里面有一份a = 1,C里面有一份a = 2。(数据冗余)
内存窗口看出,数据确实冗余了。(内存分布为小端分布)
如何解决这个问题呢?
接下来引入虚继承:用virtual修饰继承方法
class A
{
public:
int _a;
};
class B :virtual public A //修改为虚继承
{
public:
int _b;
};
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; //内存里面没有1,是因为虚继承只有一份a数据,被2给覆盖了
//d.C::_a = 2;
d._a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
内存窗口看出,2只变成1份了,放在了类D的最下面,但是B、C里面多了4字节的数据。(内存分布为小端分布)
那么像B、C类多出来的4字节数据是什么呢?
多出来的4字节数据其实是一个指针,该指针指向的内容是偏移量,而这个偏移量和该指针的地址相加得到的结果其实就是A类成员的地址。
因此虚继承是通过这种方式解决数据冗余的问题的。
相关的术语解释是:B、C的这两个指针,指向的一张表。这两个指针叫做虚基表指针,这两个表叫虚基表 。虚基表中存的偏移量,通过偏移量找到下面的A。
疑问?
为什么D中B和C部分要去寻找属于自己的A?请看下面的赋值发生时,d是不是要去找出B/C成员中的A才能赋值过去呢?
D d;
B b = d;
C c = d;
总结
多继承会导致菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
继承和组合
继承是一种is -a 的关系,组合是has-a。
区别:
- 如果我们改动A的保护成员,有可能会影响B;但是我们改动C的保护,基本不会影响D,因为C访问不到D。
- B可以访问A的保护成员,但是D不能访问A的保护成员,只能间接访问。
- 继承耦合度高,组合耦合度低。
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用:在继承方式中,基类的内部细节对子类可见。继承一定程度上破坏了封装,基类的改变,对派生类有很大影响。派生类和基类间的依赖关系强,耦合度高。
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好的接口。这种复用风格被称为黑箱复用。组合类之间没有很强的依赖性,耦合度低。
总结:能用组合就不用继承。