目录
- 🚄什么是继承
- 🚉继承的概念
- 🚃继承的定义
- 🚇继承基类成员访问方式的变化
- 🚆基类和派生类对象赋值转换
- 🚐继承时的作用域
- 🚗派生类的默认成员函数
- 🚓继承、友元、静态成员
- 🚚复杂的菱形继承及菱形虚拟继承
- 🏍️继承总结
🚄什么是继承
🚉继承的概念
继承是面向对象程序设计中的一种重要机制,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法,并在此基础上进行扩展和修改。通过继承,子类可以获得父类的所有公共成员,包括字段、属性、方法等,并且可以添加自己的额外成员。
继承的概念体现了面向对象程序设计的层次结构。父类通常表示一般化的概念,而子类则表示更具体的概念。例如,一个“动物”类可以作为父类,而“狗”和“猫”类可以作为子类,它们继承了“动物”的共同特征,如呼吸、移动等,同时也可以定义自己特有的行为,如“狗”可以叫和咬东西,“猫”可以爬树。
继承的好处之一是代码复用
。通过继承,我们可以在不改变原有类的情况下扩展功能,减少了重复编写代码的工作量。如果多个类具有共同的属性和方法,我们可以将它们抽象成一个父类,然后让这些类继承该父类,从而实现代码的复用。
另外,继承还可以提高代码的可维护性和扩展性。当需要修改或添加功能时,我们只需要在父类中进行修改或添加,而不需要改动所有的子类。这样可以减少潜在的错误和重复的劳动。
总之,继承是面向对象程序设计中非常重要的概念,它通过建立类之间的层次结构,实现了代码的复用、可维护性和扩展性。合理地使用继承可以使代码更加清晰、简洁和易于理解。
🚃继承的定义
C++中类的继承使用关键字 public
、protected
或 private
来指定继承的访问级别。继承的定义格式如下:
class 派生类名 : 访问级别 基类名
{
// 派生类的成员
};
其中,派生类名表示派生类的名称,基类名表示基类的名称。访问级别可以是 public、protected 或 private,用来指定继承的访问级别。如果没有指定访问级别,则默认为 private 继承。
派生类可以继承多个基类,每个基类之间用逗号,
分隔。继承多个基类时,访问级别可以分别指定,也可以一起指定。
下面是一个简单的 C++ 类继承的示例:
#include <iostream>
using namespace std;
// 定义一个基类
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 定义一个派生类
class Rectangle : public Shape
{
public:
int getArea()
{
return (width * height);
}
};
// 主函数
int main()
{
Rectangle rect;
rect.setWidth(5);
rect.setHeight(7);
cout << "Area of the rectangle: " << rect.getArea() << endl;
return 0;
}
在这个示例中,Shape 是一个基类,Rectangle 是一个派生类。Rectangle 继承了 Shape 的 setWidth() 和 setHeight() 方法,并添加了自己的 getArea() 方法。在主函数中,创建了一个 Rectangle 对象,调用了它的方法来计算矩形的面积。
运行结果:
🚇继承基类成员访问方式的变化
继承方式的访问控制规则是:public继承保持基类的访问级别不变,protected继承将基类的public和protected成员变为派生类的protected成员,private继承将基类的public和protected成员变为派生类的private成员。而基类的private成员在任何情况下都不可见。
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
总结:
基类的 private 成员在派生类中无论以什么方式继承都是不可见的。这意味着,虽然基类的 private 成员被派生类继承了,但是派生类中无法直接访问它们,也不能在派生类的对象中使用这些成员。因此,如果需要在派生类中访问基类的成员,应该将它们定义为 protected 成员,而不是 private 成员。
在实际使用中,public 继承是最常用的继承方式,因为它能够保持基类的接口不变,同时也能够访问基类的 protected 成员。而 protected 和 private 继承则很少使用,因为它们会导致代码的可读性和可维护性变差。因此,建议在设计类的时候,根据实际需求选择合适的继承方式和访问控制规则,以提高代码的可读性和可维护性。
🚆基类和派生类对象赋值转换
派生类对象可以赋值给基类的对象、基类的指针和基类的引用,这种赋值操作被称为切片或切割。这意味着派生类对象中的基类部分可以被赋值给基类对象。
基类对象不能直接赋值给派生类对象,因为派生类对象包含了基类对象的成员,而基类对象没有派生类对象的成员。
基类的指针或引用可以通过强制类型转换赋值给派生类的指针或引用。但是,只有当基类的指针指向派生类对象时,这样的转换才是安全的。
如果基类是多态类型(即含有虚函数),可以使用运行时类型信息(RTTI)的dynamic_cast来进行识别并进行安全转换。dynamic_cast可以在运行时检查指针或引用的类型,并在类型不匹配时返回空指针或引发异常。
需要注意的是,dynamic_cast只能用于具有多态性的类层次结构中,即必须至少有一个虚函数。
示例代码:
#include <iostream>
using namespace std;
#include <iostream>
using namespace std;
class Animal
{
protected:
string _name; // 名称
int _age; // 年龄
public:
Animal(string name, int age) : _name(name), _age(age)
{}
void sound()
{
cout << "Animal sound" << endl;
}
};
class Cat : public Animal
{
public:
Cat(string name, int age) : Animal(name, age)
{}
void sound()
{
cout << "Meow" << endl;
}
};
class Dog : public Animal
{
public:
Dog(string name, int age) : Animal(name, age)
{}
void sound()
{
cout << "Woof" << endl;
}
};
void Test()
{
Cat cat("Tom", 3);
Dog dog("Max", 5);
// 子类对象可以赋值给父类对象/指针/引用
Animal animal = cat;
Animal* animalPtr = &dog;
Animal& animalRef = cat;
// 基类对象不能赋值给派生类对象
//cat = animal;
// 基类的指针可以通过强制类型转换赋值给派生类的指针
animalPtr = &cat;
Cat* catPtr1 = (Cat*)animalPtr;
catPtr1->sound();
animalPtr = &dog;
Cat* catPtr2 = (Cat*)animalPtr;
catPtr2->sound(); // 这种情况转换后会存在越界访问的问题,因为实际对象是Dog而不是Cat
}
int main()
{
Test();
return 0;
}
运行结果:
🚐继承时的作用域
在继承体系中,基类和派生类都有独立的作用域。这意味着它们可以拥有相同名称的成员,但在使用时会有一些规则需要注意:
-
子类和父类中有同名成员时,子类成员会屏蔽父类对同名成员的直接访问。这种情况被称为隐藏或重定义。在子类成员函数中,如果想要访问父类的同名成员,可以使用基类名加上作用域解析运算符(
::
)来进行显示访问。 -
需要注意的是,如果是成员函数的隐藏,只需要函数名相同就会构成隐藏。即使参数列表不同,也会隐藏父类中的同名函数。
-
在实际编程中,最好避免在继承体系中定义同名的成员,以避免产生混淆和错误。
总之,继承体系中的同名成员会发生隐藏,子类可以通过基类名加作用域解析运算符(::
)来访问父类的同名成员。在设计继承体系时,应该避免同名成员的存在,以减少潜在的问题。
🚗派生类的默认成员函数
在派生类中,如果我们没有显式地定义以下6个成员函数:构造函数、拷贝构造函数、赋值重载运算符、析构函数和取地址重载,编译器会自动生成它们。这些自动生成的成员函数会按照以下规则进行生成和调用:
-
派生类的构造函数必须调用基类的构造函数来初始化基类的成员。如果基类没有默认的构造函数,派生类的构造函数必须在初始化列表中显式调用基类的构造函数来完成基类成员的初始化。
-
派生类的拷贝构造函数必须调用基类的拷贝构造函数来完成基类成员的拷贝初始化。
-
派生类的赋值运算符必须调用基类的赋值运算符来完成基类成员的赋值。
-
派生类的析构函数会在被调用完成后自动调用基类的析构函数来清理基类成员。这样才能保证派生类对象先清理派生类成员,再清理基类成员的顺序。
-
派生类对象的初始化顺序是先调用基类的构造函数,然后调用派生类的构造函数。
-
派生类对象的析构顺序是先调用派生类的析构函数,然后调用基类的析构函数。
需要注意的是,在一些场景中,析构函数需要进行重写(即基类的析构函数需要声明为虚函数)。重写的条件之一是函数名相同。由于析构函数名会被编译器特殊处理为destructor()
,所以如果父类的析构函数没有被声明为虚函数,子类的析构函数和父类的析构函数会构成隐藏关系,无法实现多态的析构。因此,在继承体系中,如果需要使用多态的析构,应该将基类的析构函数声明为虚函数。
🚓继承、友元、静态成员
继承与友元:
友元关系不能被继承。在C++中,基类的友元不能访问子类的私有和保护成员。这是因为继承是一种"is-a"关系,子类应该继承基类的接口,而不是继承基类的友元关系。
如果基类的友元需要访问子类的私有和保护成员,可以考虑将子类的私有和保护成员提升为公有成员,或者将子类作为友元类。但是,这样会破坏封装性,应该谨慎使用。
继承与静态成员:
当基类定义了静态成员时,整个继承体系中只有一个这样的成员实例。无论有多少个派生类,它们都共享同一个静态成员。
静态成员是属于类的,而不是属于对象的。每个类只有一个静态成员,不管创建多少个对象,都只有一个静态成员实例。在继承体系中,派生类继承了基类的静态成员,并且它们共享同一个静态成员实例。这意味着无论创建多少个派生类的对象,它们都可以访问和修改同一个静态成员。
这种特性可以用于在整个继承体系中共享一些数据或方法,而不需要为每个派生类都创建一个独立的静态成员。这可以提高代码的效率和可维护性。
🚚复杂的菱形继承及菱形虚拟继承
复杂的菱形继承和菱形虚拟继承是继承关系中的两个特殊情况。
菱形继承指的是一个派生类同时继承了两个直接或间接基类,而这两个基类又共同继承自同一个基类。这种继承关系形成了一个菱形的结构,因此得名。例如,假设有一个基类Animal,两个派生类Cat和Dog分别继承自Animal,然后再有一个类Pet同时继承自Cat和Dog,这样就形成了一个菱形继承关系。
菱形继承会导致一些问题,例如,Pet对象中会包含两个Animal对象的副本,造成资源的浪费和冗余。为了解决这个问题,C++引入了菱形虚拟继承。
菱形虚拟继承是通过在虚拟继承时使用关键字virtual
来解决菱形继承问题的。在菱形继承中,Pet类可以使用虚拟继承来继承自Cat和Dog,这样就只会有一个Animal对象的副本被共享,避免了资源的浪费和冗余。
使用菱形虚拟继承时,需要注意解决虚基类的初始化问题,通常在派生类的构造函数中显式调用虚基类的构造函数来完成初始化。
总之,复杂的菱形继承和菱形虚拟继承是在多重继承中出现的特殊情况,需要特别注意继承关系和初始化问题。
示例代码:
#include <iostream>
#include <string>
using namespace std;
// 基类 Animal
class Animal {
public:
Animal(string name, int age)
{
_name = name;
_age = age;
cout << "Animal()" << endl;
}
~Animal()
{
cout << "~Animal()" << endl;
}
string _name;
int _age;
};
// 派生类 Cat
class Cat : virtual public Animal {
public:
Cat(string name, int age)
:Animal(name, age)
{
cout << "Cat()" << endl;
}
~Cat()
{
cout << "~Cat()" << endl;
}
void speak()
{
cout << "cat speak" << endl;
}
};
// 派生类 Dog
class Dog : virtual public Animal {
public:
Dog(string name, int age)
:Animal(name, age)
{
cout << "Dog()" << endl;
}
~Dog()
{
cout << "~Dog()" << endl;
}
void speak()
{
cout << "dog speak" << endl;
}
};
// 派生类 Pet
class Pet : public Cat, public Dog {
public:
Pet(string name, int age)
:Cat(name, age), Dog(name, age), Animal(name, age)
{
cout << "Pet()" << endl;
}
~Pet()
{
cout << "~Pet()" << endl;
}
void speak()
{
cout << "Pet speak" << endl;
}
};
int main()
{
Pet pet("Tom", 18);
pet.speak();
return 0;
}
运行结果:
🏍️继承总结
多继承确实是C++语法中一个复杂的特性,特别是在存在菱形继承和菱形虚拟继承的情况下,底层实现会更加复杂。因此,一般情况下不建议设计多继承和菱形继承,以避免复杂度和性能上的问题。许多后来的面向对象语言,如Java,都不支持多继承,这也可以视为C++中的一个缺陷。
继承和组合是两种不同的关系:
- 公有继承表示一种is-a(是一个)的关系,即派生类对象也是基类对象。这种继承方式通过生成派生类的复用,通常被称为白箱复用。在继承中,基类的内部细节对子类是可见的,这可能破坏基类的封装性,基类的改变会对派生类产生很大影响,且派生类和基类间的依赖关系很强,耦合度较高。
- 对象组合是一种has-a(有一个)的关系。假设B组合了A,每个B对象中都有一个A对象。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用,因为对象的内部细节对外部是不可见的,对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度较低。
所以,在实际开发中,建议优先使用对象组合而不是类继承。对象组合具有较低的耦合度和良好的封装性,有助于提高代码的维护性。不过,继承仍然有其适用的场景,比如实现多态性时是必需的。在选择继承或组合时,应根据具体的关系和需求来判断,优先选择组合,只有在适合继承和需要实现多态性时才使用继承。