C++ string类的简单实现(1)

http://coolshell.cn/articles/10478.html

成员变量

data_ : 指向一个’\0’结尾的字符串
length_: 占用空间长度(包含’\0’)

成员函数

实现了:默认构造函数、拷贝构造函数、拷贝赋值操作符、析构函数、移动拷贝构造函数、移动赋值操作符。最后总结了copy and swap技术。

对于拷贝赋值操作符注意:
0. 保证异常安全:new可能抛出异常时,保证原对象仍然有效
1. 处理自赋值:先new一个副本,再delete自己。能够正确处理自赋值,也能保证异常安全
2. 返回引用:遵循这一习惯,与标准库行为保持一致

#include <sys/types.h>
#include <string.h>
#include <iostream>
#include <vector>
#include <algorithm>

class MyString 
{
    friend std::ostream& operator<< (std::ostream& os, const MyString& ms);
 public:
    MyString() : length_(1), data_(new char[1])
    {
        std::cout << "ctor(default)" << std::endl;
        data_[0] = '\0';
    }

    MyString(const char* str) : length_(strlen(str) + 1), data_(new char[length_])
    {
        std::cout << "ctor(const char*)" << std::endl;
        memcpy(data_, str, length_);
    }

    ~MyString()
    {
        std::cout << "dtor" << std::endl;
        delete[] data_;
    }

    MyString(const MyString& rhs)
        : length_(rhs.length_), data_(new char[length_])
    {
       std::cout << "copy ctor" << std::endl;
       memcpy(data_, rhs.data_, length_) ;
    }

    MyString& operator= (const MyString& rhs)
    {
        std::cout << "copy assign" << std::endl;
        size_t newlength = rhs.length_;
        char* newdata = new char[newlength];
        memcpy(newdata, rhs.data_, newlength);

        delete[] data_;
        data_ = newdata;
        length_ = newlength; 

        return *this;
    }

    const char* c_str() const {
        return const_cast<const char*>(data_);
    }

    size_t length() const {
        return length_;
    }

 private:
    size_t length_;
    char* data_;       
};

std::ostream& operator<< (std::ostream& os, const MyString& ms)
{
    os << "data: \"" << ms.c_str() << "\" "
       << "length: " << ms.length() << std::endl;
    return os;
}

int main()
{
    MyString ms; // 默认构造函数
    std::cout << ms ;

    ms = "hello world"; // 先通过隐式类型转换生成一个临时的对象,然后进行copy assign,最后临时对象被析构
    std::cout << ms ;

    MyString ms2(ms); // copy ctor

    std::cout << "--------------" << std::endl;

    std::vector<MyString> vec; // 空的vector
    vec.push_back(ms); // vector先分配空间,大小为1,然后通过copy ctor放入ms的副本
    vec.push_back(ms2); // vector 发现空间不足,重新分配空间(扩大一倍,变为2),然后拷贝原始空间的对象到新空间,接着将原始空间对象析构,最后将ms2拷贝到vector中。
    vec.push_back("network"); // 先是隐式类型转换构造一个临时变量,vector发现空间不足,进行空间扩容(变为4,伴随着旧空间对象的拷贝和释放),最后将临时对象拷贝到vector,然后临时对象析构。

    std::cout << "--------------" << std::endl;
    for_each(vec.begin(), vec.end(), [](const MyString &s) { std::cout << s; });

    //std::vector<MyString>::const_iterator it;
    //for(it = vec.begin(); it != vec.end(); it++) {
    //    std::cout << *it;
    //}
    return 0; // 最后 ms、ms2、vector中的对象被析构
}

输出:

ctor(default)
data: "" length: 1
ctor(const char*)
copy assign
dtor
data: "hello world" length: 12
copy ctor
--------------
copy ctor
copy ctor
copy ctor
dtor
ctor(const char*)
copy ctor
copy ctor
copy ctor
dtor
dtor
dtor
--------------
data: "hello world" length: 12
data: "hello world" length: 12
data: "network" length: 8
dtor
dtor
dtor
dtor
dtor

可能对于输出一眼看不出来,和注释相结合着看应该没什么问题。

为了避免临时对象的创建,可以对operator=进行重载:

    MyString& operator= (const char* s)
    {
        std::cout << "copy assign(const char*)" << std::endl;
        delete[] data_;
        length_ = strlen(s) + 1;
        data_ = new char[length_];
        memcpy(data_, s, length_);
        return *this;
    }

定义了该函数后,对const char*的赋值将通过精确匹配到该函数,而不必经过临时对象的创建。

移动构造函数

在某些情况下,对象拷贝完后就立即被销毁了,那么移动代替拷贝将提升性能。c++11引入了右值引用的概念,即绑定到右值的引用。这里给出移动构造函数的实现,移动操作如果不抛出异常需要加上noexcept

/* move ctor */
MyString(MyString&& s) noexcept : length_(s.length_), data_(s.data_)
{
    std::cout << "move ctor" << std::endl;
    s.length_ = 0;
    s.data_ = nullptr;
}

测试代码:

    MyString ms("move me");
    MyString ms2(ms); // copy ctor
    MyString ms3(std::move(ms)); // move ctor
    std::cout << ms;
    std::cout << ms2;
    std::cout << ms3;

这里使用了std::move获取了一个右值,因此会调用移动构造函数,直接“接管”资源。

有了移动构造函数之后,在将一个临时对象push到vector中的时候将调用移动构造函数,而不是拷贝构造函数,效率得到提升。

vec.push_back("network"); // 先创建一个临时对象,然后使用移动构造函数直接接管资源,省去了复制的过程。

移动拷贝赋值运算符

/* move copy assign */
MyString& operator= (MyString&& rhs) noexcept
{
    std::cout << "move copy assign" << std::endl;
    if(this != &rhs) { // 检测自赋值
        delete[] data_; // 释放原来的资源
        length_ = rhs.length_; // 接管
        data_ = rhs.data_;
        rhs.length_ = 0; // 将rhs置于可析构的状态
        rhs.data_ = nullptr;
    }
    return *this; // 返回引用
}

copy and swap

在编写拷贝赋值运算符时需要考虑异常安全(new可能抛出异常bad_alloc,这时需要保证this的数据不被污染保持原样)、自赋值(如果先释放资源那么自赋值时会出现问题)等问题。

在为类编写移动拷贝构造函数时,可以使用copy and swap技术,使用一个函数整合copy assign 和 move copy assign:

/* copy and swap */
MyString& operator= (MyString rhs)
{
    swap(rhs);
    return *this;
}

void swap(MyString& rhs)
{
    std::swap(data_, rhs.data_);
}

MyString只重载了一个operator=,它的参数是pass-by-value的。what?这么做不是会进行拷贝吗?

惊奇过后,你会发现,operator=的老版本中也进行了拷贝,既然拷贝是不可避免的,那么将参数设计成pass-by-value也是没有问题的,反而简化了代码。

当参数是左值时,将使用拷贝构造函数创建rhs;如果是右值,则使用移动拷贝构造函数。然后使用swap交换数据即可。这么做是异常安全的,因为new操作是在参数列表中进行的,抛出异常时,原始数据不会被污染;而且这么做也能处理“自赋值”,因为先做了一份拷贝嘛。

此外,要注意这么做对于右值引用会多一对儿ctor和dtor,但是右值引用的移动构造函数做的是“接管”资源,开销不是很大。

关于std::move

关于“引用折叠”和std::move见《c++ primer 5th》16.2.5和16.2.6

Return by value 会不会有额外的copy等开销?

https://isocpp.org/wiki/faq/ctors#return-by-value-optimization

C++ FAQ 指出,经过编译器的优化,不会有额外的开销。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++中的string是一个非常常用的字符串,其实现原理比较复杂,需要涉及到动态内存分配、指针、操作符重载等多个知识点。下面是一个简单string实现: ```c++ class String { private: char* data; // 存储字符串的字符数组 int length; // 字符串的长度 public: String(); // 默认构造函数 String(const char* str); // 带参数构造函数 String(const String& str); // 复制构造函数 ~String(); // 析构函数 int size() const; // 获取字符串长度 char& operator[](int index); // 重载下标操作符 const char& operator[](int index) const; // 重载下标操作符(常量版本) String& operator=(const char* str); // 重载赋值操作符 String& operator=(const String& str); // 重载赋值操作符 String operator+(const char* str) const; // 重载加法操作符 String operator+(const String& str) const; // 重载加法操作符 bool operator==(const char* str) const; // 重载等于操作符 bool operator==(const String& str) const; // 重载等于操作符 friend ostream& operator<<(ostream& os, const String& str); // 重载输出流操作符 }; String::String() { data = new char[1]; data[0] = '\0'; length = 0; } String::String(const char* str) { if (str == nullptr) { data = new char[1]; data[0] = '\0'; length = 0; } else { length = strlen(str); data = new char[length + 1]; strcpy(data, str); } } String::String(const String& str) { length = str.length; data = new char[length + 1]; strcpy(data, str.data); } String::~String() { delete[] data; } int String::size() const { return length; } char& String::operator[](int index) { return data[index]; } const char& String::operator[](int index) const { return data[index]; } String& String::operator=(const char* str) { if (data != str) { delete[] data; if (str == nullptr) { data = new char[1]; data[0] = '\0'; length = 0; } else { length = strlen(str); data = new char[length + 1]; strcpy(data, str); } } return *this; } String& String::operator=(const String& str) { if (&str != this) { delete[] data; length = str.length; data = new char[length + 1]; strcpy(data, str.data); } return *this; } String String::operator+(const char* str) const { String newStr; newStr.length = length + strlen(str); newStr.data = new char[newStr.length + 1]; strcpy(newStr.data, data); strcat(newStr.data, str); return newStr; } String String::operator+(const String& str) const { String newStr; newStr.length = length + str.length; newStr.data = new char[newStr.length + 1]; strcpy(newStr.data, data); strcat(newStr.data, str.data); return newStr; } bool String::operator==(const char* str) const { if (strcmp(data, str) == 0) { return true; } else { return false; } } bool String::operator==(const String& str) const { if (strcmp(data, str.data) == 0) { return true; } else { return false; } } ostream& operator<<(ostream& os, const String& str) { os << str.data; return os; } ``` 上述实现中,我们使用了动态内存分配来存储字符串的字符数组,同时重载了常用的操作符,如赋值操作符、加法操作符、等于操作符和输出流操作符等。需要注意的是,在使用重载操作符时,我们需要保证其语义与原始操作符一致,这样才能保证程序的正确性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值