C++继承
继承
一个新类继承了原有类的属性和方法,并增加了自己的新属性和新方法,称之为派生类,派生类就继承了原有类(基类)。继承表达了对象的一般与特殊的关系。特殊类的对象具有一般类的全部属性和服务。
(1)相关概念
1、被继承的已有类称为基类(或父类)。
2、派生出的新类称为派生类(或子类)。
3、直接参与派生出某类的基类称为直接基类。
4、基类的基类甚至更高层的基类称为间接基类。
(2)作用:
1、继承性大大简化了对问题的描述,大大提高了程序的可重用性,从而提高了程序设计、修改、扩充的效率。
2、当定义了一个类后,又需定义一个新类,这个新类与原来的类相比,只是增加或修改了部分属性和操作,这时可以用原来的类派生出新类,新类中只需描述自己所特有的属性和操作。
(3)特征:
1、子类拥有父类的所有成员变量和成员函数
2、子类可以拥有父类没有的方法和属性
3、子类就是一种特殊的父类
4、子类对象可以当作父类对象使用
5、基类和派生类都有独立的作用域
6、友元关系和继承:A是B的友元类,C是A的派生类,但C不是B的友元类
(4)定义语法
一、单继承时派生类的定义方式
class 派生类名称:继承方式 基类名称
{
public:
成员声明1;
protected:
成员声明2;
private:
成员声明3;
}
//举例如下:
Class Base
{
public:
Base()://构造函数
{}
~Base()//析构函数
{}
};
Class Derived:public Base
{
public:
Derived()://构造函数
{}
~Derived()//析构函数
{}
};
二、多继承时派生类的定义方式
class 派生类名称:继承方式1 基类名称1,继承方式2 基类名称2...
{
public:
成员声明1;
protected:
成员声明2;
private:
成员声明3;
}
//举例如下:
Class Base1
{
public:
Base1()://构造函数
{}
~Base1()//析构函数
{}
};
Class Base2
{
public:
Base2()://构造函数
{}
~Base2()//析构函数
{}
};
Class Derived:public Base1,protected Base2
{
public:
Derived()://构造函数
{}
~Derived()//析构函数
{}
};
继承和组合
类之间的关系:has-A,uses-A 和 is-A
1、has-A 包含关系,用以描述一个类由多个“部件类”构成。实现has-A关系用类成员表示,即一个类中的数据成员是另一种已经定义的类。
2、uses-A 一个类部分地使用另一个类。通过类之间成员函数的相互联系,定义友员或对象参数传递实现。
3、is-A 机制称为“继承”。关系具有传递性,不具有对称性。
组合(has-A):将一个类作为另一个类的对象成员,一种委托机制。
//举例如下:
Class Base
{
public:
Base()://构造函数
{}
void ShowB()
{
cout << "Show()......" << endl;
}
~Base()//析构函数
{}
};
Class Derived
{
public:
Derived()://构造函数
{}
void ShowD()
{
b_.ShowB();
}
~Derived()//析构函数
{}
private:
Base b_;
};
特征:
(1)无论是继承与组合,本质上都是把子对象放在新类型中,两者都是使用构造函数的初始化列表去构造这些子对象。
(2)组合通常中是在希望新类内部具有已存在的类的功能时使用,只是使用他的功能而已,而不是希望已存在的类作为它的接口。组合通过嵌入一个对象以实现新类的功能,而新类用户看到的是新定义的接口,而不是来自老类的接口 (has-a);
eg、以汽车对象为例,汽车作为对象,具有行驶 的功能:汽车的行驶,需要借助(1)引擎的启动、加减速、停止等。(2)需要轮胎的滚动
Derive()
{
engine.start();
wheel.roll();
}
汽车仅仅是借助这两个对象的提供的接口,利用其功能,并不是把他们当做自己的接口对外提供,所以属于组合关系。
(3)如果希望新类与已存在的类有相同的接口(在这个基础上增加自己的成员),也就是继承 (is a)
class Base
{
public:
Base() : x_(0)
{
}
int GetBaseX() const
{
return x_;
}
void Show()
{
cout << "Base::Show()......" << endl;
}
int x_;
};
class Derived:public Base
{
public:
Derived() :x_(0)
{
}
int GetDerivedX() const
{
return x_;
}
void Show(int n)
{
cout << "Derived::Show(): " << n << endl;
}
void Show()
{
cout << "Derived::Show()......" << endl;
}
int x_;
};
class Test
{
public:
Base b;
int n_;
};
cout << sizeof(Derived) << endl;//继承类大小,8字节
cout << sizeof(Test) << endl;//组合类大小,8字节
接口继承与实现继承
我们将类的公有成员函数称之为接口;
(1)公有继承,基类的公有成员函数在派生类中仍然是共有的,换句话就是基类的接口成为了派生类的接口,因而将他称为接口继承。
(2)实现继承,对于私有、保护继承,派生类不继承基类的接口。派生类将不再支持基类的公有接口,他希望能重用基类的实现而已,因而将他称为实现继承。
公有、私有、保护继承
(1)关键词public后面声明,他们是类和外部的接口,任何外部函数都可以访问公有类型数据和函数。
(2)关键词private后面声明,只允许本类的函数访问,外部的任何函数都不能访问。
(3)关键词protected后面声明,与private类似,其差别变现在继承和派生时对派生类的影响。
Class Base
{
public:
int x_;
protected:
int y_;
private:
int z_;
};
Class Derived:public Base
{
public:
void Test()
{
x_ = 10;
y_ = 20;
z_ = 30;//Error,不可直接访问
}
private:
int a_;
};
访问关系表如下:
继承和重定义
对基类成员函数的重定义分为两种:
(1)overwrite
1、与基类成员函数名相同、参数不同(此时,不论有无virtual关键字,基类的函数将被隐藏)
#include<iostream>
using namespace std;
class Base
{
public:
Base() : x_(0)
{}
int GetBaseX() const
{
return x_;
}
void Show()
{
cout << "Base::Show()......" << endl;
}
int x_;
};
class Derived:public Base
{
public:
Derived() :x_(0)
{}
int GetDerivedX() const
{
return x_;
}
void Show(int n)
{
cout << "Derived::Show(): " << n << endl;
}
/*void Show()
{
cout << "Derived::Show()......" << endl;
}*/
int x_;
};
int main()
{
Derived d;
//这里调用的是Derived类自身的x_,隐藏了基类的X_
d.x_ = 10;
//cout << d.GetBaseX() << endl;//输出0
//cout << d.GetDerivedX() << endl;//输出10
//Error ,基类的Show被隐藏了,
//所以不接受没有参数的基类的Show
d.Show();
return 0;
}
2、与基类完全相同(但是基类函数没有virtual关键字。此时,基类的函数被隐藏)
//情况2:与基类完全相同
d.Show();
d.Base::Show();//只能加上Base::才能访问基类的Show函数
不能自动继承的成员函数
(1)构造函数(包括拷贝构造函数)
(2)析构函数
(3)=运算符
构造函数和析构函数不能自动继承?
构造函数和析构函数是用来处理对象的创建和析构的,它们只知道对在它们的特殊层次的对象做什么。所以,在整个层次中的所有的构造函数和析构函数都必须被调用
赋值运算符重载函数不能被继承?
1,每一个类对象实例在创建的时候,如果用户没有定义“赋值运算符重载函数”,那么,编译器会自动生成一个隐含和默认的“赋值运算符重载函数”。
2、如果派生类中声明的成员与基类的成员同名,那么,基类的成员会被覆盖,哪怕基类的成员与派生类的成员的数据类型和参数个数都完全不同。显然,派生类中的赋值运算符函数名operator =和基类中的operator =同名,所以,基类中的赋值运算符函数 被派生类中的隐含的赋值运算符函数所覆盖
构造函数
1. 构造函数定义:
在对象被创建时使用特定的值构造对象,将对象初始化为一个特定的初始状态。声明构造函数时,只需要对本类中新增成员进行初始化,对继承来的基类成员函数的初始化(调用基类构造函数完成),如果基类有默认构造函数,继承类可以不提供初始化,默认调用基类构造函数初始化。
2. 构造函数的形式:
1) 函数名与类名相同;
2) 不能定义返回值类型,也不能有return语句;
3) 可以有形式参数,也可以没有形式参数;
4) 可以是内联函数(只要是类的成员函数,都可以放在类内定义,在类内定义的函数,编译器一般视作内联);
5) 可以重载(eg、无参和有参构造);
构造函数可以是内联函数
1、只要是类的成员函数,都可以放在类内定义,在类内定义的函数,编译器一般视作内联
2、inline只是一种申请,其目的是为了提高函数的执行效率(速度)。究竟是不是内联还得由编译器决定,自动地取消不值得的内联。一般情况下,构造函数比较小的情况下,不管你是否指定其为内联函数,C++编译器会自动将其置为内联,如果函数太大,你即使将其指定为内联函数系统也会不理的。因为这会使程序过大。if ((构造函数的函数体写在类里面 || 定义构造函数时加上inline)&& 函数体合适用内联)
{
构造函数是内联函数 = true;
}
else
{
构造函数是内联函数 = false;
}
3. 构造原则
1). 如果子类没有定义构造方法,则调用父类的无参数的构造方法。
2). 如果子类定义了构造方法,不论是无参数还是带参数,在创建子类的对象的时候,首先执行父类无参数的构造方法,然后执行自己的构造方法。
3). 在创建子类对象时候,如果子类的构造函数没有显示调用父类的构造函数,则会调用父类的默认无参构造函数。
4). 在创建子类对象时候,如果子类的构造函数没有显示调用父类的构造函数且父类自己提供了无参构造函数,则会调用父类自己的无参构造函数。
5). 在创建子类对象时候,如果子类的构造函数没有显示调用父类的构造函数且父类只定义了自己的有参构造函数,则会出错(如果父类只有有参数的构造方法,则子类必须显示调用此带参构造方法)。
6). 如果子类调用父类带参数的构造方法,需要用初始化父类成员对象的方式
4. 默认构造函数
- 定义:
如果程序中未定义构造函数,编译器将在需要时自动生成一个默认构造函数 - 性质
1) 参数列表为空,不为数据成员设置初始值;
2) 如果类内定义了成员的初始值,则使用内类定义的初始值;
3) 如果没有定义类内的初始值,则以默认方式初始化;
4) 基本类型的数据默认初始化的值是不确定的。
5)一个类只能有一个默认构造函数;
6)如果程序中已定义构造函数,默认情况下编译器就不再隐含生成默认构造函数。如果此时依然希望编译器隐含生成默认构造函数,可以使用“=default”。
//使用“=default”
class Clock {
public:
Clock() = default;
Clock(int newH , int newM , int newS ):hour_(newH), minute_(newM), second_(newS)
{ }
private:
int hour_, minute_, second_;
};
//一个类只能有一个默认构造函数
#include<iostream>
using namespace std;
class Clock {
public:
Clock()
{
hour_ = 0;
minute_ = 0;
second_ = 0;
}
Clock(int newH = 0, int newM = 0, int newS = 0)
{
hour_ = newH;
minute_ = newM;
second_ = newS;
}
private:
int hour_, minute_, second_;
};
int main()
{
Clock c1;//ERROR
return 0;
}
问题:什么情况下,需要在派生类初始化列表中初始化,不能在构造函数体内初始化(应该叫做赋值)?
(1)const成员:因为定义的时候需要初始化
(2)引用成员:因为定义的时候需要初始化
(3)基类没有默认构造函数,在初始化列表中调用基类构造函数完成
(4)派生类里面的对象成员没有默认构造函数
//基类没有默认构造函数,在初始化列表中调用基类构造函数完成
#include<iostream>
using namespace std;
class Base
{
public:
Base(int b):b_(b)
{
cout << "Base()......" << endl;
}
~Base()
{
cout << "~Base()......" << endl;
}
//private:
int b_;
};
class Derived :public Base
{
public:
Derived(int d):d_(d)//Error,因为基类自己定义了构造函数,所以没有了默认构造函数,所以这里需要对自定义构造函数初始化
{
cout << "Derived()......" << endl;
}
~Derived()
{
cout << "~Derived()......" << endl;
}
//private:
int d_;
};
int main()
{
Derived d(10);
cout << d.b_ << " " << d.d_ << endl;
return 0;
}
//解决方法:Derived构造函数初始化列表进行初始化
Derived(int d):d_(d),Base(100)
{
cout << "Derived()......" << endl;
}
说明:虽然在初始化列表中,Base初始化在后,但是首先调用基类的构造函数初始化,然后调用派生类构造函数初始化
5. 派生类对象构造次序
(1)继承的基类的构造函数(2)派生类的对象成员的构造函数(3)派生类自身的构造函数
class ObjectD
{
public:
ObjectD()
{
cout << "ObjectD()......" << endl;
}
~ObjectD()
{
cout << "~ObjectD()......" << endl;
}
};
class Base
{
public:
Base(int b):b_(b)
{
cout << "Base()......" << endl;
}
~Base()
{
cout << "~Base()......" << endl;
}
//private:
int b_;
};
class Derived :public Base
{
public:
Derived(int d):d_(d),Base(100)
{
cout << "Derived()......" << endl;
}
~Derived()
{
cout << "~Derived()......" << endl;
}
int d_;
ObjectD objd_;
};
Derived d(10);
注意:
当基类中也有定义的类成员对象,同样遵从上述顺序,先构造成员对象,在构造基类。
class ObjectB
{
public:
ObjectB()
{
cout << "ObjectB()......" << endl;
}
~ObjectB()
{
cout << "~ObjectB()......" << endl;
}
};
class Base
{
public:
Base(int b):b_(b)
{
cout << "Base()......" << endl;
}
~Base()
{
cout << "~Base()......" << endl;
}
//private:
int b_;
ObjectB objb_;
};
6. 基类里面自定义拷贝构造函数
基类的成员对象也应在拷贝构造函数初始化列表中初始化,同时基类的数据成员也需要在拷贝构造函数初始化列表中初始化,否则会对其设置随机数,因为Base类里面没有了默认构造函数对其初始化。
class Base
{
public:
Base(int b):b_(b), objb(111)
{
cout << "Base()......" << endl;
}
Base(const Base& other):objb(other.objb),b_(other.b_)
{}
~Base()
{
cout << "~Base()......" << endl;
}
//private:
int b_;
ObjectB objb;
};
//注释掉,b_(other.b_)
Base(const Base& other):objb(other.objb)//,b_(other.b_)
{}
//输出
Base b1(100);
Base b2(b1);
cout << b2.b_ << endl;
2、取消注释掉,b_(other.b_)
注意:
继承类里面自定义拷贝构造函数
//和构造函数一样,需要对这三个进行拷贝初始化
Derived(const Derived& other):d_(other.d_),objd(other.objd),Base(other)
{}
静态成员与继承
class Base
{
public:
static int b_;
};
class Derived :public Base
{
};
int Base::b_ = 100;
int main()
{
Base b;
cout << b.b_ << endl;//不建议这么使用,static为类对象公有的,不仅限某个类
cout << Base::b_ << endl;
cout << Derived::b_ << endl;
return 0;
}
转换与继承
派生类到基类的转换
(1)当派生类以public继承基类时,编译器可自动执行转换(向上转型upcasting(派生->基类) 安全转换)
1、派生类对象指针自动转化为基类对象指针
2、派生类对象引用自动转化为基类对象引用
3、派生类对象自动转换为基类对象(特有的成员消失)
class Employee
{
public:
Employee(const string& name,const int age,const int deptno):name_(name),age_(age),deptno_(deptno)
{}
private:
string name_;
int age_;
int deptno_;
};
class Manager :public Employee
{
public:
Manager(const string& name, const int age, const int deptno, int level) :Employee(name, age, deptno), level_(level)
{
}
private:
int level_;
};
class Manager2 :private Employee
{
public:
Manager2(const string& name, const int age, const int deptno, int level) :Employee(name, age, deptno), level_(level)
{
}
private:
int level_;
};
Employee el("zhangsan", 25, 20);
Manager ml("lisi", 38, 20, 10);
Employee* pe;
Manager* pm;
pe = ⪙
pm = &ml;
//public继承方式,派生类指针可以转化为基类指针。将派生类对象看成基类对象
pe = &ml;
//基类指针无法转化为派生类指针。无法将基类对象看成派生类对象
//pm = ⪙
//派生类对象可以转化为基类对象。将派生类对象看成基类对象
//这里调用=运算符,派生类对象赋值给基类对象。
//对象切割object slicing,特有成员(level)就要去除
el = ml;//public继承方式,直接赋值
(2)当派生类以private/protected方式继承基类时
1、派生类对象指针(引用)转换为基类对象指针(引用)需要进行强制类型转换,不能用static_cast,需要用reinterpret_cast
2、不能把派生类对象强制转换为基类对象
//将继承方式改为private,输出证明
Manager2* pm2;
pm2 = &m2;
//Error, private、protected继承方式,派生类指针不可以自动转化为基类指针
//pe = &m2;
//手动转化两种方式
pe = (Employee*)pm2;//(Employee*)强制转化
pe = reinterpret_cast<Employee*>(pm2);//reinterpret_cast转化
//不能把派生类对象强制转换为基类对象,也无法强制转化
//el = m2; Error
//el = (Employee)m2; Error
//el = reinterpret_cast<Employee>(m2);Error
基类到派生类的转换
(1)基类对象指针(引用)可用强制类型转换为派生类对象指针(引用),而基类对象无法执行这类转换
(2)向下转型不安全,没有自动转换机制
pm = static_cast<Manager*>(pe);
注意:
基类对象到派生类对象的转换(没有意义):两种方法
1、转换构造函数:将其他类型转换为类类型
class Employee
{
public:
Employee(const string& name, const int age, const int deptno) :name_(name), age_(age), deptno_(deptno)
{}
private:
string name_;
int age_;
int deptno_;
};
class Manager :public Employee
{
public:
Manager(const string& name, const int age, const int deptno, int level) :Employee(name, age, deptno), level_(level)
{
}
//转换构造函数,Employee -》 Manager
Manager(const Employee& other):Employee(other),level_(-1)
{}
private:
int level_;
};
int main()
{
Employee e1("zhangsan", 25, 20);
Manager m1("lisi", 38, 20, 10);
m1 = e1;
return 0;
}
2、类型转换运算符重载:将类类型转换为其他类型
#include<iostream>
using namespace std;
class Manager;
class Employee
{
public:
Employee(const string& name, const int age, const int deptno) :name_(name), age_(age), deptno_(deptno)
{}
operator Manager();
private:
string name_;
int age_;
int deptno_;
};
class Manager :public Employee
{
public:
Manager(const string& name, const int age, const int deptno, int level) :Employee(name, age, deptno), level_(level)
{
}
private:
int level_;
};
//类型转换运算符重载
Employee::operator Manager()
{
return Manager(name_, age_, deptno_, -1);
}
int main()
{
Employee e1("zhangsan", 25, 20);
Manager m1("lisi", 38, 20, 10);
m1 = e1;
return 0;
}
多重继承
- 单重继承——一个派生类最多只能有一个基类
- 多重继承——一个派生类可以有多个基类
1、class类名:继承方式 基类1,继承方式 基类2,…
2、二义性,多个基类可能包含同名变量或者函数
- 多重继承二义性解决:
基类名::数据成员名(或者成员函数(参数列表))
明确指明要访问定义在哪个基类中的成员
#include<iostream>
using namespace std;
class Furniture
{
public:
Furniture(int weight):weight_(weight)
{}
int weight_;
};
class Bed:public Furniture
{
public:
Bed(int weight) :Furniture(weight)
{
}
void Sleep()
{
cout << "Sleep()......" << endl;
}
};
class Sofa:public Furniture
{
public:
Sofa(int weight) :Furniture(weight)
{
}
void WatchTV()
{
cout << "WatchTV()......" << endl;
}
};
class SofaBed :public Bed, public Sofa
{
public:
SofaBed(int weight) :Bed(weight), Sofa(weight)
{
FoldIn();
}
void FoldOut()
{
cout << "FoldOut()......" << endl;
}
void FoldIn()
{
cout << "FoldIn()......" << endl;
}
};
int main()
{
SofaBed sofabed(10);
//重量这个成员被继承两次,一个物体两个重量,还是不符合逻辑
sofabed.Bed::weight_ = 10;
sofabed.Sofa::weight_ = 20;
sofabed.WatchTV();
sofabed.FoldOut();
sofabed.Sleep();
return 0;
}
虚继承与虚基类
- 定义
当派生类从多个基类派生,而这些基类又共同基类,则在访问此共同基类中的成员时,将产生冗余,并有可能因冗余带来不一致性。(在第一级继承时就要将共同基类设计为虚基类)
虚基类声明:
class B1:virtual public B
- 作用
1、主要用来解决多继承对同一基类继承的二义性问题;
2、为最远的派生类提供唯一的基类成员,不重复产生多次拷贝。 - 虚基类及其派生类构造函数:
1、建立对象时所指定的类称为最远派生类。
2、虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。
(在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中为虚基类的构造函数列出参数。如果未列出,则表示调用该虚基类的默认构造函数。)
3、在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数,其他类对虚基类构造函数的调用被忽略
以上节:沙发、窗、沙发床之间继承关系为例 (钻石继承体系(菱形))
class Furniture
{
public:
int weight_;
};
class Bed :virtual public Furniture
{
public:
void Sleep()
{
cout << "Sleep()......" << endl;
}
};
class Sofa :virtual public Furniture
{
public:
void WatchTV()
{
cout << "WatchTV()......" << endl;
}
};
class SofaBed :public Bed, public Sofa
{
public:
void FoldOut()
{
cout << "FoldOut()......" << endl;
}
void FoldIn()
{
cout << "FoldIn()......" << endl;
}
};
int main()
{
SofaBed sofabed;
sofabed.weight_ = 10;
return 0;
}
调用顺序:(1)Furniture构造函数,(2)Bed(weight)(3)Sofa(weight)这两个根据继承的顺序依次调用构造函数
虚继承的内存结构举例说明
虚继承和虚函数是完全无相关的两个概念。
虚继承底层实现原理与编译器相关,一般通过虚基类指针(virtual base table pointer)和虚基类表(virtual base table)实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
对比虚函数的实现原理:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)。具体如下:
1、虚基类依旧存在继承类中,只占用存储空间;虚函数不占用存储空间。
2、虚基类表存储的是虚基类相对直接继承类的偏移;而虚函数表存储的是虚函数地址。
说明:
1、DD继承了B1,B2虚基类
2、B1,B2继承了BB类
#include<iostream>
using namespace std;
class BB
{
public:
int bb_;
};
class B1 :virtual public BB
{
public:
int b1_;
};
class B2 :virtual public BB
{
public:
int b2_;
};
class DD : public B1, public B2
{
public:
int dd_;
};
int main()
{
cout << sizeof(BB) << endl;//4 字节
cout << sizeof(B1) << endl;//12 字节
cout << sizeof(DD) << endl;//24 字节
B1 b1;
cout << &b1 << endl;
cout << &b1.bb_ << endl;
cout << &b1.b1_ << endl;
//vbptr为虚基类表指针,同样vbptr自身也需要有被存储的地址,即指向指针的指针
long** p;
p = (long**)&b1;
//cout << &p[0] << endl;//&p[0],就是&b1,即vbptr
cout << p[0][0] << endl;// 输出0
cout << p[0][1] << endl;//输出 8
return 0;
}
1、virtual base table (虚基类表)
(1)本类地址与虚基类指针地址的差
(2)虚基类地址与虚基类表指针地址的差
2、virtual base table pointer(vbptr )(虚基类表指针)
DD dd;
cout << &dd << endl;
cout << &dd.bb_ << endl;
cout << &dd.b1_ << endl;
cout << &dd.b2_ << endl;
cout << &dd.dd_ << endl;
p = (long**)ⅆ
cout << p[0][0] << endl;// 输出0
cout << p[0][1] << endl;// 输出20
cout << endl;
cout << p[2][0] << endl;// 输出0
cout << p[2][1] << endl;// 输出12
注意:
//通过指针来访问虚基类里面的数据成员,不是直接访问,而是间接访问的,
//运行时刻判断,而不是编译时刻决定
//通过虚基类表指针,找到虚基类地址与虚基类表指针的差,这里是20,从而找到虚基类的地址
BB* bb1;
bb1 = ⅆ
bb1->bb_;