浅析C++继承与派生

测试环境:

Target: x86_64-linux-gnu

gcc version 5.3.1 20160413 (Ubuntu 5.3.1-14ubuntu2.1) 

定义

要分析继承,首先当然要知道什么是继承:继承是面向对象程旭设计中使代码可以复用的最重要的手段,它允许程序员在原有类特性的基础上进行扩展,增加功能。这样产生的新类,就叫做派生类(子类)。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。

继承的格式

class 子类名 :继承权限 基类名

比如下面分别定义了两个类:

class A
{
public:
	int pub;
protected:
	int pro;
private:
	int pri;
};

class B: public A
{
};
如上我们就说类B继承了类A,类B叫做类A的派生类或者子类,A类叫做B类的基类或者父类。

继承关系&访问限定符

之前学习类的成员访问限定符的时候都知道public, protected, private 这三种访问限定符的作用,public修饰的类成员可以在类外被访问,而protected与private则不可以。这三种访问权限又对应这三种继承关系:

 

继承关系可以影响子类中继承自父类的成员变量的访问权限,还是在上个栗子的基础上,我们定义一个B类对象进行如下操作;

int main()
{
	B b1;
	b1.pub;
	b1.pro;
	b1.pri;


	return 0;
}
编译则会报错:

              

会提示pro与pri变量访问权限分别为protected和private,我们不能在类外使用它们。类似的,在B中定义这样一个成员函数:

class B: public A
{
	void fun()
	{
		cout<<pub;
		cout<<pro;
		cout<<pri;
	}
};
会报这样的错:

               

即基类中的私有成员在子类中是不可见的。关于三种继承方式的成员访问权限总结如下表:

             

总结:
1. 基类的 private 成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为 protected 。可以看出保护成员限定符是因继承才出现的。
2. public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象。
3. protetced/private继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分,是 has-a 的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大多数的场景下使用的都是公有继承。
4. 不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,基类的私有成员存在但是在子类中不可见(不能访问)。
5. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
6. 在实际运用中一般使用都是public继承,极少场景下才会使用protetced/private继承

继承关系中构造/析构函数调用顺序

在现有类的基础上添加如下的构造与析构函数:

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

	~A()
	{
		cout<<"~A()"<<endl;
	}

public:
	int pub;	
protected:
	int pro;
private:
	int pri;
};

class B: public A
{
public:
	
	B()
	{
		cout<<"B()"<<endl;
	}

	~B()
	{
		cout<<"~B()"<<endl;
	}
};
然后,在main函数中定义一个类B的对象:B b; 编译运行,看看输出语句的顺序:

                

先基类构造,后子类构造;析构的时候先析构子类,后析构基类。依旧和以前一样,先构造的后析构(因为在栈上)。

让我们走进几行代码的反汇编世界:

               

这是程序现在运行到了b的定义语句。=> 所指,是当前运行的汇编语句。可以看到,第三条汇编语句调用了B类的构造函数。咦?怎么跟我们刚刚看到的顺序不太一样!不急,先往下看。直接 ni 运行到第三条汇编,然后用 si 命令跟进去:

               

可以看到,程序在正式进入B类的构造函数之前,先调用了A类的构造函数,照这么来看,可以推测出是编译器自动的在B类的构造数的初始化列表位置调用了A类的构造函数。还是让我们把程序看完:

                    

果然,又进入了类A的构造函数。

                    

从A类构造函数出来后,才正式进入类B构造函数。

                  

出main函数作用域时,先调用了B类的构造函数

                

在B类构造函数的末尾调用了A类构造函数。整个过程与我们看到的输出信息一致。

如果类B中还有一个成员变量是一个类对象,那么构造与析构调用顺序又是哪样?

class T
{

public:
	T(int i = 1) 
	{
		cout<<"T()"<<endl;
	}

	~T()
	{
		cout<<"~T()"<<endl;
	}
};

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

	~A()
	{
		cout<<"~A()"<<endl;
	}

public:
	int pub;	
protected:
	int pro;
private:
	int pri;
};

class B: public A
{
public:
	
	B()
	{
		cout<<"B()"<<endl;
	}

	~B()
	{
		cout<<"~B()"<<endl;
	}
public:
    T t;
};
还是刚刚的main函数,在运行一下程序:

                      

很明显,先调用基类构造,然后是成员对象的构造函数,最后是该类自身的构造函数,析构函数顺序则相反。具体的汇编代码就不演示了。总结一下:

【说明】
1、基类没有缺省构造函数,派生类必须要在初始化列表中显式给出基类名和参数列表。
2、基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数。
3、基类定义了带有形参表构造函数,派生类就一定要定义构造函数。

继承体系中的作用域

  1. 基类和派生类是不同的作用域
  2. 同名隐藏:子类和父类中有同名成员时,子类成员将屏蔽父类对成员的直接访问。(在子类成员函数中,可以使用 基类::基类成员 访问父类成员)
  3. 在实际中在继承体系里面最好不要定义同名的成员

class A
{
public:
	int pub;
};

class B: public A
{
public:
	int pub;
};

int main()
{
    B b;
    b.pub = 1;    //访问的是派生类的成员变量,基类同名被隐藏
    b.A::pub = 2;    //指明作用域,访问基类成员变量

    return 0;
}
 

继承与转换--赋值兼容规则--(前提:public继承)

  1. 子类对象可以赋值给父类对象
  2. 父类对象不能赋值给子类对象
  3. 父类的指针/引用可以指向子类对象
  4. 子类的指针/引用不能指向父类对象(但可以通过强制类型转换完成)

友元与继承

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。

class Person
{
    friend void Display(Person &p , Student&s);
protected :
    string _name ;
};

class Student: public Person
{
protected :
    int _stuNum ;
};

void Display(Person &p , Student &s)
{
    cout<<p._name<<endl;
    cout<<s._name<<endl;
    cout<<s._stuNum<<endl; //error
}
int main()
{
    Person p;
    Student s;
    Display (p, s); 
    return 0;
}
 

继承与静态成员

基类定义了static成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。如下:

class A
{
public:
	static int i;	//注意这里只是声明
};

int A::i = 0;

class B : public A
{};

int main()
{
	A a;
	B b;
	
	cout<<"a.i="<<a.i<<" "<<"b.i="<<b.i<<endl;
	a.i++;
	b.i++;
	cout<<"a.i="<<a.i<<" "<<"b.i="<<b.i<<endl;

	return 0;
}
输出:

                  

单继承&多继承&菱形继承

【单继承】

一个子类只有一个直接父类时称这个继承关系为单继承。

                                                            

【多继承】

一个子类有两个或以上直接父类时称这个继承关系为多继承。

                                  

【菱形继承】

              

                                                

例:

class Person
{
public :
	string _name ; // 姓名
};
class Student : public Person
{
	protected :
	int _num ; //学号
};
class Teacher : public Person
{
protected :
	int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
	string _majorCourse ; // 主修课程
};
void Test ()
{
	// 显示指定访问哪个父类的成员
	Assistant a ;
	a.Student ::_name = "xxx";
	a.Teacher ::_name = "yyy";
}

看一下菱形继承的构造与析构函数调用顺序:(main函数中创建了一个D类对象)

B类和C类继承A类,D类继承B和C类:

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

	~A(){cout<<"~A()"<<endl;}
};

class B: public A
{
public:
	
	B(){cout<<"B()"<<endl;}

	~B(){cout<<"~B()"<<endl;}
};

class C : public A
{
public:
	C(){cout<<"C()"<<endl;}

	~C(){cout<<"~C()"<<endl;}
};

class D : public B, public C
{
public:
	D(){cout<<"D()"<<endl;}
	
	~D(){cout<<"~D()"<<endl;}
};

        

对照对象模型来看会很清楚。

虚继承--解决菱形继承的二义性和数据冗余的问题

1. 虚继承解决了在菱形继承体系里面子类对象包含多份父类对象的数据冗余&浪费空间的问题。
2. 虚继承体系看起来好复杂,在实际应用我们通常不会定义如此复杂的继承体系。一般不到万不得已都不要定义菱形结构的虚继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能上的损耗。

                           

实际存放情况:

两个Address处存放的是地址,这个地址所代表的空间存放了由当前Address这个位置到_name的偏移量。具体情况略显繁琐,不便演示,可以查看反汇编。

再看一下上面的类中虚继承的情况下构造与析构函数调用顺序:B类和C类虚继承A类

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

	~A(){cout<<"~A()"<<endl;}
};

class B: virtual  public A
{
public:
	
	B(){cout<<"B()"<<endl;}

	~B(){cout<<"~B()"<<endl;}
};

class C : virtual public A
{
public:
	C(){cout<<"C()"<<endl;}

	~C(){cout<<"~C()"<<endl;}
};

class D : public B, public C
{
public:
	D(){cout<<"D()"<<endl;}
	
	~D(){cout<<"~D()"<<endl;}
};

             

对照着对象模型看,只需要调用一次B类构造函数即可。

  • 27
    点赞
  • 121
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
### 回答1: 继承派生程序需要注意以下几点: 1. 基类必须是公有的,派生类可以是公有的、保护的或私有的。 2. 在派生类中,基类的构造函数和析构函数会自动地被调用,无需手动调用。 3. 在派生类中,可以通过“基类名::成员函数”来调用基类的成员函数。 4. 在派生类中,可以重新定义基类的虚函数,使得它们可以根据需要被派生类的对象调用。 5. 在派生类中,可以增加成员变量和成员函数,以满足派生类的需要。 6. 在派生类中,可以访问基类的公有成员和保护成员,但不能访问基类的私有成员。 ### 回答2: 编写继承派生程序时,需要注意以下几个问题: 一、设计良好的继承体系:在设计继承体系时,需考虑到各个类之间的继承关系,确保每个类都有其自身的特性和功能,并且每个派生类都能够正确地继承和扩展基类的属性和方法。合理划分继承关系,并减少派生类之间的耦合度,是设计良好的继承体系的关键。 二、正确使用访问修饰符:在继承派生中,访问修饰符(public、protected、private)的使用非常重要。需要合理地选择和使用这些修饰符,以确保基类的成员在派生类中能够得到正确的访问权限。一般来说,将基类的数据成员设置为private,而将基类的成员函数设置为public或protected,可以保证数据成员的封装性和代码的可维护性。 三、避免菱形继承问题:菱形继承指的是由于多级继承而导致的类之间的继承关系类似于菱形形状的问题。例如,A类派生出B类和C类,而B类和C类又同时派生出D类,这就形成了菱形继承问题。为避免菱形继承问题,可以使用虚继承或其他方式解决,以确保派生类对基类的成员访问没有歧义。 四、避免多重继承引发的冲突:多重继承指的是一个派生类同时继承了多个基类。在使用多重继承时,可能会出现不同基类中存在同名的成员函数或数据成员,进而导致函数调用或成员访问的二义性。因此,在继承派生中,需要避免多重继承引发的冲突。可以使用作用域限定符来明确指明要调用的成员函数或成员变量所属的类。 总之,编写继承派生程序需要仔细设计继承体系,正确使用访问修饰符,避免菱形继承和多重继承引发的问题。这样可以确保程序的正确性、可维护性和扩展性。 ### 回答3: 在编写继承派生程序时,需要注意以下问题: 1. 基类的选择:在设计继承派生关系时,要确保基类是合适的,它应该具有被派生类共享的属性和行为。选择合适的基类是确保程序结构和逻辑正确的重要一步。 2. 访问权限控制:继承的目的是为了重用代码和扩展功能,但这并不意味着子类可以随意访问基类的所有成员。需要根据实际需求,使用适当的访问权限修饰符来限制派生类对基类成员的访问。 3. 虚函数和纯虚函数:虚函数是基类中声明为virtual的函数,它可以在派生类中被重新定义。纯虚函数是在基类中声明为纯虚的,派生类必须实现这个函数。在派生类中,尽量使用虚函数和纯虚函数来实现多态性,以提高程序的灵活性和可扩展性。 4. 析构函数:如果基类中有虚函数,必须在基类中定义一个虚析构函数。这样,在通过基类指针删除一个派生类对象时,将会调用派生类对象的析构函数,释放派生类对象占用的资源。 5. 多重继承与菱形继承:多重继承是指一个派生类从多个基类继承。菱形继承是多重继承的特殊情况,其中派生继承了两个基类,而这两个基类又各自继承了同一个基类。在使用多重继承和菱形继承时,需要格外小心并遵循正确的继承派生规则。 继承派生是面向对象编程的重要概念之一,合理地使用继承派生可以提高代码的重用性和可维护性。不过,在编写继承派生程序时,我们需要特别注意上述问题,以确保程序的正确性和可靠性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fireplusplus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值