本篇文章主要讲解 C++ 以及面向对象语言三大特性之一的继承特性。


1  继承概念(什么是继承)

        面向对象语言(C++、Java、python 等)都会具有三大特性:封装、继承、多态,其中继承我们已经在类和对象中体会过了,本质就是将一个类的属性和方法的实现封装在一个类的内部,只对外部暴露一些少量的接口;外部就可以使用该类来实例化对象,一旦使用一个类实例化出了一个对象,那么该对象就具有了属性和方法。而且将属性和方法的实现封装在类的内部,用户只需要了解接口的使用就可以了,降低了用户的使用成本。

        其实操作系统就是一种封装特性的体现。操作系统分为操作系统内核与外壳,内核整体被封装在操作系统内部,而且会对外提供一些接口,也就是系统调用。这样我们仅仅需要了解系统调用如何使用就可以了,不用关心其实现,大大降低了操作系统的使用成本。

        继承是面向对象语言中的另一大特性。继承特性就很像现实生活中儿子继承爸爸的财产一样,爸爸会将自己的房子与车子等等财产留给儿子,儿子来继承爸爸的房子和车子,这样儿子就可以复用爸爸的房子和车子,就不用自己赚钱买车买房了;面向对象语言的继承特性也是一样的,只要一个类继承了另一个类,那么继承的类就拥有了被继承类的属性和方法,也就是儿子继承了爸爸的房子和车子。所以继承特性本质上就是复用,而且是类的复用(C语言中主要是函数的复用)。

        在继承特性中,被继承的类称之为基类或者父类,被继承的类称之为派生类或者子类。比如下面的场景:

#include <iostream>
#include <string>

class Person
{
public:
    void Eat()
    {
        std:cout << "eat" << std::endl;
    }
    

    void Drink()
    {
        std::cout << "drink" << std::endl;
    }

private:
    std::string _name;
    double _weight;
    double _height;
};

//Student 继承了 Person 这一个类
//那么 Student 类也具有了 _name、_weight、_height 这几个属性, Eat、Drink 这两个方法
//但是 Student 类也具有自己独立的属性和方法,学号和学习方法
class Student : public Person
{
public:
    void Study()
    {
        std::cout << "Study" << std::endl;
    }

private:
    std::string _snumber;
};


//Teacher 也继承了 Person 这一个类
//那么 Teacher 类也具有了 _name、_weight、_height 这几个属性, Eat、Drink 这两个方法
//但是 Teacher 类也具有自己独立的属性和方法,教职工号和教学方法
class Teacher : public Person
{
public:
    void Teaching()
    {
        std::cout << "Teaching" << std::endl;
    }

private:
    std::string _tnumber;
};

其中 Teacher 与 Student 中具有相同的属性(姓名、身高、体重)与方法(吃、喝),那么我们就可以将相同的属性放到 Person 类中,再让 Teacher 与 Student 继承 Person 这个类,那么 Student 与 Teacher 就都有了这些属性与方法了;如果我们没有使用继承,那么就需要在 Teacher 和 Student 中各自写一份属性和方法。所以继承就实现了类之间的复用。

        在上述例子中,Person 是被继承的类,是基类/父类;Teacher 与 Student 是继承的类,是派生类/子类


2  继承语法(如何实现继承)

        继承的语法很简单,只要在需要继承的类后面跟上  : 继承方式 父类名 就可以了,比如 Student 类来继承 Person 类:

class Student : public Person
{
public:
private:
    std::string _snumber;
}

其中 public 是继承方式,Person 是被继承的类名。

        继承方式与类中的访问限定符一样,具有三种方式,分别是 public、protected、private,也就是公有、保护、私有继承,那么这三种继承方式有何不同呢?其实继承方式与基类中的访问限定付共同决定了派生类中对于基类成员的访问权限。我们将继承方式与基类中的访问限定符两两组合,就可以组合出 9 中派生类对于基类成员的访问权限:

派生类对于基类成员的访问权限
基类访问限定符 / 继承方式 private protected public
private 基类成员不可见 基类成员不可见 基类成员不可见
protected 派生类的 private 成员 派生类的 protected 成员 派生类的 protected 成员
public 派生类的 private 成员 派生类的 protected 成员 派生类的 public 成员

基类中的成员只要被 private 访问限定符限制,那么在派生类中就是不可见的,这里的不可见是指基类的 private 成员在子类中被继承了,但是是无法访问基类中的成员的。

        之前我们在类和对象中讲过,private 与 protected 访问限定符是没有区别的,在类外都是无法访问的;但是在继承这里 private 与 protected 访问限定符就有区别了,基类 protected 访问限定付限定的成员在派生类中是可见的,也就是在子类中可以直接访问;但是 private 成员在子类中是无法被访问的,但是也被继承了下来,也就是不可见。

#include <iostream>
#include <string>

using namespace std;

class Person
{
public:
	void Eat()
	{
		cout << "eat" << endl;
	}

protected:
	std::string _name;

private:
	int _age;
};

//私有继承会使得基类中的成员都变为派生类的私有成员
//class Student : private Person
//保护继承会使得基类中的成员都变为派生类的保护成员
//class Student : protected Person
//公有继承最常用
class Student : public Person
{
public:
	void Study()
	{
		cout << "study" << endl;
	}

	//基类的私有成员在派生类中不可见
	/*void PrintAge()
	{
		cout << _age << endl;
	}*/

	//基类的保护成员在派生类中是可见的
	void PrintName()
	{
		cout << _name << endl;
	}

private:
	string _snumber;
};

int main()
{
	Student s1;
	//私有继承类外无法访问基类的公有方法
	//s1.Eat();
	//保护继承类外无法访问基类的公有方法
	//s1.Eat();
	//共有继承类外可以访问基类的公有方法
	s1.Eat();

	return 0;
}

        上面的访问限定符与继承方式共有 9 种组合,看似很多,其实很好记:

(1) 不管采用什么方式,基类的 private 成员始终在派生类中是不可见的。

(2) 其余的基类成员在派生类的访问限定方式 = min(访问限定符号, 继承方式),其中 public > protected > private。比如基类中的 protected 成员,使用 private 方式继承,那么该成员在派生类中就是 pivate 成员;基类中的 protected 成员,使用 public 方式继承,那么该成员在派生类中就是 protected 成员。由于 private 与 protected 继承方式会使得基类中的 public 成员也变为私有或者保护,外界无法访问,在实际使用中扩展性会变差,所以在继承中使用的一般就是 public 继承。

        在类和对象中,class 关键字与 struct 关键字的不同就在于默认的访问限定符是不同的,class 默认为 private,而 struct 默认为 public,那么默认继承方式有没有什么不同呢?答案是有的,class 默认继承方式为 private,struct 默认继承方式为 public,其实与默认的访问限定符是相同的:

#include <iostream>
#include <string>

using namespace std;

class Person
{
public:
	void Eat()
	{
		cout << "eat" << endl;
	}

protected:
	std::string _name;
	int _age;
};

class Student :  Person
{
public:
	void Study()
	{
		cout << "study" << endl;
	}

private:
	string _snumber;
};

int main()
{
	Student s1;
	//class 默认继承方式为 private,基类的公有方法无法在类外访问
	s1.Eat();

	return 0;
}

运行结果:

#include <iostream>
#include <string>

using namespace std;

class Person
{
public:
	void Eat()
	{
		cout << "eat" << endl;
	}

protected:
	std::string _name;
	int _age;
};

struct Student : Person
{
public:
	void Study()
	{
		cout << "study" << endl;
	}

private:
	string _snumber;
};

int main()
{
	Student s1;
	//struct 默认继承方式为 public,基类的公有方法可以在类外访问
	s1.Eat();

	return 0;
}

输出结果:

eat

3  赋值兼容转换

        之前我们在 C++ 基础语法讲过,一个类型的对象是不能够直接赋值给一个另一个类型的引用的:

#include <iostream>

int main()
{
    double d = 3.14;
    //这样是会直接报错的
    //int& x = d;
    //必须在引用前加上 const
    const int& x = d;    

    return 0;
}

在代码中 x 直接引用 d 是会报错的,但是在前面加上 const 就可以了,本质上就是因为 x 引用的并不是 d 这个对象,而是引用的 d 发生隐式类型转换得到的临时对象,从以下这个代码中我们就可以看出来:

#include <iostream>

using namespace std;

class B
{
public:
    B(int b = 1) :_b(b)
    {}

    int GetB() const { return _b; }

private:
    int _b;
};

class A
{
public:
    A(int a) :_a(a)
    {
        cout << "A(int a = 10)" << endl;
    }

    A(const B& b)
    {
        _a = b.GetB();
        cout << "A(const B& b)" << endl;
    }

private:
    int _a;
};

int main()
{
    B b;
    //这里会调用发生隐式类型转化,a 引用中间的临时对象
    const A& a = b;

    return 0;
}

输出结果:

A(const B& b)

        但是在继承这里存在一种特殊情况,那就是派生类的对象可以直接赋值给基类的指针或者引用

#include <iostream>
#include <string>

using namespace std;

class Person
{
public:
	void Eat()
	{
		cout << "eat" << endl;
	}

protected:
	string _name;
	int _age;
    string _sex;
};

struct Student : Person
{
public:
	void Study()
	{
		cout << "study" << endl;
	}

private:
	string _number;
};

int main()
{
	Student s1;
	//这里不是报错,这里是发生了复制兼容转换
	Person& p = s1;
    Person* p2 = &s1;

	return 0;
}

这里直接把子类对象或者地址传递给父类的引用或者指针,在语法上称之为赋值兼容转换,其实就是父类的指针或者引用指向了子类中父类的那一部分

所以赋值兼容转换还有一个更形象的说法是切割或者是切片,含义就是将子类中属于父类的那一部分给切出来赋值给父类的指针或者引用。但是需要注意父类对象是不能传递给子类对象的指针或者引用的,因为子类对象中可能会有父类中没有的成员变量。


4  继承中的隐藏规则

        我们在 C++ 基础语法中说过,C++ 是支持函数重载的,只要函数名相同,参数不同(参数类型、个数不同),那么两个函数就可以构成重载,但是前提是两个函数必须是同一作用域。而在继承中,如果基类与子类不管是成员变量还是成员函数,只要名字相同,那么基类与子类就会构成隐藏。隐藏的规则有如下几条:

(1) 基类与子类有各自的作用域。所以同名函数在基类与子类中并不构成重载,而是隐藏。

(2) 派生类与基类中,只要有同名的成员,那么就构成隐藏,访问对应的成员时,会屏蔽对基类隐藏成员的访问,但是可以通过 基类::成员 来访问,与访问静态成员类似。需要注意的是,对于成员函数,只要函数名相同,那么就构成隐藏。

struct Base
{
	Base(int x = 1):_x(x) 
	{}

	void Func(int val = 1)
	{
		cout << "val: " << val << endl;
	}

	int _x;
};

struct Deriv : public Base
{
	Deriv(int x = 2):_x(x)
	{}

	void Func(int val = 2)
	{
		cout << "val: " << val << endl;
	}

	void Base_Func()
	{
		//可以使用 Base::Func 来访问基类中的成员
		Base::Func();
	}

	int _x;
};

int main()
{
	Deriv d;
	//这里 _x 与 Func 构成隐藏,优先访问派生类中的成员 
	cout << d._x << endl;
	d.Func();

	return 0;
}

输出结果:

2
val: 2

考察隐藏规则题目

        下面这里有道题目就是考察隐藏的:

class A
{
public:
    void func()
    {
        cout << "func()" << endl;
    }
};

class B : public A
{
public:
    void func(int i)
    {
        cout << "func(int i)" <<i<<endl;
    }
};

int main()
{
    B b;
    b.func(10);
    b.func();

    return 0;
};

1. A 和 B 的两个 fun 函数构成什么关系?

2. 上面那段代码编译的结果什么?

上面我们说过,在基类与派生类只要函数名相同,那就构成隐藏,所以两个 Func 函数的关系是隐藏;而既然构成隐藏,那么b.func() 就调用的都是 B 类中的 func 函数,显然需要传递参数,而 b.func() 并没有传递参数,所以代码会编译报错。

        其实我们可以这样理解隐藏:如果我们使用派生类中的成员,那么首先代码会先在派生类中查找对应的成员变量或者成员函数,只要找到了名字相同的成员,那么就去访问对应的成员变量或者成员函数;如果在派生类中没有找到,那就再去基类中寻找,找到了就访问基类中的成员变量或者函数,找不到就报错。所以,继承其实就是修改编译器访问权限与查找规则,而是在派生类里找不到时,就会去基类里面查找,修改的是查找规则。


5  派生类的默认成员函数

回顾类的默认成员函数

        在类和对象中,我们说一个类有 6 大默认成员函数:


派生类的默认成员函数与普通类的默认成员函数的区别

        派生类与普通类最大的区别,就是派生类中有基类的一部分,初始化、拷贝构造/赋值、析构时不仅要考虑派生类自己,还要去考虑基类的那一部分。在派生类的默认成员函数中,我们只要始终将基类当做一个自定义类型,就可以写出派生类对应的默认成员函数。

派生类的构造函数

        派生类的构造函数主要是完成派生类的初始化工作。那么如果我们不写,编译器自动生成,其构造函数默认行为是什么呢?上面我们说把基类当成一个自定义类型,所以如果是编译器自动生成的构造函数对于自定义类型不做处理,自定义类型回去调用其构造函数,最重要的是会去调用基类的默认构造函数:

#inlcude <iostream>

using namespace std;

class Base
{
public:
	Base(int b = 1):_b(b) 
	{
		cout << "Base(int x = 1)" << endl;
	}

private:
	int _b;
};

class Deriv : public Base
{
public:
private:
	int _d;
};

int main()
{
	Deriv d;

	return 0;
}

运行结果:

Base(int x = 1)

而如果基类没有默认构造函数的话,就会报错:

#inlcude <iostream>

using namespace std;

class Base
{
public:
	Base(int b):_b(b) 
	{
		cout << "Base(int x)" << endl;
	}

private:
	int _b;
};

class Deriv : public Base
{
public:
private:
	int _d;
};

int main()
{
	Deriv d;

	return 0;
}

运行结果:

        那么如果基类中就是没有默认构造函数呢,我们该如何写出派生类的构造函数呢?这时候就要借助初始化列表来初始化派生类了,在初始化列表中我们会像初始化匿名对象一样来初始化基类中的成员:

#include <iostream>

using namespace std;

class Base
{
public:
	Base(int b):_b(b) 
	{
		cout << "Base(int x)" << endl;
	}

private:
	int _b;
};

class Deriv : public Base
{
public:
	Deriv(int b = 1, int d = 1)
		:Base(b)
		,_d(d)
	{}

private:
	int _d;
};

int main()
{
	Deriv d;

	return 0;
}

运行结果:

Base(int x)

派生类的拷贝构造函数

       我们依然把基类当做一个自定义类型,所以如果是编译器自动生成派生类的拷贝构造函数,那么对于自定义类型会完成浅拷贝,自定义类型会去调用其拷贝构造函数,最重要的是基类会去调用其拷贝构造函数:

#include <iostream>

using namespace std;

class Base
{
public:
	Base(int b):_b(b) 
	{
		cout << "Base(int x)" << endl;
	}

    Base(const Base& b)
    {
        _b = b._b;
        cout << "Base(const Base& b)" << endl;
    }

private:
	int _b;
};

class Deriv : public Base
{
public:
	Deriv(int b = 1, int d = 1)
		:Base(b)
		,_d(d)
	{}

private:
	int _d;
};

int main()
{
	Deriv d1;
    Deriv d2 = d1;

	return 0;
}

运行结果:

Base(int x)
Base(const Base& b)

        但是如果派生要完成深拷贝呢,此时我们该如何调用基类的拷贝构造函数呢?我们要显示调用基类的拷贝构造函数吗?实际上我们只要像派生类构造函数中一样,将派生类对象传递过去即可,因为基类的拷贝构造函数参数为 const Base& b,所以将派生类对象传递给基类的拷贝构造函数会发生切片,此时基类引用就只能看到派生类中属于基类的那一部分,此时就完成了基类拷贝构造函数的调用:

#include <iostream>

using namespace std;

class Base
{
public:
	Base(int b) :_b(b)
	{
		cout << "Base(int x)" << endl;
	}

	Base(const Base& b)
	{
		_b = b._b;
		cout << "Base(const Base& b)" << endl;
	}

private:
	int _b;
};

class Deriv : public Base
{
public:
	Deriv(int b = 1, int d = 1)
		:Base(b)
		, _d(d)
	{}

	Deriv(const Deriv& d)
		:Base(d) //发生切片
	{
	    _d = d._d;
		_p = new int(d._d);
		//在析构函数中完成 delete 释放
	}

private:
	int _d;
	//假设有一个指针对象,要完成深拷贝
	int* _p;
};

int main()
{
	Deriv d1;
	Deriv d2 = d1;

	return 0;
}

运行结果:

Base(int x)
Base(const Base& b)

派生类的拷贝赋值重载函数

        派生类的拷贝赋值重载其实与拷贝构造函数是一样的,编译器自动生成的,内置类型会实现浅拷贝,自定义类型会去调用其拷贝赋值函数,对于基类对象会去调用其拷贝赋值重载函数。如果想要显示调用,只要将派生类对象传过去即可,因为会发生切片:

#include <iostream>

using namespace std;

class Base
{
public:
	Base(int b) :_b(b)
	{
		cout << "Base(int x)" << endl;
	}

	Base(const Base& b)
	{
		_b = b._b;
		cout << "Base(const Base& b)" << endl;
	}

	Base& operator=(const Base& b)
	{
		if (this != &b)
		{
			_b = b._b;
			cout << "operator=(const Base& b)" << endl;
		}

		return *this;
	}

private:
	int _b;
};

class Deriv : public Base
{
public:
	Deriv(int b = 1, int d = 1)
		:Base(b)
		, _d(d)
	{}

	Deriv(const Deriv& d)
		:Base(d) //发生切片
	{
		_d = d._d;
		_p = new int(d._d);
		//在析构函数中完成 delete 释放
	}

	Deriv& operator=(const Deriv& d)
	{
		if (this != &d)
		{
			//一定要加上 Base::,因为 operator= 构成隐藏,要不然会递归调用导致栈溢出
			Base::operator=(d);
			_d = d._d;
			_p = new int(d._d);
			//在析构函数中完成 delete 释放
		}

		return *this;
	}

private:
	int _d;
	//假设有一个指针对象,要完成深拷贝
	int* _p;
};

int main()
{
	Deriv d1;
	Deriv d2 = d1;
	Deriv d3;
	d3 = d1;

	return 0;
}

在这里只需要注意一点,那就是在调用基类的拷贝赋值重载函数时,一定要加上 Base::,因为基类与派生类的 operator= 构成隐藏,如果不加上 Base::,那么就会引发派生类拷贝赋值重载函数的递归调用导致栈溢出。


派生类的析构函数

        派生类的析构函数与前面看待基类的观点相同,编译器自动生成的对于内置类型不做处理,对于自定义类型会去调用其析构函数,更重要的是会去调用基类的析构函数:

#include <iostream>

using namespace std;

class Base
{
public:
	Base(int b) :_b(b)
	{
		cout << "Base(int b)" << endl;
	}

	Base(const Base& b)
	{
		_b = b._b;
	}

	Base& operator=(const Base& b)
	{
		if (this != &b)
		{
			_b = b._b;
		}

		return *this;
	}

	~Base()
	{
		cout << "~Base()" << endl;
	}

private:
	int _b;
};

class Deriv : public Base
{
public:
	Deriv(int b = 1, int d = 1)
		:Base(b)
		, _d(d)
	{}

	Deriv(const Deriv& d)
		:Base(d) //发生切片
	{
		_d = d._d;
		_p = new int(d._d);
		//在析构函数中完成 delete 释放
	}

	Deriv& operator=(const Deriv& d)
	{
		if (this != &d)
		{
			//一定要加上 Base::,因为 operator= 构成隐藏,要不然会递归调用导致栈溢出
			Base::operator=(d);
			_d = d._d;
			_p = new int(d._d);
			//在析构函数中完成 delete 释放
		}

		return *this;
	}

private:
	int _d;
	//假设有一个指针对象,要完成深拷贝
	int* _p;
};

int main()
{
	Deriv d1;

	return 0;
}

运行结果:

Base(int b)
~Base()

        那么要是派生类中有动态开辟资源需要释放呢?此时需要我们去显示写出派生类的构造函数,所以我们要去调用基类的析构函数:

#include <iostream>

using namespace std;

class Base
{
public:
	Base(int b) :_b(b)
	{
		cout << "Base(int b)" << endl;
	}

	Base(const Base& b)
	{
		_b = b._b;
	}

	Base& operator=(const Base& b)
	{
		if (this != &b)
		{
			_b = b._b;
		}

		return *this;
	}

	~Base()
	{
		cout << "~Base()" << endl;
	}

private:
	int _b;
};

class Deriv : public Base
{
public:
	Deriv(int b = 1, int d = 1)
		:Base(b)
		, _d(d)
	{}

	Deriv(const Deriv& d)
		:Base(d) //发生切片
	{
		_d = d._d;
		_p = new int(d._d);
		//在析构函数中完成 delete 释放
	}

	Deriv& operator=(const Deriv& d)
	{
		if (this != &d)
		{
			//一定要加上 Base::,因为 operator= 构成隐藏,要不然会递归调用导致栈溢出
			Base::operator=(d);
			_d = d._d;
			_p = new int(d._d);
			//在析构函数中完成 delete 释放
		}

		return *this;
	}

	~Deriv()
	{
		//调用基类的构造函数
		Base::~Base();
		delete _p;
	}

private:
	int _d;
	//假设有一个指针对象,要完成深拷贝
	int* _p;
};

int main()
{
	Deriv d1;

	return 0;
}

运行结果:

Base(int b)
~Base()
~Base()

看到运行结果,我们只创建了一个对象 d1,为什么调用了两次基类的析构函数?而且为什么调用基类的析构函数要 Base::~Base(),~Base 与 ~Deriv 不是不重名吗?难道会构成隐藏吗?其实编译器在处理基类和派生类的析构函数时,会做一个特殊处理:将派生类和基类的析构函数全部重命名为 destructor()所以其实基类与派生类的析构函数是会构成隐藏的。之所以这样处理,跟多态有关,会在多态一篇文章中进行讲解。至于为什么会调用两次基类的析构函数,是因为尽管我们显示实现了派生类的析构函数,编译器还是会自己调用基类的析构函数,这样处理主要是为了保证释放资源的正确性,所以我们不需要在派生类的析构函数中显示调用基类的析构函数:

#include <iostream>

using namespace std;

class Base
{
public:
	Base(int b) :_b(b)
	{
		cout << "Base(int b)" << endl;
	}

	Base(const Base& b)
	{
		_b = b._b;
	}

	Base& operator=(const Base& b)
	{
		if (this != &b)
		{
			_b = b._b;
		}

		return *this;
	}

	~Base()
	{
		cout << "~Base()" << endl;
	}

private:
	int _b;
};

class Deriv : public Base
{
public:
	Deriv(int b = 1, int d = 1)
		:Base(b)
		, _d(d)
	{}

	Deriv(const Deriv& d)
		:Base(d) //发生切片
	{
		_d = d._d;
		_p = new int(d._d);
		//在析构函数中完成 delete 释放
	}

	Deriv& operator=(const Deriv& d)
	{
		if (this != &d)
		{
			//一定要加上 Base::,因为 operator= 构成隐藏,要不然会递归调用导致栈溢出
			Base::operator=(d);
			_d = d._d;
			_p = new int(d._d);
			//在析构函数中完成 delete 释放
		}

		return *this;
	}

	~Deriv()
	{
		//不需要自己调用基类的析构函数
		//Base::~Base();
		delete _p;
	}

private:
	int _d;
	//假设有一个指针对象,要完成深拷贝
	int* _p;
};

int main()
{
	Deriv d1;

	return 0;
}

运行结果:

Base(int b)
~Base()

总结

        派生类的默认成员函数与普通类的默认成员函数是有区别的,主要是因为派生类还需要去考虑基类成员的资源初始化、拷贝与释放工作,但是我们只需要记住一点,将基类认为是一个自定义类型即可。

        编译器在初始化类成员时,是默认按照声明顺序初始化的,而在派生类中,基类默认是排在派生类其他成员前面的,所以在初始化时,会优先初始化基类这个整体对象,析构时也是默认优先析构基类这个整体对象,初始化与析构顺序如下:


不能被继承的类

        如果你想要一个类不被继承有两种方法:

(1)将基类的构造函数私有化,这是 C++11 前的主流方法。只要将基类的构造函数私有化,派生类实例化对象时,必须去调用基类的构造函数,但是基类构造函数在派生类中不可见,那就不能实例化出对象了。但是这个基类也没法实例化出对象了,不太推荐这种做法。

(2)在 C++11 之后提供了一个 final 关键字,只要在一个类后面加上 final 关键字,那该类就不可被继承。

//方法1
#include <iostream>

using namespace std;

class Base
{
public:
	Base(const Base& b)
	{
		_b = b._b;
	}

	Base& operator=(const Base& b)
	{
		if (this != &b)
		{
			_b = b._b;
		}

		return *this;
	}

	~Base()
	{
		cout << "~Base()" << endl;
	}

private:
	//构造函数私有化就不可被继承
	Base(int b) :_b(b)
	{
		cout << "Base(int b)" << endl;
	}

	int _b;
};

class Deriv : public Base
{
public:
	Deriv(int b = 1, int d = 1)
		:Base(b)
		, _d(d)
	{}

	Deriv(const Deriv& d)
		:Base(d) //发生切片
	{
		_d = d._d;
		_p = new int(d._d);
		//在析构函数中完成 delete 释放
	}

	Deriv& operator=(const Deriv& d)
	{
		if (this != &d)
		{
			//一定要加上 Base::,因为 operator= 构成隐藏,要不然会递归调用导致栈溢出
			Base::operator=(d);
			_d = d._d;
			_p = new int(d._d);
			//在析构函数中完成 delete 释放
		}

		return *this;
	}

	~Deriv()
	{
		//不需要自己调用基类的析构函数
		//Base::~Base();
		delete _p;
	}

private:
	int _d;
	//假设有一个指针对象,要完成深拷贝
	int* _p;
};

int main()
{
	Deriv d1;

	return 0;
}

//方法2
#include <iostream>

using namespace std;

class Base final
{
public:
	Base(int b) :_b(b)
	{
		cout << "Base(int b)" << endl;
	}

	Base(const Base& b)
	{
		_b = b._b;
	}

	Base& operator=(const Base& b)
	{
		if (this != &b)
		{
			_b = b._b;
		}

		return *this;
	}

	~Base()
	{
		cout << "~Base()" << endl;
	}

private:
	int _b;
};

class Deriv : public Base
{
public:
	Deriv(int b = 1, int d = 1)
		:Base(b)
		, _d(d)
	{}

	Deriv(const Deriv& d)
		:Base(d) //发生切片
	{
		_d = d._d;
		_p = new int(d._d);
		//在析构函数中完成 delete 释放
	}

	Deriv& operator=(const Deriv& d)
	{
		if (this != &d)
		{
			//一定要加上 Base::,因为 operator= 构成隐藏,要不然会递归调用导致栈溢出
			Base::operator=(d);
			_d = d._d;
			_p = new int(d._d);
			//在析构函数中完成 delete 释放
		}

		return *this;
	}

	~Deriv()
	{
		//不需要自己调用基类的析构函数
		//Base::~Base();
		delete _p;
	}

private:
	int _d;
	//假设有一个指针对象,要完成深拷贝
	int* _p;
};

int main()
{
	Deriv d1;

	return 0;
}

 运行结果:


6  继承与友元

        在类和对象中,有一种方式可以突破访问限定符限制,那就是友元,如果一个函数或者一个类是另一个类的友元,那么友元函数就可以访问类中的私有或者保护成员了:

#include <iostream>

using namespace std;

class Base
{
	friend void Func(const Base& b);
public:
	Base(int b) :_b(b)
	{
		cout << "Base(int b)" << endl;
	}

	Base(const Base& b)
	{
		_b = b._b;
	}

	Base& operator=(const Base& b)
	{
		if (this != &b)
		{
			_b = b._b;
		}

		return *this;
	}

	~Base()
	{
		cout << "~Base()" << endl;
	}

private:
	int _b;
};

void Func(const Base& b)
{
	cout << b._b << endl;
}

int main()
{
    Func(Base(1));

    return 0;
}

运行结果:

Base(int b)
1
~Base()

那么这种友元关系在继承中可不可以被继承呢?答案是不能的,在现实生活中是可以理解的,也就是你父母的朋友并不会是你的朋友,这种友元关系是不会被继承的:

#include <iostream>

using namespace std;

class Deriv;

class Base
{
	friend void Func(const Base& b, const Deriv& d);
public:
	Base(int b) :_b(b)
	{
		cout << "Base(int b)" << endl;
	}

	Base(const Base& b)
	{
		_b = b._b;
	}

	Base& operator=(const Base& b)
	{
		if (this != &b)
		{
			_b = b._b;
		}

		return *this;
	}

	~Base()
	{
		cout << "~Base()" << endl;
	}

private:
	int _b;
};

class Deriv : public Base
{
public:
	Deriv(int b = 1, int d = 1)
		:Base(b)
		, _d(d)
	{}

	Deriv(const Deriv& d)
		:Base(d) //发生切片
	{
		_d = d._d;
		_p = new int(d._d);
		//在析构函数中完成 delete 释放
	}

	Deriv& operator=(const Deriv& d)
	{
		if (this != &d)
		{
			//一定要加上 Base::,因为 operator= 构成隐藏,要不然会递归调用导致栈溢出
			Base::operator=(d);
			_d = d._d;
			_p = new int(d._d);
			//在析构函数中完成 delete 释放
		}

		return *this;
	}

	~Deriv()
	{
		//不需要自己调用基类的析构函数
		//Base::~Base();
		delete _p;
	}

private:
	int _d;
	//假设有一个指针对象,要完成深拷贝
	int* _p;
};

void Func(const Base& b, const Deriv& d)
{
	cout << "Base: " << b._b << endl;
	cout << "Deriv: " << d._d << endl;
}

int main()
{
	Func(Base(1), Deriv());

	return 0;
}

运行结果:


7  继承与静态成员

        在继承特性中,基类的非静态成员变量会在基类和派生类中各自存在一份;但是静态成员变量只会在基类和所有派生类中存在一份,也就是不管有多少派生类,基类与派生类始终共享这一个静态成员变量

#include <iostream>

using namespace std;

class Base
{
public:
	int _b;
	static int _s;
};

class Deriv : public Base
{
private:
	int _d;
};

int Base::_s = 1;

int main()
{
	Base b;
	Deriv d;

	//非静态成员变量地址不同
	cout << "&b._b: " << "0x" << hex << & b._b << endl;
	cout << "&d._b: " << "0x" << hex << & d._b << endl;

	//静态成员变量地址相同
	cout << "b._s: " << "0x" << hex << &b._s << endl;
	cout << "d._s: " << "0x" << hex << &d._s << endl;

	return 0;
}

C++ 中可以使用 hex 全局函数来修改后续整形数字的显示方式,将后续显示为 16 进制,函数原型为:

可以看到其会返回一个 ios_base 类,而 iostream 其实就是继承了 ios_base 这个类:

而在 ostream 类重载了 << 这一符号,也就是 oerator<<,所以就可以通过 hex 来修改 ios_base 类的输出方式,从而使后面的整形输出为 16 进制,如果想要改回 10 进制,只需要使用 dec 函数对象就可以了:

运行结果:

&b._b: 0x0046F7E4
&d._b: 0x0046F7D4
&b._s: 0x00515018
&d._s: 0x00515018

可以看到静态成员变量的地址在基类与派生类中是相同的,也就验证了基类与派生类共用一个静态成员变量。


8  多继承与菱形继承

继承模型

单继承

        单继承模型就是派生类只有一个直接父类就是单继承模型:


多继承

        多继承与单继承相反,当一个派生类有多个基类时,就称之为多继承:

#include <iostream>

using namespace std;

class Base1
{
public:
	Base1(int b = 1) :_b1(b)
	{
		cout << "Base1(int b = 1)" << endl;
	}

private:
	int _b1;
};

class Base2
{
public:
	Base2(int b = 1) :_b2(b)
	{
		cout << "Base2(int b = 1)" << endl;
	}

private:
	int _b2;
};

//用 , 隔开多个基类就可以实现多继承
//先继承谁,那么哪个基类就先被声明
class Deriv : public Base2, public Base1
{
private:
	int _d;
};

int main()
{
	Deriv d;

	return 0;
}

运行结果:

Base2(int b = 1)
Base1(int b = 1)

在多继承中需要注意的一点就是,先继承哪个基类,那么哪个基类就先被声明,就先调用哪个基类的构造函数。


菱形继承

        菱形继承是在多继承基础上引申出来的一种特殊情况:

菱形继承就是指一个类 Deriv3 多继承的至少两个父类 Deriv1、Deriv2 又同时继承了同一个父类 Base,此时在这个类中就会有多份 Base,那么多份 Base 之间就会产生数据冗余与二义性,编译器就不知道使用的是哪个 Base 了。

        为了解决菱形继承产生的数据冗余和二义性问题,我们可以采用虚继承的方式来解决该问题:

#include <iostream>

using namespace std;

class Base
{
public:
	Base(int b = 1) :_b(b)
	{
		cout << "Base(int b = 1)" << endl;
	}

private:
	int _b;
};

//这里必须为虚继承
class Deriv1 : virtual public Base 
{
public:
	Deriv1(int d = 1) :_d1(d)
	{
		cout << "Deriv1(int d = 1)" << endl;
	}

private:
	int _d1;
};

//用 , 隔开多个基类就可以实现多继承
//先继承谁,那么哪个基类就先被声明
class Deriv2 : virtual public Base
{
public:
	Deriv2(int d = 2) :_d2(d)
	{
		cout << "Deriv2(int d = 1)" << endl;
	}

private:
	int _d2;
};

class Deriv3 : public Deriv1, public Deriv2
{
public:
	Deriv3(int d = 3) :_d3(d)
	{
		cout << "Deriv3(int d = 3)" << endl;
	}
private:
	int _d3;
};

int main()
{
	Deriv3 d;

	return 0;
}

运行结果:

Base(int b = 1)
Deriv1(int d = 1)
Deriv2(int d = 1)
Deriv3(int d = 3)

虚继承的原理

        我们首先来看下面这段代码:

#include <iostream>
#include <string>
#include <vector>

using namespace std;

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 = 4;
	d._c = 5;
	d._d = 6;
	d._a = 1;

	B* p1 = &d;
	C* p2 = &d;

	p1->_a++;
	p2->_a++;

	return 0;
}

这段代码中,B、C 类共同虚继承了同一个父类 A,然后 D 又继承了 B、C,所以这就构成了菱形继承。如果 B、C 不虚继承的话,就会变成如下情况:

        那么如果B、C变成虚继承,其内存分布情况如下图所示:

我们可以看到,其实虚继承之后,原来会存两份的 A 成员变量,不管是切片后后的 B,还是切片后的 C,还是 D,其中的 A 成员变量 _a 都会变成一份,在 vs 中是存到了 B、C、D 自己成员变量的最下面,当然有的编译器也可能存在最上面。这样就解决了 A 成员变量数据冗余与二义性的问题。

        但是在 d 对象中不仅存在 _a、_b、_c、_d 这四个成员变量,还多出了 003f0d14、00f30d20 (vs 中是小端,高位放在高地址处)两个整形数字,这个是什么呢?其实就是地址,他们会分别指向两段 8 字节空间,在其中放的就是偏移量,是当前地址相对于 _a 成员的偏移量。比如:0x00CFFA5C 地址处放的内容为 00f30d14,而 00f30d14 地址开始向后的 8 个字节存放的内容为 20(十六进制 14 转十进制为 20),所以从 0x00CFFA5C 地址处向后偏移 20 字节,也就是 0x00CFFA5C + 0x14 = 0x00CFFA70,正好就是 _a 的起始地址,所以就找到 _a 了。所以虚继承之后其内存分布其实是这样的:

        但是 D 中 _a 不就在 _d 的下面吗,直接找不就可以找到了吗?但是还有切片的场景啊,就比如上面代码中的 p1 和 p2,他们只能看到属于他们自己的那一部分啊,根本不知道 _d 在哪,所以他们就可以根据基类的切片部分中的偏移量来找到 _a 的内存地址,从而找到 _a。所有偏移量是给切片场景使用的。


9  继承与组合

        上面我们说继承是代码复用的一种方式,其实还有一种代码复用的方式,就是组合。但是二者的本质是不同的,继承是 "is-a" 的关系,组合是 "has-a" 的关系。就比如 Student 类继承了 Person 类,我们可以说学生是一个人,但是不能说学生中有一个人,很显然这里是 "is-a" 的关系,所以这里必须选用继承。再比如我们之前使用适配器模式实现了 stack 和 queue,适配器模式本身就是一种组合,因为我们可以说 stack 与 queue 中有 vector,是一种 "has-a" 的关系,所以我们就可以使用组合来实现 stack。当然,stack 也可以是一个 vector,也可以使用继承来实现:

#include <iostream>
#include <string>
#include <vector>

using namespace std;

// stack和vector的关系,既符合is-a,也符合has-a
template<class T>
class stack : public std::vector<T>
{
public:
	void push(const T& x)
	{
		// 基类是类模板时,需要指定⼀下类域,
		// 因为stack<int>实例化时,也实例化vector<int>了
		// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
		vector<T>::push_back(x);
	}
	void pop()
	{
		vector<T>::pop_back();
	}
	const T& top()
	{
		return vector<T>::back();
	}
	bool empty()
	{
		return vector<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;
}

如果可以同时使用继承或者组合的场景下,推荐优先使用组合,因为继承会导致类爆炸和耦合度过高。比如,如果猫继承动物,狗继承动物,后续新增宠物类,可能需要重构整个继承链,维护成本高。但是组合整体类和部分类耦合度低,部分类的修改,不会影响整体类;部分类可以被多个整体类复用。一句话就是继承会增加代码的耦合度,而组合相对于继承来说,耦合度并没有那么高。但是如果是要实现多态的话,那就必须要使用继承才可以了。


总结

        本篇文章我们讲解了继承这一语法。继承有 public、protected、private 三种继承方式,最常用的就是 public 继承。在继承中有一个特别重要的特殊规则,那就是如果子类对象赋值给基类的指针或者引用,就会发生赋值兼容转换,也叫做切片或者切割,这一点特别重要,在多态场景中也会用到。再来就是基类与子类如果存在同名成员,那么就会构成隐藏;如果使用的是派生类的成员,那么编译器会优先使用派生类的成员。

        在继承场景中,派生类的默认成员与普通类是不同的,因为派生类中还有基类,在这里我们只需要记住一点,那就是把基类当做一个自定义类型。最重要的记住基类和派生类的析构函数会被编译器统一重命名为 destructor,所以基类与派生类的析构函数构成隐藏,所以为了防止基类资源的泄露,在派生类的析构函数中会自动调用基类的析构函数。

        友元关系是不能够被继承的,同时静态成员变量在基类和所有派生类中仅会存在一份。C++ 中支持多继承,但是如果是菱形继承,就需要 virtual 关键字来实现虚继承。

        最后,继承是代码复用的一种方式,是 "is-a" 的关系。但是还有一种代码复用的方式—组合,组合是 "has-a" 的关系。如果可以同时用继承和组合,优先使用组合,因为继承会大大加重类与类之间的耦合。

更多推荐