初识C++ · 继承(1)

目录

前言:

[1 继承的概念和定义](#1 继承的概念和定义)

[2 基类与子类的赋值转换](#2 基类与子类的赋值转换)

[3 继承中的作用域](#3 继承中的作用域)

[4 派生类的默认成员函数](#4 派生类的默认成员函数)

[4.1 构造函数](#4.1 构造函数)

[4.2 拷贝构造](#4.2 拷贝构造)

[4.3 赋值重载](#4.3 赋值重载)

[4.4 析构函数](#4.4 析构函数)


前言:

对于面向对象这门语言的三大特性 -> 封装 继承 多态,我们已经学习了封装,这里简单理解一下封装,在面向过程的时候,数据和方法(函数)的分离开来的,所以C语言干什么事情都是要自己造轮子,比较麻烦,对于C++ 来说,有了类和对象这个概念,就可以把数据和方法放在一起,那么访问数据就更容易,不需要自己造轮子,这是一种封装,比如不同的数据结构,顺序表链表等,C++有专门的头文件,这也是一种封装,对于反向迭代器来说,是对迭代器的一种封装。

那么今天,就进入到继承这一特点来。


1 继承的概念和定义

继承,顾名思义,从上一代传下来的,比如家中的传家宝可以继承下来给你使用,比如你父亲的财产也可以继承给你,再比如说,某种情况下,私房钱明面上不能继承给你,但是可以间接的继承给你。

在C++中的继承,比如有两个类,他们有着一样的成员变量,比如说人和学生,都有名字,年龄,性别,身高这些概念,我们定义两个类的时候,重复的元素太多,写起来就没那么舒服,那么使用继承,即学生有人的特点,也有属于自己的特点,比如学号等等,就不需要重复定义许多东西了。

继承的定义如下:

cpp 复制代码
class Person
{
public:

private:
	
};

class student : public Person
{
public:

private:
	
};

定义的格式就是class 类名 :继承方式 类名.

其中继承方式一共有3种,public protected private。

在类和对象章节protected private是没有什么区别的,在继承这里,就有区别了,我们先看不同的继承方式对于访问权限的区别:

不难发现一个规律就是 两两权限继承之后,权限都是变成两两中权限小的那个,比如public 继承基类 ,也就是父类中的protected成员后,权限变成了protected,这里protected 和 private的区别就出来了:

权限大小public > protected > private。

但是问题来了,protected private的成员都访问不了,继承下来有什么用处呢?实际上protected的成员变量我们可以间接的访问:

cpp 复制代码
class Person
{
public:
	void Func()
	{
		cout << _num << endl;
	}
protected:
	int _num = 18;
private:
	string _name = "zhangsan";
};
class student : public Person
{
public:

private:
	int _id = 232323;
};

int main()
{
	student s1;
	s1.Func();
	return 0;
}

比如这里的打印就是一种间接的访问,通过基类继承下来的成员函数等,进行修改访问打印都是没有问题的。

C++的基础是C语言,那么C语言常用的是struct,C++里面可不可以使用struct来继承呢?

答案是可以,但是这里的继承默认是public,使用class默认的是private继承:

cpp 复制代码
struct St
{
	void Func()
	{
		cout << "Func()" << endl;
		cout << _num << endl;
	}
	int _num = 0;
	string _name = "xx";
	int _age = 18;
	string address = "earth";
};

struct Ss : St
{
	void Func()
	{
		cout << _age << endl;
	}
};

2 基类与子类的赋值转换

对于内置类型来说,直接赋值是没有问题的,对于自定义类型来说,直接赋值一般也是没有问题的,大不了就是涉及到拷贝临时对象然后赋值而已:

cpp 复制代码
int a = 1;
int b = a;
string s1 = "xxxx";

那么基类和子类直接能否赋值呢?答案是可以,但是也不完全可以。

子类可以赋值给基类,但是基类不能赋值给子类,这里引入一个切片的概念,假如基类有5个成员变量,继承给子类后,子类加上自己的成员变量,就有7个成员变量了,那么子类赋值过去的时候,就会把自己的成员变量给切掉,赋值的部分是继承的部分,那么相反的,如果是基类赋值给子类,怎么赋值?子类有那么多成员变量,基类没办法赋值:

cpp 复制代码
class Person
{
public:

protected:
	int _num = 18;
private:
	string _name = "zhangsan";
};

class student : public Person
{
public:

private:
	int _id = 232323;
};

int main()
{
	student s1("aaa");
	Person p1;
	p1 = s1;
	return 0;
}

3 继承中的作用域

截止到现在,我们学习了局部域 全局域 类域 命名空间域,加上今天继承中的作用域,就有5个域了。

当局部域和全局域中都有一个整型a的时候,打印往往是打印的局部域中的a,那么同理,如果基类和子类都有一个相同名字的变量,打印的往往是最近的那个,比如:

cpp 复制代码
class Person
{
public:

protected:
	int _num = 18;
};

class student : public Person
{
public:
	void Func()
	{
		cout << _num << endl;
	}

private:
	int _id = 232323;
	int _num = 0;
};

int main()
{
	student s1;
	s1.Func();
}

这段代码的打印结果就是0,那么我们就是想要打印基类中的_num怎么办呢?这时候就需要域名访问限定符:

cpp 复制代码
class Person
{
public:

protected:
	int _num = 18;
};

class student : public Person
{
public:
	void Func()
	{
		cout << Person::_num << endl;
	}

private:
	int _id = 232323;
	int _num = 0;
};

这时候的打印结果就是18。

这种现象叫做隐藏,因为两个变量名是一样的,但是继承下来之后,基类的就被隐藏了,我们需要加点手段才能访问得到。

cpp 复制代码
class A
{
public:
 void fun()
 {
 cout << "func()" << endl;
 }
};
class B : public A
{
public:
 void fun(int i)
 {
 A::fun();
 cout << "func(int i)->" <<i<<endl;
 }
};

函数也是同理可得。


4 派生类的默认成员函数

默认成员函数有6个,取地址重载那两个不用管,我们需要注意构造函数,析构函数,拷贝构造函数以及赋值重载。

4.1 构造函数

cpp 复制代码
class Person
{
public:
	Person(const char* name)
		:_name(name)
	{}

private:
	string _name = "zhangsan";
};
class student : public Person
{
public:
	student()
		:_id(1)
	{}

private:
	int _id = 232323;
};

类的构造函数对于自定义类型来说会调用它自己的构造函数,那么student继承了Person,Person相当于在student里面了,所以我们应该把student看成两部分,一部分是student,一部分是Person,那么构造函数调用的时候,就需要初始化两部分,一部分是student自己的成员变量,一部分是Person的成员变量。

按照上面的写法,是有问题的,student的对象创建好了后,_id的初始化没问题,但是Person的初始化就有问题了,因为没有默认构造函数,name是什么编译器也不知道,就会报错,那么如果加上:

cpp 复制代码
class Person
{
public:
	Person(const char* name = "hhh")
		:_name(name)
	{}

private:
	string _name = "zhangsan";
};

就不会报错了。

那么问题来了,如果我们不想提供默认构造函数怎么办,就是想要传参,这时候需要用到student的初始化列表了:

cpp 复制代码
class Person
{
public:
	Person(const char* name)
		:_name(name)
	{}

private:
	string _name = "zhangsan";
};
class student : public Person
{
public:
	Person(const char* name)
		:Person(name)
        ,_id(1)
	{}

private:
	int _id = 232323;
};

这里的语法看起来有点怪?

我们可以这样理解,构造的时候,调用的有两个构造函数,一个是子类自己的,一个是基类的,但是基类的如果没有默认的拷贝构造函数,我们就需要自己显式的去调用基类的构造函数。

4.2 拷贝构造

拷贝构造和构造函数一样,都要为基类考虑,这里有个问题就是,应该怎么调用它自己的拷贝构造?

cpp 复制代码
class student : public Person
{
public:
	student(const char* name)
		:_id(1)
		,Person(name)
	{}

	student(const student& s)
		:_id(s._id)
		,Person(s)
	{}

private:
	int _id = 232323;
};

就直接传s就可以了,语法稍稍有点怪,但不大。

4.3 赋值重载

赋值重载这里,我们要考虑的是如何调用基类函数的赋值重载:

cpp 复制代码
	student& operator=(const student& s)
	{
		if (this != &s)
		{
			operator=(s);
			_id = s._id;
		}

		return *this;
	}

如果这样调用,就会栈溢出,基类和子类的赋值重载的函数名一样,那么就构成了隐藏关系,所以需要我们显式的调用基类的赋值重载函数:

cpp 复制代码
student& operator=(const student& s)
{
	if (this != &s)
	{
		Person::operator=(s);
		_id = s._id;
	}
	return *this;
}

4.4 析构函数

析构函数在这里就是很有说法的了,如果我们显式的去调用析构函数,就无法满足析构函数的先子后父原则。

这里引入两个原则,构造是先父后子,析构是先子后父,这也好理解,构造方面,如果基类都没有先构造好怎么继承给子类?

析构函数同理,如果我们先显式的调用了基类的析构函数,就无法满足先子后父了,所以如果加上打印观察的话,就会发生析构了两次的情况出现,如果碰上了动态开辟,就会析构两次从而导致程序挂掉。

那么这里,我们写析构函数只需要:

cpp 复制代码
~student()
{
	// 显示写无法先子后父
	//Person::~Person();
	cout << "~Student()" << endl;
	// 注意,为了析构顺序是先子后父,子类析构函数结束后会自动调用父类析构
}

只调用子类的析构就好了,基类的析构会自己调用的。

实际上,子类的析构和基类的析构构成了隐藏关系,这里先了解,因为都是析构,编译器对函数名继续特殊处理,使析构函数的名字都变成destructor。


感谢阅读!

相关推荐
猷咪6 分钟前
C++基础
开发语言·c++
IT·小灰灰8 分钟前
30行PHP,利用硅基流动API,网页客服瞬间上线
开发语言·人工智能·aigc·php
快点好好学习吧9 分钟前
phpize 依赖 php-config 获取 PHP 信息的庖丁解牛
android·开发语言·php
秦老师Q10 分钟前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
烟锁池塘柳010 分钟前
解决Google Scholar “We‘re sorry... but your computer or network may be sending automated queries.”的问题
开发语言
是誰萆微了承諾10 分钟前
php 对接deepseek
android·开发语言·php
CSDN_RTKLIB13 分钟前
WideCharToMultiByte与T2A
c++
2601_9498683614 分钟前
Flutter for OpenHarmony 电子合同签署App实战 - 已签合同实现
java·开发语言·flutter
星火开发设计28 分钟前
类型别名 typedef:让复杂类型更简洁
开发语言·c++·学习·算法·函数·知识
蒹葭玉树39 分钟前
【C++上岸】C++常见面试题目--操作系统篇(第二十八期)
linux·c++·面试