[C++随想录] 继承

这篇具有很好参考价值的文章主要介绍了[C++随想录] 继承。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

继承的引言

  1. 概念
    继承(inheritance)机制是面向对象程序设计使代码可以 复用的最重要的手段,它允许程序员在保
    持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象
    程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继 承是类设计层次的复用。
  2. 定义
class People
{
public:
	People(string name = "John", int age = 18)
	{
		_name = name;
		_age = age;
	}

	void print() 
	{
		cout << "姓名->" << _name << endl;
		cout << "年龄->" << _age << endl;

	}

protected:
	int _age ;
	string _name;
};

class Student : public People
{
public:
	Student(string name = "muyu", int age = 20, string id = "9210401227")
		:People(name, age)
	{
		_id = id;
	}

protected:
	string _id;
};

class Teacher : public People
{
public:
	Teacher()
	{
		People::_name = "mutong";
		People::_age = 22;
	}

protected:
	int _JobNumber;
};

int main()
{
	People p;
	p.print();
	cout << endl;

	Student s;
	s.print();
	cout << endl;

	Teacher t;
	t.print();

	return 0;
}

运行结果:

姓名->John
年龄->18

姓名->muyu
年龄->20

姓名->mutong
年龄->22
  1. 解释 class Student : public People
    [C++随想录] 继承,C++,c++,开发语言,stl,数据结构
    People类是 父类/ 基类, Student类是 子类/ 派生类
    Student类继承People类的本质就是 复用Student 对象可以使用 People类里面的成员
    成员包括 成员变量 和 成员函数.
    成员变量是在对象空间内的, 而成员函数是不在对象空间内的, 属于整个类.
    成员的 访问限定符 有三种 public, protected, private
    继承方式不同 && 基类成员的访问限定符不同决定了基类的成员在派生类中的存在情况

  2. 继承的方式
    继承方式有三种: public, protected, private
    成员的 限定符 有三种 public, protected, private
    所以, 一共有 九种继承方式 👇👇👇
    [C++随想录] 继承,C++,c++,开发语言,stl,数据结构

    1. 基类中的private成员, 在派生类中都是 不可见的
      • 不可见 和 private成员是不一样的, private成员是 类里面可以访问, 类外面不可访问, 不可见是 类里面看不见/ 不可访问, 类外面不可访问
    2. 其余继承方式, 派生类中的情况是 继承方式 和 类成员访问限定符中 权限小的那一个
      • 权限的大小: public > protected > private
    3. 父类如果是 class, 默认继承是 私有继承, 父类如果是 struct, 默认继承是 公有继承. 不过建议显示继承方式
    4. 常用的继承方式为 图中绿色的区域 ⇐ 继承的本质是 复用, 私有继承 和 基类中的私有成员在继承中是没有复用的意义的.
  3. 为什么 派生类没有 print函数 , 但能调用 print函数?
    我们可以认为 子类对象里面包含两个部分: 父类对象成员变量 + 子类本身成员变量
    [C++随想录] 继承,C++,c++,开发语言,stl,数据结构
    子类对象中的 成员变量 = 自己本身的成员变量 + 父类的成员变量 (受访问限定符 和 继承方式共同限制)
    子类对象中的 成员函数 = 自己本身的成员函数 + 父类的成员函数 (受访问限定符 和 继承方式共同限制)
    print函数 是公有继承 && 访问限定符是公有 ⇒ 子类对象可以调用

  4. 为什么在 Teacher类中 可以People::_name = "mutong";
    我们已经知道了 派生类对象的基本结构了.
    那么派生类对象在 初始化阶段, 即调用默认构造 是先父类还是先子类呢?
    通过调试, 我们发现: 子类对象调用构造函数的时候, 先调用父类的默认构造函数去初始化子类中父类对象的那一部分, 然后在调用子类对象的默认构造函数
    Person类中有默认构造函数, 但是我们想改变一下 Teacher类对象中的 关于父类对象的那一部分, 那我们该怎么做呢?
    首先, 我们不能直接写

_name = "mutong";
_age = 22;

因为受 的影响, 域是编译器在编译阶段查找变量的规则.
虽然, 我们可以认为子类对象中有 父类对象成员 + 子类对象成员, 但彼此是 独立的.
调用默认构造函数还是去 Person类中去调用
编译器在 编译阶段默认查找的顺序是 局部域 , 子类域, 父类域, 全局域
我们在子类中去给父类对象成员赋值 ⇒ 我们应该告诉编译器, 这个变量直接去父类中去查找就OK
即, 这个时候我们要用 Person(父类)::

  1. 为什么在 Student类中 可以 :People(name, age)
    子类对象调用构造函数的时候, 先调用父类的默认构造函数去初始化子类中父类对象的那一部分, 然后在调用子类对象的默认构造函数.
    那么如果 父类对象没有默认构造函数呢?
    我们就需要 在子类的初始化列表处 显示调用父类的构造

基类和子类的赋值转换

int main()
{
	People p;
	Student st;

	st = p; // error
	p = st; // 可以进行转换

	return 0;
}
  • 父类对象 不能 赋值给子类对象, 而子类对象 可以 赋值给父类对象
    可以这样想: 子类对象的成员 > 父类对象的成员可以 变小一点, 但不能变大一点

父类对象 = 子类对象, 属于不同类型之间的赋值 ⇒ 一般都会发生 类型转换 ⇒ 类型转换, 那就意味着要产生 临时常量拷贝. 但结果真的如我们想的这般吗?

  • 验证 父类对象 = 子类对象 是否有临时常量拷贝
    拷贝是 常量的 ⇒ 要进行区分, 我们可以使用 引用 &
    如果生成了临时拷贝, 我们用普通引用 就会导致 权限的放大 , 就会报错
    如果没有生成临时拷贝, 我们用普通引用, 就是 权限的平移, 就不会报错
int main()
{
	// 类型转换
	int i = 0;
	double d = i;
	// double& dd = i // error
	const double& dd = i;

	// 赋值兼容转换
	Student st;
	People ps = st;
	People& p = st;

	return 0;
}

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫 切片 或者切割 . 寓意把派生类中父类那部分切来赋值过去
[C++随想录] 继承,C++,c++,开发语言,stl,数据结构
🗨️那么这个切片是怎样完成的呢?
[C++随想录] 继承,C++,c++,开发语言,stl,数据结构

继承中的作用域

🗨️在继承过程中, 可能会出现 父类中的成员名 和 子类中的成员名相同的情况, 那么派生类对象调用该成员会是怎样的情况呢?

  • 先看下面的代码:
class People
{
public:
	People(string name = "John", int age = 18)
	{
		_name = name;
		_age = age;
	}

	void print() 
	{
		cout << "class People" << endl;
	}

protected:
	int _age ;
	string _name;
};

class Student : public People
{
public:
	Student(string name = "muyu", int age = 20, string id = "9210401227")
		:People(name, age)
	{
		_id = id;
	}

	void print()
	{
		cout << "class Student : public People" << endl;
	}

protected:
	string _id;
};

void test1()
{
	Student st;

	st.print();
}

int main()
{
	test1();

	return 0;
}

运行结果:

class Student : public People

父类和子类中都有 print函数, 通过结果显示 派生类内部的print函数
这是因为 , 跟上面的People::_name = "muyu";是一样的道理
那么, 如果我们非要通过派生类对象 调用基类中的print函数呢?👇👇👇

void test1()
{
	Student st;
	
	st.People::print();
}

总结:

  1. 子类和父类中的成员尽量不同名!
  2. 上面的例子, 子类和父类有同名的成员, 子类隐藏父类的成员, 这种关系叫做 隐藏/ 重定义
    • 注意: 隐藏 != 重载
      重载的前提条件是 同一作用域, 而隐藏是 父类和子类成员同名
    • 隐藏 != 重写
      隐藏是 子类中同名成员隐藏父类中同名成员, 而重写是 子类中重写父类有关函数的实现

派生类中的默认成员函数

6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类
中,这几个成员函数是如何生成的呢?

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认 的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
    保证派生类对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调派生类构造。
  6. 派生类对象析构清理先调用派生类析构再调基类的析构。
  7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲
    解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加
    virtual的情况下,子类析构函数和父类析构函数构成隐藏关系
class Person
{
public:

	Person(string name = "muyu", int age = 20)
		:_name(name)
		,_age(age)
	{
		cout << "Person()" << endl;
	}

	Person(const Person& tem)
	{
		_name = tem._name;
		_age = tem._age;

		cout << "Person(const Person& tem)" << endl;
	}

	Person& operator=(const Person& tem)
	{
		_name = tem._name;
		_age = tem._age;

		return *this;

		cout << "Person& operator=(Person& tem)" << endl;
	}

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

protected:
	string _name;
	int _age;
};

class Student : public Person
{
public:
	Student(const string name,const int age, const int num)
		: Person(name,age)
		, _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; //学号
};

void test2()
{
	Student st1("牧童", 20, 20230101);
	Student st2(st1);

	Student st3("沐雨", 18, 20230102);
	st3 = st1;

}

int main()
{
	// test1();

	test2();

	return 0;
}

运行结果:

Person()
Student()
Person(const Person& tem)
Student(const Student& s)
Person()
Student()
Student& operator= (const Student& s)
~Student()
~Person()
~Student()
~Person()
~Student()
~Person()

🗨️其他函数都是 先父类, 后子类, 唯独 析构函数 先子类后父类?

  • 首先, 构造函数是 先父类, 后子类
    , 先进后出 ⇒ 析构的时候, 先子类, 后父类.
    其次, 父类可以调用子类的成员, 而子类不能调用父类的成员
    如果先析构父类, 如果子类对象还想调用父类的成员,那就完蛋了!

🗨️在子类的析构函数中, 调用父类的析构函数

  • 首先,
	~Student()
	{
		~Person(); // 提示有一个重载

		cout << "~Student()" << endl;
	}

纳闷? 这个还能有重载?
因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲
解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor().
那么子类和父类中的 析构函数名 都是 destruction ⇒ 那么就构成了隐藏关系
那么我们在子类中调用父类的析构函数应该如下:

	~Student()
	{
		Person::~Person();

		cout << "~Student()" << endl;
	}

结果如下:
[C++随想录] 继承,C++,c++,开发语言,stl,数据结构

  • 编译器默认帮我们 先调用了父类的析构函数不信任我们用户, 由编译器自己完成

继承与友元

基类的友元, 派生类不会继承, 即基类的友元不能访问 子类中的 私有和保护成员

// 类的声明
class Student;

class Person
{
public:

	friend void Display(const Person& p, const Student& s);

protected:
	string _name; // 姓名

};

class Student : public Person
{
public:
	// friend void Display(const Person& p, const Student& s);
protected:
	int _stuNum; // 学号

};

void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stuNum << endl; // error: “Student::_stuNum”: 无法访问 protected 成员(在“Student”类中声明)
}

void test3()
{
	Person p;
	Student s;
	Display(p, s);
}

int main()
{
	test3();

	return 0;
}

解决方法就是: 让 Display函数也充当 子类的友元👇👇👇

class Student : public Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	int _stuNum; // 学号

};

继承与静态成员

基类中定义了一个静态成员, 则在整个继承体系中, 仅此一份. 子类不会单独拷贝一份, 继承的是使用权

🗨️只创建子类对象, 问一共创建了多少个子类对象?

  • 1. 可以在子类的默认构造中创建一个静态成员变量.
class A
{
public:
	A(){}
};

class B :public A
{
public:
	B()
	{
		_count++;
	}
public:
	static int _count;
};

int B::_count = 0;

void test4()
{
	B b1;
	B b2;
	B b3;
	B b4;
	B b5;
	B b6;


	cout << "子类中的个数->" << B::_count << endl;

}


int main()
{
	test4();

	return 0;
}

运行结果:

子类中的个数->6
  1. 可以在父类的默认构造中创建一个静态成员变量.
class A
{
public:
	A()
	{
		++_count;
	}
public:
	static int _count;
};

int A::_count = 0;

class B :public A
{

};

void test4()
{
	B b1;
	B b2;
	B b3;
	B b4;
	B b5;
	B b6;


	cout << "子类的个数->" << A::_count << endl;

}


int main()
{
	test4();

	return 0;
}

运行结果:

子类中的个数->6

多继承的结构

一个子类只有一个直接父类, 叫做 单继承
[C++随想录] 继承,C++,c++,开发语言,stl,数据结构

一个子类有两个及以上的父类, 叫做 多继承
[C++随想录] 继承,C++,c++,开发语言,stl,数据结构

  1. 单继承的结构
    [C++随想录] 继承,C++,c++,开发语言,stl,数据结构
    其实在 内存中不是这样 "点开" 的关系, 而是连续的空间
    [C++随想录] 继承,C++,c++,开发语言,stl,数据结构

  2. 多继承的机构
    [C++随想录] 继承,C++,c++,开发语言,stl,数据结构
    当然, 也是连续的空间
    [C++随想录] 继承,C++,c++,开发语言,stl,数据结构

棱形继承的结构

多继承会有一种情况是 棱形继承
[C++随想录] 继承,C++,c++,开发语言,stl,数据结构

D继承B和C, B和C又同时继承A ⇒ 就会导致D对象中有两个A对象成员
[C++随想录] 继承,C++,c++,开发语言,stl,数据结构
[C++随想录] 继承,C++,c++,开发语言,stl,数据结构

这样就会导致 冗余性和二义性
[C++随想录] 继承,C++,c++,开发语言,stl,数据结构
其实, 解决 访问不明确/ 二义性 可以使用 基类::
[C++随想录] 继承,C++,c++,开发语言,stl,数据结构
但是 内存中D还是存储了两份 A类对象 造成的数据冗余性问题还没解决呢?

棱形虚拟继承的结构

棱形虚拟继承解决的就是 数据冗余性 和 二义性的问题
[C++随想录] 继承,C++,c++,开发语言,stl,数据结构
[C++随想录] 继承,C++,c++,开发语言,stl,数据结构
[C++随想录] 继承,C++,c++,开发语言,stl,数据结构

通过 内存窗口, 我们发现:

  1. 把A从B 和 C中抽出来了, 让A既不属于B, 也不属于C
  2. B和C类中多了一个位置出来
  • B和C类中多了一个位置的用处是什么?
    [C++随想录] 继承,C++,c++,开发语言,stl,数据结构
    我们发现: 地址指向的空间第一个位置是 0, 第二个位置分别是 20(十六进制转二进制) 和 12(十六进制转二进制)
    [C++随想录] 继承,C++,c++,开发语言,stl,数据结构
    虽然, 把A类单独放在一个空间, 但 A类中的成员还是B和C类得一部分 =>
    这里是通过了B和C的两个指针,指向的一张表。这两个指针叫 虚基表指针,这两个表叫 虚基表。虚基表中存的 偏移量 。通过偏移量可以找到下面的A。

那么这个时候, 我们修改A类的对象, 就不会有 冗余性和二义性的问题了👇👇👇
[C++随想录] 继承,C++,c++,开发语言,stl,数据结构

继承与组合

继承是一种 is-a的关系, 是一种 白箱复用, 子类跟父类之间的 耦合度高
对象组合是一种 has-a的关系, 是一种 黑箱复用, 耦合度低

[C++随想录] 继承,C++,c++,开发语言,stl,数据结构

  • is-a 和 has-a
    is-a,就表示 子类是一个特殊的父类
    has-a, 就表示 A对象中有B对象

  • 白箱复用 和 黑箱复用
    白箱复用, 透明的, 即 子类知道父类内部的细节, 方法的实现
    黑箱复用, 不透明的, 即 对象之间不知道彼此的内部的细节

  • 耦合度
    打个比方:
    父类A中的成员 有20个是public, 80个是protected的; 派生类是public继承
    那么在派生类B中, A的成员都是可见的 ⇒ 耦合度是 100%
    同样的,
    A对象中的成员, 有20个是public, 80个是protected的;
    那么B对象想用A对象里面的成员, 只能使用 20个public的成员 ⇒ 耦合度是 20%

🗨️那对象组合这么好, 我们就用对象组合, 不用继承了是吧?

  • 首先, 存在即合理 ⇒ 全部都这样, 或者全部都那样的想法就是错误的
    1. 合理使用: 符合is-a 关系的就使用 继承; 符合 has-a关系就使用 对象组合; 如果 既符合has-a, 又符合 is-a关系使用 对象组合
    2. 实现 多态 , 必须使用继承

学后反思:

  1. 什么是菱形继承?菱形继承的问题是什么?
  2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的
  3. 继承和组合的区别?什么时候用继承?什么时候用组合?

与朋友论学,须委曲谦下,宽以居之。 — — 王阳明
译文:与朋友谈论学问,必须婉转曲从谦虚下问,与之宽和相处
文章来源地址https://www.toymoban.com/news/detail-736095.html

到了这里,关于[C++随想录] 继承的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 代码随想录——贪心算法

    代码随想录——回溯 代码随想录——贪心算法 分发饼干 链接:https://leetcode.cn/problems/assign-cookies/description/ 这道题我自己一开始的想法是从大到小遍历孩子数组,对于每个元素从大到小遍历饼干数组,满足则total+1,并且该元素置0防止被再次使用。这样虽然是可以的,但时间复

    2024年02月22日
    浏览(57)
  • 代码随想录 - 链表

    链表是一种通过指针串联的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。 链表的类型  1、单链表  单链表中的指针域只能指向节点的下一个节点。  2、双链表 双链表:

    2024年02月13日
    浏览(44)
  • 【导航】繁星学习随想录

    01 庖丁解牛 序号 博文名称/链接 01 编程启示录:栈的拿手好戏 02 编程启示录:链表七怪 03 二叉树的遍历大冒险 02 力扣打卡 序号 博文名称/链接 01 ShinyLeetCode-打卡第1周 02 ShinyLeetCode-打卡第2周 03 编程小车轮 序号 博文名称/链接 01 编程启示录:图Graph的可视化方案 01 数据结构

    2024年01月21日
    浏览(33)
  • 代码随想录 739. 每日温度

    题目 给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。 示例 1: temperatures 示例 2: 输入: temperatures = [30,40,50,60] 输出: [1,1,1,0] 示例 3: 输入

    2024年01月18日
    浏览(36)
  • 数据类岗位面试随想录

    数据分析或者是偏向数据分析的数据开发岗,要求无非就是SQL、Python和业务相关的问题。         基本这些问题和期末考试的难度比,是简单的。和学校所教的比,基本超纲的问题只会有窗口函数。这一部分面试官一般不会问你难的问题,例如三范式、事务、完整性、锁这些

    2024年02月06日
    浏览(38)
  • 代码随想录第52天

    1.最长递增子序列 接下来,我们依然用动规五部曲来详细分析一波: dp[i]的定义 本题中,正确定义dp数组的含义十分重要。 dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度 为什么一定表示 “以nums[i]结尾的最长递增子序” ,因为我们在 做 递增比较的时候,如果比

    2024年02月07日
    浏览(36)
  • 代码随想录第19天

    需要重新做的题目 617. 合并二叉树 98. 验证二叉搜索树

    2024年02月15日
    浏览(36)
  • 代码随想录之额外题目

    1207 独一无二的出现次数 看数组的大小和长度都没有很大,所以可以直接用数组来做哈希表,用一个数组来记录出现次数,再用一个数组来标记出现次数的值是否出现过。就是O(n) 189 旋转数组 思路就是反转三次,先全部反转,然后前K个反转一次,后面剩下的反转一次。 要注

    2024年02月03日
    浏览(39)
  • 代码随想录第55天

    1.判断子序列: 动态规划五部曲分析如下: 确定dp数组(dp table)以及下标的含义 dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j] 。 注意这里是判断s是否为t的子序列。即t的长度是大于等于s的。 如果t的长度小于s那直接retur

    2024年02月08日
    浏览(32)
  • 【代码随想录打卡】快慢指针

    力扣链接: 力扣 给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。 不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。 元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

    2024年02月12日
    浏览(44)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包