c++为函数添加的新特性:
内联函数、默认参数、模板函数、按引用传递参数、函数重载。本篇文章将围绕这五大方面进行介绍。
内联函数:
设计初衷:普通函数调用需要做繁杂的工作,譬如:保存当前代码的地址、将函数参数复制到分配的堆栈中、跳转到指定的函数入口、执行函数代码、返回被保存的指令的地址。当被调用函数的执行时间很短时,大部分的时间都用来作预处理,效率不高,特别是,该函数存在多次调用。为此,c++提出了新的机制,直接用代码替换调用语句,以免去程序跳转。
如何声明内联函数呢?
inline修饰函数,通常在写函数原型的地方来直接定义函数!
举个简单的例子:
//inline 函数简单用法 适用于简短的程序
#include "stdafx.h"
#include<iostream>
using namespace std;
inline int square(int x)
{
return x*x;
}
int main()
{
int x;
cin >> x;
cout << square(x) << endl;
cin.get();
cin.get();
return 0;
}
引用变量及作为函数参数的典型用法:
c++新增了一种复合类型:引用类型,已定义变量的别名,最主要的用法是将其作为参数时,直接操作原始数据,而无需拷贝数据。
先来简单的介绍一下:定义引用变量和使用引用变量。
定义引用变量:这里要注意,定义引用变量时一定要为其初始化,与定义指针变量不同,可以先声明,之后再初始化,引用变量不可以,而且一旦为其初始化就不可以再改变引用变量所指向的变量了!
//理解引用变量
#include "stdafx.h"
#include<iostream>
using namespace std;
int main()
{
int x = 100;
int & y = x;
int z = 200;
y = z;//引用是终身制的,无法改变引用变量的指向了,这里就是简单的赋值操作!没有达到更改引用对象的效果
cout << x << " " << y << endl;
cout << z << endl;
cout << &x << " " << &y << endl;
cout << &z << endl;
cin.get();
return 0;
}
按引用传递(引用变量作为函数参数):
使得在函数内部可直接操作调用程序的变量。按值传递两个变量,两个名词;引用传递一个变量,两个名称。
//值传递、引用传递、指针传递三种方式交换变量值
#include "stdafx.h"
#include<iostream>
using namespace std;
void swap_value(int ,int );
void swap_pointer(int *, int *);
void swap_yinyong(int &,int &);
int main()
{
int x, y;
x = 10;
y = 2;
swap_value(x,y);
cout << x << " " << y << endl;
swap_pointer(&x, &y);
cout << x << " " << y << endl;
swap_yinyong(x,y);
cout << x << " " << y << endl;
cin.get();
return 0;
}
void swap_value(int a, int b)
{
int temp;
temp = a;
a = b;
b = temp;
}
void swap_pointer(int *a, int *b)
{
//地址值不变,指向的内存空间不变,直接操作内存空间的值
int temp;
temp = *a;
*a = *b;
*b = temp;
}
void swap_yinyong(int &a, int &b)
{
int temp;
temp = a;
a = b;
b = temp;
}
引用参数的特别之处:
在被调用函数中修改引用对象的数据值,可能会破坏调用程序中的数据。如果我们的初衷只是使用调用程序的数据而不修改它,那么在定义函数参数时,用const修饰引用参数。
引用传参、值传参适用环境不同,对于基本类型的数据来说,值传参更合适,数据量不大,不怎么费时,而且实参的表达方式更广泛,但对于数组、结构、对象来说,数据值较大,拷贝工程量大,更适合引用传参,但是实参的种类会被限制,这是因为形参说明要指向已知的变量,因而实参也应该是变量形式。
如果实参以表达式形式出现,需要先生成临时变量,使得形参指向该临时变量,这里产生了一个矛盾:如果在被调用函数中希望修改调用程序中的值,但被调用程序仅仅修改了临时变量的值,而为达到目的,为此,C++设计者们,为了避免这类错误,对于非const类型的引用变量,不允许实参为表达式或是实参数据类型与形参不一致,生成临时变量。因而,const 引用类型做形参,其对应的实参表达范围更广泛。
何时产生临时变量?
实参类型正确,但不是左值;(什么是左值?指针、变量、数组元素、引用等常规变量和const变量)
实参类型不正确,但可以转为正确的类型。
对于非const修饰的形参,实参种类得到了限制
用const修饰形参,以防止无意中修改实参的值,编译器给出错误提示,但此时可以生成临时变量,实参表达的范围更广了。
引用类型的结构变量作为函数参数、返回值:
提出三个问题:为何返回引用(省去拷贝结果值,效率高)、返回值是引用类型时函数内部需要注意的问题、何时需要在const引用的返回值!!!
引用很适合用于结构、类变量,接下来我们将探讨结构引用作为函数参数和函数返回值。
对于不修改内容的结构,声明为const类型的引用更为安全,否则相反。无论结构引用做函数参数或是函数返回值,都需要想好是否用来cosnt修饰结构。
问题2的解答:
对于返回值为结构引用,要注意一个问题,引用的结构不能随着函数终止而被释放,因而不能返回临时结构(在函数内部声明定义的结构),通常的做法:返回函数参数(结构引用,此时所指向的内存是在调用程序中分配的);或者是返回new分配内存的变量的引用。此外,返回值为引用类型,函数名相当于是所饮用对象的一个别名。
问题3的解答:
比较,传统返回机制和返回引用类型的差别:传统返回机制将返回值复制到指定的内存或是寄存器,返回类型属于右值,不能出现在赋值表达式的左侧;返回引用值返回的是一个变量,可以出现在赋值表达式的左侧,如果不希望返回类型是引用的函数调用出现在赋值表达式的左侧,则将返回类型声明为const的引用,若犯错编译器将提示。
//参数、返回值是结构引用
#include "stdafx.h"
#include<iostream>
#include<string>
using namespace std;
struct mark
{
string name;
int attempt;
int made;
double percent;
};
void set(mark &);
void show(const mark &);
mark & add(mark &, const mark&);
int main()
{
mark one = { "li",5,3 };
mark two = { "xin",7,6 };
set(one);
show(one);
add(one, two);
show(one);
add(one, two) = two;//最终的执行结果就是就是将two内容赋值给one
show(one);
cin.get();
cin.get();
return 0;
}
void set(mark & x)
{
x.percent = 100 * x.attempt / x.made;
}
void show(const mark &x)
{
//cout << x.name << endl;
cout << x.name << endl;
cout << x.percent << endl;
}
mark & add(mark &x, const mark&y)
{
x.made += y.made;
x.attempt += y.attempt;
x.percent = 100 * x.attempt / x.made;
return x;
}
类对象引用作为函数参数:
使用方法与结构引用类似。
所需注意的细节:
注意返回值为引用类型时,函数内部的定义 ,不能返回临时变量;返回值或是参数是否应该用cosnt修饰,能用const就用cosnt,考虑清楚传统返回机制和引用返回机制。
//string类对象引用作为函数参数、返回值
#include "stdafx.h"
#include<iostream>
#include<string>
using namespace std;
string add1(const string&, const string &);
string & add2(string &, const string &);
int main()
{
string s1;
cin >> s1;
cout << add1(s1, "****") << endl; //在函数形参中明确制定了string对象,为什么c风格的字符串也可以用呢?string类封装了对c风格字符串的类型转化;此外用临时变量机制来存储转化后的值,再将形参指向临时变量。
cout << add2(s1, "####") << endl;
cout << s1 << endl;
cin.get();
cin.get();
return 0;
}
string add1(const string& s1, const string & s2)
{
string temp;
temp = s1 + s2 + s1;
return temp;
}
string & add2(string & s1, const string & s2)
{
s1 = s1 + s2 + s1;
return s1;
}
对象、继承与引用:
函数参数是基类对象的引用时,实参可以使基类对象也可以是派生类对象,无需进行强制类型转化。
目前为止,我们已经学习了值传递、指针传递、引用传递。那么如何选择最好的参数传递方式呢?
不修改调用程序数据值:
基本类型变量:值传递
数组:指针传递,且用const修饰
结构变量、类对象:引用传递
修改调用程序数据值:
基本类型变量:引用传递、指针传递
数组变量:指针传递
结构变量、类对象:引用传递
类对象做函数参数,确定使用引用传参,因为引用传参就是因为类的存在而被设计出来的。数组作为函数的参数,只能使用指针传递,因为数组名就是指针。
默认参数:
增加函数调用的灵活性,有实参时,实参代替默认值,无实参时,使用默认值。
在哪里设定默认值呢?
在函数原型中设定默认值,而函数的定义与没有默认参数时一致。
函数有多个参数时要为其赋默认值,从右向左开始,右边没有默认值,其左侧也不能有默认值,为某个参数赋默认值,必须其右侧所有的参数都得有默认值。
//抽取部分字符串,理解默认参数的用法
#include "stdafx.h"
#include<iostream>
#include<cstring>
using namespace std;
char * extract(const char * p, int n = 1);
const int SIZE = 10;
int main()
{
char input[SIZE];
cin.get(input, SIZE);
cout << extract(input) << endl;
int n;
cin >> n;
cout << extract(input, n) << endl;
cin.get();
cin.get();
return 0;
}
char * extract(const char * p, int n )
{
int len=strlen(p);
len = len < n ? len : n;
char * string1 = new char[len + 1];
int i;
for ( i = 0; i < len; i++)
{
string1[i] = p[i];
}
string1[i] = '\0';
return string1;
}
函数重载:
参数列表(参数类型和数目)不同的多个同名函数,编译器在处理函数重载时,会根据参数类型和数目重新修饰函数名。
调用重载函数时,实参的类型与形参不匹配,将尝试强制类型转化,但转化的结果若与多个重载函数相匹配,编译器会报错。
变量本身与引用变量是同一个东西,因为如下图所示,编译器根本不知道该调用哪个函数:
何时使用函数重载?
相同的任务不同的数据形式。
对于类型相同的参数(个数可能不同),可以使用默认参数来舍去多余的重载函数。
有一个问题:用const修饰指针或是引用,分别有两个函数,唯一不同的地方是参数一个有const,一个没有const,那么实参为非const类型变量时,该调用哪个函数呢?尚未解决!!!
结合上个例子,提取字符串中的前n个字符,此时改为处理int型数据:
//抽取部分字符串,理解默认参数的用法
//实现函数重载
#include "stdafx.h"
#include<iostream>
#include<cstring>
using namespace std;
char * extract(const char * p, int n = 1);
int extract(int, int);
const int SIZE = 10;
int main()
{
char input[SIZE];
cin.get(input, SIZE);
cout << extract(input) << endl;
int n;
cin >> n;
cout << extract(input, n) << endl;
int num;
cin >> num;
cout << extract(num, 2) << endl;
cin.get();
cin.get();
return 0;
}
char * extract(const char * p, int n )
{
int len=strlen(p);
len = len < n ? len : n;
char * string1 = new char[len + 1];
int i;
for ( i = 0; i < len; i++)
{
string1[i] = p[i];
}
string1[i] = '\0';
return string1;
}
int extract(int x, int n)
{
if (n == 0 || x == 0)
return 0;
int count = 1;
int z = x;
while (z/= 10)
count++; //求出数据位数 低级错误 使用形参求位数时 使得形参为0 之后又使用了这个形参 问题出在了赋值运算上
if (count < n)
return x;
else
{
int y=count - n;
while (y--)
{
x /= 10;
}
return x;
}
}
函数模板
省去编程的复杂性,相同类型的参数可用同一个模板函数来生成函数定义。
用泛型来描述函数功能,在调用的时候用具体类型来替换泛型。具体类型以函数参数方式传递给模板再由编译器生成该类型的函数,类型由参数表示因此模板特性也称为参数化类型。模板并不创建函数只是告知编译器如何定义函数,编译器会根据模板为具体的类型生成函数,因而,最终编译后的代码是不包含模板函数的,也不会缩减代码量,但会增加程序的可靠性。常见的情况,模板写在头文件汇总,需要时我们包含相应的头文件后,便可直接调用(传递类型参数)。
基本语法:
template <typename或是class 随便取个抽象类型名称>
函数的定义
与普通的函数一样,可以将模板函数原型声明放在程序的头部,定义放在main之后;也可以在头部直接定义函数,省去原型声明。
模板重载:
函数名相同,特征标不同。模板函数参数列表中的参数不一定都得是抽象类型参数,可以为具体类型。
显示具体化(具体化模板函数):
通用模板函数内部定义可能无法适用于特别的数据类型(例如,结构、数组无法支持+),为某个类型提供独立定义,同时存在函数模板和具体化模板时且调用函数均匹配这两个原型时,有具体类型的模板函数得到优先调用。
定义:
template<>
函数原型声明
或是:
相同函数名,可以有非模板函数、模板函数、具体化模板函数;具体化模板函数优先级高于模板函数、非模板函数优先级高于具体化模板函数。
实例化与具体化:
调用函数传入实参的过程可视为隐式模板实例化过程;下图为显示模板实例化,与隐式不同,没有传入实际的实参,以template开头同时传入参数类型,并以<类型 >表示,编译器遇到显示实例化语句,则生成相应类型的函数代码,后续可直接调用该类型的函数。
显示实例化也可通过调用函数的另一种写法来实现:
显示实例化与具体化写法上的差别:
显示实例化:template 函数返回类型 函数名<类型>(参数类型1,参数类型2…)
具体化:template<> 函数原型声明
编译器选择哪一个函数定义呢?
对于重载函数、函数模板、重载函数模板,编译器需要为调用函数选择一个最佳的函数定义。
1.确定函数列表:与被调用函数同名的函数和模板函数
2.根据参数类型和个数,确定可行的候选函数:通过类型转化,实参能与形参相匹配上
3.确定最佳的函数定义:根据调用函数参数与候选函数间参数转化来选择最佳的函数定义
完全匹配有多个时,如何确定最佳匹配:
存在非模板函数,优先使用非模板函数
仅有模板函数,优先使用更具体的模板函数(实质:都是模板函数,看哪个参数所需的转化最少)
实参为非const指针和引用,优先使用形参为非cosnt引用和指针(但这也是仅仅对对象和引用才有用的!)
不想要编译器为我们选择函数定义,我们希望通过一些明确的提示引导编译器选择我们希望的函数定义:
也许最佳的是非模板函数定义,但我们就是希望使用模板函数定义,则可以在调用时,函数名后加<>来提示编译器选择模板函数。
//选择合适的函数定义
#include "stdafx.h"
#include<iostream>
using namespace std;
template <typename T>
void showarray(T *a, int size);//指向T类型的指针
template<typename T>
void showarray(T *a[], int size);//指针数组
const int SIZE = 5;
int main()
{
int a[SIZE] = { 2,3,4 };
showarray(a, 3);
int * b[SIZE] = { &a[0],&a[1],&a[2] };
showarray(b, 3);//两个原型都能匹配到,都是数组,但第二个函数定义的参数与实参都接近,因此选择了第二个函数定义
cin.get();
return 0;
}
template <typename T>
void showarray(T *a, int size)//指向T类型的指针
{
int i;
for (i = 0; i < size; i++)
cout << a[i] << " ";
cout << endl;
}
template<typename T>
void showarray(T *a[], int size)//指针数组
{
for (int i = 0; i < size; i++)
cout << *a[i]<<endl;
cout << endl;
}
c++11新增的特性:
模板函数定义中总会遇到不知道类型的变量,为此引入decltype语法:decltype()相当于类型。
编译器如何理解decltype?
1.若expression是不加括号的标识符,则var类型域expression相同
2.若expression是函数调用,则var类型与函数返回值相同
3.若expression是加括号的标识符,则var类型是指向expression类型的引用
4.expression不在上述条件内,则var类型与expression类型相同
编写程序时如果出现多次使用decltype(expression),为了程序编写的简单性,不妨利用typedef语法:将decltype()定义为一个新的类型,typedef decltype(expression)new_typename
上面我们解决了在模板函数内部定义时可能会遇到变量类型不确定的情况,还有一种情况是:如果函数的返回值类型是不确定的呢?
由于返回类型出现在函数参数之前 ,因而无法在函数返回值的位置直接用decltype(expression),expression要求是已知变量,为了解决这个问题,c++11提出了后置返回类型,语法如下,通过引入auto,将实际的返回类型写在参数列表之后: