目录
前言
继承的概念和定义
访问权限表
基类和派生类对象的赋值转换
继承中的作用域
派生类的默认成员函数
继承与友元
继承与静态成员
复杂的菱形继承和菱形虚拟继承
菱形虚拟继承
观察内存
注意事项:对象在内存中的存储顺序是按声明的顺序存储的
继承(is-a)和组合(has-a)
前言
STL中不同的“容器”的迭代器在使用时是一样的,但实际上迭代器的底层实现并不相同,对于vector、string等空间连续的“容器”,它们的迭代器可以直接使用原生指针,而对于list、stack和quque等空间不连续的“容器”,它们的迭代器需要对原生指针进行封装后才能使用
在STL中,“容器”指代各种数据结构,容器的概念是在docker中才出现的
封装:
- 数据和方法放在一起,将可以被外部访问的定义成共有,不想访问的定义成私有和保护
- 一个类型放到另一个类型内,通过“typedef”或 “成员函数的调整”封装出另一个全新的类型
继承的概念和定义
基本概念:允许一个类(称为子类或派生类)获取另一个类(称为父类或基类)的属性和方法。通过继承,子类可以重用父类的代码,并且可以在此基础上添加新的属性和方法,或者修改现有行为
格式:class 派生类(子类) : 继承方式 基类(父类)
- 继承方式:public(共有继承)、protect(保护继承)、private(私有继承)
- 访问限定符:public(共有访问)、protect(保护访问)、private(私有访问)
#include <iostream>
#include <string >
using namespace std;
//基类Person
class Person
{
public://共有
void Print()
{
cout << "name: " << _name << endl;
cout << "age: " << _age << endl;
}
private://私有
int _age = 18;
string _name = "Mike";
};
//派生类Student
class Student : public Person
{
protected:
int _stuid;//学号
};
//派生类Teacher
class Teacher : public Person
{
protected:
int _jobid;//工号
};
int main()
{
Student std;
Teacher tea;
tea.Print();//成功调用基类中的Print函数
std.Print();//成功调用基类中的Print函数
return 0;
}
注意事项:继承后基类的的成员都会变成子类的一部分
访问权限表
访问限定 / 继承方式 | 公有继承 | 保护继承 | 私有继承 |
基类的公有成员 | 是派生类的公有成员(重点) | 是派生类的保护成员(重点) |
是派生类的
私有
成员
|
基类的保护成员
|
是派生类的
保护
成员
(重点)
|
是派生类的
保护
成员
(重点)
|
是派生类的
私有
成员
|
基类的私有成员
|
派生类中不可见
|
派生类中不可见
| 同左 |
结论:
1、基类的private成员在派生类中不可见是指,虽然该private成员被继承到了派生类对象中,但是派生类在类内和类外都不能直接访问基类的private成员,但是可以通过调用基类中的成员函数间接访问private成员(在派生类中通过调用基类的Print函数间接访问不能直接访问的_age)
2、 基类的其它成员在子类的访问方式 = Min(成员在基类的访问限定符,派生类的继承方式),public > protected > private(_name在基类中的访问限定是保护,派生类Student共有继承,则_name在Student中的访问权限是保护,在类外不能直接访问,Print函数在基类中的访问限定共有,共有碰共有,则Print函数在Student中的访问权限也是共有,在类外可以直接访问)
3、若想要基类的成员不被类外直接访问,就将该成员的访问限定设置为为private
4、若想要基类的成员不被类外直接访问,但需要让其派生类可以访问,就将该成员的访问限定设置为protect(保护成员限定符是因为继承才出现的)
4、struct默认继承方式和访问限定符都是公有, class 默认继承方式和访问限定符都是私有
struct Student (默认为public): Person;
class Student (默认为private): Person;
struct Student
{
//public
}
class Student
{
//private
}
5、在实际应用中一般都是public继承,很少使用protected / private继承
基类和派生类对象的赋值转换
基本概念:派生类以公有的方式继承基类,则可以将该派生类视为一个特殊的基类(public继承,每个子类对象都是一个特殊的父类对象)
切片/割:子类对象赋值给父类对象的过程
注意事项:
1、只能子赋值给父,不能父赋值给子
2、内数置类型直接赋值,自定义类型会调用该自定义类型的拷贝构造函
3、切片具有赋值兼容,在赋值过程中不会产生临时变量
4、派生类赋值给引用 或 指针,则引用或指针指向的就是派生类中的基类部分的内容,且可以通过引用和指针修改这一部分内容
#include <iostream>
#include <string >
using namespace std;
//基类Person
class Person
{
public://共有
void Print()
{
cout << "name: " << _name << endl;
cout << "age: " << _age << endl;
}
string _name = "Mike";
protected://保护
private://私有
int _age = 18;
};
//派生类Student
class Student : public Person
{
public:
void func()
{
cout << _name << endl;
//cout << _age << endl;//派生类不能直接访问基类的私有成员
Print();//但是可以通过调用基类的成员函数间接访问
}
protected:
int _stuid;//学号
};
//派生类Teacher
class Teacher : public Person
{
protected:
int _jobid;//工号
};
int main()
{
//基类和派生类对象的赋值转换
Student st;
Person p = st;
Person& ref = st;
Person* ptr = &st;
ref._name += 'x';
ptr->_name += 'y';
return 0;
}
5、在特殊情况下,指针和引用也可以将子类自己的内容赋值给父类
继承中的作用域
目前已知的四个域:局部域、全局域、命名空间域、类域
局部域和全局域影响生命周期,命名空间域和类域不影响生命周期
同一域中,不构成重载的函数不能重名,变量不能存在重名
基本概念:
1、基类和派生类都有独立的作用域(二者是独立的作用域)
2、子类和父类中有同名成员,子类成员会屏蔽对父类的同名成员的直接访问,这种情况叫隐藏也叫重定义,如果在子类中真的想要访问父类的同名成员,则需要使用域作用限定符::显示访问(基类::重名的基类成员)
3、父类和子类中的成员函数只要重名就会隐藏,不会构成重载
4、在继承体系中最好不要定义同名成员
#include <iostream>
#include <string >
using namespace std;
class A
{
public:
void fun()
{
cout << "父类fun()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "子类fun()" << endl;
}
};
int main()
{
//继承中的作用域
B bb;
bb.fun(1);
//bb.fun();//尝试访问父类的fun函数失败
bb.A::fun();
return 0;
}
派生类的默认成员函数
构造函数:必须调用基类的构造函数初始化基类的那一部分的成员,如果基类没有默认构造就显示调用基类的构造函数(默认构造和构造函数不一样,注意二者区别),然后处理派生类独有的成员
- 隐式调用:
#include <iostream>
#include <vector>
using namespace std;
class Base {
public:
Base()
{
cout << "Base constructor called." << endl;
}
};
class Derived : public Base {
public:
Derived()
{ //隐式调用基类的默认构造函数:编译器自动调用了,通过调试可以发现
cout << "Derived constructor called." << endl;
}
};
int main() {
Derived d; // 创建派生对象
}
- 显示调用:
#include <iostream>
using namespace std;
class Base {
public:
Base(int x)
{
cout << "Base constructor called with x = " << x << endl;
}
};
class Derived : public Base {
public:
Derived(int y)
:Base(10) //显式调用基类的构造函数(而不是默认构造函数)
{
cout << "Derived constructor called with y = " << y << endl;
}
};
int main() {
Derived d(20); // 创建派生对象
}
拷贝构造函数:必须调用基类的拷贝构造完成对基类那一部分的成员的拷贝,然后处理派生类独有的成员
赋值重载函数:必须调用基类的赋值运算符重载函数,完成对基类成员的复制,然后处理派生类独有的成员
析构函数:派生类的析构函数会在被调用玩抽自动调用基类的析构函数从而清理基类成员,只有这样才能保证析构时是先子后父(子类析构中可能还会用到父类的一些内容,父类先析构了子类就访问不到了)
注意事项:因为多态,析构函数名字会被统一处理成destructor,所以子类的析构也会隐藏父类,
#include <iostream>
#include <string >
using namespace std;
class Person
{
public:
Person(const char* name)//初始化队列
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)//拷贝构造,(this.s)
: _name(p._name)//this->_name(s._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(int num, const char* str, const char* name)//初始化队列
:Person(name)//父类的就调用父类的初始化列表
, _num(num)//子类
, _str(str)//子类
{
cout << "Student()" << endl;
}
// s2(s1)
Student(const Student& s)//拷贝构造,(const Student& this ,const Student& s)
:Person(s)//传引用切片,初始化一个父类的内容,调用父类的拷贝构造,Person(this,s)
, _num(s._num)//然后再将子类中特有的进行初始化
, _str(s._str)
{}
Student& operator=(const Student& s)//赋值重载
{
if (this != &s)//不能自己给自己赋值
{
Person::operator=(s);//调用父类的赋值重载,如果不指定父类的话子类会将父类的赋值重载隐藏,无限循环调用子类自己的赋值重载最后栈溢出
_num = s._num;
_str = s._str;
}
return *this;
}
// 子类的析构也会隐藏父类
// 因为后续多态的需要,析构函数名字会被统一处理成destructor
~Student()//析构函数
{
cout << _name << endl;
cout << "~Student()" << endl;
}
protected:
int _num;
string _str;
};
int main()
{
Student s1(1, "xxxx", "张三");//初始化
//Student s2(s1);//拷贝构造
//Student s3(2, "yyy", "李四");//初始化
//s1 = s3;//赋值重载
//Person p("李四");//初始化父类
//p.~Person();//析构函数可以直接显示调用
return 0;
}
继承与友元
基本概念:父类的友元关系子类不能继承,若派生类也想要和父类的友元函数成为自己的友元函数则需要再次声明(你父亲的朋友不是你的朋友)
//正确示例:
#include <iostream>
#include <string>
using namespace std;
class Student;//前向声明,person的友元函数需要一个Student类类型的对象,如果不事先声明则该友元函数因为没有可匹配的对象会被忽略
class Person
{
public:
friend void Display(const Person& p, const Student& s);//可以访问_name
protected:
string _name;
};
class Student : public Person
{
public:
friend void Display(const Person& p, const Student& s);//可以访问//_stuNum
protected:
int _stuNum;
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
Display(p,s);
return 0;
}
注意事项:
1、某类的友元函数可以访问该类中被保护的或是私有的成员
2、有时需要前向声明,否则会报错
继承与静态成员
基本概念:静态成员变量属于当前类也属于当前类的所有派生类(一家子公用的)
#include <iostream>
#include <string>
using namespace std;
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; // 研究项目
};
int main()
{
Person p;
Student s;
cout << &Person::_count << endl;
cout << &Student::_count << endl;
cout << &Graduate::_count << endl;
return 0;
}
注意事项:派生类在实例化时都会调用父类的相关构造(孙子也会调用爷爷的)
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
Person()//默认构造
{
++_count;//调用默认构造++_count
}
Person(const Person& p)//拷贝构造
{
++_count;//调用拷贝构造++_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; // 研究项目
};
int main()
{
Person p;
Student s1;
Student s2;
Student s3;
Student s4(s3);
Graduate g1;
cout << &Person::_count << endl;
cout << &Student::_count << endl;
cout << &Graduate::_count << endl;
cout << Person::_count << endl;
cout << Student::_count << endl;
cout << Graduate::_count << endl;
return 0;
}
复杂的菱形继承和菱形虚拟继承
单继承:一个子类只有一个直接父类
多继承:一个子类有两个或以上直接父类(番茄即是水果又是蔬菜)
菱形继承:多继承的一种特殊情况(助教即是老师也是学生)
virtual修饰的是在第一次分叉处的类
菱形继承的问题: 数据冗余和二义性问题
数据冗余:Assistant的对象中会有两份Person的成员,并且修改student中的person中成员name时,teacher中的相关name也会被修改,但是一名助教在作为学生时的名字是小赵,但是作为老师时的名字是张助教,即student和teacher中存的_name应该不同,但菱形继承导致助教只能用一个称呼
二义性:访问Assistan类类型的对象中的_name(Person中的)时,编译器不知道要访问哪个父类中的成员_name(当然可以通过域作用限定符显示访问,但是数据冗余问题得不到解决)
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _num; //学号
};
class Teacher : public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
int main()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a;
a._name = "peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
return 0;
}
菱形虚拟继承
基本概念:在菱形继承的腰部位置,加上virtual关键字
功能:解决数据冗余和二义性问题
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
string _name; // 姓名
};
class Student : virtual public Person
{
protected:
int _num; //学号
};
class Teacher : virtual public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
int main()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a;
a._name = "peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
return 0;
}
观察内存
#include <iostream>
using namespace std;
class A
{
public:
int _a;
};
// class B : public A
class B : virtual public A
{
public:
int _b;
};
// class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
virual修饰时,_a被放在了最下方的位置,同时在B和C类中有一个存放某地址的指针,由于是小端,低地址存低字节,两个地址分别是:0x0038AE1C和0x0038AE24,在内存窗口取地址发现它们应该分别指向两块空间,即虚基表,而那两个指针叫做虚基指针
第一个虚基表中存放的是16进制的14,对应的十进制是20,第二个虚基表中存放的是16进制的0c,对应的十进制是12,它们是虚基表中存放的偏移量,虚基指针可以通过这些偏移量找到虚基A(0X005EF75C + 14 = 0X005EF770、0X005EF764 + 0C = 0X005EF770)
//派生类赋值给基类时会发生切片
D d;
B b = d;
C c = d;
//两个将基类的指针存放派生类的地址时,会发生指针偏移,指向自己的对象位置
B* pb = &d;
C* pc = &d;
pb->_a++;
pc->_a++;
B、C类虚继承A类,_a 直接被放到了一个公共地址,如果 B 要找 _a 就需要虚基表
注意事项:对象在内存中的存储顺序是按声明的顺序存储的
结论:尽量不设计菱形继承
继承(is-a)和组合(has-a)
//组合
class A
{
private:
int _a;
};
class B
{
private:
A __aa;
int _b;
};
//继承
class A
{
private:
int _a;
};
class B : public A
{
private:
int _b;
};
注意事项:
1、默认情况下public继承,是一种is-a关系,即每个派生类对象都是一个基类对象(猴子是一种动物,猴子是派生类,动物是基类)
2、组合是一种has-a关系,B组合了A,则每个B类对象中都有一个A类对象(动物园拥有猴子)
3、优先使用对象组合,而不是类继承
4、组合和继承都是一种复用
5、继承允许根据基类的实现来定义派生类的实现,这种通过生成派生类的复用通常称为白箱复用,因为基类的内部细节对子类可见,派生类和基类间的依赖关系很强,耦合度高
7、软件设计应该是低耦合高内聚
- 低耦合:类和类之间的关系、模块和模块之间的关系,不那么紧密,关联度不高
- 高内聚:当前类或模块中各种方法的关联程度高
8、适合is-a关系就用继承,适合has-a关系就用组合,都适合就用组合
~over~