[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模板网!

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

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

相关文章

  • 【导航】繁星学习随想录

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

    2024年01月21日
    浏览(25)
  • 代码随想录——贪心算法

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

    2024年02月22日
    浏览(33)
  • [代码随想录]二叉树

    二叉树可以链式存储,也可以顺序存储。 那么链式存储方式就用指针, 顺序存储的方式就是用数组。 顾名思义就是顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在各个地址的节点串联一起。 链式存储如图: 链式存储是大家很熟悉的一种方式,那么

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

    链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。 链表的入口节点称为链表的头结点也就是head。 单链表 与上面所说一致。 单链表中的指针

    2024年02月04日
    浏览(30)
  • 代码随想录day11

    20. 有效的括号   思路:这里用模拟栈的方法会更好理解,也就是我们每次遇到朝左方向的三种类型的时候,就加入相反方向的右括号到result栈中。由于栈是一个先进后出的方式,所以我们会有一个判断stack当前为不为空,和stack[-1]是不是和当前循环到的括号相同。如果说相同

    2024年02月13日
    浏览(27)
  • 代码随想录day44

    完全背包 其实就是每个物品可以使用无数次,给我们一个容器,装满这个容器的最大价值是多少。 思路: 如果求组合数就是外层for循环遍历物品,内层for遍历背包。 如果求排列数就是外层for遍历背包,内层for循环遍历物品。 完全背包的组合和排序 518. 零钱兑换 II 题目 给你

    2023年04月17日
    浏览(39)
  • 【代码随想录打卡】快慢指针

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

    2024年02月12日
    浏览(29)
  • 代码随想录day59

    647. 回文子串 给你一个字符串  s  ,请你统计并返回这个字符串中  回文子串  的数目。 回文字符串  是正着读和倒过来读一样的字符串。 子字符串  是字符串中的由连续字符组成的一个序列。 具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不

    2024年02月07日
    浏览(27)
  • 代码随想录Day50

    昨天因为准备面试所以咕咕了一天。今天继续学习动规算法,尽管背包问题已经结束但其中的各类思想仍需要进一步理解。 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两

    2023年04月14日
    浏览(31)
  • 代码随想录Day58

    昨天因为志愿活动和笔试耽误了一整天,今天继续学习动规解决子序列问题。 给定字符串 s 和 t ,判断 s 是否为 t 的子序列。 字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,\\\"ace\\\"是\\\"abcde\\\"的一个子序列,

    2023年04月27日
    浏览(32)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包