继承
- 友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
- 基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。
什么是继承
继承是面向对象编程(OOP)中的一个重要概念,它是一种创建新类(称为派生类或子类)的机制,新类从现有的类(称为基类或父类)继承属性和行为。这使得代码可以复用,并且可以建立类之间的层次关系。
class Animal {
public:int age;void eat() {std::cout << "The animal is eating." << std::endl;}
};class Dog : public Animal {
public:void bark() {std::cout << "The dog is barking." << std::endl;}
};
在这个例子中,子类是Dog,父类是Animal,public是继承方式。继承之后,子类不仅仅有自己的成员,也有父类中的成员。

继承方式
一个类中有public,protected,private访问限定符,继承也分为三种方式,public继承,protected继承和private继承。两两组合就有了如下的表。
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public继承 | 派生类的protected继承 | 派生类的private继承 |
基类的protected成员 | 派生类的protected继承 | 派生类的protected继承 | 派生类的private继承 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
总结:
- 基类private成员在派生类中无论以什么方式继承都是不可见的。在派生类中不可见是指基类的私有成员还是被继承到了派生类中,但是在语法上限定了派生类对象无论是在类内还是类外都无法去访问它。
- 如果基类成员不想在类外直接被访问,但在派生类中可以访问,就可以定义为protected。该限定符是因继承才出现的。
- 关键字class默认继承方式是private,关键字struct默认继承方式是public。
- 除了基类的私有成员外,其它成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式)。
赋值转换
class Person
{
public:void Print() {cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "wzh";int _age = 18;
};class Student : public Person
{
protected:int _stuid;
};
-
派生类对象可以赋值给基类的对象、基类的指针以及基类的引用。这种方式称为切片或切割。
Student s;// 子类对象可以赋值给父类对象/指针/引用Person p = s;Person* p_ptr = &s;Person& p_ref = s;
-
基类对象不能赋值给派生类对象。因为派生类对象期望的内存空间比基类对象提供的要大,如果允许基类对象直接赋值给派生类对象,就可能会出现内存访问越界的问题
-
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。
//基类的指针可以通过强制类型转换赋值给派生类的指针,且基类的指针是指向派生类对象p_ptr = &s;Student* s_ptr1 = (Student*)p_ptr;s_ptr1->_stuid = 10;//基类的指针不是指向派生类对象,存在越界访问p_ptr = &p;Student* s_ptr2 = (Student*)p_ptr;s_ptr2->_stuid = 10;
隐藏(重定义)
在继承体系中,派生类和基类有着独立的作用域。当派生类中的函数与基类中的函数同名(函数名相同,参数列表可以不同,与返回值无关)时,派生类函数会屏蔽基类中的同名函数。成员变量同样可以被隐藏。(在派生类成员函数中,可以使用 基类::基类成员 显示访问)。
class Base {
public:void func(int a) {std::cout << "Base::func(int), a = " << a << std::endl;}
};
class Derived : public Base {
public:void func(double a) {std::cout << "Derived::func(double), a = " << a << std::endl;}
};
int main() {Derived d;d.func(3.14); // 调用Derived::func(double)// d.func(3); // 错误,因为Derived类中的func函数隐藏了Base类中的func(int)函数return 0;
}
在这个例子中,Derived
类中的func
函数隐藏了Base
类中的func
函数。当通过Derived
类对象d
调用func
函数时,传入3.14
会调用Derived::func(double)
。但是直接传入3
会产生错误,因为编译器只会看到Derived
类中的func
函数,它无法自动将这个调用转换为对Base::func(int)
的调用。
派生类的默认成员函数
构造函数
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 派生类对象初始化先调用基类构造再调派生类构造。
class Base {
public:int baseVar;//先被调用Base(int var = 2) : baseVar(var) { std::cout << "Base part: " << baseVar << std::endl; }
};class Derived : public Base {
public:int derivedVar;//后调用Derived(int derivedVal = 1) : derivedVar(derivedVal){std::cout << "Derived part: " << derivedVar << std::endl;}
};int main() {Derived d;return 0;
}

析构函数
- 派生类对象先调用派生类析构再调基类的析构。
class Base {
public:int baseVar;Base(int var) : baseVar(var) {}~Base() {std::cout << "Base destructor" << std::endl;}
};
class Derived : public Base {
public:int derivedVar;Derived(int baseVal, int derivedVal) : Base(baseVal), derivedVar(derivedVal) {}~Derived() {std::cout << "Derived destructor" << std::endl;}
};
int main() {Derived* d = new Derived(1, 2);delete d;return 0;
}

拷贝构造
-
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
class Base { public:int baseVar;Base(int var) : baseVar(var) {}Base(const Base& other) : baseVar(other.baseVar) {} }; class Derived : public Base { public:int derivedVar;//构造Derived(int baseVal, int derivedVal) : Base(baseVal), derivedVar(derivedVal) {}//拷贝构造Derived(const Derived& other) : Base(other), derivedVar(other.derivedVar) {} }; int main() {Derived d1(1, 2);Derived d2(d1);std::cout << "Base part of d2: " << d2.baseVar << ", Derived part of d2: " << d2.derivedVar << std::endl;return 0; }
赋值运算符重载
-
派生类的operator=必须要调用基类的operator=完成基类的赋值。
class Base { public:int baseVar;Base(int var) : baseVar(var) {}Base& operator=(const Base& other) {if (this != &other) baseVar = other.baseVar;return *this;} }; class Derived : public Base { public:int derivedVar;Derived(int baseVal, int derivedVal) : Base(baseVal), derivedVar(derivedVal) {}Derived& operator=(const Derived& other) {if (this != &other) {Base::operator=(other);//调用基类的赋值derivedVar = other.derivedVar;}return *this;} }; int main() {Derived d1(1, 2);Derived d2(3, 4);d2 = d1;std::cout << "Base part of d2: " << d2.baseVar << ", Derived part of d2: " << d2.derivedVar << std::endl;return 0; }
菱形继承
-
单继承:一个子类只有一个直接父类。
-
多继承:一个子类有两个或以上直接父类。
-
菱形继承:一个子类有多个父类,而多个父类又继承自同一个父类。(是多继承的一种特殊情况)
菱形继承如图:
class A
{
public:int _a;
};
class B : public A
{
public:int _b;
};
class C : public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

如上,菱形继承产生了数据冗余和二义性的问题。二义性的问题好解决,指明访问的作用域就好了,但是数据冗余如何解决呢?继续往下看。
菱形虚拟继承
为了解决菱形继承带来的问题,可以使用虚继承。虚继承可以保证在派生类中只有一份共享的基类子对象。
class A
{
public:int _a;
};
class B : virtual public A
{
public:int _b;
};
class C : virtual public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

从上图可以看出,D对象中将A放到的了对象组成的最下 面,这个A同时属于B和C。然后在B和C中存了一个指针,这个指针叫虚基表指针,它指向虚基表,这张表里存放的是偏移量,通过偏移量可以找到A。
继承和组合
组合:
- 组合是一种has-a的关系,即一个类包含另一个类的实例作为成员变量。
- 组合是一种黑箱复用,也就是说对象的内部细节是不可见的。
class Engine {
public:void start() { /* 启动引擎 */ }void stop() { /* 停止引擎 */ }
};class Car {
private:Engine engine; // 组合关系public:void drive() {engine.start();// 驾驶汽车的其他操作engine.stop();}
};
继承:
- 继承是一种is-a的关系,即一个类派生自另一个类,从而获得父类的属性和方法。
- 继承是一种白箱复用,即基类的内部细节对子类可见,由此可见继承一定程度上破坏了基类的封装。
class Vehicle {
public:void move() { /* 移动 */ }
};class Car : public Vehicle { // 继承关系
public:void drive() { /* 驾驶汽车 */ }
};
总的来说:组合体现了代码的封装性和复用性,而继承体现了代码的多态性和扩展性。在使用上,若两者都可用那么优先使用组合。