What & How & Why

变量和基本类型

C++ Primer 笔记 第二章


在 C++ 中,基础类型(Primitive types)包括算数类型(Arithmetic types)和一种特殊的类型 void。 Void 类型主要适用与无返回值的函数

算术类型

在C++中, 整型(Integral)和浮点型( Floating-point) 统称算术类型。而整型又分为3种类型:integersbooleanchar.

  1. bool, 存放 true or False
  2. char, 存放字符,C++ 中有比标准 char 大一倍的 wchar_t,有适用于 Unicode 的 char16_t / char32_t
  3. int, 存放整数,有 short / int / long / long long 适应不同大小的数。

计算机使用二进制序列(比如01010101)来处理数据。数列中的每一位称为 bit。大部分情况下,计算机会将二进制序列按块划分,块的大小往往是 2 的 n 次方。这些块中,最小的被称为 Byte(Byte 需要足够大来装载计算机中的基础字符集)。Byte 拥有自身唯一的地址。
计算机一次运算能够处理的 Byte 的数量,称为 Word,这也往往是存储器的单位大小。现代计算机的 Byte 大小一般为 8 bits, word 大小为 32 或 64 bits.

unsigned 类型

对于所有 Integral 类型的变量来说,除开 bool 类型,intchar 都可以带有符号或者不带有符号。 int 不同, char 有三种类型: char, signed char, unsigned char.

unsigned 类型的所有位数都用于存储值,因此一般来说范围是从 0 开始。signed 类型由于需要多出一位记录正负,因此能表达的数值范围会以 0 为中心点。

关于类型使用的注意事项

  1. unsigned 类型请使用非负值赋值。
  2. 对整型操作中,int 会比 short 类型使用起来更方便。
  3. 不要将 boolchar 用于算术表达式中。 带符号的 char 类型会在不同的系统里表达出不同的正负。如果非要使用请指定正负。
  4. 浮点数运算请使用 doubledouble 的精度更高,在现代计算机上有时候速度反而比 float 快。

类型转换

Bool 与其他类型的转换
  • nobool → bool : 0 = False, 其他值 = True
  • bool → nobool(算数类型) : False = 0, True = 1
Float point 与 整型的转换
  • float-pointintergral types: value truncated,保留小数点之前的部分。
  • intergral typesfloat-point: 小数部分记 0,如果整数占用 bits 大于浮点数,精度可能会丢失。
out of range value
  • 超过范围的值 → unsigned 类型: 求模运算。比如:unsigned char c = -1; -1+256 = 255; 所以 c 值应该等于 255.
  • 超过范围的值 → signed 类型: undefined.

在很多环境中,编译器不会(也不能)检查未定义行为Undefined behavior)。同时,是否能检查出未定义行为也跟具体的编译器有关。因此,我们应该尽量避免某些基于实现环境依赖的行为(比如当前编译器认为 int 为 16 位,在实现中就按 16 位的大小来处理 int)。这样的程序是不可移植的,而且编译器很可能找不出这样的错误。

unsigned+singed 运算导致的类型转换

如果进行 unsigned+singed 运算,结果将会被转换为 unsigned 类型。也就是说,如果结果为负数,会进行取模运算。

如无必要,请不要把 unsignedsigned 类型混淆运算。

Literals

字面值常量Literals) 实际上指值不变的变量,也就是常量。字面值常量的类型和值决定了它自身的数据类型。

整型 / 浮点型的字面值

整型和浮点型的字面值可以写成十进制(decimal)、八进制(octal)或者十六进制(hexdecimal)的形式,比如:

20 decimal //signed by default
024 octal //start with 0
0x14 hexadeicmal //start with 0x
当本类型常量为八进制或者十进制的时候,其数据类型为最小的可以容纳下该数据的类型(比如 int 能容纳下,就是 int,否则就尝试 long,以此类推)。

技术上来说,十进制下的常量不会为负值。也就是说,符号不包含在字面值常量中,而是一种对字面值常量取负的运算。

浮点型的字面值为小数,可以由科学计数法的小数表示。

字符 / 字符串字面值
  • 字符 / 字符串的字面值由单引号 / 双引号之间的内容表示。
  • 字符串比看上去的字面值长度要大 1,因为编译器会在字符串末尾加 \0 作为结束的标记。
  • 如果字符串之间仅由空格、缩进、换行符分割,那么实际上他们是一个整体。
转义序列

Escape Sequence 是 char / string type literal,因此输出的时候需要加上 quote.

Ref:ecsape sequences

转义序列ecsape sequences)指需要打印(不可打印字符或特殊字符)时需要用到的命令,比如 \n 等等。所有的这些命令必须要使用 \ 开头才能生效。

Numeric escape sequences

也被称为 generalized escape sequence。该序列通过两种组合方式来表现不同的不可打印字符(总之就是用数字来表示对应的字符):

  • \x 起头,后面接 1 位或者更多位的十六进制数,比如 \x4d
  • \ 起头,后面接 1位或者更多位数的八进制数,比如 \115

上面的数对应了字符的字符值(ascii 值,简单的对照表

变量

变量被存储于计算机里,可以被计算机做修改操作。C++ 中变量的一般包括变量( Variables )和对象( Objects )

变量的定义与声明

在C++中,变量的定义Definition )与声明Declaration )是分开的。C++ 在编译的过程中是支持分别编译Separate Compliation) 的。为了支持这种行为,C++ 严格区分了定义和声明声明指定了变量的名字,而定义建立了与之名字对应的实体(分配存储空间,赋予初始值)。 所以,任何带有初始值的声明,都是定义。

因此,我们通过在一个文件中定义一个对象(变量),然后在其他文件中需要使用的时候声明便可以使用了。正常的使用流程是,首先做出定义(存到指定的 header),然后在到需要使用的地方声明一遍即可。

keyword extern

如果希望只声明而不定义变量,可以使用 extern 关键字。有两点要注意:

  • 如果在含有 extern 的声明中进行了赋值初始化,那么表达式就会从声明转化为定义extern 的功能将会被覆盖
  • extern 不能用于函数内部

extern int i; //declaration but not definition 
extern int i = 10; //definition, extern has been overridden.

C++ 是静态语言,因此在编译的时候编译器必须知道变量的类型。编译过程中编译器会检查程序中的运算是否支持当前变量,如果不支持则会报错。

整型变量的定义和初始化

在C++中,初始化和赋值是两个完全不同的概念。初始化发生在对象创建的时候,而赋值是替换已存在对象的值。

初始化的形式

C++提供了4种形式的整形变量初始化:

int a = 0; // expression
char c = {'x'}; // { initializer-list }
int b{0}; // { initializer-list }
char d('y'); // ( expression-list )

List 初始化

其中带 curly braces 的初始化形式是新标准, 称之为 List Initialization。 C++ Primer 的作者推荐使用这种形式。原因是 List 初始化自带类型校验:List 初始化不允许类型转换 (Type Conversion) 带来的 Narrowing Conversion。比如:

double x = 3.1415926;
int a{x}; // error: narrowing conversion
int b(x), c = x; //ok: b and c are truncated

默认初始化

如果在初始化的时候没有制定初始化的值,那么编译器就会对变量做默认初始化( Default Initialization )。默认初始化的值跟以下两点有关系:

  1. 对象初始化的位置:算术类型定义在函数外部的,默认值为 0,定义在函数内部的,会导致 undefined
  2. 对象的类型: 上述的规则是针对于 Build-inType 和没有定义默认初始化的自定义类型来说的。如果自定义类型(比如类)中有默认构造函数进行初始化,那么该类型的默认初始化值就是该类型定义的初始值
Identifiers / 如何命名

总的来说,Identifiers 指广义范围内变量 / 对象的名字。Identifiers 有几个特性:

  • 必须以字母letters)或者下划线underscore)开头。
  • 区分大小写。
  • 不能与系统保留的关键字重名。

一些约定俗成的规矩:

  • Identifiers 需要表意
  • variable Identifiers 一般情况下小写
  • class Identifiers 一般开头大写
  • 较长的 Identifiers 使用下划线或者骆驼写法区分,比如 student_loan 或者 studentLoan

Scope of a name

如果希望同一个 name 对应不同的实体,可以使用 Scope 实现。通过在指定的 scope 内声明变量,就可以在指定的作用域内绑定name 与实体。作用域为的 scope 的范围,其通过 curly brace来指定。有几个例子:

  • main{},自身就是一个 scope。这种定义在所有 scope 外部的 scope,称之为 Global Scope。只要程序正在运行,该类 scope 就会一直存在。
  • 在 scope 中定义的变量,其作用域范围从其被看见(声明)的位置开始直到该 scope 结束。这种变量具有 block scope

变量 / 对象的定义最好是放置在其对应的 scope 附近。这样可以提高程序的可读性,也更容易给与该变量/对象一个有用的初始值。

Nested Scope

scope 是可以嵌套的。外部的被称为 outter scope,内部的则被称为 inner scope。通常情况下:

  • outter scope 定义的变量可以被 Inner scope 使用
  • inner scope 可以定义与 outter scope 名字相同的变量,并重新定义,也就是就近使用

如果在 inner scope 已经做了与外部变量重名的定义,但又要使用外部重名的变量,可以使用 Scope operator :: 调用外部变量:

#include <iostream>
int reused = 42;
int main(int argc, char const *argv[])
{
	std::cout << reused << std::endl; //print the outter reused, the 42
	int reused = 0;
	std::cout << reused << std::endl; //print the inner reused, the 0, outter definition overwrited
	std::cout << ::reused << std::endl; //print the outter reused, the 42, explictly accesss the outter variable

	return 0;
}

复合类型

复合类型Compound Type )是指基于某种指定的类型而定义下来的类型。C++ 中 比较重要的复合类型有:指针Pointer )、引用Reference)。

引用

引用定义了一个对象的别名Alternative Name)。我们可以把引用的定义看做是一种绑定(bind)。因此在定义引用的时候,必须对引用进行初始化。比如:

int ival = 1024; //object will be bind.
int &refVal =ival; //a reference will bind to ival
int &refVal2; // error: no binding object, reference must be initialized.

引用的定义

我们用符号 & 来定义引用。引用的定义方法如下:

int i = 1024, i2 = 2048; //two int objects that will be used for bind
int &r = i; &r2 = i2; // r是i的引用,r2是i2的引用。

引用的几个注意点
  1. 引用必须指向一个对象(必须用对象来初始化 / 必须绑定对象),literal 不能直接引用。
  2. 引用自身不是对象,是别名(所以不存在引用的引用)。
  3. 引用的类型必须和被引用的对象类型一致。

Reference to const 是可以直接引用 literal 的。 Literal initialization for const references

指针

指针(Pointer)与引用类似,也是通过间接方式访问对象(通过地址)。但是指针跟引用不同在于,指针本身自己是个对象。所以,指针具有对象(变量)的一切性质:可访问,可赋值,等等。 相较于引用,指针拥有以下特性:

  • 可以被赋值和复制
  • 可以改变指向的内存位置
  • 定义的时候不需要被初始化(不用绑定已存在对象)

类似 build-in type,函数内的指针默认初始化也会导致 Undefined

Ref:What are the differences between a pointer variable and a reference variable in C++?

指针的定义

指针变量的定义使用 * 符号进行定义。同时,指针是用于存储内存地址的对象,所以指针的初始化值必须是一个地址。我们用 & (在定义中是引用,在运算中是取地址操作符)来得到对象的地址,那么指针的一般初始化形式就应该是:

int i = 1;
int *p = &i; //store the address of i in pointer p
同时定义几个指针的时候,每个指针前面必须加上 * 号,例如:
int *p1, *p2;
需要注意的是:

  • 引用自身并没有地址,因此不能创建一个指向引用的指针.
  • 指针在 initialization / assignment 的时候,类型必须与所处指向地址的数据类型相同(指向不同的类型,或者指向指向该类型的指针,都是不行的。)

double dval;
double *pd = &dval; // ok: initializer is the address of a double, address to address
double *pd2 = pd; // ok: initializer is a pointer to double, address to address
int *pi = pd; // error: types of pi and pd differ
pi = &dval; // error: a pointer pointed to double can't assign to an int pointer

指针的形式
  • 第一类,指向某个对象
  • 第二类,指向某个位置,该位置正好在某个对象占用空间的下个地址格
  • 第三类,不指向任何对象,也就是空指针
  • 除以上三种,都是无效指针(对此类型指针操作都将导致 undefined 行为)

需要注意的是,访问所有没有指向明确对象的指针都属于 undefined 行为。

使用指针访问

*,在运算中被称为解引用运算符(Dereference Operator)。我们使用该运算符来获取指针指向的对象中的内容:

int ival = 42;
int *p = &ival; // 这里的 * 是定义,是定义指针的标识,不是解引用。这里的 & 是运算,是取对象地址的运算,不是定义引用的标识。
cout << *p; // 这里的 * 是运算,是解引用,是将指针指向的对象中的内容取出。

再次强调,安全访问指针指向的内容的前提是该指针必须指向一个存在的对象(第一类指针)。换句话说,尽量使用初始化后的指针。访问指向未初始化内容的指针会带来一系列的后果,包含但不限于:

  • run-time crash,而且非常难以 debug
  • 指针可能指向正在被程序其他部分访问(使用)的内存,导致非法的内存空间申请带来的冲突。

如果没有可以指向的对象,尽量将指针初始化为空指针。

关于 '*' 和 '&' 的用法
  • 定义下的用法(Type modifier):
    • * 用于定义指针,int *p; 这里的 * 代表 p 是一个指针。
    • & 用于定义引用:int &r = i;。这里的 & 代表 r 是绑定到 i 的引用。
  • 运算下的用法
    • 访问指针/引用指向的元素的时候(Derefenece): *p = i; 。这里的 *解引用运算符
    • 取地址的时候:p = &i;。 这里 & 代表取 i 的地址。

总的来说,就是在声明的时候,&* 都是用于定义复合类型的。在表达式中,这两个符号都是运算符(* 还可以用作乘法运算),一个是解引用(访问地址中的内容) 一个是 取地址(访问指针中的地址值)。

空指针

如果定义指针的时候不明确指向对象,我们可以用到 null 指针。null 指针不指向任何地方,只是用来初始化。在C++11中,作者推荐用 nullptr 来取代 NULL (C 语言中 cstdlib)对空指针进行初始化。

为什么在C++中使用 nullptr 取代 NULL 可以参考这篇文章: C++11中的 nullptr 详解

某些情况下给指针赋值 0 也等同于创建空指针的效果。但需要注意的是,这里的 0 只能是 literal,将值为 0 的 int 类型变量赋值给指针是非法的。

指针的赋值

对指针的赋值和对引用的赋值是不同的。引用一旦绑定,就不可能再进行引用对象的更改,任何对引用的操作都是对其初始对象的操作。指针则不同,指针是可以通过赋值来改变指向位置的:

int i =42;
int *pi = 0; //initialize pointer pi as a null pointer.
int *pi2 = &i; //initialize pi2 as a pointer points to the addresss of i
int *pi3; //bad pratice, pi3 would be uninitialized if it is not a global pointer
pi3 = pi2; // pi3 and pi2 point to the same address
pi2 = 0; // pi2 points is null pointer now.
一个通用的规则是,赋值改变的是赋值运算符左边算子的内容。这个算子是指的一个整体。比如:
pi = &ival; //改变的是 pi 的内容
那么如果是如下形式:
*pi = 0; //改变的是 pi 指向的内容
由于这里是赋值运算,那么左边的 * 被视作解引用运算符,左边整体可以被视作被 pi 指向的对象。则上面的语句实际上将 pi 指向的对象中的内容改写为了 0,但该对象在内存中的地址,也就是 pi 中存储的地址,是没有发生变化的。

指针的其他运算
  • 指针在某些情况下可以用做条件判断。当指针为空指针的时候,条件为假,反之为真。
  • 指针之间可以进行比较(通过比较运算符 == 等等)。两者相等的条件是两个指针指向的地址相同(或者是指向相同对象的二类指针,或者是都指向空指针)

还有一类特殊类型的指针 *vold。这种类型的指针与普通指针不同的地方在于它可以指向任意类型的对象(有点 O型血的那么个意思)。以下是几种该指针的应用:

  • 与其他指针比较
  • 传递给函数或者作为函数的返回值
  • 赋值给另外一个 void 类型的指针

这种指针的局限性在于,不能通过 void 指针来对其指向的内容进行操作,因为对指向对象操作的前提是必须知晓该对象的类型。

复合类型的声明

定义多个变量的情况

在定义(* 或者 & 作为 type modifier 使用)的时候,type modifier 只对最近的一个变量起作用,因此定义多个变量需要在每个变量前都加上 type modifier:

int* p; //bad practice, misleading
int *p0; //good practice to define a pointer with int type
int* p1, p2; //p1 is a pointer , p2 is an int variable
int *p3, *p4; //good way to define multiple pointers in 1 statement.

在定义复合类型的时候,Type Modifier 是每个变量不可缺少的一部分。本书推荐使用 type modifier 靠近 变量名的写法。

指向指针的指针

指针本身是对象,也会拥有自身的地址。因此定义指向指针的指针也是可以的:

int i = 1024;
int *pi = &i;
int **ppi = &pi; //&pi is the address of the pointer, not the address which pointer points




当然,如果需要通过 ppi 来访问 i 的内容,我们需要解引用两次:

std::cout << **ppi << std::endl;

指针的引用

引用不是对象,因此不存在指向引用的指针。但反过来则可以:

int i = 0;
int *p;
int *&r = p; //reference to pointer
r = &i; //r is an altertive name to p, so assign the address of i to r makes p points to i.
*r = 0; // dereference r means dereference p, which yields i. then changes i to 0
复合类型的混用向来比较让人困惑。书上讲了一个阅读的规则,那就是从右往左读定义。也就是说,离变量名右侧最近的type modifer,决定了该定义的复合类型(比如下面在 *&r 中的 & )。

比如这个例子:
int *&r =  i;
来分析一下这个步骤:

  1. 首先找到 r,确定变量名
  2. 往右看看到 &,确定定义的是名称为 r 的引用
  3. 继续往左看则是声明的引用 refer 的指针类型:int*,这里实际上写成 int* &r = i 更好理解。

因此这就是一个与整型指针绑定的引用的声明过程。

Const 修饰符

const 修饰符(Qualifier)是用来定义我们不希望被改变的变量。因为这个变量不能再建立之后改变,所以我们只能在初始化的时候给其赋值,也就是说:const 修饰的变量必须被初始化。

const int i = 10; //ok
const int j; //error, const variable must be initialized.
i = 20; //error, attempt to write to const object
我们也可以用表达式来作为 const 初始化的值,比如:
const int k = getsize();//initialized at runtime

const 只对当前初始化的变量起作用,旨在限制对当前变量的修改操作。只要不涉及到更改变量的操作,const 对象与普通对象的操作是一致的。

Const 作用范围是文件以内

由于编译的时候,const 变量会被编译器自动替换为其值,编译器会在编译期寻找 const 变量对应的初始值。因此 const 修饰的变量是必须是编译器可见,即拥有完整定义的变量。由于 C++ 支持分离编译,如果该变量在多个文件中均存在,那么需要在每个文件中都有定义。但问题在于,C++ 遵循唯一定义规则,因此需要通过两种方法来解决:

  1. 针对每个文件单独创建一个 const 变量。
  2. 通过 extern 关键字共享唯一的 const 变量。

第二种情况下,为了使用外部的 const 变量,我们需要定义以及声明两个步骤:

  • 在指定的源文件(.cc / .cpp)中定义 const 变量。这里的 const 变量可以接受对象,甚至函数:

/*file.cc, defines and initializes a const, that is accessible to other files*/
extern const int bufSize = fcn(); //initializer can be a function

  • 之后通过头文件对该变量进行声明,来达到共享的目的:

/*file.h, declar the bufSize. Include the header to share variable bufSize*/
extern const int bufSize;
一个简单的多文件共享全局变量的例子如下:

  • extern_const_test_def.cpp 文件用于定义需要共享的全局变量
  • extern_const_test_def.h 用于声明需要共享的全局变量
  • extern_const_test.cpp 用于使用共享的全局变量

/*extern_const_test_def.cpp*/
extern const int bufSize = 512;

/*extern_const_test_def.h*/
extern const int bufSize;

/*extern_const_test.cpp*/
#include <iostream>
#include "extern_const_test_def.h"
int main(int argc, char const *argv[])
{
	std::cout << "The size of global buffer is: "<< bufSize << std::endl;
	return 0;
}
编译以及链接的过程:
#分别对 cpp 文件进行编译
#如果没有 main 函数,g++ 需要使用 -c 来编译 c++源文件
g++ -c extern_const_test.cpp
g++ -c extern_const_test_def.cpp
#对得到的 .o文件进行链接
g++ -o a.out extern_const_test.o extern_const_test_def.o
#访问
./a.out

Const、指针和引用

Reference to const

对 const 的引用指“reference that refers to a const type”,基于const 修饰类型的引用。这类引用确保了不能通过该引用修改绑定对象的内容。

const int ci = 1024;
const int &r1 = ci; // defines a refernece refers to a const variable
int &r2 = ci; // error. a plain reference can't refers to a const object
绝大多数情况下,引用的类型必须与被引用的对象一致;但有一种例外:如果被绑定的对象的类型,能够通过类型转换变得与引用的类型一致,那么这样的对象也是可以用于引用的初始化的:
int i = 42; //variable i
const int &r1 = i; // r1 is the reference refers to a const varible, initialized by a non-const int object
const int &r2 = 42; // ok. initializor can be convert to const int
const int &r3 = r1*2; //ok. initializor can be convert to const int
int &r4 = r1 * 2; //error, const int initializor can't be convert to non-const int.

这种情况下,引用的绑定实际上分为两步:

  1. 创建一个临时的 const 对象,使用目标对象对其进行初始化;
  2. 将引用与该临时的对象绑定

类型转换将发生在第一步,也就是说临时对象的类型会与引用一致。那么实际上,我们定义的引用绑定的是一个不可更改的,经过类型转换的临时对象。该对象显然不能被 refer to non-const 的引用绑定。

只要被绑定的对象可以转化为 const 类型,那么无论被绑定的对象是不是 const, 都不会影响 reference to const 的初始化(注:literal 也可以通过上述步骤转化为临时对象!)。下面是一个实例:

int i  = 42;
int &r1 = i; //ok, non-const bind to non-const
const int &r2 = i; // ok const bind to non-const
//*now, r1 and r2 refer to the same int variable i*//
r1 = 0; //ok, we can modify i through non-const reference r1
r2 = 0; //error, we can't modify i through const-const r2 
/*and finally i is changed to 0*/

可以将 reference 理解为门,reference to const 是一扇关闭的门,告诉你不能通过这扇门去改变屋里的环境。至于有没有其他门,这扇门保证不了。

Ref:const reference can be assigned an int?

Const 和指针

指针是对象。所以 const 和指针连用就分成了以下的两种情况:

  • pointer to const:与 reference to const 类似,指不能通过该指针去修改被指向的内容(指针绑定的内容)
  • const pointer:指该指针指向的地址无法被改变(指针本身)
两种情况的初始化

这两种组合的初始化也比较容易混淆:

int i = 10;
const int *p = &i; //define a pointer to const
int *const q = &i; //define a const pointer that point to i
这里我们的从右向左看的方法依然适用(C语言中还有 int const *p, 这个写法跟 const int *p 等同)。

还有一个用法:
const int *const ptr = &i; // ptr is a const pointer,its value can't be changed, and the value pointed by it cannot be changed through itsself(the pointer).
同样先前说到的初始化的时候类型要匹配规则同样适用于这里。比如:
const double x = 3.1415;
double *xptr = &x;// error, type not match.

Top_level 和 Low_level

根据 pointer 与 const 的两种组合方式,我们可以定义两种等级:

  • 自身为 const 的等级为 top level,比如 const pointer
  • 无法通过自身修改对应内容的等级为 low level, 比如 pointer to const

示例代码如下:

int i = 10;
// const pointer, its-self cannot be changed -> this const produces a forbidden operation of manipulating the object -> top_level
int *const p = &i; 
// pointer to const, the pointer its-self can be changed, but the value of i can't be changed by using the pointer(expression)-> this const produces a forbidden operation of manipulating the i(what the expression did once)-> low_level
const int *q = &i;
还有一个例子:
const int *const ptr = &i; //right-most const is top_level, left_most is not
右边的 const 定义了 指针,因此是 top level, 左边的 const 定义了该指针指向的是一个不能改变的 int 值,因此属于 Low level。

之所以提出这两个 level, 是因为绝大多数的,带 const 初始化/赋值的合法性,都可以使用该概念来判断:

首先,拷贝 const 对象的时候,top-level 可以无视,因为拷贝并不影响 const 对象的内容:
int i = 0;
const int ci = 42;
i = ci; //top-level ci is ignored.
其次,low-level 的 const 在任何时候都不能忽略。如果想要成功拷贝,那么等号两边都必须是 Low-level 的 const,或者有可以转换为 const 的对象;因此,non-const 可以转化为 low-level 的 const,但反过来不行(也就是不能拷贝 const 到 non - const):
/*接上面*/
int &r = ci; //error, ci is on low-level, r must at the same level.
下面是特殊情况,指向 const 且自身是 const pointer:
/*接上面*/
const int *p2 = &ci; 
const int *const p3 = p2;
该指针左边的 const 为 low-level, 右边的 const 为 top-level。因此:

  • p3 可以初始化、赋值 p2, 拷贝的时候 top-level 自动忽略了。
  • p2 可以初始化 p3。两边的 low-level 匹配。

个人感觉拷贝的过程中,只要是 top-level 的 const 就可以不用管。因为 top-level 的 const 只强调对象本身的不可更改性;也就是拷贝过程中只需要考虑两边 low-level 的匹配就行。通俗点来说,如果 low-level const 是不能打开,但可以装到任意的墙上的门。那么 top-level const 就是让门自身无法被移动和摧毁的力场。这样一来,我们就有两个任务:

  • low-level const 对象初始化 top | low-level 混搭的对象:这种情况就像是工头指定了位置让你装门,门外已经有力场保证了你的门无法被移动和摧毁。你只要保证你的门打不开就对了。
  • top | low-level 混搭的对象初始化 / 赋值 low-level const 对象:工头让你找一扇不能开的门随便找个地方装了。同样你只需要保证你的门打不开,至于那个门之前在不在力场里工头根本不 care。

const 语句的合法性,归根结底诠释了一个权限的概念。如果对象的赋值算是一个链条,那么 low-level const 的对象会对其下游(左边)对象有同样的限制。只要下游存在修改 low-level const 对象的风险,那么编译器就会报错。

Constexpr

这一节有一个概念,常量表达式Const Expression)。 常量表达式是指在自身值无法改变,且初始化值在编译阶段就可以确定的表达式,比如 literal、被常量初始化的 const 对象等等。

因此,所有非常量的表达式,或者需要在 Run Time 下才能确定初始值的表达式(比如函数),都不是常量表达式。比如:

int a = 10; // a is not const object
const int b = getsize(); // getsize() fincution is an uncertain initializor during the compling time. 
const int c = 100; // const expression

Constexpr Variables

因为在大型系统中,判断一个表达式是不是 const 比较麻烦–你觉得你定义的常量是被“常量表达式”初始化的,但往往并不是。为了确保得到真正的常量表达式,C++11 新标准中提出了一个关键字 constexprconstexpr 会要求编译器验证表达式是不是常量表达式。被 constexpr 修饰的变量定义表达式,必须满足常量表达式的两个特性,即:

  • 声明的变量必须是明确的 const (Top const)
  • 该变量必须被一个常量表达式初始化。

A constexpr specifier used in an object declaration or non-static member function (until C++14) implies const. A constexpr specifier used in a function or static data member (since C++17) declaration implies inline.

constexpr 的优势主要在于能将初始化的过程放到编译期,而不是在 run-time 期让程序自己计算,这样可以提高客户端程序的效率。但这样带来的弊端就是定义下来的量是绝对无法更改的(除非重新编译)。因此,constexpr 才对定义的过程有要求:只有一个不可更改的,并在编译期就可以计算出结果的变量,才称之为 constexpr 变量。

Limitation of constexpr

因为常量表达式的初始值需要在编译期确定,因此 constexpr 的对象的初始化会有一些局限性:

  1. constexpr 只接受 Literal Types (比如算术类型和复合类型)
  2. constexpr 不支持校验自定义对象类型(比如类)
  3. constexpr 指针不能指向函数内定义的变量(地址不确定)
指针与 constexpr

在定义 constexpr 指针的时候,constexpr 特指指针的类型(top-level),而不是指针指向对象的类型:

const int *p = nullptr; // p is a pointer to const int
constexpr int *q = nullptr; // q is a const pointer to int

愚见:从结果上来讲,constexpr pointer 与 const pointer 对指针自身的限制都是一样的:都定义了一个无法更改指向地址的指针。但 const pointer 指向的地址中的内容是可以在 run-time 时期改变的;constexpr 只是确保初始化在编译期内进行。如果能确定指针指向的初始值全程不变,比如 const int *const p 这样的形式,完全可以用 constexpr 替代。

从这个角度来看,实际上绝大部分情况下,如果只是对指针本身的不可更改性有要求,那么 constexpr 是可以取代 const的;如果仅仅是要求无法通过指针来修改某对象,那么就使用 const。也就是:

  • top + low level 使用 constexpr
  • low-level 使用 const
  • top-level 酌情使用

const int *const p 这样的形式换成 constexpr 来定义可以写成这样:

constexpr const int *p = &i;
Ref:Constexpr - Generalized Constant Expressions in C++11

类型处理

类型随着程序的复杂性变大会出现两个比较显著的问题:

  • 类型名会变得越来越长,变得难懂并且容易输错
  • 类型的形式太复杂导致需要看完类型的内容才能明白

Type Alias

解决第一类问题可以用到 Type Alias。 总的来说,Type Alias 就是为某种类型找一个同义词,通过简化复杂的类型使其更为易用。

有两种方法可以定义 Type Alias:

  1. typedef,格式为: typedef + 被定义的类型 + 同义类型

typedef double wages; // wages is a synonym for double''
typedef wages base; //base is synonym for double
typedef wages *p; // p is synonym for double*

  1. C++11的新标准:using
    using SI = Sales_items;// Si is a synonym for Sales_items
    SI item; // same sa Sales_item type
Pointer, const and Type Aliases

Type Alias 也可以与复合类型,const 一起使用,但需要注意的是,const 修饰的是 Type Alias。因此在与指针连用的时候要注意,const 修饰的永远是 Type Alias 中的变量类型,比如:

typedef char *pstring; 
const pstring cstr = 0; 
// const 修饰的是经过type alias化的指针
// const pstring -> const char *,cstr 是指向 char 的常量指针
// 而不是指向常量 char 的指针(char const*)

Auto Type Specifier

在很多时候,我们想自己决定表达式的类型是非常困难的。C++11引入了一种新的类型说明符 auto,让编译器自动决定表达式的类型。因为编译器需要根据变量的初始值来判断类型,所以 auto 声明的变量必须初始化
和别的说明符一样,auto 定义多个变量的时候,所有变量必须是同一类型

Auto、复合类型、Const

auto 与复合类型还有 const 连用的时候,有几个点需要注意:

首先,使用引用对某类型进行初始化的时候,auto 会将该类型推断为引用 refer to 的类型

int = 0, &r = i;
auto a = r; // a is an int
其次,在有 const 参与的类型初始化中,auto 会自动忽略 top-level const:
const int ci = i, &cr = ci;
auto *p = &ci; // p points to a const variable, low-level
auto b = i;// b is an int
auto c = cr; // c is an int
auto d = &i; // d is an int*
如果需要保留 top-level const, 需要显式的添加上 const:
const int i = 0;
const auto a = i;
除此之外,low-level const 不会被 auto 忽略,因此 auto 不会将 low-level const 的引用类型识别为其绑定的类型:
const int ci = i;
auto &g = ci; // g is an reference refer to const int ci
auto &h = 42; // error, plain reference can be bound to a literal
const auto &j = 42;// reference to const can be bound to literal

Decltype Type Specifier

decltypeauto 类似,也用于自动判断变量的类型。与 auto 不同的是,decltype 是通过表达式编译期推断出变量的类型。因为 decltype 并不要求执行表达式,因此不需要初始化。那么有时候我们想让编译器自己推断一个表达式的类型,但是又不想用这个表达式做初始化,这时候我们就要用 decltype 了。decltype 的用法如下:

decltype(f()) sum = x; //sun has whatever x returns
auto 不一样,变量是什么类型,decltype 就返回什么类型,包括 top_level const 和 引用类型:
const int ci = 0; &cj = ci;
decltype(ci)  x = 0; //x is const int
decltype(cj) y = x //y is reference to const int x
decltype(cj) z; //error, exception; if z requires initialize, then the initialization is a must.
注意 decltype 与引用连用的时候需要初始化(因为引用需要)。与 auto 不同的是,decltype 会返回其表达式的类型,因此这里返回的是引用,所以必须初始化。

Decltype 和 表达式

decltype 的返回类型是根据表达式的表现形式来判断的。我们设定 e 的类型为 T

  1. 如果表达式是函数,那么返回的就是函数返回值的类型
  2. 如果 decltype(e) 中,e是一个实体(Entity)的名字(id 表达式),那么 decltype 返回的是名字为 e 的实体(Entity)的类型。
  3. 如果 e 是表达式:
    1. 如果表达式 e 是个左值,decltype(e) 返回的就是 T&T 的引用),注意:如果有括号,括号里的表达式将被视为左值
    2. 否则,返回的是 e 的返回结果的类型。

Refs:

一些例子:

int i = 10;
int &r = i;
int *p = &i;
decltype(i) x1 = 0; //decltype ( entity )形式,返回 i 的类型int
decltype(r) x2 = x1;//decltype ( entity )形式,返回绑定int变量的引用(r绑定的是i)
decltype(r+0) x3;//decltype ( expression )形式,r+0的结果类型是int,但r+0不能作为lvalue,所以返回int
decltype(*p) x4 = i;//decltype ( expression )形式,*p是lvalue,那么返回的是T& .*p表示的是int变量i的值,那么返回的类型就是对int变量的引用。
decltype((i)) x5 = i;// if the name of an object is parenthesized, it is treated as an ordinary lvalue expression.(i)是lvalue,那么返回T&,也就是int的引用。
decltype('a') x6;//e是prvalue(pure value),返回e自身的类型。

赋值表达式也是一种左值,因此会返回引用。引用的类型由赋值操作符左边的变量决定。比如 decltype(i=0) 会返回int&。

Defining Our Own Data Structures

一些注意事项:

  • class 定义的末尾要加一个 semicolon
  • object 的定义最好与 class 的定义分开
  • 因为 class 的复用性,因此 class 的定义通常应该放到 header 中。
使用Preprocessor解决多重包含

Preprocessor 继承至 C,指一系列在编译器改变源代码之前运行的程序,比如 #include。Preprocessor 有一个用途是解决多重包含。

在我们的 C++ 程序中,资源往往来自不同的头文件,某一些头文件之间可能会有互相的依赖。比如我们自定义的头文件 Sales_item.h,为了输出字符串,必须包含 string 头文件。如果在接下来的某个源文件中需要同时使用 Sale_itemstring 中定义的资源,那么该文件就会多重包含 string 头文件。

多重包含会带来什么问题?

C++ 中,某对象的声明可以有很多次,定义只能有一次。多重包含往往会导致某个对象的重复定义,这是不被允许的。

为了解决这个问题,C++ 提供了 Header guardsHeader guards 使用 preprocessor 提供的变量来确保当前 header 只被包含一次:

#ifndef SALES_DATA_H //检测之前该 header 有没有被定义(包含)
#define SALES_DATA_H //如果 header 是被第一次包含,那么证明该 header 未被定义过,执行下列语句
#include <string>
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
#endif
其中 #define 用于存储当前变量的状态,有两种情况:定义或者未定义。而 #ifndef / #ifdef - #endif 字符段用于判断当前的变量是否被定义。#ifdef 表示如果当前变量已经定义进行下一步, #ifndef 表示当前变量没有被定义则进行下一步。因此上边代码实际上会这么运行:

  • 如果当前程序第一次包含 Sales_data.h 头文件,#ifndef 测试成功,对 SALES_DATA_H 进行定义,并拷贝 Sales_data.h 到目标程序。
  • 如果已经包含了 Sale_data.h 头文件,那么 #ifndef 测试会失败,那么代码段之间的内容会直接被忽略掉。

需要注意的是,Preprocessor 中的所有变量,必须是唯一的,并且必须全大写(避免与其他名字发生冲突)。

所有的 Header 文件都应该使用 header guards。