C++深入浅出(八)—— 继承


1. 继承的概念及定义

🍑 继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,进而产生新的类,被称派生类。

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。

举个例子:假设我现在要设计一个校园管理系统,那么肯定会设计很多角色类,比如学生、老师、保安、保洁等等之类的。

设计好以后,我们发现,有些数据和方法是每个角色都有的,而有些则是每个角色独有的。

在这里插入图片描述

像上面共同拥有的数据和方法我们可以重新设计一个类 Person,然后让 StudentTeacher 去继承它,如下:

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "name:" << _tell << endl;
		cout << "name:" << _address << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "Edison"; // 姓名
	string _tell = "18877889966"; // 电话
	string _address = "北京北京"; // 住址
	int _age = 18; // 年龄
};

class Student : public Person
{
protected:
	int _stuId; // 学号
};

class Teacher : public Person
{
protected:
	int _workId; // 工号
};

继承后,父类的 Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了 StudentTeacher 复用了 Person 的成员。

在这里插入图片描述

🍑 继承的定义

🍅 定义格式

格式如下: Person 是父类,也称作基类;Student 是子类,也称作派生类。

在这里插入图片描述

🍅 继承关系和访问限定符

继承方式如下:

在这里插入图片描述

访问限定符如下:

在这里插入图片描述

🍅 继承基类成员访问方式的变化

基类中,不同的访问限定符修饰的成员,以不同的继承方式被继承到派生类当中后,该成员最终在派生类当中的访问方式将会发生变化。

在这里插入图片描述

对于上面的表格,其实不用去死记硬背,我们进行一下总结:

(1)基类 private 成员在派生类中无论以什么方式继承都是不可见的。(这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它)

(2)基类 private 成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为 protected。(可以看出保护成员限定符是因继承才出现的)

(3)基类的私有成员在子类都是不可见。

基类的其他成员在子类的访问方式 = = M i n ( 成员在基类的访问限定符,继承方式 ) 基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式) 基类的其他成员在子类的访问方式==Min(成员在基类的访问限定符,继承方式)

(三种访问限定符的权限大小为:public > protected > private

(4)使用关键字 class 时默认的继承方式是 private,使用 struct 时默认的继承方式是 public ,不过最好显示的写出继承方式。

重点: 其实,在实际运用中一般使用都是 public 继承,几乎很少使用 protetced/private 继承,也不提倡使用 protetced/private 继承,因为 protetced/private 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

2. 基类和派生类对象赋值转换

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用

(1)子类对象可以赋值给父类对象

//基类
class Person
{
public:
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};

//派生类
class Student : public Person
{
public:
	int _id;
};

int main()
{
	Person p;
	Student s;

	s._name = "张三";
	s._sex = "男";
	s._age = 20;
	s._id = 8888;
	
	p = s; // 子类对象赋值给父类对象

	return 0;
}

通过调式可以看到,为什么没有把 id 赋值过去呢?

在这里插入图片描述

这里有个形象的说法叫切片或者切割,相当于把派生类中父类那部分切来赋值过去,如图所示:

在这里插入图片描述

(2)子类对象可以赋值给父类指针

//基类
class Person
{
public:
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};

//派生类
class Student : public Person
{
public:
	int _id;
};


int main()
{
	Student s;

	s._name = "张三";
	s._sex = "男";
	s._age = 20;
	s._id = 8888;

	Person* p = &s;

	return 0;
}

可以看到,当父类对象是一个指针的时候,照样可以赋值过去:

在这里插入图片描述

子类对象赋值给父类指针切片图示:

在这里插入图片描述

(3)子类对象可以赋值给父类引用

//基类
class Person
{
public:
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};

//派生类
class Student : public Person
{
public:
	int _id;
};

int main()
{
	Student s;

	s._name = "张三";
	s._sex = "男";
	s._age = 20;
	s._id = 8888;

	Person& rp = s;

	return 0;
}

可以看到,当父类对象是一个引用的时候,也可以赋值过去:

在这里插入图片描述

子类对象赋值给父类引用切片图示:

在这里插入图片描述

(4)父类对象不能赋值给子类对象

//基类
class Person
{
public:
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};

//派生类
class Student : public Person
{
public:
	int _id;
};

int main()
{
	Student s;
	Person p;

	s = p;

	return 0;
}

编译会报错:

在这里插入图片描述

(5)父类的指针或者引用可以通过强制类型转换赋值给子类的指针或者引用。

//基类
class Person
{
public:
	string _name = "Edison"; // 姓名
	string _sex = "男"; // 性别
	int _age = 20; // 年龄
};

//派生类
class Student : public Person
{
public:
	int _id;
};

int main()
{
	Student s;
	
	Person* pp = &s;
	Student* ps1 = (Student*)pp;
	ps1->_id = 10;

	return 0;
}

可以看到这种情况下是可以赋值的:

在这里插入图片描述

但是必须是父类的指针是指向子类对象时才是安全的。这里父类如果是多态类型,可以使用 RTTI(Run-Time Type Information) 的 dynamic_cast 来进行识别后进行安全转换。

3. 继承中的作用域

在继承体系中基类和派生类都有独立的作用域,如果子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。

代码示例:Student 的 _num 和 Person 的 _num 构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆。

// 基类
class Person
{
protected:
	string _name = "Edison"; // 姓名
	int _num = 555; // 身份证号
};

// 派生类
class Student : public Person
{
public:
	void Print()
	{
		cout << "姓名:" << _name << endl;
		cout << "学号:" << _num << endl;
	}
protected:
	int _num = 888; // 学号
};

int main()
{
	Student s1;
	s1.Print();

	return 0;
}

运行可以看到,访问的是子类中的 _num(类似于局部优先的原则)

在这里插入图片描述

那么如果我想访问父类中的 _num 呢?可以使用 基类::基类成员 显示的去访问:

// 基类
class Person
{
protected:
	string _name = "Edison"; // 姓名
	int _num = 555; // 身份证号
};

// 派生类
class Student : public Person
{
public:
	void Print()
	{
		cout << "姓名:" << _name << endl;
		cout << " 身份证号:" << Person::_num << endl;
	}
protected:
	int _num = 888; // 学号
};

int main()
{
	Student s1;
	s1.Print();

	return 0;
}

可以看到,此时就是访问的父类中的 _num

在这里插入图片描述

还有一点需要注意的是:如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

// 基类
class A
{
public:
	void fun()
	{
		cout << "A::func()" << endl;
	}
};

// 派生类
class B : public A
{
public:
	void fun(int i)
	{
		cout << "B::func()" << endl;
		cout << "func(int i)->" << i << endl;
	}
};

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

	return 0;
}

可以看到,默认是去调用子类的 fun() 函数,因为成员函数满足函数名相同就构成隐藏。

在这里插入图片描述

如果想调用父类的 fun() 还是需要指定作用域

// 基类
class A
{
public:
	void fun()
	{
		cout << "A::func()" << endl;
	}
};

// 派生类
class B : public A
{
public:
	void fun(int i)
	{
		cout << "B::func()" << endl;
		cout << "func(int i)->" << i << endl;
	}
};

int main()
{
	B b;
	b.A::fun();

	return 0;
}

运行可以看到,此时就是调用父类中的 fun()

在这里插入图片描述

注意:B 中的 fun 和 A 中的 fun 不是构成函数重载,而是隐藏!函数重载的要求是在同一作用域里面!!!

另外,在实际中在继承体系里面最好不要定义同名的成员。

4. 派生类的默认成员函数

派生类一共有 6 个默认成员函数,“默认” 的意思就是指我们不写,编译器会变我们自动生成一个,如图所示:

在这里插入图片描述

这几个成员函数的生成规则如下:

(1)派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。(如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用)

(2)派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。

(3)派生类的赋值重载必须要调用基类的赋值重载完成基类的赋值。

(4)派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。(因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序)

(5)派生类对象初始化先调用基类构造再调派生类构造。

(6)派生类对象析构清理先调用派生类析构再调基类的析构。

基类成员函数代码如下:

// 基类
class Person
{
public:
    // 构造函数
    Person(const char* name = "Edison")
        : _name(name)
    {
        cout << "Person()" << endl;
    }

    // 拷贝构造
    Person(const Person& p)
        : _name(p._name)
    {
        cout << "Person(const Person& p)" << endl;
    }

    // 赋值重载
    Person& operator=(const Person& p)
    {
        cout << "Person operator=(const Person& p)" << endl;
        if (this != &p)
            _name = p._name;

        return *this;
    }

    // 析构函数
    ~Person()
    {
        cout << "~Person()" << endl;
    }
protected:
    string _name; // 姓名
};

派生类成员函数代码如下:

// 派生类
class Student : public Person
{
public:
    // 构造函数
    Student(const char* name, int num)
        : Person(name)
        , _num(num)
    {
        cout << "Student()" << endl;
    }

    // 拷贝构造
    Student(const Student& s)
        : Person(s)
        , _num(s._num)
    {
        cout << "Student(const Student& s)" << endl;
    }

    // 赋值重载
    Student& operator = (const Student& s)
    {
        cout << "Student& operator= (const Student& s)" << endl;
        if (this != &s)
        {
            Person::operator =(s);
            _num = s._num;
        }
        return *this;
    }

    // 析构函数
    ~Student()
    {
        cout << "~Student()" << endl;
    }
protected:
    int _num; //学号
};

注意:子类析构函数不需要去调用父类的析构,因为父子类的析构函数构成隐藏关系!后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成 destrutor(),所以父类析构函数不加 virtual 的情况下,子类析构函数和父类析构函数构成隐藏关系。

所以为了保证析构顺序(先子后父),子类析构函数完成后会自动调用析构函数,所以一般不用显示的去写父类的析构!

基类和派生类的构造图示:

在这里插入图片描述

5. 继承与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员,只能访问自己的私有和保护成员。

下面代码中,Display 函数是基类 Person 的友元,但是 Display 函数不是派生类 Student 的友元,也就是说 Display 函数无法访问派生类 Student 当中的私有和保护成员。

class Student;

class Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};

class Student : public Person
{
protected:
	int _stuNum; // 学号
};

void Display(const Person& p, const Student& s)
{
	cout << p._name << endl; // 可以访问
	cout << s._stuNum << endl; // 无法访问
}

int main()
{
	Person p;
	Student s;
	Display(p, s);

	return 0;
}

可以看到运行会报错:

在这里插入图片描述

如果想让 Display 函数也能够访问派生类 Student 的私有和保护成员,只需要在派生类 Student 当中进行友元声明。

class Student;

class Person
{
public:
	friend void Display(const Person& p, const Student& s); // 声明Display是Person的友元
protected:
	string _name; // 姓名
};

class Student : public Person
{
public:
	friend void Display(const Person& p, const Student& s); // 声明Display是Student的友元
protected:
	int _stuNum; // 学号
};

void Display(const Person& p, const Student& s)
{
	cout << p._name << endl; // 可以访问
	cout << s._stuNum << endl; // 可以访问
}

int main()
{
	Person p;
	Student s;
	Display(p, s);

	return 0;
}

6. 继承与静态成员

如果基类中定义了 static 静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个 static 成员实例 。

下面代码中,在基类 Person 当中定义了静态成员变量 _count,派生类 StudentGraduate 继承了 Person,但是,在整个继承体系里面只有一个静态成员。

// 基类
class Person
{
public:
	Person() { ++_count; }
protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。
};

// 静态成员在类外面定义
int Person::_count = 0; 

// 派生类
class Student : public Person
{
protected:
	int _stuNum; // 学号
};

// 派生类
class Graduate : public Student
{
protected:
	string _seminarCourse; // 研究科目
};

int main()
{
	Student s1;
	Student s2;
	Student s3;
	Graduate s4;
	Person s;

	cout << " 人数 :" << Person::_count << endl;
	cout << " 人数 :" << Student::_count << endl;
	cout << " 人数 :" << s4._count << endl;

	return 0;
}

我们定义了 5 个对象,那么每定义一个对象都会去调用一次 ++_count,打印以后可以看到,这几个对象里面的 _count 都是一样的:

在这里插入图片描述

同时,我们还可以打印一下地址,可以看到也是同一个:

在这里插入图片描述

总结:关于父类中的静态成员,子类继续下来以后都是同一个,类似于 “传家宝”。

7. 复杂的菱形继承及菱形虚拟继承

🍑 单继承

一个子类只有一个直接父类时称这个继承关系为单继承。

在这里插入图片描述

🍑 多继承

一个子类有两个或以上直接父类时称这个继承关系为多继承。

在这里插入图片描述

🍑 菱形继承

菱形继承是多继承的一种特殊情况。

在这里插入图片描述

🍑 菱形继承的问题

菱形继承有数据冗余和二义性的问题。

下面代码是一个菱形继承中,当我们实例化 Assistant 对象 a 以后,会有二义性无法明确知道访问的是哪一个。

// 基类
class Person
{
public:
	string _name; // 姓名
	int _a[10000];
};


class Student : virtual public Person
{
protected:
	int _num; //学号
};


class Teacher : virtual public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

int main()
{
	// 这样会有二义性无法明确知道访问的是哪一个
	Assistant a;
	a._name = "Edison";

	return 0;
}

Assistant 类继承了 StudentTeacher,而 StudentTeacher 当中都继承了 Person,因此 StudentTeacher 当中都有 _name 成员,若是直接访问 _a 对象的 _name 成员会出现访问不明确的报错。

那么我们可以显示指定访问哪个父类的成员可以解决二义性问题。

// 基类
class Person
{
public:
	string _name; // 姓名
	int _a[10000];
};


class Student : virtual public Person
{
protected:
	int _num; //学号
};


class Teacher : virtual public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

int main()
{
	// 显示指定访问哪个父类的成员
	a.Student::_name = "Edison";
	a.Teacher::_name = "Harry";

	return 0;
}

虽然可以解决二义性的问题,但仍然不能解决数据冗余的问题。因为在 Assistant 的对象在 Person 成员始终会存在两份。

在这里插入图片描述

🍑 虚拟继承

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。

在这里插入图片描述

如下的继承关系,在 StudentTeacher 的继承 Person 时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其它地方去使用。

class Person
{
public:
	string _name; // 姓名
};

class Student : virtual public Person
{
protected:
	int _num; //学号
};

class Teacher : virtual public Person
{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

int main()
{
	Assistant a;
	a._name = "Edison";

	return 0;
}

我们可以直接打印看下 Assistant 对象的 _name 成员,访问到的都是同一个结果,解决了二义性的问题。

在这里插入图片描述

当我们打印 _name 成员的地址时,显示的也是同一个地址,解决了数据冗余的问题。

在这里插入图片描述

🍑 菱形虚拟继承的原理

为了研究菱形虚拟继承原理,我们先研究虚拟继承体系。

🍅 菱形继承的原理

如图,是一个菱形继承的模型图,那么它的原理到底是什么呢?

在这里插入图片描述

下面是一个简单的测试菱形继承的代码:

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;
}

我们通过内存窗口,看到菱形继承 d 对象的内存对象成员分布如下:

在这里插入图片描述

从上面的内存分布出中可以看出,d 对象当中含有两个 _a 成员,所以这就是菱形继承导致了二义性和数据冗余的原因。

🍅 菱形虚拟继承的原理

如图,是一个菱形虚拟继承的模型图,那么它的原理到底是什么呢?

在这里插入图片描述

下面是一个简单的测试菱形虚拟继承的代码:

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 对象的内存对象成员分布如下:

在这里插入图片描述

这里可以分析出 D 对象中将 A 放到了对象组成的最下面,这个 A 同时属于 BC ,那么 BC 如何去找到公共的 A 呢?

这里是通过了 BC 的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。

虚基表中包含两个数据,第一个数据是为多态的虚表预留的存偏移量的位置,第二个数据就是当前类对象位置距离公共虚基类的偏移量。通过偏移量可以找到下面的 A

在这里插入图片描述

我相信大家肯定会有疑问:为什么 DBC 部分要去找属于自己的 A

那么大家看看当下面的赋值发生时,c 是不是要去找出 B/C 成员中的 A 才能赋值过去?

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	
	B = d;
	C c = d;
	
	return 0;
}

如果将 D 类对象赋值给 C 类对象,在这个切片过程中,就需要通过虚基表中的第二个数据找到公共虚基类 A 的成员,得到切片后该 C 类对象在内存中仍然保持这种分布情况。

切片后,该 C 类对象当中各个成员在内存当中的分布情况如下:

C 类对象无法知道也不关心自己指向的是谁,但是和上面一样,都是先找到虚基表中的偏移量,然后通过偏移量计算 A 类成员的位置。

在这里插入图片描述

下图是 Person 关系菱形虚拟继承的原理解释:

在这里插入图片描述

8. 继承的总结和反思

很多人说 C++ 语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。

多继承可以认为是 C++ 的缺陷之一,很多后来的 OO 语言都没有多继承,如 Java。

🍑 继承和组合

public 继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象。

而组合是一种 has-a 的关系。假设 B 组合了 A,每个 B 对象中都有一个 A 对象。

举个例子: 轿车和奔驰就构成 is-a 的关系,所以可以使用继承。

// 车类
class Car
{
protected:
	string _colour = "黑色"; // 颜色
	string _num = "川A66688"; // 车牌号
};

// 奔驰
class Benz : public Car
{
public:
	void Drive()
	{
		cout << "好开-操控" << endl;
	}
};

再举个例子:汽车和轮胎之间就是 has-a 的关系,它们之间则适合使用组合。

// 轮胎
class Tire {
protected:
	string _brand = "Michelin"; // 品牌
	size_t _size = 17; // 尺寸

};

// 汽车
class Car {
protected:
	string _colour = "黑色"; // 颜色
	string _num = "川A66688"; // 车牌号
	Tire _t; // 轮胎
};

注意:如果两个类既适合 is-a 关系,又适合 has-a 关系,那么优先建议使用对象组合,而不是类继承。

为什么呢?原因如下:

  • 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为 白箱复用(white-box reuse)。术语 “白箱” 是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。

  • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为 黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以 “黑箱” 的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

  • 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系既可以用继承,又可以用组合,优先考虑用组合。

注意:模块与模块之间的关系应该是遵循 低耦合,高内聚

9. 关于继承的考点

(1)什么是菱形继承?菱形继承的问题是什么?

菱形继承:菱形继承是多继承一种特殊的继承方式。

如下图所示,可以看到,两个派生类继承同一个基类,同时两个派生类又作为基本继承给同一个派生类。这种继承形如菱形,故又称为菱形继承。

在这里插入图片描述

菱形继承的问题: 菱形继承主要有数据冗余和二义性的问题。

由于最底层的派生类继承了两个基类,同时这两个基类有继承的是一个基类,故而会造成最顶部基类的两次调用,会造成数据冗余及二义性问题。

如下图所示,在 Assistant 的对象中 Person 成员会有两份

在这里插入图片描述

(2)什么是菱形虚拟继承?它是如何解决数据冗余和二义性的?

虚拟继承: 在继承列表中基类继承权限前加上 virtual 关键字

以单继承为例,在继承列表中基类继承权限前加上 virtual 关键字后,子类的对象模型与普通继承有所不同。虚拟继承而来的子类中第一部分为一个指向偏移量表格(虚基表)的指针,在虚基表中存储着继承自基类的成员在子类中存储的位置,以子类对象首地址的偏移量来表示;在子类对象构造时,由编译器自动填充虚基表指针和虚基表的内容,以此来确定基类的存放位置。

需要注意的是,在单继承中,我们并不需要采用虚拟继承,虚拟继承一般只用在解决菱形继承中存在的问题。

采用菱形虚拟继承,使菱形继承中最顶层基类的成员在最底层对象中只存储一份,这样就解决了访问的二义性和数据冗余的问题。

在这里插入图片描述

菱形虚拟继承的对象模型与多继承的对象模型基本一致,继承的基类成员依照在继承列表中的先后次序排在子类新增成员之前,不同的是,由于 StudentTeacher 虚拟继承自同一类,各自都在最开始多存储了虚基表指针,通过虚基表中的内容找到由最顶层基类继承下来的成员,且这些成员只有一份。

在这里插入图片描述

(3)继承和组合的区别?什么时候用继承?什么时候用组合?

继承是一种 is-a 的关系,而组合是一种 has-a 的关系。

如果两个类之间是 is-a 的关系,使用继承;如果两个类之间是 has-a 的关系,则使用组合;

如果两个类之间的关系既可以是 is-a 的关系,也可以是 has-a 的关系,则优先考虑使用组合。

  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 17
    评论
C++ 默认构造函数是在没有显式定义构造函数的情况下自动生成的特殊成员函数。它通常用于在创建对象时进行初始化操作。默认构造函数无参数,不接受任何实参。当我们通过调用类的构造函数来创建对象时,如果没有提供实参,则编译器会自动调用默认构造函数。 默认构造函数的作用是确保对象的所有成员变量都被正确初始化。例如,如果一个类有一个int类型的成员变量,那么在默认构造函数中,可以将该成员变量初始化为0。如果没有默认构造函数,当我们创建对象时,该成员变量可能会未被初始化,导致程序运行时出现意外结果。 另一个重要的地方是,当我们定义了类的其他构造函数时(比如有参数的构造函数),默认构造函数依然会被生成。这是因为在某些情况下,我们可能只想使用默认构造函数来创建对象,而不希望传递实参。此时,默认构造函数就能满足需求。当我们重载构造函数时,可以使用默认参数来实现默认构造函数的功能。 需要注意的是,默认构造函数在一些特殊情况下可能不会被生成。例如,如果我们显式定义了有参数的构造函数,但没有提供默认构造函数,那么编译器将不会自动生成默认构造函数,这意味着我们不能再使用无参的方式来创建对象。 总之,理解C++默认构造函数的作用和用法对于编写高质量的代码至关重要。它可以帮助我们确保对象的正确初始化,并且在一些特殊情况下可以提供方便的使用方式。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Albert Edison

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值