[C++]继承
文章目录
一、概念
继承(inheritance) 机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类(子类)。而以前我们接触的复用都是函数复用,继承是类设计层次的复用 。
它指的是一个类(子类)可以从另一个类中(父类)中继承方法和成员,就是对父类代码的一种复用。
比如我们要设计一个学校人员的管理系统,系统中我们需要给老师、学生、职工等人设计一个类,而这些类的一些成员属性是有共同点的。(姓名、工号或学号、电话等等)
class Student
{
public:
string _name; // 姓名
int _age; // 年龄
int _stuid; // 学号
};
class Teacher
{
public:
string _name; // 姓名
int _age; // 年龄
int _jobid; // 工号
};
如果我们这样设计就很大的浪费了资源。大部分的类的成员属性、方法都是有共同点的,而我们可以将这些共同点提取出来,设计成一个单独的父类,让其他类来继承父类,从而实现复用。
例如:
class Person
{
public:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
class Student : public Person
{
public:
int _stuid; // 学号
};
class Teacher : public Person
{
public:
int _jobid; // 工号
};
我们可以看到子类继承了父类的成员属性,完成了代码的复用。
二、定义
格式如下:Student是子类(派生类),public 是继承方式 , Person是父类(基类)。
1.继承方法
C++中的类的访问限定符有三种:
- public
- protected
- private
继承的方式也有三种:
- public继承
- protected继承
- private继承
他们组合到一起就是9种继承方式。
2.总结
1.基类 private 成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它,而不是派生类没有继承该成员。
2.基类 private 成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出 protected 限定符是因继承才出现的。(没有关系继承时,protected 和 private 没有区别)
3.基类的其他成员在派生类的访问方式等于成员在基类中的访问权限与继承方式取较小值,其中访问权限大小为 public > protected > private。
4.用关键字 class 时默认的继承方式是 private,使用 struct 时默认的继承方式是 public,不过最好显示的写出继承方式。
5.在实际运用中一般使用都是 public 继承,几乎很少使用 protetced/private 继承,也不提倡使用 protetced/private 继承,因为protetced/private 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
6.一般继承如果不特指protected、private,就指的是public继承
三、赋值转换(切片赋值)
在继承关系中,派生类(子类)对象可以直接赋值给基类(父类)的对象/基类(父类)的指针/基类(父类)的引用,而不产生类型转换。这个赋值的过程也被形象的叫做切片或者切割,寓意把派生类中父类那部分切来赋值过去。
派生类向基类赋值时,将派生类中属于基类的那一部分切割给基类(如:_name/ _sex/ _age)、引用和指针都是同样的。(基类的引用为派生类中属于基类的那一部分成员的别名、基类的指针指向派生类中属于基类的那一部分成员)
注:
1.派生类可以向基类赋值,而子类向基类不能赋值。
2.派生类对象赋给基类对象时中间不会参数临时变量,所以基类对象可以直接引用/指向派生类对象,而不需要使用 const 修饰。
四、作用域(隐藏)
-
在继承体系中基类和派生类都有独立的作用域。
-
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫
隐藏
,也叫重定义
。 -
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
-
注意:在实际中在继承体系里面最好不要定义同名的成员。
class Person
{
public:
void test()
{
cout << "Person : test()" << endl;
}
string _name = "张三"; // 姓名
int _num = 001; // 身份证号
};
class Student : public Person
{
public:
void test(int i = 0)
{
cout << "Student : test(int i = 0)" << endl;
}
int _num = 123; // 学号
};
int main()
{
Student s1;
s1.test(2);
s1.Person::test();
return 0;
}
-
在派生类中因为有一个和基类名字相同的test()函数,于是我们无法直接访问到基类中的test()函数。
-
我们在派生类成员函数中,可以使用 基类::基类成员 显示访问。
五、派生类的默认成员函数
1.普通类中的默认成员函数
普通类的成员变量可以分为两类:自定义类型和内置类型。
对于普通类成员,默认生成四个成员函数:构造、拷贝构造、赋值拷贝和析构函数。
对于其中的内置类型:构造和析构不做处理,拷贝和赋值进行浅拷贝/值拷贝。
对于其中的自定义类型:构造和析构调用对应的构造函数和析构函数,拷贝和赋值调用对应的拷贝构造和赋值拷贝
取地址重载和 const 取地址重载这两个默认成员函数我们一般使用编译器自动生成的即可。
2.派生类的默认成员函数
派生类的成员函数分为三种:内置类型、自定义类型、基类(父类)成员变量
对于派生类的内置类型和自定义类型和普通类的成员函数相同,而父类成员变量必须由父类成员进行处理。
1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用基类的构造函数。
2.派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3.派生类的operator=必须要调用基类的operator=完成基类的复制。
4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。
5.派生类对象初始化先调用基类构造再调派生类构造,派生类对象析构清理先调用派生类析构再调基类的析构。
6.子类析构和父类析构构成隐藏关系。(由于多态关系需求,所有的析构函数的函数名都会被编译器处理为 destructor)
也就是说派生类的析构函数不需要我们显式调用父类的析构函数,而是会在子类析构函数调用完毕后自动调用父类的析构,父类成员变量必须由父类成员进行处理。
例如:
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, int num)
: Person(name) //父类构造
, _num(num)
{
cout << "Student()" << endl;
}
Student(const Student& s)
: Person(s) //父类拷贝构造
, _num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator = (const Student& s) {
cout << "Student& operator= (const Student& s)" << endl;
if (this != &s)
{
Person::operator =(s); //父类赋值重载
_num = s._num;
}
return *this;
}
~Student() {
cout << "~Student()" << endl;
}
protected:
int _num; //学号
};
- 父类没有提供默认构造,我们需要在子类中构造函数中初始化列表调用父类的构造函数进行对父类成员的初始化。
- 子类的拷贝构造必须调用父类的拷贝构造完成对父类成员的拷贝,而父类的拷贝需要的是Person对象,这里子类却是Student的对象,但是由于切片的原因不会存在类型转换。
- 子类的赋值拷贝必须调用父类的赋值拷贝完成父类对象的赋值。需要注意的是子类和父类的赋值拷贝为同名函数,构成了隐藏,所以我们需要指定父类的作用域,否则会陷入无限调用子类的赋值拷贝中。
- 子类析构在调用完成后会自动调用父类的析构,所以我们不用在子类析构中去手动析构子类中的父类成员。
六、友元
友元关系无法继承(父类的友元无法访问子类的保护和私有成员),而解决办法就是让该子类同样友元同一个函数。
class Student;
class Person
{
public:
friend void Print(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stuNum; // 学号
};
void Print(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
Print(p, s);
return 0;
}
七、静态成员
1.继承与静态成员
在继承中,如果父类定义了 static 静态成员,则该静态成员也属于所有派生类及其对象;即整个继承体系里面只有一个这样的成员,并且无论派生出多少个子类,都只有一个 static 成员。
class Person {
public:
Person() { ++_count; }
void Print() {
cout << this << endl;
}
public:
string _name; // 姓名
static int _count; // 统计人的个数。
};
int Person::_count = 0; //在类外对静态成员进行定义初始化
class Student : public Person {
protected:
int _stuNum; // 学号
};
int main()
{
Person p1;
Person p2;
Student s1;
Student s2;
cout << p1._count << endl;
cout << s1._count << endl;
cout << &p1._count << endl;
cout << &s1._count << endl;
return 0;
}
无论派生出多少个子类,都只有一个 static 成员。
2.问
class Person {
public:
Person() { ++_count; }
void Print() {
cout << this << endl;
}
public:
string _name; // 姓名
static int _count; // 统计人的个数。
};
int Person::_count = 0; //在类外对静态成员进行定义初始化
class Student : public Person {
protected:
int _stuNum; // 学号
};
int main()
{
Person* ptr = nullptr;
cout << ptr->_name << endl; //1
ptr->Print(); //2
cout << ptr->_count << endl; //3
(*ptr).Print(); //4
cout << (*ptr)._count << endl; //5
return 0;
}
1.编译错误
_name为类中的普通成员,存在于对象中,当我们访问 _name时编译器会到ptr指向的Person类中去找 _name,会引发
空指针解引用问题
。2.正确
Print()是类中的成员函数,成员函数存在于代码段中,不存在对象里,所以调用成员函数需要传递this指针,并且ptr也是Person类型的指针,ptr的值0x000会被当作this指针传递给Print()函数。Print()函数内没有访问类的成员所以不会引发空指针解引用问题。
3.正确
_count是静态成员变量,存在于静态区,不存在于对象中,访问 _count会到静态区访问,不会对ptr解引用。此处的ptr是指明类域。(类似于Person:: _count)
4.正确
编译器处理后类似于2。
5.正确
编译器处理后类似于3。
八、多继承与菱形继承
1.多继承
单继承:一个子类只有一个直接父类
就叫这个继承关系叫单继承。
多继承一个子类有两个及以上的直接父类
就叫这个继承关系为多继承。
class A
{
public:
int _a;
};
class B
{
public:
int _b;
};
class C : public A, public B
{
public:
int _c;
};
int main()
{
A a;
B b;
C c;
return 0;
}
多继承可以让子类同时拥有多个父类的成员,进一步提供代码的复用率。
2.菱形继承
因为有了多继承,这才引发了菱形继承的问题。
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()
{
A a;
B b;
C c;
D d;
return 0;
}
我们发现一个子类继承两个父类,同时这两个父类又是一个父类的子类。
这时这个D中就同时存在了两份A的内容,造成了空间浪费。当我们去直接访问时,就会造成访问不明确的问题。
此时就应该指明作用域访问,但是数据的冗余性仍然存在。
简单来说就是菱形继承存在数据的冗余和二义性的问题。
3.virtual
为了解决菱形继承造成的数据冗余性和二义性,C++引入了虚拟继承(virtual)。
我们给B和C加上virtual
就可以解决这两个问题,也就是给产生菱形继承的位置(菱形的腰部),加上virtual。
虚继承前:
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;
}
虚继承后:
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._a = 1;
d._b = 2;
d._c = 3;
d._d = 4;
return 0;
}
我们发现对象中只有一分A类的成员了,B和C中分别多了一个指针,去内存窗口中查看时两个指针中分别存了对A地址的偏移量。
-
多出来的指针指向的内容叫做
虚基表
,后四个自己内容存放的是对象的起始地址与虚基表类起始地址的偏移量。 -
只有一个A类成员,解决了数据冗余和二义性。当类非常大时,对解决数据冗余和二义性问题才更加明显。
注:不建议设计菱形继承。
九、组合
- 继承:每一个派生类对象都是一个基类对象,是一种is-a的关系。
- 组合:在一个类中包含另外一个类的对象成员,是一种has-a的关系。
class A
{
public:
int _a;
};
class B
{
public:
int _b;
A a1;
};
class D:public A
{
public:
int _b;
};
一般情况下,能使用组合就使用组合。
- 通过基类的实现定义派生类的实现,对基类内部细节可见(打破了类和对象的封装),可以通过派生类访问基类的protected成员。这种通过使用派生类实现代码复用我们称为白箱复用。
- 一般通过对被组合对象的组装或者组合来使用对象的接口产生复用,我们称为黑箱复用。这种复用方式,我们对组合的对象内部细节是不可见的,只能访问该类的公有成员,减小了对象之间的关联性,且耦合度低更有利于封装。
简而言之,优先使用组合。(有些关系只适合继承那就使用继承)
十、final
如果我们不希望一个类永远不被继承,一般来说就是将构造方法私有(私有析构也可以,但是他仍然能对类进行构造和访问)。
class A
{
private:
A()
:_a(0)
{}
public:
int _a;
};
class B :public A
{
public:
B(int a, int b)
:A(a)
,_b(b)
{}
int _b;
};
int main()
{
B b(10, 20);
return 0;
}
它虽然阻止了子类创建对象,但是构造私有化也使得它本身也不能创建对象。
而C++11给我们提供了一个方便简洁方案。
class A final
{
public:
A()
:_a(0)
{}
int _a;
};
class B :public A
{
public:
B(int a, int b)
:A(a)
,_b(b)
{}
int _b;
};
int main()
{
B b(10, 20);
return 0;
}
我们用final修饰我们不想被继承的基类即可。