第8章 函数探幽
- 内联函数
- 引用函数
- 如何按引用传递函数参数
- 默认参数
- 函数重载
- 函数模板
- 函数模板具体化
8.1 内联函数
-
作用: 为提高程序运行的速度。
-
与常规函数的区别: 编译原理不同。
-
适应场景: 函数执行时间耗时短且使用频繁。
-
实现: 将整个函数定义放在原本提供原型的地方,并在定义前加上关键字
inline
.
# include <iostream>
inline double square(double x){ return x*x;}
int main()
{
using namespace std;
double a,b;
a = square(5.0);
b = square(4.0+3.0);
cout<< a << ", " << b;
return 0;
}
25, 49
- 如果使用C语言的宏执行了类似函数的功能,应该考虑将它们转换为内联函数。但是,宏与内联有区别,宏类似于文本替代,而内联是函数。
8.2 引用变量
- 何为引用: 引用是已定义变量的别名,是原始数据的另一个名称,指向相同的值和地址不是创建的副本。
- 用来干啥: 主要用来作函数的形参。
8.2.1 创建引用变量
- 下面介绍引用变量是如何工作的
int a = 1;
int & b = a; // &不是地址运算符,而是作为类型标识符(引用)
cout<<a<<b;
11
//using a reference
#include<iostream>
using namespace std;
int main()
{
int a = 101;
int & b = a;
cout<< " a = " << a<< ", " <<" b = "<<b << endl;
a ++ ; // a++ , b也++
cout<< " a = " << a<< ", " <<" b = "<<b << endl;
cout<< " a address = " << &a << ", " << " b address = " << &b;
return 0;
}
a = 101, b = 101
a = 102, b = 102
a address = 0x7fc2e04fd050, b address = 0x7fc2e04fd050
- 引用不仅值相同,地址也相同。对其中一个变量操作相当于对2个变量都进行了操作。
- 引用区别于指针:引用必须在声明时就必须初始化。更像const指针,必须在创建时就初始化,并一直效忠于它。
int & b = a 等价于 int* cont b = &a
//引用变量必须声明就初始化
#include <iostream>
using namespace std;
int main()
{
int a =1;
int &b = a;
cout<< "a = " << a <<", b = " << b<<endl;
int c = 2;
b = c;
cout<< " a = " << a << ", b= "<< b <<", c = " << c;
return 0;
}
a = 1, b = 1
a = 2, b= 2, c = 2
- 对引用变量赋值会同时改变引用变量对应的变量。
8.2.2 将引用用作函数参数
- 作用:使得被调用的函数可以访问调用函数中的变量,而不是其副本。这种传递方法也称为——
引用传递
.
#include <iostream>
void swapr( int &a , int &b)
{
int temp ;
temp = a;
a = b;
b = temp;
}
void swapp( int *a ,int *b)
{
int temp;
temp = *a;
*a = *b;
*b = temp;
}
void swapv( int a ,int b)//不改变传递进来的参数
{
int temp;
temp =a;
a=b;
b=temp;
}
using namespace std;
int main()
{
int x1 = 1;
int x2 = 2;
swapr(x1,x2);//x1,x2交换
cout<< " 引用传递后 " << x1 << x2 <<endl;
swapp(&x1, &x2);//x1,x2交换
cout<< " 指针传递后 " << x1 << x2 << endl;
swapv( x1, x2);//x1,x2本身没有交换
cout<< "按值传递后 " << x1 << x2<< endl;
return 0;
}
引用传递后 21
指针传递后 12
按值传递后 12
- 按值传递不能交换传入函数的2个参数,因为传递来的是2个副本,交换副本后,原始变量没有交换。而引用传递参数是变量的别名,形参被改变,实参实际上也被改变。
8.2.3 引用的属性和特别之处
- 引用传参必须传入函数声明时的同类型变量,同类型参量都不行。
- const 引用比引用更兼容,类似按值传递。当传入的类型不对时,会生成一个临时变量传入函数。
&&
右值引用可以引用右值,比如表达式
double cube (double a)//按值传递的函数实现立方
{
a *= a*a;
return a;
}
double cude_reference(double &a)//引用传递的函数实现立方
{
a *= a*a;
return a;
}
double x =3.0;
cout<<cube(x);
27.000000
cout<<x;
3.0000000
cout<<cude_reference(x);
27.000000
cout<<x;
27.000000
cude_reference()
是引用传递参数,所以实际上也改变了x。cube()
是按值传递,所以不改变传入的参数。
- 如果只是想使用引用传递,而不修改参数,可以使用常量引用,这样在修改参数时会报错。
double cude_ref( const double & a)
{
a *= a*a;//这里修改了a 的值
return a;
}
[1minput_line_36:3:7: [0m[0;1;31merror: [0m[1mcannot assign to variable 'a' with const-qualified type 'const double &'[0m
a *= a*a;
[0;1;32m ~ ^
[0m[1minput_line_36:1:33: [0m[0;1;30mnote: [0mvariable 'a' declared const here[0m
double cude_ref( const double & a)
[0;1;32m ~~~~~~~~~~~~~~~^
[0m
Interpreter Error:
- 另外,按值传递因为传递是变量的副本,所以可以使用多种类型的实参,包括表达式,定值常数。下面这些都是合法的。
double z = cube(x+2.0);
z = cube(8.0);
int y = 3;
z = cube(y); // int 类型转换为 double
double yy[3] = {1.0, 2.0, 3.0};
z = cube(yy[2]);
cout<<z;
27.000000
- 而引用传递参数时更加严格。实参只能是double变量,而不能是表达式,常量或是其他类型的变量。
double cude_reference(double &a)
{
return a*a*a;
}
double z = cude_reference(x+2.0);
[1minput_line_91:2:13: [0m[0;1;31merror: [0m[1mno matching function for call to 'cude_reference'[0m
double z = cude_reference(x+2.0);
[0;1;32m ^~~~~~~~~~~~~~
[0m[1minput_line_90:1:8: [0m[0;1;30mnote: [0mcandidate function not viable: expects an l-value for 1st argument[0m
double cude_reference(double &a)
[0;1;32m ^
[0m
Interpreter Error:
double z = cude_reference(2.0);
[1minput_line_92:2:13: [0m[0;1;31merror: [0m[1mno matching function for call to 'cude_reference'[0m
double z = cude_reference(2.0);
[0;1;32m ^~~~~~~~~~~~~~
[0m[1minput_line_90:1:8: [0m[0;1;30mnote: [0mcandidate function not viable: expects an l-value for 1st argument[0m
double cude_reference(double &a)
[0;1;32m ^
[0m
Interpreter Error:
long y = 5L;
double z = cude_reference(y);
[1minput_line_93:3:12: [0m[0;1;31merror: [0m[1mno matching function for call to 'cude_reference'[0m
double z = cude_reference(y);
[0;1;32m ^~~~~~~~~~~~~~
[0m[1minput_line_90:1:8: [0m[0;1;30mnote: [0mcandidate function not viable: no known conversion from 'long' to 'double &' for 1st argument[0m
double cude_reference(double &a)
[0;1;32m ^
[0m
Interpreter Error:
- 反而,
const
引用允许一些实参与引用参数不匹配的情况,因为c++这时将会生成临时变量。
double cube_const_ref( const double & a){
return a*a*a;// a的值没有被修改
}
double a = 3.0;
cout<<cube_const_ref(a);
27.000000
cout<<a;
3.0000000
double *b = &a;
cout<<cube_const_ref(*b);//传入指针
27.000000
cout<<*b;
3.0000000
long c = 3L;
cout<<cube_const_ref(c);//传入long类型
27.000000
cout<<c;
3
cout<<cube_const_ref(3.0); //传入常量
27.000000
cout<<cube_const_ref(1.0+2.0);//传入表达式
27.000000
- 注意: 对于
const 引用传参
如果函数调用的参数不是左值或参数类型不匹配,则c++将创建类型正确的匿名临时变量,将函数调用的参数的值传递给该临时变量,并让参数引用该临时变量。这就类似于按值传递。
如果使用引用应尽可能使用const引用
- 补充: 右值引用
&&
double a =1.0;
double &&b = 2.0*a + 1.0;//&&右值引用可以引用表达书这类右值
cout<<b;
3.0000000
8.2.4 将引用用于结构
#include <iostream>
#include <string>
struct free_throws//定义一个结构体
{
std::string name;
int made;
int attempts;
float percent;
}
void display( const free_throws &ft)//打印结构体的成员
{
using namespace std;
cout<< "name: "<< ft.name;
cout<< " Made: "<< ft.made;
cout<< " attempts: "<< ft.attempts;
cout<< " percent: "<< ft.percent << '\n';
}
void set_pc(free_throws & ft)//计算percent
{
if( ft.attempts != 0)
{
ft.percent = 100.0f*float(ft.made)/float(ft.attempts);
}
else
{
ft.percent = 0;
}
}
free_throws & accumulate (free_throws & target, const free_throws & source)//把结构体2加到结构体1上
{
target.attempts += source.attempts;
target.made += source.made;
set_pc(target);
return target;
}
int main(){
using namespace std;
free_throws one = {"aaa", 13,14};
free_throws two = {"bbb", 10,16};
free_throws three = {"ccc", 7, 9};
free_throws four = {"ddd", 5,9};
free_throws five = {"eee", 6, 14};
free_throws acc = {"fff" ,0, 0};
free_throws dup;
set_pc(one);
display(one);
accumulate(acc, one);
display(acc);
display(accumulate( acc, two));
display(accumulate(accumulate(acc,three), four));
dup = accumulate(acc, five);
display(acc);
display(dup);
accumulate(dup,five) = four;
display(dup);
return 0;
}
name: aaa Made: 13 attempts: 14 percent: 92.8571
name: fff Made: 13 attempts: 14 percent: 92.8571
name: fff Made: 23 attempts: 30 percent: 76.6667
name: fff Made: 35 attempts: 48 percent: 72.9167
name: fff Made: 41 attempts: 62 percent: 66.129
name: fff Made: 41 attempts: 62 percent: 66.129
name: ddd Made: 5 attempts: 9 percent: 0
程序说明:
set_pc()
的形参为引用,因此调用的是结构体本体,可以直接对成员进行修改,而若采用按值传递则无法做到。另一种做法是使用指针参数传递地址,但要复杂一些。void set_pcp( free_throws * pt) { if(pt->attempts != 0) { pt->percent = 100.0f*float(pt->made)/float(pt->attempts); } else { pt->percent = 0; } } set_pcp( &one);
display()
形参是const引用,因为只显示而不修改。这里也可以使用按值传递,但是更耗费时间和内存。accumulate()
第一个形参为引用,第二个形参为const引用,因为不修改第二个实参,只修改第一个实参。返回值为引用,引用指向修改后的第一个实参。这样可以在返回时减少了复制的成本。
注意: 应避免返回函数终止时不再存在的内存单元引用。因为临时变量在函数允许完毕后不再存在。
8.2.5 将引用用于类对象
- 将类对象传递给函数时,c++通常使用引用。
#include <iostream>
#include <string>
std::string version1 (const std::string & s1, const std::string &s2)
{
std::string temp;
temp = s2+ s1+ s2;
return temp;
}
const std::string & version2( std::string & s1, const std::string & s2)
{
s1 = s2 + s1 + s2;
return s1;
}
const std::string & version3 ( std::string & s1, const std::string & s2)
{
std::string temp;
temp = s2 + s1 +s2;
return temp;
}
int main()
{
using namespace std;
string s1 = "aaa";
string result;
result = version1( s1, "**");//version1返回一个string临时变量
cout<< "version1 " << result<< "\n";
cout<< " s1: " << s1<< "\n";
result = version2( s1, "##");//version2引用s1且直接修改s1,返回修改s1后的引用。
cout<< " version2 " << result << "\n";
cout<< " s1: " << s1<< "\n";
result = version3( s1, "//");//version3返回一个临时变量的引用,这是不允许的。
cout << " version3 " << result << "/n";
cout<< " s1: " << s1;
}
version1 **aaa**
s1: aaa
version2 ##aaa##
s1: ##aaa##
8.2.6 对象、继承和引用
继承的2个特性:
- 派生类继承了基类的方法,可以使用基类的特性;
- 基类的引用可以指向派生类对象。eg. 参数类型为ostream& 的函数可以接受ostream对象以及派生类ofstream对象作为实参。
8.2.7 何时使用引用参数
8.3 默认参数
- 默认函数让你能够使用不同数目的参数调用同一个函数
- 必须通过函数原型设置参数默认值;
- 必须从右往左添加默认值,实参必须从左往右给出。
#include <iostream>
char * left (const char * str, int n = 1)
{
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;
}
int main(){
using namespace std;
const int Arsize = 80;
char sample[Arsize];
cin.get(sample,Arsize);
char * ps = left( sample, 4);
cout << ps << "\n";
ps = left(sample);
cout<< ps << "\n";
return 0;
}
ssssssss
ssss
s
8.4 函数重载(多态)
- 多态让你使用多个同名函数,它们完成相同的工作,但使用不同的参数列表
- 何时使用: 仅当函数基本上执行相同任务,但是使用不同数据类型时。
//我可以定义一组原型如下的print()函数
void print( const char* pr, int width);
void print ( double d, int width);
void print( long l, int width);
void print ( int i, int width);
- 定义多个同名函数的关键是,它们的参数数目或类型不同,即它们的函数特征标不同。
- 使用被重载函数时,需要在函数调用中使用正确的参数类型。如果没有匹配的原型,c++会尝试进行强制类型转换,此时如果会有多个函数匹配,这将被视为错误。
- 类型引用和类型本身是同一种特征标
- 函数重载,返回类型可以不同,但是特征标必须不同。
#include <iostream>
unsigned 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;
}
int main()
{
using namespace std;
char * trip = "Hawii!!";
unsigned long n = 12345678;
int i ;
char * temp;
for ( i = 1; i < 10; i++)
{
cout << left( n, i) << "\n";
temp = left(trip, i);
cout<< temp << "\n";
delete [] temp;
}
return 0;
}
[1minput_line_47:3:15: [0m[0;1;35mwarning: [0m[1mISO C++11 does not allow conversion from string literal to 'char *' [-Wwritable-strings][0m
char * trip = "Hawii!!";
[0;1;32m ^
[0m
1
H
12
Ha
123
Haw
1234
Hawi
12345
Hawii
123456
Hawii!
1234567
Hawii!!
12345678
Hawii!!
12345678
Hawii!!
8.5 函数模板
- 模板允许你同一个函数使用不同的参数类型。
#include <iostream>
template <typename Anytype>//template,typename是关键字,指出要建立一个模板函数。typename也可以用class代替
void Swap(Anytype &a, Anytype &b)
{
Anytype temp;
temp = a;
a = b;
b = temp;
}
int main()
{
using namespace std;
int i = 10;
int j =20;
cout<< "i,j = " << i<< ", " << j<< ".\n";
Swap(i,j);// Swap接收了2个int类型参数
cout<<"i,j = " << i << ", "<<j<< ".\n";
double x = 23.5;
double y = 81.7;
cout<< " x,y = " << x <<", " << y <<".\n";
Swap(x,y);// Swap 接收了2个double类型参数
cout<< " x,y = " << x <<", " << y <<".\n";
return 0;
}
i,j = 10, 20.
i,j = 20, 10.
x,y = 23.5, 81.7.
x,y = 81.7, 23.5.
8.5.1 重载的模板
- 可以像重载常规函数那样重载模板函数
#include <iostream>
template <typename T>
void Swap( T &a, T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}
template <typename T>
void Swap( T a[], T b[], int n )
{
T temp;
for(int i =0; i<n; i++)
{
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
void show ( int a[])
{
using namespace std;
cout<< a[0] << a[1] << "/";
cout<< a[2] << a[3]<< "/";
for ( int i =4; i < 8; i++)
{
cout << a[i];
}
cout<< "\n";
}
int main()
{
using namespace std;
int i =10, j =20;
cout<< "i,j = " << i<< ", " << j<< ".\n";
Swap(i,j);// Swap接收了2个int类型参数
cout<<"i,j = " << i << ", "<<j<< ".\n";
int a1[8] = {1,2,3,4,5,6,7,8};
int a2[8] = {8,7,6,5,4,3,2,1};
show(a1);
show(a2);
Swap(a1,a2,8);
cout<< " After swap: "<<".\n";
show(a1);
show(a2);
return 0;
}
i,j = 10, 20.
i,j = 20, 10.
12/34/5678
87/65/4321
After swap: .
87/65/4321
12/34/5678
8.5.2 模板的局限性
- 模板函数是一种泛化函数,虽然对某些类型是合适的,但是对其他类型可能会引发问题。例如,int类型可以加减,但是数组和结构体加减却没有意义。
- 一个解决方法是,重载运算符,使之能够用于特定的结构体和类;
- 另一个是, 为特点类型提供具体化的模板定义。
8.5.3 显式具体化
下面是交换job结构的非模板函数,模板函数和具体化原型:
struct job
{
char name[40];
double salary;
int floor;
}
void Swap(job &, job &);//非模板函数
template <typename T>//模板函数
void Swap(T &,T &);
template <> void Swap<job>(job &, job &);//具体化//<job>是可选的
template <typename T>
void Swap(T &a, T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}
template <> void Swap<job> ( job &j1, job &j2)
{
double d1;
int i1;
d1 = j1.salary;
j1.salary = j2.salary;
j2.salary = d1;
i1 = j1.floor;
j1.floor = j2.floor;
j2.floor = i1;
}
void show( job & j)
{
using namespace std;
cout<< j.name << ", " << j.floor << ".\n";
}
int main()
{
using namespace std;
cout.precision(2);
cout.setf(ios::fixed, ios::floatfield);
int i = 10, j =20;
cout<< "i,j = " << i << ", " << j << ".\n";
Swap(i,j);
cout<< "After Swap: ";
cout<< "i,j = " << i << ", " << j << ".\n";
job j1 = {"aaaaa", 7300.666, 7};
job j2 = {"bbbb", 111.1111, 11};
show (j1);
show(j2);
Swap(j1, j2);
cout<< "After Swap: ";
show(j1);
show(j2);
return 0;
}
i,j = 10, 20.
After Swap: i,j = 20, 10.
aaaaa, 7.
bbbb, 11.
After Swap: aaaaa, 11.
bbbb, 7.
8.5.4 实例化和具体化
- 模板函数并非函数的定义,它只是一个用于生成函数的方案。具体的函数由编译器将模板具体化得到.
- 具体化包括实例化和显示具体化,而实例化又包括隐式实例化和显示实例化。
- 隐式实例化和显式实例化
- 隐式具体化是指调用模板函数时,编译器根据参数类型自动生成一个具体函数的方法;
- 显示实例化是指直接命令编译器创建特定的实例。
- 显式实例化和显式具体化
- 显式具体化正如上一节介绍,需要另外提供函数定义,语法上也略有区别。
- 显式实例化可以不用原型,直接在main函数中创建。
- 注意:同一种类型的显式实例化和显式具体化将会出错
- 显示实例化:
template void Swap<int>(int, int);//使用Swap()模板生成int类型的函数定义
main中:Swap(x, y);//其中x,y是int型变量
- 显示具体化:
template<> void Swap<int> (int &, int &);
template<> void Swap( int &, int & );//两种声明等价
8.5.5 模板函数的发展
- 在编写模板函数时,有个问题是,并非总能知道应该在声明中使用哪种类型。
template<class t1, class t2>
void ft(t1 x, t2 y)
{
...
?type? xpy = x+y;//xpy的类型将是不确定的
...
}
怎么办呢???
8.5.5.1 关键字decltype
//可以对上面的模板函数修改为
int x ;
double y;
decltype(x+y) xpy = x+y;// xpy类型为x+y的类型包括const等限定符
8.5.5.2 另一种函数声明语法(后置放回类型)
template<class T1, class T2>
?type? gt (T1 x, T2 y)
{
...
return x+y;
...
}
由于还不知道x+y的类型,所以decltype无法用于函数声明。
template<class T1, class T2>
auto gt (T1 x, T2 y) -> decltype(x+y)// 后置返回类型
{
...
return x+y;
...
}