C++—函数探幽
2015-07-02 13:57
399 查看
一、内联函数
1、内联函数的机制
内联函数是C++为提高程序运行速度而做的一项改进。
函数调用机制:常规函数调用使程序使程序跳到被掉函数的地址,并在函数结束时返回。
内联函数的机制:内联函数的代码与其他的程序代码内联起来,即编译器将使用函数的代码替换函数调用。对于内联代码,程序无需跳到另外一处执行代码,再跳回来。因此,内联函数的运行速度比常规函数快,但代价是需要占用更多内存。
2、使用内联函数
要使用内联函数,必须采取下列措施:
* 在函数声明前加上关键字inline;
* 在函数定义前加上关键字inline。
通常的做法是省略原型,将整个定义放在本应提供原型的地方。注意,内联函数不能递归。
补充:
inline工具C++新增的特性。C语言使用预处理语句#define来提供宏——内联代码的原始实现。例如:
二、引用变量
引用变量的主要作用是用作函数的形参。通过将引用变量作为形参,函数将使用原始数据,而不是其副本。这样,除了指针。引用也为函数处理大型结构提供了一种非常方便的途径,同时对于设计类来说,引用也是必不可少的。
下述的讨论用于说明引用是如何工作的,而不是其典型用法。
1、创建引用变量
C++给&赋予了另一个新的含义,将其用来声明引用。例如,要将dog作为animal变量的别名,可以这样做:
int animal;
int & dog = animal;
其中&不是取地址符,而是类型标识的一部分。上述引用声明允许将dog和animal互换——它们指向相同的值和内存单元。
注意:必须在声明引用变量时进行初始化。
同时,由于引用是变量的别名,因此引用与变量等效,例如:
说明:上述例子说明可以通过初始化来设置引用,但是不能通过赋值来设置引用,即,假设C是A的引用,那么就不能通过C = B来将C设置成B的引用。
2、将引用作为函数参数
引用经常被用作函数的参数,使得函数中的变量名称为调用程序中的变量的别名。这种传递参数的方法叫做按引用传递。按引用传递允许被调函数能够访问调用函数的变量。
说明:前面说过,应在定义引用时对其进行初始化。函数调用使用实参初始化形参,因此函数的引用参数被初始化函数调用传递的实参。
3、引用的属性和特别之处
注意:不能将常量或表达式传给按引用传递的函数。比如:函数原型为void cube(int & num)的cube函数就不能这样使用:cube(12)或者cube(x+3)。
临时变量、引用参数和const
如果实参和引用参数不匹配,C++将生成临时变量。当前,仅当参数为const引用时,C++才允许这样做。
(1)什么时候创建临时变量
如果引用参数是const,则编译器在下面两种情况生成临时变量(临时变量只在函数调用期间存在):
*实参的类型正确,但不是左值;
*实参的类型不正确,但可以转换为正确的类型。
左值:左值参数是可以被引用的数据对象,例如,变量、数组元素、结构成员、引用和解除引用的指针都是左值。
非左值:非左值包含字面常量(用引号括起来的字符串除外,它们由其地址表示)和包含多项的表达式。
常规变量和const变量都可以视为左值,因为可以通过地址访问他们。但常规变量属于可修改的左值,而const变量属于不可修改的左值。
如果接受引用参数的函数意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现。对于形参为const引用的函数,如果实参不匹配,则其行为类似按值传递,为确保数据不被修改,将使用临时变量存储值。
注意:如果函数调用的参数不是左值或与相应const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。
(2)应尽可能使用const
将引用参数声明为常量的数据的引用的理由有三:
*使用const可以避免无意中修改数据的编程错误;
*使用const使函数可以处理const和非const实参,否则将只能接受非const数据;
*使用const引用是函数能够正确生成并使用临时变量。
因此,尽可能将引用形参声明为const。
4、将引用用于结构
使用结构引用参数的方式与使用基本变量引用方式相同,只需在声明结构参数时使用引用运算符&即可。
讨论:
(1)为何要返回引用?
传统返回机制和按值传递机制相似,将关键字return后的返回值(如果为表达式,会计算表达式的值并将值反回)拷贝到一个临时位置,调用函数实际上使用的是被调用函数返回值的一个拷贝。但在返回值为引用时,直接把该引用给调用函数(返回值为引用的函数实际上被引用变量的别名),省略拷贝到临时位置这一步。相比传统返回机制,返回引用更加快速,更加节省内存。
(2)返回值为引用时需要注意的问题
*返回值为引用时最重要的一点是,应避免返回函数终止时不再存在的内存单元的引用。同样,也应避免返回指向临时变量的指针。例如,应避免编写下面这样的代码:
int & clone(int &a){
int c;
c = a ;
return c;
}
说明:该函数返回一个指向临时变量(c)的引用,函数结束时它将不复存在。为避免这种情况,最简单的做法是,返回一个作为参数传递给函数的引用。作为参数的引用将指向调用函数使用的数据,因此返回的引用也将指向这些数据。另一种办法是用new来分配新的内存空间,但是在使用完了以后需要delete。
(3)将const用于引用返回类型
在赋值语句中,左边必须是可修改的左值。上述例子中change(m) = n之所以能过通过,是因为函数返回指向m的引用,他确实标识的是这样的一个内存块,因此这条语句是合法的。在编程过程中,应该尽量避免这种语句,可以通过将函数返回值类型声明为const,避免上以上情况。
5、将引用用于类对象
将类对象传递给函数时,C++通常的做法是使用引用。
6、何时使用引用参数
使用引用参数的主要原因有两个:
*程序员能够修改调用函数中的数据;
*通过传递引用而不是整个数据对象,可以提高程序的运行速度。
对于使用传递的值而不做修改的函数:
*如果数据对象很小,如内置数据类型和小型结构,则按值传递
*如果对象数据是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针
*如果数据对象是较大的结构,则使用const指针或const引用,以提高程序效率。这样可以节省复制对象的时间和空间。
*如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用,传递类对象的标准方式是按引用传递。
对于修改调用函数中数据的函数:
*如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码(其中x是int),则很明显,该函数将修改x。
*如果数据对象时数组,则只能使用指针
*如果数据对象是结构,则使用引用或指针
*如果数据对象是类对象,则使用引用。
三、默认参数
默认参数:指的是当函数调用中省略了实参时自动使用的一个值。默认参数可以用来实现使用不同数目的参数调用同一个函数。
设置默认参数:由于编译器通过查看函数原型来了解函数所使用的参数数目,因此函数原型必须将可能的默认参数告知编译器。方法是将值赋给原型中的参数。
四、函数重载(函数多态)
1、函数重载
函数重载(函数多态)的关键是函数的参数——也称为函数特征标。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的。C++允许定义名称相同的函数,条件是它们的特征标不同。如果参数数目和/或参数类型不同,则它们的特征标也不同。
编译器在检查函数特征标时,将把类型引用和类型本身视为同一个特征标。
匹配函数时,并不区分const和非const变量。
是特征标,而不是函数类型(函数返回值类型)使得可以对函数进行重载。
2、何时使用函数重载
仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应采用函数重载。
五、函数模板
函数模板是通用的函数描述,也就是说,它们使用范型来定义函数,其中的范型可以用具体的类型来替换。通过将类型作为参数传递给函数模板,可使编译器生成该类型的函数。由于模板允许以范型的方式编写程序,因此有时也被称为通用编程。由于类型使用参数表示的,因此模板特性有时也被称为参数化类型。
模板并不创建任何函数,只是告诉编译器如何定义函数。
提示:如果需要多个将同一种算法用于不同类型的函数,请使用函数模板。如果不考虑向后兼容的问题,在声明类型参数时,应使用typename而不是class。并非所有的模板参数都是模板参数类型。
1、模板的重载
被重载的模板的函数特征标必须不同。
2、显示具体化
(1)第三代具体化选择了下面的方法:
*对于给定的函数名,可以有非模板函数、模板函数和显示具体化模板函数以及他们的重载版本;
*显示具体化的原型和定义应以template<>打头,并通过名称来指出类型;
*具体化优先于常规模板,而非模板函数优先于具体化和常规模板。
下面是一个用于交换job结构的非模板函数、模板函数和具体化的函数原型:
void Swap(job &,job &);//非模板化函数
template <typename T> //模板函数
void Swap(T &, T &);
template <>void Swap<job>(job & ,job &);//显示具体化模板函数;
3、实例化和具体化
注意:在代码中包含函数模板本身并不会生成函数定义,它只是一个生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例。
如下模板:
调用:
说明:调用Swap(num1,num2)导致编译器生成Swap()的一个实例,该实例使用int类型。函数模板并非函数定义,但使用int的模板实例是函数定义。这种实例化方式被称为隐式实例化。编译器之所以知道要进行实例化,是因为函数调用提供了一个int类型参数。隐式实例化即,在函数调用时,编译器根据调用函数提供的类型参数生成函数模板实例。
显式实例化
C++允许显式实例化,其语法是声明所需的类型——用<>指示类型,并在声明前加上template关键字。例如:
template void Swap<int>(int &,int &);//显式实例化
编译器看到上述声明后,将使用Swap()模板生成一个int类型的实例。即,该声明的意思是“使用Swap()模板生成一个int类型的定义”。
显式具体化
显式具体化使用下面两种等价的方式之一:
template<> void Swap<int>(int &, int &);
template<> void Swap(int &,int &);
上述声明的含义是,“不要使用Swap()模板生成函数定义,而应使用专门为int类型显示地定义的函数定义”。这些原型必须有自己的定义。显式具体化声明在关键字template后面包含<>,而显式实例没有。
显式实例化与显式具体化的区别:
*在形式上:显式具体化在关键字template后面有<>,而显式实例化没有;
*在含义上:显式实例化的含义是,让编译器使用模板生成一个在显式实例化声明中指出的类型的函数定义,原型的定义由编译器来定义。显式具体化的含义是,不要使用模板生成定义,而应使用专门为显式具体化声明中指出的类型所定义的函数,原型的定义需要自己来定义。
说明:显式具体化,主要用在,某种特殊类型的功能定义与通用功能定义有所不同。例如,声明和定义了一个函数模板,其功能是在屏幕上打印,该函数模板用来打印常规类型的变量可以正常实现;但是在打印结构体或者类对象的时候,就需要重新定义函数的功能。
隐式实例化、显式实例化和显式具体化统称为具体化。它们的相同之处在于,它们表示的都是使用具体类型的函数定义,而不是通用类型。
引入显式实例化后,必须使用新的语法——在声明中使用前缀template和template<>,用以区分显示实例化和现实具体化。
4、编译器选择使用哪个函数版本
对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析。其大致过程如下:
*第一步:创建候选函数列表。其中包含与被调用函数名称相同的函数和模板函数;
*第二步:使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包含实参类型与响应的形参类型完全匹配的情况。
*第三步:确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用会出错。
5、模板函数的发展
(1)decltype关键字
decltype(x) y;
含义:声明y,使y的类型与x的类型相同,其中x可以是表达式或函数调用等。例如:
* int num1 ;
decltype(num1) num2;//作出这样的声明以后,num2的类型和num1的类型一样为int
* int a = 12;
float b = 15.2;
decltype(a + b) c;//这样声明以后,c的类型将会是表达式类型转换后得到的值的类型,在这里转换后为float,因此c的类型为float。
* decltype(x + y) sum = x + y;//sum的类型为x+y的值的类型,并且用x + y的值初始化了sum。
为确定类型,编译器必须遍历一个核对表。假定有如下声明:
decltype(expression) var;
则核对表的简化版如下:
第一步:如果expression是一个没有用括号扩起来的标识符,则var的类型和该标识符的类型相同,包括const限定符:
int num;
const float num2;
decltype(num) num3 ;//num3的类型为int
decltype(num2) num4;//num4的类型为const float
第二步:如果expression为函数调用,则var的类型和函数的返回值类型相同。
long sum();
decltype(sum()) num;//num的类型和函数sum()的返回值类型相同,在这里为long
注意:在这里,并不会实际调用函数。编译器通过查看函数原型来获悉返回值类型,而无序实际调用函数。
第三步:如果expression是一个左值,则var为指向其类型的引用。要进入第三步,expression必须是用括号扩起来的标识符。
int num;
decltype(num) num1;//num1的类型为int
decltype((num)) num2;//num2的类型为int &;
第四步:如果前面的条件都不满足,则var的类型与expression的类型相同。
注意:如果需要多次声明,可以结合decltype和typedef,例如:
typedef decltype(num + num2) theType;
theType num3,num4;//num3和num4的类型和表达式的类型一样
(2)另一种函数声明语法(C++11后置返回值类型)
有一个相关的问题是decltype本身无法解决的。请看下面这个不完整的函数模板:
template <typename T1, typename T2>
?type? gt(T1 x, T2 y){
....
return x + y;
}
同样,无法预先知道x和y相加得到的类型;同时,也无法通过decltype(x + y)来得到返回值的类型,因为x和y是局部变量,此时还没声明x和y。必须在声明后使用decltype,为此C++新增了一种声明和定义函数的语法——后置返回值类型。
诸如以前的函数声明形式:
typeName funcName(arguments);
可以改成:
auto funcName(arguments) -> typeName;
其中,->typeName被称为后置返回类型;auto是一个占位符,表示后置返回类型提供的类型,这种方法也可以用于函数定义。例如,可以做出如下的声明:
auto sum(int a, float b) -> double{
/*函数体*/
}
通过结合使用这种语法和decltype,便可给gt()指定返回类型
template <typename T1, typename T2>
auto gt(T1 x, T2 y) -> decltype(x + y){
...
return x+y;
}
1、内联函数的机制
内联函数是C++为提高程序运行速度而做的一项改进。
函数调用机制:常规函数调用使程序使程序跳到被掉函数的地址,并在函数结束时返回。
内联函数的机制:内联函数的代码与其他的程序代码内联起来,即编译器将使用函数的代码替换函数调用。对于内联代码,程序无需跳到另外一处执行代码,再跳回来。因此,内联函数的运行速度比常规函数快,但代价是需要占用更多内存。
2、使用内联函数
要使用内联函数,必须采取下列措施:
* 在函数声明前加上关键字inline;
* 在函数定义前加上关键字inline。
通常的做法是省略原型,将整个定义放在本应提供原型的地方。注意,内联函数不能递归。
#include <iostream> using namespace std; inline int sum(int x, int y){//通常省略内联函数的原型,直接将函数定义放在本应放函数原型的地方;另外,内联函数不能递归。 return x + y; } int main(int argc, const char * argv[]) { cout << sum(12,32) << endl; return 0; } 输出结果: 44
补充:
inline工具C++新增的特性。C语言使用预处理语句#define来提供宏——内联代码的原始实现。例如:
#include <iostream> #define MULT(X) X*X //这不是通过传递参数来实现的,而是通过文本替换来实现的。即在使用该宏的时候,后面的X*X中的X将用宏小括号中的完整文本来替换。如果使用宏时,小括号中的内容为12+21,那么X*X就会被替换成12+21*12+21,而不是先计算12+21的值得到33,然后用33来替换X*X中的X #define MUTL2(X) (X)*(X) using namespace std; int main(int argc, const char * argv[]) { cout << "MULT(12):"<< MULT(12) << "\nMULT(6+6):" << MULT(6+6) << endl; cout << "MUTL2(10): " << MUTL2(10) << "\nMUTL2(5+5):" << MUTL2(5+5) << endl; return 0; } 输出结果: MULT(12):144 MULT(6+6):48 MUTL2(10): 100 MUTL2(5+5):100
二、引用变量
引用变量的主要作用是用作函数的形参。通过将引用变量作为形参,函数将使用原始数据,而不是其副本。这样,除了指针。引用也为函数处理大型结构提供了一种非常方便的途径,同时对于设计类来说,引用也是必不可少的。
下述的讨论用于说明引用是如何工作的,而不是其典型用法。
1、创建引用变量
C++给&赋予了另一个新的含义,将其用来声明引用。例如,要将dog作为animal变量的别名,可以这样做:
int animal;
int & dog = animal;
其中&不是取地址符,而是类型标识的一部分。上述引用声明允许将dog和animal互换——它们指向相同的值和内存单元。
#include <iostream> using namespace std; void print(int & num); void print2(int num); int main(int argc, const char * argv[]) { int mun = 12; cout << "mun :" << mun << " at " << &mun << endl; print(mun); print2(mun); return 0; } void print(int & num){ cout <<"num :"<< num << " at " << &num << endl; } void print2(int num){ cout << "num :"<< num << " at " << &num << endl; } 输出结果: mun :12 at 0x7fff5fbff7ec num :12 at 0x7fff5fbff7ec num :12 at 0x7fff5fbff7bc
注意:必须在声明引用变量时进行初始化。
同时,由于引用是变量的别名,因此引用与变量等效,例如:
#include <iostream> using namespace std; int main(int argc, const char * argv[]) { int num1 = 10; int num2 = 13; int & num3 = num1; cout << "num1 = " << num1 << " at " << &num1 << endl; cout << "num2 = " << num2 << " at " << &num2 << endl; cout << "num3 = " << num3 << " at " << &num3 << endl << endl; num3 = num2;//由于num3是num1的引用,num3就相当于num1的别名,num3 = num2 与num1 = num2是等价的 cout << "After num3 = num2 :\n"; cout << "num1 = " << num1 << " at " << &num1 << endl; cout << "num2 = " << num2 << " at " << &num2 << endl; cout << "num3 = " << num3 << " at " << &num3 << endl; return 0; } 输出结果: num1 = 10 at 0x7fff5fbff78c num2 = 13 at 0x7fff5fbff788 num3 = 10 at 0x7fff5fbff78c After num3 = num2 : num1 = 13 at 0x7fff5fbff78c num2 = 13 at 0x7fff5fbff788 num3 = 13 at 0x7fff5fbff78c
说明:上述例子说明可以通过初始化来设置引用,但是不能通过赋值来设置引用,即,假设C是A的引用,那么就不能通过C = B来将C设置成B的引用。
2、将引用作为函数参数
引用经常被用作函数的参数,使得函数中的变量名称为调用程序中的变量的别名。这种传递参数的方法叫做按引用传递。按引用传递允许被调函数能够访问调用函数的变量。
#include <iostream> using namespace std; void change(int & num1,int & num2);//用来交换两个变量中的值 int main(int argc, const char * argv[]) { int number1 = 25; int number2 = 45; cout << "Before change:\n"; cout << "number1 = " << number1 << " at " << &number1 << endl; cout << "number2 = " << number2 << " at " << &number2 << endl<< endl; cout << "After change:\n"; change(number1, number2); cout << "number1 = " << number1 << " at " << &number1 << endl; cout << "number2 = " << number2 << " at " << &number2 << endl; return 0; } void change(int & num1,int & num2){ num1 = num1 + num2; num2 = num1 -num2; num1 = num1 - num2; } 输出结果: Before change: number1 = 25 at 0x7fff5fbff7ac number2 = 45 at 0x7fff5fbff7a8 After change: number1 = 45 at 0x7fff5fbff7ac number2 = 25 at 0x7fff5fbff7a8
说明:前面说过,应在定义引用时对其进行初始化。函数调用使用实参初始化形参,因此函数的引用参数被初始化函数调用传递的实参。
3、引用的属性和特别之处
#include <iostream> using namespace std; double cube(double x); double refcube(double & rx);//这两个函数都用来计算立方 int main(int argc, const char * argv[]) { double num = 4.0; cout << cube(num) << " = cube of " << num << endl; cout << refcube(num) << " = cube of " << num << endl; return 0; } double cube(double x){ x *= x * x; return x; } double refcube(double & rx){ rx *= rx * rx; return rx; } 输出结果: 64 = cube of 4 64 = cube of 64
注意:不能将常量或表达式传给按引用传递的函数。比如:函数原型为void cube(int & num)的cube函数就不能这样使用:cube(12)或者cube(x+3)。
临时变量、引用参数和const
如果实参和引用参数不匹配,C++将生成临时变量。当前,仅当参数为const引用时,C++才允许这样做。
(1)什么时候创建临时变量
如果引用参数是const,则编译器在下面两种情况生成临时变量(临时变量只在函数调用期间存在):
*实参的类型正确,但不是左值;
*实参的类型不正确,但可以转换为正确的类型。
左值:左值参数是可以被引用的数据对象,例如,变量、数组元素、结构成员、引用和解除引用的指针都是左值。
非左值:非左值包含字面常量(用引号括起来的字符串除外,它们由其地址表示)和包含多项的表达式。
常规变量和const变量都可以视为左值,因为可以通过地址访问他们。但常规变量属于可修改的左值,而const变量属于不可修改的左值。
如果接受引用参数的函数意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现。对于形参为const引用的函数,如果实参不匹配,则其行为类似按值传递,为确保数据不被修改,将使用临时变量存储值。
注意:如果函数调用的参数不是左值或与相应const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。
(2)应尽可能使用const
将引用参数声明为常量的数据的引用的理由有三:
*使用const可以避免无意中修改数据的编程错误;
*使用const使函数可以处理const和非const实参,否则将只能接受非const数据;
*使用const引用是函数能够正确生成并使用临时变量。
因此,尽可能将引用形参声明为const。
4、将引用用于结构
使用结构引用参数的方式与使用基本变量引用方式相同,只需在声明结构参数时使用引用运算符&即可。
#include <iostream> using namespace std; typedef struct { string name; int made; int attempts; float persent; }free_throws; void display(const free_throws &ft); void set_pc(free_throws &ft); free_throws & accumulate(free_throws & target,const free_throws & source); int main(int argc, const char * argv[]) { free_throws one = {"Ifelsa Branch",13,14}; free_throws two = {"Andor Knott",10,16}; free_throws three = {"Minnie Max",7,9}; free_throws four = {"Whily Looper",5,9}; free_throws five = {"Long Long",6, 14}; free_throws team = {"Throwgoods",0,0}; free_throws dup; set_pc(one); display(one); accumulate(team, one); display(accumulate(team, two)); accumulate(accumulate(team, three), four); display(team); dup = accumulate(team, five); cout << "DisPlay team:\n"; display(team); cout << "Display dup after assignment:\n"; display(dup); set_pc(four); accumulate(dup, five) = four; cout << "Display dup fter ill-advised assignment:\n"; display(dup); return 0; } void display(const free_throws &ft){ cout << "Name:" << ft.name << endl; cout << "Made:" << ft.made << endl; cout << "Attempts: " << ft.attempts << endl; cout << "Percent:" << ft.persent << endl; } void set_pc(free_throws &ft){ if (ft.attempts != 0) { ft.attempts = 100.0f * float(ft.made)/float(ft.attempts); } else{ ft.attempts = 0; } } free_throws & accumulate(free_throws & target, const free_throws &source){ target.attempts += source.attempts; target.made += source.made; set_pc(target); return target; } 输出结果: Name:Ifelsa Branch Made:13 Attempts: 92 Percent:0 Name:Throwgoods Made:23 Attempts: 76 Percent:0 Name:Throwgoods Made:35 Attempts: 79 Percent:0 DisPlay team: Name:Throwgoods Made:41 Attempts: 44 Percent:0 Display dup after assignment: Name:Throwgoods Made:41 Attempts: 44 Percent:0 Display dup fter ill-advised assignment: Name:Whily Looper Made:5 Attempts: 55 Percent:0
讨论:
(1)为何要返回引用?
传统返回机制和按值传递机制相似,将关键字return后的返回值(如果为表达式,会计算表达式的值并将值反回)拷贝到一个临时位置,调用函数实际上使用的是被调用函数返回值的一个拷贝。但在返回值为引用时,直接把该引用给调用函数(返回值为引用的函数实际上被引用变量的别名),省略拷贝到临时位置这一步。相比传统返回机制,返回引用更加快速,更加节省内存。
(2)返回值为引用时需要注意的问题
*返回值为引用时最重要的一点是,应避免返回函数终止时不再存在的内存单元的引用。同样,也应避免返回指向临时变量的指针。例如,应避免编写下面这样的代码:
int & clone(int &a){
int c;
c = a ;
return c;
}
说明:该函数返回一个指向临时变量(c)的引用,函数结束时它将不复存在。为避免这种情况,最简单的做法是,返回一个作为参数传递给函数的引用。作为参数的引用将指向调用函数使用的数据,因此返回的引用也将指向这些数据。另一种办法是用new来分配新的内存空间,但是在使用完了以后需要delete。
(3)将const用于引用返回类型
#include <iostream> using namespace std; int & change(int &); int main(int argc, const char * argv[]) { int m = 12; int n = 15; change(m) = n;//由于函数change()返回的是一个指向调用函数中m变量的引用,该函数就相当于m的别名 cout << "m:" << m << endl; return 0; } int & change(int &m){ m += 5; return m; } 输出结果: m:15
在赋值语句中,左边必须是可修改的左值。上述例子中change(m) = n之所以能过通过,是因为函数返回指向m的引用,他确实标识的是这样的一个内存块,因此这条语句是合法的。在编程过程中,应该尽量避免这种语句,可以通过将函数返回值类型声明为const,避免上以上情况。
5、将引用用于类对象
将类对象传递给函数时,C++通常的做法是使用引用。
#include <iostream> #include <string> using namespace std; string version1(const string & str1, const string & str2); string & version2(string & str1,const string &str2); string & version3(const string &str1, const string &str2); int main(int argc, const char * argv[]) { string str1 = "Good morning!"; string str2 = "###"; cout << " version1(str1,str2):" << version1(str1, str2) << endl; cout << R"+(version1(str1,"***"):)+"; cout << version1(str1, "***");//虽然"***"是const char*类型,不是string对象,但是可以转换成string对象,这时函数会创建一个临时变量,使引用形参str2指向该临时变量 cout << endl << endl; cout << "version2(str1,str2):" << version2(str1, str2) << endl; cout << " str1:" << str1 << endl << endl; cout << "version3(str1,str2): " ; cout << version3(str1, str2) << endl; return 0; } string version1(const string & str1, const string &str2){ string temp; temp = str2 + str1 + str2; return temp; } string & version2(string &str1, const string &str2){//该函数有一定的副作用,他修改了引用str1指向的内存的值 str1 = str2 + str1 + str2; return str1; } string & version3(const string &str1, const string &str2){//该函数可能会导致系统崩溃,因为该函数返回了一个指向已释放了的内存的引用,temp为临时变量,在函数调用结束时将会被释放,而该函数却反回了指向temp的引用 string temp; temp = str2 + str1 + str2; return temp; } 输出结果: version1(str1,str2):###Good morning!### version1(str1,"***"):***Good morning!*** version2(str1,str2):###Good morning!### str1:###Good morning!### version3(str1,str2): ######Good morning!######
6、何时使用引用参数
使用引用参数的主要原因有两个:
*程序员能够修改调用函数中的数据;
*通过传递引用而不是整个数据对象,可以提高程序的运行速度。
对于使用传递的值而不做修改的函数:
*如果数据对象很小,如内置数据类型和小型结构,则按值传递
*如果对象数据是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针
*如果数据对象是较大的结构,则使用const指针或const引用,以提高程序效率。这样可以节省复制对象的时间和空间。
*如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用,传递类对象的标准方式是按引用传递。
对于修改调用函数中数据的函数:
*如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码(其中x是int),则很明显,该函数将修改x。
*如果数据对象时数组,则只能使用指针
*如果数据对象是结构,则使用引用或指针
*如果数据对象是类对象,则使用引用。
三、默认参数
默认参数:指的是当函数调用中省略了实参时自动使用的一个值。默认参数可以用来实现使用不同数目的参数调用同一个函数。
设置默认参数:由于编译器通过查看函数原型来了解函数所使用的参数数目,因此函数原型必须将可能的默认参数告知编译器。方法是将值赋给原型中的参数。
#include <iostream> #include <string> using namespace std; void print(string str = "Hello World!");//设置默认参数 int main(int argc, const char * argv[]) { print(); print("nihao"); return 0; } void print(string str){//函数定义和没有默认参数时一样 cout << str << endl;; } 输出结果: Hello World! nihao
四、函数重载(函数多态)
1、函数重载
函数重载(函数多态)的关键是函数的参数——也称为函数特征标。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的。C++允许定义名称相同的函数,条件是它们的特征标不同。如果参数数目和/或参数类型不同,则它们的特征标也不同。
编译器在检查函数特征标时,将把类型引用和类型本身视为同一个特征标。
匹配函数时,并不区分const和非const变量。
是特征标,而不是函数类型(函数返回值类型)使得可以对函数进行重载。
2、何时使用函数重载
仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应采用函数重载。
五、函数模板
函数模板是通用的函数描述,也就是说,它们使用范型来定义函数,其中的范型可以用具体的类型来替换。通过将类型作为参数传递给函数模板,可使编译器生成该类型的函数。由于模板允许以范型的方式编写程序,因此有时也被称为通用编程。由于类型使用参数表示的,因此模板特性有时也被称为参数化类型。
模板并不创建任何函数,只是告诉编译器如何定义函数。
#include <iostream> using namespace std; template <typename T>//template表明接下来要创建一个函数模板,必须使用尖括号。typename可以用class替换 void change(T &num1, T &num2){ num1 = num1 + num2; num2 = num1 - num2; num1 = num1 - num2; } template <class T>//class可以用typename替换 void show(T, T );//声明函数模板原型 int main(int argc, const char * argv[]) { int a = 12; int b = 13; float c = 10.2; float d = 3.5; cout << "Before change:\n"; show(a, b);//编译器将根据函数模版,来创建对应的函数定义 show(c, d); cout << "After change:\n"; change(a, b); change( c, d); show(a, b); show(c, d); return 0; } template <class AnyType>//提供函数模板定义 void show(AnyType num1, AnyType num2){ cout << num1 << endl; cout << num2 << endl; } 输出结果: Before change: 12 13 10.2 3.5 After change: 13 12 3.5 10.2
提示:如果需要多个将同一种算法用于不同类型的函数,请使用函数模板。如果不考虑向后兼容的问题,在声明类型参数时,应使用typename而不是class。并非所有的模板参数都是模板参数类型。
1、模板的重载
被重载的模板的函数特征标必须不同。
#include <iostream> #include <string> using namespace std; template <typename T> void change(T &,T &); template <typename T> void change(T [],T [],int );//并非所有的模板参数都是模板参数类型 template <typename T> void show(T,string); template <typename T> void show(T [],int,string); int main(int argc, const char * argv[]) { int num1 = 12; int num2 = 15; float fnum1 = 10.5; float fnum2 = 5.5; int arr1[3] {11,22,33}; int arr2[3] {100,200,300}; float farr1[3] {11.5,22.5,33.5}; float farr2[3] {100.1,200.2,300.4}; cout << "Before change:\n"; show(num1, " num1"); show(num2, " num2"); show(fnum1, "fnum1"); show(fnum2, "fnum2"); show(arr1,3,"arr1"); show(arr2,3,"arr2"); show(farr1,3,"farr1"); show(farr2,3,"farr2"); cout <<endl<< "After change:\n"; change(num1, num2); change(fnum1, fnum2); change(arr1, arr2, 3); change(farr1, farr2, 3); show(num1, " num1"); show(num2, " num2"); show(fnum1, "fnum1"); show(fnum2, "fnum2"); show(arr1,3,"arr1"); show(arr2,3,"arr2"); show(farr1,3,"farr1"); show(farr2,3,"farr2"); return 0; } template <typename T> void change(T & num1, T & num2){ num1 = num1 + num2; num2 = num1 - num2; num1 = num1 - num2; } template <typename T> void change(T arr1[], T arr2[],int count){ for (int i = 0; i < count; i++) { change(arr1[i], arr2[i]); } } template <typename T> void show(T num, string str){ cout << str << ":" << num << endl; } template <typename T> void show(T arr[], int num, string str){ for (int i = 0; i < num; i++) { cout<< str << " #" << i + 1 << " :" << arr[i] << "; "; } cout << endl; } 输出结果: Before change: num1:12 num2:15 fnum1:10.5 fnum2:5.5 arr1 #1 :11; arr1 #2 :22; arr1 #3 :33; arr2 #1 :100; arr2 #2 :200; arr2 #3 :300; farr1 #1 :11.5; farr1 #2 :22.5; farr1 #3 :33.5; farr2 #1 :100.1; farr2 #2 :200.2; farr2 #3 :300.4; After change: num1:15 num2:12 fnum1:5.5 fnum2:10.5 arr1 #1 :100; arr1 #2 :200; arr1 #3 :300; arr2 #1 :11; arr2 #2 :22; arr2 #3 :33; farr1 #1 :100.1; farr1 #2 :200.2; farr1 #3 :300.4; farr2 #1 :11.5; farr2 #2 :22.5; farr2 #3 :33.5;
2、显示具体化
(1)第三代具体化选择了下面的方法:
*对于给定的函数名,可以有非模板函数、模板函数和显示具体化模板函数以及他们的重载版本;
*显示具体化的原型和定义应以template<>打头,并通过名称来指出类型;
*具体化优先于常规模板,而非模板函数优先于具体化和常规模板。
下面是一个用于交换job结构的非模板函数、模板函数和具体化的函数原型:
void Swap(job &,job &);//非模板化函数
template <typename T> //模板函数
void Swap(T &, T &);
template <>void Swap<job>(job & ,job &);//显示具体化模板函数;
#include <iostream> #include <string> using namespace std; typedef struct { int age; string name; } Person; void Swap(int &, int &); template <typename T> void Swap(T &, T &); template <>void Swap<Person>(Person &,Person &); void show(const int &); template <typename T> void show(const T &); template<>void show<Person>(const Person &); int main(int argc, const char * argv[]) { int a1 = 12; int a2 = 15; float f1 = 12.5; float f2 = 10.5; Person p1{24, "MuPiaomiao"}; Person p2{25, "HongMeng"}; Swap(a1, a2); Swap(f1, f2); Swap(p1, p2); show(a1); show(f1); show(p1); return 0; } void Swap(int & a, int & b){ a = a + b; b = a - b; a = a - b; cout << "调用了Swap(int &, int &) \n"; } template <typename T> void Swap(T & a, T & b){ T c; c = a; a = b; b = c; cout << "调用了Swap(T &,T &)\n"; } template <>void Swap<Person>(Person & a,Person & b){ Person onePer = a; a.age = b.age; b.age = onePer.age; cout << "调用了Swap<Person>(Person &, Person & b)\n"; } void show(const int & a){ cout << "调用了show(const int &):" << a << endl ; } template <typename T> void show(const T & a){ cout << "调用了show(const T &):" << a << endl; } template<>void show<Person>(const Person & per){ cout << "调用了show<Person>(const Person &):" << per.name << ", " << per.age << endl; } 输出结果: 调用了Swap(int &, int &) 调用了Swap(T &,T &) 调用了Swap<Person>(Person &, Person & b) 调用了show(const int &):15 调用了show(const T &):10.5 调用了show<Person>(const Person &):MuPiaomiao, 25
3、实例化和具体化
注意:在代码中包含函数模板本身并不会生成函数定义,它只是一个生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例。
如下模板:
template <typename T> void Swap(T & a, T & b){ a = a + b; b = a - b; a = a - b; }
调用:
int main(){ int num1 = 12; int num2 = 15; Swap(num1,num2); return 0; }
说明:调用Swap(num1,num2)导致编译器生成Swap()的一个实例,该实例使用int类型。函数模板并非函数定义,但使用int的模板实例是函数定义。这种实例化方式被称为隐式实例化。编译器之所以知道要进行实例化,是因为函数调用提供了一个int类型参数。隐式实例化即,在函数调用时,编译器根据调用函数提供的类型参数生成函数模板实例。
显式实例化
C++允许显式实例化,其语法是声明所需的类型——用<>指示类型,并在声明前加上template关键字。例如:
template void Swap<int>(int &,int &);//显式实例化
编译器看到上述声明后,将使用Swap()模板生成一个int类型的实例。即,该声明的意思是“使用Swap()模板生成一个int类型的定义”。
显式具体化
显式具体化使用下面两种等价的方式之一:
template<> void Swap<int>(int &, int &);
template<> void Swap(int &,int &);
上述声明的含义是,“不要使用Swap()模板生成函数定义,而应使用专门为int类型显示地定义的函数定义”。这些原型必须有自己的定义。显式具体化声明在关键字template后面包含<>,而显式实例没有。
显式实例化与显式具体化的区别:
*在形式上:显式具体化在关键字template后面有<>,而显式实例化没有;
*在含义上:显式实例化的含义是,让编译器使用模板生成一个在显式实例化声明中指出的类型的函数定义,原型的定义由编译器来定义。显式具体化的含义是,不要使用模板生成定义,而应使用专门为显式具体化声明中指出的类型所定义的函数,原型的定义需要自己来定义。
说明:显式具体化,主要用在,某种特殊类型的功能定义与通用功能定义有所不同。例如,声明和定义了一个函数模板,其功能是在屏幕上打印,该函数模板用来打印常规类型的变量可以正常实现;但是在打印结构体或者类对象的时候,就需要重新定义函数的功能。
隐式实例化、显式实例化和显式具体化统称为具体化。它们的相同之处在于,它们表示的都是使用具体类型的函数定义,而不是通用类型。
引入显式实例化后,必须使用新的语法——在声明中使用前缀template和template<>,用以区分显示实例化和现实具体化。
#include <iostream> #include <string> //显式具体化,可以理解成是典型化,即为某种不能通过函数模板通用定义实现函数功能的特殊类型(比如结构,类对象)单独地定义适合该类型的函数定义。 using namespace std; typedef struct { int age; string name; }Person; template <typename T> void change(T &, T &);//声明函数模板 template <> void change<Person>(Person &, Person &); //显式具体化,需要自己定义函数定义 template <typename T> void show(const T &); template <> void show<Person>(const Person &); int main(int argc, const char * argv[]) { int num1 = 15; int num2 = 13; float fnum1 = 22.5; float fnum2 = 25.0; Person p1 {60,"木缥缈"}; Person p2 {25,"小红"}; show(num1); show<int>(num2); show(fnum1); show<float>(fnum2); show(p1); show(p2); change(num1, num2); change<float>(fnum1,fnum2);//可以在程序中使用函数创建显示实例化 change(p1, p2);//编译器不会生成Person类型的函数定义,而是使用显式具体化定义的函数定义 show(num1); show<int>(num2); show(fnum1); show<float>(fnum2); show(p1); show(p2); return 0; } template <typename T> void change(T & a, T & b){ a = a + b; b = a - b; a = a - b; } template <> void change<Person>(Person &p1, Person &p2){ p1.age = p1.age + p2.age; p2.age = p1.age - p2.age; p1.age = p1.age - p2.age; } template <typename T> void show(const T & a){ cout << &a << " : " << a << endl; } template <> void show<Person>(const Person &p){ cout << &p <<" : " << "name--" << p.name << ", age--" << p.age <<";\n"; } 输出结果: 0x7fff5fbff7ac : 15 0x7fff5fbff7a8 : 13 0x7fff5fbff7a4 : 22.5 0x7fff5fbff7a0 : 25 0x7fff5fbff780 : name--木缥缈, age--60; 0x7fff5fbff760 : name--小红, age--25; 0x7fff5fbff7ac : 13 0x7fff5fbff7a8 : 15 0x7fff5fbff7a4 : 25 0x7fff5fbff7a0 : 22.5 0x7fff5fbff780 : name--木缥缈, age--25; 0x7fff5fbff760 : name--小红, age--60;
4、编译器选择使用哪个函数版本
对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析。其大致过程如下:
*第一步:创建候选函数列表。其中包含与被调用函数名称相同的函数和模板函数;
*第二步:使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包含实参类型与响应的形参类型完全匹配的情况。
*第三步:确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用会出错。
5、模板函数的发展
(1)decltype关键字
decltype(x) y;
含义:声明y,使y的类型与x的类型相同,其中x可以是表达式或函数调用等。例如:
* int num1 ;
decltype(num1) num2;//作出这样的声明以后,num2的类型和num1的类型一样为int
* int a = 12;
float b = 15.2;
decltype(a + b) c;//这样声明以后,c的类型将会是表达式类型转换后得到的值的类型,在这里转换后为float,因此c的类型为float。
* decltype(x + y) sum = x + y;//sum的类型为x+y的值的类型,并且用x + y的值初始化了sum。
为确定类型,编译器必须遍历一个核对表。假定有如下声明:
decltype(expression) var;
则核对表的简化版如下:
第一步:如果expression是一个没有用括号扩起来的标识符,则var的类型和该标识符的类型相同,包括const限定符:
int num;
const float num2;
decltype(num) num3 ;//num3的类型为int
decltype(num2) num4;//num4的类型为const float
第二步:如果expression为函数调用,则var的类型和函数的返回值类型相同。
long sum();
decltype(sum()) num;//num的类型和函数sum()的返回值类型相同,在这里为long
注意:在这里,并不会实际调用函数。编译器通过查看函数原型来获悉返回值类型,而无序实际调用函数。
第三步:如果expression是一个左值,则var为指向其类型的引用。要进入第三步,expression必须是用括号扩起来的标识符。
int num;
decltype(num) num1;//num1的类型为int
decltype((num)) num2;//num2的类型为int &;
第四步:如果前面的条件都不满足,则var的类型与expression的类型相同。
注意:如果需要多次声明,可以结合decltype和typedef,例如:
typedef decltype(num + num2) theType;
theType num3,num4;//num3和num4的类型和表达式的类型一样
(2)另一种函数声明语法(C++11后置返回值类型)
有一个相关的问题是decltype本身无法解决的。请看下面这个不完整的函数模板:
template <typename T1, typename T2>
?type? gt(T1 x, T2 y){
....
return x + y;
}
同样,无法预先知道x和y相加得到的类型;同时,也无法通过decltype(x + y)来得到返回值的类型,因为x和y是局部变量,此时还没声明x和y。必须在声明后使用decltype,为此C++新增了一种声明和定义函数的语法——后置返回值类型。
诸如以前的函数声明形式:
typeName funcName(arguments);
可以改成:
auto funcName(arguments) -> typeName;
其中,->typeName被称为后置返回类型;auto是一个占位符,表示后置返回类型提供的类型,这种方法也可以用于函数定义。例如,可以做出如下的声明:
auto sum(int a, float b) -> double{
/*函数体*/
}
通过结合使用这种语法和decltype,便可给gt()指定返回类型
template <typename T1, typename T2>
auto gt(T1 x, T2 y) -> decltype(x + y){
...
return x+y;
}
#include <iostream> using namespace std; template <typename T1, typename T2> auto sum(T1 x, T2 y) ->decltype(x + y); int main(int argc, const char * argv[]) { int num1 = 12; long double num2 = 14.9; decltype(sum(num1,num2)) num3 = sum(num1, num2); cout << "sizeof num1:" << sizeof(num1) << endl; cout << "sizeof num2:" << sizeof(num2) << endl; cout << "sizeof num3:" << sizeof(num3) << endl; return 0; } template <typename T1, typename T2> auto sum(T1 x, T2 y) ->decltype(x + y){ return x + y; } 输出结果: sizeof num1:4 sizeof num2:16 sizeof num3:16
相关文章推荐
- C语言中main函数的参数
- Java调用C语言
- VC++ List Control 的具体用法实例
- 关于C++派生类中构造函数调用顺序的问题
- LeetCode-Best Time to Buy and Sell Stock IV -解题报告
- 第十七周oj刷题——Problem B: 分数类的四则运算【C++】
- C++服务编程
- LeetCode-Best Time to Buy and Sell Stock III -解题报告
- C语言中const的用法
- (7)风色从零单排《C++ Primer》 string
- 给出年、月、日,计算该日是该年的第几天。
- C++ 排序函数 sort(),qsort()的用法
- C++ STL中Map的按Key排序和按Value排序
- C++学习:** 多重指针
- 用递归法将一个整数n转换成字符串
- C++sort函数的用法
- c++ 提升应用程序的权限
- 将字符串中最长的单词输出
- c语言中static用法总结
- C/C++中的输入与输出及如何读取一行文本