文章目录
一、继承的概念和定义
1、继承的概念
继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复⽤,继承是类设计层次的复⽤。
2、继承的定义
class Person
{
public:
void identity()
{
cout << "void identity()" << _name << endl;
}
protected:
string _name;
string _address;
string _tel;
int _age = 18;
};
class Student : public Person
{
public:
void study()
{
}
protected:
int _stuid;
};
class Teacher : public Person
{
public:
void teaching()
{
}
protected:
string title;
};
Person类称为父类也叫基类,Teacher和Student类称为子类也叫派生类。
3、继承基类成员访问方式的变化
类成员/继承方式 | public | protected继承 | private继承 |
---|---|---|---|
基类的publice成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类不可见 | 在派生类中不可见 | 在派生类中不可见 |
二、基类和派生类之间的转换
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
public:
int _age = 18; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
int main()
{
Student sobj;
// 赋值兼容转换,特殊处理
// 1.派生类对象可以赋值给基类的指针/引用
Person* pp = &sobj;
Person& rp = sobj;
return 0;
}
- 赋值兼容转换:就相当于把派生类直接赋值给基类中间不会产生临时对象
三、继承中的作用域
1、隐藏规则
class Person
{
protected:
string _name = "张三";//姓名
int _num = 4209999;//身份证
};
class Student : public Person
{
public:
void Print()
{
cout << "姓名:" << _name << endl;
cout << "学号:" << _num << endl;
cout << "身份证号:" << Person::_num << endl;
}
protected:
int _num = 16;//学号
};
基类和派生类成员同名,优先访问派生类中的成员,如果想访问基类成员需要指定类域显示调用
四、派生类的默认成员函数
1、常见默认成员函数
- 构造函数、析构函数、拷贝构造、赋值运算符重载、取地址运算符重载(const对象和普通对象)
class Person
{
public:
Person(const char* 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;
}
// destructor()
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student(int num = 18, const char* name = "张三", const char* address = "武汉")
:_num(num)
, _address(address)
, Person(name)//把父类当成整体初始化
{
cout << "Student()" << endl;
}
Student(const Student& s)
:Person(s)
, _num(s._num)
,_address(s._address)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator = (const Student& s)
{
if (this != &s)
{
_num = s._num;
_address = s._address;
Person::operator=(s);
cout << "Student& operator = (const Student& s)" << endl;
}
return *this;
}
~Student()
{
cout << "~Student()" << endl;
}
protected:
int _num; //学号
string _address;
};
- 初始列表中先定义的先声明,在派生类父类是最先声明的也是最先初始化的。
- 析构后定义的先析构
2、实现一个不能被继承的类
⽅法1:基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。
⽅法2:C++11新增了⼀个final关键字,final修饰的基类,派⽣类就不能继承了。
class Base final//C++ 11的方法
{
public:
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
private:
// C++98的方法
/*Base()
{}*/
};
五、继承与友元
友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员 。
六、继承与静态成员变量
基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都只有⼀个static成员实例。
七、多继承和菱形继承
1、继承模型
- 单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承。
- 多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。
- 菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。
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()
{
// 编译报错:error C2385: 对“_name”的访问不明确
Assistant a;
//a._name = "peter";
// 需要显示指定访问哪个基类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
return 0;
}
3、虚继承(关键词virtual)
很多⼈说C++语法复杂,其实多继承就是⼀个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,性能也会有⼀些损失,所以最好不要设计出菱形继承。多继承可以认为是C++的缺陷之⼀,后来的⼀些编程语⾔都没有多继承,如Java。
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()
{
// 编译报错:error C2385: 对“_name”的访问不明确
Assistant a;
a._name = "peter";
// 需要显示指定访问哪个基类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
return 0;
}
在存在二义性的继承前面加virtual
八、继承和组合
-
public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。
-
组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。
-
继承允许你根据基类的实现来定义派⽣类的实现。这种通过⽣成派⽣类的复⽤通常被称为⽩箱复⽤(white-box reuse)。术语“⽩箱”是相对可视性⽽⾔:在继承⽅式中,基类的内部细节对派⽣类可⻅。继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依赖关系很强,耦合度⾼。
-
对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-boxreuse),因为对象的内部细节是不可⻅的。对象只以“⿊箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。
-
优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合。