目录
1.继承的概念及定义
1.1继承的概念
1.2.1继承格式
1.2.2继承关系和访问限定符
1.2.3继承基类成员访问方式的变化
2.基类和派生类对象赋值转换
3.继承中的作用域
同名成员
同名函数
4.派生类的默认成员函数
5.继承与友元
6.继承与静态成员
7.复杂的菱形继承及菱形虚拟继承
虚拟继承解决数据冗余和二义性的原理:
8.继承的总结和反思
9.笔试面试题
1.继承的概念及定义
1.1继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保 持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。
class Person
{
public:
void Print()
{
cout << " name: " << _name << endl;
cout << " age: " << _age << endl;
}
protected:
string _name = "张三"; ///姓名
int _age = 19;//年龄
};
class Student :public Person
{
protected:
int _stuid;//学号
};
class Teacher :public Person
{
protected:
int _jobid;//工号
};
继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了 Student和Teacher复用了Person的成员。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
1.2.1继承格式
继承于基类的代码在子类中的是默认就有的。
1.2.2继承关系和访问限定符
1.2.3继承基类成员访问方式的变化
继承方式包括public(公有的)、private(私有的)和protected(受保护的),如果未显示写继承方式时使用class关键字的默认继承方式为private,使用struct时默认的继承方式是public。
class默认继承方式:private
class person
{
public:
string _name = "张三";
};
class Student:public person
{
public:
void print()
{
cout << "name: " << _name << endl;
}
public:
int _stuid=1001;
};
struct默认继承方式:public
#include<iostream>
#include<string.h>
using namespace std;
class person
{
public:
string _name = "张三";
};
struct Student: person
{
public:
void print()
{
cout << "name: " << _name << endl;
}
public:
int _stuid=1001;
};
int main()
{
Student s;
cout << s._name << endl;
return 0;
}
继承方式
(1)public继承:
.基类中所有public成员在派生类中访问权限不变,也为public。
.基类中所有protected成员在派生类中访问权限不变,也为protected。
.基类中所有private成员在派过类中不能使用。
(2)protected继承:
.基类中所有public成员在派生类中访问权限变为protected。
.基类中所有protected成员在派生类中访问权限不变,也为protected。
.基类中所有private成员在派过类中不能使用。
(3)private继承
.基类中所有public成员在派生类中访问权限变为private属性。
.基类中所有protected成员在派生类中访问权限变为private属性。
.基类中所有private成员在派过类中不能使用。
总结:
1.基类中的private成员不论派生类以什么样的方式继承在派生类中都是不能使用的,这里的不能使用是指基类的private成员,并没有说private成员不能被继承,实际上基类的,private也是被继承到派生类中的,并且会占用派生类事对象的内存,只是在语法上限制派生类对象不管是在类内还是类外都不能去访问它。
2. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他 成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
3. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡 使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里 面使用,实际中扩展维护性不强。
2.基类和派生类对象赋值转换
基类与派生类对象之间的赋值兼容关系,具体表现在哪些方面
(1)派生类对象可以向基类对象赋值;
(2)派生类对象可以代替基类对象向基类对象的引用进行赋值或者初始化。
(3)如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象。
(4)指向基类对象的指针也可以指向派生类对象。
//基类
class Person
{
protected:
string _name ;
string _sex;
int _age;
};
//派生类
class Student:public Person
{
public:
int _stuid;
};
Student s;
Person p = s;//派生类对象可以向基类对象赋值。
Person* ptr = &s;//指向基类对象的指针也可以指向派生类对象。
Person& ref = s;//派生类对象可以代替基类对象向基类对象的引用进行赋值。
Person p1(s);//)派生类对象可以代替基类对象向基类对象的初始化。
注意:
派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
派生类对象可以赋值给基类的指针
派生类对象可以赋值给基类的引用
基类对象不能直接赋值给派生类对象。
3.继承中的作用域
当类定义时也就有了自己的作用域,在这个作用域中定义类的成员。当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。
1. 在继承体系中基类和派生类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员。
同名成员
#include<iostream>
#include<string>
using namespace std;
//基类
class person
{
protected:
int _test=6666;
};
//派生类
class Student:public person
{
public:
void fun()
{
cout << _test << endl;
}
protected:
int _test=9999;
};
int main()
{
Student s;
s.fun();//9999
return 0;
}
以上程序中,我们创建了一个Student类对象s,我们调用Strdent作用域里的fun函数,所以编译器会自动优先在Strdent类里去找_test成员,如果Student类里没有_test成员,再去Student的父类person里去找。在以上程序里person类和Student都有_test成员,Student的_test成员将屏蔽person对同名成员的直接访问所以以上程序输出的是Student的_test的值9999。
可以使用 基类::基类成员 显示访问。如:
cout <<person:: _test << endl;
同名函数
#include<iostream>
#include<string>
using namespace std;
//基类
class person
{
public:
void fun(int i)
{
cout << i<< endl;
}
protected:
int _test=6666;
};
//派生类
class Student:public person
{
public:
void fun()
{
cout <<_test<< endl;
}
protected:
int _test=9999;
};
int main()
{
Student s;
s.fun(10);
return 0;
}
在这个程序中基类和派生类都定义了名字为fun的函数,但不同的是基类的fun函数有一个int的形参,我们创建了一个Student一岁类对象s,我们的本意是通过s对象调用fun函数并传入一个实参10,调用基类的fun函数,可是7编译器告诉我们说参数过多,也就是说从基类继承的 fun(int i)被隐藏了。只需要函数名相同就构成隐藏。
可以通过显示访问基类的同名函数
s.person::fun(10);
4.派生类的默认成员函数
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类 中,这几个成员函数是如何生成的呢?
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认 的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能 保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
class Person
{
public:
//构造函数
Person(const string& name="张三")
:_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;//姓名
};
//派生类
class Student:public Person
{
public:
//构造函数
Student(const Student& name,int stuid)
:Person(name)//调用基类的构造函数初始化基类的那一部分成员
,_stuid(stuid)//初始化派生类成员
{
cout << "Student(const Student & name, int stuid)" << endl;
}
//拷贝构造函数
Student(const Student&s)
:Person(s)//调用基类的拷贝构造完成基类的拷贝初始化
,_stuid(s._stuid )//将要拷贝的值初始化派生类成员
{
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=完成基类的复制
_stuid = s._stuid;//派生类成员的赋值
}
return *this;
}
//析构函数
~Student()
{
cout << "~Student()" << endl;
//派生类对象析构清理先调用派生类析构再调基类的析构。
}
private:
int _stuid;//学号
};
注意:
派生类和基类的赋值运算符重载函数因为函数名相同构成隐藏,因此在派生类当中调用基类的赋值运算符重载函数时,需要使用作用域限定符进行指定调用。
由于多态的某些原因,任何类的析构函数名都会被统一处理为destructor();因此,派生类和基类的析构函数也会因为函数名相同构成隐藏,若是我们需要在某处调用基类的析构函数,那么就要使用作用域限定符进行指定调用。
在派生类的拷贝构造函数和operator=当中调用基类的拷贝构造函数和operator=的传参方式是一个切片行为,都是将派生类对象直接赋值给基类的引用。
说明一下:
基类的构造函数、拷贝构造函数、赋值运算符重载函数我们都可以在派生类当中自行进行调用,而基类的析构函数是当派生类的析构函数被调用后由编译器自动调用的,我们若是自行调用基类的构造函数就会导致基类被析构多次的问题。
我们知道,创建派生类对象时是先创建的基类成员再创建的派生类成员,编译器为了保证析构时先析构派生类成员再析构基类成员的顺序析构,所以编译器会在派生类的析构函数被调用后自动调用基类的析构函数。
5.继承与友元
友元关系不能被继承,也就是说基类友元只能访问基类的私有和保护成员,不能访问派生类私有和保护成员。
例如,在以下程序中声明show( )是基类Person的友元函数,Student类继承基类,我们调用show()时,无法访问派生类Stusent的私有成员和保护成员。所以可知友元函数是不能被继承的。
#include <iostream>
#include <string>
class Student;
class Person
{
public:
//声明该函数是基类的友元函数
friend void show(const Person& p,const Student &s);
protected:
string _name;
};
class Student :public Person
{
protected:
int _stuid ;
};
void show(const Person& p, const Student& s)
{
cout <<"p._name" <<p._name<< endl;//可以在类外访问基类的protected、private成员
cout << "s._stuid" << s._stuid << endl;//不可以在类外访问派生类的protected、private成员
}
int main()
{
Person p;
Student s;
show(p, s);
return 0;
}
6.继承与静态成员
若基类中定义了一个static静态成员变量,则在整个继承体系中,无论派生出多少个子类,都只有一个static成员实例。例如以下程序我们在基类Person 定义一个静态,成员变量取基类的静态成员和不同派生类的静态成员变量的地址发现地址编号相同。
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()
{
cout <<&(Person::_count)<<endl;
cout << &(Student::_count) << endl;
cout << &(Graduate::_count) << endl;
return 0;
}
还可以通过在基类的构造函数、拷贝构造函数、赋值重载函数将静态成员变量进行自增,来统计整个继承体系中创建出多少个对象。
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;
Person p1;
cout << " 人数 :" << Person::_count << endl;
Student::_count = 0;
cout << " 人数 :" << Person::_count << endl;
Person::_count = 100;
cout << " 人数 :" << Person::_count << endl;
}
int main()
{
TestPerson();
return 0;
}
7.复杂的菱形继承及菱形虚拟继承
单继承:派生类只有一个基类。在单继承中,一个基类可以生成多个派生类,但是每个派生类只有一个基类。
例如:
class Person
{
};
class Student :public Person
{
};
class PostGraduate :public Person
{
};
多继承(Multiple inheritance),即一个子类可以有多个父类,它继承了多个父类的特性。多继承可以看作是单继承的扩展。所谓多继承是指派生类具有多个基类,派生类与每个基类之间的关系仍可看作是一个单继承。
例如:
class Teacher
{
};
class Student
{
};
class PostGraduate :public Teacher, public Student
{
};
菱形继承:菱形继承是多继承的一种特殊情况。
class Person
{
};
class Teacher:public Person
{
};
class Student :public Person
{
};
class PostGraduate :public Teacher, public Student
{
};
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。 在Assistant的对象中Person成员会有两份。
class Person
{
public:
string _name;//姓名
};
class Student :public Person
{
protected:
int _stuid;
};
class Teacher :public Person
{
protected:
int _teaid;//工号
};
class Assistant :public Student, public Teacher
{
protected:
string _majorcoures;//课程名称
};
int main()
{
Assistant s;
s._name;
return 0;
}
这样会有二义性无法明确知道访问的是哪一个。
需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决。
s.Student::_name = "aaaaaaa";
s.Teacher::_name="bbbbbbb";
cout << s.Student::_name << endl;
cout << s.Teacher::_name << endl;
通过以上的测试用例我们可以更清晰的认识菱形继承带来的数据冗余和二义性问题,为了解这一间题,在之后C++又引入了虚拟继承。
虚拟菱形继承代码:
class Person
{
public:
string _name;//姓名
};
class Student :virtual public Person
{
protected:
int _stuid;
};
class Teacher :virtual public Person
{
protected:
int _teaid;//工号
};
class Assistant :public Student, public Teacher
{
protected:
string _majorcoures;//课程名称
};
int main()
{
Assistant s;
s._name="peter";
cout << s._name << endl;//无二义性
cout << s.Student::_name << endl;//peter
cout << s.Teacher::_name << endl;//peter
return 0;
}
虚拟菱形继承不但解决了二义性,同时也解决了数据冗余的问题。
虚拟继承解决数据冗余和二义性的原理:
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和 Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
虚拟继承解决数据冗余和二义性的原理为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。
我们先来看不使用虚拟继承的菱形继承的对象模型的内存布局。
菱形继承逻辑图:
菱形继承代码:
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类对象中含有B类对象和C类对象,而B类对象和C类对象内都各有一个A类对象。也就是说B和C都继承了A,D继承了B、C从而使对象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;
}
虚拟菱形继承的对象内存布局:
通过内存布局我们可以知道_a成员被放在了D类对象的结尾(通过内存对齐规则),而原来(菱形继承)里B类和c类中的_a成员的位置在虚拟菱形继承后变成了两个指针,这两个指针叫虚基表指针,指向同一虚基表。虚基表里包含的份数据(目前不关心),第二个数据就是当前类对象成员位置距离公共虚基类的偏移量。
虚拟菱形继承的对象模型:
对象赋值转换,将D类对象赋值给B类对象,在这个切片过程中,就需要通过虚基表中的第二个数据找到公共虚基类A的成员,得到切片后该B类对象在内存中仍然保持这种分布情况。
内存布局:
8.继承的总结和反思
1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱 形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设 计出菱形继承。否则在复杂度及性能上都有问题。
2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
3. 继承和组合 public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
优先使用对象组合,而不是类继承 。
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称 为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的 内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很 大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象 来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复 用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有 些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用 继承,可以用组合,就用组合。
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
};
class BMW : public Car{
public:
void Drive() {cout << "好开-操控" << endl;}
};
class Benz : public Car{
public:
void Drive() {cout << "好坐-舒适" << endl;}
};
// Tire和Car构成has-a的关系
class Tire{
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "旧ABIT00"; // 车牌号
Tire _t; // 轮胎
};
9.笔试面试题
1. 什么是菱形继承?菱形继承的问题是什么?
菱形继承是指在继承关系中,子类继承了两个不同父类,而这两个父类又继承自同一个父类。这样会形成一个类似菱形的继承图,因此被称为菱形继承。
菱形继承的问题是会导致代码冗余、资源浪费和程序性能降低。具体来说,当子类继承了包含相同成员的两个父类时,就会出现成员变量和成员方法的重复定义,导致代码冗余和资源浪费。此外,由于存在多重继承的情况,调用某个被继承方法时就不确定到底要调用哪一个父类的方法,这就会降低程序的性能。
2.什么是菱形虚拟继承?如何解决数据冗余和二义性的?
菱形虚拟继承是指在一个多层次的继承关系中,某个基类被虚拟继承,使得它的派生类在继承时只保留一份该基类的成员。这种继承方式被称为“菱形继承”是因为在继承关系图中,多个派生类通过虚拟继承连接到同一个基类的方式构成了一个菱形。
使用菱形虚拟继承可以解决数据冗余和二义性的问题。数据冗余指的是在多个派生类中出现了重复的基类成员,如果不使用虚拟继承,每个派生类都会包含一份该基类成员的副本,导致数据冗余。使用虚拟继承后,每个派生类只保留一份该基类成员,避免了数据冗余。
二义性指的是在多层次继承关系中,某个派生类通过两条不同路径继承了同一个基类的成员,导致访问该成员时出现了二义性。使用虚拟继承可以解决二义性问题,因为虚拟继承只保留一份基类成员,避免了重复继承,从而避免了二义性。
虚拟继承解决数据冗余和二义性的奥秘就在于,它在继承之后并不会创造出两个基类成员给派生类各自继承,而是在派生类中记录两个偏移量,大小为从派生类中继承的基类成员的地址到真正的基类成员地址,而这个真正的成员,被放在最后一次继承的派生类的末尾。
通过在派生类中各添加一个指针,指向一张表,这两个指针叫做虚基表指针,这两个表叫虚基表,虚基表中存的偏移量,通过偏移量可以找到下面的A。
3. 继承和组合的区别?什么时候用继承?什么时候用组合?
继承和组合都是面向对象编程中常见的代码复用技术。
继承是一种基于类的代码复用方式,它允许派生类继承基类的属性和方法,并在基础上添加新的属性和方法。继承使得代码的重用性更高,也可以更轻松地实现多态。但是,继承也可能导致代码的复杂性增加,容易引起耦合问题,使得修改基类的实现会影响到所有派生类。
组合是一种对象之间的关系,它允许一个对象包含其他对象,通过组合来实现代码复用。组合可以避免继承带来的耦合问题,增强对象之间的独立性和灵活性,也有助于实现更好的代码复用和模块化。但是,组合也可能导致类之间的关系变得更加复杂,需要付出更多的设计和实现的成本。
继承和组合各有优缺点,应根据具体情况来选择使用哪种方式。通常,如果两个类之间存在“is-a”的关系,也就是派生类是基类的一种特化形式,就可以考虑使用继承。如果两个类之间存在“has-a”的关系,也就是一个类需要其他类的对象来完成某些功能,就可以使用组合。