C++——继承

目录

引言

继承的概念和定义

1.继承的概念

2.继承的定义

[2.1 继承的语法形式](#2.1 继承的语法形式)

[2.2 继承中类的叫法](#2.2 继承中类的叫法)

[2.3 继承后的子类成员访问权限](#2.3 继承后的子类成员访问权限)

基类与派生类的赋值转换

1.派生类对象赋值给基类对象

2.派生类对象的引用赋值给基类对象

3.派生类对象的指针赋值给基类对象

4.基类指针赋值给派生类指针

继承的作用域

1.同名变量

2.同名函数

派生类的默认成员函数

1.对象的构造和析构遵循特定的顺序

2.派生类构造函数调用基类构造函数

3.析构函数的特殊处理

4.拷贝构造与赋值重载必须调用基类的拷贝构造与赋值重载完成对基类的初始化

继承与友元

继承与静态成员

菱形继承与虚拟继承

1.菱形继承

[1.1 单继承](#1.1 单继承)

2.多继承

3.菱形继承

2.虚拟继承

结束语


引言

C++是一门功能强大的面向对象编程语言,其中继承是其面向对象特性的重要组成部分。今天我们就来学习一下,C++中的继承这一重要内容。

继承的概念和定义

1.继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用继承是类设计层次的复用

举个简单的例子:

比如说一个人,他具有年龄,姓名等个人信息,然后我们可以将这些信息整合为一个Person类,如果说我们还想要定义一个Student的类,这些学生当然也是人,因此我们可以复用Person这个类,然后再添加一些其他的信息,例如学号之类的。

2.继承的定义

2.1 继承的语法形式

继承的定义是通过在子类的声明中使用基类名,并加上冒号(:)和关键字public(或其他访问修饰符,如protected或private)来实现的。例如:

class BaseClass 
{  
    // 基类的成员  
};  
  
class DerivedClass : public BaseClass // 这里的public可以替换成protected或者private
{  
    // 子类的成员  
};

下面是一个简单的使用例子:

class Person
{
public:
	void Print()
	{
		cout << _name << endl;
		cout << _age << endl;
		cout << _height << endl;
	}
private:
	string _name = "Kana";	//姓名
	int _age = 18;			//年龄
	double _height = 1.50;	//身高
};

class Student :public Person
{
private:
	int _id = 233333;	//学号
	int _grade = 10;		//年级
};

可以通过监视窗口来看看:

显然,我们可以看到 Student类 继承了 Person类 的成员与函数。

2.2 继承中类的叫法

(1)子类(或派生类):这是指继承其他类(即父类)的类。子类可以使用父类的所有非私有属性和方法,同时也可以添加自己的属性和方法或重写父类的方法。

(2)父类(或基类):这是指被其他类(即子类)继承的类。父类提供了通用的属性和方法,这些可以被子类继承和使用。

2.3 继承后的子类成员访问权限

不同的继承方式产生的继承效果自然也不一样。

如下表所示:

|----------------|-----------------|-----------------|---------------|
| 类成员/继承方式 | public继承 | protected继承 | private继承 |
| 基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
| 基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
| 基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |

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

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

  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。

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

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

基类与派生类的赋值转换

在面向对象编程中,基类和派生类之间的赋值转换涉及到对象的类型兼容性和多态性。

1.派生类对象赋值给基类对象

派生类对象可以赋值给基类的对象。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。

相反,基类成员无法赋值给派生类成员,因为有些成员派生类有,而基类没有。

例如这样:

class Person 
{
public:
    Person(string name = "Kana", int age = 18, double height = 1.50):
        _name(name), 
        _age(age), 
        _height(height) 
        {
            // ...
        }

    void Print() const 
    {
        cout << _name << endl;
        cout << _age << endl;
        cout << _height << endl;
    }

private:
    string _name; // 姓名  
    int _age;     // 年龄  
    double _height; // 身高  
};

class Student : public Person 
{
public:
    Student(string name = "Kana", int age = 18, double height = 1.50, int id = 233333, int grade = 10)
        : Person(name, age, height), _id(id), _grade(grade) {}

    void Print() const 
    {
        // 首先调用基类的 Print 方法  
        Person::Print();
        // 然后打印学生特有的属性  
        cout << "ID: " << _id << endl;
        cout << "Grade: " << _grade << endl;
    }

private:
    int _id;    // 学号  
    int _grade; // 年级  
};

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

    // 尝试将 Student 对象切片为 Person 对象(不推荐,因为会丢失信息)  
    Person p = s;
    p.Print(); // 仅打印 Person 的信息(姓名、年龄、身高)  

    return 0;
}

输出结果为:

2.派生类对象的引用赋值给基类对象

我们可以将一个派生类对象的引用赋值给一个基类类型的引用,而不需要const修饰符。

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

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

int main()
{
	Student s;
	Person& p = s;
	p.Print();

	return 0;
}

输出结果为:

3.派生类对象的指针赋值给基类对象

派生类对象的指针可以赋值给基类对象的指针。

class Person
{
public:
    void Print() const
    {
        cout << "Person: Name = " << _name << ", Age = " << _age << endl;
    }

    Person(string name, int age) : _name(name), _age(age) {}

private:
    string _name;
    int _age;
};

class Student : public Person
{
public:
    void Print() const
    {
        cout << "Student: ID = " << _id << ", Grade = " << _grade << endl;
    }

    Student(string name, int age, int id, int grade) : Person(name, age), _id(id), _grade(grade) {}

private:
    int _id;    // 学号    
    int _grade; // 年级    
};

int main()
{
    Student student("Kana", 18, 12345, 10);

    // 将Student对象的指针赋值给Person对象的指针  
    Person* p = &student;

    p->Print();    

    // 如果要访问Student特有的成员,需要使用Student类型的指针或引用  
    Student* s = &student;
    s->Print();  

    return 0;
}

输出结果为:

4.基类指针赋值给派生类指针

在C++中,将基类指针直接强制转换为派生类指针是一种危险的做法,通常是不被推荐的,因为它违反了类型安全的原则,并且可能导致未定义行为,包括越界访问或访问无效内存。

	Person p;
	Student *s = (Student*) & p; // right

总结:

1.派生类对象可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。

2.基类对象不能赋值给派生类对象。

3.基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。

继承的作用域

继承的作用域决定了从基类继承到派生类的成员(包括变量和方法)的访问权限。

在C++的继承体系中,基类和派生类各自拥有独立的作用域。当派生类和基类中定义了同名的成员(无论是变量还是函数),派生类中的成员会"隐藏"或"重定义"基类中的同名成员。这意味着在派生类的作用域内,直接访问该同名成员将引用派生类的成员,而不是基类的成员。

1.同名变量

来看看这段代码:基类和派生类都有_height这个变量

class Person
{
protected:
	string _name = "Kana";
	int _age = 18;
	double _height = 1.50;
};

class Student :public Person
{
public:
	void Print()
	{
		cout << _name << endl;
		cout << _age << endl;
		cout << _height << endl;
	}

private:
	double _height = 1.70;
	int _id = 123456;
	int _grade = 10;
};

int main()
{
	Student s;
	s.Print();
	return 0;
}

输出结果为:

如果想要打印基类中的_height,则需要使用 :: 限定符:

void Print()
{
	cout << _name << endl;
	cout << _age << endl;
	cout << Person::_height << endl;
}

输出结果为:

2.同名函数

如果基类和派生类存在同名函数,会发生什么呢?

来看看这段代码:

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

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

int main()
{
	B b;
	b.fun(1);
};

输出结果为:

B 中的 fun 和 A 中的 fun 构成隐藏,成员函数满足函数名相同就构成隐藏。

由于函数重载针对的是同一个作用域的函数,而基类与派生类直接作用域不同。因此不是函数重载。

同样的,如果需要访问其他作用域的函数,我们需要使用 :: 操作符:

	B b;
	b.A::fun();	// 访问A中的fun函数

输出结果为:

派生类的默认成员函数

我们知道:在类中有6个默认成员函数,如果不显示定义,编译会自动生成。

那么在派生类中,这些成员函数如何生成?我们来学习一下:

1.对象的构造和析构遵循特定的顺序

对象的构造和析构遵循特定的顺序,以确保对象的正确初始化和清理

构造函数调用顺序

(1)创建派生类对象时,从最顶层的基类开始,逐层向下调用构造函数,直到派生类。

(2)接着,按照派生类中成员变量的声明顺序初始化成员变量(若成员是对象,则调用其构造函数)。

(3)最后,执行派生类构造函数体中的代码。

析构函数调用顺序

(1)销毁派生类对象时,首先调用派生类的析构函数。

(2)然后,按照成员变量声明的逆序调用成员变量的析构函数(若成员是对象)。

(3)最后,从最顶层的基类开始,逐层向上调用析构函数,直到派生类的基类。

来看看这个示例:

class Person
{
public:
	Person(string name = "Kana")
		: _name(name)
	{
		cout << "Person()" << endl;
	}
	~Person()
	{
		cout << "~Person()" << endl;
	}
private:
	string _name; // 姓名
};
class Student : public Person
{
public:
	Student()
	{
		cout << "Student()" << endl;
	}
	~Student()
	{
		cout << "~Student()" << endl;
	}
private:
	int _id; 
};

int main()
{
	Student s;
	return 0;
}

输出结果为:

2.派生类构造函数调用基类构造函数

(1)派生类的构造函数必须调用基类的构造函数来初始化基类的成员。

(2)如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表中显式调用基类的一个构造函数。

class Person 
{
public:
    // Person类的构造函数,用于初始化名字
    Person(const char* name) : 
        _name(name) 
    {
        // ...
    }
    // Person类的拷贝构造函数
    Person(const Person& p) : 
        _name(p._name) 
    {
    
    }
private:
    string _name;
};

class Student : public Person 
{
public:
    // Student类的构造函数,接收学号和名字
    Student(int id, const char* name) : 
        _id(id), 
        Person(name) 
        {
        // ...
        }
    // Student类的默认构造函数
    Student() : 
        Person("Default Student Name"), 
        _id(0) 
        {
            // ...
        }
private:
    int _id;
};

3.析构函数的特殊处理

因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destructor()。

如下所示:

class Person
{
public:
	Person(const char* name)
		: _name(name)
	{
		cout << "Person()" << endl;
	}
	~Person()
	{
		cout << "~Person()" << endl;
	}
private:
	string _name;
};

class Student : public Person
{
public: 
    Student(int num, const char* name)
        : _num(num), Person(name)
    {
        cout << "Student()" << endl;
    }
    ~Student() 
    {
        cout << "~Student()" << endl;
    }
private:
    int _num;
};

输出结果为:

4.拷贝构造与赋值重载必须调用基类的拷贝构造与赋值重载完成对基类的初始化

在派生类中实现拷贝构造函数和赋值操作符重载时,需要确保调用基类的相应函数来完成基类部分的拷贝或赋值操作。

来看个简单的例子:

class Person 
{
public:
    // 默认构造函数  
    Person(const string& name) : 
        _name(name) 
        {
        // ...
        }

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

    // 赋值操作符重载  
    Person& operator=(const Person& p) 
    {
        if (this != &p) 
        {
            _name = p._name;
            cout << "Assign Person(" << _name << ")" << endl;
        }
        return *this;
    }

    // 析构函数  
    ~Person() 
    {
        cout << "~Person(" << _name << ")" << endl;
    }

    string _name;
};

class Student : public Person 
{
public:
    // 构造函数  
    Student(int num, const string& name) : Person(name), _num(num) {
        cout << "Student(" << _num << ", " << _name << ")" << endl;
    }

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

    // 赋值操作符重载  
    Student& operator=(const Student& s) 
    {
        if (this != &s) 
        {
            Person::operator=(s); // 调用基类的赋值操作符  
            _num = s._num;
        }
        return *this;
    }

    // 析构函数  
    ~Student() 
    {
        cout << "~Student(" << _num << ", " << _name << ")" << endl;
    }
    int _num;
};

继承与友元

**友元关系不能继承。**友元关系(friendship)是一种单向的、非传递性的关系,它不能被继承。这意味着,如果一个类是另一个类的友元,那么这个友元类可以访问那个类的私有(private)和保护(protected)成员。但是,这种访问权限不会延伸到该类的子类。

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

报错信息为:

继承与静态成员

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

来看看这段代码:

class Person 
{
public:
    Person() 
    {
        ++_count; // 每次创建对象时增加计数  
    }
protected:
    string _name; // 姓名  
public:
    static int _count; 
};

int Person::_count = 0; // 初始化静态成员变量  

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

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

int main() 
{
    Student s1;
    Student s2;
    Student s3;
    Graduate s4;
    cout << "人数: " << Person::_count << endl; 

    // 重置计数  
    Person::_count = 0;  
    cout << "人数: " << Person::_count << endl; 

    return 0;
}

输出结果为:

菱形继承与虚拟继承

1.菱形继承

1.1 单继承

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

2.多继承

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

3.菱形继承

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

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。

来看看这段代码:

class Person
{
public:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _num; //学号
};
class Teacher : public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};
int main()
{
	Assistant a;
	// a._name = "peter"; 这样会产生二义性无法明确知道访问的是哪一个类
	// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
	return 0;
}

2.虚拟继承

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和 Teacher的继承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 = "peter";
	return 0;
}

输出结果为:

结束语

假期太摆了,导致写得太慢了。。。

求点赞收藏评论关注!!!

感谢各位大佬的支持!!!

相关推荐
IT技术分享社区26 分钟前
C#实战:使用腾讯云识别服务轻松提取火车票信息
开发语言·c#·云计算·腾讯云·共识算法
极客代码29 分钟前
【Python TensorFlow】入门到精通
开发语言·人工智能·python·深度学习·tensorflow
疯一样的码农35 分钟前
Python 正则表达式(RegEx)
开发语言·python·正则表达式
&岁月不待人&1 小时前
Kotlin by lazy和lateinit的使用及区别
android·开发语言·kotlin
StayInLove1 小时前
G1垃圾回收器日志详解
java·开发语言
无尽的大道1 小时前
Java字符串深度解析:String的实现、常量池与性能优化
java·开发语言·性能优化
爱吃生蚝的于勒1 小时前
深入学习指针(5)!!!!!!!!!!!!!!!
c语言·开发语言·数据结构·学习·计算机网络·算法
羊小猪~~1 小时前
数据结构C语言描述2(图文结合)--有头单链表,无头单链表(两种方法),链表反转、有序链表构建、排序等操作,考研可看
c语言·数据结构·c++·考研·算法·链表·visual studio
binishuaio1 小时前
Java 第11天 (git版本控制器基础用法)
java·开发语言·git
zz.YE1 小时前
【Java SE】StringBuffer
java·开发语言