前言
先来看C++ Primer中的概述
OOP:概述
面向对象程序设计(object-oriented programming)的核心思想是数据抽象,继承和动态绑定(动态多态),通过使用数据抽象,我们可以将类的接口与实现分离,使用继承,可以定义相似的类型并对其相似关系建模,使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用他们的对象。
定义:
继承:在C++中,继承是面向对象编程的核心概念之一,它允许根据一个已存在的类来定义新的类。继承机制使得代码重用成为可能,同时也提高了代码的维护性和执行效率。在继承关系中,已存在的类称为基类,而新定义的类称为派生类。
(也可以叫做父类和子类)
到这你可能初步了解了继承,通俗的讲,就是有一些属性是共同的,又没必要去都写一个类出来,这样就用到了继承,比如,要写一个学生管理系统,对于每一个学生,他们都有身份证号,id,姓名,我们可以把这些统一扔在一个类中,让学生去继承这个类就可以了
注意:继承是Is - a关系,(子类一定是父类 或者说 派生类一定是父类)
比如:
class Person{};
class Student: public Person{
};
什么是has - a关系?比如
class D//......
class c{
private:
D d;
};
这里是c中有D,而不能说c是D。看到这里你已经对继承有了一定的认识,下面让我们进行学习。
下面这个是Effective C++里的内容,我看了之后受益匪浅,虽然理解的还不够透彻但是建议大家一看(在学完继承和多态之后)
继承我们学什么?那我问你,继承是不是一个类继承了另一个类?那我们在类的学习里面学习了什么继承就应该学习什么,六大默认函数,static,友元等等
一、继承语法格式
class Person{};
class Student: public Person{
}; 派生类 继承方式 基类
1.继承关系和访问限定符
继承方式有三种:public,protected,private
访问限定符: public访问,protected访问,private访问
嘶,那继承基类成员访问方式的变化一共有九种?确实
不用具体去记,看我结论。
结论:1.如果是基类的private成员,你别管以什么方式去继承,派生类中均不可见(指的是类内和类外均不可见),基类中类内可见
2.其他方式,派生类的成员的访问方式 = min(继承方式,访问限定符)
(public > protected > private)
3.基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在
派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
4.可以class Student:Person
默认的继承方式是private
5.最好都使用public继承,那两个继承只能类里面玩,维护性不强
6,你可能会问,protected继承和private继承都是在派生类里面可见(public和protected成员),类外面不可见,这俩设计的有意义吗?有的兄弟有的
如果这个派生类再被继承呢?应该想到区别了,此时protected还可以在类内访问,private不可以。
大家可以自己以不同方式跑一跑
class Person
{
public :
void Print ()
{
cout<<_name <<endl;
}
protected :
string _name ;
private :
int _age ;
};
//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected :
int _stunum ;
};
int main(){
Person p;
Student s;
//s.......
return 0;
}
二、基类和派生类对象赋值转换
这里比较重要,对多态的学习也有帮助
1.派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片
或者切割。寓意把派生类中父类那部分切来赋值过去。
2.基类对象不能赋值给派生类对象。
3.基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类
的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换。
对于1,2条想一想就明白了,如果父类可以给子类,那子类的额外对象给什么?所以这个切片的说法很形象的
class Person
{
protected :
string _name;
string _sex;
int _age;
};
class Student : public Person
{
public :
int _No ;
};
void Test ()
{
Student sobj ;
// 1.子类对象可以赋值给父类对象/指针/引用
Person pobj = sobj ;
Person* pp = &sobj;
Person& rp = sobj;
//2.基类对象不能赋值给派生类对象
sobj = pobj;
// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
pp = &sobj
Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
ps1->_No = 10;
pp = &pobj;
Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问
题
ps2->_No = 10;
}
一定要弄明白!不然到多态还会出问题。
三、重定义(隐藏)
先看规则:
1. 在继承体系中基类和派生类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员。(上面那张图片的第二条就提到了)别自己恶心自己
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i = 1)
{
cout << "func(int i)" <<i<<endl;
}
};
void Test()
{
B b;
b.fun();
};
那我先问你,这里A中的fun和B中的fun构成重载吗?
答:不构成,因为上面都说构成重定义了(doge,注意,实际情况是重载需要在相同的作用域中,而A和B不在相同的作用域中
小改一下我再问你
class A
{
public:
void fun(int i)
{
cout << "func(int i)" << endl;
}
};
class B : public A
{
public:
void fun()
{
cout << "func()"<<endl;
}
};
void Test()
{
B b;
b.fun(1);
};
int main(){
Test();
return 0;
}
这段代码运行的结果是?
答:编译错误,子类b调用fun去b这个类里面找,发现你这个不需要参数,坏了,直接报错,会报出(函数调用中的参数太多)的错误,不要把编译器想的那么厉害,说子类找不到再去父类,如果我想调用父类的函数?可以显示调用
void Test()
{
B b;
b.A::fun(1);
};
另外,这俩还构成重定义,有点隐藏的意思了。
四、派生类的六大默认成员函数
欸?六大默认成员函数是啥?
忘了建议拖出去斩了(doge,三对,初始化和清理(构造函数和析构函数),拷贝复制(拷贝构造和赋值运算符重载),取地址重载(const和重载)这个一般没用
先看规则再解释:
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
class A
{
private:
int _a = 0;
};
class B : public A
{
public:
B()
:A(),_b(1)
{}
private:
int _b;
};
int main() {
return 0;
}
其实对于前三条,就记住:一定要先调用父类的相应函数,在搞子类的
下面是一个比较完整的类,可以看看构造函数和拷贝还有重载=
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 ;
};
void Test ()
{
Student s1 ("jack", 18);
Student s2 (s1);
Student s3 ("rose", 17);
s1 = s3 ;
}
最好自己对应一遍,还是比较简单的
关于析构函数容易错,
我们讲说析构函数是构造函数的逆过程,所以构造函数先构造父类再构造子类,那析构函数就应该是先子后父,如果先析构父类再析构子类会出问题,如果我子类的析构函数想用到父类呢?但是父类不可能会用到子类,举个例子
所以要先子后父,另外,!!!!派生类的析构函数调用完不需要显示调用基类的析构函数,编译器会自动调用,为什么要自动调用?因为避免先父后子的析构,你如果显示调用析构就会先父后子了
编译器确实自动调用了父类的析构函数,另外,父类的析构函数和子类的构成了隐藏,因为编译器都将他们处理成destructor()(至于为什么,多态再讲)
五、static成员和友元
友元函数就一句话:子类无法继承父类的友元关系,
父类static成员也就一句话,所有成员共用这个static成员
count放在了静态区供所有成员使用
六、复杂的菱形继承及菱形虚拟继承
这块确实是很恶心,听我慢慢道来
第一张图片最后一节“明智而审慎的使用多重继承”
首先要区分什么是多重继承和单继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
其实到这吧还没啥问题,到了菱形继承就出问题了,
什么是菱形继承?
要注意:这种也是菱形继承
菱形继承有什么问题?
Assistant里面有一份Student和Teacher,Student里面有一份_age,Teacher里面也有一份_age,嘶,难道说他作为老师和学生的时候年龄不一样?没必要有两份,这里就会造成数据冗余和二义性的问题,记住这个结构,将来讲完菱形虚拟继承的时候会进行对比。
有问题就要解决,所以搞出来了一个叫做菱形虚拟继承的东西
写法:
在中间继承方式写成virtual public
这样就保证了只有一份age,原来的代码也能跑过了,嘶,怎么实现的呢?—虚基表,(注意:要和虚表区分开来,这俩完全没关系)
原理:
为了方便讲解,换一个模型
class A {
public:
int _a = 1;
};
class B : public A {
public:
int _b = 2;
};
class C : public A {
public:
int _c = 3;
};
class D : public B, public C {
public:
int _d = 4;
};
int main()
{
D d;
return 0;
}
看一下内存(调试->窗口->内存)
这个结构很清楚前两个是A,中间两个是B,最后那个是C,此时A,B里都有一份C
变成菱形虚拟继承后:
注意:这是vsx86环境下,x64环境下不一样
确实只有一份1了,但仔细一看多了两个玩意,这这这谁把数据放到我内存中的啊,A中和B中都多了个地址,好像还挨着,进一步调查,注意:
vs是小端机,地址要倒着输入
再次注意:这是16进制下的数字,14在10进制下应该是20,0c是12,上内存中一看,我去,这个地址加上这个数字正好找到了A,这就是虚表的实现原理:
D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基中存的偏移量。通过偏移量可以找到下面的A。
这样就解决了数据冗余和二义性问题了,但是对于这个结构来说好像size还变大了,原来是20,现在变成24了,所以这个有用吗?当然是有用的了,当数据非常大的时候,这个虚基表指针就起了作用
所以还是有用的,那有人会疑问为什么B和C要去找A?
如果D d;
B b = d; //这种切片就需要找到A
所以:多继承谨慎使用,菱形继承不要使用,这种恶心别人也恶心自己,也别整什么虚拟继承。
七、关于继承的总结和反思
- 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱
形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设
计出菱形继承。否则在复杂度及性能上都有问题。 - 多继承可以认为是C++的缺陷之一,很多后来的OO语言(面向对象)都没有多继承,如Java。
- 继承和组合
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
优先使用对象组合,而不是类继承 。
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称
为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的
内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很
大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象
来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复
用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。
组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有
些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
继承,可以用组合,就用组合。
总结
继承一定要学明白,学校的C++课程也一定会讲(感觉多继承那块不会),继承整明白也更好的进入多态,马上更新多态