一、继承的概念及其定义
1.1 继承的概念
?继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。 例如:person就是父类,而学生类和老师类就是继承自父类
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter";
int _age = 18;
};
class Student : public Person
{
protected:
int _stuid;
};
class Teacher : public Person
{
protected:
int _jobid;
};
- 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student和Teacher复用了Person的成员。
- 我们使用监视窗口查看Student和Teacher对象,可以看到变量的复用。
- 调用Print可以看到成员函数的复用。
1.2 继承定义
1.2.1 定义格式
下面person是父类,也叫基类,student是子类,也叫派生类。
1.2.2 继承方式和访问权限符
1.2.3 继承基类成员访问方式的变化
类成员/继承方式 | public | protected | private |
---|
父类的public成员 | 子类的public成员 | 子类的protected成员 | 子类的private成员 | 父类的protected成员 | 子类的protected成员 | 子类的protected成员 | 子类的private成员 | 父类的private成员 | 在子类中不可见 | 在子类中不可见 | 在子类中不可见 |
总结:
- 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指虽然基类的私有成员被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 基类的其他成员在子类的访问方式为,取父子权限较小的那一个,权限大小关系:
public > protected > private 。 (比如父类成员为protected成员,子类继承方式为public,取两者较小的那一个,就是protected) - 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
注意: 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
二、基类和派生类对象赋值转化
?我们可以知道,父类赋值给父类,子类赋值给子类是没有问题的。但是如果父类赋值给子类,子类赋值给父类会出现什么问题呢?
class Person
{
protected:
string _name;
string _sex;
int _age;
};
class Student : public Person
{
public:
int _No;
};
int main()
{
Person p;
Student s;
p = s;
Person* ptr = &s;
Person& ref = s;
s = p;
Student* sptr = (Student*)&p;
Student& rref = (Student&)p;
return 0;
}
总结:
- 常说的切片,寓意就是把子类中,父类的那一部分切下来,赋值过去。(这里也叫赋值兼容,仅限于public继承),这里不存在类型的转换,是天然的语法支持行为。
- 父类是不能够赋值给子类的。
- 父类的指针可以通过强转类型赋值给子类,但是很危险,存在访问越界的风险。
三、继承中的作用域
- 在继承体系中,父类和子类都有独立的作用域
- 子类和父类中有同名成员时,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫做隐藏,也叫做重定义。(在子类成员函数中,可以显式指出要访问的函数,使用
基类::基类成员 )
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;
};
int main()
{
Student s1;
s1.Print();
return 0;
}
父子类出现了相同的变量,子类对父类的_num变量会隐藏起来,想要访问父类的成员,必须指定类域。
- 如果是成员函数的隐藏,只要函数名字相同,就构成隐藏。
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)->" << i << endl;
}
};
int a = 90;
int main()
{
B b;
b.fun(100);
b.A::fun();
b.fun();
return 0;
}
注意:最好在继承里面不要定义同名的成员!
四、派生类的默认成员函数
6个默认成员函数,默认的意思就是,我们不写,编译器自动生成的,那么在子类中编译器默认生成的,会干什么呢?
我们不写默认生成的子类的构造和析构怎么处理? a:对于继承下来的成员,调用父类的默认构造和析构函数 b:对于自己的成员,根普通的类处理方式一样(内置类型不做处理,自定义类型调用它对应的默认构造和析构函数)
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
protected:
int _num;
string _s="hello";
};
int main()
{
Student s;
return 0;
}
结果展示:
我们不写默认生成的子类的拷贝构造和operator= 怎么处理? a:对于继承下来的成员,调用父类的拷贝构造和operator= b:对于自己的成员,跟普通类一样处理
总结:原则上,继承下来的成员调用父类的处理,自己的成员按照普通类的处理即可。
什么情况下必须自己写?
- 父类没有默认构造,需要我们自己显示写构造。
- 如果子类有资源需要释放,就需要自己显示写析构。(比如有new的资源)
- 如果子类存在浅拷贝问题,就需要自己实现拷贝构造和赋值解决浅拷贝问题。
如果我们要自己写,如何自己写?
- 父类成员调用父类的对应构造、拷贝构造、operator=和析构处理
- 自己成员按普通类处理。
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student(const char* name="zhangsan",int num=0)
:Person(name)
,_num(num)
{}
Student(const Student& s)
:Person(s)
,_num(s._num)
{}
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
return *this;
}
~Student()
{}
protected:
int _num;
};
int main()
{
Student s;
Student s1(s);
s1 = s;
return 0;
}
有一个细节知识点: 父子析构函数名字会被统一处理成为"destructor( ) ",那么父子的析构函数就会构成隐藏关系,当子类调用析构函数时,必须指定作用域。
~Student()
{
Person::~Person();
}
?子类析构函数结束时,出了作用域会自动调用父类的析构函数,所以我们自己实现析构函数时,不需要显示调用父类的析构函数,这样才能保证先析构子类成员,后析构父类成员。(创建s对象时,里面继承的有父类的那一部分,会被先构造父类成员,后构造子类成员,析构时先析构子类成员,后析构父类成员)
五、继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
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;
}
int main()
{
Person p;
Student s;
Display(p, s);
return 0;
}
六、继承与静态成员
基类定义了一个static静态成员,则整个继承体系里面只有这么一个成员,无论派生多少个子类,都只有一个static成员。
class Person
{
public:
Person() { ++_count; }
protected:
string _name;
public:
static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum;
};
class Graduate : public Student
{
protected:
string _seminarCourse;
};
int main()
{
Person p;
Student s;
Graduate g;
cout << Person::_count << endl;
cout << Student::_count << endl;
cout << Graduate::_count << endl;
cout << &Person::_count << endl;
cout << &Student::_count << endl;
cout << &Graduate::_count << endl;
return 0;
}
七、复杂的菱形继承及菱形虚拟继承
单继承
一个子类只有一个直接父类时称为单继承
多继承
一个子类有两个或者两个以上的直接父类时称为多继承
菱形继承
菱形继承是多继承的一种特殊情况
菱形继承的问题:数据冗余和二义性 从下面的对象成员来看,在Assistant的对象中就有两份Person成员,访问_name时就不知道访问谁的,就会导致问题。
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 _majorCourse;
};
void Test()
{
Assistant a;
a._name = "peter";
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
虚拟继承
虚拟继承就可以解决菱形继承的二义性和数据冗余问题。 如上面的关系,在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 = "张三";
}
注意:虚拟继承不要在其他地方去使用!
虚拟继承原理
简化模型方便观察,再借助内存窗口观察对象成员模型。
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;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
不使用虚拟继承: 这里我们可以看到数据冗余!存在着两份A数据。
现在是使用虚拟继承: ?这里分析得出D对象将A放在了最下面,这个A是同时属于B和C的,那么B和C是如何找到公共的A呢?我们还发现,B和C中还存在两个指针,指向了一张表。这两个表叫做虚基表指针,这两个表叫做虚基表。虚基表中存放的是偏移量,这个偏移量是B和C公共A成员的偏移量或者相对距离。 ?如图所示,14是用16进制表示的,换算成10进制就是20,我们通过虚基表指针查找到了偏移量20,用起始地址+偏移量就可以找到公共A成员变量存放的位置了。
八、继承的总结与反思
- 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
- 多继承可以认为是C++的缺陷之一,很多后来的语言都没有多继承,如Java。
继承和组合
- public继承是一种
is-a 的关系。也就是说每个派生类对象都是一个基类对象。 - 组合是一种
has-a 的关系。假设B组合了A,每个B对象中都有一个A对象。
注意:优先使用对象组合,而不是类继承!
class A {
protected:
int _a;
};
class B : public A {
protected:
int _b;
};
class C {
protected:
int _c;
};
class D {
protected:
C _obj;
int _d;
};
谢谢观看!
|