继承性
继承机制(inheritance)是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生的类,称为派生类,继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认识过程。
概念
通过继承的机制可对类分层,提供类/派生类的关系,C++通过类派生的机制支持继承,被继承的类称为基类或超类,新产生的类为派生类。基类和派生类的集合叫做继承的层次结构。
公有私有问题
- struct在声明一个类型时,默认情况下其属性值为公有,将其看做一个数据集合
- class在声明一个类型时,默认情况下其属性值为私有,这是对象,因为还有方法
继承关系:
两个struct之间继承时,没有访问限定符,缺省情况下为公有继承
- 派生类为class设计时,不论基类是class还是struct,都是私有继承
- 派生类为struct设计时,不论基类是class还是struct,都是公有继承
构建顺序
看下面的代码:
class Object
{
private:
int value;
int num;
public:
Object(int x = 0,int y = 0) :value(x),num(y) { cout << "Create Object: " << this << endl; }
~Object() { cout << "Destroy Object: " << this << endl; }
Object(const Object& obj) :value(obj.value),num(obj.num)
{
cout << "Copy Create Object; " << this << endl;
}
};
class Base : public Object
{
private:
int sum;
int fib;
public:
Base(int a = 0,int b = 0) :sum(a),fib(b) { cout << "Create Base: " << this << endl; }
~Base() { cout << "Destroy Base: " << this << endl; }
Base(const Base& base) :sum(base.sum),fib(base.fib)
{
cout << "Copy Creater Base: " << this << endl;
}
};
int main()
{
Base base(10);
return 0;
}
我们可以看到,优先创建的是基类,两个对象都为8Byte,对于Base的构建,总大小为16Byte,但此时的Object对象没有名字,再构建Base对象。
注意
:公有继承也并不是简单地将基类的属性继承下来,而是派生类Base中有三个成员:
打开监视器:
- 隐藏基对象(value,num)
- sum
- fib
继承的访问问题
两层继承关系
公有继承
私有继承
将上述代码改为私有继承后,仍然是ax不能,因为隐藏基对象变成了派生类的一个私有成员,fun函数是成员函数,既然对象B可以访问自己的私有属性bx,那么它也可以访问它私有对象的公有和保护属性,而不能访问私有对象的私有属性。
保护继承
总结;无论是公有继承、私有继承还是保护继承的方式,子类对象的方法
可以访问隐藏基对象的公有和保护属性。
要从对象结构来看,不要过于关注类的继承关系。
再看下面的继承问题:
对于继承关系中的基类有名对象和继承无名对象(隐藏基对象),最大的区别就是对于保护属性的访问,
- 我们可以访问无名对象的保护属性,因为此时会将其当做公有属性来看待
- 然而对于有名对象的保护属性,我们无法访问,我们只能够访问其公有属性
- 并且,该成员对象的有名对象无论在私有、保护、公有的情况下,都只能访问其公有属性。
再来看下面的代码示例,我们能否进行如下的五个修改:
这里和之前的区别就在于,main是一个外部函数,不是一个B对象的成员函数,因此只能访问B对象的公有。
想一想它的内存结构,得出:
- b.bz可以访问;
- b.az可以访问,因为它是公有继承的公有属性;
- b.ay不可以访问;
- b.aa.az可以访问,因为aa被设计成公有;
- b.aa.ay不可以访问。
通过以上的示例,我们需要理解外部函数与成员函数之间的区别,即外部函数只能够访问公有。
三层继承关系
上述示例中,对于类test的成员函数,我们首先可以访问自身的三个属性值,其次我们可以访问基类的保护和公有属性,还可以访问Object的公有和保护属性,因为都是公有继承。
然而,一旦第一组继承关系改为私有继承,想象一下内存结构图,就不可以访问Object的所有属性,因为此时该类在内存结构中成为了私有的结构。
最后,若将第一组继承关系改为保护,因为对于类test的成员函数,既然可以访问Base的共有和保护,其实OBject此时也被看做保护,那么也可以访问。
派生类可以继承基类的所有的成员和方法,除过构造函数和析构函数。
问题:派生类怎么初始化从基类继承来的成员变量?
答:在列表方式初始化的情况下,通过调用基类的相应的构造函数来初始化。
同名隐藏
同名隐藏,即在C++的继承结构中,只要子类的函数名和父类的函数名相同,子类中的函数将会隐藏所有父类中和子类的成员函数同名的函数
特别注意:
- 和函数之间的重载不同,这里
只要求函数的名字相同
,而对函数的参数列表是否相同不做要求。话句话说父类中和子类的成员函数名相同但参数列表不同的成员函数也会被隐藏——作用域的隐藏。 - 同名隐藏发生在函数的编译过程
派生类和基类出现同名属性的命名冲突时,派生类的方法只可以访问派生类的属性,而不会访问基类的属性,这就是同名隐藏。
如果非要访问基类的该属性或方法,就必须写上类名加作用域解析符,但如此访问的前提必须是公有继承,如果是保护或者私有继承,外部函数就无法访问继承而来的属性或方法。。
赋值兼容规则
关键点
:继承关系中:记住,默认情况下,只允许从下(派生类)到上(基类)的赋值规则。
在任何需要基类对象的地方都可以用公有派生类的对象来代替,这条规则叫做赋值兼容性规则,只有公有继承才有这条规则。
公有继承意味着:是一个,由此产生了类推的性质。
只能子给父。
由上图可知,基类指针是可以访问派生类的对象,但是由于指针类型的限制,限制了其解引用的能力,因此只能够访问派生类中的隐藏基对象。除非强转该指针的类型。派生类的指针是不可以访问基类的对象,因为其指针解引用后的内存比基类自身的内存要大,会访问到不合法的内存区域。
切片问题
该赋值过程:基类对象 = 派生类对象
,子对象的隐藏父对象可以赋值给父对象,这一过程中发生了切片。然而,派生类对象 = 基类对象
,这种是办不到的,
同时,还可以Object *op = &base; Object & op = base;
,但此时并不能识别整个base对象的内容识别出来,因为指针的解析能力被约束了,sizeof(Object) = 4Byte,sizeof(Base) = 8Byte,所以该类型指针也只能够访问四字节的空间。
拷贝构造函数的继承性
拷贝构造不具有继承性
对于下面的代码,在拷贝构造时,对于隐藏基对象的构造,系统默认调动构造函数:
class Object
{
private:
int value;
public:
Object(int x = 0) :value(x) { cout << "Create Object: " << this << endl; }
~Object() { cout << "Destroy Object: " << this << endl; }
Object(const Object& obj) :value(obj.value)
{
cout << "Copy Create Object: " << this << endl;
}
};
class Base : public Object
{
private:
int num;
public:
Base(int x = 0) :num(x), Object(x + 10) { cout << "Create Base: " << this << endl; }
~Base() { cout << "Destroy Base: " << this << endl; }
Base(const Base& base) :num(base.num)
{
cout << "Copy Create Base: " << this << endl;
}
};
int main()
{
Base base(10);
Base base2(base);
return 0;
}
利用赋值兼容性规则,可以使其调用拷贝构造函数构建Base2的隐藏基对象:
Base(const Base& base) :num(base.num),Object(Base)
{
cout << "Copy Create Base: " << this << endl;
}
若将上述base的拷贝构造改成下面的形式:
Base(const Base& base) :num(base.num)
{
Object(Base);
cout << "Copy Create Base: " << this << endl;
}
此时并没有在列表方式中明确告知调动Object的拷贝构造函数,所以会默认调动构造函数,因此此时Base2的value就为0,再来构建当前Base2对象,其num值为10,在函数体内,构建了一个无名对象,一旦构建出来就会被析构掉。注意这两种写法的异同。
赋值语句的继承性
赋值语句同样不具有继承性
class Object
{
private:
int value;
public:
Object(int x = 0) :value(x) { cout << "Create Object: " << this << endl; }
~Object() { cout << "Destroy Object: " << this << endl; }
Object(const Object& obj) :value(obj.value)
{
cout << "Copy Create Object: " << this << endl;
}
Object & operator=(const Object & obj)
{
if(this != &obj)
{
value = obj.value;
}
return *this;
}
};
class Base : public Object
{
private:
int num;
public:
Base(int x = 0) :num(x), Object(x + 10) { cout << "Create Base: " << this << endl; }
~Base() { cout << "Destroy Base: " << this << endl; }
Base(const Base& base):num(base.num),Object(base)
{
cout << "Copy Create Base: " << this << endl;
}
Base & operator=(const Base & base)
{
if(this != &base)
{
num = base.num;
}
return *this;
}
};
int main()
{
Base basea(10);
Base baseb(20);
basea = baseb;
return 0;
}
经过调试,我们发现,不会去调动Object的赋值语句,只会调动base的;
打开监视器,我们发现,只赋值了baseb结构的 “下半身” num
那么如何编写代码,才能达到我们理想的赋值语句呢?
Base & operator=(const Base & base)
{
if(this != &base)
{
*(Object*)this = base;
num = base.num;
}
return *this;
}
强行掉用父类的赋值函数,此时必须强转成为父对象,否则就会出现无穷递归掉用自身,溢出栈帧空间抛出异常