继承
概念:
继承是使代码可以复用的重要手段,它允许在保持原有类性能的基础性进行扩展,增加功能产生的新类,这个类称为派生类或者子类,那么被继承的类就叫做基类或者双亲类。继承是类层次的复用。
派生类:派生类是继承了基类中的成员变量。
有以下代码可以验证:
class base
{
public:
void setbase(int year, int month)
{
_year = year;
_month = month;
}
void print()
{
cout << _year << _month << endl;
}
public:
int _year;
int _month;
};
class Derived :public base
{};
int main()
{
cout << sizeof(Derived) << endl;
system("pause");
return 0;
}
由此可以看出派生类Derived继承了基类base的算有成员变量。
继承的格式:
继承的方式:
继承的方式与访问权限一致,都是有三方式:
public继承:
1.派生类可以访问基类中public和protected权限的成员。
2.不能访问基类中private权限的成员。但此时基类中private权限的成员已经继承到派生类中。
protected继承:
1.基类中的public访问权限的成员,在派生类中已经变为protected访问权限。
2.基类中protected访问权限的成员,在派生类中权限不变。
3.基类中的private访问权限的成员,在派生类中权限不变。在派生类中依旧不可见,但已继承在派生类中。
private继承:
1.基类中的public访问权限的成员,在派生类中已经变为private访问权限。
2.基类中protected访问权限的成员,在派生类中已经变为private访问权限。
3.基类中的private访问权限的成员,在派生类中权限不变。在派生类中依旧不可见,但已继承在派生类中。
图表如下:
补充:
使用关键字class时,默认的继承方式为private。用struct关键字时,默认的继承方式为public。最好直接给出继承方式。
基类与派生类对象之间的赋值转换:
赋值的规则:
1.必须是public继承权限。
2.在public权限下,可以将一个派生类对象看成一个基类对象。
3.所有用到基类对象的位置,都可以用派生类对象来代替。
对象模型:
对象中成员变量在内存中的布局形式。
赋值方式:
1.子类对象可以给基类对象赋值,相反则不行。
class B
{
public:
void SetB(int b)
{
_b = b;
}
protected:
int _b;
};
class D : public B
{
public:
void SetD(int b, int d)
{
_b = b;
_d = d;
}
int _d;
};
int main()
{
B b;
D d;
b.SetB(10);
d.SetD(20, 30);
b = d;
system("pause");
return 0;
}
2.可以用基类指针指向子类对象,可以用强转的方式让子类指针指向基类对象但是存在越界的风险。
int main()
{
B b;//基类
D d;//子类
B* pb = &d;
/*D* ppd =(D*)&b;//当给ppd中的_d赋值时,因为基类对象模型中没有派生类的自己独有的成员,所以会造成越界访问的问题。
ppd->_d = 1;*/
return 0;
}
3.子类的对象可以赋给基类的引用。反之则不行。
int main()
{
B b;//基类
D d;//子类
B& ppb = d;
return 0;
}
继承中的作用域:
1.在继承体系中,子类和基类不在同一个作用域,他们都有独立的作用域。
2.如果子类和基类中有同名的成员变量(函数),通过子类对象直接访问同名成员,优先访问到子类自己的成员,基类的同名成员不能直接访问(原因:子类将基类中的同名成员隐藏),此为同名隐藏。
注意:
(1)不管基类和子类的成员变量类型是否相同,都会产生同名隐藏。
(2)不论基类和子类的成员函数原型是否相同,只要函数名相同,就会产生同名隐藏。(因为不在同一作用域,所以没有函数重载)
(3)如果在同名隐藏的情况下,访问基类中的成员,应该在子类对象同名的成员前加上基类的作用域即可。
d._b = '1';//访问子类对象中的成员变量
d.B::_b = '2';//访问基类对象中的成员变量
d.Test(10);//访问子类对象中的成员函数
d.B::Test(10);//访问基类对象中的成员桉树
派生类的默认成员函数:
1.构造函数
》基类中如果没有显示定义构造函数,子类中也可以不用定义。
》基类中具有无参或者全缺省的构造函数,在子类构造函数初始化列表的位置调用或不调用基类的构造函数都可以,如果用户没有调用,则编译器会默认调用(如果子类中没有定义,编译器会自动生成默认的构造函数)。除非需要做特定的事情,子类可以给出构造函数。
》基类中具有带参数的构造函数(不是全缺省构造函数),则子类的构造函数必须显示提供,而且要在其初始化列表的位置显示调用基类的构造函数,已完成基类的部分成员的初始化。
2.拷贝构造函数
》基类中如果没有定义拷贝构造函数,则子类中也可以不用定义拷贝构造函数。
两个类都采用默认拷贝构造函数。(注意:前提是类中不会涉及资源管理,例如string类中的浅拷贝)
》基类中如果显示定义了自己的拷贝构造函数,则子类中必须定义拷贝构造函数,而且要在其初始化列表的位置显式调用基类的拷贝构造函数。
3.赋值运算符的重载
》基类中如果没有定义,子类中也可以不提供,除非子类需要进行其他操作。
》如果基类中显式定义了,则子类中也需要显式定义,且要在子类中(注意:这里不是在初始化列表中)调用基类的赋值运算符重载。
4.析构函数
》基类中如果没有定义,子类中也可以不提供,除非子类需要进行其他操作。
》如果基类中显式定义了,则子类中也需要显式定义,而且要在其初始化列表的位置显式调用基类的析构函数。
在继承体系中基类和派生类中的构造函数和析构函数调用次序:
class Father
{
public:
Father(int n = 10)
{
cout << "基类构造" << endl;
}
~Father()
{
cout << "基类析构" << endl;
}
};
class boy:public Father
{
public:
boy(int n = 10)
:Father(10)
{
cout << "派生类构造" << endl;
}
~boy()
{
cout << "派生类析构" << endl;
}
};
void test()
{
boy b;
}
int main()
{
test();
system("pause");
return 0;
}
注意:此处一定要区分 调用次序 和 函数体执行次序
函数调用次序为:派生类构造->基类构造->派生类析构->基类析构;
函数体执行次序(打印次序):基类构造->派生类构造->派生类析构->基类析构;
函数体执行次序如图:
如何让一个类无法被继承?
1.C++98中,可以将类中的构造函数私有化,则无法被继承。
2.C++11中,给出了final关键字可以禁止类的继承。
用法:class 类名 fina
继承与友元
我们先来写一段代码验证一下
class Base
{
friend void Print();
public:
Base(int b)
: _b(b)
{}
int GetB()
{
return _b;
}
protected:
int _b;
};
class Derived : public Base
{
public:
Derived(int b, int d)
: Base(b)
, _d(d)
{}
protected:
int _d;
};
void Print()
{
Base b(10);
cout << b._b << endl;
cout << b.GetB() << endl;
Derived d(20,30);
cout << d._d << endl;
}
int main()
{
Print();
return 0;
}
由结果可知,基类中的友元函数不能访问子类中的protected和private成员,所以友元函数无法被继承。
继承与静态变量
同样我们写一段代码来验证:
class Person
{
public:
Person(const string& name, const string& gender, int age)
: _name(name)
, _gender(gender)
, _age(age)
{
_count++;
}
Person(const Person& p)
: _name(p._name)
, _gender(p._gender)
, _age(p._age)
{
++_count;
}
~Person()
{
--_count;
}
protected:
string _name;
string _gender;
int _age;
public:
static size_t _count;
};
size_t Person::_count = 0;
class Student : public Person
{
public:
Student(const string& name, const string& gender, int age, int stuId)
: Person(name, gender, age)
, _stuId(stuId)
{}
Student(const Student& s)
: Person(s)
, _stuId(s._stuId)
{}
protected:
int _stuId;
};
class Teacher : public Person
{
public:
Teacher(const string& name, const string& gender, int age, int stuId)
: Person(name, gender, age)
, _stuId(stuId)
{}
Teacher(const Teacher& s)
: Person(s)
, _stuId(s._stuId)
{}
protected:
int _stuId;
};
void test()
{
Person p("aaa", "男", 18);
Student s("vvv", "女", 18, 20);
cout << p._count << endl;
cout << s._count << endl;
cout << &p._count << endl;
cout << &s._count << endl;
}
结论:
1.基类中静态成员变量可以被继承。
2.在整个继承体系中,静态成员变量只有一份。
复杂的菱形继承及菱形虚拟继承
单继承:一个子类中只有一个基类。
多继承:一个子类中有多个基类。(继承中,成员在子类对象中的排序与继承列表中的基类次序一致)
菱形继承:是多继承的一种特殊情况,是多继承和单继承的复合。
菱形虚拟继承
目的:让最顶层基类中成员在D类中只储存一份,解决菱形继承的二义性问题。
在了解菱形虚拟继承之前,我们先了解一下虚拟继承
虚拟继承:在C++11中给出了一个关键字(virtual)。
书写格式:class 派生类:virtual 继承权限 基类。
我们先来看一段虚拟继承的代码
class B
{
public:
int _b;
};
class D : virtual public B
{
public:
int _d;
};
int main()
{
cout << sizeof(D) << endl;
D d;
d._b = 1;
d._d = 2;
return 0;
}
从结果中,我们可以看出用虚拟继承之后派生类的大小多了四个字节,这是因为用虚拟继承的派生类里面会再生成一个指针。第二张图是定义一个派生类的对象时对象的地址,前四个字节就是指针,我们发现前四个字节中已经有数据。这是在构造对象的同时,编译器给对象前四个字节给的数据,这只能在构造函数中完成,因此编译器就会给派生类生成一个默认的构造函数。
从此图中,我们可以看出虚拟继承的派生类对象的对象模型中:基类成员变量在派生类自己的成员变量下面,与普通对象不同,这是因为派生类对象中的指针,指向的是一个偏移量表格(也可以叫虚机表)。虚机表里放的是偏移量,看图:
注意:一般不会实现虚拟继承,除非是为了解决菱形继承中的二义性问题才用。
现在我们回到菱形虚拟继承: