《C++ Primer Plus》(第6版)中文版—学习笔记—函数探幽

第8章 函数探幽

书中程序清单和练习将在https://github.com/linlll/CppPrimePlus发布

C++内联函数

内联函数是C++为提高程序运行速度所做的一项改进。

执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许也需将返回值放入到寄存器中),然后跳回到地址被保存的指令处(这与阅读文章时停下来看脚注,并在阅读完脚注后返回到以前阅读的地方类似)。来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。

这便是函数的底层实现机制,但这往往会有比较大的开销,使用内联函数可以抵消这种开销,但是却耗费了内存,对于一些不需要执行很多代码的函数而言,就可以使用这种内联函数机制。

内联函数需要采取下述措施:

  1. 在函数声明前加上关键字inline
  2. 在函数定义前加上关键字inline

通常可以省略函数原理,将整个定义放在函数原型的位置。例如

#include <iostream>
using namespace std;
inline double square(double x) {
    
     return x * x }
int main(){
    
    /*code*/}

需要注意的是,程序员请求将函数作为内联函数时,编译器并不一定会满足这种要求。它可能认为该函数多大或注意到函数调用了自己(内联函数不能递归),因此不将其作为内联函数;而有些编译器没有启用或实现这种特性。


内联函数的前身其实是C语言的宏,宏和内联函数不一样,内联函数是使用按值传递的,但是宏是通过文本替代来实现的,如以下示例

#define SQUARE(X) X*X

a = SQUARE(5.0);			// is replaced by a = 5.0 * 5.0
b = SQUARE(4.5 + 7.5);		// is replaced by b = 4.5 + 7.5 * 4.5 + 7.5
c = SQUARE(C++);			// is replaced by c = C++ * C++

显然,要实现平方的功能,上述代码只有第一个可以正常运行。对于第二种的改进如下

#define SQUARE(X) ((X) * (X))

引用变量

前面提到的函数指针的概念,如果不适用自动类型判断,会出现一些很麻烦的情况,这使得我们在编写代码的时候会花费很多的时间在理解逻辑上面。C++新增了一种复合类型——引用变量,引用是已定义的变量的别名(另一个名称)。通过引用,我们就可以使用原始数据了,而不用想形参一样按值传递,这样耗费了时间和内存,对于一些结构体和对象而言,是十分不友好的,因为结构和对象一般会花费较大的内存,这也隐隐透露着引用好像就是为结构和对象量身打造的一样。让我们来看看引用是如何实现的吧,并且指出引用和指针的区别。

那么引用时如何实现的呢,使用&符号,这里的&并不是取地址符,而是类型标识符的一部分。如下代码,这里需要注意的是,在声明引用变量的时候,必须要有初始化,就好像这个引用一定要有一个人来为你取名一样,不然就相当于没有上户口的孤儿一样,C++不允许这样的存在。例如注释处的代码不能够实现,这一点不能像指针一样进行赋值操作

int rats;
int & rodents = rats;
// int & rodents;	rodents = rats

引用更接近const指针,因为引用必须在声明的时候初始化,也就是一旦引用变量出生,就一定要有名字。指针的写法如下。

int * const pr = &rats;

引用最常用也是最典范的用法就是引用传递,利用引用的特点,函数就可以使用主函数中的原始数据了。例如

void swap(int & a, int & b)
{
    
    
	int temp;
	temp = a;
	a = b;
	b = temp;
}

但是引用传递和按值传递还是有一些区别的,我们说过,引用变量是一个变量的别名,我们知道,变量和表达式是不一样的,一个是可修改的左值,一个是右值,按值传递是可以传递表达式的,只要表达式的类型是正确的或者可以强制转换。例如

double cube(double a)
{
    
    
	a *= a * a;
	return a;
}
double refcube(double & ra)
{
    
    
	ra *= ra * ra;
	return ra;
}

int x = 10;
cube(x + 10);
cube(x);
cube(10);

refcube(x + 10);
refcube(x);
refcube(10);

你会发现,使用引用传递的函数是不合法的,对于现在的C++是错误的,大多数编译器会指出这一点,较老的编译器会发出警告。

早期的C++确实可以采取这样温和的反应,这取决于临时变量的功劳,当实参与引用参数不匹配的时候,C++将会生成临时变量。对于现在,只有const引用时,C++才允许这样做。 我们还是规矩点,不这样做了吧。


void display(const free_throws & ft)
{
    
    
	using namespace std;
	...
}
void set_pc(free_throws & ft)
{
    
    
	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)
{
    
    
	...
	return target;
}

accumulate(dup, five) = four;

观察上述代码(完整代码请看原文),其中最值得讲解的就是accumulate(dup, five) = four;accumulate这个函数有两个参数,两个参数都是引用变量,其中一个是const类型的,并且函数传回了引用的类型。这个等式其实等同于下述代码

accumulate(dup, five);
dup = four;

我们还是再来讨论这个等式吧。你会注意到等式的左值是一个函数,一般的按值返回的函数是不可以将函数放于左值的,因为赋值语句要求等式的左边是可修改的左值,那么为什么accumulate这个函数却可以呢,因为它返回的是引用,引用即使一个变量的别名,相当于一个变量,也就是说是可以修改的,所以可以使用上述的等式进行赋值。

accumulate这个函数写的没有一点问题,但是我们写的时候,可以随便使用吗?假如我们写出了下面的代码

double & clone(const & a)
{
    
    
	double b = a;
	return b;
}

这样的代码是否可行呢,答案是不可行,因为b变量仅仅是clone函数中的局部变量,在函数返回的时候属于临时变量,随时会被编译器销毁,我们不能返回一个即将被销毁的临时变量。如何解决呢,使用指针便可以,例如

const double & clone(const & a)
{
    
    
	double *b = &a;
	return *b;
}
double & clone(const & a)
{
    
    
	double *b = &a;
	return *b;
}

由于引用其实是主函数中的变量,而指针又是指向了主函数中的变量,但会这个变量,其实就是返回主函数中的变量。上面的代码给出了两个函数,这两个函数都是可行的,但是我们本着尽量使用const的原则,返回的值是不可修改的,但这也要看具体语境的。就比如accumulate函数,因为它的返回值允许被修改。


将类对象传递给函数,C++通常的做法就是使用引用。下面是一些string类的例子

string version1(const string & s1, const string & s2)
{
    
    
    string temp;
    temp = s2 + s1 + s2;
    return temp;
}

const string & version2(string & s1, const string & s2)
{
    
    
    s1 = s2 + s1 + s2;
    return s1; 
}

const string & version3(string & s1, const string & s2)
{
    
    
    string temp;
    temp = s2 + s1 + s2;
    return temp;
}
result = version1(input, "***");
result = version2(input, "###");
result = version3(input, "@@@");

这里值得一提的是,我们知道,C-风格字符串的类型为const char *类型,上述三个version函数的第二个参数我们都是以字符串字面值输入的,也就是const char *类型的,但是函数原型是string类的输入类型,为什么可以这样呢?这是C++允许的,string·类定义了一种char *string的转换功能,所以其实是可以的。

我们知道ostreamofstream是一种对象,对象也是可以使用引用,实际上最好使用引用传递。ofstream是继承与ostream的,具体继承的内容今后会讲述。用法请看原文程序清单

那么什么时候应该使用引用参数呢?下面列出了很多

首先使用引用参数的主要原因有两个

  1. 程序员能够修改调用函数中的数据对象
  2. 通过传递引用而不是整个数据对象,可以提高程序的运行速度

当数据很大时,比如结构体和对象等,使用引用参数可以提高程序的运行速度。

对于使用传递的值而不作修改的函数

  1. 如果数据对象很小,如内置数据类型或小型结构,则按值传递
  2. 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针
  3. 如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率。这样可以节省复制结构所需的时间和空间
  4. 如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用,这是C++新增这项特性的主要原因。因此,传递类对象的标准方式是按引用传递

对于修改调用函数中数据的函数

  1. 如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码(其中xint),则很明显,该函数将修改x
  2. 如果数据对象是数组,则只能使用指针
  3. 如果数据对象是结构,则使用引用或指针
  4. 如果数据对象是类对象,则使用引用。

默认参数

默认参数是C++的一个新内容,我们可以通过函数原型实现默认参数,例如

char * left(const char * str, int n = 1);
left("LinJia", 10);
left("LinJia");

但是对于带参数列表的函数,必须从右向左添加默认值,也就是说,要为某个参数设置默认值,则必须为它右边的所有参数提供默认值。请看

int fun(int a, int b = 1, int c = 2);		//VALID
int fun(int a, int b = 1, int c);			//INVALID
int fun(int a = 1, int b = 2, int c = 3);	//VALID

而调用的时候允许提供1个、2个、或3个参数:

beeps = fun(4);
beeps = fun(4, 5);
beeps = fun(4, 5, 6);

这些参数必需按从左向右的顺序依次被赋值给相应的形参,而不能跳过任何参数,例如beeps = fun(3, , 8)

函数重载

函数重载又为函数多态,重载可以让你能够使用多个同名的函数,这有什么好处呢,就好比一个单词有好几种意思,每种意思都有独特的用法一样,函数重载就能够这样处理,能够接受不同类型的参数,并对不同的参数实施不同的运算,并且返回值也能够重载。例如

void print(const char * str, int width);
void print(double d, int width);
void print(long l, int width);
void print(int i, int width);
void print(const char * str);

还有一种编译器会判断不出是什么类型,因为doubledouble &传入一个变量的时候都可以使用,所以为了避免这种混乱,编译器在检查函数特征标时,将把类型引用和类型本身是为同一种特征标。

double cube(double x);
double cube(double & x);

还有一点,对于非const值是可以赋值给const值的,但是反之是违法的。例如

void dribble(char *bits);
void dribble(const char *cbits);
void dabble(char *bits);
void drivel(const char *bits);

const char p1[20] = "How's the weather?";
char p2[20] = "How's business?";
dribble(p1);
dribble(p2);
dabbale(p1); // not match
dabbale(p2);
drivel(p1);
drivel(p2);

可以看到有一句是不能匹配的,这也就是为什么对于非const值是可以赋值给const值的,但是反之是违法的。


对于特征标才是让函数能够重载的标志,例如

long fun(int n);
double fun(int n);

对于函数重载来说,返回类型不一样是允许的,但是上述代码为什么就是不行呢,可以看到,参数列表中只有一个int类型的变量,但是两个函数都一样,这样就不行,因为特征标是括号里面的参数,不能够完全一样,下面的代码就是OK的

long fun(int n);
double fun(double n);

请看原文的重载引用参数

void fun(double && r);

&&符号表示的是右值参数输入。

函数模板

函数模板也是C++新增的一个特性。使得我们可以使用泛型定义来定义函数。所以有时候也被称为通用编程。下面建立了一个函数模板

template <typename AnyType>
void swap(AnyType & a, AnyType & b)
{
    
    
	AnyType temp;
	temp = a;
	a = b;
	b = temp;
}

这样就定义个函数模板。


函数模板并不能够缩短可执行程序,也就是说,如果我们在主函数中以不同类型的变量调用了swap函数,最终编译器会编译成两个独立的函数定义,函数模板的出现只是将我们在编程的过程通用化,而对编译器而言还是一样的。


函数模板也能够重载,例如

template <typename T>
void swap(T & a, T & b){
    
     }
void swap(T *a, T *b){
    
     }

通用编程也带来了一定的局限性,比如说赋值操作,当你定义了一个函数模板,如果程序员不提示输入,有些用户不清楚需要传入的数据类型,若传入的是一个数组,我们知道数组是不能够使用赋值操作的,所以这将导致错误。另一个例子是判断式,比如a > b,如果两个变量是结构体,这样是能够进行比较的。


1. 隐式实例化

当我们写好了一个函数模板,假如之前所写的交换函数swap(),当在主函数中调用了swap(i, j),而ij这两个变量都是int类型的,那么编译器就会根据函数模板生成一个intswap()的函数定义,这样就成之为隐式实例化。

2. 显式实例化

template void swap<int>(int, int);

这样的就是显示实例化,能过够指定编译器使用指向的函数定义。这样的实例化是使用函数模板生成一个使用int类型的实例。就像隐式实例化一样。

3. 显式具体化

template <> void swap<int>(int, int);
template <> void swap(int, int);

这样的声明的意思是:“不要使用swap()模板来生成函数定义。而应使用专门为int类型显式地定义的函数定义”,也就是说显式具体化必须要有自己的函数定义

编译器如何选择函数版本

请看原文

函数模板中遇到的问题

是什么类型

template<typename T1, typename T2>
void fun(T1 x, T2, y)
{
    
    
	...
	?type? xpy = x + y;
	...
}

这样一来,xpy的类型我们不从而知,这样会出现很多麻烦。

关键字decltype(C++)

template<typename T1, typename T2>
void fun(T1 x, T2, y)
{
    
    
	...
	decltype(xpy) = x + y;
	...
}

这样就能够判断类型了。

另一种函数声明语法(C++11 后置返回类型)

就像之前的问题一样,利用return语法来返回数值,对于返回的类型我们也不从而知,后置返回类型是另一种函数声明语法,如下

template<typename T1, typename T2>
auto fun(T1 x, T2, y)->decltype(x + y)
{
    
    
	return x + y;
}

猜你喜欢

转载自blog.csdn.net/weixin_49643423/article/details/107676523