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

【C++】经典string类问题

目录

1. 浅拷贝

2. 深拷贝 

3. string类传统写法

4. string类现代版写法 

5. 自定义类实现swap成员函数

6. 标准库swap函数的调用 

7. 引用计数和写时拷贝


1. 浅拷贝

若string类没有显示定义拷贝构造函数与赋值运算符重载,编译器会自动生成默认的,编译器生成的默认版本只会简单的复制指针地址,当用s1构造s2时,s1的_str指针存了"hello"的地址,拷贝给s2后,两者都指向同一块内存,这种拷贝方式叫做浅拷贝。当s1和s2先后析构时,这块内存会被delete两次,一旦其中一个对象释放了这块内存,_str所指的空间被释放掉,另一个对象的_str指针就会变成野指针,再次释放就会导致程序崩溃。

2. 深拷贝 

要避免浅拷贝问题,就需要自己实现深拷贝,让s2有独立的内存复制s1的内容,每个对象都有自己独立的内存空间,这样析构时各删各的就不会出问题。

//拷贝构造函数(实现深拷贝)
string(const string& s)
{_str = new char[s._capacity + 1]; //新开辟内存strcpy(_str, s._str); //复制内容_size = s._size;_capacity = s._capacity;
}

如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显示给出来。

3. string类传统写法

class string
{
public://默认构造函数string(const char* str=""){if (str == nullptr) //strlen(nullptr)会触发未定义行为可能导致程序崩溃{str = "";     //将nullptr转为空字符串}_size = strlen(str);_str = new char[_size + 1];_capacity = _size;strcpy(_str, str);}//拷贝构造函数(深拷贝)string(const string& s):_str(new char[s._capacity + 1]), _size(s._size), _capacity(s._capacity){		strcpy(_str, s._str);  //拷贝字符串内容		}//赋值运算符重载(深拷贝)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;}}//析构函数~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;	
};

4. string类现代版写法 

class string
{
public://默认构造函数string(const char* str=""){if (str == nullptr){str = "";     //将nullptr转为空字符串}_size = strlen(str);_str = new char[_size + 1];_capacity = _size;strcpy(_str, str);}//swap成员函数void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}//拷贝构造函数优化 s2 = s1string(const string& s) //没有在成员初始化列表进行显式初始化时,使用了默认成员初始化器,防止默认初始化成随机值。{string tmp(s._str); //用s的字符串数据创建局部对象tmp   这里也可以调用拷贝构造string tmp(s);swap(tmp);          //交换当前对象s2和tmp     tmp出了作用域调用析构函数销毁}赋值运算符重载优化  s2 = s1//string& operator=(const string& s)//{		//	string tmp(s._str);//	swap(tmp);		//	return *this;//}//赋值运算符重载再优化  s2 = s1string& operator=(string tmp)  //传值传参触发拷贝构造!使用s1构造局部对象tmp{swap(tmp);                 //交换当前对象s2和tmp return *this;}//析构函数~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;
};

5. 自定义类实现swap成员函数

当自定义string类实现了swap成员函数,执行 std::swap(s1,s2) 时会通过模版机制自动转发到swap成员函数即s1.swap(s2),完成高效交换(交换内部指针,长度等,避免深拷贝),实现高效交换。如果自定义类中不实现成员swap,std::swap会走模版逻辑:

  • C++98:T c(a); a=b; b=c; 走“1次拷贝构造+2次拷贝赋值”的逻辑,3次深拷贝开销。
  • C++11:T c(std::move(a)); a=std::move(b); b=std::move(c);移动构造+两次移动赋值,依赖类的移动语义。

std::swap是一个模版函数,它的标准实现大致如下:

namespace std 
{// 默认模板:通过三次赋值实现交换(对于无swap成员函数的类类型交换:走深拷贝 对于内置类型的交换:无性能损耗)template <class T>void swap(T& a, T& b) {T temp = std::move(a);a = std::move(b);b = std::move(temp);}// 特化模板:若类型T存在swap成员函数,则调用该成员函数template <class T>void swap(T& a, T& b, std::enable_if_t<std::is_class_v<T> &&     {a.swap(b);     //……检测T是否有swap成员函数,如果有就调用它。}
}

这个模版的特别之处在于:它会自动检测类型T是否存在swap成员函数,若类型T未定义swap成员函数,std::swap会使用默认模版逻辑。

注意:std::swap模版的核心机制是严格匹配成员函数名swap,若成员函数名叫其它名字(如my_swap)无法转发调用成员函数,只能走模版逻辑。

总结:所以,只要类有swap成员函数,调用std::swap(s1,s2)最终会转调s1.swap(s2)。

    //swap成员函数void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}

示例:

int main()
{string s1("hello world");string s2("xxxxxxxxxxxxxxxxxxx");std::swap(s1, s2);//通过模版机制自动转发调用成员函数s1.swap(s2)cout << s1 << endl;cout << s2 << endl;s1.swap(s2); //调用成员函数cout << s1 << endl;cout << s2 << endl;return 0;
}

运行结果:

std::string类自身实现了swap成员函数,用于高效交换两个字符串的内部数据,直接交换内部的指针、大小、容量,相当于只交换“容器的壳”,数据本体不动,效率接近O(1),几乎是“零拷贝”操作,比默认的拷贝逻辑快得多。

6. 标准库swap函数的调用 

当调用 std::swap(basic_string)  这个全局函数时,它内部实际上会转发调用 basic_string 类的成员函数swap,也就是 std::basic_string::swap  。 所以,当swap(s1,s2)时,会匹配到特化版本,实际执行的是string对象成员的swap逻辑,等价于s1.swap(s2),例:

#include <string>
#include <iostream>
using namespace std;int main() 
{string s1 = "Hello";string s2 = "World";//调用全局 swap(匹配特化版本)swap(s1, s2); //内部转发调用s1.swap(s2);cout << "s1: " << s1 << ", s2: " << s2 << endl; // 输出 s1: World, s2: Helloreturn 0;
}

简单来说,怎么方便怎么写,想写什么写什么,它们最终执行的是同一个逻辑。

成员函数风格:s1.swap(s2)   

全局函数风格:swap(s1,s2) std::(s1,s2)

7. 引用计数和写时拷贝

引用计数

用来记录有多少个对象正在引用该资源。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源, 如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。

写时拷贝 

当对象被复制时,采用浅拷贝的方式,不立即拷贝实际数据(“写时”才拷贝),此时多个对象共享同一资源。普通浅拷贝若多个对象共享数据,修改时会影响所有对象,所以当某个对象需要修改数据时,会触发数据的深拷贝,确保修改不会影响其他共享该数据的对象。

比如:先构造s1,再调用拷贝构造构造s2,我们走浅拷贝,将引用计数+1变成2,销毁s2的时候,不需要释放资源,只需要将引用计数-1变成1,对象的资源留给最后一个使用者释放。但是如果要修改s2,就需要引用计数-1,再执行深拷贝,修改s2的数据,对s2的修改不影响s1。

引用计数和写时拷贝通过“延迟拷贝”和“共享资源”减少开销,适用于读多写少的场景。

http://www.dtcms.com/a/263444.html

相关文章:

  • 【数字人开发】结合nextHuman平台进行数字人网页端开发
  • VMware 在局域网环境将虚拟机内部ip 端口开放
  • 【读代码】TradingAgents:基于多智能体LLM的金融交易框架深度解析
  • STM32 rs485实现中断DMA模式收发不定长数据
  • STM32-第一节-新建工程,GPIO输出(LED,蜂鸣器)
  • SQuirreL SQL:一个免费的通用数据库开发工具
  • 华为云Flexus+DeepSeek征文 | 基于华为云Dify-LLM搭建知识库问答助手
  • 怎么在手机上预约心理咨询师
  • MySQL索引失效场景
  • 【软考高项论文】信息系统项目的资源管理
  • 大模型在急性左心衰竭预测与临床方案制定中的应用研究
  • 【Redis面试篇】Redis高频八股汇总
  • 长短期记忆网络(LSTM):让神经网络拥有 “持久记忆力” 的神奇魔法
  • 周赛98补题
  • Go语言安装使用教程
  • Golang的多环境配置
  • 「Java流程控制」while循环
  • Redis 实现消息队列
  • 【软考高项论文】论信息系统项目的质量管理
  • js代码01
  • 【数据分析】环境数据降维与聚类分析教程:从PCA到可视化
  • uniapp+vue2 input不显示明文密码,点击小眼睛显示或隐藏密码
  • “对象创建”模式之原型模式
  • window显示驱动开发—全屏模式
  • SuperGlue:基于图神经网络的特征匹配技术解析
  • 【Linux系统部分】在Linux命令行中写一个简单的shell外壳
  • ansible的剧本文件一般放在什么地方?
  • creo 2.0学习笔记
  • Stanford_CS224W----Machine learning with graph
  • (5)pytest-yield操作