第七章 类的定制

        这个话题比较大了,所以我们往简单点说,那就是我们想要控制一个类的行为,嗯,这里说的不是一个类对象的行为,好吧,如果接触C++不是太深的话这可能不太好理解,因为似乎我们都不怎么关心这个问题,我们往往关心的是一个对象的行为,但是不表示我们就不应该关心类的行为。
 

那么类有哪些行为是我们可以或者说应该关心的?
 

首先我们来说类的构建方式,类的构建方式不都很简单吗?直接声明定义或者new出来即可,那么问题来了,直接声明定义出来的对象都是栈对象,但是计算机栈空间远不及堆空间大,所以可能我们会要求使用该类的人必须将对象分配在堆上面,那么问题来了,你不知道谁在使用你所提供的类,所以你不可能对个使用你class的人都耳提面命的告诉他们你必须要new出来,否则可能会导致某种不可预期的问题,所以为了让使用该类的人必须在堆上构建,那么就要从根源上来解决该问题,让他必须new出来,而打算在栈上构建该对象则无法通过编译,要定制这样的话很简单,我们只需要把析构函数设定为protected或者private即可。
 

//+—————————
 

class Test{
public:
Test(){ }

void Destroy(){

delete this;



}

protected:
~Test(){ }
};


 

int main(){
//
// 正确的使用方式
//
Test* obj = new Test();


obj->Destroy();


 

//
// 下面的方式无法通过编译
//
Test obj;


 

system(“pause”)
 

return 0;


}
 

 

//+—————————–
 

 

上面是让类只能在堆上构建的技巧,那么如果让类只能在栈上构建呢?也就是我们不可以使用new来完成该行为,比如Direct3D里面的XNA上的XMMATRIX就只能在栈上才能保证16字节对齐(好吧,几字节对齐记得不太清,总之如果选择堆内存会出现频繁崩溃),那么如何实现这样的类呢?实现方法也是相当的简单,我们只需要将new操作符都实现为私有的即可,既然是私有的那么就可以不需要实现,而只需要声明即可:
 

 

//+—————————–
 

class Test{
public:
Test(){ }
~Test(){ }
protected:
static void* operator new(size_t size);


static
void operator delete(void* ptr, std::size_t size);


};


 

int main(){
//
// 错误的使用方式,无法通过编译
//
Test* obj = new Test();


 

 

//
// 正确的使用方式
//
Test obj;


 

system(“pause”)
 

return 0;


}
 

 

//+———————————
 

 

这是将类设计为只能在栈上分配的技巧,当然你可以这么认为:使用malloc获取内存,然后转换为Test*:
 

 

//+————————————-
 

Test* obj = (Test*)malloc(sizeof(Test));


 

 

//+————————————-
 

但只是一块raw内存而已,并没有实质上的意义,虽然对于上面我们这个空类来说是可行的,但是当我们在构造函数中有初始化行为时这就不可行,比如:
 

 

//+————————————–
 

class Test{
public:
Test(){
m_value = 10;



}

~Test(){ }
 


int value() const{

return m_value;



}

protected:
static void* operator new(size_t size);


static
void operator delete(void* ptr, std::size_t size);


 

private:
int   m_value;


};


 

int main(){
Test* obj = (Test*)malloc(sizeof(Test));


std::cout <<obj->value() <<std::endl;


free(obj);


system(“pause”);


return 0;


}
 

 

//+—————————————–
 

我们会看到打印的结果是一个随机数,所以这不是正确的行为。
 

接下来我们设计整个进程中只存在一个类对象,其实这属于一种设计模式——单例模式,单例简单点说就是一个经过改进后的全局对象,然而我们不能简单的以为是静态数据 + 静态函数  = 单例。
 

 

//+————————————
 

class Test{…… };


 

class MyOneTest{
public:
static
void doSomething(Test&

test);


private:
static std::queue<Test>testQueue;


};


 

 

Test test;


 

 

MyOneTest::doSomething(test);


 

//+———————————–
 

看上去没啥问题,运用起来也没啥问题,但在某些使用情况下有很多问题,比如我的行为要求有所改变,doSomething不再适合(当然如果这不是个静态函数而是个虚函数那么这一切都不是问题),这一切的修改需要建立在拥有源码的基础上才行,同时静态数据加静态函数的做法对数据的初始化和清理同样带来难题。
 

关于单例的实现方式有多种,这里我就主要说说我使用的方案:
 

//+————————————
 

class Singleton{
public:
static Singleton* Instance(){
if(__sPtr){

return __sPtr;



}

else{
__sPtr = new Singleton;


return __sPtr;



}


}

 

static
void Destroy(){

if(__sPtr){
delete __sPtr;


__sPtr = nullptr;



}


}

 

protected:
 

Singleton(){ };


~Singleton(){ }
Singleton(const Singleton&

other) = delete;


Singleton&

operator(const Singleton&

other) = delete;


 

private:
static Singleton*  __sPtr;


};


 

Singleton*  Singleton::__sPtr = nullptr;


 

 

//+———————————
 

这种设计的好处是我不需要的时候就不会有Singleton对象的创建,这对于开销很大的对象来说很重要,同时还有多态的性质,因为我们是动态创建,考虑一下我们为什么不用下面的方式:
 

 

//+———————————-
 

class Singleton{
public:
static Singleton* Instance(){

return &

__sObj;



}

 

 

protected:
 

Singleton(){ };


~Singleton(){ }
Singleton(const Singleton&

other) = delete;


Singleton&

operator(const Singleton&

other) = delete;


 

private:
static Singleton  __sObj;


};


 

Singleton*  Singleton::__sObj;


 

 

//+————————————-
 

当然还有为什么Instance返回的是指针而不是引用?
 

关于资源的管理似乎有必要争论一下,如果我们采用上面的实现方式那么我们可以不用关心内存的释放,但是带来的问题是该单列不具有多态性,而且无论我们用与不用都得构造该对象出来,当然我们可以使用另一种方式:
 

 

//+————————————
 

static Singleton* Instance(){
static Singleton Obj;


return &

Obj;


}
 

//+————————————-
 

该方式解决了内存的管理同时解决了用时才进行实例化的问题,但是引入了一个新的问题,那就是你不知道编译器在实例化该对象的时候到底都做了些什么,经过别人验证该模型在某些情况下他确实会失效,所以我一直使用上面指针的模型,那么到目前为此,我们的单例在单线程模型下工作得很好,但是一旦进入多线程就可以出现多个不同的对象出来。
 

 

//+————————————–
 

static Singleton* Instance(){
if(__sPtr){ // 如果多条线程同时执行到该位置,那么都会检查到__sPtr为nullptr,自然都走到else里面

return __sPtr;



}

else{
__sPtr = new Singleton;


return __sPtr;



}

}
 

//+—————————————
 

 

具体问题如上,我们可以这样来解决该问题:
 

 

//+——————————————
 

static Singleton* Instance(){
std::mutex mtx;


std::unique_lock<std::mutex>lock(mtx);


if(__sPtr){ 

return __sPtr;



}

else{
__sPtr = new Singleton;


return __sPtr;



}

}
 

//+——————————————-
 

现在可以正常工作但是又引入了另一个问题,这是一个关于效率的问题,因为每一次进入Instance都会引起中断,这不是一个好习惯,因为后面的都是不必要的,所以我们可以重新优化一下:
 

 

//+——————————————–
 

static Singleton* Instance(){
 

if(__sPtr){ 

return __sPtr;



}

else{
std::mutex mtx;


std::unique_lock<std::mutex>lock(mtx);


if(__sPtr){

return __sPtr;



}

else{
__sPtr = new Singleton;


return __sPtr;



}


}

}
 

//+——————————————-
 

现在效率有了,安全性也有了,我们可以重新装订一下,让他支持任意类实现单例模型:
 

 

//+——————————————-
 

 

//
// 无论通过那个函数获取的实例都是同一个实例对象
// 当使用完成之后需要使用 Destroy 来销毁对象
// 可以直接使用MSingleton创建对象单例,当还可以可以使用他作为基类来让子类具有创建单例的能力
// eg:
// 1:class A;


// A* ptr = MSingleton<A>::Instance();


// 2 : class A : public MSingleton<A>{ };


// A* ptr = A::Instance();


//
template<class T>
class MSingleton
{
public:
    MSingleton(){ }
    ~MSingleton(){ }
 

    MSingleton(const MSingleton&

)=delete;


    MSingleton&

operator=(const MSingleton&

)=delete;


 

    //
    // 单例普通指针类型
    //
    static T* Instance(){
        try{
            if(__sPtr == nullptr){
std::mutex mtx;


std::unique_lock<std::mutex>lock(mtx);


if(__sPtr == nullptr){
    __sPtr = new T;



}

           
}

           

return __sPtr;


       
}

        catch(…){
           

return nullptr;


       
}

   
}

 

//
// 带参数创建
//
template<class…Args>
static T* InstanceWithArgs(Args…args){
try{
if (__sPtr == nullptr){
std::mutex mtx;


std::unique_lock<std::mutex>lock(mtx);


if (__sPtr == nullptr){
__sPtr = new T(args…);



}


}

return __sPtr;



}

catch (…){

return nullptr;



}


}

 

    //
    // 使用工厂函数进行创建对象
    //
    template<class F>
    static T* Instance(F fun){
        try{
            if(__sPtr == nullptr){
std::mutex mtx;


std::unique_lock<std::mutex>lock(mtx);


if(__sPtr == nullptr){
                __sPtr = fun();


                if(__sPtr){
                    __sIsFactory = true;


               
}


}

           
}

           

return __sPtr;


       
}

        catch(…){
           

return nullptr;


       
}

   
}

 

    static
void Destroy(){

        if(__sPtr &

&

!__sIsFactory){
            delete __sPtr;


            __sPtr = nullptr;


       
}

   
}

 

    //
    // 使用辅助函数进行释放对象
    //
    template<class F>
    static
void Destroy(F fun){

        if(__sPtr &

&

__sIsFactory){
            fun(__sPtr);


            __sPtr = nullptr;


       
}

   
}

 

protected:
    static T* __sPtr;


    static bool __sIsFactory;


};


 

template<class T>
T* MSingleton<T>::__sPtr = nullptr;


 

template<class T>
bool MSingleton<T>::__sIsFactory = false;


 

 

//
// 测试代码
//
class Test{
public:
Test(){
m_value = 10;



}

~Test(){ }
 


int value() const{

return m_value;



}

 

 

private:
int   m_value;


};


 

 

typedef MSingleton<Test>SingleTest;


 


void Destroy(){

SingleTest::Destroy();


}
 

int main(){
std::atexit(Destroy);


Test* obj = SingleTest::Instance();


std::cout <<obj->value() <<std::endl;


return 0;


}
 

 

//+—————————–
 

到此,我们实现了一个简单的单例模型,这个单例的使用方式也相当的简单,一如源码里面的说明,接下来我们再来说一个另外一种类的定制——控制类的内存分配行为,这句话不太好理解,当然是我说得不够专业的原因,我们用代码是说明会更好一些:
 

 

//+—————————–
 

int main(){

int
num = 1e8;


for(int i=0;

i<num;

++i){
int* ptr =  new int;


delete ptr;



}

return 0;


}
 

//+——————————-
 

这个例子没有任何实际的意义,但是刚好能够说明我们的问题,就是说在我们需要创建大量对象的时候我们想要对内存进行一些优化,众所周知,上面的代码执行效率相当的低,所以我们这里说的定制就是要优化这个效率,要进行该项定制我们需要实现new操作符的重写:
 

 

//+——————————–
 

class MAllocator{
public:
static void* operator new(std::size_t size);


static
void operator delete(void* p, std::size_t size);


};


 

//+———————————
 

假如我们这个类是我们的基类,而且已经重新好了新的new操作符,那么我们只需要继承至该类就可以使用高效率的new操作符了:
 

 

//+———————————-
 

class Test : public MAllocator{
public:
Test(){
m_value = 10;



}

~Test(){ }
 


int value() const{

return m_value;



}

 

 

private:
int   m_value;


};


 

//+————————————–
 

该技术的难点在于分配器 MAllocator 的实现上面,当然该分配器有各种实现方案,我这里简单的说一种:
 

 

//+————————————–
 

struct MEmptyType{ };


 

 

template<class Allocator = MEmptyType>
class MAllocator{
public:
static void* operator new(std::size_t size);


static
void operator delete(void* p, std::size_t size);


};


 

/
// 分配器转发
//
template<class Allocator>
struct __Allocator_Dispatch__{
static void* malloc(std::size_t size){

return Allocator::Instance()->Allocator(size);



}

 

static void  free(void* p, std::size_t size){
Allocator::Instance()->Deallocator(p, size);



}

};


 

template<>
struct __Allocator_Dispatch__<MEmptyType>{
static void* malloc(std::size_t size){

return ::operator new(size);



}

 

static void  free(void* p, std::size_t size){
::operator delete(p);



}

};


 

//
// 分配器实现
//
template<class Allocator>
void* MAllocator<Allocator>::operator new(std::size_t size){

return __Allocator_Dispatch__<Allocator>::malloc(size);


}
 

template<class Allocator>

void MAllocator<Allocator>::operator delete(void* p, std::size_t size){

__Allocator_Dispatch__<Allocator>::free(p, size);


}
 

 

//+——————————-
 

现在我们将问题抛给了模板参数,默认情况下我们使用常规的new操作符,效率自然不会得到提高,所以我们想要提高new的效率就需要提供一个真正的分配器作为模板参数传递进去:
 

 

//+——————————–
 

//
// 普通内存分配器
//
template<class Type,
int N = 1024, class ThreadMode = MSingleThreadMode>

class MPoolAllocator : public ThreadMode, public MSingleton<MPoolAllocator<Type, N,ThreadMode>>
{
public:
enum{ value = sizeof(Type)
};


void* Allocator(std::size_t size);



void Deallocator(void* p, std::size_t size);


 

template<class OtherType>
struct rebind
{
typedef MPoolAllocator<OtherType, N, ThreadMode>AllocatorType;



};


private:
std::vector<MFixMemroy<N>*>mPools;


MFixMemroy<N>*   pAllocator{ nullptr
};


std::deque<void*>mFastCache;


 

template<class CableLock>
class UniquiLock{
public:
UniquiLock(CableLock* obj) :pObj(obj){
pObj->Lock();



}

~UniquiLock(){
pObj->UnLock();



}

private:
CableLock*  pObj{ nullptr
};



};


};


 

 

//+———————————
 

 

到这里我们可以将分配器,中间层组装起来作为一个包装器:
 

 

//+———————————
template<class Type>
class MFasWrap : public MAllocator<MPoolAllocator<Type>>
{
public:
typedef typename MConstParamWrape<Type>::type const_reference_type;

// 也许是引用,也许不是,比如标量类型
typedef typename MParamWrape<Type>::type reference_type;

     // 也许是引用,也许不是,比如标量类型
public:
MFasWrap(const_reference_type val = Type()) :mObj(val){ }
 

MFasWrap(const MFasWrap&

other) :mObj(other.mObj){ }
 

MFasWrap(MFasWrap&

&

other){
mObj = other.mObj;


other.mObj.~Type();



}

 

MFasWrap&

operator =(const_reference_type val){
mObj = val;


return *this;



}

 

MFasWrap&

operator =(Type&

&

val){
mObj = val;


val.~Type();


return *this;



}

 

MFasWrap&

operator =(const MFasWrap&

other){
mObj = other.mObj;


return *this;



}

 

MFasWrap&

operator =(MFasWrap&

&

other){
mObj = other.mObj;


other.mObj.~Type();


return *this;



}

 

operator Type(){

return mObj;



}

 

reference_type toType(){

return mObj;



}

 

const_reference_type toType() const{

return mObj;



}

 

 

friend std::ostream&

operator<<(std::ostream&

os, const MFasWrap&

other){
os <<other.mObj;


return os;



}

 

friend std::wostream&

operator<<(std::wostream&

os, const MFasWrap&

other){
os <<other.mObj;


return os;



}

 

friend std::istream&

operator>>(std::istream&

is, MFasWrap&

other){
is >>other.mObj;


return is;



}

 

friend std::wistream&

operator>>(std::wistream&

is, MFasWrap&

other){
is >>other.mObj;


return is;



}

private:
Type mObj;


};


 

//+—————————-
 

于是我们就可以像下面进行使用啦:
 

//+—————————–
 

int main(){
 

typedef MFasWrap<int>FInt;


 


int
num = 1e7;


boost::timer t;


t.restart();



for (int i = 0;

i <num;

++i){
FInt* ptr = new FInt;


delete ptr;



}

std::cout <<“time1 = ” <<t.elapsed() * 1000 <<std::endl;


t.restart();


 


for (int i = 0;

i <num;

++i){
int* ptr = new int;


delete ptr;



}

std::cout <<“time2 = ” <<t.elapsed() * 1000 <<std::endl;


system(“pause”);


 

return 0;


}
 

 

//+—————————–
 

我们可以对比一下效率,可以看出差距是相当大的:
第七章 类的定制
 

第七章 类的定制
关于类的定制就说到这里,大家有兴趣挖掘的可以继续深入研究。

第六章 C++程序设计(一)

前面的三章,我们说了多态的一些技术内幕还有一些关于C++对象模型的内容,所以我就在想是要继续深入C++的知识点呢还是就目前的内容我们来聊聊如何来设计一个应用程序,最后选择了后者,这一章的内容我们来说说如何搭建一个GUI框架,由于GUI框架涉及到方方面面,所以我们这里只能算是一个简单的切入点,不涉及详细的编码的实现。
 

GUI框架很多,在windows上面C++有MFC,WTL,还有跨平台的Qt等等,我们可以随便找一个来作为参考,有了参考之后我们还需要对我们的框架的模块规划。
 

我们打算写一个DirectUI框架, 所以我们需要一个窗口——CDxWindowWnd。
 

CDxWindowWnd,作为我们的基本窗口类,该类我们只需要对HWND进行简单的包装。
 

//+————————–
 

//
// class CDxWindowWnd
// windows的基本窗口、
//
//
class CDxWindowWnd{
public:
  CDxWindowWnd();


virtual ~CDxWindowWnd();


operator HWND() const;



void ShowWindow(bool bShow = true, bool bTakeFocus = true);


UINT ShowModal();


virtual
void CloseHwnd(UINT nRet = IDOK);



void CenterWindow();


 

protected:
virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);


 

private:
HWND m_Hwnd{nullptr };


};


 

//+——————————-
 

 

窗口有了,我们需要一个消息循环,对于windows应用程序来说写个消息循环很简单:
 

//+——————————–
 

MSG msg ={ 0
};


while (::GetMessage(&

msg, NULL, 0, 0))
{
::TranslateMessage(&

msg);


::DispatchMessage(&

msg);


if (msg.message == WM_CLOSE &

&

::GetParent(msg.hwnd) == NULL)
break;


}
 

//+——————————-
 

 

虽然这几句代码可以实现我们所要的消息循环,但是我们可以将该消息循环进行封装,并且将该模块作为单例,这样一样我们可以在里面进行更多的操作:
 

//+——————————-
 

 

//
// class CDxApplication 
// 负责窗口程序的消息循环
// 以即一些公有资料的管理
//
class CDxApplication{
public:
CDxApplication(HINSTANCE __hInstance = nullptr);


~CDxApplication();


 

static CDxApplication* InstanceWithArgs(HINSTANCE __hInstance);


static CDxApplication* Instance();


static
void Destroy();


 

static
void SetFont(const MString&

fontName, unsigned fSize, bool isBold = false, bool isItalic = false);


static HINSTANCE GetHInstance();


static HFONT GetFont();


static DXFontInfo GetFontInfo();


static MString GetExePath();


static MString GetCurrentTime();


static MString GetUUIDStr();


 

//
// 消息循环
//

void Quit();



int Run();


 

protected:
static CDxApplication* __sPtr;


};


 

 

 

//
// 现在我们可以直接创建窗口并显示出来
//
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE prevInstance, LPWSTR cmdLine,
int cmdShow)

{
 

DxUI::CDxApplication* App = DxUI::CDxApplication::InstanceWithArgs(hInstance);


 

DxUI::CDxWindowWnd WndWindow;


WndWindow.Create(nullptr, L”TestWindow”, DXUI_WNDSTYLE_FRAME, 0);


WndWindow.ShowWindow();


App->Run();


DxUI::CDxApplication::Destroy();


return 0;


}
 

//+——————————–
 

第六章 C++程序设计(一)
 

窗口我们创建出来了,但不符我们的DirectUI的预期,我们需要将标题栏去掉,所以我们可以在CDxWindowWnd的基础上进一步修改:
 

 

//+——————————–
 

class CDxWindowImpl : public CDxWindowWnd
{
public:
CDxWindowImpl();


~CDxWindowImpl();


 

 

virtual LRESULT OnNcHitTest(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL&

bHandled);


virtual LRESULT OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL&

bHandled);


virtual LRESULT OnSysCommand(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL&

bHandled);


virtual LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL&

bHandled);


virtual LRESULT OnLButtonDown(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL&

bHandled);


 

LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);

// 从基类基础而来
 

 

//
// 其他
//
protected:

int mIdentifyId{ DX_InvalidID
};


bool bIsVisible{ true
};


bool bIsZoomable{ true
};


RECT mCaptionBox;


RECT mSizeBox;


SIZE mMaxSize;


SIZE mMinSize;


SIZE mRoundRectSize;


};


 

//+——————————-
 

修改窗口风格我们放在OnCreate函数中进行实现:
 

//+——————————
 

LRESULT CDxWindowImpl::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL&

bHandled)
{
//+——————-
//
// 调整窗口样式
//
//+——————–
LONG styleValue = ::GetWindowLong(*this, GWL_STYLE);


styleValue &

= ~WS_CAPTION;


::SetWindowLong(*this, GWL_STYLE, styleValue | WS_CLIPSIBLINGS | WS_CLIPCHILDREN);


return 0;


}
 

//+——————————
 

当然,如果我们就这样把标题栏去掉之后,窗口就没法拉动,也没法关闭,就一直停在桌面上,一动不动,所以为了解决这个问题,我们必须换种方式把标题栏给重新绘制出来,这就是 OnNcHitTest 的功劳了。
 

//+—————————–
 

LRESULT CDxWindowImpl::OnNcHitTest(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL&

bHandled)
{
POINT pt;

 
pt.x = GET_X_LPARAM(lParam);

 
pt.y = GET_Y_LPARAM(lParam);


::ScreenToClient(*this, &

pt);


 

RECT rcClient;


::GetClientRect(*this, &

rcClient);


 

if (!::IsZoomed(*this) &

&

bIsZoomable)
{
RECT rcSizeBox = mSizeBox;


if (pt.y <rcClient.top + rcSizeBox.top)
{
if (pt.x <rcClient.left + rcSizeBox.left)

return HTTOPLEFT;


if (pt.x >rcClient.right – rcSizeBox.right)

return HTTOPRIGHT;


return HTTOP;



}

else if (pt.y >rcClient.bottom – rcSizeBox.bottom)
{
if (pt.x <rcClient.left + rcSizeBox.left)

return HTBOTTOMLEFT;


if (pt.x >rcClient.right – rcSizeBox.right)

return HTBOTTOMRIGHT;


return HTBOTTOM;



}

 

if (pt.x <rcClient.left + rcSizeBox.left)

return HTLEFT;


if (pt.x >rcClient.right – rcSizeBox.right)

return HTRIGHT;



}

 

RECT rcCaption = mCaptionBox;


if (-1 == rcCaption.bottom)
{
rcCaption.bottom = rcClient.bottom;



}

 

if (pt.x >= rcClient.left + rcCaption.left &

&

pt.x <rcClient.right – rcCaption.right
&

&

pt.y >= rcCaption.top &

&

pt.y <rcCaption.bottom)
{

return HTCAPTION;



}

 

return HTCLIENT;


}
 

//+————————————-
 

标题栏的关键在于mCaptionBoxbottom的值,所以我们可以设置mCaptionBox来修改标题栏的高度。
 

我们现在虽然得到了我们想要的无标题栏窗口,但是这只是一张白板,所以我们还需要对齐进行绘制,我们称该层为绘制资源管理层:
 

 

//+————————————-
 

class CDxRendImpl : public CDxWindowImpl
{
public:
CDxRendImpl();


~CDxRendImpl();


 

 

virtual bool OnInitResource2D();


virtual bool OnInitResource3D();


virtual
void UnInitResource();


virtual
void OnRender();


virtual
void OnRender2D();


virtual
void OnRender3D();


virtual
void SaveToFile(const MString&

fileName);


virtual
void OnRendWindow(IPainterInterface* painter);


};


 

 

//+————————————–
 

我们想要绘制那么我们就需要一个绘制模块,绘制的时候我们还需要效果,所以我们还需要两个模块:
 

 

//+————————————-
 

 

//
// 效果接口
//
class CDxEffects
{
///
/// 多种效果
///
};


 

 

 

//
// 二维平面变换矩阵
//
struct TransformMatrix{
FLOAT _11;


FLOAT _12;


FLOAT _21;


FLOAT _22;


FLOAT _31;


FLOAT _32;


};


 

 

 

//
// 绘图接口
//
class IPainterInterface{
public:
 

//+——————————————-
//
// 为绘制方便,下面的接口都得实现
// 当然如果实际没有用处的可以简单的实现即可
//
//+——————————————-
virtual ~IPainterInterface(){ };


 

virtual
void BeginDraw() = 0;

  // 开始绘制
virtual
void Clear(const DxColor&

col) = 0;

// 使用特定色彩擦除背景
virtual
void EndDraw() = 0;

  // 结束绘制
 

virtual
void DrawRectangle(const RECT&

rc, const DxColor&

col,double size) = 0;


virtual
void DrawRoundedRectangle(const RECT&

rc, const SIZE&

radius, const DxColor&

col, double size) = 0;


virtual
void DrawEllipse(const RECT&

rc, const SIZE&

radius, const DxColor&

col, double size) = 0;


 

virtual
void DrawDashRectangle(const RECT&

rc, const DxColor&

col, double size) = 0;


virtual
void DrawDashRoundedRectangle(const RECT&

rc, const SIZE&

radius, const DxColor&

col, double size) = 0;


virtual
void DrawDashEllipse(const RECT&

rc, const SIZE&

radius, const DxColor&

col, double size) = 0;


 

virtual
void FillRectangle(const RECT&

rc, const DxColor&

col) = 0;


virtual
void FillRoundedRectangle(const RECT&

rc, const SIZE&

radius, const DxColor&

col) = 0;


virtual
void FillEllipse(const RECT&

rc, const SIZE&

radius, const DxColor&

col) = 0;


 

virtual
void FillRectangle(const RECT&

rc, CDxEffects* pEffects) = 0;


virtual
void FillRoundedRectangle(const RECT&

rc, const SIZE&

radius, CDxEffects* pEffects) = 0;


virtual
void FillEllipse(const RECT&

rc, const SIZE&

radius, CDxEffects* pEffects) = 0;


 

 

virtual
void DrawBitmap(const RECT&

rc, CDxEffects* pEffects) = 0;


virtual
void DrawBitmap(const RECT&

rc, const MString&

bitmap,int w = -1,int h = -1) = 0;


 

 

virtual
void DrawText(const MString&

Text, const RECT&

rc, CDxEffects* pEffects) = 0;

  // 只绘制文本,超出区域不管 效率更高
virtual
void DrawText(const MString&

Text, const RECT&

rc, const DxColor&

col, const DXFontInfo&

font,DXAlignment alignt) = 0;

// 不使用效果直接绘制
virtual
void DrawTextWithClip(const MString&

Text, const RECT&

rc, CDxEffects* pEffects, const RECT&

cliprc ={ 0, 0, 0, 0
}) = 0;

// 绘制文本,超出区域裁剪
 

virtual
void BeginClipRect(const RECT&

rc) = 0;

  // 在绘制之前调用
virtual
void BeginClipRect(const std::vector<DxPointD>&

points) = 0;

  // 在绘制之前调用
virtual
void EndClipRect() = 0;

// 在绘制完成之后调用
 

 

//
// 绘制线体
// DrawLines效率更高
//
virtual
void DrawLine(const DxPointD&

first, const DxPointD&

second, const DxColor&

col, double Size) = 0;


virtual
void DrawLines(const std::vector<DxPointD>&

points, const DxColor&

col, double Size) = 0;


 

 

virtual
void DrawDashLine(const DxPointD&

first, const DxPointD&

second, const DxColor&

col, double Size) = 0;


virtual
void DrawDashLines(const std::vector<DxPointD>&

points, const DxColor&

col, double Size) = 0;


 

 

//
// 变换
// 
virtual
void SetTransform(const TransformMatrix&

mat) = 0;


};


 

//+—————————
 

IPainterInterface 是一个纯虚类,换句话说就是接口类,该类没有对他的子类提供任何便利,反而要求子类必须实现自己定义的所有纯虚接口,而所谓纯虚接口就是虚函数等于0的函数,而只要含有纯虚函数的类就是纯虚类,也就是所谓的接口类,之所以我们这里要将绘制操作定义为纯虚类主要是考虑到以后可能会使用不同的图像引擎来绘制图形,比如我们这里可以使用D2D,也可以使用OpenGL,还可以使用GDI等等,那么我们为什么能够同时展示二维和三维图形,所以我们选择D2D:
 

 

//+—————————
 

class CDxPainter : public IPainterInterface
{
public:
typedef ID2D1RenderTarget* LPID2D1HwndRenderTarget;


public:
CDxPainter(ID2D1RenderTarget* render);


~CDxPainter();


ID2D1RenderTarget* getRenderTarget() const;


ID2D1RenderTarget* operator->() const;


operator LPID2D1HwndRenderTarget() const;


operator bool() const;


 

//
// 下面是 IPainterInterface 继承而来的所有接口
//
 

private:
ID2D1RenderTarget* pRenderTarget;


CDxCharaterFormat* pTextRender{ nullptr
};


ID2D1Layer* p_ClipLayout{ nullptr
};


ID2D1Geometry* p_ClipGeometry{ nullptr
};


};


 

//+——————————
 

 

到现在我们已经拥有了窗口,效果,绘图三个模块,所以我们只需要将三个模块组合起来就可以进行图形绘制,这个操作我们放在CDxRendImpl::OnRender()中,不过CDxRendImpl::OnRender()中我们默认绘制2D平面,所以真正的操作我们放在CDxRendImpl::OnRender2D()中:
 

 

//+——————————
 

 


void CDxRendImpl::OnRender(){

OnRender2D();


}
 

 


void CDxRendImpl::OnRender2D(){

CDxPainter painter(pRendTarget);


painter.BeginDraw();


this->OnRendWindow(&

painter);


painter.EndDraw();


}
 

 


void CDxRendImpl::OnRendWindow(IPainterInterface* painter){

;


}
 

 

//+——————————-
 

事实上我们并不对该层进行渲染,所以该层的OnRendWindow函数被实现为空,因为我们需要的是做一个DirectUI框架,而我们现在还是基于窗口HWND的,所以我们还差我们的DirectUI窗口,当然DirectUI至少需要一个持有HWND,所以我们必须继承至CDxRendImpl.
 

//+——————————-
 

 

//
// DirectUI 窗口类
//
class CDxWidget : public  CDxRendImpl
{
public:
CDxWidget();


~CDxWidget();


 

//+—————————
//
// 创建Hwnd
//
//+—————————
virtual
void CreateHwnd();


virtual
void CreateHwnd(HWND parent);


 

//
// 其他
//
 

};


 

 

//+———————————
 

我们可以通过CDxWidget::CreateHwnd()决定是否需要创建HWND,我们只对主窗口创建HWND对于子窗口不创建,在渲染的时候我们先渲染当前窗口,再对子窗口进行渲染。
 

 

//+——————————–
 

 

//
// 绘制窗口
//

void CDxWidget::OnRendWindow(IPainterInterface* painter){

if (bIsVisible == false){
return;



}

 

mEffects.SetCurrentStatus(GetWindowStatus());


 

 

//+—————
//
// 渲染Title
//
//+————–
if (mHwnd &

&

!pCaptionLabel&

&

!::GetParent(mHwnd)){
pCaptionLabel = new CDxCaption;


pCaptionLabel->SetParent(this);


RECT rc = mFrameArea;


rc.bottom =  mCaptionBox.bottom;


pCaptionLabel->SetGeomety(rc);


mRendArea = mFrameArea;


mRendArea.X(mFrameArea.X() + mSizeBox.left);


mRendArea.Y(mRendArea.Y() + mCaptionBox.bottom);


mRendArea.Width(mFrameArea.Width() – mSizeBox.left – mSizeBox.right);


mRendArea.Height(mFrameArea.Height() – mCaptionBox.bottom – mSizeBox.bottom);


if (!mIcon.empty()){
pCaptionLabel->GetIconEffects()->SetBitmaps(Dx_Normal, mIcon);



}

 

UpdateChildWindowPos();



}

if (pCaptionLabel){
RECT rc = mFrameArea;


rc.bottom = rc.top + pCaptionLabel->GetFrameRect().Height();


pCaptionLabel->SetGeomety(rc);


pCaptionLabel->SetText(mTitle);


pCaptionLabel->OnRendWindow(painter);



}

 

if (mEffects.GetEffectType() == CDxEffects::Dx_ImageType){
painter->DrawBitmap(mImageRendArea, &

mEffects);



}

else if (mEffects.GetEffectType() == CDxEffects::Dx_ColorType){
DXShape shape = GetWindowShape();


switch (shape)
{
case DxUI::Dx_Rectangle:
painter->FillRectangle(mRendArea, &

mEffects);


break;


 

case DxUI::Dx_RoundedRectangle:
painter->FillRoundedRectangle(mRendArea, mRoundRectSize, &

mEffects);


if (bIsNeedBorder &

&

mBorderWidth >0){
painter->DrawRoundedRectangle(mRendArea, mRoundRectSize, mBorderColor, mBorderWidth);



}

break;


 

case DxUI::Dx_Ellipse:
painter->FillEllipse(mRendArea, mRoundRectSize, &

mEffects);


break;


 

default:
break;



}


}

else{
DXShape shape = GetWindowShape();


switch (shape)
{
case DxUI::Dx_Rectangle:
painter->FillRectangle(mRendArea, &

mEffects);


painter->DrawBitmap(mImageRendArea, &

mEffects);


break;


 

case DxUI::Dx_RoundedRectangle:
painter->FillRoundedRectangle(mRendArea, mRoundRectSize, &

mEffects);


painter->DrawBitmap(mImageRendArea, &

mEffects);


break;


 

case DxUI::Dx_Ellipse:
painter->FillEllipse(mRendArea, mRoundRectSize, &

mEffects);


painter->DrawBitmap(mImageRendArea, &

mEffects);


break;


 

default:
break;



}


}

 

if (!mText.empty() ){
painter->DrawText(mText, mTextRendArea, &

mEffects);



}

 

if (pLayout){
pLayout->OnRendWindow(painter);



}

 

 

//+—————————–
//
// 渲染子窗口
//
//+—————————–
if (!mChildList.empty()){
UpdateChildWindowPos();



for (auto&

window : mChildList){
CDxWidget*&

windowref = window.ref();


if (windowref->GetHwnd() == nullptr){
windowref->OnRendWindow(painter);



}


}


}

 

if (bIsNeedBorder){
RECT rc = mRendArea;


rc.left += 1;


rc.right -= 1;


rc.top += 1;


rc.bottom -= 1;


if (mEffects.GetEffectType() == CDxEffects::Dx_ImageType){
painter->DrawRectangle(rc, mBorderColor, 1);



}

else{
DXShape shape = GetWindowShape();


switch (shape)
{
case DxUI::Dx_Rectangle:
painter->DrawRectangle(rc, mBorderColor, 1);


break;


 

case DxUI::Dx_RoundedRectangle:
painter->DrawRoundedRectangle(rc, mRoundRectSize, mBorderColor, 1);


break;


 

case DxUI::Dx_Ellipse:
painter->DrawEllipse(rc, mRoundRectSize, mBorderColor, 1);


break;


 

default:
break;



}


}


}

 

 

if (!bIsEnabel){
DXShape shape = GetWindowShape();


mEffects.SetCurrentStatus(Dx_Disable);


mEffects.SetDisabelColor(mDisabelColor);


switch (shape)
{
case DxUI::Dx_Rectangle:
painter->FillRectangle(mRendArea, &

mEffects);


break;


 

case DxUI::Dx_RoundedRectangle:
painter->FillRoundedRectangle(mRendArea, mRoundRectSize, &

mEffects);


break;


 

case DxUI::Dx_Ellipse:
painter->FillEllipse(mRendArea, mRoundRectSize, &

mEffects);


break;


 

default:
break;



}


}

}
 

//+———————————-
 

显然对CDxRendImpl::OnRender2D()进行完善:
 

 

//+———————————

void CDxRendImpl::OnRender2D(){

 

CDxWidget* window = dynamic_cast<CDxWidget*>(this);


if (window->IsNeedRender() == false)
return;


 

 

if (pRendTarget &

&

window){
 

pRendTarget->BeginDraw();


 

pRendTarget->Clear(ToD2DColor(window->GetBackGroundColor()));


window->OnRendWindow(pPainter);


if(window->GetWindowSelfDesc() == Dx_PopWindow)
pPainter->DrawRoundedRectangle(window->GetFrameRect(), window->GetRoundRectSize(), RgbI(128, 128, 128), 2);


 

HRESULT hr = pRendTarget->EndDraw();


if (FAILED(hr)){
DxTRACE(L”渲染出错[%1]n”, hr);



}


}

else{
if (window->GetWindowSelfDesc() == Dx_Layout){
if (window->GetParent()){
window->GetParent()->OnRender2D();


return;



}


}

else if (window &

&

window->IsVisible() ){
if (window->GetOpacity() <0.99999999){
if (window->GetParent()){
window->GetParent()->OnRender2D();


return;



}


}

DxColor col = window->GetEraseColor();


CDxWidget* parent = window->GetParent();


if (col.rgb.a == 0 || (parent &

&

parent->HasFloatWindow())){
if (parent){
parent->OnRender2D();


return;



}


}

auto render = GetRenderTarget();


if (render){
CDxPainter* painter = nullptr;


if (g_PainterMap.count(render)){
painter = g_PainterMap.at(render);



}

else{
painter = new CDxPainter(render);


g_PainterMap[render] = painter;



}

 

render->BeginDraw();


 

ID2D1Layer* p_ClipLayout{ nullptr
};


ID2D1Geometry* p_ClipGeometry{ nullptr
};


safe_release(p_ClipGeometry);


safe_release(p_ClipLayout);


render->CreateLayer(&

p_ClipLayout);


RECT rc = window->GetInvalidateRect();


p_ClipGeometry = CDxResource::CreateRectGeometry(rc);


if (p_ClipLayout == nullptr || p_ClipGeometry == nullptr)
return;


 

 

render->PushLayer(D2D1::Layer参数(
D2D1::InfiniteRect(),
p_ClipGeometry,
D2D1_ANTIALIAS_MODE_PER_PRIMITIVE,
D2D1::IdentityMatrix(),
1.0f,
NULL,
D2D1_LAYER_OPTIONS_NONE
), p_ClipLayout);


 

 

render->Clear(ToD2DColor(col));


window->OnRendWindow(painter);


render->PopLayer();


safe_release(p_ClipGeometry);


safe_release(p_ClipLayout);


 

HRESULT hr = render->EndDraw();


if (FAILED(hr)){
DxTRACE(L”渲染出错[%1]n”, hr);



}


}


}


}

}
 

//+———————————
 

我们现在我们可以通过子类化CDxWidget实现各种控件,实现不同的效果,最终我们可以很简单的编写出各种样式的界面。
 

这一章的重点并非是在这个GUI框架的实现上,而是主要让我们对class,继承和多态的使用有进一步的理解,以及对纯虚类的引入,当然至于对该框架感兴趣的同学,我们会在后续的内容里面一点点的引入,毕竟里面有很多模板的东西,现在一下子拿出来很多东西可能会比较吃力,所以这一章的内容还是在对C++程序设计上的一些理解,怎么组合class,怎么使用继承,怎么使用多态,怎么使用纯虚类等,这些其实是仁者见仁智者见智的问题,但在自己没有比较好的想法的时候有个参考总归是好的。
 

第六章 C++程序设计(一)
第六章 C++程序设计(一)
第六章 C++程序设计(一)
第六章 C++程序设计(一)
第六章 C++程序设计(一)
第六章 C++程序设计(一)
第六章 C++程序设计(一)
第六章 C++程序设计(一)
第六章 C++程序设计(一)

第四章 多态

概念
使用基类的指针或者引用在运行期执行正确的操作这种行为我们称之为多态。
 

从这个基本的概念,我们可以简单的推断出多态所需要的关键技术之一便是继承,关于继承在第三章已经接触,不过并没有真正的深入,那么除了继承之外支撑多态行为的便是关键字virtual啦。那么,在介绍virtual之前我们先来看看下面这个模型:
 

 

//+—————————-
 

class Ios{ };


class IStream : public virtual Ios{ };


class OStream : public virtual Ios{ };


class IOStream : public IStream,public OStream{ };


 

//+—————————-
 

这是典型的钻石模型,他结构如下(由于家里电脑没有安装viso,所以这里的模型图都是用纸绘制出来的,粗糙了点):
第四章 多态
 

下面我们来讨论IOStream的各个部分的大小:
 

//+—————————–
 

std::cout<<sizeof(Ios)<<std::endl;


std::cout<<sizeof(IStream)<<std::endl;


std::cout<<sizeof(OStream)<<std::endl;


std::cout<<sizeof(IOStream)<<std::endl;


 

//+—————————–
 

如果我们不执行上面代码的话我们能够知道答案吗?好吧,我们简单的分析一下:
 

首先,Ios是一个空类,所以我们可以认为他的大小是0,但是……但是以前的都是废话,我们可以想想一下:
 

//+—————————-
 

Ios os;


std::cout<<&

os<<std::endl;


 

//+—————————
 

就比如上面的操作,当我们想对一个没有数据成员的对象取地址时,如果我们认为他的大小是0的,那么这里是不是非法呢?事实上我们这样做是没问题的,也就是说这种操作是合法,那么问题来了,既然合法,那我们还能够认为这个空类的大小是0吗?当然不是,事实是这样,当一个类为空类——也就是没有任何数据成员的类,编译器会他安插一个char进去,所以他的大小应该为1。
 

对于IStream的大小,想要分析那就稍微麻烦点了,在分析他大小之前我们先来说说虚基类的概念,虚基类不同于普通的基类,虚基类属于一个共享对象,所以在IStream的数据中并没有基类的对象,而是包含一个指向基类对象的指针,该模型如下:
第四章 多态
 

 

在明白了虚基类这个概念之后我们进一步分析IStream的大小就比较容易了,首先IStream的对象里面包含了一个指针,该指针指向Ios对象,在win32平台下,一个指针的大小是4,这里需要注意,虽然IStream也是一个空类,但是此处我们有了一个指向基类对象的指针,所以就不需要在安插一个char,所以IStream的大小便是4。
 

 

同理OStream的大小也是4。
 

对于IOStream而言,这里他的大小也就是IStream的大小加上OStream的大小,所以IOStream的大小便是8.
 

我们上面的模型只是基本的继承,不具有多态性质,所以我们将问题稍微的复杂话一下,让他具有多态性质:
 

 

//+———————-
 

class Ios{
public:
virtual Ios&

operator<<(int){ }
virtual Ios&

operator>>(int&

){ }
};


 

class IStream : public virtual Ios{ };


class OStream : public virtual Ios{ };


class IOStream : public IStream,public OStream{ };


 

//+———————–
 

我们现在再来分析一下各部分的大小,首先还是Ios,此处Ios依旧没有数据成员,那么我们是不是任务他的大小是1呢?嗯,这里就复杂了,首先我们可以肯定他的大小不是1,那么是多少呢?这里我们就来挖掘多态背后的真相。
 

当一个类里面至少拥有一个虚函数时,那么编译器就会为该类生成一张虚函数表——vtbl,而该类的对象将会有持有一个指向该该表的指针——vptr。那么现在我们先来看看编译器是如何调用普通成员函数的:
 

 

//+————————–
 

class B{
public:

void Fun(){ }

};


 

 

int main(){
B b;


b.Fun();


}
 

//+————————-
 

如上,当我们定义出一个B的对象后,我们调用Fun函数,编译背后的操作如下:
 

//+————————-
 

首先编译器会将成员函数Fun改成一个普通的外部函数函数:

void B_Fun(B* const this);

// B_Fun 不一定是这个名字,编译器有他自己的命名规则,这里仅仅作为一个参考
 

于是
b.Fun() ==>B_Fun(&

b)
 

//+————————-
 

从上面的过程我们可以看到使用class的封装和C语言的直接函数调用比起并没有带来任何效率上的损失。也就是说:
 

 

//+————————–
 


void Fun(B* const this);



void B::Fun();


 

B b;


b.Fun();


Fun(&

b);


 

//+————————–
 

上面的两种执行效率是一样的。
 

那么如果成员函数有关键字virtual修饰的时候编译器还会这样将他改写为普通的外部函数吗?答案是否定,对于有virtual关键字修饰的成员数,编译器会将他放进虚函数表中,添加一个索引,使用chunk技术(该技术有些复杂,目的是为了提高调用效率,所以这里不细说),比如:
 

//+—————————
 

class B{
public:
virtual
void Fun(){ }

};


 

int main(){
 

B b;


b.Fun();


}
 

//+—————————-
 

上面的b.Fun()的调用实际上将是执行下面的代码:
 

 

//+—————————-
 

(*b.vptr[0])(&

b);


 

//+—————————-
 

此处的vptr是就虚函数指针,由于他指向的是一张虚函数表,B只有一个虚函数,就是索引为0的位置。
 

 

到了这一步,我们离揭开多态的技术内幕也仅仅只有一步之遥了,OK,那么我们现在来分析一下Ios的大小,我们上面说了,此时引入虚函数的Ios的大小已经不再是1,由于他是空类,所以他的大小为一个指针大小,该指针指向Ios的虚函数表,该虚函数表中有两个slot,分别是operator<<和operator>>的函数指针,所以此处的Ios的大小是4。
第四章 多态
 

知道Ios的大小是4之后我们来计算IStream的大小,由于IStream是虚列继承至Ios,所以他的大小不仅包含了Ios的大小,同时还有一个Ios的指针,模型如下,如果他的大小是8。
第四章 多态
 

同理OStream的大小也是8,得出IStream和OStream的大小之后我们来看看IOStream的大小,那么弱弱的问一下,IOStream的大小是16吗?答案同样是否定,上面我们说了,对于虚基类整个对象中只有一个对象实例存在,所以对于IOStream来说,并不是IStream的大小加上OStream的大小这么简单,因为这里仅仅只有一个Ios对象存在,而一个Ios的大小是4,所以这里IOStream的大小为12,模型如下:
第四章 多态
到了这里,我们可以继续深入啦:
 

//+—————————-
 

class Ios{
public:
virtual Ios&

operator<<(int){ }
virtual Ios&

operator>>(int&

){ }
};


 

class IStream : public virtual Ios{
public:
Ios&

operator>>(int&

){ }
};


 

class OStream : public virtual Ios{
public:
Ios&

operator<<(int){ }
};


 

class IOStream : public IStream,public OStream{ };


 

//+————————–
 

这里我们真正的引入了多态,在彻底揭开他背后的神秘面纱之前我们先来看看构造函数都做些啥,首先从简单的Ios说起:
 

 

//+————————–
 

Ios* Ios:Ios(Ios* const this){
this->vptr = vbtl;


return this;


}
 

//+————————–
 

上面的代码不能当真,可以把他当作伪码,这段代码实在构造一个对象的时候编译器扩展出来的,我们是不需要关心,这是编译器会为我们做的,所以当我们在完成一个对象构造之后我们就拥有一张行为正确的虚函数表,在执行期就能够得到正确行为,当然由于Ios本身就没有继承,所以这无关紧要,那么对于IStream的构造函数就比较麻烦一些,大致会扩展成如下:
 

//+————————–
 

IStream* IStream::IStream(IStream* const this,bool is_most_derived)
{
if(is_most_derived != false)
this->Ios::Ios();

// 构造基类
this->vptr = vbtl;

// 设置虚函数表
this->Ios.vptr = vbtl_Ios;

// vbtl是修改后的基类的虚函数表,对于这里来说是将slot2的地址换成&

IStream::operator>>

return this;


}
 

//+————————–
 

当然这是虚继承的情况下会这么做,如果不是虚继承的时候就相对简单一些,比如:
 

//+————————–
 

class B{
public:

void Fun(){ }

};


 

class D : public B{
public:

void Fun(){ }

};


 

Dd构造函数的扩展如下
 

D* D::D(D* const this)
{
this->B::B();


this->vptr = vbtl;


this->vptr_B = vbtl_B;


return this;


}
 

//+————————
 

同理,我们知道了OStream的构造过程,那么对于IOStream的呢?他应该怎么构造呢?其实也是大同小异,如下:
 

//+————————
 

IOStream* IOStream::IOStream(IOStream* const this,bool is_most_derived)
{
if(is_most_derived != false)
this->Ios::Ios();

// 构造共享部分基类
this->IStream::IStream(false);


this->OStream::OStream(false);


// 初始化vptr

return this;


}
 

//+————————-
第四章 多态
到了这里,多态背后的神秘面纱是不是已经被彻底揭开了呢?所谓多态就是根据基类持有的虚函数指针找到对应的虚函数表,从中执行指定函数指针然后得到正确的行为,对于构造函数的执行,都是先执行基类的构造函数,最后一层层的执行上来,最终执行到自己的构造函数,每一个构造函数中都有对基类的vptr进行修改,所以基类的vptr最终都是指向了最新的vbtl,这就是多态的秘密,而从这个结论中我们还得到一个铁律,那就是在构造函数中调用虚函数实际上等同于调用普通函数,因为在当前的构造函数我们所能够拿到最新的vbtl是当前类的vbtl,并非是最后的vbtl,所以我们所执行的函数就是当前类的函数,所以不论是是否为虚函数,那么他的效果都如果非虚函数,当然调用的过程可以有所区别:
 

//+—————————–
 

class B{
public:
B(){
fun();



}

 

virtual
void fun(){ }

};


 

其中的构造函数将会被扩展如下形式:
 

B* B::B(B* const this)
{
this->vptr = vbtl;


(*this->vptr[0])(this);


return this;


}
 

//+——————————–
 

ok,到此处,对于多态,想必大家都已经有了深入的认识,如果觉得还不够,那么大家可以去研究一下《深度探索C++对象模型》一书。

第四章 继承

C++是一门可面向对象可面向过程编程的语言,当然它和C语言比起的优势就是面向对象,面向对象是C++的一大特点,那么,上一讲将了面向对象的第一要素,那么这一讲的内容就是面向对象的第二要素——继承。
 

为什么要有继承?
 

如果没有继承,那么类仅仅只是具有一些相关行为的数据结构,所以,这仅仅只是对过程语言的一大改进,而继承的引入,则开辟完全不同的新天地,那么继承能够做些什么?
 

使用继承构建类。
 

使用继承扩展功能。
 

使用继承实现多态(多态的实现不只是继承,还有虚函数,两者缺一不可)。
 

那么什么时候可以使用继承?简单点说有一个原则,就是当我们认为“是一个”的时候就可以考虑继承了,关于“是一个”的概念可能有点模糊,简单点说就是当我们认为某个东西属于某类时就可以使用该原则了,比如猫属于动物,学生也是人,所以这时就可以使用继承啦。在C++里面,有“是一个”的概念,同时也有“有一个”的概念,“有一个”“是一个”这两个概念可能有时候会有些模糊,当我们理解不清的时候可能会套错了模型,“是一个”的概念决定我们使用继承,而且是共有继承,而“有一个”的概念我们不应该使用共有继承,更多的时候我们选择使用复合,当然有时候我们可以使用私有继承,当然在私有继承和复合类型之间怎么决策又有一些技巧性,这里大家可以通过《C++ Effective》一书进行了解。
 

使用继承构建类。
 

如果按照上面我们的“是一个”的原则去写代码可能我们会发现代码没法往下写,因为很多时候“是一个”并不是那么容易被看透,否则也就没有多重继承这种难以理解的语法存在,所以很多时候我们我们使用继承没别的想法,仅仅只是想要复用现有的代码而已。
 

//+————————–
 

class Super{
public:
Super(){ }
virtual ~Super(){ }

void doSomeThing(){ }

};


 

class Sub : public Super{
public:
Sub()

void doOtherThing(){ }

};


 

 

//+—————————
 

我们可以认为Sub就是一个Super,这没毛病,也合情合理,但是当出现多重继承的时候,比如:
 

//+—————————-
 

class Super2{
public:
Super2(){ }
virtual
void ~Super2(){ }


void doSomeOtherThing(){ }

};


 

class Sub : public Super,public Super2{
public:
Sub(){ }

void doOtherThing(){ }

};


 

//+—————————
 

如果我们现在还在认为Sub是一个Super的话就有点难理解啦,但它确确实实具有Super和Super2的功能,他们确确实实也满足“是一个”的原则(否则多态也就没有意义),但是这里我们似乎要澄清一件事,这里并非是因为Sub满足“是一个”才使用继承(当然或许我们设计之初就是这么考虑的),而是因为被继承才被是一个,嗯,好吧,理解起来有些拗口,所以我们才这么认为继承可以用来构建类,是一个或许只是它附加的一个功能。
 

和使用继承构建类比起,使用继承扩展功能似乎更好理解一些,因为它是实实在在的是一个,我们要的也就是这个是一个原则,比如我们手里有一个处理字符串的类——String,而现有的接口都是该String的引用作为参数,而我们想要在新的开发中使用更加便捷的String,但同时有需要使用原有依赖该String的一些接口功能,所以我们不可能重新实现一个String,我们应该做的就是扩展这个String,子类化一个类出来,他提供有我们需要的功能,同时他还是一个String,那些使用String引用作为参数的接口依然不受任何任何影响:
 

//+————————–
 

class MyString : public String{
public:
……
using String::append;

// 加入基类的append只能接受字符串
template<class T>

void append(const T&

val){
std::ostringstream os;


os<<val;


String::append(os.str());



}

……
};


 

 


void testFun(const String&

str){
std::cout<<str<<std::endl;


}
 

 

int main(){
MyString str;


str.append(123);

// 调用子类的append
str.append(“,”);

// 调用基类的append
str.append(128.82);

// 调用子类的append
std::cout<<str<<std::endl;


testFun(str);


return 0;


}
 

 

//+—————————-
 

这就是典型的使用继承去扩展现有功能的例子啦,那么我们下面看看继承的一些高级用法,我们将思路回到前面的点上,我们不再去考虑扩展功能这件事,我们只想如何做好一件事,比如想要编写一个类,他具有比较大小,判断是否相等的操作,那么我们可以如下:
 

//+—————————
class MObj{
public:
MObj(){ }
virtual ~MObj(){ }
friend bool operator<(const MObj&

obj,const MObj&

obj2);


friend bool operator>(const MObj&

obj,const MObj&

obj2);


friend bool operator==(const MObj&

obj,const MObj&

obj2);


friend bool operator!=(const MObj&

obj,const MObj&

obj2);


……
};


 

//+—————————
 

如果我们上面所见,当我们想要实现这些功能,我们需要完成四个函数的编写,如果我们需要再编写一个类也需要这些操作,那么我们又得重新再来一遍,这……如果一个两个还好,要是我们经常这么干一定很不爽,所以这就是我们这里要说的重点,我们只需要完成一部分操作就能够实现全部操作即可,比如我们规定,当我们提供operator<时自动提供operator>,同时提供operator==以及提供operator!=等其他操作。
 

//+—————————
template<class T>
class CmpOrder{
public:
friend bool operator>(const T&

left,const T&

right){

return !(left <right);



}

 

friend bool operator == (const T&

left, const T&

right){

return !(left <right) &

&

!(left >right);



}

 

friend bool operator!=(const T&

left, const T&

right){

return !(left == right);



}

};


 

//+—————————–
 

这是一个通用的比较基类,所以只需要我们的类继承至该类而且实现operator<操作符即可得到这些其他的操作,比如:
 

//+—————————–
 

class MInt : public CmpOrder<MInt>{
private:

int mVal;


public:
MInt(int v) :mVal(v){ }
friend bool operator<(const MInt&

left,const MInt&

right){

return left.mVal <right.mVal;



}

};


 

 

int main(){
MInt val1(7);


MInt val2(8);


std::cout <<(val1 <val2) <<std::endl;


std::cout <<(val1 >val2) <<std::endl;


std::cout <<(val1 == val2) <<std::endl;


std::cout <<(val1 != val2) <<std::endl;


system(“pause”);


return 0;


}
 

//+——————————-
 

当然这种操作手法有一个奇怪的名字,叫奇特的递归模板模式,他的模式:
 

//+——————————-
 

class Sub : public Super<Sub>{…… };


 

//+——————————-
 

他使用模板加继承的手法实现一些强大的功能,当然还有一些更奇怪的继承方法我们后续在实践中慢慢说,比如:
 

 

//+——————————-
 

template<class A,class…Args>
class Sub : public Sub<Args…>{…… }
 

template<>
class Sub{ }
 

//+——————————–
这种继承手法能够实现一些功能强大的组件,比如我们可以以此为基础实现一个数据库。
 

好吧,继承我们就暂时介绍到这里,接下来还有多态等着说呢。

第二章 stringstream的使用

如果按照内容来给一个标题的话,那么这一讲的内容其实是上一讲的后续部分,所以都属于C++标准流一类,但是又碍于我们这不是写书,而是按照文章来推送,所以这算是一个新的章节,然尽管如此,这依然算是C++的流的介绍,所以,在上一章文章中我们了解了C++标准流的用法后那么我们现在从文件流说起。
程序无非是数据的操作,最常用的莫过于数据的读写了,还记得我们在上一讲的内容中使用自定义的流扩展了一个文件流——FileOStream,该类继承至MyStrIobase,当然我们可以直接继承至OStream,好吧,想想为什么要继承至OStream,继承至OStream的优势又是什么。
我们不对FileOstream进行讨论,至少大家已经知道了C++标准流背后的一些原理,所以我们这一讲的内容将是站在上一讲的基础上来对fstream和stream的探索。
从fstream说起
在C++标准库中,fstream继承至iostream,所以iostream该有的操作他都有,fstream还具有iostream不具有的能力——文件的度读写。
 

如同上一讲的内容,文件的读写简单点说就是将数据发送到指定的目的地或者是从指定的地方将数据取回来,所以,我们可以这么来解读iostream所干的事——将这个目的地给固定了,而fstream却可以自由指定这个目的地——文件,对于文件我们可以使用构造函数来指定,同样可以使用open接口来重定向:
//+———————–
#include <
fstream>

int main(){
std::ofstream outFile(“1.txt”,std::ios::out);


outFile<<“Hello World”<<std::endl;


outFile.close();


outFile.open(“2.txt”,std::ios::out);


outFile<<“Hello World2″<<std::endl;


outFile.close();


std::ifstream inFile(“1.txt”,std::ios::in);


std::string str;


std::getline(inFile,str);


std::cout<<str<<std::endl;


inFile.close();


return 0;


}
 

//+————————
从上面的代码中,我们可以看到,我们可以通过构造函数来打开文件,同样也可以通过提供的成员函数open来打开文件,当我们写完数据之后我们可以使用close来关闭文件,关闭当前文件后又可以打开其他文件,ofstream用来将数据写入文件,ifstream用来从文件中读取,所以,有了第一章的基础后来使用fstream是非常简单的,当然或许我们要说说的是对于二进制文件的读写,对于二进制数据还记得我们上一讲中说到的write函数吗?
//+———————–
ostream&

write(const char*,streamsize);


//+———————–
 

当时我们说这个函数可以用来处理字符串,其实它不只是能够处理字符串,他能够处理一切数据,为什么这么说呢?首先,它的第一个参数是一个char的指针,第二个参数是一个大小,而char*可以转换为任意数据的指针,同样任意数据都可以转换char*,比如:
 

//+———————–
 

 

int main(){

int
a = 10;


char* ch = (char*)(&

a);



int d = *(int*)(ch);


std::cout<<d<<std::endl;


return 0;


}
 

//+———————–
 

我们将一个int的对象存储在一个char*中然后又再取出来,数据得到很好的还原。我们再来看一些更为复杂的:
 

 

//+———————–
 

struct Test{

int a;


double b;


long long c;


};


 

int main(){
Test test ={10,20.0,1000LL };


char* ch = (char*)(&

test);


Test test2 = *(Test*)(ch);


std::cout<<test2.a<<std::endl;


std::cout<<test.b<<std::endl;


std::cout<<test.c<<std::endl;


return 0;


}
 

//+————————
 

就算是复合类型也毫无问题,那么我们是不是明白了write这个函数对于数据的读写能力的强大之处了呢?所以当我们要保存或是恢复一个对象的时候可以如下操作:
 

 

//+———————–
 

struct Test{

int a;


double b;


long long c;


};


 

int main(){
Test test ={ 10, 20.0, 1000LL
};


std::ofstream outFile(“1.txt”, std::ios::binary | std::ios::out);


outFile.write((char*)(&

test), sizeof(Test));


outFile.close();


 

std::ifstream inFile(“1.txt”, std::ios::binary | std::ios::in);


 

char* ch = new char[sizeof(Test)];


memset(ch, 0, sizeof(Test));


inFile.read(ch, sizeof(Test));


inFile.close();


 

Test test2 = *(Test*)(ch);


std::cout <<test2.a <<std::endl;


std::cout <<test.b <<std::endl;


std::cout <<test.c <<std::endl;


return 0;


}
 

 

//+————————
 

我们可以将一个对象存储在硬盘里面需要的时候可以将他恢复出来,这就是fstream的write和read的妙用。上一讲我们并没有接触过read,那么read函数的原型如下:
 

//+———————–
 

ifsteram&

read(char*, streamsize)
 

//+———————–
 

该函数的功能是从流中读取指定大小的字节数据,数据填充在指定的地方——第一个参数指定的地址。
 

 

至此,使用C++读写文件对我们来说已经是很轻松的事了,那么接下来我们来看看在C++流中我认为算是一个很高级的东西——stringstream
 

stringstream,顾名思义就是字符串流,对于不少C++程序员来说这个这个组件可能用得比较少,或许可能很多人没听说过,比如就我就遇到有人不知道该流的存在,更别说用法了,因为这东西实在用得比较少,而且如果只是普通的使用C++的话stringstream是可以完全无视的。噢……既然可以被无视的东西为什么我们这里要说呢?而且更是将stringstream的使用来作为这一章的标题。好吧,原因很简单,在我看来stringstream虽然不常用,但并不表示它没用。
 

设想一个场景,假如有两个函数,两个函数需求的参数类型各不相同,但如今我们会用到这两个函数,为了简便操作,我们将两个函数封装成一个函数:
 

//+————————-
 


void f(const std::string&

str){
std::cout<<str<<std::endl;


}
 


void g(int a){

std::cout<<a<<std::endl;


}
 

template<class T>

void fun(T val){

//
// 根据val的类型不同来调用不同的函数
// 如果是int调用g
// 如果是字符串调用f
// 否则不执行
//
}
 

 

//+————————–
 

现在我们拿到的f和g是由不同的人提供给我们的函数,我们要将这两个功能应用到我们的程序之中,这时为更方便我们可以对其进行封装得到我们的fun函数,当我们使用的时候就不需要关心我们到底调用的是哪一个函数,它会根据我们的参数类型而选择适合的函数进行调用。这里想要优雅的实现我们的fun说简单也不简单,说难也不难,而这正是这一讲要讲的东西。
 

 

将任意非字符串对象转换为字符串有多少种方法呢?常用的可能就是sprintf,这是C语言提供的库函数,如果不考虑跨平台可能用得最多的应该就是itoa,ltoa,ultoa…等一系列转换函数,尽管这些方法虽然都很好用,但我还是觉得stringstream可以比他们更加优雅,而且stringstream 可以做的事情远比想想的要多,下面是 stringstream 的基本使用:
 

//+—————————
 


void f(const std::string&

str);


 


void fun(int i){

//
// 将i转换为string然后调用f
//
std::stringstream os;


os<<i;


f(os.str());


}
 


void fun2(const char* ch){

//
// 将ch 转换为 int然后调用fun
//
std::stringstream is;


is<<ch;



int i;


is>>i;


fun(i);


}
 

 

//+——————————
 

我们将数据流到stringstream中,然后使用成员函数str提取出来就是字符串,同时我们还可以将他作为数据源,然后使用>>操作符流出我们需要的类型,所以,他的妙用就是作为类型的转换工具,而这一点使用spintf或者atoi等这些函数是很难做到的,关于这一点大家可以想想是为什么。
 

 

//+—————————
template<class L,class R>

void convert(L&

val,const R&

right)
{
    stringstream os;


    if(!(os<<right))
        return;


    os>>val;


}
 

//+—————————
 

这个小工具可以将右边的类型转换到左边的类型,我们可以这样使用:
 

 

//+————————-
 
int main(){
    const char* ch = “100.568”;


    doubel val = 0;


convert(val,ch)
    cout<<val<<endl;


   

return 0;


}
 

//+———————-
 

是不是很是方便,……嗯,但是他带来一个问题,比如说:
 

 

//+———————-
 

int
a = 10;


long b;


convert(b,a);


 

 

//+———————–
 

这种情况下我们原本是可以使用 b = a 来直接进行赋值的,但是由于使用了统一的操作接口,我们却让事情变得更加复杂啦,所以现在我们要解决一个问题,也就是说,如果我们可以直接使用b = a 时我们就使用 b = a,只有在不能使用 b = a 的时候才进行上面的转换操作。问题回到了我们的上面的假设场景啦。
 

什么时候能够使用b = a 呢?从面向对象的角度来分析的话就是只有下面两种情况能够使用该操作:
 

//+———————-
 

class A;


 

class B{
//
//
//
B(const A&

);


B&

operator=(const A&

);


};


 

//+———————
 

那么问题又来了,我们如何判断B是否有这种成员函数呢?而对于基本类型来说又没有这些成员函数——比如上面的int,double,long……等这些基础类型,那么我们又当如何处理呢?所以检查是否存在赋值函数的存在的方法不可取,我们只有另辟蹊径,我们可以将范围放得更大一些,直接检查是否存在可隐式转换,而重载函数的试探正好可以解决这个问题,听起来是不是有些玄妙,好吧,我们不妨来试试,毕竟直截了当的代码胜过含糊其辞的千言万语:
 

//+——————–
 

 

template<class T,class U>
class MConvertsion{
static __int64 test(T);


static __int8  test(…);


static U genU();


enum{value = (sizeof(test(genU())) == sizeof(__int64)) };


};


 

 

//+——————–
 

就是这么简单,我们使用两个重载函数test,一个有指定的类型作为参数,一个是变参,但是他们的返回类型不同,所以我们可以针对返回类型的不同进而判断出U是否可以转换到T,而这些函数都不需要实现,因为我们可以在编译期就完成了这个判断,如果你们现在还在怀疑这段程序的可执行性,那么你们不妨亲自测试一下:
 

 

//+——————-
 

std::cout <<MConvertsion<int, std::string>::value <<std::endl;


std::cout <<MConvertsion<int, long>::value <<std::endl;


 

//+——————-
 

我们经过测试,程序能够很好的判断是否能够进行转换,所以接下来我们可以完成我们上面的类型转换工具了,我们将转换的过程分两步,一步是可以进行转换操作的,一步是不可进行转换操作的,我们可以使用bool变量来进行表示,但是下面的操作是不能够工作的:
 

 

//+——————
 

template<class L,class R>

void convert(L&

val,const R&

right)
{
if(MConvertsion<L, R>::value){
val = right;



}

else{
    stringstream os;


    if(!(os<<right))
        return;


    os>>val;



}

}
 

//+——————-
 

上面的程序在MConvertsion<L, R>::value == false 的时候将无法通过编译,虽然if…else…就算不执行块也必须要通过编译,所以说到底if…else…是运行期的分发,而我们要解决的是编译期的分发,所以我们只能使用模板来派发,当编译变量为true的时候我们编译第一步,为false的时候我们编译第二步,实现代码如下:
 

//+——————-
 

template<bool>
struct MCopyValue{
    template<class T,class U>
    static
void apply(T&

val1,const U&

val2){
        val1 = val2;


   
}

};


 

template<>
struct MCopyValue<
false>{

    template<class T,class U>
    static
void apply(T&

val1,const U&

val2){
        std::stringstream ss;


        ss<<val2;


        ss>>val1;


   
}

 

    template<class U>
    static
void apply(std::string&

str,const U&

val2){
        std::stringstream ss;


        ss<<val2;


        str = ss.str();


   
}

};


 

 

//+———————
 

我们将bool作为模板类型,该类型在编译期间能够直接被确认,如果为false的时候就直接编译MCopyValue<
false>
,否则就编译MCopyValue<true>,那么该模板类型由谁来提供呢?当然就是MConvertsion<L, R>::value :
 

//+———————
 

 

template<class L,class R>

void convert(L&

val,const R&

right)
{
MCopyValue<MConvertsion<L, R>::value>::apply(val,right);


}
 

 

 

int main(){
std::string str;


convert(str,123);


std::cout<<str<<std::endl;


 

long
a = 10;


convert(a,str);


std::cout<<a<<std::endl;


 


int i = 0;


convert(i,a);


std::cout<<i<<std::endl;


 

double d = 0.0;


convert(d,i);


std::cout<<d<<std::endl;


 

return 0;


}
 

//+——————-
 

 

如果我们使用步进模式来调试上面的程序,我们会很明确的看到程序每一段都走进自己认为效率最好的代码段中。
 

最后,关于C++的标准流的讲解就到此为止,接下来我们将回过头来看看C++中最为重要的关键字——class

第一章 深入浅出IO流

如果没有iostream
如果没有iostream,那么我们的第一个程序应该怎么写呢?如果有iostream,我们的第一个程序成通常会是:
//+——————–
#include <iostream>
int main(){
std::cout<<“Hello World”<<std::endl;


return 0;


}
 

//+———————
 

这通常是我们学习C++最开始引入的程序,这个程序依赖了C++标准库的输出流ostream的对象cout,那么问题来了,如果我们自定义了一个类型,简单一点比如MyString,那么我们没有理由不让它支持打印这个操作,这时候如果没有iostream,我们应该怎么办呢?
//+———————–
class MyString{
//
// 实现细节
//
};


 

//+———————–
 

通常来说,如果我们需要让我们的类型支持流的的输出,那么我们就会考虑如下的方式:
 

//+———————–
 

std::ostream&

operator<<(std::ostream&

os,const MyString&

str)
//
// 实现细节
//

return os;


}
 

//+————————-
 

这种方式是我们常用的方式,是每一个C++程序员必须第一时间掌握的实现方式,下面我们会细说。现在的问题是这个函数我们应该怎么实现才能减少设计上带来的耦合,我们可以这么做:
 

//+————————-
namespace std{
class ostream;


}
 

std::ostream&

operator<<(std::ostream&

os,const MyString&

str);


 

//+————————-
 

注意,我们这里仅声明不实现,我们在其他的地方单独定义该函数,这样可以减少我们的类型MyString对iostream的耦合,但是在我们定义该函数的地方必须要包含iostream头文件,否则ostream就是未定义的类,当我们实现了这个函数之后,我们便可这么使用:
 

//+————————
 

#include “MyString.h”
#include <iostream>
 

int main(){
MyString str = “Hello World”;


std::cout<<str<<std::endl;


return 0;


}
 

//+————————
 

这个问题解决了设计上的耦合问题,但是并没有解决上面我们所提出的问题:如果我们没有iostream的话呢?
 

没有iostream没有关系,iostream依赖于C++标准库,但是有不依赖于C++ 标准库的东西,比如put系列函数,该系列函数是C运行库提供的函数,由于C语言是很多编程语言的祖先,而C++更是C的扩展,所以在C++中使用C函数是毫无问题的,但是此处使用put系列函数显得太不C++,当然可以对其进行封装再使用——面向对象编程不就是这样的吗?到这一步,我们可以来考虑使用类的概念来对put系列函数的封装,此处我们选择使用putc,putc函数的作用是将一个字符写进指定的文件中,这刚好满足我们的需求。
 

将数据写进目标地址中,秉着这一概念,我们可以先为我们的io定义一个函数:
 

//+————————
 


void sendstr(FILE* fp,const char* str,int n)

for(int i=0;

i<n;

++i){
putc(*str++,fp);



}

}
 

//+————————-
 

现在回到我们的MyString上面:
 

//+————————-
 

class MyString{… };


 

FILE* operator<<(FILE* fp,const MyString&

str)
sendstr(fp,str.c_str(),str.size());


return fp;


}
 

//+————————-
 

有了这个操作符之后我们便可以写出下面的代码:
 

//+————————-
 

int main(){
MyString str = “Hello World”;


stdout<<str;


return 0;


}
 

//+————————–
 

注意了,我们这里使用的是stdout而不是std::cout,stdout是C语言定义的标准输出,也就是控制台,而std::cout是C++的ostream的对象,目的地也是控制台,他是对stdout的封装。回到我们上面的代码,这种写法没毛病,但是很别扭,尤其在C++里面,所以我们应该尝试对stdout进行封装,如下:
 

//+————————-
 

class MyStrIobase{
public:
virtual ~MyIobase(){ }
virtual
void send(const char* ptr,int n){ }

};


 

//+————————
 

这是一个接口操作,有了这个接口,再加上我们上面的概念,所以我们可以对sendstr进行扩展:
 

//+————————
 


void sendstr(MyStrIobase&

strio,const char* ptr,int n)
strio.send(ptr,n)
}
 

//+————————
 

现在还没法工作,因为我们的MyStrIoBase啥都没做,如果我们想要做些什么,那么就需要从MyStrIoBase派生出我们自己的类,比如打印到控制台的OStream:
 

//+———————–
 

class OStream : public MyStrIobase{
public:
virtual
void send(const char* ptr,int n){

for(int i=0;

i<n;

++i){
putc(*ptr++,stdout)

}


}

};


 

 

 

OStream gOut;


int main(){
MyString str = “Hello World”;


gOut<<str;


return 0;


}
 

 

//+——————–
 

现在编译上面的程序我们会到编译错误,没有相应的操作符供我们使用,不过这都不是事,我们只需要扩展一下我们的operator<<操作符即可,我们可以使用模板让他做更多的事:
 

 

//+———————
 

template<class T>
T&

operator<<(T&

io,const MyString&

str){
sendstr(io,str.c_str(),str.size());


return io;


}
 

//+——————–
 

现在上面的代码能够正常工作了,当我们运行程序会在黑乎乎的控制台上看到Hello World的输出,那么如果我们想要使用std::ostream来输出呢?这时候我们应该怎么做呢?我们说了,我们使用模板来实现operator<<操作符,目的就是让他可以做更多事,所以对于std::ostream来说,MyString一样可以支持,只需要我们重载一个sendstr即可,如下:
 

 

//+——————–
 


void sendstr(std::ostream&

os,const char* ptr,int n){
os.write(ptr,n);


}
 

 

int main(){
MyString str = “Hello World”;


std::cout<<str;


return 0;


}
 

//+——————–
 

程序如同我们预期正常工作,那么,现在我们再回过头去看看我们的MyStrIobase,如果我们想要让他对文件的支持,那么我们应该怎么做呢?这些问题如果放到后面来说可能很简单,当然放在这里说也是很简单的,我们对FILE*进行简单的封装即可:
 

 

//+——————–
 

class FileOStream : public MyStrIobase{
public:
FileOStream(const char* fileName,const char* mode){
mFile = fopen(fileName, mode);



}

virtual
void send(const char* ptr,
int n){

if (mFile == nullptr)
return;



for (int i = 0;

i <n;

++i){
putc(*ptr++, mFile);



}


}

 


void close(){

fclose(mFile);


mFile = nullptr;



}

private:
FILE* mFile{ nullptr
};


};


 

int main(){
MyString str = “Hello World”;


FileOStream out(“text.txt”,”w+”);


out <<str;


out.close();


return 0;


}
 

//+———————
 

运行程序后我们便将字符写进了文件之中。
 

 

———————————–
 

 

iostream
 

到此,我们对io的操作有了一个大概的了解,接下里我们深入的观察一下iostream的工作原理,我们还是从最简单的开始:
 

 

//+———————-
 

int main(){
std::cout<<“Hello World”<<std::endl;


return 0;


}
 

//+———————
 

当我们执行上面程序的时候会发现控制台上打印出Hello World字符,同时光标移动到下一行的起始处。我们用一句通俗的话来对这句代码的描述:将”Hello World”塞进std::cout中,然后std::endl操作std::cout,有了前面的基础,我们要理解这句话不难,程序是由左向右执行,所以第一步的执行是如下的函数:
 

//———————
 

std::ostream&

operator<<(std::ostream&

os,const char* msg){
os.write(msg,strlen(msg));


return os;


}
 

//+——————–
 

在执行完上面这句代码之后返回std::cout,所以接下来执行的确实:std::cout<<std::endl;

为什么此处不能理解为将std::endl塞进std::cout中呢?原因很简单,因为std::endl是一个函数指针,他大致可以定义为下面的样子:
 

//+——————-
 

std::ostream&

endl(std::ostrem&

os){
os<<“n”;


os.flush();


return os;


}
 

//+——————
 

 

 

看到这里是不是觉得C++很有趣呢?那么问题来了,std::endl又是如何与流操作符联系上的呢?解决方式有很多,这里我们简单的说一种方式:
 

//+——————-
 

typedef std::function<std::ostream&

(std::ostream&

)>streamoperationtype;


std::ostream&

operator<<(std::ostream&

os,streamoperationtype fun){

return fun(os);


}
 

//+—————-
 

是不是很有意思……当然标准库可不是这么干的,这只是我个人这么干,但是思路应该是一致的,就算不一致,至少我们又知道了另一种解决方案。那么,打印在控制台的操作是不是只有cout呢?当然不是,下面是vs中的iostream声明的几个流对象,w开头的是针对unicode字符的流对象,常用的是cin和cout以及cerr,至于clog是用于输出日志的,它写的目的地和cerr一样都是stderr。
 

//+—————-
 

__PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 istream cin, *_Ptr_cin;


__PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 ostream cout, *_Ptr_cout;


__PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 ostream cerr, *_Ptr_cerr;


__PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 ostream clog, *_Ptr_clog;


 

__PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 wistream wcin, *_Ptr_wcin;


__PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 wostream wcout, *_Ptr_wcout;


__PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 wostream wcerr, *_Ptr_wcerr;


__PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 wostream wclog, *_Ptr_wclog;


 

 

//+—————–
 

在ostream里面定义了处理内部信息的输出操作符:
 

//+—————-
 


 

ostream&

operator<<(short);


ostream&

operator<<(int);


ostream&

operator<<(long);


ostream&

operator<<(long long);


 

ostream&

operator<<(unsigned short);


ostream&

operator<<(unsigned int);


ostream&

operator<<(unsigned long);


ostream&

operator<<(unsigned long long);


 

ostream&

operator<<(float);


ostream&

operator<<(double);


ostream&

operator<<(long double);


 

ostream&

operator<<(bool);


ostream&

operator<<(const void*);


ostream&

put(char);


ostream&

write(const char*,streamsize);



 

//+——————
 

我们看到里面缺少了对char的操作符,但是put和write可以简单地写出字符,所以就没必要实现一个流操作符(这是C++之父所说,因为标准就是这么定义),可以通过全局函数来实现:
 

 

//+——————
 

ostream&

operator<<(ostream&

os,char ch){
os.put(ch);


return os;


}
 

ostream&

operator<<(ostream&

os,const char* msg){
os.write(msg,strlen(msg));


return os;


}
 

int main(){
char A(‘A’);


cout<<“A = “<<A<<” = “<<int(A)<<endl;


}
 

//+——————–
 

write可以将一段buffer写到指定地方,所以换句话说,write除了写字符串之外还能够将数据按照二进制的放写进文件储存起来。
 

我们再来看另一个细节:
 

//+——————-
 

int main(){
cout<<true<<“t”<<
false<<endl;


}
 

//+——————-
 

运行程序,我们得到的结果是1和0,那么我们可不可以希望他输出的是true和false,当然可以:
 

//+——————-
 

#include <iostream>
#include <iomanip>
 

int main(){
cout<<true<<“t”<<
false<<endl;


cout<<boolalpha;


cout<<true<<“t”<<
false<<endl;


}
 

输出结果:
1 0;


true false;


 

//+——————-
 

boolalpha 在iomanip中定义,当使用他之后所有的bool类型操作将按照字符形式打印。
 

下面这个函数有点特殊:
 

//+——————-
 

ostream&

operator<<(const void*);


 

//+——————-
 

但他却让我们打印指针成了可能,很多时候我们确实很需要打印指针,当我们需要追踪一个对象的时候:
 

//+——————-
 

int main(){
int* ptr = new int(10);


cout<<&

p<<“t”<<p<<endl;


}
 

输出结果:
 

0x7788ff45 0x7895f2ff
 

//+——————-
 

对于内置的类型标准库都为我们实现了流的操作符,那么对于我们自定义的类型,如果我们有需要的话那就需要我们自行定义了,比如:
 

//+——————
 

class MInt{
public:
MInt(int val):mVal(val){ }
MInt(const MInt&

other):mVal(other.mVal){ }
private:

int mVal;


};


 

int main(){
MInt a(10);


cout<<a<<endl;


}
 

//+——————
 

上面的程序没法通过编译,因为cout无法对MInt操作。
cout同样没有对char实现流操作符,但是却提供了一个全局函数来操作char,让char如同其他内置类型一样可以使用流操作,这是一个思路,当然也是规则,重载流操作符的形式必须如下:
 

//+—————–
 

ostream&

operator<<(ostream&

os,const T&

other);


 

//+—————–
 

 

上面的 T 是就是要操作的类型,针对上面的MInt,可以如下:
 

 

//+——————
 

ostream&

operator<<(ostream&

os,const MInt&

other){
os<<other.mVal;


return os;


}
 

//+—————-
 

现在又一个问题来了,mVal是MInt的私有变量,所以是不能直接访问的,但是除此之外又没他发,当然可以给MInt提供一个接口让他返回mVal,不过除此之外还是有其他办法的——友元函数。
 

 

//+—————-
 

class MInt{
public:
MInt(int val):mVal(val){ }
MInt(const MInt&

other):mVal(other.mVal){ }
friend ostream&

operator<<(ostream&

os,const MInt&

other);


private:

int mVal;


};


 

int main(){
MInt a(10);


cout<<a<<endl;


}
 

//+—————
 

现在一切ok,可以看到想象中的结果。
 

和ostream一样,istream同样针对内置类型都实现输入流操作符。
 

//+—————
 

 

istream&

operator>>(short&

);


istream&

operator>>(int&

);


istream&

operator>>(long&

);


istream&

operator>>(long long&

);


 

istream&

operator>>(unsigned short&

);


istream&

operator>>(unsigned int&

);


istream&

operator>>(unsigned long&

);


istream&

operator>>(unsigned long long&

);


 

istream&

operator>>(float&

);


istream&

operator>>(double&

);


istream&

operator>>(long double&

);


 

istream&

operator>>(bool&

);


istream&

operator>>(void*&

);


istream&

get(char);



 

//+——————
 

想要通过cin来初始化对象,我们只需要实现相应的函数即可,而这个函数的样子如下:
 

//+——————
 

template<class T>
istream&

operator>>(T&

val);


 

//+——————
 

但是很多时候我们不能这样写,为什么呢?回顾上面我们说过的ostream,或者简单点总结一下:如果我们想要用一个对象去操作另一个对象,那么该样式如下:
 

//+——————
 

template<class T,class A,class B>
T op(A a B b);


 

//+——————
 

这个函数的意义我们可以简单的理解为A使用op操作B返回T,按照这个思想我们来看看下面的声明表示的意义:
 

//+——————
 

template<class T>
T operator+(const T&

left,const T&

right);

// 表示 T res = left + right;


 

template<class T>
T operator-(const T&

left,const T&

right);

// 表示 T res = left – right;


 

 

template<class T>
T operator*(const T&

left,const T&

right);

// 表示 T res = left*right;


 

template<class T>
T operator/(const T&

left,const T&

right);

// 表示 T res = left/right;


 

….
 

 

//+——————–
 

 

现在我们回头来看看让cin使用>>来操作我们的对象,cin对应的是上面我们的left,而我们自己的对象就是上面对应的right,而返回的对象依然还是cin,所以要实现这个功能我们只需要实现诸如下面类型的函数:
 

 

//+——————-
 

template<class T>
istream&

operator>>(istream&

is,T&

res);


 

//+——————-
 

此处 T 表示我们想要表达的类型,当然都是一些复合类型,但是复合类型是由简单类型组合而成,所以我们在具体实现的时候只要针对复合类型的数据成员进行cin即可,比如:
 

//+——————-
 

class MInt{
public:
MInt(int val = 0):mVal(val){ }
MInt(const MInt&

other):mVal(other.mVal){ }
friend ostream&

operator<<(ostream&

os,const MInt&

other){
os<<other.mVal;



}

friend istream&

operator>>(istream&

is,MInt&

out){
is>>out.mVal;


return is;



}

 

friend MInt operator+(const MInt&

left,const MInt&

right){

return MInt(left.mVal + right.mVal);



}

 

friend MInt operator-(const MInt&

left,const MInt&

right){

return MInt(left.mVal – right.mVal);



}

 

friend MInt operator*(const MInt&

left,const MInt&

right){

return MInt(left.mVal*right.mVal);



}

 

friend MInt operator/(const MInt&

left,const MInt&

right){

return MInt(left.mVal/right.mVal);



}

private:

int mVal;


};


 

int main(){
MInt a;


cin>>a;


cout<<a<<endl;


MInt b = 10;


MInt c = a + b;


MInt d = c*a;


MInt e = d/c;


cout<<b<<endl<<c<<endl<<d<<endl<<e<<endl;


return 0;


}
 

//+——————-
 

 

这一章虽然在说iostream,但是内容却不局限于iostream,和其他的C++书籍比起,我们可能比较超前了些,不过我觉得以这样的方式开局应该是不错的,我属于这样的人,在接触到一门语言的时候首先考虑的是如何快如的建立交互,所以第一章的时候我们边开始思考流操作符重载这些问题,当然这些操作符重载是C++所必须的知识点,但又是一个难点,所以这些内容还是不太适用于从没接触过编程的初学者,因为我们并没有像教科书一般从基本类型开始,关于这一点,曾和同学讨论过,他认为这种难度的东西会不会吓退一部分人,我说不会,我们深入浅出的说,如果实在是对原理不甚清楚,但到后面知道怎么简单使用这也足够了,并且之所以会认为我们的第一章的难度颇大,是因为我们第一章几乎涵盖了C++的大部分知识要点,比如操作符重载,操纵器连杆器这些高级知识等,但这不些仅仅是为了让大家在C++的领域里面不只是浅尝辄止,不仅知其然而且更知其所以然,当然,这些东西后续会细说的,因为上一版很多东西就说的不清不楚,所以这一次会采取教训,当然大家也可以即使反馈。

第131讲 使用鼠标

上一讲我们绘制一个无休止旋转的立方体,但是这一切都不由我们控制,所以现在我们来看看如何使用鼠标控制物体。


关于鼠标,我们有简单的包装,所以我们只需要重写MDx11Base的dx_MouseEvent函数,所有鼠标事件都会转发到该函数中,所以我们只需要重写该数据即可。


//=========================


bool dx_MouseEvent(MDx11::MouseButton button, MDx11::MouseAction action,float x,float y, bool isglobAxis);


//

// 鼠标按键

//

enum MouseButton{

LeftButton = 0,  

//鼠标左键

WheelButton,  

 

 

//鼠标中键

RightButton,  

 

 

//鼠标右键

NoMouseButton  

 

 

//没动作

};


//

// 鼠标动作

//

enum MouseAction{

ButtonDown = 0,  

// 鼠标按下

ButtonMove,  

// 鼠标移动

ButtonUp,  

// 鼠标移动

ButtonDoubleClick, // 鼠标双击

ButtonHover  

 

 

 

// 鼠标悬浮

};


// x y 为坐标


isglobAxis 是否为屏幕坐标,所以我们现在只关心窗口坐标


//========================


好吧,我们打算使用右键控制物体的旋转,我们添加几个成员变量记录当前旋转的角度以及上一刻鼠标的位置:


//========================


float mXAngle{ 0.0
};

float mYAngle{ 0.0
};

float mPreXPos{ mj::InfinityF
};

float  

mPreYPos{ mj::InfinityF
};


//=========================


现在我们来简单的实现这个dx_MouseEvent响应函数:


//=========================


bool FirstDirectWindow::dx_MouseEvent(MDx11::MouseButton button, MDx11::MouseAction action,float x,float y, bool isglobAxis){

if (isglobAxis == false){

if (button == MDx11::RightButton &

&

action == MDx11::ButtonMove){

if (mPreXPos == mj::InfinityF){

mPreXPos = x;

mPreYPos = y;

}

mXAngle += (x – mPreXPos);

mYAngle += (y – mPreYPos);


mPreXPos = x;

mPreYPos = y;

}

else if (button == MDx11::RightButton &

&

action == MDx11::ButtonUp){

mPreXPos = mj::InfinityF;

mPreYPos = mj::InfinityF;

}

}


return true;

}



//

// 使用我们记录的值来更新旋转矩阵

//

XMMATRIX word = XMMatrixRotationY(mXAngle / 180.0*mj::PI);

word *= XMMatrixRotationZ(mYAngle / 180.0*mj::PI);

word *= XMMatrixRotationX(mXAngle / 180.0*mj::PI);


//=============================


到此,我们可以使用鼠标右键自由控制立方体的旋转了。如果我们想要关心键盘的话就实现dx_KeyEvent(wchar_t,bool)即可,该函数和dx_MouseEvent一样是MDx11Base里面预定义的接口。它的原型如下所示:


//=============================


bool dx_KeyEvent(wchar_t ch,bool isDown);


//

// ch 是键的编码

//


//

// isDown true是按下,false为抬起

//


//===============================


第131讲 使用鼠标


//===============================


回复D查看目录,回复数字查看章节

有些同学问有没有博客或者什么的,毕竟微信上面的代码排版不是很好,所以自己就做了一个简单的web服务器,不过功能简陋,就只是打算在上面放点C++的文件,顺便将源码放在上面。如果有兴趣的同学可以访问下面地址:


http://2076e1b.nat123.cc:43007/


因为外网映射使用的是nat123的免费版,所以会发现下载非常的缓慢,而且可能还会连接不上,还有由于服务器就在电脑上,平时上班家里电脑没开是没法访问网站的,不过下班后就能够访问了,O(∩_∩)O,因为我呢没做过前端,对html和css,js这些不熟悉,所以网站其实就是乱七八糟的,推荐使用win10的edge浏览器,因为实在这个浏览器上面测试的,目前发现chrome不能下载。


对于C++的东西微信里面说得比较深了些,很多同学反应太难看不懂,所以在网站上面打算多说基础。


l150039b70.imwork.net:36629/ 这是花生壳映射的地址,同样是免费的,不过好像比上面那个稳定,所以两个地址都可以进行访问。


原文始发于微信公众号(

C/C++的编程教室

):第131讲 使用鼠标

|

第130讲 绘制立方体

继续上一讲的内容,这一讲的东西很简单,就是在上一讲的代码里稍作修改便完成了一个立方体的绘制,效果图如下:

第130讲  绘制立方体

//===========================


//

// 使用基础几何图形创建一个立方体

//

MDx11BasicGeometry gene_geometry;

MDx11BasicGeometry::MeshData __data;

gene_geometry.CreateBox(400, 400,400,__data);

MDx11Window::MDx11MeshPointType datas;

std::vector<unsigned>indexs;

std::vector<BasicPCT>points;

points.resize(__data.vertices.size());

for (int i = 0;

i <__data.vertices.size();

++i){

points[i].Pos = __data.vertices[i].pos;

points[i].Color = XMFLOAT4(gen(), gen(), gen(), 1.0f);

points[i].Tex = __data.vertices[i].tex;

}

indexs.clear();

for (int i = 0;

i <__data.indices.size();

++i){

indexs.push_back(__data.indices[i]);

}


pLayoutManager = new MDx11LayoutManage(dx_GetDevice());

pBufferManager = new MDx11BufferManage(dx_GetDevice());

pLayoutManager->dx_FxComplieFile(“FX/MDx11WindowFX.fx”);

pTechnique  

 

= pLayoutManager->dx_GetTechFromEF(“MDx11WindowTech”);

pLayout  

 

 

 

= pLayoutManager->dx_CreatInputLayout(PCTLayout, ARRAYSIZE(PCTLayout), “MDx11WindowTech”);


pVertexBuffer = pBufferManager->dx_CreateVectexBuffer(&

points[0], points.size());

pIndexBuffer  

= pBufferManager->dx_CreateIndexBuffer(&

indexs[0], indexs.size());

mIndexNum  

 

= indexs.size();


auto it = pBufferManager->dx_CreateTexTureResouceOnly(“Res/IMG_8266.JPG”);

pLayoutManager->dx_GetShaderResourceFromEF(“MWindowTexture”)->SetResource(it);


//=============================


绘制函数不需要任何修改。但是为了更好的体验一下3D效果,我们可以让立方体旋转起来。我们只需要设置一个世界变换矩阵即可。


//=============================


staticfloat angle = 0;

angle += 0.1;

XMMATRIX word = XMMatrixRotationY(angle  

/ 180.0*mj::PI);

word *= XMMatrixRotationZ(angle  

/ 180.0*mj::PI);

word *= XMMatrixRotationX(angle  

/ 180.0*mj::PI);

pLayoutManager->dx_GetMatrixFromEF(“MWorldMatrix”)->SetMatrix(reinterpret_cast<
float*>(&

word));


//==============================


这样一来立方体就会无休止的旋转起来。

第130讲  绘制立方体

其实DirectX一旦入门之后就是自己慢慢摸索了,主要是对立体几何敏感些,对线性代数了解些就没问题啦。接下来我们可以考虑人机交互了,我们用鼠标或者键盘来控制3D物体。


//==========================

回复D查看目录,回复数字查看相应章节


有些同学问有没有博客或者什么的,毕竟微信上面的代码排版不是很好,所以自己就做了一个简单的web服务器,不过功能简陋,就只是打算在上面放点C++的文件,顺便将源码放在上面。如果有兴趣的同学可以访问下面地址:


http://2076e1b.nat123.cc:43007/


因为外网映射使用的是nat123的免费版,所以会发现下载非常的缓慢,而且可能还会下载不下来,还有由于服务器就在电脑上,平时上班家里电脑没开是没法访问网站的,不过下班后就能够访问了,O(∩_∩)O,因为我呢没做过前端,对html和css,js这些不熟悉,所以网站其实就是乱七八糟的,推荐使用win10的edge浏览器,因为实在这个浏览器上面测试的,目前发现chrome不能下载。


对于C++的东西微信里面说得比较深了些,很多同学反应太难看不懂,所以在网站上面打算多说基础。


原文始发于微信公众号(

C/C++的编程教室

):第130讲 绘制立方体

|

第129讲 纹理贴图

第129讲  纹理贴图

上一讲我们说了怎么绘制基本的图形,而且我们已经简单的绘制出一个正方形了,嗯,别纠结了,不管绘制什么都是一样的,将你想绘制的模型分解为多个三角形即可,在上一讲里面我们使用了顶点buffer绘制,顶点buffer的好处是简单,但是带来的问题是内存的开销,所以DirectX提供了更方便的绘制,使用索引buffer。所谓索引就是顶点的所以,简单点说,我们在上一讲里面为了绘制正方向使用了6个顶点,但事实上有两个是重复,当然模型越复杂重复的顶点会越多,而顶点的开销又是自定义的,所以重复的顶点越多需要开销的内存就越大,当然这不关键,关键是还不好控制,所以我们这里引出索引buffer,好了,现在我们可以直接定义出正方形的四个顶点:


//—————————


BasicPCT  

points[] ={

{ -400, 400, 0
},{ 1.f, 1.f, 1.f, 1.f
},{ 0.f, 0.f
}
},

{ 400, 400, 0
},{ 1.f, 1.f, 1.f, 1.f
},{ 1.f, 0.f
}
},

{ -400, -400, 0
},{ 1.f, 1.f, 1.f, 1.f
},{ 0.f, 1.f

} ,

{ 400, -400, 0
},{ 1.f, 1.f, 1.f, 1.f
},{ 1.f, 1.f
}
}

};


//————————–


这和上一讲的是一样的,不过是不是发现有点小小的不一样了呢?当然不是说现在只有四个顶点而上一讲有6个顶点这事,我们看到每个顶点的第三段数据和上一讲的有稍微的不同,这是纹理坐标,上一讲我们曾说过,纹理坐标的预留着,所以在上一讲的顶点数据里并没有关心纹理坐标,而全都设置为0,这里我们为了不让颜色影响到我们的贴图,所以我们把顶点颜色全部设置白色(rgb都为1表示白色,rgb为0表示黑色)


纹理坐标的数值在0到1之间,对于水平方向来说,0是最左端,1是最右端,中间值采用线性插值方式,对于水平方向来说,0是最顶端,1是最下端。结合想象力是不是很好理解上面这段定点数据了呢?


好吧,现在我们已经把四边形的顶点给定义出来了,同样纹理坐标也描述好了,现在就差一个索引buffer和一张图片,嗯,DirectX支持很多种格式的图片,所以可以随意的找一张图来作为我们的贴图。


索引是从0开始的,索然当然就是顶点buffer的索引了,所以要绘制一个四边形就是绘制两个三角形,那么前面三个顶点可以绘制左上角的三角形,这个三角行所用到的顶点索引为0,1,2,而右下半三角行就是由2,1,3这个三个顶点组成,所以索引buffer可以为:


//———————–

unsigned indexs[] ={

0,1,2,

2,1,3

};


//

// 这三个对象作为类成员声明

//

ID3D11Buffer* pVertexBuffer{ nullptr
};

ID3D11Buffer* pIndexBuffer{ nullptr
};

unsigned  

 

 

 

mIndexNum;



pVertexBuffer = pBufferManager->dx_CreateVectexBuffer(points, ARRAYSIZE(points));

pIndexBuffer = pBufferManager->dx_CreateIndexBuffer(indexs, ARRAYSIZE(indexs));

mIndexNum = ARRAYSIZE(indexs);


//

// 在dx_LoadContent函数我们顺便创建出纹理

// 我们使用的纹理贴图为:IMG_8266.JPG

// 我们创建出来后就设置到Effect里面后面就不需要关心他

// 以后的事都在着色器里面完成

//

auto it = pBufferManager->dx_CreateTexTureResouceOnly(“Res/IMG_8266.JPG”);

pLayoutManager->dx_GetShaderResourceFromEF(“MWindowTexture”)->SetResource(it);


//————————–


一切准备妥当之后就进入绘制阶段了,绘制很简单,现在我们多添加了一个索引buffer,索引将该buffer在IA阶段进行设置好,然后我们不再使用Draw函数绘制,而是选用DrawIndexed进行绘制。


//————————-


//

// 设置索引buffer

//

dx_GetContext()->IASetIndexBuffer(pIndexBuffer, DXGI_FORMAT_R32_UINT, 0);


//

// 绘制索引buffer

//

dx_GetContext()->DrawIndexed(mIndexNum,0, 0);


//

// 对FX文件的像素着色器进行修改如下

//

float4 PSMain(PSSceneIn input) : SV_Target

{

float4 colour = MWindowTexture.Sample(WindowSampler, input.tex);


return input.colour*colour;

}


//————————




完整代码如下:


//———————-

class FirstDirectWindow : public MDx11Base{

public:

FirstDirectWindow(){

}


~FirstDirectWindow(){

safe_delete(pBufferManager);

safe_delete(pLayoutManager);

}

bool dx_LoadContent(){ 

try{

std::vector<unsigned>indexs;

std::vector<BasicPCT>points;


points.push_back({{ -400.5, 300.5, 0
},{ 1.f, 1.f, 1.f, 1.f
},{ 0.f, 0.f
}
});

points.push_back({{ 400.5, 300.5, 0
},{ 1.f, 1.f, 1.f, 1.f
},{ 1.f, 0.f
}
});

points.push_back({{ -400.5, -300.5, 0
},{ 1.f, 1.f, 1.f, 1.f
},{ 0.f, 1.f
}
});

points.push_back({{ 400.5, -300.5, 0
},{ 1.f, 1.f, 1.f, 1.f
},{ 1.f, 1.f
}
});


indexs.push_back(2);

indexs.push_back(0);

indexs.push_back(1);


indexs.push_back(2);

indexs.push_back(1);

indexs.push_back(3);

pLayoutManager = new MDx11LayoutManage(dx_GetDevice());

pBufferManager = new MDx11BufferManage(dx_GetDevice());

pLayoutManager->dx_FxComplieFile(“FX/MDx11WindowFX.fx”);

pTechnique = pLayoutManager->dx_GetTechFromEF(“MDx11WindowTech”);

pLayout = pLayoutManager->dx_CreatInputLayout(PCTLayout, ARRAYSIZE(PCTLayout), “MDx11WindowTech”);

//

// 由BufferManager创建的buffer除非特殊指出否则Manager会对buffer进行回收

//

pVertexBuffer = pBufferManager->dx_CreateVectexBuffer(&

points[0], points.size());

pIndexBuffer = pBufferManager->dx_CreateIndexBuffer(&

indexs[0], indexs.size());

mIndexNum = indexs.size();


auto it = pBufferManager->dx_CreateTexTureResouceOnly(“Res/IMG_8266.JPG”);

pLayoutManager->dx_GetShaderResourceFromEF(“MWindowTexture”)->SetResource(it);


return true;

}

catch (std::runtime_error e){

box::ErrorBox(e.what());


return false;

}

}


void dx_Render();


private:

MDx11BufferManage* pBufferManager{ nullptr
};

MDx11LayoutManage* pLayoutManager{ nullptr
};

LPD3D11EFFECTTECHNIQUE pTechnique{ nullptr
};

ID3D11Buffer* pVertexBuffer{ nullptr
};

ID3D11Buffer* pIndexBuffer{ nullptr
};

unsigned mIndexNum{ 0
};

ID3D11InputLayout*  

 

pLayout{ nullptr
};

};


void FirstDirectWindow::dx_Render(){

if (!dx_GetDevice() || !dx_GetContext()){

return;

}

float clearColor[4] ={ 1.f, 1.f, 1.f, 1.0f
};

dx_GetContext()->ClearRenderTargetView(p_backBufferTarget, clearColor);

dx_GetContext()->ClearDepthStencilView(p_depthStencilView, D3D11_CLEAR_DEPTH, 1.0f, 0.0f);

size_t strit = sizeof(BasicPCT);

size_t offset = 0;

dx_GetContext()->IASetVertexBuffers(0, 1, &

pVertexBuffer, &

strit, &

offset);

dx_GetContext()->IASetIndexBuffer(pIndexBuffer, DXGI_FORMAT_R32_UINT, 0);

dx_GetContext()->IASetInputLayout(pLayout);

dx_GetContext()->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

//D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST D3D11_PRIMITIVE_TOPOLOGY_POINTLIST

pLayoutManager->dx_GetMatrixFromEF(“MWorldMatrix”)->SetMatrix(reinterpret_cast<
float*>(&

word));

XMMATRIX vp = mCamera.dx_GetModeViewMatrix() *mCamera.dx_GetProjectMatrix();

pLayoutManager->dx_GetMatrixFromEF(“MVPMatrix”)->SetMatrix(reinterpret_cast<
float*>(&

vp));

word *= vp;

pLayoutManager->dx_GetMatrixFromEF(“MWVPMatrix”)->SetMatrix(reinterpret_cast<
float*>(&

word));

D3DX11_TECHNIQUE_DESC techdesc = pLayoutManager->dx_GetTichDescFromEF(“MDx11WindowTech”);


for (int i = 0;

i <techdesc.Passes;

++i){

pTechnique->GetPassByIndex(i)->Apply(i, dx_GetContext());

dx_GetContext()->DrawIndexed(mIndexNum,0, 0);

}

dx_GetSwapChain()->Present(0, 0);

}



int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE prevInstance, LPWSTR cmdLine,
int cmdShow)

{

INITCOM;

MWindow w;

FirstDirectWindow dw;

if (!dw.dx_Init(&

w,false,false)){


return 0;

}

w.Show();


return w.Run();

}


//—————————

第129讲  纹理贴图


贴图就到这里,我们主要使用着色器编程,所以大家可以了解一下DirectX11的着色器编程,对理解我们所说的内容很有帮助,比如说我们可以尝试修改采样器的设置看看其他效果是什么:


//—————————


//

// 修改下面的描述可以看看另外的效果

//

SamplerState WindowSampler

{

Filter = MIN_MAG_MIP_LINEAR;

AddressU = Border;

AddressV = Border;

};


//————————–



//==========================

回复D查看目录,回复数字查看相应章节


原文始发于微信公众号(

C/C++的编程教室

):第129讲 纹理贴图

|

第128讲 MDx11Frame(8)


上一讲我们说了DirectX11的初始化,初始化很简单,因为是固定的步骤和方法,简单的理一下:


1,使用D3D11CreateDevice接口创建ID3D11Device和ID3D11DeviceContext。

2,使用ID3D11Device::CheckMultisampleQualityLevels检查是否支持多重采样。

3,填充DXGI_SWAP_CHAIN_DESC结构体创建交换链。

4,IDXGISwapChain的创建需要几个步骤,相对来说这是最讨厌的一个了,下面大概说一下创建过程:


DXGI_SWAP_CHAIN_DESC swapChainDesc ={ 0
};

swapChainDesc.BufferDesc.Width = width;

//窗口的宽度

swapChainDesc.BufferDesc.Height = height;

//窗口高度

swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;

// 刷新率 分子

swapChainDesc.BufferDesc.RefreshRate.Numerator = 60;

// 分母

swapChainDesc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;

//用于秒速缩放枚举,想要了解更多查看MSDN

swapChainDesc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;

//用于描述扫描线绘制模式的枚举,同上

swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;

//用于描述显示格式的枚举类型,同上

swapChainDesc.BufferCount = 1;

//后缓冲区个数,1个足够

swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;

//Usage,这些具体的查看MSDN就好

swapChainDesc.Flags = 0;

// 描述交换链行为,默认为0既可

swapChainDesc.OutputWindow = m_hwnd;

//主窗口句柄,指定显示的窗口


这个结构体这么填充即可,如果想要了解得更多,可以查看MSDN。


在填充好这个结构体后我们接下来看看怎么创建交换链,这个过程不是有参考的书籍我也不知道怎么创建,或者要慢慢的查阅MSDN了。

为了创建交换链,我们需要获取IDXGIFactory工厂接口,但是没有一个方法可以直接获取该接口,所以我们需要从ID3D11Device着手,COM组件就是这样,很多东西没有直接暴露,但是我们可以通过一个现有的查询未知的,所以我们现在拥有ID3D11Device接口,于是可以查询IDXGIDevice接口。

IDXGIDevice *dxgiDevice(NULL);

p_d3dDevice->QueryInterface(__uuidof(IDXGIDevice), reinterpret_cast<void**>(&

dxgiDevice));

IDXGIDevice 可以获取IDXGIAdapter接口,IDXGIAdapter接口最终可以获取我们的工厂接口IDXGIFactory。

IDXGIAdapter *dxgiAdapter(NULL);

dxgiDevice->GetParent(__uuidof(IDXGIAdapter), reinterpret_cast<void**>(&

dxgiAdapter));

IDXGIFactory *dxgiFactory(NULL);

dxgiAdapter->GetParent(__uuidof(IDXGIFactory), reinterpret_cast<void**>(&

dxgiFactory));

result = dxgiFactory->CreateSwapChain(p_d3dDevice, &

swapChainDesc, &

p_swapChain);


最后终于创建出我们想要的交换链。


5,有了交换链后可以创建渲染目标视图:


ID3D11Texture2D* backBufferTexture;


result = p_swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&

backBufferTexture);

result = p_d3dDevice->CreateRenderTargetView(backBufferTexture, 0, &

p_backBufferTarget);



6, 创建深度模板buffer

7,将深度模板buffer和渲染目标视图绑定到渲染管道。

8,设置渲染窗口


ok,整个DirectX11的初始化就这样,好吧,我不善于说,也不善于写,不过还是希望能够对家有点帮助。


上一讲我们将DirectX的第一个程序给跑起来了,不过上面没啥东西,这一讲我们准备在上面花点什么东西,那就从最简单的开始,画一个正方行。


DirectX 对图元的绘制主要是按三角形来绘制,好吧,我的表达感觉怎么这么怪呢,当然不是只能画三角形,只是常规都是画三角形,因为三角形效率高啊,嗯,这可能不是很有力的说服力,它可以绘制各种图元,这里不展开说,为什么呢,因为等大家熟悉了之后这些东西也就全都明白了,这里我们还是说三角形吧。


我们来看看这么绘制三角形,三角形当然要有三个点,所以如果要绘制一个四边形那么我们就要绘制两个三角形,那么就是6个点,比如说四边形的四个顶点为ABCD,那么我们绘制的时候就要准备两组三角形的数据,分别为什么ABC和CBD。每一个顶点要有属于的自己的坐标,当然这时最基本的,我们可以还有色彩,在DirectX里面色彩通常用四个浮点数表示,分别为rgba,同样我们可以纹理坐标,纹理为什么先预留,后面再说。


在DirectX里面通常用XMFLOAT3类型表示顶点,他有xyz三个分量,用XMFLOAT4表示颜色,有xyzw分量,用XMFLOAT2表示二维纹理坐标,有xy分量。ok,知道这些后我们可以简单的定义出四边形的顶点类型:


//——————-


statuc VertexType{

XMFLOAT3 Pos;

XMFLOAT4 Colour;

XMFLOAT2 Tex;

};


D3D11_INPUT_ELEMENT_DESC PCTLayout[3] =

{

 

 

{ “POSITION”, 0, DXGI_FORMAT_R32G32B32_FLOAT,

 

 

 

0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0
},

 

 

{ “COLOR”, 0, DXGI_FORMAT_R32G32B32A32_FLOAT,

 

 

 

0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0
},

 

 

{ “TEXCOORD”, 0, DXGI_FORMAT_R32G32_FLOAT,

 

 

 

0, 28, D3D11_INPUT_PER_VERTEX_DATA, 0
},

};



//——————


D3D11_INPUT_ELEMENT_DESC 关于这个结构体 ,怎么说呢,要写说的话得不少篇幅,好吧,但又好像没多大用,自己百度一下就好了,那就这么愉快的决定了,我们继续向前。我们接下来创建一个布局和顶点buffer


//——————–


pLayoutManager = new MDx11LayoutManage(dx_GetDevice());

pBufferManager = new MDx11BufferManage(dx_GetDevice());


pLayoutManager->dx_FxComplieFile(“FX/MDx11WindowFX.fx”);

pTechnique = pLayoutManager->dx_GetTechFromEF(“MDx11WindowTech”);

pLayout = pLayoutManager->dx_CreatInputLayout(PCTLayout, ARRAYSIZE(PCTLayout), “MDx11WindowTech”);


VertexType Points[] ={

{{ -100.5, 100.5, 1
},{ 1.f, 0.f, 0.f, 1.f
},{ 0.f, 0.f
}
},

{{ 100.5, 100.5, 1
},{ 0.f, 1.f, 0.f, 1.f
},{ 0.f, 0.f
}
},

{{ -100.5, -100.5, 1
},{ 0.f, 0.f, 1.f, 1.f
},{ 0.f, 0.f
}
},


{{ -100.5, -100.5, 1
},{ 0.f, 0.f, 1.f, 1.f
},{ 0.f, 0.f
}
},

{{ 100.5, 100.5, 1
},{ 0.f, 1.f, 0.f, 1.f
},{ 0.f, 0.f
}
},

{{ 100.5, -100.5, 1
},{ 1.f, 0.f, 0.f, 1.f
},{ 0.f, 0.f
}
}

};


pVertexBuffer = pBufferManager->dx_CreateVectexBuffer(Points, ARRAYSIZE(Points));


//————————

MDx11WindowFX.fx 文件的内容待会贴出。ok,现在我们可以绘制了


//————————

class FirstDirectWindow : public MDx11Base{

public:

FirstDirectWindow(){

}


~FirstDirectWindow(){ }

bool dx_LoadContent(){

try{

std::vector<unsigned>indexs;

std::vector<BasicPCT>points;


points.push_back({{ -100.5, 100.5, 1
},{ 1.f, 0.f, 0.f, 1.f
},{ 0.f, 0.f
}
});

points.push_back({{ 100.5, 100.5, 1
},{ 0.f, 1.f, 0.f, 1.f
},{ 0.f, 0.f
}
});

points.push_back({{ -100.5, -100.5, 1
},{ 0.f, 0.f, 1.f, 1.f
},{ 0.f, 0.f
}
});


points.push_back({{ -100.5, -100.5, 1
},{ 0.f, 0.f, 1.f, 1.f
},{ 0.f, 0.f
}
});

points.push_back({{ 100.5, 100.5, 1
},{ 0.f, 1.f, 0.f, 1.f
},{ 0.f, 0.f
}
});

points.push_back({{ 100.5, -100.5, 1
},{ 1.f, 0.f, 0.f, 1.f
},{ 0.f, 0.f
}
});

pLayoutManager = new MDx11LayoutManage(dx_GetDevice());

pBufferManager = new MDx11BufferManage(dx_GetDevice());

pLayoutManager->dx_FxComplieFile(“FX/MDx11WindowFX.fx”);

pTechnique = pLayoutManager->dx_GetTechFromEF(“MDx11WindowTech”);

pLayout = pLayoutManager->dx_CreatInputLayout(PCTLayout, ARRAYSIZE(PCTLayout), “MDx11WindowTech”);


pVertexBuffer = pBufferManager->dx_CreateVectexBuffer(&

points[0], points.size());


return true;

}

catch (std::runtime_error e){

box::ErrorBox(e.what());


return false;

}

}


void dx_Render();


private:

MDx11BufferManage* pBufferManager{ nullptr
};

MDx11LayoutManage* pLayoutManager{ nullptr
};

LPD3D11EFFECTTECHNIQUE pTechnique{ nullptr
};

ID3D11Buffer*  

pVertexBuffer{ nullptr
};

;

ID3D11InputLayout*  

 

pLayout{ nullptr
};


};


void FirstDirectWindow::dx_Render(){

if (!dx_GetDevice() || !dx_GetContext()){

return;

}

float clearColor[4] ={ 0.f, 0.f, 0.25f, 1.0f
};

dx_GetContext()->ClearRenderTargetView(p_backBufferTarget, clearColor);

dx_GetContext()->ClearDepthStencilView(p_depthStencilView, D3D11_CLEAR_DEPTH, 1.0f, 0.0f);

size_t strit = sizeof(BasicPCT);

size_t offset = 0;

staticfloat walk = -0.01;

mCamera.dx_Walk(walk);

mCamera.dx_UpDateMatrix();

staticfloat angle = 0;

angle += 0.1;

dx_GetContext()->IASetVertexBuffers(0, 1, &

pVertexBuffer, &

strit, &

offset);

dx_GetContext()->IASetIndexBuffer(pIndexBuffer, DXGI_FORMAT_R32_UINT, 0);

dx_GetContext()->IASetInputLayout(pLayout);

dx_GetContext()->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);


XMMATRIX word = XMMatrixIdentity();

word = word*mCamera.dx_GetModeViewMatrix() *mCamera.dx_GetProjectMatrix();

pLayoutManager->dx_GetMatrixFromEF(“MWorldMatrix”)->SetMatrix(reinterpret_cast<
float*>(&

word));


D3DX11_TECHNIQUE_DESC techdesc = pLayoutManager->dx_GetTichDescFromEF(“MDx11WindowTech”);


for (int i = 0;

i <techdesc.Passes;

++i){

pTechnique->GetPassByIndex(i)->Apply(i, dx_GetContext());

dx_GetContext()->Draw(6, 0);

}

dx_GetSwapChain()->Present(0, 0);

}



下面是Fx文件


//==================//


matrix  

MWorldMatrix;


struct VSSceneIn

{

float3 pos : POSITION;

float4 colour : COLOR;

 

 

float2 tex : TEXCOORD;

};


struct PSSceneIn

{

float4 pos : SV_Position;

float4 colour : COLOR;

float2 tex : TEXCOORD;

};



DepthStencilState DisableDepth

{

DepthEnable = FALSE;

DepthWriteMask = ZERO;

};


DepthStencilState LessEqualDSS

{

DepthEnable = TRUE;

 

DepthFunc = LESS_EQUAL;

 

};

 



SamplerState WindowSampler

{

Filter = MIN_MAG_MIP_LINEAR;

AddressU = Border;

AddressV = Border;

};


RasterizerState Rasterstate

{

DepthClipEnable = false;

FillMode = Solid;

CullMode = Back;

FrontCounterClockwise = false;

ScissorEnable = false;

};



RasterizerState RasterstateForObj

{

DepthClipEnable = false;

FillMode = Solid;

CullMode = Back;

FrontCounterClockwise = true;

ScissorEnable = false;

};


// Vertex shader

PSSceneIn VSMain(VSSceneIn input)

{

PSSceneIn output = (PSSceneIn)0.0;

output.pos = mul(float4(input.pos,1),MWorldMatrix);

output.tex = input.tex;

output.colour = input.colour;


return output;

}



float4 PSMain(PSSceneIn input) : SV_Target

{


return input.colour;

float4 colour = MWindowTexture.Sample(WindowSampler, input.tex);


return input.colour*colour;

}



// Techniques

technique11 MDx11WindowTech

{

pass P0

{

SetVertexShader(CompileShader(vs_5_0, VSMain()));

 

SetGeometryShader(NULL);

 

SetPixelShader(CompileShader(ps_5_0, PSMain()));

 

SetDepthStencilState(LessEqualDSS, 0);

 

SetRasterizerState(Rasterstate);

 

}

}

//=================//


//———————–

下面我们来执行程序看看。

//========================

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE prevInstance, LPWSTR cmdLine,
int cmdShow)

{

INITCOM;

MWindow w;

TestDirctWindow d3dwindow;

if (!d3dwindow.dx_Init(&

w)){


return 0;

}

w.Show();


return w.Run();

}


//========================

第128讲 MDx11Frame(8)

好吧,我们算是接触了DirectX的东西,接下来就是大家慢慢摸索的事啦,哈哈。


///========================

回复D查看目录,回复数字查看相应章节


原文始发于微信公众号(

C/C++的编程教室

):第128讲 MDx11Frame(8)

|