前言
我们前面的章节,已经介绍了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栈帧中的参数域中参数变量。内存状态图如下
然后在构造函数执行的操作,也就是说我们通过参数F和L来初始化m_first和m_last,意味着我们另外为m_first和m_last创建了这些临时对象的副本。
我们创建了Engineer对象e,然后删除临时参数F和L伴随着临时参数F和L内部各自内部指针指向的堆内存块也会被释放
所以正确的答案是在我们执行了4次的malloc,先是创建了两次string对象作为临时参数,后是在Engineer对象的构造中两次拷贝string对象到其Engineer对象的数据成员m_first和m_last。
如果你对我之前有疑问的话,下面是示例代码测试的输出,能说明很多过中的C++内存管理的知识点,前提是你对堆和栈有一个基本的了解。
后记
这种情况并不太好。我们该如何改善呢?由于本篇开始之后是针对字符串的优化,这里先卖个关子,敬请期待。