C++string类详解
Posted Corwttaml
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++string类详解相关的知识,希望对你有一定的参考价值。
文章目录
🎪 string类
C语言中,字符串是以’\\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问
并且OJ中的字符串也基本以string类出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数
🚀1.标准库中的string类
这里给大家推荐一个查库函数的网站,我们学习C++一般以这个作为参照:
string类详解
- 字符串是表示字符序列的类
- 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
- string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。
- string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。
- 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作
总结:
string
是表示字符串的字符串类- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
- string在底层实际是:
basic_string
模板类的别名,typedef basic_string<char, char_traits, allocator>string;
- 不能操作多字节或者变长字符的序列
在使用string类时,必须包含#include头文件以及using namespace std
⭐1.1 string构造函数
常用接口如下:
constructor函数名称 | 功能说明 | 函数原型 |
---|---|---|
string() | 构造空的string类对象,即空字符串 | string(); |
string(const char * s) | 用C-string来构造string类对象 | string (const char* s, size_t n); |
string(size_t n, char c) | string类对象中包含n个字符c | string (size_t n, char c); |
string(const string&s) | 拷贝构造函数 | string (const string& str, size_t pos, size_t len = npos); |
int main()
string s1;
string s2("hello wold");
//隐式类型转换
string s3 = "hello world";
//从s3的第六个字符开始取三个字符
string s4(s3, 6, 3);
cout << s4 << endl;
//超过了范围就取到末尾
string s5(s3, 6, 12);
cout << s5 << endl;
//如果不给第三个参数呢? 官方文档给了一个缺省值 -- npos -- 42亿多
//一个字符串不可能有那么长,所有默认取到结尾
string s6(s3, 6);
cout << s6 << endl;
string s7("hello world", 6);
cout << s7 << endl;
string s8(10, 'x');
cout << s8 << endl;
//size_t是一种无符号整数类型就是unsigned int
for (size_t i = 0; i < s2.size(); i++)
s2[i]++;
cout << s2 << endl;
for (size_t i = 0; i < s2.size(); i++)
cout << s2[i] << " ";
//析构函数了解即可
return 0;
对于拷贝构造函数,如果我们没有给第三个参数,会默认是缺省值npos
,我们从官方文档可见,它是一个极大的数,我们所能用到的字符串长度不可能有这么大的,所以不给值的就默认是这个缺省值——即取到字符串末尾
对于析构函数自行看一下官方文档即可.
⭐1.2 string容量操作
常用接口如下:
函数名称 | 功能说明 | 函数原型 |
---|---|---|
size | 返回字符串有效字符长度 | size_t size() const; |
length | 返回字符串有效字符长度 | size_t length() const; |
capacity | 返回空间总大小 | size_t capacity() const; |
empty | 检测字符串释放为空串,是返回true,否则返回false | bool empty() const; |
clear | 清空有效字符 | void clear(); |
reserve | 为字符串预留空间 | void reserve (size_t n = 0); |
resize | 将有效字符的个数改成n个,多出的空间给出的字符c填充 | void resize (size_t n);void resize (size_t n, char c); |
string的容量
int main()
string s1("hello world");
//字符串有效数据位数
//size方法跟length是没有区别的
//size是为了跟stl保持一致后面再加入的,建议用size
cout << s1.size() << endl;//11
cout << s1.length() << endl;//11
//字符串容量
cout << s1.capacity() << endl;//15
//字符串所能达到的最大长度
cout << s1.max_size() << endl;//字符串的最大长度,不同编译器的值可能不同
return 0;
string的扩容
我们可以用push_back
方法向string对象里面插入一个字符
//string扩容
int main()
//观察扩容的情况,不同对象可能不同
//对于库里面的string第一次是2倍扩容,其余次数是1.5倍扩容
string s("hello world");
size_t sz = s.capacity();
cout << "capacity: " << sz << '\\n';
cout << "making s grow:\\n";
for (int i = 0; i < 100; ++i)
s.push_back('c');
if (sz != s.capacity())
sz = s.capacity();
cout << "capacity changed: " << sz << '\\n';
return 0;
实际上,string类的简单成员如下:、
class string
private:
char* _ptr;
char _buf[16];
size_t _size;
size_t _capacity;
;
当我们调试观察string内部成员,capacity超过16的话就不会存在buf
数组里面了,而是存在于ptr
申请的堆空间上了,如果没超过16那么就会存在buf数组上
但是我们知道,凡是关于扩容,我们的系统就会加大开销,所以无论我们是在做oj题还是在工作中,都尽量的事先扩好需要的容量,会大大的减小开销,以下是两个常用string扩容函数
reserve && resize
int main()
string s1("hello world");
//扩容但不初始化 111
s1.reserve(100);
cout << s1.size() << endl;//11
cout << s1.capacity() << endl;//111
string s2("hello world");
//扩容初始化,未给第二个参数的话初始化成缺省值0
s2.resize(100);
cout << s2.size() << endl;//100
cout << s2.capacity() << endl;//111
//扩容初始化,将一百个位置全部初始化为x
//但是字符串的100个位置全有值,所以字符串无变化
s2.resize(100, 'x');
cout << s2.size() << endl;
cout << s2.capacity() << endl;
//如果比size小,还可以删除数据,保留前五个数据不变,但容量不会缩容
s2.resize(5);
cout << s2.size() << endl;//5
cout << s2.capacity() << endl;//111
return 0;
resize
初始化不会动原始数据的值,空间不足则开辟的空间,那段开辟的空间里面才会存放初始化的值
无论是reserve还是resize均不会缩容(即变小_capacity的值),因为底层的OS并不允许这么做,并且这么做会造成其它的一些问题
注意:
- size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
- clear()只是将string中有效字符清空,不改变底层空间大小。
resize(size_t n)
与resize(size_t n, char c)
都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。- reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小
⭐1.3 string类对象的访问遍历操作
常用接口如下:
函数名称 | 功能说明 | 函数原型 |
---|---|---|
operator[] | 返回pos位置的字符,const string类对象调用 | char& operator[] (size_t pos); const char& operator[] (size_t pos) const; |
at | 返回pos位置的字符,const string类对象调用 | char& at (size_t pos);const char& at (size_t pos) const; |
begin,end | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 | iterator begin();const_iterator begin() const;iterator end();const_iterator end() const; |
rbegin,rend | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 | reverse_iterator rbegin();const_reverse_iterator rbegin() const;reverse_iterator rend();const_reverse_iterator rend() const; |
范围for | C++11支持更简洁的范围for的新遍历方式 | null |
operator[] && at()
//string访问操作
int main()
string s("hello world");
cout << s[0] << endl;
cout << s.at(0) << endl;
//操作符越界:内部是直接assert断言的
s[100];//断言终止
//at方法越界的话是抛异常,可以被捕获到
try
s.at(100);
catch (const exception& e)
cout << e.what() << endl;//打印异常信息
return 0;
C++有一套异常机制,一些数组越界之类的问题可以靠抛异常来解决,这个我们后面再谈.
begin | end && rbegin | rend
//string遍历操作
void Func(const string& str)
//如果继续用正常迭代器编译不通过 - const对象不允许写权限放大
//可以使用const迭代器,遍历和读容器的数据,不能写
//string::const_iterator it = str.begin();
auto it = str.begin();
while (it != str.end())
cout << *it << " ";
it++;
cout << endl;
//string::const_reverse_iterator rit = str.rbegin();
auto rit = str.rbegin();
while (rit != str.rend())
cout << *rit << " ";
++rit;
cout << endl;
//string内迭代器
int main()
string s("hello world");
//正向迭代器
//string::iterator it = s.begin();
auto it = s.begin();
while (it != s.end())
cout << *it << " ";
it++;
cout << endl;
//反向迭代器
//string::reverse_iterator rit = s.rbegin();
auto rit = s.rbegin();
while (rit != s.rend())
cout << *rit << " ";
++rit;
cout << endl;
Func(s);
return 0;
对于迭代器这种类型比较复杂,建议用auto
自动识别类型,因为const迭代器需要特别区分,所以比较容易出错.我们再用范围for试试遍历string对象(底层也是迭代器)
//范围for
int main()
string str("hello world");
for(auto c : str)
cout << c << endl;
return 0;
在string这里使用迭代器明显没有下标访问有用,那为啥还会存在迭代器呢??我们想想二叉树的遍历还能用下标吗?不能,而迭代器是通用的,并不拘泥于顺序表类型
C++11提供了cbegin
,cend
以及crbegin
,crend
来区分const对象,了解即可
⭐1.4 string类对象的修改操作
函数名称 | 功能说明 | 函数原型 |
---|---|---|
operator+= | 在字符串尾部追加一个字符串或字符 | |
append | 在字符串后追加一个字符串 | |
push_back | 在字符串后尾插字符c | |
assign | 把字符串赋给当前对象 | |
insert | 在字符串任意位置插入一个字符串 | |
erase | 删除任意位置的任意字符 | |
replace | 替换掉字符串任意位置的字符串 | |
swap | 交换两个字符串 | |
pop_back | 将字符串尾部字符删除 |
注意:
- 在string尾部追加字符时,
s.push_back(c)
/s.append(1, c)
/s += 'c'
三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。 - 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好
字符串追加
int main()
string s1("hello");
//追加一个字符
s1.push_back(' *');
cout << s1 << endl;
//追加一个字符串
s1.append("world");
cout << s1 << endl;
s1.clear();
//通用 -- 推荐使用
s1 += "hello ";
s1 += 'w';
s1 += "orld";
cout << s1 << endl;
return 0;
字符串插入删除替换
//insert erase replace
int main()
//insert:插入数据,不推荐经常使用 - 时间复杂度高,效率低
string s1("world");
//任意位置插入字符串
s1.insert(0, "hello");
cout << s1 << endl;
//插入多个字符
s1.insert(5, 3, 'a');
cout << s1 << endl;
//迭代器大法
s1.insert(s1.begin() + 5, ' ');
cout << s1 << endl;
//迭代器插入区间后面我们再讲
//erase:删除数据,也存在挪动数据,效率低下
string s2("hello world");
//第五个位置开始删除1个字符
s2.erase(5, 1);
cout << s2 << endl;
//传入迭代器,删除该位置字符
s2.erase(s2.begin() + 5);
cout << s2 << endl;
//超过长度的时候,直接删到字符串尾部,因为给了缺省值npos
s2.erase(5, 30);
cout << s2 << endl;
//replace:替换字符,效率也比较低下
string s3("hello world");
s3.replace(5, 2, "%20");
cout << s3 << endl;
//swap
s1.swap(s2);
cout << s1 << endl;
cout << s2 << endl;
swap(s1, s2);
cout << s1 << endl;
cout << s2 << endl;
//有什么区别?
//实际上我们直接库里面的swap函数会比用模板类生成的函数高效许多
return 0;
⭐1.5 string类对象的常用功能
常用接口如下:
函数名称 | 功能说明 | 函数原型 |
---|---|---|
c_str | 打印字符串吗,以\\0结尾标志结束 | |
find | 从字符串pos位置开始往后找字符串,完全匹配则返回第一个字符出现的位置 | |
refind | 从字符串pos位置开始往前找字符串,完全匹配则返回最后一个字符出现的位置 | |
find_first_of | 从字符串pos位置开始从前往后找,找到输入的字符串中任意一个字符的位置并返回 | |
find_last_of | 从字符串pos位置开始从后往前找,找到输入的字符串中任意一个字符的位置并返回 | |
substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
void Test1()
//c_str:将字符串识别成const char*,以\\0为准认定为末尾
string s1("hello world");
//区别:流插入按照size()打印,c_str按照\\0终止位置进行打印
s1 += '\\0';
s1 += "xxxxx";
cout << s1 << endl;
cout << s1.c_str() << endl;
//我们可以用c_str来将string对象转换为const char*
string filename("Work.cpp");
FILE* fout = fopen(filename.c_str(), "r");
if (fout == nullptr)
perror("fopen fail");
exit(-1);
cout << "Work.cpp:" << endl;
char ch = fgetc(fout);
while (ch != EOF)
cout << ch;
ch = fgetc(fout);
fclose(fout);
void Test2()
string file("string.cpp.tar.zip");
//从后往前找rfind
size_t pos = file.rfind('.');
if (pos != string::npos)
//string suffix = file.substr(pos, file.size() - pos);
string suffix = file.substr(pos);
cout << suffix << endl;
// 取出url中的域名
string url("http://www.cplusplus.com/reference/string/string/find/");
cout << url << endl;
size_t start = url.find("://");
if (start == string::npos)
cout << "invalid url" << endl;
return 0;
start += 3;
size_t finish = url.find('/', start);
string address = url.substr(start, finish - start);
cout << address << endl;
// 删除url的协议前缀
pos = url.find("://");
url.erase(0, pos + 3);
cout << url << endl;
void Test3()
//find_first_of 顺着找,一旦输入字符串中任意一个字符匹配成功则返回其位置
//find_last_of 倒着找,一旦输入字符串中任意一个字符匹配成功则返回其位置
string str("Please, replace the vowels in this sentence by asterisks.");
size_t found = str.find_first_of("aeiou");
while (found != string::npos)
str[found] = '*';
found = str.find_first_of("aeiou", found java中的String类常量池详解
test1:
package StringTest;
public class test1 {
/**
* @param args
*/
public static void main(String[] args){
String a = "a1";
String b = "a"+ 1;
System.out.println(a==b);
}//true
}
test2:
package StringTest;
public class test2 {
/**
* @param args
*/
public static void main(String[] args){
String a = "ab";
String bb = "b";
String b = "a"+ bb; //编译器不能确定为常量
System.out.println(a==b);
}//false
}
test3:
package StringTest;
public class test3 {
/**
* @param args
*/
public static void main(String[] args){
String a = "ab";
final String bb = "b";
String b = "a"+ bb; //bb加final后是常量,可以在编译器确定b
System.out.println(a==b);
}//true
}
test4:
package StringTest;
public class test4 {
/**
* @param args
*/
public static void main(String[] args){
String a = "ab";
final String bb = getBB();
String b = "a"+ bb;//bb是通过函数返回的,虽然知道它是final的,但不知道具体是啥,要到运行期才知道bb的值
System.out.println(a==b);
}//false
private static String getBB(){ return "b"; }
}
test5:
package StringTest;
public class test5 {
/**
* @param args
*/
private static String a = "ab";
public static void main(String[] args){
String s1 = "a";
String s2 = "b";
String s = s1 + s2;//+的用法
System.out.println(s == a);
System.out.println(s.intern() == a);//intern的含义
}//flase true
}
test6:
package StringTest;
public class test6 {
/**
* @param args
*/
private static String a = new String("ab");
public static void main(String[] args){
String s1 = "a";
String s2 = "b";
String s = s1 + s2;
System.out.println(s == a);
System.out.println(s.intern() == a);
System.out.println(s.intern() == a.intern());
}//flase false true
}
String常量池详解:
1.String使用private final char value[]来实现字符串的存储,也就是说String对象创建之后,就不能再修改此对象中存储的字符串内容,就是因为如此,才说String类型是不 可变的(immutable)。String类有一个特殊的创建方法,就是使用""双引号来创建.例如new String("i am")实际创建了2个
String对象,一个是"i am"通过""双引号创建的,另一个是通过new创建的.只不过他们创建的时期不同,
一个是编译期,一个是运行期!java对String类型重载了+操作符,可以直接使用+对两个字符串进行连接。运行期调用String类的intern()方法可以向String Pool中动态添加对象。
例1
String s1 = "sss111";
//此语句同上
String s2 = "sss111";
System.out.println(s1 == s2); //结果为true
例2
String s1 = new String("sss111");
String s2 = "sss111";
System.out.println(s1 == s2); //结果为false
例3
String s1 = new String("sss111");
s1 = s1.intern();
String s2 = "sss111";
System.out.println(s1 == s2);//结果为true
例4
String s1 = new String("111");
String s2 = "sss111";
String s3 = "sss" + "111";
String s4 = "sss" + s1;
System.out.println(s2 == s3); //true
System.out.println(s2 == s4); //false
System.out.println(s2 == s4.intern()); //true
结果上面分析,总结如下:
1.单独使用""引号创建的字符串都是常量,编译期就已经确定存储到String Pool中;
2,使用new String("")创建的对象会存储到heap中,是运行期新创建的;
3,使用只包含常量的字符串连接符如"aa" + "aa"创建的也是常量,编译期就能确定,已经确定存储到String Pool中;
4,使用包含变量的字符串连接符如"aa" + s1创建的对象是运行期才创建的,存储在heap中;
还有几个经常考的面试题:
String s1 = new String("s1") ;
String s2 = new String("s1") ;
上面创建了几个String对象?
答案:3个 ,编译期Constant Pool中创建1个,运行期heap中创建2个.(用new创建的每new一次就在堆上创建一个对象,用引号创建的如果在常量池中已有就直接指向,不用创建)
String s1 = "s1";
String s2 = s1;
s2 = "s2";
s1指向的对象中的字符串是什么?
答案: "s1"。(永远不要忘了String不可变的,s2 = "s2";实际上s2的指向就变了,因为你不可以去改变一个String,)
--------------------------------------------------------------------------------------------------------------------------------------------------
String是一个特殊的包装类数据。可以用:
String str = new String("abc");
String str = "abc";
两种的形式来创建,第一种是用new()来新建对象的,它会在存放于堆中。每调用一次就会创建一个新的对象。
而第二种是先在栈中创建一个对String类的对象引用变量str,然后通过符号引用去字符串常量池里找有没有"abc",如果没有,则将"abc"存放进字符串常量池,并令str指向”abc”,如果已经有”abc” 则直接令str指向“abc”。
比较类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==,下面用例子说明上面的理论。
String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); //true
可以看出str1和str2是指向同一个对象的。
String str1 =new String ("abc");
String str2 =new String ("abc");
System.out.println(str1==str2); // false
用new的方式是生成不同的对象。每一次生成一个。
因 此用第二种方式创建多个”abc”字符串,在内存中其实只存在一个对象而已. 这种写法有利与节省内存空间. 同时它可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于String str = new String("abc");的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。
另 一方面, 要注意: 我们在使用诸如String str = "abc";的格式定义类时,总是想当然地认为,创建了String类的对象str。担心陷阱!对象可能并没有被创建!而可能只是指向一个先前已经创建的 对象。只有通过new()方法才能保证每次都创建一个新的对象。
由于String类的immutable性质,当String变量需要经常变换其值时,应该考虑使用StringBuffer类,以提高程序效率。
1. 首先String不属于8种基本数据类型,String是一个对象。
因为对象的默认值是null,所以String的默认值也是null;但它又是一种特殊的对象,有其它对象没有的一些特性。
2. new String()和new String(”")都是申明一个新的空字符串,是空串不是null;
3. String str=”kvill”;String str=new String (”kvill”)的区别
看例1:
String s0="kvill";
String s1="kvill";
String s2="kv" + "ill";
System.out.println( s0==s1 );
System.out.println( s0==s2 );
结果为:
true
true
首先,我们要知结果为道Java会确保一个字符串常量只有一个拷贝。
因 为例子中的s0和s1中的”kvill”都是字符串常量,它们在编译期就被确定了,所以s0==s1为true;而”kv”和”ill”也都是字符串常 量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被解析为一个字符串常量,所以s2也是常量池中” kvill”的一个引用。所以我们得出s0==s1==s2;用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。
看例2:
String s0="kvill";
String s1=new String("kvill");
String s2="kv" + new String("ill");
System.out.println( s0==s1 );
System.out.println( s0==s2 );
System.out.println( s1==s2 );
结果为:
false
false
false
例 2中s0还是常量池中"kvill”的应用,s1因为无法在编译期确定,所以是运行时创建的新对象”kvill”的引用,s2因为有后半部分 new String(”ill”)所以也无法在编译期确定,所以也是一个新创建对象”kvill”的应用;明白了这些也就知道为何得出此结果了。
4. String.intern():
再补充介绍一点:存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的intern()方法就是扩充常量池的 一个方法;当一个String实例str调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用;看例3就清楚了
例3:
String s0= "kvill";
String s1=new String("kvill");
String s2=new String("kvill");
System.out.println( s0==s1 );
System.out.println( "**********" );
s1.intern();
s2=s2.intern(); //把常量池中"kvill"的引用赋给s2
System.out.println( s0==s1);
System.out.println( s0==s1.intern() );
System.out.println( s0==s2 );
结果为:
false
**********
false //虽然执行了s1.intern(),但它的返回值没有赋给s1
true //说明s1.intern()返回的是常量池中"kvill"的引用
true
最 后我再破除一个错误的理解:有人说,“使用 String.intern() 方法则可以将一个 String 类的保存到一个全局 String 表中 ,如果具有相同值的 Unicode 字符串已经在这个表中,那么该方法返回表中已有字符串的地址,如果在表中没有相同值的字符串,则将自己的地址注册到表中”如果我把他说的这个全局的 String 表理解为常量池的话,他的最后一句话,”如果在表中没有相同值的字符串,则将自己的地址注册到表中”是错的:
看例4:
String s1=new String("kvill");
String s2=s1.intern();
System.out.println( s1==s1.intern() );
System.out.println( s1+" "+s2 );
System.out.println( s2==s1.intern() );
结果:
false
kvill kvill
true
在这个类中我们没有声名一个”kvill”常量,所以常量池中一开始是没有”kvill”的,当我们调用s1.intern()后就在常量池中新添加了一个”kvill”常量,原来的不在常量池中的”kvill”仍然存在,也就不是“将自己的地址注册到常量池中”了。
s1==s1.intern()为false说明原来的”kvill”仍然存在;s2现在为常量池中”kvill”的地址,所以有s2==s1.intern()为true。
以上是关于C++string类详解的主要内容,如果未能解决你的问题,请参考以下文章