欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

[C++ Primer Plus] 类基础知识--类继承

程序员文章站 2022-03-09 09:22:54
...

13.1.1

一,公有派生类

公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。


13.1.2

二,派生类构造函数的要点

1,首先创建基类对象(基类对象应当在程序进入派生类构造函数之前被创建);

2,派生类构造应通过成员初始化列表将基类信息传递给基类构造函数;

3,派生类构造应当初始化派生类新增的数据成员;

创建派生类对象时,程序首先调用基类构造函数,然后在调用派生类的构造函数。基类的构造函数负责初始化继承的数据成员,派生类构造负责初始化新增的数据成员。派生类构造总是调用基类的一个构造。可以使用初始化列表语法来指明要是用的基类构造,否则将使用默认的基类构造。

派生类对象过期时,程序首先将调用派生类的析构函数,然后在调用基类的析构函数。


13.1.4

三,派生类与基类的特殊关系

1,派生类可以使用基类公有方法;

2,基类指针可以在不进行显示类型转换的情况下指向派生类对象;基类指针只能调用基类方法,不能调用派生类方法。即:

//TableTennisPlayer为基类,RatedPlayer为派生类

RatedPlayer rplayer1(1140, ''mallory", "duck", true);
TableTennisPlayer *pt = &rplayer1;pt->Name();
3,基类引用可以再不进行显示类型转换的情况下引用派生类对象;基类引用只能调用基类方法,不能调用派生类方法。

RatedPlayer rplayer1(1140, ''mallory", "duck", true);
TableTennisPlayer &pt = rplayer1;
pt.Name();
4,不可以将基类对象和地址赋给派生类引用和指针。
TableTennisPlayer palyer("betsy", "bloop",true);
RatedPlayer &rr = palyer; //not allowed
RatedPlayer *rp = &palyer; //not allowed
由于2,3的成立,所以:
对于形参为指向基类的指针的函数,可以使用基类对象的地址或者派生类对象的地址作为实参。如:
void Wohs(const TableTennisPlayer *pt);
{...}

RatedPlayer rplayer1(1140, ''mallory", "duck", true);
TableTennisPlayer player1("ally", "boona", true);
Wohs(&player1);
Wohs(&rplayer1);
同样形参为基类的引用的函数,可以使用基类对象或者派生类对象作为实参。
RatedPlayer rplayer1(1140, ''mallory", "duck", true);

TableTennisPlayer Olaf2(rplayer1);
//↑ 使用TableTennisPlayer的复制构造:TableTennisPlayer(const TableTennisPlayer &); 

TableTennisPlayer Olaf3 = rplayer1;
//↑ 使用TableTennisPlayer的隐式重载复制运算符:TableTennisPlayer & operator=(const TableTennisPlayer &)const;

13.2
四,C++有3中继承方式

1,公有继承;
2,私有继承;
3,保护继承;

13.3.1

五,虚方法
如果方法是通过引用或者指针而不是对象调用的,它将确定使用哪一种方法:
1,如果没有使用virtual关键字,程序将根据引用类型或指针类型选择方法;
2,如果使用virtual,程序将根据引用或者指针指向的对象的类型来选择方法。
通常,如果派生类中重新定义基类的方法,通常应将基类方法声明为虚的。这样,程序将根据对象类型而不是引用或者指针的类型来选择方法版本。

//假设以下两个类都有方法ViewAcct()
Brass dom("dominic", 11224, 4183.5);//基类
BrassPlus dot("dorothy", 12118, 2592.0);//派生类

Brass & b1_ref = dom;
Brass & b2_ref = dot;
//假如ViewAcct()不是虚方法
b1_ref.ViewAcct(); //use Brass::ViewAcct()
b2_ref.ViewAcct(); //use Brass::ViewAcct()

//假如ViewAcct()虚方法
b1_ref.ViewAcct(); //use Brass::ViewAcct()
b2_ref.ViewAcct(); //use BrassPlus::ViewAcct()

六,在派生类方法中,应使用作用域解析符来调用基类方法。

void BrassPlus::ViewAcct() const
{
        //...
        Brass::ViewAcct();
        ViewAcct();//use BrassPlus::ViewAcct(), Error!将创建一个不会终止的递归函数。
}

七,虚析构的重要性
虚析构能够保证正确的析构函数序列被调用。
1,如果不是虚的,则将只调用指针类型的析构函数。
2,如果是虚的,则将调用相应对象类型的析构函数。

class A
{
    void method();
    ~A(){};
}

class B: public A
{
    void method();
    ~B(){};
}

A * ptr1 = new A;
A * ptr2 = new B;
//此处ptr1、ptr2都是A类型的指针,但ptr1指向类A,ptr2指向类B

ptr1->method();//调用A类的method()
ptr2->method();//调用A类的method()

delete ptr1;//调用A的析构
delete ptr2;//调用A的析构,虽然pet2指向B

//如果类A的method()方法与析构都声明为vitual,即
class A
{
    virtual void method();
    virtual ~A(){};
}

class B: public A
{
    void method();
    ~B(){};
}
//则

ptr1->method();//调用A类的method()
ptr2->method();//调用B类的method()

delete ptr1;//调用A的析构
delete ptr2;//先调用B的析构,然后自动调用A的析构。此例子看不出调用顺序的重要性,
//但如果B的析构是一个包含执行某些操作的析构函数,如释放成员内存,则必须声明A的析构为虚析构。

13.4.2

八,虚函数的工作原理

通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表。虚函数表中存储了为类对象进行声明的虚函数的地址。

例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址。如果派生类没有重新定义虚函数,该虚函数表将保存函数原始版本的地址(即:基本虚函数表中保存的该函数地址)。如果派生类定义了新的虚函数,则该函数的地址也将添加到虚函数表中。

具体看P504 图13.5

调用虚函数时,程序将查看存储在对象中的虚函数表地址(即:上面提到的隐藏成员),然后转向相应的虚函数地址表。如果使用类声明的中第一个虚函数,则程序将使用数组中的第一个函数地址,并执行具有该地址的函数。如果使用第三个虚函数,则将使用地址为数组中的第三个元素的函数。

13.4.3

九,有关虚函数的注意事项

1,给类定义一个虚析构函数并非错误,即使这个类不用做基类;这是一个效率方面的问题。

2,友元不是是虚函数,因为友元不是类成员,而只有成员才能成为虚函数。但可以通过让友元函数使用虚函数来解决。

3,重新定义将隐藏方法。这里引出两条规则:

第一,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或者指针,则可以修改为指向派生类的引用或指针。这种特性叫做返回类型协变,因为允许返回类型随类类型的变化而变化。但这种特性只适用于返回值,而不适用于参数。

class Dwelling
{
public:
	//showperks()有三个重载的版本
	virtual void showperks(int a) const;
	virtual void showperks(double x) const;
	virtual void showperks() const;
};

class Hovel : public Dwelling
{
	//showperks()需要重新定义所有版本
	virtual void showperks(int a) const;
	virtual	void showperks(double x) const;
	virtual void showperks() const;
};

第二,如果基类中方法被重载,则应在派生类中重新定义所有的基类版本。如果只重新定义了部分重载函数,则不需要修改的重载函数,可以在新定义的函数中直接调用基类版本。

class Dwelling
{
public:
	//showperks()有三个重载的版本
	virtual void showperks(int a) const {int b = a;}
	virtual void showperks(double x) const{double y = x;}
	virtual void showperks() const; 
};

class Hovel : public Dwelling
{
	//showperks()需要重新定义所有版本
	virtual void showperks(int a) const {a = 5;}//有修改
	virtual	void showperks(double x) const {Dwelling::showperks(x);}//无修改,直接调用基类版本
	virtual void showperks() const {Dwelling::showperks();}//无修改,直接调用基类版本
};

13.5

十,访问控制:protected

关键字protected和private相似,在类外只能用公有类成员来访问protected部分中的类成员。二者的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。

对于外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。

13.6

十一,纯虚函数

1,c++使用纯虚函数提供未实现的函数。纯虚函数声明的结尾处为=0。

	virtual double Area() = 0;
	virtual double Area() const = 0;
原型中的=0,使虚函数成为纯虚函数。

2,当类中包含纯虚函数时,则不能创建该类的对象。这里的理念是纯虚函数的类只用作基类。

3,要成为真正的ABC(abstract bass class 抽象基类),至少包含一个纯虚函数。

4,纯虚函数可以在基类中定义,也可以不在基类中定义。

假如所有派生类拥有的该方法都一样,则可以在基类中定义该纯虚函数。

如果不用的派生类需定义不同的方法,则可以在派生类中定义该纯虚函数。

5,由于c++具有多态性,则可以用纯虚函数所在的基类组成的指针数组,同时管理所有的派生类。

13.7

十二,继承与动态内存分配

加入基类使用动态内存分配,并重新定义赋值运算符、复制构造和析构函数。对于派生类的印象,取决于派生类的属性。

第一种情况,派生类中不使用new

答:派生类不需要定义显示析构函数、复制构造函数和赋值运算符。

1,派生类默认构造:该构造会执行一些操作:执行自身的代码后自动调用基类析构函数,所以默认析构是合适的。

2,派生类复制构造:由于派生类没有使用new,所以默认的复制构造是合适的。需要知道的是,在使用派生类的默认复制构造时,会自动使用基类显示复制构造,来复制派生类对象中的基类部分。

3,派生类赋值运算符:派生类的默认赋值运算符将自动使用基类的复制运算符来对基类组间进行赋值。所以默认赋值运算符也是合适的。

第二种情况,派生类中使用new

答:这种情况必须为派生类定义显示析构函数、复制构造和赋值运算符。

1,派生类析构函数:派生类析构函数会自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行工作的清理。即:自己的析构管理自己申请的内存。

2,派生类复制构造:由于派生类复制构造只能访问派生类数据,因此它必须手动显示调用基类的复制构造函数来处理共享的基类数据。

需要注意的一点是,成员初始化列表将一个派生类引用传递给基类的复制构造函数(由于基类的复制构造有一个基类的引用为参数,而基类引用可以指向派生类类型),所以基类的复制构造将使用派生类参数中的基类部分来构造新对象的基类部分。

3,赋值运算符:由于派生类也有动态内存分配,所以它也需要一个显示赋值运算符。由于派生类的方法只能访问派生类数据,而派生类的显示赋值运算符,必须负责包括基类对象的赋值。所以,可以通过手动显示调用基类赋值运算符类完成这项工作。

class parent
{
private:
	char* test;
public:
	parent(const char* ll = NULL);//construct
	parent(const parent & as);//copy construct
	virtual ~parent();// virtual destruct
	parent & operator=(const parent & rs); //operator =
	//...
};
class child: public parent
{
private:
	char* test2;
public:
	child(const char * pp = NULL);
	child(const child & cs):parent(cs)//copy construct
	{
		test2 = new char[std::strlen(cs.test2)+1];
		std::strcpy(test2, cs.test2);
	};
	child& operator=(const child & rs)
	{
		if (this == &rs)
		{
			return *this;
		}
		parent::operator=(rs);//copy base partion
		delete test2;//prepare for new test2
		test2 = new char[std::strlen(rs.test2)+1];
		std::strcpy(test2, rs.test2);
		return *this;
	};
};
总之,当基类和派生类都采用动态内存分配时,派生类的析构、复制构造、赋值运算符都必须使用相应的基类方法来处理基类元素。这种要求是通过三种不同的方式来满足的:析构函数,自动完成。构造函数,通过在初始化列表中调用基类的复制构造函数来完成。如果不这样做,将自动调用基类的默认构造函数。对于赋值运算符,这是通过使用作用域解析运算符显示地调用基类的赋值运算符来完成的。

13.7.3

十三,使用动态内存分配和友元的继承示例

作为派生类的友元,能够访问派生类的成员,但不能访问基类的成员。这时可以在基类中定义相同的友元函数,在派生类的友元中调用基类的友元就可以访问到基类的成员。但由于友元不是成员函数,不能使用作用域解析运算符,故可以使用强制类型转换,以便匹配原型时能够选择正确的函数。

class parent
{
private:
	char* test;
public:
	parent(const char* ll = NULL);//construct
	parent(const parent & as);//copy construct
	virtual ~parent();// virtual destruct
	parent & operator=(const parent & rs); //operator =
	friend std::ostream & operator<<(std::ostream & os, const parent & rr);
	//...
};
class child: public parent
{
private:
	char* test2;
public:
	child(const char * pp = NULL);
	child(const child & cs):parent(cs)//copy construct
	{
		test2 = new char[std::strlen(cs.test2)+1];
		std::strcpy(test2, cs.test2);
	};
	child& operator=(const child & rs)
	{
		if (this == &rs)
		{
			return *this;
		}
		parent::operator=(rs);//copy base partion
		delete test2;//prepare for new test2
		test2 = new char[std::strlen(rs.test2)+1];
		std::strcpy(test2, rs.test2);
		return *this;
	};
	friend std::ostream & operator<<(std::ostream & os, const child & rr);
};

std::ostream & operator<<(std::ostream & os, const child & rr)
{
	os << (const parent &)rr;//强制类型转换为基类类型,则调用基类的友元
	os << "test2: " << rr.test2 << std::endl;//调用派生类的友元
	return os;
}

13.8.1

十四,编译器生成的成员函数。

1,默认构造。

默认构造要么没参数,要么所有的参数都有默认值,以便能够创建对象。

另一个功能是,调用基类的默认构造以及调用本身是对象的成员所属类的默认构造。

另外,如果派生类的成员初始化列表没有显示调用基类构造,则编译器将使用基类的默认构造函数来构造派生类对象的基类部分。


其他请参考13.8,p523具体内容,是比较好的总结。