DLL地狱及其解决方案

2023-11-08

原作者:Ivan S Zapreev

概要

本文将要介绍DLL的向后兼容性问题,也就是著名的“DLL Hell”问题。首先我会列出自己的研究结果,其中包括其它一些研究者的成果。在本文的最后,我还将给出“DLL Hell”问题的一个解决方案。


介绍

我曾经接受过一个任务,去解决一个DLL版本更新的问题————某个公司给用户提供了一套SDK,这个SDK是由一系列DLL组成的;DLL中导出了很多类,用户使用这些类(直接使用或派生新的子类)来继续他们的C++程序开发。用户在使用这些DLL时没有得到很详细的使用说明(比如使用这些DLL中导出的类有什么限制等)。当这些DLL更新为新的版本之后,他们发现他们开发的基于这些DLL的应用程序会经常崩溃(他们的应用程序从SDK的导出类派生了新的子类)。为了解决这个问题,用户必须重新编译他们的应用程序,重新连接新版本的SDK DLL。

我将对这个问题给出我的研究结果,同时还有我从其它地方搜集过来的相关信息。最后,我将来解决这个“DLL Hell”问题。


研究结果

就我个人的理解,这个问题是由SDK DLL中导出的基类改动之后引起的。我查看了一些文章后发现,DLL的向后兼容性问题其实早有人提出。但作为一个实在的研究者,我决定自己做一些试验。结果,我发现如下的问题:

1. 在DLL的导出类中增加一个新的虚函数将导致如下问题:
  (1)如果这个类以前就有一个虚函数B,此时在它之前增加一个新的虚函数A。这样,我们改变了类的虚函数表。于是,表中的第一个函数指向了函数A(而不是原来的B)。此时,客户程序(假设没有在拿到新版本的DLL之后重新编译、连接)调用函数B就会产生异常。因为此时调用函数B实际上转向了调用函数A,而如果函数A和函数B的参数类型、返回值类型迥异的话问题就出来了!
  (2)如果这个类原本没有虚函数(它的父类也没有虚函数),那么给这个类增加一个新的虚函数(或者在它的父类增加一个虚函数)将导致新增加一个类成员,这个成员是一个指针类型的,指向虚函数表。于是,这个类的尺寸将会被改变(因为增加了一个成员变量)。这种情况下,客户程序如果创建了这个类的实例,并且需要直接或间接修改类成员的值的时候就会有问题了。因为虚函数表的指针是作为类的第一个成员加入的,也就是说,原本这个类定义的成员因为虚函数表指针的加入而都产生了地址的偏移。客户程序对原成员的操作自然就出现异常了。
  (3)如果这个类原本就有虚函数(或者只要它的父类有虚函数),而且这个类被导出了,被客户程序当作父类来用。那么,我们不要给这个类增加虚函数!不仅在类声明的开头不能加,即使在末尾处也不能加。因为加入虚函数会导致虚函数表内的函数映射产生偏移;即使你将虚函数加在类声明的末尾,这个类的派生类的虚函数表也会因此产生偏移。

2. 在DLL的导出类中增加一个新的成员变量将导致如下问题:
   (1)给一个类增加一个成员变量将导致类尺寸的改变(给原本有虚函数表的类增加一个虚函数将不会改变类的尺寸)。假设这个成员增加在类声明的最后。如果客户程序为创建这个类的实例少分配了内存,那么可能在访问这个成员时导致内存越界。
   (2)如果在原有的类成员中间增加一个新的成员,情况会更糟糕。因为这样会导致原有类成员的地址产生偏移。客户程序操作的是一个错误的地址表,对于新成员后面的成员尤其是这样(它们都因为新成员的加入而导致了自己在类中的偏移的变化)。

(注:上述的客户程序就是指使用SDK DLL的应用程序。)

除了上面这些原因外,还有其它操作会导致DLL的向后兼容性问题。下面列出了解决(大部分)这些问题的方法。


DLL编码约定简述

下面是我搜集到的所有的解决方案,其中一些是从网上的文章中拿来的,一些是跟不同的开发者交流后得到的。

下面的约定主要针对DLL开发,而且是为解决DLL的向后兼容性问题:

1. 编码约定:
   (1)DLL的每个导出类(或者它的父类)至少包含一个虚函数。这样,这个类就会始终保存一个指向虚函数表的指针成员。这么做可以方便后来新的虚函数的加入。
   (2)如果你要给一个类增加一个虚函数,那么将它加在所有其它虚函数的后面。这样就不会改变虚函数表中原有函数的地址映射顺序。
   (3)如果你打算以后给一个类扩充类成员,那么现在预留一个指向一个数据结构的指针。这样的话,增加一个成员直接在这个数据结构中修改,而不是在类中修改。于是,新成员的加入不会导致类尺寸的改变。当然,为了访问新成员,需要给这个类定义几个操作函数。这种情况下,DLL必须是被客户程序隐式(implicitly)连接的。
   (4)为了解决前一点的问题,也可以给所有的导出类设计一个纯接口的类,但此时,客户程序将无法从这些导出类继续派生,DLL导出类的层次机构也将无法维持。
   (5)发布两个版本的DLL和LIB文件(Debug版本和Release版本)。因为如果只发布Release版本,开发者将无法调试他们的程序,因为Release版与Debug版使用了不同的堆(Heap)管理器,因而当Debug版本的客户程序释放Release版本DLL申请的内存时,会导致运行时错误(Runtime failure)。有一种办法可以解决这个问题,就是DLL同时提供申请和释放内存的函数供客户程序调用;DLL中也保证不释放客户程序申请的内容。通常遵守这个约定不是那么简单!
   (6)在编译的时候,不要改变DLL导出类函数的默认参数,如果这些参数将被传递到客户程序的话。
   (7)注意内联(inline)函数的更改。
   (8)检查所有的枚举没有默认的元素值。因为当增加/删除一个新的枚举成员,你可能移动旧枚举成员的值。这就是为什么每一个成员应该拥有一个唯一标识值。如果枚举可以被扩展,也应该对其进行文档说明。这样,客户程序开发者就会引起注意。
   (9)不要改变DLL提供的头文件中定义的宏。

2. 对DLL进行版本控制:如果主要的DLL发生了改变,最好同时将DLL文件的名字也改掉,就象微软的MFC DLL一样。例如,DLL文件可以按照如下格式命名:Dll_name_xx.dll,其中xx就是DLL的版本号。有时候DLL中做了很大的改动,使得向后兼容性问题无法解决。此时应该生成一个全新的DLL。将这个新DLL安装到系统时,旧的DLL仍然保留。于是,旧的客户程序仍然能够使用旧的DLL,而新的客户程序(使用新DLL编译、连接)可以使用新的DLL,两者互不干涉。

3. DLL的向后兼容性测试:还有很多很多中可能会破坏DLL的向后兼容性,因此实施DLL的向后兼容性测试是非常必要的!

接下去,我将来讨论一个虚函数的问题,以及对应的一个解决方案。


虚函数与继承

首先来看一下如下的虚函数和继承结构:

/**********DLL导出的类 **********/
class EXPORT_DLL_PREFIX VirtFunctClass{
public:
    VirtFunctClass(){}
    ~VirtFunctClass(){}
    virtual void DoSmth(){
        //this->DoAnything(); 
              // Uncomment of this line after the corresponding method
              //will be added to the class declaration
    }
    //virtual void DoAnything(){}
         // Adding of this virtual method will make shift in
         // table of virtual methods
};

/**********客户程序,从DLL导出类派生一个新的子类**********/
class VirtFunctClassChild : public VirtFunctClass {
public:
    VirtFunctClassChild() : VirtFunctClass (){}
    ~VirtFunctClassChild(){};
    virtual void DoSomething(){}
};

假设上面的两个类,VirtFunctClass在my.dll中实现,而VirtFunctClassChild在客户程序中实现。接下去,我们做一些改变,将如下两个注释行放开:
//virtual void DoAnything(){}

//this->DoAnything();

也就是说,DLL导出的类作了改动!现在如果客户程序没有重新编译,那么客户程序中的VirtFunctClassChild将不知道DLL中VirtFunctClass类已经改变了:增加了一个虚函数void DoAnything()。因此,VirtFunctClassChild类的虚函数表仍然包含两个函数的映射:
1. void DoSmth()
2. void DoSomething()

而事实上这已经不对了,正确的虚函数表应该是:
1. void DoSmth()
2. void DoAnything()
3. void DoSomething()

问题就在于,当实例化VirtFunctClassChild之后,如果调用它的void DoSmth()函数,DoSmth()函数转而要调用void DoAnything()函数,但此时基类VirtFunctClass只知道要调用虚函数表中的第二个函数,而VirtFunctClassChild类的虚函数表中的第二个函数仍然是void DoSomething(),于是问题就出来了!

另外,禁止在DLL的导出类的派生类(上例中的VirtFunctClassChild)中增加虚函数也是于事无补的。因为,如果VirtFunctClassChild类中没有virtual void DoSomething()函数,基类中的void DoAnything()函数(虚函数表中的第二个函数)调用将会指向一个空的内存地址(因为VirtFunctClassChild类维持的虚函数表仅仅维持有一个函数地址)。

现在可以看出,在DLL的导出类中增加虚函数是一个多么严重的问题!不过,如果虚函数是用来处理回调事件的,我们有办法来解决这个问题(下文有介绍)。
 

COM及其它

现在可以看出,DLL的向后兼容性问题是一个很出名的问题。解决这些问题,不仅可以借助于一些约定,而且可以通过其它一些先进的技术,比如COM技术。因此,如果你想摆脱“DLL Hell”问题,请使用COM技术或者其它一些合适的技术。

让我们回到我接受的那个任务(我在本文开头的地方讲到的那个任务)————解决一个使用DLL的产品的向后兼容性问题。

我对COM有些了解,因此我的第一个建议是使用COM技术来克服那个项目中的所有问题。但这个建议因为如下原因最终被否决了:
1. 那个产品已经在某个内部层中有一个COM服务器。
2. 将一大堆接口类重写到COM的形式,投入比较大。
3. 因为那个产品是DLL库,而且已经有很多应用程序在使用它了。因此,他们不想强制他们的客户重写他们的应用程序。

换句话说,我被要求完成的任务是,以最小的代价来解决这个DLL向后兼容性问题。当然,我应该指出,这个项目最主要的问题在于增加新的成员和接口类上的虚回调函数。第一个问题可以简单地通过在类声明中增加一个指向一个数据结构的指针来解决(这样可以任意增加新的成员)。这种方法我在上面已经提到过。但是第二个问题,虚回调函数的问题是新提出的。因此,我提出了下面的最小代价、最有效的解决方法。


虚回调函数与继承

然我们想象一下,我们有一个DLL,它导出了几个类;客户应用程序会从这些导出类派生新的类,以实现虚函数来处理回调事件。我们想在DLL中做一个很小的改动。这个改动允许我们将来可以给导出类“无痛地”增加新的虚回调函数。同时,我们也不想影响使用当前版本DLL的应用程序。我们期望的就是,这些应用程序只有在不得已的时候才协同新版本的DLL进行一次重新编译。因此,我给出了下面的解决方案:

我们可以保留DLL导出类中的每个虚回调函数。我们只需记住,在任何一个类定义中增加一个新的虚函数,如果应用程序不协同新版本的DLL重新编译,将导致严重的问题。我们所做的,就是想要避免这个问题。这里我们可以一个“监听”机制。如果在DLL导出类中定义并导出的虚函数被用作处理回调,我们可以将这些虚函数转移到独立的接口中去。

让我们来看下面的例子:

// 如果想要测试改动过的DLL,请将下面的定义放开
//#define DLL_EXAMPLE_MODIFIED

#ifdef DLL_EXPORT
    #define DLL_PREFIX __declspec(dllexport)
#else
    #define DLL_PREFIX __declspec(dllimport)
#endif

/********** DLL的导出类 **********/
#define CLASS_UIID_DEF static short GetClassUIID(){return 0;}
#define OBJECT_UIID_DEF virtual short
      GetObjectUIID(){return this->GetClassUIID();}

// 所有回调处理的基本接口
struct DLL_PREFIX ICallBack
{
    CLASS_UIID_DEF
    OBJECT_UIID_DEF
};

#undef CLASS_UIID_DEF

#define CLASS_UIID_DEF(X) public: static
     short GetClassUIID(){return X::GetClassUIID()+1;}

// 仅当DLL_EXAMPLE_MODIFIED宏已经定义的时候,进行接口扩展
#if defined(DLL_EXAMPLE_MODIFIED)
// 新增加的接口扩展
struct DLL_PREFIX ICallBack01 : public ICallBack
{
    CLASS_UIID_DEF(ICallBack)
    OBJECT_UIID_DEF
    virtual void DoCallBack01(int event) = 0;  // 新的回调函数
};
#endif // defined(DLL_EXAMPLE_MODIFIED)

class DLL_PREFIX CExample{
public:
    CExample(){mpHandler = 0;}
    virtual ~CExample(){}
    virtual void DoCallBack(int event) = 0;
    ICallBack * SetCallBackHandler(ICallBack *handler);
    void Run();
private:
    ICallBack * mpHandler;
};

很显然,为了给扩展DLL的导出类(增加新的虚函数)提供方便,我们必须做如下工作:
1. 增加ICallBack * SetCallBackHandler(ICallBack *handler);函数;
2. 在每个导出类的定义中增加相应的指针;
3. 定义3个宏;
4. 定义一个通用的ICallBack接口。

为了演示给CExample类增加新的虚回调函数,我在这里增加了一个ICallBack01接口的定义。很显然,新的虚回调函数应该加在新的接口中。每次DLL更新都新增一个接口(当然,每次DLL更新时,我们也可以给一个类同时增加多个虚回调函数)。

注意,每个新接口必须从上一个版本的接口继承。在我的例子中,我只定义了一个扩展接口ICallBack01。如果DLL再下个版本还要增加新的虚回调函数,我们可以在定义一个ICallBack02接口,注意ICallBack02接口要从ICallBack01接口派生,就跟当初ICallBack01接口是从ICallBack接口派生的一样。

上面代码中还定义了几个宏,用于定义需要检查接口版本的函数。例如我们要为新接口ICallBack01增加新函数DoCallBack01,如果我们要调用ICallBack * mpHandler; 成员的话,就应该在CExample类进行一下检查。这个检查应该如下实现:

if(mpHandler != NULL && mpHandler->GetObjectUIID()>=ICallBack01::GetClassUIID()){
    ((ICallBack01 *) mpHandler)->DoCallBack01(2);
}

我们看到,新回调接口增加之后,在CExample类的实现中只需简单地插入新的回调调用。

现在你可以看出,我们上述对DLL的改动并不会影响客户应用程序。唯一需要做的,只是在采用这种新设计后的第一个DLL版本(为DLL导出类增加了宏定义、回调基本接口ICallBack、设置回调处理的SetCallBackHandler函数,以及ICallBack接口的指针)发布后,应用程序进行一次重编译。(以后扩展新的回调接口,应用程序的重新编译不是必需的!)

以后如果有人想要增加新的回调处理,他就可以通过增加新接口的方式来实现(向上例中我们增加ICallBack01一样)。显然,这种改动不会引起任何问题,因为虚函数的顺序并没有改变。因此应用程序仍然以以前的方式运行。唯一你要注意的是,除非你在应用程序中实现了新的接口,否则你就接收不到新增加的回调调用。

我们应该注意到,DLL的用户仍然能够很容易与它协同工作。下面是客户程序中的某个类的实现例子:

// 如果DLL_EXAMPLE_MODIFIED没有定义,使用以前版本的DLL
#if !defined(DLL_EXAMPLE_MODIFIED)
// 此时没有使用扩展接口ICallBack01
class CClient : public CExample{
public:
    CClient();
    void DoCallBack(int event);
};

#else // !defined(DLL_EXAMPLE_MODIFIED)
// 当DLL增加了新接口ICallBack01后,客户程序可以修改自己的类
// (但不是必须的,如果他不想处理新的回调事件的话)
class CClient : public CExample, public ICallBack01{
public:
    CClient();
    void DoCallBack(int event);

    // 声明DoCallBack01函数(客户程序要实现它,以处理新的回调事件)
    // (DoCallBack01是ICallBack01接口新增加的虚函数)
    void DoCallBack01(int event);
};
#endif // defined(DLL_EXAMPLE_MODIFIED)


例程 ---> 代码下载(6.26K)

与本文的内容配套,我提供了演示程序Dll_Hell_Solution。

1. Dll_example: DLL的实现项目;
2. Dll_Client_example: DLL的客户应用程序项目。

注意:目前Dll_Hell_Solution/Dll_example/dll_example.h文件中的DLL_EXAMPLE_MODIFIED定义被注释掉了。如果放开这个注释,可以生成更新后的DLL版本;然后可以再次测试客户应用程序。

为了保证读者能够正常演示,请遵循如下步骤:
1. 不要改动任何代码(此时DLL_EXAMPLE_MODIFIED没有定义)编译Dll_example和Dll_Client_example两个项目。运行客户程序,体验最初的情况。
2. 放开DLL_EXAMPLE_MODIFIED的注释,然后重新编译Dll_example。重新运行客户程序(此时使用了新版本的DLL),应该仍然运行正常。
3. 重新编译Dll_Client_example,生成新的客户程序。我们看到新增加的回调函数被调用了!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

DLL地狱及其解决方案 的相关文章

  • 在非指针变量和类成员上放置 new

    考虑以下示例 include
  • 类中的易失性变量:“‘易失性’之前预期有非限定 ID”?

    我有两个static volatile我的类中定义的变量ADC 该类写为 裁剪以节省空间 pragma once include PeriodicProcess PeriodicProcess h include
  • 在 C# 中创建可移植(非安装)Windows 应用程序

    我有一个 net 3 5 应用程序 我想让它变得可移植 它很简单并且运行完美 我已将 EXE DLL 发送给一些朋友 当在同一文件夹中运行带有 DLL 和 ICO 我在其中使用过 的 exe 时 它 按预期工作 我想要的很简单 创建一个单独
  • 为 C++ 类播种 rand()

    我正在开发一个 C 类 它使用rand 在构造函数中 我真的希望这个班级在几乎所有方面都能照顾好自己 但我不知道在哪里播种rand 如果我播种rand 在构造函数中 每次构造我的对象类型的新实例时都会对其进行播种 因此 如果我按顺序创建 3
  • 如何检查类中是否存在成员名称(变量或函数),无论是否指定类型? [复制]

    这个问题在这里已经有答案了 这个Q是以下内容的延伸 模板化检查类成员函数是否存在 https stackoverflow com q 257288 514235 是否有任何实用程序可以帮助找到 类中是否存在成员名称 该成员可以是 变量或方法
  • MSBuild 命令行 - 添加 dll 引用

    我使用 makefile 来编译我的 C 项目 在这个makefile中 我创建了一个库tools dll 调用csc exe OK 现在 我想在我的项目中使用这个 dll 由于某些原因 我必须使用使用 csproj 文件的 MSBuild
  • 如何在Electron App中调用C# dll方法?

    我有一个电子应用程序 可以从读卡器读取信用卡详细信息 他们提供了一个 c dll 来与应用程序交互 我不知道如何从电子应用程序读取 dll 方法 首先使用以下命令检查 dll 中公开的函数依赖步行者 http www dependencyw
  • 在 Python 中使用类作为命名空间是个好主意吗

    我正在将一堆相关的东西放入一个类中 主要目的是将它们组织到命名空间中 class Direction north 0 east 1 south 2 west 3 staticmethod def turn right d return tu
  • 使用createremotethread注入dll

    createremotethread如何在进程内执行dll 它使用的参数之一是 loadlibraryA 所以我得到了它在进程中执行 loadlibrary 函数的部分 然后它应该将上下文切换到内核模式 dll 是否由具有 loadlibr
  • WriteLine 与类

    我正在制作一个 SchoolApp 程序来学习 C 并且我正在尝试实现以下主要功能 namespace SchoolApp class Program public static void Main string args School s
  • 将 Scala 文件转换为 Dll

    我有一些使用 IntelliJ 和 SBT Plugin 编写的 scala 代码 并希望将代码作为 C 的 DLL 提供给我 我已经尝试使用 ikvmc 我通过 package 将所有类打包在一个罐子中 之后 我手动设置一个 jar 其中
  • 使用 xerces 链接 DLL 会给出未定义的符号

    我正在使用 cygwin 创建一个共享库 DLL 它使用 Xerces 当我从主应用程序调用 xercesc 函数时 一切都很好 但是当我尝试将一些代码放入库中时 我会得到 xerxesc 定义的所有静态内容的未定义符号 例如 std st
  • 将文本文件扫描到对象数组中

    我有一个逗号分隔的文本文件 其信 息格式如下 名字 姓氏 餐1 餐2 餐3 餐4 每个新学生都在新的一行 我有以下学生对象 public class Student private String first null private Str
  • 用于创建类图的工具[关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 请建议用于创建符合以下标准的类图的工具 它应该是平台无关的 因为我使用 Linux 并且该文件预计由使用 Windows Mac 的团队其
  • 常量表达式包含无效操作[重复]

    这个问题在这里已经有答案了 我有以下代码 出现错误 PHP 致命错误 常量表达式包含无效操作 当我在构造函数中定义变量时 它工作得很好 我正在使用 Laravel 框架
  • 我应该使用课程吗? (Python)

    我正在尝试编写一个包含一些数学函数的小型 Python 模块 例如 它可能包含如下函数 def quad x a b c return a x 2 b x c 您可能会注意到它包含几个参数 即a b c 除了变量x 现在 如果我将其放入文件
  • 如何从本机 C(++) DLL 调用 .NET (C#) 代码?

    我有一个 C app exe 和一个 C my dll my dll NET 项目链接到本机 C DLL mynat dll 外部 C DLL 接口 并且从 C 调用 C DLL 可以正常工作 通过使用 DllImport mynat dl
  • 在 Objective C 的类方法中引用类本身

    我希望我没有跳过 ObjC 手册中的这一部分 但是是否可以从类的一个类方法中引用该类 就像在 PHP 中一样 您将使用 this 来引用当前实例 而 self 引用实例的类 this 的 ObjC 等价物将是 self 那么 PHP 的 s
  • 两个类可以使用 C++ 互相查看吗?

    所以我有一个 A 类 我想在其中调用一些 B 类函数 所以我包括 b h 但是 在 B 类中 我想调用 A 类函数 如果我包含 a h 它最终会陷入无限循环 对吗 我能做什么呢 仅将成员函数声明放在头文件 h 中 并将成员函数定义放在实现文
  • 用于检查类是否具有运算符/成员的 C++ 类型特征[重复]

    这个问题在这里已经有答案了 可能的重复 是否可以编写一个 C 模板来检查函数是否存在 https stackoverflow com questions 257288 is it possible to write a c template

随机推荐

  • elasticsearch得分设置以及分词器不同层次定义

    GET cat indices GET hotel search GET search query constant score filter term lvg mc 酒店 boost 1 2 DELETE my index PUT my
  • NLP——机器翻译中的Seq2Seq

    文章目录 框架 简介 Encoder Decoder CNN Seq2Seq Seq2Seq模型缺点 框架 简介 Seq2Seq 全称Sequence to Sequence 序列到序列 它是一种通用的编码器 解码器框架 这个框架最初是为了
  • Python中的MetaPathFinder

    MetaPathFinder 是 Python 导入系统中的一个关键组件 它与 sys meta path 列表紧密相关 sys meta path 是一个包含 MetaPathFinder 实例的列表 这些实例用于自定义模块的查找和加载逻
  • 外汇市场与交易系统读书笔记(1)

    本文为 外汇市场与交易系统 这本书的读书笔记 1 a 汇率报价一般有五个数字 包括小输掉 其中 精度最高到小数点后四位 一般而言 我们以五个数字中的最低位作为基本点 b 外汇即期汇率报价一般包含买入汇率和卖出汇率 例如usd jpy的汇率可
  • 【知识积累】分析并实现O(N^2)的算法(对数器验证)

    1 选择排序 package com example demo algorithm import java util Arrays Description 选择排序 数据规模 N 0 N 1 看 比 交换 1 N 1 看 比 交换 2 N
  • SoC的开发

    怎么做SoC SoC是干啥的 SoC就是将CPU GPU Uart I2C WiFi Etherne等硬件IP连起来 做到一个芯片上 主要工作有 1 用verilog将这些IP core连起来 在verilog仿真器上进行验证 也要写一些C
  • Axure Repeater系列---排序

    最新学习整理Repeater 网上也能找到一些实现排序的帖子 但是对于不熟悉中继器的同学来说 直接上手还是有点难度的 我也遇到一些坑 特整理记录下来 共同学习 学习之前最好了解下中继器的各个属性以及函数的含义 工具 Axure8 0 学习目
  • 使用人声分离在线网站获得音乐

    有时候碰到一个广告中的音乐 觉得非常悦耳 想将它占为己有 使用到自己的片子中 但奈何其中有广告人声 想获得纯的音乐 如何做到呢 本文给出了方法 希望对你有用 注 本教程使用到了几个工具 1 fdm 下载片源 2 视频编辑大师 分离视频中的音
  • MySQL必知必会——第二十章更新和删除数据

    更新和删除数据 本章介绍如何利用UPDATE和DELETE语句进一步操纵表数据 更新数据 为了更新 修改 表中的数据 可以使用UPDATE语句 UPDATE的两种用法 更新表中特定行 更新表中所有行 不要省略WHERE子句 缺少WHERE子
  • 树莓派教程 - 2.1 树莓派USB摄像头 树莓派罗技免驱摄像头 fswebcam常用参数

    树莓派外接摄像头 最常用的有两种 CSI摄像头 USB摄像头 当然网络摄像头也是可以的 一般的USB摄像头都是UVC免驱的 而且可以方便的插拔和安装 平时最为常用 一 硬件设备 usb摄像头使用的 罗技c310 只要是UVC免驱就可以 二
  • QT实现聊天室

    qt实现聊天室 项目功能简介 1 连接 客户端 需要先连接服务器 就是输入服务器端的IP和端口连接服务器 如果连接成功 连接按钮显示文字会显示已连接 颜色变浅 2 注册 接下来是注册 如果申请的用户名还有人用户注册 则可以注册成功 如果之前
  • JS之instanceof详解

    instanceof 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上 语法 object instanceof constructor object 某个实例对象 constructor 某个构造函数 用来检测
  • Github使用学习笔记(四)

    第四节任务 Github中奇怪的后缀文件都是什么 一 README md 1 README md的作用 在构建完整项目结构的根目录下应该有一个名为ReadMe的文件来说明当前版本源码结构或版本信息 如果你常看开源项目也会发现一个规律 在你拿
  • QuestMobile 2017年中国移动互联网年度报告

    来源 QuestMobile 2017年 科技的风口兜兜转转 从直播 VR到AI再到区块链 短视频泛娱乐IP 最终在2017年底定格在了知识付费上 然而这并没有结束 紧随知识付费而来的就是撒币 大撒币 这就是中国移动互联网的奇妙之处 再严肃
  • 2021-07-19王汕7.19国际黄金今日行情趋势分析,期货原油白银最新操作建议

    黄金行情走势分析 刚刚过去的一周 现货黄金冲高回落 美联储主席多次发表鸽派言论 多个国家新冠疫情回升 一度帮助金价创一个月新高至1834 12美元 盎司 散户和机构也看涨后市 但美国零售销售等数据表现靓丽 仍使投资者坚定美联储未来逐步收紧货
  • vue后端传值1和0怎么绑上对应得值?

    目录 前言 解决 前言 在做表格绑定后端返回得数据后 发现后端返回得有些字段值是0或者1等数字 但是我们在表格中需要展示得却是相对应得男 女 是 否等等 下面是我得解决办法 解决 我使用得是element ui库 后端返回得参数中是否签到字
  • 洛谷 P5715 三个数按照从小到大排序

    这是一个经典的例题 与比较两个数的大小的方式相同 建立一个中间变量 对数的大小进行排序 但不同的是 这个题在思路上较为复杂一点 思路 我们规定好输出的顺序从小到大依次是a b c 建立一个中间变量t 像比较两个数的大小的方法那样 对大小顺序
  • CAS,AQS,volatile,native,synchronized,lock关键字解读以及它们之间的联系(高频面试)

    1 CAS CAS比较并交换 没啥好说的 下面来说一下具体实现底层 CAS底层是由native修饰的 native是调用的本地C 代码Safe app类中的 lock IF MP方法 什么意思呢 就是说如果 IF 计算机是多核状态下 MP
  • ERROR: No matching distribution found for XXXXX 国内的镜像源来加速网络

    用国内的镜像源来加速网络 pip install 包名 i http pypi douban com simple trusted host pypi douban com 其中 trusted host pypi douban com 是
  • DLL地狱及其解决方案

    原作者 Ivan S Zapreev 概要 本文将要介绍DLL的向后兼容性问题 也就是著名的 DLL Hell 问题 首先我会列出自己的研究结果 其中包括其它一些研究者的成果 在本文的最后 我还将给出 DLL Hell 问题的一个解决方案