一.继承概念及定义方式
概念:
继承机制是面向对象程序设计时,让代码可以复用的一种手段。它允许在保持原类的基础之上,对这个类的内容进行扩展,延申,由此而产生的新类称之为子类,又名派生类。(继承是类设计层次的复用)。
定义方式:
//定义一个管理学校人员的类
//基类(父类)
class Person
{
public:
void Print()
{
cout << "姓名:" << _name << endl;
cout << "年龄:" << _age << endl;
}
private:
string _name = "txt";
int _age = 23;
};
//派生类(子类)
class Student :public Person
{
protected:
int _studentID;//学号
};
//派生类(子类)
class Teacher : Person//默认其继承方式为私有(private)
{
protected:
int _jobID;//工号
};
其中Student,Teacher为Person的派生类,public为继承方式,Person为基类。
注意:
基类成员在派生类中的访问方式 = min(成员在基类中的访问限定符,派生类的继承方式)
对上表的总结:
1.基类的private成员在派生类的继承中都是不可见的,但不可见是指语法上的限制,即派生类无论在类外还是类里边,都不能访问基类的私有成员。其实基类的私有成员还是会被继承到其派生类的对象中的。
2.如果基类的私有成员不想在类外直接被访问,但需要在其派生类中可以被访问,此时就需要将这个私有成员改为保护的(protected)。这里就体现出了private与protected访问权限的区别了。
3.在使用关键字class时,默认的继承方式为private,而struct关键字默认的继承方式为public。建议在定义时将继承方式标写清楚。
二.基类和派生类对象间的赋值转换
1.派生类对象可以赋值给基类的对象/基类的指针/基类的引用,就是派生类将自己继承的部分在赋值给基类,这个过程也被叫做切片或者切割。
2.基类的对象不可赋值给派生类对象。
3.基类的指针在指向派生类对象的情况下,可以通过强制类型转换的方式赋值给派生类指针。
class Person
{
protected:
int _ID;
int _age;
};
class Student : public Person
{
protected:
int _telnum = 158;//注意:此处要初始化一下,不然下边的赋值会中断
};
int main()
{
Student first;
Person one;
one = first; //赋值给基类的对象
//first = one;//编译不通过
Person* second = &first;//赋值给基类的指针
Person &third = first;//赋值给基类的引用
//基类的指针可以通过强制类型转换赋值给派生类的指针
Person* p = &first;
Student *pp = (Student*)p;
return 0;
}
三.继承的作用域
1.基类和派生类在继承体系中都有独立的作用域。
2.若基类与派生类中存在同名的成员,就会发生隐藏(重定义)的情况,即派生类成员将会屏蔽基类对同名成员的访问。如果还想在派生类中访问基类的这个成员,则需要使用:基类名称::基类成员的方式进行显示访问。
3.如果是成员函数隐藏,则只需要函数名相同即可。
4.在派生类中最好不要定义同名的成员或函数,以避免混淆。
成员隐藏
class Person
{
protected :
string _name = "txt"; // 姓名
int _num = 610; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout<<" 姓名:"<<_name<< endl;
//隐藏后想在派生类中访问基类成员,则需要显示访问
cout<<" 身份证号:"<<Person::_num<< endl;
cout<<" 学号:"<<_num<<endl;
}
protected:
int _num = 170; // 学号
};
函数隐藏
class A {
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A {
public:
void fun(int i)//与基类函数同名,构成隐藏
{
A::fun();
cout << "func(int i)->" <<i<<endl;
}
};
四.派生类的默认成员函数
1.构造函数:派生类构造函数会先调用基类的构造函数,去初始化从基类继承的成员。如果基类没有写默认构造函数,则需要派生类构造函数的初始化列表显示调用基类的默认构造函数。
2.拷贝构造函数:必须调用基类的拷贝构造函数完成所继承的基类成员的拷贝初始化。
3.operator=:与上同理。
3.析构函数:派生类的析构函数在调用完成后会自动调用基类的析构函数,以保证派生类对象先清理派生类成员之后在清理基类成员的顺序(构造时压栈的顺序)。
class Person
{
public:
Person(const char* name="txt")
:_name(name)
{
cout << "父类构造" << endl;
}
Person(const Person& name)
:_name(name._name)
{
cout << "父类拷贝构造" << endl;
}
Person& operator=(const Person& p)
{
cout << "父类赋值重载" << endl;
if (this != &p)
{
_name = p._name;
}
return *this;
}
~Person()
{
cout << "父类析构" << endl;
}
private:
string _name;
};
class Student :public Person
{
public:
Student(const char* name, int num)
:Person(name)
, _ID(num)
{
cout << "子类构造" << endl;
}
Student(const Student& s)
:Person(s)
, _ID(s._ID)
{
cout << "子类拷贝构造" << endl;
}
Student& operator=(const Student& s)
{
cout << "子类复制重载" << endl;
if (this != &s)
{
Person::operator=(s);//调用基类的赋值(其底层会发生切片赋值)
_ID = s._ID;
}
return *this;
}
~Student()
{
cout << "子类析构" << endl;
}
private:
int _ID;
};
测试:
int main()
{
//如果没有显示调用基类的构造函数,则编译器自动调用,就会导致,
//构造的对象与实际名字不符合的现象,此处预想名字为tong,而实际生成的名字为txt
Student a1("tong", 170);
Student a2(a1);//拷贝构造
Student a3 = a1;//赋值
return 0;
}
调用顺序截图:
扩展:实现一个不能被 继承的类
1.由以上概念可知,如果派生类不可调到基类的构造函数或者析构函数就说明:这个类不能被继承;
2.在C++11中给出了一个关键字(final),可以直接解决问题。
方法一:将构造或者析构函数的范文权限写成私有的,让派生类不可访问
class A
{
private:
A()
{}
//~A()
//{}
};
方法二:在类名字后边加上关键字final
class B final
{};
五.继承中的友元问题
基类的友元关系不可被继承:即基类的友元不能访问派生类的私有和保护成员(权限问题,一个类的友元可以随意访问本类的任意成员,但你不能因为继承了就可以访问人家子类的私有和保护成员了),但基类的友元是可以访问派生类的公有成员的。
class B;
class A
{
public:
friend void func(const A& a, const B& b);
protected:
int _age;
};
class B:public A
{
protected: //改为public权限,基类友元便可访问
int _ID;
};
void func(const A& a,const B& b)
{
cout << a._age << endl;
//cout << b._ID << endl;//语法编不过
}
int main()
{
A a;
B b;
func(a, b);
return 0;
}
六.继承体系中基类的静态成员
基类的静态成员:整个继承体系中中只有这样一个成员,无论其子类的多少,static成员只会有一个实例。
class A
{
public:
A(){ ++_count;}
public:
static int _count;
};
int A::_count = 0;
class B:public A
{
protected:
int _b;
};
class C :public A
{
protected:
int _c;
};
void main()
{
A a;
B b;//会先调用基类的构造函数,因此_count会加一次
C c;//同上
cout << " _count:" << A:: _count << endl;//输出:3
A::_count = 0;
cout << " _count:" << A::_count << endl;//输出:1
}
七.多继承与菱形继承
单继承:
一个子类只有一个直接父类,这个继承关系为单继承。如下A与B的关系
class A
{};
class B:public A
{};
多继承:
一个子类有俩个或以上的父类时,关系为多继承。如下C与A,B的关系
class A
{};
class B
{};
class C:public A,public B
{};
菱形继承:
多继承的一种特殊情况。如下所示的D类
class A
{
public:
int _num;
};
class B:public A
{};
class C:public A
{};
class D:public B,public C
{};
菱形继承的问题:
会造成数据的冗余和二义性问题。如上的D类里,有A类的俩份一样的数据_num,而在D类中想要访问_num数据时,不能明确的肯定访问的是BC里边的哪一个_num数据。
解决方法:
1.显示的指定访问内容
2.加virtual关键字,虚拟继承。
class A
{
public:
int _num;
};
class B: virtual public A
{};
class C: virtual public A
{};
class D:public B,public C
{};
虚拟继承的原理:
在D类对象的内存存储中,将所继承的A放在最下面,让B通过一个虚基表指针去访问存储了B到A的偏移量的虚基表,这样就可以通过偏移量找到下边存储的A了。C也是同理。
注意:虚继承因为会引入间接性指针,因此其大小会增加四个字节的大小。
八.继承与组合的比较
1.public继承是一种is-a的关系,即每个子类对象都是一个基类对象。
2.组合是一种has-a的关系,即B组合了A,每个B对象中都会有一个A对象。
3.继承是一种白箱复用,派生类可见基类的内部细节,在一定程度上破环了基类的封装性。基类的改变会直接对派生类产生巨大的影响,耦合度高。
3.组合是一种黑箱复用,组合之间依赖性不高,因此耦合度低。
4.除非必要,尽量使用组合(例如:多态的实现必须使用继承)。