文章目录
- 1.回顾库函数
- strcpy
- memcpy
- strcmp
- strstr
- 2.回顾类和对象
- 哪些函数里会有this指针?
- this指针调用方法
- 结论:只要是不修改this指针指向的对象内容的成员函数,都可以加上const
- 自己写了构造函数,编译器不会自动生成默认构造
- 2.1构造和拷贝构造都可以使用初始化列表进行初始化
- 2.2 C和C++区别:C中结构体里面没有函数方法
- 3.string 模拟实现
- 构造函数
- 默认构造的错误写法
- 默认构造的正确写法
- 拷贝构造
- 深浅拷贝
- 赋值重载
- 深浅拷贝
- 传统写法
- 现代写法
- swap()
- 析构函数
- const char* c_str() const C的接口
- size() 返回有效字符个数
- []符号重载
- reserve()
- 模拟实现reserve
- void push_back(char c)
- string& append(const char* s)
- void insert(size_t pos, size_t n, char c)
- void insert(size_t pos, const char* s)
- void erase(size_t pos, size_t len = npos)
- size_t find(const char* str, size_t pos = 0)
- string substr(size_t pos = 0, size_t len = npos)
- iterator迭代器 左闭右开[ )
- 定义
- 使用
1.回顾库函数
strcpy
strcpy 都是char 类型 考完就算完成
char* my_strcpy(char* dest, const char* src)
{
assert(dest && src);
char* ret = dest; //先对src后置++ 在解引用 后置++优先级高于* 这个写法\0也被拷贝,表达式结果为\0就结束
while ((*dest++ = *src++))//判断*dest的结果是不是'\0',如果是就结束,会将源字符串中的 '\0' 拷贝到目标空间
{
;
}
return ret;
}
memcpy
void * memcpy ( void * destination, const void * source, size_t num );
涉及多种类型,可能是char* int*
所以传入形参num ,多少字节需要被拷贝
num
Number of bytes to copy.
size_t is an unsigned integral type.
其中就需要利用char*指针 1个字节一个字节的拷贝
要用此函数拷贝,要保证源和目的地都至少有num字节个空间
思路如图所示:
void* my_memcpy(void* dest, const void* src, size_t num)
{//如果source和destination有任何的重叠,复制的结果都是未定义的。
assert(dest && src);
void* ret = dest;
//前->后
while (num--)//循环num次
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;//++(char*)dest;如果换成(char*)dest++,dest++报错(“void *”: 未知的大小),因为char*转换是临时的
src = (char*)src + 1;//++(char*)src;
}
return ret;
}
strcmp
strcmp能比较不同长度的字符串吗?可以,并且其中字符串提前结束后最后一位\0参与了对比,这和string里面的<复写有所区别,string里面\0不应该参与对比
int my_strcmp(const char* str1, const char* str2)
{
assert(str1 && str2);
while(*str1 == *str2)
{
if (*str1 == '\0')//abc和abc相等情况判断,如果*str1 == \0 那么*str2也等于\0
return 0;
str1++;
str2++;
}
return *str1 - *str2;//不相等看*str1 - *str2谁大 返回>0 <0
}
strstr
思路:
BF
暴力求解,定义下标i,j开始从头遍历,i和j相同就一起+1,遇到不同的字符就让j返回到子串0,i回到开始位置+1
时间复杂度O(m*n)
char* my_strstr(const char* str1, const char* str2)
{
assert(str1 && str2);
if (*str2 == '\0')//传入空字符串 直接返回str1
{
return (char*)str1;
}
const char* s1 = NULL;
const char* s2 = NULL;
const char* cp = str1;//记录可能的起始位置
while (*cp)
{
s1 = cp;
s2 = str2;//重置要查找子串的开始位置
while (*s1 != '\0' && *s2 != '\0' && *s1 == *s2)
{
s1++;
s2++;
}
if (*s2 == '\0')
{
return (char*)cp;
}
cp++;
}
return NULL;
}
2.回顾类和对象
哪些函数里会有this指针?
答:只要是类成员函数里面都有
this指针调用方法
this->push_back(); 等价于 (*this).push_back();
string& operator+=(char c)
{
//this->push_back(c);
//(*this).push_back(c);
push_back(c);
return *this;
}
结论:只要是不修改this指针指向的对象内容的成员函数,都可以加上const
好处是 非const和const的对象都可以调用,但对于要修改this指向内容的对象加上const就没法改了
自己写了构造函数,编译器不会自动生成默认构造
2.1构造和拷贝构造都可以使用初始化列表进行初始化
2.2 C和C++区别:C中结构体里面没有函数方法
3.string 模拟实现
要注意_size和_capacity的更新
也要注意 成员函数插入或删除的一些函数 pos位置的范围 assert(pos<size)…等
构造函数
默认构造的错误写法
这种写法导致了 cout<< nullptr << endl的错误,输出nullptr会报错
然而cout << NULL << endl;并不会报错,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量,默认编译器认为NULL是0,要搞成指针必须(void*)0
在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
string()
:_size(0)
,_capacity(0)
,_str(nullptr)
{ }
默认构造的正确写法
/string()
:_size(0)
,_capacity(0)
,_str(new char[1])
{
_str[0] = ‘\0’;
}/
即使stirng对象是空,string认为也是要有一个\0的
string类 capacity不计算\0
c的字符数组,以\o为终止算长度
string不看\0,以size为终止算长度
下面将默认构造和有参构造合并
此种实现的是最常用的构造 string (const char* s);
说人话就是用一个C字符串来构造string对象
开头罗列出了默认构造的几个错误缺省参数,原因如后面所示
这里并没有使用初始化列表进行初始化,因为都是内置类型,初始化列表也无法完成memcpy或其他可能的工作,所以干脆没用
根据_size+1也拷贝了\0,这里不用strcpy()而改用memcpy() 的原因是:遇到hello\0xxxxx这种string对象拷贝就不能用strcpy了
//string(const char* s = '\0')//类型不匹配 char* = char
//string(const char* s = nullptr) //下面strlen会崩
//string(const char* s = "\0") //内存有2个 \0 \0 没必要,但是正确,下面会开辟一个空间,strcpy会把\0拷贝到_str
string(const char* s = "")//空字符串,默认1个\0,下面会开辟一个空间,strcpy会把\0拷贝到_str
{
_size = strlen(s);
_capacity = _size;//string类 capacity不计算\0
_str = new char[_size + 1];//为\0留一个位置
//strcpy(_str, s);
memcpy(_str,s,_size+1);//遇到hello\0xxxxx这种string对象拷贝就不能用strcpy了
}
拷贝构造
深浅拷贝
浅拷贝的问题
内存泄漏
析构两次
形参中const string& s 加const 让const对象也可以传参,并且防止了this对象和形参赋值顺序可能发生颠倒
_str = new char[s._capacity + 1];为什么不开辟 s._size+1大小的空间?
因为目的就是拷贝和源相同capacity大小的空间,如果开s._size+1大小 _capacity就可能变小
这里我写出了一个错误,类似于C初阶冒泡函数形参传递无法在函数中sizeof出整个数组大小,我试图sizeof(s._str)求 整个数组的大小,这是做不到的,只有在同一个函数栈帧中才能sizeof(数组名)求出整个数组的大小,s是作为形参传入,s._str就类似于冒泡函数中的形参sort(int a[]) 里面sizeof(a)是求不出整个数组大小的
string(const string& s)
{
_str = new char[s._capacity + 1];
//memcpy(_str, s._str, sizeof(s._str));//形参传入的是指针,参考C初阶冒泡的错误写法
memcpy(_str, s._str, s._size+1);
_size = s._size;
_capacity = s._capacity;
}
赋值重载
深浅拷贝
问题
析构两次
一个对象的修改造成另一个对象一起改变
类似于拷贝构造,但是拷贝构造不需要释放原来的数据空间
赋值重载要做的事情是释放原有的空间,开辟一块新空间,赋值另一个string对象的内容
//原有空间可能小,要扩容
//可能大,多的浪费了
//也可能刚好够
//不管原来到底是够还是大或者小,反正都是要释放原空间,开辟新空间
//s1 = s2
传统写法
自己开新空间,赋值s2的数据,再释放原有空间
赋值代表拷贝,这里不能开s._size+1个,比源对象的容量少,和拷贝构造一样
string& operator=(const string& s)
{
if (this != &s)//this指向s1 ,s1 != s才赋值
{
char* tmp = new char[s._capacity + 1];//赋值代表拷贝,这里不能开s._size+1个,比源对象的容量少
memcpy(tmp,s._str, s._size+1);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
现代写法
过渡版
s1 = s2
思路:打工人是拷贝构造,利用拷贝构造构造出的tmp,再让tmp和this交换,s1也就换好了
这里面不能直接交换两个对象,会造成栈溢出,原因是swap里面也是赋值又继续调用赋值重载又swap…
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s);
std::swap(_str, tmp._str);
std::swap(_size, tmp._size);
std::swap(_capacity, tmp._capacity);
//swap(tmp, *this);//对象直接交换,交换里面还是赋值重载,就会造成栈溢出
}
return *this;
}
swap()
string里面的是交换成员变量,与算法库 的swap()里面不同的是:库里面的swap是深拷贝,对比string里的swap效率低一些
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
现代写法最终版
思路:在形参中就调用拷贝构造,构造形参tmp,进行与s1的交换
注意形参不要写const,const导致形参无法被修改也就不能交换了
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string& operator=(string tmp)
{
//this->swap(tmp);
swap(tmp);
return *this;
}
析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
const char* c_str() const C的接口
返回C字符串_str
const char* c_str() const
{
return _str;
}
size() 返回有效字符个数
size_t size() const
{
return _size;
}
[]符号重载
char& operator[](size_t pos)//无法加const,需要读写修改成员变量
{
assert(pos < _size);
return _str[pos];
}
//const对象版本 只读
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
reserve()
作用:修改capacity,单纯改变空间
不具约束力的请求,编译器没有缩容
测试发现reserve只扩容,不缩容
测试代码
int main()
{
string s1("hello linux");
s1.reserve(100);//单纯改变空间
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.reserve(5);//不具约束力的请求,编译器没有缩容
cout << s1[9] << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
//开空间+填值初始化
//比size大的部分用'\0'填充
s1.resize(105);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.resize(0);//即使是0 也没缩容
cout << s1.size() << endl;
cout << s1.capacity() << endl;
return 0;
}
模拟实现reserve
思路:如果需要的空间>capacity就扩容,开辟需要n个空间,将原有数据拷贝过去,释放原来的空间,更新成员变量
void reserve(size_t n)
{//调整想要的有效字符的空间,可能缩容但不进行缩容
if (n > _capacity)
{
cout << "reserve->" << n << endl;
char* tmp = new char[n + 1];//n是有效个数,还需要加\0
//strcpy(tmp, _str);
memcpy(tmp, _str, _size + 1);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char c)
追加一个字符
思路:在字符串末尾追加一个字符,可以二倍扩容
注意事项:因为涉及2倍,原有空间如果是0就需要利用三目来规避此问题
void push_back(char c)
{
//二倍扩容
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size++] = c;
_str[_size] = '\0';
}
string& append(const char* s)
追加字符串
思路:不能二倍扩容,当len很大,len + _size有可能大于二倍扩
至少要开到len+_size
注意事项:reserve(len + _size);//这里不需要len+_size+1 因为reserve里面已经+1
string& append(const char* s)
{
//不能二倍扩容,当len很大,len + _size有可能大于二倍扩
//至少要开到len+_size
size_t len = strlen(s);
if (len + _size > _capacity)
{
reserve(len + _size);//这里不需要len+_size+1 因为reserve里面已经+1
}
//strcpy(_str + _size, s);
memcpy(_str + _size, s, len + 1);//多拷贝一个\0
_size += len;
return *this;
}
void insert(size_t pos, size_t n, char c)
插入n个字符
思路:n + _size <= _capacity就刚好够,不用扩容,不然就扩容到n+_size
之后就是挪动数据,把_size位置的数据往后挪动n个,才能在Pos(包括pos)往后的位置空出n个位置来插入
注意:挪动数据时,pos位置在中间都没事,如果Pos==0,那么因为size_t是无符号整形,end–导致size_t = 42亿几,判断条件就停不下来了
那么解决方法有三种
1.将end和pos都强转为int,pos如果不强转那么会导致int end算术转换为size_t,仍然会进入循环
2.设置npos = -1 这是设置静态变量npos那么如果end == -1就结束
3.把前一个数据挪动到后面去
思路图
while(end>pos+n)
_str[end] = _str[end-3]
end–
void insert(size_t pos, size_t n, char c)
{
assert(pos <= _size);
//扩容
if (n + _size > _capacity)
{
reserve(n + _size);
}
// 挪动数据
/*int end = _size;
while (end >= (int)pos)
{
_str[end + n] = _str[end];
end--;
}*/
size_t end = _size;
while (end >= pos && end != npos)
{
_str[end + n] = _str[end];
end--;
}
for (size_t i = 0; i < n; i++)
{
_str[pos + i] = c;
}
_size += n;
}
void insert(size_t pos, const char* s)
插入字符串
思路:和插入n个字符没啥区别,都是挪动然后插入n个字符
void insert(size_t pos, const char* s)
{
assert(pos <= _size);
size_t len = strlen(s);
//扩容// 至少扩容到_size + len
if (len + _size > _capacity)
{
reserve(len + _size);
}
size_t end = _size;
while (end >= pos && end != npos)
{
_str[end + len] = _str[end];
end--;
}
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = s[i];
}
_size += len;
}
void erase(size_t pos, size_t len = npos)
删除pos位置(包括Pos)后续的len个字符
思路图:
思路:从pos位置开始删除,如果pos+len >= _size or len == npos那么很简单,相当于直接从pos位置是\0截断,如果不是这种情况,那么就需要把end = pos + len 的位置移动到Pos位置,他们俩++即可。
注意:先判断len = = npos如果是这种情况,如果先判断pos + len 会溢出
void erase(size_t pos, size_t len = npos)
{
assert(pos <= _size);
if (len == npos ||pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
size_t end = pos + len;
while (end <= _size)
{
/*_str[end - len] = _str[end];
end++;*/
_str[pos++] = _str[end++];
}
}
_size -= len;
}
size_t find(const char* str, size_t pos = 0)
查找字符串子串,返回子串在主串中的下标
注意:指针-指针,p2指向的位置不计算在内,而是p1和p2之间的元素个数,以下标版本的也一样
size_t find(const char* str, size_t pos = 0)
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (ptr)
{
return ptr - _str;
}
else
{
return npos;
}
}
string substr(size_t pos = 0, size_t len = npos)
返回主串中的子串,返回从pos位置开始(包含pos)的len个字符
涉及小知识点:数组中下标也符合指针-指针,可以计算中间的元素个数,结合测试案例来看
小知识点思路图
测试代码:
void test_string4()
{
ljh::string url1 = "ftp://www.baidu.com/?tn=65081411_1_oem_dg";
// 协议 域名 资源名
size_t pos1 = url1.find("://");
if (pos1 != ljh::string::npos)
{
ljh::string protocol = url1.substr(0, pos1);
cout << protocol.c_str() << endl;
}
size_t pos2 = url1.find('/', pos1 + 3);
if (pos2 != ljh::string::npos)
{
ljh::string domain = url1.substr(pos1 + 3, pos2 - (pos1 + 3));
cout << domain.c_str() << endl;
ljh::string uri = url1.substr(pos2 + 1);
cout << uri.c_str() << endl;
}
//cout << "pos1 = " << pos1 << "pos2 = " << pos2 << endl;
}
模拟实现代码:
思路:如果要从pos位置开始提取的子串长度>=_size 那么就直接将n设置为_size-pos,也就是直接提取这一段,不然,就是不会超过_size,那么直接就开n个大小空间,利用循环并且控制好下标放到tmp里面就可以了
这里面pos + len 其实等价于 比如 pos +1 pos+1 - pos = 1这样其实算的是从pos位置开始计算的个数,也就是_size-pos,和上面的小知识点一样
string substr(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
size_t n = len;
if (len == npos || pos + len >= _size)
{
n = _size - pos;
}
string tmp;
tmp.reserve(n);
for (size_t i = pos; i < pos + n; i++)
{
tmp += _str[i];
}
return tmp;//返回值是自定义类型,必须调用拷贝构造进行深拷贝
}
iterator迭代器 左闭右开[ )
在string类域里是一个char*指针类型重命名
typedef 这里的规律是 原本的类型放在中间,自定义名字放在右边,要加分号,宏才不加分号
定义
这里实现了两个版本,一个是读写 ,另一个是const对象的只读
end指针 return _str+_size 保证了end是开区间,指向最后一个的下一个
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
使用
细节:只要支持了迭代器,编译器自动支持范围for
并且使用迭代器时用auto自动推导(迭代器类型有点长)
ljh::string::iterator it = s1.begin();
while (it < s1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
const ljh::string s3("hello world");
s3[0];
//ljh::string::const_iterator it1 = s3.begin();
auto it1 = s3.begin();
while (it1 < s3.end())
{
cout << *it1;
it1++;
}
cout << endl;