C++一些注意点之继承和派生

1.继承权限:

       public:公有派生,基类中所有成员在公有派生类中保持各个成员的访问权限。基类public成员,在派生类也为public成员;基类protected成员,在派生类可以访问,在派生类之外不能直接访问;基类private成员,除了基类的成员函数能访问之外,其他都不能访问。基类对基类对象和派生类对象的访问权限时一样的。

       protected:与私有继承的情况相同。两者的区别在于基类成员对其对象的可见性与一般类及其对象可见性相同。公有成员可见,其他成员不可见。基类的公有成员和保护成员都作为派生类的保护成员,并且不能被这个派生类的子类所访问。基类对派生类对象所有成员都不可见。保护继承无法再往下继承。

       private: 私有派生,基类中公有成员和保护成员在派生类中均变为私有的,在派生类中仍然可以直接访问这些成员,但是在派生类之外均不可直接使用基类的公有或私有成员。基类对基类对象保持原有权限,基类对派生类对派生类对象来说所有都是私有成员。私有继承无法再往下继承。

2.抽象类:定义一个类,只用作基类派生新类,而不能用作定义对象,该类称为抽象类。

      当把一个类的构造函数或析构函数访问权限定义为保护时,这种类称为抽象类。因为在定义对象的时候要调用构造函数和析构函数,所以构造函数或者析构函数定义为保护的权限时都行。但是派生类在构造时要调用基类的构造函数和析构函数,所以也不能将构造函数或者析构函数声明为私有的。

      另一种定义:当类中含有纯虚函数时,为抽象类。

3.初始化基类成员:派生类一方面要调用派生类的构造函数来初始化在派生类新增的成员,另一方面要调用基类的构造函数来初始化派生类中的基类成员。调用顺序是这样的:

      (1)调用基类的构造函数;(调用顺序取决于继承的一个顺序)

      (2)调用对象成员的构造函数;(调用顺序取决于在类对象在派生类中的声明顺序)

      (3)调用自己的构造函数。(初始化成员时,跟变量的定义顺序有关)

注:析构函数调用与这个过程相反。

例子:

#include<iostream>


using namespace std;


class Base1{
    int x;
public:
	Base1(int a)
	{
		x = a;
		cout<<"调用基类1的构造函数"<<endl;
	}
	~Base1()
	{cout<<"调用基类1的析构函数"<<endl;}
};


class Base2{
	int y;
public:
	Base2(int b)
	{
		y = b;
		cout<<"调用基类2的构造函数"<<endl;
	}
	~Base2()
	{cout<<"调用基类2的析构函数"<<endl;}
};


class Der:public Base1,public Base2{
private:
	int z;
	Base1 b1,b2;
public:
	Der(int a,int b):Base2(a),b1(200),b2(a+b),Base1(20)
	{
		z = b;
		cout<<"调用派生类的构造函数"<<endl;
	}
	~Der()
	{cout<<"调用派生类的析构函数"<<endl;}
};




int main()
{
	if(1){
	   Der d(100,200);
	}
	system("pause");
	return 0;
}

运行结果:

3.冲突与支配原则和赋值兼容性

(1)冲突

        一个派生类从多个基类派生而来,当基类中成员的访问权限设为public,且不同的基类成员就有相同的名字时,就会出现冲突。(这个一定同时继承的两个基类中有相同的名字才会冲突,派生类和基类有相同的名字不会冲突)例如:

#include<iostream>

using namespace std;

class Base1{
public:
    int x;
public:
	Base1(int a)
	{x = a;}
	void show(){cout<<"x="<<x<<endl;}
};

class Base2{
public:
	int x;
public:
	Base2(int b)
	{x = b;}
	void show(){cout<<"x="<<x<<endl;}
};

class Der:public Base1,public Base2{
private:
	int z;
public:
	void setx(int a){x=a;}  //D
};


int main()
{
	Der d; 
	d.show();               //E
	return 0;
}


        这时,D行访问基类中的成员,这是编译器无法确定是要访问属于哪一个类的x,即出现编译错误;同样的x也会错误,编译器不知道调用哪个基类的show函数。 

        解决这种冲突办法:使各基类中的成员不同;各基类将成员设为私有的,并设置相应的访问函数,访问函数不能重名;使用作用域来说明。

#include<iostream>

using namespace std;

class Base1{
    int x;
public:
	Base1(int a)
	{x = a;}
	void show(){cout<<"x="<<x<<endl;}
};

class Base2{
	int x;
public:
	Base2(int b)
	{x = b;}
	void show(){cout<<"x="<<x<<endl;}
};

class Der:public Base1,public Base2{
private:
	int z;
public:
	void setAx(int a){Base1:x=a;}  //D
	void setBx(int a){Base2:x=a;}  //D
};


int main()
{
	Der d; 
	d.Base1::show();
	d.Base2::show();
	return 0;
}


(2)作用域不能连续使用。例如,Der又派生一个类,则不能像这样使用:

#include<iostream>

using namespace std;

class Base1{
    int x;
public:
	Base1(int a)
	{x = a;}
	void show(){cout<<"x="<<x<<endl;}
};

class Base2{
	int x;
public:
	Base2(int b)
	{x = b;}
	void show(){cout<<"x="<<x<<endl;}
};

class Der:public Base1,public Base2{
private:
	int z;
public:
	void setAx(int a){Base1:x=a;}  
	void setBx(int a){Base2:x=a;}  
};

class Der1:public Der{
private:
public:
};

int main()
{
	Der1 d; 
	d.Der1::Base1::show();//这样的写法是错的
	d.Der1::Base2::show();//这样的写法是错的
	return 0;
}

解决方法:在Der增加成员:

      void showA(){cout<<"x="<<Base1::x<<endl;}
      void showB(){cout<<"x="<<Base2::x<<endl;}

(3)允许派生类的成员和基类的成员重名,但是访问的时候如果不带有作用域,在表示访问派生类中的成员,带有作用域可以访问基类的成员。

(4)赋值兼容规则:派生类的对象可以赋值给基类对象,但是丢生了派生了的信息,基类的对象不能赋值给派生类对象;派生类对象的地址可以赋值给基类的指针变量(派生类对象可以赋值给基类的引用)。


5.虚基类

(1)同一个基类引起的多份copy

        这样一个继承关系,A派生B和C,C又派生出D,这样D就有A中的两份copy。如下:

#include<iostream>

using namespace std;

class A{
public:
    int x;
	A(int a){x = a;}
};

class B:public A{
	int y;
public:
	B(int a=0,int b=0):A(a){y = b;}
	void PB(){cout<<"x="<<x<<'\t'<<"y="<<y<<endl;}
};

class C:public A{
	int z;
public:
	C(int a=0,int b=0):A(a){z = b;}
	void PC(){cout<<"x="<<x<<'\t'<<"z="<<z<<endl;}
};

class D:public B,public C{
	int m;
public:
	D(int a,int b,int d,int e,int f):B(a,b),C(d,e)
	{m = f;}
	void PD()
	{
		PB();//D   不能换成{cout<<"x="<<x<<'\t'<<"y="<<y<<endl;}
		PC();//E   不能换成{cout<<"x="<<x<<'\t'<<"z="<<z<<endl;}
		cout<<"m="<<m<<endl;
	}
};

int main()
{
	D d(100,200,300,400,500);
	d.PD();
	return 0;
}
运行结果:


注:D行和E行不能换成相应的语句,这样会引起冲突,必须采用前面作用域的解决方法。


(2)虚继承

       在多重派生的过程中,若欲使公共的基类在派生类中只有一份copy,可以将这种基类说明为虚基类。在派生的定义中,在基类的名字前面加上virtual关键字。

      在类的对象中都一个虚表指针,指向虚表(每个类含有一个虚表,续表中存放特定类的虚函数地址),虚表里存放了虚函数的地址。虚函数表里顺序存放虚函数地址的,不是链表存储的。在每个带虚函数的类中,编译器秘密设置一指针,称为vpoionter,指向这个对象的虚表,通过基类指针调用的时候静态插入取得这个指针,并在表中查找出函数地址的代码。《宝典p131》

#include<iostream>

using namespace std;

class A{
public:
    int x;
	A(int a=0){x = a;}
};

class B:virtual public A{
	int y;
public:
	B(int a=0,int b=0):A(a){y = b;}
	void PB(){cout<<"x="<<x<<'\t'<<"y="<<y<<endl;}
};

class C:public virtual A{
	int z;
public:
	C(int a=0,int b=0):A(a){z = b;}
	void PC(){cout<<"x="<<x<<'\t'<<"z="<<z<<endl;}
};

class D:public B,public C{
	int m;
public:
	D(int a,int b,int d,int e,int f):B(a,b),C(d,e)
	{
		m = f;
	}
	void PD()
	{
		PB();
		PC();
		cout<<"m="<<m<<endl;
	}
};

int main()
{
	D d(100,200,300,400,500);//D
	d.PD();
	d.x=400;                 //E
	d.PD();
	return 0;
}
 执行结果:


        首先在派生类D的对象d中只有基类的一个拷贝,因为改变x的值,输出时相同的;

       其次,x的初值为0,虽然在执行D行时调用了类D的构造函数,而x的初值仍然为0,为什么呢?因为调用虚基类的构造函数方法与一般的构造函数不同。由于虚基类经过一次或多次派生出来的派生类,每一个派生类中的构造函数中都调用了基类的构造函数。在本例中,B和C都调用了A的构造函数对A的成员进行初始化,这样编译器就无法确定是用B的构函数还是C的构造函数来初始化A中的变量。于是,编译器调用A的默认构造函数来初始化A中的变量,也就是说A中必须有默认的构造函数,或者编译器将报错。

        再次强调:用虚基类进行多重派生时,若虚基类没有缺省的构造函数,则派生类的每一个派生类的构造函数的初始化成员列表中都必须有对虚基类构造函数的调用。


6.一些例子《面试宝典P123》

(1)范例1

#include<iostream>

using namespace std;

class A{
protected:
	int x;
public:
	A(int a=0):x(a){};
	int getData()const
	{
		return doGetData();
	}
	virtual int doGetData() const 
	{
		return x;
	}
};

class B:public A{
protected:
	int x;
public:
    B(int a=1):x(a){};
	int doGetData()const
	{
		return x;
	}
};

class C:public B{
protected:
	int x;
public:
	C(int a=2):x(a){};
};


int main()
{
    C c(10);                        //1
    cout<<c.getData()<<endl;        //2
    cout<<c.A::getData()<<endl;     //3
    cout<<c.B::getData()<<endl;     //4
    cout<<c.C::getData()<<endl;     //5
    cout<<c.doGetData()<<endl;      //6
    cout<<c.A::doGetData()<<endl;   //7
    cout<<c.B::doGetData()<<endl;   //8
    cout<<c.C::doGetData()<<endl;   //9
    system("pause");
    return 0;
}

运行结果 :1 1 1 1 1 0 1 1

解析:

        //1:首先调用A类的构造函数,再调用B类构造函数,最后调用自己的构造函数。

        //2:调用C中的getData,在C中未定义,于是调用基类B的getData,发现基类B也没有这个函数,继续往上走,则调用A中的getData。getData调用了虚函数doGetData,于是首先搜索C有没有定义这个函数,发现没定义,于是搜寻C的基类B有没有定义该虚函数,发现定义了,则调用B::doGetData。在B中的 doGetData返回的是B类的x,所以打印1。

        //3:调用A::getData。后面分析同上红色地方。

        //4:直接调用B::getData,然后B没有,转到B的基类A,所以调用A::getData。后面的分析同上红色的地方。

        //5:同//2。

        //6:调用C类的虚函数doGetData,C中未定义,于是就转到C类的基类B的goGetData,所以打印出1。

        //7:直接调用A::doGetData,所以这次虚函数没有动态绑定了,直接打印出0。

        //8:直接调用B::doGetData,所以这次虚函数没有动态绑定了,直接打印出1。

        //9:直接调用 C::doGetData,但是C没有定义该虚函数,于是转到基类B,又打印出1。

注:这里有一个规则就是就近调用,及父辈如果有相关接口则调用父辈接口,父辈没有则转到祖父辈接口。

(2)范例2《面试宝典P125》

class A{
public:
    virtual void fun();
}
class B:public A{
public:
    virtual void fun();
}
void main(){
     A* pa = new A();
     B* pb = (B*)pa;  //1
}

     //1 最终的结果还是运行A::fun,因为虽然转换了指针类型,但是pa指向的内容始终没有发生变化。


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值