字符串复制的优化策略(C++实现)

字符串复制

  • 深拷贝
  • 写时复制
    • 简介
    • COW的原理(以String类来说明)
    • 代码实现
  • 短字符串优化(SSO)
  • FBString

深拷贝

浅拷贝只是拷贝内存空间,而深拷贝是拷贝内存空间的内容。

int main()
{
	//浅拷贝
    char* c1 = "abc";
	char* c2 = c1;
    cout << c1 << endl << static_cast<const void*>(c1) << endl;
    cout << c2 << endl << static_cast<const void*>(c2) << endl;
    
	//深拷贝
	char* c3 = "def";
	char* c4 = new char[4]();
	strcpy(c4, c3);
    cout << c3 << endl << static_cast<const void*>(c3) << endl;
    cout << c4 << endl << static_cast<const void*>(c4) << endl;

    return 0;
}

写时复制

简介

Scott Meyers推荐我们,在真正需要一个存储空间时才去声明变量(分配内存),这样会得到程序在运行时最小的内存花销。写时复制(Copy-On- Write)技术,是编程界“懒惰行为”——拖延战术的产物。C++曾在性能问题上被广泛地质疑和指责过,为了提高性能,STL中的许多类都采用了Copy-On-Write技术。这种偷懒的行为的确使用STL的程序有着比较高的性能。

COW的原理(以String类来说明)

Copy-On-Write一定使用了“引用计数”,必然有一个变量类似于ref_count。当第一个String对象str1构造时,String的构造函数会根据传入的参数从堆上分配内存。当有其它String对象复制str1时,这个ref_count会自动加1;当有对象析构时,这个计数会减1;直到最后一个对象析构时,ref_count为0。此时,程序才会真正的释放这块从堆上分配的内存。

代码实现

#include 
#include //用到字符串的操作
#include //用到printf

using namespace std;

#define SIZEOF_INT sizeof(int)

class String
{
private:
    class CharProxy//用一个char的代理类实现String类的[]重载操作
    {
    public:
        CharProxy(size_t index, String & str)
            :m_index(index)
             ,m_str(str)
        {}

        char & operator=(const char & ch);

        friend ostream & operator<<(ostream & os, const CharProxy & rhs);
    private:
        size_t m_index;
        String & m_str;
    };

public:
    String();
    ~String();
    String(const char*);
    String(const String & rhs);
    String & operator=(const String & rhs);

    const char & operator[](size_t index) const{ return m_pstr[index]; }
    CharProxy operator[](size_t index);

    size_t get_ref_count()
    {
        return (*(int*)(m_pstr - SIZEOF_INT));
    }

    size_t size()
    {
        return strlen(m_pstr);
    }

    const char* c_str() const
    {
        return m_pstr;
    }

    friend ostream & operator<<(ostream & os, const String & rhs);
    friend ostream & operator<<(ostream & os, const CharProxy & rhs);

private:
    void init_ref_count()
    {
        (*(int*)(m_pstr - SIZEOF_INT)) = 1;
    }

    void increase_ref_count()
    {
        (*(int*)(m_pstr - SIZEOF_INT)) ++;
    }

    void decrease_ref_count()
    {
        (*(int*)(m_pstr - SIZEOF_INT )) --;
    }

    void release_ref_count()
    {
        decrease_ref_count();
        size_t refc = get_ref_count();
        if(refc == 0)
        {
            cout << "function:release_ref_count()" << endl;
            delete [] (m_pstr - SIZEOF_INT);
        }
    }

private:
    char* m_pstr;
};

//重载String的输出流运算符
ostream & operator<<(ostream & os, const String & rhs)
{
    os << rhs.m_pstr;
    return os;
}

//重载CharProxy的输出流运算符
ostream & operator<<(ostream & os, const String::CharProxy & rhs)
{
    os << rhs.m_str.m_pstr[rhs.m_index];
    return os;
}

String::String()
    :m_pstr(new char[SIZEOF_INT + 1]())//前SIZEOF_INT个字节用于存放引用计数,最后1个字节存放\0,下同
{
    cout << "function:String()" << endl; 
    m_pstr += SIZEOF_INT;//初始化时移动指针到第五个字节,然后开始存放字符串,下同
    init_ref_count();
}

String::String(const char* pstr)
    :m_pstr(new char(strlen(pstr) + SIZEOF_INT + 1))
{
    cout << "function:String(const char*)" << endl;
    m_pstr += SIZEOF_INT;
    init_ref_count();
    strcpy(m_pstr, pstr);
}

String::String(const String & rhs)
    :m_pstr(rhs.m_pstr)
{
    cout << "function:String(const String & rhs)" << endl;
    increase_ref_count();
}

String & String::operator=(const String & rhs)
{
    if(this != &rhs)
    {
        release_ref_count();//先处理原String的引用计数
        m_pstr = rhs.m_pstr;//然后指针赋值
        increase_ref_count();//最后,现在的String的引用计数加1
    }
    return *this;//返回自身引用
}

/* 
 * 下标访问运算符无法区分是进行读操作还是写操作
 * 所以在用的时候,都会进行深拷贝
 * 我们用CharProxy来区分读操作和写操作
 * 1.读操作用CharProxy的<<重载来实现
 * 2.写操作用String的[]重载和CharProxy的=重载来实现
 */
String::CharProxy String::operator[](size_t index)
{
    return CharProxy(index, *this);
}

char & String::CharProxy::operator=(const char & ch)
{
    if(m_str.get_ref_count() > 1)
    {
        m_str.decrease_ref_count();
        char* p_temp = new char[m_str.size() + SIZEOF_INT + 1]();//深拷贝
        strcpy(p_temp + SIZEOF_INT, m_str.m_pstr);
        m_str.m_pstr = p_temp;
        m_str.m_pstr += SIZEOF_INT;
        m_str.init_ref_count();
    }
    m_str.m_pstr[m_index] = ch;
    return m_str.m_pstr[m_index];
}

String::~String()
{
    cout << "function:~String()" << "  str = " << m_pstr << endl;
    release_ref_count();
}

void test1()
{
    String s1;
    String s2(s1);
    cout << "s1 = " << s1 << endl;
    cout << "s2 = " << s2 << endl;
    cout << "s1 ref count = " << s1.get_ref_count() << endl;
    cout << "s2 ref count = " << s2.get_ref_count() << endl;
    printf("s1 addr = %p\n", s1.c_str());
    printf("s2 addr = %p\n", s2.c_str());

}

void test2()
{
    String s3("hello 333");
    String s4(s3);
    String s5("hello 555");
    /* s3 = s5; */
    s5 = s3;
    s5[0] = 'H';
    cout << "s3 = " << s3 << endl;
    cout << "s5 = " << s5 << endl;
    cout << "s3 ref count = " << s3.get_ref_count() << endl;
    cout << "s5 ref count = " << s5.get_ref_count() << endl;
    printf("s3 addr = %p\n", s3.c_str());
    printf("s5 addr = %p\n", s5.c_str());

   	cout << endl <<"执行读操作..." << endl << "s4[0] = " << s4[0] << endl;
	cout << "s3' refcount = " << s3.get_ref_count() << endl;
	cout << "s4' refcount = " << s4.get_ref_count() << endl;
	cout << "s5' refcount = " << s5.get_ref_count() << endl;
}

int main()
{
    //test1();
    test2();
}

短字符串优化(SSO)

在SSO策略中,拷贝均是深拷贝,即立即复制字符串。其优点是,当字符串的字节比较少的时候,直接将该字符串存在栈中,而在栈上,开辟空间速度非常快。当字符串的字节较多时,才在堆上开辟空间。如:

int main()
{
    string s1 = "abcdefghijklmno";//15个字符
    string s2(s1);
    string s3 = "abcdefghijklmnop";//16个字符
    string s4(s3);

	//从下面的输出可以看出s1和s2都是在栈空间上的,而s3和s4是在堆空间。
	cout << "s1 addr = " << static_cast<const void*>(s1.c_str()) << endl;
	cout << "s2 addr = " << static_cast<const void*>(s2.c_str()) << endl;
	cout << "s3 addr = " << static_cast<const void*>(s3.c_str()) << endl;
	cout << "s4 addr = " << static_cast<const void*>(s4.c_str()) << endl;

	return 0;
}

FBString

facebook的开源库folly中的FBString的实现采用了上述的三种方式:

  1. 0-23字节用SSO策略。存储在栈缓冲区。
  2. 24-254字节用深拷贝。存储在堆空间。
  3. 大于254字节的用写时复制。

你可能感兴趣的:(C++)