C++初探 4-1(复合类型 - 数组 字符串 string类)

目录

注:

数组

案例分析

数组的初始化规则

C++11数组初始化方法

字符串

拼接字符串常量

在数组中使用字符串

字符串输入 

每次读取一行字符串输入

1. 面向行的输入:getline( )

2. 面向行的输入:get( ) 

空行和其他问题

混合输入字符串和数字

string类简介

C++11字符串初始化

赋值、拼接和附加

string类的其他操作

string类I/O

其他形式的字符串字面值


注:

        本笔记参考:《C++ PRIMER PLUS》


        复合类型,即基于整型和浮点类型创建的类型。其中,最有名的就是了。除此之外,C++还继承了几种来自C语言的复合类型:

  • 数组:存储字符串;
  • 结构:存储多个不同类型的值;
  • 指针:将数据所处位置告诉计算机。

数组

        数组,是一种数据格式,能够存储多个同类型的值。每个值单独存储在一个独立的数组元素内(可以将每一个元素看成一个简单变量),这些数组元素会在内存中依次排列。

        通过声明语句创建数组要指出:

  • 存储在每个元素中的值的类型;
  • 数组名;
  • 数组中的元素数。

例子:创建一个数组,这个数组有6个short类型的元素。

short month[6];

  注意:months的类型不是数组,而是 short数组 。数组是不存在通用数组类型的

        可以总结出数组声明的通用格式:

        其中,arraySize不能是变量(变量的值需要在程序运行时设置)

        数组的一个重要特性就是:可以通过下标或者索引单独访问数组元素,并且下标是从 0 开始访问的。

  注意:编译器 不会 检测使用的下标是否有效。但如果使用一个超出范围的下标,如:months[100] ,程序运行时,就会引发问题。

案例分析

例子:马铃薯的统计与分析

#include<iostream>
int main()
{
	using namespace std;
	int yams[3];
	yams[0] = 7;
	yams[1] = 8;
	yams[2] = 6;

	int yamcosts[3] = { 20, 30, 5 };

	cout << "马铃薯的数量总共是 = ";
	cout << yams[0] + yams[1] + yams[2] << endl;

	cout << "有 " << yams[1] << " 个马铃薯的那一袋,"
		<< "马铃薯是 " << yamcosts[1] << " 元一个。\n";

	int total = yams[0] * yamcosts[0] + yams[1] * yamcosts[1];
	total = total + yams[2] * yamcosts[2];

	cout << "\n数组yams的大小是 " << sizeof yams << " bytes。\n";
	cout << "数组yams中的一个元素的大小是 " << sizeof yams[0] << " bytes。\n";

	return 0;
}

程序执行的结果是:

【程序分析】

        首先,在上述代码中提及了一些关于数组的使用:

  1. yams 是一个包含了3个元素的数组。所以我们需要使用索引 0~2 来yams中的三个元素赋值;
  2. yams 内的每一个元素都是int类型,都具有int类型的访问权限;
  3. C++允许在声明语句中初始化数组元素,如上述代码中的 yamcosts
    int yamcosts[3] = { 20, 30, 5 };    //使用了用逗号分隔的值列表,即初始化列表

      若此处数组没有进行过初始化,数组内元素的值将会是之前存放在该内存单元内的值。

        其次,上述代码还使用了 sizeof运算符 (作用:以字节为单位返回类型或者数据对象的长度),此处需要注意:当出现类似于这种形式 sizeof 数组名 时,返回的将是整个数组的字节数

        所以我们可以通过sizeof计算得到数组的大小(使用sizeof计算 类型名 时需使用 小括号() ):

int num = sizeof yamcosts / sizeof (int);    

数组的初始化规则

  • 只有在定义数组时才能使用初始化,之后不能使用;
  • 不能将一个数组赋给另一个数组。
int card[4] = {3, 6, 8, 10};    //可行的初始化
int hand[4];                    //可行的声明

hand[4] = {5, 6, 7, 9};         //初始化不可行
hand = card;                   //不被允许的赋值方式

        但是,使用下标为元素单独赋值是被允许的。

------

  • 如果只对数组中的一部分元素进行初始化,编译器将把剩下的元素设置为0。
float tips[5] = {2.4, 5.1};    //只对一部分元素进行赋值

        通过上述规则,我们可以得出将数组元素全部设置为0的简便方法:

long totals[100] = { 0 };

------

  • 如果初始化数组时方括号([ ])内为空,编译器将计算元素个数。如下文的数组things包含4个元素。
short things[] = {1, 5, 3, 8};

C++11数组初始化方法

        C++11为列表初始化新增了一些功能。

  1. 初始化数组时,可以省略等号(=):

double earnings[4]{ 1.2e4, 1.6e4, 1.1e4, 1.7e4 };

------

  2. 初始化数组时,大括号内可以不包含任何东西,这样做也会把所有元素设置为 0 :

unsigned int counts[10] = {};
float balances[100] {};

------

  3. 列表初始化禁止将超出某类型长度的值赋给该类型(会引起编译器报错):

#include<iostream>

int main()
{
    long plifs[] = {25, 92, 3.0};                //无法通过编译
    char slifs[4] {'h', 'i', 1122011, '\0' };    //无法通过编译
    char tlifs[4] {'h', 'i', 112, '\0'};         //可通过,112仍在char类型取值范围内

    return 0;
}

编译器报错:

   ps:此处笔者使用Ubantu系统下的GNU(即g++)编译。

字符串

        字符串,是存储在内存的连续字节中的一系列字符。C++处理字符串的方式也是两种,一种来自C语言(C-style string),另一种基于string库。

        字符串可以被存储在char类型的数组当中,其中的每一个字符都被存储在各自的数组元素内。C风格的字符串有一个特点:以空字符(即 \0 ,ASCII码是 0)结尾:

char dogs[8] = {'b', 'e','a','u','x',' ', 'I','I'};		//不是字符串
char cats[8] = { 'f','a','t','e','s','s','a','\0' };	//是字符串

        如果使用 cout 输出上述两个数组:

如上图所见:

  • 数组cats将在输出7个字符后停止(系统检测到了 空字符\0 );
  • 数组dogs在输出8个字符后将会输出乱码(系统将会打印数组dogs内存中随后的字节,直到遇见 \0 )。

        除上述初始化方法外,还有一种更加简便的初始化方法 —— 字符串常量(或称字符串字面值),例如:

char bird[11] = "Mr.Cheeps";	//字符串末尾默认存在一个 \0
char fish[] = "Bubbles";

        使用各种C++输入工具,通过键盘键入,将字符串读入char数组时,将自动在数组结尾补上空字符。

  在确定存储字符串所需的数组的大小时,需要将 空字符 考虑在内。

||| 注意:

  1. 单引号 —— 字符常量;
  2. 双引号 —— 字符串常量。

        基于引号的使用,会出现如下的区别:

  使用单引号

char shirt_size = 'S';

        上述的 'S' 是一个字符常量,是字符串编码的简写形式。从ASCII码的角度来看,这种写法实际上是将 83('S' 对应的ASCII值)赋给了shirt_size。

使用双引号

char shirt_size = "S";

        上述的 "S" 表示:

  1. 两个字符(字符S 和 \0)组成的字符串;
  2. 字符串所在的内存地址

        也就是说,上述代码实际上试图将一个内存地址赋给一个char类型的变量,这会触发报错:

拼接字符串常量

        有时字符串过长,会导致其在一行内无法被完整放入。为了处理这种情况,C++允许拼接字符串字面值,即可以将两个由引号引起来的字符串合二为一。例如:

cout << "我要给你一块钱。\n";
cout << "我要给你" "一块钱。\n";
cout << "我要给你"
	"一块钱。\n";

注:这三个输出是等效的。

        在上述代码拼接字符串的过程中,第一个字符串中的 \0 将被第二个字符串中的第一个字符取代。

  由上述代码可以推出:任何两个由空白(即空格、制表符和换行符)分隔的字符串常量将被自动拼接。

在数组中使用字符串

例子:

#include<iostream>
int main()
{
	using namespace std;
	const int Size = 15;
	char name_1[Size];					//空数组
	char name_2[Size] = "C++writer";	//初始化数组

	cout << "你好!我是 " << name_2;
	cout << "!你叫什么?\n";
	cin >> name_1;						//通过输入设备输入字符串

	cout << "不错的名字。" << name_1 << ",你的名字有 ";
	cout << strlen(name_1) << " 个字,并且被存储\n";		//求字符串长度
	cout << "在一个有 " << sizeof(name_1) << "字节的数组内。\n";
	cout << "你的名字开头是 " << name_1[0] << "。\n";

	name_2[3] = '\0';					//在字符串中放置空字符
	cout << "这是我名字的前3个字符:";
	cout << name_2 << endl;

	return 0;
}

程序执行的结果是:

【分析】

函数strlen:

  该函数返回的是存储在数组中的字符串的长度(并不是数组本身的长度)。因为函数strlen在遇到 \0 时停止计算长度,所以函数strlen不会将 \0 计入字符串的长度。

        上述程序在指定数组的长度时使用了字符常量Size。这种写法在修改数组大小时较为方便。

字符串输入 

例子:

#include<iostream>
int main()
{
	using namespace std;
	const int ArSize = 20;
	char name[ArSize];
	char dessert[ArSize];

	cout << "请输入你的名字:\n";
	cin >> name;
	cout << "请输入你喜欢的甜点:\n";
	cin >> dessert;

	cout << "我有一些 " << dessert << " 要给你," << name << ".\n";

	return 0;
}

该程序执行的结果:

【分析】

        注意:此处程序并未在【输入甜点】处暂停,提示用户进行输入操作。这就涉及到 cin 确定字符串输入完毕的方式。

||| cin 获取字符串的方式:

  1. 当 cin 遇到空白(空格、制表符和换行符)时,cin 认为字符串输入完毕;
  2. 读取字符串完毕后,cin 将把该字符串放入数组内,并自动在结尾添加 \0

  除了遇到空白的情况,当输入字符串长于当前数组时,使用 cin 无法防止字符串溢出的现象出现。

每次读取一行字符串输入

        在上个例子中,我们可以发现:使用 cin 实现输入带有空白的短语较为麻烦。譬如,现在需要输入 New York 或 Sao Paulo ,如果只使用一个 cin ,那么如何获取整个名字就是个问题。

        为了解决这种情况,istream中的类提供了面向行的类成员函数,它们会读取输入,直到出现换行符(\n):

  1. getline( ):读取字符串后将丢弃换行符
  2. get( ):读取字符串后,将换行符保留在输入序列中

1. 面向行的输入:getline( )

        getline( )判断字符串输入完毕的条件:读取字符数目到达目标,或者检测到回车键输入的换行符。

        调用方式:

使用例(更改上一个例子):

#include<iostream>
int main()
{
	using namespace std;
	const int ArSize = 20;
	char name[ArSize];
	char dessert[ArSize];

	cout << "请输入你的名字:\n";
	cin.getline(name, 20);				//使用getline( )
	cout << "请输入你喜欢的甜点:\n";
	cin.getline(dessert, 20);			//使用getline( )

	cout << "我有一些 " << dessert << " 要给你," << name << ".\n";

	return 0;
}

程序执行的结果是:


2. 面向行的输入:get( ) 

        函数get( )的用法类似于函数getline( )。但在上文中提到过,函数get( )在执行之后,输入中的换行符会保留在输入序列。这意味着如果我们连续两次调用函数get( ):

#include<iostream>
int main()
{
	using namespace std;
	char str_1[15] = { 0 };
	char str_2[15] = { 0 };

	cin.get(str_1, 15);		//执行*1
	cin.get(str_2, 15);		//执行*2

	cout << str_1 << endl;
	cout << str_2 << endl;

	return 0;
}

就会出现类似于:

这样的情况。

        在这种情况中,程序并未要求我们进行第二次输入,这是因为:当第一次调用函数后,换行符被留在输入队列中,当第二次调用时,函数检测到的第一个字符就是换行符,此时程序认为已经检测到行尾。

        为了解决上述问题,可以使用函数get( )的一个变体:cin.get(); 。在不带有任何参数的情况下,函数get( )将直接读取输入序列中的下一个字符(实际上这就是一种函数重载)。因此我们可以这样做:

#include<iostream>
int main()
{
	using namespace std;
	char str_1[15] = { 0 };
	char str_2[15] = { 0 };

	cin.get(str_1, 15);		//执行*1
	cin.get();
	cin.get(str_2, 15);		//执行*2

	cout << str_1 << endl;
	cout << str_2 << endl;

	return 0;
}

上述的执行*1和执行*2还有其他几种形式:

  • cin.get(str_1, 10).get();	//拼接两个类成员函数
    cin.get(str_2, 10).get();
  •  
    cin.getline(str_1, 10).getline(str_2, 10);    //拼接getline()

程序执行的结果是:

  如果要求上述两种输入函数输入的字符串比目标数组更长,例如:

并且在输入中,输入长度大于9的字符串:

那么在尝试输出该字符串时,就会出现警告:

  观察str_1和str_2内部的存储情况:

发现:由于数组长度不够,get( )函数无法在行尾添加 \0 ,导致最终的输出报错。

  所以,在使用输入函数时,需要注意第二个参数的大小是否于目标数组大小匹配。

        上述介绍的两个类成员函数各有优缺点,譬如getline( )相比于get( )更加方便,但是get( )可以使错误的检测更加简单,具体使用应该考虑清楚。(不过在老式的实现中是没有getline( )的,要注意)


空行和其他问题

  • 问题①:目前,当 get( ) 读取空行后将会设置失效位(failbit)

        上述的失效位意味着在该函数之后的输入将被阻断,这种阻断可以被下方命令取消:

cin.clear();
  • 问题②:输入字符串可能比分配的空间长。

        如果输入行包括的字符数比指定的多:

  1. getline( )和get( )将把余下的字符留在输入序列中;
  2. getline( )还会设置失效位。

混合输入字符串和数字

例子:

#include<iostream>
int main()
{
	using namespace std;

	cout << "请问您的房子已经有几年了?\n";
	int year = 0;
	cin >> year;                    //输入数字
	cout << "这套房子的地址是什么?\n";
	char address[80];
	cin.getline(address, 80);       //输入字符串

	cout << "已建成多久:" << year << endl;
	cout << "地址:" << address << endl;
	cout << "完毕。\n";

	return 0;
}

程序执行的结果是:

【分析】

        上述通过 cin >> year; 进行输入数字时,并未处理cin滞留在输入序列中的换行符,导致之后的函数 getline( ) 读取到该换行符,以至于输入失败。

        处理上述问题可以用之前提到的方法:

cin >> year;
cin.get();

        或者拼接类成员函数:

(cin >> year).get();

        处理之后程序正常执行的结果:

string类简介

        C++98为C++库添加了string类,现在可以通过string类型的变量(即对象)实现字符串的存储。要使用string类,有几个前提:

  1. 程序必须包含头文件string;
  2. string类位于名称空间std中,需要先调用名称空间std。

例子(string类的定义隐藏了字符串的数组类型):

#include<iostream>
#include<string>
int main()
{
	using namespace std;
	char arr_1[20];
	char arr_2[20] = "jaguar";		//初始化数组,jaguar:美洲豹
	string str1;
	string str2 = "panther";		//初始化string类,panther:黑豹

	cout << "请输入一种猫科动物:";
	cin >> arr_1;
	cout << "请输入另一种猫科动物:";
	cin >> str1;

	cout << "\n现在这里有一些猫科动物:\n";
	cout << arr_1 << " " << arr_2 << " "
		<< str1 << " " << str2 << endl << endl;

	cout << arr_2 << " 名字的第三个字母是 " << arr_2[2] << endl;
	cout << str2 << " 名字的第三个字母是 " << str2[2] << endl;

	return 0;
}

程序执行的结果是:

        从上述例子可以看出,string对象的使用方法与字符数组的相似之处:

  • 可以使用C-风格字符串来初始化string对象;
  • 可以使用cin来将键盘输入存储到string对象中;
  • 可以使用cout来显示string对象;
  • 可以使用数组表示法来访问存储在string对象中的字符。

        但是string对象和字符数组之间也存在区别:

  • string对象可以被声明为简单对象,而不是数组:
    string str1;                //声明为简单变量
    string str2 = "panther";    //初始化变量
  • 程序可以自动处理string的大小:
    string str1;    //此处的声明创建了一个 长度为0 的string对象
    cin >> str1;    //当输入被读取到str1内时,程序会自动调整str1的大小

        故:相比于数组,string对象更方便、安全。

总结:

        从理论上看:

  • char数组 —— 一组用于存储一个字符串的 char存储单元 ;
  • string类变量 —— 一个表示字符串的 实体 。

C++11字符串初始化

        C++11允许对C-风格字符串和string对象使用 列表初始化 :

char firat_ch[] = { "哈哈哈 111 222" };
char seconf_ch[] { "Elegant 大象" };
string thirdStr = { "the golden age" };
string fourStr = { "Alice HATTER" };

赋值、拼接和附加

        string类简化了某些操作,比如:

  1. 可以将一个string对象赋给另一个string对象(数组不行)

string str_1;
string str_2 = { "flying to the moon" };
str_1 = str_2;

  2. string类简化了字符串合并操作

① 可以使用 +运算符 —— 合并两个string对象;

② 可以使用 +=运算符 —— 将字符串附加到运算符左边的string对象的末尾。

string str_3;
str_3 = str_1 + str_2;
str_1 += str_2;			//将str_2加到str_1的末尾

------

例子:

#include<iostream>
#include<string>
int main()
{
	using namespace std;
	string s_1 = "penguin";
	string s_2, s_3;

	cout << "执行操作:s_2 = s_1\n";
	s_2 = s_1;
	cout << "s_1:" << s_1 << "  s_2:" << s_2 << endl << endl;

	cout << "执行操作:s_2 = \"buzzard\"\n";
	s_2 = "buzzard";
	cout << "s_2:" << s_2 << endl << endl;

	cout << "执行操作:s_3 = s_1 + s_2\n";
	s_3 = s_1 + s_2;
	cout << "s_3:" << s_3 << endl << endl;

	cout << "执行操作:s_1 += s_2\n";
	s_1 += s_2;
	cout << "s_1:" << s_1 << endl << endl;

	cout << "执行操作:s_2 += \" for a day\"\n";
	s_2 += " for a day";
	cout << "s_2:" << s_2 << endl;

	return 0;
}

执行程序的结果是:

string类的其他操作

        C语言库中的函数也可以完成为字符串赋值等上述提到过的工作。例如:

  • 函数strcpy( ) —— 将字符串复制到字符数组中:
  • 函数strcat( ) —— 将字符串附加到字符数组末尾:
  • ……

使用例:

#include<iostream>
#include<cstring>
int main()
{
	using namespace std;

	//strcpy的使用例------
	char arr_1[20] = "你好";
	char arr_2[20] = "Hello World";
	strcpy(arr_1, arr_2);
	cout << "arr_1:" << arr_1 
		<< "\t长度 = " << strlen(arr_1) << endl;

	//strcat的使用例------
	char arr_3[20] = "再见!";
	char arr_4[20] = "GoodBye!";
	strcat(arr_3, arr_4);
	cout << "arr_3:" << arr_3 
		<< "\t长度 = " << strlen(arr_3) << endl;

	return 0;
}

程序执行的结果:

  ps:因为一个中文字符的长度是两个字节,所以arr_3的长度是14。

        由上述例子可以看出,处理string的语法要比处理C字符串来得简单,这一点在进行复杂操作时尤为明显,譬如对于以下操作:

str_3 = str_1 + str_2;

        使用C语言库中的函数需要两条语句才能做到:

strcpy(arr_3, arr_1);
strcpy(arr_3, arr_2);

------

        另一方面,字符数组可能会因为自身空间过小而无法存储指定信息,从而导致程序终止或者数据损坏(C语言库提供的strncat( )和strncpy( )可以使代码更安全,但也导致代码更复杂)

        但是string类因为本身具有调整大小的功能,所以避免了上述问题。

------

        确定字符串中字符数的方法:

int len_1 = str_1.size();
int len_2 = strlen(arr_1);

        上述的两种计算方法中:

  • 函数strlen( ):是一个常规函数。该函数参数是C-风格字符串,返回该字符串包含的字符数;
  • size( ):是一个类方法(方法是一个函数,但是只能通过其所属类的对象进行调用)。它前面的 str_1 并不是函数参数,而是一个对象。

string类I/O

        使用cin和运算符将输入存储到string对象内时,如果读取的是一行而不是一个单词时,就会需要特殊的语法。

例子

#include<iostream>
#include<string>
#include<cstring>
int main()
{
	using namespace std;
	char chArr[20];	//未进行初始化的数组其内容是未定义的
	string str;		//为初始化的string对象长度默认是 0

	cout << "输入前,chArr 中字符串的长度 = " 
		<< strlen(chArr) << endl;	//此处的输出可能每次都不相同
	cout << "输入前,str 中字符串的长度 = " 
		<< str.size() << endl;	

	cout << "\n请输入一行文本:\n";
	cin.getline(chArr, 20);
	cout << "你输入了:" << chArr << endl;
	cout << "请输入另一行文本:\n";
	getline(cin, str);				//重载,此处没有长度设定
	cout << "你输入了:" << str << endl;

	cout << "\n输入后,chArr 中字符串的长度 = " 
		<< strlen(chArr) << endl;
	cout << "输入后,str 中字符串的长度 = " 
		<< strlen(chArr) << endl;

	return 0;
}

程序执行的结果是:

【分析】

        在上述的代码中,我们会发现一行将输入读取到数组中的代码:

        发现:在 cin 之后存在 [ . ]。这表明,函数getline( )是一个(istream类)的类方法(cin是istream对象)

        除此之外,还有一行将输入读取到string对象中的代码:

注:此处没有使用句点表示法,所以这个getline( )不是类方法。

        此处函数getline( )之所以没有出现关于数组长度的参数,是因为string对象可以根据字符串的长度调整大小。

  上述的这个函数getline( )之所以不是istream的类方法,是因为在引入string类之前,C++就已经有了istream类。

  但这又会引发另一个问题:cin >> str; 为何能够执行。这就要涉及友元函数的知识了。(笔者能力有限,之后再继续讨论该问题)

其他形式的字符串字面值

        ① 先复习如何使用 wchar_t(后缀L)、char16_t(后缀u) 和 char32_t(后缀U) 这三种类型:

wchar_t title[] = L"super idol";
char16_t name[] = u"Aili Young";
char32_t car[] = U"Humber Super Snipe";

        ② 前缀u8 —— 保证字符串被存储时使用UTF-8编码方案,可在一定程度上保证字符串显示正常(C++11)。

        ③ 前缀R —— 原始(raw)字符串,该字符串将 [ "( ] 和 [ )" ] 用作界定符,使字符只显示自身而不带有功能性。例如:序列\n不表示换行符,而是表示常规字符 [ \ ] 和 [ n ]。

使用例

cout << R"(\n 被用作换行符,类似于 endl 。)" << '\n';

打印结果:

        但如果要在原始字符串中包含 [ "( ] 或 [ )" ] ,为了进行区分,可以使用 R"+*(内容)+*" 的形式标识原始字符串的结尾,例如:

cout << R"+*("(他是谁?)",她这么问。)+*" << endl;

打印结果:

        前缀R也可以与其他字符串前缀结合使用,位置没有具体要求。例如:Ru、UR等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值