C++ Primer Plus 阅读记录

  1. float只有6位有效数字,超出的部分会被四舍五入。

  2. 数值类型转换可能出现的问题:

    转换 潜在的问题
    double转换为float 精度丢失,double64位,float32位,转换之后的结果是不确定的
    浮点型转换为整数型 小数位丢失,转换之后的数值可能会造成整型溢出,其结果是不确定的
    较大的整型转换为小的整形 只复制较大整型右面(低位)的字节。

    这里的结果不确定指的是C++并未定义这种情况下会发生的结果,会根据系统不同出现不同的结果。

  3. 类型转换,以 int 为分界线

    整型提升 将位数比 int 小的类型转换为 int 进行表达式的计算,可称之为 提升转换

    和其它类型同时出现的情况 (int 除外),转换为和其它类型中位数最大的类型进行计算,可称之为 标准转换

  4. 强制类型转换的结果是一个右值。

  5. R"(xxxxx)"是C++的新特性,原始(raw)字符串。其含义是,括号中的字符会被直接的表达出来而不需要手动转义。

  6. 枚举:

    enum bit
    {
        one = 1,
        two = 2,
        four = 4,
        eight = 8
    };
    bit(6); // 强制类型转换
    

    定义了一个枚举类型名称bitbit(6)是将整型数字6强制转换成类型bit

    用于强制类型转换的值存在正确的取值范围。

    假设一个枚举类型中最大的枚举值为A,最小枚举值为B,那么

    转换所允许的上限Max应满足 $2^{n-1} < A \leq 2^n - 1 = Max$

    下限Min有两种情况:

    1. $B \geq 0$ 此时 $Min = 0$
    2. $B < 0$ 此时 $2^{n-1} <\vert B\vert \leq 2^n - 1 = -Min$
  7. 理解指针的新方法:

    使用常规的量时,值是指定的量,地址是派生的量。

    使用指针的时候,值是派生 的量,地址 是指定的量。

  8. newdelete :

    new的结果是一个指针,为该指针指向的元素申请内存。

    int* num = new int;     // 为int元素申请了内存
    int* nums = new int[5]; // 为具有5个int元素的数组申请了内存
    

    delete释放通过new申请的内存,释放的方式要和new申请的方式相匹配:

    delete num;
    delete[] nums; // delete[] 是整体的操作符
    

    delete仅仅是将指针所指向的内存清除,并没有消除指针变量,后续可以将该指针指向别的地址。

    int main(int argc, char const *argv[])
    {
        int *num = new int;
        delete num;
        cout << num << endl;
        int b = 1;
        num = &b;				// 指向了一个变量的地址,这样做应该没有意义,因为delete只能释放new出来的内存,换句话说,delete只能释放堆中的内存。
        cout << num << endl;
        delete num;				// 无效的操作:munmap_chunk(): invalid pointer
        cout << num << endl;
        return 0;
    }
    

    delete 一个空指针是可以的;不可以两次释放同一块内存。

  9. 可以通过数组下标访问指针形式的数组

    int* nums = new int[10];
    *(nums + 1) = 15;
    nums[1] = 15;
    

    以上两种方式的效果相同,不过指针形式的数组不会检查越界问题。

  10. 自动存储、静态存储和动态存储

    自动存储 是在函数中定义的常规变量(在栈内存中存储的变量),在函数执行完毕后自动释放。

    静态存储static声明的变量(全局变量)内容在第九章进行更新。

    动态存储 使用new操作符申请内存的变量(在堆内存中存储的变量),不会自动释放,需要调用delete.

  11. C++函数返回值不可以是数组,但可以是其它类型的数据结构,指针也是可以返回的(刻意强调)。

  12. C++函数以数组为参数时使用的是数组指针,且数组指针不会检查越界问题,这说明以下操作是可以的

    void find(int* arr,int length); // 函数声明
    void find(int arr[],int length); // 另一种等效的函数声明
    int* arr = {1,2,3,4,5};
    find(arr,5); // 传入数组的第一个元素是 1,数组长度为5
    find(arr + 2,9); // 传入数组的第一个元素是3,数组长度设定为9
    

    我认为这种特性合适利用起来会比较好玩,比如在递归的时候,每次处理一个区间数组,这时就可以通过改变传递的首地址和元素个数的方式来实现。

    使用数组区间的思路,就是要确定数组的起始位置和结尾位置,除了上面的方式还可以直接传递起始和结尾指针,结尾指针是STL中超尾的概念,比如以下函数的实现。

    int sumArray(int* begin, int* end)
    {
        int sum = 0;
        for(int* ptr = begin; ptr != end; ++ptr)
        {
            sum += *ptr;
        }
        return sum;
    }
    
  13. 递归是我需要去熟练使用的一个课题。

  14. 内联函数,提醒编译器在编译时将函数体复制到函数调用的位置。使用内联函数,只要在声明或者定义时将inline关键字加到前面即可。内联函数不能递归。

  15. 引用变量,左值引用int& temp = num指出了tempnum的别名。主要的用途在函数参数上,通过左值引用可以直接使用实参的数据,而不是通过副本,当该参数所占内存非常大的时候使用左值引用可以提高性能。

    引用必须在声明的时候进行初始化,初始化之后就不可以绑定其它对象。

    使用左值引用时,引用的值只能是左值,也就是说右值没法作为左值引用参数进行传递。

  16. 引用参数为常量的情况,C++会按情况生成临时变量。

    情况1:类型正确但不是左值。

    情况2:类型不正确但可以转换为正确的类型。

  17. 返回引用,比值返回效率要高。值返回是将return后的表达式计算的结果复制到一个临时的位置供调用方去使用,使用的过程可能还会进行一次值的复制。而返回引用将最多产生一次复制,这次复制仅会在调用方进行赋值操作的时候进行。

    在返回引用的时候要避免引用自动变量,即函数运行完毕后不会再存在的量。

  18. C++中有类似于Java上转型的机制,即基类引用可以指向派生类的对象。

  19. 函数参数默认值,需要从左向右不间断的设置默认值:

    int method (int a,int b = 0,int c = 1,int d = 2); // 合法
    int method (int a,int b = 0,int c,int d = 1); // 不合法
    

    不合法的原因是,如果参数列表开始设定默认值,那么从该参数向后都应该设置默认值,int c 没有设置,所以不合法。

  20. 函数的重载

    函数的参数列表被称为特征标,函数名相同而特征标不同,则称之为重载,返回值不是重载的必要条件。

    C++会根据特征标调用相应的函数,而当参数列表与声明不匹配时,C++尝试进行类型转换以得到合适的函数匹配,如果匹配时存在多个选择则该函数将被拒绝调用。

    类型引用和类型被看作是同一种特征标。

    在使用函数的重载时,其实编译器将重载的函数进行了 名称修饰,根据特征标为函数加密。

  21. 函数模板

    使用泛型来定义函数,是用于生成 函数定义 的一种方式。

    template <typename T> // template <class T>
    void swap(T& a, T& b)
    {
        T temp = a;
        a = b;
        b = temp;
    }
    

    编译器会在编译时将T替换成函数调用时的参数类型。

  22. 显示具体化

    想将函数模板重载于某个特定的类型的时候使用显示具体化。

    template <> void swap<int>(int& a,int& b);
    template <> void swap(int& a,int& b); // 与上面效果相同的声明
    

    使用以上声明,具体化一个处理int类型的模板,这里的<int>是可以省略的部分。

    有多个原型存在时的匹配顺序:非模板函数 > 显式具体化函数 > 模板函数

  23. 具体化实例化

    声明了一个函数模板,这个过程并没有进行函数的定义,仅仅是提供了函数定义的方案。

    通过模板生成真正函数定义的过程称作实例化。调用函数模板时提供的类型参数会产生隐式实例化的过程,这一过程是在编译的过程中完成。

    这时可以引出显示实例化,显式实例化就像平时使用泛型那样swap<int>(a, b);,这个调用的过程为swap模板生成了一个int类型的实例。

    显式具体化是专门为int类型制作的实例化模板。

    实例化和具体化统称为具体化,

    不能对同一个类型同时使用 显式实例化显式实例化,这样编译器会无法匹配生成函数定义。

  24. 重载解析

    C++用来决定为函数调用使用哪一个定义。

    步骤:

    1. 创建候选函数列表,包括名称与调用函数相同的函数和函数模板。
    2. 创建可行函数列表,这些函数从候选函数列表中选出,函数的参数合适,类型正确或可以通过转换来匹配正确的类型。
    3. 确定最佳的可行函数。最佳可行是所需转换最少的函数,如果没有则调用出错(无法判断使用哪一个函数,二义性)。
  25. 最佳匹配顺序

    1. 完全匹配,常规函数优先匹配
    2. 提升转换
    3. 标准转换
    4. 用户定义的转换(这一点不是很明白)