C++学习笔记(第13章->类继承)

本文深入探讨面向对象编程的核心概念,重点介绍C++中类的继承机制,包括虚函数、多态性、派生类与基类之间的关系及交互,通过实例演示如何利用类库和继承来提升代码复用性和灵活性。

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

如果你不努力,一年后的你还是原来的你,只不过老了一岁。-----for myself
还是老话,面向对象编程的主要目的之一就是提供可重用的代码,尤其是开发大工程时,重用经过测试的代码比重新编写要好的多。C有函数库,C++则提供了更高层次的重用性,类库。我们可以通过已有的类库中的类,不必通过对其源码修改,而是通过类继承。即通过已有的类派生出新的类,而派生类继承了原有类的特征。
创建派生类的时候,程序首先调用基类的构造函数,然后在调用派生类的构造函数,基类的构造函数负责初始化继承的数据成员,派生类的构造函数主要用于初始化新增的数据成员。派生类的构造函数总是调用基类的构造函数,可以通过初始化列表句法指明要使用的基类构造函数,否则,将使用基类的默认构造函数。
派生类对象过期时,程序将首先调用派生类析构函数,然后在调用基类的析构函数。

1.派生类与基类之间的特殊关系

(1)派生类对象可以直接使用基类的方法,条件是方法不是私有的.
(2) 基类指针可以在不进行显示类型转换的情况下,指向派生类对象.基类引用也是如此.不过基类指针和引用只能调用基类的方法.

2.继承---is-a关系

C++有3中继承方式:公有继承,私有继承,保护继承.公有继承是最常用的继承方式,他建立一种is-a关系.公有继承不建立is-like-a,is-implemented-as-a,uses-a关系.

3.多态的公有继承

(1)在派生类中重新定义基类的方法.
(2)使用虚方法。
下面我们来看个例子,基类Brass和派生类BrassPlus.
#ifndef _BRASS_H_
#define _BRASS_H_
class Brass
{
private:
	enum{MAX = 35};
	char fullname[MAX];
	long acctNum;
	double balance;
public:
	Brass(const char* s = "Nullbody", long an = -1, double bal = 0.0);
	void Deposit(double amt);
	virtual void Withdraw(double amt);
	double balance()const;
	virtual void ViewAcct()const;
	virtual ~Brass(){}
};
class BrassPlus:public Brass
{
private:
	double maxLoan;
	double rate;
	double owesBank;
public:
	BrassPlus(const char * s = "Nullbody", long an = -1,double bal = 0.0, double ml = 500, double r = 0.10);
	BrassPlus(const Brass & ba, double ml = 500, double r = 0.1);
	virtual void ViewAcct()const;
	virtual void Withdraw(double amt);
	void ResetMax(double m){maxLoan = m;}
	void ResetRate(double r){rate = r;}
	void ResetOwes(){owesBank = 0;}
};
#endif
以上可以看到,
A. ViewAcct()和Withdraw都使用了新关键字,virtual,这些方法被称为虚方法.基类和派生类对这两个方法都有各自的定义.如果这两个方法是通过指针或者引用来调用,他能够根据指针和引用所指向的对象的类型来选择方法.如果没有virtual关键字声明,程序将根据指针或者引用的类型(不是指向对象的类型,注意)来选择方法.
如下,假如声明了virtual,pa将调用Brass的withdraw方法,pb调用BrassPlus的withdraw方法.否则,pa,pb都只调用Brass的方法.
Brass a;
BrassPlus b;
Brass &pa = a;
Brass &pb = b;
pa.Withdraw();
pb.Withdraw();
B.基类声明了一个虚拟析构函数,这样是为了确保释放派生类对象时,按正确的顺序调用析构函数.
记住:如果在派生类中重新定义基类的方法,通常应将基类的方法声明为虚拟的.这样,程序将根据对象类型而不是引用或指针的类型来选择版本.为基类声明一个虚拟析构函数也是一种惯例.
我们在来看一下类实现
#include<iostream>
#include<cstring>
#include"brass.h"
using namespace std;
Brass::Brass(const char *s, long an, double bal)
{
	strncpy(fullname, s, MAX-1);
	fullname[MAX-1] = '\0';
	acctNum = an;
	balance = bal;
}
void Brass::Deposit(double amt)
{
	if(amt < 0)
		cout<<"Negative deposit is not allowed;deposit is cancelled.\n";
	else
		balance += amt;
}
void Brass::Withdraw(double amt)
{
	if(amt < 0)
		cout<<"withdraw amount must be positive,withdraw cancelled.\n";
	else if(amt <= balance)
		balance -= amt;
	else
		cout<<"withdraw amout of $"<<amt<<"exceed your balance.\n,withdraw cancelled.\n";
}
double Brass::Balance()const
{
	return balance;
}
void Brass::ViewAcct()const
{
	cout<<"client: "<<fullname<<endl;
	cout<<"Accout number: "<<acctNum<<endl;
	cout<<"Balance:$"<<balance<<endl;
}
BrassPlus::BrassPlus(const char *s, long an, double bal, double ml, double r):Brass(s, an, bal)
{
	maxLoan = ml;
	owesBank = 0.0;
	rate = r;
}
BrassPlus::BrassPlus(const Brass & ba, double ml, double r):Brass(ba)
{
	maxLoan = ml;
	owesBank = 0.0;
	rate = r;
}
void BrassPlus::ViewAcct()const
{
	Brass::ViewAcct();
	cout<<"Maximum loan:$"<<maxLoan<<endl;
	cout<<"owed to bank:$"<<owesBank<<endl;
	cout<<"Loan Rate:"<<100*rate<<"%\n"<<endl;
}
void BrassPlus::Withdraw(double amt)
{
	double bal = Balance();
	if(amt < bal)
		Brass::Withdraw();
	else if(amt <= bal+maxLoan - owesBank)
	{
		double advance = amt - bal;
		owesBank += advance * (1.0 + rate);
		cout<<"Bank advace:$"<<advance<<endl;
		cout<<"Fiance charge:$"<<advance *rate<<endl;
		Deposit(advance);
		Brass::Withdraw(amt);
	}
	else
		cout<<"credit limit exceeded,transation cancelled.\n";
}
派生类不能直接访问基类的私有数据,而必须使用基类的公有方法才能访问这些数据.派生类构造函数在初始化基类私有数据时,采用的是成员初始化列表句法.将基类的信息传递给基类的构造函数,然后使用构造函数体初始化派生类的新增成员.
注意,非构造函数不能使用初始化列表句法.
在派生类中,必须使用作用域解析操作符来调用基类的虚方法,假如想下面编写,将会产生递归:
void BrassPlus::ViewAcct()const
{
    ...
    ViewAcct();//recursive call
    ...
}
假如不是虚方法,派生类没有重新定义该方法,则不必加作用域解析操作符。例如Balance()的调用。
最后,我们再来看下测试文件,了解虚方法的行为是怎么样的。
#include<iostream>
#include"brass.h"
const int CLIENT = 4;
const int LEN = 40;
int main()
{
	using namespace std;
	Brass *p_client[CLIENT];
	int i;
	for(i = 0; i < CLIENT; i++)
	{
		char temp[LEN];
		long tempnum;
		double tempbal;
		char kind;
		cout<<"enter client's name: ";
		cin.getline(temp, LEN);
		cout<<"enter cilent's account number: ";
		cin>>tempnum;
		cout<<"enter openning balance: $";
		cin>>tempbal;
		cout<<"enter 1 for Brass Account or"<<"2 for BrassPlus Account:";
		while(cin>>kind && (kind != '1' && kind != '2'))
			cout<<"enter either 1 or 2: ";
		if(kind == '1')
			p_client[i] = new Brass(temp, tempnum, tempbal);
		else
		{
			double tmax, trate;
			cout<<"enter the overdraft limit:$";
			cin>>tmax;
			cout<<"enter thr interest rate as a decimal fraction: ";
			cin>>trate;
			p_client[i] = new BrassPlus(temp, tempnum, tempbal, tmax, trate);
		}
		while(cin.get() != '\n')
			continue;
	}
	cout<<endl;
	for(i = 0; i < CLIENT; i++)
	{
		p_client[i]->ViewAcct();
		cout<<endl;
	}
	for(i = 0; i < CLIENT; i++)
	{
		delete p_client[i];
	}
	cout<<"done.\n";
	while(1);
	return 0;
}
多态特性是由下述代码提供的:
	for(i = 0; i < CLIENT; i++)
	{
		p_client[i]->ViewAcct();
		cout<<endl;
	}
如果数组成员指向Brass对象,则调用Brass::ViewAcct();如果指向的是BrassPlus对象,则调用BrassPlus::ViewAcct()。原因是这个方法是虚方法并且派生类重新定义了。
我们再看看为何需要虚析构函数?
测试文件中,使用delete释放有new分配的对象代码说明了为何需要,如果析构函数不是虚拟的,则将只调用对应于指针类型的析构函数,也就是说上面delete只调用Brass的析构函数即使指向的是BrassPlus。所以,如果析构函数是虚拟的,将调用相应对象类型的析构函数。因此,如果指针指向的是BrassPlus对象,将调用BrassPlus的析构函数,然后自动调用基类的析构函数。因此,使用虚拟析构函数可以保证正确的析构函数序列被调用。

4.静态联编和动态联编

将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。C/C++编译器可以在编译过程中完成这种联编,在编译过程中进行联编被称为静态联编(static binding),又称早期联编。但是,虚函数使得这项工作变得困难,因为使用哪一个函数在编译过程中不能确定,因为编译器不知道用户将选择哪种类型的对象。所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(dynamic binding),又称晚期联编。

4.1指针和引用类型兼容性

向上强制转换(upcasting):将派生类引用或指针准换为基类引用或者指针。相反过程,将基类指针或引用转换为派生类引用或指针(需使用显式类型转换),成为向下强制转换。

4.2虚拟成员函数和动态联编

(1)编译器对非虚方法使用静态联编。
(2)编译器对虚方法使用动态联编。
(3)为什么需要这两种类型的联编和为什么默认使用静态联编呢?
答案是:首先,静态联编效率更高;其次,不需要重新定义该函数假如非虚方法。派生类不需要重新定义函数的话,不需要动态联编。
(4)如果要在派生类中重新定义基类的方法,则将他设置为虚方法。否则,设置为非虚方法。

4.3虚函数工作原理

编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址的数组指针,这种数组被称为虚函数表(virtual function table,vtbl)。虚函数表存储了为每个类对象进行声明的虚函数的地址。调用虚函数,首先查看存储在对象中的vtbl地址,然后转到相应的函数地址表。
使用虚函数,在内存和执行速度方面有一定的成本,包括:
(1)每个对象都将增大,增大量为存储地址的空间。
(2)对每个类,编译器都将创建一个虚函数地址表(数组)。
(3)每个函数调用都需要执行一步额外的操作,即到表中查找地址。

4.4有关虚函数的注意事项

(1)在基类方法中的声明中使用virtual关键字,使该方法在基类以及所有的派生类中是虚拟的。
(2)如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用类型或指针类型定义的方法。这种称为动态联编。
(3)如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚拟的。
(4)析构函数应该是虚拟d,除非类不用做基类。为什么要使用虚析构函数,上面有提到原理,这里不再重复。
(5)给类定义一个虚析构函数并非错误,即使这个类不用做基类。只是效率低了。通常应该给基类提供一个虚拟析构函数,即使他并不需要析构函数。
(6)友元不能是虚函数,因为友元不是类成员,而只有成员才能使虚函数。
(7)重新定义隐藏方法,
class Dwelling
{
    public:
        virtual void showperks(int a)const;
         ....
};
class Hovel:public Dwelling
{
    public:
    virtual void showperks()const;
    ....
};
由此可见,Hovel的方法将会覆盖掉Dwelling的方法。所以:
Hovel hh;
hh.showperks();//valid
hh.showperks(7);//invalid
所以,如果重新定义该方法,应确保与原来的原型相同。但如果返回的是基类引用或指针,则可以修改为指向派生类的引用或指针。这种特性被称为返回类型协变。这只使用与返回值。所以,如果基类的声明被重载了,应该重载所有的基类版本。

5访问控制:protected

private 和 protected的区别只有在基类派生类中才会表现出来,派生类可以直接访问基类的保护成员,但不能直接访问基类的私有成员。但是,最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时,通过基类方法使派生类能够访问基类数据。

6.抽象基类

简单用例子来说明,打个比方,园和椭圆,抽离出他们的共性ABC,然后把ABC作为抽象基类,圆和椭圆都可以从ABC派生出来,方便基类指针进行管理这两个对象。C++通过使用纯虚函数提供未实现的函数,纯虚函数在结尾处为=0。当类声明中包含纯虚函数时,则不能创建该类的对象。这里的理念是:包含纯虚函数的类只用作基类。
总之,在原型中,使用=0指出类是一个抽象基类,在类中可以不定义也可以定义该函数。
我们再来把前面的例子,来修改一下
#ifndef _ACCTABC_H_
#define _ACCTABC_H_
class AcctABC
{
private:
	enum{MAX = 35};
	char fullname[MAX];
	long acctNum;
	double balance;
protected:
	const char * FullName()const{return fullname;}
	long AcctNum()const{return acctNum;}
public:
	AcctABC(const char *s = "NullBody", long an = -1, double bal = 0.0);
	void Deposit(double amt);
	virtual void WithDraw(double amt)=0;//pure virtual function
	double Balance()const{return balance;}
	virtual void ViewAcct()const = 0;//pure virtual function
	virtual ~AcctABC(){}
};
class Brass:public AcctABC
{
public:
	Brass(const char *s = "nullbody", long an = -1, double bal = 0.0):AcctABC(s, an, bal){}
	virtual void WithDraw(double amt);
	virtual void ViewAcct()const;
	virtual ~Brass(){}
};
class BrassPlus :public AcctABC
{
private:
	double maxLoan;
	double rate;
	double owesBank;
public:
	BrassPlus(const char *s = "nullbody", long an = -1, double bal = 0.0,double ml = 500, double r = 0.10);
	BrassPlus(const Brass & ba, double ml = 500, double r = 0.1);
	virtual void ViewAcct()const;
	virtual void WithDraw(double amt);
	void ResetMax(double m){maxLoan = m;}
	void ResetRate(double r){rate = r;}
	void ResetOwes(){owesBank = 0;}
};
#endif
#include<iostream>
#include<cstring>
using namespace std;
#include"acctABC.h"
AcctABC::AcctABC(const char * s, long an, double bal)
{
	strncpy(fullname, s, MAX-1);
	fullname[MAX-1] = '\0';
	acctNum = an;
	balance = bal;
}

void AcctABC::Deposit(double amt)
{
	if(amt < 0)
	{
		cout<<"negative deposit not allowed;deposit is cancelled.\n";
	}
	else
		balance += amt;
}

void AcctABC::WithDraw(double amt)
{
	balance -= amt;
}

void Brass::WithDraw(double amt)
{
	if(amt < 0)
	{
		cout<<"negative deposit not allowed;deposit is cancelled.\n";
	}
	else if(amt <= Balance())
		AcctABC::WithDraw(amt);
	else
		cout<<"withdraw amount of $"<<amt<<"exceeded your balance.\nwithdraw cancelled";
}

void Brass::ViewAcct()const
{
	cout<<"Brass client: "<<FullName()<<endl;
	cout<<"Account Number: "<<AcctNum()<<endl;
	cout<<"Balance:$ "<<Balance()<<endl;
}

BrassPlus::BrassPlus(const Brass & ba, double ml, double r):AcctABC(ba)
{
	maxLoan = ml;
	owesBank = 0.0;
	rate = r;
}

BrassPlus::BrassPlus(const char *s, long an, double bal, double ml, double r):AcctABC(s, an, bal)
{
	maxLoan = ml;
	owesBank = 0.0;
	rate = r;
}

void BrassPlus::ViewAcct()const
{
	cout<<"BrassPlus Cilent: "<<FullName()<<endl;
	cout<<"account number: "<<AcctNum()<<endl;
	cout<<"Balance:$ "<<Balance()<<endl;
	cout<<"Maxinum loan:$ "<<maxLoan<<endl;
	cout<<"owed to bank: $"<<owesBank<<endl;
	cout<<"Loan Rate: "<<100 * rate<<"%\n";
}

void BrassPlus::WithDraw(double amt)
{
	double bal = Balance();
	if(amt < bal)
		AcctABC::WithDraw(amt);
	else if(amt <= bal + maxLoan - owesBank)
	{
		double advance = amt - bal;
		owesBank += advance*(1.0+rate);
		cout<<"bank advance: $"<<advance<<endl;
		cout<<"Finace charge: $"<<advance * rate<<endl;
		Deposit(advance);
		AcctABC::WithDraw(amt);
	}
	else
	{
		cout<<"credit limit exceeded.transaction cancelled.\n";
	}
}
测试文件 我就不贴了,和上面的例子共用,稍微修改一下,将Brass* p_cilent []改为AcctABC* p_cilent[]。
ABC方法更具系统性,更规范。设计ABC之前,首先应该开发一个模型,指出编程问题所需的类以及他们之间相互关系。可以将ABC看作必须实施的接口,ABC要求具体派生类覆盖其纯虚函数。迫使派生类遵循ABC所设置的接口规则。

7.继承和动态内存分配

继承怎样与动态内存分配互动呢,假设基类使用动态内存分配,并重新定义赋值和复制构造函数。两种情况派生类不使用new和派生类使用new。

7.1派生类不使用new

我们先来看个使用内存分配的基类,和使用new的派生类。

class baseDMA
{
private:
	char* label;
	int rating;
public:
	baseDMA(const char * l = "null", int r = 0);
	baseDMA(const baseDMA & s);
	virtual ~baseDMA();
	baseDMA & operator = (const baseDMA & rs);
};
class lackDMA:public baseDMA
{
private:
	char color[10];
public:
	....
};
首先,派生类没有析构函数,那么将调用不执行任何操作默认的析构函数,对现在的派生类来说,是合适的;
其次,派生类的默认赋值构造函数,也是可以的,进行派生类成员的常规赋值。派生类使用显示的基类复制构造函数完成基类部分的复制,这对派生类来说也是合适的。
最后,对于赋值操作符,类的默认赋值操作符将自动使用基类的赋值操作符来对基类进行赋值,这也是合适的。

7.2派生类使用new

假设派生类也使用了new,这种情况下,也必须为派生类定义显示的析构函数,赋值操作符和赋值构造函数。
class hasDMA:public baseDMA
{
private:
	char * style;
public:
	...
};
首先,hasDMA析构函数必须释放style指针管理的内存,并依赖baseDMA析构函数来释放指针label的管理。
baseDMA::~baseDMA()
{
	delete [] label;
}
hasDMA::baseDMA()
{
	delete [] style;
}
其次,先来看基类的复制构造函数:
baseDMA::baseDMA(const baseDMA & rs) //复制构造函数
{
	label = new char[std::strlen(rs.label) + 1];
	std::strcpy(label, rs.label);
	rating = rs.rating;
}
hasDMA只能访问自身类成员,故他必须调用基类的复制构造函数来完成基类部分的复制。
hasDMA::hasDMA(const hasDMA & hs):baseDMA(hs)
{
	style = new char[std::strlen(hs.style) + 1];
	std::strcpy(style, hs.style);
}
最后,看看赋值操作符,基类
baseDMA & baseDMA::operator=(const baseDMA & rs)
{
	if(this == &rs)
		return *this;
	delete [] label;
	label = new char[std::strlen(rs.label) + 1];
	std::strcpy(label, rs.label);
	rating = rs.rating;
	return *this;
}
由于派生类也是用new,他也需要一个显示的赋值操作符:
hasDMA & hasDMA::operator=(const hasDMA & hs)
{
	if(this == &hs)
		return *this;
	baseDMA::operator=(hs);//调用基类
	delete[] style;
	style = new char[std::strlen(hs.style) + 1];
	std::strcpy(style, hs.style);
	return *this;
}
以上,总之,基类和派生类都是用内存分配时,派生类的析构函数,赋值操作符,以及复制构造函数都必须使用基类的相应方法来处理基类元素。析构函数,这会自动调用。对于构造函数,通过在初始化成员列表中调用基类的复制构造函数来完成的,否则,将自动调用基类的复制构造函数。对于赋值操作符,这是通过使用作用域解析操作符显示的调用基类的赋值操作符来完成的。

8.类设计回顾

8.1编译器生成的成员函数

(1)默认构造函数
(2)复制构造函数
(3)赋值操作符

8.2其他类方法

(1)转换
使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换,下面构造函数:
Star(const char * s);//convert char * to star
Start north;
north = "string";//
将使用Star::operator=(const Star*)函数,使用Star::Star(const char *)生成一个Star对象。
在一个带参数的构造函数原型里声明explict将禁止进行隐式转换,担仍允许显示转换:
class Star
{
public:
explict Star(const char * s);
};
Star north;
north = "polaris";//not allow
north = Star("polaris");//allow
(2)按值传递和按引用传递,返回对象和返回引用
作为参数时,一般按引用传递而非按值传递。提高效率(调用复制构造函数,调用析构函数),使用虚函数时,可以接受基类指针,指向派生类。
应采用返回引用的原因在于,返回对象涉及到生成返回对象的临时拷贝,时间成本包括复制构造函数,析构函数所需时间;返回引用则可以节省内存和时间.当然,并不总能返回应用,因为不能返回函数中创建的临时对象的引用.
A.如果函数返回在函数中创建的临时对象,则不要使用返回引用
B.如果函数返回的是通过引用或者指针传递给它的对象,则应按引用返回.
(3)使用const
A.确保方法不修改参数
Star::Star(const char *s){}//won't change the string to which s points
B.确保方法不修改调用他的对象
void Star::show()const{}//won't change involing object
这const表示const Star* this,而this指向调用的对象
C.确保引用或指针返回的值不能用于修改对象中的数据.
const Stock & Stock::topval(const Stock & s)const
{
    if(s.total_val > total_val)
    {
         return s;
     }
     else
     {
         return *this;
      }
}
该方法this,s都不能被修改,这意味着返回的引用也必须被声明为const.

8.3公有继承考虑的因素

(1)is-a关系
表示is-a关系的方式之一是,无须进行显示类型转换,基类指针就可以指向派生类对象.
(2)构造函数,析构函数不能被继承
(3)赋值操作符
赋值操作符不能被继承,如果编译器发现程序将一个对象赋给同一个类的另一个对象,他将自动为这个类提供一个赋值操作符.如果构造函数使用new初始化指针,则需要提供一个显示赋值操作符,可以看上面的例子.将派生类赋给基类,可行吗?
Brass base;
BrassPlus bp(a,b,c);
base = bp;//allowed
这样是允许的,他会将派生类基类的部分赋给base.
相反过来是不可行的,除非有转换构造函数.
BrassPlus(const Brass & s);
BrassPlus(const Brass & s,double ml = 0, double r = 0.1);
BrassPlus & BrassPlus::operator=(const Brass &){}
(4)私有成员和保护成员
对派生类而言,保护成员和公有成员类似;但对于外部而言,保护成员和私有成员类似.
(5)虚方法
设计基类时,必须确定是否将类方法声明为虚拟的.如果派生类能够重新定义方法,则应该在基类中将方法定义为虚拟的.
(6)析构函数
基类的析构函数应当是虚拟的.这样,通过指向对象的基类指针或引用来回删除派生类对象时,程序首先调用派生类的析构函数,然后调用基类的析构函数,而不仅仅调用基类的析构函数.
(7)友元函数
不能被继承因为并非类成员.如果想从派生类的友元函数使用基类的友元函数,则可以功过强制类型转换将派生类的引用或指针转换为基类引用或指针,然后使用转换后的指针或引用来调用基类的友元函数.
ostream & operator<<(ostream & os, const hasDMA & hs)
{
   os<<(const baseDMA &)hs;
   os<<"style:"<<hs.style<<endl;
   return os;
}
也可以使用
os<<dynamic_cast<const baseDMS&>(hs);
(8)有关使用基类方法的说明
A.派生类对象自动使用继承而来的基类方法,假如派生类没有重新定义.
B.派生类构造函数自动调用基类的构造函数.
C.派生类构造函数将自动调用基类的默认构造函数,如果没有在成员初始化列表中指定其他构造函数.
D.派生类构造函数显示的调用成员初始化列表中指定的基类构造函数.
E.派生类方法可以使用作用域解析操作符来调用公有和受保护的基类方法.
F.派生类的友元函数可以通过强制类型转换,将派生类引用或指针转换为基类引用或指针,然后使用该引用或指针来调用基类的友元函数.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值