C++学习-入门到精通【9】面向对象编程:继承

C++从入门到精通系列教程之第七篇:面向对象编程 —— 类与对象 面向对象编程是一种程序设计范式,它将数据与对数据的操作封装在一起,形成“对象”。在C++中,类是构造对象的蓝图。模块化:将复杂问题拆分成小模块,每个类负责一部分功能。封装性:数据与操作封装在一起,保护数据不被外部随意修改。继承性:允许从已有类中派生新类,实现代码复用。多态性:通过虚函数实现不同对象对同一接口的不同响应,使程序更灵活。本篇主要聚焦于类与对象的基础,帮助新手理解如何定义类、创建对象及使用其成员。类是对一类事物的抽象描述,包含数据成员和成员函数。 阅读详情

C++学习-入门到精通【9】面向对象编程:继承



一、基类与派生类

我们先从下表可以很清楚的看出基类和派生类之间的关系

基类派生类
学生本科生、研究生
形状圆、三角形、矩形、球体、立方体
贷款汽车贷款、住房贷款、抵押贷款
雇员教职员工、后勤人员
账户支票账户、储蓄账户

可以看到上图中的基类更加通用,派生类更加具体。

继承关系构成了类的层次结构。基类和它的派生类间存在这种层次关系。虽然类可以独立存在,但是一旦投入到继承关系中,它们和其他的类就有了关联。在继承关系中,类作为基类、或是派生类、或以二者兼有的身份出现。

CommunityMember类的继承层次结构

下面设计一个简单的继承层次结构,这个层次有5层。一个大学群体有数以千计的CommunityMember对象。

这些CommunityMember对象由Employee(雇员)、Student(学生)和Alumnus(校友)对象组成。Employee对象可能Faculty(教职员工)或者Staff(后勤人员),一个Faculty对象可能是Administrator(行政管理人员)或者Teacher(教师)对象。有些Administrator对象同时也可能是Teacher对象。所以这里我们使用多重继承来设计类AdministratorTeacher。多重继承会在后续章节详细介绍,但是并不建议使用多重继承。

下面是先前设计的CommunityMember类的继承层次结构的UML图。

在这里插入图片描述

上图中每个箭头表示一个is-a关系(继承)(同时还有has-a关系表示组成)。当我们跟踪这些类层次的箭头时,可以知道Employee对象是CommunityMember对象。

CommunityMember是Employee、Student和Alumnus的直接基类。是图中其他类的间接基类(在类层次中被继承了两层或以上)。

如何定义一个派生类呢

那么我们应该如何将一个类定义为另一个类的派生(从另一个类继承)呢?以以下形式进行定义,我们可以指定类Employee是由类CommunityMember派生而来的,类Employee定义的开始部分有:

class Employee : public CommunityMember

这是最常用的继承形式——public继承的一个例子。本文后续部分会继续讨论private继承和protected继承。

在各种形式的继承中,基类的private成员都不能被它的派生类直接访问,但是这些private基类成员仍被继承(派生类也包含它们)。在public继承中,基类的所有其他成员在成为派生类的成员时仍保持其原始的成员访问权限(比如,基类的public成员在派生类中仍是public成员,protected成员仍是protected成员)。那么派生类如何访问继承来的private成员呢——使用从基类继承来的成员函数来访问。注意:友元函数无法继承。

二、基类和派生类间的关系

这一节将通过一个雇员的继承层次结构来讨论基类和派生类之间的关系。这个层次结构包含了一个公司的工资发放系统所涉及的多种类型的雇员。佣金雇员(CommisisionEmployee)将被表示为基类的对象,他们的薪水完全由销售提成;带底薪佣金雇员(base-salaried commission employee)将被表示为派生类的对象,他们的薪水上底薪和销售提成组成。

下面我们将用5个例子来循序渐进地讨论佣金雇员和带底薪佣金雇员之间的关系。

1.创建并使用类CommissionEmployee

CommissionEmployee.h

#include <string>

class CommissionEmployee
{
public:
	// 参数分别为firstName、lastName、socialSecurityNumber、grossSales和commnissionRate
	CommissionEmployee(const std::string&, const std::string&,
		const std::string&, double = 0.0, double = 0.0);

	void setFirstName(const std::string&);
	std::string getFirstName() const;

	void setLastName(const std::string&);
	std::string getLastName() const;

	void setSocialSecurityNumber(const std::string&);
	std::string getSocialSecurityNumber() const;

	void setGrossSales(double);
	double getGrossSales() const;

	void setCommissionRate(double);
	double getCommissionRate() const;

	double earnings() const; // 计算工资
	void print() const; // 输出CommissionEmployee的成员
private:
	std::string firstName;
	std::string lastName; 
	std::string socialSecurityNumber; // 社保号码
	double grossSales; // 销售总额
	double commissionRate; // 提成比例
};

CommissionEmployee.cpp

#include <iostream>
#include <string>
#include <stdexcept>
#include "CommissionEmployee.h"

using namespace std;

CommissionEmployee::CommissionEmployee(const string& first, const string& last,
	const string& ssn, double sales, double rate)
	: firstName(first), lastName(last),
	socialSecurityNumber(ssn), grossSales(sales), commissionRate(rate)
{
	// 也可以不使用初始化列表进行初始化,在构造函数体中调用set成员函数来进行初始化
	// 这样可以对这些值的有效性进行检查
}

void CommissionEmployee::setFirstName(const string& first)
{
	// 还可以增加对名的有效性的限制
	firstName = first; 
}

string CommissionEmployee::getFirstName() const
{
	return firstName;
}

void CommissionEmployee::setLastName(const string& last)
{
	// 还可以增加对姓的有效性的限制
	lastName = last;
}

string CommissionEmployee::getLastName() const
{
	return lastName;
}

void CommissionEmployee::setSocialSecurityNumber(const string& ssn)
{
	// 还可以增加对姓的有效性的限制
	socialSecurityNumber = ssn;
}

string CommissionEmployee::getSocialSecurityNumber() const
{
	return socialSecurityNumber;
}

void CommissionEmployee::setGrossSales(double sales)
{
	if (sales >= 0.0)
	{
		grossSales = sales;
	}
	else
	{
		throw invalid_argument("Gross Sales must be greater than 0.0.");
	}
}

double CommissionEmployee::getGrossSales() const 
{
	return grossSales;
}

void CommissionEmployee::setCommissionRate(double rate)
{
	if (rate > 0.0 && rate < 1.0)
	{
		commissionRate = rate;
	}
	else
	{
		throw invalid_argument("Commission rate must be greater than 0.0 and smaller than 1.0.");
	}
}

double CommissionEmployee::getCommissionRate() const
{
	return commissionRate;
}

double CommissionEmployee::earnings() const
{
	return commissionRate * grossSales;
}

void CommissionEmployee::print() const
{
	cout << "commission employee: " << firstName << ' ' << lastName
		<< "\nsocial security number: " << socialSecurityNumber
		<< "\ngrossSales: " << grossSales
		<< "\ncommission rate: " << commissionRate;
}

test.cpp

#include <iostream>
#include <iomanip>
#include "CommissionEmployee.h"
using namespace std;

int main()
{
	// 实例化一个CommissionEmployee对象
	CommissionEmployee employee(
		"Peter", "Griffin", "111-22-3333", 10000, .06
	);

	// 设置浮点数格式,固定保留小数点后两位
	cout << fixed << setprecision(2);

	// 调用get成员函数获取employee的信息
	cout << "Employee information obtained by get functions: \n"
		<< "\nFirst name is " << employee.getFirstName()
		<< "\nLast name is " << employee.getLastName()
		<< "\nSocial security number is " << employee.getSocialSecurityNumber()
		<< "\nGross sales is " << employee.getGrossSales()
		<< "\nCommission rate is " << employee.getCommissionRate() << endl;

	// 设置employee的总销售额和提成比例
	employee.setGrossSales(10000);
	employee.setCommissionRate(.1);

	cout << "\nUpdated employee information output by print function: \n\n";
	employee.print();

	// 打印employee的收入
	cout << "\n\nEmployee`s earings is $" << employee.earnings() << endl;
}

运行结果:

在这里插入图片描述

2.不使用继承创建类BasePlusCommissionEmployee

下面创建一个全新的类,比上面的类多了一个数据成员:baseSalary(底薪)。

BasePlusCommissionEmployee.h

#include <string>

class BasePlusCommissionEmployee
{
public:
	// 参数分别为firstName、lastName、socialSecurityNumber、grossSales、commnissionRate和baseSalary
	BasePlusCommissionEmployee(const std::string&, const std::string&,
		const std::string&, double = 0.0, double = 0.0, double = 0.0);

	void setFirstName(const std::string&);
	std::string getFirstName() const;

	void setLastName(const std::string&);
	std::string getLastName() const;

	void setSocialSecurityNumber(const std::string&);
	std::string getSocialSecurityNumber() const;

	void setGrossSales(double);
	double getGrossSales() const;

	void setCommissionRate(double);
	double getCommissionRate() const;

	void setBaseSalary(double);
	double getBaseSalary() const;

	double earnings() const; // 计算工资
	void print() const; // 输出CommissionEmployee的成员
private:
	std::string firstName;
	std::string lastName;
	std::string socialSecurityNumber; // 社保号码
	double grossSales; // 销售总额
	double commissionRate; // 提成比例
	double baseSalary; // 底薪
};

BasePlusCommissionEmployee.cpp

#include <iostream>
#include <string>
#include <stdexcept>
#include "BasePlusCommissionEmployee.h"

using namespace std;

BasePlusCommissionEmployee::BasePlusCommissionEmployee(const string& first, const string& last,
	const string& ssn, double sales, double rate, double base)
	: firstName(first), lastName(last),
	socialSecurityNumber(ssn), grossSales(sales), commissionRate(rate),
	baseSalary(base)
{
	// 也可以不使用初始化列表进行初始化,在构造函数体中调用set成员函数来进行初始化
	// 这样可以对这些值的有效性进行检查
}

void BasePlusCommissionEmployee::setFirstName(const string& first)
{
	// 还可以增加对名的有效性的限制
	firstName = first;
}

string BasePlusCommissionEmployee::getFirstName() const
{
	return firstName;
}

void BasePlusCommissionEmployee::setLastName(const string& last)
{
	// 还可以增加对姓的有效性的限制
	lastName = last;
}

string BasePlusCommissionEmployee::getLastName() const
{
	return lastName;
}

void BasePlusCommissionEmployee::setSocialSecurityNumber(const string& ssn)
{
	// 还可以增加对姓的有效性的限制
	socialSecurityNumber = ssn;
}

string BasePlusCommissionEmployee::getSocialSecurityNumber() const
{
	return socialSecurityNumber;
}

void BasePlusCommissionEmployee::setGrossSales(double sales)
{
	if (sales >= 0.0)
	{
		grossSales = sales;
	}
	else
	{
		throw invalid_argument("Gross Sales must be greater than 0.0.");
	}
}

double BasePlusCommissionEmployee::getGrossSales() const
{
	return grossSales;
}

void BasePlusCommissionEmployee::setCommissionRate(double rate)
{
	if (rate > 0.0 && rate < 1.0)
	{
		commissionRate = rate;
	}
	else
	{
		throw invalid_argument("Commission rate must be greater than 0.0 and smaller than 1.0.");
	}
}

double BasePlusCommissionEmployee::getCommissionRate() const
{
	return commissionRate;
}

void BasePlusCommissionEmployee::setBaseSalary(double base)
{
	if (base > 0.0)
	{
		baseSalary = base;
	}
	else
	{
		throw invalid_argument("base must be greater than 0.0.");
	}
}

double BasePlusCommissionEmployee::getBaseSalary() const
{
	return baseSalary;
}

double BasePlusCommissionEmployee::earnings() const
{
	return commissionRate * grossSales + baseSalary;
}

void BasePlusCommissionEmployee::print() const
{
	cout << "commission employee: " << firstName << ' ' << lastName
		<< "\nsocial security number: " << socialSecurityNumber
		<< "\ngrossSales: " << grossSales
		<< "\ncommission rate: " << commissionRate
		<< "\nbase salary: " << baseSalary;
}

对比类BasePlusCommissionEmployee和类CommissionEmployee,我们能够贬责这个新类的定义中只有大部分都是重复代码,只有涉及到新加入的数据成员的地方才有一些变化。

使用继承时,类层次结构中所有类共同的数据成员和成员函数应该在基类中声明。当需要对这些共同特征进行修改时,只需在基类中进行修改即可,派生类会继承这些修改。如果不使用继承机制,而是采用上面的方法,当这些共同部分出错时,我们就需要对所有包含有问题代码副本的源文件进行修改。

3.创建CommissionEmployee-BasePlusCommissionEmployee继承层次结构

现在我们采用继承的方式来创建BasePlusCommissionEmployee类。它由CommissionEmployee派生而来。一个BasePlusCommissionEmployee类的对象同时也是一个CommissionEmployee类的对象。但是BasePlusCommissionEmployee中除了具有所有CommissionEmployee类对象具有的成员之外,还有一个特有的数据成员baseSalary。

使用public继承,继承了基类中除构造函数、析构函数和重载的赋值运算符函数(如果有的话)之外的所有成员,每个类都会提供特定于自己的构造函数和析构函数。

BasePlusCommissionEmployee.h

#include "CommissionEmployee.h"

class BasePlusCommissionEmployee : public CommissionEmployee
{
public:
	BasePlusCommissionEmployee(const std::string&, const std::string&,
			const std::string&, double = 0.0, double = 0.0, double = 0.0);

	void setBaseSalary(double);
	double getBaseSalary() const;
	
	// 收入发生变化,所以earnings成员函数需要重写
	double earnings() const;
	// 同样的打印函数一样需要重写
	void print() const;
private:
	double baseSalary;
};

BasePlusCommissionEmployee.cpp

#include <iostream>
#include <stdexcept>
#include "BasePlusCommissionEmployee.h"
using namespace std;

BasePlusCommissionEmployee::BasePlusCommissionEmployee(
	const string & first, const string& last, const string& ssn,
	double sales, double rate, double salary)
	: CommissionEmployee(first, last, ssn, sales, rate)
{
	setBaseSalary(salary);
}

void BasePlusCommissionEmployee::setBaseSalary(double salary)
{
	if (salary >= 0.0)
	{
		baseSalary = salary;
	}
	else
	{
		throw invalid_argument("Base salary must be greater than 0.0.");
	}
}

double BasePlusCommissionEmployee::getBaseSalary() const
{
	return baseSalary;
}

double BasePlusCommissionEmployee::earnings() const
{
	return baseSalary + commissionRate * grossSales;
}

void BasePlusCommissionEmployee::print() const
{
	cout << "commission employee: " << firstName << ' ' << lastName
		<< "\nsocial security number: " << socialSecurityNumber
		<< "\ngrossSales: " << grossSales
		<< "\ncommission rate: " << commissionRate
		<< "\nbase salary: " << baseSalary;
}

编译结果:

在这里插入图片描述

上面的构造函数中引入了基类初始化器语法,使用成员初始化器将参数传递给基类的构造函数。C++要求派生类调用其基类的构造函数来初始化继承到派生类的基类的数据成员。为此在该类的构造函数函数中在它的成员初始化器中将显式的调用了基类的构造函数。如果类BasePlusCommissionEmployee的构造函数没有显式地调用基类的构造函数,C++将会尝试隐式调用基类的默认构造函数,但是该类并没有提供默认的构造函数,且显式的提供了构造函数,所以编译器也不会提供一个无参的默认构造函数,那么就会导致错误。

派生类的构造函数只能通过成员初始化器来初始化基类中的数据成员

常见错误:在派生类构造函数调用其基类的构造函数时,如果传递给基类构造函数的参数,它的个数和类型与基类构造函数不符,将导致编译错误。

注意:大家肯定发现了在派生类中我们又重新定义了earnings和print函数,因为基类中的这两个函数并没有声明为虚函数,所以这里是进行了函数隐藏,派生类中新定义的函数隐藏了基类中的相同函数原型的函数,派生类对象直接调用该函数会调用派生类中定义的函数,如果想要调用基类中的同名函数需要显式的指定作用域,例如:Commission::earnings()

除了上面的初始化问题外,可以看到上面的类在编译是出现了错误,无法访问基类的私有成员。那么我们在派生类中要如何访问基类的私有数据成员呢?公有的成员函数(基类中的私有成员函数同样的也不能调用)。还有没有其他方法呢?有的,兄弟有的。使用protected成员访问访问说明符。

4.使用protected数据的CommissionEmployee-BasePlusCommissionEmployee继承层次结构

之前我们已经介绍了访问说明符public和private。基类的public成员在该类的体内以及程序中任何有该类或其派生类对象的句柄(即名字、引用或指针)的地方,都是可以访问的。而基类的private成员只能在该类的体内被基类的成员和友元访问。

下面我们来介绍protected说明符,它在public和private之间之间提供了一级折中的保护。为了使类BasePlusCommissionEmployee能够直接访问类CommissionEmployee的数据成员,可以在基类中将它们声明为protected。基类中的protected成员即可以在基类的体内被基类的成员和友元访问,又可以被由基类派生的任何类的成员或友元访问。

使用protected数据定义基类CommissionEmployee

将CommissionEmployee.h进行如下的修改

#include <string>

class CommissionEmployee
{
public:
	// 参数分别为firstName、lastName、socialSecurityNumber、grossSales和commnissionRate
	CommissionEmployee(const std::string&, const std::string&,
		const std::string&, double = 0.0, double = 0.0);

	void setFirstName(const std::string&);
	std::string getFirstName() const;

	void setLastName(const std::string&);
	std::string getLastName() const;

	void setSocialSecurityNumber(const std::string&);
	std::string getSocialSecurityNumber() const;

	void setGrossSales(double);
	double getGrossSales() const;

	void setCommissionRate(double);
	double getCommissionRate() const;

	double earnings() const; // 计算工资
	void print() const; // 输出CommissionEmployee的成员
protected:
	std::string firstName;
	std::string lastName; 
	std::string socialSecurityNumber; // 社保号码
	double grossSales; // 销售总额
	double commissionRate; // 提成比例
};

测试修改后的BasePlusCommissionEmployee类

test.cpp

#include <iostream>
#include <iomanip>
#include "BasePlusCommissionEmployee.h"
using namespace std;

int main()
{
	// 实例化一个BasePlusCommissionEmployee对象
	BasePlusCommissionEmployee employee(
		"Chris", "Griffin", "333-22-1111", 8000, .06, 300
	);

	// 设置浮点数格式,固定保留小数点后两位
	cout << fixed << setprecision(2);

	// 调用get成员函数获取employee的信息
	cout << "Employee information obtained by get functions: \n"
		<< "\nFirst name is " << employee.getFirstName()
		<< "\nLast name is " << employee.getLastName()
		<< "\nSocial security number is " << employee.getSocialSecurityNumber()
		<< "\nGross sales is " << employee.getGrossSales()
		<< "\nCommission rate is " << employee.getCommissionRate() 
		<< "\nBase salary is " << employee.getBaseSalary() << endl;

	// 设置employee的总销售额和提成比例
	employee.setGrossSales(1000);
	employee.setCommissionRate(.08);

	cout << "\nUpdated employee information output by print function: \n\n";
	employee.print();

	// 打印employee的收入
	cout << "\n\nEmployee`s earings is $" << employee.earnings() << endl;
}

运行结果:

在这里插入图片描述

使用protected数据的一些注意事项
因为派生类可以直接访问基类中声明为protected的数据成员,所以派生类可以免去调用函数来设置或获取这些数据成员的额外开销。所以继承protected数据成员会使程序的性能稍稍提升。

但是,使用protected数据将会产生以下两个问题:

  • 派生类对象不必使用成员函数设置protected数据成员的值,因此,派生类很容易将无效的值赋给基类的protected数据,导致对象处于不一致的状态。例如,为基类的数据成员grossSales赋一个负值。
  • 派生类成员函数很可能太依赖基类的实现,派生类应该只依赖于基类提供的服务(非private的成员函数),而不是基类的实现。在派生类中使用基类的protected数据时,如果修改了基类的实现,那么很有可能还需要修改所有直接使用了protected数据成员的派生类,这极大的降低了代码的可维护性。

5.使用private数据的CommissionEmployee-BasePlusCommissionEmployee继承层次结构

下面对之前使用的代码进行优化:

将基类CommissionEmployee中的数据成员改回private的数据成员。

CommissionEmployee.cpp,修改了构造函数、earnings和print函数的实现

#include <iostream>
#include <string>
#include <stdexcept>
#include "CommissionEmployee.h"

using namespace std;

CommissionEmployee::CommissionEmployee(const string& first, const string& last,
	const string& ssn, double sales, double rate)
	: firstName(first), lastName(last),
	socialSecurityNumber(ssn)
{
	// 调用成员函数来设置销售总额和提成比例,在设置之前进行有效性检查
	setGrossSales(sales);
	setCommissionRate(rate);
}

void CommissionEmployee::setFirstName(const string& first)
{
	// 还可以增加对名的有效性的限制
	firstName = first; 
}

string CommissionEmployee::getFirstName() const
{
	return firstName;
}

void CommissionEmployee::setLastName(const string& last)
{
	// 还可以增加对姓的有效性的限制
	lastName = last;
}

string CommissionEmployee::getLastName() const
{
	return lastName;
}

void CommissionEmployee::setSocialSecurityNumber(const string& ssn)
{
	// 还可以增加对姓的有效性的限制
	socialSecurityNumber = ssn;
}

string CommissionEmployee::getSocialSecurityNumber() const
{
	return socialSecurityNumber;
}

void CommissionEmployee::setGrossSales(double sales)
{
	if (sales >= 0.0)
	{
		grossSales = sales;
	}
	else
	{
		throw invalid_argument("Gross Sales must be greater than 0.0.");
	}
}

double CommissionEmployee::getGrossSales() const 
{
	return grossSales;
}

void CommissionEmployee::setCommissionRate(double rate)
{
	if (rate > 0.0 && rate < 1.0)
	{
		commissionRate = rate;
	}
	else
	{
		throw invalid_argument("Commission rate must be greater than 0.0 and smaller than 1.0.");
	}
}

double CommissionEmployee::getCommissionRate() const
{
	return commissionRate;
}

double CommissionEmployee::earnings() const
{
	// 调用get成员函数来获取数据成员
	// 当修改这些数据成员的名字时,不需要修改该函数的实现
	return getCommissionRate() * getGrossSales();
}

void CommissionEmployee::print() const
{
	// 同样的调用成员函数来访问数据成员
	cout << "commission employee: " << getFirstName() << ' ' << getLastName()
		<< "\nsocial security number: " << getSocialSecurityNumber()
		<< "\ngrossSales: " << getGrossSales()
		<< "\ncommission rate: " << getCommissionRate();
}

BasePlusCommissionEmployee.cpp

#include <iostream>
#include <stdexcept>
#include "BasePlusCommissionEmployee.h"
using namespace std;

BasePlusCommissionEmployee::BasePlusCommissionEmployee(
	const string & first, const string& last, const string& ssn,
	double sales, double rate, double salary)
	: CommissionEmployee(first, last, ssn, sales, rate)
{
	setBaseSalary(salary);
}

void BasePlusCommissionEmployee::setBaseSalary(double salary)
{
	if (salary >= 0.0)
	{
		baseSalary = salary;
	}
	else
	{
		throw invalid_argument("Base salary must be greater than 0.0.");
	}
}

double BasePlusCommissionEmployee::getBaseSalary() const
{
	return baseSalary;
}

double BasePlusCommissionEmployee::earnings() const
{
	// 使用作用域分辨运算符指定基类的作用域来调用在派生类中被隐藏的基类的earnings函数
	return getBaseSalary() + CommissionEmployee::earnings();
}

void BasePlusCommissionEmployee::print() const
{
	cout << "base-salaried ";
	CommissionEmployee::print();
	cout << "\nbase salary: " << getBaseSalary();
}

提示
利用成员函数访问数据成员的值可能比直接访问这些数据稍慢些。但是,如今优化的编译器经过精心设置,可以隐式地执行许多优化工作(例如,把设置和获取成员函数的调用进行内联)。所以,程序员应该致力于编写出符合软件工程原则的代码,而将优化问题留给编译器去做。一条好的准则是:不要怀疑编译器。

可以使用上面相同的测试代码来进行测试,看看是否输出相同的结果。

三、派生类中的构造函数和析构函数

从上面的例子中我们可以看出,当我们要实例化一个派生类的对象时,会调用一连串的构造函数。其中派生类的构造函数在执行它自己的任务之前,先显式地(通过成员初始化器)或隐式地(调用基类的默认构造函数)调用其直接基类的构造函数。同样如果该基类也是从其他类派生而来的,则该基类构造函数需要它的基类的构造函数。在这个构造函数调用链中,调用的最后一个构造函数是在继承层次结构中最顶层的构造函数。
例如,在我们前面研究的CommissionEmployee-BasePlusCommissionEmployee继承层次结构中,当程序要创建类BasePlusComissionEmployee的对象时,将调用类CommissionEmployee的构造函数。因为类CommissionEmployee是这个继承层次结构中的基类,所以它的构造函数先执行,所初始化的CommissionEmployee的private数据成员同时也是类BasePlusCommissionEmployee对象的一部分。当类CommissionEmployee的构造函数执行完毕时,它将程序控制权还给类BasePlusCommissionEmployee的构造函数,后者再初始化该类对象的数据成员baseSalary。
派生类的对象在创建时,最先执行的是继承层次结构顶层的类的构造函数。

当销毁派生类的对象时,程序将调用对象的析构函数。这又将展开一连串的析构函数的调用,其中派生类的析构函数,派生类的直接基类、派生类的间接基类及类的成员的析构函数,会按照它们的构造函数执行次序相反顺序依次执行。当调用派生类对象的析构函数时,该析构函数执行其任务,然后调用继承层次结构中上一层基类的析构函数。重复进行这一过程,直到继承结构顶层的最后一个基类的析构函数被调用。之后,该对象就从内存中删除了。

构造函数和析构函数的执行顺序
假设我们创建一个派生类对象,这个派生类及其基类中都包含(通过组成)其他类的对象。当这个派生类的对象被创建时,首先执行的是基类成员对象的构造函数,然后执行基类构造函数的函数体,接着执行派生类成员对象的构造函数,最后执行派生类构造函数的函数体。派生类对象析构函数函数的调用与相应的构造函数的调用顺序正好相反。

C++11:继承基类的构造函数

C++11中,派生类可以继承基类的构造函数。只要在派生类中定义中包含下面的声明:
using BaseClass::BaseClass;
上述声明中的BaseClass表示基类的名称。除了下列的少数例外情况外,对于基类的每个构造函数,编译器都生成一个派生类构造函数,它调用相应的基类构造函数。生成的构造函数对派生类新增的数据成员只执行默认的初始化。在继承构造函数时:

  • 默认情况下,每个继承而来的构造函数和它相应基类构造函数具有相同的访问级别(public、protected和private);
  • 缺省构造函数、拷贝构造函数和移动构造函数不被继承;
  • 如果在基类构造函数的原型中放置 = delete而在基类中删除这个构造函数,那么在派生类中相应的构造函数也被删除;
  • 如果派生类没有显式地定义构造函数,那么编译器在派生类生成一个默认构造函数,即使它从基类继承了其他构造函数;
  • 如果显式地定义在派生类的构造函数和基类的构造函数具有相同的形参列表,那么该基类的构造函数不被继承;
  • 基类构造函数的默认实参是不被继承的,编译器会在派生类中生成重载的构造函数。例如,如果基类声明了如下的构造函数:
    BaseClass(int = 0, double = 0.0);
    那么编译器生成如下的两个没有默认实参的派生类构造函数:
    DerivedClass(int);
    DerivedClass(int, double);
    这两个构造函数都调用这个指定默认实参的BaseClass构造函数。(不指定参数时,调用派生类DerivedClass的默认构造函数,如果没有显式定义,编译器自动生成,调用基类的默认构造函数进行初始化,所以执行起来效果相同。)

四、public、protected和private继承

从基类派生出一个类时,继承基类的方式有三种,即public继承、protected继承和private继承。下图总结了每种继承方式下,在派生类中对基类成员的可访问性。

在这里插入图片描述

当采用public继承派生一个类时,基类的public成员成为派生类的public成员,基类的protected成员成为派生类中的protected成员,基类的private成员对派生类隐藏;

当采用protected继承派生一个类时,基类的public和protected成员都变成派生类的protected成员,基类的private成员对派生类隐藏;

当采用private继承派生一个类时,基类的public和protected成员都变成派生类中的private成员,基类的private成员对派生类隐藏;

派生类永远不能直接访问基类的private成员,继承会将保护级别低于它的成员升级到保护级别相同(protected继承会将public的成员升级成protected,private继承是将成员升级成private,它仍在派生类中可以被访问,所以可以被友元函数访问。而private成员不论什么继承方式,都是被隐藏,无法通过友元函数访问。)

private和protected继承不满足 “is-a” 的关系
“is-a”关系的核心在派生类对象可以被当作基类对象使用,如果继承方式不是public,那么派生类无法使用基类替代,因为基类的接口在派生类中可能不可访问(工具函数等)。

C++ 继承深度解析:面向对象编程的核心特性 继承是面向对象编程(OOP)中一个重要的机制,它允许一个类(派生类)继承另一个类(基类)的成员(属性和方法)。继承有助于代码复用和逻辑组织,使得程序具有更强的扩展性和可维护性。在 C++ 中,继承遵循"is-a"关系。即:如果类 B 从类 A 继承,那么 B 是 A 的一种特殊类型。例如,Cat类可以继承Animal类,因为猫是动物的一种。继承是 C++ 面向对象编程的基石之一,它提供了代码复用、扩展性和抽象能力。然而,继承并不是一个万能的工具,过度使用继承可能导致设计复杂化、耦合度过高和维护困难。 阅读详情

相关推荐

以下是C++从入门到精通的学习路径及实践方法,结合系统学习框架与实战经验,帮助普通人和程序员高效掌握并开发第一个产品

通过以上路径,普通人可在6-12个月掌握C++基础并开发小型应用,程序员可缩短至3-6个月。:坚持编码实践,从“模仿”到“创造”,逐步积累项目经验。

欢乐打代码! 862

C++学习路线:从基础到精通

从基础语法到面向对象,掌握STL与内存管理,进阶模板与多线程,实战项目提升能力,持续学习新标准与优秀代码,成为C++高手。

byte轻骑兵的技术小窝 1万+

《Visual C++开发从入门到精通》完整实例代码包

C++是Bjarne Stroustrup于1983年在贝尔实验室开始设计的一种编程语言,最初被称为C with Classes,之后逐渐演化成我们今天所熟知的C++。作为一种静态类型、编译式、通用的编程语言,C++支持多种编程范式,包括过程化、面向对象和泛型编程。// 启动引擎的方法实现代码编辑器是开发者日常工作的重要工具,而Visual Studio允许你对其高度自定义。设置编辑器选项: 可以自定义字体、颜色主题、缩进等编辑器设置,以适应个人偏好。

weixin_29859471的博客 817

如何学懂C++语言:C++从入门到精通的全面指南(完整C++学习笔记)

如何学懂C++语言:C++从入门到精通的全面指南(完整C++学习笔记)

编程技术探索者,分享C/C++、C#、Java、数据库等开发经验,聚焦实战技巧与AI兴趣,助力编程爱好者成长。 7131

C++编程新手到高手:CS42Cpp课程学习之路

本文还有配套的精品资源,点击获取 简介:"CS42Cpp"课程设计为初学者提供了一个系统的C++学习路径,涵盖了从基础知识到高级概念的全面内容。课程分为两个部分:前4周的基础阶段,后16周的深入阶段,逐渐引导学员掌握C++的语法、面向对象编程、高级特性如模板和STL使用。课程利用"CS42Cpp-master"资源包提供实践材料,帮助学员通过实际编码提高编程技能和解决问题的...

weixin_35982453的博客 1603

ScottPlot vs LiveCharts:WPF图表库性能对比与MVVM适配方案

本文深度对比了WPF图表库ScottPlot与LiveCharts在性能与MVVM架构适配上的表现。通过十万级数据点的基准测试,ScottPlot在渲染速度与内存占用上优势显著。文章重点探讨了两种库在MVVM模式下的集成方案,为开发者根据项目需求(如实时监控或快速原型)进行技术选型提供了实战指导。

lll78的博客 245

【C++】面向对象编程的三大特性:深入解析继承机制

本文将深入解析面向对象编程的三大核心特性,特别是对继承机制的详细探讨。通过对这些概念的优化理解,帮助读者更好地掌握面向对象编程的精髓。

专注于 C/C++ 后端开发,涵盖基础语法、Linux系统与网络、MySQL数据库、常见算法及项目经验。 3709

拆解歌尔2718硅麦:2.75mm超小封装背后的MEMS技术解析

本文深度解析歌尔2718硅麦的2.75mm超小封装技术,探讨MEMS麦克风在微型化过程中的声学性能与结构可靠性平衡。通过ASIC芯片堆叠、纳米级应力控制等创新技术,歌尔2718在TWS耳机和智能手机中实现卓越性能,同时克服微型化带来的声学挑战。

weixin_28292761的博客 610

C++学习-入门到精通【10】面向对象编程:多态性

文章中详细介绍了C++中多态性的原理

PLKDTHFS的博客 2768

WinForm轻松打造AntDesign现代化工具栏

摘要: 本文介绍如何在WinForm中利用AntdUI库实现AntDesign风格工具栏,包括安装配置、动态生成按钮/下拉菜单、布局优化及主题美化。通过AntButton、AntDropdown等控件结合事件绑定,可快速构建支持图标、工具提示、权限控制的现代化工具栏,并解决常见图标加载、布局错乱问题。最后提供代码示例与响应式设计技巧,助你打造高效美观的WinForm界面。 (字数:150)

java专栏 1590

Windows 11下WSL2安装CUDA避坑指南:从驱动版本匹配到环境变量配置

本文详细解析了在Windows 11的WSL2中搭建CUDA开发环境的完整流程与核心避坑要点。关键在于理解“驱动在Windows,工具链在Linux”的独特架构,并确保显卡驱动与CUDA Toolkit版本严格匹配。指南涵盖了从驱动验证、WSL-Ubuntu专用仓库安装、环境变量配置到GCC版本冲突、GPU算力不匹配等常见问题的解决方案,帮助开发者高效构建稳定的深度学习与高性能计算环境。

smartcontract5的博客 466

C++ 学习路线:从入门到精通,全面掌握 C++ 编程

C++ 是一门强大且灵活的编程语言,适合各种类型的开发项目。通过遵循本文提供的学习路线,你将能够逐步掌握 C++ 的核心概念和高级特性,并最终成为一名熟练的 C++ 开发者。

不会编程的码农研究僧 2828

多模型对抗性审查:用AI辩论提升技术文档质量

在软件工程领域,技术文档是团队协作和项目成功的关键基础。传统的文档评审依赖人工经验,存在盲区和效率瓶颈。通过引入多模型对抗性审查机制,不同大语言模型从各自视角对文档进行批判性分析,模拟现实中的专家评审过程。这种技术方法的核心价值在于利用模型间的思维差异,自动发现单个视角容易忽略的边界情况、逻辑漏洞和隐含假设。在应用场景上,特别适合产品需求文档和技术规格书这类需要多角度审视的复杂文档。adversarial-spec工具通过Claude协调多模型辩论,实现从概念到实现的完整文档质量保障,显著提升文档的完整性和

weixin_42531579的博客 379

C++面向对象编程快速入门

C++面向对象编程快速入门 以快速读懂代码为目的,概念与其他面向对象语言通用 作者:周捷 zhoujie@micron.com foxzhou@shu.edu.cn 吐血原创,侵权请客,没有科班学过,菜鸟码农 ​ 针对学过一点C没学过C++的人,请注意这里的代码只是为了举例说明原理,精简了很多东西,并不可以被编译的 目录 文章目录C++面向对象编程快速入门目录part I1. 什么是类...

weixin_43240387的博客 2065

数值计算方法 matlab用二分法或简单迭代法求_快速求解方程的根——二分法与牛顿迭代法

今天是周四高等数学专题的第7篇文章。之前的文章和大家聊了许多数学上的理论,今天和大家聊点有用的东西。我们都知道,工业上的很多问题经过抽象和建模之后,本质还是数学问题。而说到数学问题就离不开方程,在数学上我们可以用各种推算、公式,但是有没有想过在计算机领域我们如何解一个比较复杂的方程?如果之前没有想过,那你可能得想一想,因为以后很有可能会在面试题当中遇到。二分法我们要介绍的第一个方法是二分法。说到二...

weixin_39945915的博客 1303

从USB 2.0到USB 3.x:Synopsys SVT USB VIP配置避坑与接口选择指南

本文详细解析了从USB 2.0到USB 3.x的Synopsys SVT USB VIP配置要点,涵盖协议版本选择、物理接口匹配及UVM层次化架构实现。重点介绍了layering sequence的配置技巧和常见问题解决方案,帮助验证工程师高效避坑并优化验证流程。

weixin_42653691的博客 238

Cpp入门基础

定义命名空间,需要使⽤到namespace关键字,后⾯跟命名空间的名字,然后接⼀对{}即可,{}中 即为命名空间的成员。命名空间中可以定义变量/函数/类型等。namespace本质是定义出⼀个域,这个域跟全局域各⾃独⽴,不同的域可以定义同名变量,所以下 ⾯的rand不在冲突了。C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/ 类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。

2401_86658278的博客 5106

halcon3d点云补全方法

属于halcon实例中的内容,后面还接了一部分的匹配

Tianwen_running的博客 1231

如何学习C++:从入门到精通的实用指南

学习C++可能是一段艰难但充满回报的旅程。通过合理的学习计划和实践,你可以逐步掌握C++的基础和高级知识,并将其应用到实际项目中。记住,编程不仅仅是学习语法,更是培养解决问题的能力和编写高质量代码的技巧。坚持学习和探索,你将能够在C++领域成为一名出色的开发者。

热爱技术 1万+

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

《C++》继承完全指南:从入门到精通

C++中继承的详解!希望我能继承各位的运气(doge)

Maysheeo的博客 912

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

【c++】cpp类的继承

c++类的继承

hutaotaotao的博客 2297

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

C++ 全栈学习路线指南:从入门到精通

C++ 学习是一场马拉松,而非短跑。起步:扎实掌握语法与内存模型。进阶:精通 STL、OOP 与现代特性。高阶:驾驭模板元编程、并发设计与系统架构。卓越:通过海量实战与开源贡献,形成自己的技术哲学。遵循此路线,保持好奇心与持续学习的态度,你将能够驾驭这门强大而优雅的语言,成为构建未来数字世界的基石工程师。

专注于前沿技术的探索与实践,热衷于从代码逻辑、架构设计到行业应用等多维度深挖技术本质。期待在技术之路上与你共同成长~ 2476

C++面向对象编程学习

本文适合学习了C++基础的朋友学习面向对象编程这块内容,不适合小白哟。

2401_83624776的博客 1903

C++从入门到精通,超全学习路线,配学习网站

这是一个详细的的 C++ 学习路线,涵盖了基础到高级的内容,帮助你系统地掌握 C++ 编程语言及其应用。

zxsdyyds的博客 3953

C++学习路径:从入门到精通的理论与实战指南

C++作为一种高效、灵活的编程语言,在软件开发、系统编程、游戏开发等领域占据着重要的地位。无论你是编程新手,还是希望提升技能的开发者,学习C++都能为你打开更多职业发展和项目实践的大门。本文将为你提供一份全面的C++学习路径,助你从基础逐步进阶到高级应用。C++的学习路径从基础语法到高级应用,涵盖了众多知识点和技能点。通过系统地学习每个阶段的内容,并结合实战项目进行练习,可以逐步掌握C++的核心技术和应用方法。

zwLoneranger的博客 1123

C++从入门到精通

C++是一门功能丰富且灵活的编程语言,适用于各种应用领域,包括系统编程、游戏开发、嵌入式系统和科学计算等。它的多样化特性使得开发者可以根据项目需求选择合适的编程风格和技术。GitHub上有许多适合新手学习的C++开源项目,这些项目的代码质量通常较高,并且具有教育性质。

代码大师麦克拉瑞的博客 1357
上一篇: 一些运算符重载的例子
下一篇: C++学习-入门到精通【10】面向对象编程:多态性
YKPG
博客等级 码龄6年 344粉丝 37原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值