海原县建设局网站,旅游做攻略用什么网站好,网站怎么做ipfs,wordpress 禁用 提示目录
一、继承的概念及定义
1.1 继承概念
1.2 继承定义
1.3 继承基类成员访问方式的变化
二、基类和派生类对象赋值转换
三、继承中的作用域
四、派生类的默认成员函数
五、继承与友元
六、继承与静态成员
七、菱形继承及菱形虚拟继承
7.1 继承的分类
7.2 菱形虚拟…目录
一、继承的概念及定义
1.1 继承概念
1.2 继承定义
1.3 继承基类成员访问方式的变化
二、基类和派生类对象赋值转换
三、继承中的作用域
四、派生类的默认成员函数
五、继承与友元
六、继承与静态成员
七、菱形继承及菱形虚拟继承
7.1 继承的分类
7.2 菱形虚拟继承
7.3 菱形虚拟继承原理
八、继承总结 前言面向对象三大特性是封装、继承、多态封装初阶的时候已经讲了进阶开始讲解继承和多态和一些更复杂的结构今天的篇章是讲解继承
一、继承的概念及定义
1.1 继承概念 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段它允许程序员在保持原有类特性的基础上进行扩展增加功能这样产生新的类称派生类。 继承呈现了面向对象程序设计的层次结构体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用继承是类设计层次的复用 比如一个学生管理系统里面有不同的角色如学生、老师、管理者等等每个角色都要定义一个类
class Student
{string _name;string _tel;string _address;int _age;// ...string _stuID; // 学号
};class Teacher
{string _name;string _tel;string _address;int _age;// ...string _wordID; // 工号
};... 不难发现其存在大量冗余的代码有些信息是共有的有些信息是每个角色独有的要对这些共有的代码进行复用就要使用 “继承”下面使用继承展示一下
// 把大家共有的东西写进来
class Person
{
public:string _name;string _tel;string _address;string _age;
};//Student类 继承了 Person类
class Student : public Person
{string _stuID; // 学号
};
//Teacher类 继承了 Person类
class Teacher : public Person
{string _wordID; // 工号
}; Student 和 Teacher 类就是使用了继承继承于 Person 类下面开始进行讲解继承
1.2 继承定义 以上面的代码为例Student类和 Teacher类继承了 Person类Person类称为基类也叫父类而 Student类和 Teacher类 则称为派生类也叫子类 要让一个子类进行继承父类需要在子类的类的类名后加上冒号并跟上继承方式和父类类名即可比如上面的子类 Student class Student : public Person
冒号右边的 public 是子类进行继承的继承方式Person 则是父类的类名 初阶的时候已经学过访问限定符有以下三种 public公有访问protected保护访问private私有访问 public 修饰的成员变量可以在类外面直接访问protected 和 private 修饰的成员变量不能在类外访问但可以在类里面进行访问
继承方式也有三种 public公有继承protected保护继承private私有继承 这三个不仅能当访问限定符也能当继承方式
1.3 继承基类成员访问方式的变化 基类当中被不同访问限定符修饰的成员以不同的继承方式继承到派生类当中后该成员最终在派生类当中的访问方式将会发生变化如下图 1基类 private 成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它如果进行访问会直接报错 测试代码
//基类
class Person
{
private:string _name;
};//派生类
class Student : public Person
{
public:void Print(){//在派生类当中访问基类的private成员error!cout _name endl;}
protected:string _stuID; // 学号
}; 2因此基类的 private 成员在派生类中是不能被访问的如果基类成员不想在类外直接被访问但需要在派生类中能访问就需要定义为 protected由此可以看出protected 限定符是因继承才出现的 3实际上面的表格我们进行一下总结会发现基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 Min(成员在基类的访问限定符继承方式)public protected private 比如基类的成员变量是 protected 访问派生类进行继承时继承方式如果是 public 继承那派生类对基继承继承下来的成员变量的访问方式就是 protected因为 public protected private访问方式取小的那个minpublic protectedmin为protected 4使用关键字class时默认的继承方式是private使用struct时默认的继承方式是public不过最好显示的写出继承方式 //基类
class Person
{
private:string _name;
};//派生类
class Student : Person //class不写访问方式默认为 private但建议还是写出
{
protected:string _stuID; // 学号
}; 5在实际运用中一般使用都是 public 继承几乎很少使用 protetced/private 继承也不提倡使用protetced/private继承因为protetced/private继承下来的成员都只能在派生类的类里面使用实际中扩展维护性不强 所以常用的就两个其他基本不会用到大佬设计的时候设计过多了没有考虑实际会用到的 二、基类和派生类对象赋值转换 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用 测试代码
//基类
class Person
{
protected:string _name;string _age;string _sex;
};//派生类
class Student : public Person
{
public:string _stuID; // 学号
};void Test()
{Student s;// 1.子类对象可以赋值给父类对象/指针/引用Person p s; //派生类对象赋值给基类对象Person* pp s; //派生类对象赋值给基类指针Person rp s; //派生类对象赋值给基类引用
}这里有个形象的说法叫切片或者切割寓意把派生类中父类那部分切来赋值过去
派生类对象赋值给基类对象 派生类对象赋值给基类指针 派生类对象赋值给基类引用; 注意基类对象不能赋值给派生类对象
//基类
class Person
{
protected:string _name;string _age;string _sex;
};//派生类
class Student : public Person
{
public:string _stuID; // 学号
};void Test()
{//2.基类对象不能赋值给派生类对象Student s;Person p; s p;//error
} 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型可以使用 RTTI(RunTime Type Information)的 dynamic_cast 来进行识别后进行安全转换。这个后面再讲解这里先了解一下
测试代码基类和派生类依旧是上面的
void Test()
{//3.基类的指针可以通过强制类型转换赋值给派生类的指针Student s;Person p;Person* pp;pp s;Student * ps1 (Student*)pp; // 这种情况转换时可以的ps1-_stuID 10;pp p;Student* ps2 (Student*)pp; // 这种情况转换时虽然可以但是会存在越界访问的问ps1-_stuID 10;
}
三、继承中的作用域 在继承体系中基类和派生类都有独立的作用域。子类和父类中有同名成员子类成员将屏蔽父类对同名成员的直接访问这种情况叫隐藏也叫重定义。在子类成员函数中可以使用 基类::基类成员 显示访问
测试代码
// Student的_num和Person的_num构成隐藏关系可以看出这样代码虽然能跑但是非常容易混淆
class Person
{
protected:string _name 张三; // 姓名int _num 111111; // 身份证号
};
class Student : public Person
{
public:void Print(){cout 姓名: _name endl;cout 学号: _num endl;cout 身份证号 : _num endl;}
protected:int _num 222; // 学号
};
void Test()
{Student s1;s1.Print();
};
运行结果 如果要使用基类里面的 _num可以使用 基类::基类成员 显示访问修改代码 下面看同名函数的隐藏
测试代码
// B中的fun和A中的fun不是构成重载因为不是在同一作用域
// B中的fun和A中的fun构成隐藏成员函数满足函数名相同就构成隐藏。
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;}
};
void Test()
{B b;b.fun(10);
};
运行结果 如果 fun不传参数就会报错因为B中的 fun和A中的 fun构成隐藏无参的 fun 调不到
void Test()
{B b;b.fun();
}; 注意需要注意的是如果是成员函数的隐藏只需要函数名相同就构成隐藏所以在实际中在继承体系里面最好不要定义同名的成员
四、派生类的默认成员函数
默认成员函数即我们不写编译器会自动生成的函数类当中的默认成员函数有以下六个 最后两个基本不使用对于前四个构造和析构拷贝和赋值重载普通类 默认生成的的四个成员函数构造函数和析构函数对于内置类型不做处理自定义类型则调用对应的构造函数和析构函数 拷贝构造函数和赋值重载函数对于内置类型进行浅拷贝值拷贝对于自定义类型则调用对于的拷贝构造和赋值重载函数 对于派生类除了内置类型和自定义类型外还多了基类对象对于基类对象则调用基类对应的函数完成初始化、清理、拷贝 派生类当中的默认成员函数与普通类的默认成员函数的不同之处
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数则必须在派生类构造函数的初始化列表阶段显示调用派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化派生类的 operator 必须要调用基类的 operator 完成基类的复制派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序派生类对象初始化先调用基类构造再调派生类构造派生类对象析构清理先调用派生类析构再调基类的析构因为后续一些场景析构函数需要构成重写重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理处理成destrutor()所以父类析构函数不加virtual的情况下子类析构函数和父类析构函数构成隐藏关系
1对于派生类的构造函数
//基类
class Person
{
public://构造函数Person(const string name peter):_name(name){cout Person() endl;}
private:string _name; //姓名
};//派生类
class Student : public Person
{
public://构造函数Student(const string name, int id):Person(name) //调用基类的构造函数初始化基类的那一部分成员, _id(id) //初始化派生类的成员{cout Student() endl;}
private:int _id; //学号
};int main()
{Student s(zhangsan, 1111);return 0;
} 2对于派生类的拷贝构造函数
//基类
class Person
{
public://构造函数Person(const string name peter):_name(name){}//拷贝构造函数Person(const Person p):_name(p._name){cout Person(const Person p) endl;}
private:string _name; //姓名
};//派生类
class Student : public Person
{
public://构造函数Student(const string name, int id):Person(name) , _id(id) {}//拷贝构造函数Student(const Student s):Person(s) //调用基类的拷贝构造函数完成基类成员的拷贝构造, _id(s._id) //拷贝构造派生类的成员{cout Student(const Student s) endl;}
private:int _id; //学号
};int main()
{Student s(zhangsan, 1111);Student s2(s);return 0;
} 3对于派生类的赋值重载函数
//基类
class Person
{
public://构造函数Person(const string name peter):_name(name){}//赋值运算符重载函数Person operator(const Person p){cout Person operator(const Person p) endl;if (this ! p){_name p._name;}return *this;}
private:string _name; //姓名
};//派生类
class Student : public Person
{
public://构造函数Student(const string name, int id):Person(name), _id(id){}//赋值运算符重载函数Student operator(const Student s){cout Student operator(const Student s) endl;if (this ! s){Person::operator(s); //调用基类的operator完成基类成员的赋值_id s._id; //完成派生类成员的赋值}return *this;}
private:int _id; //学号
};int main()
{Student s1(zhangsan, 1111);Student s2(xioahong, 2222);s2 s1;return 0;
} 4对于派生类的析构函数派生类析构先子后父派生类对象的析构清理是先调用派生类析构再调基类析构。派生类析构函数完成后会自动调用基类的析构函数所以不需要我们显式调用
//基类
class Person
{
public://构造函数Person(const string name peter):_name(name){}//析构函数~Person(){cout ~Person() endl;}
private:string _name; //姓名
};//派生类
class Student : public Person
{
public://构造函数Student(const string name, int id):Person(name), _id(id){}//析构函数~Student(){cout ~Student() endl;//派生类的析构函数会在被调用完成后自动调用基类的析构函数}
private:int _id; //学号
};int main()
{Student s1(zhangsan, 1111);return 0;
} 五、继承与友元
友元关系不能继承也就是说基类的友元可以访问基类的私有和保护成员但是不能访问派生类的私有和保护成员
class Student;
class Person
{
public://声明Display是Person的友元friend void Display(const Person p, const Student s);
protected:string _name; //姓名
};
class Student : public Person
{
protected:int _id; //学号
};
void Display(const Person p, const Student s)
{cout p._name endl; //可以访问cout s._id endl; //无法访问
}
int main()
{Person p;Student s;Display(p, s);return 0;
} 六、继承与静态成员 基类定义了static静态成员则整个继承体系里面只有一个这样的成员。无论派生出多少个子类都只有一个static成员实例 测试代码
//基类
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;//4Student::_count 0;cout 人数 : Person::_count endl;//0
}int main()
{TestPerson();return 0;
} 七、菱形继承及菱形虚拟继承
7.1 继承的分类 单继承一个子类只有一个直接父类时称这个继承关系为单继承 多继承一个子类有两个或以上直接父类时称这个继承关系为多继承 菱形继承菱形继承是多继承的一种特殊情况 多继承本身没有问题但多继承形成的菱形继承就有问题从上面的菱形继承的模型构造就可以看出菱形继承的继承方式存在数据冗余和二义性的问题
例如对于上面菱形继承的模型对于菱形继承 Assistant类实例化出一个对象后访问成员时就会出现二义性问题
//基类
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; // 主修课程
};
void Test()
{// 这样会有二义性无法明确知道访问的是哪一个Assistant a;a._name peter;
} Assistant 对象是多继承的 Student 和 Teacher而 Student 和 Teacher 当中都继承了 Person因此 Student 和 Teacher 当中都有 _name 成员若是直接访问 Assistant 对象的 _name 成员会出现访问不明确的报错
对于此可以显示指定访问 Assistant 哪个父类的 _name 成员二义性解决了但是数据冗余无法解决
void Test()
{Assistant a;// 需要显示指定访问哪个父类的成员可以解决二义性问题但是数据冗余问题无法解决a.Student::_name xxx;a.Teacher::_name yyy;
}
菱形继承的问题从下面的对象成员模型构造可以看出菱形继承有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份 7.2 菱形虚拟继承 为了解决菱形继承的问题出现了菱形虚拟继承。虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系在 Student 和 Teacher 的继承 Person 时使用虚拟继承即可解决问题。需要注意的是虚拟继承不要在其他地方去使用。
菱形虚拟继承在继承方式前加 virtual 即可注意加 virtual 的位置 测试代码
//基类
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; // 主修课程
};
void Test()
{Assistant a;a._name peter;//编译通过
}
7.3 菱形虚拟继承原理
在此之前我们先看虚拟继承有A、B、C、D四个类B、C继承AD继承B、C也就是菱形继承 测试的代码
//基类
class A
{
public:int _a;
};
//派生类
class B : public A
{
public:int _b;
};
//派生类
class C : 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;
} 进行调试通过内存窗口查看注意这里不要通过监视窗口查看监视窗口被编译器优化过了不好看 查看如下 从内存窗口可以看出 d对象的分布情况 D类对象 d当中各个成员在内存当中的分布情况如下: 通过这里就可以看出为什么菱形继承导致了数据冗余和二义性根本原因就是 D类对象当中含有两个 _a 成员
下面看菱形虚拟继承 测试代码
//基类
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;d.B::_a 1;d.C::_a 2;d._b 3;d._c 4;d._d 5;return 0;
} 调试内存窗口查看 其中D类对象当中的 _a 成员被放到了最后而在原来存放两个 _a 成员的位置变成了两个指针这两个指针叫虚基表指针它们分别指向一个虚基表。虚基表中存的偏移量通过偏移量可以找到下面的A 虚基表中包含两个数据第一个数据全为0的是为多态的虚表预留的存偏移量的位置暂时不理会第二个数据就是当前类对象位置距离公共虚基类的偏移量由于VS是小端地址需要成对的倒着读比如 14 00 00 00 读的话就是0x00 00 00 14十进制就是20 也就是说这两个指针经过一系列的计算最终都可以找到成员 _a 八、继承总结 很多人说C语法复杂其实多继承就是一个体现。有了多继承就存在菱形继承有了菱形继承就有菱形虚拟继承底层实现就很复杂。所以一般不建议设计出多继承一定不要设计出菱形继承否则在复杂度及性能上都有问题。 多继承可以认为是C的缺陷之一很多后来的OO语言都没有多继承如Java 继承和组合 public 继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象组合是一种 has-a 的关系。假设B组合了A每个B对象中都有一个A对象优先使用对象组合而不是类继承
例如车类和宝马类就是 is-a 的关系它们之间适合使用继承
class Car
{
protected:string _colour; //颜色string _num; //车牌号
};
class BMW : public Car
{
public:void Drive(){cout this is BMW endl;}
};而车和轮胎之间就是 has-a 的关系它们之间则适合使用组合
class Tire
{
protected:string _brand; //品牌size_t _size; //尺寸
};
class Car
{
protected:string _colour; //颜色string _num; //车牌号Tire _t; //轮胎
};若是两个类之间既可以看作is-a的关系又可以看作has-a的关系则优先使用组合 原因
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言在继承方式中基类的内部细节对子类可见 。继承一定程度破坏了基类的封装基类的改变对派生类有很大的影响。派生类和基类间的依赖关系很强耦合度高。对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse)因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系耦合度低。优先使用对象组合有助于你保持每个类被封装。实际尽量多去用组合。组合的耦合度低代码维护性好。不过继承也有用武之地的有些关系就适合继承那就用继承另外要实现多态也必须要继承。类之间的关系可以用继承可以用组合就用组合
笔试面试题 上面已有解释不再解释
----------------我是分割线---------------
文章到这里就结束了下一篇即将更新