学习继承,掌握如何在原来的类的基础快速增加新的功能,而不影响原来的类,也不改变原来类的代码,方便对于功能的扩展
类和类之间的关系:包含(组合和聚合),友元,继承
一、继承和派生
1.继承的概念
1.继承是一种创建新类的方式,新建的类可以继承一个或者多个类。可以理解为子承父业
2.所以继承描述的是类和类之间的关系
3.新建的类被称之为派生类(子类),之前就存在的类被称之为基类(父类)
2.继承和派生
1.继承和派生是同一个过程,从不同的角度来看的
2.一个新类从已经存在的类那里获取其原来已有的特性,这个就叫继承(儿子继承父亲的财产)
3.从已存在的类产生一个子类,这个就叫派生(父亲把财产给儿子)
3.继承分类
1.新建的类继承一个类,这个就是单继承
2.新建的类继承多个类,这个就叫多继承
二、继承方式
1.继承方式
class A{};
class B:public A{};//单继承
class C{};
class D{};
class E:public C,public D{};//多继承
2.示例
#include<iostream>
using namespace std;
//三种继承方式:就是三种访问属性对应
class father//基类,父类
{
public:
int id;
void fun()
{
cout << id << endl;
}
protected:
int b;
private:
int c;
};
class son :public father
{
public:
void fun1()
{
id = 1;
b = 2;
//c = 3;基类的私有成员可以继承,但是不能访问
}
};
void publicfun()
{
son p1;
p1.id;
//公有继承,在类外通过子类对象只能访问公有属性下面的成员
}
class son1:protected father//类外都不能访问了,需要公有属性的接口
{
public:
void fun()
{
id = 1;
b = 2;
//c = 3;私有的还是不能访问
}
};
class son2 :private father
{
public:
void fun()
{
id = 4;
b = 3;
}
};
int main()
{
son s;
s.id = 20;
son* p = &s;
s.fun();
system("pause");
return 0;
}
3.继承和派生的特点
-
派生类继承基类之后,派生类拥有父类所有的成员(构造析构除外)
-
保护属性和私有属性的区别就是保护属性的成员继承之后,可以在类中访问,私有属性的成员不行,类外二者都不能被访问
-
根据继承方式和基类的访问属性,来决定基类的成员在派生类中的访问属性
-
看继承方式和基类的访问属性,谁更严格就按照更严格的来访问 public<protected<private
-
基类的私有成员可以继承,但是不能直接访问
-
继承方式,建议使用public继承,这样会最大限度的保留访问属性
-
公有继承,在类外通过子类对象只能访问公有属性下面的成员
-
一般把父类的数据成员,定义为protected属性,这样既能保护数据成员不被类外访问,也可以让子类方便的访问父类的数据成员
-
protected属性和private属性的唯一区别:
protecte: 子类的成员函数中可以直接访问
private: 子类的成员函数中不可以直接访问
三、继承之后
1.派生类的构成
1.派生类会继承除基类的构造析构函数之外的所有数据成员和函数成员(但是创建对象会使用构造函数,对象死亡时会调用析构函数)
2.可以在派生类中添加新成员,通过派生类对象来调用
3.如果派生类中添加的成员名和基类成员名相同,那么派生类会隐藏基类的成员,可以通过<基类名::基类成员名>来访问,如果是继承的多个基类,而多个基类也有同名的,也是通过这种方法调用同名的成员。
就是通过作用域运算符( ::)说明一下这个成员是谁的
2.子类型
公有继承时,派生类的对象可以作为基类的对象处理,派生类是基类的子类型
//class B:public A
//B类继承A类
void test(A a) {
a.kill(); //调用的是A类对象的kill方法
}
A a;
B b;
test(a);调用的是A的kill函数
test(b);//调用的是A的kill函数
//即使B类中有同名的kill函数,也会调用A类的kill函数
子类型关系具有单向传递性。
C类是B类的子类型
B类是A类的子类型
那么C类型是A类型的子类型
作用:
在需要父类对象的任何地方, 可以使用”公有派生”的子类的对象来替代,
从而可以使用相同的函数统一处理基类对象和公有派生类对象
即:形参为基类对象时,实参可以是派生类对象
应用:
1.基类的指针,可以指向派生类(子类型)的对象
Son yangGuo;
Father * f = &yangGuo;
2.公有派生类(子类型)的对象可以初始化基类的引用
Son yangGuo;
Father &f2 = yangGuo;
3.公有派生类的对象可以赋值给基类的对象
Son yangGuo;
Father f1 = yangGuo;
注意:以上的应用,反过来就会编译失败!
注意:意思就是可以用子类对象给父类赋值,但是不能用父类对象给子类赋值
3.派生类的构造析构顺序
1.派生类对象在实例化的时候是会调用基类的构造函数的,先基类后派生类(先有父亲后有儿子),释放就是先父亲后儿子,因为在栈区,先进后出
2.如果是多继承与单继承中构造顺序一致,区别在于,在构造基类时有多个基类,那么会按照基类在初始化列表的声明顺序来依次调用基类的构造函数
3.所以在写继承的时候,要确保基类有可以调用的构造函数,如果是带参构造,就需要使用初始化列表了。初始化列表写在构造函数后面,可以传形参,也可以传实参
class A
{
public:
}
class B:public A
{
public:
}
//如果这样实例化B类对象,那么会调用A的默认构造,A有自己写的构造函数,那就调用自己写的
class A
{
public:
A(int a)
{
}
}
class B:public A
{
public:
}
//这样实例化B类对象,那么就会报错,会提示A类没有合适的构造函数调用,所以需要初始化列表调用父类的带参构造
//带参构造
B():A(1)
{
}
4.如果类中有对象成员,默认调用的是成员的无参构造(默认构造),如果没有这个构造函数,那么就要出错,需要自己写一个构造函数
构造函数调用顺序
1.创建子类对象时,会先调用父类的构造函数,用来初始化从父类继承的数据,再调用自己的构造函数,用来初始化自己定义的数据
2.如果在写子类的构造函数的时候,没有用初始化列表调用父类的带参构造,那么系统就会自动调用父类的无参构造函数。
3.在父类实现了一个带参构造,而没有声明一个无参构造,如果子类不调用父类的带参构造时,就会出错。因为父类有了带参构造后,系统就不会提供默认构造函数了。
当创建子类对象时, 构造函数的调用顺序:
静态数据成员对象的构造函数 -> 父类的构造函数 -> 普通的数据成员对象的构造函数 -> 自己的构造函数
注意:无论创建几个对象, 该类的静态成员对象只构建一次, 所以静态成员对象的构造函数只调用1次!!!
#include<iostream>
using namespace std;
class A
{
public:
A()
{
cout << "A的构造函数" << endl;
}
~A()
{
cout << "A的析构函数" << endl;
}
};
class B
{
public:
B()
{
cout << "B的构造函数" << endl;
}
~B()
{
cout << "B的析构函数" << endl;
}
};
class father
{
public:
father()
{
cout << "father的构造函数" << endl;
}
~father()
{
cout << "father的析构函数" << endl;
}
};
class son :public father
{
static A* a;//静态数据成员对象
B b;//普通数据成员对象
public:
son()
{
cout << "son的构造函数" << endl;
}
~son()
{
cout << "son的析构函数" << endl;
}
};
A* son::a = new A;
int main()
{
{
son s;
}
system("pause");
return 0;
}
//程序运行结果
/*
A的构造函数
father的构造函数
B的构造函数
son的构造函数
son的析构函数
B的析构函数
father的析构函数
*/
//如果实例化son类对象,调用构造函数的顺序
//a构造——》father构造——》b构造——》son构造
析构函数调用顺序
子类的析构函数的调用顺序,和子类的构造函数的调用顺序相反!!!
记住,相反即可。
和栈一样,先进后出,先调用构造函数的对象,后调用析构函数
注意:
静态数据对象在程序终止时被销毁,所以:
静态成员的析构函数,在程序结束前,是不会被调用的!
所以对于上面实例化的son类对象,这个对象死亡后,调用析构函数的顺序:
son析构——》b析构——》father析构
a对象在程序结束时才会调用析构函数
4.方法调用
子类对象调用方法时, 先在自己定义的方法中去寻找
如果有, 就调用自己定义的方法,如果找不到, 就到父类的方法中去找
如果有, 就调用父类的这个同名方法,如果还是找不到, 就是发生错误!
四、菱形继承
1、菱形继承:有类A,类B和类C分别继承A,类D继承类B,C。
这样的继承会导致类D继承了两份类A的成员,在类D对象,想要访问类A的成员的时候,会导致访问不明确,因为类B,C各自继承类A的成员
2、解决方法
1.通过类名::成员来调用那个成员
2.通过虚继承:
在继承方式的前面加上关键字virtual,虚继承之后会使在虚继承的类中多一个指针,但是在最后的D类不会在继承两份A的成员,那么就不会有访问的问题
正常:
class A //4字节
{
int a = 1;
};
class B :public A//8字节
{
int c = 2;
};
class C :public A//8字节
{
int d = 3;
};
class D :public B, public C//20字节,两份A的内存
{
int e = 4;
};
虚继承之后:
class A
{
int a = 1;
};
class B :virtual public A//多了一个指针,内存+4,一共为12字节
{
int c = 2;
};
class C :virtual public A//多了一个指针,内存+4,一共为12字节
{
int d = 3;
};
class D :public B, public C//内存为:两个类B,C的指针地址(8字节),3个int型(12字节)[只有1分A的内存了],1个自身的int型(4字节),一共24字节
{
int e = 4;
};