更新于 2020.11.1
C++ Primer Note
Chapter 1:开始
1.1 编译、运行程序
-
大多数系统中,
main
函数的返回值用来指示程序运行的状态,如果返回0
则表示成功,返回其他值一般用来指示遇到的错误。 -
**编译:**运行GUN编译器编译
proj1.cpp
文件,并将可执行文件命名为proj1
的命令。
g++ -o proj1 proj1.cpp
- 查看运行状态
echo %ERRORLEVEL%
1.2 初识输入输出
- 标准库:
iostream
- 标准输入:
cin
;标准输出:cout
;标准错误:cerr
;一般信息:clog
- 使用标准输入输出的加法计算程序:
#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
对象,右侧是要打印的值。std::cout << "Enter two numbers:" << std::endl;
等价于(std::cout << "Enter two numbers:") << std::endl;
-
endl
被称为操纵符。 -
std
是标准库的命名空间 (namespace)。必须使用作用域运算符::
来指出项实用定义在某一个命名空间的函数名。简洁的写法可以使用using std::cin
来将声明,当我们使用cin
时,从std
中来获取,但是这样需要对cout
等其他的也进行相同的声明。using namespace std
可以一步到位,对std
中的所有函数名进行声明。 -
输入运算符:
>>
与输出类似。std::cin >> v1 >> v2;
等价于(std::cin >> v1) >> v2;
等价于:
std::cin >> v1;
std::cin >> v2;
- **文件结束符:**在 Windows 系统中,输入文件结束符的方法是敲
Ctrl + Z
,然后再按Enter
或Return
。在 UNIX 系统中,是输入Ctrl D
1.5 类简介
在 C++ 中,我们通过定义一个**类(class)**来定义自己的数据结构。一个类定义了一个类型,以及与其关联的一组操作。每个类定义了一个新的类型
-
文件重定向
假定一个程序是
addItems.exe
,文件重定向机制允许我们将标准输入输出与文件名对应起来
addItems <infile>outfile
- **成员函数:**例如
book.isbn()
中的isbn()
就是成员函数,有时也称为方法。点运算符.
只能用于类类型的对象,其左侧必须是一个类类型的对象,右侧运算对象必须是该类型的一个成员。
Chapter 2:变量和基本类型
2.1 基本内置类型
C++ 中的基本数据类型包括算术类型和空类型(void),算术类型又分为整型和浮点型。
2.1.1 算术类型
类型 | 含义 | 最小尺寸 |
---|---|---|
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++ 规定:short <= int <= long <= long long (规定了每种整型的下界大小)。可寻址的最小内存块称为**“字节(byte)”,存储的基本单元称为“字(word)“**,大多数机器的字节有 8 比特,字由 32 或 64 比特构成。通常, float 以一个字(32 比特)来表示,double 以 2 个字(64 比特)来表示,long double 以 3 或 4 个字来表示,一般分别有 7 和 16 个有效位。
带符号类型和无符号类型
除布尔型和扩展的字符型,其他整型可划分为**带符号(signed)和无符号(unsigned)**两种。unsigned int 可以缩写为 unsigned。char 也有 signed char 和 unsigned char 两种类型,char 默认表示成哪种由编译器决定。unsigned char 可以表示 0 至 255 区间内的值,C++ 没有规定 signed char,约定正负区间应该平衡,常用的范围是 -128 至 127。
选择类型的经验准则:
- 当明确知晓数值不能为负时,选用无符号类型。
- 使用 int 执行整数运算,超出范围备选 long long。( short 一般太小,long 一般和 int 一样大)
- 算术表达式中不要使用 char 和 bool。如果需要使用不大的整数运算,需要明确指出 unsigned char 或 signed char。
- 执行浮点数运算选用 double。( float 精度常常不够,两个的计算代价也差不多)
2.1.2 类型转换
- 非布尔类型的算术值赋给布尔型时,0 则为 false,其他为 true
- 把浮点数赋给整数型时,仅保留小数前的整数部分
- 当给无符号类型赋超过范围的值,赋的是取模后的结果
- 给带符号类型赋超过范围的值,结果是未定义的
含有无符号类型的表达式
当表达式既有无符号数又有有符号数时,会把有符号数转换成无符号数进行运算。
2.2 变量
2.2.1 变量初始化
- 初始化和赋值是不同的两种概念,= 既有初始化的功能,也有赋值的功能
- 列表初始化使用
{ }
,例如int units_sold = { 0 };
这种初始化有一个特点,如果初始值存在信息丢失的风险,编译器会报错。 - 默认初始化:内置属性的值在函数体外会被初始化为 0 ,定义在函数体内则会不被初始化,变量值是未定义的,如果访问会引发错误(尝试了下不会报错,这个错误应该指的是程序运行错误)。每个类可以决定初始化对象的方式,可以设定拒绝未显示初始化的创建对象。
提示:未初始化变量引发运行时故障
未初始化的变量含有一个不确定的值,使用未初始化变量的值是一种错误的编程行为并且很难调试。
2.2.2 变量声明与定义的关系
添加关键字 extern
标记对一个变量的声明而非定义,声明不会分配内存。任何包含显示初始化的声明都还会是一个定义,在函数体内部,试图初始化一个有 extern 标记的变量将引发错误。
变量能且只能被定义一次,但可以声明很多次。如果在多个文件中使用同一个变量,就必须将声明和定义分离,此时,变量的定义必须出现在且只能出现在一个文件,而其他用到该变量的文件必须对其声明。
关键概念:静态类型
C++ 是静态类型语言,在编译阶段检查类型,其过程称为类型检查。对象的类型决定了能参与的运算,在 C++ 中,编译器需要检查数据类型是否支持要执行的运算。所以要求在使用变量前必须声明。
2.2.3 标识符
C++ 的标识符由字母、数字、下划线组成。必须以字母和下划线开头,对大小写敏感。用户自定义的标识符不能连续出现两个下划线,也不能以下划线紧接大写字母开头,函数体外的标识符不能以下划线开头。
变量命名规范
- 标识符要能体现实际含义
- 变量名一般用小写字母
- 类名一般大写字母开头
- 多个单词构成的标识符单词间应该有明显区分,如
student_loan
或studentLoan
2.2.4 名字的作用域
大多数作用域以花括号分隔,分为全局作用域和块作用域。建议:当第一次使用变量时再定义。内层作用域中能使用外层作用域定义的变量,也允许在内层作用域中重新定义变量。
2.3 符合类型(引用、指针)
2.3.1 引用
引用是为对象另外取一个名字,声明引用时必须被初始化。定义引用时,程序是将引用和它的初始值绑定在一起,这种绑定是不能被改变的。引用并非对象,相反的,它只是给一个已经存在的对象取了一个别名。
引用只能绑定对象,不能绑字面值或某个表达式的计算结果,绑定的对象的类型必须和自己声明的类型相同。
2.3.2 指针
&
在什么变量的时候是声明引用的意思,在其他时候是取地址符的意思。 *
在声明变量时是声明指针的意思,在其他是解引用符的意思。
建议:初始化所有指针
Void *指针可以存放任何对象的地址,是一种特殊的指针类型
指向指针的引用:
int i = 42;
int *p; //p 是一个 int 型指针
int *&r = p; //r 是一个对指针 p 的引用
2.4 const 限定符
const
可以对变量进行限定,使其不能被改变。const
对象必须进行初始化,初始值可以是任意复杂的表达式。
const int i = get_size(); //正确,运行时初始化
const int i = 42; //正确,编译时初始化
const int k; //错误
默认状态下,const 对象仅在文件内有效
如果想上其他变量一样,可以在文件中共享,必须得在 const 变量前都添加 extern 关键字,定义和什么都要加,这样仅需定义一次就可以了。
// file_1.cc 定义并初始化一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
// file_1.h 头文件
extern const int bufSize; //与 file_1.cc中定义的 bufSize 是同一个
2.4.1 const 的引用
const 对象也可以被引用绑定,称之为对常量的引用,对常量的引用不能修改它所绑定的对象:
const int ci = 1024;
const int &r1 = ci; //正确:引用及其对象都是常量
r1 = 42; //错误
int &r2 = ci; //错误:试图让一个非常量引用指向一个常量引用
初始化和对 const 的引用
前面提到,引用必须绑定相同类型的对象,但是又两个例外, 第一个是初始化常量引用时,允许用任意表达式作为初始值,只要该表达式能转换为引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是一个表达式。
int i = 42;
const int &r1 = i; //允许将 const int 绑定在普通 int 对象上。这样不允许修改r1,但是可以修改 i 来改变 r1 的值
const int &r2 = 42; //正确:r2 是一个常量引用
const int &r3 = r1 * 2; //正确:r3是一个常量引用,这个操作可以让 r3 恒等于 r1 的两倍,关系不能改变
int &r4 = r1 * 2; //错误,r4是一个普通非常量引用
对常量的引用可以做如下等价:
double dval = 3.14;
const int &r1 = dval; //这个等价于下面两行
const int temp = dval; //由双精度浮点数生成一个临时的整型变量
const int &r1 = temp; //让r1绑定这个临时的整型变量,dval 的改变不会改变 temp ,所以也不会改变 r1 的取值
当 r1 不是常量时,r1 也会绑定到临时量上,绑定到临时量并不能对 dval 进行改变,所以这么弄没有意义,所以 C++ 将这种行为归为非法。
2.4.2 指针和 const
被 const 声明的变量变成了常量对象,想要存放常量对象的地址,必须使用指向常量的指针。
const double dval = 3.14;
double *p1 = &dval; //错误:p1 是一个普通指针
const double *p2 = &dval; //正确:p2 是一个指向常量的指针
*p2 = 42; //错误,不能给 *p2赋值
前面提到,指针的类型必须与所指对象的类型一样。有两个例外,允许一个指向常量的指针指向一个非常量对象:
double val = 3.55;
int vi = 4;
p2 = &val; //正确,但不能通过 p2 改变 val 的值
p2 = &vi; //错误,变量类型还是得相同的
和常量引用一样,指向常量的指针没有规定其所指的对象也必须是常量,甚至指的对象都可以变,只是不能通过该指针改变所指对象的值。
const 指针
指针本身也是一种对象(引用不是),所以运行把指针本身也定义为常量,这就是常量指针,常量指针所指的对象是不允许改变的。常量指针必须初始化。
int num = 10;
int *const curErr = # // curErr 将一直指向 num ,不能改变
const double pi = 3.14;
const double *const pip = π // pip 是一个指向常量对象的常量指针
2.4.3 顶层 const
顶层 const表示指针本身是一个常量,底层 const表示指针所指的对象是一个常量。顶层 const 可以表示任意的对象是常量,底层 const 与指针和引用等符合类型有关。
底层 const 的限制不能忽视,执行对象的拷贝操作时,两个对象必须具有相同的底层 const 资格。
2.4.4 constexpr 和常量表达式
常量表达式是指在编译过程就能计算得到的结果,C++ 11 新规定允许将变量声明为constexpr来让编译器验证是否为常量表达式。
const int max_files = 20; //是常量表达式
const int limit = max_files + 1; //是常量表达式
int staff_size = 27; //不是
const int sz = get_size(); //不是
constexpr int sz = size(); //只有当 size 是一个 constexpr 函数时才是,只有这样才是正确的语句
一般来说,如果认定变量是一个常量表达式,最好声明为 constexpr
指针与 constexpr
const int *p = nullptr;
constexpr int *q = nullptr;
p 和 q 相差甚远,一个是指向常量的指针,一个是常量指针。constexpr 把其定义的对象置为了顶层 const。
2.5 处理类型
2.5.1 类型别名
两种方法可以实现类型别名,传统方法是使用 typedef
关键字,新规定使用别名声明来定义类型的别名,使用关键字 using
,类型别名和类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名。
typedef double wages;
typedef wages base, *p; //base 是 double 的同义词,p 是 double * 的同义词
using SI = Sales_item; //SI 是 Sales_item 的同义词
指针、常量和类型别名(注意!)
类型别名指代的是符合类型或常量时会有意想不到的后果。
typedef char *pstring; // pstring 等同于 char *, 声明的是一个 char 型指针
const pstring p; // p 是一个常量指针
const pstring *ps; // ps 是一个指针,它的对象是指向 char 的常量指针
const 是对给定类型的修饰,pstring 是指向 char 的指针,所以 const pstring 是指向 char 的常量指针,而非指向常量字符的指针。遇到一条使用了类型别名的声明语句时,人们往往会错误地尝试把类型别名替换成它本来的样子以理解该句的含义。const pstring 是指向 char 的常量指针,const 修饰的对象是指针,const char * 是指向 const char 的指针,const 修饰的对象是 char,这两个意思完全不同。
2.5.2 auto 类型说明符
auto
类型说明符时 c++ 11 新引入的,可以让编译器代替我们去分析表达式所属的类型,它通过初始值来推算变量的类型,所以 auto
定义的变量必须要有初始值。使用 auto
可以一条语句声明多个变量,但是所有变量的初始基本数据类型必须一样。
auto item = val1 + val2; //item初始化为 val1 和 val2 相加的结果
auto i = 0, *p = &i; //正确,i 是整数, p 是整型指针
auto sz = 0, pi = 3.14; //错误,sz 和 pi 类型不一致
符合类型、常量和 auto
使用引用其实是使用引用的对象,当使用引用当做 auto 的初始值时,编译器会自动以引用的对象的类型作为 auto 的类型。其次 auto 一般会忽视顶层 const ,底层 const 会被保留。如果希望 auto 类型是一个顶层 const ,需要明确指出。也可以将引用的类型设为 auto。设置一个类型为 auto 的引用时,顶层特性任然保留。
int i = 5, &s = i;
auto j = s; //j 是整型
const int ci = i,&cr = ci;
auto b = ci; //b 是一个整数(ci 的顶层特性会被忽略)
auto d = &i; //d 是一个整型指针(const在这里是顶层特性,被忽略)
auto e = &ci; //e 是一个指向整型常量的指针(const在这里是底层特性,被保留)
const auto f = ci; // ci 推断出来是 int ,f 是 const int
auto &g = ci; //g 是一个整型常量引用,绑定到 ci 上
auto &h = 42; //错误,不能将引用绑定到非常量
const auto &i = 42; //正确,可以将常量引用绑定到字面值
2.5.3 decltype 类型指示符
只推断类型,不赋初始值,c++ 11 引入了 decltype
说明符。并不实际调用函数 f ,而是使用当调用发生时 f 的返回值作为 sum 类型。decltype 处理顶层 const 和应用的方式不同。
decltype(f()) sum = x; //sum 的类型就是函数 f 的返回类型
const int ci = 0, &cj = ci;
decltype(ci) x = 0; //x 是 const int类型
decltype(cj) y = x; //y 是 const int&,y绑定到 x 上
decltype(cj) z; //错误,z 是引用,必须初始化
decltype 和引用
int i = 42, *p = &i, &r = i;
decltype(r + 0) b; //正确:加法结果是 int ,所以 b 是一个未初始化的 int
decltype(*p) c; //错误:c 是 int & ,必须初始化
r 是引用,所以 decltype® 的结果是一个引用类型,如果向上结果类型时 r 所指的类型,可以把 r 作为表达式的一部分,如 r + 0。另一方面,如果表达式是解引用操作,则 decltype 得到的是引用类型。
decltype 和 auto 的另一处重要区别是,decltype 的结果形式与表达式形式密切相关。
decltype((i)) d; // 错误,d 是 int &,必须初始化
decltype(i) e; //正确,e 是 int
切记:decltype((variable)) 双层括号的结果永远是引用,单层括号只有当 variable 本身是引用时才是
2.6 自定义数据结构
2.6.1 定义结构体类型
用struct
关键字来定义数据结构体。不建议把类体和变量声明写在一起。C++ 11 新规定中可以为数据成员提供一个类内初始值。没有初始值的成员将被默认初始化。
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
}; //分号必不可少
struct s { /* ...... */} a, *p; //不建议这么写,建议写成下面这样
struct s { /* ...... */}; //类体
s a, *p; //变量声明
2.6.3 编写自己的头文件
类通常被定义在头文件中,且类在头文件中的名字应该与类的名字一样。头文件通常包含那些只能被定义一次的实体,如类、const 和 constecpr 变量等。同时头文件通常也要使用到别的头文件,可能造成同一个头文件被多次直接或间接包含,所以有必要做适当处理,使其遇到多次包含的情况也能安全正常工作。头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明
预处理器概述
预处理器技术由 c 继承过来,是在编译前执行的一段程序,#include
就是一项预处理功能。这里要用到的是头文件保护符,头文件保护符依赖于预处理变量。预处理变量有两种状态:已定义和未定义。#define
把一个名字设为预处理变量,另外两个指令分别检查某个指定的预处理变量是否以定义。#ifdef
当且仅当已定义为真,#ifndef
当且叮当未定义为真时,#endif
前面的为真时运行到这。
#ifndef SALES_DATA_H //SALES_DATA_H 是 Sales_data.h 的头文件保护符,一般和头文件名相同
#define SALES_DATA_H //SALES_DATA_H 相当于是 Sales_data.h 是否已定义的 flag
#include <string>
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
#endif
预处理变量无视C++语言中关于作用域的规定。头文件即使没有被包含在任何其他头文件中也应该设置保护符。
Chapter 3:字符串、向量和数组
3.1 命名空间的 using 声明
使用 using
声明可以无需专门的前缀(例如命名空间 ::
)也能使用所需的名字,这个在前面说到过。注意:头文件一般不包含 using 什么,防止不经意的命名冲突问题。
3.2 标准库类型 string
标准库类型 string
表示可变长的字符序列,包含在 <string>
头文件中,string
定义在命名空间 std
中,所以要想简便实用,得使用 using std::string
或 using namespace std
。标准库类型对于一般应用场合有足够的效率
3.2.1 定义和初始化 string 对象
string 对象的初始化方法:()
是直接初始化,=
是拷贝初始化,有的两个都可以,有的不太方便拷贝初始化
string s1; //默认初始化,s1 是一个空串
string s2(s1); //s2 是 s1 的一个副本,直接初始化
string s2 = s1; //等价于 s2(s1) ,是一个副本,拷贝初始化
string s3("value"); //s3 是字面值 “value” 的副本,除了字面值最后那个空字符外
string s3 = "value"; //和上面的等价
string s4(n, 'c'); //把 s4 初始化为连续 n 个字符 c 组成的串,不太方便拷贝初始化
string s4 = string(n, 'c'); //强行拷贝初始化,等价于下面两行
string temp(n, 'c'); //先创一个临时的,再拷贝
string s4 = temp;
3.2.2 string 对象上的操作
string 对象上的操作:
语法 | 作用 |
---|---|
os << s | 将s写到输入流 os 当中,返回 os |
is >> s | 从 is 上读取字符串赋给 s ,字符串以空白分隔,返回 is |
getline(is, s) | 从 is 中读取一行赋给 s, 返回 is |
s.empty() | s 为空返回 true,否则返回 false |
s.size() | 返回 s 中字符个数 |
s[n] | 返回 s 中第 n 个字符的引用,位置 n 从 0 记起 |
s1 + s2 | 返回 s1 和 s2 连接后的结果 |
s1 = s2 | 用 s2 的副本代替 s1 中原来的字符 |
s1 == s2 | 如果两字符串完全相等,返回 true;对大小写敏感 |
s1 != s2 | |
<, <=, >, >= | 利用字符在字典中的顺序进行比较,对大小写敏感 |
读写 string 对象
string s;
cin >> s; //忽略开头的空白,从第一个字符开始存,直到空白结束
cout << s <<endl;
string s1, s2;
cin >> s1 >> s2; //先输入到 s1 ,再输入到 s2;
cout << s1 << s2 << endl; //写输出 s1 ,再输出 s2;
使用 getline 读取一整行
string line;
getline(cin, line); // cin 是标准流输入,line 是 string 对象,这样读可以保留空格符号,读到换行符才结束,换行符不存
触发函数返回的那个换行符实际上被丢掉了,得到的 string 对象不包含换行符
string::size_type 类型
s.size()
返回的是一个 size_type
类型的值,它是一个无符号类型的值而且足够放下任何 string 对象的大小。所有用于存放 string 类的 size 函数返回值变量都应该是 string::size_type 类型的。因为是无符号数,所以表达式中要是有有符号数可能会出现意想不到的错误。
auto len = line.size(); //len 的类型是 string::size_type
比较 string 对象
依次取字符进行比较,比较的标准是字符在字典中的顺序。
注意:C++ 语言中的字符串字面值并不是标准库类型 string 的对象。切记,字符串字面值类型和 string 是不同的类型。
3.2.3 处理 string 对象中的字符
在 cctype
头文件中定义了一部分处理字符的函数。
函数名 | 作用 |
---|---|
isalnum© | 当 c 为字母或数字时为真 |
isalpha© | 字母 |
iscntrl© | 控制字符 |
isdigit© | 数字 |
isgraph© | 不是空格但可以打印 |
islower© | 小写字母 |
isprint© | 可以打印,具有可视形式 |
ispunct© | 标点符号 |
isspace© | 空白(空格,横向制表符,纵向制表符,回车,换行,进纸) |
isupper© | 大写字母 |
isxdigit© | 16进制数 |
tolower© | 将字母变成小写 |
toupper© | 将字母变成大写 |
建议使用 C++ 版本的 C 标准库头文件
C 中头文件形如 name.h
,C++ 则将这些文件命名为 cname
,去掉了 .h
,c 表面是一个 C 语言的头文件。cname
头文件定义的名字输出 std 命名空间,name.h
则不是。
使用基于范围的 for 语句
string str("some string");
for(auto c : str) //对于 str 中的每个字符
cout << c << endl; //输出每个字符,这样对 c 操作是改变不了 str 的,要想该改变的话得用引用
for(suto &c :str) //要用引用声明 C
c = toupper(c);
访问部分字符
可以使用下标[]
也可以使用迭代器,迭代器部分在第九章讲。下标运算符 []
接受的是 string::size_type
类型的值,返回值是对应位置的引用。逻辑与运算符 && , C++ 规定,只有左边为 True 才会检查右边是否为真。C++ 标准并不要求检查下标是否合法。一但使用了超出范围的下标,会有不可预知的后果。
3.3 标准库类型 vector
标准库类型 vertor
表示对象的集合,其中所有对象类型必须相同,他也常被称为**“容器”。使用时必须包含头文件 <vector>
。vector 是 C++ 的一个类模板**。模板本身不是类,可以将模板看作编译器生成类的一份说明。编译器根据模板创建类的过程称为实例化 (instantiation),使用时需指出实例化为何种类型。
vector<int> ivec;
vector<vector<string>> file; //老式编译器需要右边>>中有一个空格,变成> >
3.3.1 定义和初始化 vector 对象
vector<T> v1; //v1 是一个空vector,他潜在的元素是 T 类型,执行默认初始化
vector<T> v2(v1); //v2包含v1所有元素的副本
vector<T> v2 = v1; //同上
vector<T> v3(n, val); //v3 包含 n 个重复的元素,每个值都是 val
vector<T> v4(n); //v4 包含 n 个元素,每个默认初始化
vector<T> v5{a,b,c,d,e}; //v5 包含 5 个元素,依次是 a,b,c,...
vector<T> v5 = {a,b,c,d,e}; //同上,列表初始化v5
3.3.2 向 vector 对象中添加元素
vector 的成员函数 push_back
用于向其中添加元素。push_back
负责将一个值当成 vector 对象的尾元素放在 vector 对象的尾端。
vector<int> v2;
for(int i = 0; i != 100; i++){
v2.push_back(i); //依次将值放入 v2 尾端
}
关键概念:vector 对象能高效增长
C++标准要求 vector 应该能在运行时高效快速的添加元素,在定义 vector 对象时没必要设定大小,否则性能可能更差(除了所有初始值都一样的时候)。更有效的方法是先创建一个空的 vector 对象,再在运行时添加具体值。这一点与 C 或者 Java 在开始时最好指定容量不同。
3.3.3 其他 vector 操作
函数名 | 功能 |
---|---|
v.empty() | v 空时返回真 |
v.size() | 返回v元素个数 |
v.push_back(t) | 向v尾端添加一个值为t的元素 |
v[n] | 返回v中位置n的引用 |
v1 = v2 | 用v2元素的拷贝替换v1的元素 |
v1 = {a,b,c…} | 用列表中的元素替换v1中元素 |
v1 == v2 | 相同时返回true |
v1 != v2 | |
<, <=, >, >= | 以字典顺序进行比较 |
size()
的返回值类型是由 vector 定义的 size_type 类型。是 vector<int>::size_type
而不是 vector::size_type
。不能通过 vector 对象的下标来添加元素,只能对已存在的元素执行下标操作,元素的下标也为 size_type 类型。缓冲区溢出指的就是下标访问到不存在的元素。Tips:尽量使用 for 语句可以确保下标合法。
3.4 迭代器(iterator)介绍
所有标准库容器都可以使用迭代器,但只有少数几个可以使用下标运算符。类似与指针类型,迭代器也提供了对对象的间接访问。有效的迭代器指向某个元素,或最后一个元素的下一个位置,其他都无效。
3.4.1 使用迭代器
容器都有 begin
和 end
的成员,一个返回第一个元素的迭代器,一个返回最后一个元素的下一个位置的迭代器。end 成员返回的迭代器常被称为尾后迭代器。容器为空时,两个返回的是同一个迭代器。一般来说,我们不清楚(不在意)迭代器准确的类型是什么,所以使用 auto 来定义迭代器对象。
auto b = v.begin(), c = v.end();
迭代器运算符
运算符 | 功能 |
---|---|
*iter | 返回迭代器iter所指元素的引用 |
iter->mem | 解引用iter,并获取该元素名为mem的成员,等价于(*iter).mem |
++iter | 令iter指向容器中的下一个元素 |
–iter | 令iter指向容器中的上一个元素 |
iter1 == iter2 | |
iter1 != iter2 |
不能对尾迭代器进行 ++ 操作或解引用操作。
迭代器类型
拥有迭代器的标准库类型使用 iterator 和 const_iterator 来表示迭代器的类型:
vector<int>::iterator it; //it能读写vector<int>的元素
string::iterator it2; //能读写string对象中的字符
vector<int>::const_iterator it3; //只读不能写
string::const_iterator it4; //只读不能写
begin 和 end 运算符
返回的类型由对象是否为常量决定,但是 C++ 11引入了新的函数 cbegin
和 cend
:
vector<int> v;
const vector<int> cv;
auto v1 = v.begin(); //v1 是 vector<int>::iterator 类型
auto v2 = cv.begin(); //v2 是 vector<int>::const_iterator 类型
auto v3 = v.cbegin(); //v1 是 vector<int>::const_iterator 类型
解引用和成员访问操作
对迭代器解引用可以得到所指对象,该对象可能还有成员函数,所以访问其成员函数的方法可以先解引用再访问,为了简化该操作的表达式,C++ 定义了箭头运算符(->):
(*it).empty(); //括号必不可少
it->empty(); //和上面的等价
某些对 vector 对象的操作会使迭代器失效
不能在范围 for 循环中向 vector 对象添加元素,任何一个可能改变 vector 对象容量的操作,如 push_back 都会使该 vector 对象的迭代器失效。谨记:凡是使用了迭代器的循环体,都不要向迭代器所属的容量添加元素
3.4.2 迭代器运算
运算 | 功能 |
---|---|
iter + n | 向后移动n个位置 |
iter - n | 向前移动n个位置 |
iter1 += n | |
iter1 -= n | |
iter1 - iter2 | 相减结果是两个迭代器之间的距离 |
>, >=, <, <= | 比较两个迭代器所指位置,必须为同一容器 |
两迭代器相减结果的类型是 difference_type
,是一个带符号整形数。
3.5 数组
数组是一种类似与 vector 的数据结构,两者在灵活性和性能的权衡上有所不同,数组也是存放类型相同的对象的容器,通过位置进行访问。数组的大小确定不变,不能随意增加元素,如果不清楚元素确切个数,请使用vector。
3.5.1 定义和初始化内置数组
数组的声明行如 a[d]
,a 是名字, d 是维度。维度必须在编译时已知,所以必须是一个常量表达式。默认情况下,数组的元素被默认初始化,定义时必须指明类型,不能 auto,也不存在引用的数组。
unsigned cnt = 42; //非常量表达式
constexpr unsigned sz = 42; //常量表达式
int arr[10]; //正确
int *parr[sz]; //正确,整型指针的数组
int bad[cnt]; //错误,cnt 不是常量表达式
显示初始化数组元素
可进行列表初始化,此时允许忽略数组的维数。不允许拷贝赋值。
const unsigned int sz = 3;
int ia1[sz] = {0,1,2}; //正确
int a2[] = {0,1,2}; //正确,数组含有三个元素
int a3[5] = {0,1,2}; //a3 = {0,1,2,0,0}
string a4[3] = {"hi", "bye"}; //a4 = {"hi", "bye", ""}
int a5[2] = {0,1,2}; //错误,列表元素超过了2
int a[] = a2; //错误,不能拷贝赋值
复杂的数组声明
int *ptrs[10]; //ptrs 是一个含有10个整型指针的数组,从右往左读
int &refs[10] = /* */; //错误,不存在引用的数组
int (*Parray)[10] = &arr; //Parray 指向含有10个整数的数组,从括号里往外读
int (&arrRef)[10] = arr; //arrRef 引用一个含有10个整数的数组
int *(&arry)[10] = ptrs; //arry 是数组的引用,该数组含有10个指针
3.5.2 访问数组元素
数组的元素使用下标来进行访问,下标是 size_t
类型的数,这是一种机器相关的无符号类型,他足够的大以便能够表示内存中任意大小的对象,在 cstddef
头文件中定义了这个类型。遍历数组元素的最好办法是使用范围 for 语句。
unsigned scores[11] = {};
for(auto i : scores)
count << i << " ";
cout << endl;
注意:大多数常见的安全问题都源于缓冲区溢出错误。当数组或其他类似的数据结构的下标越界并试图访问飞叉内存区域时会产生此类错误。
3.5.3 指针和数组
在 C++ 语言中,使用数组的时候编译器一般会把它转换为指针,指向数组的第一个元素。
int ia[] = {1,2,3};
auto ia2(ia); //ia2 是一个整型指针,指向 ia 的第一个元素
ia2 = 42; //错误
指针也是迭代器
指针也拥有迭代器的运算功能,使用指针也能遍历数组中的元素,例如一个 a[10] ,他的首元素地址为 &a[0],尾元素后一个地址为 &a[10],可以如下方式遍历数组。
int *end = &a[10];
for(int *start = &a[0]; start != end; start ++){
cout << *start << endl;
}
标准库函数 begin 和 end
C++ 11引入两个函数来得到数组收元素和尾元素后一个位置的指针,分别是 begin
和 end
。特别注意,指向尾元素下一个位置的指针不能进行赋值、解引用和递增操作
int ia[] = {0,1,2,3};
int *beg = begin(ia); //指向 ia 首元素
int *last = end(ia); //指向 ia 最后一个元素
指针运算
指针和迭代器一样能进行解引用、增增、比较、与整数相加减、两个指针相减等运算。指针加上一个数仍是指针。两个指针相减的数据类型是 ptrdiff_t
,定义在 cstddef
头文件中,是一个带符号的数。
int arr[5] = {1,2,3,4,5};
int *ip = arr; //等价于 int *ip = &arr[0]
int *ip2 = ip + 4; //ip2 指向 arr[4]
auto n = end(arr) - begin(arr); //n 是5,类型为 ptrdiff_t
**注意:内置的下标运算符所用的索引值不是无符号类型,这一点与 vector 和 string 不一样,p[-2] 可能是有效的,例如当 p 指向 a[2] 时,p[-2] 就是 a[0] **
3.5.4 C 风格字符串
尽管 C++ 支持 C 风格字符串,但在 C++ 程序中最好还是不要使用。C 风格字符串习惯将字符串存在字符数组中,并以空字符结束(’\0’)。
C 标准库 String 函数
C 中可操作字符串的函数定义在 cstring
头文件中。进行字符串比较时要使用 strcmp 函数,而不能之间 < 比较,因为p1, p2 其实都是指针。对大多数应用来说,使用 string 要比使用C风格字符串更安全
函数 | 功能 |
---|---|
strlen§ | 返回 p 的长度 |
strcmp(p1, p2) | 比较相等性,相等为0,p1>p2返回正数,否则返回负数 |
strcat(p1, p2) | 将 p2 赋到 p1 后面,返回 p1 |
strcpy(p1, p2) | 将 p2 拷贝到 p1,返回 p1 |
3.5.5 与旧代码的接口
- 允许使用以空字符结束的字符数组来初始化 string 对象,或为它赋值
- 在 string 对象的加法运算中,允许其中一个对象是以空字符结束的字符数组
- 无法直接只用 string 对象来代替 C 风格字符串。string 提供了一个成员函数
c_str()
来获得指向字符的指针,指针的类型是 const char* ,当 s 中的值改变时,该指针可能失效。
string s = "adadada";
char *str = s; //错误
const char *str = s.c_str(); //正确
- 可以使用数组来初始化 vector 对象,给出首位元素地址即可
int int_arr[] = {0,1,2,3,4,5};
vector<int> ivec(begin(int_arr), end(int_arr)); //第二个元素需要指向尾部的后一个位置
建议:尽量使用标准库类型而非数组
3.6 多维数组
严格来说 C++ 没有多维数组,常说的多维数组其实是数组的数组。对于二维数组,常把第一个维度称为行,第二个称为列。
多维数组初始化
int ia[3][4] = {
{0,1,2,3},
{4,5,6,7},
{8,9,10,11}
};
int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11}; //和上面的结果一样
int ia[3][4] = {{0},{4},{8}} //第一行是0,第二行是4,第三行是8
int ix[3][4] = {0,1,2,3}; //显示初始化第一行,其他默认初始化
多维数组和下标的引用
可以使用下标运算符来访问多维数组的元素,如果给的下标和维度一样多,则访问的对应位置的元素,如果小于维度,访问的是你个内层数组。
ia[2][3] = arr[0][0][0]; //访问的是一个元素
int (&row)[4] = ia[1]; //访问的是第2行的数组
使用范围 for 语句处理多维数组
size_t cnt = 0;
for (auto &row : ia) //不用引用将会被转成指针,造成编译无法通过
for(auto &col :row){ //如果不要该元素值,只访问的话可以不要引用
col = cnt;
cnt ++;
}
注意:使用范围 for 语句处理多维数组时,除了最内层循环,其他所有循环控制变量都应该是引用类型