C++ Primer 总结——第二章 变量和基本类型

一. 基本内置类型

  1. C++ 基本数据类型包含
    • 算术类型(arithmetic type)
    • 空类型(void)
  2. 算术类型
    • 算术类型分为
      • 整型(integral type),包含字符和布尔类型在内
      • 浮点型
    • 算术类型的尺寸,在不同机器上有所差别。C++ 标准只规定尺寸的最小值,允许编译器赋予类型更大的尺寸。
    • 字符类型
      • 基本字符类型——char
        • char 的空间应该确保能够存放机器基本字符集中任意字符对应的数字值,即一个char的大小应和一个机器字节相同。
      • 其他字符类型——用于扩展字符集
        • wchar_t类型——用于确保可以存放机器最大扩展字符集中的任意字符。
        • char16_t、char_32_t用于Unicode字符集。
    • 注意,并非int就比short大,long就比int大。C++ 中的规定实际是大于等于:
      • int至少和short一样大;
      • long至少和int一样大;
      • long long至少和long一样大。
    • 浮点型可以表示单精度、双精度、扩展精度值。
      • C++ 标准制定了浮点数有效位数的最小值,但大多数编译器都实现了更高的精度。
    • 除了布尔型和扩展字符型,其他整型可以分为带符号型(signed)无符号型(unsigned)
      • unsigned int可以缩写为unsigned。
      • 字符型被分为三种
        • char
        • signed char
        • unsigned char
        • 注意,char和signed char并不一样,字符的表现形式只有两种,char具体表现为signed char还是unsigned char由编译器决定
  3. 选择类型的几条原则
    • 数值不可能为负,选择无符号类型
    • 使用int进行整数运算,如果int不够,使用long long。
    • 如果要使用不大的整数,需要明确指出signed char或unsigned char,因为char在一些机器上有符号,在另一些机器上又没有符号。
    • 浮点数运算直接上double。float精度不够,而且float和double的计算代价差不多。
  4. 内置类型的机器实现
    • 计算机以比特序列存储数据(bit)。
    • 大多数计算机以2的整数次幂个比特作为块来处理内存,
      • 可寻址的最小内存块称为字节(byte)
        • 字节至少能容纳机器基本字符集中的字符,大多为8比特。
      • 存储的基本单元称为“字(word)”
        • 字大多是4或8字节。
      • 上面讲到,字节是可寻址的最小内存块。所以,地址是和字节绑定的。
    • 想明确内存中某个地址的具体含义,首先要知道存储在该地址的数据类型,这决定了数据占多少bit,如何解释这些数据。
  5. 类型转换
    • 给某种类型的对象强行赋另一种类型的值时会发生什么?
      • 非布尔值赋给布尔类型
        • 初始值为0:false
        • 初始值不为0:true
      • 布尔值赋给非布尔值
        • 初始值为false:0
        • 初始值为true:1
      • 浮点数赋给整数类型:仅保留小数点前的部分(取整)
      • 整数值赋给浮点数:小数部分记为0。若整数所占的空间超过浮点类型容量,精度可能损失。
      • 给无符号类型赋一个超出其表示范围的值,结果是初始值对此无符号类型表示的数值总数取模后的余数。譬如,unsigned char表示范围是0-255,我们为其赋此区间外的值,实际结果是对256取模所得余数。例如赋值-1,则结果为255
      • 给带符号类型赋超出表示范围的值,结果是未定义的(undefined)。
    • 含有无符号类型的表达式
      • 切勿混用带符号类型和无符号类型,在这种情况下,带符号类型会自动转为无符号类型,从而可能给无符号类型赋一个小于0的值,这会导致数据发生预料之外的错误。而且给无符号类型赋负值实际上会进行运算,编译器并不会报错。
  6. 字面值常量(literal)
    • 形如 42 这样的值就是字面值常量
    • 每个字面值常量对应一种数据类型,数据类型由字面值常量的形式和值来决定。
    • 整型字面值
      • 十进制
        • 默认带符号
        • 类型为 int、long、long long 中尺寸最小的那个
        • 严格来说,十进制字面值不会是负数。诸如 -42 的负十进制字面值,那个负号并不在字面值之内,只是对字面值取负值而已。
      • 八进制:0 开头
        • 可能带符号,也可能无符号
        • 类型为 int、unsigned int、long、unsigned long、long long、unsigned long long中尺寸最小的。
      • 十六进制:0x 开头
        • 可能带符号,也可能无符号
      • 整型字面值的数据类型由它的值和符号来决定。
    • 浮点型字面值
      • 默认为double
      • 浮点型字面值表现为一个小数或一个科学计数法表示的指数(例如3.14E10)
    • 字符字面值
      • 由单引号括起来的一个字符。
    • 字符串字面值
      • 由双引号括起来的零个或多个字符。
      • 实际是由常量字符构成的数组(array),编译器会在每个字符串的结尾增加一个空字符’\0’。所以,字符串字面值的实际长度要比它的内容多1。
      • 若两个字符串字面值位置紧邻且仅由空格、缩进、换行符分隔,则它们实际上是一个整体。
    • 转义序列
      • 不可打印的(nonprintable)字符、有特殊含义的字符,都是不能直接使用的,需要用到转义序列(escape sequence)
      • 转义序列以反斜线作为开始。
      • 我们可以使用\n这样的转义序列,也可以使用泛化转义序列
      • 泛化转义序列:\x后紧跟一个或多个十六进制数字,或\后紧跟1-3个八进制数字。数字表示的是字符对应的数值。
        • 如果\后面的八进制数超过3个,只有前3个与\构成转义序列。
        • \x会用到后面跟着的所有数字。例如”\x1234”表示一个16位的字符
    • 指定字面值的类型
      • 添加前缀和后缀,可改变整型、浮点型、字符型字面值的默认类型
      • 对于整型字面值,我们可以指定它是否带符号以及占用多少空间。
    • 布尔字面值
      • true
      • false
    • 指针字面值
      • nullptr

二. 变量

  1. 变量的意义在于提供一个具名的、可供程序操作的存储空间。
  2. 变量的数据类型决定了变量所占内存空间的大小和布局方式、该空间能存储的值的范围,变量能参与的运算。
    • 机器是不知道内存地址中具体放了什么东西,能做什么操作的。这些都要根据数据类型来判断。
  3. 何为对象?
    • 对象指一块能存储数据并具有某种类型的内存空间。
  4. 在C++中,变量(variable)和对象(object)通常可以互换使用。
  5. 变量定义
    • 基本形式:类型说明符(type specifier)  变量名1(=初始值),  变量名2(=初始值)…;
    • 初始化(initialize):对象创建时赋予特定值。
      • 注意:一条语句中定义多个变量时,先定义的变量马上就可以使用,所以可以用先定义的变量来初始化同一条语句中后定义的变量。
    • 在C++中,初始化和赋值是完全不用的操作。
      • 初始化:创建变量时赋予一个初始值
      • 赋值:把对象的当前值擦除,用一个新值来替代。
    • 列表初始化(list initialization)
      • 使用花括号来初始化变量的形式被称作列表初始化
      • 初始化对象、为对象赋新值,都可以使用这样一组由花括号括起来的初始值。
      • 内置类型的变量做列表初始化,如果初始值存在丢失信息的风险,则编译器会报错。
        long double ld = 3.1415926; 
        int a{ld}, b = {ld}; // 错误:转换未执行,因为存在丢失信息的风险
        int c(ld), d = ld; // 正确:转换执行,且确实丢失部分值
    • 默认初始化
      • 如果定义变量时没有显式初始化,则变量会被默认初始化(default initialized)。
      • 默认初始化会给变量赋予“默认值”,具体数据由变量类型决定,同时受到变量定义位置的影响。
        • 内置类型变量:
          • 定义在任何函数体之外:被初始化为0;
          • 定义在函数体内部:不被初始化(uninitialized)。这种变量的值是未定义的,试图拷贝或以其他形式访问此类值将引发错误
        • 类类型变量:
          • 每个类各自决定其初始化对象的方式,是否允许不经初始化就定义对象也由类自己决定(类会决定对象的初始值是什么)。
          • 绝大多数类支持无需显示初始化而定义对象,类会提供一个合适的默认值。
    • 实际上,我们应当养成定义变量时显式初始化的习惯。这样可以防止意外情况下因未初始化变量引发的错误。coverity等静态代码分析工具也会对此项进行检查。
  6. 变量声明和定义的关系

    • 分离式编译(separate compilation):该机制允许将程序分割为若干个文件,每个文件可被独立编译。
    • 为了支持分离式编译,C++语言将声明和定义区分开来。
      • 声明(declaration)使得名字为程序所知,一个文件想使用在其他文件定义的变量,则必须包含对这个名字的声明。
      • 定义(definition)负责创建和名字相关联的实体。
    • 变量声明规定了变量的类型和名字,这一点和定义相同。但除此之外,定义申请了存储空间,而且可能会为变量赋初始值。
    • 想声明一个变量而非定义它,在变量名之前加extern,而且不能对其显式初始化,一旦对其显式初始化,就是定义而非声明了。
    • 函数内部,如果试图初始化一个由extern标记的变量,会引发错误。
    • 示例

      extern int i;            // 声明而非定义
      extern int i = 50;      // 定义
      
      void testFuntion ()
      {
          extern int j = 10;  // 错误,在函数内部,不能对extern标记的变量做初始化
      }
      
  7. 静态类型(statically typed)
    • 含义:在编译阶段检查类型。
    • 检查类型的过程称为“类型检查(type checking)”。
    • 静态类型的前提是编译器知道每个实体对象的类型,这要求我们在使用某个变量之前必须声明其类型。
  8. 标识符(identifier)
    • 组成:字母、数字、下划线,数字不能作为开头。
    • 用户自定义标识符的几个禁忌
      • 不能连续出现两个下划线。
      • 不能以下划线紧连大写字母开头
      • 定义在函数体外的标识符不能以下划线开头。(但codeblocks能编译过???)
    • 变量命名规范
      • 标识符要能体现实际含义
      • 变量名一般用小写字母开头(小驼峰)
      • 用户自定义的类名通常以大写字母开头,如Sales_item
      • 如果标识符由多个单词组成,则单词间应有明显区分,如下划线、小驼峰、大驼峰
  9. 名字的作用域(scope)

    • 在作用域中,名字有其特定的含义。
    • C++中,大多数作用域都以花括号分隔。
    • 同一个名字,在不同作用域中,可能指向不同的实体。
    • 名字的有效区域始于名字的声明语句,止于生命语句所在作用域的末端。
    • 定义在函数体之外——全局作用域(global scope)
      • 一旦声明,全局作用域内的名字在整个程序范围内都可使用。
    • 块作用域(block scope)
    • 嵌套的作用域——作用域彼此包含

      • 内层作用域(inner scope)——被包含的作用域
      • 外层作用域(outer scope)——包含其他作用域的作用域
      • 外层作用域中一旦声明了某个名字,则其所嵌套的所有内层作用域中都可以访问该名字
      • 允许内层作用域中重新定义外层作用域已有的名字,此时使用,局部变量会覆盖全局变量。
      • 可以使用作用域操作符(左侧为空)指定使用全局作用域中定义的版本
      • 一般说来,尽量不要使全局变量和局部变量同名。
      • 示例

        #include 
        
        using namespace std;
        
        int reused = 42;    // 全局作用域
        
        int main ()
        {
            int unique = 0; // unique 拥有块作用域
            cout << reused << ", " << unique << endl; // 输出"42, 0"
            int reused = 0; // 在内层作用域中声明局部变量reused,覆盖了外层作用域中的全局变量
            cout << reused << ", " << unique << endl; // 输出"0, 0"
            cout << ::reused << ", " << unique << endl; // 显式地访问全局变量reused,输出"42, 0"
        }

三. 复合类型(compound type)

  1. 定义:复合类型是指基于其他类型定义的类型。
  2. 引用(reference)
    • 引用就是为对象(字面值、表达式计算结果都不行)起了另外一个名字,引用类型引用(refers to)另外一种类型,引用并非对象
    • 定义:将声明符写成“&d”的形式。
    • 引用必须被初始化,因为定义引用时,程序会将引用和其初始值绑定(bind)在一起。一旦初始化完成,这种绑定是不能修改的。
    • 一定要注意,引用不是对象,它只是为一个已经存在的对象起的别名。对引用所做的所有操作,都是在和它绑定的对象上进行的。
    • 以引用作为初始值,实际上是以与引用绑定的对象作为初始值。
    • 因为引用本身不是对象,所以不能定义引用的引用。
    • 引用的类型要和与之绑定的对象严格匹配,仅有两种例外
      • 初始化常量引用时,允许用任意表达式作为初始值,只要该表达式的结果能够转换成引用的类型。允许为一个常量引用绑定非常量的对象、字面值、表达式。
    • 示例
      int iVal = 1024; 
      int &refVal = iVal; // refVal是iVal的引用
      int &refVal_2; // 错误,引用必须初始化
      int &refVal_3 = refVal; // 错误,引用不是对象,不能定义引用的引用。
      int &refVal_4 = 10; // 错误,不能定义字面值的引用。
      double &refVal_5 = iVal; // 错误,引用的类型和其绑定的对象不符
  3. 指针(pointer)
    • 指针与引用的不同点
      • 指针本身是对象,允许对指针引用
      • 指针不是绑定的,可以赋值、拷贝
      • 指针无需在定义时赋初值(和其他内置类型一样,如果在函数内定义指针但未初始化,将是一个不确定的值)
    • 指针存放的是对象的地址。想获取地址,需要用取地址符&。
    • 引用不是对象,没有实际地址,所以不能定义指向引用的指针。
    • 指针的类型要和它所指向的对象严格匹配。
    • 指针值的四种状态
      • 指向一个对象
      • 指向紧邻对象所占空间的下一个位置
      • 空指针
      • 无效指针(除去以上三种情况之外的所有情况)
    • 访问无效指针的值会引发错误,结果无法预计。而编译器并不会去检查这种错误,所以使用指针时,必须要知道,这个指针是否有效。
    • 使用解引用符(*)可以访问指针指向的对象。但这种操作仅适用于有效指针。
    • 空指针(null pointer)
      • 不指向任何对象
      • 生成空指针的方法
        • 使用字面值nullptr,它可以转换为任意其他指针类型
        • 直接将指针初始化为字面值0
        • 使用名为NULL的预处理变量(preprocessor variable),这个变量在cstdlib中定义,其值为0 。使用NULL要include cstdlib。
        • 示例
          int *p1 = nullptr; 
          int *p2 = 0;
          int *p3 = NULL; // 需要先 include cstdlib
        • 最好使用nullptr,尽量避免使用NULL。
    • 建议对所有的指针进行初始化,不确定所指对象的,初始化为nullptr。因为未经初始化的指针,其所占内存空间中的内容将被看作一个地址,如果对该指针进行解引用,则是对一个本不存在的地址中本不存在的对象操作,这很容易引发问题。
    • 赋值和指针
      • 我们可以给指针赋值,给它一个新的地址,使其指向新的对象。这一点和引用不同(引用是绑定的,不可重新赋值)。
    • 其他指针操作(注意,必须是合法指针)
      • 如果指针有合法值,就可以用在条件表达式中。
        • 空指针:false
        • 非空指针:true
      • 对于类型相同合法指针,可以使用==和!=来比较,结果为布尔类型。
    • void*指针
      • void*可用于存放任意类型对象的地址
      • 我们并不了解这个地址中到底是什么对象,所以不能直接操作void*指针所指的对象
      • void*能做的事较少
        • 和其他指针比较
        • 作为函数的输入、输出
        • 赋给另一个void*指针
      • 从void*指针的角度来看,内存空间就是内存空间,我们并不能使用这个地址中所存放的对象(因为很多操作和对象类型有关,而我们并不清楚它所指对象的类型)
  4. 复合类型的声明
    • 变量的定义包括一个基本数据类型(base type)一组声明符
    • 一条定义语句定义多个变量的情况下,*、&这种类型修饰符,只修饰其后的一个变量,对同一语句中的其他变量并无影响。所以,在一条定义语句中,基本数据类型只能有一个,但声明符可以有多种,一条定义语句可能定义出几种不同类型的变量。
      int i = 1024, *p = &i, &r = i; 
      // i是int型变量,p是指向int类型的指针,r是对整型变量i的引用
    • 指向指针的指针
      • 通过*的个数,可以区分指针的级别。
    • 指向指针的引用
      • 引用不是对象,所以不能定义指向引用的指针。但指针是对象,所以可以定义对指针的引用。
      • 对于较为复杂的指针或引用声明语句,从右向左读。
        int i=42; 
        int *p;
        int *&r = p; // r 是对整型指针 p 的引用
        r = &i; // 整型指针 p 指向整形变量 i
        *r = 0; // 将 i 的值改为 0

四. const限定符

  1. 有些情况下,我们希望变量的值不能改变。此时可以使用const来进行限定,将其定义为常量。任何对常量赋值的行为都会引发错误。
    const int iVal = 40; 
  2. const对象一旦创建,其值就不能改变,所以const对象必须初始化。
  3. 我们之前说过,对象的类型决定了其能执行的操作。对于const类型的对象而言,我们需要把握一条原则——const类型对象只能执行不改变其内容的操作
  4. 默认状态下,const对象仅在文件内有效
    • 对于编译时初始化的方式定义的const对象,编译器在编译过程中将所有用到该变量的地方都替换成对应的值。想做到这样的替换,编译器必须知道变量的初始值。如果程序包含多个文件,每个用了const对象的文件都必须有它的定义。
    • 默认情况下,const对象仅在文件内有效。多个文件出现同名的const文件,实际上是在不同文件中定义了各自的变量。
    • 但有些时候,我们确实需要在多个文件中共用一个const变量。此时我们要在一个文件中定义const变量,在其他文件中声明并使用它。
      • 不管是声明还是定义,都添加extern关键字
        extern const int bufSize = 10;   // 有初始化,所以是定义 
        extern const int bufSize; // 无初始化,是声明
  5. const的引用
    • 把引用绑定在const对象上,称之为对常量的引用(reference to const),也称之为常量引用
    • 对常量的引用不能用来修改它所绑定的对象(因为这个对象是常量)。
    • 如果一个对象是常量,那么与它绑定的引用也必须是const。否则,引用和对象一个不是const,一个是const,自相矛盾,到底能不能对其重新赋值呢?
      const int ci = 1024; 
      const int &r1 = ci; // 正确,引用和其对应的对象都是常量
      r1 = 42; // 错误,r1是对常量的引用,不能对其赋值
      int &r2 = ci; // 错误,非常量引用不能指向常量对象
    • 前面提到,引用的类型必须和其引用对象的类型严格一致,但有两个例外,此处即为其一。
      • 初始化常量引用时,允许用任意表达式作为初始值,只要该表达式的结果能够转换成引用的类型。允许为一个常量引用绑定非常量的对象、字面值、表达式。
        int i = 42; 
        const int &r1 = i; // 正确,常量引用可以绑定非常量对象
        const int &r2 = 42; // 正确,常量引用可以绑定字面值。
        const int &r3 = r1 * 2; // 正确,常量引用可以绑定表达式
        int &r4 = r1 *2; // 错误,r4不是常量引用
      • 为啥常量引用如此特殊?让我们看看常量引用和对象绑定的时候发生了什么
        double dVal = 3.14; 
        const int &ri = dVal;

        ri引用的是int型变量,但实际绑定的是一个double,为了使之正确,编译器将上述代码变成下面这样:
        double dVal = 3.14; 
        const int temp = dVal; // 由双精度浮点数生成一个临时的整型变量
        const int &ri = temp; // ri绑定这个临时变量

        在这种情况下,ri绑定了一个临时量(temporary)对象。所谓临时量对象,就是编译器需要一个空间来暂存表达式的求值结果时,临时创建的一个未命名的对象。
        如果ri不是常量,那就允许对其赋值,如此就会改变ri所引用对象的值。此时绑定的对象是一个临时量对象,这种行为没有意义,C++将其归类为非法。
    • 常量引用(const引用)只是限制引用可参与的操作,并没有限制与常量引用绑定的对象。因此,与之绑定的对象可以是非常量,我们不能用常量引用来改变其值,但可以用其他方式实现这种操作。
      int i = 42; 
      int &r1 = i; // 引用r1绑定对象i
      const int &r2 = i; // 常量引用r2也绑定对象i
      r1 = 0; // r1不是常量引用,可以对其赋值
      r2 = 0; // 错误。r2是常量引用,不可对其赋值
  6. 指针和const
    • 我们也可以使指针指向常量或非常量。
      • 和常量引用类似,指向常量的指针(pointer to const)不能用于改变其所指对象的值。
      • 想存放常量对象的地址,只能使用指向常量的指针。
        const double pi = 3.14   // pi是个常量,其值不能改变 
        double *ptr = & pi; // 错误,ptr不是const指针,不能指向const对象。
        const double *cptr = & pi; // 正确
        *cptr = 12; // 错误,不能对const指针指向的对象赋值
    • 前面提到,指针的类型必须和其所指对象的类型严格一致,但有两处例外,此处即为其一。
      • 允许一个指向常量的指针指向一个非常量对象
      • 指向常量的指针,只是要求不能通过该指针改变对象的值,并非要求该对象必须是常量
    • const指针
      • 我们可以把指针定义为常量,但常量指针(const pointer)必须初始化,而且初始化一旦完成,其值(就是它指向的地址)就不能再改变。
      • 把*放在const前面,用以说明指针是一个常量,这样书写隐含的意思是:不变的是指针本身的值,而非指向的那个值。
        int errNumb = 0; 
        int *const curErr = &errNumb; // curErr 将一直指向 errNumb
        const double pi = 3.14;
        const double *const pip = & pi; // pip 是一个指向常量的常量指针
      • 这里从右向左阅读较为明了
      • 注意:常量指针指向常量的指针不同。常量指针只是固定地指向这个地址,但可以通过指针改变所指对象的值(具体能不能改由对象决定)。
  7. 顶层const
    • 顶层const(top-level const):指针本身是个常量
      • 更一般的,顶层const可以表示任意对象本身是常量,这一点对任何数据类型都适用,譬如算数类型、类、指针等。
    • 底层const(low-level const):指针所指对象是个常量
      • 底层const和指针、引用等复合类型的基本类型部分有关。
    • 指针类型既可以是顶层const也可以是底层const
      int i = 0; 
      int *const p1 = &i; // 不能改变p1的值,这是一个顶层const
      const int ci = 42; // 不能改变ci的值,这是一个顶层const
      const int *p2 = &ci; // 不能改变p2所指对象的值,这是一个底层const
      const int *const p3 = p2; // 靠右的const是顶层const,左边一个是底层const
      const int &r = ci; // 用于声明引用的const都是底层const
    • 执行对象的拷贝操作时,常量是顶层const还是底层const区别明显
      • 顶层const不受什么影响
        i = ci;      // 正确。拷贝ci的值,ci是一个顶层const对象,对此无影响 
        p2 = p3; // 正确。p2和p3指向的对象类型相同,p3顶层const的部分不影响
      • 底层const的限制不能忽视。
        • 执行对象拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换为常量,反之则不行。
          int *p = p3; // 错误,p3包含底层const的定义。而p没有。 
          p2 = p3; // 正确,p2和p3都是底层const
          p2 = &i; // 正确,int*能转换成const int *
          int &r = ci; // 错误,普通的int&不能绑定到int常量上
          const int &r2 = i; // 正确,const int &可以绑定到一个普通int上
      • 说白了一句话,还是跟之前一样,非指向常量的指针、非常量引用,是不能指向常量对象的。
  8. constexpr和常量表达式
    • 常量表达式(const expression):值不会改变在编译过程就能得到计算结果的表达式。
    • 字面值属于常量表达式;
    • 用常量表达式初始化的const对象也是常量表达式。
      • 对象是const类型
      • 对象的值在编译时能确定
    • 一个对象或表达式是不是常量表达式由它的数据类型和初始值共同决定
      const int max_files = 20;    // 常量表达式 
      const int limit = max_files + 1; // 常量表达式
      int staff_size = 27; // 非常量表达式,不是const
      const int sz = get_size(); // 非常量表达式,值在运行时才能确定
    • constexpr变量
      • 在一个复杂系统中,很难分辨一个初始值是不是常量表达式。
      • 在这种情况下,我们可以将变量声明为constexpr类型。编译器会来验证变量的值是否是一个常量表达式。
        • 声明为constexpr的变量一定是一个常量
        • 必须用常量表达式初始化
          constexpr int mf = 20;   // 20是常量表达式 
          constexpr int limit = mf + 1; // mf + 1是常量表达式
          constexpr int sz = size(); // 只有size是一个constexpr函数时才是一条正确的声明语句
      • 通常来说,只要认定一个变量是常量表达式,就用constexpr来声明,这样编译器会检查。
    • constexpr函数
      • constexpr函数足够简单,在编译时可以确定其结果
      • 可以用constexpr函数初始化constexpr变量
    • 字面值类型(literal type)
      • 常量表达式的值要在编译时确定,所以声明constexpr用到的类型必然有限制。这些简单易得的类型就称之为字面值类型
      • 算数类型、引用、指针都属于字面值类型
      • 自定义类、IO库、string类型不属于字面值类型(也就不能定义为constexpr)
      • 尽管指针和引用能定义为constexpr,但初始值受到严格限制
        • constexpr指针的初始值必须是nullptr或者0,或者是储存在某个固定地址中的对象。
        • 定义在函数中的变量并非存在固定地址中,所以constexpr指针不能指向这样的变量。
        • 定义在所有函数体之外的变量地址固定不变,可以用于初始化constexpr指针。
        • 允许函数定义一类有效范围超出函数本身的变量,这类变量也有固定地址,可以和constexpr指针、constexpr引用搭配使用。
        • constexpr指针是顶层const,也就是说,它指向固定的地址,至于这个地址中存储的是或否是常量对象,这个并无规定。
          const int *p = nullptr;  // p是指向整型常量的指针 
          constexpr int *q = nullptr; // q是指向整型变量的常量指针

五. 处理类型

  1. 类型别名(type alias)
    • 类型别名就是给某个类型起个别名。
    • 有两种方法定义类型别名
      • typedef
        • 格式:typedef 原名 别名;
          typedef double wages;    // wages是double的别名 
          typedef wages base, p; // base是double的别名,p是double的别名
      • 使用别名声明(alias declaration)
        • 格式:using 别名 = 原名;
          using SI = Sales_item; 
    • 类型别名和原名等价,原名能使用的地方,别名都可以使用。
    • 指针、常量和类型别名
      • 如果某个类型别名指代的是符合类型或常量,将其用在生命语句中会产生意外的后果。
        typedef char *pstring; 
        const pstring cstr = 0; // cstr是指向char的常量指针
        const pstring *ps; // ps是指向char的常量指针的指针
      • 注意,不能将类型别名替换成它原本的样子去理解,这是错误的。
  2. auto类型说明符
    • 我们要把表达式的值赋给变量,那么在声明变量的时候要清楚表达式的类型。这一点很困难。
    • 引入auto类型说明符,让编译器去分析表达式所属的类型。
    • auto让编译器通过初始值来推算变量类型,所以auto定义的变量必须有初始值
      // 由val_1和val_2相加的结果来推断item的类型 
      auto item = val_1 + val_2; // item初始化为val_1和val_2相加的结果
    • 使用auto也能在一条语句中声明多个变量。但该语句中所有变量的初始基本类型都必须一样。
      auto i = 0, *p = &i; // 正确:i是整数,p是整型指针 
      auto sz = 0, pi = 3.14; // 错误:sz和pi的类型不一致
    • 复合类型、常量和auto
      • 编译器推断出来的auto类型有时候和初始值类型并不完全一样。编译器会适当修改,使其更符合初始化规则。
      • 当引用作为初始值,真正参与初始化的是引用对象。所以我们会把引用对象的类型作为auto的类型。
      • auto一般会忽略顶层const,而底层const会保留下来。
        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,需要明确指出。
        const auto f = ci;   // ci的推演类型是int,f是const int 
      • 可以将引用的类型设为auto,原来的初始化规则仍然适用。
        auto &g = ci;    // g是与整型变量绑定的常量引用 
        auto &h = 42; // 错误,非常量引用不能和字面值绑定
        const auto &j = 42; // 正确,常量引用可以绑定字面值
      • 设置一个类型为auto的引用,初始值中的顶层常量属性保留。
  3. decltype类型指示符
    • decltype:选择并返回操作数的数据类型。在这个过程中,编译器分析表达式并得到其类型,但不实际计算表达式的值。
    • 功能:仅从表达式的类型推断出要定义的变量的类型,但不用该表达式的值来初始化变量。
    • 示例:
      decltype(f()) sum = x;  // sum的类型就是f()的返回值类型。注意:编译器并不实际调用f(),只是使用它的返回值类型 
    • decltype处理顶层const和引用的方式与auto有些许不同。如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)。
      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使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。
      • 有些表达式将向decltype返回一个引用类型。
        • 一般来说,这种情况发生时,意味着该表达式的结果对象能作为一条赋值语句的左值。
          // decltype的结果可以是引用类型 
          int i = 42, *p = &i, &r = i;
          decltype(r + 0) b; // 正确:加法的结果是int,因此b是一个(未初始化)的int
          decltype(*p) c; // c是int&,必须初始化
        • 因为r是一个引用,decltype(r)的结果是引用类型。想让结果类型是r所指向的类型,可以将r作为表达式的一部分,如r+0。
        • 如果表达式的内容是解引用,decltype将得到引用类型。
    • decltype和auto的另一处重要区别是:decltype的结果类型与表达式密切相关。
      • 特别注意:对于decltype所用的表达式来说,如果变量名加上了一对括号,得到的类型和不加括号时有所不同。
        • 不加括号,得到的是变量的类型。
        • 加括号,编译器将其当做表达式。变量是一种可以作为赋值语句左值的特殊表达式,这样的declt会得到引用类型。
          decltype((i)) d; // 错误:d是int&,必须初始化 
          decltype(i) e; // 正确:e是一个(未初始化的)int
      • 切记:decltype((vatiable))的结果永远是引用(注意双层括号)。decltype(variable)只有当variable本身是一个引用时才是引用。
    • 赋值会产生引用,引用的类型就是左值的类型。如果i是int,表达式i=x的类型是int&。

六. 自定义数据结构

  1. C++语言允许用户以类的形式自定义数据类型
  2. 我们的类以struct开始,紧跟类名和类体。类体用花括号包围形成了一个新的作用域。
  3. 类内部定义的名字必须唯一,但可以和类外部定义的名字重复。
  4. 类体右侧的花括号必须跟一个分号,因为类体后面可以紧跟变量名以示对该类型对象的定义。但通常把类的定义和该类型对象的定义分开写。
    struct Sales_data 
    {
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
    };
  5. 类数据成员(data member)
    • 类体定义类的成员
    • 数据成员定义了类的对象的具体内容。
    • 可以为数据成员提供一个类内初始值(in-class initializer),用于在创建对象时初始化数据成员。
    • 没有初始值的成员会被默认初始化。
  6. string类型其实就是字符的序列
    • 操作:
      • >>(读入字符串)
      • <<(写出字符串)
      • ==(比较字符串)
  7. 编写头文件
    • 为了确保各个文件中类的定义一致,类通常被定义在头文件中,类所在头文件的名字应该和类的名字一致。
    • 头文件通常包含那些只能被定义一次的实体。
    • 确保头文件多次包含仍能安全工作的技术是预处理器(preprocessor)
      • 预处理器是编译之前执行的一段程序,可以部分地改变我们所写的程序。
      • 头文件保护符(header guard):依赖于预处理变量(通常名字全部大写),把头文件整个包起来。
        #ifndef SALES_DATA_H 
        #define SALES_DATA_H
        内容...
        #endif

你可能感兴趣的:(C/C++)