7.继承
继承是子类(派生类)自动地共享父类(基类)中定义的属性和方法的机制。通过不同程度的抽象原则,可以得到一般的类——父类,较为特殊的类——子类,子类继承父类的属性和方法。
7.1继承
定义派生类
要在定义派生类是指定该中继承关系,需添加一个冒号,后跟关键字public以及父类名称。
实例:
#include <iostream>
using namespace std;
//基类
class A
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
// protected: //此处为受保护限定符
int width;
int height;
};
//派生类
class B :public A
{
public:
int getArea()
{
return width * height;
}
};
int main()
{
B b1;
b1.setWidth(1);
b1.setHeight(2);
//输出面积
cout << "area = " << b1.getArea() <<endl;
return 0;
}
//输出:
//area = 2
访问控制和继承
派生类可以访问基类中的所有非私有成员。
访问权限:
访问 | public | protected | private |
---|---|---|---|
同一个类 | yes | yes | yes |
派生类 | yes | yes | no |
外部的类 | yes | no | no |
一个派生类继承了所有基类的方法,但下列情况除外:
- 基类构造函数、析构函数、拷贝构造函数
- 基类的重载运算符
- 基类的友元函数
继承类型(继承权限)
当一个类派生自基类时,该基类可以被继承为public、protected或private几种类型。
我们几乎不使用protected或private继承,通常使用public继承。
判断能否访问的三看方法
- 看访问的位置,是派生类内,还是类外
- 看继承方式
- 看基类里面的访问属性
不同类型继承的规则:
公有继承(public):当一个类派生自公有基类时,基类的公有成员是派生类的公有成员,基类的保护成员是派生类的保护成员,基类的私有成员不能被派生类直接访问,但可以通过调用基类的公有成员和保护成员进行访问。
保护继承(protected):当一个类派生自保护基类时,基类的公有成员和保护成员将成为派生类的保护成员。
私有继承(private):当一个类派生自私有基类时,基类的公有成员和保护成员将成为派生类的私有成员。
去除个别成员
如果进行private或protected继承,基类成员的访问级别在派生类中比在基类中更受限:
class Base
{
public:
int size() const
{
return n;
}
protected:
int n;
};
class Derived :private Base
{
...
};
size在Base中为Public,在Derived为private。为了使size在Derived中为public,可以在Derived的public部分添加一个using声明。如此修改,则可以使size成员可以被用户访问。
class Derived :private Base
{
public:
using Base::size;
protected:
using Base::n;
}
using的声明的使用形式与命名空间的用法相似,唯一差异是在作用域解析符用类名代替命名空间名。
派生类中可以恢复数据成员的访问级别,但不能使访问级别比基类中原来指定的更严格或更宽松。
注意:using声明只适用于private继承,也就是说,只能访问在基类中为公有,在派生类中为私有的数据成员,不可以使用using声明访问基类的私有成员。
class Base
{
public:
protected:
private:
int x;
};
class Derived :public Base
{
public:
using Base::x; //error:不能访问基类的私有成员
int get()
{
return x;
}
};
默认继承保护级别
使用struct和class保留字定义的类既有不同的默认访问级别,同样,继承中使用不同的保留字派生类访问级别也不同,使用class定义的派生类默认具有private继承,使用struct定义的类默认具有public继承。
友元关系与继承
友元关系不能继承,基类的友元对派生类没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问关系,派生类不能访问授予友元关系的类。
继承与静态成员
如果基类定义了静态成员,在整个继承层次中只有一个这样的成员,无论派生了多少派生类,每个static成员都只有一个实例。
如果static成员在基类中定义为private,则派生类不能访问,可以使用作用域操作符(::)或使用点或箭头成员访问符。
转换与继承
每一个派生类中都包含了一个基类部分,意味着使用基类对象一样在派生类对象上执行操作,所以存在从派生类类型引用到基类类型的自动装换,也就是说,可以将派生类对象的引用装换成基类的引用,对指针也类似。
virtual与其他成员函数
C++中的函数调用默认不使用动态绑定。要触发动态绑定(多态知识),要满足两个条件:
- 只有指定为虚函数的成员函数才可以进行动态绑定,成员函数默认是非虚函数,非虚函数不进行动态绑定
- 必须通过基类类型的引用或指针 进行函数调用
可以在运行时确定virtual函数的调用
将基类类型的引用或指针绑定到派生类对象对基类对象没有影响,对象本身不会改变,仍为派生类对象。对象的类型的实际类型可能不同于对象引用或指针的静态类型,这是C++动态绑定的关键。
派生类到基类转换的可访问性
如果是public继承,则用户代码和后代类都可以使用派生类到基类的转换。如果是protectd和private继承,则用户代码不能将派生类对象转换为基类对象,如果是private继承,则private继承的派生类对象不能转换成基类对象,如果是protected继承,则后续的派生类对象可以转换成基类对象。
派生类到基类的转换
- 派生类对象对基类对象赋值
如果有一个派生类的对象,可以使用它来给基类对象进行赋值或初始化。
ClassA obj_a; //定义基类ClassA对象obj_a
ClassB obj_b; //定义派生类ClassB对象obj_b
obj_a = obj_b; //用派生类对象obj_b对基类对象obj_a赋值
派生类对象对基类对象赋值时 ,将基类数据成员赋值,派生类新增的数据成员值被舍弃,不存在对成员函数的赋值。
注意:
- 赋值后不能通过基类对象obj_a访问派生类对象obj_b,因为obj_b与obj_a的成员不同
- 派生关系是单向的,不可逆。ClassB是ClassA的派生类,只能用派生类对象给基类对象赋值,不能用基类对象给派生类对象赋值,原因是基类对象不包括派生类对象的全部成员,无法进行赋值,同理,同一基类的不同派生类对象之间不能赋值。
- 派生类对象可以代替基类对象向基类对象的引用进行赋值或初始化
ClassA obj_a; //定义基类ClassA对象obj_a
ClassB obj_b; //定义派生ClassB对象obj_b
ClassA &refa = obj_a; //定义基类ClassA对象变量refa,并用obj_a来初始化
引用refa是obj_a的别名,refa和obj_a共享一段内存单元。可以派生类对象来初始化引用refa,将上面最后一行代码改为:
//定义基类ClassA对象的引用变量refa,并用派生类ClassB对象obj_b进行初始化
ClassA &refa = obj_b;
- 如果函数的参数是基类对象或基类对象的引用,函数调用的实参可以是派生类对象
void func(ClassA &ref) //定义基类ClassA的的对象的引用
{
cout << ref.num << endl; //输出该引用所代表的的对象的数据成员num
}
函数的形参是对基类ClassA对象的引用,本来实参应该为ClassA的对象。**由于派生类对象与基类对象赋值兼容,派生类对象能自动转换类型。**在调用func函数时用派生类ClassB的对象obj_b作为实参:
func(obj_b); //输出派生类ClassB对象obj_b的数据成员num
- 派生类对象的地址可以赋值给基类指针变量
ClassA obj_a;
ClassB obj_b;
ClassA *a = &obj_a;
ClassB *b = a; //error:不能从基类指针转换成派生类指针
ClassB* b = &obj_b;
ClassA* a = b; //派生类指针转换成基类指针
obj_a.setnum(1);
obj_b.setnum(2);
cout << a->getnum() << endl; //输出:2,派生类转换成了基类,但输出的还是派生类中的数据成员
7.2派生类
构造函数和拷贝构造函数
每个派生类对象由派生类定义的(非static成员)加上一个或多个基类对象构成。
构造函数和拷贝构造函数不能被继承,每个类定义自己的构造函数和拷贝构造函数,如果没有定义,就会使用合成版本。
基类构造函数和拷贝构造函数
本身不是派生类的基类,其构造函数和拷贝构造函数基本不受继承影响。
继承对基类构造函数的唯一影响是,某些类需要只希望派生类使用的特殊构造函数,这样的函数可以用protected定义。
派生类构造函数
派生类的构造函数受继承的影响,每一个派生类的构造函数除了需要初始化自己的数据成员,还需要初始化基类。
所以,一般的,派生类的构造函数中会包含基类的构造函数。
- 合成的派生类默认构造函数
除了初始化派生类的数据成员之外,还会初始化派生类对象的基类部分。基类部分由基类的默认构造函数初始化。
- 定义默认构造函数
class item:public Item_base{
public:
item():a(0),discount(0.0){ }
}
- 向基类构造函数传递实参
一般的,用户可以初始化基类的数据成员,即向基类的构造函数传递实参。
派生类构造函数的初始化列表只能初始化派生类的成员,不能初始化继承成员。
因此,在派生类的构造函数中初始化基类部分,必须使用基类的构造函数。
class A {
public:
A(int m,int n):a1(m),a2(n)
{}
private:
int a1, a2;
};
class B :public A {
public:
//B():a1(1),a2(2),b1(1),b2(2) //error
//{}
B() :A(1,2), b1(1), b2(2)
{}
private:
int b1, b2;
};
构造函数初始化列表为类的基类和成员提供初始值,它并不指定初始化的执行顺序。首先初始化基类,然后根据声明次序初始化派生类的成员。
- 在派生类构造函数中使用默认实参
class item:public Item_base{
public:
item(string book,int price=1.1):book(book),price(price)
{ }
}
- 只能初始化直接基类
一个类只能初始化自己的直接基类。直接基类就是在派生列表中指定的类。如果C类是从B类派生,B类是从A类派生,B类就是C类的直接基类。虽然每一个C类对象都包含一个A类部分,但C类的构造函数不能直接初始化A部分。需要由类C初始化类B,而类B的构造函数初始化类A。
派生类的拷贝构造函数
class Base
{
private:
int b1;
};
class Derived :public Base
{
public:
Derived(const Derived& d);
private:
int d1;
};
Derived::Derived(const Derived& d):d1(d.d1),Base(d) //派生类到基类的转换
{}
析构函数
与构造函数类似,派生类不能继承基类的析构函数,想完成派生类数据成员资源的释放,需要在派生类中定义析构函数。同样,若派生类没有显式定义析构函数,编译器会提供一个默认的析构函数。派生类析构函数不负责撤销基类对象的成员,每个析构函数只负责清除自己的成员。
析构函数的执行次序与构造函数次序相反,先执行派生类析构函数,在调用基类的析构函数。
虚析构函数
在C++中,不能声明虚构造函数,因为构造函数执行时,对象还没有构造好,不可以按照虚函数方式进行调用,但可以声明虚析构函数。
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针销毁派生类对象的应用产生的。通常,使用基类指针指向一个new生成的派生类对象,通过delete销毁基类指针指向的派生类对象时,有以下两种情况:
- 如果基类析构函数不是虚析构函数,则只会调用基类析构函数,派生类析构函数不被调用,此时派生类中申请的资源不被回收。
- 如果基类析构函数是虚析构函数,则释放基类指针指向的对象时会调用基类及派生类的析构函数,派生类中的所有资源被回收。
实例:(下列程序对派生类对象的销毁是不彻底的,修改避免内存泄露)
#include <iostream>
using namespace std;
class base
{
public:
base()
{
cout<<"base constructor"<<endl;
}
~base()
{
cout<<"base destructor"<<endl;
}
};
class derived:public base
{
private:
char * buf;
public:
derived()
{
buf=new char[10];
cout<<"derived constructor"<<endl;
}
~derived()
{
delete []buf;
cout<<"derived destructor"<<endl;
}
};
void main()
{
base * p=new derived;
delete p;
}
//输出:
//base constructor
//derived constructor
//base destructor
解决方法:
virtual ~base() //基类析构函数定义为虚函数
{
cout<<"base destructor"<<endl;
}
隐藏基类函数
有时候派生类需要根据自身特点改写从基类继承的函数,比如动物都会有叫声,在描述动物叫声speak()函数,不同的动物有不同的叫声,就需要定义不同的speake()函数.
派生类重新定义基类重名函数的方法,称为对基类函数的覆盖或改写,覆盖后基类同名函数在派生类中被隐藏。定义派生类对象调用该函数时,调用的是自身的函数,基类同名函数不被调用。
覆盖虚函数机制
在某些情况下,希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本。
简单的说就是,添加了virtual关键字使用虚函数机制,但是我偏偏想调用基类的函数。
这时就可以使用域操作符:
Item_base *baseP = &derived;
double d = baseP->Item_base::net_price(42);
这段代码强制将net_price调用为Item_base中定义的版本,该调用将在编译时确定。
只有成员函数的代码中才应该使用作用域操作符覆盖虚函数机制。
派生类虚函数调用基类版本时,必须显式使用作用域操作符。
7.3多继承
一个子类有多个父类称为多继承,它继承了多个父类的特性。
多继承语法:
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};
实例:
#include <iostream>
using namespace std;
//基类 Shape
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
//基类 Paintcost
class Paintcost
{
public:
int getCost(int area)
{
return 70 * area;
}
};
//派生类
class Rectangle :public Shape,public Paintcost
{
public:
int getArea()
{
return width * height;
}
};
int main()
{
Rectangle rect;
rect.setWidth(1);
rect.setHeight(2);
int area = rect.getArea();
//输出面积
cout << "area = " << rect.getArea() <<endl;
//输出花费
cout << "cost = " << rect.getCost(area) <<endl;
return 0;
}
//输出:
//area = 2
//cost = 140
多重继承的派生类构造函数和析构函数
构造派生类类型的对象包括构造和初始化它的所有基类子对象。与单继承构造类似,多重继承在其构造函数中调用基类的构造函数进行初始化。
构造的次序
例:
class Bear:public ZooAnimal
{};
class Panda :public Bear,public Endangered
{};
对Panda而言,构造次序为:
- ZooAnimal,从Panda的直接基类Bear沿层次向上的最终基类。
- Bear,第一个直接基类。
- Endangered,第二个直接基类,它本身没有基类。
- Panda,初始化Panda本身的成员,然后运行它的构造函数的函数体。
注:多个基类的构造次序取决于多个基类的声明的顺序。构造函数调用次序既不受构造函数初始化表中出现的基类的影响,也不受基类在构造函数初始化表中出现次序的影响。
析构的次序
析构的次序与构造的次序相反,例子中析构次序为:~ Panda,~ Endangered,~ Bear,~ ZooAnimal.
多重继承引发的两种二义性问题
- 多个基类里存在重名函数:a.可以在派生类里写同名函数隐藏基类的成员;b.可以通过作用域解析运算符来访问基类的同名函数;c.通过using声明来访问基类的成员函数(派生类中隐藏基类函数知识)
//第一种和第二种
#include <iostream>
using namespace std;
class A
{
public:
void test()
{
cout << "This is A!" << endl;
}
};
class B :public A
{
public:
void test()
{
cout << "This is B!" << endl;
}
};
int main()
{
B b;
b.test(); //输出:This is B!
b.A::test(); //使用作用域解析运算符
//输出:This is A!
return 0;
}
//第三种
#include <iostream>
using namespace std;
class A
{
public:
void test()
{
cout << "This is A!" << endl;
}
};
class B :public A
{
public:
using A::test;
};
int main()
{
B b;
b.test(); //类B中没有定义test函数,使用using声明访问类A的声明
//输出:This is A!:
return 0;
}
- 多个基类又来自相同的基类,那么存在相同基类成员有两份拷贝的问题(虚继承知识)
典型的实例有菱形继承问题
#include <iostream>
using namespace std;
class A
{
protected:
int a;
};
class B :public A
{
};
class C :public A
{
};
class D :public B, public C
{
};
int main()
{
D d;
cout << d.a << endl; //error:对a的访问不明确
//类B中含有成员a,类C中也含有成员a,类D作为B和C的派生类,就拷贝了两者的数据成员a
return 0;
}
解决方法是进行虚继承的定义
多重继承引起的二义性
单个继承中,派生类的指针或引用可以自动转换为基类的指针或引用,同样,在多重继承中同样如此,派生类的指针或引用可以转换为其任意基类的指针或引用。如Panda指针或引用可以转换为ZooAnimal、Bear、Endangered的指针或引用。
在多重继承中,遇到二义性的转换的可能性更大,编译器不会试图通过派生类的转换来区分基类的转换。例如,有print()函数的重载版本
void print(const Bear&);
void print(const Endangered&);
Panda pa;
print(pa); //error:ambiguous
虚继承
在多重继承中,一个基类可以在派生类层次中出现多次。事实上,我们经常通过继承层次多次继承同一基类的类。
在C++中,虚继承可以解决此类问题,虚继承是一种机制,类通过类继承指出它希望共享其基类的状态。在虚继承下,对于给定基类,无论该类作为虚基类出现多少次,只继承一个共享的基类子对象。共享的基类子对象称为虚基类。
虚基类的定义形式是在派生类定义时基类名称前加virtual关键字,具体形式如下所示:
class 派生类名:virtual 继承方式 基类名
{
派生类成员
};
虚继承的构造函数
实例:
#include <iostream>
using namespace std;
class Base
{
public:
Base():a(666)
{
cout << "1 base constructor!" << endl;
}
Base(int na) :a(na)
{
cout << "2 base constructor!" << endl;
}
void print()
{
cout << "a=" << a << endl;
}
protected:
int a;
};
class Derived1 :virtual public Base
{
public:
Derived1():Base(2)
{
cout << "Derived1 constructor!" << endl;
}
protected:
};
class Derived2 :virtual public Base
{
public:
Derived2():Base(3)
{
cout << "Derived2 contructor!" << endl;
}
protected:
};
class Derived :public Derived1, public Derived2
{
public:
protected:
};
int main()
{
Derived d; //定义Derived的对象,执行Derived类中的虚基类构造函数
d.print();
Derived2 d_; //定义Derived2的对象,执行Derived2类中的虚基类构造函数
d_.print();
return 0;
}
本来的,在普通的继承中,派生类只需要构造自己的直接基类。
而在类似于菱形继承的继承层次中,将一个类定义为虚基类,每一个包含该虚基类的类构造自己的直接基类,还要构造自己的虚基类。
中间基类的构造函数必须写虚基类的构造函数用于对虚基类数据成员的初始化,但是不一定会被调用中间基类的构造函数。定义哪一个类的对象,就调用哪一个类的构造函数。
7.4多态
接口的多种不同的实现方式即为多态。
C++程序设计中,消息即对类的成员函数的调用,不同的行为指的是不同的实现,也就是调用不同的函数。因此,多态的本质是指一个函数的多种形态。
C++语言支持的多态可以按照实现的时机分为编译时多态和运行时多态两种:
- 编译时多态又称静态联编,是程序在编译时就可确定的多态性,通过重载机制来实现。
- 运行时多态又称动态联编,是指必须在进行中才可确定的多态性,通过继承和虚函数实现。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
实例:
#include <iostream>
using namespace std;
class A
{
public:
void test()
{
cout << "Parent class A test:" << endl;
}
};
class B :public A
{
public:
void test()
{
cout << "class B test:" << endl;
}
};
class C :public A
{
public:
void test()
{
cout << "class C test:" << endl;
}
};
int main()
{
A* a;
B b;
C c;
a = &b;
a->test();
a = &c;
a->test();
return 0;
}
//输出:
//Parent class A test:
//Parent class A test:
导致错误输出的原因是,调用函数test()被编译器设置为基类的部分,这就是所谓的静态多态,或静态链接-函数调用在程序执行前就准备好了。有时候这也被称为早绑定。因为test()函数在程序编译器件就设置好了。
这时,我们可以稍作修改,在基类A中test()的声明前放置关键字virtual,如下所示:
class A
{
public:
virtual void test()
{
cout << "Parent class A test:" << endl;
}
};
//输出:
//class B test:
//class C test:
此时,编译器看的是指针的内容,而不是它的类型。
每一个派生类都有一个test()函数的独立实现。这就是多态的一般实现方式。有了多态,可以有不同的类,都带同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。
虚函数
虚函数是在基类中使用关键字virtual的函数。在派生类中重新定义基类中定义的函数时,会告诉编译器不要静态链接到该函数。
我们想要的是在程序中任意点根据所调用的对象类型来选择调用的函数,这种操作被称为动态绑定,或后期绑定。
虚函数是C++中用于实现多态的机制。其核心理念就是通过基类访问派生类定义的函数。
基类通常将派生类中需要中定义的函数定义为虚函数(在基类中定义)
同名函数:重载,隐藏,覆盖(重定义)
纯虚函数
有时我们会在基类中定义虚函数,以便在派生类中重新定义该函数,但是不能在基类中对虚函数给出有意义的实现,这时就会用到纯虚函数。
实例:
class A
{
public:
virtual void test()=0;
};
=0告诉编译器,函数没有主体,上面的虚函数是纯虚函数。
虚函数与纯虚函数的区别
首先,强调一个概念:
定义一个函数为虚函数,不代表函数为不被实现的函数。
定义它为虚函数是为了允许用基类的指针来调用派生类的这个函数。
定义一个函数为纯虚函数,才代表函数没有被实现。
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
虚函数
虚函数只能借助于指针或引用来实现多态的效果。
纯虚函数
纯虚函数是在基类中定义的虚函数,它在基类中没有定义,但要求任何派生类都必须定义自己的实现方法。
引入原因:
- 为了方便使用多态特性,我们常常在基类中定义虚拟函数。
- 在很多情况下,基类本身生成对象是很不合理的。例如,动物作为基类可以派生出狮子、老虎等派生类,但动物本身生成对象明显不合理。
为解决以上问题,引入了纯虚函数的概念,将函数定义为纯虚函数,则编译器要求必须在派生类中重写实现多态性。同时含有纯虚函数的类称为抽象类(6.7.5),它不能产生对象。
纯虚函数的显著特征:必须在继承类中重新声明该函数(不要后面的=0,不然该派生类也不能实例化),而且它们在抽象类中往往没有定义。
定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
纯虚函数的意义,让所有的类对象(主要是派生类对象)都能够执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的默认实现。所以纯虚函数的声明就是在告诉设计者,“你必须提供一个纯虚函数的实现,但我不知道你怎么实现它。”
7.5抽象类与内部类
抽象类
抽象类是一种特殊的类,它是以抽象和设计为目的建立的,它处于继承层次的较上层。
- 抽象类的定义:称带有纯虚函数的类为抽象类。
- 抽象类的作用:抽象类的主要作用是将有关的操作作为结果接口组织在同一个继承层次结构中,由它来为派生类提供一个公共的跟,派生类将具体实现在其基类中作为接口的操作。所以基类实际上刻画了一组派生类的操作接口的通用语义,这些通用语义也传给派生类,派生类可以具体实现这些语义,也可以将这些语义传个自己的派生类。
- 使用抽象类注意:
- 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函数,而是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果该派生类中给出类基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以键里对象的具体的类。
例如:
#include <iostream>
using namespace std;
class A
{
public:
virtual void test() = 0;
};
class B :public A
{
public:
};
int main()
{
A a; //error:基类A中含有纯虚函数,所以类A是一个抽象类,不能创建对象
B b; //error:B中没有给出基类纯虚函数的实现,所以继承了基类的纯虚函数,所以类B也是一个抽象类,不能创建对象
return 0;
}