1、内联函数
内联函数是C++为提高程序运行速度所做的一个改进。常规函数与内联函数的区别在于编译器如何将它们组合到程序中。
可执行程序由一组机器语言指令组成。运行程序时,操作系统将这些指令载入计算机内存中,因此每条指令都有特定的内存地址。计算机随后执行这些指令。有时候,循环语句或分支语句会跳过一些指令,向前或向后跳到特定地址。常规函数调用也使得程序跳到另外一个地址,并在函数结束时返回。
过程如下:
执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需要将返回值放入到寄存器中),然后跳回到地址被保存的指令处(这与阅读文章时暂时停下来看注脚,并在阅读完注脚后返回到以前阅读的地方类似)。来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。
对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数比常规函数运行速度快,但是占用更多内存。
要使用这项特征,必须采用以下特征:
在函数声明前加上关键字inline;
在函数定义前加上关键字inline;
示例
inline double square(double x){ return x*x;}
int main(){
double a=square(5.0);
return 0;
}
引用变量
&有两种情况,1、地址运算符;2,类型标识符
int rats=101;
int * pt=&rats;//指针pt取rats的地址 1、地址运算符
int & rodents=*pt;//rodent是引用,引用 指针pt的值 2,类型标识符
int bun=50;
pt=&bun;
临时变量、引用参数和const
如果实参与引用参数不匹配,C++将生成临时变量。当前,仅当参数为const引用时,C++才允许这样做,但以前不是这样的。
如果引用参数是const,则编译器将在下面两种情况下生成临时变量:
1、实参类型正确,但不是左值;
2、实参类型不正确,但是可以转换为正确的类型。
左值参数是可以被引用的数据对象,例如,变量、数组元素、结构成员、引用和解除引用的指针都是左值。非左值包括字面常量(用引号括起的字符串除外,它们由其地址表示)和包含多项的表达式。常规变量属于可以修改的左值,const变量属于不可修改的左值。
下面代码:
double ref(const double &ra){
return ra*ra*ra;
}
double side=ra;
double *pd=&side;
double &rd=side;
lond edge=5L;
double lens[4]={2.1,2,3,4};
double c1=ref(side);//ra 是side
double c2=ref(lens[2]);//ra 是lens[2]
double c3=ref(rd);//ra是rd 也就是side
double c4=ref(*pd);//ra是*pd也就是side
double c5=ref(edge);//ra是临时变量, 类型不正确
double c6=ref(7.0);//同上 类型正确,但是没有名称
double c7=ref(side+10.0);//同上 类型正确,但没有名称
如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现。解决方法就是禁止创建临时变量,现在的C++标准就是这样做的(然而,在默认的情况下,有些编译器仍将发出警告,而不是错误信息,因此,如果看到临时变量的警告,请不要忽略)。
尽可能使用const
将引用参数声明为常量数据的引用的理由有三个:
1、使用const可以避免无意中修改数据的错误;
2、使用const使函数能够处理const和非const实参,否则只能接受非const数据;
3、使用const引用使函数能够正确生成并使用临时变量。
右值引用
这种引用可以指向右值,使用&&声明
double && rref=std::sqrt(36.00);//对于double &是不被允许的
double j=15.0;
double & jref=2.0*j+18.5;//对于double &是不被允许的
返回引用时需要注意的问题
返回引用时最重要的一点是应该避免返回函数终止时不在存在的内存单元引用。
应该避免下面的代码:
const free_throws &clone2(free_throws &ft){
free_throws newguy;
newguy=ft;
return newguy;
}
该函数返回一个临时变量的引用,函数运行完毕后它将不再存在。
为了避免这个问题,返回一个作为参数传递给函数的引用。
如下:
free_throws &accumulate(free_throws &target,const free_throws &source){
target.attempts+=source.attempts;
target.made+=source.made;
return target;
}
另一种是用new来分配新的存储空间。使用new为字符串分配内存空间,并将返回该内存空间的指针。
const free_throws &clone(free_throws &ft){
free_throws *pt;
*pt=ft;
return *pt;
}
free_throws *pt;创建一个无名的free_throws结构,并将指针pt指向该结构,因此 * pt就是该结构。代码似乎返回该结构,但是函数声明表明,该函数实际上将返回这个结构的引用。
使用
free_throws & jolly=clone(three);
jolly成为新结构的引用。这种方法存在一个问题:在不需要new分配内存的时候,应该使用delete来释放它们。
为何将返回值用于返回类型
以下代码是合法的
free_throws &accumulate(free_throws &target,const free_throws &source){
target.attempts+=source.attempts;
target.made+=source.made;
return target;
}
accumulate(dup,five)=four;
为什么合法呢?在赋值语句,左边必须是可以修改的左值。accumulate返回值是可修改的引用,因此合法。
但是另外一方面,常规(非引用)返回类型是右值——不能通过地址访问的值。这种返回值位于临时内存单元中,运行到下一个语句时,它们可能不存在了。
const free_throws &accumulate(free_throws &target,const free_throws &source){
target.attempts+=source.attempts;
target.made+=source.made;
return target;
}
//因为返回值类型为const,因此下面的语句不合法
accumulate(dup,five)=four;
何时使用引用参数
使用引用参数有两个原因:
1、程序员能够修改调用函数中的数据对象;
2、通过传递引用而不是整个数据对象,可以提高程序的运行速度。
使用引用、指针、按值传递的各种情况,使用原则:
对于使用传递的值而不做修改的函数。
1、如果数据对象很小,如内置数据类型或小型结构,则按值传递。
2、如果数据对象是数组,则使用指向const的指针。
3、如果数据对象是较大的结构体,使用const指针或const引用,可以节约时间和空间。
4、如果数据对象是类对象,则使用const引用。
对于修改调用函数中的数据的函数:
1、如果数据对象是内置数据类型,使用指针;
2、如果数据对象是数组,使用指针
3、如果数据对象是结构体,使用引用或指针
4、如果数据对象是类对象,则使用引用
默认参数
对于带参数列表的函数,必须从右到左添加默认值。也就是说,要为某个参数设置默认值,则必须为它右边的所有参数提供默认值:
int harpo(int n,int m=4,int j=5);//合法
int harpo(int n,int m=4,int j);//非法
int harpo(int n=0,int m=4,int j=5);//合法
函数重载
函数多态是C++在C语言的基础上新增的功能。函数多态就是函数重载,指的是可以有多个同名的函数,因此对于名称进行了重载。函数重载使用不同的参数列表完成了相同的功能。
函数重载的关键是函数的参数列表——也称为函数特征标。C++允许定义名称相同的函数,条件是它们的特征标不同。如果参数数目和/或参数类型不同,则特征标也不同。
如下:
void print(float l,int w)
void print(int l,int w)
void print(long l,int w)
void print(int m)
unsigned int year;
print(year,6);//有歧义的调用
print(year,6);不与任何原型匹配,没有匹配的原型并不会自动停止使用其中的某个函数,因为C++会尝试使用标准类型强制转换进行匹配。如果void print(int l,int w)作为唯一的原型,这种调用是没有歧义的,将year转换成Int类型,但是上面有三个函数可以转换,C++拒绝这种函数的调用,并将视为错误。
double cube(double x);
double cube(double &x);
参数x与double x原型和double &x原型都匹配,因此编译器无法确定究竟使用哪一个原型。为了避免这种混乱,编译器在查函数特征标时,将 类型引用和 类型本身视为同一个特征标。
void dribble(char * bits);//重载
void dribble(const char * bits);//重载
void dabble(char * bits);//非重载
void drivel(const char * bits);//非重载
const char p1[4]="how";
char p2[4]="how";
dribble(p1);//void dribble(const char * bits);//重载
dribble(p2);//void dribble(char * bits);//重载
dabble(p1);//无匹配
dabble(p2);//void dabble(char * bits);//非重载
drivel(p1);//void drivel(const char * bits);//非重载
drivel(p2);//void drivel(const char * bits);//非重载
dribble有两个原型,一个用于const指针,一个常规指针,编译器将根据实参是否为const来决定使用哪个原型。dabble函数只能与非const参数的调用匹配,而drivel可以与const或非const参数的调用。drivel和dabble之所以在行为上有这种差别,主要由于将非const值赋给const变量是合法的,但反之则是非法的。
记住:是特征标,而不是函数类型使得可以对函数进行重载。例如,下面两个声明是互斥(不能在一段程序中同时存在)的:
long gro(int n,float m);
double gro(int n,float m);
上面的函数是不可以重载,返回类型可以不同,但是特征标必须不同,像下面的就可以重载
long gro(int n,float m);
double gro(int n,float m);
何时使用函数重载
仅当函数执行相同的任务,但使用不同形式的数据时,才采用函数重载。
C++如何跟踪每一个重载函数,使用C++开发工具中的编译器编写和编译程序时,C++编译器将执行一些神奇的操作——名称修饰或名称矫正,它根据函数原型中指定的参数类型对每个函数进行加密。例如:
long func(int ,float);
编译器可能会转换成以下
?func@@YAXH
函数模板
template和typename关键字是必须的,当然也可以使用class替换typename。模板不创建函数,只是告诉编译器如何定义函数。需要交换Int的函数时,编译器将按模板创建这样的函数,用int替代Type/
template<typename Type>
void swap(Type &a,Type &b){
Type temp;
temp=a;
a=b;
b=temp;
}
使用函数模板不能缩短可执行程序,最终代码不包含模板,只包含实际函数。使用模板的好处是,使得生成多个函数更加简单、可靠。
#include <iostream>
template<typename T>
void swap(T &a,T &b);
template<typename T>
void swap(T a[],T b[],int n);
int main()
{
using namespace std;
int i=3;
int j=1;
swap(i,j);//void swap(T &a,T &b)
int d[3]={1,2,3};
int c[3]={1,2,5};
swap(d,c,3);//void swap(T a[],T b[],int n)
cout << "Hello World!" << endl;
return 0;
}
template<typename T>
void swap(T &a,T &b)
{
T tmp;
tmp=a;
a=b;
b=tmp;
}
template<typename T>
void swap(T a[],T b[],int n)
{
T tmp;
for (int i=0;i<n;i++) {
tmp=a[i];
a[i]=b[i];
b[i]=tmp;
}
}
模板的局限性
编写模板函数可能无法处理某些类型。
例如,比较结构体、数组、指针的大小。
#include <iostream>
struct job{
char name[4];
double salary;
};
//显式具体化 输入类型为job结构体时,调用
template <> void swap<job>(job &j1,job &j2){
double t1;
t1=j1.salary;
j1.salary=j2.salary;
j2.salary=t1;
}
template<typename T>
void swap(T &a,T &b);
template<typename T>
void swap(T a[],T b[],int n);
int main()
{
using namespace std;
int i=3;
int j=1;
swap(i,j);//void swap(T &a,T &b)
int d[3]={1,2,3};
int c[3]={1,2,5};
swap(d,c,3);//void swap(T a[],T b[],int n)
job su={"su",1.2};
job sid={"sid",1.3};
swap(su,sid);//template <> void swap<job>(job &j1,job &j2)
cout << "Hello World!" << endl;
return 0;
}
template<typename T>
void swap(T &a,T &b)
{
T tmp;
tmp=a;
a=b;
b=tmp;
}
template<typename T>
void swap(T a[],T b[],int n)
{
T tmp;
for (int i=0;i<n;i++) {
tmp=a[i];
a[i]=b[i];
b[i]=tmp;
}
}
第三代具体化:
1、对于给定的函数名,可以用非函数模板、函数模板和显式具体化模板函数以及它们的重载版本。
2、显式具体化的原型和定义应该以templatr<>打头,并通过名称来指出类型。
3、具体化优先于常规模板、而非模板函数优先于具体化和常规模板
实例化和具体化
C++允许显式实例化,意味着可以直接命令编译器创建特定的实例,如swap()。示例如下
template void swap<int>(int,int)
实现了这种特征的编译器看到上述声明后,将使用swap模板生成一个Int类型的实例。,即是“使用swap()模板生成Int类型的函数定义。”
下面的是,显式具体化使用下面两个等价的声明:
template<> void swap<int>(int &,int &)
template<> void swap<int>(int &,int &)
上面声明的意思是:不要使用swap()模板来生成函数定义,而应该使用专门为int类型显式地定义的函数定义。
隐式实例化、显式实例化和显式具体化统称为具体化。
隐式实例化
template<typename T>
T add(T t1, T2)
{
return t1 + t2;
}
编译器选择使用哪个函数版本
对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为定义函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程为重载解析。大致过程如下:
1、创建候选参数列表。其中包含与被调函数的名称相同的函数和模板函数。
2、使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的参数类型完全正确的情况。例如,使用float参数的函数调用可以将改参数转换为double,从而与double形参匹配,而模板可以为float生成一个实参。
3、确定是否有最佳的可行函数。如果有,则调用它,否则该函数调用出错。
假如有以下例子的函数调用:
may('B');//实际参数是char类型
首先编译器寻找候选者,即名称为may()的函数和函数模板。然后寻找那些可以用一个参数调用的函数。例如,下面的函数符合要求,因为其名称与被调函数相同,而且只能给它们传递一个参数。
void may(int);//#1
float may(float,float=3);//#2
void may(char);//#3
char * may(const char *);//#4
char * may( char *);//#5
template<class T> void may(const T &);//#6
template<class T> void may( T *);//#7
注意,只考虑特征标,而不考虑返回类型。#4和#7不可行,因为整数类型不能被隐式地转换为指针类型。剩余的一个模板可以用来具体化,其中T被替换为char类型,剩下五个可行函数,其中的每一个函数,如果它是声明的唯一一个函数,都可以被使用。
接下来编译器必须确定哪个可行函数是最佳的,它查看为使函数调用参数与可行的候选函数的参数匹配所需要进行的转换。
通常,从最佳到最差的顺序如下:
1、完全匹配,但常规函数优先于模板。
2、提升转换(例如,char和short自动转换为Int,float自动转换为double)。
3、标准转换(例如,int转换为char,long转换为double)。
4、用户定义的转换,如类声明中定义的转焕。
完全匹配和最佳匹配
进行完全匹配时,C++允许某些”无关紧要的转换“。
Type表示任意类型。例如,Int实参与int &实参完全匹配。
如果有多个匹配的原型,则编译器将无法完成重载解析的过程;如果没有最佳可行函数,则编译器将生成一条错误消息,消息可能包含“二义性”这样的词语。
然而,有时候即是两个函数完全匹配,仍然可以完成重载解析。首先。非const数据的指针和引用优先与非const指针和引用参数完全匹配。然而,const和非const之间的区别只适用于指针和引用指向的数据。
一个完全匹配优先于另一个的另一种情况是,一个是非模板函数,另一个不是。非模板函数优先于模板函数(包括显式具体化)。
如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先。
术语“最具体”并不一定意味着显式具体化,而是指编译器推断使用哪种类型时执行的转换最少。例如下面的模板:
template<class Type> void recycle(Type t);//#1
template<class Type> void recycle(Type *t);//#2
调用的代码如下
struct blot {int a;char b[10];};
blot ink ={25,"spots"};
recycle(&ink);
recycle(&ink)与#1模板匹配,匹配时将Type解析为blot* 。
recycle(&ink)与#2模板匹配,匹配时将Type解析为blot 。
将两个隐式实例,recycle<blot * >(blot * ) recycle(blot * )
发送到可行函数池中。#1的Type解析为blot * 。#2模板中,Type已经为具体化为指针,因此更加具体。
template<template T>
void showArray(T arr[],int n) //#1
template<template T>
void showArray(T *arr[],int n)//#2
struct deb{
char name[50];
double amount;
};
void main{
int things[6]={1,2,3,4,5,6};
struct deb ms[3]={
{"iva",12.0},{"ivaa",14.0},{"ihva",22.0}
};
double *pd[3];
for (int i=0;i<3;++i)
pd[i]=&ms[i].amount;
showArray(things,6);//调用#1
showArray(pd,3);//调用#2
}
创建自定义选择
有些情况,可以通过编写合适的函数调用,引导编译器做出你希望的选择。
template<class T>
T lesser(T a,T b) //#1
int lesser(int a,int b)//#2
int main(){
int m=20;
int n=-30;
double x=15.5;
double y=25.9;
lesser(m,n);//#2
lesser(x,y);//#1
lesser<>(m,n);//#1 <>指出编译器应该选择模板函数,而不是非模板函数,编译器注意到实参的类型为Int,因此int替代T对模板进行实例化。
lesser<int>(x,y);//#1 表示要求进行实例化,将使用显式实例化得到的函数,
}
关键字decltype(C++11)
int x;
decltype(x) y;//使得y和x相同的类型
假设有以下声明:
decltype(expression ) var;
第一步:如果expression是一个没有用括号括起来的标识符,则var的类型与该标识符的类型相同,包括const等限定符:
double x=5.5;
double y=5.6;
double &rx=x;
const double * pd;
decltype(x) w;//w is double
decltype(rx) u=y;//u is double &
decltype(pd) v;//vw is const double *
第二步:如果expression是一个函数调用,则var的类型与函数的返回类型相同。
注意:并不会实际调用函数,编译器通过查看函数的原型来获悉返回类型,而无需实际调用函数。
第三步:如果expression是一个左值,则var为指向其类型的引用。一种显而易见的情况,expression是用括号括起的标识符。
double xx=4.4;
decltype((xx)) r2=xx;//r2 is double &
decltype(xx) w=xx;//w is double
括号并不会改变表达式的值和左值性,例如:
xx=98.6;
(xx)=98.6;//括号并不影响xx的使用
第四步:如果前面条件都不满足,则var与expression类型相同:
int j=3;
int &k=j;
int &n=j;
decltype(j+6) i1;//i1 is int
decltype(100L) i2;//i2 is long
decltype(k+n) i3;//i3 is int ;k+n为int类型
另一种函数声明语法(C++11后置返回类型)
decltype无法解决的时候,请看下面的不完整模板:
template<class T1,class T2>
?type? gt(T1 x,T2 y){
return x+y;
}
无法预知x+y的返回类型,还没有声明x,y;而decltype的使用必须在声明参数后,因此decltype无法解决。
double h(int x,float y);
新增语法可以表示为如下:
auto h(int x,float y)->double;
这将返回类型移到类参数声明后,->double被称为后置返回类型;其中auto是一个占位符,表示后置返回类型提供的类型。
因此上面不完整模板函数可以表述如下
template<class T1,class T2>
auto gt(T1 x,T2 y)->decltype(x+y)
{
return x+y;
}