1.继承的概念和定义
1.1继承的概念
继承 (inheritance) 机制是面向对象程序设计 使代码可以复用 的最重要的手段,它允许程序员在 保 持原有类特性的基础上进行扩展 ,增加功能,这样产生新的类,称派生类。继承 呈现了面向对象 程序设计的层次结构 ,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用, 继 承是类设计层次的复用。
#include <iostream>
class A
{
public:
int ma;
private:
int mb;
protected:
int mc;
};
class B : public A // 继承:A叫做基类/父类 B叫派生类/子类
{
public:
void func()
{
std::cout << ma << std::endl;
}
int md;
private:
int me;
protected:
int mf;
};
int main()
{
A a;
B b;
std::cout << sizeof(a) << std::endl; // 12
std::cout << sizeof(b) << std::endl; // 24
}
1.2继承基类成员访问方式的变化
总结:
1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私 有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它。
2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在 派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过 最好显示的写出继承方式。
5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡 使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里 面使用,实际中扩展维护性不强
1.3默认的继承方式?
class定义的默认private,struct定义的默认public的.
1.4派生类的构造函数
派生类可以继承基类的构造函数和析构函数,用来初始化和释放从基类继承来的成员变量
派生类的构造函数和析构函数,负责初始化和清理派生类
派生来从基类继承来的成员的初始化和清理由基类的构造函数和析构函数来负责。
2.基类和派生类对象赋值转换
- 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片
- 或者切割。寓意把派生类中父类那部分切来赋值过去。
- 基类对象不能赋值给派生类对象。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类
- 的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run
- Time Type Information)的dynamic_cast 来进行识别后进行安全转换。
#include <iostream>
/*
1.把继承结构,也说成从上(基类)到下(派生类)的结构
2.
基类对象 -> 派生类对象
派生类对象 -> 基类对象
基类指针(引用) -> 派生类对象
派生类指针(引用) -> 基类对象
总结:在继承结构中进行上下的类型转换,默认支持从下到上的类型的转换
*/
class Base
{
public:
Base(int data = 10) :ma(data)
{}
void show()
{
std::cout << "Base:show()" << std::endl;
}
void show(int)
{
std::cout << "Base:show(int)" << std::endl;
}
private:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 20):Base(data), mb(data)
{}
void show()
{
std::cout << "Derivr::show()" << std::endl;
}
private:
int mb;
};
int main()
{
Base b(10);
Derive d(20);
// 基类对象 <- 派生类对象 类型从下到上的转换 Y
b = d;
// 派生类对象 <- 基类对象 类型从上到下的转换 N
// d = b;
// 基类指针(引用)<- 派生类对象 类型从下到上的转换 Y
// 只能访问派生类继承的基类的那部分内容
Base* pb = &d;
pb->show();
pb->show(10);
((Derive*)pb)->show(); // 类型强转为派生类的指针就可以访问派生类的内容
// 派生类指针(引用)<- 基类对象 类型从上到下的转换 N
/*Derive* pd = (Derive*) & b; 不安全,涉及了内存的非法访问
pd->show();*/
d.show();
d.Base::show(10);
// d.show(20); // 优先找的是派生类自己作用域的show名字成员;没有的话才去基类里面找
return 0;
}
3.重载、隐藏、覆盖
1.重载关系
一组函数要重载,必须处在同一个作用域中;而且函数名字相同,参数列表不同
2.隐藏关系
在继承结构当中,派生类的同名成员把基类的同名成员给隐藏调用了
3.覆盖关系/相当于重写方法
虚函数表中虚函数地址的覆盖
4.虚函数、静态绑定、动态绑定
#include <iostream>
#include <typeinfo>
class Base
{
public:
Base(int data = 10) :ma(data)
{}
// 虚函数
virtual void show()
{
std::cout << "Base:show()" << std::endl;
}
// 虚函数
virtual void show(int)
{
std::cout << "Base:show(int)" << std::endl;
}
private:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 20) :Base(data), mb(data)
{}
void show()
{
std::cout << "Derivr::show()" << std::endl;
}
private:
int mb;
};
int main()
{
Derive d(50);
Base* pb = &d;
/*
pb是基类类型 Base::show如果发现show是普通函数,就进行静态绑定call Base::show()
如果发现pb是基类类型,编译阶段在Base类中去看show函数,发现是虚函数,就进行动态绑定了
*/
pb->show(); // 静态(编译时期)的绑定(函数的调用)
pb->show(10);
std::cout << sizeof(Base) << std::endl;
std::cout << sizeof(Derive) << std::endl;
/*
pb的类型:Base -> 有没有虚函数
如果Base没有虚函数,*pb识别的就是编译时期的类型, *pb -> Base类型
如果Base有虚函数,*pb识别的就是运行时期的类型 RTTI 类型 "calss Derive"
*/
std::cout << typeid(pb).name() << std::endl;
std::cout << typeid(*pb).name() << std::endl;
return 0;
}
总结一:
如果一个类里面定义了虚函数,那么编译阶段,编译器给这个类类型产生了一个唯一的vftable虚函数表,
虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。
RTTI:run-time type information 运行时的类型信息 指向类型字符串,例如我的类是Base那么&RTTI指向"Base"
当程序运行时,每一张虚函数表都会加载到内存的.rodata区,是个只读数据区,也叫常量区。总结二:
一个类里面定义了虚函数,那么这个类定义的对象。其运行时,内存中开始部分,多存储一个vfptr虚函数指针,指向
类型的虚函数表vftable。一个类型定义的n个对象,它们的vfptr指向的都是同一张虚函数表。总结三:
一个类里面虚函数的个数,不影响对象内存大小,都是多一个虚函数指针(vfptr 4个字节),影响的是虚函数表的大小总结四:
如果中的方法和基类继承来的某个方法,返回值,函数名,参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法,自动处理成虚函数。这两个函数的关系是覆盖的关系,相当于重写了这个函数
5.派生类的默认成员函数
6 个默认成员函数, “ 默认 ” 的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。3. 派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。5. 派生类对象初始化先调用基类构造再调派生类构造。6. 派生类对象析构清理先调用派生类析构再调基类的析构。7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同 ( 这个我们后面会讲解) 。那么编译器会对析构函数名进行特殊处理,处理成 destrutor() ,所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
6.继承和友元
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
void main()
{
Person p;
Student s;
Display(p, s);
}
7.继承与静态成员
class Person
{
public :
Person () {++ _count ;}
protected :
string _name ; // 姓名
public :
static int _count; // 统计人的个数。
};
int Person :: _count = 0;
class Student : public Person
{
protected :
int _stuNum ; // 学号
};
class Graduate : public Student
{
protected :
string _seminarCourse ; // 研究科目
};
void TestPerson()
{
Student s1 ;
Student s2 ;
Student s3 ;
Graduate s4 ;
cout <<" 人数 :"<< Person ::_count << endl;
Student ::_count = 0;
cout <<" 人数 :"<< Person ::_count << endl;
}