继承的概念:
父辈的钱财(优点)被子孙继承。(就是这么简单)
一切空大的文字都是无力的,只有代码才是真。
class Person//基类(父类)
{
protected:
string _name;
string _sex;
int age;
};
class Student : public Person//派生类(子类)
{
public:
int _No;//学号
};
//这样就可以在Student依然拥有Person所有成员变量
补充一些知识:在父类中public表示对子类和类外公开,protected表示对子类公开但是对子类和自己以外私有,private表示仅对自己公开,子类和类外为私有、
为了让你更加明白父类与子类的关系,我制作了一下表格和帮助记忆的条款:
个人认为条款比表格更加实用
表格
继续补充:
可能你们会好奇为什么class Student : public Person为什么使用public,可以不给吗?
答案是:可以,但是如果class不给public则出现不能使用父类一切成员函数(变量),而struct则默认是全部公有不给public依然可以使用,也就是说class默认为private
赋值
子类和父类之间的赋值兼容规则:
子类对象可以赋值给父类对象/指针/引用,这个过程叫作切割或者切片
int main()
{
Person P;
Student s;
P = s;
Person* ptr = &s;
Person& ref = s;
//父类对象赋值给子类对象只有在特殊条件下可以,只有为什么会在多态说明
return 0;
}
重定义
当子类有相同名字的函数or成员变量时会隐藏父类的函数or成员变量
class A
{
public:
void fun()//因为与子类相同名字的函数,会被隐藏
{
cout << "hello world" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "xiaobo" << endl;
}
};
你会发现,如果你需要调用父类的fun()时不能直接调用,必须要声明它来自父类也就是A::fun()才可以调用,否则会报错
在学习派生(子)类的默认成员函数之前我们应当炒一下冷饭
6个默认成员函数
初始化和清理:1、构造函数主要完成初始化工作
2、析构函数主要完成清理工作
拷贝复制:3、拷贝构造时使用同类对象初始化创建对象
4、赋值重载主要是把一个对象赋值给另一个对象
取地址重载:5、主要是普通对象和const对象去地址,这两个很少会自己实现
子类默认成员函数
class Person
{
public:
Person(const char* name="pater")//构造
:_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 stuid)//构造
:Person(name),//只能调用父类的成员函数来处理父类的成员初始化等工作
_stuid(stuid)
{
cout << "Student()" << endl;
}
Student(const Student& s)//拷贝
:Person(s),
_stuid(s._stuid)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);//如果直接写operator=(s),编译器已经隐藏了父类赋值
//他会以为是递归,会导致无限递归栈溢出
_stuid = s._stuid;
cout << "Student& operator=(const Student& s)" << endl;
}
return *this;
}
//子类的析构函数和父类的析构函数构成隐藏
//因为他们的名字会被编译器统一处理成destructor(跟多态有关系)
~Student()
{
//Person::~Person();//正确用法不需要你显示调用,结束好会自动调用父类
cout << "~Student()" << endl;//结束后会自动调用~Person()
//应该像在栈中一样,因为先进入Person(),后进入Student()
//所以应该先出Student()后出Person()
}
protected:
int _stuid;
};
友元:
友元关系不能继承,也就是说父类友元不能访问子类私有和保护成员
//该代码无法运行,因为友元在父类中是无法使用的
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._studNum << endl;
}
int main()
{
Person p;
Student s;
Display(p, s);
}
继承和静态变量
//继承与静态成员
class Person
{
public:
Person()
{
++_count;
}
string _name;//姓名
static int _count;//统计人的个数
};
int Person::_count = 0;
class Student :public Person
{
protected:
int _stuNum;//学号
};
//静态变量存在公共区,无论子类还是父类改变都是永久性的
int main()
{
Person p;
Student s;
p._name = "jack";
s._name = "rose";
p._count = 1;
s._count = 2;
++Person::_count;
cout << Person::_count << endl;//输出3
cout << s._count << endl;//同样输出3
return 0;
}
复杂的继承及虚拟继承
单继承:一个子类只有一个总结父类时称这个继承关系为单继承
多继承:一个类有两个或以上直接父类时称中国继承关系为多继承
菱形继承:棱形继承是多继承的一种特殊情况
问题:数据冗余,二义性
如果直接使用菱形继承,会导致student和Teacher都有Person的属性,例如一个研究生他即是学生又是助教,但是他只有一个姓名,而非虚继承的会有两个名字导致数据冗余或二义性,当你使用虚拟继承后就可解决该问题,先看代码。
class Person
{
public:
string _name;//姓名
};
//虚拟继承
class Student:virtual public Person//想要解决菱形继承只能使用虚拟继承virtual
{
public:
int _num;//学号
};
class Teacher :virtual public Person
{
public:
int _id;//职工编号
};
class Assistant:public Student,public Teacher
{
protected:
string _majorCourse;//主修课程
};
void main()
{
Assistant a;
a._name = "pater";
//需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
接下来我将从内存的角度告诉你虚拟继承是如何解决该问题的。
class A
{
public:
int _a;
};
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;
cout << sizeof(d) << endl;//非虚拟继承大小为20,虚拟继承大小为24
d.B::_a = 1;//非虚拟继承B,C类有单独_a的空间,两个赋不同的值互不影响
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
内存的模样
大家可能会很好奇,为什么需要存放指针,指针内为何存放在从_a到指针的偏移量。这是为了子类给父类赋值时可以找到_a,例如:
int main()
{
D d;
A a;
a._a = 10000;
d=a;
return 0;
}
a中的_a会找到d中的A::_a对应指针,用偏移量加上地址找到_a后进行赋值
如今已经把所有的知识全部表述完成,我想我们应该做一些面试题来提升和检验一下自己的能力
面试题:
1、设计一个不能被继承的类
解答:
将构造函数和析构函数放到private中即可
2、平时使用继承还是组合?
解答:
is-a关系:car与奔驰和宝马的关系(继承)
hsa-a关系:tire(轮胎)和car的关系(组合)
都符合就使用组合
笔试题(自己写吧)
1、什么是菱形继承?菱形继承的问题是什么?
2、什么是菱形虚拟继承?如何解决数据冗余和二义性的
3、继承和组合的区别?什么时候用继承?什么时候用组合?