第一章——开始

编译、运行程序

返回值

访问main的返回值的方法依赖于系统,在UNIX和Windows系统中,执行完一个程序后,可以通过以下方法活得其返回值。

1
echo $?
1
echo %ERRORLEVEL%
1
[echo] $?

其中PowerShell的echo可有可无,如果程序返回值为0则显示为True,否则为False。

初识输入输出

输入输出运算符

输出运算符(<<)为例,<<运算符接受两个运算对象,左侧的运算对象必须是一个ostream对象,右侧的运算对象就是要打印的值,此运算符将给定的值写到ostream对象中,输出运算符的结果就是其左侧运算对象,即该运算符的返回值为左侧的运算对象,所以以下两条语句等价

1
2
std::cout << "hello world!" << std::endl;
(std::cout << "hello world!") << std::endl;

读取数量不定的输入数据

当我们使用一个istream对象作为条件时,其效果就是检测流的状态。如果遇到文件结束符(end-of-file),或遇到一个无效的输入(读取类型不匹配)时,istream对象的状态就会变为无效,出于无效状态的istream对象会使条件为假。

缓冲区

默认情况下,读cin会刷新cout;程序非正常终止时也会刷新cout

Cerr

一个ostream对象,关联到标准错误,通常写入到与标准输出相同的设备。默认情况下,写到cerr的数据是不缓冲的。

Clog

一个ostream对象,关联到标准错误。默认情况下,写到clog的数据是被缓冲的。

变量与基本类型

基本内置类型

C++定义了一套包括算术类型(arithmetic type)和空类型(void)在内的基本数据类型,其中算数类型包含了字符、整型数、布尔值和浮点数。

字符型

字符型被分为了三种charsigned charunsigned char,但是尽管字符型有三种,字符的表现形式只有两种:带符号的不带符号的。类型char实际上会表现为上述的其中一种,具体是哪种由编译器决定。

类型转换

  • 将浮点数赋给整数类型时,进行近似处理,结果值仅保留浮点数中小数点前的部分。

    1
    int val = 3.9999; // val = 3
  • 赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示的数值总数取模后的余数。

    1
    unsigned char val = -1; // val = 255
  • 赋给带符号类型一个超出它表示范围的值时,结果是未定义(undefined)的。

  • 如果表达式里既有带符号类型又有无符号类型,那么带符号类型会自动转换成无符号类型。

字面值常量

整型和浮点型字面值
  • 以0开头的整数代表八进制数,以0x或0X开头的代表十六进制数。

    1
    20 /*十进制*/    024/*八进制*/    0x14/*十六进制*/
  • 默认情况下,十进制字面值是带符号数,八进制和十六进制可能是带符号的也可能是不带符号的。类型会从能容纳其数值的最小尺寸者([unsigned] int, long, long long),short类型没有其字面值。

  • 默认的,浮点型字面值是一个double。

转义序列
关于问号

问号也在转义序列中,主要是为了防止 三标符 (trigraph),会在辨识注释和字面量之前被分析,如??<会被编译成{,不过三标符在c++17中已经被移除也就不需要转义了。(平常普通使用也不需要)

泛化的转义序列

其形式是\x后紧跟一个或多个十六进制数字,或者\后紧跟1-3个八进制数字。

如果反斜线后跟着的八进制数字超过3个,只有前3个数字构成转义序列。

相反,\x会用到后面跟着的所有数字,超过范围会报错。

指定字面值的类型
字符和字符串字面值
前缀 含义 类型
u Unicode16字符 char16_t
U Unicode32字符 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

注意对于整型字面值,是规定其最小的匹配值,例如以UL为后缀的数据类型将根据具体数值或者取unsigned long,或者取unsigned long long

变量

变量定义

初始值

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

列表初始化
1
2
3
4
int val = 0;
int val(0);
int val = {0};
int val{0};

后两种初始化的形式被称为列表初始化(list initialization),当用于内置类型的变量时,这种形式有一个重要特点:如果我们使用列表初始化存在丢失信息的风险,则编译器将报错

1
2
3
float pi = 3.14;
int val{pi};
// Type 'float' cannot be narrowed to 'int' in initializer list (fix available)
默认初始化

定义于函数体内的内置数据类型的对象如果没有初始化,则其值未定义。类的对象如果没有显示地初始化,则其值由类确定。

变量声明和定义的关系

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

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

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

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

任何包含了显式初始化的声明即成为定义。extern语句包含初始值就不再是声明而是定义,抵消掉了extern的作用。

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

复合类型

引用(左值引用)

引用并非对象,相反的,它只是为一个已经存在的对象所起的另一个名字。

获取引用的值,实际上是获取了与引用绑定的对象的值。

指针

指针与引用的不同点:

  1. 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向及规格不同的对象。
  2. 指针无须在定义时赋初值。

引用不是对象,没有实际地址,所以不能定义指向引用的指针。

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

const限定符

const的引用

常量引用实际上是对const的引用的简称。

初始化和对const的引用

引用的类型必须与其所引用对象的类型一致,但是有两个例外,其中一个就是在初始化常量引用的时候允许使用任意表达式作为初始值,只要该表达式的结果能够转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象,字面值,甚至是个一般表达式。

1
2
3
4
int i = 42;
const int &r1 = i;
const int &r2 = 42;
const int &r3 = r1 * 2;

当一个常量引用被绑定到另外一种类型时

1
2
double dval = 3.14;
const int &ri = dval;

为了确保让ri绑定一个整数,编译器把上述代码变成了如下形式

1
2
const int temp = dval;
const int &ri = temp;

在这种情况下,ri绑定了一个临时量(temporary),临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象,上述r3也会进行这样的转换。(r2说不定也是,只是没法验证)

对const的引用可能引用一个非const的对象

常量引用仅仅对引用可参与的操作作出了限定,对于引用的对象本身是否是一个常量未作限定,允许通过其他方式改变对象的值。

1
2
3
4
5
int i = 42;
int &r1 = i;
const int &r2 = i;
r1 = 0;
r2 = 0; // 错误

当通过r1修改i的值之后,r2的值实际上也变成了0;

指针和const

指向常量的指针

类似常量引用,指向常量的指针(pointer to const)不能用于修改所指对象的值。

1
2
const double pi = 3.14;
const double *cptr = &pi;

指针类型必须与其所指对象的类型一致,但是有两个例外,其中一个就是允许一个指向常量的指针指向一个非常量的对象。

1
2
double dval = 3.14;
cptr = &dval;

与常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量,所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,对象本身可以通过其他方式改变。

const指针

常量指针(const pointer)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针的地址)就不能改变了。

1
2
int errNumber = 0;
int *const curErr = &errNumber;

弄清这些声明的含义最有效的方法就是从右向左读。

顶层const

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

执行拷贝操作的时候,顶层const不受影响。但是底层const的限制不能忽视,拷入和拷出的对象必须具有相同的底层const资格或者两个对象的数据类型能够转换,一般来说非常量能够转换成常量。

constexpr和常量表达式

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

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

constexpr变量

C++11规定允许将变量声明为constexpr类型以便编译器来验证变量的值是否是一个常量表达式。

1
2
3
constexpr int mf = 20;
constexpr int limit = mf + 1;
constexpr int sz = size(); // 只有size是一个constexpr函数时正确
字面值类型

常量表达式的值在编译时就得到计算,所以用到的类型必须有所限制,因为这些类型一般比较简单,值也显而易见、容易得到,就将它们称为“字面值类型”(literal type)。

目前,算术类型、引用和指针都属于字面值类型。

尽管指针和引用能够定义为constexpr,但他们的初始值受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是储存于某个固定地址的对象(全局或者static,引用限制为后者)。

指针和constexpr

在constexpr声明中定义一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关。(顶层const)

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

处理类型

类型别名(type alias)

typedef
1
2
typedef double wages;
typedef wages base, *p; //base是double的同义词,p是double*的同义词
别名声明(alias declaration)
1
using myint = int *;// myint是int*的同义词
指针、常量和类型别名
1
2
3
typedef char *pstring;
const pstring cstr = 0;// cstr是一个指向char的常量指针
const pstring *ps;// ps是一个指针,它的对象是指向char的常量指针

const是对给定类型的修饰,声明语句中用到pstring时,其基本类型是指针

auto类型说明符

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

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

复合类型、常量和auto

引用作为初始值时,真正参与初始化的是引用对象的值,所以编译器会使用引用对象的类型作为auto的类型,而不是引用。

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

1
2
3
4
5
6
int i = 0;
const int ci = i, &cr = ci;
auto a = ci;
auto b = cr;// 都是整数
auto c = &i;// 整型指针
auto d = &ci;// 指向整数常量的指针(对常量对象取地址是一种底层const)

可以明确指出顶层const

设置一个类型为auto的引用时,初始值的顶层常量属性保留

1
2
auto &e = ci;// 整型常量引用
const auto &f = 42;// 为常量引用绑定字面值

decltype类型指示符

如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用)

1
2
3
const int ci = 0, &cj = ci;
decltype(ci) x = 0;// const int
decltype(cj) y = 0;// const int &
decltype和引用

decltype使用的表达式不是一个变量,则返回表达式对应的类型。

1
2
3
int i = 42, *p = &i, &r = i;
decltype(r + 0) b;// int
decltype(*p) c;// 错误

如果表达式的内容是解引用操作,则decltype将得到引用类型。解引用指针可以得到指针所指的对象,而且还能给该对象赋值。

如果给变量加上了一层或多层括号,编译器就会把它当作一个表达式,变量是一种可以作为赋值语句左值的特殊表达式,所以这样的decltype会得到引用类型。

1
2
decltype((i)) d; // 错误,int&,引用需要初始化
decltype(i) e;// int

自定义数据结构

C++11规定,可以为数据成员提供一个类内初始值,不能使用圆括号

1
2
3
4
5
6
class Test{
private:
int val1 = 42;
int val2 = {42};
int val3{42};
};

预处理变量无视C++关于作用域的规则(即使在函数中定义并且没调用,后续(物理后续即后面行的)的代码也依然视其已定义)