复合是类型之间的一种关系,当一种类型的对象包含了其他种类型的对象的时候,我们就可以说这是复合关系,复合关系也是我们在画UML类图的时候经常碰到的一种类与类之间的关系。
class Address{...};
class PhoneNumber{...};
class Person
{
public:
...
private:
std::string name{};
Address address{};
PhoneNumber phone{};
}
上面的这个例子中类 Person 对象由 std::string
, Address
和 PhoneNumber
构成。
我们都知道,public继承其实构建的是一种 is-a 的关系,就是如果派生对象是一种基类对象,那么对派生对象为真的事情对基类对象也一定为真。而复合构建的是一种 has-a 的关系,也就是说,某个对象内含了其他的某个对象。
复合也可以构建一种 is implemented in terms of 的关系,就是根据什么实现出什么。
上面的这个例子我们构建的就是一种 has-a 的关系,因为,我们可以说,人有一个地址,但不能说,人是一个地址。
比较麻烦的是很难区分 has-a 和 is implemented in terms of 这两种的关系。
其实复合这种设计在我们平时的工作中会经常遇到。我们也经常根据一些比较方便的 template 来实现自己的数据结构,比如,在一个空间比时间重要的程序里面,我们可能会这样实现 set template
。
为什么不直接用 std::set
呢?因为模版库的set 在实现的时候为了在一些操作上保证对数时间的效率,每个元素都会有三个指针的额外开销。
template<typename T>
class Set : public std::list<T>
{
public:
...
}
这样对吗?上面的这个关系是 is-a 的关系,但是对Set来说,对他为真的某些事情对 list 并不一定为真,比如 Set不允许重复元素,但是 list 可以。
template<typename T>
class Set
{
public:
bool member(const T& item) const;
void insert(const T& item);
void remove(const T& item);
std::size_t size() const;
...
private:
std::list<T> m_data;
}
template<typename T>
bool member(const T& item) const
{
return std::find(m_data.begin(), m_data.end(), item) != m_data.end();
}
template<typename T>
bool insert(const T& item)
{
if(!member(item)){ m_data.push_back(item); }
}
template<typename T>
bool remove(const T& item)
{
auto it = std::find(m_data.begin(), m_data.end(), item);
if(it != m_data.end())
{
m_data.erase(it);
}
}
template<typename T>
std::size_t Set<T>::size() const
{
return m_data.size()
}
我们都知道,public 继承是塑造了一种 is-a 的关系,那么如果我们手贱,不小心将public继承写成了private继承会怎么样呢?
#include<stdio.h>
#include<iostream>
#include<string>
using namespace std;
class A
{
public:
A(string a) : m_data(a){}
~A(){}
virtual void display() const
{
cout << "this is A function..." << m_data << endl;
}
private:
string m_data{ "" };
};
class B : private A
{
public:
B(string a) : A(a), m_data(a){}
~B(){}
virtual void display() const
{
cout << "this is B function..." << m_data << endl;
}
private:
string m_data{ "" };
};
void displayByValue(const B& w)
{
w.display();
}
void displayByReference(const A& w)
{
w.display();
}
int main(int* argc, char* argv[])
{
B b("this is a test...");
displayByValue(b);
displayByReference(b);
return 0;
}
上面的这个例子是我从前面的某篇文章中抄过来改了改,我们可以看到,第一个 displayByValue
能正常调用,但是 displayByReference
编译器会报错。 'A' is an inaccessible base of 'B'
。
如果继承关系是private
,那么编译器是不会自动的将一个派生类转换为基类的。同样的,派生类通过private继承继承到的成员的属性都将会被改为private。而无论这些成员在基类中是public 还是 protected。
private 继承意味着 is implemented in terms of 的关系。继承的是一种实现而不是接口。我们前面刚说过复合也是 is implemented in terms of 的关系,这就会让我们在实现的时候面对一个选择。如何取舍?尽可能使用复合,必要时再选择private继承。
通常级别比复合低,但当派生类需要访问基类的protected成员,或者需要重新定义继承而来的虚函数时,私有继承是比较好的选择。
private继承的访问控制
- 基类的public和protected成员:都以private身份出现在派生类中;
- 基类的private成员:不可直接访问。
private访问权限
- 派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员;
- 通过派生类的对象:不能直接访问从基类继承的任何成员
看下面这个例子。
class Empty{};
class Test
{
private:
int a;
Empty e;
};
int main(int *argc, char* argv[])
{
Test t;
std::cout << sizeof(t);
}
按照我们正常的理解,因为Empty
是个空类,所以,应该是不会有任何内存控件的占用,但实际上,输出结果会大于 sizeof(int)
的大小,大多数编译器获取 sizeof(Empty)
的大小应该是1。又由于对齐的原因,测试结果应该是 sizeof(int)
的 2 倍。
那如果按照下面的继承呢?
class Test : private Empty
{
private:
int a;
};
这样的测试结果是4,也就是 sizeof(int)
的大小。这也就是空基类最优化。
所以, private继承通常意味着 is implemented in terms of 的一种关系,慎用。private继承能够实现空基类的最优化。