目录
注:
本笔记参考:《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;
}
程序执行的结果是:
【程序分析】
首先,在上述代码中提及了一些关于数组的使用:
- yams 是一个包含了3个元素的数组。所以我们需要使用索引 0~2 来yams中的三个元素赋值;
- yams 内的每一个元素都是int类型,都具有int类型的访问权限;
- 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数组时,将自动在数组结尾补上空字符。
在确定存储字符串所需的数组的大小时,需要将 空字符 考虑在内。
||| 注意:
- 单引号 —— 字符常量;
- 双引号 —— 字符串常量。
基于引号的使用,会出现如下的区别:
使用单引号:
char shirt_size = 'S';
上述的 'S' 是一个字符常量,是字符串编码的简写形式。从ASCII码的角度来看,这种写法实际上是将 83('S' 对应的ASCII值)赋给了shirt_size。
使用双引号:
char shirt_size = "S";
上述的 "S" 表示:
- 两个字符(字符S 和 \0)组成的字符串;
- 字符串所在的内存地址。
也就是说,上述代码实际上试图将一个内存地址赋给一个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 获取字符串的方式:
- 当 cin 遇到空白(空格、制表符和换行符)时,cin 认为字符串输入完毕;
- 读取字符串完毕后,cin 将把该字符串放入数组内,并自动在结尾添加 \0 。
除了遇到空白的情况,当输入字符串长于当前数组时,使用 cin 无法防止字符串溢出的现象出现。
每次读取一行字符串输入
在上个例子中,我们可以发现:使用 cin 实现输入带有空白的短语较为麻烦。譬如,现在需要输入 New York 或 Sao Paulo ,如果只使用一个 cin ,那么如何获取整个名字就是个问题。
为了解决这种情况,istream中的类提供了面向行的类成员函数,它们会读取输入,直到出现换行符(\n):
- getline( ):读取字符串后将丢弃换行符;
- 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();
- 问题②:输入字符串可能比分配的空间长。
如果输入行包括的字符数比指定的多:
- getline( )和get( )将把余下的字符留在输入序列中;
- 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类,有几个前提:
- 程序必须包含头文件string;
- 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等。