C了个++:03 - C++的字符串与 string 类

简单梳理总结了C++中字符和字符串的知识


简单基础知识部分

  • C++数据类型有3种级别:基本级类型(整型、浮点型)、复合级类型(数组、字符串、指针、地址)、抽象级类型(结构、类)。
  • 这里跟字符和字符串相关的是:char ch(基本类型)、char ar[]或char* ar(复合类型)、string str(抽象类型),另外这里不讨论wchar_t类型,字符串提供了一种存储文本信息和输出文本信息的便捷方式,因为它是存储在的连续字节的一系列字符。


基本类型char ch

  • char类型是一种特殊的整型,专为存储字符而设计的,它是8位二进制数(一个字节),存储的是128(2^7)个基本字符(包括字母和数字、标点符号等)编码值(以ASCII码为例)区别于其他整型或浮点型存储数字的方式(它们二进制的存储方式和数字的大小相关联,比如“1”对应二进制“0001”),char类型的二进制存储数字的编码值,并且和数字大小本身没有关系(比如“1”对应char类型的二进制数“0x31”)。
  • 因为char是整型(比short类型更小的整型),所以char变量可以比较大小也可以进行加减操作。字符的字面值(字符常量)就是相应字符的编码值(该值是整数常量)单引号运算符(字符值符)加单个字符得到该字符的编码值(字符常量)。一个char类型变量的值也是一个相应字符的编码值双引号运算符(字符串分割符)加字符串得到该字符串的编码值序列的首地址(并在该字符串-编码值序列的末尾自动添加NULL(空字符)的编码值,这个带地址和NULL字符的编码值序列也叫字符串常量或字符串字面值)
  • (例如在ASCII码中,字符A,该字符的  字面值==字符常量==编码值=='A'==65==0x41==0101==01000001;而对于字符变量char ch,如果ch==65/69,那么ch变量存储的是字符A/E的编码值)
  • C++将字符进行编码并用整数表示,使得操作字符值更容易,至于实现使用哪种编码和编译器(C++编译器)没关系,和系统有关系,一般windows系统使用的是ASCII码。另外,国际Unicode字符集有更多的字符(多于128个)。
  • 重要或特殊的字符的编码值及其转义序列:振铃\a\7、换行endl\n\10、回车\r\13、双引号\"\34、单引号\'\39、反斜杠\\\92、问号?\?\63、tab\t\9、退格\b\8、NULL(ctrl+@)\0、空格\32、0\48、@\64、A\65、a\97、del\127,转义序列与对应字符等价,对转义序列使用单引号运算符得到相应字符常量,应尽量使用符号转义序列不使用数字转义序列,因为数字转移序列依赖于系统的编码方式(如ASCII码)。
  • 成员函数ostream & put(char);或<<运算符打印一个字符。成员函数istream & get(char);或int get(void);或>>运算符读取一个字符。


复合类型char ar[]

  • C-风格字符串char ar[],字符串是存储在内存的连续字节中的一系列字符,NULL对于C-字符串至关重要,因为它用来标记字符串的结尾,char数组是否是char字符串的唯一要求是末尾是否有NULL字符。字符串常量或字符串字面值(即双引号格式)可以自动在字符串结尾处隐式地添加NULL字符。注意,要求字符串数组的长度>=有效字符串长度+1。另外,通过键盘输入到char数组的字符串也会自动在末尾添加NULL字符。
  • 单引号运算符(字符常量)和双引号运算符(字符串常量)的主要区别是,单引号只是对应一个字符的编码值并可以将其传递给char变量双引号对应末尾添加了一个NULL字符的一串字符的编码值序列,并且双引号传递给char*或char[]变量的是一个指向char类型字符串的首地址(注意,char[]地址的内存长度是确定的,就是数组占的字节长度,但char*的内存长度就4字节的地址长度不变,见sizeof()),并非传递字符串的编码值序列。双引号运算符(字符串常量)属于C-风格字符串。记住,数组名是数组的首地址,同样,字符串常量也是首地址。
  • 概念区分:字符常量是属于char类型的常量,它可以赋值给char类型变量ch,ch存储的内容是其字符的编码值;字符串常量是属于char*或char[]类型的常量,它是将字符串的首地址赋值给char*或char[]类型变量ar,则ar存储的内容是字符串首地址,而此地址存储的内容是一串字符的编码值序列的第一个编码值。不过ar是数组名,变量名ar通过索引运算符[]可以访问字符串数组中的特定位置的字符,即ar[i]相当于是个char类型的变量,它对应字符串中第i+1个字符的字符常量,也就是说可以通过字符常量给ar[i]赋值来修改ar字符串中第i+1个的内容。另外,也可以用 *(ar++/ar+i) 的方法访问ar字符串中的内容。
  • C-风格char字符串有三种常用操作,将char数组初始化为字符串常量、通过键盘或文件读入赋值到char数组中、通过索引运算符修改字符串中特定字符。注意,字符串常量或初始化列表({ })对char数组的赋值操作只限于初始化,因为每次双引号传递的是字符串首地址,数组名(数组首地址)在初始化过程完成赋值(或在数组变量声明时默认赋值),并在后期是不可修改的。再注意,有些实现对数组(包括char数组)的初始化操作(字符串常量或{ })需要使用static关键字,static使该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值。char数组声明或初始化,地址是分配的(已赋值)长度是确定的,不存在char ar[];的声明(函数参数除外),但有char ar[]="...";的初始化。char数组在使用字符串常量初始化时,会自动用NULL字符将char数组中字符串常量的字符长度+1(strlen("...")+1)之外多余的空间填满,然而,未初始化的数组的内容是未定义的(即是以前的内容)。
  • strlen(char*ar);(头文件cstring)会寻找NULL字符进行统计ar字符串的长度,只计算可见的字符而不把空字符计算在内,单位为个字符;sizeof(ar);指出整个数组ar(其实是指针)的内存空间的长度,单位为个字节。注意sizeof();的使用针对char*指针变量和char[]数组变量是有区别的,指针的sizeof()始终是4个字节的长度(取决于系统),而数组名的sizeof()是整个数组的内存空间的长度,尽管数组变量名从功能上来讲就是一个指针,但严格来说并不是真正的指针类型。
  • 创建一个字符串的副本可以使用ps=new char(strlen(ar)+1);strcpy(ps,ar);。char数组的内容是可以通过键盘或文件输入或strcpy()或strncpy()重新写入或修改的。注意,char*指针引导的字符串不能重新写入。
  • 字符串的输入,除了cin>>方法,常用的两个方法是getline和get(两个istream类的成员函数)。cin.getline(char*ar,int+1,char);只能在分隔符之前输入int个字符,多出的1是为了添加NULL字符来保证是char数组字符串,所以要求int+1<=siziof(ar),这里的ar是数组名,前面getline参数中的ar是一个指针(地址),意思是说getline接收一个指向char类型的地址,只不过这里的地址是由数组名ar提供的。另外,getline读取并丢弃分隔符。cin.get(char*ar,int+1,char).get(ch);这个方法和getline的功能是一致的,不过相比于getline它的好处是,可以用get(ch)来查看是因为分隔符停止输入的还是因为数组已满停止输入的(因为get(char*ar,int+1,char)不读取分隔符),并且不会像getline因为数组已满设置failbit关闭输入。但对于空行,get会设置failbit,getline不会。清除failbit恢复输入使用cin.clear();。
  • 符串的输出,常用cout<<方法。只要给cout提供一个char的地址(不管它是字符串常量还是数组名还是指针),它就从该字符开始打印,直到遇到NULL字符为止。
  • 其它,任何两个由空白(空格、制表符和换行符)分割的字符串常量(双引号)都将自动拼接成一个,并且会自动去掉拼接处的NULL字符。C-风格字符串的的复制和附加,char*strcpy(ar1,ar2);  char*strncpy(ar1,ar2,n);char*strcat(ar1,ar2);  char*strncat(ar1,ar2,n);(头文件cstring),(=赋值运算符只用于初始化,不能用于后期的赋值)。调用拼接(cin>>int).get(); 要小心,输入一个int数据是后边还有个换行符的空白留在了输入队列里了。



抽象类型string str

  • C++的string类库(头文件string,注意不是头文件cstring),位于名称空间std中,实现了将字符串作为一种数据类型(类,抽象数据类型),处理string对象(变量)使处理字符串像处理基本类型变量(如char ch)一样简单方便,例如赋值或重新赋值,并且此时string对象内存储的字符串的长度可以自动调整(最大长度限制string::npos,通常是unsigned int的最大值)。未被初始化的string对象的字符串的长度自动设置为0。从理论上讲,可以将char数组视为一组用于存储一个字符串的char存储单元,而string类对象(变量)是一个表示字符串的实体。双引号运算符(字符串常量)属于C-风格字符串,但也可以看作一个字符串的实体(一个未命名的string对象)。
  • 字符串常量(双引号形式)在char数组中仅限于初始化使用,而它对于string对象没有限制,所以可以利用字符串常量重新赋值=给已存在的string对象(类似给基本变量重新赋值=);两个char数组不能相互赋值,而两个string对象可以(类似两个基本变量的赋值传递=),对应于C-风格字符串中的常规函数char*strcpy(ar1,ar2);  char*strncpy(ar1,ar2,n);;两个string对象可以通过加法运算符+/+=实现合并操作(类似基本变量的运算赋值),对应于C-风格字符串中的常规函数char*strcat(ar1,ar2);  char*strncat(ar1,ar2,n);。还有,成员函数int size(); int length();对应于C-风格字符串中的常规函数int strlen(char*);。=/+/+=运算符也可以用于单个char值(ch变量或字符常量)赋值给string对象。另外,string对象也可以使用索引运算符[]访问特定位置的字符。
  • string对象的输入,除了cin>>方法,常用的方法是getline(cin/fin/instr,str,ch);(不是成员函数,它是string类的一个友元函数),并且这里并没有限制长度的参数,因为string对象可以自动调整自己的长度,这与C-风格字符串char数组getline的用法不同。另外,getline读取并丢弃分隔符。string对象的输出,常用cout<<方法。
  • string实际上是模板具体化basic_string<char>的一个typedef。size_type是一个依赖于实现的整型,在头文件string中定义的。string类7个构造函数:string(const char*);(字符串常量初始化或赋值时使用的方式)、string(size_type,char);、string(const string&);(赋值构造函数)、string();、string(const char*,size_type);、template<class Eter> string(Iter begin,Iter end);、string(const string&,string size_type =0,size_type =npos);
  • iostream类比string类出现早,在iostream类中没有处理string对象的类方法,但是方法cin>>和cout<<能用于string对象,并不是因为在iostream类中有函数定义,而是因为string类中的友元运算符重载函数(operator>>/<<(cin/cout,str);)。另外,string类还对全部6个关系运算符进行了重载,每个运算符都以三种方式被重载,以便能够将string对象与另一个string对象、C-风格字符串(数组名或字符串常量)进行比较,并能够将C-风格字符串(数组名或字符串常量)与string对象进行比较。通过运算符重载,有字符串常量参与时,可以将它视为未命名的string对象。
  • 其它,在结构中使用string对象做数据成员是可以的,但只添加string头文件还不行,一定要让结构定义能够访问名称空间std,即需使用using声明或using编译指令,或使用作用域解析运算符形式std::string str。如果需要多个字符串,可以声明一个string对象数组(字符串数组string str[];,str是一个指向string对象类型地址的指针,str[i]代表第i+1个string对象),而不是使用二维char数组。在字符串中搜索给定的子字符串或字符,可使用一系列成员函数find()。



字符串与函数

  • 字符串的比较:C-风格字符串比较int strcmp(char*,char*);(区分大小写),参数可以是char指针、字符串常量、数组名,因为这三个都是char地址类型(char指针,指向char类型地址的指针)。虽然不能用关系运算符比较字符串,但是可以让关系运算符比较字符,因为字符char是整型(可比较大小)。string类字符串比较可以使用!=等各种运算符,因为运算符重载,而且string对象和字符串常量(可视为未命名的string对象)作为一个表示字符串的实体。
  • C-风格字符串作为字符串的参数(char*ar)(const char*ar)(const char const*ar)(或使用char ar[]替换char*ar),传递的都是地址(数组名、字符串常量、char指针),可以使用const来禁止对char地址内的内容(字符串)的修改,第一个const固定内容第二个const固定指针。因为字符串内有NULL字符的停止标志,所以不必将字符串的长度作为参数传递给函数。while(*ar){... ar++;}或while(ar[i]){... i++;} 是一种处理字符串中字符的标准方式。
  • C-风格字符串做返回值char* function();,函数无法返回一个字符串数组,但可以返回一个字符串数组的首地址,这样比逐个返回字符会效率更高。函数使用new动态创建一个指向长度n的字符串的首地址的指针,然后返回该指针(首地址)。char*ar=new char[n+1];ar[n]='\0'; ... return ar; 是一种返回字符串首地址的标准方式。记得,new分配长度要为NULL字符多加一个长度(+1),并且在函数调用结束后使用delete[]释放new[]分配的内存(释放字符串使用的内存,并不释放当前的指针变量),而当函数调用结束,会释放指针变量ar的内存,不释放字符串使用的内存
  • string对象和函数,string function(string str)(const string str)(const string str[]); string对象做函数参数和返回值都是可以的,但只添加string头文件还不行,一定要让该函数声明和定义能访问名称空间std即需使用using声明或using编译指令,或使用作用域解析运算符形式std::string str。



字符串与指针

  • 指针名和数组名有点不同,指针在后期可以被修改、赋值、传递,不过字符串常量赋值仅限于初始化过程。尽管数组变量名从功能上来讲就是一个指针,但是数组名(数组首地址)在初始化过程完成赋值(或在数组变量声明时默认赋值),并在后期是不可修改的。字符串常量是个常量,所以它对char*的初始化会使用const关键字(const不是必须的,依赖编译器)。还有一点不同,就是使用sizeof()时,见上文。
  • 地址是C++中一种独立的类型,是指向某个基本类型(抽象类型)的一种复合类型,(数组名、字符串常量、char指针 等等)都是地址事实上,字符串常使用指针(而不是数组)来处理字符串。注意,未初始化的指针太随机有危险,尤其是输入操作,所以在建立指针时尽量尽快确定它指的地址。建议使用new动态分配内存的方法初始化指针,虽随机但没有危险。在讲字符串读入程序时,应使用已分配的内存地址,该地址可以是数组名,也可以是使用new初始化过的指针。
  • 如果要求cout<<输出char*指针变量的地址的话,需要类型转换(int*)ar或(void*)ar,其它类型指针不需要类型转换。






示例1:指针和字符串

#include<iostream>
#include<cstring>

int main()
{
	using namespace std;
	char animal[20] = "bear";// 字符串常量初始化,数组名(指针)声明后或初始化后不可以改
	const char * bird = "wren";// 字符串常量初始化,const不是必须的,另外,bird指针可以修改
	char * ps;// 指针未初始化,危险

	cout << animal << " and ";
	cout << bird << "\n";
	//cout << ps << "\n";// 错误

	cout << "Enter a kind of animal: ";
	cin >> animal;// char数组可以重新写入,char*指针引导的字符串不可以重新写入字符
	//cin >> ps;// 危险

	ps = animal;// 不能用字符串常量,字符串常量只限制于初始化操作。另外,这里只是复制地址,并没有复制字符串
	bird = animal;// 修改指针bird
	cout << ps << "!\n";
	cout << bird << "!\n";
	cout << "Before using strcpy():\n";
	cout << animal << " at " << (int *) animal << endl;// 其它指针不需要类型转换(int*)
	cout << ps << " at " << (int *) ps << endl;

	ps = new char[strlen(animal) + 1];// 动态分配新的内存,没有危险
	strcpy(ps, animal);// 结合strlen()和new的动态分配,创建一个字符串副本
	cout << "After using strcpy():\n";
	cout << animal << " at " << (int *) animal << endl;
	cout << ps << " at " << (int *) ps << endl;
	delete [] ps;

	return 0;
}




示例2:字符串在函数中的输入与返回

#include<iostream>
#include<string>

char * buildstr(char ch, int n);
std::string strback(const std::string str[], int n);// 一定要让该函数声明和定义能访问名称空间std,才能使用string对象

int main()
{
	using namespace std;
	int times;
	char ch;
	string str1[2];

	cout << "Enter a character: ";
	cin >> ch;
	cout << "Enter an integer: ";
	cin >> times;
	
	char * ps = buildstr(ch, times);
	cout << ps << endl;
	delete [] ps;
	ps = buildstr('+', 20);
	cout << ps << "-DONE-" << ps << endl;
	delete [] ps;// 释放字符串使用的内存,不释放变量使用的内存

	cout << "Enter a string1: ";
	cin >> str1[0];
	cout << "Enter a string2: ";
	cin >> str1[1];
	string str2 = strback(str1, 2);
	cout << str2 << endl;

	return 0;
}

char * buildstr(char ch, int n)
{
	char * p_ar = new char[n + 1];// +1是为了存储NULL字符
	p_ar[n] = '\0'; 
	while (n-- > 0)// 从后向前填充字符是为了避免使用额外的变量
		p_ar[n] = ch;
	return p_ar;// 函数结束后,释放变量p_ar的内存,不释放字符串使用的内存
}

std::string strback(const std::string str[], int n)
{
	std::string strb;
	while (n--)
		strb += str[n];
	strb += "  back-OK !";
	return strb;
}




示例3:非图形版本的Hangman拼字游戏

#include<iostream>
#include<string>
#include<cstdlib>// for exit();
#include<ctime>// for time();
#include<cctype>// for tolower();

using std::string;
const int NUM = 26;
const string wordlist[NUM] = {"apiary", "beetle", "cereal", 
	"danger", "ensign", "florid", "garage", "health", "insult", 
	"jackal", "keeper", "loaner", "mamage", "nonce", "oneset", 
	"plaid", "quilt", "remote", "stolid", "train", "useful", 
	"valid", "whence", "xenon", "yearn", "zippy"};// string对象数组

int main()
{
	using std::cin;
	using std::cout;
	using std::endl;
	using std::tolower;// 转换为小写字母的函数的名
	std::srand(std::time(0));// ???????????????
	
	char play;
	cout << "Will you play a word game? <y/n> ";
	cin >> play;
	play = tolower(play);// 转换成小写字母

	while (play == 'y')
	{
		string target = wordlist[std::rand() % NUM];// 随机产生,第3个构造函数
		int length = target.size();
		string attempt(length, '*');// 第2个构造函数
		string badchars;// 第4个构造函数
		int guesses = 6;
		cout << "Guess my secret word. Tt has " << length 
			 << " letters, and you guess one letter at a time. You get " << guesses 
			 << " wrong guesses." << endl;
		cout << "Your word: " << attempt << endl;

		while (guesses > 0 && attempt != target)
		{
			char letter;
			cout << "Guess a letter: ";
			cin >> letter;

			if (badchars.find(letter) != string::npos || 
				attempt.find(letter) != string::npos)// find()查询字符或子字符串
			{
				cout << "You already guessed that, Try again." << endl;
				continue;
			}

			int loc = target.find(letter);// 若返回npos,说明没找到
			if (loc == string::npos)
			{
				cout << "Oh, bad guess!" << endl;
				--guesses;
				badchars += letter;// string对象与char字符
			}
			else
			{
				cout << "Good guess!" << endl;
				attempt[loc] = letter;// string对象与char字符
				// check if letter appears again
				loc = target.find(letter, loc + 1);
				while (loc != string::npos)
				{
					attempt[loc] = letter;// string对象使用[]
					loc = target.find(letter, loc + 1);
				}
			}
			cout << "You word: " << attempt << endl;
			if (attempt != target)// 两个string对象的比较
			{
				if (badchars.length() > 0)// 与size()一样
					cout << "Bad choies: " << badchars << endl;
				cout << guesses << " bad choies left." << endl;
			}
		}
		if (guesses > 0)
			cout << "That's right!" << endl;
		else
			cout << "Sorry, the word is " << target << "." << endl;
		cout << "Will you play again? <y/n> ";
		cin >> play;
		play = tolower(play);
	}
	cout << "BYE!" << endl;
	
	return 0;
}






本文总结自《C++ primer plus》(第六版中文版)第三、四、五、七、十六章

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值