目录
继承可以在已有类的基础上创建新的类,新类可以从一个或多个已有类中继承大部分成员函数和全部数据成员,而且可以重新定义或加进新的数据和函数,从而形成类的层次或等级。其中,已有类称为基类或父类,在它基础上建立的新类称为派生类或子类。
继承与派生的概念
类的继承是新的类从已有类那里得到已有的特性。从另一个角度来看这个问题,从已有类产生新类的过程就是类的派生。类的继承和派生机制较好地解决了代码重用的问题。
关于基类和派生类的关系,可以表述为:派生类是基类的具体化,而基类则是派生类的抽象。
下面我们看代码来了解继承的使用:
#include<iostream>
using namespace std;
class Father{ //基类
public:
int money;
protected:
void setMoney(int money){}
private:
int eyes;
};
// 派生出 son 子类(也可以定义一个子类son 继承于 Father类)
// 子类继承了 父类所有数据成员和部分成员函数
class Son : public Father{
public:
int getMoney(){
// eyes = 2; // 4、公有继承:派生类内部(派生类成员函数中),不能访问基类的 private成员
setMoney(2000); // 5、公有继承:派生类内部, 可以访问基类的 public 和 protected成员
money = 5000;
return 0;
}
};
int main(int argc, char *argv[])
{
Son s;
s.money = 100; //1、公有继承:派生类对象,可以访问基类 public成员
//s.setMoney(1000); //2、公有继承:派生类对象,不能访问基类 protected成员
//s.eyes = 2; //3、公有继承:派生类对象,不能访问基类 private成员
return 0;
}
#include<iostream>
using namespace std;
/************************************************************************
* 文件说明
************************************************************************/
class Father{ //基类
public:
int money;
protected:
void setMoney(int money){}
private:
int eyes;
};
// 派生出 son 子类(也可以定义一个子类son 继承于 Father类)
// 子类继承了 父类所有数据成员和成员函数
class Son : protected Father{
public:
int getMoney(){
//eyes = 2; // 4、保护继承:派生类内部(派生类成员函数中),不能访问基类的 private成员
setMoney(2000); // 5、保护继承:派生类内部, 可以访问基类的 public 和 protected成员
money = 5000;
return 0;
}
};
int main(int argc, char *argv[])
{
Son s;
//s.money = 100; //1、保护继承:派生类对象,不能访问基类 public成员
//s.setMoney(1000); //2、保护继承:派生类对象,不能访问基类 protected成员
//s.eyes = 2; //3、保护继承:派生类对象,不能访问基类 private成员
return 0;
}
#include<iostream>
using namespace std;
/************************************************************************
* 文件说明
************************************************************************/
class Father{ //基类
public:
int money;
protected:
void setMoney(int money){}
private:
int eyes;
};
/*
* public继承时,基类所有成员权限不变
* protected继承时,基类 public变protected,基类其他不变
* private继承时,基类 public和protected 变成 private
* */
class Son : Father{ //默认为私有继承
public:
int getMoney(){
//eyes = 2; // 4、私有继承:派生类内部(派生类成员函数中),不能访问基类的 private成员
setMoney(2000); // 5、私有继承:派生类内部, 可以访问基类的 public 和 protected成员
money = 5000;
return 0;
}
};
int main(int argc, char *argv[])
{
Son s;
//s.money = 100; //1、私有继承:派生类对象,不能访问基类 public成员
//s.setMoney(1000); //2、私有继承:派生类对象,不能访问基类 protected成员
//s.eyes = 2; //3、私有继承:派生类对象,不能访问基类 private成员
return 0;
}
继承的三种方式:
1.公有继承:不会改变基类的访问属性
2.保护继承:会将基类的公有属性变成保护属性
3.私有继承:会将基类的公有,保护属性变成私有属性
注意:
1.三种继承方式,基类的私有成员都是无法被访问的
2.子类会继承父类所有的成员数据和大部分成员函数,构造函数不会被继承,父类中私有成员属性是被编译器给隐藏了因此无法访问,但是确实是继承了的!!所以子类既有自己的空间也有基类的空间
派生类对基类成员的访问规则
基类的成员可以有public、protected、private3中访问属性,基类的成员函数可以访问基类中其他成员,但是在类外通过基类的对象,就只能访问该基类的公有成员。同样,派生类的成员也可以有public、protected、private3种访问属性,派生类的成员函数可以访问派生类中自己增加的成员,但是在派生类外通过派生类的对象,就只能访问该派生类的公有成员。
派生类对基类成员的访问形式主要有以下两种:
- 内部访问:由派生类中新增的成员函数对基类继承来的成员的访问,能访问公有和保护属性成员。
- 对象访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问,但只能访问公有属性的成员。
继承同名成员的处理方式
当子类和父类出现同名的成员,要如何处理里,这时我们如何通过子类对象来访问子类或者父类中同名的成员呢?
如果子类中出现和父类同名的成员,子类的同名成员会隐藏掉父类中所有的同名成员,如果要访问父类同名,需要加作用域,访问子类同名成员,直接访问即可,下面我们来看一段代码
#include<iostream>
using namespace std;
class Father{ //基类
public:
int money;
protected:
void setMoney(int money){}
private:
int eyes;
};
class Son : public Father{
public:
void getMoney(){
//访问自身的公有成员
money = 10;
//访问基类的公有成员
Father::money = 1000;
}
int money;
protected:
void setMoney(int money){}
private:
int eyes;
};
/*
* 派生类中有 与基类同名的成员时,派生类的成员隐藏 基类同名成员。
* 派生类内部或对象,访问基类中的同名成员,必须使用
* 基类名::成员
* */
int main(int argc, char *argv[])
{
Son s;
// 访问派生类自己的成员
s.money = 100;
// 访问基类的公有成员(也就是访问的派生类中继承过来的成员)
s.Father::money = 100;
return 0;
}
派生类的构造函数和析构函数
注意派生类不会继承基类的析构和构造函数,但派生类继承基类后,当创建派生类对象也会调用基类的构造函数,那么父类和子类的构造和析构顺序是谁先谁后呢?我们来看一段代码:
#include<iostream>
using namespace std;
/*
* 1、类中如果声明定义 有参构造,默认构造就不会自动生成
* 2、当定义派生类对象时,派生类会自动调用 基类的默认构造函数
* 3、基类默认构造函数在派生类 构造函数参数初始化列表中,被隐式调用。
* 4、定义派生类对象时,系统先 实例化基类部分,然后执行 子类构造函数函数体实例化 子类对象。
** */
#define pri() cout<<__func__<<"line: "<<__LINE__<<endl;
/*
* __func__ :打印当前执行函数名
* __LINE__ :打印当前程序执行的行数
* */
class Base{
public:
Base(){ pri(); }
Base(int x):x(x){ pri(); }
void setValue(int x){ this->x = x; }
int getValue(){ return x; }
private:
int x;
};
class Subclass : public Base{
public:
Subclass() : Base(){
pri();
}
};
int main(int argc, char *argv[])
{
Subclass son;
return 0;
}
看看运行结果:
可以看出先构造基类,再构造派生类,析构函数的调用顺序与构造函数的调用顺序正好相反,先调用派生类的析构函数,后调用基类的析构函数。
总结注意下面几点:
1、类中如果声明定义 有参构造,默认构造就不会自动生成
2、当定义派生类对象时,派生类会自动调用 基类的默认构造函数
3、基类默认构造函数在派生类 构造函数参数初始化列表中,被隐式调用。
4、定义派生类对象时,系统先 实例化基类部分,然后执行 子类构造函数函数体实例化 子类对象。
派生类构造函数和析构函数的构造语法
派生类构造函数的一般格式为:
派生类名(参数总表):基类名(参数表) {
派生类新增数据成员的初始化语句
}
-----------------------------------------------------------------
含有子对象的派生类的构造函数:
派生类名(参数总表):基类名(参数表0),子对象名1(参数表1),...,子对象名n(参数表n)
{
派生类新增成员的初始化语句
}
注意:
1.当基类构造函数不带参数时,派生类不一定需要定义构造函数;然而当基类的构造函数哪怕只带有一个参数,它所有的派生类都必须定义构造函数,甚至所定义的派生类构造函数的函数体可能为空,它仅仅起参数的传递作用。
2.若基类使用默认构造函数或不带参数的构造函数,则在派生类中定义构造函数时可略去“:基类构造函数名(参数表)
”,此时若派生类也不需要构造函数,则可不定义构造函数。
类型兼容规则
在基类和派生类对象之间存有类型兼容关系,基类和派生类对象之间的赋值兼容规则是指在需要基类对象的任何地方,都可以用子类的对象代替。下面我们总结两句话来记住这个知识点
可以向上隐式转换(小范围转大范围这是抽像的说法,也就是派生类转基类)
不可以向下隐式转换(大范围转小范围,也就是基类转派生类)
具体表现在下面几个方面:
class Base{
·····
};
class Derived: public Base{
·····
};
1.派生类对象可以赋值给基类对象,即用派生类对象中从基类继承来的数据成员,逐个赋值给基类对象的数据成员
Base b;
Derived d;
b = d;
2.派生类对象可以初始化基类对象的引用。
Derived d;
Base &br = d;
3.派生类对象的地址可以赋值给指向基类对象的指针
Derived d;
Base *bp = &d;
我们看一段综合示例代码:
#include<iostream>
using namespace std;
#define pri() cout<<__func__<<"line: "<<__LINE__<<endl;
class Base{
public:
Base(int x):x(x){ pri(); }
void setValue(int x){ this->x = x; }
int getValue(){ return x; }
private:
int x;
};
class Subclass : public Base{
public:
Subclass(int x) : x(x), Base(12) {
pri();
}
int getValue(){ return x; }
private:
int x;
};
int main(int argc, char *argv[])
{
Base obj1(121);
Subclass obj2(3);
cout << obj2.Base::getValue() << endl;
//小范围转大范围(抽象说法)
//向上隐式转换,将;派生类对象隐式转换成 基类类型,只保留了派生类对象中基类部分的内容,然后进行赋值操作
obj1 = obj2;
cout << obj1.getValue() << endl;
Base *q = &obj2;
Base &obj4 = obj2;
//大范围转小范围(抽象说法)
//不允许向下隐式转换,因为obj1对象空间中没有 派生类部分的数据
//obj2 = obj1;
// Subclass *p = &obj1;
// Subclass &obj3 = obj1;
return 0;
}
多继承
C++中允许一个类继承多个类
class 子类名:继承方式 父类1,继承方式 父类2......
但是多继承可能会引发二义性,因为父类中可能会有同名成员出现,那么这时子类去访问父类成员就需要加上作用域区分。
#include<iostream>
using namespace std;
/************************************************************************
* 文件说明
************************************************************************/
#define pri() cout<<__func__<<" line: "<<__LINE__<<endl;
class man{
public:
man(){ pri(); }
~man(){ pri(); }
int x = 12;
};
class wolf{
public:
wolf(){ pri(); }
~wolf(){ pri(); }
int x = 13; //C++11标准后,可以直接在类体中定义初始化 成员变量
};
/*
* 多重继承: 派生类有多个直接继承的基类
* 多重继承构造顺序:
* 先左起第一个基类构造,再左起第二个基类构造.... 最后派生类构造
* 多重基类析构顺序:
* 先派生类析构,再右起第一个基类析构....
* */
class wolfman : public wolf, public man{
public:
wolfman(){ pri(); }
~wolfman(){ pri(); }
};
int main(int argc, char *argv[])
{
wolfman obj;
// 错误,有歧义,又称为 名字的二义性
//cout << obj.x << endl;
//使用 作用域访问符,解决多重继承 名字二义性
cout << obj.man::x << endl;
cout << obj.wolf::x << endl;
return 0;
}
注意:
多重继承构造顺序:
先左起第一个基类构造,再左起第二个基类构造.... 最后派生类构造
多重基类析构顺序:
先派生类析构,再从右边起第一个基类析构....
虚基类
虚基类的作用:如果一个类有多个直接基类,而这些直接基类又有一个共同的基类,则在最低层的派生类中会保留这个间接的共同基类数据成员的多份同名成员。在访问这些同名成员时,必须在派生类对象名后增加直接基类名,使其唯一地标识一个成员,以免产生二义性。
#include<iostream>
using namespace std;
/************************************************************************
* 文件说明
************************************************************************/
#define pri() cout<<__func__<<" line: "<<__LINE__<<endl;
class animal{
public:
animal(){ pri(); }
~animal(){ pri(); }
int x = 100;
};
class man : virtual public animal{
public:
man(){ pri(); }
~man(){ pri(); }
};
class wolf : virtual public animal{
public:
wolf(){ pri(); }
~wolf(){ pri(); }
};
// 多重继承时,虚拟继承的基类 优先被构造
class wolfman : public wolf, virtual public man{
public:
wolfman(){ pri(); }
~wolfman(){ pri(); }
};
int main(int argc, char *argv[])
{
wolfman obj;
// 错误:有歧义,路径二义性: 因为animal基类被构造 2次,内存基类部分有两份不同的空间,也就是 x存在于两个地址空间
// cout << obj.x << endl;
// 路径二义性解决方式 1:作用域访问符
//cout << obj.wolf::x << endl;
//cout << obj.man::x << endl;
// 路径二义性解决方式 2:设置虚基类(虚继承的共同基类)
cout << obj.x << endl;
return 0;
}
如果我们不使用虚基类,那么基类animal就会出现两份,导致二义性,在C++中,可以通过将这个公共的基类声明为虚基类来解决这个问题。这就要求从基类派生新类时,使用关键字virtual
将基类声明为虚基类。语法如下
class 派生类:virtual 继承方式 类名{
·····
};
虚基类的初始化与一般的多继承的初始化在语法上是一样的,但构造函数的调用顺序不同。在使用虚基类机制时应该注意以下几点:
1.如果在虚基类中定义有带形参的构造函数,并且没有定义默认形式的构造函数,则整个继承结构中,所有直接或间接的派生类都必须在构造函数的成员初始化列表中列出对虚基类构造函数的调用,以初始化在虚基类中定义的数据成员。
2.建立一个对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。该派生类的其他基类对虚基类构造函数的调用都被自动忽略。
3.若同一层次中同时包含虚基类和非虚基类,应先调用虚基类的构造函数,再调用非虚基类的构造函数,最后调用派生类构造函数。
4.对于多个虚基类,构造函数的执行顺序仍然是先左后右,自上而下。
5.若虚基类由非虚基类派生而来,则仍然先调用基类构造函数,再调用派生类的构造函数。