C++学习记录(二):实现继承

2 实现继承

面向对象编程基于四个重要方面:封装、抽象、继承和多态。继承是一种强大的属性重用方式,是通向多态的跳板。

这章中将学习:

  1. 编程意义上的继承;
  2. C++继承语法;
  3. 公有继承、私有继承和保护继承;
  4. 多继承;
  5. 隐藏基类方法和切除(slicing)导致的问题。

2.1 继承基础

在编程领域,经常会遇到具有类似属性,但细节或行为存在细微差异的组件。在这中情况下,一种解决之道是将每个组件声明为一个类,并在每个类中实现所有属性,这将重复实现相同的属性。另一种解决办法是继承,从同一个基类派生出类似的类,在基类实现所有通用的功能,并在派生类中覆盖基本功能,以实现让每个类都独一无二。第二种方法更好。面向对象编程支持继承。看下图:

 

2.1.1 继承和派生

上面的图说明了基类与派生之间的关系。派生类继承了基类。

2.1.2  C++派生语法

如何从Fish类派生出Carp类呢?C++派生语法如下:

//declaring a super class
class Base
{
//...base class member
};
//declaring a sub-class
class Derived:access-specifer Base
{
//...derived class members
} ;

其中access-specifier可以使public(这是最常见的,表示派生类是一个基类)、private或protected(表示派生类有一个基类)。

下面的继承结构表明,Carp类是从Fish类派生而来的:

class Fish
{
//...Fish's members
};

class Carp:public Fish
{
//...Carp's members
};

基类也称为超类,从基类派生而来的类称为派生类,也叫子类。

#include <iostream>
using namspace std;

class Fish
{
public:
bool FreshWaterFish;

void Swim()
{
if (FreshWaterFish)
cout<<"Swim in lake"<<endl;
else
cout<<"Swim in sea"<<endl;
}

};

class Tuna:public Fish
{
public:
Tuna()
{
FreshWaterFish = False;
}
};

class Carp:public Fish
{
public:
Carp()
{
FreshWaterFish = true;
}
};

int main()
{
Carp myLunch;
Tuna myDinner;
cout<<"Getting my food to swim"<<endl;
cout<<"Lunch: ";
myLaunch.Swim();

cout<<"Dinner: ";
myDinner.Swim();

return 0;

}

2.1.3 访问限定符protected

避免某些篡改,可以让基类的某些属性能在派生中访问,但不能在继承结构层次之外部访问。这意味着,Fish类的布尔标记FreshWaterFish可在派生类Tuna和Carp中访问,但不能在实例化Tuna和Carp的main()中访问。为此,可以使用关键字protected。

myDinner.FreshWaterFish = true;

与public和private一样,protected也是一个访问限定符。将属性声明为protected是,相当于允许派生类和友元访问它,但是禁止在继承层次结构外部(包括main())访问它。

总结一下:要让派生类能访问基类的某个属性,可使用访问限定符protected。

#include <iostream>
using namespace std;

class Fish
{
protected:
bool FreshWaterFish;

public:
void Swim()
{
if(FreshWaterFish)
cout<<"Swims in lake"<<endl;
else
cout<<"Swims in sea"<<endl;
}
};

class Tuna:public Fish
{
public:
Tuna()
{
FreshWaterFish = false;
}
};

class Carp:public Fish
{
public:
Carp()
{
FreshWaterFish = true;
}
};

int main()
{
Carp myLaunch;
Tuna myDinner;

cout<<"Getting my food to swim"<<endl;

cout<<"Launch: ";
myLaunch.Swim();

cout<<"Dinner: "
myDinner.Swim();

return 0;

}

这是面向对象编程的一个非常重要的的方面,它与数据抽象和继承一起确保派生类可安全地继承基类的属性,同时禁止在继承层次结构外部对其进行修改。

1.1.4 基类初始化——向基类传递参数

如果基类包含重载的构造函数,需要在实例化时给它提供实参。

采用初始化列表即可,并通过派生类的构造函数调用合适的基类构造函数:

class Base
{
public:
Base(int SomeNumber) //Overloaded constructor
{
//Do something with SomeNumber
}
};

class Derived:public Base
{
public:
Derived():Base(25) //instantiate class Base with argument 25
{
//derived class constructor code
}

};

对于Fish类来说,这种机制很有用。通过给Fish的构造函数提供一个布尔参数,以初始化Fish::REshWaterFish,可以强制每个派生类都指出它自己是淡水鱼还是海水鱼,代码如下:

#include <iostream>
using namespace std;

class Fish
{
protected:
bool FreshWaterFish;

public:
Fish(bool IsFreshWaterFish):FreshWaterFish(IsFreshWaterFish){}

void Swim()
{
if (FreshWaterFish)
cout<<"Swim in Lake."<<endl;
else
cout<<"Swim in Sea."<<endl;
}

};

class Tuna:public Fish
{
public:
Tuna():Fish(false){}
};

class Carp:public Fish
{
public:
Carp():Fish(true){}
};

int main()
{
Carp myLaunch;
Tuna myDinner;

cout<<"Getting my food to swim."<<endl;

cout<<"Launch: ";
myLaunch.Swim();

cout<<"Dinner: ";
myDinner.Swim();

return 0 ;

}

现在,Fish有一个构造函数,它接受一个默认参数,用于初始化Fish::FreshWaterFish。因此要创建Fish对象,必须提供一个用于初始化该保护成员的参数。这样,Fish类便避免了保护成员包含随机值的情况,尤其是派生类忘记设置它是。派生类Tuna和Carp被迫定义这样一个构造函数,即使用何时的参数(true或者false,表示是否是淡水鱼)来实例化Fish。

注意:0派生类没有直接访问布尔成员变量Fish::FreshWaterFish,虽然这是一个派生类可以访问的保护成员。这是因为这个变量是通过Fish的构造函数设置的。为最大限度地体改安全性,对于派生类不需要访问的基类属性,别忘了将其声明为私有的。

2.1.5 在派生类中覆盖基类的方法

如果派生类实现了从基类继承的函数,且返回值和特征标相同,就相当于覆盖了基类的这个方法,如下面的代码

class Base
{
public:
void DoSomething()
{
//implementation code ... Dose something
}
};

class Derived:public Base
{
public:
void DoSomething()
{
//implementation code ... Dose something else
}
};

因此,如果使用Derived类的实例调用方法DoSomething(),调用的将不是Base类中的这个方法。

如果Tuna和Carp中实现了自己的Swim()方法,则相当于覆盖了基类中的Swim()方法。看下面的程序:

#include <iostream>
using namespace std;

class Fish
{
private:
bool FreshWaterFish;

public:
Fish(bool IsFreshWaterFish):FreshWaterFish(IsFreshWaterFish){}

void Swim()
{
if (FreshWaterFish)
cout<<"Swim in Lake."<<endl;
else 
cout<<"Swim in Sea."<<endl;
}

};

class Tuna:public Fish
{
public:
Tuna():Fish(false){}

void Swim()
{
cout<<"Tuna swims real fast."<<endl;
}

};

class Carp:public Fish
{
public:
Carp():Fish(true){}

void Swim()
{
cout<<"Carp swims real slow."<<endl;

}
};

int main()
{

Carp myLunch;
Tuna myDinner;

cout<<"Getting my food swim."<<endl;

cout<<"Lunch: ";
myLunch.Swim();

cout<<"Dinner: ";
myDinner.Swim();

return 0;


}

运行下代码可以看出,Tuna和Carp的类中的Swim()覆盖了Fish中的Swim()。

如果想调用Fish::Swim(),要么让派生类在其成员中显式地使用它,要么在main()中使用作用域解析符显式地调用它。

这里具体怎么做?看之后内容。

1.1.6 调用基类中被覆盖的方法

如果要在main()中调用Fish::Swim(),需要使用作用域解析运算符(::),看下面的代码:

myDinner.Fish::Swim();

1.1.7 在派生类中调用基类方法

通常,Fish::Swim()包含适用于所有鱼类的通用实现。如果要在Tuna::Swim()和Carp::Swim()中重用Fish::Swim()的通用实现,可以使用作用域解析运算符(::),代码如下:

class Carp:public Fish
{
public:
Carp():Fish(true){}

void Swim()
{
cout<<"Carp swims real slow."<<endl;
Fish::Swim();
}

};

看下面的实践代码:

#include <iostream>
using namespace std;

class Fish
{
private:
bool FreshWaterFish;

public:
Fish(bool IsFreshWaterFish):FreshWaterFish(IsFreshWaterFish){}

Void Swim()
{
if (FreshWaterFish)
cout<<"Swim in Lake."<<endl;
else
cout<<"Swim in Sea."<<endl;
}

};

class Tuna:public Fish
{
public:
Tuna():Fish(false){}

void Swim()
{
cout<<"Tuan swims real fast."<<endl;
}

};

class Carp:public Fish
{
public:
Carp():Fish(true){}

void Swim()
{
cout<<"Carp swims real slow."<<endl;
Fish::Swim();
}
};


int main()
{
Carp myLunch;
Tuna myDinner;

cout<<"Getting my food to swim."<<endl;
cout<<"Lunch: ";
myLunch.Swim();

cout<<"Dinner: ";
myDinner.Swim();

return 0;


}

1.1.8 在派生类中隐藏基类的方法

覆盖的一种极端情况,Tuna::Swim()可能隐藏Fish::Swim()的所有重载版本,使得调用这些重载版本会导致编译错误(因此称为被隐藏),看下面的程序:

#include <iostream>
using namespace std;

class Fish
{
public:
void Swim()
{
cout<<"Fish swims...!"<<endl;
}
void Swim(bool FreshWaterFish)
{
if(FreshWaterFish)
{
cout<<"Swims in lake."<<endl;
}
else
{
cout<<"Swims in sea."<<endl;
}
}

void Swim(bool FreshWaterFish)
{
if(FreshWaterFish)
cout<<"Swims in lake."<<endl;
else
cout<<"Swims in Sea."<<endl;
}

};

class Tuna:public Fish
{
public:
void Swim()
{
cout<<"Tuna swims real fast."<<endl;
}
};

int main()
{
Tuna myDinner;
cout<<"Getting my food to swim."<<endl;

//myDinner.Swim(false); //compile failure: Fish::Swim(bool) is hiden by //Tuna::Swim()

myDinner.Swim();

return 0;



]

这个Fish类与之前的Fish有所不同。除尽可能简单的版本诠释当前问题之外,这个Fish版本还包含两个重载的Swim()方法:一个不接受任何参数,另一个接受一个bool参数。鉴于Tuna以公有方式继承了Fish,理所当然以为可以通过Tuna实例可调用这两个版本的Fish::Swim()。然而,由于Tuna实现了自己的Tuna::Swim(),这对编译器隐藏了Fish::Swim(bool)。如果取消注释,将出现编译错误。

要通过Tuna实例调用Fish::Swim(bool),可采用如下解决方案:

  1. 解决方案一:在main()中使用作用域解析运算符(::)  myDinner.Fish::Swim();
  2. 解决方案二:在Tuna类中,使用关键字using 解除对Fish::Swim()的隐藏
    class Tuna:public Fish
    {
    using Fish::Swim(); 
    
    void Swim()
    {
    cout<<"Tuna swims real fast."<<endl;
    }
    
    };

    3.解决方案三,覆盖Fish::Swim()所有重构版本(如果需要,可以通过Tuna::Fish(...)调用方法Fish::Swim())

    class Tuna:public Fish
    {
    public:
    void Swim(bool FreshWaterFish)
    {
    Fish::Swim(FreshWaterFIsh);
    }
    
    void Swim()
    {
    
    cout<<"Tuna swims real fast."<<endl;
    }
    };

    2.1.9 顺序构造

如果Tuna是从Fish派生而来的,创建Tuna对象时,先调用Tuna的构造函数还是Fish的构造函数?另外,实例化对象时,成员属性(如Fish::FreshWaterFish)是调用构造函数之前还是之后之后实例化?基类对象在派生类对象之前被实例化,因此,首先构造Tuna对象的Fish部分,这样,实例化Tuna部分时,成员属性(具体地说应该是Fish的保护和公有属性)已经准备就绪,可以使用了。实例化Fish部分和Tuna部分时,先实例化成员属性(如Fish::FreshWaterFish),再调用构造函数,确保成员属性准备就绪,可供构造函数使用。这也适用于Tuna::Tuna()。

2.1.10 析构顺序

Tuna实例不再在作用域内时,析构顺序与构造顺序。看如下代码:

#include <iostream>
using namespace std;

class FishDummyMember
{
public:
FishDummyMember()
{
cout<<"FishDummyMember constructor"<<endl;
}


~FishDummyMember()
{
cout<<"FishDummyMember destructor"<<endl;
}
};


class Fish
{
protected:
FishDummyMember dummy;

public:
//Fish constructor 
Fish()
{
cout<<"Fish constructor"<<endl;
}

~Fish()
{
cout<<"Fish destructor"<<endl;
}

};

class TunaDummyMember
{
public:
TunaDummyMember()
{
cout<<"TunaDummyMember constructor"<<endl;
}

~TunaDummyMember()
{
cout<<"TunaDummyMember destructor"<<endl;
}

};

class Tuna: public Fish
{
private:
TunaDummyMember dummy;

public:
Tuna()
{
cout<<"Tuna constructor"<<endl;
}

~Tuna()
{
cout<<"Tuna destructor"<<endl;
}

};

int main()
{
Tuna myDinner;
}

虽然主函数里面就一行代码,但是输出的东西还是比较多的。实例化一个Tuna对象就产生了这些输出,这是因为构造函数和析构函数里面包含了cout语句。为了帮助理解成员变量是如何被实例化和销毁的,定义了两个毫无用途的类———FishDummyMember和TunaDummyMember,并在其构造函数和析构函数中包含了cout语句。Fish和Tuna类分别将这些类的对象作为成员。输出表明,实例化Tuna对象时,将从继承层次结构底部开始,因此首先实例化Tuna对象的Fish部分。为此实例化Fish的成员属性,即Fish::dummy。构造好成员属性(如dummy)后,将调用Fish的构造函数。构造好基类部分后,将实例化Tuna部分——首先实例化成员Tuna::dummy,再执行构造函数Tuna::Tuna()的代码。输出表明,析构顺序正好相反。

2.2 私有继承

前面介绍的都是公有继承,私有继承与公有继承的不同之处在于,指定派生类的基类时使用关键字private:

class Base
{
//...base class member and methods

};


class Derived:private Base
{
//...derived class members and methods
};

私有继承意味着在派生类的实例中,基类的所有成员和方法都是私有的——不能从外部访问。换句话说,即便是Base类的公有成员和方法,也只能被Derived类使用,无法通过Derived实例来使用它们。

私有继承使得只有子类才能使用基类的属性和方法。

#include <iostream>
using namespace std;

class Motor
{
public:
void SwitchIgnition()
{
cout<<"Ignition On"<<endl;
}

void PumpFuel()
{
cout<<"Fuel in cylinders"<<endl;
}

void FireCylinders()
{
cout<<"Vroooom"<<endl;
}

};

class Car:private Motor
{
public:
void Move()
{
SwitchIgnition();
PumpFuel();
FireCylinders();
}

};

int main()
{
Car myDreamCar;
myDreamCar.Move();

return 0;
}

Motor类非常简单,包含三个公有的成员函数。Car类使用关键字private集成了Motor类。公有函数Car::Move()调用了基类Motor的成员函数。如果在上面的main()函数中插入:

myDreamCar.PumpFuel();

这将无法通过编译。

注意:如果有一个SuperCar,它继承了Car类,则不管SuperCar和Car之间的继承关系是什么样的,SuperCar都不能访问基类Motor的公有成员和方法,这是因为Car和Motor之间是私有继承关系,这意味着除了Car之外,其他所有实体都不能访问Motor的公有成员。换句话说,编译器在确定派生类能否访问基类的公有或保护成员时,考虑的是继承层次中最严格的访问限定符。

2.3 保护继承

保护继承不同于公有继承在于,声明派生类继承基类时的关键字protected:

class Base
{
//... base class members and methods
};

class Derived: protected Base
{
//... derived class members and methods
};

保护继承与私有继承的类似之处如下:

  • 它也表示has-a关系;
  • 它也让派生类能够访问基类的所有公有和保护成员;
  • 在继承层次结构外面,也不能通过派生类实例访问基类的公有成员。

随着继承层次结构的加深,保护继承与私有继承有些不同:

class Derived2:protected Derived
{
//can access members of Base
};

在保护继承层次中,子类的子类(即Derived2)能够访问Base类的公有成员。但如果Derived和Base之间的继承关系是私有的,就不能这样做。

#include <iostream>
using namespace std;

class Motor
{
public:
void SwitchIgnition()
{
cout<<"Ignition On"<<endl;
}

void PumpFuel()
{
cout<<"Fuel in cylinders"<<endl;
}

void FireCyliders()
{
cout<<"Vroooom"<<endl;
}

};

class Car:protected Motor
{
public:

void Move()
{
SwitchIgnition();
PumpFuel();
FireCylinders();
}

};

class SuperCar:protected Car
{
public:
void Move()
{
SwitchIgnition();
PumpFuel();
FireCylinders();
FireCylinders();
FireCylinders();
}
};

int main()
{
SuperCar myDreamCar;
myDreamCar.Move();

return 0;

}

Car类以保护方式继承了Motor类,而SuperCar类以保护方式继承了Car类。正如所看到的,SuperCar::Move()实现使用了基类Motor中定义的方法。

能否经由中间基类Car访问终极基类Motor呢?这取决于Car和Motor之间的继承关系。如果继承关系是私有的,而不是保护的,SuperCar将不能访问Motor类的公有成员,因为编译器根据最严格的访问限定符来确定访问权限。

警告:仅当必要时才使用私有或保护继承。

对于大多数使用私有继承的情形(如Car和Motor之间的私有继承),更好的选择是,将基类对象作为一个成员属性,通过继承Motor类,相当于对Car进行了限制,使其只能有一台发动机。

将Motor对象作为Car类的私有成员被称为组合或者聚合,这样的Car类类似于下面这样:

class Car
{
private:
Motor heartOfCar;

public:
void Move()
{
heartOfCar.SwitchIgnition();
heartOfCar.PumpFuel();
heartOfCar.FireCylinders();
}
};

这是一种不错的设计,可以轻松地在Car类中添加Motor成员,而无需改变继承层次结构,也不用修改客户看到的设计。

2.4 切除问题

如果这样做,结果会如何?

Derived objectDerived;
Base objectBase = objectDerived;

如果这样做,结果又会如何?

void FuncUseBase(Base Input);
...
Derived objectDerived;
FuncUseBase(objectDerived);

它们都将Derived对象复制给Base对象,一个是通过显式复制,另一个是通过传递参数。在这些情境下,编译器将只复制objectDerived的Base部分,即不是整个对象,而是Base容纳的部分,这通常不是程序员本意,这种无意间的剪裁数据,导致Derived变成Base的行为称为切除。

警告;要避免切除问题,不要按值传递参数,而硬以指向基类的指针或const引用的方式传递。

2.5 多传递

C++允许继承多个类

class Derived:access-specifier Base1,access-specifier Base2
{
//class members
};

我们直接看代码吧

#include <iostream>
using namespace std;

class Mammal
{
public:

void FeedBabyMilk()
{
cout<<"Mammal: Baby says glug!"<<endl;
}

};

class Reptile
{
public:

void SpitVenom()
{
cout<<"Reptile: Shoo enemy! Spits venom!"<<endl;
}
};

class Bird
{
public:
 
void LayEggs()
{
cout<<"Bird: Laid my eggs, am lighter now!"<<endl;
}
};

class Platypus: public Mammal, public Bird, public Reptile
{
public:
void Swim()
{
cout<<"Platypus: Voila, I can swim!"<<endl;
}
};

int main()
{
Platypus realFreak;
realFreak.LayEggs();
realFreak.FeedBabyMilk();
realFreak.SpitVenom();
realFreak.swim();

return 0;

}

务必牢记:公有继承意味着继承派生类的类能够访问基类的公有和保护成员。

务必牢记,私有继承意味着继承派生类的类也不能访问基类的成员。

务必牢记,保护继承意味着派生继承类的类能够访问基类的公有和保护方法。

务必牢记,无论继承关系是什么,派生类都不能访问基类的私有成员。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值