C++继承

1,136 阅读7分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

什么是继承?

继承是代码复用的一种体现。在已有类的基础上进行扩展。 那么,继承的形式是什么样的呢?

class son:public father
{}

son叫做派生类/子类,father叫做基类/父类;public是继承方式。 继承方式有3种,公有继承public,保护继承protected,私有继承private。 当没有写是什么方式继承的时候为私有继承。

不同继承方式的访问限定

我们知道类也有三种访问限定:公有public,继承protected,继承private。 那么基类不同的限定访问,在不同方式的继承之后,派生类会出现怎么样的访问限定。

结论:

  • 基类的私有成员,无论是哪一种形式的继承,继承之后在派生类中也不能访问。
  • 基类任意一种访问限定符限定的成员,当为私有继承的时候,在派生类外面都不能进行访问。
  • 我们之前在讲类的时候说,类的protectedprivate的访问限定是一样的,在类外面都不能访问。而继承就体现了它用法,当为protected继承的时候,在派生类的里面是可以访问到基类的成员的。
  • 大多数的情况下都是公共继承的,因为继承之后在派生类的外面是可以访问的。
基类 / 派生类public继承protected继承private继承
public成员派生类public成员派生类protected成员派生类private私有成员
protected成员派生类protected成员派生类protected成员派生类private私有成员
private成员不可访问不可访问不可访问

派生类给基类赋值

  • 派生类是可以给基类赋值的,可以是赋值给基类的指针,基类的引用,基类的对象。外面把这种操作叫做切片操作。
  • 基类不可以给派生类继承。

下面来解释一下赋值的底层:

  • 测试的两个类
class father
{
public:
	int _a;
};

class son :public father
{
public:
	int _b;
};
  • 赋值给对象
int main()
{
	son x;
	x._a = 1;
	x._b = 2;

	father y = x;
	y._a = 0;
	return 0;
}

  • 赋值给指针
int main()
{
	son x;
	x._a = 1;
	x._b = 2;

	father* y = &x;
	y->_a = 0;//此时派生类中继承的基类的成员的数据会改变。
	return 0;
}
  • 赋值给引用
int main()
{
	son x;
	x._a = 1;
	x._b = 2;

	father y = &x;
	y._a = 0;
	return 0;
}

image.png

继承中的作用域

不管是基类还是派生类它都有独立的作用域,都在该类域里面。

  • 当基类中的成员和派生类中的成员名相同的时候,此时基类中的该成员隐藏。要显示基类的类域才可以访问,没有显示类域默认访问派生类中的。
  • 注意:只要名字一样就构成隐藏。

成员变量构成的隐藏

class A
{
public:
	int _a=10;
};
class B :public A
{
public:
	int _a=20;
};
int main()
{
	B x;
	cout <<"B中:" <<x._a << endl;
	cout << "A中:" << x.A::_a << endl;//访问A中的_a要指定类域
	return 0;
}

image.png 成员函数构成的隐藏

class A
{
public:
	void f()
	{
		cout << "A" << endl;
	}
public:
	int _a=10;
};
class B :public A
{
public:
	void f()
	{
		cout << "B" << endl;
	}
public:
	int _a=20;
};
int main()
{
	B x;
	x.f();
	x.A::f();
	return 0;
}

image.png

派生类中的默认成员函数

在基类中,如果没有默认成员函数,我们必须在派生类中显示的写出。如果基类中有默认成员函数,当派生类中不显示调用的时候,会自动调用。

  • 对于构造函数,都会在初始化列表的时候自动调用基类的构造函数。
  • 对于析构函数,在对象销毁的时候,会自动调用基类的析构函数,在调用自身的析构函数,所以,不需要自己在析构函数里面显示的调用基类的析构函数。这样才能保证先析构派生类,再析构基类

构造函数

  • 1.当基类有默认的构造函数的时候,可以只初始化派生类新的成员变量,也可以自己调用基类的默认构造,看自己的心情。
  • 2.当基类没有默认的构造函数的时候,必须自己要写构造函数调用基类的
class A
{
	int _a;
public:
	A()
	{
		_a = 10;
	}
};
class B :public A
{
	int _b;
public:
	B()
	{
		_b = 20;
	}
};
int main()
{
	B x;
	return 0;
}
class A
{
	int _a;
public:
	A(int a)
	{
		_a = 10;
	}
};
//把A的构造函数改成不是默认构造的时候,那么上面的代码就会报错

image.png 此时B就应该在初始化列表显示的调用A中的构造函数。

class B :public A
{
	int _b;
public:
	B()
		:A(0)
	{
		_b = 20;
	}
};

拷贝构造,赋值运算符重载

必须调用基类的。派生类自己的成员还是和类的拷贝构造,赋值运算符重载一样。 赋值运算符要指定一下域

class A
{
	int _a;
public:
	A(int a=0)
	{
		_a = a;
	}
	A(const A& x)
	{
		_a = x._a;
	}
	A& operator=(const A& x)
	{
		_a = x._a;
		return *this;
	}
};
class B :public A
{
	int _b;
public:
	B()
		:A(0)
	{
		_b = 20;
	}
	B(const B& x)
		:A(x)//把参数X传给A的拷贝构造,派生类可以传给基类,上面讲了。也可以传其他值哦。
	{
		_b = x._b;
	}
	B& operator=(const B& x)
	{
		A::operator=(x);//指明域
		_b = x._b;
		return *this;
	}
};
int main()
{
	B x;
	B y(x);
	return 0;
}

析构函数

派生类的析构函数在对象销毁的时候,会自动调用基类的析构函数,所有不需要自己再手动调用基类的析构函数。

class A
{
public:
	~A()
	{
		cout << "A析构" << endl;
	}
};
class B :public A
{
public:
	~B()
	{
		cout << "B析构" << endl;
	}
};
int main()
{
	B x;
	return 0;
}

image.png

如果在B的析构函数里面手动析构A,A::~A(),会把A析构两次,如果A中的成员是动态开辟的,将会产生野指针。 image.png

继承与友元

友元是不能继承的——基类友元不能访问派生类的私有和保护成员。

class B;
class A
{
	friend void print(const A& x, const B& y)
	{
		cout << x._a << y._b << endl;
	}
protected:
	int _a;
};
class B :public A
{
protected:
	int _b;
};
int main()
{
	print(A(), B());
	return 0;
}

image.png

继承与静态成员

对于基类的静态成员,无论它派生出多个派生类,所有继承体只要这么应该静态成员。

菱形继承与菱形虚拟继承

菱形继承

  • 什么是菱形继承?

如下图:B继承了A,C也继承了A,D既继承了B也继承了C。这种继承关系就是菱形继承

  • 为什么会出现菱形继承?

C++支持多继承

  • 菱形继承有什么缺点?

二义性和数据冗余

  • 二义性

如上图:对于D来说,含有2个A,当对他们访问的时候,不知道是哪一个

  • 数据冗余

2个A都需要占用内存

代码验证

  • 菱形继承
class father
{
public:
	int _f;
};
class son1:public father
{
public:
	int _s1;
};
class son2 :public father
{
public:
	int _s2;
};
class kunkun :public son1, public son2
{
public:
	int _kk;
};

测试二义性

int main()
{
	kunkun x;
	x._f = 1;
	return 0;
}

我们会发现它就会报错,因为存在二义性

image.png

冗余

看它的内存分布:从它的内存分布上可以看出,father有两份,所有会冗余,也是当kunkun木有指定类域的时候不知道访问的哪一个。 image.png

菱形虚拟继承

关键子virtual,可以解决二义性和冗余的问题。只需要son1``son2虚拟继承father即可。

  • 下面解释一下解决这两个问题的原理
class father
{
public:
	int _f;
};
class son1:virtual public father
{
public:
	int _s1;
};
class son2 :virtual public father
{
public:
	int _s2;
};
class kunkun :public son1, public son2
{
public:
	int _kk;
};
int main()
{
	kunkun x;
	x._s1 = 1;
	x._s2 = 2;
	x._f = 0;
	x._kk = 3;
	return 0;
}

看它的内存分布: image.png

我们通过观察他们在内存中的存储,可以看出father的成员_f只有一份,放在了如图所示的地方。

  • 现在有个问题,对于son1,son2他们怎么找到_f在哪里的?

通过观察上图,会发现son1,son2里面存了两个地址,0x005f7bdc,0x005f7be4 我们看看地址指向哪里: image.png image.png 16进制的14为20,这里的20是偏移量,相对_s1的偏移量,加上20,此时就是指向_f,下面那个含义也是一样的。

继承与组合

继承是is-a的关系——派生类是基类 组合是has-a的关系——A类中有B类

  • 在既可以用继承,也可以用组合的地方,选择用组合,因为继承的情况下,基类的成员是完成暴露给派生类的,而组合只会提供相应的方法,不会暴露底层的实现。