一、继承概念:是面向对象程序设计使代码可以复用的重要手段,保持原有类特性基础进行扩展,增加功能,产生新的类,称为派生类。
定义格式:
三种继承关系:public,protected,private
三种类成员访问限定符:public,protected,private(需要与三种继承关系区别,不能混淆)
举一个简单类:B公有继承(public)与A,B为派生类,A为基类
class A
{
public:
void FunTest1()
{
cout<<"FunTest1"<<endl;
}
int _a1;
protected:
int _a2;
private:
int _a3;
};
class B:public A
{
public:
void FunTest2()
{
cout<<"FunTest2"<<endl;
}
private:
int _b;
};
void FunTest()
{
B b;
b.FunTest1();
b._a1=1;
}
继承方式与基类成员访问限定符关系变化:
总结:1.派生类继承了(无论哪一种继承方式)基类内部所有数据成员和成员函数,在派生类内部中可以访问基类的公有(public)成员和保护(protected)成员,基类的私有(private)成员存在但不可见,派生类不可以直接访问;
2.若基类成员不想在类外直接被访问,但需要在派生类中被访问,就定义为protected(即保护成员限定符是因继承才出现的);若在类外定义派生类对象,其也只能访问派生类中的public成员;
3.public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象;protetced/private继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分,是 has-a 的关系原则,所以非特殊情况下不会使用这两种继承关系,一般使用的都是公有继承。
4.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
二、派生类的构造函数和析构函数(区别调用构造函数和执行函数体两种顺序)
类继承关系中派生类调用构造函数顺序:
派生类构造函数————>>基类构造函数(在派生类初始化列表调用)
函数体执行顺序:
基类构造函数————->>派生类构造函数(因为在派生类初始化列表又调用了基类构造函数,所以先执行基类函数体)
调用析构函数顺序:
派生类析构函数————>>基类析构函数
函数体执行顺序:
派生类析构函数————>>基类析构函数(因为:1派生类对象中自己独有成员创建晚,生命周期短,2派生类析构自己独有成员,基类析构自己独有成员,二者没有关系)
举例如:
class A
{
public:
A()
{
cout<<"A()"<<endl;
}
~A()
{
cout<<"~A()"<<endl;
}
int _a1;
protected:
int _a2;
private:
int _a3;
};
class B:public A
{
public:
B()
{
cout<<"B()"<<endl;
}
~B()
{
cout<<"~B()"<<endl;
}
private:
int _b;
};
void FunTest()
{
B b;
}
在VS2010中其运行结果显示为派生类对象b调用构造函数和析构函数的函数体执行顺序:
注意:
1.基类有缺省构造函数,派生类没有显示定义构造函数,编译器会给派生类合成默认构造函数;
2.基类没有缺省构造函数,派生类必须要在初始化列表中显式给出基类名和参数列表;如:
class A
{
public:
A(int a)
{
cout<<"A()"<<endl;
}
};
class B:public A
{
public:
B(int a)
:A(a)
{
cout<<"B()"<<endl;
}
};
void FunTest()
{
B b(2);
}
3.基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数;
4.基类定义了带有形参表构造函数,派生类就一定定义构造函数。
三.继承与转换
同名隐藏:若基类与派生类中有相同名字函数或数据成员,在类外派生类对象调用基类同名成员时,加基类作用域,如:
class A
{
public:
void FunTest()
{
cout<<"A::FunTest()"<<endl;
}
int _a;
};
class B:public A
{
public:
void FunTest()
{
cout<<"B::FunTest()"<<endl;
}
int _a;
};
void FunTest()
{
B b;
b.FunTest(); //访问的是派生类中的FunTest();
b.A::FunTest();//访问基类A中FunTest();
b._a; //访问派生类成员
b.A::_a; //访问基类成员
}
赋值兼容规则 --public 继承
1. 子类对象可以赋值给父类对象;
2. 父类对象不能赋值给子类对象;
3. 父类的指针/引用可以指向子类对象;
4. 子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)
在VS2010中举例如:
class A
{
public:
int _a;
};
class B:public A
{
public:
int _b;
};
void FunTest()
{
A a; //父类对象
B b; //子类对象
a=b;
//b=a; 编译错误
b=*(B*)&a; //可强制类型转换(使用时需要谨慎)
A* pa=&b;
A& ra=b;
//B* pb=&a; 编译错误
//B& rb=a; 编译错误
B* pb=(B*)&a; //强转
B& rb=*((B*)&a);
}
四.继承方式
单继承:一个子类只有一个直接父类时称这个继承关系为单继承(简单的基本继承)
多继承:一个子类有两个或以上直接父类称为多继承
菱形继承:如
在VS2010中举简单例子:
class A
{
public:
int _a;
};
class B:public A
{
public:
int _b;
};
class C:public A
{
public:
int _c;
};
class D:public B,public C
{
public:
int _d;
};
void FunTest()
{
D d;
}
运行监视d对象组成:
它的内存组成方式为:(内存分布按照继承顺序)
由上可知,d中_a占了两份内存空间,因为类C与类D都继承了类A的成员,所以菱形继承存在二义性与数据冗余问题,浪费空间,为解决这一问题,出现
虚继承:在继承方式前加关键字:virtual(使_a只占一份空间,能够共用)
如:
class A
{
public:
int _a;
};
class B:virtual public A
{
public:
int _b;
};
class C:virtual public A
{
public:
int _c;
};
class D:public B,public C
{
public:
int _d;
};
void FunTest()
{
D d;
d._a=1;
d._b=2;
d._c=3;
d._d=4;
}
运行完成查看d内存:
即内存分布方式为:(即内存第一份和第三份空间是个指针,存储地址,监视查看此地址内容,存储的为分别相对于B和C自身偏移地址与相对于_a偏移地址(可把它们看成一个表))
一般派生类是虚继承时,编译器会为其合成默认构造函数,作用:将存储偏移地址的表首地址赋给其对象内存存储顺序第一个空间。
虽然虚继承解决了菱形继承的二义性和数据冗余的问题,但虚继承效率太低,每次都要访问表的偏移地址后才能访问其成员。
注意:
1.友元与继承友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员;
2.如果类中包含静态成员,无论继承多少派生类,静态成员只保存一份;
3.构造函数和析构函数不能被继承。