C++ Primer Plus学习笔记之复合类型(上)

前言

个人觉得学习编程最有效的方法是阅读专业的书籍,通过阅读专业书籍可以构建更加系统化的知识体系。

一直以来都很想深入学习一下C++,将其作为自己的主力开发语言。现在为了完成自己这一直以来的心愿,准备认真学习《C++ Primer Plus》。

为了提高学习效率,在学习的过程中将通过发布学习笔记的方式,持续记录自己学习C++的过程。

本篇前言

本章首先将介绍除类以外的所有复合类型,以及将介绍newdelete及如何使用它们来管理数据。另外,还将简要地介绍string类,它提供了另一种处理字符串的途径。

一、数组

数组(array)是一种数据格式,能够存储多个同类型的值。

要创建数组,可使用声明语句。数组声明应指出以下三点:

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

声明数组的通用格式如下:

typeName arrayName[arraySize]

表达式arraySize指定元素数目,它必须是整型常数(如10)或const值,也可以是常量表达式(如8*sizeof(int)),即其中所有的值在编译时都是已知的。
具体示例如下代码:

short months[12];

通过以上代码,声明了一个名为months的拥有12short类型值的数组。

通过使用下标或索引可以单独访问数组元素。C++规定数组从0开始编号。C++使用带索引的方括号表示法来指定数组元素。例如:months[0]months数组的第一个元素,months[11]months数组的最后一个元素。因为开始数字为0,加上元素数量12个,最后一个元素比总数量少1。

在编写代码的过程中要注意下标值是否有效,例如:months[-1]months[12]就是无效的下标值,这可能会导致程序出现异常。

接下来我们根据程序清单4.1了解数组的一些属性,包括声明数组、给数组元素赋值以及初始化数组:

// arrayone.cpp -- 小型整数数组
#include <iostream>
int main()
{
    using namespace std;
    int orange[2]; // 创建有2个元素的数组
    orange[0] = 15; // 赋值给第1个元素
    orange[1] = 28;
    int orangeCosts[2] = {3, 2}; // 创建并初始化数组
    cout << "橙子的总数量 = ";
    cout << orange[0] + orange[1] << endl;
    cout << "第一个包装里有" << orange[0] << "个橙子,";
    cout << "其中每个橙子的成本为" << orangeCosts[0] << "元。\n";
    cout << "第二个包装里有" << orange[1] << "个橙子,";
    cout << "其中每个橙子的成本为" << orangeCosts[1] << "元。\n";
    int total = orange[0] * orangeCosts[0] + orange[1] * orangeCosts[1];
    cout << "全部橙子的总成本为" << total << "元。\n";
    cout << "\norange数组的长度 = " << sizeof orange;
    cout << " bytes.\n";
    cout << "第一个数组的长度 = " << sizeof orange[0];
    cout << " bytes.\n";
    return 0;
}

运行结果如下:

橙子的总数量 = 43
第一个包装里有15个橙子,其中每个橙子的成本为3元。
第二个包装里有28个橙子,其中每个橙子的成本为2元。
全部橙子的总成本为101元。
orange数组的长度 = 8 bytes.
第一个数组的长度 = 4 bytes.

1、程序说明

该程序首先创建一个名为orange包含2个int类型元素的数组。然后我们根据下标值分别对2个元素进行了赋值。因为orange的每个元素都是int类型,因此能够将数值赋值给元素、并将它们相加和相乘。

程序在给数组中元素赋值时,采用了两种方式,分别是通过下标值对每个元素进行赋值和提供一个用逗号分隔的值列表(初始化列表),并将它们用花括号括起即可。

接下来,程序通过下标值对数组中元素进行访问并进行一些计算。

sizeof运算符返回类型或数据对象的长度(单位为字节)。将sizeof运算符用于数组名,得到的将是整个数组中的字节数,如果将sizeof用于数组元素,则得到的将是元素的长度(单位为字节)。

2、数组的初始化规则

C++只有在定义数组时才能使用初始化,此后只能通过下标值分别对单个元素进行赋值:

int orangeCosts[2] = {3, 2}; // 有效
int bananaCosts[2]; // 有效
bananaCosts[2] = {7,10}; // 无效
bananaCosts = orangeCosts; // 无效
bananaCosts[0] = 7; // 有效
bananaCosts[1] = 10; // 有效

初始化数组时,提供的值可以少于数组的元素数目。例如,下面的语句只初始化orangeCosts数组的第一个元素:

int orangeCosts[2] = {3}; // 有效

以上代码,将第一个元素赋值为3,第二个元素编译器默认赋值为0。

如果初始化数组时方括号内([]) 为空,C++编译器将计算元素个数。例如:

int appleCosts = {2 , 5}; 

以上代码,编译器将使appleCosts数组包含2个元素。

3、C++11数组初始化方法

C++11将使用大括号的初始化(列表初始化)作为一种通用初始化方式,可用于所有类型。C++11在以前列表初始化的基础上增加了一些新的功能:

  • 初始化数组时,可省略等号 (=):
int appleCosts[2] {2 , 5}; 
  • 可不在大括号内包含任何东西,这将把所有元素都设置为零
int appleCosts[2] {}; 
  • 列表初始化禁止缩窄转换:
int appleCosts[2] {2.0 , 5}; 

在上述代码不能通过编译,因为将浮点数转换为整型是缩窄操作,即使浮点数的小数点后面为零。

二、字符串

字符串是存储在内存的连续字节中的一系列字符。C++处理字符串的方式有两种:1、继承自C语言的C风格字符串;2、基于string类库的方法。

C风格字符串具有一种特殊的性质:以空字符(null character)结尾,空字符被写作\0,其ASCII码为0,用来标记字符串的结尾,如下代码:

char hi[2]={'h', 'i' };//不是字符串
char hello[6]={'h',  'e',  'l',  'l',  'o', '\0'};//是字符串

以上方法初始化字符串的方法较为复杂,可以使用引号括起字符串即可。这种字符串被称为字符串常量(string constant)或字符串字面值(string literal),如下代码:

char hello[6]="hello";
char good[]="good";//让编译器计算长度

用引号括起的字符串隐式地包括结尾的空字符,因此不用显式地包括它。

C++对字符串的长度没有限制,处理字符串的函数根据空字符的位置判断字符串是否结束。在确定存储字符串所需的最短数组时,需要将结尾的空字符计算在内。

注意:字符串常量(使用双引号)不能与字符常量(使用单引号)互换。

1、拼接字符串常量

有时候,字符串很长,无法放到一行中。C++允许拼接字符串字面值,即将两个用引号括起的字符串合并为一个。事实上,任何两个由空白(空格、制表符和换行符)分隔的字符串常量都将自动拼接成一个。
因此,,下面所有的输出语句都是等效的:

cout << "hi i am " "kangkang\n";
cout << "hi i am " "kangkang\n";
cout << "hi i am "
    "kangkang\n";

注意,拼接时不会在被连接的字符串之间添加空格,第二个字符串的第一个字符将紧跟在第一个字符串的最后一个字符 (不考虑\0)后面。第一个字符串中的\0字符将被第二个字符串的第一个字符取代。

2、在数组中使用字符串

要将字符串存储到数组中,最常用的方法有两种将数组初始化为字符串常量、将键盘或文件输入读人到数组中。代码如下:

// strings.cpp -- 在数组中存储字符串
#include <iostream>
#include <cstring> // strlen()函数所在头文件
int main()
{
    using namespace std;
    const int Size = 15;
    char name1[Size]; // 空数组
    char name2[Size] = "Jane"; // 初始化数组
    cout << "你好!我是" << name2;
    cout << "!请问你叫什么名字?\n";
    cin >> name1;
    cout << "我叫" << name1 << ",我的名字占";
    cout << strlen(name1) << "个字符";
    cout << ",在存储数组中占" << sizeof(name1) << "字节。\n";
    cout << "第一个字符是" << name1[0] << "。\n";
    name2[1] = '\0'; // 设置空字符串
    cout << "很高兴认识你,我的第一个字符是";
    cout << name2 << endl;
    return 0;
}

运行结果如下:

你好!我是Jane!请问你叫什么名字?
Kang
我叫Kang,我的名字占4个字符,在存储数组中占15字节。
第一个字符是K。
很高兴认识你,我的第一个字符是J

通过使用sizeof运算符可以计算出数组的长度,strlen()函数只计算存储在数组中可见的字符的长度。

3、字符串输入

由于不能通过键盘输入空字符,cin使用空白(空格、制表符和换行符)来确定字符串的结束位置,这意味着cin在读取字符数组输入时如果存在空白则会导致,只读取空白之前的内容放到数组中,并自动在结尾添加空字符。例如:

// instr1.cpp -- 读取多个字符串
#include <iostream>
int main()
{
    using namespace std;
    const int ArSize = 20;
    char name[ArSize];
    char city[ArSize];
    cout << "请输入你的姓名(姓和名空格分开):\n";
    cin >> name;
    cout << "输入你的城市:\n";
    cin >> city;
    cout << "请确定输入内容:姓名为" << name << ",所在城市为" << city << endl;
    return 0;
}

运行结果如下:

请输入你的姓名(姓和名空格分开):
张 三
输入你的城市:
请确定输入内容:姓名为张,所在城市为三

出现以上情况,就是因为张和三字之间有空白。cin在读取字符数组输入时遇到空白时,将读取的空白之前的内容放到数组中,并自动在结尾添加空字符,而空白之后的内容在遇到下一个cin时又会重复上面的规则,这才导致三字被写入city数组中。

要解决以上问题有两种方法:getline()函数和get()函数,这两个函数都读取一行输入,直到到达换行符。然而,随后getline()将丢弃换行符,而get()将换行符保留在输入序列中。

将上述代码分别用getline()函数和get()函数替换如下:

// instr1.cpp -- 读取多个字符串
#include <iostream>
int main()
{
    using namespace std;
    const int ArSize = 20;
    char name[ArSize];
    char city[ArSize];
    cout << "请输入你的姓名(姓和名空格分开):\n";
    cin.get(name,20).get() ;
    cout << "输入你的城市:\n";
    cin.getline(city,20);
    cout << "请确定输入内容:姓名为" << name << ",所在城市为" << city << endl;
    return 0;
}

运行结果如下:

请输入你的姓名(姓和名空格分开):
张 三
输入你的城市:
北京
请确定输入内容:姓名为张 三,所在城市为北京

因为get()将换行符保留在输入序列中,需要调用get()将尾部的换行符读取掉。否则还是会出现上面的情况,无法输入城市的内容。

三、string类简介

string类定义隐藏了字符串的数组性质,可以像处理普通变量那样处理字符串。

使用string对象更方便,也更安全。从理论上说,可以将char数组视为一组这使得与使用数组相比,用于存储一个字符串的char存储单元,而string类变量是一个表示字符串的实体

1、C++11字符串初始化

C++11也允许将列表初始化用于C风格字符串和string对象,代码如下:

char hi[] = { "hi" };
string hello = { "hello" };

2、赋值、拼接和附加

使用string类时,可以将一个string对象赋给另一个string对象,代码如下:

string str1 = "test";
string str2 = str1;

string类简化了字符串的合并操作。可以使用运算符+或者+=将两个string对象合并起来,代码如下:

string str3 = str1 + str2;
str1 += str2;

3、string类的其他操作

接下来我们对比一下string类和字符数组的不同,代码如下:

// strtype3.cpp -- 更多字符串类特性
#include <iostream>
#include <string> // string类的头文件
#include <cstring> // C风格字符串头文件
int main()
{
    using namespace std;
    char charr1[20];
    char charr2[20] = "jaguar";
    string str1;
    string str2 = "panther";
    // string对象和字符数组的赋值
    str1 = str2; // 将 str2 复制给 str1
    strcpy(charr1, charr2); // 将 charr2 复制给 charr1
    // 用于string对象和字符数组的追加
    str1 += " paste"; // 将“ paste”追加到 str1 结尾
    strcat(charr1, " juice"); // 将“ juice”追加到 charr1 结尾
    // 查找string对象和C风格字符串的长度
    int len1 = str1.size(); // 获取 str1 长度
    int len2 = strlen(charr1); // 获取 charr1 长度
    cout << "字符串 " << str1 << " 包含 "
         << len1 << " 个字符。\n";
    cout << "字符串 " << charr1 << " 包含 "
         << len2 << " 个字符。\n";
    return 0;
}

运行结果如下:

字符串 panther paste 包含 13 个字符。
字符串 jaguar juice 包含 12 个字符。

从上述代码,我们可以看出string对象的语法通常比使用C字符串函数简单,同时因为string对象会自动调整大小,相对于字符数组一不小心就超出目标数组大小,显得更为安全。

其中还有一点差别,就是在计算字符串的长度时,str1不是被用作函数的参数,而是通过句点.连接了size()方法。这是因为str1作为string类对象,可以使用对象名和句点.运算符来指出方法要使用哪个字符串,换一种说法就是string类对象通过句点.运算符可以调用string类中的方法。

4、string类 I/O

未初始化的数组的内容是未定义的;其次,函数strlen()从数组的第一个元素开始计算字节数,直到遇到空字符。

将输入读取到string对象,还可以通过如下方式:

getline(cin, str1);

5、其他形式的字符串字面值

C++有很多类型的字符串字面值,具体如下:

char str[] = "hello";
wchar_t str1[] = L"hello";
char16_t str2[] = u"hello";
char32_t str3[] = U"hello";

C++11新增的另一种类型是原始(raw)字符串。将"()"作为定界符,并使用前缀R来标识原始字符串:

cout << R"("Apple" 的意思是苹果。)" << endl;

运行结果如下

"Apple" 的意思是苹果。

根据运行结果我们可以看到,"()"定界符之间的内容被原封不动的进行了输出。但当我们需要在"()"定界符之间输入)"时,可以在原始字符串语法的"(之间添加其他字符,同时结尾")之间也必须包含这些字符。例如在"(之间添加+*,需要使用R"+*()+*",示例代码如下:

cout << R"+*("(Apple)" 的意思是苹果。)+*" << endl;

运行结果如下

"(Apple)" 的意思是苹果。

四、结构简介

当我们有一组不同类型的数据需要存储时,可以使用C++中的结构类型。

结构是用户定义的类型,而结构声明定义了这种类型的数据属性。定义了类型后,便可以创建这种类型的变量。因此创建结构包括两步。首先,定义结构描述——它描评并标记了能够存储在结构中的各种数据类型。然后按描述创建结构变量(结构数据对象)。最后可以定义结构类型后,通过成员运算符(.)来访问各个成员。

结构定义语法:

struct 结构名
{
    //数据类型
}

1、在程序中使用结构

// structur.cpp -- 一个简单的结构
#include <iostream>
struct Person // 声明结构
{
    char name[20];
    int age;
};
int main()
{
    using namespace std;
    Person bjPerson =
    {
            "张三", // name 值
            18 // age 值
    };
    Person shPerson =
    {
            "李四",
            32
    };
    cout << "请北京来的同学介绍一下自己:" << "\n";
    cout << "大叫好,我是" << bjPerson.name << ",今年" << bjPerson.age << "岁\n";
    cout << "接下来请上海来的同学介绍一下自己:" << "\n";
    cout << "大叫好,我是" << shPerson.name << ",今年" << shPerson.age << "岁\n";
    return 0;
}

运行结果如下:

请北京来的同学介绍一下自己:
大叫好,我是张三,今年18岁
接下来请上海来的同学介绍一下自己:
大叫好,我是李四,今年32岁

在使用结构之前,首先声明结构,然后如声明数组变量一样,使用结构类型名加上变量名即可表明一个结构类型,接着和数组一样,使用由逗号分隔值列表,并将这些值用花括号括起。

要调用结构类型中的成员,使用成员运算符(.)即可。

2、C++11结构初始化

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

Person bjPerson{"张三", 18};

当大括号内未包含任何东西,各个成员都将被设置为零。

Person bjPerson{};

bjPerson.name的每个字节都被设置为零,bjPerson.age被设置为零。

同数组一样,不允许缩窄转换。

3、结构可以将string类作为成员吗

只要编译器支持对以string对象作为成员的结构进行初始化:

#include <string>
struct Person // 声明结构
{
    std::string name;
    int age;
};

4、其他结构属性

C++使用户定义的类到与内置类型尽可能相似。例如,可以将结构作为参数传递给函数,也可以让函数返回一个结构。另外,还可以使用赋值运算行(=)将结构赋给另一个同类型的结构,这样结构中每个成员都将被设置为另一个结构中相应成员的值,即使成员是数组。这种赋值被称为成员赋值(assignment))。

5、结构数组

结构是用户定义的类型,也可以如其他基本类型一样,创建对应的数组,代码如下:

Person persons[2] = 
{
    {
            "张三", 
            18 
    },
    {
            "李四",
            32
    }
};
    cout << "请第一位同学介绍一下自己:" << "\n";
    cout << "大叫好,我是" << persons[0].name << ",今年" << persons[0].age << "岁\n";

6、结构中的位字段

字段的类型应为整型或枚举,接下来是冒号,冒号后面是指定了使用位数的数字。可以使用没有名称的字段提供间距。每个成员都被称为位字段(bit field)。示例代码:

struct ComputerInfo
{
    unsigned int SN : 4;//序列号占4bit
    unsigned int : 4;//空白间距占4bit
    unsigned int Version : 1;//版本号占1bit
}

可以像通常那样初始化这些字段:

ComputerInfo computerInfo = {14, 3 };

五、 共用体

共用体 (union)是一种数据格式,它能够存储不同的数据类型,但只能同时存储其中的一种类型。也就是说,结构可以同时存储 intlongdouble,共用体只能存储intlongdouble。共用体的句法与结构相似,但含义不同。例如,请看下面的声明:

#include <iostream>
using namespace std;
struct Person // 声明结构
{
    union
    {
        unsigned short insideId;
        long globalId;
    } PersonId;
    string name;
    int age;
};
int main()
{
    Person person;
    person.PersonId.insideId = 101;
    person.name = "张三";
    person.age = 18;
    cout << "人员内部Id是" << person.PersonId.insideId << endl;
    person.PersonId.globalId = 122834;
    cout << "人员全局Id是" << person.PersonId.globalId << endl;
}

共用体常用于操作系统数据结构或硬件数据结构。

六、枚举

C++的enum工具提供了另一种创建符号常量的方式,这种方式可以代替const。它还允许定义新类型,但必须按严格的限制进行。使用enum的句法与使用结构相似。示例代码如下:

enum myColor
{
    red,
    blue,
    green
};

以上代码让myColor成为新类型的名称; myColor被称为枚举(enumeration)。就像struct变量被称为结构一样。将redbluegreen作为符号常量,它们对应整数值0~2。这些常量叫作枚举量(enumerator)。

在默认情况下,将整数值赋给枚举量,第一个枚举量的值为0,第二个枚举量的值为1,依次类推。

枚举通常被用来定义相关的符号变量,而不是新类型。

演示代码:

enum myColor
{
    red,
    blue,
    green
};
int main()
{
    myColor color1;
    color1 = red;
    color1 = myColor(1);
}

在使用整型转换为枚举时,需要使用枚举括号包含整型值进行转换才行。

1、设置枚举量的值

可以使用赋值运算符来显式地设置枚举量的值:

enum myColor
{
    red = 2,
    blue = 4,
    green = 7
};

指定的值必须是整数。也可以只显式地定义其中一些校举量的值,甚至可以将多个枚举值设置为同一整数。

2、枚举的取值范围

取值范围的定义如下:

  • 找出上限,需要知道枚举量的最大值。找到大于这个最大值的、最小值的2的幂,将它减去1,得到的便是取值范围的上限。
  • 计算下限,需要知道枚举量的最小值。如果它不小于0,则取值范围的下限为0;否则,采用与寻找上限方式相同的方式,但加上负号。

选择用多少空间来存储枚举由编评器决定。

posted @ 2022-10-29 23:45  好先生FX  阅读(56)  评论(0编辑  收藏  举报