目录

前言:

1 继承的概念和定义

2 基类与子类的赋值转换

3 继承中的作用域

4 派生类的默认成员函数

4.1 构造函数

4.2 拷贝构造

4.3 赋值重载

4.4 析构函数


前言:

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

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


1 继承的概念和定义

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

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

继承的定义如下:

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的成员变量我们可以间接的访问:

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继承:

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 基类与子类的赋值转换

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

int a = 1;
int b = a;
string s1 = "xxxx";

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

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

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,那么同理,如果基类和子类都有一个相同名字的变量,打印的往往是最近的那个,比如:

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怎么办呢?这时候就需要域名访问限定符:

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。

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

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 构造函数

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是什么编译器也不知道,就会报错,那么如果加上:

class Person
{
public:
	Person(const char* name = "hhh")
		:_name(name)
	{}

private:
	string _name = "zhangsan";
};

就不会报错了。

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

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 拷贝构造

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

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 赋值重载

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

	student& operator=(const student& s)
	{
		if (this != &s)
		{
			operator=(s);
			_id = s._id;
		}

		return *this;
	}

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

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

4.4 析构函数

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

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

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

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

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

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

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


感谢阅读!

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐