C++学习笔记(四) 类(三) 继承和多态

本文详细介绍了C++中的继承方式,包括公有继承、保护继承和私有继承,并通过示例展示了它们的区别。接着讨论了构造和析构函数的调用顺序。同时,文章阐述了同名成员处理的方法,以及如何在子类中访问父类的成员。在多态部分,讲解了虚函数、纯虚函数和抽象类的概念,以及虚析构函数的重要性,强调了多态在防止内存泄漏中的角色。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.继承

1.1 继承方式

继承有三种方式:公有继承、保护继承和私有继承,三种继承方式都可以继承父类所有非静态成员变量和函数,但是无法访问父类的私有权限的成员变量和函数,被编译器隐藏了。
公有继承:使用关键字public,子类继承的所有变量和函数权限不变
保护继承:使用关键字protected,子类继承的所有变量和函数权限都会变成保护权限
私有继承:使用关键字private,子类继承的所有变量和函数权限都会变为私有权限

这就看出C++保护权限变量的作用了,类外无法访问,但是继承时可以继承。跟python不同,python的保护权限属性其实是一种约定俗成的写法,并没有特殊的作用,类外仍可以访问。

#include<iostream>
#include<string>
using namespace std;

class PersonFather
{
public:
	PersonFather()
	{
		age = 40;
		phone_num = 10086;
		id = 1234;
	}

	void get_father_age_addr(void)
	{
		cout << "father' age addr: " << &this->age << endl;
	}
	int age;

protected:
	int phone_num;

private:
	int id;
};

/* 公有继承,除了不可访问父类的私有变量,其他都可以访问。
并且父类的public在子类中仍为public,protected仍未protected */
class Son1 : public PersonFather 
{
public:
	void get_father_age(void)
	{
		cout << "son1' father age:" << age << endl;
	}
	void get_father_phone(void)
	{
		cout << "son1' father phone num:" << phone_num << endl;
	}
};

/* 保护继承,保护继承父类的public和protected在子类中全变为了protected,private仍不可访问 */
class Son2 : protected PersonFather
{
public:
	void get_son2_age_addr(void)
	{
		cout << "son2' age addr: " << &this->age << endl;
	}
	void get_father_age(void)
	{
		cout << "son2' father age:" << age << endl;
	}
	void get_father_phone(void)
	{
		cout << "son2' father phone num:" << phone_num << endl;
	}
};

/* 私有继承,保护继承父类的public和protected在子类中全变为了private,private仍不可访问*/
class Son3 : private PersonFather 
{
public:
	void get_father_age(void)
	{
		cout << "son3' father age:" << age << endl;
	}
	void get_father_phone(void)
	{
		cout << "son3' father phone num:" << phone_num << endl;
	}
};

class GrandSon2 : public Son2
{
public:
	void get_son2_age(void)
	{
		cout << "grandson2' father age:" << age << endl;
	}

	void get_grandson2_age_addr(void)
	{
		cout << "grandson2' age addr: " << &this->age << endl;
	}
};

/* 已经无法继承son3的age,因为son3的变量在私有继承之后变为了私有变量 */
class GrandSon3 : public Son3
{
public:
	void get_son3_age(void)
	{
		// cout << "grandSon3' father age:" << age << endl;
	}
};


void test_1(void)
{
	PersonFather p1;
	Son1 s1;
	Son2 s2;
	Son3 s3;
	GrandSon2 gs2;
	GrandSon3 gs3;
	cout << "类外调用s1.age: "<< s1.age << endl;
	// cout << "类外调用s3.age: "<< s3.age << endl;		/* s3的age已经是*/
	gs2.get_son2_age();								/* */									
	gs3.get_son3_age();								/* 如果get_son3_age中调用了age就会出错*/
	p1.get_father_age_addr();
	s2.get_son2_age_addr();
	gs2.get_grandson2_age_addr();

}


int main(void)
{
	test_1();
	return 0;
}

1.2 构造和析构顺序

先构造父类,再构造子类,先析构子类,再析构父类

#include<iostream>
#include<string>
using namespace std;

class PersonFather
{
public:
	PersonFather()
	{
		cout << "父类构造函数" << endl;
	}
	
	~PersonFather()
	{
		cout << "父类析构函数" << endl;
	}
	
	int age;

protected:
	int phone_num;

private:
	int id;
};


/* 已经无法继承son3的age,因为son3的变量在私有继承之后变为了私有变量 */
class Son1 : public PersonFather
{
public:
	Son1()
	{
		cout << "子类构造函数" << endl;
	}

	~Son1()
	{
		cout << "子类析构函数" << endl;
	}

};


void test_1(void)
{
	Son1 s1;
}


int main(void)
{
	test_1();
	return 0;
}

1.3 同名成员处理

跟python一样,如果子类覆盖了父类的成员函数或者变量(静态和非静态),子类的实例化对象在访问父类成员函数或者变量时需要加上作用域才能访问。python是使用了关键词super()

#include<iostream>
#include<string>
using namespace std;

class PersonFather
{
public:
	void FuncTest()
	{
		cout << "Father's func" << endl;
	}
	
	void FuncTest(int a)
	{
		cout << "带参的Father's func: " << a << endl;
	}

	int age = 40;
	static int num;

protected:
	int phone_num;

private:
	int id;
};
int PersonFather::num = 10086;

/* 已经无法继承son3的age,因为son3的变量在私有继承之后变为了私有变量 */
class Son1 : public PersonFather
{
public:
	void FuncTest()
	{
		cout << "Son's func" << endl;
	}
	
	int age = 10;
	static int num;
};
int Son1::num = 10000;

void test_1(void)
{
	Son1 s1;
	cout << "Father' age: " << s1.PersonFather::age << endl; /* 如果覆盖了父类的变量或函数,需要加上作用域才能访问父类的变量和函数 */
	cout << "Son' age: " << s1.age << endl; /* 如果覆盖了父类的变量或函数,需要加上作用域才能访问父类的变量和函数 */
	s1.PersonFather::FuncTest();
	s1.PersonFather::FuncTest(100);
	s1.FuncTest();
	
	/* 静态变量跟非静态变量一样 */
	cout << "\n静态变量测试" << endl;
	cout << "Father' num: " << s1.PersonFather::num << endl; /* 如果覆盖了父类的变量或函数,需要加上作用域才能访问父类的变量和函数 */
	cout << "Son' num: " << s1.num << endl; /* 如果覆盖了父类的变量或函数,需要加上作用域才能访问父类的变量和函数 */
	cout << "也可以通过类名访问" << endl;
	cout << "Father' num: " << Son1::num << endl; 
	cout << "Son' num: " << Son1::PersonFather::num << endl; 
}

int main(void)
{
	test_1();
	return 0;
}

1.4 菱形继承和虚继承

菱形继承就是子类继承自多个父类时,这些父类又继承自同一个父类(就先叫爷爷类吧),这样这些父类就都继承了爷爷类类中的变量,这样就会导致子类在访问爷爷类中定义的变量时出现问题。这是需要避免的情况,解决的办法有两种,一种是在访问该变量时加作用域,加上你访问的哪个父类的该变量。

第二种则是使用虚继承。虚继承使用关键字virtual,这样父类在虚继承同一个类的时候,并没有继承爷爷类的变量,而是添加了一个指向虚基类表的指针,虚基类表中记录了爷爷类中的成员变量的偏移量,也就是这个成员变量与这个子类的地址相差多少。使用虚继承之后,再访问爷爷类中的成员变量时,实际上就是根据这个虚基类表中记录的偏移量和这个子类的地址来找到爷爷类中成员变量,所以子类访问爷爷类中的成员变量时实际上访问的是同一内存地址中的变量。

#include<iostream>
#include<string>
using namespace std;

class Animal
{
public:
	int age;
};

/* 虚继承 */
class Cow : virtual public Animal {};
class Horse : virtual public Animal {};

class Sheep : public Animal {};
class Camel : public Animal {};

class CowHorse : public Cow, public Horse {};
class SheepCamel :  public Sheep, public Camel {};
void test_1(void)
{
	/* 如果继承了多个父类,并且父类中有相同的数据(菱形继承,子类的父类继承自同一个父类),则需要加上作用域来区分 */
	SheepCamel sc;
	sc.Sheep::age = 10;
	sc.Camel::age = 20;
	// cout << sc.age << endl; /* 如果不加作用域,编译器不知道你用的是哪个,所以会报错error: request for member 'age' is ambiguous */
	cout << "菱形继承,继承的两个age:" << sc.Sheep::age << "  " << sc.Camel::age << endl;

	/* 如果是虚继承的话,则不用加作用域,因为实际上虚继承中传下来的age指向了同一块内存 */
	CowHorse ch;
	ch.Cow::age = 10;
	cout << "虚继承的age:" << ch.age << endl; 
	ch.Horse::age = 20;
	cout << "修改后的虚继承的age:" << ch.age << endl;
}


int main(void)
{
	test_1();
	return 0;
}

程序运行结果如下:
在这里插入图片描述

2.多态

2.1 多态的基本语法和原理

首先说明使用多态的条件:

  1. 存在继承关系
  2. 子类重写父类的虚函数
  3. 调用该虚函数的必须得是类的指针或者引用才可以

其他语言是没有这么复杂的,java、python直接覆写就好了,他们的普通函数就是动态绑定的

#include<iostream>
#include<string>
using namespace std;

class Animal
{
public:
	void speak(void)
	{
		cout << "各种各样的叫声" << endl;
	}
	virtual void vspeak()
	{
		cout << "各种各样的虚叫声 " << endl;
	}
};

class SnowLeopard : public Animal
{
public:
	void speak(void)
	{
		cout << "芝士雪豹" << endl;
	}
	virtual void vspeak()
	{
		cout << "芝士虚雪豹 " << endl;
	}
};

void AnimalSpeak(Animal & animal)
{
	animal.speak();
	animal.vspeak();
}

void AnimalSpeakTest(SnowLeopard & snowleopard)
{
	snowleopard.speak();
}

void test_1(void)
{
	SnowLeopard sl;
	AnimalSpeak(sl); /* c++中允许父子之间转换,可以自动强制转换,子类可以被强转为父类 */
	// Animal a;
	// AnimalSpeakTest(a); /* 但是父类不能被强转为子类 */
}

int main(void)
{
	test_1();
	return 0;
}

上述代码运行结果如下:
在这里插入图片描述
用法已经明白,但是为什么需要使用虚函数,以及为什么必须使用引用或者指针来实现多态并没有明白,查阅多方资料也没搞明白,这篇博客探究了为什么使用引用或者指针来实现多态,但是也没解决为什么必须使用引用或者指针的问题。

2.2 纯虚函数和抽象类

因为父类的虚函数一般不会用到(或者说,子类在调用这个虚函数的时候都会重新设计他的功能),所以父类中的虚函数就直接不写函数体了,称为纯虚函数。有纯虚函数的类称为抽象类,抽象类无法实例化对象

而继承该父类的子类必须覆写纯虚函数,否则也会变成抽象类,无法实例化对象。

#include <iostream>
#include <string>
using namespace std;

class Animal
{
public:
	virtual void vspeak(void) = 0; /* 这样的写法叫纯虚函数,没有函数体;有纯虚函数的类叫抽象类,抽象类无法实例化对象 */
};

class SnowLeopard : public Animal
{
public:
	virtual void vspeak()			/* 子类必须重写父类中的纯虚函数,否则也变成了抽象类,无法实例化对象 */
	{
		cout << "芝士虚雪豹 " << endl;
	}
};

void AnimalSpeak(Animal &animal)
{
	animal.vspeak();
}

void test_1(void)
{
	SnowLeopard sl;
	AnimalSpeak(sl); 
}

int main(void)
{
	test_1();
	return 0;
}

上述代码的运行结果如下:
在这里插入图片描述

2.3 虚析构和纯虚析构

总的来说,虚析构的作用是为了防止内存泄漏。如果子类中开辟了堆区内存,并且把堆区内存的释放放在了子类的析构函数中时,如果声明了一个指向父类类型的指针,但是赋值的时候开辟了子类类型的内存并强转为该父类指针,那么在delete该指针时,只会调用父类的析构函数,而不会调用子类的析构函数。

但是因为赋值的时候是将子类强转为了父类,所以已经调用了子类的构造函数,如果构造函数中开辟了堆区内存,并且因为无法调用子类的析构函数(如果子类开辟的内存是在析构函数中释放的话),无法释放该内存,进而导致内存泄漏。

解决办法就是使用虚析构,即把父类的析构函数变为虚析构函数,在析构函数前加关键字virtual,这样delete 父类指针时,就会动态绑定到子类类型,所以会调用子类的析构函数。

至于为什么仍然会执行父类的析构函数,这篇博客说子类的虚析构函数调用后会自动调用父类的析构函数,这个是编译器强制规定的。但是应该有更好的解释,后续继续研究下。

#include <iostream>
#include <string>
using namespace std;

class Animal
{
public:
	Animal()
	{
		cout << "Animal构造函数" << endl; 
	}

	virtual void speak(void) = 0; /* 这样的写法叫纯虚函数,没有函数体;有纯虚函数的类叫抽象类,抽象类无法实例化对象 */

	~Animal()
	{
		cout << "Animal析构函数" << endl; 
	}
};

class VAnimal
{
public:
	virtual ~VAnimal()
	{
		cout << "Animal析构函数" << endl; 
	}
};

class Cat:public Animal
{
public:

	Cat(string name)
	{
		cout << "Cat构造函数调用" <<endl;
		m_name = new string(name);
	}

	virtual void speak()
	{
		cout << *m_name << "小猫在说话" << endl;
	}

	~Cat()
	{
		if (m_name!=NULL)
		{
			cout << "Cat析构函数调用,释放堆区内存" << endl;
			delete m_name;
			m_name = NULL;
		}
	}

	string *m_name;
};

class Dog: public VAnimal
{
public:
	~Dog()
	{
		cout << "Dog析构函数调用" << endl;
	}
};

void test_1(void)
{
	/*在最后delete animal的时候,只会调用父类的析构函数,而不会调用子类的析构函数 */
	Animal * animal = new Cat("Tom"); /* Animal构造函数,Cat构造函数调用 */
	animal->speak();	/* Tom小猫在说话 */
	delete animal;		/* Animal析构函数 */

	/* 但是如果父类中的析构函数时虚函数的话,会调用子类的析构函数,并且还会调用父类的析构函数 */
	cout << endl;
	VAnimal * vanimal = new Dog;
	delete vanimal;
}

int main(void)
{
	test_1();
	return 0;
}

上述代码运行结果如下:
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值