VC++ class 具有 VARIANT 成员的对象有奇怪的行为

VC++ class objects with a VARIANT member have strange behavior

我有一个 class 定义如下:

class CVariable  
{
   public:
     CVariable(CString strData, int nNum);
     CVariable(BSTR bsData);
     ~CVariable();
   public:
     VARIANT GetVariant(){return m_bsVa;};
   private:
     VARIANT m_bsVa;
   VARIANT m_nVa;
};

工具是:

CVariable::CVariable(CString strData, int nNum)
{
  VariantInit(&m_bsVa);
  BSTR bsData = ::SysAllocString(strData);
  m_bsVa.vt = VT_BSTR;
  m_bsVa.bstrVal = bsData;
  ::SysFreeString(bsData);

  VariantInit(&m_nVa);
  m_nVa.vt = VT_I2;
  m_nVa.lVal = nNum;
}

CVariable::CVariable(BSTR bsData) {
  m_bsVa.vt = VT_BSTR;
  m_bsVa.bstrVal = bsData;
}

CVariable::~CVariable()
{
  VariantClear(&m_bsVa);
  VariantClear(&m_nVa);
}

当我尝试使用构造函数 CVariable(CString,int) 构造两个实例时, class成员m_bsVa总是有相同的值,而m_nVa是different.The 结果如下:

如您所见,v1v2 具有相同的 m_bsVa 但不同的 m_nVa,同时使用constructor CVariable(BSTR) 导致正确的结果。我不知道为什么会这样? 任何帮助将不胜感激。

在这个构造函数中:

CVariable::CVariable(BSTR bsData) {
  m_bsVa.vt = VT_BSTR;
  m_bsVa.bstrVal = bsData;
}

您将 m_nVa 设为未初始化 - 它会获得一些随机值。它应该看起来像这样:

CVariable::CVariable(BSTR bsData) {
  VariantInit(&m_bsVa);
  m_bsVa.vt = VT_BSTR;
  m_bsVa.bstrVal = bsData;

  VariantInit(&m_nVa);
}

在这个构造函数中:

CVariable::CVariable(CString strData, int nNum)
{
  VariantInit(&m_bsVa);
  BSTR bsData = ::SysAllocString(strData);
  m_bsVa.vt = VT_BSTR;
  m_bsVa.bstrVal = bsData;
  ::SysFreeString(bsData);

  VariantInit(&m_nVa);
  m_nVa.vt = VT_I2;
  m_nVa.lVal = nNum;
}

不要调用 ::SysFreeString(bsData);,因为 bsData 属于 m_bsVaSysFreeString() 释放内存,下一个 SysAllocString() 调用可能会在同一内存地址创建一个新的 BSTR 字符串。

我建议您不要使用 naked VARIANT,而是使用 _variant_t class。在这种情况下,您根本不需要担心 VariantInit()/VariantClear(),因为它以 C++ 风格为您实现了所有权策略。

我发现你的代码有几个问题。

  1. CVariable(CString, int) 构造函数为 m_bsVa 分配了一个 BSTR,但随后立即释放了 BSTR,留下 m_bsVa 指向无效内存,并允许下一个 CVariable 实例可能为其分配的 BSTR 重用相同的内存地址。您需要保留分配的 BSTR 直到您使用 m_bsVa 完成(或者至少直到您想要为其分配一个新值)。 VariantClear() 将为您免费 BSTR

  2. CVariable(BSTR)构造函数根本没有初始化m_nVa,这会导致后续操作出现问题,包括VariantClear()。此外,构造函数正在取得调用者 BSTR 的所有权。这可能可行也可能不可行,具体取决于您如何使用此构造函数。如果来电者不希望您获得所有权,那么您需要使用 SysAllocString/Len().

  3. 复制 BSTR
  4. VARIANT 不可简单复制。您需要使用 VariantCopy() 函数将数据从一个 VARIANT 复制到另一个。这意味着您的 CVariable class 需要实现复制构造函数和复制赋值运算符。无论如何你都需要这样做,这样你的 class 就符合 Rule of Three.

  5. GetVariant() 按原样 returning m_bsVa,因此编译器将简单地复制 m_bsVa 字段的值作为-进入来电接收VARIANT。由于 BSTR 是一个指针,调用者将 直接访问 到您的 class 中的原始 BSTR。这可能会或可能不会,取决于您如何使用 GetVariant()。在当前的实现中,对 returned BSTR 的任何访问都应被视为只读 - 调用者 不得 在其上调用 SysFreeString() ,并且必须预料到 CVariable 对象的任何更改 可能 会使 BSTR 无效。如果这不符合您的需要,那么 GetVariant() 应该 return 一个新的 VARIANT 已经通过 VariantCopy() 复制了数据,然后调用者可以调用 VariantClear()使用 returned VARIANT.

  6. 完成后

话虽如此,试试这样的东西:

class CVariable  
{
public:
    CVariable(const CString &strData, int nNum);
    CVariable(BSTR bsData);
    CVariable(const CVariable &src);
    ~CVariable();

    VARIANT GetVariant() const;

    CVariable& operator=(const CVariable &src);
    CVariable& operator=(BSTR src);

private:
    VARIANT m_bsVa;
    VARIANT m_nVa;
};

CVariable::CVariable(const CString &strData, int nNum)
{
    ::VariantInit(&m_bsVa);
    m_bsVa.vt = VT_BSTR;
    m_bsVa.bstrVal = ::SysAllocString(strData);

    ::VariantInit(&m_nVa);
    m_nVa.vt = VT_I2;
    m_nVa.lVal = nNum;
}

CVariable::CVariable(BSTR bsData)
{
    ::VariantInit(&m_bsVa);
    m_bsVa.vt = VT_BSTR;
    m_bsVa.bstrVal = bsData;
    /* or this, if needed:
    m_bsVa.bstrVal = ::SysAllocStringLen(bsData, ::SysStringLen(bsData));
    */

    ::VariantInit(&m_nVa);
}

CVariable::~CVariable()
{
    ::VariantClear(&m_bsVa);
    ::VariantClear(&m_nVa);
}

VARIANT CVariable::GetVariant() const
{
    return m_bsVa;
    /* or this, if needed:
    VARIANT result;
    ::VariantInit(&result);
    ::VariantCopy(&result, &m_bsVa);
    return result;
    */
}

CVariable& CVariable::operator=(const CVariable &src)
{
    if (&src != this)
    {
        ::VariantClear(&m_bsVa);
        ::VariantCopy(&m_bsVa, &src.m_bsVa);

        ::VariantClear(&m_nVa);
        ::VariantCopy(&m_nVa, &src.m_nVa);
    }

    return *this;
}

CVariable& CVariable::operator=(BSTR src)
{
    ::VariantClear(&m_bsVa);
    m_bsVa.vt = VT_BSTR;
    m_bsVa.bstrVal = src;
    /* or this, if needed:
    m_bsVa.bstrVal = ::SysAllocStringLen(src, ::SysStringLen(src));
    */

    ::VariantClear(&m_nVa);

    return *this;
}

如果您直接使用 variant_t class 而不是 VARIANT,您可以大大简化代码,同时仍然解决上述所有问题:

class CVariable  
{
public:
    CVariable(const CString &strData, int nNum);
    CVariable(BSTR bsData);

    variant_t GetVariant() const;

private:
    variant_t m_bsVa;
    variant_t m_nVa;
};

CVariable::CVariable(const CString &strData, int nNum)
    : m_bsVa(strData), m_nVa(nNum)
{
}

CVariable::CVariable(BSTR bsData)
    : m_bsVa(bsData)
{
}

variant_t CVariable::GetVariant() const
{
    return m_bsVa;
}

我建议您在原始 C VARIANT 周围使用方便的 C++ RAII 包装器,例如来自 ATL 的 CComVariant。

这将简化您的代码,因为 CComVariant 将正确初始化其包装的原始 VARIANT,并清理它。

您可以用更安全的 CComVariant 包装器替换您的 VARIANT 数据成员:

CComVariant m_bsVa;
CComVariant m_nVa;

然后你可以像这样在构造函数中初始化它们:

CVariable::CVariable(const CString& strData, int nNum)
    : m_bsVa(strData), m_nVa(nNum)
{}

CVariable::CVariable(BSTR bsData)
    : m_bsVa(bsData)
{}

请注意,您无需显式定义析构函数,因为在这种情况下,CComVariant 的析构函数将正确清理数据成员。

您的 getter 可以这样实现:

const CComVariant& CVariable::GetVariant() const
{
    return m_bsVa;
}