本章之后就是c++与c最大的不同,也就是c++独有的内容。
内联函数
常规函数与内联函数的区别主要不在于编写方式,而是c++编译器如何将它们组合到程序中。
常规函数:执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈,跳到标记函数起点的内存单元,执行函数代码,然后跳回到地址被保存的指令出,这样来回跳跃位置意味使用函数需要一定的开销。
内联函数:编译器将使用相应的函数代码替换函数调用,无需跳到另一个位置执行代码,所以内联函数运行速度比常规函数快,但需要占用更多的内存。(内联函数不能递归)
内联函数的声明:不需要函数原型,将定义放在前面,函数头前加inline
//常规函数
int fuction(int); //原型
int fuction(int n) //定义
{
...
}
//内联函数
inline int fuction(int n) //无需函数原型
{
...
}
如果一个程序调用一个常规函数十次,程序需要十次保存地址与来回跳转位置,时间过长。如果是调用内联函数十次,程序中将包含内联函数10个副本,不用来回跳转,节约时间,但因为有10个副本,程序占用内存过多。因此在使用内联函数时要考虑时间与内存的比值,来决定是否采用内联函数,如果一个函数代码很短,且需要多次调用,用内联函数再适合不过了。
内敛与宏:
c语言使用预处理语句#define来提供宏 ----内敛代码的原始实现。
inline工具是c++新增的特性。
如果计算一个数的平方宏:
#define SQUARE(X) X*X
这并不是函数,不是传递参数得结果的,而是通过文本替换得到的。
SQUARE (5) -------------5*5
SQUARE(4.5+5.5)------------4.5+5.5*4.5+5.5
很明显,在使用宏的时候,根据X不同,有时会出现错误
引用变量
c++新增的一种复合类型----引用变量,是已定义的变量的别名。目前的主要用途是作为函数的形参,通过引用变量作为参数,函数将使用原始数据,而不是副本,之前的指针可以使用原始数据,现在引用变量也可以。
使用&(取址运算符)声明引用:
int numbers;
int & nu = =numbers;
这样,nu与numbers有着相同的含义-------指向相同的值和内存单元。这一点要与指针区分,在值与址上有着相同的含义,只是表示方法不同,除此之外,还有一个差别,声明引用时必须将其初始化,而指针可以先声明,再赋值。这一点与const指针相似,必须在创建时进行初始化,一旦与某个变量关联起来,就将一直表示它,不能改变。
这里只需要注意,引用变量值改变,其关联的变量也将改变。
其次,要尽可能使用const:
- 使用const可以避免无疑中修改数据的编程错误
- 使用const使函数能够处理const和非const实参,否则只能接受非const实参
- 使用const引用使函数能够正确生成并使用临时变量
对最后一点做些说明,引用变量作为函数参数时,传入的实参必须是一个变量,而不能是一个值:function(x),而不能是function(5),但是使用const引用变量作为函数参数,则可以接受值,此时,函数会建立相应的临时变量。
可以声明一个返回引用变量的函数,传统的返回值的函数,程序会将返回值传递一个临时变量,然后再将这个临时变量拷贝给要赋值的变量,如果函数直接返回引用变量,则无需中间过程,直接赋值给变量,效率更高。
#include<iostream>
using namespace std;
string version1(string s1,const string &s);
string &version2(string &s1,const string &s);
int main()
{
string s1,nu,nu2;
cout<<"please enter a string: ";
getline(cin,s1);
cout<<"now,your string is :"<<s1<<endl;
cout<<"-----------------------------"<<endl;
nu = version1(s1,"***");
cout<<"now,your string is :"<<nu<<endl;
cout<<"the ordinay string is :"<<s1<<endl;cout<<"-----------------------------"<<endl;
nu2 = version2(s1,"###");
cout<<"now,your string is :"<<nu2<<endl;
cout<<"the ordinay string is :"<<s1<<endl;
return 0;
}
string version1(string s1,const string &s)
{
string temp;
temp = s + s1 + s;
return temp;
}//传入常规变量
string &version2(string &s1,const string &s)
{
s1 = s +s1+ s;
return s1;
}//传递的是引用,并且返回引用,所以原始值s1也改变。
最后补充一个内容,左值与右值:
a = 10;
上述代码中,a为左值,10是右值,通常来说,左值是可以取地址的,而右值不可以。我们前面说的都是左值引用,如果想要使用左值引用对一个右值进行引用操作,前面需要加上const:
const int &x = 10;
但此时,x仅只读,不能修改x的值,也成为常引用。
c++11中提供了另一种引用-------右值引用:两个&&
int &&x =10;
此时x可修改,用法与左值引用相同,但不常用,仅会在之后的移动相关问题中使用。
函数默认形参
函数定义是可以给参数增加默认值,如果调用时没有传递参数,则使用默认值,定义时必须从右至左定义默认参数,也就是说,一个默认参数的右边参数都应该是默认参数。实参按照从左往右的顺序赋给形参。
函数重载
函数重载就是同一个函数名,但函数参数的类型不同,可以传递一个int或者double等等,这与函数默认参数不同,默认参数是同一个类型参数,传递的参数数目不同。
函数重载的关键是函数的参数列表------也称函数特征标,如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则他们的特征标相同。
void index(int x, const char *str);
void index(double y,const char *str);
只有一个函数时,如果函数声明是int类型,但传入double数据,此时会强制转换,但如果是函数重载,传入其他类型参数,因为程序不知道应该转换为哪个类型的,因此会直接报错。
函数重载是特征标的不同,跟返回值类型无关
函数重载是区分const的,但不区分引用,也就是说,单独加一个const是函数重载,但加一个引用并不是函数重载。
函数模板
函数重载需要定义多个函数,实现功能相同,但参数类型不同的函数,这样有点繁琐,因此提出函数模板,只需要定义一个函数,但函数参数类型用自定义字符代替,同时利用template<typename ***>声明该字符:
template<typename T>
void Swap(T &a,T &b);
....
template<typename T>
void Swap(T &a,T &b)
{
T temp;
temp = b;
b = a;
a= temp;
}
要注意,函数原型和定义前都需要加上template<typename ***>,其中的typename可以用class代替,但防止与类声明混淆,建议使用typename。
这样利用函数模板定义,无论传入参数是那种类型,都可以实现其功能。
重载的模板,如果传入的是两个数组,那么之前的函数模板就不能发挥其作用,需要利用函数重载,但可以使用重载模板,也就是我们传入的数组可以是int,double等不同数据类型的。
函数模板是有一定的局限性的,例如,此时两个同种结构体的变量,如果只是交换内容,那么函数模板是可以实现的,因为结构体支持整体赋值。但如果,结构体中有三个变量,我们只想交换两个结构体变量中的两个元素,这时,普通的函数模板就不能实现该操作。这时就需要用到,显式具体化:表示,遇到指定的变量类型,不能按照普通的函数模板处理,应该使用指定的函数内容
template <> void Swap<job>(job &a, job &b)
{
...
}
job是我们声明的一种结构体,该函数表示,如果Swap函数调用时传入的参数时job结构体类型,则执行这个特殊的代码,这样就与声明的普通函数Swap区分开。
如果将Swap函数重载三种,一个常规函数,一个函数模板,一个函数显式具体化:
void Swap(int a, int b) //普通函数
template<typename T>
void Swap(T a,T b); //函数模板
template<> void Swap<job>(job &a, job &b); //函数具体化
三个优先级是普通函数>函数具体化>函数模板,如果传入两个int类型参数,程序将调用普通函数而不执行模板函数,如果函数传入两个结构体变量,程序将调用具体化函数而不执行模板函数。
这里要补充一个知识点,前面函数重载时讲到,增加引用不是函数重载,即下面函数原型不是函数重载,是错误:
void index(int a, int b);
void index(int &a, int &b); //不是函数重载
但是,如果其中一个是函数模板,则算是函数重载:
template<typename T>
void index(T a, T b);
void index(int &a, int &b); //此时是属于函数重载的
最后要区分一些概念,首先要明确,代码中包含函数模板本身并不会生成函数定义,也就是说,函数模板并不是函数定义,它只是一个用于生成函数定义的方案,根据调用时传入的参数类型不同,才会生成不同的函数定义,也就是生成一个该函数的实例化,这种实例化方式称作隐式实例化,即在函数被调用时才会实例化,例如,一个模板函数被调用了十次,那么每调用一次,函数就是实例化一次,即程序中该函数实例化了五次。 但是,c++还允许显式实例化,意味着直接命令编译器创建特定的实例,其语法是:
template void Swap<int>(int, int); //显式实例化
经过上述的声明,将使用Swap()模板生成一个使用int类型的实例,也就是“使用Swap()模板生成int类型的函数定义”,这是才是真正的函数定义。这里要跟前面的显式具体化区分(template后面有无<>):
template<> void Swap<job>(job &a, job &b); //显式具体化
隐式实例化,显式实例化,显式具体化统称为具体化,区别是,隐式实例化是只有在函数被调用时才会生成相应类型的函数定义,显式实例化则是提前就生成了相应参数类型的函数定义,直接调用即可,最后的显式具体化是,遇到指定的特殊类型参数,不使用正常的函数模板,而使用指定的特殊函数,但也是在被调用是才会生成函数定义。
对于函数重载,函数模板,函数模板重载,程序需要决定调用使用哪一个函数定义,这个过程称作重载解析,这里详细的过于复杂,调用函数符合完全匹配,在其基础上选择最佳匹配的函数,这是通过传递参数使程序自己决定选择哪一个函数。也可以自己选择:
void function(int a, int b); //常规函数
void function(T a, T b); //函数模板
function<>(m,n); //加上<>表示调用函数模板
function<int>(m,n); //<>中加变量类型
<>中加变量类型,表示使用该类型的函数模板,即使传入参数不符合该类型,也会强制转换为该类型。
最后一个问题,有时候不清楚参数类型具体是哪个,比如:一个函数模板,两个不同类型数据相加,那他们和的类型呢?根据参数不同,无法具体表示和的类型:
template<class T1, class T2>
void index(T1 a, T2 b)
{
...
?type? t = a + b;
...
}
因此此时t的类型无法确定,c++11新增的关键字decltype可以解决该问题:
int a;
decltype(a) b;
表示生成一个与a类型相同的变量b,因此,我们可以这样:
decltype(a+b) t = a + b;
这样可以声明各种类型,包括引用,指针,常数指针等等。如果括号中的是一个函数调用,那么表示该函数返回值的类型(并不会调用该函数,通过查看函数原型获取返回值类型)。如果括号中变量有另一个括号,则表示该类型的引用:
double z = 10;
decltype( (z) ) a = z;
a是一个double类型的引用。
上述表示在函数中不知道类型的情况下的解决方法,但是函数的返回值类型未知呢?则不适用,但可以这样:
auto index(int a, double b) ->double
//所以可以这样:
auto index(int a, double b) ->decltype(a+b)
将返回类型移到了参数声明后,被称为后置返回类型,auto是一个占位符,表示后置返回类型提供的类型。