继承:
面向对象程序设计的一个重要特点就是可以在既有类的基础上定义新的类,而不用将既有类的内容重新书写一遍,这称为“继承”(inheritance),既有类称为“基类”或“父类”,在它的基础上建立的类称为“派生类”或“子类”
qt , com组件
什么是继承:
面向对象的程序设计(OOP)而言,继承的引入使程序员可以按现实世界、按自然的方式去思考和解决问题,组织信息,提高了效率,其次,可以复用基类的代码,并可以在派生类中增加新代码或者覆盖基类的成员函数,为基类成员函数赋予新的意义,实现最大限度的代码复用。
1、继承的定义形式一般如下:
class 派生类 : 派生方式 基类 {…}; //public, protected, private
2、派生类生成过程包含3个步骤:
——吸收基类的成员
——改造基类的成员
——添加自己新的成员
继承的层次性:
任何一个类都可以派生出新类,派生类还可以再派生出新的类,因此,基类和派生类是相对而言的。一个基类可以是另一个基类的派生类,这样便构建了层次性的类结构,如所示,类B是A的派生类,同时又派生了新类C,B又可以看作是C的基类。
继承的局限:
1、不论何种继承方式,下面这些基类的特征是不能从基类继承下来的:
——构造函数
——析构函数
——用户重载的new 、delete运算符
operator new /operator delete
——用户重载的=运算符
——友元关系
|
派生方式:
派生有public、protected、private三种方式,不同的派生方式下,派生类对基类成员的访问权限以及外部对基类成员的访问权限有所不同。
1、public派生与private派生
C++中,利用基类派生其子类(派生类)的基本格式为:
class 派生类名:派生方式 基类名
{
private:
新增私有成员列表;
public:
新增公开成员列表;
};
|
2、派生类访问权限
● 通过继承,派生类自动得到了除基类私有成员以外的其它所有数据成员和成员函数,在派生类中可以直接访问。
● private成员是私有成员,只能被本类的成员函数所访问,派生类和类外都不能访问。
● public成员是公有成员,在本类、派生类和外部都可访问
● protected成员是保护成员,只能在本类和派生类中访问。是一种区分血缘关系内外有别的成员。
● 派生类的访问权限规则如下:
1.不管是什么继承方式,子类中都不能访问父类的私有成员。
2.子类内部除了基类的私有成员不可以访问外,其他的都可以访问。
3.除了公有继承基类中的公有成员,子类对象可以访问外,其他的,子类对象一律不能访问 。
|
#include <iostream>
using namespace std;
class Point
{
public:
Point(int ix=0,int iy=0):_ix(ix),_iy(iy)
{ cout<<"Point(int,int)"<<endl; }
int getx()const
{ return _ix; }
int gety()const
{ return _iy; }
void display()
{ cout<<"("<<_ix<<","<<_iy<<")"<<endl; }
private:
int _ix;
int _iy;
};
class Point3D:public Point
{
public:
Point3D(int ix,int iy,int iz):Point(ix,iy),_iz(iz)
{ cout<<"Point3D(int,int,int)"<<endl; }
void display() //隐藏了基类中的同名函数
{
cout<<"("<<getx()<<","<<gety()<<","<<_iz<<")"<<endl; //不能直接访问基类的私有成员
}
private:
int _iz;
};
int main()
{
Point3D point3d(1,2,3);
point3d.display();
return 0;
}
|
#include <iostream>
using namespace std;
class Point
{
public:
Point(int ix=0,int iy=0):_ix(ix),_iy(iy)
{ cout<<"Point(int,int)"<<endl; }
int getx()const
{ return _ix; }
protected:
int gety()const
{ return _iy; }
public:
void display()
{ cout<<"("<<_ix<<","<<_iy<<")"<<endl; }
private:
int _ix;
int _iy;
};
class Point3D:private Point
{
public:
Point3D(int ix,int iy,int iz):Point(ix,iy),_iz(iz)
{ cout<<"Point3D(int,int,int)"<<endl; }
void display() //隐藏了基类中的同名函数
{
cout<<"("<<getx()<<","<<gety()<<","<<_iz<<")"<<endl; //不能直接访问基类的私有成员
}
private:
int _iz;
};
int main()
{
Point point(1,2);
//point.gety(); //protected修饰的成员不能通过对象访问
point.display();
Point3D point3d(1,2,3);
point3d.display();
return 0;
}
|
3、多基派生
派生类只有一个基类时,称为单基派生,在实际运用中,我们经常需要派生类同时具有多个基类,这种方法称为多基派生或多重继承,下图所示是双基继承的示意,在实际应用中,还允许使用三基甚至是更多基继承。
4、多基派生的声明和定义
在C++中,声明和定义具有两个以上基类的派生类与声明单基派生类的形式类似,只需将要继承的多个基类用逗号分开即可,如
class 派生类名:继承方式1 基类名1,继承方式2 基类名2,…,继承方式n 基类名n
{
private:
新增私有成员列表;
public:
新增公开成员列表;
};
例如,从A类和B类派生出C类的方式如下:
class C : public A, public B
{
//……
};
|
5、多基派生的二义性问题
在派生类中对基类成员的访问应当具有唯一性,但在多基继承时,如果多个基类中存在同名成员的情况,造成编译器无从判断具体要访问的哪个基类中的成员,则称为对基类成员访问的二义性问题
6、多基派生二义性问题的解决方案
●作用域限定符
●虚基类
7、共同基类(虚基类)
多基派生中,如果在多条继承路径上有一个共同的基类,如图10.4所示,不难看出,在D类对象中,会有来自两条不同路径的共同基类(类A)的双重拷贝。
8、共同基类的二义性
共同基类和多基派生的共同作用,使得在派生类中会出现多个共同基类的拷贝,这很容易带来二义性问题
如下图所示,D类通过B类和C类各继承了一次A类的函数SetX()和Print(),从而产生了二份拷贝,进而导致了二义性
9、共同基类二义性问题的解决方案
使用关键字virtual将共同基类A声明为虚基类,可有效解决上述问题。在定义由共同基类直接派生的类(类B和类C)时,使用下列格式定义:
class 派生类名 : virtual 派生方式 基类名
{
//类定义
};
|
虚基派生和多基派生带来的二义性有些细微的差别:
1、多基派生的二义性主要是成员名的二义性,通过加作用域限定符来解决。
2、虚基派生的二义性则是共同基类成员的多重拷贝带来的存储二义性,使用virtual派生来解决
另外,二义性的检查是在访问权限检查之前进行的,因此,成员的访问权限是不能消除二义性的
|
//多基派生产生的二义性
#include <iostream>
using namespace std;
class A
{
public:
void print()
{ cout<<"A::print()"<<endl; }
};
class B
{
public:
void print()
{ cout<<"B::print()"<<endl; }
};
class C:public B,public A
{
public:
void display()
{
cout<<"C::display()"<<endl;
A::print(); //解决名字冲突二义性
B::print();
}
void print()
{
cout<<"C::print()"<<endl;
}
};
int main()
{
C c;
c.display();
c.A::print(); // 调用A类额的print
c.print() // 默认调用类C里面的
return 0;
}
//
C::display()
A::print()
B::print()
A::print()
C::print()
|
//虚基派生产生的二义性
#include <iostream>
using namespace std;
class A
{
public:
A(int x=0):_x(x)
{ cout<<"A(int)"<<endl; }
void setx(int x)
{ _x=x; }
void print()
{ cout<<"A::_x="<<_x<<endl; }
protected:
int _x;
};
//存储二义性问题,采用虚继承的方式解决
class B:virtual public A {};
class C:virtual public A {};
class D:public B,public C{};
int main()
{
D d;
cout<<"sizeof(C)="<<sizeof(C)<<endl;
cout<<"sizeof(B)="<<sizeof(B)<<endl;
cout<<"sizeof(D)="<<sizeof(d)<<endl; //不加virtual结果是 4、4、8
d.setx(5); //不加virtual这里产生二义性,编译不通过
d.print(); //virtual派生,在D中只有一个版本,不会产生二义性
return 0;
}
|
单基派生类的构造函数和析构函数
★ 派生时,构造函数和析构函数是不能继承的,为了对基类成员进行初始化,必须对派生类重新定义构造函数和析构函数,并在构造函数的初始化列表中调用基类的构造函数。
★由于派生类对象通过继承而包含了基类数据成员,因此,创建派生类对象时,系统首先通过派生类的构造函数来调用基类的构造函数,完成基类成员的初始化,而后对派生类中新增的成员进行初始化。
——如果派生类有显式定义构造函数,而基类没有,则创建派生类的对象时,派生类相应的构造函数会被自动调用,此时都自动调用了基类缺省的无参构造函数。
——如果派生类没有显式定义构造函数而基类有,则基类必须拥有默认构造函数。
★如果派生类有构造函数,基类有默认构造函数,则创建派生类的对象时,基类的默认构造函数会自动调用,如果你想调用基类的有参构造函数,必须要在派生类构造函数的初始化列表中显示调用基类的有参构造函数。
★如果派生类和基类都有构造函数,但基类没有默认的无参构造函数,即基类的构造函数均带有参数,则派生类的每一个构造函数必须在其初始化列表中显示的去调用基类的某个带参的构造函数。如果派生类的初始化列表中没有显示调用则会出错,因为基类中没有默认的构造函数。
1、单基派生类的构造函数
★派生类构造函数的一般格式为:
派生类名(总参数表): 基类构造函数(参数表)
{
//函数体
};
|
★必须将基类的构造函数放在派生类的初始化表达式中,以调用基类构造函数完成基类数据成员的初始化,派生类构造函数实现的功能,或者说调用顺序为:
1、完成对象所占整块内存的开辟,由系统在调用构造函数时自动完成。
2、调用基类的构造函数完成基类成员的初始化。
3、若派生类中含对象成员、const成员或引用成员,则必须在初始化表中完成其初始化。
4、派生类构造函数体执行。
|
#include <iostream>
using namespace std;
//当派生类显示定义了构造函数,二基类没有显示定义构造函数,
//如果创建派生类对象,此时自动调用基类的默认构造函数
class Base
{
public:
Base()
{ cout<<"Base()"<<endl; }
private:
int _ix;
};
class Derived:public Base
{
public:
Derived()
{ cout<<"Derived()"<<endl; }
Derived(int ix)
{ cout<<"Derived(int)"<<endl; }
};
int main()
{
Derived d1(5);
return 0;
}
|
#include <iostream>
using namespace std;
//当派生类没有显示提供任何构造函数,而基类显示提供了有参构造函数,那
//么在创建派生类对象时,要求基类必须显示的提供一个默认构造函数
class Base
{
public:
Base()
{ cout<<"Base()"<<endl; }
Base(int x)
{ cout<<"Base(int)"<<endl; }
private:
int _x;
};
class Derived:public Base{}; //系统会提供默认的构造函数
int main()
{
Derived d1;
return 0;
}
|
#include <iostream>
using namespace std;
//当派生类和基类都显示定义了构造函数
//在创建派生类对象时,如果希望调用基类的有参
//构造函数,则需要在派生类构造函数的初始化列表
//之中显示的调用基类的构造函数
class Base
{
public:
Base()
{ cout<<"Base()"<<endl; }
Base(int x):_x(x)
{ cout<<"Base(int)"<<endl; };
private:
int _x;
};
class Derived:public Base
{
public:
Derived():Base()
//显示调用Base的默认构造函数,可写可不写
{ cout<<"Derived()"<<endl; }
Derived(int x):Base(x) //显示调用基类的有参构造函数
{ cout<<"Derived(int)"<<endl; }
};
int main()
{
Derived d1;
Derived d2(1);
return 0;
}
|
2、单基派生类的析构函数
★当对象被删除时,派生类的析构函数被执行。析构函数同样不能继承,因此,在执行派生类析构函数时,基类析构函数会被自动调用(与虚函数)。
#include <iostream>
using namespace std;
class A
{
public:
A(int x=0):_x(x)
{ cout<<"A::A(int)"<<endl; }
~A()
{ cout<<"A::~A()"<<endl; }
private:
int _x;
};
class B
{
public:
B()
{ cout<<"B::B()"<<endl; }
~B()
{ cout<<"B::~B()"<<endl; }
};
|
class C:public A
{
public:
C(int x,int y):A(x),b(),_y(y)
{ cout<<"C::C(int,int)"<<endl; }
~C()
{ cout<<"C::~C(int,int)"<<endl; }
private:
int _y;
B b;
};
int main()
{
C c(1,2);
return 0;
}
|
3、多基派生类的构造函数和析构函数
★多基派生时,派生类的构造函数格式如(假设有N个基类):
派生类名(总参数表):基类名1(参数表1),基类名2(参数表2),……,基类名N(参数表N)
{
//函数体
}
|
★和单基派生类似,总参数表中包含了后面各个基类构造函数需要的参数。
★多基派生和单基派生构造函数完成的任务和执行顺序并没有本质不同,唯一一点区别在于首先要执行所有基类的构造函数,再执行派生类构造函数中初始化表达式的其他内容和构造函数体,各基类构造函数的执行顺序与其在初始化表中的顺序无关,而是由定义派生类时指定的基类顺序决定的。
★析构函数的执行顺序同样是与构造函数的执行顺序相反。
2016/6/30 9:50:20
有继承关系的构造析构调用总结:
1、如果派生类还包括成员对象,则对成员对象的构造函数的调用,仍然在初始化列表中进行,此时,当创建派生类的一个对象时,首先基类构造函数被调用,成员对象所在类构造函数次之(在有多个成员对象的情况下,成员对象的调用顺序取决于他们在派生类中的申明顺序,与初始化列表中的顺序无关),最后执行派生类构造函数的函数体。
2、如果是多继承,在调用基类构造函数的时候,处于同一层次的各基类构造函数的调用顺序取决于定义派生类时所指定的继承基类顺序,与派生类构造函数中所定义的初始化列表顺序无关。
3、如果派生类有一个虚基类作为祖先类,那么在派生类构造函数的初始化列表中需要列出对虚基类构造函数的调用,如果未列出则表明调用的是虚基类的无参构造函数,不管初始化列表中次序如何,对虚基类构造函数的调用总是先于普通基类的构造函数。
4、继承机制下析构函数的调用顺序:
——先调用派生类的析构函数
——然后调用派生类中成员对象的析构函数
——再调用普通基类的析构函数
——最后调用虚基类的析构函数
派生类对基类成员的覆盖:
oversee 隐藏:父子类,函数名称相同,不带virtual关键字的函数
override 覆盖:父子类,函数的名称、返回值类型、参数的类型个数都相同有virtual关键字
overload 重载:同一个类,函数名称相同,参数不同(类型,顺序,个数)
|
如果基类和派生类中存在同名的成员数据或者非虚的成员函数,那么派生类的成员数据和成员函数将隐藏基类的成员数据和成员函数,子类对象不能直接调用到基类的被隐藏的成员数据和成员函数,只能通过obj.CBase::Func(3);方式访问。而且与基类和派生类的访问属性无关(?);与基类和派生类的函数间的参数和返回类型无关。
#include <iostream>
using namespace std;
class Base
{
public:
int func(int i)
{ return _ix; }
protected:
int _ix;
};
class Derived : public Base
{
public:
int func()
{
_ix=9; //访问派生类的_ix,发生了隐藏
Base::_ix = 22;
//如果要访问基类的同名成员,必须使用基类的类名
return _ix;
}
void display()
{
cout<<"Derived::_ix = "<<_ix<<endl;
cout<<"Base::_ix = "<<Base::_ix<<endl;
}
private:
int _ix;
};
int main()
{
Derived d;
//cout<<d.func(3)<<endl;
//和基类同名的函数,基类函数发生隐藏,要调用只能加上基类类名
cout<<d.func()<<endl;
cout<<d.Base::func(3)<<endl;
d.display();
return 0;
}
|
//继承“是一个”
#include <iostream>
using namespace std;
class Eye
{
public:
void look()
{ cout<<"Eye::lock()"<<endl; }
};
class Nose
{
public:
void smell()
{ cout<<"Nose::smell()"<<endl; }
};
class Mouth
{
public:
void eat()
{ cout<<"Mouth::eat()"<<endl; }
};
class Ear
{
public:
void listen()
{ cout<<"Ear::listen()"<<endl; }
};
class Head:public Eye,public Nose,public Mouth,public Ear{};
int main()
{
Head head;
head.look();
head.smell();
head.listen();
head.eat();
return 0;
}
|
//组合 “有一个”
#include <iostream>
using namespace std;
class Eye
{
public:
void look()
{ cout<<"Eye::lock()"<<endl; }
};
class Nose
{
public:
void smell()
{ cout<<"Nose::smell()"<<endl; }
};
class Mouth
{
public:
void eat()
{ cout<<"Mouth::eat()"<<endl; }
};
class Ear
{
public:
void listen()
{ cout<<"Ear::listen()"<<endl; }
};
class Head
{
public:
void look()
{ _eye.look(); }
void smell()
{ _nose.smell(); }
void listen()
{ _ear.listen(); }
void eat()
{ _mouth.eat(); }
private:
Eye _eye;
Nose _nose;
Mouth _mouth;
Ear _ear;
};
int main()
{
Head head;
head.look();
head.smell();
head.listen();
head.eat();
return 0;
}
|
从逻辑上说,继承是一种a kind of的关系(AKO)或者说IS-A(“是一个”),汽车是车,因此,汽车类可以从普通的车类继承而来,轮子类就不能从汽车类继承来,轮子是汽车的一个部件,轮子可以作为汽车类的对象成员,这就是“组合”(compositioin A has B)。 |
组合:
软件复用:代码的重复
——C语言中采用:函数 宏
——C++中采用:继承 组合
★某类以另一个类对象作数据成员,称为组合,在逻辑上,如果类A是类B的一部分(a part of)或者说HAS-A(“有一个”),不要从A类派生出类B,而应当采用组合的方式。
基类与派生类对象间的相互转换:
★“类型适应”是指两种类型之间的关系,说A类适应B类是指A类的对象能直接用于B类对象所能应用的场合,从这种意义上讲,派生类适应于基类,派生类的对象适应于基类对象,派生类对象的指针和引用也适应于基类对象的指针和引用。
——可以声明基类的对象等于派生类的对象
——可以声明基类的引用等于派生类的对象
——可以声明基类的指针指向派生类的对象 (向上转型)
★也就是说如果函数的形参是基类对象或者基类对象的引用或者基类对象的指针类型,在进行函数调用时,相应的实参可以是派生类对象
#include <iostream>
using namespace std;
class Point
{
public:
Point(int ix,int iy):_ix(ix),_iy(iy)
{ cout<<"Point(int,int)"<<endl; }
void display()
{ cout<<"("<<_ix<<","<<_iy<<")"<<endl; }
protected:
int _ix;
int _iy;
};
class Point3D : public Point
{
public:
Point3D(int ix,int iy,int iz):Point(ix,iy),_iz(iz)
{ cout<<"Point3D(int,int,int)"<<endl; }
void display()
{ cout<<"("<<_ix<<","<<_iy<<","<<_iz<<")"<<endl; }
private:
int _iz;
};
|
int main()
{
Point point(1,2);
point.display();
Point3D point3d(3,4,5);
point3d.display();
cout<<endl;
point = point3d; //派生类对象为基类对象赋值
point.display();
Point & ref = point3d; //派生类对象初始化基类对象引用
ref.display();
Point * p = & point3d; //派生类对象地址为基类指针赋值
p->display();
return 0;
}
|
派生类与派生类对象间的相互转换:
★如果用户定义了基类的拷贝构造函数,而没有定义派生类的拷贝构造函数,那么在用一个派生类对象初始化新的派生类对象时,两对象间的派生类部分执行缺省的行为,而两对象间的基类部分执行用户定义的基类拷贝构造函数。
★如果用户重载了基类的对象赋值运算符=,而没有定义派生类的对象赋值运算符,那么在用一个派生类对象给另一个已经存在的派生类对象赋值时,两对象间的派生类部分执行缺省的赋值行为,而两对象间的基类部分执行用户定义的重载赋值函数。
★如果用户定义了派生类的拷贝构造函数或者重载了派生类的对象赋值运算符=,则在用已有派生类对象初始化新的派生类对象时,或者在派生类对象间赋值时,将会执行用户定义的派生类的拷贝构造函数或者重载赋值函数,而不会再自动调用基类的拷贝构造函数和基类的重载对象赋值运算符,这时,通常需要用户在派生类的拷贝构造函数或者派生类的赋值函数中显式调用基类的拷贝构造或赋值运算符函数。
#include <iostream>
#include <string.h>
using namespace std;
class Base
{
public:
Base(const char * pbuf):_pbuf(new char[strlen(pbuf)+1])
{
strcpy(_pbuf,pbuf);
cout<<"Base(const char *)"<<endl;
}
Base(const Base & rhs):_pbuf(new char[strlen(rhs._pbuf)+1])
{
strcpy(_pbuf,rhs._pbuf);
cout<<"Base(const Base &)"<<endl;
}
Base & operator = (const Base & rhs)
{
cout<<"Base & operator = (const Base &)"<<endl;
if(this != &rhs)
{
delete []_pbuf;
_pbuf = new char[strlen(rhs._pbuf)+1];
strcpy(_pbuf,rhs._pbuf);
}
return *this;
}
void display()
{
cout<<_pbuf<<endl;
}
private:
char * _pbuf;
};
class Derived : public Base
{
public:
Derived(const char * pbuf):Base(pbuf)
{ cout<<"Derived(const char *)"<<endl; }
};
int main()
{//当派生类没有定义复制构造函数,而基类显示的定义了复制构造函数,
//则派生类对象之间进行复制时,会自动调用基类复制构造函数
Derived d1("hello");
d1.display();
Derived d2("world");
d2.display();
cout<<endl;
d1=d2; //派生类使用缺省的赋值构造函数,基类调用用户定义的赋值构造函数
d1.display();
d2.display();
cout<<endl;
Derived d3 (d1); //派生类使用缺省的拷贝构造函数,基类调用用户定义的拷贝构造函数
d3.display();
return 0;
}
//如果派生类不显示调用基类的拷贝构造函数和赋值构造函数,执行结果就是这样的
|
#include <iostream>
#include <string.h>
using namespace std;
class Base
{
public:
Base():_pbuf(new char[1])
{ cout<<"Base()"<<endl; }
Base(const char * pbuf):_pbuf(new char[strlen(pbuf)+1])
{
strcpy(_pbuf,pbuf);
cout<<"Base(const char *)"<<endl;
}
Base(const Base & rhs):_pbuf(new char[strlen(rhs._pbuf)+1])
{
strcpy(_pbuf,rhs._pbuf);
cout<<"Base(const Base &)"<<endl;
}
Base & operator = (const Base & rhs)
{
cout<<"Base & operator = (const Base &)"<<endl;
if(this != &rhs)
{
delete []_pbuf;
_pbuf = new char[strlen(rhs._pbuf)+1];
strcpy(_pbuf,rhs._pbuf);
}
return *this;
}
~Base()
{
delete []_pbuf;
cout<<"~Base()"<<endl;
}
void display()
{ cout<<_pbuf; }
private:
char * _pbuf;
};
class Derived : public Base
{
public:
Derived(const char * pbuf,const char * pbuf2):Base(pbuf),_pbuf2(new char[strlen(pbuf2)+1])
{
cout<<"Derived(const char *,const char *)"<<endl;
strcpy(_pbuf2,pbuf2);
}
Derived(const Derived & rhs):Base(rhs),_pbuf2(new char[strlen(rhs._pbuf2)+1])
{//显示调用基类的复制构造函数
cout<<"Derived(const char *,const char *)"<<endl; //这里写错了,导致最后一个结果显示这里了。
strcpy(_pbuf2,rhs._pbuf2);
}
Derived & operator = (const Derived & rhs)
{
cout<<"Derived & operator = (const Derived &)"<<endl;
if(this != &rhs);
{
Base::operator = (rhs); //显示调用基类的赋值运算符函数
delete []_pbuf2;
_pbuf2=new char[strlen(rhs._pbuf2)+1];
strcpy(_pbuf2,rhs._pbuf2);
}
return *this;
}
~Derived()
{
delete []_pbuf2;
cout<<"~Derived()"<<endl;
}
void display()
{
Base::display();
cout<<_pbuf2<<endl;
}
private:
char * _pbuf2;
};
int main()
{//当派生类显示定义了复制构造函数,而基类显示定义了复制构造函数,
//则派生类对象之间进行复制时,不会自动调用基类的复制构造函数,
//必须在派生类的复制构造函数之中显示调用基类的复制构造函数。
Derived d1("hello",",world");
d1.display();
Derived d2("shenzhen",",wangdao");
d2.display();
cout<<endl;
d1=d2;
d1.display();
d2.display();
cout<<endl;
Derived d3=d1;
d3.display();
return 0;
}
|