文件:String.h ----- 头文件
String.cpp ----- 源文件
Test.cpp ----- 源文件
实现细节:
实现带参构造:
在实现带参构造建议不使用初始化列表,初始化去写不太好:
:_str(new char[strlen(str)+1])
用初始化列表要在这new空间,String要开自己的空间,字符串初始化的时候,把字符串拷贝给str,因为这样才能修改与扩容,+1的原因是为了多开一个空间给\0
,_size(strlen(str))
,_capacity(_size)
注意:这样的话,_size初始化也要strlen,_capacity也可以要strlen
那如果先初始化_size(strlen(str)),再用_size取初始化_str呢?
这就犯了一个致命错误:初始化列表出现的顺序并不是真正的顺序,是按声明顺序,_str(_size)先走,给了个随机值,这就坑死了,那去改声明顺序,就显得不太好,所以,建议在这不使用初始化列表,直接建立在函数体里面,就不会受这些乱七八糟的影响了
String(const char* str)
{
_size = strlen(str);
_capacity = _size;//_capacity不包含\0,不要+1,所以我们在开空间的时候才会多开一个
_str = new char[_capacity + 1];
strcpy(_str, str);//将str拷贝给到_str
}
注意:_capacity不包含\0,不要+1,所以我们在开空间的时候才会多开一个
测试函数的位置:
在类外面定义函数的话,在编译时会发生链接错误:
//String.h 文件
namespace my_home
{
class String
{
public:
//无参
String()
:_str(nullptr)
, _size(0)
, _capacity(0)
{}
private:
char* _str;
size_t _size;
size_t _capacity;
};
void test_string1()
{
string s1;
string s2("hello world");
}
}
1.类里面的默认是内联,内联不会放到符号表,就不会冲突;
2.在类外面定义全局的函数,因为头文件(String.h)在String.cpp文件包含了一份,在Test.cpp也包含了一份,那么这个函数就会在两个cpp生成的目标文件各自有一份,最后链接合到一起的时候有两份就冲突了(链接错误);
解决链接错误的方法:
1.用static修饰函数,静态的,只有在当前文件可建,相当于不进入符号表
2.内联也可以解决问题,与静态达到的效果是类似的,内联还有另外一层展开,本质也是只在当前文件可建,不进入符号表
3.最标准的还是去做声明和定义的分离
无参构造的空指针解引用:
其实上面无参的实现存在问题:空指针解引用
在String类的public添加函数:
const char* c_str()
{
return _str;
}
因为当前还没有重载<<,所以仅仅打印字符串:
void test_string1()
{
String s1;//s1是空的string对象,使用空指针进行初始化的
String s2("hello world");
cout << s1.c_str() << endl;//空指针char*在打印的时候,char*会认为是字符串,会直接解引用,遇到\0才会终止,造成了空指针的解引用
cout << s2.c_str() << endl;
}
程序崩溃
1.s1是空的string对象,使用空指针进行初始化的;
2.空指针char*在打印的时候,char*会认为是字符串,会直接解引用,遇到\0才会终止,造成了空指针的解引用;
所以,我们应该对无参构造进行修改:
String()
: _str(new char[1]{'\0'}) // _str(nullptr)
, _size(0)
, _capacity(0)
{}
为了美观,我们可以将无参和带参进行整合·:全缺省构造函数
String(const char* str = "")//不要写成nullptr问题与上面一样,字符串有\0,没必要写成"\0"这样其实是两个\0
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
注意:不要写成nullptr问题与上面一样,字符串有\0,没必要写成"\0"这样其实是两个\0
类中的声明与定义分离
短小,频繁调用的函数我们直接写在类里面,默认是内联函数:
下面几个是适合做内联的函数:
String(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
~String()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
size_t capacity()const
{
return _capacity;
}
char& operator[](size_t pos)
{
assert(pos < _size);//防止越界
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);//防止越界
return _str[pos];
}
那么当前的程序支持范围否吗?
显然,当前的情况并不支持范围for:因为其实范围for的底层就是迭代器
简单迭代器的实现:(在某些场景下会有缺陷)
//在类里面定义类型有两种方式,一种是内部类,另一种是typedef
typedef char* iterator;//在String类里typedef了iterator,类型char*,属于String这个类域
//其实迭代器模拟的是指针的行为
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;
}
//主要是底层是一个物理数组
1.在类里面定义类型有两种方式,一种是内部类,另一种是typedef ,这样就是为什么说有的容器的迭代器都一个名,但是不全是指针实现的,list就不是由原生指针;
2.在String类里typedef了iterator,类型char*,属于String这个类域;
3.其实迭代器模拟的是指针的行为;
4.迭代器是种封装的体现,屏蔽了底层实现的细节,提供了统一的类似访问容器的方式,不需要关心容器底层结构和实现细节;
//范围for
for (auto e : s2)
{
cout << e << " ";
}
cout << endl;
//迭代器
String::iterator it = s2.begin();
while (it != s2.end())
{
cout << *it << endl;
++it;
}
这时候范围for便也可以遍历,侧面说明了范围for底层就是迭代器
但是for是傻瓜式模仿,一但begin()写成Begin()就行不通
浅拷贝问题:
class String
{
public:
String(const char* str = "")
{
// 构造String类对象时,如果传递nullptr指针,可以认为程序非
if (nullptr == str)
{
assert(false);
return;
}
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~String()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
// 测试
void TestString()
{
String s1("hello bit!!!");
String s2(s1);
}
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来 。如果 对象中管理资源 ,最后就会 导致 多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该 资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规 。
所以,如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
深拷贝:
传统写法:自己开空间,自己拷贝
String(const String& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
现代写法:让别人帮自己拷贝(交换)
交换_str,如果_str是随机值(因为对于内置类型编译器未必会初始化),交换给tmp,tmp作为局部对象,出了作用域要被销毁,就相当于释放野指针了,因此,可以在声明时给缺省值
void swap(String& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
String(const String& s)
{
String tmp(s.c_str());
swap(tmp);
}
流赋值拷贝: (深拷贝)
知晓了拷贝构造,其实复制拷贝跟拷构造差不多,只不过赋值拷贝是运算符重载, 复制拷贝的问题不仅仅是浅拷贝,还导致了之前的数据被覆盖,导致内存泄漏,需要我们自行实现:
传统写法:自己开空间
String& operator=(const String& s)
{
//防止自己把自己释放了
if (this != &s)
{
delete[] _str;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
现代写法:抢夺资源(与拷贝构造不同,复制拷贝还将tmp给析构了,而拷贝构造只是tmp指向nullptr)(代码块也是一个域,出了域调用析构函数)
String& operator=(const String& s)
{
//防止自己把自己释放了
if (this != &s)
{
String tmp(s);//调用拷贝构造
swap(tmp);
}
return *this;
}
最新版本:s传给tmp会调用拷贝构造(现代写法的浓缩)(只用拷贝构造才必须用引用)
String& operator=(String tmp)
{
swap(tmp);
return *this;
}
swap:
void test_string7()
{
String s1("hello world");
String s2;
std::swap(s1, s2);
s1.swap(s2);
}
对于库里面的swap与我们自行实现得swap有什么区别吗?
库中swap实现:
C++98的库的swap模板,会深拷贝3次,所以我们自行实现的是面对C++98版本更推荐的
为了防止这个问题,其实库里面就已经为我们解决了这个问题:
交换字符串对象 x 和 y 的值,这样在调用此函数后,x 的值是调用之前位于 y 上的值,y 的值是 x 的值
这是泛型算法交换的重载,它通过相互转移对其内部数据的所有权到另一个对象(即,字符串交换对其数据的引用,而不实际复制字符)来提高其性能:它的行为就像调用了 x.swap(y)
面对模板和实例化共存,是优先走实例化的(有现成吃现成的),所以以上两种其实效率一样
流提取:分隔问题
in会跳过 ' ' '\n' , 有分隔符的概念,读整数,浮点数,字符...
那么,在实现String的流提取时,需要读到分隔符达到结束的标志,然而单单的in会直接不读空格和换行。在C语言中,可以用getc()来解决问题,C++其实也有解决的办法:get()
这时候虽然到达了输入的目的,但是如果String有东西,输入的内容直接接在原来的String变量后面,这时候我们应该自行写一个clear(),清除数据,保留原来空间:
void clear()
{
_str[0] = '\0';
_size = 0;
}
istream& operator>>(istream& in, String& s)
{
s.clear();
char ch;//C语言用getc()解决
ch = in.get();//C++中,有get(),他就不用管什么分隔符,因为他不涉及任何类型,会一个字符一个字符的读//in会跳过' ,'\n',有分隔符的概念,读整数,浮点数,字符...
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
优化:面对输入大数据,+=会不断地扩容,会有消耗,效率降低
解决:提供一个buffer,每次来的字符不往String变量(s)进行+=,而是将每次来的字符都把它放到buffer里面,如果buffer差一个满了(最后一个放\0),用Stringd变量进行+=buffer,相当于把buffer当成一个缓冲区:(buffer是栈上的,开得快,且临时)
istream& operator>>(istream& in, String& s)
{
s.clear();
const int N = 256;
char buffer[N];
int i = 0;
char ch;//C语言用getc()解决
ch = in.get();//C++中,有get(),他就不用管什么分隔符,因为他不涉及任何类型,会一个字符一个字符的读//in会跳过' ,'\n',有分隔符的概念,读整数,浮点数,字符...
while (ch != ' ' && ch != '\n')
{
buffer[i++] = ch;
if (i == N - 1)
{
buffer[i] = '\0';
s += buffer;
i = 0;
}
ch = in.get();
}
if (i > 0)//说明buffer还有字符没有+=到s
{
buffer[i] = '\0';
s += buffer;
}
return in;
}
实现代码:
头文件:String.h
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
//#include<string>
#include<assert.h>
using namespace std;
namespace home
{
class String
{
public:
//在类里面定义类型有两种方式,一种是内部类,另一种是typedef
typedef char* iterator;//在String类里typedef了iterator,类型char*,属于String这个类域
//其实迭代器模拟的是指针的行为
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;
}
String(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
void swap(String& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//显示提供拷贝构造
String(const String& s)
{
String tmp(s.c_str());
swap(tmp);
}
String& operator=(String tmp)
{
swap(tmp);
return *this;
}
~String()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
size_t capacity()const
{
return _capacity;
}
char& operator[](size_t pos)
{
assert(pos < _size);//防止越界
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);//防止越界
return _str[pos];
}
//声明和定义分离
void reserve(size_t n);
void push_back(char ch);
void append(const char* str);
String& operator += (char ch);
String& operator +=(const char* str);
void insert(size_t pos, char ch);
void insert(size_t pos, const char* str);
void erase(size_t pos, size_t len = npos);
size_t find(char ch, size_t pos = 0);
size_t find(const char* str, size_t pos = 0);
String substr(size_t pos = 0, size_t len = npos);
private:
//内置类型未必初始化
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
static const size_t npos = -1;//在这可以写,核心原因是有const
//static const double N = 1.1;//只有整型才可以
/*static const int N = 1;
int buff[N];*/
};
//const size_t String::npos = -1;
//为了支持不属于String的比较(但其实可以隐式类型转换(调用构造)的,感觉没必要)(必须要有一个类类型变量)
bool operator<(const String& s1, const String& s2);
bool operator<=(const String& s1, const String& s2);
bool operator>(const String& s1, const String& s2);
bool operator>=(const String& s1, const String& s2);
bool operator==(const String& s1, const String& s2);
bool operator!=(const String& s1, const String& s2);
//不写成成员函数是因为String会抢占cout位置
ostream& operator<<(ostream& out, const String& s);
istream& operator>>(istream& in, String& s);
void test_string1();
void test_string2();
void test_string3();
void test_string4();
void test_string5();
}
源文件:String.cpp
#include"String.h"
namespace home
{
//const size_t String::npos = -1;
void String::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];//为了\0
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void String::push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';//注意
}
void String::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
strcpy(_str + _size, str);
_size += len;
}
String& String::operator += (char ch)
{
push_back(ch);
return *this;
}
String& String::operator +=(const char* str)
{
append(str);
return *this;
}
void String::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
// 挪动数据//注意有些代码的隐式类型转化
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
}
void String::insert(size_t pos, const char* s)
{
assert(pos <= _size);
size_t len = strlen(s);
if (_size + len > _capacity)
{
// 大于2倍,需要多少开多少,小于2倍按2倍扩
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = s[i];
}
_size += len;
}
void String::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (size_t i = pos + len; i <= _size; i++)
{
_str[i - len] = _str[i];
}
_size -= len;
}
}
size_t String::find(char ch, size_t pos)
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t String::find(const char* str, size_t pos)
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
String String::substr(size_t pos, size_t len)
{
assert(pos < _size);
// len大于剩余字符长度,更新一下len
if (len > _size - pos)
{
len = _size - pos;
}
String sub;
sub.reserve(len);
for (size_t i = 0; i < len; i++)
{
sub += _str[pos + i];
}
return sub;
}
bool operator<(const String& s1, const String& s2)
{
return strcmp(s1.c_str(), s2.c_str()) < 0;
}
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);
}
bool operator==(const String& s1, const String& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator!=(const String& s1, const String& s2)
{
return !(s1 == s2);
}
ostream& operator<<(ostream& out, const String& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
istream& operator>>(istream& in, String& s)
{
s.clear();
const int N = 256;
char buffer[N];
int i = 0;
char ch;//C语言用getc()解决
ch = in.get();//C++中,有get(),他就不用管什么分隔符,因为他不涉及任何类型,会一个字符一个字符的读//in会跳过' ,'\n',有分隔符的概念,读整数,浮点数,字符...
while (ch != ' ' && ch != '\n')
{
buffer[i++] = ch;
if (i == N - 1)
{
buffer[i] = '\0';
s += buffer;
i = 0;
}
ch = in.get();
}
if (i > 0)//说明buffer还有字符没有+=到s
{
buffer[i] = '\0';
s += buffer;
}
return in;
}
}