Chapter2 开始学习C++
2.1.4 头文件名
- 头文件后缀不是.h代表不是传统C库,可以使用命名空间。类、函数、变量才是C++编译器的标准组件,它们都被放置在名称空间std中
2.1.6 使用cout进行C++输出
- cout是一个预定义的对象
- C本身也有一些运算符重载的情况。例如&符号即代表地址运算符,也代表按位与运算符;*既表示乘法,也表示解除引用:编译器根据上下文来确定含义
2.2.1 声明语句和变量
- 程序中的声明语句叫做defining declaration,简称为定义(definition),它代表编译器将分配内存空间
- 声明(declaration)不一定是定义,还有可能是reference declaration
2.2.2 赋值语句
- C和C++有特殊的性质:可以连续使用赋值符号
2.4.1 使用有返回值的函数
- 被调用的函数叫做被调用函数(called function),包含函数调用的函数叫做调用函数(calling function)
- 函数原型之于函数就像变量声明之于变量
- 在程序使用sqrt()时,必须提供原型;可以用两种方法实现:
- 在源代码中输入函数原型
- 包含头文件cmath, 其中定义了原型
- 使用库函数:C++库函数储存在库文件中,编译器编译时必须在库文件中搜索你使用的函数。
- 只包含cmath头文件可以提供原型,但不一定会导致编译器搜索正确的库文件
2.4.3 用户定义的函数
- 标准C库提供了140个预定义的函数
- 和C一样,C++不允许将函数定义在函数中
- main函数的返回值返回给系统
- 关键字:main不是关键字,可以把main作为变量名(不推荐)
- 在程序中将不能将同一个名字作为对象名与变量名(比如在使用cout输出的程序中将cout用作变量名)
2.4.5 在多函数程序中使用using编译指令
- 让程序能访问名称空间std的四种方法:
- 将using namespace std; 放在函数定义之前,让文件中所有的函数都能使用std中所有元素
- 将using namespace std; 放在特定的函数定义内,让该函数能使用std中所有元素
- 在特定的函数中使用类似using std::out, 让该函数能使用特定的元素
- 完全不用编译指令using, 而在使用元素时加上std::前缀
Chapter7 函数
7.1 复习函数的基本知识
- 库函数是已经定义和编译好的函数,同时可以使用标准库头文件提供其原型
7.1.1 定义函数
- C++规定函数返回值不能是数组,但可以是其他任何类型
7.1.2 函数原型和函数调用
- 原型描述了函数到编译器的接口,将函数返回类型和参数列表告知编译器,因此编译器不用停止编译main函数而在文件内部或其他文件中寻找。
- 函数原型中可以包含变量名,也可以不包含,甚至不需要和函数定义中的变量名相同
- ANSI C中的原型是可选的,但在C++中原型是必不可少的
- C++中say_hi()和say_hi(void)等效,表示函数没有参数;C中say_hi()表示将在后面定义参数列表
- 原型的功能:
- 确保编译器正确处理参数返回值
- 确保编译器检查使用的参数数目是否正确
- 确保编译器检查使用的参数类型是否正确;若不正确则转换为正确的类型
- 通常原型自动将被传递的参数强制转换为期望的类型;只会进行有意义的转换,如不会将指针转换为整型
- 在编译阶段进行的原型化成为静态类型检查
7.2 函数参数和按值传递
- 用于接受传递值的变量成为形参(parameter),传递给函数的值称为实参(argument)
7.2.2 另一个接受两个参数的函数
- 函数中使用两种局部变量:形参和其他局部变量
7.3.1 函数如何使用指针来处理数组
- C++将数组名解释为其第一个元素的地址
cookies = &cookies[0] - 该规则存在一些例外
- 数组声明使用数组名来标记存储位置
- 对数组名使用sizeof()将返回整个数组的长度(以字节为单位)
- 将地址运算符&用于数组名时,将返回整个数组的地址(包含数组总大小个字节)
- C++中当且仅当用于函数头或函数原型中,int* arr 和int[] arr是等价的,都意味着arr是一个int*指针(而不是数组):这也是必须向数组传递数组大小的原因,int* 指针无法指出数组长度
- 将指针加一,是加上了一个sizeof(指针类型)
- arr[i] == *(arr + i)
- &arr[i] == arr + I
- 使用原始数据作为函数实参增加了破坏数据的风险,ANSI C和C++中的const限定符提供了解决这种问题的方法:不能通过const修饰的指针修改其指向的值
7.3.3 更多数组函数示例
- 自上而下和自顶向下的模式:
- 考虑通过数据类型和设计适当的函数来处理数据,然后将函数合成依序,是为自下而上的程序设计(Bottom-Up)。因为设计过程从组件到整体进行,首先强调数据的表示和操纵,这种方法适用于OOP。
- 传统的过程性编程倾向于自顶而下的模式(Top-Down),首先指定模块化设计方案,再研究细节
7.3.4 使用数组区间的函数
- STL方法使用“超尾”概念来指定区间,对于数组而言,标识数组结尾的参数将是指向最后一个元素后面的指针。
- 首3个元素:cookies, cookies+3
- 总数8个,后4个元素:cookies + 4, cookies + 8
7.3.5 指针和const
-
有两种方法将const用于指针
- 让指针指向一个常量对象,可以防止使用该指针来修改所指向的值
int age = 39; const int* pt = &age;
这里pt指向一个const int,意味着不能使用pt来修改这个值;另外这里将变量的地址赋给常量指针,是可以的。但不可以把常量赋给非常量指针。
- 将指针本身声明为常量,可以防止改变指针指向的位置
int sloth = 3; int* const finger = &sloth;
-
当进入两级间接关系时,将const和非const混用将不安全。因此仅当只有一层间接关系时,才可以将非const地址或指针赋给const指针
const int **pp2; int *p1; const int n = 13; pp2 = &p1; //const int** = &int* *pp2 = &n; //*const int** = & const int *p1 = 10; //* int* = 10;
这段代码中,p1不能直接指向n,就通过pp2指向p1,从而改变了const的p1值
-
尽可能使用const:
- 这样可以避免由于无意间修改数据而导致的编程错误
- 使用const使得函数可以处理const和非const实参,否则只能接受非const数据。
7.4 函数和二维数组
-
向函数传递二维数组的原型:
int sum(int (*ar2)[4], int size)
ar2是指针而不是数组,且指针类型指定了列数 -
与指针数组的区别:
指向int[4]的指针: int(*ar2)[4] 4个指向int的指针:int* ar2[4] 指向int[4]的指针:int ar2[][4]
-
二维数组的索引
ar2[r][c] == *(*(ar2 + r) + c) //注意不是*((ar2 + r) + c)
7.5 函数和C-风格字符串
- 将字符串作为参数时意味着传递的是地址,但可以使用const来禁止对字符串参数进行修改
- 实际传递的是字符串第一个字符的地址,因此形参声明应该是char*
- 不必传递字符串长度,因为可以检验莫问的控制字符’\0’
7.6 函数和结构
- 在函数中使用结构有三种方法
- 按值传递结构:将实参复制一份,赋值给形参
- 通过指针传递
- 通过引用传递
- 按值传递的缺点是,如果结构非常大,则复制结构将增加内存要求,降低运行速度
7.7 函数和string对象
- 与数组相比,string对象和结构更类似:
- 可以将结构\对象赋给另一个结构\对象
- 可以将结构\对象作为完整的实体传递给函数
- 如果需要多个字符串,可以声明一个string对象数组,而不是二维char数组
7.8 函数和array对象
- 要使用array类,需要包含头文件array(位于namespace std中)
7.9.1 包含一个递归调用的递归
- 与C不同的是,C++不允许main调用自己
- 递归模板1:
只要if语句为true,每个recurs()调用都将调用statements1,然后再调用recurs()。(如果if语句为false则会执行statements2)void recurs(argumentlist){ statements1 if(test){ recurs(arguments) } statements2 }
当前调用结束后返回,以相反的顺序调用statements2
7.9.2 包含多个递归调用的递归
// ruler.cpp -- using recursion to subdivide a ruler
#include <iostream>
const int Len = 66;
const int Divs = 6;
void subdivide(char ar[], int low, int high, int level);
int main(int argc, char const *argv[])
{
char ruler[Len];
int i;
for (i = 1; i < Len - 2; i++)
ruler[i] = ' ';
ruler[Len - 1] = '\0';
int max = Len -2;
int min = 0;
ruler[min] = ruler[max] = '|';
std::cout << ruler << std::endl;
for (i = 1; i <= Divs; i++)
{
subdivide(ruler,min,max,i);
std::cout << ruler << std::endl;
for (int j = 1; j < Len - 2; j++)
ruler[j] = ' '; // reset to blank ruler
}
return 0;
}
void subdivide(char ar[], int low, int high, int level)
{
if (level == 0)
return;
int mid = (high + low) / 2;
ar[mid] = '|';
subdivide(ar, low, mid, level - 1);
subdivide(ar, mid, high, level - 1);
}
输出:
| |
| | |
| | | | |
| | | | | | | | |
| | | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- 拥有N个子递归语句的函数,若调用深度为level,则调用次数为 N l e v e l N^{level} Nlevel
7.10 函数指针
- 函数也有地址,函数的地址是存储其机器语言代码的内存的开始地址
- 不带()的函数名返回函数的内存地址
- 函数指针的声明应该指定函数的返回类型和函数的特征标,例如:double (*pt) (int, float)
- 函数指针可以作为函数参数,例如:void estimate(int lines, double (*pt)(int))
- 可以允许像使用函数名一样使用函数指针
7.10.3 深入探讨函数指针
恐怖的章节,没看懂。不过大致讨论了C++11采用auto的必要性,快说,感谢auto
Chapter8 函数探幽
8.1 C++内联函数
-
程序的调用流程:
- 存储该指令的内存地址
- 将函数参数复制到堆栈中
- 跳到标记函数起点的内存单元,执行函数代码
-
内联函数的运行速度比常规函数稍快,但代价是占用更多内存,再N个地方调用将产生N个副本
-
如果代码执行时间很短,且常被调用就可以节省更多时间
-
要使用内联特性需要:
- 在函数声明前加上inline
- 在函数定义前加上inline
-
如果编译器认为函数过大或函数调用了自己(递归不能内联)则会拒绝将其作为内联函数
-
如果C语言用宏实现了某功能,应考虑将其转化为C++的内联函数
8.2.1 创建引用变量
- 引用变量的主要用途是用作函数的形参,函数将使用原始数据
- 引用变量和其引用的变量指向相同的值和内存单元
- 必须在声明引用时将其初始化,而不能像指针那样,先声明后赋值
- "int & rodents = rats"实际上是"int * const pr = &rats"的伪装
8.2.2 将引用用作函数参数
- 将引用作为函数参数称为按引用传递,该参数被初始化为实参
8.2.3 引用的属性和特别之处
- 当向函数传递数据很大时,推荐使用引用类型
- 传递引用的限制更严格,比如x+3不能赋值给引用,因为其不是变量
- 如果实参和引用参数不匹配,C++将生成临时变量。当前仅当参数为const引用时,C++采允许这样做:
- 当实参的类型正确,但不是左值
- 当实参的类型不正确,但可以转换为正确的类型
- 传统返回机制计算return后的表达式将结果返回给调用函数。这个值被复制到一个临时位置,而调用程序使用这个值
- 返回引用的函数实际上是被引用的变量的别名
- 返回引用应该注意,避免返回函数终止时不再存在的内存单元引用。因此可以:
- 返回传入函数的参数引用
- 用new来分配变量,并返回
- 如果需要返回不可赋值的引用,可以加上const
左值
左值是可以被引用的数据对象,例如:
- 变量
- 数组元素
- 结构成员
- 引用和接触引用的指针
非左值包括:
- 字面常量(有引号括起的字符串除外,它们由地址表示)
- 包含多项的表达式
常规变量和非const变量都可视为左值,因为可通过地址访问他们,但常规变量属于可修改的左值,const变量属于不可修改的左值
8.2.5 将引用用于类对象
- 如果形参为const string&, 在调用函数时实参可以是string对象或C风格字符串,如:
- 用引号括起的字符串字面量
- 以空字符结尾的char数组
- 指向char的指针变量
8.2.6 对象、继承和引用
- 继承的一个特性是基类引用可以指向派生类对象,而无需进行强制类型转换。可以将基类指针作为函数参数
8.2.7 何时使用引用参数
- 使用引用参数的原因有:
- 能修改调用函数中的数据对象
- 提高程序的运行速度
- 对于使用传递的值而不修改的函数
- 如果数据对象很小,如内置数据类型或小型结构,则按值传递
- 如果数据对象是数组,则使用指针,并改为const
- 如果数据对象是较大的结构,则使用const指针或const引用
- 如果数据对象是类对象,则使用const引用
- 对于修改调用函数中数据的函数
- 如果数据是内置数据类型,则使用指针
- 如果数据是数组,则使用指针
- 如果数据是结构,则使用引用或指针
- 如果是类对象,则使用引用
8.3 默认参数
- 通过使用默认参数,可以减少要定义的析构函数,方法以及方法重载的数量
- 只有原型指定了默认值
8.4 函数重载
- 函数重载的关键是参数列表(函数特征标)
- 如果两个函数的参数数目和类型相同,且参数的排列顺序相同,则其特征标是相同的,而变量名是无关紧要的
- 如果没有匹配的原型,C++将使用标准类型转换强制进行匹配
- 编译器检查特征标时,将引用和引用的类型视为同样的
- 返回类型是不能重载的,特征标是针对函数列表的。返回类型不同,那么函数名相同时特征标也必须不同
重载引用参数
void sink(double& r1);
void sank(const double& r2);
voud sunk(double&& r3)
- 左值引用参数r1和可修改的佐治参数(如double变量)匹配
- const左值引用r2与可修改的左值、const左值参数和右值参数(如两个double值的和)匹配
- 右值引用参数r3和右值匹配
如果重载使用这三种参数的函数,将调用最匹配的版本:
void staff(double& rs); //matches modifiable lvalue
void staff(const double& rcs); //matches rvalue, const lvalue
void stove(double& r1); //matches modifiable lvalue
void stove(const double& rcs); //matches const lvalue
void stove(double&& r3); //matches rvalue
以此决定函数的行为:
double x = 55.5;
const double y = 32.0;
stove(x); //calls stove(double& )
stove(y); //calls stove(const double&)
stove(x+y) //calls stove(double&& )
8.4.2 何时使用函数重载
- 仅当函数执行相同任务,但使用不同形式的数据时才应使用函数重载
- 名称修饰:根据原型中指定的形参类型对每个函数名加密
8.5 函数模板
- 关键词template和typename是必须的,typename和class是互换的
8.5.1 重载的模板
- 可以像重载常规函数定义那样重载模板定义
8.5.3-8.5.4 显式具体化、实例化和具体化
-
在代码中包含函数模块本身并不会生成函数定义,它只是一个用于生成函数定义的方案。
-
对于给定的函数名,可以有非模板函数,模板函数和显式具体化模板函数以及它们的重载版本
-
包括三种
-
隐式实例化(implicit instantiation)
编译器使用模板为特定类型生成函数定义时,得到的是模板实例(instantiation)。模板并非是函数定义,但使用int的模板实例是函数定义
Swap(a,b);
-
显式具体化(explicit specialization):
显式具体化的原型和定义应以template<>打头,并通过名称来指出类型
//模板函数原型 template <class T> void Swap(T&, T&) //具体化的原型,类job template<> void Swap<job>(job&, job&)
-
显式实例化(explicit instantiation)
直接命令编译器创建具体的实例:
template void Swap<int>(int, int)
-
8.5.5 编译器选择使用哪个函数版本
-
C++的重载解析:
- 创建候选函数列表,包含与被调用函数的名称相同的函数和模板函数
- 使用候选函数列表创建可行函数列表:都是参数数目正确的函数,并有一个隐式转换序列
- 确定是否有最佳的可行函数,如没有则调用出错
-
从最优到最差排序:
- 如果只存在一个这样的函数,则选择它
- 完全匹配,但常规函数优于模板函数;如果都是模板函数,则较具体的优先
- 提升转换(如char和shorts自动转换为int,float自动转换为double)
- 标准转换(如int转换为char,long转换为double)
- 用户定义的转换,如类声明中定义的转换
-
用于找出最具体的模板的规则称为函数模板的部分排序规则(partial ordering rules)
8.5.6 模板函数的发展
- decltype关键字(C++11)
decltype(x) y; //使y有和x一样的类型 decltype(x+y) z;
- 后置返回类型
template<class T1, class T2>
auto gt(T1 x, T2 y) -> decltype(x+y)
{
...
return x+y
}
//decltype在参数声明后面,因此x和y位于作用域内,可以使用