C++ 类的继承(自学笔记,可能有错)

修改记录

2020/12/27  : 修改了一下排版,还有几处程序和测试结果不匹配的问题也改正了,删除了虚继承的空间布局,因为我准备再写一篇文章,都写到一起就太长了。

继承的概念

       继承是使用已经编写好的类来创建新类,新的类具有原有类的所有属性和操作,也可以在原有类的基础上做一些修改。新类也叫子类 或 派生类 原来的类叫做 父类 或 基类。派生类是基类的具体化。

派生类对基类的3种继承方式

          这里要注意一下,区分继承方式和派生类成员访问权限属性。前者会修改派生类成员访问权限属性,后者规定了成员被访问的作用域。

派生类成员访问权限属性了成员可以使用的范围   

     public权限可以在本类的内部和外部使用,类的内部调用,类的外部调用。protected权限在派生类中是可访问的,意思是在子类内部可以调用父类中protected属性的成员。但是此属性成员在外部不能被调用,比如在main函数中使用派生类对象调用父类的protected成员。private权限只能在本类的内部被访问。

     

继承方式会修改子类继承父类成员的属性   

      在上面的图中已经有说明,需要注意的是,这里的改变并不会修改父类中成员的访问权限属性,只是修改了父类成员在子类中的访问权限属性

      public        :   父类的成员的属性在子类中不变

      protected   :   父类的成员属性在子类中会变成 protected,但是private不会改变

      private       :   父类的成员属性在子类中会变成 private

举个例子

class Father
{
        public:      int a;
        protected:   int b;
        private:     int c;
};

class Son : private Father
{

        public:      int d;
        protected:   int e;
        private:     int f;
        //           int a   private属性
        //           int b   private属性
        //           int c   不能访问          
};


class Son2 : public Father
{

        public:      int d;
        protected:   int e;
        private:     int f;
        //           int a  public属性
        //           int b  protected属性
        //           int c  不能访问
};

继承的种类

单继承     :  一个子类只继承一个父类。

多继承     :   一个子类同时继承多个父类。

多重继承  :  举例说明:  A继承B(A是子类,B是父类),C继承A(A是父类,C是子类),祖孙三代。

虚继承     :   虚继承是多重继承中特有的概念。虚继承是为解决多重继承(钻石继承)的路径二义性。(下文有解释)。

类型兼容原则

类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。通过公有继承,派生类得到了基类中除构造函数、解析函数之外的所有成员。兼容规则中所指的替代包括以下情况:

      (1)子类对象可以当作父类对象使用

      (2)子类对象可以直接赋值给父类对象

      (3)子类对象可以直接初始化父类对象

      (4)父类指针可以直接指向子类对象

      (5)父类引用可以直接引用子类对象

        两层含义  可以把父类的指针和引用指向子类对象,子类对象可以初始化父类对象

继承中的构造和析构顺序

#include <iostream>
using namespace std;

class  father
{
public:
  	father(int m,int n): a(m),b(n)   //另一种赋值的方法  初始化列表
	{
		   cout<<"调用父类的构造函数"<<endl;
	}
 
	~father()
	{
		   cout<<"调用父类的析构函数"<<endl;
	}
private:
	int a;
	int b;
};


class  son : public father
{
public:
    //继承情况下构造子类要先构造父类,这时候用构造函数初始化列表
	son(int m,int n,int z,int x): father(m,n),c(z),d(x)   
	{
		cout<<"调用子类的构造函数"<<endl;
	}
	~son()
	{
		cout<<"调用子类的析构函数"<<endl;
	}
protected:
private:
	int c;
	int d;
};



int main()
{
    //先构造父类在构造子类
	son(1,2,3,4);
    //先析构子类在析构父类
	return 0;
}

多继承中构造和析构的顺序

#include <iostream>
using namespace std;

class father1
{
public:
	father1(int a)
	{
		this->a = a;
		cout << "father1的构造函数" << endl;
	}
	~father1()
	{
		cout << "father1的析构函数" << endl;
	}
	int a;
};

class father2
{
public:
	father2(int a)
	{
		this->a = a;
		cout << "father2的构造函数" << endl;
	}
	~father2()
	{
		cout << "father2的析构函数" << endl;
	}
	int a;
};

class father3
{
public:
	father3(int a)
	{
		this->a = a;
		cout << "father3的构造函数" << endl;
	}
	~father3()
	{
		cout << "father3的析构函数" << endl;
	}
	int a;
};

//子类先构造父类,这里有三个父类,构造父类的顺序是根据下面继承的顺序
class son : public father3, public father2,public father1  
{
public:
    //构造父类的顺序 与 初始化列表的顺序没有关系
	son(int a, int b, int c): father1(a),father2(b),father3(c)
	{
		cout << "son的构造函数" << endl;
	}
	~son()
	{
		cout << "son的析构函数" << endl;
	}
};

int main()
{
	son s1(1,2,3);
	return 0;
}

测试结果如图:

继承和组合混搭下的构造和析构顺序

构造顺序:  先构造父类(父类还有爷爷类,就构造爷爷类),接着构造组合类,最后构造子类。
析构顺序:  和构造的相反


#include <iostream>
using namespace std;


class  Object      //爷爷类
{
public:
	Object(int m, int n) : a(m), b(n)
	{
		cout << "进行Object的构造函数" << endl;
	}
	~Object()
	{
		cout << "进行Object的析构函数" << endl;
	}
	int a;
	int b;
};


class father : public Object      //这个父类继承了爷爷类
{
public:

	father(int a, int b) : Object(2, 3)//为父类赋值
	{
		this->a = a;
		this->b = b;
		cout << "进行father的构造函数" << endl;
	}
	~father()
	{
		cout << "进行father的析构函数" << endl;
	}
	int  a;
	int  b;
};

class son : public father    // 子类继承了父类
{
public:
	//初始化列表 这里直接给父类和爷爷类赋了固定值
	son(int m, int n) : c(m), d(n), father(1, 2),f1(7,8), o1(9, 10)
	{
		cout << "进行son的构造函数" << endl;
	}
	~son()
	{
		cout << "进行son的析构函数" << endl;
	}
	int c;
	int d;
	father f1;   //这里是组合类
	Object o1;   //这里是组合类  
	
};



int main()
{
	son  S1(3, 4);
	return 0;
}

测试结果如图

测试结果的解读

       第一行和第二行是构造son类的父类father产生的,第三行和第四行是构造子类中的 father f1(构造father类就得先构造Object类),第五行是构造子类中的Object  o1类(不需要再构造其他了),第六行是构造子类,析构顺序和构造顺序相反。

含有虚继承的构造和析构顺序

       在下列程序中,本应该是先构造B类然后再构造B2类,但是子类C是虚继承B2类,从结果上看是优先构造虚基类,然后再构造普通基类。析构顺序和构造顺序是相反的。

#include <iostream>
using namespace std;

class B
{
 public:
	B() {   cout << "基类构造函数" << endl;   }
	~B(){   cout << "基类的析构函数" << endl; }
	int b;
	int d;
};

class B2
{
 public:
	B2() {   cout << "虚基类B2构造函数" << endl;   }
	~B2(){   cout << "虚基类B2的析构函数" << endl; }
};


class C : public  B , virtual public B2    //如果都是虚继承,那么构造顺序还是按照这里顺序进行。
{
  public:
	C() {  cout << "子类C构造函数" << endl; }
	~C(){ cout << "子类C析构函数" << endl;  }
	int c;
};

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

测试结果

虚基类和普通基类初始化的不同

       派生类构造调用了虚基类的构造函数之后,中间的类对虚基类构造函数的调用被忽略了,这也是初始化虚基类

       和初始化非虚基类不同的地方。也就是说虚基类只会被最底层类调用一次。

虚基类的初始化

/*
            A
      B            C
            D
*/
#include <iostream>
using namespace std;

class A
{
public:
	A(int a_) : a(a_)
	{
		cout << "虚基类构造函数" << endl;
	}

	int a;
};

class B : virtual public A
{
public:
	B() :A(3)
	{
		cout << "父类B构造函数" << endl;
	}
};


class C : virtual public A
{
public:
	C() :A(4)
	{
		cout << "父类C构造函数" << endl;
	}
};


class  D :public C, public B
{
public:
	D() : A(6)
	{

	}

};


int main()
{
	D D1;
	cout << "虚基类中 a = " << D1.C::A::a << endl;
	cout << "虚基类中 a = " << D1.B::A::a << endl;
	return 0;
}

测试结果

       创建子类D的过程中,只调用了一次虚基类的构造函数,虽然两个父类也写了虚基类的构造函数(这里是必须要写的),但是被忽略了。所以从不同的路径访问a的结果都是一样的。

普通基类的初始化

            这个在g++ 下编译不通过,但是vs2010下可以。

#include <iostream>
using namespace std;

class A
{
public:
	A(int a_) : a(a_){ cout << "基类构造函数" << endl; }
	int a;
};

class B: public A
{
public:
	B():A(3) { cout << "父类B构造函数" << endl; }
};


class C : public A
{
public:
	C():A(4) { cout << "父类C构造函数" << endl; }
};


class  D:public C,public B
{
public:
	D(){ }

};


int main()
{
	D D1;
    //这里这么写是为了避免多重继承的二义性,因为这么写完全指明了继承的路径,但是不建议这么使用,此处只是试验
	cout << "D1.C::A::a = " <<D1.C::A::a << endl;  
	cout << "D1.B::A::a = " <<D1.B::A::a << endl;
	return 0;
}

测试结果

         创建子类D的过程中,调用了两次基类的构造函数,并且从不同路径上去访问a得到的值还不同(因为两个父类的赋值不同,实际上也没这么用的)。

继承中会存在的问题

同名二义性

              父类和子类的成员函数和成员变量都重名了怎么办

#include <iostream>
using namespace std;

class father
{
public:
	int a;
	void _print()
	{
		cout << "I am father" << endl;
		cout << "a =" << a << endl;
	}
};


class son: public father
{
public:
	int a;
	void _print()
	{
		cout << "I am son" << endl;
		cout << "a =" << a << endl;
	}
};



int main()
{
	son s1;
	s1.a = 1;                 //子类调用重名的成员函数,默认是子类的
	s1._print();
	cout << endl;

	father f1;                //父类调用重名的成员函数,默认是父类的
	f1.a = 2;
	f1._print();
	cout << endl;


	s1.father::a = 100;       //如果想通过子类来调用父类的成员函数,需要在变量前面增加域名符号
	s1.father::_print();

	cout << endl;
	return 0;
}

路径二义性

        什么是继承的路径二义性?

        最基类B(爷爷类)被 b1(父类1)和 b2(父类2)继承 ,而C类(子类)又继承了 b1和b2,那么当C继承B类的时候,是从b1类继承的还是从b2类继承的?如果不做处理就这样继承,程序会报错。

        解决方法   使用虚继承,b1和b2虚继承B,此时B叫做虚基类。

/*先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,
  它可以看到所有相关类的内存布局,如果写上/d1 reportSingleClassLayoutXXX(XXX为类名),
  则只会打出指定类XXX的内存布局。近期的VS版本都支持这样配置。

                 B
           b1          b2
                 C

  */
#include <iostream>
using namespace std;
class B
{
public:
	B()
	{
		cout<<"B的构造函数"<<endl;
	}
	int b;
	int d;
};


class b1 :  virtual public B       //需要加上virtual关键字 变成虚继承
{
public:
	b1()
	{
		cout<<"b1的构造函数"<<endl;
	}
	int B1;
};


class b2 :  virtual public B
{
public:
	b2()
	{
		cout<<"b2的构造函数"<<endl;
	}
	int B2;
};


//C是从b1还是b2继承的B类的?
class C : public b1, public b2
{
public:
	C()
	{
		cout<<"C的构造函数"<<endl;
	}
	int c;
};

int main()
{ 
	B old;
	cout << endl;
	b1 f1;
	b2 f2;
	cout << endl;
	C c1;

	c1.B1 = 100;  //父类的
	c1.B2 = 200;  //父类的
	c1.c = 300;   //子类的

	c1.b = 400;   //爷爷类的   只有访问公共基类的时候需要虚继承
	return 0;
}

测试结果

           可以看到构造C类对象时,基类只进行了一次构造,这就是通过虚继承实现的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值