[C++学习笔记] 第 3 章 复合类型

第 3 章 复合类型

3.1 数组

  • C++11 数组的初始化方法

    之前提到过,C++11 将使用大括号的初始化(列表初始化)作为一种通用的初始化方式,可用于所有类型。数组以前就可使用列表初始化,但 C++11 中的列表初始化新增了一些功能。

    1. 初始化数组时,可以省略等号

      double earnings[4] {1.2e4, 1.6e4, 1.1e4, 1.7e4};
      
    2. 可以不在大括号内包含任何东西,这将把所有元素都设置为 0 0 0

      unsigned int counts[10] = {};
      float balances[100] {};
      
    3. 列表初始化禁止缩窄转换

      long plifs[] = {25,92,3.0};				//not allowed
      char slifs[] = {'h','i',1122011,'\0'};	//not allowed
      

      上述代码中,第一句不能通过编译,因为将浮点数转换为整型是缩窄操作。第二句也不能通过编译,因为 1122011 1122011 1122011 超出了 char 变量的取值范围。但是,这两句在 C 语言中都可以通过编译

3.2 字符串

​ C++ 处理字符串的方式有两种。第一种来自 C 语言,被称为 C 风格字符串;第二种基于 C++ string 类库。C 风格字符串具有一种特殊的性质:以空字符 \0 结尾。

3.2.1 字符串输入
#include <iostream>
int main()
{
    using namespace std;
    const int Arsize = 20;
    char name[Arsize];
    char dessert[Arsize];

    cout << "Enter your name:\n";
    cin >> name;
    cout << "Enter your favorite dessert:\n";
    cin >> dessert;
    cout << "I have some delicious " << dessert << " for you, " << name << ".\n";
    return 0;
}

​ 该程序的意图很简单:读取用户名和用户喜欢的甜点,并输出。下面是程序的运行情况:

Enter your name:
Baymax Guo
Enter your favorite dessert:
I have some delicious Guo for you, Baymax.

​ 用户还没有输入甜点,程序就已经读取并显示出来了。这和 cin 确定字符串结尾的方式有关。cin 使用空白(空格、制表和换行)来确定字符串的结束位置。这意味着 cin 一次只能读取一个单词,并不能读入整行,和 scanf("%s") 类似。

3.2.2 读取一行字符串

​ 要将整行而不是一个单词作为字符串输入,需要采用另一种字符串读取方法。具体地说,需要采用面向行而不是面向单词的方法。iostream 中的类提供了一些面向行的类成员函数:getline()get()。这两个函数都读取一行输入,直到到达换行符。随后,getline() 将丢弃换行符,而 get() 将换行符保留在输入序列中。

  1. getline()

getline() 函数读取整行,它使用通过回车键输入的换行符来确定输入结尾。要调用这种方法,可以使用 cin.getline()。该函数有两个参数:第一个是数组名,第二个是读取的字符数。如果第二个参数是 20 20 20,则函数最多读取 19 19 19 个字符(还有一个空间用于储存空字符)。getline() 成员函数在读取指定数目的字符或遇到换行符时停止读取。使用该函数修改上述程序:

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

    cout << "Enter your name:\n";
    cin.getline(name, 20);
    cout << "Enter your favorite dessert:\n";
    cin.getline(dessert, 20);
    cout << "I have some delicious " << dessert << " for you, " << name << ".\n";
    return 0;
}

​ 修改后,得到正确输出:

Enter your name:
Baymax Guo
Enter your favorite dessert:
ice cream
I have some delicious ice cream for you, Baymax Guo.

getline() 函数每次读取一行。它通过换行符来确定行尾,但不保存换行符,而是用空字符 \0 替换换行符。

  1. get()

    istream 类中有另一个名为 get() 的成员函数,该函数有几种变体。其中一种变体的工作方式与 getline() 类似,它们接受的参数相同,解释参数的方式也相同,并且都读取到行尾。但 get() 不再读取并丢弃换行符,而是将其留在输入队列中。

    假设我们连续两次调用 get()

    cin.get(name, Arsize);
    cin.get(dessert, Arsize);
    

    由于第一次调用后,换行符将留在输入队列中,因此第二次调用时看到的第一个字符便是换行符,get() 认为已到达行尾,而没有发现任何可读取的内容。如果不借助帮助,get() 将不能跨过该换行符。

    幸运的是,get() 另一种变体。使用不带任何参数的 cin.get(),将从输入中读取一个字符(相当于 getchar())。因此可以用它来处理换行符,为读取下一行做好准备:

    cin.get(name, Arsize);
    cin.get();
    cin.get(dessert, Arsize);
    

    另一种使用 get() 的方式是将两个类成员函数拼接起来,如下所示:

    cin.get(name, Arsize).get();
    

    之所以可以这样做,是因为 cin.get(name, Arsize) 返回一个 cin 对象,该对象随后调用 get() 函数。同样,也可以下面这样调用 getline()

    cin.getline(name1, Arsize).getline(name2, Arsize);
    

    需要指出的是,C++ 允许函数有多个版本,条件是这些版本的参数列表不同。这种特性叫做函数重载

  2. 混合输入字符串和数字

    混合输入时,可能会导致问题:

    #include <iostream>
    int main()
    {
        using namespace std;
        cout << "What year was your house built?\n";
        int year;
        cin >> year;
        cout << "What is its street address?\n";
        char address[80];
        cin.getline(address, 80);
        cout << "Year built: " << year << endl;
        cout << "Address: " << address << endl;
        cout << "Done!\n";
        return 0;
    }
    

    该程序的运行情况如下:

    What year was your house built?
    1966
    What is its street address?
    Year built: 1966
    Address:
    Done!
    

    用户根本没有输入地址的机会。问题在于当 cin 读取年份后,将回车键生成的 \n 留在了输入队列中。后面的 cin.getline() 看到换行符后将认为是一个空行,并将一个空字符串赋给 address 数组。所以应该在读入 year 后把换行处理掉:

    //solution 1
    cin >> year;
    cin.get();
    //solution 2
    (cin >> year).get();
    

3.3 string 类简介

​ C++ 可以使用 string 类的对象来储存字符串。要使用 string 类,必须引头文件 #include <string>string 类位于名称空间 std 中,其定义隐藏了字符串的数组性质。考虑下面程序:

#include <iostream>
#include <string>
int main()
{
    using namespace std;
    char charr1[20];
    char charr2[20] = "jaguar";
    string str1;
    string str2 = "panther";

    cout << "Enter a kind of feline: ";
    cin >> charr1;
    cout << "Enter another kind of feline: ";
    cin >> str1;
    cout << "Here are some felines:\n";
    cout << charr1 << " " << charr2 << " " << str1 << " " << str2 << endl;
    cout << "The third letter in " << charr2 << " is " << charr2[2] << endl;
    cout << "The third letter in " << str2 << " is " << str2[2] << endl;
    return 0;
}

下面是程序运行情况:

Enter a kind of feline: ocelot
Enter another kind of feline: tiger
Here are some felines:
ocelot jaguar tiger panther
The third letter in jaguar is g
The third letter in panther is n

从这个示例可知:在很多方面,使用 string 对象的方式和使用字符数组相同:

  • 可以使用 C 风格字符串来初始化 string 对象
  • 可以使用 cin 读入 string 对象,用 cout 输出 string 对象
  • 可以使用数组表示法来访问存储在 string 对象中的字符(通过下标访问)

上述程序表明,string 对象和字符数组的主要区别是,可以将 string 对象声明为简单变量,而不是数组。

类设计让程序能够自动处理 string 的大小。例如,str1 的声明创建一个长度为 0 0 0string 对象,但当读入 str1 时,将自动调整 str1 的长度。与使用字符数组相比,使用 string 对象更方便,安全。

3.3.1 赋值、拼接和附加

​ 使用 string 类时,某些操作比使用数组时更简单。例如,不能将一个数组赋给另一个数组,但能将一个 string 对象赋值给另一个 string 对象:

char charr1[20];
char charr2[20] = "jaguar";
string str1;
string str2 = "panther";
charr1 = charr2;	//错误,不能将数组赋值给另一个数组
str1 = str2;		//正确

string 类简化了字符串合并操作。可以使用 + 将两个 string 对象合并起来,还可以使用 += 将字符串附加到 string 对象的末尾。也可以使用 C 风格字符串和 string 对象相加,或附加到 string 对象的末尾:

string str3;
str3 = str1 + str2;
str1 += str2;
str3 = str1 + "abc";
str1 += "defg";
3.3.2 string 类 I/O

​ 当 string 需要每次读取一行而不是一个单词时,需要使用以下方法读取:

string str;
getline(cin, str);

​ 对于 string,不能用 cin.getline() 来给 string 赋值,因为 cin.getline() 的第一个参数的类型为 char *。应该使用 getline() 函数,该函数不是类方法,它将 cin 作为参数,指出到哪里去查找输入。另外,也没有指出字符串长度的参数,因为 string 对象会根据字符串长度自动调整大小。

3.3.3 其他形式的字符串字面值

​ 之前说过,除了 char 之外, C++ 还有类型 wchar_t;而 C++11 新增了类型 char16_t、char32_t,下面是一个使用这些类型的例子:

wchar_t title[] = L"Chief Astrogator";
char16_t name[] = u"Felonia Ripova";
char32_t car[] = U"Humber Super Snipe";

​ C++11 新增了另一种类型:原始字符串。在原始字符串中,字符表示的就是自己。例如,\n 不表示换行符,而表示两个常规字符\n。因此在屏幕上打印时,将显示这两个字符;此外,可以在字符串中直接使用 ",而不需要再转义。当然,既然可以在字符串中包含 ",就不能再使用它来表示字符串的开头和结尾。因此,原始字符串将使用 "()" 用作定界符,用前缀 R 来标识原始字符串:

cout << R"(Jim "King" Tutt uses "\n" instead of endl.)";

​ 上述代码将输出:Jim "King" Tutt uses "\n" instead of endl.

​ 如果要在原始字符串中包含 )",该怎么办呢?编译器见到第一个 )" 会认为字符串到此结束,但原始字符串语法允许在表示字符串开头的 "( 之间添加其他字符,这意味着表示字符串结尾的 )" 之间也要包含这些字符。因此,下面语句:

cout << R"+*("(Who wouldn't?)", she whispered.)+*";

​ 将输出:

"(Who wouldn't?)", she whispered.

​ 自定义定界符时,在默认定界符之间可以添加任何数量的基本字符,但空格、括号、斜杆和控制字符(如换行符)除外。

3.4 结构体

​ 与 C 不同,C++ 允许在声明结构时省略 struct 关键字:

struct stu
{
    string name;
    int age;
}
struct stu a; //C 风格
stu b; //C++ 风格
3.4.1 C++11 结构初始化

​ 与数组一样, C++11 也支持将列表初始化用于结构,且 = 是可选的:

stu a {"baymax", 20};
stu b = {"cbetula", 20};

​ 其次,如果打括号内未包含任何东西,则各个成员都将被设置成 0 0 0

​ 最后,不要忘了列表初始化不允许缩窄。

3.4.2 其它结构属性
C++ 允许使用赋值运算符 `=` 将结构赋给另一个同类型的结构,这样结构中的每个成员都将被设置成另一个结构中相应成员的值,即使成员是数组。这种赋值被称为**成员赋值**。

​ C++ 还可以声明没有名称的结构类型,方法是省略名称,同时定义一种结构类型和一个这种类型的变量:

struct		//省略结构名称
{
    int x,y;
} position;

​ 这样将创建一个名为 position 的结构体变量。但这种类型没有名称,因此以后无法创建这种类型的变量。

​ 与 C 不同, C++ 结构还允许有成员函数,将在讨论类的时候再描述。

3.5 共用体(联合体)

​ 共用体是一种数据格式,它能存储不同的数据类型,但只能同时存储其中的一种类型。也就是说,结构体可以同时存储 int、longdouble,而共用体只能存储 int、longdouble。例如:

union one4all
{
    int int_val;
    long long_val;
    double double_val;
}

​ 可以用 one4all 变量来存储 int、longdouble,条件是在不同的时间进行:

one4all pail;
pail.int_val = 15;
cout << pail.int_val;
pail.double_val = 1.38;
cout << pail.double_val;

​ 成员名称标识了变量的容量。由于共用体一次只能存储一个值,因此它必须有足够的空间来存储最大的成员。所以共用体的长度为其最大成员的长度。

​ 共用体的主要用途是节省空间

3.6 枚举

​ C++ 的 enum 工具提供了另一种创建符号常量的方式,这种方式可以代替 const。使用 enum 的句法与使用结构相似。例如:

enum spectrum {red, orange, yellow, green, blue, violet, indigo, ultraviolet};

​ 这条语句完成两项工作:

1. 让 `spectrum` 成为新类型的名称,其被称为枚举
1. 将 `red、orange、yellow` 等作为符号常量,对应整数值 $0\sim 7$。这些常量叫做枚举量。

​ 枚举具有一些特殊属性:

  • 在不进行强制类型转换的情况下,只能将枚举量赋给枚举变量:

    spectrum band;
    band = blue; //正确
    band = 2000; //错误,2000不是枚举量
    
  • 对于枚举,只定义了赋值运算符,而没有定义算术运算:

    band = orange;
    band++;					//错误,没有给枚举定义++运算符
    band = orange + red;	//错误,没有给枚举定义假发
    
  • 枚举量是整型,可被提升为 int 类型,但 int 类型不能自动转换为枚举类型:

    int color = blue;	//正确
    band = 3;			//错误, 3不能自动转换为枚举类型
    color = 3 + red;	//正确, red提升为整型,可以和3相加
    
  • 如果 int 值在枚举的范围内,则可以通过强制类型转换将其赋值给枚举变量:

    band = spectrum(3);	//正确,枚举变量范围是 0~7
    

    如果 int 值不在枚举的范围内,结果是不确定的,但编译器并不会报错。

3.6.1 设置枚举量的值

​ 可以使用赋值运算符来显示地设置枚举量的值,指定的值必须是整数,也可以只显示地定义其中一部分枚举量的值:

enum bits{one = 1, two = 2, four = 4, eight = 8};
enum bigstep{first, second = 100, third}; //first=0,third=101

​ 后面未被初始化的枚举量的值将比前面的枚举量大 1 1 1

​ 可以创建多个值相同的枚举变量:

enum {zero, null=0, one, numero_uno=1};
3.6.2 枚举的取值范围

​ 枚举的取值范围定义如下。首先找到枚举量的最大值,再找到大于这个最大值的、最小的 2 2 2 的幂,再减去 1 1 1,得到的便是枚举取值的上限。下限类似,例如:

enum {a = -6, b = 101};

​ 最大枚举值为 101 101 101,满足条件的 2 2 2 的幂为 128 128 128,减去 1 1 1 得枚举上限为 127 127 127;最小枚举值为 − 6 -6 6,满足条件的 2 2 2 的幂为 − 8 -8 8,加上 1 1 1 得下限为 − 7 -7 7

​ 在枚举的取值范围内,通过强制转换,可以将取值范围中的任何整数值赋值给枚举变量,即使这个值不是整数值,如:

enum bits{one = 1, two = 2, four = 4, eight = 8};
bits myflag = bits(6);

3.7 指针和自由存储空间

3.7.1 使用 new 分配内存

​ 在 C 语言中,可以用 malloc() 来分配内存;在 C++ 中仍然可以这样做,但还有更好的方法——使用 new 运算符。程序员要告诉 new 为哪种数据类型分配内存;new 将找到一个长度正确的内存块,并返回该内存块的地址。程序员的责任是将该地址赋给一个指针,例如:int * pn = new int

​ 为一个数据对象(可以是结构,也可以是基本类型)分配内存的通用格式如下:

typeName * pointer_name = new typeName;

​ 需要指出的是,new()malloc() 一样,都是从自由存储区)的内存区域分配内存的。

3.7.2 使用 delete 释放内存

​ 使用 delete 可以释放用 new 分配的内存:

int * ps = new int;
...
delete ps;

​ 这将释放 ps 指向的内存,但不会删除指针 ps 本身,可重新令 ps 指向另一片内存。一定要配对地使用 newdelete,否则将发生内存泄漏。

​ 不要尝试释放已经释放的内存块,C++ 标准指出这样做的结果是不确定的,这意味着什么都有可能发生。另外,不能使用 delete 来释放由声明变量所获得的内存:

int * ps = new int;
delete ps;
delete ps;	//未定义的行为
int a = 5;
int b* = &a;
delete b;  //错误,不能释放由声明变量所获得的内存

警告:只能用 delete 来释放使用 new 分配的内存。然而,对空指针使用 delete 是安全的。

​ 注意,使用 delete 的关键在于:将它用于 new 分配的内存。这并不意味着只能用于分配内存时的指针,只要是指向 new 分配的内存的指针都可以,如:

int * ps = new int;
int * pq = ps;
delete pq; //正确
3.7.3 使用 new 来创建动态数组

new 后面加上方括号,包含元素数目,即可创建动态数组:int * arr = new int [10];

对于使用 new 创建的数组,应使用另一种格式的 delete 释放:delete [] arr;

[] 告诉程序,应该释放整个数组,而不仅仅是指针指向的元素。

​ 使用 newdelete 时,应该遵循以下规则:

  • 不要使用 delete 释放不是 new 分配的内存

  • 不要使用 delete 释放同一片内存两次

  • 如果使用 new [] 为数组分配内存,则应使用 delete [] 来释放;如果使用 new 时没有方括号,使用 delete 时也不应该带上

  • 对空指针使用 delete 是安全的

3.8 指针算术

  • 数组的地址

    数组名一般被认为是首元素的地址,除了以下两种情况:

    1. sizeof(arr)

      这种情况下,sizeof(arr) 将返回整个数组的大小

    2. &arr

      &arr 取出的是整个数组的地址:

      short tell[10];
      cout << tell << endl;
      cout << &tell << endl;
      

      从数字上说,这两个地址相同;但从概念上说,tell 是一个 2 2 2 字节内存块的地址,而 &tell 是一个 20 字节内存块的地址。因此,表达式 tell+1 将地址值加 2 2 2,表达式 &tell+1 将地址值加 20

      换句话说,tell 的类型是 short *,而 &tell 的类型是 short (*)[10]

  • const 修饰指针

    const* 的前面:不可以修改指针指向的值,但可以修改指针指向的位置。

    const* 的后面:不可以修改指针指向的位置,但可以修改指针指向的值。

    int a = 5;
    const int * p = &a;
    *p = 6; //error
    p = new int; //valid
    
    int * const r = &a;
    *r = 6; //valid
    r = new int;//error
    
  • 自动存储、静态存储和动态存储

    根据用于分配内存的方法,C++ 主要有 3 3 3 种管理数据内存的方式:自动存储、静态存储和动态存储。

    1. 自动存储

      相当于存储在栈区空间中。

    2. 静态存储

      静态存储是整个程序执行期间都存在的存储方式。使变量成为静态的方式有两种:一种是在函数外面定义它(全局变量);另一种是在声明变量时使用关键字 static

      static double fee = 56.50;
      
    3. 动态存储

      newdelete 运算符管理的是称为堆的内存池。

3.9 数组的替代品
3.9.1 模板类 vector

​ 模板类 vector 类似于 string 类,也是一种动态数组。可以在运行阶段设置 vector 对象的长度,也可以在末尾附加新数据,还可以在中间插入新数据。这是通过 newdelete 来管理内存实现的,但这种工作是自动完成的。

​ 下面介绍一些基本的实用知识。首先,要使用 vector 对象,必须包含头文件 #include <vector>。其次, vector 包含在名称空间 std 中。再次,需要向模板类提供存储的数据类型。最后,vector 类使用不同的语法来指定元素数。如:

#include <vector>
...
using namespace std;
vector<int> vi;
int n;
cin >> n;
vector<double> vd(n);

​ 由于 vector 对象可以在插入和添加值时自动调整长度,因此可以将 vi 的长度设置为零。但要调整长度,需要使用 vector 类的方法,将在以后介绍。

3.9.2 模板类 array

vector 类的功能比数组强大,但代价是效率稍低。如果需要长度固定的数组,使用 array 类是一个更好的选择。与数组一样,array 对象的长度是固定的,且使用栈区分配内存,效率和普通数组相同,但更加方便安全。

​ 要创建 array 对象,需包含头文件 #include <array>。例如:

#include <array>
...
using namespace std;
array<int, 5> ai;
array<double, 4> ad = {1.2, 2.1, 3.43, 4.3};

​ 要创建包含 n_elem 个类型为 typename 的元素,应使用以下语法:

array<typeName, n_elem> arr;

​ 可以将 array 对象赋给另一个 array 对象;而对于数组,只能逐元素复制数据。

​ 此外,可以通过成员函数 at() 访问 arrayvector 的元素,arr.at(i)arr[i] 等价,但前者可以禁止如 i=-1 时产生的非法行为,更加安全可靠。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值