继承
什么是继承?
继承其实就是胆码复用的一种手段,它允许我们定义一个类来继承另一个类的属性和方法。
非继承:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
using namespace std;
class Teacher //教师类
{
public:
void print()
{
cout << _name << endl;
cout << _age << endl;
}
private:
string _name = "zhangsan"; //名字
int _age = 20; //年龄
string _num; //工号
};
class student
{
public:
void print()
{
cout << _name << endl;
cout << _age << endl;
}
private:
string _name = "zhangsan"; //名字
int _age = 20; //年龄
string _id; //学号
};
int main()
{
Teacher t;
student s;
t.print();
s.print();
return 0;
}
继承:
class Person
{
public:
void Print()
{
cout << "名字:" << _name << endl;
cout << "年龄:" << _age << endl;
}
protected:
string _name = "renqing"; // 姓名
int _age = 20; //年龄
};
class Student : public Person
{
public:
void playgame()
{
// ...
}
private:
int _id; // 学号
};
继承的概念
继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复⽤,继承是类设计层次的复⽤。
继承的定义
下⾯我们看到Person是基类,也称作⽗类。Student是派⽣类,也称作⼦类。(因为翻译的原因,所以既叫基类/派⽣类,也叫⽗类/⼦类)
继承基类成员访问⽅式的变化
基类的private成员只是不可访问,但是派生类还是得到了继承
public和protected的区别
class Base {
public:
void publicMethod() {}
protected:
void protectedMethod() {}
};
class Derived : public Base {
public:
void anotherMethod() {
publicMethod(); // 可以访问基类的public成员
protectedMethod(); // 可以访问基类的protected成员
}
};
int main() {
Derived d;
d.publicMethod(); // 正确:可以通过Derived实例访问public成员
// d.protectedMethod(); // 错误:不能从Derived实例的外部访问protected成员
}
在这个示例中,Derived类可以访问基类Base的public和protected成员,但外界(如main函数中的代码)只能访问Derived的public成员以及从基类继承来的public成员,不能访问protected成员。
总结:
1.在实际运用中一般使用都是public继承,几乎很少使用protetced / private继承,也不提倡 使用protetced / private继承,因为protetced / private继承下来的成员都只能在派生类的类里 面使用,实际中扩展维护性不强。
2. 父类的私有 (private) 成员在子类中无论以何种方式继承都是不可见的。这意味着尽管这些成员被继承到了子类对象中,但由于语法上的限制,子类对象无论是在类内部还是外部都无法直接访问这些私有成员。
3. 基类的私有成员在子类都是不可见。基类的其他 成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected>private。
4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过 最好显示的写出继承方式。
5. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在 派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
继承类模版
#include <vector>
#include <list>
#include <deque>
#define CLASSTYPE std::vector
//#define CLASSTYPE std:list
//#define CLASSTYPE std:deque
template<class T>
class stack : public CLASSTYPE<T>
{
public:
void push(const T& x)
{
// 基类是类模板时,需要指定⼀下类域,
// 否则编译报错:error C3861: “push_back”: 找不到标识符
// 因为stack<int>实例化时,也实例化vector<int>了
// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
//push_back(x);
CLASSTYPE<T>::push_back(x);
}
void pop()
{
CLASSTYPE<T>::pop_back();
}
const T& top()
{
return CLASSTYPE<T>::back();
}
bool empty() const
{
return ClASSTYPE<T>::empty();
}
};
int main()
{
stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty())
{
cout << st.top() << " ";
st.pop();
}
return 0;
}
基类与派生类的赋值转换
派生类可以赋值给基类,但是基类不能赋值给派生类
Person p;
Student s;
p = s;//ok
s = p;//error
原因有以下几点:
- 类型不匹配:
虽然基类和派生类有关系,但是它们仍然是两个不同的类型,派生类通过继承机制从基类继承了属性和方法,但是它也有可能添加了自己的属性和方法,或者重写了基类的某些部分。- 违反了继承的目的:
派生类本就是通过继承基类后再根据需求对于基类进行扩展或修改。如果把基类赋值给派生类,那么派生类的属性和方法就会被影响,那不就白继承了。- 可能导致数据丢失:
如果基类把派生类扩展或修改的属性和方法中特有的数据给覆盖了,就会导致数据丢失
派生类对象的引用赋值给基类
Student s;
Person& rp = s;
派生类对象的指针赋值给基类对象
Student s;
Person* pp = &s;
继承的作用域
隐藏(重定义):
在继承机制中基类和派生类都有独立的作用域,如果派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
同名成员变量
class Person
{
protected:
double _height = 1.75;//身高
int _age = 20;//年龄
string _name = "renqing";//姓名
};
class Student :public Person
{
public:
void Print()//隐藏
{
cout << _height << endl;
cout << _age << endl;
cout << _name << endl;
}
private:
double _height = 1.65;//身高
int _stuid = 123456;//学号
int _grade = 10;//年级
};
int main()
{
Student s;
s.Print();
return 0;
}
如果想打印基类的_height,加上作用域限定符 : :
void Print()//隐藏
{
cout << Person::_height << endl;
cout << _age << endl;
cout << _name << endl;
}
同名成员函数
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;
}
};
int main()
{
B b;
b.fun(10);
return 0;
};
在隐藏关系中,同名函数默认调用的当前作用域的函数,如果想调用其他作用域的函数,则需要使用域作用限定符。
派生类的默认成员函数
构造函数与析构函数
派生类对象在调用构造函数时会先调用基类的构造函数,再调用派生类的构造函数。调用析构函数时会先调用派生类的析构函数,再调用基类的析构函数。
class Person
{
public:
Person(const char* name = "renqing")
: _name(name)
{
cout << "Person()" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student()
{
cout << "Student()" << endl;
}
~Student()
{
cout << "~Student()" << endl;
}
protected:
int _num; //学号
};
int main()
{
Student s;
return 0;
}
拷贝构造
派生类构造函数:
派生类需要定义自己的构造函数来初始化自己的成员变量,同时它还需要通过初始化列表来调用基类的构造函数以初始化从基类继承的成员。如果派生类构造函数没有显式地调用基类构造函数,编译器会尝试调用基类的默认构造函数(如果存在的话)。
我举个基类没有默认构造函数的例子:
class Person
{
public:
Person(const char* name)//没有默认构造
: _name(name)
{
cout << "Person()" << endl;
}
~Person()//析构
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student(int num, const char* name)//构造
:_num(num)
//,_name(name) error
, Person(name)// 基类没有默认构造函数,需要显示调用基类的构造函数
{
cout << "Student()" << endl;
}
~Student()
{
//因为构成覆盖关系,所以指定域作用限定符
Person::~Person();
cout << "~Student()" << endl;
}
protected:
int _num; //学号
};
int main()
{
Student s(20, "renqing");
return 0;
}
编译器会对派生类与基类的析构函数名进行特殊处理,都会被处理成destrutor(),所以派生类与基类的析构函数构成隐藏关系。
但是为什么Person的析构函数会多调用一次呢?因为编译器为了保证基类的析构最后调用,所以在调用派生类析构函数之后会自动调用基类的构造函数。所以为了保证调用的正确顺序,派生类的析构函数我们不需要显示定义。
赋值重载
赋值运算符 operator=:
子类的 operator= 需要显式调用父类的 operator=
来完成父类成员的复制,并需要指定父类的作用域。
派生类赋值重载调用基类赋值重载时一定要加域作用限定符,不然就会发生死循环。
//拷贝构造
Person(const Person& p)
: _name(p._name)
{
}
//赋值重载
Person& operator=(const Person& p)
{
if (this != &p)
_name = p._name;
return *this;
}
Student(const Student& s)//拷贝构造
:_num(s._num)
, Person(s)//派生类赋值给基类
{
;
}
//赋值重载
Student& operator = (const Student& s)
{
if (this != &s)
{
//加域作用限定,否则发生死循环
Person::operator =(s);
_num = s._num;
}
return *this;
}
不能被继承的类
⽅法1:基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。
⽅法2:C++11新增了⼀个final关键字,final修改基类,派⽣类就不能继承了。
// C++11的⽅法
class Base final
{
public:
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
private:
// C++98的⽅法
/*Base()
{}*/
};
class Derive :public Base
{
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
继承与友元
友元关系不能继承,也就是说父类友元不能访问子类私有和保护成员。
class Student;//声明
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
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;
}
继承与静态成员
基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都只有⼀个static成员实例。
class Person
{
public:
string _name;
static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum;
};
int main()
{
Person p;
Student s;
// 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的
// 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份
cout << &p._name << endl;
cout << &s._name << endl;
// 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的
// 说明派⽣类和基类共⽤同⼀份静态成员
cout << &p._count << endl;
cout << &s._count << endl;
// 公有的情况下,⽗派⽣类指定类域都可以访问静态成员
cout << Person::_count << endl;
cout << Student::_count << endl;
return 0;
}
多继承与菱形继承
单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承
多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。
// 单继承
class Person {
public:
string _name;
};
class Student : public Person {
public:
int _number;
};
class info : public Student {
public:
int year;
};
int main() {
info s1;
return 0;
}
// 多继承
class Person {
public:
string _name;
};
class Student {
public:
int _number;
};
class info : public Student
, public Person
{
public:
int year;
};
int main()
{
info s1;
return 0;
}
// 菱形继承
class Person {
public:
string _name;
};
class Student : public Person {
public:
int _number;
};
class info : public Person
{
public:
int year;
};
class Son : public Student, public info {
public:
string wang;
};
int main() {
Son s1;
s1._name = "李四"; //E0266 "Son::_name" 不明确
return 0;
}
菱形继承发生在多继承环境中,当一个类(称为“孙子类”)继承自两个或多个基类,而这些基类又共同继承自同一个基类时,这就叫菱形继承。一定要避开菱形继承,因为菱形继承会造成两个问题:数据冗余和二义性。
虚继承
虚继承主要用于解决多重继承中可能出现的二义性和数据冗余问题。
虚拟继承(Virtual Inheritance)在C++中通过在继承声明中加上virtual关键字来实现,使得派生类在继承多个基类时,对于某个共享基类的继承只保留一份基类拷贝,而不是在每个基类中都保留一份。
class Person
{
public:
string _name; // 姓名
};
//虚继承
class Student : virtual public Person
{
protected:
int _num; //学号
};
//虚继承
class Teacher : virtual public Person
{
protected:
int _id; // 职工编号
};
菱形继承(二义性和数据冗余)
菱形虚拟继承(解决了二义性和数据冗余)
继承与组合
is-a关系
public继承就是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。基类的内部细节对子类可见。
class A
{};
class B : public A
{};
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为**白箱复用(white - box reuse)。**术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对派生类类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
has-a关机
class A
{};
class B
{
A _aa;
};
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black - box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。
总结:
继承耦合度高,所以依赖关系很强。
组合耦合度低,依赖关系不强。
所以推荐使用组合而不是继承。