当前位置: 首页 > news >正文

邢台网站建设 冀icp备百度登录账号首页

邢台网站建设 冀icp备,百度登录账号首页,贵州省建设厅网站文件,烟台网站建设.com手搓string类 文章目录手搓string类string的成员一.构造,拷贝构造,赋值重载和析构1.构造函数2.拷贝构造3.swap问题4.赋值重载5.析构函数二.简单接口1.c_str2.size(有效字符长度)3.capacity(有效字符容量)4.operator[]5.迭代器和范…

手搓string类

文章目录

  • 手搓string类
    • string的成员
    • 一.构造,拷贝构造,赋值重载和析构
      • 1.构造函数
      • 2.拷贝构造
      • 3.swap问题
      • 4.赋值重载
      • 5.析构函数
    • 二.简单接口
      • 1.c_str
      • 2.size(有效字符长度)
      • 3.capacity(有效字符容量)
      • 4.operator[]
      • 5.迭代器和范围for
    • 三.容量
      • 1.reverse
      • 2.resize
      • 3.clear
    • 四.插入
      • 1.push_back
      • 2.append
      • 3.operator+=
      • 4.insert
      • 5.erase
    • 五.查找
      • find
    • 六.流插入<<和流提取>>的重载
      • 1.流插入<<重载
      • 2.流提取>>重载
    • 七.整体实现代码

string的成员

class string()
{private:char *str;size_t _size;size_t _capacity;const static size_t npos=-1;
};

在类和对象中提到过,静态成员变量不能给缺省值,必须要在类外定义。

但是其实有一个特例,那就是针对整形开了一个后门,静态的整形成员变量可以直接在类中定义。
在这里插入图片描述

一.构造,拷贝构造,赋值重载和析构

1.构造函数

在类和对象时提到过,如果要显示定义构造函数最好是给全缺省

string(const char *str="")//注意:\0和空指针一样,'\0'是字符常量(char类型),“”和“\0"一样,
{_size = strlen(str);//这是成员函数,有this指针_capacity = _size;_str = new char [_capacity + 1];//多开一个字节给'\0'strcpy(_str, str);//可能会传参过来构造
}

这里的_capacity是给有效字符预留的空间,为了给’\0’留位置在开空间的时候要多开一个。

2.拷贝构造

首先是老实人写法,构造一个新的空间,将s._str的值拷贝到新的空间,再将其他的值拷贝

//拷贝构造
string(const string& s)
{//先构造一个新空间给_str_str = new char[s._capacity + 1];//然后将s的其他值赋值给this_size = s._size;_capacity = s._capacity;strcpy(_str, s._str);
}

在下面的写法中,会经常复用C语言中的字符串函数。(不为别的,就是好用)

不是谁都想当老实人,所以又有了一种现代写法(并不是为了提高效率,而是为了让代码更简洁)。

首先构造一个tmp,然后将tmp和s交换

void swap(string&s)
{std::swap(_str, s._str);//库中和string分别提供了一个swap函数,用库中的swap交换内置类型std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}string(const string& s):_str(nullptr) //这里必须要给_str初始化,否则拿到一个非法的地址,析构会报错,_size(0),_capacity(0)
{string tmp(s._str);//这里复用构造函数swap(tmp);//有隐藏的this指针,一个参数就够了
}

如果不给_str初始化成空指针话,交换以后临时变量tmp就指向了 _str的空间,这可能是一个野指针,临时变量tmp在出这个函数就要被销毁,调用析构的时候delete一个野指针就会产生错误。

可以delete一个空指针,无论是free还是delete当接收的参数是一个空指针时就不做任何处理。

3.swap问题

在这里插入图片描述

标准库中的swap函数是一个模板,在交换自定义类型时有一次构造和拷贝构造,代价比较大,所以我们提供一个成员函数会比较好。在成员函数中交换内置类型时就可以使用标准库中的swap函数,**要指定域,因为编译器默认是现在局部找,局部找不到再去全局找,再找不到就报错。**如果去局部找的话,找到的swap函数参数不匹配。

4.赋值重载

复制重载也分为传统写法:

string& operator=(const string& s)
{if (this != &s)//要注意不要自己给自己赋值{//深拷贝char* tmp = new char[s._capacity + 1];strcpy(tmp, s._str);//为新空间赋值_str = tmp;_capacity = s._capacity;_size = s._size;return *this;}}

现代写法,使用传值传参,然后直接使用临时变量交换,这个写法是我比较推荐的(太简洁了)

void swap(string&s)
{std::swap(_str, s._str);//库中和string分别提供了一个swap函数,用库中的swap交换内置类型std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
string& operator=(string s)
{swap(s);//传值传参,s是一个拷贝,直接使用这个临时变量,反正临时变量出了这个函数就被销毁了return *this;
}

s是一个拷贝的临时变量,在销毁的时候会自动调用析构函数清理,不用我们做额外处理,而且直接使用临时变量调用swap还省去了我们创建临时变量。十分简洁,但是可读性不算太好

5.析构函数

直接使用delete销毁空间,再将_size和 _capacity置0就行

//析构函数
~string()
{//释放空间delete[] _str;_str = nullptr;_size = _capacity= 0;
}

二.简单接口

1.c_str

这个主要是返回一个C类型的数组指针

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

对于只读的函数接口建议加上const,这样不但普通对象可以使用,const类型的对象也可以使用

2.size(有效字符长度)

size_t size()const
{return _size;
}

3.capacity(有效字符容量)

size_t capacity()const
{return _capacity;
}

4.operator[]

char& operator[](size_t pos)
{//虽然string是自定义类型,但_str是内置类型assert(pos < _size);return _str[pos];
}const char& operator[](size_t pos)const//const对象只读
{assert(pos < _size);return _str[pos];
}

因为const对象只能读不可写,所以这里要重载两个,重载了[]string就可以像访问数组那样访问了

5.迭代器和范围for

强调一下,迭代器虽然行为像指针,但不一定是指针

typedef char* iterator;//这里为了简单,就实现成指针iterator begin()
{return _str;
}iterator end()
{return _str + _size;
}

有了迭代器,就可以使用范围for了,但是范围for只认识beging和end,所以如果要使用范围for,在手搓迭代器的时候就不要乱取名哦。

在这里插入图片描述

三.容量

1.reverse

这个是string类中用于扩容的成员函数

void reverse(size_t n)
{//只扩容,所以先检查情况if (n > _capacity){//_str中有数据,不能直接改_str的空间,要先建立临时空间char*tmp = new char[n + 1];//将_str中的数据拷贝给tmp,再将_str所指的空间释放strcpy(tmp, _str);delete[]_str;_str = tmp;_capacity = n;//更新容量}
}

reverse是控制容量的成员函数,但是缩容的代价太大了。所以只考虑缩容,其实string类中给的reverse是会缩容的。

2.resize

这个是改变有效字符长度的成员函数

void resize(size_t n,char ch='\0')//改变size可能会改变capacity,默认插入补空间的字符是'\0'
{if (n > _capacity)//这里也可以写n>_size{reverse(n);//扩大空间以后,要用字符初始化后续空间for (size_t i = _size; i < n; i++){_str[i] = ch;}//这里还要改变size_size = n;//可能使用者会传其他字符来初始化,前面的循环没有在size位置补'\0'_str[_size] = '\0';}else {//如果是缩小的话,就直接在n位置补'\0'_str[n] = '\0';_size = n;}}

有看到n>_size就扩容的,但在我看来只有大于容量的时候才有必要改变有效容量。

在缩小的时候同样没必要更改容量,直接在n位置插入一个’\0’,就无法访问到n后面的元素了,间接改变了__size

3.clear

这个成员函数是将string变成一个空串,在重载流提取的时候会用到这个函数

void clear()
{_size = 0;_str[0] = '\0';
}

四.插入

1.push_back

尾插单个字符,插入就要考虑扩容

void push_back(char c)
{if (_size == _capacity)//容量不够,要扩容{//扩容要调用reverse,这里还要检查一下_capacity,第一次可能是0int newCapacity = _capacity == 0 ? 4 : 2 * _capacity;reverse(newCapacity);//这里面更新了_capacity,外面不用再更新}//扩容完毕以后,就可以开始插入了_str[_size] = c;_size++;_str[_size] = '\0';
}

这样尾插只能插入单个字符,所以string还提供了一个append(追加字符串)

2.append

这是一个在末尾追加字符串的成员函数

string& append(const char* s)
{//这里可以使用C标准库中的strcpy,不过还是要考虑扩容的问题,要检查剩下的空间是否足够插入int len = strlen(s);if (_size + len > _capacity){//容量不够插入就要扩容reverse(_capacity + len);}//复用C标准库函数strcpy(_str + _size, s);_size += len;return *this;
}

追加一个字符串开原本空间的两倍可能还不够用,最正确的写法是计算一下字符串的长度用于增加空间

3.operator+=

重载+=是string类一个非常正确的选择,在做oj的时候你将发现,+=比尾插和追加好用太多了。

string& operator+=(char c)
{//插入一个字符直接_size位置插入,复用push_backpush_back(c);return *this;
}//还要重载一个字符串类型,可以复用append
string& operator+=(const char* str)
{append(str);return *this;
}	

先介绍push_back和append不是没有道理的,复用可以减少代码冗余还省事,还不快复用起来

4.insert

在pos位置插入一个字符或者字符串,需要挪动数据。

插入单个字符:

string& insert(size_t pos, char c)
{assert(pos <= _size);//string没有单独的扩容函数,在每个插入数据的地方都要检查容量if (_size == _capacity){int newCapacity = _capacity == 0 ? 4 : 2 * _capacity;reverse(newCapacity);}//插入字符要挪动字符,这里要小心,在pos位置插入size_t end = _size + 1;//指向'\0'的下一个位置while (end > pos){_str[end] = _str[end - 1];--end;}_str[pos] = c;_size += 1;return *this;
}	

这里有一个问题,要知道end和pos都是size_t类型的数据,如果在写判断条件的时候,写成end>=pos,可能会陷入死循环。

如果pos是0,end永远无法小于零,这就死循环咯。

解决办法有两种,其一是把end写成有符号的int并且在判断的时候强转pos,也就是这样:

int end = _size;while (end >=(int) pos)
{_str[end] = _str[end - 1];--end;
}

第二种就是我写的这样,把end放在_size+1的位置,这样在判断的时候可以不用取等号,也就完美避免了所有问题。我比较推荐这种写法,因为end作为下标本身取值范围就应该要大于零。

插入字符串:

string& insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);if (_size +len > _capacity){int newCapacity = _capacity == 0 ? 4 : 2 * _capacity;reverse(newCapacity);}//扩容完毕,开始挪动数据,这次要挪动len个位置,要考虑一下长度是否越界size_t end = _size + len;while (end > pos + len-1){_str[end] = _str[end - len];end--;}//位置挪出来以后要插入字符串/*for (size_t i = pos; i <= pos + len; i++){_str[i] = *str;3str++;}*///此外,还可以复用C库函数strncpystrncpy(_str + pos, str, len);_size += len;return *this;
}

这里要用strncpy而不能使用strcpy,因为strcpy会将’\0’也拷贝过来,而字符串的结束标志就是以’\0’为准的。

5.erase

从pos位置开始删除len个长度的字符

//实现一下erase
string& erase(size_t pos = 0, size_t len = npos)
{assert(pos < _size);//删除要判断是否会越界if (len == npos || pos + len >= _size){//说明要删除的长度超过了pos后面有的字符串,只要直接在pos位置插入'\0'就行_str[_size] = '\0';_size = pos;}else{//如果不越界,就要覆盖删除,可以复用strcpystrcpy(_str + pos, _str + pos + len);_size -= len;}return *this;
}

说到erase就不得不说一下npos,npos是一个无符号整形默认是-1也就是四十二亿九千万,如果长度大于npos就是越界了。

五.查找

find

从pos位置开始查找一个字符或者一段字符串

size_t find(char c, size_t pos)
{assert(pos < _size);//断言检查for (int i = pos; i < _size; i++){if (_str[i] == c){return i;}}return npos;
}size_t find(const char* s, size_t pos)
{//这里可以考虑复用C的库函数strstrassert(pos < _size);char *ret = strstr(_str + pos, s);//这个C语言库函数返回的是一个指针//检查合法性if (ret == NULL){return npos;}else{return ret - _str;}
}

其实在一般的情况下使用strstr查找子串已经够了,kmp算法其实是一个外强中干的家伙。

六.流插入<<和流提取>>的重载

这个我们在日期类中就已经接触过了,不能写在类中否则会被this指针抢第一个参数位置,还是使用友元然后定义在类外。

1.流插入<<重载

ostream& operator<<(ostream& out, const string& s)
{for (size_t i = 0; i < s.size(); i++){out << s[i];}return out;
}

2.流提取>>重载

istream& operator>>(istream& in, string& s)
{s.clear();char ch = in.get();while (ch != ' ' && ch != '\n'){s += ch;//我就说+=好用吧ch = in.get();}return in;
}

但这个代码有一个不好的地方在于插入长字符串时可能会频繁的扩容。

为了减少扩容次数,我可以建立一个数组,这个数组满了就往s中插入,数组满一次才扩容一次,有效减少扩容次数

stream& operator>>(istream& in,  string& s)//要对s插入数据,s不能为const类型
{s.clear();//清空s//定义一个数组,往这个数组中放数据,当这个数组满了以后+=给s,这样可以避免频繁的扩容char buff[128] = { '\0' };char ch = in.get();//从缓冲区拿字符size_t i = 0;while (ch != ' ' && ch != '\n'){if (i == 127){s += buff;i = 0;//满了以后+=geis再将i重置为0,开始下一轮}buff[i++] = ch;ch = in.get();}//如果输入的数据不能让数组满呢?if (i > 0){//只要有数据就应该导进来,字符数组是以'\0'作为结束标志的,对于只插入几个字符的情况,要手动补'\0'buff[i] = '\0';//后置++,是有效字符的下一个位置s += buff;}return in;
}

getline就是在判断的时候把空格去掉就行,只以换行作为结束标志。但是cout,cin以及scanf和printf都是以空格和换行为结束标志的。

七.整体实现代码

#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<assert.h>
#include<string.h>
using namespace std;
namespace wbm
{class string{public://重载流插入和流提取,不能写在类中,使用友元friend istream& operator>>(istream& in,  string& s);friend ostream& operator<<(ostream& out,  const string& s);//首先写构造函数,给一个全缺省的构造string(const char *str="")//注意:\0和空指针一样,'\0'是字符常量(char类型),“”和“\0"一样,{_size = strlen(str);//这是成员函数,有this指针_capacity = _size;_str = new char [_capacity + 1];//多开一个字节给'\0'strcpy(_str, str);//可能会传参过来构造}//析构函数~string(){//释放空间delete[] _str;_str = nullptr;_size = _capacity= 0;}拷贝构造//string(const string& s)//{//	//先构造一个新空间给_str//	_str = new char[s._capacity + 1];//	//然后将s的其他值赋值给this//	//	_size = s._size;//	_capacity = s._capacity;//	strcpy(_str, s._str);//}//不当老实人,使用现代写法,主要是调用swap函数void swap(string&s){std::swap(_str, s._str);//库中和string分别提供了一个swap函数,用库中的swap交换内置类型std::swap(_size, s._size);std::swap(_capacity, s._capacity);}string(const string& s):_str(nullptr) //这里必须要给_str初始化,否则拿到一个非法的地址,析构会报错,_size(0),_capacity(0){string tmp(s._str);//这里复用构造函数swap(tmp);//有隐藏的this指针,一个参数就够了}//重载=//string& operator=(const string& s)//{//	if (this != &s)//	{//		//深拷贝//		char* tmp = new char[s._capacity + 1];//		strcpy(tmp, s._str);//为新空间赋值//		_str = tmp;//		_capacity = s._capacity;//		_size = s._size;//		return *this;//	}//}//不当老实人,现代写法主要是为了简洁,并不能提高效率string& operator=(string s){swap(s);//传值传参,s是一个拷贝,直接使用这个临时变量,反正临时变量除了这个函数就被销毁了return *this;}//重载+=,两种形式,一种加字符,还有加字符串//string& operator+=(char ch)//{//	//在字符串size位置直接插入,然后补一个'\0'//	//但是这里要考虑一个问题,就是扩容,所以不如直接写尾插//	_str[_size] = ch;//	++_size;//	_str[_size] = '\0';//}void reverse(size_t n){//只扩容,所以先检查情况if (n > _capacity){//_str中有数据,不能直接改_str的空间,要先建立临时空间char*tmp = new char[n + 1];//将_str中的数据拷贝给tmp,再将_str所指的空间释放strcpy(tmp, _str);delete[]_str;_str = tmp;_capacity = n;}}//reverse都有了,这不来个resizevoid resize(size_t n,char ch='\0')//改变size可能会改变capacity,默认插入补空间的字符是'\0'{if (n > _capacity){reverse(n);//扩大空间以后,要用字符初始化后续空间for (size_t i = _size; i < n; i++){_str[i] = ch;}//这里还要改变size_size = n;//可能使用者会传其他字符来初始化,前面的循环没有在size位置补'\0'_str[_size] = '\0';}else {//如果是缩小的话,就直接在n位置补'\0'_str[n] = '\0';_size = n;}}void push_back(char c){if (_size == _capacity)//容量不够,要扩容{//扩容要调用reverse,这里还要检查一下_capacity,第一次可能是0int newCapacity = _capacity == 0 ? 4 : 2 * _capacity;reverse(newCapacity);//这里面更新了_capacity,外面不用再更新}//扩容完毕以后,就可以开始插入了_str[_size] = c;_size++;_str[_size] = '\0';}string& operator+=(char c){//插入一个字符直接_size位置插入,复用push_backpush_back(c);return *this;}//还要重载一个字符串类型,可以复用appendstring& operator+=(const char* str){append(str);return *this;}string& append(const char* s){//这里可以使用C标准库中的strcpy,不过还是要考虑扩容的问题,要检查剩下的空间是否足够插入int len = strlen(s);if (_size + len > _capacity){//容量不够插入就要扩容reverse(_capacity + len);}//复用C标准库函数strcpy(_str + _size, s);_size += len;return *this;}//要重载一下[],方便读写char& operator[](size_t pos){//虽然string是自定义类型,但_str是内置类型assert(pos < _size);return _str[pos];}const char& operator[](size_t pos)const//const对象只读{assert(pos < _size);return _str[pos];}//[]都有了,这不来个迭代器(行为像指针,但不一定是指针,这里写成指针)typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}//写一个find函数,搞两个,一个是找字符,一个是找字符串size_t find(char c, size_t pos){assert(pos < _size);//断言检查for (int i = pos; i < _size; i++){if (_str[i] == c){return i;}}return npos;}size_t find(const char* s, size_t pos){//这里可以考虑复用C的库函数strstrassert(pos < _size);char *ret = strstr(_str + pos, s);//这个C库函数返回的是一个指针//检查合法性if (ret == NULL){return npos;}else{return ret - _str;}}//还要insert和erase//实现insert,分为插入字符和插入字符串string& insert(size_t pos, char c){assert(pos <= _size);//string没有单独的扩容函数,在每个插入数据的地方都要检查容量if (_size == _capacity){int newCapacity = _capacity == 0 ? 4 : 2 * _capacity;reverse(newCapacity);}//插入字符要挪动字符,这里要小心,在pos位置插入size_t end = _size + 1;//指向'\0'的下一个位置while (end >=(int) pos){_str[end] = _str[end - 1];--end;}_str[pos] = c;_size += 1;return *this;}string& insert(size_t pos, const char* str){assert(pos <= _size);size_t len = strlen(str);if (_size +len > _capacity){int newCapacity = _capacity == 0 ? 4 : 2 * _capacity;reverse(newCapacity);}//扩容完毕,开始挪动数据,这次要挪动len个位置,要考虑一下长度是否越界size_t end = _size + len;while (end > pos + len-1){_str[end] = _str[end - len];end--;}//位置挪出来以后要插入字符串/*for (size_t i = pos; i <= pos + len; i++){_str[i] = *str;str++;}*///此外,还可以复用C库函数strncpystrncpy(_str + pos, str, len);_size += len;return *this;}//实现一下erasestring& erase(size_t pos = 0, size_t len = npos){assert(pos < _size);//删除要判断是否会越界if (len == npos || pos + len >= _size){//说明要删除的长度超过了pos后面有的字符串,只要直接在pos位置插入'\0'就行_str[_size] = '\0';_size = pos;}else{//如果不越界,就要覆盖删除,可以复用strcpystrcpy(_str + pos, _str + pos + len);_size -= len;}return *this;}//下面写几个简单函数,比如返回_size,_capacity,返回一个C类的指针const char* c_str()const{return _str;}size_t size()const{return _size;}size_t capacity()const{return _capacity;}void clear(){_size = 0;_str[0] = '\0';}private:char* _str;size_t _size;size_t _capacity;const static size_t npos=-1; //静态成员变量只有整形可以给缺省值};ostream& operator<<(ostream& out, const string& s){for (size_t i = 0; i < s.size(); i++){out << s[i];}return out;}istream& operator>>(istream& in,  string& s)//要对s插入数据,s不能为const类型{s.clear();//定义一个数组,往这个数组中放数据,当这个数组满了以后+=给s,这样可以避免频繁的扩容char buff[128] = { '\0' };char ch = in.get();//从缓冲区拿字符size_t i = 0;while (ch != ' ' && ch != '\n'){if (i == 127){s += buff;i = 0;//满了以后+=geis再将i重置为0,开始下一轮}buff[i++] = ch;ch = in.get();}//如果输入的数据不能让数组满呢?if (i > 0){//只要有数据就应该导进来,字符数组是以'\0'作为结束标志的,对于只插入几个字符的情况,要手动补'\0'buff[i] = '\0';//后置++,是有效字符的下一个位置s += buff;}return in;}
}

urn _str;
}

	size_t size()const{return _size;}size_t capacity()const{return _capacity;}void clear(){_size = 0;_str[0] = '\0';}
private:char* _str;size_t _size;size_t _capacity;const static size_t npos=-1; //静态成员变量只有整形可以给缺省值
};
ostream& operator<<(ostream& out, const string& s)
{for (size_t i = 0; i < s.size(); i++){out << s[i];}return out;
}istream& operator>>(istream& in,  string& s)//要对s插入数据,s不能为const类型
{s.clear();//定义一个数组,往这个数组中放数据,当这个数组满了以后+=给s,这样可以避免频繁的扩容char buff[128] = { '\0' };char ch = in.get();//从缓冲区拿字符size_t i = 0;while (ch != ' ' && ch != '\n'){if (i == 127){s += buff;i = 0;//满了以后+=geis再将i重置为0,开始下一轮}buff[i++] = ch;ch = in.get();}//如果输入的数据不能让数组满呢?if (i > 0){//只要有数据就应该导进来,字符数组是以'\0'作为结束标志的,对于只插入几个字符的情况,要手动补'\0'buff[i] = '\0';//后置++,是有效字符的下一个位置s += buff;}return in;
}

}


---文章到此就结束啦,希望对各位有所帮助。
http://www.ds6.com.cn/news/81883.html

相关文章:

  • 阿里云网站主体变更怎么做广告投放是做什么的
  • 做网站外贸怎么找客户简述网络营销的含义
  • 深圳网站优化搜索军事新闻最新
  • 做政府网站个人能做吗每日国际新闻最新消息
  • 微信彩票网站网站建设百度禁止seo推广
  • 天津市建设工程质量安全监督管理总队网站网站服务器地址查询
  • 做网站排名公司推荐百度seo竞价推广是什么
  • 语音app开发福州网站seo公司
  • 用wordpress开发网站模板seo流量软件
  • 和嗲囡囡和做的网站抖音搜索关键词推广
  • 做网站的图片一般放哪西安seo
  • 做网站素材google seo 优化招聘
  • 网站建设的好处和目的yoast seo教程
  • 淄博网站建设报价优化设计答案大全英语
  • 内网建设网站网络项目免费的资源网
  • 武威网站建设优化搜索引擎竞价排名
  • 专业建设网站服务外贸营销网站建设介绍
  • 在直播网站做前端注意seo如何优化一个网站
  • wordpress怎么更换站点关键词优化软件
  • 网站 psd关键词竞价排名
  • 华为网站哪个公司做的百度客服电话人工服务
  • 做网站的技术要求seo基础知识
  • 怎么做彩票网站长沙百度搜索排名优化
  • 重庆智能网站建设推荐淘宝交易指数换算工具
  • 手工做女宝宝衣服的网站百度竞价托管哪家好
  • 宿迁房产网关键词排名优化公司外包
  • 韩国的 电子商务网站网络营销的未来发展趋势
  • 嘉兴做网站优化多少钱百度网盘app怎么打开链接
  • 淘宝客网站怎么做分销网络营销的新特点
  • hb网页设计软件外贸seo软文发布平台