C++面向对象三大特性之——继承
- 一.继承的概念及定义
- 1.1概念
- 1.2 继承的定义
- 1.3继承基类成员访问方式的变化
- 二.基类和派生类对象赋值转换
- 三.继承中的作用域
- 四. 派生类的默认成员函数
- 4.1.派生类构造函数
- 4.2派生类拷贝构造函数
- 4.3派生类的赋值重载函数(operator=)
- 4.4派生类中的析构函数
- 五.继承与友元
- 六. 继承与静态成员
- 七.复杂的菱形继承及菱形虚拟继承
- 八.继承和组合
一.继承的概念及定义
1.1概念
继承是面向对象编程中的一个核心概念,它指的是一种类与类之间的关系,其中一个类可以继承另一个类的属性和方法。通过继承,子类可以重用父类的代码,并且可以在此基础上进行扩展或修改。
1.2 继承的定义
例如:
class Person
{
public:void Print(){cout << _name << endl;cout << _age << endl;}
protected:string _name = "xiaowang";int _age = 0;
};class Student : public Person
{
public:
protected://年级int _grade = 1;
};
int main()
{Person p1;//s1中也拥有_name 和 _age 成员变量Student s1;return 0;
}
1.3继承基类成员访问方式的变化
- 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,最好是显示继承
- **在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,**也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强
二.基类和派生类对象赋值转换
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
class Person
{
public:void Print(){cout << _name << endl;cout << _age << endl;}
protected:string _name = "xiaowang";int _age = 0;
};class Student : public Person
{
public:Student(const string& name){_name = name;}
protected://年级int _grade = 1;
};int main()
{Student s1("小王");Person p1 = s1;p1.Print();Person* p2 = &s1;p2->Print();Person& p3 = s1;p2->Print();return 0;
}
基类对象不能赋值给派生类对象。
下图中的这一行为叫做切片
三.继承中的作用域
之前学过的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 注意在实际中在继承体系里面最好不要定义同名的成员。
class Person
{
public:void Print(){cout << _name << endl;cout << _age << endl;}string _name = "xiaowang";int _age = 0;
};class Student : public Person
{
public:Student(const string& name,int age,int grade){_name = name;_age = age;_grade = grade;} string _name = "";int _grade = 1;
};int main()
{Student s1("小李", 20, 1);cout << s1._name << endl;cout << s1._age << endl << endl;cout << s1.Person::_name << endl;cout << s1.Person::_age << endl;return 0;
}
总结:
基类和派生类中都有name成员变量,优先访问派生类中的成员变量
想要访问基类中的成员变量需要指定基类的类域
他们两的关系可以理解为全局变量与局部变量的关系
基类与派生类的同名函数不构成重载!!!
- 它们的关系是隐藏(重定义),只有函数名相同
- 在没有指定类域的时候,编译器只会调用派生类中的函数
- 如果调用函数时传递的参数与派生类中的参数不符合,程序会直接报错,不回去基类中匹配基类的函数
解释如下:
有如下代码:
class Person
{
public:void fun(int i){cout << "void fun(int i)" << endl;}
};class Student : public Person
{
public:void fun(){cout << "void fun()" << endl;}
};int main()
{Student s1;s1.fun(1);return 0;
}
这段代码会直接报错,他会先在派生类(子类)中查找fun函数,如果找到了就会调用它,调用时发现参数不匹配,直接报错。
报错信息如下:
如果将派生类中的fun注释掉,就会会调用父类的函数,所以说派生类与基类中的同名函数关系为隐藏(也可以说是重定义)。
派生类与基类中有同名函数,还要调用基类中的函数的方法如下:
四. 派生类的默认成员函数
结论如下(下面代码介绍这些函数的显示调用):
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的operator=必须要调用基类的operator=完成基类的复制。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 派生类对象初始化先调用基类构造再调派生类构造。
- 派生类对象析构清理先调用派生类析构再调基类的析构。
解释:
4.1.派生类构造函数
派生类的构造函数会优先调用基类的构造函数,再初始化自己的成员
派生类显示调用构造的正确方法:
class Person
{
public:Person(const string& name, int age):_name(name),_age(age){}
protected:string _name = "";int _age = 0;
};class Student : public Person
{
public:Student(const string& name, int age, int grade):Person(name,age),_grade(grade){}void Print(){cout << _name << endl;cout << _age << endl;cout << _grade << endl;}
protected:int _grade = 0;
};int main()
{Student s1("小刘",20,2);s1.Print();return 0;
}
输出如下:
重点注意:
基类中的 _name 和 _age 必须要在初始化列表中初始化,要显示调用基类中的构造函数。Student构造函数 错误写法如下:
Student(const string& name, int age, int grade):_name(name),_age(age),_grade(grade){}
初始化列表的顺序即使是打乱写,也会先调用Person(name,age)
因为初始化列表的初始化顺序是按照声明顺序初始化的。基类的成员变量是在派生类前面声明的。可以理解为:
4.2派生类拷贝构造函数
class Person
{
public:Person(const string& name, int age):_name(name),_age(age){}Person(const Person& p){_name = p._name;_age = p._age;}protected:string _name = "";int _age = 0;
};class Student : public Person
{
public://构造函数Student(const string& name, int age, int grade):Person(name, age),_grade(grade){}//拷贝构造Student(const Student& s):Person(s),_grade(s._grade){}void Print(){cout << _name << endl;cout << _age << endl;cout << _grade << endl;}
protected:int _grade = 0;
};int main()
{Student s1("小刘",20,2);s1.Print();Student s2(s1);s2.Print();return 0;
}
派生类的拷贝构造也必须要在初始化列表调用基类的拷贝构造
派生类的 对象s 作为基类拷贝构造的参数,拷贝构造的参数是一个引用,所以这里的操作就是切片。把派生类的基类的那一部分参数切出来了。
4.3派生类的赋值重载函数(operator=)
派生类的operator=必须要调用基类的operator=完成基类的复制。
class Person
{
public:Person(const string& name, int age):_name(name),_age(age){}Person(const Person& p):_name(p._name),_age(p._age){}Person& operator=(const Person& p){if (this != &p){_name = p._name;_age = p._age;}return *this;}protected:string _name = "";int _age = 0;
};class Student : public Person
{
public://构造函数Student(const string& name, int age, int grade):Person(name, age),_grade(grade){}//拷贝构造Student(const Student& s):Person(s),_grade(s._grade){}Student& operator=(const Student& s){if (this != &s){Person::operator=(s);_grade = s._grade;}return *this;}void Print(){cout << _name << endl;cout << _age << endl;cout << _grade << endl;}
protected:int _grade = 0;
};int main()
{Student s1("小刘",20,2);s1.Print();Student s2(s1);s2.Print();Student s3("小李", 22, 3);s1 = s3;s1.Print();return 0;
}
- 派生类的赋值拷贝函数在赋值基类的那部分时必须调用基类的赋值拷贝
- 这里必须指定基类的类域,因为派生类与基类的赋值拷贝是同名函数,它们构成隐藏,如果不指定类域程序会一直重复调用派生类中的赋值拷贝,进入死循环
- 这里也同样是切片,用基类的引用接收派生类。
虽然下面的下面代码也可以通过,但是如果有涉及复杂的资源管理或深拷贝等逻辑下面这种做法就是可能会导致内存泄漏,或者如果 _name 和 _ age 在基类中的权限时prevate,程序依然会报错。调用基类的赋值操作符,这样更加符合良好的编程习惯,特别是当基类发生变化时,能够确保赋值逻辑的一致性和正确性。
Student& operator=(const Student& s)
{if (this != &s){_name = s._name;_age = s._age;_grade = s._grade;}return *this;
}
4.4派生类中的析构函数
class Person
{
public:~Person(){cout << "~Person()" << endl;}protected:string _name = "";int _age = 0;
};class Student : public Person
{
public:~Student(){Person::~Person();cout << "~Student()" << endl;}
protected:int _grade = 0;
};int main()
{Student s1;return 0;
}
先看输出结果
调用了两次基类的析构函数,这是为什么呢?最后为什么会再多调用一个析构函数?
实际上基类的析构函数会在派生类的析构结束后自动调用。
所以基类的析构不能显示调用
派生类对象析构清理先调用派生类析构再调基类的析构。
五.继承与友元
友元关系不能继承
下面代码注释部分展开程序会报错
class Person
{
public:friend void Print(Person p);//friend void Print(Student s);protected:int _age = 10;int _a = 20;};class Student : public Person
{
public:protected:int _grade = 0;
};void Print(Person p)
{cout << p._age << endl;cout << p._a << endl;
}//void Print(Student s)
//{
// cout << s._age << endl;
// cout << s._a << endl;
//}int main()
{Person p;Print(p);Student s;//Print(s);return 0;
}
这条语句应该写在 Student类中 就可以运行成功了
六. 继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例 。
class Person
{
public:Person(int a = 1):_a(a){}static int _age;
protected:int _a;};
int Person::_age = 1;class Student : public Person
{
public:protected:int _grade = 0;
};int main()
{Person p1;cout << p1._age << endl;p1._age++;Student s1;cout << s1._age << endl;return 0;
}
_age只有一份,因为它存在静态区
七.复杂的菱形继承及菱形虚拟继承
什么是菱形继承?
class Person
{
public:Person(int a = 1):_a(a){}int _a;};class Student : public Person
{
public:protected:int _grade = 0;
};class Teacher :public Person
{
public:protected:int _t = 1;
};class assistant : public Student, public Teacher
{
public:void Print(){cout << _a << endl;cout << _grade << endl;cout << _t << endl;cout << _ass << endl;}
private:int _ass;
};int main()
{Person p1;Student s1;Teacher t1;assistant a1;return 0;
}
这里的 _a 导致程序运行不了,出现了调用歧义
需要指明类域访问a,这样就可以访问到指定的类域
发生这种情况的原因:
Student和Teacher都继承了Person,它们当中都有**_a变量,
Assistent继承了Student和Teacher**,Assistent中会出现两份_a变量,这就是菱形继承
它导致了数据冗余和二义性,调用时导致调用不明确.
当然也可以指定类域访问冗余的数据,但这并不是一个好的办法!
菱形虚拟继承
class Student :virtual public Person
{
public:
protected:int _grade = 0;
};class Teacher :virtual public Person
{
public:protected:int _t = 1;
};
在可能出现数据冗余和二义性的类继承时加上 virtual 关键字
这样就不会导致数据冗余和二义性了
虚拟继承的方法是使用了一个偏移量地址.
八.继承和组合
- public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
- 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
- 优先使用对象组合,而不是类继承 。
优先使用组合的原因:
继承通常会导致子类对父类的高度依赖,继承关系一旦形成,子类与父类的耦合度就变得很高,修改父类可能会影响子类。
组合则是通过将多个对象的功能组合在一起,让它们协同工作。这样,功能之间的耦合度降低,系统更具弹性和扩展性。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
继承和组合的区别
继承:是一种类之间的关系,子类从父类继承属性和方法,表示“是一个”(is-a)关系。子类可以重用父类的代码,也可以扩展或修改父类的行为。
组合:是通过在一个对象内部包含其他对象的方式来复用代码,表示“有一个”(has-a)关系。一个对象由多个组件对象组成,每个组件负责自己的功能。
什么时候使用继承,是么时候使用组合
例如:轮胎和汽车的关系是 has-a 车有一个轮胎, 像这种有一个的就用组合
例如:植物和水果的关系是 is-a 水果是一个植物,像这种是一个的就用继承
例如:list 和 queue 的关系即使 has-a 又是 is-a ,这种情况一般情况下都是是哦给你继承
使用继承:
当类之间有明确的“是一个”关系时。
当子类需要重用父类的代码并扩展其功能时。
当需要多态性时。
继承带来的紧耦合不会成为问题时。
使用组合:
当类之间有“有一个”关系时。
当需要灵活地改变、替换或扩展对象的功能时。
当继承导致紧耦合、层次结构复杂或行为不符合时。
需要实现高内聚、低耦合时。