C++进阶剖析(十 九)之继承

1.1

  • 继承是非常重要的,在面向对象编程中有着举足轻重的地位。尤其是在设计模式中。

1.1.1 生活中的继承
龙生龙,凤生凤,老鼠儿子会打洞。
生活中的继承是一种长相和行为的继承。

1.1.2 类之间的关系

  • 组合关系(思想很重要
    一台电脑有CPU,主板,显示器等组成
  • 继承关系(思想很重要
    电脑分为惠普,联想等(is a )
    1.1.3

1.2 类的组合关系
1.2.1代码实例
电脑类的实现

#include <iostream>
using namespace std;

class Disk
{
public:
	Disk()
	{
		printf("Disk()\n");
	}
	~Disk()
	{
		printf("~Disk()\n");
	}

};

class CPU
{
public:
	CPU()
	{
		printf("CPU()\n");
	}
	~CPU()
	{
		printf("~CPU()\n");
	}
};
class MainBoard
{
public:
	MainBoard()
	{
		printf("MainBoard()\n");
	}
	~MainBoard()
	{
		printf("~MainBoard()\n");
	}

};
class Memory
{
public:
	Memory()
	{
		printf("Memory()\n");
	}
	~Memory()
	{
		printf("~Memory()\n");
	}

};

class  Computer
{
public:
	Computer()
	{
		printf("Computer()\n");
	}
	~Computer()
	{
		printf("~Computer()\n");
	}

private:
	Disk disk;
	Memory memory;
	MainBoard mainBoard;
	CPU  cpu;
};

int main()
{	
	Computer c1;
	return 0;
}

  • 先父母 再客人,后自己

运行结果
在这里插入图片描述

1.2.2 组合关系的特点

  • 将其他类的对象作为当前类的成员来使用
  • 当前类的对象于成员对象的生命期相同
  • 成员对象在用法上于普通对象完全一致
  • 组合关系是同生死,共存亡

注意: 实际中我们如果能用组合尽量不要用继承,因为继承的复杂度远大于组合的复杂度。

1.3 继承关系
1.3.1 继承

  • 子类对象拥有父类的所有属性和行为
  • 子类就是一种特殊的父类
  • 子类对象可以当做父类对象使用,(子类对象当做父类对象使用会退化成父类对象,也就是子类中属性和方法不能使用了)
  • 子类中可以添加父类中没有的方法和属性
  • 继承可以进行代码复用

1.3.2 重要规则

  • 子类是一个特殊的父类
  • 子类对象可以直接初始化父类对象
  • 子类对象可以直接赋值给父类对象

1.3.3 示例代码

class Parent
{
private:
	int mv;
public:
	Parent()
	{
		cout<<"Parent()"<<endl;
		mv = 10;
	}
	void method()
	{
		cout<< "mv= "<<mv <<endl;
	}
};

class Child :public Parent  //
{
public:
	void print()
	{
		cout<<"i'm child class"<<endl;
	}
};

int main()
{  
	Child c ;
	c.method();  //继承父类的方法

	Parent p1 = c; // 调用父类copy 构造

	Parent p2 ;		// 
	p2 =c;			//子类对象给父类对象赋值
	return 0;
}

在这里插入图片描述

1.4 继承中的情况分析
1.4.1对类的属性在继承中如何初始化

  • 类的属性在继承中初始化的时候分工是很明确的。
    (1)父类的成员变量的初始化在父类中进行
    (2)子类的成员变量在子类中进行
    (3)子类中的类对象的初始化在自己的类中进行初始化,仅仅是在子类中调用初始化列表就行

  • 实例程序代码包含了:
    (1)二阶构造(父类中二阶构造,子类中二阶构造),注意父类和子类中二级构造的方式,
    父类中二级构造和子类中二阶构造是分工进行完成的,只是简单的函数调用而已。
    (2)子类中包含类对象(组合的关系),
    (3)子类中包含类对象的指针(利用二阶构造进行初始化,),
    (4)成员函数是类对象的指针,类对象如何利用组合类的成员函数对类对象进行初始化(这里只能利用成员函数,因为这在类的外部 )
    (5)父类和子类中包含同名的成员函数 print函数,成员函数如何被调用(这是后面多态的必要条件)
    (6)子类对象初始化父类对象以后,父类对象不能使用子类新增加的成员变量和成员函数。
    (7)使用了二阶构造以后,copy构造函数还有用吗?
    Child * p2 =child; //true ,因为仅仅是将一个指针指向了一个地址
    Child c2 =*child;//error //浅copy ,发生多次释放内存情况,导致程序错误。
    具体情况看下面的代码。

#include <iostream>
using namespace std;

class Test
{
protected:
	int  a;
public:
	Test(int a =0)
	{
		this->a =a;
	}
	int getA()const
	{
		return a;
	}
	void setA(int a)
	{
		this->a =a;
	}
	void print()
	{
		cout <<"Test() a =" << a <<endl;
	}
	~Test()
	{
		cout<<"~Test()"<<endl;
	}
};
class Parent
{
protected:
	int mi;
	char * name1;

	Parent(int i =0)
	{
		this->mi =i;
	}
	bool TwoConstructor(const char* name1)
	{
		bool ret =true;
		this->name1 =new char[strlen(name1)+1];  //importance
		if(this->name1 != NULL)
		{
			strcpy(this->name1,name1);
		}
		else
		{
			ret =false;
		}
		return ret;
	}
	
public:
	static Parent * NewInstance(int i,const char* name1)
	{
		Parent * ret = new Parent(i);
		if( !(ret && ret->TwoConstructor(name1)))
		{
			delete ret;
			ret = NULL;
			cout<< "Parent::NewInstance(int i,const char* name1) failure "<<endl;
			return ret;
		}
		cout<< "Parent::NewInstance(int i,const char* name1) success "<<endl;
		
		return ret;
	}
	void print()
	{
		cout << "mi = "<< mi<< ",  name1 = "<<name1<<endl;
	}
	~Parent()
	{
		delete[]  name1;
		name1 = NULL;
		cout <<"~Parent()"<<endl;
	}
};

class Child :public Parent
{
protected:
	int mj;
	char *name2;
	Test t1;
	Test *p1;
	Child(int j,int a):t1(a)
	{
		this->mj =j;
	}
	bool TwoConstructor(const char* name1,const char* name2,const Test& obj)
	{
		bool ret =true;
		ret =Parent::TwoConstructor(name1);
		if(!ret) return ret;
		this->name2 = new char[strlen(name2)+1];
		if(name2 != NULL)
		{
			strcpy(this->name2,name2);
		}
		else
		{
			return false;
		}
		p1= new Test;
		p1->setA(obj.getA());

		return ret;
	}
public:
	static Child * NewInstance(int j,int a,const char* name1,const char* name2,const Test& obj)
	{
		Child * ret = new Child(j,a);
		if(!(ret && ret->TwoConstructor(name1,name2,obj)))
		{
			delete ret;
			ret = NULL;
		cout<< "Child::NewInstance(int i,const char* name1) failure "<<endl;
		}
		cout<< "Child::NewInstance(int i,const char* name1) success "<<endl;
		return ret;
	}
	void print()
	{
		Parent::print();
		cout<< "mj  =  "<< mj <<"    , name2 = "<<name2 <<endl;
		t1.print();
		p1->print();
	}
	void test1()
	{
		printf("test子类对象初始化父类对象以后,子类对象新添加的成员变量的成员函数是否还能用\n ");
	}
	~Child()
	{
		delete[] name2;
		delete p1;
		cout <<"~Child()"<<endl;
	}
}; 


int main()
{	
	/*
	Parent * parent = Parent::NewInstance(4,"zhangsan");
	if(parent != NULL)
	{
		parent->print();
		delete parent;
	} 
	*/

	Test t1(2);
	Child * child  = Child::NewInstance(10,20,"zhangsan","lisi",t1);
	
	if(child != NULL)
	{
		child->print();
		delete child;
	}

	Parent * parent1 =child;
	//parent1->test();//error 子类对象初始化父类对象以后,子类对象新添加的成员变量和成员函数不能使用
	return 0;
}

1.4.2 继承的意义

  • 继承是C++中代码复用的重要手段。通过继承,可以活动父类的所有功能,并且可以再子类中重写已有功能,或者添加新功能。

1.4.3

1.5 继承的中的访问级别
1.5.1 思考

  • 子类是否可以直接访问父类的私有成员?

根据面向对象理论

  • 子类拥有父类的一切属性和行为 ===========》子类能够直接访问父类的私有成员
    根据C++语法:
  • 外界不能直接访问类的private成员=======》子类不能直接访问父类的私有成员

实验证明:

class Parent
{
private:
	int mv;
public:
	Parent()
	{	
		mv = 10;
	}
	int value()
	{
		return mv;
	}
};
class Child :public Parent  //
{
public:
	int addValue(int v)
	{
		mv = mv +v;
	}
};
int main()
{ 
	return 0;
}

在这里插入图片描述
如何解决呢?父类中使用protected关键字

1.5.2 protected

  • 面向对象中的访问级别不只是 publicprivate
  • 可以定义protected访问级别
  • 关键字protected的意义
    • 修饰的成员不能被外界直接访问
    • 修饰的成员可以被子类直接访问

1.5.3 思考

  • 为什么需要protected关键字
    在这里插入图片描述
    1.5.4 组合和继承的综合实例
  • object 类(被继承),Point(点类) ,Line(类)
#include <iostream>
#include <string>
#include <sstream>
using namespace  std;
class Object
{
protected:
	string mName;
	string mInfo;
public:
	Object()
	{
		mName = "Object";
		mInfo = "";
	}
	string name()
	{
		
		return mName;
	}
	string info()
	{
		return mInfo;
	}
};
class Point :public Object
{
private:
	int mX;
	int mY;
public:
	Point(int mx = 0,int my =0)
	{
		ostringstream s;	
		mName = "Point";
		mX = mx;
		mY = my;
		s <<"p("<<mX<<","<<mY<<")";
		mInfo = s.str();
	}
};
class Line :public Object
{
private:
	Point mP1;
	Point mP2;
public:
	Line(Point mp1,Point mp2)
	{
		ostringstream s;
		mP1 = mp1;
		mP2 = mp2;
		mName ="Line";
		s <<"Line from "<<mP1.info() <<" to "<<mP2.info();  
		mInfo = s.str();
	}
};
int main()
{  
	Object o;
	cout<<o.name()<<endl;
	cout<<o.info()<<endl;
	cout<< endl;
	
	Point p1(10,20);
	cout<<p1.info()<<endl;;
	cout<<p1.name()<<endl;
	
	Point p2(1,2);
	
	cout<<endl;
	Line line(p1,p2);
	cout<< line.info()<<endl;
	cout<< line.name()<<endl;
	return 0;
}

在这里插入图片描述

  • 注意在写代码过程中,上面代码中下面一行报错了
    s <<"Line from "<<mP1.info() <<" to "<<mP2.info();报错的原因不在于这一行,而是在Object类中的info()函数的时候返回值是void ,应该改成 string

1.6 继承中的构造和析构
1.6.1子类构造函数

  • 子类构造函数必须对继承来的成员进行初始化
    (1)直接通过初始化列表或者赋值的方式进程初始化
    (2)调用父类构造函数进行初始化

1.6.2 父类构造函数在子类中的调用方式

  • 默认调用
    (1)适用于无参数构造函数和使用默认参数的构造函数
  • 显示调用
    (1)通过初始化列表进行调用
    (2)适用于所有父类构造函数

1.6.3 对象创建时构造函数调用顺序
1.调用父类构造函数
2. 调用成员变量的构造函数
3. 调用类自身的构造函数

总结1:先父母,再客人,后自己

总结2:析构与构造顺序相反

1.7 父子间的冲突
1.7.1 思考

  • 子类中是否可以定义父类中的同名成员
  • 如果可以,如何区分?如果不可以,为什么?
  • 写代码说明
class Parent
{
public:
    int mi;
};
class Child : public Parent
{
public:
    int mi;
};
int main()
{
    Child c;
    c.mi = 100;    // mi 究竟是子类自定义的,还是从父类继承得到的?
    return 0;
}
  • mi 究竟是子类自定义的,还是从父类继承得到的?

1.7.2 理论

  • 子类可以定义父类中的同名成员

  • 子类中的成员将隐藏父类中的同名成员

  • 父类中的同名成员依然存在于子类中

  • 通过 作用域分辨符(::)访问父类中的同名成员

  • 代码

namespace A
{
    int g_i = 0;
}

namespace B
{
    int g_i = 1;
}

class Parent
{
public:
    int mi;
    
    Parent()
    {
        cout << "Parent() : " << "&mi = " << &mi << endl;
    }
};

class Child : public Parent
{
public:
    int mi;
    
    Child()
    {
        cout << "Child() : " << "&mi = " << &mi << endl;
    }
};

int main()
{
    Child c;
    c.mi = 10;      
    c.Parent::mi = 100;
    
    cout << "&c.mi = " << &c.mi << endl;
    cout << "c.mi = " << c.mi << endl;
    cout << "&c.Parent::mi = " << &c.Parent::mi << endl;
    cout << "c.Parent::mi = " << c.Parent::mi << endl;
    
    return 0;
}

1.7.3 函数重载再论

  • 子类中定义的函数是否能重载父类中的同名函数?
  • 示例1
class Parent
{
public:
	int mi;
	void add(int v)
	{
		mi += v; 
	}
	void add(int i ,int j)
	{
		mi += (i + j);
	}
};
class Child :public Parent
{
public:
	int mi;
};
int main()
{  
	Child c1 ;
	c1.mi = 100;
	c1.Parent::mi  =1000;
	c1.add(1);    //why    为什么都累加到父类中的mi了? 正常逻辑来看,当父类定义mi,以及add函数的时候,还没有子类中的mi,add作用于父类的mi是合情合理的。
	c1.add(2,3);
	cout<< c1.mi<<endl;   // 100
	cout<< c1.Parent::mi <<endl;  //1006
	return 0;
}
  • 示例代码2:
class Parent
{
public:
	int mi;
	void add(int v)
	{
		mi += v; 
	}
	void add(int i ,int j)
	{
		mi += (i + j);
	}
};
class Child :public Parent
{
public:
	int mi;
	void add(int a,int b, int c)
	{
		mi += (a + b + c);
	}
};

int main()
{  
	Child c1 ;
	c1.mi = 100;
	c1.Parent::mi  =1000;
	c1.add(1);     
	c1.add(2,3);
	cout<< c1.mi<<endl;  
	cout<< c1.Parent::mi <<endl; 
	return 0;
}

在这里插入图片描述

  • 为啥会报错

  • 原因: 在子类中定义和父类同名的函数也会发生同名覆盖,父类中的add函数被隐藏 了。父类中的add 和子类中的add 不可能是重载,因为父类中的add和子类中的add在不同的作用域中。

  • 那么如何解决上面的问题呢? 加作用域符 c1.Parent::add(1);

  • 实验结论: 同样会发生同名覆盖。

  • 子类中的函数将隐藏父类的同名函数

  • 子类无法重载父类中的成员函数

  • 使用作用域分辨符访问父类中的同名函数

  • 子类可以定义父类中完全相同的成员函数(多态的必备)

1.8 同名覆盖引发的问题
1.8.1 父子兼容性

  • 子类对象可以直接赋值给父类对象

  • 子类对象可以直接初始化父类对象

  • 父类指针可以直接指向子类对象

  • 父类引用可以直接引用子类对象

  • 代码示例

class Parent
{
public:
	int mi;
	Parent(int mi =0)
	{
		this->mi =mi;
	}
	void add(int v)
	{
		mi += v; 
	}
	void add(int i ,int j)
	{
		mi += (i + j);
	}
};
class Child :public Parent
{
public:
	int mv;
	Child(int mv = 0)
	{
		this->mv = mv;
	}
	void add(int a,int b, int c)
	{
		mv += (a + b + c);
	}
};

int main()
{  
	Parent  p;
	Child c;   
	p = c;				 // 子类对象可以直接赋值给父类对象
	Parent p1(c);		 // 子类对象可以直接初始化父类对象

	Parent &p3 =c;		 //父类引用可以直接引用子类对象

	Parent * p4 = &c;	// 父类指针可以直接指向子类对象

	p3.mi = 100;
	p3.add(1);		// // 没有发生同名覆盖
	p3.add(2,3);   //   没有发生同名覆盖

	// 为什么编译不过? 
//	p4->mv = 0;
//	p4->add(1,2,3);

	return 0;
}

1.8.2 父类指针(引用)指向子类对象时

  • 子类对象退化成父类对象
  • 只能访问父类中定义的成员
  • 可以直接访问被子类覆盖的同名成员

1.8.3函数重写

  • 子类中可以重定义父类中已经存在的成员函数

  • 这种重定义发生在继承中,叫函数重写

  • 函数重写是同名覆盖的一种特殊情况

  • 当函数重新遇上赋值兼容会发生什么?
    1.8.4 举例

class Parent
{
public:
	int mi;
	Parent(int mi =0)
	{
		this->mi =mi;
	}
	void print()
	{
		cout<<"this is parent"<<endl;
	}
};
class Child :public Parent
{
public:
	int mv;
	Child(int mv = 0)
	{
		this->mv = mv;
	}
	void print()
	{
		cout<<"this is child"<<endl;
	}
};
void how_to_print(Parent * p)
{
	p->print();
}
int main()
{  
	Parent  p;
	Child c;   
	p.print();
	c.print();
	how_to_print(&p);   // this is parent ?
	how_to_print(&c);    // this is parent?
	return 0;
}
  • 结果
    在这里插入图片描述
    1.8.5 问题分析
  • 编译期间 ,编译器只能根据指针的类型判断所指向的对象
  • 根据赋值兼容,编译器认为父类指针指向的是父类对象
  • 因此,编译结果只可能是调用父类中定义的同名函数
void how_to_print(Parent * p)
{
	p->print();
}

在编译这个函数的时候,编译器不可能知道指针p究竟指向了什么。但是编译器没有理由报错,于是,编译器认为最安全的做法是调用父类的print函数,因为父类和子类肯定都有相同的print函数。

  • 问题引出,编译器的处理方法是合理的吗? 是期望的吗?下节介绍
    参考一 :狄泰软件学院C++进阶剖析
    参考二 : C++ primer中文版
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值