淘宝做图片的网站,手机网站北京,什么网站加盟代理做的专业,网站换公司吗目录
继承的概念及定义 继承的概念 继承的定义 定义格式 继承方式和访问限定符 继承基类成员访问方式的变化 默认继承方式
基类和派生类对象赋值转换
继承中的作用域
派生类的默认成员函数
继承与友元
继承与静态成员
继承的方式
菱形虚拟继承
菱形虚拟继承原理
继承…目录
继承的概念及定义 继承的概念 继承的定义 定义格式 继承方式和访问限定符 继承基类成员访问方式的变化 默认继承方式
基类和派生类对象赋值转换
继承中的作用域
派生类的默认成员函数
继承与友元
继承与静态成员
继承的方式
菱形虚拟继承
菱形虚拟继承原理
继承的总结和反思
相关笔试面试题 继承的概念及定义 继承的概念 C中的继承是面向对象编程的一个核心特性它允许创建一个新类派生类或子类基于已存在的类基类或父类的结构和行为。 继承的主要目的是实现代码的复用和促进软件的模块化设计。
代码举例 以下代码中Student类和Teacher类继承了Person类。
//父类
class Person
{
public:void Print(){cout name: _name endl;cout age: _age endl;}
protected:string _name 张三; //姓名int _age 18; //年龄
};//子类
class Student : public Person
{
protected:int _stuid; //学号
};//子类
class Teacher : public Person
{
protected:int _jobid; //工号
};继承后父类Person的成员包括成员函数和成员变量都会变成子类的一部分也就是说子类Student和Teacher复用了父类Person的成员。 继承的定义 定义格式 class 派生类名 : 访问修饰符 基类名
{// 派生类的成员定义
}; 其中 派生类名是要定义的新类的名称。访问修饰符是public、protected或private用于指定基类成员在派生类中的访问权限。基类名是已经被定义的、要从中继承的类的名称。 继承方式和访问限定符 我们知道访问限定符有以下三种 public访问protected访问private访问 而继承的方式也有类似的三种 public继承protected继承private继承 继承基类成员访问方式的变化 基类当中被不同访问限定符修饰的成员以不同的继承方式继承到派生类当中后该成员最终在派生类当中的访问方式将会发生变化。
类成员/继承方式public继承protected继承private继承基类的public成员派生类的public成员派生类的protected成员派生类的private成员基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见 实际上基类成员访问方式的变化规则也不是无迹可寻的我们可以认为三种访问限定符的权限大小为public protected private基类成员访问方式的变化规则如下 公有继承public inheritance: 基类的公有成员在派生类中保持公有。基类的保护成员在派生类中变为保护成员。基类的私有成员在派生类中不可直接访问但会影响派生类的接口比如通过基类的公有或保护成员函数间接访问。 保护继承protected inheritance: 基类的公有和保护成员在派生类中都变为保护成员。基类的私有成员在派生类中同样不可直接访问但可能影响派生类的内部实现。 私有继承private inheritance: 不论基类成员的原始访问权限如何它们在派生类中都变为私有成员。这意味着基类的公有和保护成员在派生类的外部都是不可见的且派生类的子类也不能访问这些成员。 虽然Student类继承了Person类但是我们无法在Student类当中访问Person类当中的private成员_name。
代码举例
#include string
#include iostreamusing namespace std;//基类
class Person
{
private:string _name 张三; //姓名
};//派生类
class Student : public Person
{
public:void Print(){//在派生类当中访问基类的private成员error!cout _name endl;}
protected:int _stuid; //学号
};代码结果 基类的private成员在派生类中是不可直接访问的无论是从派生类的内部还是外部。这些成员虽然被继承影响了派生类的内存布局和构造过程但直接的访问途径被编译器禁止了这是为了维护封装性。如果希望基类的某些成员能够在派生类中访问但不暴露给更广泛的外界应该将这些成员声明为protected。这确实是protected访问限定符的重要应用场景之一它就是为了在继承体系内部提供访问权限同时阻止外部直接访问。 public继承是最常见的形式它体现了“is-a”关系即派生类是基类的一种。这种继承方式最符合面向对象设计的原则支持多态、接口复用等特性便于理解和维护代码。 protected和private继承相对较少使用它们更多地用于实现细节的封装或是为了重用基类的实现而不暴露其接口。特别是private继承它经常被用来实现“has-a”关系而非直接的类型层次关系。虽然这些继承方式在特定场景下有其用途但确实不如public继承常见且可能使代码结构变得复杂影响代码的可读性和维护性。 因此在设计时应谨慎考虑是否采用protected或private继承。 默认继承方式 在使用继承的时候也可以不指定继承方式使用关键字class时默认的继承方式是private使用struct时默认的继承方式是public。
例如 在关键字为class的派生类当中所继承的基类成员_name的访问方式变为private。
//基类
class Person
{
public:string _name 张三; //姓名
};//派生类
class Student : Person //默认为private继承
{
protected:int _stuid; //学号
};在关键字为struct的派生类当中所继承的基类成员_name的访问方式仍为public。
//基类
class Person
{
public:string _name 张三; //姓名
};//派生类
struct Student : Person //默认为public继承
{
protected:int _stuid; //学号
};注意 虽然继承时可以不指定继承方式而采用默认的继承方式但还是最好显示的写出继承方式。 基类和派生类对象赋值转换 派生类对象可以赋值给基类的对象、基类的指针以及基类的引用。 但在这个过程中会发生基类和派生类对象之间的赋值转换。
代码举例
//基类
class Person
{
protected:string _name; //姓名string _sex; //性别int _age; //年龄
};//派生类
class Student : public Person
{
protected:int _stuid; //学号
};代码当中可以出现以下逻辑
Student s;
Person p s; //派生类对象赋值给基类对象
Person* ptr s; //派生类对象赋值给基类指针
Person ref s; //派生类对象赋值给基类引用对于这种做法有个形象的说法叫做 切片/切割寓意把派生类中基类那部分切来赋值过去。 派生类对象赋值给基类对象图示 派生类对象赋值给基类指针图示 派生类对象赋值给基类引用图示 注意 基类对象不能赋值给派生类对象基类的指针可以通过强制类型转换赋值给派生类的指针但是此时基类的指针必须是指向派生类的对象才是安全的。 继承中的作用域 在继承体系中基类和派生类都有独立的作用域。子类和父类中有同名成员子类成员将屏蔽父类对同名成员的直接访问这种情况叫隐藏也叫重定义。在子类成员函数中可以使用 基类::基类成员 显示访问需要注意的是如果是成员函数的隐藏只需要函数名相同就构成隐藏。注意在实际中在继承体系里面最好不要定义同名的成员。 代码举例 对于以下代码访问成员_num时将访问到子类当中的_num。
#include iostream
#include stringusing namespace std;//父类
class Person
{
protected:int _num 111;
};//子类
class Student : public Person
{
public:void fun(){cout _num endl;}
protected:int _num 999;
};int main()
{Student s;s.fun(); //999return 0;
}代码结果 若此时我们就是要访问父类当中的_num成员我们可以使用作用域限定符进行指定访问。
void fun()
{cout Person::_num endl; //指定访问父类当中的_num成员
}如果是成员函数的隐藏只需要函数名相同就构成隐藏。 代码举例 对于以下代码调用成员函数fun时将直接调用子类当中的fun若想调用父类当中的fun则需使用作用域限定符指定类域。
#include iostream
#include stringusing namespace std;//父类
class Person
{
public:void fun(int x){cout x endl;}
};//子类
class Student : public Person
{
public:void fun(double x){cout x endl;}
};int main()
{Student s;s.fun(3.14); //直接调用子类当中的成员函数funs.Person::fun(20); //指定调用父类当中的成员函数funreturn 0;
}代码结果 代码当中父类中的fun和子类中的fun不是构成函数重载因为函数重载要求两个函数在同一作用域而此时这两个fun函数并不在同一作用域。 注意为了避免类似问题实际在继承体系当中最好不要定义同名的成员。 派生类的默认成员函数 当我们不写编译器会自动生成的函数类当中的默认成员函数有以下六个 下面我们看看派生类当中的默认成员函数与普通类的默认成员函数的不同之处。
代码举例 以下面这个Person类为基类。
//基类
class Person
{
public://构造函数Person(const string name peter):_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;}
private:string _name; //姓名
};用该基类派生出Student类Student类当中的默认成员函数的基本逻辑如下
//派生类
class Student : public Person
{
public://构造函数Student(const string name, int id):Person(name) //调用基类的构造函数初始化基类的那一部分成员, _id(id) //初始化派生类的成员{cout Student() endl;}//拷贝构造函数Student(const Student s):Person(s) //调用基类的拷贝构造函数完成基类成员的拷贝构造, _id(s._id) //拷贝构造派生类的成员{cout Student(const Student s) endl;}//赋值运算符重载函数Student operator(const Student s){cout Student operator(const Student s) endl;if (this ! s){Person::operator(s); //调用基类的operator完成基类成员的赋值_id s._id; //完成派生类成员的赋值}return *this;}//析构函数~Student(){cout ~Student() endl;//派生类的析构函数会在被调用完成后自动调用基类的析构函数}
private:int _id; //学号
};派生类与普通类的默认成员函数的不同之处概括为以下几点 构造函数当创建一个派生类对象时首先需要确保基类部分得到恰当的初始化。因此派生类的构造函数执行前会自动调用基类的构造函数。如果基类有无参构造函数默认构造函数编译器会自动调用它如果没有默认不会自动调用这时就必须在派生类的构造函数初始化列表中显式调用一个基类的构造函数。 拷贝构造函数当通过已有的派生类对象来创建一个新的派生类对象时派生类的拷贝构造函数不仅要复制派生类新增的成员还需要调用基类的拷贝构造函数来复制基类的成员以保证新对象的基类部分与原对象相同。 赋值运算符重载类似地在使用赋值运算符给一个派生类对象赋值时派生类的赋值运算符重载函数需确保不仅复制派生类特有的数据成员还要正确地调用基类的赋值运算符来处理基类的数据成员实现深拷贝以维护对象状态的一致性。 析构函数对象生命周期结束时派生类的析构函数执行完毕后会自动调用基类的析构函数以确保基类资源的正确释放。这个顺序是先派生类后基类与构造过程的顺序相反遵循“后构造先析构”的原则确保所有资源被妥善清理。 初始化顺序这一点是对构造函数调用顺序的强调明确指出在派生类对象的构造过程中首先执行的是基类的构造函数随后才是派生类自身的构造函数确保从基到派的构建逻辑。 析构顺序与初始化顺序相反当对象生命周期结束进行清理时派生类的析构函数先执行完成派生类特有资源的清理之后基类的析构函数被调用释放基类资源这是确保资源释放的正确顺序。 在编写派生类的默认成员函数如构造函数、拷贝构造函数、赋值运算符、析构函数等时需要注意以下几个关键点 构造函数 如果基类有非默认构造函数且你希望在派生类构造时也调用它你需要在派生类的构造函数初始化列表中显式调用基类的构造函数。确保初始化派生类新增的成员变量。这通常也在初始化列表中完成。 拷贝构造函数和赋值运算符 如果基类定义了自己的拷贝构造函数或赋值运算符派生类需要显式调用它们以确保基类部分被正确拷贝或赋值。这通常通过使用BaseClass(baseObj)或BaseClass operator(const BaseClass baseObj)的方式实现。实现派生类自己的拷贝控制成员时要遵循“深拷贝”的原则避免资源共享引起的问题。 析构函数 派生类的析构函数不需要显式调用基类的析构函数C会自动保证在派生类对象销毁时先执行派生类的析构函数再执行基类的析构函数。但需确保在派生类析构函数中释放派生类特有的资源。 虚函数 如果基类中有虚函数确保在派生类中正确地重写这些虚函数并考虑其行为是否符合预期。特别是当基类指针或引用指向派生类对象时通过基类接口调用的应是派生类的实现。 访问权限 注意基类成员的访问权限。派生类构造函数不能直接访问基类的私有成员即使在初始化列表中也是如此。如果需要在派生类构造函数中操作基类私有成员应通过基类提供的公有或受保护的接口进行。 继承方式 明确指定继承方式public, protected, private。默认为private可能不是你想要的。公有继承public inheritance通常表示“is-a”关系确保派生类可以作为基类的替代使用。 构造顺序与析构顺序 构造时先调用基类的构造函数再调用派生类的构造函数。析构时顺序相反先调用派生类的析构函数再调用基类的析构函数。 继承与友元 友元关系不能继承也就是说基类的友元可以访问基类的私有和保护成员但是不能访问派生类的私有和保护成员。
代码举例 以下代码中Display函数是基类Person的友元当时Display函数不是派生类Student的友元即Display函数无法访问派生类Student当中的私有和保护成员。
#include iostream
#include stringusing namespace std;// 前向声明为了让Display函数能够识别Student类型这里先声明Student类
class Student;// Person类定义
class Person
{
public:// 声明Display为Person类的友元函数这意味着Display可以访问Person的所有成员包括私有和保护成员friend void Display(const Person p, const Student s);protected:string _name; // 姓名声明为保护成员允许派生类访问
};// Student类从Person公有继承
class Student : public Person
{
protected:int _id; // 学号声明为保护成员
};// Display函数实现它是Person和Student类的友元因此可以访问这两个类的保护和公有成员
void Display(const Person p, const Student s)
{cout p._name endl; // 可以访问因为Person类将Display设为友元_name是Person的保护成员cout s._id endl; // 这行代码如果取消注释会编译错误因为虽然Display是Person的友元但并非Student的直接友元// // 因此不能访问Student的保护成员_id。即使Student继承自Person也不行。
}int main()
{Person p; // 创建一个Person对象Student s; // 创建一个Student对象Display(p, s); // 调用Display函数传入p和s的对象return 0;
}
代码结果 若想让Display函数也能够访问派生类Student的私有和保护成员只能在派生类Student当中进行友元声明。
class Student : public Person
{
public://声明Display是Student的友元friend void Display(const Person p, const Student s);
protected:int _id; //学号
};继承与静态成员 若基类当中定义了一个static静态成员变量则在整个继承体系里面只有一个该静态成员。无论派生出多少个子类都只有一个static成员实例。
代码举例 在基类Person当中定义了静态成员变量_count尽管Person又继承了派生类Student和Graduate但在整个继承体系里面只有一个该静态成员。 我们若是在基类Person的构造函数和拷贝构造函数当中设置_count进行自增那么我们就可以随时通过_count来获取该时刻已经实例化的Person、Student以及Graduate对象的总个数。
#include iostream
#include stringusing namespace std;// 基类 Person
class Person
{
public:// 默认构造函数每当创建一个Person对象时计数器_count加一Person() { _count; }// 拷贝构造函数同样增加计数器_count用于追踪所有Person及其派生类实例的数量Person(const Person p) {_count;}
protected:string _name; // 姓名保护成员允许派生类访问
public:static int _count; // 静态成员变量用于统计Person类及派生类实例的总数
};// 在类外部对静态成员变量_count进行初始化
int Person::_count 0;// 派生类 Student 继承自 Person
class Student : public Person
{
protected:int _stuNum; // 学号保护成员
};// 派生类 Graduate 继承自 Person
class Graduate : public Person
{
protected:string _seminarCourse; // 研究科目保护成员
};int main()
{// 创建Student对象s1由于Person类的构造函数_count加1变为1Student s1;// 使用s1对象拷贝构造s2调用Person类的拷贝构造函数_count再次加1变为2Student s2(s1);// 创建Student对象s3_count加1变为3Student s3;// 创建Graduate对象s4由于Graduate也是Person的派生类_count继续加1变为4Graduate s4;// 输出Person类及其所有派生类的实例总数即_count的值结果为4cout Person::_count endl; // 4// 注意这里尝试输出Student类的_count是不正确的因为静态成员是属于类的而不是特定的派生类。// 所有的Person、Student和Graduate实例共享同一个_count所以直接写Student::_count是多余的// 应该只通过基类Person来访问静态成员_count。不过由于静态成员的特性这里实际上也会输出4// 但这不代表正确的编程习惯应该避免。cout Student::_count endl; // 4return 0;
} 此时我们也可以通过打印Person类和Student类当中静态成员_count的地址来证明它们就是同一个变量。
cout Person::_count endl;
cout Student::_count endl; 代码结果
继承的方式 单继承 (Single Inheritance) 定义单继承是最简单的继承形式指一个子类派生类只从一个基类继承。这种继承结构清晰易于理解。特点简化了对象模型减少了继承带来的复杂性如内存布局和方法解析顺序相对简单。 多继承 (Multiple Inheritance) 定义多继承允许一个子类继承自两个或更多个基类。这使得子类能够合并多个基类的特性。特点 提供了更灵活的代码复用但也引入了潜在的复杂性比如可能面临的“菱形问题”Diamond Problem。需要注意的是多继承可能导致对象大小增加因为子类需要包含所有基类的实例变量。 菱形继承 (Diamond Inheritance) 定义菱形继承是多继承的一种特殊情况当一个类直接继承自两个类而这两个类又共同继承自同一个基类时就形成了菱形继承结构。问题最直接的问题是如果有同名成员尤其是数据成员在基类中定义那么在派生类中访问时会出现二义性。解决方案C通过引入虚继承Virtual Inheritance来解决菱形继承中的二义性问题。虚继承确保基类只被继承一次从而在派生类中只有一个基类的副本。 从菱形继承的模型构造就可以看出菱形继承的继承方式存在数据冗余和二义性的问题。
代码举例 对于以上菱形继承的模型当我们实例化出一个Assistant对象后访问成员时就会出现二义性问题。
#include iostream
#include stringusing namespace std;// 定义基类Person包含姓名成员变量
class Person
{
public:string _name; // 姓名
};// 定义派生类Student从Person公有继承添加学号成员变量
class Student : public Person
{
protected:int _num; // 学号
};// 定义派生类Teacher从Person公有继承添加职工编号成员变量
class Teacher : public Person
{
protected:int _id; // 职工编号
};// 定义派生类Assistant同时从Student和Teacher公有继承形成菱形继承结构添加主修课程成员变量
// 注意这里没有使用虚继承因此Person的内容被间接继承了两次
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};int main()
{Assistant a;// 尝试给Assistant对象a的_name赋值但由于_person是通过两条路径Student和Teacher被继承到Assistant中// 编译器无法确定应访问哪个基类版本的_name成员导致二义性错误。// 解决方法是在Person类前加上virtual关键字使Student和Teacher虚继承Person消除二义性。a._name peter; // 此处会导致编译错误指出_name是二义性的return 0;
} 代码结果 Assistant对象是多继承的Student和Teacher而Student和Teacher当中都继承了Person因此Student和Teacher当中都有_name成员若是直接访问Assistant对象的_name成员会出现访问不明确的报错。 对于此我们可以显示指定访问Assistant哪个父类的_name成员。
//显示指定访问哪个父类的成员
a.Student::_name 张同学;
a.Teacher::_name 张老师;注意虽然该方法可以解决二义性的问题但仍然不能解决数据冗余的问题。因为在Assistant的对象在Person成员始终会存在两份。 菱形虚拟继承 为了解决菱形继承的二义性和数据冗余问题出现了虚拟继承。 如前面说到的菱形继承关系在Student和Teacher继承Person是使用虚拟继承即可解决问题。 代码如下
#include iostream
#include stringusing namespace std;// 基类 Person定义姓名成员变量
class Person
{
public:string _name; // 姓名
};// 派生类 Student使用虚拟继承自 Person添加学号成员变量
class Student : virtual public Person
{
protected:int _num; // 学号
};// 派生类 Teacher使用虚拟继承自 Person添加职工编号成员变量
class Teacher : virtual public Person
{
protected:int _id; // 职工编号
};// 派生类 Assistant同时继承自 Student 和 Teacher由于 Student 和 Teacher 都是虚拟继承自 Person
// 因此 Person 的内容在 Assistant 中只有一份实例消除了二义性问题。
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};int main()
{Assistant a;// 现在可以为 a 对象的 _name 成员赋值因为通过虚拟继承消除了二义性。a._name peter; // 无二义性正确设置姓名return 0;
} 此时就可以直接访问Assistant对象的_name成员了并且之后就算我们指定访问Assistant的Student父类和Teacher父类的_name成员访问到的都是同一个结果解决了二义性的问题。
cout a.Student::_name endl;
cout a.Teacher::_name endl; 打印Assistant的Student父类和Teacher父类的_name成员的地址时显示的也是同一个地址解决了数据冗余的问题。
cout a.Student::_name endl;
cout a.Teacher::_name endl; 代码结果
菱形虚拟继承原理 若不使用菱形虚拟继承时以下菱形继承当中D类对象的各个成员在内存当中的分布情况。 代码如下
#include iostreamusing namespace std;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类对象当中含有两个_a成员。 使用菱形虚拟继承时以下菱形继承当中D类对象的各个成员在内存当中的分布情况。 代码如下
#include iostreamusing namespace std;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类对象当中各个成员在内存当中的分布情况如下 其中D类对象当中的_a成员被放到了最后而在原来存放两个_a成员的位置变成了两个指针这两个指针叫虚基表指针它们分别指向一个虚基表。 虚基表中包含两个数据第一个数据是为多态的虚表预留的存偏移量的位置这里我们不必关心第二个数据就是当前类对象位置距离公共虚基类的偏移量。 也就是说这两个指针经过一系列的计算最终都可以找到成员_a。 若是将D类对象赋值给B类对象在这个切片过程中就需要通过虚基表中的第二个数据找到公共虚基类A的成员得到切片后该B类对象在内存中仍然保持这种分布情况。
D d;
B b d; //切片行为得到切片后该B类对象当中各个成员在内存当中的分布情况如下 其中_a对象仍然存储在该B类对象的最后。
继承的总结和反思 1.很多人说C语法复杂其实多继承就是一个体现。有了多继承就存在菱形继承有了菱形继承就有菱形虚拟继承底层实现就很复杂。所以一般不建议设计出多继承一定不要设计出菱形继承。否则在复杂度及性能上都有问题。 2. 多继承可以认为是C的缺陷之一很多后来的OO语言都没有多继承如Java。 3. 继承和组合
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。组合是一种has-a的关系。假设B组合了A每个B对象中都有一个A对象。优先使用对象组合而不是类继承 。继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言在继承方式中基类的内部细节对子类可见 。继承一定程度破坏了基类的封装基类的改变对派生类有很大的影响。派生类和基类间的依赖关系很强耦合度高。对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse)因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系耦合度低。优先使用对象组合有助于你保持每个类被封装。实际尽量多去用组合。组合的耦合度低代码维护性好。不过继承也有用武之地的有些关系就适合继承那就用继承另外要实现多态也必须要继承。类之间的关系可以用继承可以用组合就用组合。 继承关系is-a class Car
{
protected:string _colour 白色; // 颜色string _num ; // 车牌号
};class BMW : public Car
{
public:void Drive() {cout 好开-操控 endl;}
};class Benz : public Car
{
public:void Drive() {cout 好坐-舒适 endl;}
}; Car类定义了一些基本属性如颜色和车牌号。BMW和Benz类通过公有继承public自Car类表示宝马车和奔驰车都是车的一种它们“是”车。这种继承关系体现了“is-a”原则宝马和奔驰车具备车的基本属性颜色、车牌号同时各自还定义了特有的Drive()方法来展示不同品牌的驾驶体验。 组合关系has-a class Tire
{
protected:string _brand Michelin; // 品牌size_t _size 17; // 尺寸
};class Car
{
protected:string _colour 白色; // 颜色string _num ; // 车牌号Tire _t; // 轮胎
}; Tire类定义了一个轮胎应有的属性如品牌和尺寸。在重新定义的Car类中通过包含一个Tire类型的成员变量_t表明一辆车“有”一个轮胎。这种包含关系体现了“has-a”原则即车并不继承轮胎的行为或状态而是直接拥有轮胎作为其组成部分。 注意若是两个类之间既可以看作is-a的关系又可以看作has-a的关系则优先使用组合。 相关笔试面试题 什么是菱形继承菱形继承的问题是什么 菱形继承是多继承的一种特殊情况两个子类继承同一个父类而又有子类同时继承这两个子类我们称这种继承为菱形继承。 菱形继承因为子类对象当中会有两份父类的成员因此会导致数据冗余和二义性的问题。 什么是菱形虚拟继承如何解决数据冗余和二义性 菱形虚拟继承是指在菱形继承的腰部使用虚拟继承(virtual)的继承方式菱形虚拟继承对于D类对象当中重复的A类成员只存储一份然后采用虚基表指针和虚基表使得D类对象当中继承的B类和C类可以找到自己继承的A类成员从而解决了数据冗余和二义性的问题。 继承和组合的区别什么时候用继承什么时候用组合 继承是一种is-a的关系而组合是一种has-a的关系。如果两个类之间是is-a的关系使用继承如果两个类之间是has-a的关系则使用组合如果两个类之间的关系既可以看作is-a的关系又可以看作has-a的关系则优先使用组合。