目录

  • 1. 简易string(无增删查改)
    • 1.1 深浅拷贝
  • 2. string的传统写法
    • 2.1 四个默认成员函数
  • 3. string的现代写法
    • 3.1 构造函数与析构函数
    • 3.2 拷贝构造
    • 3.3 赋值重载
    • 3.4 std中的swap与string中的swap
  • 4. 修改
  • 5. 增加
  • 6. 删除
  • 7. 查找
  • 8. 重载的全局函数
  • 最终代码

1. 简易string(无增删查改)

开始必须重新定义一个命名空间,把我们自定义的string放进命名空间里。

假如不考虑增删查改,我们先可以做一个简单的string,几乎下意识的就写出了这样的构造函数

string(char* str):_str(str){}

这是非常不合理的,假如外面传入的是一个常量字符串。

string s("1234");

假如要修改常量字符串的话,会直接中断。
那我们就开一个和他同样大小的空间,再把字符拷贝进去。使用new开辟后面还可以动态增长。

namespace zjn
{class string{public:string(char* str):_str(new char[strlen( str)+1]){strcpy(_str, str);}~string(){delete[] _str;}private:char* _str;};
}

有时我们也会定义无参的string。那么这样写可以吗,我们配合一个接口c_str进行验证

         string():_str(nullptr){;}
const char* c_str()
{return _str;
}
int main()
{zjn::string s1("1234");zjn::string s2;cout << s1.c_str() << endl;cout << s2.c_str ()<< endl;return 0;}

cout是自动识别类型,是因为他重载了各种类型的函数,打印的原理无非就是遇到‘\0’停止,可是在s2由于我们构造的时候使用nullptr构造的,所以cout会解引用进行访问,就会崩溃。(析构函数时delet[]所包含的free(NULL)并不会报错

所以正确的做法应该是

         string():_str(new char[1]){_str[0] = '\0';}

其实只要写成默认构造函数就好了。这样就不用写两个构造函数

     string(char* str=""):_str(new char[strlen( str)+1]){strcpy(_str, str);}

1.1 深浅拷贝

在学习拷贝构造的时候,我们讲到一个stack对象的拷贝,自动生成的构造函数只是浅拷贝,两个指针指向的是同一块区域,所以执行析构函数的时候会将同一块资源释放两次。


所以默认生成的满足不了需求,必须自己写一个拷贝构造函数来实现深拷贝。

#include<iostream>
#include<assert.h>
#pragma warning(disable:4996)
using namespace std;namespace zjn
{class string{public:string(char* str=""):_str(new char[strlen( str)+1]){strcpy(_str, str);}~string(){delete[] _str;_str = nullptr;}const char* c_str(){return _str;}string(const string& s):_str(new char[strlen(s._str)+1]){strcpy(_str,s._str);}char&  operator[](size_t i){assert(i < _size);return _str[i];}//释放原空间,然后深拷贝string& operator=(const string& s){if (this!=&s){delete[] _str;//不能直接strcpy因为空间可能不够_str = new char[strlen(s._str) + 1];strcpy(_str, s._str);}return *this;}private:char* _str;};
}
int main()
{zjn::string s1("1234");zjn::string s2;zjn::string s3(s1);s3[1] = 'x';cout << s1.c_str() << endl;cout << s2.c_str ()<< endl;return 0;
}

2. string的传统写法

一个真正的string是要支持增删查改的,所以必须要有size和capcacity。
size和capacity是内置类型几乎不需要注意什么地方

2.1 四个默认成员函数

     /*string(char* str):_str(new char[strlen(str)+1]),_size(strlen( str)), _capacity(_size){}*/string(char* str=""){_str=new char[strlen(str)+1];_size = strlen(str);_capacity = _size;strcpy(_str, str);}string(const string& s){_size = s._size;_capacity = s._capacity;_str = new char[strlen(s._str) + 1];strcpy(_str, s._str);}~string(){delete[] _str;_str = nullptr;}string& operator=(const string& s){if (this != &s){delete[] str;_str = new char[strlen(s._str) + 1];_capacity = s._capacity;_size = s._size;}return *this;}
  • 默认构造函数:当默认构造函数选择为为参数列表声明时,最好不要写有依赖关系的,因为实际初始化的顺序只和声明的顺序有关。所以我们直接选择写在函数体内
  • 拷贝构造函数:需要深拷贝实现,即重新开一段空间,把数据拷贝过来
  • 重载赋值函数:需要注意自己给自己赋值的情况,释放原空间进行深拷贝,假如是自己给自己赋值的话,原空间释放,指针指向新开的一段空间,strlen时遇到’\0’停止,那段空间是随机值。

3. string的现代写法

3.1 构造函数与析构函数

     string(const char* str=""){_size = strlen(str);_capacity = _size;_str = new char[_size + 1];strcpy(_str, str);}
         ~string(){delete[] _str;_str = nullptr;_capacity = _size=0;}
  • 构造函数与析构函数没有什么差别。

3.2 拷贝构造

一开始是这么写的

     string(const string& s){.//局部对象拥有相同的str,用于和this._str进行交换string temp(s._str);std::swap(_str,temp._str);std::swap(_size,temp._size);std::swap(_capacity,temp._capacity);}

很明显是有错误的,因为_str没有初始化是一个野指针。当与temp._str进行交换时,_str指向temp._str那块空间。temp._str指向那块随机空间,当出了函数体,调用析构函数,清理资源_str,free掉temp._str,就会报错,即free了一个野指针。

        string(const string& s):_str(nullptr),_size(0),_capacity(0){string temp(s._str);std::swap(_str,temp._str);std::swap(_size,temp._size);std::swap(_capacity,temp._capacity);}

那么我们将它初始化成nullptr就好了。

3.3 赋值重载

string& operator=(const string& s){if (this != &s){string temp(s._str);std::swap(_str,temp._str);std::swap(_size,temp._size);std::swap(_capacity,temp._capacity);}return *this;}


交换了之后,出了函数体,调用析构,刚好也会清理掉不要的"worldx"那块资源。

其实还有一种更简洁的写法

      //参数不能加const因为我们会修改s,不能传引用因为我们修改了s,传引用外面的也会修改string& operator=( string s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);return *this;}

s就代替了之前的temp对象,且s也是一个局部对象,也会调用析构函数。
可以看到存在了大量的swap代码复用,我们可以将swap写成一个函数。然后对所有代码进行修改。

string(const char* str=""){_size = strlen(str);_capacity = _size;_str = new char[_size + 1];strcpy(_str, str);}// std中的swap并不好void swap(string& s){std::swap(_str, s._str);std::swap(_size,s._size);std::swap(_capacity, s._capacity);}//拷贝构造//string s1(s2);string(const string& s):_str(nullptr), _capacity(0), _size(0){string temp(s._str);swap(temp);}~string(){delete[] _str;_str = nullptr;_capacity = _size=0;}string& operator=( string s){swap(s);return *this;}

这就是一个有资源管理的string的现代写法。

3.4 std中的swap与string中的swap

那么为什么不直接调用库中的swap交换两个对象,而是用库中的swap一个个交换属性,最后将他们封装成一个专门的swap呢?
swap(s1,s2);
s1.swap(s2);
这两种差异是特别大的。
std中的swap是一个由函数模板推演而来的模板函数。其中经历了一次拷贝构造,两次赋值。代价太大,效率会大打折扣。

4. 修改

         //[]重载char& operator[](size_t i){assert(i < _size);return _str[i];}//[]重载(const对象调用)const char& operator[](size_t i)const{assert(i < _size);return _str[i];}//迭代器typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str+_size;}//迭代器(const对象)typedef const char* const_iterator;const_iterator begin()const{return _str;}const_iterator end()const{return _str + _size;}//范围forfor (auto& e : s){e += 1;}
  • 重载[]:需要注意,i需要小于size,size作为下表的话是’\0’的位置。
  • string 的迭代器实际就是一个指针。typedef为iterator是为了和其它接口保持一致,假如你是一个链表就不能继续使用指针了,因为指针指向那个节点,解引用之后是一个结构体,并不是节点的值。所以在链表中迭代器实际是这样定义的。
struct ListIterator{operator*();operator++();Node* node;};

重载他的*和++来达到目的。

  • 范围for:会自动转换为迭代器,也就是说自动调用begin(),与end()。

5. 增加

          void resize(size_t n, char ch='\0'){//_size变小,capacity不变,直接在n处放'\0'if (n<_size){_str[n] ='\0';_size = n;}else{if (n>_capacity){reserve(n);}//1 2 3 4 for (size_t i = _size; i < n; i++){_str[i] = ch;}_str[n] = '\0';_size = n;}}//增加就可能会引起扩容void reserve(int  n){if (n > _capacity){//需要temp不然直接delete的话字符串的内容会丢失char* temp = new char[n + 1];strcpy(temp, _str);delete[] _str;_str = temp;_capacity = n;}}//字符尾插string& push_back(char ch){if (_size == _capacity){reserve(2 * _capacity);}_str[_size] = ch;_size++;_str[_size] = '\0';return *this;}//字符串拼接void append(const char* str){int len = strlen(str);if (len + _size> _capacity){reserve(len + _size);}//忘记strcpy,参数忘记+_sizestrcpy(_str+_size, str);_size += len;}//字符+=string& operator+=(char ch){push_back(ch);return *this;}//字符串+=string& operator+=(const char* str){append(str);return *this;}//string对象+=string& operator+=(const string& s){append(s._str);return *this;}//插入字符string& insert(size_t pos, char ch){assert(pos <= _size);if (_size == _capacity){size_t newcapacity = _capacity == 0 ? 8 : 2 * _capacity;reserve(newcapacity);}/*    逻辑没有问题,但是当pos等于0时,要把第一位往前挪,end--就会变成-1,才能由于类型为size_t他就会变为整形最大值。假如把end变为int类型也避免不了,因为pos为size_t,身为int类型的end会类型提升为size_t,依旧是整形最大值。那把end变成int类型,再把pos显示的转为int(pos不要直接改成int与库不相符),这样是可以的。size_t end = _size;while(end>=pos){_str[end + 1] = _str[end];end--;}*///虽然_size是'\0',但是_size+1不会越界因为假如空间不够前面会扩容,数组是有这么大空间的,只不过没有存字符。//这样当pos等于0时,end为1字符已经全部挪走,end--为0,此时不会再进入循环,也就不会变成-1了。size_t end = _size + 1;while (end > pos){_str[end] = _str[end - 1];end--;}_str[pos] = ch;_size++;return *this;}//插入字符串string& insert(size_t pos, const char* str){assert(pos <= _size);int len = strlen(str);if (_size + strlen(str) > _capacity){reserve(_size+len);}int end = _size + len;//画图分析//避免类型提升while (end  >= (int)pos+len){_str[end] = _str[end - len];end--;}strncpy(_str + pos, str, len);_size += len;return *this;}
  • reserve:只要增加就可能会有扩容的场景。
  • push_back:追加一个字符
  • append:拼接一个字符串。
  • +=:分别有字符,字符串,string三种函数
  • insert:push_back,append,+=可以复用这个插入函数,但是要注意类型提升问题。

6. 删除

//删除,和库类似,我们给一个npos缺省值void erase(size_t pos, size_t len = npos){//从pos起,删除len个字符。assert(pos < _size);//len为npos的时候会溢出//if (pos+len>_size)////假如要求删的超过字符串,a b c d,要求从c删除100个,就把d置成'\0'就行了。if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;}else{//直接拷过去,然后覆盖,'\0'也会拷贝过去strcpy(_str + pos, _str + pos + len);_size -= len;}}
  • erase,注意溢出,直接使用strcpy覆盖。

7. 查找

        //查找字符,缺省默认从第一个位置开始size_t find(char ch, size_t pos = 0){for (size_t i = 0; i < _size; i++){if (ch == _str[i]){return i;}}return npos;}//查找子串size_t find(const char* sub, size_t pos = 0){//在_str+pos这个字符串里找subconst char* ret = strstr(_str + pos, sub);if (nullptr == ret){return npos;}else{return ret - _str;}}
  • find: 一个查找字符,一个查找子串,缺省值都是从第一个位置开始查找

8. 重载的全局函数

//大于重载bool operator>(const string& s1, const string& s2){size_t l1=0, l2 = 0;while (l1 < s1.size() && l2 < s2.size()){if (s1[l1] == s1[l2]){l1++;l2++;}else{if (s1[l1]>s2[l2]){return true;}else{return false;}}}//假如有一个长一个短if (l1 < s1.size()){return true;}else if (l2 < s2.size()){return false;}else{return false;}}//==重载bool operator==(const string& s1, const string& s2){size_t l1 = 0, l2 = 0;while (l1 < s1.size() && l2 < s2.size()){if (s1[l1] == s1[l2]){l1++;l2++;}else{return false;}}//假如有一个长一个短if (l1 < s1.size()){return false;}else if (l2 < s2.size()){return false;}else{return true;}}//输出std::ostream& operator<<(std::ostream& out, const string& s){for (size_t i = 0; i < s.size(); i++){out << s[i] ;}return out;}//输入std::istream& operator>>(std::istream& in,  string& s){char ch;while (1){//这样不行,默认空格为下一个,不能123这样输,只能1 2 3//in >> ch;in.get(ch);if ( ch==' '||ch=='\n'){break;}else{s += ch;}}return in;}

最终代码

#include<iostream>
#include<assert.h>#pragma warning(disable:4996)namespace zjn
{class string{public://传统写法构造//string(const char* str = "")//{// _size = strlen(str);// _capacity = _size;//   _str = new char[_capacity + 1];// strcpy(_str, str);//}析构//~string()//{// delete[] _str;//    _str = nullptr;//  _capacity = _size = 0;//}拷贝构造//string(const string& s)//{//   _str = new char[strlen(s._str) + 1];//    strcpy(_str, s._str);// _capacity = s._capacity;// _size = s._size;//}赋值重载//string& operator=(const string& s)//{//  if (this != &s)//  {//     delete[] _str;//        _str = new char[strlen(s._str) + 1];//        strcpy(_str, s._str);//     _size = s._size;//     _capacity = s._capacity;// }// return *this;//}//构造string(const char* str = ""){_size = strlen(str);_capacity = _size;_str = new char[_size + 1];strcpy(_str, str);}//std中的swap并不好void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}//拷贝构造//string s1(s2);string(const string& s):_str(nullptr), _capacity(0), _size(0){string temp(s._str);swap(temp);}//析构~string(){delete[] _str;_str = nullptr;_capacity = _size = 0;}//赋值string& operator=(string s){swap(s);return *this;}size_t size()const{return _size;}//重载[]//返回值忘记引用了char& operator[](size_t i){assert(i < _size);return _str[i];}const char& operator[](size_t i)const{assert(i < _size);return _str[i];}typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}typedef const char* const_iterator;const_iterator begin()const{return _str;}const_iterator end()const{return _str + _size;}//增//扩容+初始化。//当前n<capacity的话,不改变capacity,只改变size。n>capacity,改变size改变capacity。void resize(size_t n, char ch='\0'){//_size变小,capacity不变,直接在n处放'\0'if (n<_size){_str[n] ='\0';_size = n;}else{if (n>_capacity){reserve(n);}//1 2 3 4 for (size_t i = _size; i < n; i++){_str[i] = ch;}_str[n] = '\0';_size = n;}}//扩容void reserve(size_t  n){if (n > _capacity){//需要temp不然直接delete的话字符串的内容会丢失char* temp = new char[n + 1];strcpy(temp, _str);delete[] _str;_str = temp;_capacity = n;}}//字符尾插void push_back(char ch){/*if (_size == _capacity){reserve(2 * _capacity);}_str[_size] = ch;_size++;_str[_size] = '\0';*/insert(_size, ch);}void append(const char* str){//int len = strlen(str);//if (len + _size> _capacity)//{//  reserve(len + _size);//}忘记strcpy,参数忘记+_size//strcpy(_str+_size, str);//_size += len;insert(_size, str);}//字符+=string& operator+=(char ch){push_back(ch);return *this;}//字符串+=,参数忘记conststring& operator+=(const char* str){append(str);return *this;}//string对象+=string& operator+=(const string& s){append(s._str);return *this;}//插入字符void insert(size_t pos, char ch){assert(pos <= _size);if (_size == _capacity){//假如capacity为0一开始开8个对象类型(char)空间size_t newcapacity = _capacity == 0 ? 8 : 2 * _capacity;reserve(newcapacity);}/*    逻辑没有问题,但是当pos等于0时,要把第一位往前挪,end--就会变成-1,才能由于类型为size_t他就会变为整形最大值。假如把end变为int类型也避免不了,因为pos为size_t,身为int类型的end会类型提升为size_t,依旧是整形最大值。那把end变成int类型,再把pos显示的转为int(pos不要直接改成int与库不相符),这样是可以的。size_t end = _size;while(end>=pos){_str[end + 1] = _str[end];end--;}*///虽然_size是'\0',但是_size+1不会越界因为假如空间不够前面会扩容,数组是有这么大空间的,只不过没有存字符。//这样当pos等于0时,end为1字符已经全部挪走,end--为0,此时不会再进入循环,也就不会变成-1了。size_t end = _size + 1;while (end > pos){_str[end] = _str[end - 1];end--;}_str[pos] = ch;_size++;}//插入字符串void insert(size_t pos, const char* str){assert(pos <= _size);int len = strlen(str);if (_size + strlen(str) > _capacity){reserve(_size + len);}int end = _size + len;//画图分析//避免整形提升while (end >= (int)pos + len){_str[end] = _str[end - len];end--;}strncpy(_str + pos, str, len);_size += len;}//删除,和库类似,我们给一个npos缺省值void erase(size_t pos, size_t len = npos){//从pos起,删除len个字符。assert(pos < _size);//len为npos的时候会溢出//if (pos+len>_size)////假如要求删的超过字符串,a b c d,要求从c删除100个,就把d置成'\0'就行了。if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;}else{//直接拷过去,然后覆盖,'\0'也会拷贝过去strcpy(_str + pos, _str + pos + len);_size -= len;}}//查找字符,缺省默认从第一个位置开始size_t find(char ch, size_t pos = 0){for (size_t i = 0; i < _size; i++){if (ch == _str[i]){return i;}}return npos;}//查找子串size_t find(const char* sub, size_t pos = 0){//在_str+pos这个字符串里找subconst char* ret = strstr(_str + pos, sub);if (nullptr == ret){return npos;}else{return ret - _str;}}private:char* _str;size_t _capacity;size_t _size;//声明static const size_t npos;};//定义初始化const size_t string::npos = -1;//大于重载bool operator>(const string& s1, const string& s2){size_t l1=0, l2 = 0;while (l1 < s1.size() && l2 < s2.size()){if (s1[l1] == s1[l2]){l1++;l2++;}else{if (s1[l1]>s2[l2]){return true;}else{return false;}}}//假如有一个长一个短if (l1 < s1.size()){return true;}else if (l2 < s2.size()){return false;}else{return false;}}//==重载bool operator==(const string& s1, const string& s2){size_t l1 = 0, l2 = 0;while (l1 < s1.size() && l2 < s2.size()){if (s1[l1] == s1[l2]){l1++;l2++;}else{return false;}}//假如有一个长一个短if (l1 < s1.size()){return false;}else if (l2 < s2.size()){return false;}else{return true;}}//大于等于bool operator>=(const string& s1, const string& s2){return s1 > s2 || s1 == s2;}//小于bool operator<(const string& s1, const string& s2){return  !(s1>=s2);}//小于等于bool operator<=(const string& s1, const string& s2){return  !(s1>s2);}//输出std::ostream& operator<<(std::ostream& out, const string& s){for (size_t i = 0; i < s.size(); i++){out << s[i] ;}return out;}//输入std::istream& operator>>(std::istream& in,  string& s){//防止对一个原本就有数据的对象输入s.resize(0);char ch;while (1){//这样不行,默认空格为下一个,不能123这样输,只能1 2 3//in >> ch;in.get(ch);if ( ch==' '||ch=='\n'){break;}else{s += ch;}}return in;}}

模拟实现string(传统写法与现代写法)相关推荐

  1. android kotlin中的when语句写法与for写法

    示例代码: fun getScore(name: String) = if (name == "Tom") {85 } else if (name == "AA" ...

  2. 浅拷贝+引用计数--写时拷贝---模拟实现string容器

    引用计数 深拷贝 多个对象共享同一份资源时,最后能够保证该资源只被释放一次 应该由哪个对象释放资源? 由最后一个使用该资源的对象去释放 怎么知道一个对象是最后一个使用该资源的对象? 给一个计数,记录使 ...

  3. JSX列表渲染(五)——4种写法:基本写法 遍历写法 map遍历写法(常用-加工处理数组的每一项数据,最后形成一个新的数组) 列表遍历可以直接写到表达式中

    JSX列表渲染--4种写法:基本写法 & 遍历写法 & map遍历写法(常用-加工处理数组的每一项数据,最后形成一个新的数组) & 列表遍历可以直接写到表达式中 渲染多个元素 ...

  4. 【C++修行之路】STL——模拟实现string类

    文章目录 前言 类框架 构造与析构 迭代器 操作符重载 []: =: > == >= < <= !=: reverse与resize reverse resize push_b ...

  5. ajax的通用写法,ajax的jquery写法和原生写法

    一.ajax的简介 Ajax被认为是(Asynchronous(异步) JavaScript And Xml的缩写).现在,允许浏览器与服务器通信而无须刷新当前页面的技术都被叫做Ajax. 同步是指: ...

  6. 背景图片等比缩放的写法background-size简写法

    1.背景图片或图标也可像img一样给其宽高就能指定其缩放大小了. 比如一个实际宽高36*28的图标,要缩小一半引用进来的写法就是: background:rgba(0, 0, 0, 0) url(&q ...

  7. 设计模式:单例模式的写法(基础写法和线程安全写法)

    单例模式的写法非常多.先给出一种最基础的写法: (A种写法): package singleton;public class SingletonInstance {private static Sin ...

  8. .ajax get 写法,原生Ajax写法(GET)

    ajax的GET提交方式的原生代码: var xhr = null; if(window.XMLHttpRequest){ xhr = new XMLHttpRequest(); }else if(w ...

  9. 单片机小白学步系列(四) 模拟电路、传统数字电路与单片机

    大家都用过计算器,有没有想过它是怎么实现的呢?这里我不详述计算器的原理,而只对思路进行简单介绍.等我们学会了单片机,也可以亲手制作一个计算器.通过电路进行数学计算,应该怎么做呢?为了便于理解,下面我举 ...

最新文章

  1. 面向对象进阶2 组合
  2. 宏病毒的研究与实例分析06——终结篇 进击的MACRO
  3. JDBC的入门案列以及JDBC的对事务的管理
  4. python 查看当前目录_Python学习第156课--ls的运用、环境变量以及PATH
  5. 我们找了3家制造企业,问问他们是怎么破解供应链难题的
  6. Prometheus 原理和实践,含docker部署Prometheus、node Exporters、Alertmanager、Push Gateway、grafana
  7. linux shutdown 命令
  8. Windows中安装Electron说明
  9. linux centos Intel® Centrino® Wireless-N 1000 无线网卡的安装
  10. ERROR:STATUS_BROM_CMD_SEND_DA_FALL(0xC0060003)
  11. 还在用PPT做组织架构图?公司都在用的架构图软件是什么?
  12. 合工大苍穹战队视觉组培训Day8——视觉,目标识别
  13. 使用element走马灯 + video-player实现图片和视频混合轮播
  14. Manjaro Linux KDE 安装后的配置
  15. 叶子结点和分支节点_结点数和叶子结点数有什么区别
  16. Epson XP-225 驱动
  17. 电脑用的时间长了,如何给电脑提速?
  18. python 中range(10)什么意思_请问在Python中for i in range(10,0,-1)是什么意思
  19. 微信公众号开发之如何将本机IP映射成外网域名
  20. 算法工程师八股文——序言

热门文章

  1. train-images-idx3-ubyte.gz train-labels-idx1-ubyte.gz 下载
  2. 机器视觉之工业摄像机知识点(一)
  3. 大恒水星相机开发记录
  4. android3D摄像机
  5. 单片机四路抢答器课程C语言设计,四路抢答器单片机课程设计.doc
  6. 小猫爪:嵌入式小知识06-KEIL scf分散加载文件解析-链接代码至RAM
  7. 武林外传经典108句
  8. 数学不好学java好学吗_数字不好能学java吗?
  9. 第十三周翻译:Stairway to SQL Server Replication: Level 1 - Introduction to SQL Server Replication
  10. Android视频技术探索之旅:美团外卖商家端的实践