第02章_变量和基本类型

基本内置类型

  • C++ 定义了一套包括算术类型空类型在内的基本数据类型。算术类型包括了字符、整形数、布尔值和浮点数;空类型不对应具体的值,仅用于一些特殊的场合

算术类型

  • 整型和浮点型: bool, char, wchar_t, char16_t, char32_t, short, int, long, long long, float, double, long double
  • 尺寸在不同机器上有所差别
  • 除去布尔型和拓展的字符型外,其他整型可以划分为带符号的和无符号的
  • char 类型既可能是无符号类型,又可能是有符号类型,由编译器决定

类型转换

  • no bool –> bool: 初始值为 0,则结果为 false,否则结果为 true
  • bool –> no bool: 初始值为 false,则结果为 0,否则为 1
  • 浮点数 –> 整型数: 近似处理,结果值仅保留浮点数中小数点之前的部分
  • 整型数 –> 浮点数: 小数部分记为 0,如果该整数所占的空间超过了浮点类型的容量,精度可能有损失
  • 当赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数
  • 当赋给带符号类型一个超出它表示范围的值时,结果是为定义的

字面值常量

  • 形如 42 的值被称作字面值常量,这样的值一望而知。每个字面值常量都对应一种数据类系,字面值常量的形式和值决定了它的数据类型

整型和浮点型字面值

  • 整型字面值具体的数据类型由它的值和符号决定
  • 默认情况下,十进制字面值是带符号数,八进制和十六进制字面值既可能是带符号的也可能是无符号的
  • 十进制字面值的类型是 int、long 和 long long 中尺寸最小的那个(前提是这种类型要能容纳下当前的值)
  • 八进制和十六进制字面值的类型是能容纳其数值的 int、unsigned int、long、unsigned long、long long 和 unsigned long long 中的尺寸最小者
  • 如果一个字面值连与之关联的最大的数据类型都放不下,将产生错误
  • 类型 short 没有对应的字面值
  • 尽管整型字面值可以存储在带符号数据类型中,但严格来说,十进制字面值不会是负数。形如 -42 的负十进制字面值,那个负号并不在字面值之内,它的作用仅仅是对字面值取负值而已
  • 浮点型字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用 E 或 e 标识。默认的,浮点型字面值是一个 double 类型

字符和字符串字面值

  • 由单引号括起来的一个字符称为 char 型字面值,双引号括起来的零个或多个字符则构成字符串字面值
  • 字符串字面值的类型实际上是由常量字符构成的数组,编译器在每个字符串的结尾处添加一个空字符(\0)
  • 两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,则它们实际上是一个整体
  • 通过添加前缀和后缀,可以改变转型、浮点型和字符型字面值的默认类型
    1
    2
    3
    4
    5
    L'a' // wchar_t
    u8"hi!" // utf-8 字符串字面值(utf-8 用 8 位编码一个 Unicode 字符)
    42ULL // unsigned long long
    1E-3F // float
    3.14L // long double

布尔字面值和指针字面值

  • true 和 false 是布尔类型的字面值
  • nullptr 是指针字面值

变量

  • 变量提供一个具名的、可供程序操作的存储空间

变量定义

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

列表初始化

  • 即用花括号来初始化变量
  • 当用于内置类型的变量时,使用列表初始化且初始值存在丢失的风险,则编译器将报错
    1
    int a{3.14};

默认初始化

  • 定义变量时没有指定初值,则变量被默认初始化,此时变量被赋予了默认值。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响
  • 内置类型的变量未被显示初始化,它的值由定义的位置决定
    • 定义于任何函数体之外的变量被初始化为 0
    • 定义在函数体内部的内置类型变量将不被初始化
  • 每个类各自决定其初始化对象的方式。而且,是否允许不经初始化就定义对象也有类自己决定。如果类允许这种行为,它将决定对象的初始值到底是什么
  • 绝大多数类都支持无须显式初始化而定义对象,这样的类提供了一个合适的默认值

变量声明和定义的关系

  • 为了支持分离式编译,C++ 语言将声明和定义区分开来。声明使得名字为程序所知(一个文件如果想使用别处定义的名字则必须包含对那个名字的声明);定义负责创建与名字关联的实体
  • 若想要声明一个变量而非定义它,就在变量名前添加关键字 extern,而且不要显式地初始化变量
  • 任何包含了显式初始化的声明即成为定义
  • 变量能且只能被定义一次,但是可以被多次声明

标识符

名字的作用域

  • 作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字。同时,允许在内存作用域中重新定义外层作用域已有的名字

复合类型

  • 指基于其他类型定义的类型

引用

  • 引用为对象起了另外一个名字,引用类型引用另外一种类型
  • 引用一旦初始化完成,将和它的初始化对象一直绑定在一起
  • 由于无法令引用重新绑定到另外一个对象,因此引用必须初始化
  • 引用本身不是一个对象,所以不能定义引用的引用
  • 正常情况下,引用的类型要和与之绑定的对象严格匹配;引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起

指针

  • 指针是指向另外一种类型的符合类型
  • nullptr 是一种特殊类型的字面值,可以被转换成任意其他的指针类型
  • 对于两个类型相同的合法指针,可以用相等操作符 == 或不相等操作符 != 类比较它们,比较的结果是布尔类型。如果两个指针存放的地址值相同,则它们相等;反之它们不相等。相等有三种可能:
    • 都为空
    • 都指向同一个对象
    • 都指向了同一个对象的下一地址
      • 需要注意的是,一个指针指向某对象,同时另一个指针指向另外对象的下一地址,此时也有可能出项这两个指针指相同的情况(不太理解)
  • void* 是一种特殊的指针类型,可用于存放任意对象的地址

与引用的不同点

  • 指针本身就是一个对象,允许对指针赋值和拷贝
  • 指针的生命周期内可以先后指向几个不同的对象
  • 指针无须在定义时赋初值
  • 和其他内置类型一样,在块作用域内定义的指针如果没有初始化,也将拥有一个不确定的值

理解复合类型的声明

  • int *p1, p2; // p1 是指向 int 的指针,p2 是 int
  • 引用本身不是一个对象,因此不能定义指向引用的指针;但指针是对象,所以存在对指针的引用
    1
    2
    3
    int *p;
    int *&r = p; // r 是一个对指针 p 的引用
    // 注: 要理解 r 的类型到底是什么,最简单的办法是从右向左阅读 r 的定义。离变量名最近的符号对变量的类型有最直接的影响,因此 r 是一个引用。声明符的其余部分用以确定 r 引用的类型是什么,此例中的符号 * 说明 r 引用的是一个指针

const 限定符

  • const 对象一旦创建后其值就不能再改变,所以 const 对象必须初始化
    1
    2
    3
    const int i = getSize(); // 正确,运行时初始化
    const int j = 42; // 正确,编译时初始化
    const int k; // 错误
  • 只能在 const 对象上执行不改变其内容的操作
  • 默认情况下,const 对象仅在文件内有效(编译时初始化的 const 变量),编译器将在编译过程中把用到该变量的地方都替换成对应的值。为了执行上述替换,编译器必须知道变量的初始值,如果程序包含多个文件,则每个用了 const 对象的文件都必须得能访问它的初始值才行。所以就必须在每一个用到变量的文件中都有对它的定义。当多个文件中出现了同名的 const 变量时,其实等同于在不同文件中分别定义了独立的变量。
  • 有些 const 变量的初始值不是一个常量表达式,但又想在文件间共享。这种情况下,不希望编译器为每个文件分别生成独立的变量,想让这类 const 对象和其他对象一样,只在一个文件中定义 const,而在多个文件中声明并使用它。解决的办法是对于 const 变量不管是声明还是定义都添加 extern 关键字
    1
    2
    extern const int bufSize = function(); // 源文件中定义
    extern const int bufSize(); // 头文件中声明

const 的引用

  • 把引用绑定到 const 对象上,称之为对常量的引用。(不能修改它所绑定的对象)
    1
    2
    3
    4
    const int ci = 1024;
    const int &r1 = ci;
    r1 = 42; // error
    int &r2 = ci; // error,试图让一个非常量引用指向一个常量对象
  • 引用的类型必须与其引用对象的类型一直,但有两个例外
    • 初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型就行(允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式)
      1
      2
      3
      4
      5
      int i = 42;
      const int &r1 = 1; // 允许绑定到一个普通对象上
      const int &r2 = 42;
      const int &r3 = r1 * 2;
      int &r4 = r1 * 2; // error,r4 是非常量引用
      • 当一个常量引用被绑定到另外一种类型上时到底发生了什么
        1
        2
        3
        4
        5
        6
        7
        double dval = 3.14;
        const int &ri = dval;
        // 此处 ri 引用了一个 int 类型的数。对 ri 的操作应该是整数运算,但 dval 却是一个 double 而非 int。因此为了确保让 ri 绑定一个整数,编译器把上述代码变成了如下形式:
        const int temp = dval; // 由 double 生成一个临时的 int 常量
        const int &ri = temp; // 让 ri 绑定到这个临时量
        // 在这种情况下,ri 绑定了一个临时量对象。所谓临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象。
        // 探讨 ri 不是常量时,若执行了类似于上述代码会有什么后果?若 ri 不是常量,就允许对 ri 赋值,这样就会改变 ri 所引用对象的值。但此时绑定的对象是一个临时量而非 dval。程序员既然让 ri 引用 dval,就肯定想通过 ri 改变 dval 的值,否则干什么要给 ri 赋值呢?所以,非常量引用初始化时类型必须一致。
  • 对 const 的引用可能引用一个并非 const 的对象(因为对象也可能是个非常量,所以允许通过其他途径改变它的值)
    1
    2
    3
    4
    5
    6
    int i = 42;
    int &r = i;
    const int &cr = i;
    r = 0;
    std::cout << i << ' ' << r << ' ' << cr << std::endl; // 0 0 0
    // cr = 1; // error

指针和 const

指向常量的指针(const type *)

  • 与引用一样,也可以令指针指向常量或非常量。类似于常量引用,指向常量的指针不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针
    1
    2
    3
    4
    const double pi = 3.14;
    double *ptr = &pi; // error
    const double *cptr = &pi; // yes
    *cptr = 3; // error
  • 指针的类型必须与其所指对象的类型一致,但是有两个例外
    • 允许令一个指向常量的指针指向一个非常量对象
  • 和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变

常量指针(type *const)

  • 指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定位常量。常量指针必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。
    1
    2
    int a;
    int *const pa = &a; pa 将一直指向 a

顶层 const

  • 指针本身是一个对象,它又可以指向另外一个对象。因此,指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。
  • 顶层 const 表示指针本身是个常量
  • 底层const 表示指针所指的对象是个常量
  • 顶层 const 可以表示任意的对象是常量,这一点对任何数据类型都适用
  • 底层 const 则与指针和引用等复合类型的基本类型部分有关
  • 指针类型既可以是顶层 const 也可以是底层 const
    1
    2
    3
    4
    int i = 0;
    int *const p1 = &i; // 不能改变 p1 的值,顶层
    const int ci = 42; // 不能改变 ci 的值,顶层
    const int *p2 = &ci; // 允许改变 p2 的值,底层
  • 当执行对象的拷贝操作时,常量是顶层 const 还是 底层 const 区别明显
    • 顶层 const 不受什么影响(拷贝操作并不会改变被拷贝对象的值,因此,拷入和拷出的对象是否是常量都没什么影响)
    • 底层 const 的限制不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层 const 资格(等号左边的值必须具有底层 const,否则赋值之后没有底层 const 就能修改值了),或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换为常量,反之则不行。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      int i = 0;
      int *const p1 = &i; // 不能改变 p1 的值,顶层
      const int ci = 42; // 不能改变 ci 的值,顶层
      const int *p2 = &ci; // 允许改变 p2 的值,底层
      const int *const p3 = p2; // 底层 和 顶层
      const int &r = ci; // 底层
      int *p = p3; // error,p3 包含底层 const 定义,而 p 没有
      p2 = p3; // yes,p2 和 p3 都是底层 const
      p2 = &i; // yes,int* 能转换成 const int*
      int &r = ci; // error,普通 int& 不能绑定到 int 常量上
      const int &r2 = i; // yes,const int& 可以绑定到一个普通 int 上
  • const type &var 是顶层 const(const 使得 var 内部的值不能改变,引用使得 var 本身不能改变)。顶层 const 和底层 const 应该是针对指针

constexpr 和常量表达式

  • 常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。
  • 显然,字面值属于常量表达式
  • 用常量表达式初始化的 const 对象也是常量表达式
  • 一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定
    1
    2
    3
    4
    const int max_files = 20;        // yes
    const int limit = max_files + 1; // yes
    int size = 27; // no,这是变量
    const int sz = get_size(); // no,get_size()不是常量表达式

constexpr 变量

  • 允许将变量声明为 constexpr 类型以便编译器来验证变量的值是否是一个常量表达式
    • 为了防止用 const 声明时,可能不是有常量赋值的隐藏 bug
    • 声明为 constexpr 的变量一定是一个常量,而且必须用常量表达式初始化
  • 定义于所有函数体之外的对象其地址固定不变,能用来初始化 constexpr 制作
  • constexpr 声明中如果定义了一个指针,限定符 constexpr 仅对指针有效,与指针所指的对象无关(常量指针,不能跟换指针)

处理类型

类型别名

typedef

1
2
3
4
typedef int *IntPtr, Int;
// IntPtr = int*
// Int = int
// 与定义变量一样,星号只对旁边起作用

using

1
using IntPtr = int*;

auto 类型说明符

  • auto 让编译器通过初始值来推算变量的类型
  • 编译器推断出来的 auto 类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则
    • 当引用被当作初始值时,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为 auto 的类型
      1
      2
      int i = 0, &r = i;
      auto a = r; // a 是 int 类型
    • auto 一般会忽略顶层 const,同时底层 const 则会保留下来
      1
      2
      3
      4
      5
      6
      7
      8
      int i = 0, &r = i;
      auto a = r; // a 是 int 类型
      const int ci = i, &cr = ci, *pi = &ci;
      auto b = ci; // b 是 int 类型
      auto c = cr; // c 是 int 类型(const int &cr 是顶层 const)
      auto d = &i; // d 是 int* 类型
      auto e = &ci; // e 是 const int* 类型
      auto f = pi; // f 是 const int* 类型(const int *pi 是底层 const)
  • 如果希望推断出的 auto 类型是一个顶层 const,需要明确指出
    1
    2
    const int ci = i;
    const auto g = ci; // auto = int, g = const int
  • 还可以将引用的类型设为 auto
    1
    2
    3
    auto &u = ci; // 此时 u 是 const int & 类型(会默认带上 const,若不带上 =,u 就可以改变 ci 的值了,这样是不行的)
    auto &v = 42; // error,不能为非常量引用绑定字面值
    const auto &w = 42; // yes
  • 设置一个类型为 auto 的引用时,初始值中的顶层常量属性依然保留。(eg: u)

decltype 类型指示符

  • 从表达式的类型推断出要定义的变量的类型
  • 编译器分析表达式并得到其类型,却不实际计算表达式的值
  • decltype 处理顶层 const 和引用的方式与 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 &
    decltype(cj) z; // error
  • 引用从来都作为其所指对象的同义词出现,只有用在 decltype 处是个例外
  • 若 decltype 使用的表达式不是一个变量,则 decltype 返回表达式结果对应的类型
    1
    2
    3
    4
    int i = 42, *p = &i, &r = i;
    decltype(r) c = i; // c = int &
    decltype(r + 0) d = i; // d = int
    decltype(*p) e; // error,e = int &
  • 若表达式的内容是解引用,则 decltype 将得到引用类型(解引用指针可以得到指针所指的对象,并且还能给这个对象赋值,因此,decltype(*p) 的结果就是 int&,而非 int)
  • 若 decltype 使用的是一个不加括号的变量,则得到的结果就是该变量的类型;若给变量加上了一层或多层括号,编译器就会把它当成是一个表达式。
    • 变量是一种可以作为赋值语句左值的特殊表达式,所以这样的 decltype 就会得到引用类型
    • decltype((var)) 的结果永远是引用
    • decltype(var) 结果只有当 var 本身就是一个引用时才是引用
      1
      2
      3
      int i = 42;
      decltype(i) a = i; // a = int
      decltype((i)) b = i; // b = int&