什么是d-pointer

什么是d-pointer

如果你曾经查看过Qt的源代码文件,例如 这个 [qt.gitorious.com],你会发现里面有很多的 Q_D 和 Q_Q 宏定义。本文就来揭开这些宏使用的目的。

Q_D 和 Q_Q 宏定义是d-pointer((也被称为 opaque pointer [en.wikipedia.org]))设计模式的一部分, 它可以把一个类库的实施细节对使用的用户隐藏, 而且对实施的更改不会打破二进制兼容。

什么是二进制兼容

在设计像 Qt 这样的类库的时候,理想的行为应该是动态连接到 Qt 的应用程序,甚至在 Qt 类库升级或者替换到另外一个版本的时候,不需要重新编译就可以继续运行。例如,你的应用程序 CuteApp 是基于 Qt 4.5 的,你应该可以把你的 Qt 由4.5升级到 Qt 4.6 (在 Windows 下用安装程序,在 Linux 下通常有包管理器自动完成),而你用 Qt 4.5 构建的 CuteApp 应该还可以运行。

什么会打破二进制兼容

那么,什么时候类库的变化需要应用程序的重新编译呢? 我们来看一个简单的例子:

  1.      class Widget  {
  2.            ...
  3.      private :
  4.         Rect m_geometry ;
  5.      } ;
  6.  
  7.      class Label  :  public Widget  {
  8.             ...
  9.          String text ( )  const  {  return m_text ;  }
  10.      private :
  11.         String m_text ;
  12.      } ;

这里,我们有一个 Widget, 包含一个 geometry 作为成员变量。 我们编译 Widget 并且发布为  WidgetLib 1.0  。

对于 WidgetLib 1.1 , 有人有了新的主意要添加样式表支持。没问题,我们只需要添加一个新的方法和一个新的 数据成员 。

  1.      class Widget  {
  2.          ...
  3.      private :
  4.          Rect m_geometry ;
  5.          String m_stylesheet ;  // NEW in WidgetLib 1.1
  6.      } ;
  7.  
  8.      class Label  :  public Widget  {
  9.      public :
  10.         ...
  11.          String text ( )  const  {  return m_text ;  }
  12.      private :
  13.         String m_text ;
  14.      } ;

我们发布 WidgetLib 1.1,仅仅包含上面的变化,却发现和 WidgetLib 1.0一起编译并且运行正常的 CuteApp 光荣地崩溃了!

为什么会崩溃

究其原因,通过添加了一个新的数据成员,我们最终改变了 Widget 和 Label 对象的大小。为什么会这样?因为当你的C++编译器生成代码的时候,他会用`偏移量`来访问对象的数据。

下面是一个 POD 对象在内存里面布局的一个简化版本。

Label 对象在 WidgetLib 1.0的布局 Label 对象在 WidgetLib 1.1的布局
m_geometry <偏移量 0> m_geometry <偏移量 0>
———————— m_stylesheet <偏移量 1>
m_text <偏移量 1> ————————  
———————— m_text <偏移量 2>

在 WidgetLib 1.0中,Label 的 text 成员在(逻辑)偏移量为1的位置。在编译器生成的代码里,应用程序的方法 Label::text() 被翻译成访问 Label 对象里面偏移量为1的位置。
在 WidgetLib 1.1中,Label 的 text 成员的(逻辑)偏移量被转移到了2的位置!由于应用程序没有重新编译,它仍然认为 text在偏移量1的位置,结果却访问了 stylesheet 变量!

我确信,这个时候,会有人问,为什么Label::text()的偏移量的计算的代码会在CuteApp二进制文件结束,而不是在WidgetLib的二进制文件。 答案是因为Label::text() 的代码定义在头文件里面,最终被 内联 [en.wikipedia.org]

那么,如果 Label::text() 没有定义为内联函数,情况会改变吗?这么讲,Label::text() 被移到源文件里面?嗯,不会。C++编译器依赖对象大小在编译时和运行时相同。比如,堆栈的 winding/unwinding – 如果你在堆栈上创建了一个 Label 对象, 编译器产生的代码会根据 Label 对象在编译时的大小在堆栈上分配空间。由于Label的大小在 WidgetLib 1.1 运行时已经不同,Label 的构造函数会覆盖已经存在的堆栈数据,最终破坏堆栈。

不要改变导出的 C++ 类的大小

总之,一旦你的类库发布了,永远不要改变 导出的 C++ 类的大小或者布局(不要移动成员)。C++ 编译器生成的代码会假定,一个类的大小和成员的顺序 编译后 就不会改变.

那么,如何在不改变对象的大小的同时添加新的功能呢?

d-pointer

诀窍是通过保存唯一的一个指针而保持一个类库所有公共类的大小不变。这个指针指向一个包含所有数据的私有的(内部的)数据结构。内部结构的大小可以增大或者减小,而不会对应用程序带来副作用,因为指针只会被类库里面的代码访问,从应用程序的视角来看,对象的大小并没有改变 – 它永远是指针的大小。 这个指针被叫做 d-pointer 。

这个模式的精髓可以由下面的代码来概述(本文中的所有代码都没有析构函数,在实际使用的时候应该加上它)。

  1.      /* widget.h */
  2.      // 前置声明. 定义在 widget.cpp 或者
  3.      // 单独的一个文件,比如 widget_p.h
  4.      class WidgetPrivate ;
  5.  
  6.      class Widget  {
  7.        ...
  8.         Rect geometry ( ) const ;
  9.        ...
  10.      private :
  11.         // d-pointer never referenced in header file.
  12.         // Since WidgetPrivate is not defined in this header,
  13.         // any access will be a compile error
  14.        WidgetPrivate  *d_ptr ;
  15.      } ;
  16.  
  17.      /* widget_p.h */  (_p 意味着私有 )
  18.      struct WidgetPrivate  {
  19.         Rect geometry ;
  20.         String stylesheet ;
  21.      } ;
  22.  
  23.      /* widget.cpp */
  24.      #include "widget_p.h"
  25.     Widget :: Widget ( )
  26.          : d_ptr ( new WidgetPrivate )  {
  27.          // 创建私有数据
  28.      }
  29.  
  30.     Rect Widget :: geoemtry ( )  const  {
  31.          //  d-ptr 仅仅被类库代码访问
  32.          return d_ptr -> geometry ;
  33.      }
  34.  
  35.      /* label.h */
  36.      class Label  :  public Widget  {
  37.        ...
  38.         String text ( ) ;
  39.      private :
  40.         // 每个类维护自己的 d-pointer
  41.        LabelPrivate  *d_ptr ;
  42.      } ;
  43.  
  44.      /* label.cpp */
  45.      // 和 WidgetPrivate 不同, 我们在它自己的源文件里定义 LabelPrivate
  46.      struct LabelPrivate  {
  47.         String text ;
  48.      } ;
  49.    
  50.     Label :: Label ( )
  51.          : d_ptr ( new LabelPrivate )  {
  52.      }
  53.  
  54.     String Label :: text ( )  {
  55.          return d_ptr -> text ;
  56.      }

有了上面的机构,CuteApp 从来不需要直接访问 d-pointer。由于 d-pointer 只是在 WidgetLib 被访问,而 WidgetLib 在每一次发布都被重新编译,私有的类可以随意的改变而不会对 CuteApp 带来影响。

d-pointer 的其它好处

这里不全都是和二进制兼容有关。d-pointer 还有其它的好处:

  • 隐藏了实现细节 – 我们可以只发布带有头文件和二进制文件的 WidgetLib。源文件可以是闭源代码的。
  • 头文件很干净,不包含实现细节,可以直接作为 API 参考。
  • 由于实施需要的包含的头文件从头文件里已到了实施(源文件)里面,编译速更快。(译:降低了编译依赖)

事实上,上边的好处是微乎其微的。Qt 使用 d-pointer 的真正原因是为了二进制兼容和 Qt 最初是封闭源代码的.(译:Qt 好像没有封闭源代码)

q-pointer

到目前为止,我们仅仅看到的是作为 C 风格的数据机构的 d-pointer。实际上,它可以包含私有的方法(辅助函数)。例如,LabelPrivate 可以有一个getLinkTargetFromPoint() 辅助函数,当鼠标点击的时候找到目标链接。在很多情况下,这些辅助函数需要访问公有类,也就是 Label 或者它的父类 Widget 的一些函数。比如,一个辅助函数 setTextAndUpdateWidget() 想要调用一个安排重画Widget的公有方法 Widget::update()。所以,WidgetPrivate 存储了一个指向公有类的指针,称为q-pointer。修改上边的代码引入q-pointer,我们得到下面代码:

  1.      /* widget.h */
  2.      // 前置声明. 定义在 widget.cpp
  3.      // 或者单独的一个文件,比如 widget_p.h
  4.      class WidgetPrivate ;
  5.  
  6.      class Widget  {
  7.        ...
  8.         Rect geometry ( ) const ;
  9.        ...
  10.      private :
  11.         // d-pointer 从来不在头文件被引用
  12.         // 因为WidgetPrivate没有在头文件里定义,
  13.         // 所有对它的访问都会带来编译错误
  14.        WidgetPrivate  *d_ptr ;
  15.      } ;
  16.  
  17.      /* widget_p.h */  (_p意味着私有 )
  18.      struct WidgetPrivate  {
  19.          // constructor that initializes the q-ptr
  20.         WidgetPrivate (Widget  *q )  : q_ptr (q )  {  }
  21.         Widget  *q_ptr ;  // q-ptr that points to the API class
  22.         Rect geometry ;
  23.         String stylesheet ;
  24.      } ;
  25.  
  26.      /* widget.cpp */
  27.      #include "widget_p.h"
  28.      // create private data. pass the 'this' pointer to initialize the q-ptr
  29.     Widget :: Widget ( )
  30.          : d_ptr ( new WidgetPrivate ( this ) )  {
  31.      }
  32.  
  33.     Rect Widget :: geoemtry ( )  const  {
  34.          // the d-ptr is only accessed in the library code
  35.          return d_ptr -> geometry ;
  36.      }
  37.  
  38.      /* label.h */
  39.      class Label  :  public Widget  {
  40.        ...
  41.         String text ( ) const ;
  42.      private :
  43.        LabelPrivate  *d_ptr ;  // each class maitains it's own d-pointer
  44.      } ;
  45.  
  46.      /* label.cpp */
  47.      // Unlike WidgetPrivate, we define LabelPrivate in the source file
  48.      struct LabelPrivate  {
  49.         LabelPrivate (Label  *q )  : q_ptr (q )  {  }
  50.         Label  *q_ptr ;
  51.         String text ;
  52.      } ;
  53.    
  54.     Label :: Label ( )
  55.          : d_ptr ( new LabelPrivate ( this ) )  {
  56.      }
  57.  
  58.     String Label :: text ( )  {
  59.          return d_ptr -> text ;
  60.      }

进一步优化

对于上边的代码,创建一个 Label 会带来 LabelPrivate 和 WidgetPrivate 的内存分配。如果我们在Qt里面采用这种策略,对已一些像 QListWidget 的类,情况会相当糟糕 – 它有6层的继承层次,也就会带来最多6次的内存分配。

通过对我们的 私有 类添加一个继承层次,解决了这个问题,这样类实例化时将一个 d-pointer 层层向上传递。

  1.     /* widget.h */
  2.     class Widget  {
  3.     public :
  4.       Widget ( ) ;
  5.        ...
  6.     protected :
  7.        // only sublasses may access the below
  8.       Widget (WidgetPrivate  &d ) ;  // 允许子类通过他们自己的实体私有对象来初始化
  9.       WidgetPrivate  *d_ptr ;
  10.     } ;
  11.  
  12.      /* widget_p.h */  (_p means  private )
  13.      struct WidgetPrivate  {
  14.         WidgetPrivate (Widget  *q )  : q_ptr (q )  {  }  // 构造函数初始化 q-ptr
  15.         Widget  *q_ptr ;  // 指向API类的
  16.         Rect geometry ;
  17.         String stylesheet ;
  18.      } ;
  19.  
  20.     /* widget.cpp */
  21.    Widget :: Widget ( )
  22.       : d_ptr ( new WidgetPrivate ( this ) )  {
  23.     }
  24.    
  25.    Widget :: Widget (WidgetPrivate  &d )
  26.       : d_ptr ( &d )  {
  27.     }
  28.  
  29.     /* label.h */
  30.     class Label  :  public Widget  {
  31.     public :
  32.       Label ( ) ;
  33.        ...
  34.     protected :
  35.       Label (LabelPrivate  &d ) ;  // 允许Label的子类传递自己的私有数据
  36.        //注意 Label 没有 d_ptr!它用了父类 Widget 的 d_ptr。
  37.     } ;
  38.  
  39.     /* label.cpp */
  40.     #include "widget_p.h" // 所以我们能够访问 WidgetPrivate
  41.  
  42.     class LabelPrivate  :  public WidgetPrivate  {
  43.     public :
  44.        String text ;
  45.     } ;
  46.  
  47.    Label :: Label ( )
  48.        : Widget ( * new LabelPrivate )  {
  49.        // 用我们自己的私有对象来初始化 d-pointer
  50.     }
  51.    
  52.    Label :: Label (LabelPrivate  &d )
  53.        : Widget (d )  {
  54.     }

是不是很漂亮?现在当我们创建一个 Label 对象时,它会创建一个 LabelPrivate(它继承了 WidgetPrivate)。它把一个 d-pointer 实体传递给Widget的保护的构造函数。Label 也有这样一个保护的构造函数,可以被继承 Label 的类提供自己的私有类来使用。

把 q-ptr 和 d-ptr 转型到正确的类型(QPTR 和 DPTR)

上一步优化的一个副作用是 q-ptr 和 d-ptr 的类型分别是 Widget 和 WidgetPrivate
这就意味着下面的代码不能工作。

  1.      void Label :: setText ( const String  &text )  {
  2.          // 不工作。虽然 d_ptr 指向 LabelPrivate 对象,但是它是 WidgetPrivate 类型
  3.         d_ptr -> text  = text ;
  4.      }

因此,在子类里访问 d-pointer 的时候,需要用 static_cast 转型到合适的类型。

  1.      void Label :: setText ( const String  &text )  {
  2.         LabelPrivate  *=  static_cast <LabelPrivate  *> (d_ptr ) ;  // cast to our private type
  3.         d -> text  = text ;
  4.      }

代码里到处都是 static_cast 看起来不是那么漂亮,所以我们定义了下面的宏,
  1.      // global.h (macros)
  2.      #define DPTR(Class) Class##Private *d = static_cast<Class##Private *>(d_ptr)
  3.      #define QPTR(Class) Class *q = static_cast<Class *>(q_ptr)
  4.  
  5.      // label.cpp
  6.      void Label :: setText ( const String  &text )  {
  7.         DPTR (Label ) ;
  8.         d -> text  = text ;
  9.      }
  10.  
  11.      void LabelPrivate :: someHelperFunction ( )  {
  12.         QPTR (label ) ;
  13.         q -> selectAll ( ) ;  // we can call functions in Label now
  14.      }

Qt 中的 d-pointers

在 Qt 中,几乎所有的公有类都使用了 d-pointer。唯一不用的情况是如果事先知道某个类永远不会添加额外的成员变量。例如,像 QPointQRect 这些类,我们不期望有新的成员添加,因此它们的数据成员直接保存在类里而没用 d-pointer。

  • 在Qt中,所有私有对象的基类是 QObjectPrivate
  • Q_DQ_Q 宏提供了上边讨论的 QPTR 和 DPTR 的功能.
  • Qt 的公有类有一个 Q_DECLARE_PRIVATE 的宏。这个宏的代码:
    1.      // qlabel.h
    2.      class  QLabel  {
    3.      private :
    4.         Q_DECLARE_PRIVATE ( QLabel ) ;
    5.      } ;
    6.  
    7.      // qglobal.h
    8.      #define Q_DECLARE_PRIVATE(Class) \
    9.         inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(qGetPtrHelper(d_ptr)); } \
    10.         inline const Class##Private* d_func() const { \
    11.              return reinterpret_cast<const Class##Private *>(qGetPtrHelper(d_ptr)); } \
    12.         friend class Class##Private;
这里的想法是  QLabel  提供了一个函数  d_func()  允许访问它的私有内部类。这个方法本身是私有的(因为这个宏定义在 qlabel.h 私有区域)。 但是  d_func()  可以被  QLabel  的  友元函数  (C++ 友元)调用。这主要对一些 Qt 类想获得  QLabel  的无法通过公有  API  访问的一些信息有用。例如, QLabel  可能要跟踪用户点击了一个链接多少次。但是没有公有  API  访问这个信息。 QStatistics  是需要这个信息的一个类。Qt开发人员可以添加  QStatistics  作为  QLabel  的一个友元类,这样 QStatistics  就可以  label->d_func()->linkClickCount  来访问。

d_func 还有一个优点是保证了const正确性:在 MyClass 的一个 const 成员函数里,你需要 Q_D(const MyClass),因此你只能调用 MyClassPrivate 的const成员函数。如果用 “naked” 的 d_ptr 你可以调用非const函数。

还有一个 Q_DECLARE_PUBLIC 宏做相反的事情。


refer to http://qt-project.org/wiki/Dpointer_SimplifiedChinese 

<think>我们遇到了一个编译警告:passingargument3of'ASE_init'fromincompatiblepointertype[-Wincompatible-pointer-types]这个警告通常发生在函数调用时,传递的参数类型与函数声明中的参数类型不匹配。假设我们有如下的函数声明:voidASE_init(inta,charb,void(*callback)(int,char));而在调用时,我们传递了这样的参数:voidmy_callback(inta,intb);//注意这里第二个参数是int,而函数声明要求charASE_init(1,'a',my_callback);//这里第三个参数就会触发上述警告因为my_callback的类型是void(*)(int,int),而ASE_init期望的是void(*)(int,char)解决方案:1.检查函数ASE_init的声明,确定第三个参数要求的函数指针类型。2.修改我们传递的函数指针,使其类型匹配。下面我们将通过一个完整的例子来演示如何解决这个问题。</think># 解决 "incompatible pointer type" 编译错误 这个错误表明您向函数传递了类型不匹配的指针。在 C 语言中,函数指针类型必须完全匹配,否则编译器会报错。我将详细解释这个问题并提供完整解决方案。 ## 问题分析 当您看到以下错误时: ``` passing argument 3 of 'ASE_init' from incompatible pointer type ``` 通常意味着: 1. 函数 `ASE_init` 期望特定类型的函数指针作为第 3 个参数 2. 您传递的函数指针类型与声明不匹配 3. 常见原因包括函数签名不一致或未正确声明回调类型 ## 完整解决方案 ### 1. 定义正确的回调类型 ```c // ASE.h 中的原始声明 void ASE_init(int mode, int config, void (*callback)(int status, void* user_data)); // 定义匹配的函数指针类型 typedef void (*ASE_Callback)(int status, void* user_data); ``` ### 2. 实现匹配的回调函数 ```c // 正确实现的回调函数 void my_callback_function(int status, void* user_data) { printf("状态: %d, 用户数据: %p\n", status, user_data); // 实际处理逻辑... } // 错误示例(会导致 incompatible pointer type 错误) // void wrong_callback(int status) { ... } // 缺少 user_data 参数 // void another_wrong_callback(char* msg) { ... } // 参数类型不匹配 ``` ### 3. 正确调用 ASE_init 函数 ```c int main() { int mode = 1; int config = 0x0F; void* user_data = (void*)0x1234; // 可以是任何用户数据指针 // 正确调用方式 ASE_init(mode, config, my_callback_function); // 使用 typedef 更清晰 ASE_Callback cb = my_callback_function; ASE_init(mode, config, cb); // 传递用户数据(如果需要) ASE_init_with_data(mode, config, my_callback_function, user_data); return 0; } ``` ### 4. 处理用户数据的完整示例 ```c #include <stdio.h> // 1. 声明函数原型 void ASE_init(int mode, int config, void (*callback)(int, void*)); // 2. 定义回调类型 typedef void (*ASE_Callback)(int status, void* user_data); // 3. 用户数据结构 typedef struct { int id; char name[32]; } UserData; // 4. 实现回调函数 void status_callback(int status, void* user_data) { UserData* data = (UserData*)user_data; printf("设备状态: %d\n", status); printf("用户ID: %d, 名称: %s\n", data->id, data->name); // 根据状态执行操作 if (status == 0) { printf("初始化成功\n"); } else { printf("错误代码: %d\n", status); } } // 5. ASE_init 的模拟实现 void ASE_init(int mode, int config, void (*callback)(int, void*)) { printf("初始化 ASE (模式: %d, 配置: 0x%X)\n", mode, config); // 模拟初始化过程 int status = 0; // 0 表示成功 // 创建用户数据 UserData data = { .id = 1001, .name = "ASE_User" }; // 调用回调函数 if (callback != NULL) { callback(status, &data); } } int main() { // 正确调用 ASE_init(1, 0x0F, status_callback); // 使用类型定义更安全 ASE_Callback cb = status_callback; ASE_init(2, 0x1A, cb); return 0; } ``` ## 错误原因深度解析 ### 1. 函数签名不匹配 ```c // 声明 void ASE_init(..., void (*cb)(int, void*)); // 错误实现 void wrong_cb(int status) { ... } // 缺少 void* 参数 ASE_init(..., wrong_cb); // 错误: 参数数量不匹配 ``` ### 2. 参数类型不匹配 ```c // 声明 void ASE_init(..., void (*cb)(int, void*)); // 错误实现 void another_wrong_cb(int status, char* msg) { ... } ASE_init(..., another_wrong_cb); // 错误: void* vs char* ``` ### 3. 缺少函数原型 ```c // 未包含 ASE.h 头文件 void my_cb(int, void*); // 但编译器不知道 ASE_init 的声明 int main() { ASE_init(..., my_cb); // 可能产生隐式声明问题 } ``` ## 高级技巧:处理不同回调类型 ### 1. 使用通用回调接口 ```c typedef enum { STATUS_EVENT, DATA_EVENT, ERROR_EVENT } EventType; typedef struct { EventType type; union { int status; struct { void* data; size_t size; }; int error_code; }; } ASE_Event; typedef void (*ASE_GenericCallback)(ASE_Event event, void* user_data); void ASE_init_advanced(int mode, ASE_GenericCallback callback, void* user_data); ``` ### 2. 类型安全的包装函数 ```c #include <stdbool.h> bool ASE_init_safe(int mode, int config, void (*callback)(int, void*)) { // 检查函数指针是否匹配预期类型 if (callback == NULL) return false; // 测试调用(不执行实际逻辑) int dummy_status = -1; void* dummy_data = NULL; callback(dummy_status, dummy_data); // 实际初始化 ASE_init(mode, config, callback); return true; } ``` ### 3. 函数指针转换(谨慎使用) ```c typedef void (*GenericFuncPtr)(); void compatible_callback(int status, void* data) { ... } int main() { // 显式转换函数指针类型 GenericFuncPtr generic_cb = (GenericFuncPtr)compatible_callback; // 仅当绝对必要时使用(通常应避免) ASE_init(1, 0x0F, (void (*)(int, void*))generic_cb); } ``` ## 最佳实践总结 1. **严格类型匹配**: - 使用 `typedef` 定义函数指针类型 - 确保回调函数签名完全匹配 2. **头文件管理**: - 在公共头文件中声明函数原型 - 使用 include guard 防止重复包含 3. **错误处理**: - 检查函数指针是否为 NULL - 提供默认回调实现 4. **用户数据处理**: - 使用 `void*` 传递用户数据 - 在回调中转换为具体类型 - 确保数据生命周期有效
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值