C++ Primer Plus 阅读记录
-
float
只有6位有效数字,超出的部分会被四舍五入。 -
数值类型转换可能出现的问题:
转换 潜在的问题 double
转换为float
精度丢失, double
64位,float
32位,转换之后的结果是不确定的浮点型转换为整数型 小数位丢失,转换之后的数值可能会造成整型溢出,其结果是不确定的 较大的整型转换为小的整形 只复制较大整型右面(低位)的字节。 这里的结果不确定指的是C++并未定义这种情况下会发生的结果,会根据系统不同出现不同的结果。
-
类型转换,以
int
为分界线整型提升 将位数比
int
小的类型转换为int
进行表达式的计算,可称之为 提升转换。和其它类型同时出现的情况 (
int
除外),转换为和其它类型中位数最大的类型进行计算,可称之为 标准转换。 -
强制类型转换的结果是一个右值。
-
R"(xxxxx)"
是C++的新特性,原始(raw)字符串。其含义是,括号中的字符会被直接的表达出来而不需要手动转义。 -
枚举:
enum bit { one = 1, two = 2, four = 4, eight = 8 }; bit(6); // 强制类型转换
定义了一个枚举类型名称
bit
,bit(6)
是将整型数字6强制转换成类型bit
。用于强制类型转换的值存在正确的取值范围。
假设一个枚举类型中最大的枚举值为A,最小枚举值为B,那么
转换所允许的上限Max应满足 $2^{n-1} < A \leq 2^n - 1 = Max$
下限Min有两种情况:
- $B \geq 0$ 此时 $Min = 0$
- $B < 0$ 此时 $2^{n-1} <\vert B\vert \leq 2^n - 1 = -Min$
-
理解指针的新方法:
使用常规的量时,值是指定的量,地址是派生的量。
使用指针的时候,值是派生 的量,地址 是指定的量。
-
new
与delete
: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
一个空指针是可以的;不可以两次释放同一块内存。 -
可以通过数组下标访问指针形式的数组
int* nums = new int[10]; *(nums + 1) = 15; nums[1] = 15;
以上两种方式的效果相同,不过指针形式的数组不会检查越界问题。
-
自动存储、静态存储和动态存储
自动存储 是在函数中定义的常规变量(在栈内存中存储的变量),在函数执行完毕后自动释放。
静态存储 用
static
声明的变量(全局变量)内容在第九章进行更新。动态存储 使用
new
操作符申请内存的变量(在堆内存中存储的变量),不会自动释放,需要调用delete
. -
C++函数返回值不可以是数组,但可以是其它类型的数据结构,指针也是可以返回的(刻意强调)。
-
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; }
-
递归是我需要去熟练使用的一个课题。
-
内联函数,提醒编译器在编译时将函数体复制到函数调用的位置。使用内联函数,只要在声明或者定义时将
inline
关键字加到前面即可。内联函数不能递归。 -
引用变量,左值引用
int& temp = num
指出了temp
是num
的别名。主要的用途在函数参数上,通过左值引用可以直接使用实参的数据,而不是通过副本,当该参数所占内存非常大的时候使用左值引用可以提高性能。引用必须在声明的时候进行初始化,初始化之后就不可以绑定其它对象。
使用左值引用时,引用的值只能是左值,也就是说右值没法作为左值引用参数进行传递。
-
引用参数为常量的情况,C++会按情况生成临时变量。
情况1:类型正确但不是左值。
情况2:类型不正确但可以转换为正确的类型。
-
返回引用,比值返回效率要高。值返回是将
return
后的表达式计算的结果复制到一个临时的位置供调用方去使用,使用的过程可能还会进行一次值的复制。而返回引用将最多产生一次复制,这次复制仅会在调用方进行赋值操作的时候进行。在返回引用的时候要避免引用自动变量,即函数运行完毕后不会再存在的量。
-
C++中有类似于Java上转型的机制,即基类引用可以指向派生类的对象。
-
函数参数默认值,需要从左向右不间断的设置默认值:
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
没有设置,所以不合法。 -
函数的重载:
函数的参数列表被称为特征标,函数名相同而特征标不同,则称之为重载,返回值不是重载的必要条件。
C++会根据特征标调用相应的函数,而当参数列表与声明不匹配时,C++尝试进行类型转换以得到合适的函数匹配,如果匹配时存在多个选择则该函数将被拒绝调用。
类型引用和类型被看作是同一种特征标。
在使用函数的重载时,其实编译器将重载的函数进行了 名称修饰,根据特征标为函数加密。
-
函数模板
使用泛型来定义函数,是用于生成 函数定义 的一种方式。
template <typename T> // template <class T> void swap(T& a, T& b) { T temp = a; a = b; b = temp; }
编译器会在编译时将
T
替换成函数调用时的参数类型。 -
显示具体化
想将函数模板重载于某个特定的类型的时候使用显示具体化。
template <> void swap<int>(int& a,int& b); template <> void swap(int& a,int& b); // 与上面效果相同的声明
使用以上声明,具体化一个处理int类型的模板,这里的
<int>
是可以省略的部分。有多个原型存在时的匹配顺序:非模板函数 > 显式具体化函数 > 模板函数
-
具体化和实例化
声明了一个函数模板,这个过程并没有进行函数的定义,仅仅是提供了函数定义的方案。
通过模板生成真正函数定义的过程称作实例化。调用函数模板时提供的类型参数会产生隐式实例化的过程,这一过程是在编译的过程中完成。
这时可以引出显示实例化,显式实例化就像平时使用泛型那样
swap<int>(a, b);
,这个调用的过程为swap
模板生成了一个int
类型的实例。显式具体化是专门为
int
类型制作的实例化模板。实例化和具体化统称为具体化,
不能对同一个类型同时使用 显式实例化 和 显式实例化,这样编译器会无法匹配生成函数定义。
-
重载解析
C++用来决定为函数调用使用哪一个定义。
步骤:
- 创建候选函数列表,包括名称与调用函数相同的函数和函数模板。
- 创建可行函数列表,这些函数从候选函数列表中选出,函数的参数合适,类型正确或可以通过转换来匹配正确的类型。
- 确定最佳的可行函数。最佳可行是所需转换最少的函数,如果没有则调用出错(无法判断使用哪一个函数,二义性)。
-
最佳匹配顺序
- 完全匹配,常规函数优先匹配
- 提升转换
- 标准转换
- 用户定义的转换(这一点不是很明白)