少女祈祷中...

开场白

QAQ开学了,心血来潮立了一个flag,挑战六周读完《C++ Primer》。

是中文版,毕竟英文啃不动一点。

Emmmmm…

三分钟热度的我究竟能否完成呢?

拭目以待。

ο(=•ω<=)ρ⌒☆

Week 1: 潜龙勿用

变量和基本类型

包括算数类型(arithmetic type)空类型(void)

类型 含义 最小尺寸
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位有效数字

类型转换易错点

当出现 intunsigned int 做算术运算时,int 型会转化为无符号型,此时若该值为负数,则体现为这个负数加上无符号数的模。

1
2
3
unsigned u = 10;
int i = -42;
cout << u + i << endl; //若int占32位,输出4294967264

当出现unsigned int减去一个值时,无论该值是否为无符号型,结果均为非负。

1
2
3
unsigned u1 = 42 , u2 = 10;
cout << u1 - u2 << endl; //正确:输出32
cout << u2 - u1 << endl; //正确:但输出为取模后的值

转义序列

换行符 \n 横向制表符 \t 报警/响铃符 \a
纵向制表符 \v 退格符 \b 双引号 \ "
反斜线 \ \ 问号 ? 单引号 \ ‘’
回车符 \r 进纸符 \f

泛化转义序列

( 单 ’ \ ’ 后接八进制数字,’ \x ’ 后接十六进制数字 )

\7 响铃 \12 换行符 \40 空格
\0 空字符 \115 字符M \x4d 字符M

Tip: 反斜线跟着的八进制数字若超出3个,则只有前3个数字与\构成转义序列;而\x会用到之后的所有数字。

指定字面值类型

通过添加前缀和后缀,可以改变整型、浮点型、和字符型的字面值的默认类型。

字符和字符型字面值

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

整型字面值

后缀 最小匹配类型
u or U unsigned
l or L long
ll or LL long long

浮点型字面值

后缀 类型
f or F float
l or L long double
1
2
3
4
5
6
//举例
L'a' //宽字符型字面值
u8"hi!" //utf-8字符串字面值
42ULL //无符号整型字面值
1E-3F //单精度浮点型字面值
3.14159L //扩展精度浮点型字面值

truefalse是布尔类型的字面值;nullptr是指针字面值。

初始化

对int的四种初始化方式:

1
2
3
4
int a = 0;
int a = {0};
int a {0};
int a (0);

默认初始化:定义变量时没有指定初值

对于内置类型的变量未被初始化,值由位置决定。函数体之外的变量被初始化0,而内部将不被初始化,容易引发错误。

声明和定义

因为C++支持分离式编译(separate compilation)机制,将声明和定义区分开来。

声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。

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

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

Tip: 在函数体外给有extern标记的变量初始化,会被当成定义;而在函数体内,将引发错误

标志符定义易错点

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

变量命名规范

  • 变量名一般用小写字母
  • 用户自定义的类名一般以大写字母开头,如Sales_item
  • 多个单词组成的标识符要用下画线或大写区分,如student_loanstudentLoan

复合类型(compound type)

一条声明语句由一个基本数据类型(base type)和紧随其后的一个声明符(declarator)列表组成。

引用

Tip: C++11中新增了“右值引用(rvalue reference)”,我们通常所说的“引用(reference)”是"左值引用(lvalue reference)"。

1
2
3
int ival = 1024;
int & refVal = ival; //refVal指向ival
int & refVal2; //错误,引用必须初始化

定义引用时,程序把引用和它的初始值绑定(bind)在一起,而非拷贝。引用无法重新绑定到另一个对象,因此必须初始化。

引用即别名。因为引用本身不是一个对象,所以不能定义引用的引用。

Tip: 引用不能与字面值或某个计算结果绑定,且其类型必须与绑定对象严格匹配

指针

与引用类似,指针也实现了对其他对象的间接访问,不同之处在于:

  • 指针本身是一个对象,允许对指针赋值和拷贝,且在其生命周期内可以先后指向几个不同的对象。
  • 无须在定义时赋初值。

指针类型要和其所指向的对象严格匹配

空指针(null pointer)
1
2
3
4
5
//生成空指针的方法
int * p1 = nullptr;
int * p2 = 0;
//需要先 #include <cstdlib>
int * p3 = NULL;

nullptr是一种特殊类型的字面值,可被转化成任意其他的指针类型。(最推荐)

过去的程序中用一个名为NULL预处理变量(preprocessor variable)来给指针赋值,这个变量在头文件cstdlib中定义,它的值就是0

不能把int变量直接赋给指针,会引发错误。

void* 指针

void*是一种特殊的指针类型,可用于存放任意对象的地址。

1
2
3
4
double obj = 3.14 , * pd = & obj;
//正确,void*能存放任意对象的地址
void * pv = & obj; //obj可以是任意类型的对象
pv = pd; //pv可以存放任意类型的指针
指向指针的引用

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

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

Tip: 对于一条比较复杂的指针或引用的声明语句时,从右向左读来了解它的真实含义。

const限定符

const对象一旦创建后其值就不能再改变,故const对象必须初始化。

1
2
3
const int i = get_size();	//正确:运行时初始化
const int j = 42; //正确:编译时初始化
const int k; //错误

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

多个文件共享const对象的方法:

在声明和定义前都添加extern关键字,且只定义一次。

1
2
3
4
//file_1.cc定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
//file_1.h 头文件
extern const int bufSize; //与file_1.cc是同一个

const的引用

也简称”常量引用“。

1
2
3
4
const int ci = 1024;
const int &r1 = ci; //正确
r1 = 42; //错误:r1是对常量的引用
int &r2 = ci; //错误:试图让一个非常量引用指向一个常量对象
特例

允许为一个常量引用绑定非常量的对象、字面值、表达式。

1
2
3
4
5
int i = 42;
const int &r1 = i; //正确
const int &r2 = 42; //正确
const int &r3 = r1 * 2; //正确
int &r4 = r1 * 2; //错误
对const的引用可能引用一个并非const的对象
1
2
3
4
5
int i = 42;				
int &r1 = i; //引用r1绑定对象i
const int &r2 = i; //r2也绑定对象i,但是不允许通过r2修改i的值
r1 = 0; //正确
r2 = 0; //错误,r2是一个常量引用

指向常量的指针(pointer to const)

存放常量对象的地址。

1
2
3
4
const double pi = 3.14;
double * ptr = &pi; //错误,ptr是一个普通指针
const double * cptr = &pi; //正确
* cptr = 42; //错误,不能改变其所指对象的值

允许一个指向常量的指针指向一个非常量对象。

1
2
double dval = 3.14;
cptr = &dval; //正确,但不能通过cptr改变dval的值

常量指针(const pointer)

允许把指针本身定义为常量,且必须初始化。

1
2
3
4
5
6
7
int errNub = 0;
int * const curErr = & errNumb;
//curErr将一直指向errNumb

const double pi = 3.14159;
const double * const pip = & pi;
//pip是一个指向常量对象的常量指针

明白pip含义的过程

从右向左读,const意味着pip本身是一个常量对象,对象的类型由声明符的其余部分决定。下一个符号是 * ,意思是pip是一个常量指针。最后确定pip指向的对象是一个双精度浮点型常量。

顶层const

指针本身是不是常量以及指针所指的是不是常量是两个独立的问题。

用名词顶层const(top-level const)表示指针本身是个常量,而用名词底层const(low-level const)表示指针所指的对象是个常量。

更一般的,顶层const可以表示任意的对象是常量。

常量表达式(const expression)和constexpr

值不会改变并在编译过程就能得到计算结果的表达式。用常量表达式初始化的const对象也是常量表达式。

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

1
2
3
4
constexpr int mf = 20;			//20是常量表达式
constexpr int limit = mf + 1; //mf + 1是常量表达式
constexpr int sz = size(); //只有当size是一个constexpr函数时
//才是一条正确的声明语句

字面值类型

声明constexpr时能用的类型一般都比较简单,统称为字面值类型(literal type),包括算术类型、引用和指针。其中,引用和指针定义的constexpr的初始值会受到严格限制。(详情略)

如果在constexpr声明中定义一个指针,限定符constexpr只对指针有效,而与其所指对象无关。

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

pq的类型相差甚远,其中的关键在于constexpr把它所定义的对象置为了顶层const(而往常指针的顶层const在右边),其作用与其他常量指针类似,既可以指向常量也可以指向一个非常量:

1
2
3
4
5
6
constexpr int * np = nullptr;	//np是一个指向整数的常量指针,其值为空
int j = 0;
constexpr int i = 42; //i的类型是整型常量
//i和j都必须定义在函数体外(这样才有固定的地址作为常量表达式)
constexpr const int * p = & i; //p是常量指针,指向整型常量i
constexpr int *p1 = &j; //p1是常量指针,指向整数j

引用不是对象,因此”常量引用“的定义思路与”“指向常量的指针”类似,而与“常量指针”不同。

类型别名

类型别名(type alias)是一个名字,它是某种类型的同义词。

有两种方法可以定义类型别名:

使用关键字typedef

1
2
typedef double wages;		//wages是double的同义词
typedef wages base , *p; //base是double的同义词,p是double*的同义词

使用别名声明(alias declaration)

1
2
using SI = Sales_item;		//SI是Sales_item的同义词
//把等号左侧的名字规定成等号右侧类型的别名

类型别名与类型的名字等价。

指针、常量和类型别名

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

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

auto类型说明符

auto让编译器通过初始值来推算变量的类型,所以必须有初始值。

1
2
//由val1和val2相加的结果可以推断出item的类型
auto item = val1 + val2; //item初始化为val1和val2相加的结果

因为一条声明语句只能有一个基本数据类型,所以所有变量的初始数据类型都必须一样:

1
2
auto i = 0 , *p = &i		//正确:i是整数、p是整型指针
auto sz = 0 , pi = 3.14; //错误:sz和pi的类型不一致

复合类型、常量和auto

使用引用初始化时,真实使用的是引用对象的值。

auto一般会忽略顶层const,同时底层const则会保留下来:

1
2
3
4
5
6
7
8
int i = 0 , & r = i;
auto a = r;

const int ci = i , & cr = ci;
auto b = ci; //b是一个整数(ci的顶层const特性被忽略掉了)
auto c = cr; //c是一个整数(cr是ci的别名,ci本身是一个顶层const)
auto d = &i; //d是一个整型指针(整数的地址就是指向整数的指针)
auto e = &ci; //e是一个指向整数常量的指针(对常量对象取地址是一种底层const)

如果希望推断出的auto类型是一个顶层const,需要明确指出:

1
const auto f = ci;	//ci的推演类型是int,f是const int

还可以将引用的类型设为auto,此时原来的初始化规则仍然适用:

1
2
3
4
auto &g = ci;			//g是一个整型常量引用,绑定到ci
auto &h = 42; //错误:不能为非常量引用绑定字面值
const auto &j = 42; //正确:可以为常量引用绑定字面值
//设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。

在一条语句中定义多个变量,符号 & 和 * 只从属于某个声明符,而非基本数据类型的一部分,因此初始值必须是同一种类型。

1
2
3
auto k = ci , & l = i;		//k是整数,l是整形引用
auto &m = ci , *p = &ci; //m是对整型常量的引用,p是指向整型常量的指针
auto &n = i , *p2 = &ci; //错误:i的类型是int而&ci的类型是const int

decltype类型指示符

auto通过初始值来推算变量的类型。当希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量时,采用decltype,它的作用是选择并返回操作数的数据类型。

在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:

1
2
decltype ( f() ) sum = x;
//sum的类型就是函数f的返回类型(并不实际调用)

auto不同,如果decltype处理的是变量,则返回该变量的类型(包括顶层const和引用在内)

1
2
3
4
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和引用

如果decltype使用的表达式内容是解引用操作,则decltype得到的是引用类型。

1
2
3
4
//decltype的结果可以是引用类型
int i = 42 , * p = & i , & r = i;
decltype ( r + 0 ) b; //正确,加法的结果是int,因此b是一个未初始化的int
decltype (*p) c; //错误,c是int&,必须初始化

可以通过给变量名加上括号,让decltype的结果强制变为引用。

1
2
3
//decltype的表达式如果是加上了括号的变量,结果将是引用
decltype ((i)) d; //错误,d是int&,必须初始化
decltype (i) e; //正确,e是一个未初始化的int

头文件

头文件通常包含那些只能被定义一次的实体,如类、constconstexpr变量。

为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在的头文件的名字应该与类的名字一样。

头文件保护符(header guard)

头文件保护符依赖于预处理变量。预处理变量有两个状态:已定义和未定义。

#define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef当且仅当“变量已定义”时为真,#ifndef当且仅当”变量未定义“时为真。一旦检查结果为真,则执行后续操作直至遇到 #endif指令为止。

1
2
3
4
5
6
7
8
9
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
#endif

字符串、向量和数组

命名空间的using声明

使用using声明就无须专门的前缀(形如命名空间 : : )也能使用所需的名字,using声明具有以下形式:

1
using namespace :: name;

每个using声明引入命名空间中的一个成员。

Tip: 头文件不应该使用using声明,因为头文件的内容会拷贝到所有引用它的文件中去,可能引发名字冲突。

标准库类型string

标准库类型string表示可变长的字符序列,使用string类型必须首先包含string头文件。作为标准库的一部分,string定义在命名空间std中。以下讨论均包含下述代码:

1
2
#include <string>
using std :: string;

定义和初始化

初始化string对象的方式
string s1; 默认初始化,s1是一个空串
string s2 (s1); s2是s1的副本
string s2 = s1; 等价于s2(s1),s2是s1的副本
string s3 (“value”); s3是字面值"value"的副本,除了字面值最后的空字符外
string s3 = “value”; 等价于s3(“value”),s3是字面值"value"的副本
string s4 ( n , ‘c’ ); 把s4初始化为连续n个字符c组成的串

使用等号’=‘初始化一个变量的过程叫拷贝初始化,不使用等号’='则叫直接初始化

能执行的操作

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; s1 != s2; 如果s1和s2中所含的字符完全一样,则它们相等;该判断对大小写敏感
< , <= , > , >= 利用字典序比较;该判断对大小写敏感
读写未知数量的string对象
1
2
3
4
string word;
while ( cin >> word ){ //反复读取,直至到达文件末尾
cout << word << endl; //逐个输出单词,每个单词后面紧跟一个换行
}

当遇见文件结束标记非法输入时,循环结束。

使用getline读取一整行

getline函数的参数是一个输入流和一个string对象,函数从给定的输入流中读入内容,直到读入换行符,然后把所读的内容存到string对象中去(注意不存换行符)。如果一开始就是换行符,则存入的即是空string

1
2
3
4
5
string line;
//每次读入一整行,直至到达文件末尾。
while ( get( cin , line ) ){
cout << line << endl; //line不包含换行符,手动加上
}

和输入运算符相同,getline也会返回它的流参数(可作为判断条件)。

string::size_type类型

size()函数返回一个string::size_type类型的值。它是一个无符号类型的值,而且能足够存放下任何string对象的大小。

假设n是一个具有负值的int,则表达式s.size() < n的判断结果较大可能为true(不排除false可能),因为负值n会自动地转换成一个比较大的无符号值。

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

字面值和string对象相加

两个string对象可以相加得到一个新的string对象,而当字面值和string对象相加时,会自动将字符串字面值类型转换为string类型,但有如下要求:

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

1
2
3
4
string s4 = s1 + ", ";				//正确
string s5 = "hello" + ", "; //错误,都不是string
string s6 = s1 + ", " + "world"; //正确
string s7 = "hello" + ", " + s2; //错误,不能把字面值直接相加

s6的初始化工作原理和连续输入输出是一样的,可以用如下形式分组:

1
2
3
string s6 = ( s1 + ", " ) + "world";
//s1 + ", "的结果是一个string对象,
//同时作为第二个加法运算符的左侧运算对象

同理,s7的初始化是非法的,按其语义就成了如下分组:

1
string s7 = ( "hello" + ", " ) + s2;

括号内的子表达式试图将两个字符串字面值加在一起,这是编译器做不到的。

C++中的字符串字面值并不是标准库类型string的对象。

处理string对象中的字符

首先我们要知道目标字符的特性。在cctype头文件中定义了一组标准库函数处理这部分工作:

cctype头文件中的函数
isalnum ( c ) 当c是字母或数字时为真
isalpha ( c ) 当c是字母时为真
iscntrl ( c ) 当c是控制字符时为真
isdigit ( c ) 当c是数字时为真
isgraph ( c ) 当c不是空格但可打印时为真
islower ( c ) 当c是小写字母时为真
isprint ( c ) 当c是可打印字符时为真(空格或可视字符)
ispunct ( c ) 当c是标点符号时为真(不是控制字符、可打印空白、数字、字母)
isspace ( c ) 当c是空白时为真(空格、制表符、回车、换行、进纸)
isupper ( c ) 当c时大写字母时为真
isxdigit ( c ) 当c时十六进制数字时为真
tolower ( c ) 如果c是大写字母则返回对应小写字母;否则原样返回c
toupper ( c ) 如果c是小写字母则返回对应大写字母;否则原样返回c

cctype头文件和ctype.h头文件的内容是一样的,但cctype中定义的名字从属于命名空间std。

基于范围的for语句

如果想处理每个字符,可以使用C++11新标准提供的范围for(range for)语句,这种语句遍历给定序列中的每个元素并对序列中的每个值执行某种操作:

1
2
for ( declaration : expression )
statement

其中,expression部分是一个对象,用于表示一个序列。declaration部分负责定义一个变量,该变量用于访问序列中的基础元素。每次迭代,declaration部分的变量会被初始化为expression部分的下一个元素值。

一个string对象表示一个字符的序列,因此string对象可以作为范围for语句中的expression部分。

例如,统计string对象中标点符号的个数:

1
2
3
4
5
6
string s ( "Hello world!!!" );
//punct_cnt的类型和s.size的返回类型一样
decltype( s.size() ) punct_cnt = 0;
for( auto c : s )
if ( ispunct(c) )
++punct_cnt;

如果想改变string对象中字符的值,必须把循环变量定义成引用类型。

1
2
3
4
5
6
//把字符串改成大写
string s ("Hello World!!!");
for( auto & c : s ){
c = toupper(c);
} //c是一个引用,因此赋值语句将改变s中字符的值
cout << s << endl;

只处理部分参数时,使用下标运算符’[]',接受的输入参数是 string::size_type 类型的值(若是带符号类型的值会自动转换),返回该位置上字符的引用。

Tip: 避免使用下标访问空string

标准库类型vector

标准库类型vector表示对象的集合,其中所有对象的类型都相同。

需要包含的头文件:

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

vector是一个类模板。

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

vector为例,提供的额外信息是vector内所存放对象的类型:

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

由于引用不是对象,不存在包含引用的vector

在早期的C++版本中如果vector的元素还是vector(或其他模板类型),必须在外层vector的右尖括号和其元素类型之间添加一个空格,如写成vector<vector<int> >

定义和初始化vector对象

初始化vector对象的方法
vector < T > v1 v1是一个空vector,它潜在的元素是T类型的,执行默认初始化
vector < T > v2 (v1) v2中包含有v1所有元素的副本
vector < T > v2 = v1 等价于v2(v1),v2中包含有v1所有元素的副本
vector < T > v3 ( n , val) v3包含了n个重复的元素,每个元素的值都是val
vector < T > v4 (n) v4包含了n个重复地执行了值初始化的对象
vector < T > v5 { a, b, c… } v5包含了初始值个数的元素,每个元素被赋予相应的初始值
vector < T > v5 = { a, b, c… } 等价于v5{ a, b, c… }

列表初始化只能使用花括号,不能使用圆括号。

值初始化

只提供vector对象容纳的元素数量而略去初始值,此时库会创建一个值初始化的(value-initialized)元素初值,并把它赋给容器中所有元素。这个初值由vector对象中的元素类型决定。

1
2
vector<int> ivec(10);			//10个元素,每个都初始化为0
vector<string> svec(10); //10个元素,每个都是空string对象

如果vector对象中的元素类型不支持默认初始化,则必须提供初始值。

能执行的操作

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 , v1 != v2 v1和v2相等当且仅当它们的元素数量相同且对应位置的元素值相同
< , <= , > , >= 以字典顺序进行比较

size()返回值的类型是由vector定义的size_type类型。

要使用size_type,需首先指定它是由哪种类型定义的:

1
2
vector<int> :: size_type		//正确
vector :: size_type //错误

不能使用下标形式添加元素:

1
2
3
vector<int> ivec;		//空vector对象
for ( decltype(ivec.size()) ix = 0 ; ix !=10 ; ++ix )
ivec[ix] = ix; //严重错误,ivec不包含任何元素

正确的方法是使用push_back:

1
2
for ( decltype(ivec.size()) ix = 0 ; ix !=10 ; ++ix )
ivec.push_back( ix ); //正确

Tip: 当使用下标访问一个不存在的元素(越界)时将引发错误,所谓的缓冲区溢出(buffer overflow)指的就是这类错误。

迭代器

所有标准库容器都可以使用迭代器(iterator),但只有少数几种才支持下标运算符。迭代器类似于指针类型,提供了对对象的间接访问。

迭代器有有效和无效之分,有效的迭代器或者指向某个元素,或者指向容器中尾元素的下一位置;其它所有情况都属于无效。

使用迭代器

与指针不同的是,获取迭代器不是使用取地址符,且拥有迭代器的类型同时拥有返回迭代器的成员。begin成员负责返回指向第一个元素(或第一个字符)的迭代器,end成员负责返回指向容器(或string对象)“尾元素的下一位置(one past the end)”的迭代器。

1
2
3
//由编译器决定b和e的类型;
//b表示第一个元素,e表示v尾元素的下一位置
auto b = v.begin() , e = v.end(); //b和e的类型相同

end成员返回的迭代器常被称为尾后迭代器(off-the-end iterator)或简称尾迭代器(end iterator)。特殊情况下如果容器为空,则begin和end返回的是同一个迭代器。

迭代器运算符

标准容器迭代器的运算符
*iter 返回迭代器iter所指元素的引用
iter->mem 解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem
++iter 令iter指示容器中的下一个元素
–iter 令iter指示容器中的上一个元素
iter1 == iter2 , iter1 != iter2 判断两个迭代器是否相等(不相等)

因为end返回的迭代器并不实际指向某个元素,所以不能对其进行递增或解引用操作。

试图解引用一个非法迭代器或者尾后迭代器都是未被定义的行为。

Tip: C++程序员在for循环中更愿意使用 != 而非 < ,更愿意使用迭代器而非下标,因为这种编程风格在标准库提供的所有容器上都有效。

迭代器类型

就像不知道stringvectorsize_type成员到底是什么类型一样,一般来说我们也不知道(其实也无须知道)迭代器的精确类型。

实际上,那些拥有迭代器的标准库类型使用iteratorconst_iterator来表示迭代器的类型:

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

vector<int>::const_iterator it3; //it3只能读元素,不能写元素
string::const_iterator it4; //it4只能读字符,不能写字符

如果vector对象或string对象是一个常量,只能使用const_iterator;如果vector对象或string对象不是常量,那么既可以使用iterator也能使用const_iterator

begin和end返回的具体类型由对象是否是常量决定:如果对象是常量,begin和end返回const_iterator;如果对象不是常量,返回iterator。

为了便于专门得到const_iterator类型的返回值,C++11新标准引入了两个新函数,分别是cbegincend

1
auto it3 = v.cbegin();	//it3的类型是vector<int>::const_iterator

解引用和成员访问操作

解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就可能希望进一步访问它的成员。但是必须注意加圆括号:

1
2
(*it).empty();		//解引用it,然后调用结果对象的empty成员
*it.empty(); //错误,试图访问it的名为empty的成员,但it是个迭代器,没有empty成员

我们可以使用箭头运算符 -> 简化上述表达式,箭头运算符把解引用和成员访问两个操作结合在一起,即it->mem(*it).mem表达的意思相同。

Tip: 任何往容器中添加元素的操作,都可能使其相应的迭代器失效。故但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。

迭代器运算

迭代器的递增运算令迭代器每次移动一个元素,所有的标准库容器都有支持递增运算的迭代器。类似的,也能用 ==!= 对任意标准库类型的两个有效迭代器进行比较。

stringvector的迭代器提供了更多额外的运算符,使得迭代器的每次移动跨过多个元素,另外也支持迭代器进行关系运算。所有这些运算被称为迭代器运算(iterator arithmetic),其细节由下表列出:

vector和string迭代器支持的运算
iter + n 迭代器加上一个整数值仍得到一个迭代器,迭代器指示的新位置与原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一个位置。
iter - n 迭代器减去一个整数值仍得到一个迭代器,迭代器指示的新位置与原来相比向后移动了若干个元素。
iter1 += n 迭代器加法的复合赋值语句,将iter1加n的结果赋给iter1。
iter1 -= n 迭代器减法的复合赋值语句,将iter1减n的结果赋给iter1。
iter1 - iter2 两个迭代器相减的结果是它们的距离,参与运算的两个迭代器必须来自同一个容器。
> 、 >= 、 < 、 <= 迭代器的关系运算符,如果某迭代器指向的容器位置在另一个迭代器所指位置之前,则说前者小于后者。

上述距离的类型名为difference_type的带符号整数,string和vector都定义了difference_type。

使用迭代器完成二分搜索
1
2
3
4
5
6
7
8
9
10
11
12
//text必须是有序的
//beg和end表示我们搜索的范围
auto beg = text.begin() , end = text.end();
auto mid = text.begin() + ( end - begin ) / 2; //初始状态下的中间点
//当还有元素尚未检查并且我们还没有找到sought时执行循环
while ( mid != end && *mid != sought ) {
if ( sought < *mid ) //我们要找的元素在前半部分?
end = mid; //忽略mid后半部分
else //我们要找的元素在后半部分
beg = mid + 1; //在mid之后寻找
mid = beg + (end - beg) / 2; //新的中间点
}

循环过程终止时,mid或者等于end或者等于要找的元素。如果mid等于end,说明text中没有我们要找的元素。

数组

定义和初始化内置数组

数组是一种复合类型。数组的声明形如 a[d] ,其中a是数组的名字,d是数组的维度。维度说明了数组中元素的个数,因此必须大于0。数组元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的。也就是说,维度必须是一个常量表达式

1
2
3
4
5
6
unsigned cnt = 42;				//不是常量表达式
constexpr unsigned sz = 42; //常量表达式
int arr[10]; //含有10个整数的数组
int *parr[sz]; //含有42个整型指针的数组
string bad[cnt]; //错误,cnt不是常量表达式
string strs[get_size()]; //当get_siz是constexpr时正确;否则错误

默认情况下,数组的元素被默认初始化。

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

字符数组的特殊性

字符数组有一种额外的初始化形式,我们可以用字符串字面值对此类数组初始化。当使用这种方式进行初始化时,要注意字符串字面值的结尾处还有一个空字符,这个空字符也会被字符数组中去:

1
2
3
4
char a1[] = { 'C' , '+' , '+' };		//列表初始化,没有空字符
char a2[] = { 'C' , '+' , '+' , '\0' }; //列表初始化,含有显示的空字符
char a3[] = "C++"; //自动添加表示字符串结束的空字符
const char a4[] = "Daniel"; //错误,没有空间存放空字符

Note: 按理来讲,数组a4的定义是错误的,但Luv实测竟然是可行的(不会报错),且使用sizeof(a4)得到的值是7,可能是编译器原因。

不允许拷贝和赋值

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

1
2
3
int a[] = { 0 , 1 , 2 };		//含有3个整数的数组
int a2[] = a; //错误,不允许使用一个数组初始化另一个数组
a2 = a; //错误,不能把一个数组直接赋值给另一个数组

有些编译器支持数组的赋值,这就是所谓的编译器扩展(compiler extension)。但最好避免使用这些非标准特性。

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

访问数组元素

在使用数组下标时,通常将其定义为size_t类型。size_t是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小。在cstddef头文件中定义了size_t类型,这个文件是C标准库stddef.h头文件的C++语言版本。

数组和指针

使用数组时编译器一般会把它转换成指针。

1
2
string nums[] = { "one" , "two" , "three" };
string *p = nums; //等价于 p = &nums[0]

当使用数组作为auto变量的初始值时,推断得到的类型是指针而非数组:

1
2
3
int ia[] = {0,1,2,3,4,5,6,7,8,9};	//ia是一个含有10个整数的数组
auto ia2(ia); //ia2是一个整型指针,指向ia的第一个元素
ia2 = 42; //错误,ia2是一个指针,不能用int值给指针赋值

但是,当使用decltype关键字时,上述转换不会发生,decltype(ia)返回的类型时由10个整数构成的数组:

1
2
3
4
//ia3是一个含有10个整数的数组
decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9};
ia3 = p; //错误,不能用整形指针给数组赋值
ia3[4] = i; //正确,把i的值赋给ia3的一个元素
指针也是迭代器

vectorstring的迭代器支持的运算,数组的指针全都支持。可以通过数组名字或者数组中首元素的地址都能得到指向首元素的指针;不过获取尾后指针就要用到数组的另外一个特殊性质了:设法获取数组尾元素之后的那个并不存在的元素的地址。

1
2
3
int arr[10] = {0,1,2,3,4,5,6,7,8,9};
int *p = arr; //p指向arr的第一个元素
int *e = &arr[10]; //e指向arr尾元素的下一位置的指针

利用上面的指针输出arr的全部元素:

1
2
3
for ( int *b = arr ; b != e ; ++b ){
cout << *b << endl; //输出arr元素
}
标准库函数begin和end

尽管能通过以上方法获得尾后指针,但这种用法极易出错。

C++11新标准引入了两个名为beginend函数,功能与容器中的同名成员类似。不过数组并不是类类型,因此正确的使用形式是将数组作为它们的参数:

1
2
3
int ia[] = {0,1,2,3,4,5,6,7,8,9};
int *beg = begin(ia); //指向ia首元素的指针
int *last = end(ia); //指向arr尾元素的下一位置的指针

begin函数返回指向ia首元素的指针,end函数返回指向ia尾元素下一位置的指针,这两个函数定义在iterator头文件中。

指针运算

和迭代器一样,两个指针相减的结果是它们之间的距离。参与运算的两个指针必须指向同一个数组当中的元素:

1
auto n = end(arr) - begin(arr);	//n的值就是arr中元素的数量

两个指针相减的结果的类型是一种名为ptrdiff_t的标准库类型,和size_t一样,ptrdiff_t也是定义在cstddef头文件中的机器相关的类型。因为差值可能为负数,所以ptrdiff_t是一种带符号类型。

Tip: 数组使用的下标运算符 [] 是“内置”的,与vectorstring不同,所用的索引值可以为负值,例如 p[-2] = *(p-2),当该指针p指向的是数组元素(或尾元素下一位置)时均为合法的。

C风格字符串

Tip: 尽管 C++ 支持C风格字符串,但在 C++ 程序中最好不要使用它们。这是因为C风格字符串不仅使用起来极不方便,而且极易引发程序漏洞,是诸多安全问题的根本问题。

字符串字面值是一种通用结构的示例,这种结构即是C++由C继承而来的C风格字符串(C-style character string)。C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法。按此习惯书写的字符串存放在字符数组中并以空字符结束(null terminated)

C标准库Stirng函数

以下为C语言标准库提供的一组函数,定义在cstring头文件中,是C语言头文件string.h的C++版本。

C风格字符串的函数
strlen ( p ) 返回p的长度,空字符不计算在内
strcmp ( p1 , p2 ) 比较p1和p2的相等性,如果p1==p2返回0;如果p1>p2返回一个正值;如果p1<p2返回一个负值
strcat ( p1 , p2 ) 将p2附加到p1之后,返回p1
strcpy ( p1 , p2 ) 将p2拷贝给p1,返回p1

传入此类函数的指针必须指向以空字符结束的数组:

1
2
char ca[] = { 'C' , '+' , '+' };	//不以空字符结束
cout << strlen(ca) << endl; //严重错误,ca没有以空字符结束

当想使用strcmp和strcat函数时,需要预先估计用于存放结果的数组所需的空间大小,这导致这类代码充满了风险而且经常导致严重的安全泄漏。

比较字符串

把关系运算符和相等性运算符用在C风格字符串上,实际比较的是指针而非字符串本身:

1
2
3
const char ca1[] = "A string example";
const char ca2[] = "A different string";
if ( ca1 < ca2 ) //未定义:试图比较两个无关的地址

要想比较两个C风格字符串必须调用strcmp函数。

与旧代码的接口

很多 C++ 程序在标准库出现之前就已经写成了,它们肯定没用到stringvector类型。而且,有一些 C++ 程序实际上是与C语言或其他语言的接口程序,当然也无法使用 C++ 标准库。因此,现代的 C++ 程序不得不与那些充满了数组和C风格字符串的代码衔接。

为了使这一工作简单易行,C++专门提供了一组功能。

混用string对象和C风格字符串

前面介绍过可以用字符串字面值来初始化string对象。

任何出现字符串字面值的地方都可以用以空字符结尾的字符数组来替代:

  • 允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值。
  • string对象的加法运算中,允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是);在string对象的复合运算中,允许使用以空字符结束的字符数组作为右侧的运算对象。

但是上述性质不能反过来使用,例如不能用string对象直接初始化指向字符的指针。为了完成该功能,string专门提供了一个名为c_str的成员函数:

1
2
char *str = s;		//错误,不能用string对象初始化char*
const char *str = s.c_str(); //正确

c_str函数的返回值是一个C风格字符串,即一个指针,该指针指向一个以空字符结束的字符数组,数组所存数据与string对象一样。结果指针的类型为const char*,从而确保我们不会改变字符数组的内容。

Tip: 当s值变化时c_str返回的数组可能失效,所以如果想一直使用其返回的数组,最好使用strcpy函数将该数组重新拷贝一份。

用数组初始化vector对象

允许使用数组初始化vector对象,只需指明要拷贝区域的首元素地址和尾后地址即可:

1
2
3
int int_arr [] = {0,1,2,3,4,5};
//ivec有6个元素,分别是int_arr中对应元素的副本
vector<int> ivec ( begin(int_arr) , end(int_arr) );

其中,使用标准库函数beginend来分别计算int_arr的首指针和尾后指针。

也可以只拷贝部分元素:

1
2
//拷贝3个元素:int_arr[1]、int_arr[2]、int_arr[3]
vector<int> subVec ( int_arr + 1 , int_arr + 4 );

多维数组

初始化
1
2
3
4
5
int ia[3][4] = {			//三个元素,每个元素都是大小为4的数组
{ 0 , 1 , 2 , 3 }, //第1行的初始值
{ 4 , 5 , 6 , 7 }, //第2行的初始值
{ 8 , 9 , 10 , 11 }, //第3行的初始值
};

内嵌的花括号不是必须的:

1
2
//没有标识每行的花括号,与之前的初始化语句是等价的
int ia [3][4] = {0,1,2,3,4,5,6,7,8,9,10,11}

未列出的元素执行默认初始化:

1
2
//显式地初始化每行的首元素
int ia [3][4] = { { 0 } , { 4 } , { 8 } };
使用范围for语句处理多维数组
1
2
3
4
5
6
7
8
constexpr size_t rowCnt = 3 , colCnt = 4;
int ia[rowCnt][colCnt]; //12个未初始化的元素
size_t cnt = 0;
for ( auto &row : ia ) //对于外层数组的每一个元素
for( auto &col : row ) { //对于内层数组的每一个元素
col = cnt; //将下一个值赋给该元素
++cnt; //将 cnt 加 1
}

使用范围for语句把管理数组索引的任务交给了系统来完成。因为要改变元素的值,所以得把控制变量rowcol声明成引用类型。

但实际还有更深层的原因,举个例子:

1
2
3
for ( const auto &row : ia )	//对于外层数组的每一个元素
for ( auto col : row ) //对于内层数组的每一个元素
cout << col << endl;

这个循环中并没有任何读写操作,还是将外层循环的控制变量声明成了引用类型,这是为了避免数组被自动转换成指针。假设采取如下形式:

1
2
for ( auto row : ia )
for( auto col : row )

程序将无法通过编译。因为编译器会在初始化row时自动将这些数组形式的元素转换成指向该数组内首元素的指针,这样得到的row的类型就是int*,此时内层循环就不合法了。

Tip: 要使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。

指针和多维数组
1
2
3
4
5
6
7
8
//输出ia中每个元素的值,每个内层数组各占一行
//p指向含有4个整数的数组
for ( auto p = ia ; p != ia + 3 ; ++p ) {
//q指向4个整数数组的首元素,也就是说,q指向一个整数
for ( auto q = *p ; q != *p + 4 ; ++q )
cout << *q << ' ';
cout << endl;
}

可以用标准库函数begin和end实现相同功能。

类型别名简化多维数组的指针
1
2
3
4
5
6
7
8
using int_array = int[4];	//新标准下类型别名的声明
typedef int int_array[4]; //等价的typedef声明
//输出ia中每个元素的值,每个内层数组各占一行
for ( int_array *p = begin(ia) ; p != end(ia) ; ++p ) {
for ( int *q = begin(*p) ; q != end(*p) ; ++q )
cout << *q << ' ';
cout << endl;
}

程序将“4个整数组成的数组”命名为int_array,用类型名int_array定义外层循环的控制变量让程序显得简洁明了。

杂项

命令行编译

从命令行运行编译器:

1
2
3
$ CC prog1.cc
CC 是编译器程序名字
$ 是系统提示符

通过echo命令获得某个程序的返回值

1
2
$ echo $?							//UNIX系统
$ echo %ERRORLEVEL% //Windows系统

运行GNU编译器的g++命令:

1
2
$ g++ -o prog1 prog1.cc
-o prog1 为编译器参数

编译器检查出的错误

语法错误 syntax error
类型错误 type error
声明错误 declaration error

iostream

iostream包含istreamostream

stream” ——

istream: cin(标准输入)

ostream: cout(标准输出) , cerr , clog

cerr: 标准错误;输出警告和错误消息。

clog: 输出程序运行时一般性信息。

endl: 操纵符;结束当前行,并将缓冲区(buffer)的内容刷到设备中。

读取数量不定的输入数据

1
2
3
while( std::cin >> value ){
sum += value;
}

istream对象作为条件可以检测流,通常情况有效,遇到文件结束符EOF无效输入时,istream状态变为无效,使条件变为假。

从键盘输入文件结束符:

Windows系统:敲Ctrl+Z,然后按Enter或Return键

UNIX或Mac OS X系统:敲Ctrl+D

相关名词解释

Unicode字符 和 UTF-8

原文链接:https://blog.csdn.net/jolin678/article/details/120143320

Unicode的学名是"Universal Multiple-Octet Coded Character Set",简称为UCS,也叫统一码、万国码、单一码。

Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。

虽然我们经常说unicode编码,但它其实不是一种常规意义上的编码,我认为叫作“编号”更准确,unicode给每个字符都定义了一个不会重复的编号,例如:

“汉”对应的编号是0x6C49

早期的Unicode标准有UCS-2、UCS-4的说法。UCS-2用两个字节编码,UCS-4用4个字节编码。

那么unicode既然不是一种编码,那编码是什么,我认为编码是一种和计算机通话的规则。例如上面的“汉”字的unicode编号是0x6C49,假设现在有1种编码叫MyUtf,MyUtf会按照它自己的规则把0x6C49的二进制进行一些变换,假设变换成了0x55AA,然后只要告诉计算机当前有个字符用MyUtf来编码的,编码后的值是0x55AA,那么计算机就自动会按照MyUtf的规则进行逆运算,得到0x6C49,并正确的显示一个“汉”字。同样地不管是UTF-8,UTF-16,UTF-32,只要是对UNICODE字符进行编码,那实际上就是对这个编号进行编码,由于编号是固定的,所以不管编码规则如何编号,只要计算机最后能知道你这个编号对应的是UNICODE编号的哪一个就行了,根据编号计算机就能正确的显示出对应的字符,这正是UNICODE标准的意义。

“汉”字的Unicode编码是0x6C49,将0x6C49写成二进制是: 0110 1100 0100 1001。

采用UTF-8的编码规则编码后为:1110 0110 1011 0001 0100 1001,即0xE6B189。

那么我们能说UTF-8是UNICODE吗?

当然不能了。

UTF-8 是使用互联网上使用最广泛的 unicode 编码方式,目前已经占有整个互联网 92% 的份额。这里再强调下 UTF-8 只是 Unicode 的一种实现方式,UTF-8 是编码方式,而 Unicode 是字符集合。UTF-8它是可变长的编码方式,长度从 1 个字节到 4 个字节不等。它能够完全兼容 ASCII 码,我们知道 ASCII 码 是由 128 个字符组成的,而 Unicode 中的前 128 个字符和 ASCII 码都是对应的

Visual Studio编程使用的默认编码是GB2312