菱形继承"/>
C++中的继承以及菱形继承
继承:面向对象程序设计使代码可以复用的最重要手段,它允许程序员在保持原有类特性的基础上进行拓展,增加功能。
继承格式:class 派生类:(public,protected,private)基类
继承方式 | 基类的public成员 | 基类的protected成员 | 基类的private成员 | 继承引起的访问控制关系变化概括 |
public继承 | 仍为public成员 | 仍为protected成员 | 不可见 | 基类的非私有成员在子类的访问属性都不变 |
protected继承 | 变为protected成员 | 仍为protected成员 | 不可见 | 基类的非私有成员在子类的访问属性都变为protected成员 |
private继承 | 变为private成员 | 变为private承欢 | 不可见 | 基类的非私有成员都变为子类的私有成员 |
一个关于继承的简单例子
class Base
{
public:
Base()
{
_pri = 1;
_pro = 2;
_pub = 3;
cout << "Base()" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
void ShowBase()
{
cout << "_pri = " << _pri << endl;
cout << "_pro = " << _pro << endl;
cout << "_pub = " << _pub << endl;
}
private:
int _pri;
protected:
int _pro;
public:
int _pub;
};
class Derived:public Base
{
public:
Derived()
{
_pro *= 10;
_pub *= 10;
_dpri = 4;
_dpro = 5;
_dpub = 6;
cout << "Derived()" << endl;
}
~Derived()
{
cout << "~Derived()" << endl;
}
void ShowDerived()
{
/*cout << "_pri = " << _pri << endl;
_pri是Base类的私有成员,派生类不能够访问到*/
cout << "_pro = " << _pro << endl;
cout << "_pub = " << _pub << endl;
cout << "_dpri = " << _dpri << endl;
cout << "_dpro = " << _dpro << endl;
cout << "_dpub = " << _dpub << endl;
}
private:
int _dpri;
protected:
int _dpro;
public:
int _dpub;
};
int main()
{
Base base;
base.ShowBase();
Derived der;
der.ShowDerived();
return 0;
}
测试结果如下
Base() //调用Base类的构造函数
_pri = 1 //下面三行输出Base类中的值
_pro = 2
_pub = 3
Base() //构造派生类对象先调用基类的构造函数
Derived() //再调用派生类自己的构造函数
_pro = 20 //在派生类中可以访问到基类的protected成员变量
_pub = 30 //在派生类中可以访问到基类的public成员变量
_dpri = 4
_dpro = 5
_dpub = 6
~Derived() //析构派生类对象的时候,先调用派生类自己的析构函数
~Base() //再调用基类的析构函数
~Base() //析构基类调用基类的构造函数
关于继承的一些总结
-
父类的private成员是不能够被子类继承的,因为对于子类来说父类的private成员是不可见的,那么如果一个类不想让别的类访问到自己的某些成员,但又想让子类访问到,那么就定义成protected,protected成员就是为了继承而出现,能够让这个访问类型的成员只能够被该类的子类访问到
-
public继承相当于是一个接口继承,保持is-a原则(一个子类就是(is)一个(a)父类对象),每个父类可用的成员对子类也可用
-
protected/private继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分,是has-a的关系原则(一个子类并不是一个父类对象,父类的公有成员或保护成员在子类只是保护的或是私有,只能说一个子类中有(has)一个(a)父类对象),一般来说不是特殊情况不会用到这两个继承,基本上的用的都是public继承
-
不管是哪种继承方式,子类都可以访问到父类的公有成员和保护成员,父类的私有成员存在但是在子类中是不可见的,即不能够被子类访问到
-
使用关键字class时默认的继承方式是private,使用关键字struct时默认的继承方式是public(不过最好显示的写出来,比如可以有这种写法class B:A,但是A默认继承方式是私有的,所以最好显示写出来)
继承与转换——赋值兼容规则(public继承,对应上面的is-a的具体体现,若是保护或私有继承就不能赋值)
class A
{
public:
int _a;
};
class B:public A
{
public:
int _b;
};
int main()
{
A a;
a._a = 10;
B b;
b._a = 20;
//切片:子类对象赋值给父类的对象/指针/引用
a = b; //成功
//b = (B)a; //编译器不让有这种方式的强转
A* p1 = &b; //成功
A& r1 = b; //成功
B* p2 = (B*)&a; //强转成功,父类的指针强转成了子类的
cout << p2->_b; //能够读到_b,是个随机值
//p2->_b = 20;
//但是这里通过这个指针访问_b就越界了,_b能够被读到,是个随机值,但是不属于这个指针所管辖的地址,一旦尝试写这个_b,就会发生越界访问错误所以不被提倡这样用(是一个运行时错误,编译时发现不了)
B& r2 = (B&)a;
//r2._b = 20; //这里和上面一样,也会越界
return 0;
}
-
子类对象可以直接赋值给父类对象(切片/切割)
-
父类对象不能赋值给子类对象
-
父类的指针/引用可以指向子类对象(A* p1=&b A& r1=b;)
-
子类的指针/引用不可以指向父类对象(可以通过强转实现,但是容易发生越界的问题,不提倡使用)
-
切片:子类对象赋值给父类的对象/指针/引用(为多态打基础)
继承体系中的作用域
-
在继承体系中基类和派生类都有独立的作用域
-
子类和父类中有同名成员,将访问子类中的,要访问父类的需要加上父类的作用域——这种现象叫做隐藏或是重定义 )
-
重定义:子类重新定义和父类同名的成员(包括变量和函数),注意重定义是在不同的作用域中,而重载是在同一个作用域中的
-
在实际中在继承体系里面最好不要定义同名的成员。
class A
{
public:
int _a;
};
class B :public A
{
public:
int _a;//与父类同名的成员
int _b;
};
int main()
{
B b;
b._a = 1;//优先取子类自己作用域中的_a
b.A::_a = 2;//访问A中的_a
b._b = 2;
return 0;
}
测试结果
继承体系中的静态成员
如果一个基类定义了static成员,那么在整一个继承体系中,只有一个这样的成员,无论派生出多少子类,都只有一个
class A
{
public:
static int _a;
};
int A::_a = 0;
class B:public A
{
protected:
int _b;
};
int main()
{
B b;
cout << A::_a << " ";
cout << B::_a << " ";
B::_a = 5;
cout << A::_a << " ";
cout << B::_a << " ";
return 0;
}
输出结果
这个结果说明,在一个继承体系中,这个静态成员有且只有一份。
派生类的默认成员函数
在继承关系中,如果派生类没有显示定义类的六个默认成员函数(构造,拷贝构造,析构,赋值运算符重载,取地址运算符重载,const取地址运算符重载),编译系统会合成这六个默认的成员函数
所谓的合成默认成员函数:派生类只初始化自己的成员,父类部分调用父类的成员函数
继承关系中构造函数调用顺序
-
基类没有缺省构造函数,派生类必须要在初始化列表中显式给出基类名和参数列表
-
基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数
-
基类定义了带有形参表构造函数,派生类就一定要定义构造函数
继承关系中析构函数调用顺序
如果子类中没有显式定义构造函数
class Person
{
public:
Person(const char * name)//这个构造函数不是缺省的
: _name(name)
{
cout << "Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public://此处并没有显式的定义子类的构造函数
protected:
int _id;
};
int main()
{
Student s;//这里将会编译不通过,因为父类没有缺省的构造函数
return 0;
}
如果把父类的构造函数设置为缺省的,就可以通过了。
class Person
{
public:
Person(const char * name = "pigff")//这个构造函数是缺省的
: _name(name)
{
cout << "Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public://此处并没有显式的定义子类的构造函数
protected:
int _id;
};
int main()
{
Student s;//此时编译通过
return 0;
}
当我们把子类的构造函数显式定义出来,但是必须在初始化列表处显示给出基类名和参数列表
class Person
{
public:
Person(const char * name = "pigff")//这个构造函数是缺省的
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
:_name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
{
_name = p._name;
}
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
//合成构造函数
Student(const char* name, int id)//此时给出了子类显式定义的构造函数
:Person(name)//在初始化类别处显式给出父类名和参数列表,会去调用父类的构造函数
, _id(id)
{
cout << "Student()" << endl;
}
//合成拷贝构造函数
Student(const Student& s)
:Person(s)//一样的,也要显示给出父类名和参数列表,调用父类的拷贝构造函数,这个地方会有切片
,_id(s._id)
{
cout << "Student(const Student& s)" << endl;
}
//合成赋值运算符重载
Student& operator=(const Student& s)
{
cout << "Student operator=(const Student& s)" << endl;
if (this != &s)
{
Person::operator=(s);//显示调用基类的赋值运算符重载
_id = s._id;
}
}
~Student()
{
Person::~Person();
}
//这里析构函数和上面的函数不一样,不需要我们显式地去调用,父类的析构函数会在子类的析构函数完成后自动去调用
protected:
int _id;
};
int main()
{
Student s("pigff",5);//调用显式定义的构造函数并通过
Student s1(s);//调用显示定义的拷贝构造函数并通过
Student s2("Mike",6);
s2 = s;//调用显示定义的赋值运算符重载函数并通过
return 0;
}
关于析构函数不需要显式去调用的证明
这里输出了两句~Person(),一句是我们显式调用时输出的,另一句是在子类调用析构函数完成后自动调用了析构函数输出的。当我们把子类的析构函数中那句输出语句去掉的时候,如下
如何实现一个类不能够被继承?
-
将构造函数设成是私有的(合成默认构造函数的时候,会去调用父类的构造函数,但是父类构造函数是私有的,压根就访问不到)
继承体系中的友元关系
友元关系不能够被继承。即基类的友元能够访问基类的私有成员,但是不能访问子类的私有成员
class B;//在此处先声明一下
class A
{
friend void Print(A& a, B& b);//友元函数Print
public:
A(int a)
:_a(a)
{}
protected:
int _a;
};
class B:public A
{
public:
B(int a,int b)
:A::A(a)//用父类的构造函数来初始化父类的成员
,_b(b)
{}
protected:
int _b;
};
void Print(A& a, B& b)
{
cout << a._a << " ";//因为是A类型的友元,可以访问到A的私有或保护成员
cout << b._a << " ";
//虽然不是B类型的友元,但是是B的父类A类的友元,所以可以访问到B类型中父类的私有或保护成员
//cout << b._b << " ";//会出现编译错误,基类的友元访问不到子类的私有或是保护成员
}
int main()
{
A a(1);
B b(2, 3);
Print(a,b);
return 0;
}
输出结果
关于继承的模型
单继承:一个子类只有一个直接父类
多继承:一个子类有两个或两个以上的直接父类
菱形继承
菱形继承容易导致二义性和数据冗余的问题
现在一个D中有两个_a,一个是从B类中继承下来的,另一个是从C类中继承下来的,那么在使用的时候我们就不知道到底是哪一个_a了,所以这样就容易产生二义性的问题。我们可以在_a前面加上一个类名,比如B::_a=1,C::_a=2,那么这样就消除了二义性的问题。但是依然没有解决数据冗余的问题!!
class A
{
public:
int _a;//这里声明成public方便后面访问
};
class B:public A
{
public:
int _b;
};
class C:public A
{
public:
int _c;
};
class D:public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;//通过指定是哪个类中的_a来达到消除二义性的目的
d.C::_a = 2;
cout << d.B::_a << " " << d.C::_a << endl;
return 0;
}
输出结果如下
至于数据冗余,上面的方式消除不了,于是就引进了虚继承的概念
改一下上面的代码,引入关键字virtual
class A
{
public:
int _a;//这里声明成public方便后面访问
};
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;
};
int main()
{
D d;
d._a = 1;//这个时候直接访问就可以了,d这个对象里面只有一个_a了,解决了数据冗余问题
return 0;
}
那么虚继承到底是怎么消除数据冗余的呢?
我们先来求一下没有实现虚继承时候的D对象模型的大小
cout << sizeof(d) << endl;
结果是20,这个很好理解,可以看之前的D对象的对象模型,总共有5个int类型的变量,就是20。
再来看一下实现虚继承时候的D对象模型的大小。在此之前我们先想一下,之前没有实现虚继承的时候有两个重复的_a,那么虚继承消除了数据冗余的情况下,意味着只有一个_a了,那么答案会是16吗?
出乎意料,这里大小是24。那么问题来了,24-16=8,这多出来的8个字节的东西放了什么呢?
要了解这个东西,先来看下面的测试代码,先测试不是虚继承的情况
D d;
d.B::_a = 0;
d.C::_a = 1;
d._b = 2;
d._c = 3;
d._d = 4;
我们通过内存来看下一下d对象的对象模型
先继承B还是先继承C,这是通过声明次序的不同而不同, 比如是class D:public C,public B,那么C对象就会在B对象的上面。从这个内存模型中我们可以看到,正好是5个int,大小刚好是20个字节。
我们再来测试虚继承的情况,因为只有一个_a,我们对上面的代码稍作改动
D d;
d._a = 1;
d._b = 2;
d._c = 3;
d._d = 4;
再来看看d对象的对象模型
虚继承通过上图的方式,存了在16个字节的基础上,在B类和C类中多用了8个字节的大小来存放B类和C类到A类中_a的偏移量,所以16+8=24,所以这个d对象的大小就是24个字节。
我们把虚继承中的这个有关偏移量的表叫做虚基表
注意:至于为什么不是将偏移量存放在指定地址,而是在指定地址的后4个字节,是因为这4个字节是为多态预留的(在多态中我们还会来分析这个模型)
一般不到万不得已不要使用菱形继承虚继承,因为菱形继承虚继承也带来了性能上的损耗(毕竟多开了地址来存放偏移量)。
更多推荐
C++中的继承以及菱形继承
发布评论