c++二进制兼容及解决方法

什么是二进制兼容

二进制兼容ABI(application binary interface)主要指动态库文件单独升级,现有用到老动态库的应用程序是否受到影响。

1.升级库文件,不影响使用库文件的程序。

2.新库必然有新头文件,但是旧的二进制可执行文件还是按照旧的头文件中的“使用说明”来调用库。

意思就是你应用程序A调用库B1.0,现在库B升级了,变成B1.1,应用程序A调用库B1.1还是能够正常使用,这种就叫二进制兼容,反之就是不兼容。

如何判断一个改动是不是二进制兼容呢

C/C++ 通过头文件暴露出动态库的使用方法,这个“使用方法”主要是给编译器看的,编译器会据此生成二进制代码,然后在运行的时候通过装载器(loader)把可执行文件和动态库绑到一起。如何判断一个改动是不是二进制兼容,主要就是看头文件暴露的这份“使用说明”能否与新版本的动态库的实际使用方法兼容。因为新的库必然有新的头文件,但是现有的二进制可执行文件还是按旧的头文件来调用动态库。

二进制不兼容的示例

1 类的普通成员函数 void f( int ) 改成了 void f( double ) 。老EXE会传int进来,新库会用double的长度取数据。从而发生undefined symbol

2 基类增加虚函数会导致基类虚表发生变化。老EXE调用虚表的时候给出的slot是老的,但是新库里面的这个slot已经是另一个函数了。

3 给函数增加默认参数

4 增加默认模板类型

5 改变enum的值

6 给class Bar增加数据成员导致sizeof(Bar)的值变大。这种增加成员变量的情况通常是不安全的,但也有例外:

● 如果客户代码里有 new Bar,那么肯定不安全,因为 new 的字节数不够装下新 Bar。相反,如果 library 通过 factory 返回 Bar*  (并通过 factory 来销毁对象)或者直接返回 shared_ptr<Bar>,客户端不需要用到 sizeof(Bar),那么可能是安全的。同样的道         理,直接定义 Bar bar; 对象(无论是函数局部对象还是作为其他 class 的成员)也有二进制兼容问题。

● 如果客户代码里有 Bar* pBar; pBar->memberA = xx;,那么肯定不安全,因为 memberA 的新 Bar 的偏移可能会变。相反,如果     只通过成员函数来访问对象的数据成员,客户端不需要用到 data member 的 offsets,那么可能是安全的。

● 如果客户调用 pBar->setMemberA(xx); 而 Bar::setMemberA() 是个 inline function,那么肯定不安全,因为偏移量已经被 inline       到客户的二进制代码里了。如果 setMemberA() 是 outline function,其实现位于 shared library 中,会随着 Bar 的更新而更新,     那么可能是安全的。

7 如果EXE里调用new Bar,导致new出来的内存盛不下新的Bar对象(构造函数会使用新DLL中的构造函数来填充数据),从而:

● 如果新的库实现访问了新的数据成员肯定会访问到一个无法预知的地方;

● 如果EXE得到的是shared_ptr<Bar> 由DLL来管理内存,那么此时是安全的。

● 如果EXE调用的是p->member 那么肯定不对,因为偏移量可能因为member前面插入了新的成员而被新DLL中构造函数填充了新的成员,从而访问的并不是老的member。

● 如果EXE是使用p->get_member()来获取数据,那么是正常的。

● 如果p->get_member()是inline的,那么是不安全的,因为偏移量已经在EXE中了。

8 虚函数做接口的基本上都是二进制不兼容的。具体地说,以只包含虚函数的 class (称为 interface class)作为程序库的接口,这样的接口是僵硬的,一旦发布,无法修改。

这里没有列出的一些情况也可能二进制不兼容。

二进制安全的场景

1 增加新的class(定义在新DLL中,老的EXE里没有)

2 增加非virtual函数(定义在新DLL中,老的EXE里没有)

3 增加static成员函数(定义在新DLL中,老的EXE里没有)

还有很多,就不一一列举了

解决办法

1 采用静态链接

这个是王道。在分布式系统这,采用静态链接也带来部署上的好处,只要把可执行文件放到机器上就行运行,不用考虑它依赖的 libraries。目前 muduo 就是采用静态链接。

2 通过动态库的版本管理来控制兼容性

这需要非常小心检查每次改动的二进制兼容性并做好发布计划,比如 1.0.x 系列做到二进制兼容,1.1.x 系列做到二进制兼容,而 1.0.x 和 1.1.x 二进制不兼容。《程序员的自我修养》里边讲过 .so 文件的命名与二进制兼容性相关的话题,值得一读。

3 用pimpl技法

在头文件中只暴露 non-virtual 接口,并且 class 的大小固定为 sizeof(Impl*),这样可以随意更新库文件而不影响可执行文件。当然,这么做有多了一道间接性,可能有一定的性能损失。见 Exceptional C++ 有关条款和 C++ Coding Standards 101。

 

例子实现

Qt作为一个跨平台的开发框架,应用很广,本身肯定也是实现二进制兼容的。那Qt是怎么实现的呢?其实Qt使用的就是pimpl技法,就是Qt的d指针和q指针的使用。

首先定义一个宏定义的类:

dqglobal.h

#ifndef DQGLOBAL_H
#define DQGLOBAL_H

#include <QtGlobal>
#include <QScopedPointer>

#define DQ_DECLARE_PRIVATE(Class) \
    Q_DECLARE_PRIVATE(Class) \
    QScopedPointer<Class##Private> d_ptr;

#define DQ_DECLARE_PUBLIC(Class) \
    Q_DECLARE_PUBLIC(Class) \
    Class* q_ptr;

#define DQ_SAFE_DELETE(p) do { if(p) { delete (p); (p) = 0; } } while(0)

#endif // DQGLOBAL_H

定义一个接口类Widget:

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QObject>
#include "dqglobal.h"

class WidgetPrivate;

class Widget : public QObject
{
    Q_OBJECT
    DQ_DECLARE_PRIVATE(Widget)
public:
    explicit Widget(QObject *parent = 0);

    void print();
    void showMsg(const QString &text);

    void sendText(const QString &text);
signals:
    void sigSendText(const QString &text);

};

#endif // WIDGET_H

widget.cpp

#include "widget.h"
#include "widgetprivate.h"
#include <QDebug>

Widget::Widget(QObject *parent):
    QObject(parent), d_ptr(new WidgetPrivate(this))
{

}

void Widget::print()
{
    Q_D(Widget);
    d->testFun();
}

void Widget::showMsg(const QString &text)
{
    qDebug() << __FUNCTION__ << "song" << text;
}

void Widget::sendText(const QString &text)
{
    emit sigSendText(text);
}

定义实现类WidgetPrivate,代码的实现放在这个类中:

widgetprivate.h

#ifndef WIDGETPRIVATE_H
#define WIDGETPRIVATE_H

#include "dqglobal.h"

class Widget;

class WidgetPrivate
{
    DQ_DECLARE_PUBLIC(Widget)
public:
    WidgetPrivate(Widget *q);

    void testFun();
    void sendText();
};

#endif // WIDGETPRIVATE_H

widgetprivate.cpp

#include "widgetprivate.h"
#include "widget.h"

WidgetPrivate::WidgetPrivate(Widget *q):
    q_ptr(q)
{

}

void WidgetPrivate::testFun()
{
    Q_Q(Widget);
//    q->showMsg("555555555");
    q->sendText("666666666");
}

void WidgetPrivate::sendText()
{
    Q_Q(Widget);
    q->sendText("666666666");
}

为实现d指针和q指针,qt已经在qtglobal里面使用宏的方式定义相关的辅助函数和友元类。

其中d_ptr指针指向私有实现类,使用如下宏定义:

#define Q_DECLARE_PRIVATE(Class) \
    inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(qGetPtrHelper(d_ptr)); } \
    inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private *>(qGetPtrHelper(d_ptr)); } \
    friend class Class##Private;

里面定义了两个函数,一个是返回d_ptr指针对象,一个是d_ptr常量指针对象,并生命为友元函数。##为拼接符,这里传Widget,就变成WidgetPrivate。

我们定义d_ptr的使用使用智能指针QScopedPointer,这样就不用关注D指针的释放。这就是为什么要另外封装一层的原因。

#define DQ_DECLARE_PRIVATE(Class) \
    Q_DECLARE_PRIVATE(Class) \
    QScopedPointer<Class##Private> d_ptr;

 

其中q_ptr指针指向父类,使用如下定义:

#define Q_DECLARE_PUBLIC(Class)                                    \
    inline Class* q_func() { return static_cast<Class *>(q_ptr); } \
    inline const Class* q_func() const { return static_cast<const Class *>(q_ptr); } \
    friend class Class;

 

使用d指针和q指针的宏定义:

#define Q_D(Class) Class##Private * const d = d_func()
#define Q_Q(Class) Class * const q = q_func()

使用Q_D返回d指针对象,变量名为d,这样就可以直接使用d对象进行操作。Q_Q同理。

 

我们可以看到类Widget中只有non-virtual函数及信号等,变量只有d_ptr的指针,这样sizeo()Widget的对象大小都不会改变,因为实现都在WidgetPrivate中,里面无论如何变,Widget对应的头文件都不影响,前面说了增加non-virtual函数不会导致二进制兼容,所以这样就能够实现二进制兼容了。

d指针和q指针的使用,除了能保证代码的二进制兼容外,还能够隐藏实现细节,就是我们在使用qt craeter变成跳到qt自带的类里面的时候,只能看到接口,是看不到实现细节的,qt是开源的,当然可以另外下载源码来看,但实际使用的时候是看不到。当然还能提高编译的速度,比如我们修改了某个函数的实现,就智能编译widgetprivate及相关的类,假如不使用这种模式,不是另外提供接口,那外部调用这个函数相关的文件也要跟着编译。

所以编写模块化的程序的时候推荐使用这种模式,结构会更清晰一些。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: C++ 中可以使用 STL 库中的 atoi() 和 stoi() 函数将字符串转换为 int 值。 atoi() 函数可以将字符串转换为 int 值, 但它只能转换十进制字符串. stoi() 函数可以将字符串转换为 int 值, 并且可以支持各种进制,如十进制、八进制、十六进制。 如: ``` string str = "123"; int num = atoi(str.c_str()); cout << num << endl; string str2 = "0x11"; int num2 = stoi(str2, nullptr, 16); cout << num2 << endl; ``` 第一个输出为123,第二个输出为17 ### 回答2: 在C++中,可以使用内置的`stoi()`函数将字符串转换为int值,并兼容十进制和十六进制。`stoi()`函数接受一个字符串参数,并返回相应的int值。 要将十进制字符串转换为int值,可以直接调用`stoi()`函数,例如: ```cpp std::string decimalStr = "123"; int decimalInt = std::stoi(decimalStr); ``` 在这个示例中,`decimalStr`是要转换的十进制字符串,`decimalInt`是转换后的int值。 要将十六进制字符串转换为int值,需要在字符串前面加上"0x"前缀,以表示十六进制形式,例如: ```cpp std::string hexStr = "0x1A"; int hexInt = std::stoi(hexStr, nullptr, 16); ``` 在这个示例中,`hexStr`是要转换的十六进制字符串,`hexInt`是转换后的int值。`stoi()`函数的第三个参数为可选参数,用于指定进制,此处使用16表示十六进制。 需要注意的是,如果字符串不是有效的整数格式,或者超出了int类型的范围,将抛出`std::invalid_argument`或`std::out_of_range`异常。因此,在转换之前最好先进行有效性检查。 以上是将字符串转换为int值,兼容十进制和十六进制的方法。如果需要转换其他进制,可以通过更改`stoi()`函数的第三个参数来实现。 ### 回答3: 要将C++字符串转换为int值,并同时兼容不同进制(包括10进制和16进制),可以使用C++的内置函数和一些逻辑处理。下面是代码示例: ```cpp #include <iostream> #include <sstream> #include <string> int convertToInt(const std::string& str) { std::stringstream ss; int value = 0; if (str.substr(0, 2) == "0x") { ss << std::hex << str; // 如果字符串以0x开头,则按16进制处理 } else { ss << str; // 否则按10进制处理 } ss >> value; // 从字符串中提取整数值 return value; } int main() { std::string str1 = "12345"; // 10进制字符串 std::string str2 = "0x1F"; // 16进制字符串 int intValue1 = convertToInt(str1); int intValue2 = convertToInt(str2); std::cout << intValue1 << std::endl; // 输出:12345 std::cout << intValue2 << std::endl; // 输出:31 return 0; } ``` 上述代码中,我们使用了`std::stringstream`类来处理字符串转换。首先判断字符串的开头是否是"0x",如果是,则将其视为16进制字符串;否则将其视为10进制字符串。然后将字符串写入`std::stringstream`对象中,并使用`std::hex`标志设置为16进制。最后通过`<<`运算符提取整数值存储在变量`value`中。最终返回该值。 在`main`函数中,我们传入不同的字符串进行测试。字符串"12345"表示10进制,"0x1F"表示16进制。经过转换后,我们分别获得了相应的整数值,分别为12345和31,并将其输出到控制台上。 这种方法可以兼容不同进制的字符串转换为整数值,因为它根据字符串的开头来决定使用哪种进制进行转换。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值