文章目录
- 1. 内存管理
- operator new和operator delete
- 面试题:malloc、free和new、delete的区别
- 2. 内存泄漏
- 1. 内存泄漏:
- 2. 内存泄漏危害:
- 3.堆内存泄漏
- 4.系统资源泄漏
- 3. 模板初阶
- 函数模板
- 类模板:模板运行时不检查数据类型,也不保证类型安全,相当于类型的宏替换
- 4. STL:标准模板库:standard template libarary
- 5. STL-string -管理字符串的类
- 编码
- string 接口了解
- 迭代器
- 一般迭代器
- const迭代器
- string的增容问题:
- find查找
- insert插入
- erase删除
- opeartor<<
- string支持比较大小
- string相关题目
- 6.string类的模拟实现
- string
- string::swap()和swap()的区别
- 运算符重载
- string拓展
1. 内存管理
-
对于内置类型申请和释放,就是用法上的区别。
-
new和delete的实现原理:
在申请自定义类型时,malloc和free仅仅只会申请和释放空间,new在底层调用operator new 全局函数来申请空间,在申请的空间上实现构造函数,实现对象的初始化,同理delete会调用operator delete析构函数,清理对象中的资源,再释放空间。
-
区别:
用法上:一个是函数一个是操作符;malloc需要强转类型。
底层上:malloc失败返回空,new失败抛异常。
operator new和operator delete
-
C语言内存管理用malloc 库函数,C++用的是new delete是操作符。
-
operator new和Operator delete是系统提供的全局函数,并非是对于new和delete的重载函数。不会调用构造函数,仅仅是单纯的开空间。new转换为一段指令,调用的就是operator new,operator new是对于malloc 的封装。同理operator delete 调用的是free。
-
C语言出错是返回错误码,C++等面向对象不喜欢返回错误码,而是直接返回你的错误地方,new失败了以后,就是对于调用malloc 申请内存失败以后,从返回改为抛异常处理错误。
-
总结:new会调用operator new开空间,又是malloc的一层封装+失败抛异常处理。new开完空间之后还会调用构造函数。
-
new T[N]的原理
-
调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请。
int* p=(int*)operator new[](sizeof(int)*10);
-
在申请的空间上执行N次构造函数。
-
-
delete[]的原理
- 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
- 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间
-
重载专属的operator new
频繁的向内存申请内存效率不高,所以可以搞一个内存池。链表每创建一个节点都需要申请一次内存。正常是调用全局的operator new,也就是malloc。可以在自定义类型ListNode类中重载一个operator new函数,空间配置器allocator。
-
定位new
新申请的空间,手动调用构造函数不是以前的自动调用来初始化。这个空间如果是内存池来的,也就不是从堆来的,就可以调用定位new进行初始化。构造函数不能够显示调用,析构函数可以。
int main() { A* p=(A*)malloc(sizeof(A)); new(p)A; new(p)A(1); A* p2=new A(2); //等价于 A* p3=(A*)operator new(sizeof(A)); new(p3)A(2); }
面试题:malloc、free和new、delete的区别
- 共同点:都是在堆上申请空间,并且需要手动释放内存
- 不同点:
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new会调用函数初始化
- malloc申请空间时需要手动计算空间大小并传递,new只需要在后面跟上空间的类型就可。
- malloc的返回值是void*类型需要强制类型转换,new不需要,返回的就是后面跟的类型的指针。
- malloc申请内存失败返回的是NULL,因此使用时需要判空,new是捕获异常。
- 申请自定义一类型对象时,mallocfree只会开辟空间不会调用构造函数,而new会调用构造函数进行对象的初始化,delete在释放空间之前会调用析构函数进行空间中资源的清理。
2. 内存泄漏
1. 内存泄漏:
动态申请的内存不使用了,没有主动释放。
2. 内存泄漏危害:
内存泄漏不一定会有危害,只要进程正常结束就还给内存了。非正常结束,僵尸进程,使得内存没有还给系统。需要长期运行的程序,出现内存泄漏危害很大,系统越来越慢甚至卡死宕机。服务器程序,后台程序等。测试是不一定能够测出来的。java有回收机制。早期的安卓系统都是有内存泄漏的,后台的APP挂着,定期得重启一遍系统。
3.堆内存泄漏
通过malloc、new等从堆上分配的内存用完之后一定要用对应的free和delete删掉,如果忘了,这块内存无法再被使用产生Heap Leak。
在堆上面最大申请多大的空间呢?不能超过7fffff,2G差不多。无法申请整个的连续的空间。堆空间32位平台,4G大小,64位平台 2^32 * 4G这么大。更改的时候验证指针大小,如果是8个字节就说明改成64位平台了。
4.系统资源泄漏
程序使用系统分配的资源,套接字,文件描述符,管道等没有使用函数释放,导致资源的浪费,效能减少。
3. 模板初阶
C语言为什么不提供数据结构呢?因为他不支持泛型编程,比如栈空间只能存int类型。泛型编程(C语言并不支持,C++等进行升级,然后支持使用)。函数重载的使用可以使得不同类型的函数实现相同功能。为了避免不用类型都得书写自己的函数重载,就提供了模板。
函数模板
不给具体类型,给一个T。跟内置类型和自定义类型无关。
template<class T>或者template<typename T>//模板参数列表中都是参数类型
void Swap(T& x1,T& x2)//函数参数列表--参数对象
-
不同类型调用的是同一个函数吗?(在采用相同模板的情况下)。
不是。调用函数调用栈帧的时候,模板在实例化进程中,类型都不一样,开的空间也不一样,模板的实例化的过程就是实例化出对应函数的过程,都是编译器去完成的,以前是自己写的。比如当用double 类型进行使用函数模板的时候 ,编译器会通过对于实参类型的推演,将T设置为double类型。然后产生一份专门处理double类型的代码。你调用什么类型我给你实例化出什么类型。
但是T实例化时不能整两个类型的传过来,你传来的必须是一个类型的才能实例化成功。传来的时候你可以强转成一个可以。显示实例化也可以,指定T用什么类型,相近类型可以隐士类型转换的可以。
- 函数模板和普通函数同时存在时,会优先调用普通函数。
- C语言采用
typedef int DataType
宏的话,不能够同时使用两个类型。
类模板:模板运行时不检查数据类型,也不保证类型安全,相当于类型的宏替换
template<class T>//给定类的模板T
类型:Stack<T> //显示实例化,直接表示T是int。所以在指定迭代器时要写明类型
类名:Stack
以前是类名类型是一样的,类模板这里就要区分开来。
在全局函数中,要指定;类里面声明,类外面实现函数包含模板T时,函数指定类域,类要声明出来。调用的时候就可以正常调用了。
4. STL:标准模板库:standard template libarary
是数据结构与算法的软件框架。
5. STL-string -管理字符串的类
- string还是一个类模板,字符串还有其他类型吗?有,跟编码有关,不止于英文字母。
- c++里面管理字符串的类就是basic_string这个类。
编码
-
计算机中是二进制01010,不同值表示各种状态,引入编码表:编码二进制值和符号之间的映射关系。内存中存储的都是整形值,根据映射关系找到字符,如下图:
-
英文:ASCII码表
-
Unicode:表示全世界文字的编码表,兼容ASCII。Linux默认是utf-8。utf-16(编码-》另一门学科)
-
gbk:中文自己量身制定的编码表,各种字符并不好表示,会出现两个字节来表示一个汉字。Windows默认是gbk.
-
感受编码规律:对于存储位置的值进行修改,就会出现相近读音的字。(骂人语言谐音梗),把类似的都放到词库中,如果匹配就直接转化为****。类似:我草,卧槽,我操,把一个范围都给屏蔽了。
template <class T>
class basic_string//这里就不是简单的string类,就是考虑到编码的原因表示字符串
{}
有些字符串就是两个字符表示一个文字,所以搞出了wchar宽字节。char:一个字节。wchar:两个字节。wstring:每个字符就是wchar,都是为了方便编码。这两个区别就可以看出模板的应用场景了。平时每个字符就是T=char.
string 接口了解
s.size();//字符串现在的有效字符个数
s.capacity();//容量,数组扩容现在有多大空间。
//构造函数的几种形式:
int main()
{
std::string s1 = "yuanwei是帅比";
//拷贝构造,传的是对象
string s2(s1);
cout << s1 << s2 << endl;
//部分初始化
string s3(s2,4);//起始位置默认长度为-1,即后面所有字符有多少取多少
string s4(s2,4,6);//指定长度
cout << s3 << endl;
cout << s4 << endl;
//传的是char*
string s5(s1.c_str(),7);//指定个数
cout << s5 << endl;
string s6(s1.c_str());//默认是-1,即取得后面所有字符
cout << s6 << endl;
//迭代器区间初始化
string s7(s1.begin(),s1.end());
cout << s7 << endl;
//指定内容
string s8(10,'!');
cout << s8 << endl;
return 0;
}
//引用返回,出了作用域这个对象还在,数组和数组中的字符在堆上不在栈上。
//修改第i个位置的值
//引用返回不是以前的为了减少拷贝,是为了支持修改之后的返回的对象。
char& operator[](size_t pos)
{
return _str[pos];
}
//operator[]
string s1;
for(int i=0;i<s1.size();i++)
{
cout<<s1[i]<<" ";//实际上是编译器转换为operator[]
//s1.operator[](i)<<" ";
}
for(int i=0;i<s1.size();i++)
{
s1[i]+=1;//会对于原来的字符串中的每个字符在编码的过程中对于内容进行修改
}
//接口at
for(int i=0;i<s1.size();i++)
{
s1.at(i)-=1;//得到字符串中的字符
}
[]和at的区别:检查越界的方式不一样,[]是断言,at是抛异常
//尾插插入字符
s1.push_back('a');
//尾插字符串
s1.append("bcde");
//运算符重载:
s1+="hello world";
- string 类的遍历和修改的方式
(1)下标+[] 内在就是数组的形式进行存储的,C++的类支持运算符重载
std::string::operator[]
char& operator[] (size_t pos);//返回位置的引用
const char& operator[] (size_t pos) const;
//二者支持函数重载,一个是可修改一个是不可修改。二者的this指针不同,有无const的区别
string s1("hello world");
for(size_t i=0;i<s1.size();i++)
{
cout<<s1[i]<<" ";
}
(2)迭代器(暂时先理解为像指针一样的类型)
//Iterator是在对应的类里面定义的
//类似vector<int>::iterator,string 仍然是类模板basic_string<char>::iterator
string::iterator it=s1.begin();
while(it!=s1.end()) //end()理解为一个指向最后一个数据的下一个位置,字符串里面也就是\0
{
cout<<*it<<" ";
*it-=1;//支持修改,行为向指针一样
++it;
}
(3)范围for(C++11中才可以使用 )//自动向后迭代,自动判断结束
for(auto e:s1)//e是s1的一份临时拷贝,所以只能进行读的功能
//auto 会自动推导出数据的类型
//范围for底层也会替换成为迭代器,所以数据结构支持迭代器就可以用范围for
{
cout<<e<<" ";
}
//进行修改的功能,要修改就要加上引用标志
for(auto & e:s1)
{
e+=1;
cout<<e<<" ";
}
迭代器
一般迭代器
-
迭代器遍历的意义?-是为了支持所有的容器都可以使用迭代器来访问修改
既然存在下标和[]的组合都可以解决正反遍历。
string 可以不用,因为底层是数组的形式进行存储的,物理空间也是连续的。
但是其他的数据结构需要迭代器这一通用的遍历方式,例如list map set 等不支持**[]下标**的形式。对于string得会用迭代器,但是一般是下标+[]。
上面使用的是正向打印字符串,如果需要反向打印:
string::reverse_iterator rit=s1.rbegin();
//理解上面rbegin()作为一个指针指向的是最后一个数据的位置
while(rit!=s1.rend())//其他数据结构节点地址之间没有大小关系
{
cout<<*rit<<" ";
++rit;//是向前走,但是仍然是++,++就是指由begin-》end的正方向
}
const迭代器
一般的string类型是T, 而const T 解引用的值就不能修改即,此迭代器就不能够修改。const 修饰的是this 指针,专门用于const容器。
string的增容问题:
vs锁采用的STL得PJ版本下,第一次起始空间在对象s的数组_buf[]中,增容时malloc空间为32,后续的1.5倍空间在堆上。
capacity 的大小是其中数据的有效个数,不算/0 的那一部分空间(但是在开空间的时候就算进去了)
注:不同编译器默认的string大小是不同的,增容的方式例如几倍进行扩容也是不一样的。
void reserve(100); //申请存储至少100个字符空间,这样可以减少频繁扩容的次数。只负责开空间影响容量
void resize(100,x);//开空间然后会给空间当中的数据一个初始值
cout<<s1.c_str()<<endl;//遇到/0自动结束
find查找
//取出文件后缀
sring file("test.txt");
size_t pos=file.find('.');//返回第一个匹配的位置
if(pos != string::npos)//npos是size_t类型=-1,其实是很大值代表没找到。一个字符串很少会4G大小
{
string suffix=file.substr(pos,file.size()-pos);//起始位置,长度(size()就是最后一个数据的下一个位置-起始位置)0~9应该是10个,也就是10-0,用9的下一个位置
string suffix=file.substr(pos);//如果长度缺省,默认npos这个极大值,就会取值到结尾。
cout<<suffix<<endl;
}
//从右往左去找rfind()
sring file("test.txt.zip");
size_t pos=file.rfind('.');//从后面开始找 .
if(pos!=string::npos)
{
string suffix=file.substr(pos);
cout<<suffix<<endl;
}
//应用:将网址区分开(协议// 域名 / 路径)
string url="https://www.lyingedu.com/task-implement.html";
size_t pos1=url.find(':');//默认从0位置开始找
string protocol= url.substr(0,pos1-0);
//域名
size_t pos2=url.find('/',pos1+3);//从目标位置开始找
string somain=url.substr(pos1+3,pos2-(pos1+3));
cout<<domain<<endl;
//路径
string low=utl.find(pos2+1);//从/下一位开始向后面开始找直到最后
cout<<low<<endl;
insert插入
连续的空间插入和删除造成效率很低O(N)
erase删除
尽量少用头部和中间的删除,挪动数据效率低
opeartor<<
应用:求字符串最后面一个单词的长度
int main()
{
string s;
getline(cin ,s);
size_t pos=s.rfind(' ');//从后面开始找
if(pos==string::npos)//如果只有一个单词,也就是往前找没找到空格
{
cout<<s.size()<<endl;
}
else
{
cout<<s.size()-pos-1;
}
}
避免因空格使得输入的字符串组误操作只输入进一个字符串,cin和scanf都是以空格或者换行为分割单位,所以需要手动实现获取一行
string s1;
char ch=getchar();//一个一个去读取然后放到s1的后面
while(ch!='\n')//默认都是一行数据
{
s1+=ch;
ch=getchar();
}
cout<<s1<<endl;
//或者
string s1;
getline(cin, s);//istream&返回值,原理类似于上面写的代码
string支持比较大小
string类当中存在字符串重载
< = 等符号。
string s1("hello world");
string s2("hhhhh");
cout<<(s1<s2)<<endl;//需要考虑运算符优先级
- stoi to_string
- 调试技巧-匿名对象调用类函数
string相关题目
//判断是否为回文字符串:只考虑字符和数字,忽略大小写
bool IsHefa(char ch)//这个函数对于0的判断存在问题
{
if(ch>=0&&ch<=9)return true;
if(ch>='a'&&ch<='z')return true;
if(ch>='A'&&ch<='Z')return true;
return false;
}
bool IsPalindrome(string s){
int begin =0,end=s.size()-1;//无符号这里比较危险,--之后涉及到end<0的问题-1极大值越界
while(begin<end)
{
//左右各自找到字符和数字
while(begin<end&&!IsHefa(s[begin]))
++begin;
while(begin<end&&!IsHefa(s[end]))
--end;
if(tolower(s[begin++])!=tolower(s[end--]))//如何将大小写匹配实现?tolower(s[i]);
return false;
}
return true;
}
-
字符串相加https://leetcode.cn/problems/add-strings/submissions/
注:当整形很大相加之后越界int,就可以将数字都放在字符串中相加
头插是O(n),累计头插就是O(N^2),所以可以直接尾插加然后reverse->O(2n)
class Solution { public: string addStrings(string num1, string num2) { int end1=num1.size()-1; int end2=num2.size()-1; int next=0; string str; while(end1>=0 || end2>=0)//长的结束才算结束 { //短的那个先走完了,那么下次取值就默认取0和长的进行相加 int x1=0; if(end1>=0) { x1=num1[end1]-'0'; --end1; } int x2=0; if(end2>=0) { x2=num2[end2]-'0'; --end2; } int ret=x1+x2+next; if(ret>9) { next=1;//两数相加<20 ret-=10; } else { next=0;//每一位的运算之后都需要处理进位重置 } //头插,因为是从后往前的按位运算 // str.insert(str.begin(),ret+'0'); str+=ret+'0'; } if(next==1)//同时结束进位没处理 { str.insert(str.begin(),'1'); } reverse(str.begin(),str.end());//<algorithm.h> return str; } };
-
翻转前K个字符
//翻转字符串给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。
//如果剩余字符少于 k 个,则将剩余字符全部反转。
//如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。
string reverseStr(string s, int k)
{
int n = s.length();
for (int i = 0; i < n; i += 2 * k) {
reverse(s.begin() + i, s.begin() + min(i + k, n));//翻转的两个指针:头指针和最后一个需要翻转数据的下一个位置的指针
}
return s;
}
//字符串相加 长整形相乘就涉及到越界的问题
string assStrings(string num1,string num2)
{
int end1=num1.size()-1,end2=num2.size()-1;
int next=0;//考虑到进位的问题
string retStr;
while(end1>=0||end2>=0)//当两个都到头才会出去
{
//为了避免出现一个到头就出去的问题导致后续麻烦
//就将短的字符前面补0,实现两个字符串的数字长度相同
int x1=0;
if(end1>=0)
{
x1=num1[end1]-'0';
--end1;
}
int x2=0;
if(end2>=0)
{
x2=num2[end2]-'0';
--end2;
}
int retval=x1+x2+next;
if(retval>9)
{
next=1;
retval-=10;
}
else
{
next=0;//如果对于进位不做处理,就会出现不应该进位的时候因为未覆盖而失误就多加一位
}
retStr+=retval+'0';//尾插单个字符//注意这里是**** +号。可不能写成减号
}
//出来就说明加完了
if(next==1)
retStr+='1';
reverse (retStr.begin(),retStr.end());//将字符串逆置.这种区间是前闭后开的区间,end()是指向最后一个数据的下一个位置
return retStr;
}
6.string类的模拟实现
- 深拷贝:将原空间大小拷贝过来,再将数据拷贝,指向两个空间。
- 浅拷贝:只是拷贝一个指针,两个内容相同的指针指向一个空间,在释放空间的时候就会造成一块空间两次释放。
- 浅拷贝的危害:由于两个对象指向的是一个空间,当各自调用析构函数释放空间时第二个释放时造成崩溃。
//赋值构造 修改之后的this指向对象还存在所以用&返回
//先释放原来的空间,开辟和目标一样大小的空间再将数据拷贝过来,而不是直接拷贝造成对于空间大小的评判
string &operator =(const string s)
{
if(this!=&s)
{
delete[] _str;
_str=new char[strlen(s._str)+1];//在申请空间的时候,如果申请失败就会抛异常
strcpy(_str,s._str);
}
return *this;
}
//升级版本:先不要释放空间避免出现前后大小空间问题造成strcpy()崩溃问题,如果开空间失败就直接catch并且不对原来空间造成影响。
string &operator =(const string s)
{
if(this!=&s)
{
char* tmp=new char[strlen(s._str)+1];
delete[] _str;
strcpy(tmp,s._str);
_str=tmp;
}
return *this;
}
string
值拷贝(浅拷贝)
两个指针指向一块空间就会再释放空间的时候多释放一次。
深拷贝就会拷贝一个一样的空间,两个指针指向的是两个空间。
- 传统写法的拷贝构造
string (const string &s)
:_str(new char [strlen(s._str)+1])
//正常开一块一样大小的空间
{
strcpy(_str,s._str);
//将数据字符串进行拷贝到新的空间
}
string &operator=(const string &s)
{
if(this !=&s)
{
char *tmp=new char[strlen(s._str)+1];
stpcpy(tmp,s._str);
//值的更新的地方
delete[] _str;//将原始指针删除,避免内存泄漏
_str=tmp;
}
return *this;
}
- 现代写法的拷贝构造
string(const string &s)
:_str(nullptr)//事先完成置空否则是随机值,在调用析构函数的时候,会造成崩溃
{
string tmp(s._str);
//复用拷贝构造,当出这块空间爱你tmp复用析构函数删除原来空间
//还是完成了深拷贝
swap(_str , tmp._str);
//将两个空间的字符串进行交换
//空和字符串内容直接进行交换,一个完成赋值,一个正好置空
}
//可应用到其他数据结构,比如链表的拷贝
string &operator=(const string &s)//传参的时候有一个拷贝构造
{
//不判断自己给自己赋值
swap(_str,s._str);
return *this;
}
~string()
{
if(_str)
{
delete[]_str;
_str=nullptr;
}
}
capacity是指有效字符你的存储大小
s1.swap(s2);//成员函数swap效率更高仅仅是对成员变量进行交换
swap(s1,s2);//全局swap中间经历了三次string大项的深拷贝
插入-扩容问题:满了之后未知下一个字符串长度,不能简单的扩展2倍
push_back(' ');
append("hello world");
//扩容函数
void reserve(size_t n)
{
if(n>_capacity)
{
char* tmp=new char[n+1];
strcpy(tmp,_str);
delete[]_str;
_str=tmp;
_capacity=n;
}
}
void push_back(char ch)
{
if(_size==_capacity)
{
reserve(_capacity*2);
}
_str[_size]=ch;
++_size;
_str[_size]='\0';
}
void append(const char* str)
{
//往后面加字符串涉及到增容的问题
size_t len=strlen(str);
if(_size+len>_capacity)
{
reserve(_size+len );
}
strcpy(_str+_size,str);
_size+=len;//别忘了更新size大小
void resize(size_t n,char ch)
{
if(n<=_size)//如果空间比之前的小,就将\0直接加到n结尾就实现了删除后面不要的内容
{
_str[n]='\0';
_size=n;
}
else
{
if(n>_capacity)
{
reserve(n);
}
//将未填写字符的空间进行初始化
memset(-str+_size,ch,n-_size);//从哪里开始/初始化为什么/几个需要呗初始化
_size=n;
_str[_size]='\0';
}
}
- 查找
size_t find(const char* s,size_t pos=0)
{
const char* ptr=strstr(_str+pos,s);
if(ptr==nullptr)
{
return npos;
}
else
{
return ptr-_str;//想返回的是目标字符串的下标,指针相减得到数字 }
}
- 进行插入
string & insert(size_t pos,char ch)
{
assert(pos<=_size);//插入涉及到扩容的问题,是size和capacity之间的事情
if(_size==_capacity)
{
reserve(_capacity==0?4:_capacity*2);
}
//从\0的下一个位置开始从后往前面移动
//当头插的时候,就排除了由于size_t 类型导致的数据死循环问题
//因为到头那里就停下来了
size_t end=_size+1;
while(end-len>=pos)
{
_str[end]=_str[end-1];
--end;
}
_str[pos]=ch;
++_size;
return *this;
}
string &insert(size_t pos,const char *s)
{
assert(pos<=_size);
size_t len =strlen(_str);
if(_size+len>_capacity)
{
reserve(_size+len );
}
size_t end=_size+len;
while(end-len>=pos) //end>pos存在数组下标越界
{
_str[end]=_str[end-len];
--end;
}
//将目标字符串拷贝到现在的空位当中
strncpy(_str+pos,s,len);//那里开始插/谁被插入/插入的长度
_size+=len;
return *this;
}
//删除
string &erase (size_t pos =0,size_t len=npos)//这是一个很大的数字
{
assert(pos<_size);
if(len==npos||pos+len>=_size)
{
_str[pos]='\0';
_size=pos;
}
else
{
//说明只需要将后续的几个字符直接粘贴替代那几个删除的字符就可以了
strcpy(str+pos,str+pos+len);
_size-=len;
}
return *this;
}
//a 和 b的值虽然相同,
//但是a.c_str()==b.c_str()比较的是存储字符串位置的地址,
//a和b是两个不同的对象,内部数据存储的位置也不相同,
//因此不相等
reserve()调整容量大小,当后续容量的调整小于已经存在的空间时
容量并不会减小,就是最大的那个。
cin有空格就截断,后一个会覆盖前一个,
最终只会输出最后一个的字符串长度
题干:输出最后一个单词的长度
include<bits/stdc++.h>
using namespace std;
int main() {
string s;
while(cin >> s);
cout << s.size();
return 0;
}
//设置为全局函数,字符串大小比较。完成一个符号其他都可以复用
bool operator<(const string &s1,const string &s2)
{
}
//重载一个流插入,流提取
//using namespace std要写在包含istream和ostream的头文件的前面
注:带有返回值的支持连续赋值,链式编程
自动识别类型就是内置类型的各种函数重载,比如cout
是否设置为友元,取决于是否需要访问私有内置类型成员
ostream& operator<<(ostream& out,const string &s)
{
//一个一个字符输出
for(auto ch:s)
{
out<<ch;
}
//整个字符串输出,但是遇到空格就会停止
//当一个字符串之间,被空格\0分开,就会导致只打印第一部分
out<<s.c_str();//所以这个是不对的
return out;
}
//流提取
从流中提取数据,然后将已知数据进行替换
void clear()
{
}
istream& operator>>(istream& in,string & s)
{
//获取流里面的字符然后插入
s.clear();//为实现替换,首先将原来数据清除,因为直接插入只是在后面插入
char ch=in.get();
while(ch!=' '&&ch!='\n')
{
s+=ch;
ch=in.get();
}
return in;
}
string::swap()和swap()的区别
- 为什么string类中还要实现一个?
string中的效率更高,只是对成员变量指针参数啥的进行交换即可。
库函数中的代价更大,三次深拷贝。一次拷贝构造和两次赋值构造。
所以自定义类型最好用自己的,内置类型可以用全局的。(仅限于C++98,C++11有右值引用)
- resize
-
insert挪动数据时:
运算符重载
比如作为key值时支持比较大小。可以是成员函数,
bool operator<(const string& s) const
{
return strcmp(_str,s._str)<0;
}
也可以定义为全局函数。
string拓展
浅拷贝的问题:
- 会析构两次
- 其中一个修改会影响另外一个对象。
深拷贝:引用计数的写时拷贝,记录有多少个指向这个空间。如果不止一个,就不释放空间只–引用计数数字,当数字只有一个时再释放,解决析构多次的问题。
修改时insert /+=/erase 等函数中先查看引用计数不是1,要先进行深拷贝再去修改,解决互相影响的问题。考虑的就是如果你不进行写入,他就不进行深拷贝了就赚了,如果对于拷贝对象进行写入,效率也差不多。
缺陷;引用计数存在线程安全的问题,需要加锁,在多线程的环境之下要付出代价
在动态库、静态库中存在情景上的问题。