第5篇:C++高效使用字符串

前言

我们前面的章节,已经介绍了string的内部原理,接下来的篇章会介绍如何高性能地使用string,光是用一些简单函数来说明string容器的内部原理是远远不够的,本篇我们来看看下面的一个更为复杂的例子,我们知道在类的实例化中经常用到string类型的参数,许多入门的C++书籍基本上不会谈及用户自定义类中string对象的内存操作,本篇开始的后续篇章属于《C++ string》深挖系列.后续文章会打算使用打赏模式,如果关注我的简友信赖我的话,我会以高质量的文章服务各大C++读者。

这是一个非Trivial类 Engineer,它有三个数据成员m_first,m_last和m_id。这个类有什么问题吗?其他入门的C++读物不会考虑这些问题,我们开始点出这些问题吧。那么在此之前,我们先看一个类子

#include 

class Engineer{
    private:
        std::string m_first;
        std::string m_last;
        int m_id;

    public:
        Engineer(const std::string& F,const std::string& L="",int i=0):
        :m_first(F),m_last(L),m_id(i){}
}

首先,我们实现构造函数的方式是C++98的形式,这些绝大多数C++读物使用到形式。初始化的m_first,m_last和m_id都有默认值。我们将所有参数都用常量引用代替,因为按值传参是我们目前这里比较快的方式。然后我们使用这些参数来初始化数据成员。因此让我们创建一个Engineer对象。

下面是示例代码是全局作用域中用于跟踪堆内存的测试代码

#include "source/cust.cpp"
#include 

static uint32_t s_allocCount = 0;

void *operator new(size_t size)
{
    s_allocCount++;
    std::cout << "第" << s_allocCount << "次- 堆分配 "
              << size << " bytes" << std::endl;
    return malloc(size);
}
//即便是测试代码,也要养成有new必有delete的好习惯
void operator delete(void *p)
{
    std::cout << "释放" << p << std::endl;
    free(p);
}

当我们尝试调用字符串时,字符串长度超过使用SSO的规定都会发生堆内存分配的操作。那你估算过潜在的性能开销吗?

首先,读者思考以下实例代码,所有string类型的对象一共发生了多少次malloc,这些问题关系到你在std::string的基本运行原理是否有个正确的理解,如果你毫无概念,请先阅读我有关之前的文章。

调用示例1有多少次malloc操作?

int main(int argc, char const *argv[])
{
    Engineer c{"Yuna-Lasca-Friday ", " Alexica-JP-Sunny", 42};
    return 0;
}

调用示例2有多少次malloc操作?

int main(int argc, char const *argv[])
{
     Engineer  c{"Yuna ", " Alex", 42};
    return 0;
}

我这里只针对第一个问题,第二个问题我不想再解析,请看我之前的相关文章。

我的意思SSO无法执行优化的情况下,调用Engineer构造函数或复制赋值运算符,其Engineer构造内部的string参数会执行多少次拷贝操作呢?当这种情况发生了该如何避免这些高昂的性能开销呢?我们看看示例代码中的这些文本在程序初始化阶段在数据段中生成,我们将这两个字符串文本"Yuna-Lasca-Friday"和“Alexica-JP-Sunnyy”存储系统内存布局中的字面量池中。

分析第一个问题其实就是要分两种状态分析,首先,向构造传递匿名字符串之时,但未对Engineer的数据成员进行赋值。

Engineer e{"Yuna-Lasca-Friday ", " Alexica-JP-Sunny", 42};

我们在构造函数中有两个参数F和L,传入的参数的类型不适合构造函数中的参数类型。向构造函数传递参数,将进行隐式类型转换,这意味着我们隐式地在main栈帧中创建了两个临时变量sttring对象,又由于这些临时变量内部无法满足SSO优化,于是就触发堆内存操作(malloc操作),从内存地址的分布来看这两个临时变量恰好是main栈帧中的参数域中参数变量。内存状态图如下

ss17.png

然后在构造函数执行的操作,也就是说我们通过参数F和L来初始化m_first和m_last,意味着我们另外为m_first和m_last创建了这些临时对象的副本。


ss18.png

我们创建了Engineer对象e,然后删除临时参数F和L伴随着临时参数F和L内部各自内部指针指向的堆内存块也会被释放


临时参数string对象会隐式地被删除

所以正确的答案是在我们执行了4次的malloc,先是创建了两次string对象作为临时参数,后是在Engineer对象的构造中两次拷贝string对象到其Engineer对象的数据成员m_first和m_last。

如果你对我之前有疑问的话,下面是示例代码测试的输出,能说明很多过中的C++内存管理的知识点,前提是你对堆和栈有一个基本的了解。


后记

这种情况并不太好。我们该如何改善呢?由于本篇开始之后是针对字符串的优化,这里先卖个关子,敬请期待。

你可能感兴趣的:(第5篇:C++高效使用字符串)