第十五章 面向对象编程OPP随笔

面向对象编程的三个核心为数据抽象、继承和动态绑定。

继承:

派生类需要通过派生列表指明它从哪个或哪几个基类继承过来,这样,派生类将继承基类的所有成员(多继承将继承多个基类的所有成员)。

派生列表的访问控制是控制派生类对象对基类成员的访问权限,如果是public方式继承,则基类的public,protected,private成员继承过来后仍然为public,protected,private成员,如果是private方式继承,则继承过来后全变成private成员(即取权限小的);另外,派生列表中的访问控制并不影响派生类对基类成员的访问,即派生类的成员只能访问基类的public和private成员。

派生类除了继承基类的所有成员外,还会定义自己特殊的成员。

虚函数:基类希望某个或某些函数在派生类中被重写,就用关键字virtual指明某个或某些类为虚函数,表示这些成员函数在派生类中可能会被"重新定义"(函数名和形参表不变,仅函数体变化),如果派生后没有被"重新定义",虚函数与其他被继承的函数没什么区别,如果派生类中要对虚函数"重新定义",则在函数列表最后用关键字overite标识。重新定义后的函数被调用时是调用基类成员函数还是派生类重新定义的成员函数由调用对象类型来决定。除了构造函数外的非静态函数都可以定义为虚函数。

关键字virtual只能在基类内部的函数声明前,在类外定义函数时不能带virtual,且虚函数继承到派生类中后隐式的为虚函数。

动态绑定(运行时绑定):基类中声明为虚函数的成员,在派生类中被重写后(重写函数体)。当我们使用基类的引用或指针来调用虚函数时将发生动态绑定。

动态绑定的原因:因为派生类对象、引用、指针可以向基类类型转换,换言之,基类类型的指针或引用可以与派生类类型的对象绑定(仅绑定派生类中的基类部分),这样,我们使用一个指针或引用访问成员函数时,编译器不知道指针或引用所绑定的对象是基类对象函数派生类对象,因此编译时无法确定调用哪个版本的成员函数。

只有程序运行起来,通过调用时用户传入的对象类型,才能确定调用哪个版本的成员函数,所以动态绑定又叫运行时绑定。比如,基类类型的指针或引用如果绑定了派生类

如何回避虚函数的动态绑定机制?

如果想明确调用某个版本的成员,则只用在调用的成员前加上类名::,以明确表示调用的是哪个类的成员:

例如 p->Quote::net_price(20);

   p->Bulk_quote::net_price(20);

//基类
class Quote {
public:
	Quote() = default;
	Quote(string bn, double pr) :bookNo(bn),price(pr) {}
	string isbn()const{ return bookNo; }
	virtual double net_price(std::size_t n) const { return n * price; }
private:
	string bookNo="";
protected:
	double price = 0;
};

//派生类

class Bulk_quote:public Quote {
public:
	Bulk_quote() = default;
	Bulk_quote(string bn,double pr,size_t min,double dis):Quote(bn,pr),min_qty(min),discount(dis) {}
	double net_price(size_t n)const override {
		if (n > min_qty)
			return n * price * (1 - discount);
		return n * price;
	}
private:
	size_t min_qty = 0;
	double discount = 0;
};

//调用
int main() {
	Bulk_quote bulk_quote("红楼梦", 30, 2, 0.1);
	Quote quote = bulk_quote;          //派生类对象向基类转换(使用派生类中的基类部分初始化基类对象)
	Quote& base = bulk_quote;  //这里基类类型的引用绑定了派生类对象,所以该引用默认调用的是派生类的成员。
	cout << quote.net_price(3) << endl;
	cout << bulk_quote.net_price(3) << endl;
	cout << base.net_price(5) << endl;  //引用base绑定了派生类对象,默认调用派生类成员
	cout << base.Quote::net_price(6) << endl;  //直接说明调用基类成员
	cout << bulk_quote.Bulk_quote::net_price(6) << endl;    //直接说明调用派生类成员
    return 0;
}

 

派生类对继承成员的访问控制:

派生类继承基类的成员后,派生类的成员并不一定有权访问从基类中继承的成员。派生类对继承成员的访问控制与其他用户(对象)对基类的访问控制是一样的(仅public和private相同),即派生类只能访问基类中为public的成员,不能private成员,另外,还有一种protected 成员,能被派生类访问,但不能被其他用户访问。

(注意与派生列表中的继承方式区分,继承方式的访问控制符是控制继承过来的成员是否对派生类用户可见的,默认为private方式继承,派生类对象不能访问继承过来的成员。)

 

派生类到基类的类型转换:

由于派生类继承了基类的成员,所以在需要基类对象或基类对象的引用、指针的地方可以用派生类对象或者派生类对象的引用、指针来代替。即派生类对象或对象的引用、指针可以转化为基类对象或基类对象的引用、指针。

Qutote quote;
Bulk_quotye bulk_quote;
Quote *qo=&bulk_quote;  //派生类指针向基类指针转换,qo指针指向了派生类中的基类部分
Quote &qoo=bulk_quote;  //派生类引用向基类引用转换,引用qoo绑定了派生类中的基类部分
//
Bulk_quote bulk_quote("红楼梦", 30, 2, 0.1);
Quote quote = bulk_quote;          //派生类对象向基类对象转换,quote对象成员为派生类对象中基类部分的copy

 

派生类的构造函数

派生类的构造函数对成员初始化时应该遵循这样的原则:每个类仅负责自己成员的初始,派生类应遵循基类的接口,对于继承过来的基类成员,应该调用基类的构造函数进行初始化。

初始化顺序:先初始化基类的成员,然后按照声明顺序依次初始化派生类的成员。这样一个自上而下的过程。

Bulk_quote(string bn, double pr, size_t min, double dis) :Quote(bn, pr), min_qty(min), discount(dis) {}

 

派生类与静态成员

类的静态成员只存在于类中,对象中不存在静态成员的任何数据,且静态成员仅存在唯一实例,只能被初始化一次。而且声明周期为整个程序的始终。

基类中定义的静态成员可以被派生类继承,但仅存在唯一实例。

如果派生类能访问基类静态成员时(需是public或protected的基类成员),派生类成员对基类中静态成员的访问方式:

 

派生类的声明:class 类名;

注意:派生类声明不能加上派生列表(声明语句的存在仅用于告诉程序某个名字的存在以及这个名字是一个什么类型的实体,声明语句不应该加上具体的细节)

 

作为基类的条件和防止被继承的方法

防止继承的方法:如果不希望某个类被用作基类,可以在类定义时,在类名后加关键字final,可以限制此类被用作基类。

final如果用于成员函数,则表示该成员函数不可以被派生类重写。

作为基类的条件:无final关键字且已被定义的类都可以作为基类,注意,仅声明而未定义的类不可以作为基类。

class NoDerived final{/* */};  //NoDerived不能用作基类
void Quote::print() const final{}    //Quote的print()成员不能被派生类重写

 

 

抽象基类

含有纯虚函数的类为抽象基类。纯虚函数是指在类内的虚函数声明后加=0的函数。因为抽象基类的纯虚函数是不完整的,所以抽象基类不能创建对象,抽象基类是专门设计作为基类使用的,其派生类若是没有重写抽象基类则还是抽象基类。

抽象基类作用是设计类的接口,然后在派生类中对接口进行覆盖,不同的派生类中因要求不一样,所以接口实现也不一样。如A商家和B商家使用的是两种不同的折扣策略,A为购买量超过一定值,会有一定折扣;B为超过一定值会有折扣,但再达到一定值将按原价算。A和B两种策略都有一定共通之处,即都需要有折扣适用的购买量,折扣的大小,

所以可以设计一个中间的接口类,然后A类和B类都继承中间接口类,然后各自去实现接口,这样相比与直接继承Quote类再设计会简单很多,代码也更规范。而中间做派生用的接口我们不希望用户去创建它的对象,它的价值仅做为类似公共接口的基类用,所以,中间的类因设计为抽象基类,公共的接口设计为纯虚函数,由各派生类去重写

 

注意:继承过程中要始终牢记一个原则:每个类只管初始化自己的成员,而继承来的成员,无论是多少层继承关系,都只调用上一层的构造函数去初始化,同样的上一层的构造函数又会调用上上层的构造函数,以此类推。

//基类
class Quote {
public:
	Quote() = default;
	Quote(string bn, double pr) :bookNo(bn), price(pr) {}
	string getIsbn()const { return bookNo; }
	virtual double net_price(size_t n) const { return n * price; }   //虚函数,希望派生类重写自己的版本
private:
	string bookNo = "";      //只能被所在类的其他成员函数访问。
protected:
	double price = 0;   //可以被派生类访问,但不能被其他用户访问。
};

//抽象基类
class Disc_quote :public Quote {
public:
	Disc_quote() = default;
	Disc_quote(string bn,double pr,size_t min,double disc):Quote(bn,pr),min_qty(min),discount(disc) {}
	double net_price(size_t n) const = 0;  //声明为纯虚函数,则Disc_quote为抽象基类,只能做基类用,不能创建对象。(纯虚函数只能在类内声明)

protected:
	size_t min_qty = 0;  //能享受折扣的购买量
	double discount = 0;  //折扣
};

//派生类A
class A final:public Disc_quote { //定义派生类A继承 类Disc_quote,且不能被用作基类(final修饰)
public:
	A() = default;
	A(string bn, double pr, size_t min, double disc):Disc_quote( bn,pr,min,disc) {}
	double net_price(size_t n) const override; //重写抽象基类Disc_quote继承来的纯虚函数,注意override关键字只能在类内声明后,在类外定义时不能加override
};
double A::net_price(size_t n) const{  
	return (n > min_qty) ? (min_qty * price + (n - min_qty) * price * discount) : n * price;
}

//派生类B
class B final:public Disc_quote {//定义派生类B继承 类Disc_quote,且不能被用作基类(final修饰)
public:
	B() = default;
	B(string bn, double pr, size_t min, double disc,size_t max):Disc_quote(bn,pr,min,disc),max_qty(max) {}
	double net_price(size_t n) const override; //重写接口(纯虚函数net_peice),override只能写在类内
private:
	size_t max_qty;
};
double B::net_price(size_t n) const 
{
	if (n < min_qty)
		return n * price;
	else if (n < max_qty)
		return min_qty * price + (n - min_qty) * price * discount;
	else
		return min_qty * price + (max_qty - min_qty) * price * discount+(n-max_qty)*price;
}

//测试
A a("三国",20,3,0.8);
B b("三国",20,2,0.85,7);
cout << "买9本《三国》,两种策略的价格为:" << endl;
cout <<"A:\t"<< a.net_price(9) << endl;
cout << "B:\t" << b.net_price(9) << endl;

测试结果如下:

抽象基类 Disc_quote是重构的典型例子,重构是C++开发工作中普遍而重量的现象,需要基类重构的一些规范、经验。

 

访问控制与继承再探

友元再探 

什么是友元?

友元是一种对类的访问控制权限,如果想让一个类的非成员函数或者其他类的成员函数访问此类的私有成员,我们需要为那些非成员函数或类进行友元声明friend

一个类的友元函数或友元类能访问该类的所有成员。

友元访问方式:因为友元函数或友元类访问本类的成员属于外部用户访问,需要通过对象或者类(静态成员)来访问。

友元关系不能被继承。

在继承体系中友元关系的特性:

①基类的友元函数或友元类能通过基类对象访问基类的所有成员,这种可访问性包括对派生类中继承的基类部分的访问。

           具体为:在基类的友元函数中可以通过基类对象派生类对象访问基类的所有成员。

//基类
class Base1 {
	friend class Pal;       //声明为基类的友元类
public:
	string pub_n="Base1_pub_n";
protected :
	string pro_n="Base1_pro_n";
private:
	string pri_n="Base1_pri_n";
};

//派生类
class Base1_son :public Base1{
	int j = 0;
};

//友元类
class Pal {
public:
	void f1(Base1& base1) {//基类的友元函数中可以通过基类对象访问基类的所有成员
		cout << base1.pub_n << endl;
		cout << base1.pro_n << endl;
		cout << base1.pri_n << endl;
	}
	void f2(Base1_son& base1_son) {  //基类的友元函数中可以通过派生类对象访问派生类中的基类部分。
		cout << base1_son.pub_n << endl;
		cout << base1_son.pro_n << endl;
		cout << base1_son.pri_n << endl;
		//cout << base1_son.j << endl;  错误,友元不能被继承,所以Pal类不是派生类的友元,自然不能通过派生类对象访问派生类自己的私有和受保护成员
	}
    
 //调用
 int main(){
    Base1 base1;
	Base1_son base1_son;
	Pal pal;
	pal.f1(base1);      //Pal是基类Base1的友元类,
	pal.f2(base1_son);
 return 0;
 }
    
    
    

 

②如果是派生类的友元,通过派生类对象可以访问派生类自己定义的所有成员(注意不包括基类继承来的部分)以及基类中的受保护成员

//基类
class Base {
public:
	Base() = default;
	int pub_num=3;
protected:
	int pro_num = 2;
private:
	int pri_num = 1;
};

//派生类
class Sneaky :public Base {
	friend void f(Sneaky & sneaky);     //声明f1()为Sneaky类的友元,可以通过Sneaky对象访问Sneaky自己的所有成员,另外,还能访问Base中的受保护成员和public成员。
	friend void f(Base& base);         //静态类型为Base,不管动态类型是Base还是Sneaky都只能访问Base中的public成员。
private:
	string j="private elem. of Sneaky";

};

void f(Sneaky & sneaky) {     //友元函数
	cout <<"Sneaky中private成员: " << sneaky.j << endl;
	cout << "Sneaky中Base的protected成员: " << sneaky.pro_num << endl;
}

void f(Base& base) {        //友元函数
	cout <<"Base中的public成员" << base.pub_num << endl;
}


//调用
int main(){
    Sneaky s;
	f(s);    //f(Sneaky&)和f(Base&)是Sneaky的友元
return 0;
}

 

 

15.7 构造函数与拷贝控制

15.7.1 虚析构函数

基类的析构函数必须定义一个虚析构函数,因为,如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类类型的指针时,将不会发生动态绑定,而是调用基类的析构函数,会产生未定义错误,因为我们回收的是一个派生类对象,而不是基类对象。

虚析构函数与普通的虚函数一样,其虚属性会被继承到派生类中,无论派生类使用的是自己定义的析构函数还是编译器合成的析构函数,都是虚析构函数。(同构造函数一样,每个类都有析构函数,用于回收类的对象,如果类设计者没有定义析构函数,则编译器会提供一下默认的合成析构函数。)

//基类
class Quote{
  Quote()=default;
  virtual ~Quote()=default; //虚析构函数
};

//派生类
class Bulk_Quote:public Quote{
   Bulk_quote()=default;
  ~Bulk_quote()=default;
};

int main(){
   Quote * iter=new Bulk_Quote();
   delete iter;   //delete将调用指针iter所指向对象的析构函数以回收对象,
                  //因基类中析构函数定义为虚函数,所以将调用iter动态类型的析构函数,即Bulk_Quote的析构函数,否则,如果没有在基类中定义虚析构函数
                  //将只能调用静态类型的析构函数,将发生未定义错误。

}

 

posted @ 2022-08-24 22:35  newloser  阅读(67)  评论(0)    收藏  举报