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

C++ 学习之---string

  简单的介绍:

string的本质可以概括为存放字符的顺序表---即字符顺序表 

    string部分成员函数的介绍

1,size()和length()

虽然是两个名称不同的成员函数,但在功能上一模一样. 以后使用size()就可以.

由于string设计的时间比较早 , 所以length()这一名称(长度)是符合字符串的描述风格的.但随后的许多容器,尤其是树形结构的map,set等等无法用长度来描述元素个数的容器使用size()更为合适. 所以为了保持接口的一致性,又给string加上了size()函数接口.所以这是一个历史的包袱,知道就好.

2,clear()

用于清除string的字符串内容,但是不会缩小空间(毕竟通常删除内容就是为了给新的内容让位,这样再添加新内容的时候就避免的扩容的性能开销)

3,reserve() 

用于提前预留空间,通常是在知道大致字符串长度的情况下以此来避免频繁的扩容.

 4, opertor[]() 和 at()

  两者功能相同,都是借助下标访问字符串的元素,区别在于对越界访问的处理:

  • operator[] 在越界时会触发断言(assert)
  • at() 在访问越界时会抛异常(exception)

5, += ()

 最能体现SLT的方便和可读性之一的接口 , 在字符串后面追加一段字符.

6, c_str()

  和length()一样都体现了c++这门语言在c语言基础之上所带有的历史包袱 , 由于很多设计成型的项目都是由c语言开发的(尤其是很多操作系统的底层) , 为了在一定程度上兼容c语言 , c_str可以根据string返回c语言风格的 const char*类型的字符串.

小结:

由于string是c++标准里的第一个容器,所以在设计上比较面面俱到(就是很臃肿啦) , 不用全部掌握,在使用的过程中自然会发现哪些是常用的,自然就能记下来.

c++11标准引入的部分新语法:

1,关键字 auto

  为下面的范围for做铺垫 , 下面用一个例子就可以解释auto的基本用法和重要性了

    在下面使用范围for遍历容器的过程中,必须跟上完整的变量类型,这就是auto的绝佳应用场景!!!

2,范围for

对于像string这样的容器,我们可以按照传统的方式来遍历,如下:

  而有了范围for,再结合上关键字auto,在很多场景下就可以解放了!!!如下:

string模拟实现相关的问题:

问题一:c++语境之下再探头文件内函数的声明和定义问题:

        

问题二:如何用const char* 的参数初始化 char*的成员变量

        现象:字符顺序表string的一大优势在于可以根据实际需要,动态的增加和减少内部字符串的内容,这也就决定了他的底层必须是char* 而不是const char*的数组 . 可是在初始化时 , 外部传递的应该是只读的字符串 , 也就是const char* 类型  . 可是很明显 : char* 类型没法直接接受const char*类型的变量(如果可以,那就可以通过char*来改变常量字符串的内容了,非法!!!).

        解法:现在构造函数体内部计算出传入字符串的大小 , 以此提前为string地层的数组开好空间 , 接着使用c语言的memcpy函数逐字节将常量字符串的内容拷贝至底层的char*类型的数组里即可.

string(const char* arr = "")
{
	int size = strlen(arr); //计算传入字符串的大小(不包含\0)
	_arr = new char[size + 1]; //开空间时手动为\0开一个空间
	memcpy(_arr, arr, size+1); //逐字节拷贝传入字符串的内容
	_size = _capacity = size;  //string里的capacity大小不包含\0
} 

问题三:c++封装特性的体现---迭代器

        库里的:迭代器在c++里可以用于遍历容器 , 他提供了一套统一的函数接口 , 使得我们不用关心底层实现就能便捷的访问容器元素.

        咱自己的 : string的底层是字符数组,也就是一块连续的空间 , 即使抛开什么迭代器的高深概念不谈 , 要遍历一个数组也很简单 : 从数组名(首元素地址)开始 , 对每一个指针值解引用 , 就能得到每一个字符的值了. 因此 , 存储逻辑上地址连续的string的迭代器可以直接用原生指针实现,只用稍加封装即可.

//在我们自己的string类的内部添加这样的语句即可
typedef char* iterator;

    

//接着再实现一个和库里名称相同的begin和end的函数
iterator begin()
{
	return _arr; 
}
iterator end()
{
	return _arr+_size;
}

     

//使用咱自己的迭代器遍历(通过命名空间linhui和标准库的区分开来)
linhui::string::iterator it = str.begin();
while (it != str.end())
{
	std::cout << *(it++);
}

//甚至还可以使用范围for遍历(前提是begin和end要严格和库里的命名一样,不然范围for不认识)
//所以范围for底层还是用的迭代器!!!
for (auto au : str)
{
	std::cout << au;
}

    问题四:拷贝构造函数的优化写法 : 

        string类的拷贝构造函数的基本写法如下,简单来说就是自己进行深拷贝.

string(string& str)
{
size_t len = strlen(str._arr); // 自己计算字符串的长度
string tmp;                    //自己创建对象
tmp._arr = new char[len+1];    //自己开空间
memcpy(tmp._arr, str._arr, len + 1);  //自己拷贝数据
tmp._size = tmp._capacity = len;      //自己更新成员变量
}

         以上的写法在效率上没有问题 , 可是咱自己写起来却比较费劲 , 那有没有什么清爽一些的写法呢 ? 答案是肯定的 : 毕竟拷贝构造函数的核心作用就是在新的空间上创建一个对象并存放它原来的内容 ,避免新的对象和旧的对象析构时释放同一块空间的资源 那我们早就写好的构造函数岂不是也可以做到这一点,所以写法如下:

//string的成员交换函数
void swap(string& str)
{
    //下面三条语句通过使用算法库的swap函数来交换两个string对象的成员变量来达到交换string对象的效果
	std::swap(_arr, str._arr);
	std::swap(_size, str._size);
	std::swap(_capacity, str._capacity);
}
//改良的拷贝构造函数
string(string& str)
{
    string tmp(str._arr);  //使用目标string对象的字符串内容来构造一个新的局部域的string对象
                           //由于构造时天然的就有开辟新空间的逻辑,也就避免了和两个string底层的 
                             char*类型的指针变量指向同一块空间,实现了深拷贝
    swap(tmp);             //将临时的string对象和外部的string对象交换,达成了目的,并且局部的            
                             string对象在声明周期结束后会自动销毁
}

 大体逻辑如下:

  1. 构造函数本身就有开辟新空间的逻辑
  2. 一个局部对象出了当前作用域后自动销毁

 

 问题五:赋值运算符重载的优化写法:

        优化思路和上面的拷贝构造函数类似 ,只是由于少了类类型的引用作为参数这一强制要求后,代码实现可以更加跳脱


    //string的成员交换函数
void swap(string& str)
{
	std::swap(_arr, str._arr);
	std::swap(_size, str._size);
	std::swap(_capacity, str._capacity);
}
    //赋值运算符重载函数
string& operator=(string str)
{
	swap(str); //实际为 (*this).swap(str);
}

        下面是对代码的简单分析

问题六 : 流提取的高效写法---自制"栈缓冲区"

        string类的流提取实现的逻辑就是先清除原有数据,接着用新的数据覆盖,基础版本的代码如下:

std::istream& operator>>(std::istream& in, string& str)
{
	str.erase();  //清除原有数据
	char ch = 0;  //定义一个用于接受输入的变量
	while ((ch = getchar()) != '\n')   //只要没有读取到 \0 ,就继续(言下之意就是空格也能读取)
	{
		str.push_back(ch);    //读一个尾插一个(会导致频繁扩容,降低效率)

	}
	
	return in;

}

        上述代码的逻辑没有问题 , 但是存在一定的效率问题 , 让我们打印每次的扩容信息 , 请看下图: 

         接下来让我们引入缓冲区的概念 , 由于刚才使用push_back函数一个一个插入 , 那在扩容时可能涉及异地扩容以及销毁旧空间 , 当数据量比较大时对性能是一个不小的挑战 . 因此,不妨让我们利用栈区的空间 , 让堆区的空间暂时解放.如下:

std::istream& operator>>(std::istream& in, string& str)
{
	char arr[256] = {};  //由于是局部定义的空间,由操作系统自己分配空间,出了当前作用域后自己销毁
	char ch = 0;
	for (int i = 0; (ch = getchar()) != '\n'; i++)  //终止条件设为读取到换行符\n
	{
		arr[i] = ch;   //将每次读取到的内容存到这个临时的数组里

		if (i == 254)  //当临时数组满了(给\0多留一个空间)
		{
			arr[255] = '\0'; //当临时数组满了,为其末尾添加一个\0作为终止符,
			str += arr; //把这一整块空间一次性插入,这样一次最多就只会扩容一次啦
			i = 0;      //让索引下次又从0开始迭代
		}
	}
	str += arr;  //自己插入不足256个元素的临时数组的值
	return in;
}

         当我们改进了代码,再次输入同样的内容,发现没有扩容!!! 这样我们的目的就达成了

 

模拟实现时自己遇到的小坑!!!

1, 粗心大意:new的使用错误:

  new是c++里的一个关键字 , 在动态开辟空间时底层还是调用的c语言里就有的malloc , 其特殊性在于可以自动调用自定义类型的构造函数 . 使用上要注意的一个细节就是new和delete要匹配使用,否者很有可能出现让人费解的错误.

//string类的构造函数
string(const char* arr = "")
{
	int size = strlen(arr); //算出传入参数的元素个数(不含\0)
	_arr = new char(size + 1); //为底层的字符数组分配空间(size是元素的个数,后面的1是\0留的空间)
	memcpy(_arr, arr, size+1);  //将参数的字符串拷贝到string内部的字符数组里
	_size = _capacity = size;  //容量_capacity不包含\0
}
//string类的析构函数
	~string()
	{
		delete[] _arr;
	}

  上面的构造函数乍一看很合理, 毕竟是连续的空间,使用delete[]也很合适,可是带有这样的构造函数的程序运行起来会出错!!!
  

 2,扩容时忽略了\0的额外空间和空字符串的情况

        我相信,这样的错误肯定是由于我太久没有正儿八经的写代码导致的!!!

        错误解释在下面代码的注释里,主要问题还是围绕着字符串末尾的\0 , 因为在string里,capacity和size的大小并不包含 \0, 可是为了遵循c语言的规则,又必须以\0结尾!!!

//预留空间
void string::reserve(int capacity)
{
	if (capacity >= _capacity)
	{
		//char* newarr = new char[capacity]; //capacity并不包含末尾的\0,开空间时要手动+1 
		char* newarr = new char[capacity+1];
		//memcpy(_arr, newarr, _size + 1);   //粗心大意, _arr和newarr搞反了
		memcpy(newarr, _arr,_size+1);
		_arr = newarr;
		_capacity = capacity;
	}
}
//尾插单个字符
void string::push_back(char input)
{
	if (_size >= _capacity)
	{
		//reserve(_capacity *= 2);  // 最开始由于图方便,使用了*= , 这会导致传入上面的reserve函            
                                       数后永远大于capacity
		//reserve(_capacity * 2);   // 当我去掉了= , 又有问题:当对象是空字符串,也就是 
                                       _capacity=0,那乘上2二还是0,乘了个寂寞
		reserve(_capacity == 0 ? 4 : _capacity * 2); //这样才对!!!
	}
	_arr[_size++] = input;
	_arr[_size+1] = '\0';  //别忘了被覆盖的\0
}

3, 挪动数组数据时出现的隐式类型转换问题:

        请看这样一个等式 , 反直觉的是 : 在c和c++里可以成立!!!

size_t a = 0;
size_t b = -1;
cout << (a < b) ; //打印出来结果是1 , 表示a的确小于b , 所以, 0小于-1???

         出现这种情况的原因在于c和c++里的隐式类型转换 , 它通常发生在两个类型不同的两个操作符之间 , 并且往往是小范围转为大范围类型的值 , 因此在上面的等式里 : -1这一普通的整形(有符号)转为了无符号的整形 , 变成了整形值的最大值(具体多大和平台有关).

4,编译器的对拷贝构造的优化导致的歧义

        首先要说明,我的代码逻辑就是有问题,体现在拷贝构造函数里 ,如下所示

简单来讲就是:只要我的程序调用了拷贝构造函数,那必定无限递归导致栈溢出

//拷贝构造函数
string(string& str)
{
	string tmp(str); //这里我传递了string类型的参数,因此天然的构成了函数递归的条件
                     //由于没有终止条件,因此在涉及拷贝构造的时候,会无限递归调用
	swap(tmp);
}

        但是在下面这种情况中却可以正常执行!!!

//main.c 
string str("linhui");
std::cout << str.substr(3, 3).c_str(); //可以看到我调用substr函数构造了椅子字串,
                                          所以理应调用贝构造函数,从而引发无限递归

//MyString.cpp
//构建子串
string string::substr(size_t pos, size_t len) const //此时我把分支语句注释掉了
{
	/*if (pos > _size)
	{
		perror("your pos is out of range !!! be careful!!!\n");
		return ("");
	}
	else
	{*/
		string tmp;
		tmp._arr = new char[len + 1];
		memcpy(tmp._arr, _arr + pos, len);
		tmp._size = tmp._capacity = len;
		tmp._arr[tmp._size] = '\0';
		return tmp;
	//}
}

//拷贝构造函数
string(string& str)
{
	string tmp(str); //会引发无限递归调用函数自身的写法
	swap(tmp);
}
                                       

        接下来让我去掉上面substr函数中的注释,在运行试试,如下图

        要知道我传入的参数只会让程序进入下面那一坨程序 , 因此前后两次修改唯一的区别就是被执行的代码多套了一层 { } , 看似无伤大雅 , 起始更改了编译器的默认优化行为

  1. substr函数里 , 是传值返回 , 而string对象的成员变量涉及动态开辟的空间 , 所以需要进行深拷贝 来避免析构两次的问题, 也就是调用咱自己的拷贝构造函数
  2. 可以在这个例子里,我的拷贝构造函数是错的,会无限递归调用自己
  3.  可是我是用的visual studio 2022的编译器存在默认的优化行为:当出现连续的构造和拷贝构造时,直接跳过拷贝构造 , 用局部对象直接构造外部对象 . 
  4. 当substr没有分支语句带来的{}时 , 内部代码逻辑和外部联系的紧密,触发了编译器的优化 , 跳过了拷贝构造函数的执行,歪打正着的规避了错误.
  5. 当添加了分支语句后,内部的代码逻辑和外部有了一点隔阂,编译器的优化没有触发 , 因此在返回值进行拷贝构造中间对象时,触发了我拷贝构造函数里的无限递归,嘣!!!

附一张经典老图,共大家细细领会.... 

 

相关文章:

  • osgQt创建场景数据并显示
  • 003集——《利用 C# 与 AutoCAD API 开发 WPF 随机圆生成插件》(侧栏菜单+WPF窗体和控件+MVVM)
  • MSYS2功能、用途及在win10下安装
  • 分布式数据库HBase
  • 跨域问题前端解决
  • cut命令用法
  • 链表算法中常用操作和技巧
  • istio 灰度实验
  • L2-023 图着色问题 #DFS C++邻接矩阵存图
  • 46. 评论日记
  • 深入解析多功能模糊搜索:构建高效灵活的JavaScript搜索工具析
  • 深度学习中模型量化那些事
  • 解决Long类型前端精度丢失和正常传回后端问题
  • 北大:检索增强LLM推理建模
  • Ubuntu 64-bit 交叉编译 FFmpeg(高级用户指南)
  • 2025AIGC终极形态:多模态(文本/图像/音乐/视频)系统整合
  • 开源软件与自由软件:一场理念与实践的交锋
  • 2024 天梯赛——工业园区建设题解
  • CF2075D Equalization
  • 代码随想录算法训练营Day21
  • 电子商城网站制作/重庆seo杨洋
  • 做wps的网站赚钱/重庆seo哪个强
  • 如何wix 做 网站/关键词排名优化网站
  • 公司网站建设一条龙/seo导航
  • 淘宝联盟优惠券网站建设/关键词检索
  • 东莞什么行业做网站的多/武汉网站设计