函数探幽
C++内联函数
使用:在声明和定义前都加关键字inline
注意:内联函数不能递归
内联与普通的区别不在于编写方式,而在于C++编译器如何将他们组合到程序中
普通函数:调转需要保存现在的地址,根据函数地址,去执行函数,得到返回值,在跳回来。需要一定的开销。
内联比普通函数运行快,但需要空间大,每有一处调用,就会有一个函数的副本。
引用变量
主要用途:用作函数的形参。将引用变量用作函数参数,函数将使用原始数据而不是其副本。
- 创建引用变量。 int a = 5; int &b = a; //a和b指向相同的值和内存单元。
- 引用不同于指针:必须在声明引用时将其初始化,指针可以先声明在赋值。
- 引用更接近const 指针,必须在创建时初始化,一旦与某个变量关联起来,就将一直效忠他。
int & a = b; int *const a = &b; - 引用的属性和特别之处
double fun(double& a) //使用引用会修改实参的值,如果不想double fun(const double& a),编译器将会报错
{
a *= a * a;
return a;
}
//若fun(x+3) 不行,x+3是表达式不是变量
- 何时创建临时变量
如果实参与引用参数不匹配,C++将生成临时变量。当前,仅当参数为const引用时,C++才才允许这样。
如果引用参数为const,则以下两种情况生成临时变量。
a。实参类型正确但不是左值
b。实参类型不正确,但可以转换为正确的类型
左值参数是可被引用的数据对象。例如:变量,数组元素,结构成员,引用和解除引用的指针都是左值。非左值包括字面常量(用引号括起的字符串除外,它们由其地址表示)和包含多项的表达式。
常规变量和const变量都可以视为左值,因为可以通过地址访问它们。常规变量属于可修改的左值,const变量属于不可修改的左值。
double fun(const double& a)
{
a *= a * a;
return a;
}
double side = 3.0;
double *pd = &side;
double &rd = side;
long ege = 5l;
double lens[4] = {2.0,7.0,3,1};
double c1 = fun(side);
double c2 = fun(lens[2]);
double c3 = fun(rd);
double c3 = fun(*pd); // 上述都可以创建引用,不会创建临时对象
double c3 = fun(ege); //变量,但类型不正确
double c3 = fun(1.8); //没名称
double c3 = fun(side+10,0); //表达式
在最后三种情况下,编译器将生成一个临时的匿名变量,并让a指向它。临时变量只在函数调用期间存在,此后编译器便可以随意将其删除。
实际上,对于形参为const引用的C++函数,如果实参不匹配,则其行为类似于按值传递,为确保原始数据不被修改,将使用临时变量来存储值。
尽可能使用const
- 可以避免无意修改数据的编程错误
- 使函数能够处理const和非const实参,否则只能接受非const数据
- 使用const引用,使函数能够正确生成并使用临时变量。
C++11新增另一种引用——右值引用。可指向右值,使用&&声明。
double && ref = sqrt(36.00); //不允许double &
double j = 15.0;
double &&jref = 2.0*j+18.5; //不允许double &
cout<<ref<<endl; //6
cout<<jref<<endl; //48.5
目的:让库设计人员能够提供有些操作的更有效的实现
注意:避免返回在函数内部声明变量的引用,因为函数结束时,其内存将不存在。为避免这种,可以使用new 或者返回一个作为参数传递给4 将const 用于引用返回类型。
int & sum(int &a ,const int &b);
则 sum(a,b) = c; 可以通过编译。在赋值语句中,左边必须是可修改的左值,即左边的子表达式必须标识一个可修改的内存块。
另一方面,常规(非引用)返回类型是右值——不能通过地址访问的值。这种表达式可出现在赋值语句的右边,但不能出现在左边。其它右值包括字面值(10.0)和表达式(x+y),显然获取字面的地址没有意义 。常规函数的返回值是右值是因为这种返回值位于临时内存单元中,运行到下一条语句时,它们可能就不在存在。
若 const int & sum(int &a ,const int &b); 则 sum(a,b) = c; 非法。
何时使用引用参数
使用引用参数的主要原因有两个:
a. 程序员能够修改调用函数中的数据对象
b. 通过传递引用而不是整个数据对象,可以提高程序的运行速度。
对使用传递的值而不作修改的函数指导原则:
- 如果数据对象很小,如内置数据类型或者小型结构,则应该按值传递。
- 如果数据对象是数组,则使用指针,这是唯一的选择,并将指针声明为指向const的指针。
- 如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率,这样可以节省复制结构所需的时间和空间
- 如果数据对象是类对象,则使用const引用。
对修改调用函数中数据的函数 - 如果数据对象是内置数据类型,则使用指针。
- 如果数据对象是数组,则只能使用指针
- 如果数据对象是结构,则使用引用或指针
- 如果数据对象是类对象,则使用引用。
默认参数
int fun(int a,int b = 2,int c =3); 对于带参数列表的函数,必须从右向左添加默认值。
fun(1,2,3); fun(1,2); fun(1);都是可以的。
函数重载
函数多态(函数重载):可以有多个同名的函数。
重载的关键是函数的参数列表(函数特征标),如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同。而变量名是无关紧要的。C++允许定义同名的函数,前提是它们的特征标不同。
void print(const char* str, int width); //1
void print(double d, int width); //2
void print(long l, int width); //3
void print(int i, int width); //4
void print(const char* str); //5
print(“ssasafgsg”,5); //1
print(“ssasafgsg”); //5
print(7.0,2); //2
print(1,2); //4
print(9L,2); //3
若 unsigned int a =43; print(a,4); ? 若没有匹配的原型,C++使用标准类型转换强制进行匹配,若只有2 ,则使用2 ,但上面有3个将数字作为第一个参数的原型,因此产生二义性,无法编译通过。
double fun(double a) 和double fun(double &a) 视为同一个特征标
- 不区分const 和非const
void dribble(char* b); //重载 1
void dribble(const char* b); //重载 2
void dabble(char* b); //3
void drivel(const char* b); //4
const char p1[20] = “hello”;
char p2[23] = “world”;
dribble(p1); //2
dribble(p2); //1
dabble(p1); //无
dabble(p2); //3
drivel(p1); //4
drivel(p2); //4
注意: 返回类型可以不同,但特征标必须相同
int fun(int a,double b)
double fun(int a,double b) //不是重载,特征标必须不同 - 重载引用参数
void sink(double& r1);// 与可修改的左值(double变量)匹配
void sank(const double& r2); //与可修改的左值(double变量),const左值参数,和右值参数(两个double和)匹配
void sank( double&& r3); //与左值匹配
注意到与r1 或r3 匹配的参数都与r2匹配,若三个函数都在,则使用最匹配的版本
重载示例
unsigned long left(unsigned long num, unsigned ct)
{
unsigned digits = 1;
unsigned long n = num;
if (ct == 0 || num == 0)
return 0;
while (n/=10)
{
digits++;
}
if (digits > ct)
{
ct = digits - ct;
while (ct--)
{
num /= 10;
}
return num;
}
else
{
return num;
}
}
char* left(const char* str, int n)
{
if (n < 0)
n = 0;
char* p = new char[n + 1];
int i;
for (i = 0; i < n && str[i]; i++)
{
p[i] = str[i];
}
while (i<=n)
{
p[i++] = '\0';
}
return p;
}
何时使用重载
仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应该采用函数重载 。
C++使用名称修饰对函数名和参数表进行加密。
函数模板
template<typename T>//template<class T>
void swap(T& t1, T& t2)
{
T tmp;
tmp = t1;
t1 = t2;
t2 = tmp;
}
模板并不创建任何函数,只是告诉编译器如何定义函数,需要int时,编译器将按模板创建这样的函数。
如果需要多个将同一种算法用于不同类型的函数,请使用模板。
函数模板不能缩短可执行程序,最终的代码不包含任何模板,而是包含了为程序实际生成的函数。
更常见的做法是将模板放在头文件中,并在需要使用模板的文件中包含头文件。
重载的模板
和重载常规函数一样,被重载的模板的特征标必须不同。
template<typename T>//template<class 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;
}
}
模板的局限
template
void fun(T a,T b){}
若T为结构,则if(a>b)没有意义
若 T为数组,则a = =b 也不行
一种解决方法是重载运算符,另一种方法是为特定类型提供具有具体化的模板定义。
显示具体化
显示具体化:提供一个具体化函数的定义。
- 第三方具体化(ISO/ANSI C++标准)
a.对于给定的函数名,可以有非模板函数,模板函数和显示具体化模板函数以及它们的重载版本
b.显示具体化的原型和定义应以template<>开头,并通过名称指出类型
c. 具体化优于常规模板,而非模板函数优先于具体化和常规模板
struct Job
{
};
template<> void swap<Job>(Job& j2, Job& j1) //template<> void swap(Job& j2, Job& j1)
{
}
template<typename T>//template<class T>
void Swap(T& t1, T& t2)
{
T tmp;
tmp = t1;
t1 = t2;
t2 = tmp;
}
struct Job
{
char name[20];
int salary;
};
template<> void Swap<Job>(Job& j2, Job& j1)
{
int s;
s = j2.salary;
j2.salary = j1.salary;
j1.salary = s;
}
void showJob(const Job& j2)
{
cout << j2.name << " : " << j2.salary << endl;
}
int main()
{
int a = 1;
int b = 5;
cout << "before :" << endl;
cout << a << " : "<<b << endl;
Swap(a, b);
cout << "after :" << endl;
cout << a << " : " << b << endl;
Job j1{ "a",100 };
Job j2{ "b",1220 };
showJob(j1);
Swap(j1, j2);
showJob(j1);
}
实例化和具体化
在代码中包含模板函数本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例。
隐式实例化: int a = 1;
int b = 5;
Swap(a, b);
显式实例化:可以直接命令编译器创建特定的实例
template void Swap(int, int); 编译器看到后,将会生成一个int类型的实例
显示具体化
template<> void Swap(int, int);
template<> void Swap(int, int); 两个相同
区别在于,这个告诉编译器不要使用Swap模板生成的函数定义,而应该使用专门为int的显示定义的函数类型。这些原型必须有自己的函数定义,显示具体化在关键字 template 后有<> 而显式实例化没有。
注意:试图在同一个文件(或转换单元)中使用同一种显式具体化和显示实例化将会出错。
template<typename T>
void Add(T t1, T t2)
{
return t1 + t2;
}
...
int m = 6;
double x = 10.2;
cout << Add<double>(x, m) << endl;
- 隐式实例化,显示实例化具体化统称为具体化。相同之处在于:它们表示的都是使用具体类型的函数定义
- 总结
template<typename T>//template<class T>
void Swap(T& t1, T& t2)
{
T tmp;
tmp = t1;
t1 = t2;
t2 = tmp;
}
struct Job
{
char name[20];
int salary;
};
template<> void Swap<Job>(Job& j2, Job& j1) //显示具体化
{
int s;
s = j2.salary;
j2.salary = j1.salary;
j1.salary = s;
}
int main()
{
template void Swap<char>(char&, char&); // 显示实例化
short a, b;
Swap(a, b); //隐式实例化
Job j1, j2;
Swap(j1,j2); //显示具体化
char c, d;
Swap(c, d); //显示实例化
}
编译器选择使用哪个函数版本
对于函数重载,函数模板和函数模板重载,C++需要一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时,称为重载解析。过程如下:
- 创建候选函数列表,其中包含与被调用函数的名称相同的函数和模板函数
- 使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用float参数的函数调用,可以将该参数转为double,从而与double形参匹配,而模板可以为float生成一个实例。
- 确定是否有最佳的可行函数,如果有,则使用它,否则函数调用出错。
如下:
may(‘B’);
首先编译器将寻找候选者,即名称为may的函数和函数模板。然后寻找那些可以用一个参数调用的函数。例如下面的符合要求,因为名称与被调用函数相同,且可只给它们传递一个参数。
void may(int); //1
float may(float,float = 3); //2
void may(char); //3
char* may(const char *); //4
char may(const char&); //5
template<class T> void may(const T &); //6
template<class T> void may(T *); //7
注意:只考虑特征标不考虑返回类型,其中4和7不行,因为整数类型不能被隐式的转为指针类型(即没有显式强制类型转换),剩余的一个模板可用来生成具体化,T被换为char。若剩下的5个,每个都是唯一的,则都可以使用。
接下来,编译器确定哪个可行函数是最佳的。它查看为使函数调用参数与可行的候选函数的参数匹配所需要进行的转换,通常从最佳到最差的顺序如下:
a.完全匹配,但常规函数优于模板
b.提升转换(char,short自动转为int,float自动转为double )
c.标准转换(int->char ,long->double )
d. 用户定义的转换,如类声明中定义的转换。
如上:1优于2,因为char->int 为提升转换。 char-》float为标准转换。3 5 6 优于1,2,因为它们完全匹配,3 5 优于6,6为模板。3 5 都完全匹配怎么办,通常有有两个函数完全匹配是一种错误,但这一规则有两个例外。
1. 完全匹配和最佳匹配。进行完全匹配时,C++允许某些无关紧要的转换。Type意味着用作实参的函数名和用作形参的函数指针只要返回类型和参数列表相同,就是匹配的。
struct Student {
int id;
int level;
};
Student stu = {0,1};
recycle(stu);
//在这种情况下,下面的原型都是匹配的
void recycle(Student); //1 Student->Student
void recycle(const Student); //2 Student->const Student
void recycle(Student &); //3 Student->Student &
void recycle(const Student &); //4 Student->const Student &
有多个原型匹配,编译器将无法完成重载解析的过程,产生二义性。
然而有时候,即使两个函数都完全匹配,仍然可以完成重载解析。首先,指向非const数据的指针和引用优先与非const指针和引用的参数匹配。若只定义了3 4 则优先3. 但const 和 非const之间的区别只适用于指针和引用指向的数据,也就是说只定义1 2,则出现二义性。
一个完全匹配优于另一个的情况是,普通函数优于模板。
若都是模板,则具体的模板优先,显式具体化优于使用模板隐式生成的具体化。
最具体并不一定意味着显式具体化,而是指编译器推断使用哪种类型时执行的转化最少。
template<class T> void may(T t); //1
template<class T> void may(T* t); //2
int a;
may(&a);
may(&a)与1匹配,T被解释为int * 。 与2匹配,T被解释为int。 may<int *>(int *) 和 may(int *)两个函数都被发送到可执行函数池中。
may<int *>(int *)被认为更具体,因为在生成过程中它执行的转换更少。2已经显示指出,函数参数是指向T的指针,可以直接用int标识T,而1将T作为函数参数,T必须解释为指向int的指针。即在2中,T已经被具体化为指针,因此它更具体。
用于找出最具体的模板的规则被称为函数模板的部分排序规则。(C98)新增特性。
2. 部分排序规则示例
using namespace std;
template<class T>
void ShowArray(T a[], int n)
{
cout << "template A:\n";
for (int i = 0; i < n; i++)
{
cout << a[i] << " ";
}
cout << endl;
}
template<class T>
void ShowArray(T *a[], int n)
{
cout << "template B:\n";
for (int i=0;i<n;i++)
cout << *a[i] << " ";
cout << endl;
}
struct Test {
char name[20];
double value;
};
int main()
{
int a[5] = { 1,2,3,4,5 };
Test aT[3] = { {"www",20},{"ssw",10},{"tww",20} };
double* pd[3];
for (int i = 0; i < 3; i++)
{
pd[i] = &aT[i].value;
}
ShowArray(a, 5);// use A;
//既和A匹配 也和B匹配。A T为double*,此时打印的数组中的地址
//B T为double,此时打印的是数组中的内容,B更具体,因此使用B
ShowArray(pd, 3); //useB
}
3. 自己选择,编写 合适的函数调用,引导编译器做出希望的选择。
template<class T> //1
T lesser(T a,T b)
{
return a < b ? a : b;
}
int lesser(int a, int b)
{
a = a > 0 ? a : -a;
b = b > 0 ? b : -b;
return a < b ? a : b;
}
int main()
{
int m = 20;
int n = -30;
double x = 15.5;
double y = 25.9;
cout << lesser(m, n) << endl; //2
cout << lesser(x, y) << endl; //1
cout << lesser<>(m, n) << endl; //1
cout << lesser<int>(x, y) << endl; //1 xy的值被转为int
}
- 多个参数的函数
将有多个参数的函数调用与有多个参数的原型进行匹配时,编译器必须考虑所有的参数匹配情况。若找到比其他可行函数都合适的函数,则使用该函数。一个函数比其他函数都合适,其所有参数的匹配程度都必须不比其他函数差,同时至少有一个参数的匹配程度比其他函数都高。
模板函数的发展
- 是什么类型
template<class T1,class T2>
void fun(T1 a,T2 b)
{
...
xy = a + b; //decltype(a + b) xy = a + b;
...
}
//xy 是什么数据类型? 若double +int = double ;short +int = int; short + char = int,结构和类可能更复杂
- 关键字decltype(C++11)
int x;
decltype(x) y; y的类型和x一样
decltype(x+y) xpy;
xpy = x+y; //xpy的类型和x+y一样
decltype(x+y) xpy = x+y;
核对表
decltype(expression) var;
第一步:若expression没有括号括起,则var的类型与该标识符的类型相同,包括const限定符
double x =5.5;
double y = 7.9;
double &rx = x;
const double *pd ;
decltype(x) w; //w doulbe
decltype(rx) u = y; //u double &
decltype(pd) v; //v const double *
第二步:若expression是一个函数调用,则var与函数返回类型相同。(并不会调用函数,编译器查看函数原型,获取返回值)
decltype(fun(a)) c;
第三步:若expression是一个左值,则var为指向其类型的引用。要进入第三步,expression需要括号括起。
double xx =4.4;
decltype((xx)) r2 = xx; r2 double &
decltype(xx) w = xx; w double
第四步:若前面的条件都不满足,则var的类型与 expression相同。
int j =3;
int &k = j;
int &n = j;
decltype(j+6) i1; i1 int
decltype(100L) i2; i2 long
decltype(k+n) i3; i3 int k n 都是引用,但k+n 不是引用,表达式
若需要多次声明,可以综合使用typedef和decltype
template<class T1,class T2>
void fun(T1 a,T2 b)
{
typedef decltype(a + b) abtype;
abtype apb = a + b;
abtype arr[10];
abtype& rxy = arr[2];
}
- C++11 新增后置返回类型
auto fun(int x,int y) ->double; 返回double
template<class T1,class T2>
auto fun(T1 a,T2 b) ->decltype(a + b)
{
return a + b;
}