《C++PrimerPlus 6th Edition》第8章 函数探幽 要点记录
从这章开始内容多了起来。。。
这并非是为了应付考试弄的速成宝典,而是知识点的摘录、提取与整理,没有侧重点之分,主要目的是让看过这本书的读者看到这些知识点时能够尽可能全地回想起书本上的内容。
【本章内容】
- 内联函数
- 引用变量
- 如何按引用传递函数参数
- 默认参数
- 函数重载
- 函数模板
- 函数模板具体化
【知识点详记】
8.1 C++内联函数
- 编译器将使用内联函数的代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码再跳回来。因此,内联函数运行速度比常规函数稍快,但代价是占用更多内存——如果10个不同的地方调用同一个内联函数,则该程序将包含该函数代码的10个副本
- 内联函数使用方法:①在函数声明前加上关键字
inline
;②在函数定义前加上关键字inline
- 适合作内联函数的条件:①非递归函数(递归函数有栈的开销);②代码量少,如只有一行;③调用频繁
- 内联函数示例:
示例说明:①尽管程序没有提供独立的原型,但C++原型特性仍在起作用,这是因为在函数首次使用前出现的整个函数定义充当了原型;②内联与宏:C语言的宏也能实现类似函数的功能,但最好使用内联来代替它们,因为宏定义只是进行了文本替换,且宏定义的"函数"其参数不适合用自增或自减符号#include<iostream> inline double square(double x){ return x * x;} int main(){ using namespace std; double a, b; double c = 13.0; a = square(5.0); //25 b = square(4.5 + 7.5); //144 cout<<"a = "<<a<<", b = "<<b<<"\n"; cout<<"c = "<<c<<"\n"; cout<< ", c squared = "<<square(c++) << "\n"; //169 cout<<"Now c = "<<c<<"\n"; //14 return 0; }
8.2 引用变量
-
引用变量的创建示例:
int rats; int & rodents = rat;
,引用与指针有很多相似之处,但是引用必须要在声明时进行初始化,而指针变量不必。引用更接近const指针,因此引用变量的创建是下述代码的伪装表示:int * const rodents = &rats
-
引用一旦声明,就不能通过赋值来设置了,如以下代码所示。即便pt的指向有所改变,但并不能改变这样的事实,即
rodents
引用的是rats
。int rats = 101; int * pt = &rats; int & rodents = *pt; int bunnies = 50; pt = &bunnies;
-
C语言只能
按值传递
,但C++因为引入了引用这一概念,因此可以将引用作为函数参数,使得函数中的变量名成为调用程序中的变量的别名,即按引用传递
-
左值的意思是可以被引用的数据对象(如变量、数组元素、结构成员、引用和解除引用的指针),非左值包括字面常量(用引号括起的字符串除外,它们由其地址表示)和包含多项的表达式
-
如果实参与引用参数不匹配,C++将生成临时变量。当前,仅当参数为const引用时,C++才允许这样做,但以前不是这样。创建临时变量的条件(引用参数为const时):①实参的类型正确,但不是左值;②实参的类型不正确,但可以转换为正确的类型。临时变量的有关示例如下。
double refcube(const double& ra) { return ra * ra * ra;} int main(){ double side = 3.0; double* pd = &side; double& rd = side; long edge = 5L; double lens[4] = { 2.0, 5.0, 10.0, 12.0}; double c1 = refcube(side); // ra is side double c2 = refcube(lens[2]); // ra is lens[2] double c3 = refcube(rd); // ra is rd is side double c4 = refcube(*pd); // ra is *pd is side double c5 = refcube(edge); //ra is temporary variable double c6 = refcube(7.0); //ra is temporary variable double c7 = refcube(side + 10.0); //ra is temporary variable return 0; }
上述示例中,edge虽然是变量,类型却不正确,double引用不能指向long;另一方面,参数7.0和side + 10.0的类型都正确,但没有名称。在这些情况下,编译器都将生成一个临时匿名变量,并让ra指向它。这些临时变量只在函数调用期间存在,此后编译器便可以随意将其删除。
-
临时变量必须是对于常量引用才会生成,原因通过如下示例说明。
void swapr(int& a, int& b){ int temp; temp = a; a = b; b = temp;} int main(){ long a = 3, b = 5; swapr(a, b); return 0; }
在早期C++较宽松的规则下,执行以上代码时,由于实参类型为long,与int不匹配,因此编译器将创建两个临时变量int,将它们初始化为3和5,然后交换临时变量的内容,而a和b保持不变。
简而言之,如果接受引用参数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现。解决方法是,禁止创建临时变量,现在的C++标准正是这样做的。现在来看上上个示例中的refcube()函数,该函数的目的只是使用传递的值,而不是修改它们,因此临时变量不会造成任何不利的影响,反而会使函数在可处理的参数种类方面更通用。因此,如果声明将引用指定为const,C++将在必要时生成临时变量。实际上,对于形参为const引用的C++函数,如果实参不匹配,则其行为类似按值传递,为确保原始数据不被修改,将使用临时变量来存储值。
-
注意:如果函数调用不为左值或与相应的const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量
-
将引用参数尽可能声明为常量数据的引用的理由:①使用const可以避免无意中修改数据的编程错误;②使用const使函数能够处理const和非const实参,否则将只能接受非const数据;③使用const引用使函数能够正确生成并使用临时变量
-
C++11新增了
右值引用
,这种引用可以指向右值,如以下示例所示。引入右值引用的主要目的:让库设计人员能够提供有些操作的更有效实现。第18章将讨论如何使用右值引用来实现移动语义(move semantics)double && rref = std::sqrt(36.00); double j = 15.0; double && jref = 2.0 * j + 18.5; std::cout<<rref<<'\n'; // 6.0 std::cout<<jref<<'\n'; //48.5
-
函数返回类型为引用,这种情况下,函数处于赋值等号左边是可行的,这是因为返回引用的函数实际上是被引用的变量的别名
-
返回引用时最重要的一点是,应避免返回函数终止时不再存在的内存单元引用。 为避免这种问题,①返回一个作为参数传递给函数的引用,这样,作为参数的引用将指向调用函数使用的数据;②用new来分配新的存储空间,但这种方法有一个隐患,即容易忘记使用delete来释放这些存储空间,第16章讨论的auto_ptr模板以及C++11新增的unique_ptr可帮助程序员自动完成释放工作
-
将const用于引用返回类型的原因:①避免返回引用的函数作为左值被修改,仅供使用该引用返回值;②避免在设计中添加模糊的特性,因为模糊的特性增加了犯错的机会。然而,有时候省略const确实有道理,第11章将讨论的重载运算符<<就是一个这样的例子
-
有关函数返回引用的示例。
#include<iostream> #include<string> using namespace std; struct free_throws{ string name; int made; int attempts; float percent; }; const free_throws& clone(free_throws& ft){ free_throws* pt = new free_throws; *pt = ft; return *pt; } void show(const free_throws& ft){ cout<<"Name:"<<ft.name<<endl; cout<<"made:"<<ft.made<<endl; cout<<"Attempts:"<<ft.attempts<<endl; cout<<"Percent:"<<ft.percent<<endl; } int main(){ free_throws three{ "Minnie Max", 7, 9}; show(three); const free_throws& jolly = clone(three); show(jolly); cout<<"Now change three\nthree:\n"; three.percent = 1.0 * three.made / three.attempts; show(three); cout<<"jolly"<<endl; show(jolly); delete &jolly; //easy to forget this return 0; }
-
假设实参的类型与引用参数类型不匹配,但可被转换为引用类型,程序将创建一个正确类型的临时变量,使用转换后的实参值来初始化它,然后传递一个指向该临时变量的引用
-
对象、继承和引用:
ostream
为基类,ofstream
是派生类。一个特性:基类引用可以指向派生类对象。有一个关于该特性演示的示例如下所示。void file_it(ostream& os, double fo, const double fe[], int n){ ios_base::fmtflags initial; initial = os.setf(ios_base::fixed); //save initial formatting state os.precision(0); os<<"Focal length of objective:"<<fo<<" mm\n"; os.setf(ios::showpoint); //显示小数点模式 os.precision(1); //制定显示小数的位数 os.width(12); //设置下一次输出操作使用的字段宽度 os<<"f.1. eyepiece"; os.width(15); os<<"magnification"<<endl; for (int i=0; i<n; ++i){ os.width(12); os<<fe[i]; os.width(15); os<<int(fo/fe[i] + 0.5)<<endl; //四舍五入 } os.setf(initial); //恢复格式初始状态 } const int LIMIT = 5; //调用 int main(){ ofstream fout; double objective; double eps[LIMIT]; //... file_it(fout, objective, eps, LIMIT); file_it(cout, objective, eps, LIMIT); return 0; }
代码说明:①
width()
设置下一次输出操作使用的字段宽度,这种设置只在显示下一个值时有效,然后将恢复到默认设置。默认的字段宽度为零;②setf(ios_base::fixed)
、precision()
这些设置都将一直保持不变,直到再次调用相应的方法重新设置它们 -
使用引用参数的指导原则:
对于使用传递的值而不作修改的函数。- 如果数据对象很小,如内置数据类型或小型结构,则按值传递
- 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针
- 如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率。这样可以节省复制结构所需的时间和空间
- 如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用,这是C++新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递
对于修改调用函数中数据的函数
- 如果数据对象是内置数据类型,则使用指针
- 如果数据对象是数组,则只能使用指针
- 如果数据对象是结构,则使用引用或指针
- 如果数据对象是类对象,则使用引用
当然,以上只是一些指导原则,很可能有充分的理由做出其他的选择。例如,对于基本类型,cin使用引用,因此可以使用
cin>>n;
,而不是cin>>&n;
8.3 默认参数
- 只有原型指定了默认值,函数定义与没有默认参数时完全相同
- 对于带有参数列表的函数,默认参数一定要位于非默认参数的右边
- 在设计类时,设置默认参数可以减少要定义的析构函数、方法以及方法重载的数量
- 原型示例:
char* left(const char* str, int n = 1);
;相应函数定义:char* left(const char* str, int n){...}
8.4 函数重载
- 函数重载的关键是函数的参数列表——函数特征标。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的。C++允许定义名称相同的函数,条件是它们的特征标不同。
- 函数重载需要注意的点:①编译器在检查函数特征标时,将把类型引用和类型本身视为同一特征标;②匹配函数时,并不区分const和非const变量;③函数类型不属于特征标的判别指标;
8.5 函数模板
-
template<typename T>
或template<class T>
都可以,typename
与class
等价。如果不考虑向后兼容的问题,并愿意键入较长的单词,则声明参数时,应使用关键字typename
而不使用class
-
并非所有的模板参数都必须是模板参数类型
-
模板常置于头文件中,并在需要使用模板的文件中包含头文件
-
实例化与具体化:①模板并非函数定义,但使用int的模板实例是函数定义。这种实例化方式称为隐式实例化(implicit instantiation)。现在也可以显示实例化:
template void Swap<int>(int, int);
②显示具体化的两种等价声明:template<> void Swap<int>(int&, int&);
或template<> void Swap(int&, int&);
③警告:试图在同一文件(或转换单元)中使用同一种类的显式实例化和显式具体化将出错; -
重载解析(overloading resolution)过程:
- 创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数
- 使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此会有一个隐式转换序列,其中包括实参类型与相应形参类型完全匹配的情况
- 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错
-
编译器必须确定哪个可行函数是最佳的,由最佳到最差的顺序如下:
- 完全匹配,但常规函数优先于模板
- 提升转换(例如,char和shorts自动转换为int,float自动转换为double)
- 标准转换(例如,int转换为char,long转换为double)
- 用户定义的转换,如类声明中定义的转换
-
const和非const之间的区别只适用于指针和引用指向的数据。示例如下,其中如果同时定义了#1和#2 或 #1(#2)和#3 或 #1(#2)和#4 ,则会产生二义性错误
struct blot{ int a; char b[10];}; void recycle(blot); //#1 void recycle(const blot); //#2 void recycle(blot&); //#3 void recycle(const blot&); //#4
-
术语"最具体(most specialized)"并不一定意味着显式具体化,而是指编译器推断使用哪种类型时执行的转换最少
-
重载解析寻找最匹配函数的顺序
- 如果只存在这样一个函数,则选择它;
- 如果存在多个这样的函数,但其中只有一个是非模板函数,则选择该函数;
- 如果存在多个合适的函数,且它们都为模板函数,但其中有一个函数比其他函数更具体,则选择该函数;
- 如果有多个同样合适的非模板函数或模板函数,但没有一个函数比其他函数更具体,则函数调用将是不确定的,因此是错误的;
- 当然,如果不存在匹配的函数,则也是错误
-
模板使用的两个个示例(习题6、7)
习题6 显式具体化#include<iostream> #include<cstring> using namespace std; template<typename T> T maxn(T*, int); template<> char* maxn<char*>(char*[],int); const int ArrSize = 50; int main(){ int x[6]{ 34, 25, 67, 4396, 777, 1557}; double a[4]{ 7.77, 4.396, 5.5, 2}; char* ca[5] = { new char[ArrSize]{ 'H','e','l','l','o'}, new char[ArrSize]{ 'e','v','e','r','y','o','n','e',',',' ','m','y'}, new char[ArrSize]{ 'n','a','m','e'}, new char[ArrSize]{ 'i','s'}, new char[ArrSize]{ 'F','l','o','y','d',' ','Y','o','f','i','n','d','!'} }; cout<<"max for int: "<<maxn(x, 6)<<endl; cout<<"max for double: "<<maxn(a, 4)<<endl; cout<<"longest string in char* array: "<<maxn(ca, 5)<<endl; for (int i=0; i<5; ++i) delete[] ca[i]; return 0; } template<typename T> T maxn(T* a, int n){ T max=a[0]; for (int i=1; i<n; ++i) if(max<a[i]) max = a[i]; return max; } template<> char* maxn<char*>(char* ca[],int n){ char* ret = ca[0]; for (int i=1; i<n; ++i){ if(strlen(ca[i]) > strlen(ret)) ret = ca[i]; } return ret; }
习题7
#include<iostream> template<typename T> T SumArray(T arr[], int n); template<typename T> T SumArray(T* arr[], int n); struct debts{ char name[50]; double amount; }; int main(){ using namespace std; int things[6] = { 13, 31, 103, 301, 310, 130}; debts mr_E[3] = { { "Ima Wolfe", 2400.0}, { "Ura Foxe", 1300.0}, { "Iby Stout", 1800.0} }; double *pd[3]; for (int i=0; i<3; ++i) pd[i] = &mr_E[i].amount; cout<<"Showing Mr.E's sum of things: "; cout<<SumArray(things, 6); cout<<"\nShowing Mr.E's sum of debts: "; cout<<SumArray(pd, 3)<<endl; return 0; } template<typename T> T SumArray(T arr[], int n){ using namespace std; T sum = 0; for (int i=0; i<n; ++i){ sum += arr[i]; } return sum; } template<typename T> T SumArray(T* arr[], int n){ T sum = 0; for (int i=0; i<n; ++i){ sum += *arr[i]; } return sum; }
-
关键字
decltype
(C++11):使用模板如下:decltype(expression) var;
对expression
的讨论(下文用"它"指代):- 如果它是一个没有括号括起的标识符,则var的类型与该标识符的类型相同,包括const等限定符
- 如果它是一个函数调用,则var的类型与函数的返回类型相同(并不会实际调用函数。编译器通过查看函数的原型来获悉返回类型,而无需实际调用函数)
- 如果
expression
是一个左值,则var指向其类型的引用。这要求expression
必须是用括号括起的标识符,示例如下
顺便说一句,括号并不会改变表达式的值与左值性。例如,下面两条语句等效:double xx = 7.77; decltype(xx) r1 = xx; //r1 is double decltype((xx)) r2 = xx; //r2 is double&
xx = 98.6; (xx) = 98.6;
-
C++11后置返回类型:有时候函数返回类型不能预先确定,需要根据参数列表来确定,但通常情况下,声明完参数列表前就已经确定了返回类型,所以采用新的特性——后置返回类型来解决此问题:
template<class T1, class T2> auto gt(T1 x, T2 y) -> decltype(x + y) { ... return x + y; }
其中
->decltype(x + y)
被称为后置返回类型(trailing return type)。其中auto
是一个占位符,表示后置返回类型提供的类型,这是C++11给auto
新增的一种角色
【习题】
习题参考代码见我的github(上传后会把链接打上)
由于篇幅较长,加上作者水平有限,难免会有疏漏之处,望各位大佬们批评指正!