C++入门教程笔记

第一章 开始

1.2 初识输入输出

C++语言未包含任何输入输出(IO)语句,包含一个全面的标准库来提供IO机制。

iostream库,包含istream和ostream,表示输入流和输出流

标准输入输出对象

​ 标准库定义了4个IO对象。

  • cin :标准输入
  • cout:标准输出
  • cerr:输出警告和错误信息
  • clog:输出程序运行时的一般性信息
1
2
3
4
5
6
7
8
9
#include <iostream>

int main() {
std::cout << "Enter two numbers:" << std::endl;
int v1 = 0, v2 = 0;
std::cin >> v1 >> v2;
std::cout << "The sum of " << v1 << " and " << v2 << " is " << v1 + v2 << std::endl;
return 0;
}

输出运算符(<<):接受两个运算对象,左侧的运算对象必须是一个ostream对象,右侧的运算对象是要打印的值。此运算符将给定的值写到给定的ostream对象中。

输入运算符(>>):接受一个istream作为左侧运算对象,接受一个对象作为右侧运算对象。它从给定的istream读如数据,并存到给定的对象中。

endl:这是一个被称为操纵符的特殊值,写入endl的效果就是结束当前行,并将与设备关联的缓冲区中的内容刷到设备中。缓存刷新操作可以保证到目前为止所产生的所有输出都真正写入输出流中,而不是停留在内存中等待写入流。

:::作用域运算符

使用标准库中的名字

上面程序中使用std::cout 和 std::endl,而不是cout和endl,前缀std::指出名字cout和endl是定义在名为std的命名空间中的名字。

从流读取数据

1.3 注释

C++中注释的种类

  • 单行注释:以双斜线(//)开始,以换行符结束,可以包含任何文本,包括额外的双斜线。
  • 界定符对注释:可以包含”*/“以外的任意内容,包括换行符

1.4 控制流

第二章 C++基础

2.1 基本内置类型

​ C++定义了一套包含 算数类型空类型 在内的基本数据类型。

2.1.1 算数类型

算数类型分为两类:

  • 整型(包括字符和布尔类型在内)
  • 浮点型

算数类型数据所占的比特数在不同的机器上有所差异,下表列出C++标准规定的尺寸的最小值,同时允许编译器赋予这些类型更大的尺寸。

类型 含义 最小尺寸
bool 布尔类型 未定义
char 字符 8位
wchar_t 宽字符 16
char16_t Unicode字符 16
char32_t Unicode 32
short 短整型 16
int 整型 16
long 长整型 32
long long 长整型 64
float 单精度浮点型 6位有效数字
double 双精度浮点型 10位有效数字
long double 拓展精度浮点数 10位有效数字

​ C++提供几种字符类型,基本字符类型是char,一个char的空间应确保能存放机器字符集中任意字符对应的数字值。

​ 除了字符和布尔型,其他整型用于表示不同尺寸的整数。C++语言规定一个int至少和一个short一样大,一个long至少和一个int一样大,一个long long至少和一个long一样大,其中long long类型是在C++11中新定义的。

带符号类型和无符号类型

​ 除了布尔类型和拓展的字符类型之外,其他整型可以划分为带符号(signed)无符号的(unsigned)两种。带符号整型可以表示正数,负数或0,无符号类型能表示大于等于0的值。

​ 类型int、short、long和long long都是带符号的,前面加unsigned就是无符号的,例如unsigned long。类型unsigned int 可以缩写为unsigned。

​ 与其他整型不同,字符型被分为三种:char、signed char和unsigned char。特别注意:char和类型signed char并不一样。尽管字符型又三种,但字符的表现形式缺只有两种(有符号和无符号),类型char实际上表现为上述两种中的一座,具体由编译器决定。

2.1.2 类型转换

类型所能表示的值的范围决定了转换的过程:

  • 非布尔类型的算数值赋值给布尔类型,初始值为0 的结果为false,否则为true。

  • 布尔值赋给非布尔类型时,初始值为false则结果为0,初始值为true则结果为1。

  • 浮点数赋值给整数类型时,进行近似处理。保留浮点数中整数部分。

  • 整数赋值给浮点类型时,小数部分为0。如果改整数占的空间超过浮点类型的容量,精度可能有损失。

  • 赋值给无符号类型一个超过它访问范围的值时,结果时初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char表示0至255区间内的值,将-1赋值给unsigned char得到的结果就是-1对256取模,结果等于255。

  • 赋值给带符号类型一个超出它范围的值时,结果时未定义(undefined),此时程序程序可能继续工作,也可能崩溃,也可能生成垃圾数据。

2.1.3 字面常量

字面值常量的形式和值决定了它的数据类型

整型和浮点型字面值

​ 可以将整型字面值写作十进制数,八进制或十六进制的形式。

……

转义序列

不可直接使用的两类字符:

  • 不可打印的字符,如退格或其他控制字符,因为它们没有可视的图符

  • C++语言中有特殊含义的字符(单引号,双引号,问好,反斜线),用到是需要使用转义序列

    转义序列以反斜线座位开始

| 符号 |    含义    | 符号 |    含义    | 符号 |  含义  |
| :--: | :--------: | :--: | :--------: | :--: | :----: |
|  \n  |   换行符   |  \t  | 横向制表符 |  \a  | 报警符 |
|  \v  | 纵向制表符 |  \b  |   退格符   |  \"  | 双引号 |
|  \\  |   反斜杠   |  \?  |    问号    |  \'  | 单引号 |
|  \r  |   回车符   |  \f  |   进纸符   |      |        |

指定字面值的类型

字符和字符串字面值

前缀 含义 类型
u Unicode16字符 char16_t
U Unicode32字符 char32_t
L 宽字符 wchar_t
u8 UTF-8(仅用于字符串字面常量) char

整型字面值

后缀 类型
l or L long
u or U unsigned
ll or LL long long

浮点型字面值

后缀 类型
f或F float
l或L long double

指针字面值

nullptr是指针字面值

2.2 变量

​ 变量提供一个具名,可供操作的存储空间。

注意:初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义时把对象的的当前值擦除,而以一个新值来代替。

列表初始化

下面的作用都是定义一个units_sold的int变量并初始化为0:

int units_sold = 0;

int units_sold = {0};

int units_sold{0};

int units_sold(0);

用花括号来初始化变量时C++新标准的一部分,这种初始化的形式被称为列表初始化

默认初始化

​ 如果定义变量时没有指定初始值,则变量被默认初始化,此时变量被赋予了”默认值“。默认值时什么由变量类型决定,同时定义变量的位置也会因此有所影响。

​ 如果内置类型的变量被显示初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0。一个未被初始化的内置类型变量的值时未定义的,如果试图拷贝或者其他形式访问此类值,将引起报错。

​ 每个类各自决定其初始化对象的方式。是否不经初始化就定义对象也由类决定,假如允许这种行为,它将决定对象初始值到底时什么。

2.2.2 变量声明和定义的关系

​ 为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译(separate compilation)机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。

​ 为了支持分离式编译,C++将声明和定义区分开来,声明(declaration)使得名字为程序所知,一个文件弱国想使用别处定义的名字,则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。

​ 变量声明规定了变量的类型和名字,在这点上定义与之相同。除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。

​ 如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式得初始化变量。

1
2
extern int i; //声明i而非定义i
int j; //声明并定义j

​ 任何包含了显式初始化的声明即成为定义,我们能给由extern关键字标记的变量赋一个初始值,但这么做就抵消了extern的作用,此时就不再是声明,而是定义了。

1
extern double pi = 3.1415; //定义

​ 在函数内部,如果试图初始化一个由extern关键字标记的变量,会引发错误。

tips:变量能且只能被定义一次,但可以被多次声明。

2.2.3 标识符

​ C++的标识符由字母、数字和下划线组成,必须以字母或下划线开头。标识符长度没有限制,但对大小写敏感。

​ 自定义标识符不能连续出现两个下划线,也不能以下划线紧连大写字母开头。此外,定义在函数体外的标识符不能以下划线开头。

变量命名规范

变量命名规范是约定俗成的,不是强制的,但遵守规范能提高代码的可读性:

  • 标识符要有实际含义
  • 变量名一般用小写字母,例如index,而不是Index或INDEX。
  • 用户自定义的类名一般以大写字母开头,如Sales_item。
  • 如果标识符由多个单词组成,单词间应明显区分,应使用下划线连接或者使用驼峰法,例如player_name,或者playerName。

2.2.4 名字的作用域

​ 程序中使用到的名字都会指向一个特定的实体:变量,函数、类等,然而,在程序的不同位置,会可能指向不同的实体。

​ 名字的有效区域始于名字的声明语句,结束于语句所在的作用域末端。

2.3 复合类型

复合类型是指基于其他类型定义的类型。

2.3.1 引用

引用为对象起了另一个名字,引用类型引用另一种类型。通过声明符写成 &d 的形式来定义引用类型(d是声明的变量名)

1
2
3
int i = 10;
int &refValue = i; // refValue 指向i, 是i的另一个名字
int &refValue; // 报错,引用必须初始化

​ tips:引用并非对象,它只是为一个已经存在的对象起的另一个名字。对其进行的操作,实际上都是对其绑定的对象进行操作,例如获取引用的值,实际上是获取其绑定对象的值。

注意:

  • 因为引用本身不是对象,所有不能定义引用的引用。
  • 引用的类型要与它所绑定对象的类型严格匹配。
  • 引用只能绑定在对象上,而不能与字面值,或者某个表达式的计算结果绑定在一起。

​ 定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另一个对象,所有引用必须初始化。

​ 定义一个引用后,对其进行的所有操作都是在与之绑定的对象上进行的。

2.3.2 指针

指针(pointer)是指向另一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。

​ 定义指针类型的方法将声明符写出d的形式,其中d是变量名,如果一条语句中定义了几个指针变量,每个变量前面都必须带

获取对象地址

​ 指针存放讴歌对象的地址,想获取该地址,需要使用取地址符(操作符&):

1
2
int i = 10;
int *p = &i; // p存放i的地址,或者说p是指向i的指针

注意:

  • 引用 不是对象,没有实际地址,所以不能定义指向引用的指针。
  • 指针类型必须与它所指向的对象严格匹配。

指针值

​ 指针的值(即地址)应属于下列4中状态之一:

  1. 指向一个对象。

  2. 指向紧邻对象所占空间的下个地址。

  3. 空指针,意味着指针没有指向任何对象。

  4. 无效指针,也就是除了上述情况之外的其他值。

    试图拷贝或者其他方式访问无效指针的值都将引发错误。编译器并不检查此类错误。

利用指针访问对象

​ 如果指针指向了一个对象,则可以使用解引用符(操作符*)来访问该对象:

1
2
3
int i = 1;
int *p = &i;
cout << *p; //输出1,符号*得到指针p所指向的对象

​ 对指针解引用会达到所指对象,因此如果给解引用的结果赋值,实际上就是给指针所指对象赋值。

1
2
*p = 0;
cout << *p; //输出0

​ 解引用操作仅适用于那些确实指向了某个对象的有效指针。

空指针

空指针(null pointer)不指向任何对象。

​ 得到空指针最直接的方法就是用字面值nullptr来初始化指针,这是C++11新标准引进的方法。也可以通过将指针初始化为字面值0来生成空指针。

​ 过去会用NULL(预处理变量)来给指针赋值(变量在头文件ctsdlib中定义),它的值就是0。

​ 预处理器运行于编译之前,预处理变量不属于命名空间std,它由预处理器负责管理,所以我们能直接使用,而无需在前面加std::。

​ 当用一个预处理变量时,预处理器会自动将它替换为实际值。所以用NULL初始化指针和用0来初始化是一样的。

​ tips:新标准下,尽量使用nullstr,避免使用NULL。

赋值和指针

​ 指针和引用都能提供对其他对象的间接访问,然而在具体实现细节二者有很大的不同,最大的不同就是引用本身不是对象。一旦定义了一个引用,就无法令其再绑定其他对象,之后每次使用这个引用都是访问最初绑定的那个对象。

​ 指针和它存放的地址之间就没这种限制,给指针赋值就是令其存放一个新的地址,从而指向一个新的对象。

其他指针操作

​ 只要指针拥有一个合法值,就能将它用在条件表达式中,此时如果指针值是0,条件就为false,其他则为true(非0即为真)。

​ 对应两个类型相同的合法指针,可以用相等操作符(==)或不等操作符(!=)来进行比较,结果为布尔值。

void *指针

​ void*指针是一种特殊的指针类型,可以用于存放任意对象的地址。跟普通指针不同的是,我们不知道该地址中到底是什么类型的对象。

​ 利用void*能做的事比较有限:

  • 拿它跟别的指针进行比较
  • 作为函数的输入输出
  • 赋给另一个void*指针

​ 局限性:不能操作void*指针所指向的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定在这个对象能做哪些操作。

2.3.3 理解复合类型的声明

​ 变量的定义包括一个基本数据类型和一组声明符。 在同一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式可以不同。也就是说,一条定义语句可能定义出不同类型的变量:

1
int i = 1, *p = &i, &r = i;		//i是int型的数,p是int型指针,r是int型引用。

​ 涉及指针或引用的声明,一般有两种写法:

  1. 把修饰符和变量标识符写在一起,这种形式着重强调变量具有复合类型。

    1
    int *p1, *p2;	//p1和p2都是指向int的指针
  2. 把修饰符和类型名写在一起, 这种形式着重强调本次声明定义了一种复合类型:

    1
    2
    3
    4
    int* p1;
    int* p2;
    //注意,假如这么写,那p2就不是指向int的指针了,而是普通的int型数了
    int* p1, p2;

指向指针的指针

​ 一般来说,声明符中修饰符的个数并没有限制。以指针为例,指针也是一个对象,在内存中也拥有自己的地址,假如另一个指针指向前一个指针的地址,我们成这个指针是指向指针的指针

​ 通过的个数可以区分指针的级别。\*表示指向指针的指针,***表示指向指针的指针的指针。

​ 解引用指向指针的指针,会得到一个指针,再解一次即可得到指针的所指对象。

1
2
3
4
5
6
int i = 1;
int *p = &i; //指向一个int型
int **pp = &p; //指向一个int型的指针
//解引用
std::cout << *p; //得到i
std::cout << **p; //也能得到i

指向指针的引用

​ 引用本身不是一个对象,所以不能定义指向引用的指针,但指针是对象,存在对指针的引用。

1
2
3
4
5
6
int i = 10;
int *p; // p是一个int型指针
int *&r = p; // r是一个对指针p的引用

r = &i; // r引用了一个指针, 所以给r赋值就是令p指向i
*r = 0; // 解引用r得到i,也就是p指向的对象,将i的值改为0

​ 要理解r的类型是什么,可以从右向左阅读 r 的定义。离变量近的符号(上面例子中 &r 的符号&)对变量的类型有最直接的影响,所以 r 是一个引用。声明符的其他部分确定 r 引用的类型是什么,例子中 * 说明 r 引用的是一个指针。最后声明的基本数据类型部分指出 r 引用了一个 int 指针。

2.4 const限定符

​ const 对象一旦创建后,其值不能再改变,试图修改其大小都会引发错误。 所以 const 对象必须初始化。

初始化和 const

​ 对象的类型决定了其上的操作,const 类型跟其他类型的区别在于不能做改变其内容的操作。

​ 用 const 对象去初始化也是允许的,因为拷贝一个对象的值并不会改变它,一旦拷贝完成,新的对象就和原来的对象没什么关系了。

默认状态下,const 对象仅再文件内有效

​ 默认情况下,const对象被限定为文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。

​ tips:如果想在多个文件之间共享 const对象,必须在变量的定义之前加 extern 关键字。

2.4.1 const的引用

​ ”对const 的引用“,通常简称为 ”常量引用“。

​ 可以把引用绑定到一个 const 对象上,我们称之为 对常量的引用。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。

初始化和对const的引用

​ 一般来说,引用的类型必须与其所引用对象的类型一致,但是有例外:

  • 在初始化常量引用时允许用任意表达式座位初始值,只要该表达式的结果额能转换成引用类型即可。
  • 允许为一个常量引用绑定非常量的对象、字面值,甚至一个表达式。

2.4.2 指针和const

指向常量的指针不能用于改变其所指对象的值,但没有规定那个对象的值不能通过其他其他途径改变。

const指针

​ 允许指针本身定义为常量,常量指针必须初始化,一旦初始化完成,它的值(也就是存放在指针中的那个地址)就不能再改变了。把*放在const关键字之间用于说明指针是一个常量,这样写隐含一层含义,即不变的是指针本身的值而不是指向的那个值。

1
2
int errNum = 0;
int *const currErr = &errNum; //currErr将一直指向errNum

如何快速确定声明的含义:

从右向左阅读,上例中,离currErr最近的是const,说明currErr是一个常量对象,对象的类型由声明符的其余部分确定。声明符的下个符合是*,表面currErr是一个常量指针。

2.4.3 顶层const(未理解)

​ 指针本身是一个对象,它又可以指向另一个对象。所以指针本身是不是常量以及指针所指的是不是一个常量是两个相互独立的问题。用名词顶层const(top-level const)表示指针本身是一个常量,而名词底层 const(low-level const)表示指针所指的对象是一个常量。

​ 顶层 const 可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。

​ 底层 const 则与指针和引用等复合类型的基本类型部分有关。特殊的是,指针类型既可以是顶层const也可以是底层const,这一点和其他类型相比比较明显。

2.4.4 constexpr 和常量表达式

常量表达式(const expression) 是指值不会改变并且在编译过程中就能得到计算结果的表达式。

​ 字面值属于常量表达式,用常量表达式初始化的 const 对象也是常量表达式。

​ 一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定。

1
2
const int max_files = 10;		//max_files是常量表达式
const int limit = max_files + 1; //limit 是常量表达式

*constexpr 变量 *

​ C++11 新标准规定,允许将变量声明为 constexpr 类型以便由编译器来验证变量的值是否是一个常量表达式。声明为 constexpr 的变量一定是一个常量,而且必须用常量表达式初始化。

1
2
constexpr int mf = 20;	//20是常量表达式
constexpr int limit = mf + 1; //mf + 1 是常量表达式

字面值类型

​ 常量表达式的值需要在编译时就得到计算,因此声明constexpr时用到的类型必须有所限制。我们称这些比较简单,值也显而易见,容易得到的类型为“字面值类型”。

​ 算术类型、引用和指针都属于字面值类型。自定义类、IO库、string类型则不属于字面值类型,也不能定义成constexpr。

​ 尽管指针和引用都能定义成 constexpr, 但它们的初始值却受到严格限制。一个 constexpr 指针的初始值必须是 nullptr 或者0,或者是存储于某个固定地址中的对象。

*指针和 constexpr *

​ 在 constexpr 声明中如果定义了一个指针,限定符 constexpr 仅对指针有效,与指针所指的对象无关:

1
2
const int *p = nullptr;		//p是一个指向整型常量的指针
constexpr int *q = nullptr; //q是一个指向整数的常量指针

​ constexpr 把它所定义的对象置为了顶层 const。

2.5 处理类型

2.5.1 类型别名

类型别名(type alias) 是一个名字,是讴歌类型的同义词。使用类型别名能让复杂的类型名字变得简单明了,易于理解和使用。

定义类型别名的方法:

  • 传统方法使用关键字 typedef

    1
    2
    typedef double wages;	//wages 是 double 的同义词
    typedef wages base, *p; //base 是 double 的同义词, p是 double* 的同义词
  • 新标准规定一种新的方法,使用别名声明来定义类型的别名:

    1
    using SI = Sales_item; 	//SI 是 Sales_item 的同义词

类型别名的使用:

1
2
wages hourly, weekly;	//等价于 double hourly,weekly;
SI item; // 等价于 Sales_item item

指针、常量、和类型别名

​ 如果某个类型别名指代的是复合类型或者常量,那么把它用到声明语句里就会产生意想不到的后果。

1
2
3
typedef char *pstring;
const pstring cstr = 0; //cstr 是指向 char 的常量指针
const pstring *ps; // ps 是一个指针,它的对象是指向 char 的常量

2.5.2 auto类型说明符

​ 编程时需要把表达式的值赋给变量,这就要求在声明变量的时候知道表达式的类型,但这并非易事。

​ C++11新标准引入了 auto 类型说明符,用它就能让编译器代替我们去分析表达式所属的类型。显然,auto定义的变量必须有初始值:

1
2
//由val1 和 val2 相加的结果决定 item 的类型
auto item = val1 + val2;

复合类型、常量和 auto

​ 编译器推断出来的 auto 类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更复合初始规则。

2.5.3 decltype 类型指示符

​ 有时候,希望从表达式的类型推断出要定义的变量的类型,但又不想用该表达式的值初始化变量。为了满足这一要求,C++11 新标准引入了第二种类型说明符 decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。

1
decltype(f()) sum = x;		// sum 的类型就是函数f的返回类型

2.6 自定义数据结构

​ C++语言允许用户以类的形式自定义数据类型,而库类型 string、istream、ostream等也都是以类的形式定义的。

2.6.1 定义 Sales_data 类型

​ 我们以关键字 struct 开始,紧跟着类名和类体,来定义类。

1
2
3
4
5
struct Sales_data{
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};

​ C++11新标准规定,可以为数据成员提供一个类内初始值,创建对象时,类内初始值将用于初始化数据成员。没有初始化的成员将被默认初始化。

第二章 字符串、向量和数组

3.1 命名空间的 using 声明

​ std::cin 表示从标准输入中读取内容,作用域操作符(::)的含义是:编译器应从操作符左侧名字的作用域中寻找右侧那个名字。

​ 上面的方法比较繁琐,而使用 using声明 会更加简单安全。有了 using 声明就无需专门的前缀,异能使用所需的名字。使用形式如下:

1
using namespace::name;

​ 一旦使用了如此声明,就可以直接访问命名空间中的名字:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
// using声明,当我们使用名字 cin 时,从命名空间 std 中获取它
using std::cin;
int main()
{
int i;
cin >> i;
cout << i; //错误,没有对于的 using 声明,必须使用完整的名字
std::cout << i; //正确
return 0;
}

每个名字都需要独立的 using 声明

​ 按规定,每个using 声明引入命名空间中的一个成员,可以把要用到的标准库中的名字都以 using 声明的形式表示出来。

1
2
3
4
5
6
7
8
9
10
# include <iostream>
// 通过下列 using 声明,我们可以使用标准库中的名字
using std::cin;
using std::count; using std::endl;
int main()
{
cout << "Enter two numbers:" << endl;
……
return 0;
}

​ 上面程序中,我们一开始对 cin,cout和 endl 用 using 进行声明,这意味着我们不用再添加 std::形式的前缀就能直接使用它们。

头文件不应包含 using 声明

​ 位于头文件的代码一般来说不应该使用 using 声明。因为头文件的内容会拷贝到所有引用它的文件中去。如果头文件中有某个 using 声明,那么每个使用了该文件的文件就都会有这个声明。

3.2 标准库类型 string

3.2.1 定义和初始化 string对象

​ 初始化 string 对象的方式

1
2
3
4
5
string s1			默认初始化,s1 是一个空串
string s2(s1) s2是s1的副本
string s2 = s1 等价s2(s1)
string s3("value") s3 是字面量“value”的副本
string s4(n, 'c') 把s4初始化为连续n个字符 c 组成的串

直接初始化和拷贝初始化

如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化,编译器把等号有边界的初始值拷贝到新创建的对象中去。如果不适用等号,则执行的是直接初始化

3.2.2 string对象上的操作

读写 string 对象

​ 使用 IO 操作符读写 string 对象:

1
2
3
4
5
6
int main(){
string s;
cin >> s;
cout << s << endl;
return 0;
}

​ 在执行读取操作时,string 对象会自动忽略开头的空白(即空格符、换行符、制表符等),并从第一个真正的字符开始读起,直到遇到下一个空白为止。

​ 如果上述程序运行后,输入” Hello World “,则会输出”Hello”。

读取未知数量的 string 对象

1
2
3
4
5
6
int main(){
string world;
while(cin >> word) //反复读取,直至到达文件末尾
cout << world << endl; // 逐个输出单词,每个单词后面紧跟一个换行
return 0;
}

​ while 负责读取时检测流的情况,如果流有效,就执行 while 语句内部逻辑。

使用 getline 读取一整行

​ 假如希望在最终得到的字符串中保留输入时的空白符,可以使用 getline 函数代替原来的 >> 运算符。getline 函数的参数是一个输入流和一个 string 对象。函数从给定的输入流中读入内容,知道遇到换行符为止,然后把所读到的内容存入那个 string 对象中去(不存换行符)。

1
2
string  line;
getline(cin, line)

string 的 empty 和 size 操作

1
2
3
string line = "hello";
if (!line.empty())
……

​ 通过点操作,就能调用empty函数,因为empty也是 string 的一个成员函数

size 函数返回 string 对象的长度,使用方法跟 empty 类似,通过点即可调用。

string::size_type 类型

​ size 函数返回的是一个 string::size_type 类型的值。

​ string::size是一个无符号类型的值,而且足够存放下任何 string 对象的大小。所有用于存放 string 类的 size 函数返回值的变量,都应该是 string::size_type 类型的。

​ C++11 新标准中,允许编译器通过 auto 或者 decltype 来推动变量的类型:

1
auto len = line.size(); 	// len 的类型是 string::size_type

​ 如果表达式中混用了带符号和无符号数,将可能产生意想不到的结果,例如:假设 n 是一个具有负值的 int,则 表达式 s.size() < n 的判断结果几乎肯定是 true,因为负值 n 会自动转换成一个比较大的无符号值。

注意:如果一条表达式中已经有了 size()函数就不要再使用 int 了,这样可以避免混用 int 和 unsigned 可能带来的问题

比较 string 对象

  1. ​ 如果两个 string 对象的长度不同,而且较短 string 对象的每个字符都与较长 string 对象对应位置上的字符相同,就说较短 string 对象小于较长 string 对象。
  2. ​ 如果两个 string 对象再某些对应位置上不一致,则 string 对象比较的结果其实是 string 对象中第一对相异字符比较的结果。

字面值和 string 对象相加

​ 当把 string 对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)两侧的运算对象至少有一个是 string :

3.2.3 处理 string 对象中的字符

使用基于范围的 for 语句,处理每个字符

基本语法形式:

1
2
for (declaration : expression)
statement

例子:把string对象中的每一个字符输出来

1
2
3
string str("Hello world");
for(auto c : str)
cout << c << endl;

3.3 标准库类型 vector

​ 标准库类型 vector 表示对象的集合,其中所有对象的类型都相同。集合中的每个对象都有一个与之对应的索引,索引用于访问对象。vector 也被成为容器。

​ 要使用 vector ,必须包含适当的头文件。

1
2
#include <vector>
using std:: vecotor;

​ C++语言既有类模板,也有函数模板,其中 vector 是一个类模板。

​ 模板本身不是类或者函数,相反,可以将模板看作编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化,当使用模板时,需要指出编译器应把类或函数实例化成何种类型。

​ 对于类模板,我们提供一些额外信息来指定模板到底实例化成什么样的类,需要提供哪些信息由模板决定。

​ 提供信息的方式:在模板名字后面跟一对尖括号,在括号内放上信息。

例子:

1
2
vector<int> ivec;		//ivec 保存 int 类型的对象
vector<Sales_item> Sales_vec; //Sales_vec 保存Sales_item 类型的对象

​ vector 能容纳绝大多数类型的对象作为其元素,但不存在包含引用的 vector(因为引用不是对象)。

3.3.1 定义和初始化 vector对象

3.4 迭代器介绍

3.4.1 使用迭代器

​ 和指针不一样的是,获取迭代器不是使用取地址符,有的迭代器的类型同时拥有返回迭代器的成员。比如,这些类型拥有名为 beginend 的成员:

1
2
// b 表示 v 的第一个元素,e 表示 v 尾元素的下一个位置
auto b = v.begin(), e = v.end();

​ end 成员返回的迭代器通常被称为 尾后迭代器,该迭代器指示的是容器的一个本不存在的 尾后 元素。

​ 特殊情况下,如果容器为空,则 begin 和 end 返回的是同一个迭代器,都是尾后迭代器。

迭代器运算符

下面是标准容器迭代器的运算符:

1
2
3
4
5
*iter				// 返回迭代器 iter 所指元素的引用
iter->mem // 解引用 iter 并获取该元素的名
++iter // 令 iter 指示容器的下一个元素
--iter // 令 iter 指示容器的上一个元素
iter1 == iter2 // 判断两个迭代器是否相等,如果两个迭代器指示的是同一个元素或者它们是同一个容器的尾后迭代器,则相等

​ 和指针类似,也能通过解引用迭代器来获取它所指的元素,执行解引用的迭代器必须合法并确实指向某个元素。试图解引用一个非法的迭代器或者尾后迭代器都是未被定义的行为。

迭代器类型

​ 一般来说我们不知道(也是无须知道)迭代器的精确类型。就像不知道 string 和 vector 的 size_type 成员一样。实际上,那些拥有迭代器的标准库使用 iterator 和 const_iterator 来表示迭代器的类型。

1
2
3
4
vector<int>::iterator it;		// it 能读写 vector<int> 的元素
string::iterator it2; // it2 能读写 string 对象中的字符
vector<int>::const_iterator it3; // it3 只能不能读写
string::const_iterator it4; // it4 只能不能读写

begin 和 end 运算符

​ begin 和 end 返回的具体类型由对象是否是常量决定,常量则返回 const_iterator,非常量则返回iterator:

1
2
3
4
vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); // it1 类型是 vector<int>::iterator
auto it2 = v.begin(); // it2 类型是 vector<int>::const_iterator

​ 如果指定要返回常量类型,可以使用 cbegin 和 cend;

结合解引用和成员访问操作

​ 解引用迭代器可获得迭代器所指的对象,如果该对象是一个类,而且想进一步访问它的成员,可这样操作:

1
(*it).empty()		// 假设 it 指向字符串,想要判断字符串是否为空

​ 为了简化,C++ 语言定义了箭头运算符(->)。箭头运算符把解引用和成员访问结合在一起也就是说,it->men 和 (*it).men 等价。

3.4.2 迭代器运算

3.5 数组

​ 与 vector 比较:

  • 相同点:作为存放类型相同的对象的容器。

  • 不同点:数组的大小确定不变,不能随意向数组增加元素,虽然性能较好,但缺少了一些灵活性。

    tips:如果不确定元素的准确个数,请使用 vector。

3.5.1 定义和初始化数组

​ 定义数组的时候必须指定数组的类型,不允许用 auto 关键字由初始值的列表推断类型。另外和 vector 一样,数组的元素应为对象,所以不存在引用的数组。

不允许拷贝和赋值

​ 不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。

理解复杂的数组声明

1
2
3
4
int *ptrs[10];			// ptrs 是含有10个整数指针的数组
int *refs[10] = /* ?*/; // 错误,不存在引用的数组
int (*Parray)[10] = &arr; // Parray 指向一个含有10个整数的数组
int (&arrRef)[10] = arr; // arrRef 引用一个含有10个整数的数组

​ 理解数组声明的含义,最好的方法是从数组的名字开始,按照由内向外的顺序阅读。

​ 例如,对于上面 Parray 来说,*Parray 意味着 Parray 是一个指针,接下来观察右边,知道 Parray 是一个指向大小为10的数组的指针,最后管擦左边,知道数组中的元素是 int。最终知道,Parray 是一个指针,它指向一个包含10个元素的 int 数组。

3.5.2 访问数组元素

检查下标的值

​ 下标应该大于等于0而且小于数组的大小。