继承的概念介绍
继承是面向对象程序设计中代码复用最重要的手段。它允许在保持原有基类(父类)的基础上进行类的扩展,产生派生类(子类)。这体现了面向对象程序设计的层次结构。
在继承之前所接触的复用大多都是函数复用,继承却是类设计层次的复用。
继承的使用方式
class Person
{};
class Student : public Person
{};
继承方式与访问限定符一样,也有3种。
基类中不同的访问权限,被派生类通过不同方式继承后,也会有不同的结果。
基类成员 \ 继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
public成员 | 成为派生类的 public 成员 | 成为派生类的 protected 成员 | 成为派生类的 private 成员 |
protected成员 | 成为派生类的 protected 成员 | 成为派生类的 protected 成员 | 成为派生类的 private 成员 |
private成员 | 在派生类中“不可见” | 在派生类中“不可见” | 在派生类中“不可见” |
这里的“不可见”是指:基类的私有成员虽然被继承到了派生类中,但派生类对象是没有办法访问到的。protected
访问限定符其实是由于继承才出现的,它的目的就是被派生类继承后只能在类里面访问到,类外访问不到。
上面表格中继承后的各种结果除了基类私有成员“不可见”外,其余的结果可以通过如下公式得到。基类成员被子类继承的结果 = Min(成员在基类中被访问的权限,继承方式)
,默认public > protected > private
。
实际运用中通常都是使用public
继承,同时也不提倡使用protected/private
继承。因为protected/private
继承下来的成员在类外是无法使用的。
继承中的作用域
基类和派生类的作用域是独立的。
当派生类中的成员和基类成员有同名的时候,派生类成员将屏蔽对基类同名成员的直接访问,这种情况叫做隐藏或重定义。但仍然可以指定使用基类::基类成员
进行访问。
如果是成员函数的隐藏,只需函数名相同就构成隐藏。
所以在实际的继承体系中最好不要定义同名的成员。
// 因为是在类外访问,所以权限都改成了 public
class Person
{
public:
int _id;
};
class Student : public Person
{
public:
int _id;
};
void Test1()
{
Student s;
s._id = 1;
s.Person::_id = 2;
}
继承中的切片
派生类对象可以赋值给基类的对象/基类的指针/基类的引用。这就是继承的切片,是指把派生类中基类的那部分切下来赋值过去。
派生类对象可以切片赋值给基类对象,反过来却不行。
class Person
{
protected:
string _name;
};
class Student : public Person
{
protected:
int _num;
};
void Test2()
{
Student s;
Person p = s;
Person* pp = &s;
Person& rp = s;
}
派生类的默认成员函数
构造函数:派生类的构造函数必须调用基类的构造函数来完成基类那一部分成员的初始化工作。如果基类没有默认构造函数,那就必须在派生类的构造函数的初始化列表阶段进行显式调用。
派生类对象先调用基类构造再调用派生类构造来初始化。派生类对象先调用派生类析构再调用基类析构来进行对象的析构清理。
class Person
{
public:
Person(const char* name)
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student(const char* name, int num = 1)
: 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)
{
if (this != &s)
{
Person::operator=(s); // 构成隐藏 -> 显式调用
_num = s._num;
}
cout << "Student& operator=(const Student& s)" << endl;
return *this;
}
// 子类的析构函数跟父类的析构函数构成隐藏
// 由于多态的需要,析构函数的名字会被统一处理成destructor()
~Student()
{
// 子类析构后面,会自动调用父类的析构,这样保证先析构子类,再析构父类
cout << "~Student()" << endl;
}
private:
int _num;
};
void Test3()
{
// 子类默认生成的构造函数
// 1.对于自己的成员,跟普通类调用构造函数一样
// 2.对于继承的父类成员,必须调用父类的构造函数
Student s1("zs", 10);
Student s2("sz", 20);
// 拷贝构造
// 1.对于自己的成员,和普通类调用拷贝构造一样
// 2.对于继承的父类成员,必须调用父类的拷贝构造
Student s3 = s1;
// operator=的使用同上
s2 = s1;
继承中的友元与静态成员函数
继承体系中,友元关系不能被继承,即基类的友元函数不能访问派生类的private
和protected
成员。
基类中的友元函数如果想要访问派生类中的所有成员,就需要再成为派生类的友元。
基类中的静态成员,在整个继承体系中只存在一份(存在静态区)。所有的派生类对象都共享这一份静态成员实例。
class Person
{
public:
Person()
{
++_count;
}
public:
static int _count;
};
int Person::_count = 0;
class Student : public Person
{};
void Test4()
{
Person p;
Student s;
cout << "人数: " << Person::_count << endl;
cout << "&Person::_count: " << &Person::_count << endl;
cout << "&Student::_count: " << &Student::_count << endl;
}
菱形继承与菱形虚拟继承
单继承:一个子类只有一个直接父类时。
多继承:一个子类有两个或以上直接父类。
如果一个类不想被继承可以使用关键字final
。
class A final
{};
菱形继承:菱形继承是多继承的一种特殊情况。
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;
};
void Test5()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
}
菱形继承的问题就在于其数据冗余和二义性问题。
可以使用菱形虚拟继承来解决菱形继承的问题。
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;
};
void Test5()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
}
此例中,在菱形虚拟继承下,B对象和C对象中的_a
共用同一份的。这一份_a
存储在了为D对象开辟的高地址处。(D对象存储在栈上,栈的空间使用是从高地址向低地址开辟)
B对象和C对象是通过存储的指针(虚基表指针)找到虚基表中的偏移量,通过指针偏移来找到公共的_a
。
虚拟继承与菱形继承内存对象存储的对比。
如果单继承使用虚拟继承会是什么样子呢?
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
void Test6()
{
B b;
b._a = 1;
b._b = 2;
cout << sizeof B << endl;
}
继承的一些反思
菱形继承的问题本质上还是归咎于多继承这种设计。所以自己使用时尽量不要使用多继承,一定不要搞出菱形继承。像Java就只有单继承。
继承本质是一种白箱复用。
“白箱”是相对可视性而言的:在继承体系中,基类的内部细节对派生类是可见的,所以继承常被认为“破坏了封装性”。基类内部实现的改变也会影响到派生类。导致派生类和基类键有很强的的依赖关系,耦合度高。
所以这里要引出另一种复用手段:对象组合。
通常来说,继承是一种is-a
的关系,而组合是一种has-a
的关系。
对象组合的复用正是一种黑箱复用。文章来源:https://www.toymoban.com/news/detail-430495.html
class A
{};
// 组合
class B
{
protected:
A _a;
};
对象组合要求被组合对象具有良好定义的接口。对象的内部细节是不可见的,所以组合类之间没有很强的依赖关系,耦合度低,具有更好的封装性。
所以实际中,一个关系如果继承和组合都可以实现,那就用组合。
但是继承肯定有它存在的价值,不免有一些关系必须要使用继承来实现,另外要实现多态,也必须要继承。文章来源地址https://www.toymoban.com/news/detail-430495.html
到了这里,关于【C++】继承初识的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!