C++ Primer 笔记,主要基于书籍《C++ Primer (5th Edition)》,仅补充些笔者需要记录的知识。
1. 开始
写入 endl
或换行符的效果是结束当前行,并将设备关联的缓冲区(buffer)中内容刷到设备中,保证到目前程序产生的输出均真正写入输入流中,而非仅停留在内存中等待写入流。
2. 变量和基本类型
2.1 基本内置类型
2.1.3. 字面值常量
字符和字符串字面值(string literal)
如果两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,则它们实际是一个整体。当字符串字面值较长,写在一行不合适时,可采取此方式。
// 分多行书写的字符串字面值
std::cout << "a really, really long string literal "
"that spans two lines" << std::endl;
转义序列 (escape sequence)
对于泛化的转义序列:
- 如果反斜杠
\
后面跟着的八进制数字超过3个,只有前3个数字与\
构成转义序列。 - 相反,
\x
要用到后面跟着的所有数字。例如,”\x1234”
表示一个16位的字符,该字符由这4个十六进制数所对应的比特唯一确定。
指定字面值的类型
字符和字符串字面值 | ||
前缀 | 含义 | 类型 |
u | Unicode 16 字符 | char16_t |
U | Unicode 32 字符 | char32_t |
L | 宽字符 e.g. L’a’ | wchar_t |
u8 | UTF-8(仅用于字符串字面常量)e.g. u8”hi!” | char |
整型字面值 | |
后缀 | 最小匹配类型 |
u or U | unsigned |
l or L | long |
ll or LL | long long |
浮点型字面值 | |
后缀 | 类型 |
f or F | float |
l or L | long double |
2.2. 变量
2.2.1. 变量定义
列表初始化(list initialization)
对于 int 变量初始化为 0,有四种语句可以实现:
int i = 0;
int i = {0};
int i(0);
int i{0};
作为C++11新标准的一部分,花括号初始化变量得到全面应用,称为列表初始化,在初始化对象或者某些时候为对象赋新值均可使用。
当用于内置类型的变量时,有个特点:如果使用列表初始化,且初始值存在丢失信息的风险,则编译器将报错:
long double ld =3.1415926536;
int a{ld}, b = {ld}; // 错误:转换未执行,因为存在丢失信息的危险
int c(ld), d = ld; // 正确:转换执行,且确实丢失部分值
2.2.2. 变量声明和定义的关系
C++语言支持分离式编译(separate compilation)机制,允许程序分割为若干个可被独立编译的文件。
为了支持分离式编译,C++将声明和定义区分开。声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。
变量声明规定了类型和名字,定义在这一点与之相同。但定义还申请存储空间,也可能会为变量赋一个初始值。
如果想声明而非定义,在变量名前添加关键字 extern
,而且不要显式地初始化变量:
extern int i; // 声明 i 而非定义 i
int j; // 声明并定义 j
extern double pi = 3.1416; // 定义
任何包含了显式初始化的声明即成为定义。
在函数体内部,若试图初始化一个由 extern
关键字标记的变量,将引发错误。
变量的定义必须出现在且只能出现在一个文件中。
C++是一种静态类型(statically typed)语言,含义是在编译阶段检查类型,该过程称为类型检查(type checking)。
2.2.4. 名字的作用域
可以使用作用域操作符 ::
来覆盖默认的作用域规则,全局作用域本身没有名字,所以当作用域操作符左侧为空时,在全局作用域中获取右侧名字对应的变量。
2.3. 复合类型(compound type)
C++有几种复合类型,包括引用和指针。
通用的描述:一条简单的声明语句由一个基本数据类型(base type)和紧随其后的一个声明符(declarator)列表组成。
2.3.1. 引用
引用(reference) 为对象起了另外一个名字, 引用类型引用(refers to) 另外一种类型。 通过将声明符写成 &d
的形式来定义引用类型, 其中 d
是声明的变量名:
int ival = 1024;
int &refVal = ival; // refVal 指向 ival(是 ival 的另一个名字)
int &refVal2; // 报错:引用必须被初始化
定义引用时,程序把引用和它的初始值绑定(bind) 在一起, 而不是将初始值拷贝给引用。 一旦初始化完成, 引用将和它的初始值对象一直绑定在一起。 因为无法令引用重新绑定到另外一个对象, 因此引用必须初始化。
引用并非对象, 相反的, 它只是为一个已经存在的对象所起的另外一个名字。因为引用本身不是一个对象,所以不能定义引用的引用。
int &refVal4 = 10; // 错误:引用类型的初始值必须是一个对象
double dval = 3.14;
int &refVal5 = dval; // 错误:此处引用类型的初始值必须是int对象
2.3.2. 指针
指针(pointer)是“指向(point to)”另外一种类型的复合类型。 与引用类似, 指针也实现了对其他对象的间接访问。 然而指针与引用相比又有很多不同点。 其一, 指针本身就是一个对象, 允许对指针赋值和拷贝, 而且在指针的生命周期内它可以先后指向几个不同的对象。 其二,指针无须在定义时赋初值。 和其他内置类型一样, 在块作用域内定义的指针如果没有被初始化, 也将拥有一个不确定的值。
指针存放某个对象的地址, 要想获取该地址, 需要使用取地址符(操作符&) :
int ival = 42;
int *p = &ival; //p存放变量ival的地址,或说p是指向变量ival的指针
除了两种例外情况,其他所有指针的类型都要和它所指向的对象严格匹配,因为在声明语句中指针的类型实际上被用于指定它所指向对象的类型:
double dval;
double *pd = &dval; // 正确:初始值是double型对象的地址
double *pd2 = pd; // 正确:初始值是指向double对象的指针
int *pi = pd; // 错误:指针pi的类型和pd的类型不匹配
pi = &dval; // 错误:试图把double型对象的地址赋给int型指针
指针的值(即地址) 应属下列4种状态之一:
- 指向一个对象。
- 指向紧邻对象所占空间的下一个位置。
- 空指针, 意味着指针没有指向任何对象。
- 无效指针, 也就是上述情况之外的其他值。
如果指针指向了一个对象, 则允许使用解引用符(操作符 *
) 来访问该对象;对指针解引用会得出所指的对象, 因此如果给解引用的结果赋值,实际上也就是给指针所指的对象赋值。
int ival = 42;
int *p = &ival; // p存放着变量ival的地址,或者说p是指向变量ival的指针
cout << *p; // 由符号*得到指针p所指的对象,输出42
*p = 0; // 由符号*得到指针p所指的对象,即可经由p为变量ival赋值
cout << *p; // 输出0
空指针(null pointer) 不指向任何对象。得到空指针最直接的办法就是用字面值 nullptr
来初始化指针, 这也是 C++11 新标准刚刚引入的一种方法。过去的程序还会用到一个名为 NULL
的预处理变量(preprocessor variable) 来给指针赋值, 这个变量在头文件 cstdlib
中定义, 它的值就是0。
现在只要知道预处理器是运行于编译过程之前的一段程序就可以了。预处理变量不属于命名空间 std
, 它由预处理器负责管理, 因此我们可以直接使用预处理变量而无须在前面加上 std::
。
当用到一个预处理变量时, 预处理器会自动地将它替换为实际值,因此用 NULL
初始化指针和用 0
初始化指针是一样的。 在新标准下, 现在的 C++ 程序最好使用 nullptr
, 同时尽量避免使用 NULL
。把 int 变量直接赋给指针是错误的操作, 即使 int 变量的值恰好等于 0 也不行。
int zero = 0;
pi = zero; // 错误:不能把int变量直接赋给指针
建议初始化所有指针!
指针和引用都能提供对其他对象的间接访问, 然而在具体实现细节上二者有很大不同, 其中最重要的一点就是引用本身并非一个对象。 一旦定义了引用, 就无法令其再绑定到另外的对象, 之后每次使用这个引用都是访问它最初绑定的那个对象。
指针和它存放的地址之间就没有这种限制了。和其他任何变量(只要不是引用) 一样, 给指针赋值就是令它存放一个新的地址, 从而指向一个新的对象。
最好的办法就是记住赋值永远改变的是等号左侧的对象。
pi = &ival; // pi的值被改变,现在pi指向了ival
*pi = 0; // ival的值被改变,指针pi并没有改变
void*
是一种特殊的指针类型, 可用于存放任意对象的地址。 一个 void*
指针存放着一个地址, 这一点和其他指针类似。不同的是, 我们对该地址中到底是个什么类型的对象并不了解。
2.3.3. 理解复合类型的声明
经常有一种观点会误以为, 在定义语句中,类型修饰符( *
或 &
)作用于本次定义的全部变量。造成这种错误看法的原因有很多,其中之一是我们可以把空格写在类型修饰符和变量名中间:
int* p; // 合法但容易产生误导
我们说这种写法可能产生误导是因为 int*
放在一起好像是这条语句中所有变量共同的类型一样。其实恰恰相反,基本数据类型是 int
而非 int*
。*
仅仅是修饰了 p
而已,对该声明语句中的其他变量,它并不产生任何作用:
int* p1, p2; // p1是指向int的指针,p2是int
涉及指针或引用的声明,一般有两种写法。第一种把修饰符和变量标识符写在一起,这种形式着重强调变量具有的复合类型,这种形式常用:
int *p1, *p2; // p1和p2都是指向int的指针
第二种把修饰符和类型名写在一起,并且每条语句只定义一个变量,这种形式着重强调本次声明定义了一种复合类型:
int* p1; // p1是指向int的指针
int* p2; // p2是指向int的指针
一般来说, 声明符中修饰符的个数并没有限制。 当有多个修饰符连写在一起时, 按照其逻辑关系详加解释即可。 以指针为例, 指针是内存中的对象, 像其他对象一样也有自己的地址, 因此允许把指针的地址再存放到另一个指针当中。
引用本身不是一个对象, 因此不能定义指向引用的指针。 但指针是对象, 所以存在对指针的引用:
int i = 42;
int *p; // p是一个int型指针
int *&r = p; // r是一个对指针p的引用
r = &i; // r引用了一个指针,因此给r复制&i就是令p指向i
*r = 0; // 解引用r得到i,也就是p指向的对象,将i的值改为0
要理解 r
的类型到底是什么, 最简单的办法是从右向左阅读 r
的定义。 离变量名最近的符号(此例中是 &r
的符号 &
) 对变量的类型有最直接的影响, 因此 r
是一个引用。 声明符的其余部分用以确定 r
引用的类型是什么, 此例中的符号 *
说明 r
引用的是一个指针。 最后, 声明的基本数据类型部分指出 r
引用的是一个 int 指针。
2.4. const 限定符
默认状态下,const 对象仅在文件内有效。如果想在多个文件之间共享 const 对象,必须在变量的定义之前添加 extern
关键字。
2.4.1. const的引用
可以把引用绑定到 const 对象上,就像绑定到其他对象上一样,我们称之为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象:
const int ci = 1024;
const int &r1 = ci; // 正确:引用及其对应的对象都是常量
r1 = 42; // 错误:r1是对常量的引用
int &r2 = ci; // 错误:试图让一个非常量引用指向一个常量对象
术语: 常量引用是对const的引用:C++程序员们经常把词组“对const的引用”简称为“常量引用”,这一简称还是挺靠谱的,不过前提是你得时刻记得这就是个简称而已。严格来说,并不存在常量引用。因为引用不是一个对象, 所以我们没法让引用本身恒定不变。事实上, 由于C++语言并不允许随意改变引用所绑定的对象,所以从这层意义上理解所有的引用又都算是常量。引用的对象是常量还是非常量可以决定其所能参与的操作,却无论如何都不会影响到引用和对象的绑定关系本身。
(参见2.3.1.节)提到, 引用的类型必须与其所引用对象的类型一致, 但是有两个例外。 第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值, 只要该表达式的结果能转换成(参见2.1.2.节)引用的类型即可。
int i = 42;
const int &r1 = i; // 允许将const int&绑定到一个普通int对象上
const int &r2 = 42; // 正确:r2是一个常量引用
const int &r3 = r1 * 2; // 正确:r3是一个常量引用
int &r4 = r1 * 2; // 错误:r4是一个普通的非常量引用