1. 继承
1.1概念
继承(Inheritance)是面向对象编程中的一个重要概念,它允许一个类(称为派生类或子类)从另一个类(称为基类或父类)继承属性和方法。继承是实现类之间的关系,通过继承,子类可以重用父类的代码,并且可以在此基础上添加新的功能或修改已有的功能。
在C++中,继承可以通过关键字 class
或 struct
后面的冒号来声明和定义。语法格式如下:
class DerivedClass : accessSpecifier BaseClass {
// DerivedClass 的成员和方法
};
其中,DerivedClass
是派生类的名称,BaseClass
是基类的名称,accessSpecifier
是访问控制符,可以是 public
、protected
或 private
,用于控制派生类对基类成员的访问权限。
C++ 支持三种继承方式:
-
公有继承(Public Inheritance):通过
public
关键字声明,派生类可以访问基类的公有成员和受保护成员,基类的私有成员对派生类不可见。 -
保护继承(Protected Inheritance):通过
protected
关键字声明,派生类可以访问基类的公有成员和受保护成员,基类的私有成员对派生类不可见。 -
私有继承(Private Inheritance):通过
private
关键字声明,派生类无法访问基类的成员,除了通过友元关系。
派生类可以继承基类的成员变量和成员函数,包括公有成员、受保护成员和私有成员。继承的成员可以通过派生类对象进行访问。
继承还可以形成类的层次结构,派生类可以再次被其他类继承,形成多层次继承关系。继承链上的每个派生类都可以添加新的成员和方法,形成更丰富的功能。
需要注意的是,C++ 支持多继承,即一个派生类可以同时继承多个基类。多继承的语法类似于单继承,通过逗号分隔多个基类。
继承在面向对象编程中是一种强大的机制,它提供了代码重用和扩展的能力。通过继承,可以建立类之间的关系,并且实现了多态性和抽象性等重要概念。
例
已有的类称为父类或者基类
新的类称为子类或者派生类
#include <iostream>
using namespace std;
class Father{
public:
string first_name="王";
void work(){
cout<<"我是名厨师"<<endl;
}
void show(){
cout<<"姓氏:"<<first_name<<endl;
}
};
class Son:public Father{
};
int main()
{
Son s;
s.show();
s.work();
}
在父类基础上做更改
#include <iostream>
using namespace std;
class Father{
public:
string first_name="王";
void work(){
cout<<"我是名厨师"<<endl;
}
void show(){
cout<<"姓氏:"<<first_name<<endl;
}
};
class Son:public Father{
public:
int age=20;
void work(){
cout<<"我是名货车司机"<<endl;
}
void show(){
//调用基类的show()方法
Father::show();
//cout<<"年龄"<<age<<"姓氏:"<<first_name<<endl;
cout<<"年龄"<<age<<endl;
}
};
int main()
{
Son s;
s.show();
s.work();
}
1.2 构造函数
构造函数(Constructor)是一种特殊的成员函数,用于在创建对象时初始化对象的状态。构造函数的名称与类的名称相同,没有返回类型(包括 void),并且可以带有参数。
在C++中,每个类可以定义一个或多个构造函数。当创建类的对象时,会自动调用相应的构造函数来初始化对象的数据成员。构造函数可以执行必要的初始化操作,如分配内存、初始化成员变量、设置默认值等。
构造函数的特点如下:
-
与类同名:构造函数的名称必须与类的名称完全相同。
-
没有返回类型:构造函数没有返回类型,包括 void。在函数声明和定义时不需要指定返回类型。
-
可以带有参数:构造函数可以带有参数,用于初始化对象的数据成员。参数可以是任意类型,包括基本类型、类类型和用户自定义类型。
-
可以重载:同一个类可以定义多个构造函数,通过参数的类型和数量的不同进行重载。根据实际需要,可以使用不同的构造函数来创建对象。
以下是一个示例,展示了如何定义和使用构造函数:
class MyClass {
public:
// 默认构造函数
MyClass() {
// 初始化数据成员
value = 0;
}
// 带参数的构造函数
MyClass(int num) {
// 使用参数初始化数据成员
value = num;
}
// 成员函数
void printValue() {
std::cout << "Value: " << value << std::endl;
}
private:
int value;
};
int main() {
// 使用默认构造函数创建对象
MyClass obj1;
obj1.printValue(); // Output: Value: 0
// 使用带参数的构造函数创建对象
MyClass obj2(42);
obj2.printValue(); // Output: Value: 42
return 0;
}
在上面的示例中,MyClass
类定义了两个构造函数:一个默认构造函数和一个带参数的构造函数。默认构造函数没有参数,用于初始化对象的数据成员为默认值。带参数的构造函数接收一个整数参数,并用该参数初始化对象的数据成员。
在 main()
函数中,我们使用不同的构造函数创建了两个 MyClass
对象 obj1
和 obj2
,并调用 printValue()
成员函数打印对象的值。
构造函数在对象创建过程中自动调用,可以确保对象在使用之前处于有效的状态。构造函数的重载和参数化使得可以根据需要进行灵活的对象初始化。
1.2.1 透传构造
透传构造(Forwarding Constructors)是C++11引入的一种构造函数技术,它允许一个类的构造函数将参数透传给另一个构造函数,从而避免了重复的代码编写。
透传构造的主要思想是,一个构造函数可以接受相同的参数并将它们传递给另一个构造函数来完成对象的构造。这样可以减少代码冗余,提高代码的可维护性和重用性。
在C++中,可以使用以下语法来实现透传构造:
class MyClass {
public:
// 主构造函数
MyClass(int num, double val, const std::string& str) {
// 对参数进行初始化
// ...
}
// 透传构造函数
MyClass(int num) : MyClass(num, 0.0, "") {
// 将参数透传给主构造函数
}
// 其他成员函数
// ...
};
在上面的示例中,MyClass
类定义了一个主构造函数和一个透传构造函数。主构造函数接受三个参数,并进行相应的初始化操作。透传构造函数只接受一个整数参数,并将该参数透传给主构造函数,同时使用默认值来初始化其他参数。
通过透传构造函数,我们可以使用不同的参数列表来创建对象,而实际的初始化工作由主构造函数完成。这样可以避免在透传构造函数中重复编写相同的初始化代码,提高了代码的可读性和维护性。
需要注意的是,透传构造函数只能将参数透传给同一个类中的另一个构造函数,不能透传给父类的构造函数或其他类的构造函数。
透传构造在处理多个构造函数的情况下非常有用,它简化了构造函数的实现,减少了代码的冗余,并提供了更灵活的对象初始化方式。
例
子类直接调用父类的构造方法
#include <iostream>
using namespace std;
class Father{
private:
string first_name;
string sex;
public:
Father(string first_name){ //父类中给出构造函数 默认无参的就不存在
this->first_name=first_name;
}
Father(string first_name,string sex){
this->first_name=first_name;
this->sex=sex;
}
void show(){
cout<<"姓氏:"<<first_name<<endl;
}
};
class Son:public Father{
private:
int age;
public:
Son(string n,int a):Father(n),age(a){}
Son(string first_name,string sex,int age):Father(first_name,sex),age(age){}
void show(){
Father::show();
//cout<<"年龄"<<age<<"姓氏:"<<first_name<<endl;
cout<<"年龄"<<age<<endl;
}
};
int main()
{
Son s("李",20);
s.show();
s.work();
Son s2("王","男",30);
s2.show();
}
1.2.2 委托构造
委托构造(Delegating Constructors)是C++11引入的一种构造函数技术,它允许一个构造函数委托给同一个类的另一个构造函数完成对象的初始化。
委托构造的主要思想是,一个构造函数可以调用同一个类中的其他构造函数来完成对象的初始化。这样可以避免在不同的构造函数中重复编写相同的初始化代码。
在C++中,可以使用以下语法来实现委托构造:
class MyClass {
public:
// 构造函数1
MyClass() : MyClass(0, 0.0, "") {
// 委托给构造函数2
}
// 构造函数2
MyClass(int num, double val, const std::string& str) {
// 对参数进行初始化
// ...
}
// 其他成员函数
// ...
};
在上面的示例中,MyClass
类定义了两个构造函数:构造函数1和构造函数2。构造函数1通过使用冒号初始化列表调用构造函数2来完成对象的初始化。这样,构造函数1不再需要显式初始化对象的数据成员,而是将工作委托给构造函数2。
通过委托构造,我们可以避免在多个构造函数中重复编写相同的初始化代码,提高了代码的可维护性和重用性。同时,委托构造还可以实现更清晰和简洁的构造函数逻辑。
需要注意的是,C++11中引入的委托构造要求委托的构造函数在初始化列表中出现,而不是在函数体内部。
委托构造在处理多个构造函数的情况下非常有用,它简化了构造函数的实现,减少了代码的冗余,并提供了更灵活的对象初始化方式。
例
一个类中构造函数访问自己另一个构造函数。相当于间接的访问父类的构造函数。
Son(int a):Son(“王”,a){} —>Son(string n,int a):Father(n),age(a){} -->Father(string first_name)
#include <iostream>
using namespace std;
class Father{
private:
string first_name;
string sex;
public:
Father(string first_name){ //父类中给出构造函数 默认无参的就不存在
this->first_name=first_name;
}
Father(string first_name,string sex){
this->first_name=first_name;
this->sex=sex;
}
void show(){
cout<<"姓氏:"<<first_name<<endl;
}
};
class Son:public Father{
private:
int age;
public:
Son(int a):Son("王",a){}
Son(string n,int a):Father(n),age(a){}
Son(string first_name,string sex,int age):Father(first_name,sex),age(age){}
void show(){
Father::show();
//cout<<"年龄"<<age<<"姓氏:"<<first_name<<endl;
cout<<"年龄"<<age<<endl;
}
};
int main()
{
Son s(20);
s.show();
}
1.2.3 继承练习题
封装Person 类
name,age,sex
给出show方法要输出上面三条信息
封装Employee 职员 继承自Person
salary, work_id
给出show方法要输出除了Person类中基本的情况之外 再输出salary, work_id
封装Manager 管理者 继承自Employee
position 职位
给出show方法要输出除了Employee类中基本的情况之外 再输出position
#include <iostream>
using namespace std;
class Person{
private:
string name;
int age;
string sex;
public:
Person(string name,int age,string sex){
this->name=name;
this->age=age;
this->sex=sex;
}
void show(){
cout<<name<<" "<<age<<" "<<sex<<endl;
}
};
class Employee:public Person{ //职员类继承自Person类
private:
double salary;
string work_id;
public:
Employee(string name,int age,string sex,double salary,string work_id):Person(name,age,sex){
this->salary=salary;
this->work_id=work_id;
}
void show(){
Person::show();
cout<<salary<<" "<<work_id<<endl;
}
};
class Manager:public Employee{
private:
string position;
public:
//指明直接基类的构造函数函数如何调用
Manager(string name,int age,string sex,double salary,string work_id,string position)
:Employee(name,age,sex,salary,work_id)
{
this->position=position;
}
void show(){
Employee::show();
cout<<position<<endl;
}
};
int main()
{
Manager m("老吴",40,"男",8000,"007","经理");
m.show();
}
1.3 对象的创建与销毁
在C++中,对象的创建和销毁是通过构造函数和析构函数来完成的。
-
对象的创建:
- 静态对象的创建:静态对象是在程序运行之前就被创建的,它们存在于程序的整个生命周期中。静态对象的创建由静态存储区域负责管理,例如全局变量或静态成员变量。
- 栈上对象的创建:栈上对象是在函数内部通过声明局部变量来创建的,它们的生命周期与所在的作用域相对应。当函数退出作用域时,栈上对象会自动被销毁。
- 堆上对象的创建:堆上对象通过使用
new
运算符在堆上动态分配内存来创建。堆上对象的生命周期由程序员手动管理,需要通过delete
运算符来显式释放对象所占用的内存。
-
对象的销毁:
- 析构函数:析构函数是在对象被销毁时自动调用的特殊成员函数。它的主要作用是清理对象所占用的资源,例如释放动态分配的内存、关闭文件等。析构函数的名称与类的名称相同,以波浪号
~
作为前缀,没有返回类型和参数。 - 对象销毁的时机:
- 静态对象:在程序结束时自动销毁。
- 栈上对象:当对象所在的作用域结束时自动销毁。
- 堆上对象:需要程序员手动调用
delete
运算符来销毁对象,并释放内存。
- 析构函数:析构函数是在对象被销毁时自动调用的特殊成员函数。它的主要作用是清理对象所占用的资源,例如释放动态分配的内存、关闭文件等。析构函数的名称与类的名称相同,以波浪号
在对象销毁的过程中,析构函数会按照与构造函数相反的顺序被调用。这意味着先创建的对象的析构函数会先被调用,后创建的对象的析构函数会后被调用。
正确管理对象的创建和销毁是编写高质量C++代码的重要部分。通过适当地使用构造函数和析构函数,可以确保对象的正确初始化和资源的及时释放,避免内存泄漏和资源泄漏的问题。
例
#include <iostream>
using namespace std;
class Father{
private:
string name;
public:
Father(string n):name(n){
cout<<"父类的构造函数调用"<<endl;
}
~Father(){
cout<<"父类的析构函数调用"<<endl;
}
};
class Son:public Father{
public:
Son(string name):Father(name){
cout<<"子类的构造函数调用"<<endl;
}
~Son(){
cout<<"子类的析构函数调用"<<endl;
}
};
int main()
{
Son s("小明");
}
#include <iostream>
using namespace std;
class Value{
public:
string str;
Value(string str){
this->str=str;
cout<<this->str<<"创建了"<<endl;
}
~Value(){
cout<<str<<"销毁了"<<endl;
}
};
class Father{
public:
string name;
Value v= Value("Father中的对象成员");
static Value s_v; //static对象成员
Father(string n):name(n){
cout<<"父类的构造函数调用"<<endl;
}
~Father(){
cout<<"父类的析构函数调用"<<endl;
}
};
Value Father::s_v=Value("Father中的static对象成员");
class Son:public Father{
public:
Value v=Value("Son中的对象成员");
static Value s_v;
Son(string name):Father(name){
cout<<"子类的构造函数调用"<<endl;
}
~Son(){
cout<<"子类的析构函数调用"<<endl;
}
};
Value Son::s_v=Value("Son中的static对象成员");
int main()
{
cout<<"——————————程序运行之前————————"<<endl;
{
Son s("小明");
}
cout<<"------------"<<endl;
}
1.4 多继承
1.4.1多继承
多继承(Multiple Inheritance)是面向对象编程中的一种概念,指的是一个类可以从多个父类中继承属性和行为。
在C++中,多继承允许一个类派生自多个基类,通过使用逗号分隔的形式来指定多个基类。例如:
class Derived : public Base1, public Base2 {
// 类成员和函数
};
在上面的示例中,Derived
类通过 public
访问修饰符从 Base1
和 Base2
两个类中进行多继承。这意味着 Derived
类将继承 Base1
和 Base2
类中的所有公有成员和函数。
多继承可以带来一些优势,如代码重用、灵活性和多态性。通过从多个基类中继承,一个派生类可以拥有多个父类的特性,并具备更强大的功能。
然而,多继承也带来了一些挑战和潜在的问题。其中一个主要问题是命名冲突,当多个父类具有同名的成员时,派生类需要明确指定使用哪个父类的成员。此外,多继承也增加了类的复杂性和理解难度。
为了避免多继承带来的问题,需要谨慎设计和使用多继承,确保派生类在继承多个基类时能够保持清晰、可维护和易于理解的结构。
需要注意的是,多继承在实际开发中并不常见,更常见的是单继承或使用接口(纯虚函数)实现的多态性。在设计类的继承关系时,应根据具体情况权衡利弊,并选择合适的继承方式。
例
一个类有两个个或者多个基类叫多继承,会获得多个基类的属性和方法
#include <iostream>
using namespace std;
class Bed{
public:
void sleep(){
cout<<"可以躺着"<<endl;
}
};
class Sofa{
public:
void sit(){
cout<<"可以坐着"<<endl;
}
};
class SofaBed:public Bed,public Sofa{
};
int main()
{
SofaBed sf;
sf.sit();
sf.sleep();
}
1.4.2 多继承问题
多继承在面向对象编程中是一种强大的工具,但也带来了一些潜在的问题和挑战。以下是一些常见的多继承问题:
-
命名冲突:当多个父类拥有同名的成员(变量、函数等)时,派生类在访问这些成员时会出现冲突。需要通过作用域解析符(
::
)明确指定使用哪个父类的成员,或者使用虚拟继承来解决冲突。 -
菱形继承(Diamond Inheritance):当一个派生类继承自两个间接基类,而这两个间接基类又共同继承自同一个基类时,就形成了菱形继承结构。这种结构可能导致二义性问题和内存资源的浪费。C++通过虚拟继承(virtual inheritance)来解决菱形继承问题,确保只有一份共同基类的实例。
-
难以理解和维护:多继承会增加类的复杂性,使类的层次结构变得更加庞大和复杂。这可能使代码更难理解、调试和维护。需要谨慎设计多继承关系,使类的层次结构保持简洁、清晰和易于理解。
-
耦合性增加:多继承可能导致类之间的耦合性增加,因为派生类同时继承了多个父类的特性。修改一个父类可能会对多个派生类产生影响,增加了代码的脆弱性和维护的困难度。
为了解决多继承问题,可以采取以下几种策略:
- 使用虚拟继承(virtual inheritance)解决菱形继承问题,确保只有一份共同基类的实例。
- 使用命名空间(namespace)来解决命名冲突问题,将同名的成员放置在不同的命名空间中。
- 通过合理的设计,避免过度使用多继承,尽量保持类的层次结构的简洁和清晰。
- 使用接口(纯虚函数)实现的多态性,而不是通过多继承来实现。
总而言之,多继承是一项强大的特性,但在使用时需要谨慎考虑,权衡利弊。合理设计和使用多继承可以充分发挥其优势,同时避免潜在的问题和复杂性。
例
如果多继承的两个基类中有重名的成员时,会产生二义性.
解决方式:使用基类::同名成员 进行区分,明确成员来源
#include <iostream>
using namespace std;
class Bed{
public:
void sleep(){
cout<<"可以躺着"<<endl;
}
void positon(){
cout<<"放在卧室"<<endl;
}
};
class Sofa{
public:
void sit(){
cout<<"可以坐着"<<endl;
}
void positon(){
cout<<"放在客厅"<<endl;
}
};
class SofaBed:public Bed,public Sofa{
};
int main()
{
SofaBed sf;
sf.sit();
sf.sleep();
//sf.positon(); //二义性问题
sf.Bed::positon();
sf.Sofa::positon();
}
1.4.3 菱形继承
菱形继承(Diamond Inheritance)是指在多继承关系中存在一个菱形形状的继承结构。这种结构通常发生在一个派生类继承自两个不同的父类,而这两个父类又共同继承自同一个基类的情况。
下面是一个简单的示例来说明菱形继承的概念:
class Base {
public:
int value;
};
class Derived1 : public Base {
};
class Derived2 : public Base {
};
class Derived3 : public Derived1, public Derived2 {
};
在上面的示例中,Derived1
和 Derived2
类分别直接继承自 Base
类。然后,Derived3
类通过多继承同时继承自 Derived1
和 Derived2
类。这样就形成了一个菱形继承结构。
菱形继承可能导致以下问题:
-
冗余数据:
Derived3
类继承了两次Base
类,因此在Derived3
对象中会有两份value
成员变量。这样会导致冗余的数据存储和内存占用。 -
二义性:由于
Derived3
类继承自两个共同的基类,当访问基类的成员时可能发生二义性。例如,如果在Derived3
类中访问value
成员变量,编译器无法确定是从Derived1
还是Derived2
继承的value
。
为了解决菱形继承带来的问题,C++提供了虚拟继承(virtual inheritance)机制。通过在派生类对共同基类的继承关系前加上 virtual
关键字,可以确保在派生类中只有一份共同基类的实例。
修改示例代码如下:
class Base {
public:
int value;
};
class Derived1 : virtual public Base {
};
class Derived2 : virtual public Base {
};
class Derived3 : public Derived1, public Derived2 {
};
通过在 Derived1
和 Derived2
类的继承声明中加上 virtual
关键字,可以解决菱形继承带来的冗余数据和二义性问题。现在,在 Derived3
类中只会有一份 value
成员变量,并且通过 Derived3
类可以访问到该成员变量。
虚拟继承机制是通过在共同基类的子对象中添加一个虚拟指针(vptr)来实现的。这个虚拟指针指向一个虚拟函数表(vtable),用于解决函数调用的二义性问题。
例1
一个类的两个直接基类,也拥有一个共同基类,叫菱形继承。这时也会产生二义性问题
菱形解决二义性可以使用作用域限定符的方式区分
#include <iostream>
using namespace std;
class Furniture{
public:
int a=5;
void show(){
cout<<"家具类的show方法"<<endl;
}
};
class Bed:public Furniture{
public:
//继承一次Furniture成员
};
class Sofa:public Furniture{
public:
//继承一次Furniture成员
};
class SofaBed:public Bed,public Sofa{
//这时SofaBed会继承两次Furniture成员
};
int main()
{
SofaBed sf;
sf.Bed::show();
sf.Sofa::show();
cout<< &(sf.Bed::a)<<endl; //0x61fe88
cout<<&(sf.Sofa::a)<<endl; //0x61fe8c
}
例2
菱形继承也可以通过虚继承的方式,解决二义性。虚继承之后,最上层的基类成员相当于变成共享的。所有的派生类都只获得一份最上层基类(Furniture)的成员。
#include <iostream>
using namespace std;
class Furniture{
public:
int a=5;
void show(){
cout<<"家具类的show方法"<<endl;
}
};
class Bed:virtual public Furniture{
public:
};
class Sofa:virtual public Furniture{
public:
};
class SofaBed:public Bed,public Sofa{
};
int main()
{
SofaBed sf;
//sf.Bed::show();
//sf.Sofa::show();
sf.show(); //只有一份没有二义性
cout<< &(sf.Bed::a)<<endl; //0x61fe8c
cout<<&(sf.Sofa::a)<<endl; //0x61fe8c
}
2. 权限
2.1 不同权限修饰的成员的访问情况
public 本类中可以访问 子类中可以访问 全局中可以访问
protected 本类中可以访问 子类中可以访问 全局不可以访问
private 本类中可以访问 子类不可以访问 全局不可以访问
public | 本类中可以访问 | 子类中可以访问 | 全局中可以访问 |
protected | 本类中可以访问 | 子类中可以访问 | 全局不可以访问 |
private | 本类中可以访问 | 子类不可以访问 | 全局不可以访问 |
#include <iostream>
using namespace std;
class Father{
public:
string first_name="王";
protected:
string car="劳斯莱斯";
private:
string password="123789";
public:
void show(){
cout<<first_name<<" "<<car<<" "<<password<<endl;
}
};
class Son:public Father{
public:
void show(){
cout<<first_name<<endl;
cout<<car<<endl;
//cout<<password<<endl; //父类中私有的不能访问到
}
};
int main()
{
Father f;
f.show();
Son s;
s.show();
cout<<"-------------"<<endl;
cout<<f.first_name<<endl;//全局中可以访问类中public权限成员
//cout<<f.car<<endl; //全局中不可以访问类中protected权限成员
//cout<<f.password<<endl; //全局中不可以访问类中private权限成员
}
2.2 不同权限的继承
继承中的权限有三种
- public权限继承
- protected权限继承
- private权限继承
需要注意:
私有成员能够继承,但是不能直接访问,需要通过public接口才可以访问。
各种权限的继承都不会改变基类中的私有内容的权限类型
继承并不会改变基类中的权限,只是继承过来的内容的权限在派生类中发生了变化
2.2.1 public继承
公有继承情况下,继承过来的内容权限在子类是不变的
#include <iostream>
using namespace std;
class Father{
public:
string first_name="王";
protected:
string car="劳斯莱斯";
private:
string password="123789";
public:
void show(){
cout<<first_name<<" "<<car<<" "<<password<<endl;
}
};
class Son:public Father{
public:
void show(){
cout<<first_name<<endl;
cout<<car<<endl;
//cout<<password<<endl; //父类中私有的不能访问到
}
};
class GrandSon:public Son{
public:
void show(){
cout<<first_name<<endl;
cout<<car<<endl;
}
};
int main()
{
Son s;
s.show();
GrandSon gs;
gs.show();
}
2.2.2 protected 继承
保护继承之后,继承过来的内容在子类中的权限全变成protected
#include <iostream>
using namespace std;
class Father{
public:
string first_name="王";
protected:
string car="劳斯莱斯";
private:
string password="123789";
public:
void show(){
cout<<first_name<<" "<<car<<" "<<password<<endl;
}
};
class Son:protected Father{
public:
void show(){
cout<<first_name<<endl;
cout<<car<<endl;
//cout<<password<<endl; //父类中私有的不能访问到
}
};
class GrandSon:public Son{
public:
void show(){
cout<<first_name<<endl;
cout<<car<<endl;
}
};
int main()
{
Son s;
s.show();
//cout<<s.first_name<<endl; //父类中的public在子类中变成protected
GrandSon gs;
gs.show();
}
2.2.3 private继承
私有继承会把继承过来的内容,在子类中都变成私有
#include <iostream>
using namespace std;
class Father{
public:
string first_name="王";
protected:
string car="劳斯莱斯";
private:
string password="123789";
public:
void show(){
cout<<first_name<<" "<<car<<" "<<password<<endl;
}
};
class Son:private Father{
public:
void show(){
cout<<first_name<<endl;
cout<<car<<endl;
//cout<<password<<endl; //父类中私有的不能访问到
}
};
class GrandSon:public Son{
public:
void show(){
//cout<<first_name<<endl; Son中first_name变成私有 访问不到
//cout<<car<<endl; Son中first_name变成私有 访问不到
}
};
int main()
{
Son s;
s.show();
GrandSon gs;
gs.show();
}
3.多态
多态(Polymorphism)是面向对象编程的一个重要概念,指的是同一类型的对象在不同的情况下表现出不同的行为。
在多态中,父类的指针或引用可以指向子类的对象,通过调用相同的方法,可以实现不同子类对象的不同行为。这样的特性可以使代码更加灵活、可扩展和可维护。
多态有两种形式:静态多态和动态多态。
-
静态多态(静态多态性,Static Polymorphism)也称为编译时多态或早期绑定(Early Binding),是通过函数重载和模板实现的。在编译时,根据函数的参数类型或模板的实例化类型,确定要调用的函数或方法。
-
动态多态(动态多态性,Dynamic Polymorphism)也称为运行时多态或晚期绑定(Late Binding),是通过虚函数(virtual function)和继承关系实现的。在运行时,根据对象的实际类型来决定要调用的函数或方法。
动态多态通过虚函数和继承关系实现,允许子类重写父类的虚函数,并在运行时根据对象的实际类型调用相应的函数。这使得可以通过父类的指针或引用调用子类特定的实现,实现了多态的特性。
使用多态的好处包括:
- 代码重用:可以通过父类的指针或引用调用子类的方法,实现代码的重用,减少重复的代码。
- 灵活性:可以在运行时根据对象的实际类型来决定调用哪个函数,实现不同对象的不同行为。
- 扩展性:通过添加新的子类来扩展功能,而无需修改现有的代码。
要使用多态,需要满足以下条件:
- 使用继承关系:子类继承自父类,子类可以重写父类的虚函数。
- 声明虚函数:在父类中声明虚函数,使用
virtual
关键字进行修饰。 - 使用父类的指针或引用:通过父类的指针或引用指向子类的对象,来实现多态的调用。
总而言之,多态是面向对象编程中重要的概念,通过静态多态和动态多态可以实现代码的灵活性和可扩展性。动态多态通过虚函数和继承关系实现,在运行时根据对象的实际类型来决定调用哪个函数。
总结如下
一个接口,多种状态。程序运行的时候选择决定调用相应的代码
静态多态:运算符重载,函数重载 在编译的时候就已经确定。
动态多态的条件:
1.公有继承
2.基类指针或者基类引用指向派生类对象
3.派生类覆盖基类中的虚函数
3.1 没有使用多态的情况
在编译的时候就已经确定参数类型,运行的时候直接执行参数类型中的方法
#include <iostream>
using namespace std;
class Animla{
public:
void eat(){
cout<<"吃东西"<<endl;
}
};
class Cat:public Animla{
public:
void eat(){
cout<<"吃鱼"<<endl;
}
};
class Dog:public Animla{
public:
void eat(){
cout<<"吃肉"<<endl;
}
};
void test(Animla& a){
a.eat();
}
int main()
{
Dog d;
test(d); //吃东西;
Cat c;
test(c); //吃东西;
}
3.2 使用多态的情况
函数覆盖需要用到虚函数。虚函数就是virtual关键字修饰的函数
函数覆盖和虚函数的一些特点:
1.虚函数具有传递性,如果重写父类中的虚函数,子类加不加virtual关键字都是虚函数。
2.构造函数和静态函数不能定义成虚函数。
3.虚函数声明和定义分离时,关键字只需要加在声明处
重写:函数的名称 和参数列表要完全一致。并且返回值一致或者能自动转换
运行时根据传入对象的类型,来判断应该执行哪个对象里的方法
#include <iostream>
using namespace std;
class Animla{
public:
virtual void eat(){
cout<<"吃东西"<<endl;
}
};
class Cat:public Animla{
public:
void eat(){
cout<<"吃鱼"<<endl;
}
};
class Dog:public Animla{
public:
virtual void eat(){
cout<<"吃肉"<<endl;
}
};
//void test(Cat& c){
// c.eat();
//}
//void test(Dog& d){
// d.eat();
//}
void test(Animla& a){
a.eat();
}
void test2(Animla * a){
a->eat();
}
int main()
{
Dog d;
test(d); //吃肉
Cat c;
test(c); //吃鱼
test2(&c);//吃鱼
}
3.3 多态原理
多态的原理是基于虚函数(virtual function)和动态绑定(dynamic binding)的机制实现的。
在多态中,当一个父类的指针或引用指向一个子类的对象时,通过调用相同的函数名,可以实现对不同子类对象的不同行为。这是因为父类中的函数被声明为虚函数,子类可以重写(覆盖)这些虚函数,以提供自己的实现。
在C++中,当使用虚函数时,编译器为每个含有虚函数的类生成一个虚函数表(vtable),这个表存储了每个虚函数的地址。同时,每个对象也会存储一个指向其所属类的虚函数表的指针,这个指针称为虚函数指针(vptr)。
当通过父类的指针或引用调用虚函数时,实际调用的是虚函数表中对应的函数。编译器在编译时并不确定具体调用哪个函数,而是在运行时根据对象的实际类型来决定调用哪个函数。这个过程称为动态绑定(dynamic binding)或后期绑定(late binding)。
动态绑定的过程如下:
- 当使用父类的指针或引用调用虚函数时,先根据指针或引用的静态类型(父类类型)找到对应的虚函数表。
- 然后根据指针或引用的动态类型(实际指向的子类对象类型)在虚函数表中查找对应的虚函数。
- 最后调用找到的虚函数。
通过动态绑定,即使使用父类的指针或引用,也能够在运行时调用到子类的实现,实现了多态的特性。
需要注意的是,只有通过指针或引用调用虚函数才会触发动态绑定,直接通过对象调用虚函数会导致静态绑定,即根据对象的静态类型确定调用的函数。
总结起来,多态的原理是通过虚函数和动态绑定机制实现的。虚函数表存储了每个虚函数的地址,通过指针或引用的动态类型在虚函数表中查找对应的虚函数,并在运行时调用正确的函数实现。这使得通过父类的指针或引用可以实现对不同子类对象的不同行为。
3.4 多态的问题
形成多态可能造成内部泄漏
#include <iostream>
using namespace std;
class Animla{
public:
virtual void eat(){
cout<<"吃东西"<<endl;
}
~Animla(){
cout<<"Animal的析构函数"<<endl;
}
};
class Cat:public Animla{
public:
void eat(){
cout<<"吃鱼"<<endl;
}
~Cat(){
cout<<"Cat的析构函数"<<endl;
}
};
class Dog:public Animla{
public:
void eat(){
cout<<"吃肉"<<endl;
}
~Dog(){
cout<<"Dog的析构函数"<<endl;
}
};
int main()
{
Dog * d=new Dog;
delete d; //先调用dog类的析构 再Anima的析构函数
Animla * a=new Cat;
delete a; //只会Animal的析构函数
}
解决方式 :需要在基类的析构函数前加virtual关键字
#include <iostream>
using namespace std;
class Animla{
public:
virtual void eat(){
cout<<"吃东西"<<endl;
}
virtual ~Animla(){
cout<<"Animal的析构函数"<<endl;
}
};
class Cat:public Animla{
public:
void eat(){
cout<<"吃鱼"<<endl;
}
~Cat(){
cout<<"Cat的析构函数"<<endl;
}
};
class Dog:public Animla{
public:
void eat(){
cout<<"吃肉"<<endl;
}
~Dog(){
cout<<"Dog的析构函数"<<endl;
}
};
int main()
{
Dog * d=new Dog;
delete d;
Animla * a=new Cat;
delete a;
}
4. 抽象类
抽象类(Abstract Class)是一种特殊的类,它不能被实例化,只能作为其他类的基类来使用。抽象类用于定义一组相关的接口和行为,但无法直接创建对象。
在C++中,通过在类中声明纯虚函数(Pure Virtual Function)来定义抽象类。纯虚函数是一种在基类中声明但没有具体实现的虚函数,通过在函数声明的末尾使用 “= 0” 来表示。
抽象类的主要目的是为了提供一个通用的接口,而具体的实现则由派生类来完成。派生类必须实现基类中的纯虚函数,否则它也会成为一个抽象类。通过这种方式,抽象类可以起到一种约束和规范的作用。
下面是一个示例代码来说明抽象类的定义和使用:
class Animal {
public:
virtual void sound() = 0; // 纯虚函数,定义了抽象类Animal的接口
};
class Dog : public Animal {
public:
void sound() override {
cout << "Woof!" << endl; // 实现了抽象类Animal的纯虚函数
}
};
class Cat : public Animal {
public:
void sound() override {
cout << "Meow!" << endl; // 实现了抽象类Animal的纯虚函数
}
};
int main() {
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
animal1->sound(); // 输出: Woof!
animal2->sound(); // 输出: Meow!
delete animal1;
delete animal2;
return 0;
}
在上面的示例中,Animal
是一个抽象类,它声明了纯虚函数 sound()
,用于定义动物的声音。Dog
和 Cat
类是 Animal
类的派生类,它们必须实现 sound()
函数,否则也会成为抽象类。
在 main()
函数中,通过 Animal
类的指针创建了 Dog
和 Cat
对象,并调用了 sound()
函数。由于 sound()
函数是虚函数,并且在派生类中进行了实现,所以在运行时会根据对象的实际类型调用相应的函数实现。
需要注意的是,抽象类不能被实例化,因此不能创建抽象类的对象。但可以通过抽象类的指针或引用来调用派生类的实现。抽象类提供了一种统一的接口,可以方便地扩展和实现多态的特性。
例
只是一个抽象的概念,目的是给派生类写一个框架。抽象类不能实例化对象,抽象类不能做函数参数和函数返回值。
一个类中有纯虚函数是就是抽象类
抽象类中一定有纯虚函数函数文章来源:https://www.toymoban.com/news/detail-481702.html
#include <iostream>
using namespace std;
class Shape{
public:
virtual void area()=0;
virtual void perimeter()=0;
};
int main()
{
Shape s; //错误 抽象类不能实例化对象
}
如果继承抽象类,需要把抽象类中纯虚方法全部实现,如果没有全部实现,该派生类还是抽象类,不能实例化对象文章来源地址https://www.toymoban.com/news/detail-481702.html
#include <iostream>
using namespace std;
class Shape{
public:
virtual void area()=0;
virtual void perimeter()=0;
};
class Rectangle:public Shape{
public:
void area(){
cout<<"长方形的面积"<<endl;
}
void perimeter(){
cout<<"长方形的周长"<<endl;
}
};
int main()
{
Rectangle s;
s.area();
s.perimeter();
}
练
#include <iostream>
using namespace std;
//Role 派生出soldier ADC AP
//写一个公共接口test() 不同的的对象可以作为参数传到test()接口
//不同类型对象执行attack()攻击方法的实现是不同的
class Role{
public:
virtual void attack(){
cout<<"角色进行攻击"<<endl;
}
};
class soldier:public Role{
void attack(){
cout<<"战士进行攻击"<<endl;
}
};
class ADC:public Role{
void attack(){
cout<<"ADC进行攻击"<<endl;
}
};
class AP:public Role{
void attack(){
cout<<"AP进行攻击"<<endl;
}
};
void test(Role & r){
r.attack();
}
void test2(Role * r){
r->attack();
}
int main()
{
ADC adc;
soldier s;
AP ap;
test(adc);
test(s);
test(ap);
ADC * adc2=new ADC; //基类的指针指向派生类对象
test2(adc2);
}
到了这里,关于C++ 面向对象核心(继承、权限、多态、抽象类)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!