继承、虚函数、二进制兼容和依赖隔离

考虑有一个动态库
头文件myso.h

#ifndef __MYSO_H__
#define __MYSO_H__
class A
{
public:
	A(int i);
	int iValue();
private:
	int _i;
};

#endif

定义文件myso.cpp

#include "myso.h"
A::A(int i):_i(i)
{
}
int A::iValue()
{
	return _i;
}

编译生成动态库libmyso.so
g++ -fpic -shared myso.cpp -o libmyso.so -I.
代码main.cpp中调用 libmyso

#include 
#include "myso.h"
using namespace std;
int main()
{
	A a(10);
	cout<<a.iValue()<<endl;
	return 0;
}

编译生成可执行文件a.out
g++ -c main.cpp -I.
g++ main.o -L -lmyso

使用脚本a.sh导入库路径

LD_LIBRARY_PATH=/home/wang/program/C++/dll
export LD_LIBRARY_PATH
./a.out

./a.sh 输出10

如果为动态库中增加函数 void setIValue(int i);
头文件myso.h

#ifndef __MYSO_H__
#define __MYSO_H__
class A
{
public:
	A(int i);
	int iValue();
	void setIValue(int i);
private:
	int _i;
};

#endif
#include "myso.h"
A::A(int i):_i(i)
{
}
int A::iValue()
{
	return _i;
}
void A::setIValue(int i)
{
	_i = i;
}

更新动态库
g++ -fpic -shared myso.cpp -o libmyso.so -I.

不编译main.cpp a.sh运行功能正常,也就是说增加普通函数是二进制兼容的。

如果,添加的不是普通函数,而是变量 int _j;

头文件myso.h

#ifndef __MYSO_H__
#define __MYSO_H__
class A
{
public:
	A(int i);
	int iValue();
private:
	int _j;
	int _i;
};

#endif

定义文件myso.cpp

#include "myso.h"
A::A(int i):_i(i)
{
}
int A::iValue()
{
	return _i;
}

编译生成动态库libmyso.so
g++ -fpic -shared myso.cpp -o libmyso.so -I.

运行./a.sh 输出
在这里插入图片描述报错。原因是main.cpp在编译生成时,根据A的声明为栈上变量a分配空间,这时,A只有一个int变量。所以main函数的栈空间为4字节。

当A增加一个int变量后,a.out在调用链接函数
A a(10)
a.iValue();
相当于调用了
A(A * )
IValue(A *)
这两个函数认为main函数传递给它了一个8字节的栈上空间的首地址,构造函数给后4字节空间赋值10,IValue读取后4字节的int值。操作了未分配的非法空间,所以报错。
当然,如果把int _j放在 变量 _i之下,那么构造函数和IValue只操作了分配的4个字节,a.out不会报错。
但是,C++是面向对象语言,也就是说它会有继承。

继承

如果头文件myso2.h

#ifndef __MYSO2_H__
#define    __MYSO2_H__
#include "myso.h"
 class B:public A
 {
 public:
 		B(int i,int x) ;
 		int xValue();
 		
 private:
 		int _x;
 };
 #endif

定义文件myso2.cpp

#include "myso2.h"
B::B(int i,int x):A(i),_x(x)
{
}
int B::xValue()
{
	return _x;
}

生成动态库libmyso2.so
g++ -fpic -shared myso2.cpp -o libmyso2.so -I. -L -lmyso -l myso2

main.cpp

#include 
#include "myso2.h"
using namespace std;
int main()
{
	B b(10,20);
	cout<<b.xValue()<<endl;
	return 0;
}

编译main.cpp
g++ main.cpp -I. -L. -lmyso -lmyso2
./a.sh 输出20

如果A类中增加一个int _j,更新libmyso.so 和libmyso2.so。可执行文件a.ou不变。
./a.sh 报错,应为main() 函数中只分配了8个字节栈上空间,而在动态库更新后的B构造函数和xValue访问了8字节之后的4个字节。这是未分配的内存。

这就是继承的高耦合性,所以,大多数C++ 规范要求优先考虑使用组合替换继承。组合可以实现依赖隔离,并且封装变化点。所以组合的耦合性更低。组合依靠PIMPL实现高内聚,低耦合。

组合 PIMPL

myso.h

#ifndef __myso_h__
#define __myso_h__
class A{
public:
	A(int i);
	~A();
	A(const A&)=delete;
	A& operator=(A&)=delete;
	int iValue();
private:
	class Aimpl;
	Aimpl *pAimpl;
};
#endif

myso.cpp

#include "myso.h"
//内置类型 Aimpl 声明
class A::Aimpl{
public:
	Aimpl(int i):_i(i){}
	int iValue()
	{
		return _i;
	}
private:
	int _i;
};

A::A(int i):pAimpl(new Aimpl(i))
{
}
A::~A()
{
	delete pAimpl;
}
//函数转发
int A::iValue()
{
	return pAimpl->iValue();
}

g++ -shared -fpIc myso.cpp -o myso.so -I. 编译
我们实际要使用的是Aimp,A只是暴露的接口,同时,Aimpl对象的构造限制在共享库内部,不会有分配资源和使用资源有差异的情况发生,Aimpl的大小不论如何变化,接口A的大小都只是一个指针大小。在64位系统中,就是64位。

同理,B对象可修改为

myso2.h

#ifndef __myso2_h__
#define __myso2_h__
class B{
public:
	B(int x,int i);
	~B();
	B(const B&)=delete;
	B& operator=(B&)=delete;
	int xValue();
	int iValue();
private:
	class Bimpl;
	Aimpl *pBimpl;
};
#endif

myso2.cpp

#include "myso2.h"
#include "myso.h"
class B::Bimpl{
public:
	Bimpl(int x,int i):a(i),_x(x)
	{}
	int xValue()
	{
		return _x;
	}
	int iValue()
	{
		return a.iValue();
	}
private:
	int _x;
	A a;
};
B::B(int x,int i):pBimpl(new Bimpl(x,i))
{
}
B::~B()
{
	delete pBimpl;
}
int B::xValue()
{
	return pBimpl->xValue();
}
int B::iValue()
{
	return pBimpl->iValue();
}

g++ -shared -fpic myso2.cpp -o libmyso2.so -I. -L. -lmyso

main.c

main.cpp 
```c
#include 
#include "myso2.h"
using namespace std;
int main()
{
	B b(10,20);
	cout<<b.xValue()<<endl;
	cout<<b.iValue()<<endl;
	return 0;
}

g++ main.cpp -I. -L -lmyso -lmyso.2
不论怎么在Aimpl 和Bimpl中添加变量,更新两个动态库后,a.out的功能正常。而且这个源文件编译时依赖的myso2.h中并不依赖myso.h,实现了依赖隔离。而且对于最终用户来说,Bimpl和Aimpl中的属性是不可见的。

继承是为被复用,而非复用,也就是说,继承不应该是复用实现,而是为了复用虚函数接口。

虚函数

变量是依靠相对位置访问,所以,继承体系下增加变量,会导致其它变量的位置发生变化,变化的恶性扩散是的用户程序功能不正常。偏偏,虚函数就是依靠相对位置访问,同时,他又和继承绑在一起,无法避免。虚函数会破坏二进制兼容。
base.h

#ifndef __BASE_H__
#define __BASE_H__
class Base{
	virtual void f();
	virtual void g();
	//virtual void h();
};
#endif

base.cpp

#include 
#include "base.h"

void Base::f()
{
	std::cout<<"Base::f call"<<std::endl;
}
void Base::g()
{
	std::cout<<"Base::f call"<<std::endl;
}
//void Base::h()
//{
//	std::cout<<"Base::h call"<
//}

子类 覆盖一个虚函数 添加一个虚函数
child.h

#ifndef __CHILD_H__
#define __CHILD_H__
#include "base.h"
class Child:public Base
{
	virtual void g();

};
#endif

child.cpp

#include 
#include "child.h"

void Child::g()
{
	std::cout<<"Child::g call"<<std::endl;
}

g++ -fpic -shared child.cpp base.cpp -o libvirtual.so -I.
用户程序main.cpp

#include "child.h"
int main()
{
	Child c;
	//c 虚表中有两个个函数,B::f C:g 
	//c.g(); //调用第二个 vptrtable[1]

	return 0;
}

编译 g++ main.cpp -I. -L. -lvirtual
运行,没有任何输出,正常退出。如果去掉Base 中的h()函数注释
重新编译virtual共享 库,重新运行可执行文件。有报错输出。

Symbol `_ZTV5Child’ has different size in shared object, consider re-linking

这是因为第一次的child 对象中有两个虚函数,堆上需要两个函数指针的空间,child对象中有一个栈上表指针。child对象的创建过程是,先分配内存(一个栈上指针,两个堆上指针的空间),然后调用构造函数,给虚表赋值。可执行文件编译后,这种资源分配就固定了。当动态库改变后,链接库中的CHILD构造函数认为对象有三个函数指针,它像一个2个指针大小的栈上空间存储了三个函数指针。导致错误。所以,如果需要保证ABI,则不能使用虚函数。这时,如果需要运行时多态。可以考虑

闭包

虚函数方式的观察者模式
subjec.h

#ifndef __SUBJECT_H__
#define __SUBJECT_H__

class WeatherData;
class WeatherDataObserver
{
public:
	virtual void update(WeatherData * wd)=0;
};
class Subject
{
public:
	virtual void registerObserver(WeatherDataObserver * o)=0;
	virtual void removeObserver(WeatherDataObserver * o) =0;
	virtual void notifyObservers()=0;
};

#endif

weatherData.h

#ifndef __WEATHERDATA_H__
#define __WEATHERDATA_H__
#include 
#include "subject.h"

class WeatherData:public Subject
{
public:
	float getTemp();
	float getPressure();
	virtual void registerObserver(WeatherDataObserver *o)override ;
	virtual void removeObserver(WeatherDataObserver *o)override ;
	virtual void notifyObservers()override ;

	void setData(float temp,float pressure);
private:
	float _temp;
	float _pressure;
	std::list<WeatherDataObserver *> _observers;

};
#endif

weatherData.cpp

#include "weatherData.h"
float WeatherData::getTemp()
{
	return _temp;
}
float WeatherData::getPressure()
{
	return _pressure;
}
void WeatherData::registerObserver(WeatherDataObserver *o)
{
	_observers.push_back(o);
}
void WeatherData::removeObserver(WeatherDataObserver *o)
{
	_observers.remove(o);
}
void WeatherData::setData(float temp,float pressure)
{
	_temp = temp;
	_pressure = pressure;
	notifyObservers();
}
void WeatherData::notifyObservers()
{
	for(WeatherDataObserver *o:_observers)
	{
		o->update(this);
	}
}


weatherDataDisplay.h

#ifndef __WEATHDATADISPLAY_H__
#define __WEATHDATADISPLAY_H__
#include "weatherData.h"
#include "subject.h"

class WeatherDataDisplay:public WeatherDataObserver
{

public:
	virtual void update(WeatherData *wd);
	//void output();
};

#endif

```c
#include 
#include "weatherDataDisplay.h"


void WeatherDataDisplay::update(WeatherData *wd)
{
	std::cout<<"temp: "<<wd->getTemp()<<" getPressure"<<wd->getPressure()<<std::endl;
}



用户 main.cpp

#include "weatherData.h"
#include "weatherDataDisplay.h"

int main()
{
	WeatherDataDisplay wDD;
	WeatherData wd;
	wd.registerObserver(&wDD);
	wd.setData(12.3,101);

	return 0;
}


g++ main.cpp weatherData.cpp weatherDataDisplay.cpp -o main
./main 输出正常。

如果改为闭包,不需要父类指针作为中介传递虚函数。可以直接传递函数对象,函数即是接口。实现一个简单的一对多回调。代码如下,只是举一个例子说明function和bind的功能。这不是一个能在生产中使用的好例子。常用的框架通常使用signal/slot来实现回调。

#include 
#include 
#include 
#include 

using namespace std;

class A
{
public:
    A(int n):num(n){cout<<"A construct fun"<<endl;}

    void print(int x){cout<<"num: "<<num<<" X: "<<x<<endl;}
 private:
    int num;
};
class SignalTrivial
{
public:
    typedef function<void(int)> Functor;
    void connect(Functor&& func)
    {
        functors_.push_back(forward<Functor>(func));
    }

    void call()
    {
        for (const Functor & f:functors_)
        {
            f(10);
        }
    }
private:
    vector<Functor> functors_;
};

int main()
{
    A a1(1),a2(2);
//    function fprint1 = bind(&A::print,&a1,placeholders::_1);
//    function fprint2 = bind(&A::print,&a2,placeholders::_1);
//    fprint(3);
    SignalTrivial signalTrival;
    signalTrival.connect(bind(&A::print,&a1,placeholders::_1));
    signalTrival.connect(bind(&A::print,&a1,placeholders::_1));
    signalTrival.call();

    return 0;
}

你可能感兴趣的:(c++)