C++ 类继承学习笔记

143 阅读6分钟

继承

虚函数

  • 声明

    • 派生类必须在其内部对所有重新定义的虚函数进行声明,派生类可以在这样的函数之前加上关键字virtual关键字,但不是必须。
    • 关键词virtual只能出现在类内的声明函数语句之前,而不能用于类外部的函数定义,在派生类中相应的函数将隐式的是虚函数(不加virtual的情况)
    • 派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须要与它覆盖的基类函数完全一致,返回值也一样,但是如果是引用或指针的化,也就是说如果D由B派生得到,则基类的虚函数可以返回B*而派生类的对应函数可以返回D*.
  • 定义

    • 必须为每个虚函数提供定义,不管是否有被使用到,因为我们直到运行时才能知道到底调用了哪个模板
    • override 关键字
    • 重写基类中的虚函数
    • 在形参、const关键字、引用限定符后面
    • 只有虚函数才能被重新
    • final 关键字
    • 不允许派生类覆盖
    • override、final 关键字

    出现在形参列表包括(const和引用修饰符)以及尾置返回累心之后。

  • 范围

    • 任何构造函数指外的非静态函数都可以是虚函数,也可以是析构函数
    • 一旦某个函数被声明程虚函数,则派生类中它都是虚函数

默认实参

虚函数可以有默认实参,若函数调用了默认实参,则实参值由静态类型决定,所以基类和派生类中定义的默认实参最好一致

class Quote 
{
public:
...
	virtual void d(int a = 10,double b = 20.0 ) {
		std::cout << "Quote: "<<"a: " << a << " b: " << b << std::endl;
	};
...
};

class Bulk_quote :
    public Quote
{
public:
...
    virtual void d(int a = 20, double b = 30) {
        std::cout <<"Bulk_quote: "<< "a: " << a << " b: " << b << std::endl;
        
    };
...
};

Bulk_quote bulk_quote("1-1-1",10,10,0.5); Quote quote("1-1-2", 10); quote.d(); // Quote: a: 10 b: 20 Quote& q = bulk_quote; q.d();//Bulk_quote: a: 10 b: 20

如果派生类的默认参数和基类的默认参数不一致,传入派生类函数将是基类函数定义的默认实参

回避虚函数机制

  • 使用作用域
Bulk_quote *bulk_quote;
bulk_quote->Quote::net_price(42)

该代码强行调用Quote的net_price函数,而不管bulk_quote实际指向的对象类型是什么,该调用将在编译时完成解析

如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域符,则在运行时将被解析为对派生类版本自身调用,从而导致无限递归

基类通常应该定义一个虚析构函数,即使该函数不执行任何操作也是如此!

继承与静态成员

- 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。
- 如果静态成员是publicprotected的那么基类和派生类都能使用该静态成员

阻止继承(final 关键字)

  • 类(不能继承)
class Quote final{} // 不能作为基类
  • 函数

类型转换与继承

	Bulk_quto bulk;
	Quote* itemP = &bulk;
	Bulk_quote *bulkP = itemP;// 错误,不能将基类转换成派生类
	Bulk_quote *bulkP = (Bulk_quote *)itemP; // 
	Bulk_quote *bulkP = dynamic_cast<Bulk_quote *>(itemP);

dynamic_cast

class A{...}; 
class B:public A{...}; 
class C:public B{...}; 
void Fun1(B* pB) 
{ 
A* pA  = (A*)pB; 
C* pC  = (C*)pB; 
... 
} 

如果这样调用Fun1:

			Fun1(((B*)new C));

的确不会有问题,但如果是这样呢:

                         Fun1(new B);

pC不会为NULL,能够想到使用pC指针时就程序就悲剧了. 更严重情况下,如果是这样:

			Fun1((B*)0X00005678);//0X00005678是一个随机值

pA,pC就不会是NULL,强制类型转换总是能够成功的,但使用这两个指针时程序肯定崩溃.当然你可以使用异常处理机制来处理这样的错误,不过这有点大才小用的感觉,最好能够找到一种能够检查出类型转换能否成功的办法.这时dynamic_cast就能大显身手了.

A* pA  = dynamic_cast<A*>pB;// upcast. 
if (NULL == pA){...} 
C* pC  = dynamic_cast<C*>pB;// downcast. 
if (NULL == pC){...} 

静态类型和动态类

  • 静态类型 静态类型是在编译时已知的,是变量声明时的类型或者表达式生成的类型

  • 动态类型 其动态类型是变量或者表达式表示内存的对象的类型,直到运行时才可知

如果表达式既不是指针也不是引用,则其动态类型和静态类型会一直一样

访问控制和继承

  • protected
    1. 对类用户不可以访问,但是派生类成员和友元来说是可以访问的
    2. 派生类的成员和友元只能通过派生类对象来访问基类的受保护成员
class Quote 
{
public:
	Quote() { std::cout << "Quote default"; };
	Quote(const std::string& book, double sales_price) :bookNo(book), price(sales_price) {}
	virtual ~Quote() = default;
private:
	std::string bookNo;
protected:
	double price = 0.0;
};
class Bulk_quote :public Quote
{
public:
    double net_price(std::size_t n) const override;
    double GetPrice(Bulk_quote& bulk) { return bulk.price; }
    // double GetPrice(Quote& quote) { return quote.price; } // 错误,不能通过Quote的对象访问
};
class Quote 
{
	friend    double GetPrice(Quote& quote);
private:
	std::string bookNo;

protected:
	double price = 0.0;
};
double GetPrice(Quote& quote) { return quote.price; } // 正确,友元函数可以访问对象中的所有成员
  • 派生类访问符的影响 派生类访问符对于派生类的成员(友元)能否访问其直接基类的成员没有什么影响, 派生类访问符的目的是控制派生类用户(包括派生类的派生类)对于基成员的访问权限
#pragma once
class Base
{
public:
	int public_value = 0;
private:
	int private_vale = 0;
protected:
	int protected_value = 0;
};
class Pub_Derv :public Base {
	void debug() {
		// private_vale 不能访问
		std::cout << public_value << protected_value;
	}
};
class  Prv_Derv:private Base
{
	void debug() {
		// private_vale 不能访问
		std::cout << public_value << protected_value;
	}
};
class  Pro_Derv :protected Base
{
	void debug() {
		// private_vale 不能访问
		std::cout << public_value << protected_value;
	}
};
int main()
{
	pub.public_value;
	//pub.private_vale; // 错误,不可访问
	//pub.protected_value; 错误,不可访问
	Prv_Derv prv;
	//prv.public_value;// 错误,不可访问
	//prv.private_vale; // 错误,不可访问
	//prv.protected_value; 错误,可访问
	Pro_Derv pro;
	//prv.public_value;// 错误,不可访问
	//pub.private_vale; // 错误,私有不能访问
	//pub.protected_value; //错误不可访问
}

  • 派生访问说明符还可以控制继承派生类新类的访问权限
#pragma once
class Base
{
public:
	int public_value = 0;
private:
	int private_vale = 0;
protected:
	int protected_value = 0;
};

class Pub_Derv :public Base {
	void debug() {
		// private_vale 不能访问
		std::cout << public_value << protected_value;
	}
};
class  Prv_Derv:private Base
{
	void debug() {
		// private_vale 不能访问
		std::cout << public_value << protected_value;
	}

};
class  Pro_Derv :protected Base
{
	void debug() {
		// private_vale 不能访问
		std::cout << public_value << protected_value;
	}
};
class Derivaed_from_public :public Pub_Derv {
	int use() { return protected_value; }
	int use2() { return public_value; }
};
class Derivaed_from_private :public Prv_Derv {
	// 错误, protected_value 变成私有的
	// int use() { return protected_value; }
	// 	// 错误, public_value 变成私有的
	//int use() { return public_value; }
};

总结

派生类访问说明符会将派生类中继承基类部分的访问控制符改变,导致派生类的派生类访问会受派生类影响,也就是说孙子类要访问爷爷类中的public属性还要看父类中的派生类访问符。

派生类向基类转换的可访问性

  • 只有当D公有的继承B时,用户代码才能使用派生类向基类转换
  • 不论D以什么方式继承B,则D的成员函数和友元,都能使用派生类向基类转换
  • 如果D继承B的方式时public或protected,则D的派生类成员可以使用D向B的类型转换

友元和继承

  • 不能继承友元关系;每个类负责控制各自成员的访问权限