C++内联函数
内联函数是C++提高程序运行速度所做的一项改进。
代码编译之后会产生机器指令,每条指令都有特定的内存地址。当遇到函数调用时,将会发生地址的跳转,在函数调用完之后又会跳转回来。(过程可参照汇编语言,在跳转之前先要进行状态的保存,程序指针的保存;返回时同样需要状态的恢复,程序指针的恢复)而这来回的跳转,以及信息的保存需要一定的开销。内联函数提供另一种选择:将函数的编译代码和与其它程序“内联”起来(编译器将使用相应的函数代码替换函数调用),使得内联函数调用时无需进行地址的跳到。但是其代价就是产生了很多函数代码副本,占用内存更多(空间换时间)。
内联函数最佳使用情况:函数执行时间很短的并且经常被调用的函数。(也就是一些短小的函数)
使用方式(至上满足以下一条):
- 在函数声明前加上关键字
inline
; - 在函数定义前加上关键字
inline
;
通常做法是:省略原型,将整个定义放在本应提供原型的地方。
在以下情况下inline不会生效:
- 函数过大;
- 函数递归;
内联函数和宏
通常情况下,宏不能按值传递,如果使用C语言的宏执行了类似函数的功能,通常应将其转换为C++的内联函数。#define SQUARE(X) ((X)*(X))
inline double square(double x){ return x*x; }
引用变量
引用在之前介绍过,它其实就是为一个已知变量起一个别名。(和指针有点类似)其主要用途是作为函数的形参。
上面的int rats;
int number = 10;
int& rodents = rats;
rodents = number;
rodents
和rats
都指向相同的值和内存单元。
引用必须在声明时进行初始化。下面语句不被容许!(值得注意的是常量指针也是如此,此处的常量指针是指指针值不能修改的指针,不是指向const类型的指针)
逆向思考,如果容许引用先声明后赋值,那么岂不是这个引用就能被修改去作为其它变量的引用呢?这是不被容许的!同理,常量指针也是如此!
后面对引用变量的赋值,其实是值的传递。int rats;
int& rodents;
rodents = rats; //Invalid
引用作为参数
函数输出:#include<array>
#include<iostream>
using namespace std;
void swap(int&, int&);
int main(){
int a = 10;
int b = 20;
cout << "before swap:" << endl;
cout << "a is: " << a << endl;
cout << "b is: " << b << endl;
swap(a, b);
cout << "after swap:" << endl;
cout << "a is: " << a << endl;
cout << "b is: " << b << endl;
}
void swap(int& a, int& b){
int temp = a;
a = b;
b = temp;
}
before swap: a is: 10 b is: 20 after swap: a is: 20 b is: 10
注意事项:
利用引用进行参数传递时,传入函数的如果是表达式,则不是很合理,无法通过编译;
引用参数为const;
实参的类型正确,但不是左值;
int process_data(const int&);
......
int x = 10;
process_data(x+2);
- ·左值:可被引用的数据对象,例如变量、数组元素、结构成员、引用、指针等;
- 非左值:字面常量和包含多项的表达式;
实参的类型不正确,但可以转换为正确的类型;
- 当然,类型不正确,可以转为正确类型,实参不是左值也会创建临时变量;
int process_data(const int&);
......
long x = 10;
process_data(x);
为何有const限制
其实在早期的C++中是没有const限制的,但是这导致了一种问题:
分析上述代码,是希望将x,y进行交换,但是由于满足临时变量生成条件,在函数中会生成临时变量,这导致x,y并没有被交换,这和编程者的意图相悖,由于编译器没有报错,使得编程者很难发现这个问题!如果有了int swap(double&, double&);
......
int x = 10;
int y = 20;
swap(x, y);
const
限制,那么swap
函数本身就不能对x,y进行操作,在对x,y进行操作时会报错,这使得错误容易发现!在没有const
限制时,就不会创建临时变量,这就时才会编程者修改x,y(允许修改和不创建临时变量使得其满足编程者要求);
当然在不能创建临时变量的情况下,如果有类型不正确将会报错!
显然,多使用const可以避免一些易于出现的错误!
- 当然,类型不正确,可以转为正确类型,实参不是左值也会创建临时变量;
使用const可以避免无意中修改数据的编程错误;
- 使用const使得函数能够处理const和非const类型的数据,否则只能接受非const数据;(和const指针类似);
-
右值引用
使用&&声明:
const int c = 100;
int&& cr = c * 10;
数组引用
int array[] = {1,2,3};
int (&array_r)[3] = array;
结构引用
返回引用
double& sum_data(const double* dp, int size){
double result=0;
for(int i=0; i<size;i++){
result += dp[i];
}
return result; // 局部变量会被销毁
};
以上代码不合理,返回的局部变量的引用,局部变量是会被销毁的!
double& sum_data(const double* dp, int size, double &sum){
sum=0;
for(int i=0; i<size;i++){
sum += dp[i];
}
return sum; // 既会返回sum也会修改传进来的sum值
}
神奇用法:
sum_data(array, 3, sum) = 10;
由于sum_data返回的是引用,是左值(通常函数返回时右值),引用是可以被修改!
为了防止这种情况导致的错误,如果函数返回的引用不需要修改,那么建议使用const
修饰!对象、继承和引用
使用引用参数的原因
编程者能够修改调用函数中的数据对象;
-
什么时候使用哪种传递
如果数据对象很小,则按值传递;
- 如果数据对象是数组,则使用指针(唯一选择),并考虑是否需要加上
const
; - 如数据对象是较大的结构,则使用指针或者引用,以提高速率,并考虑是否需要
const
修饰; - 类设计常常使用引用;
- 如果数据对象是内置结构则使用指针;
- 如果数据对象是结构,则使用引用或指针;
-
默认参数
函数默认值,就是在定义函数时就给部分参数进行赋初值:
int harpo(int n, int m=4, int j=5);
注意事项:
如果给某个参数赋了默认值,那么其右边的所有参数必须赋默认值!
函数重载
函数重载也即是函数多态,同一个函数名,由于参数的不同,就可以实现不同的操作!也就是可以存在多个同名的函数,即对函数名称进行了重载! ```cpp void print(const char* str, int width); // #1 void print(double d, int width); // #2 void print(long l, int width); // #3
print(“hello”, 5); // #1 print(12.3, 10); // #2
如果调用`print((int)10, 10)`,则显然没有与之匹配的原型。如果#2是print的唯一匹配原型,虽然前面的调用没有匹配原型,但是C++会尝试进行标准类型转换进行强制匹配(就是尝试类型转换,使得与之匹配)。但是由于上面的print有多个以数组为参数的函数,所以有多种转换方法,此时会报错!<br />假设有如下四个原型:
```cpp
void dribble(char* bits);
void dribble(const char* bits);
void dabble(char* bits);
void drivel(const char* bits);
dribble
有两个原型:当输入为const的时候与后者匹配,非const时与前者匹配;dabble
只有一个原型,只能和非const得输入匹配;(const得特性,前面已提过)drivel
只有一个原型,由于形式参数const修饰,所以既可以和const参数匹配,也可以非const;
注意事项:
重载得两个函数,名称必须相同,返回类型可以不同,特征标必须不同(传入参数类型);
函数模板
函数模板是通用的函数描述,他们使用泛型来定义函数,其中的泛型可用具体的类型替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型的方式进行编程,所以也被成为通用编程。模板特性:参数化类型(以类型作为参数)。
为何需要模板?设想编写了int型的swap函数,如果数据类型是double,是char又当如何呢?当然,编译器自己转换类型可以在一定程度上解决这个问题(非常局限),但是对象像指针、引用这些在很多情况下不会进行类型转换,会直接报错。这时,模板将参数作为参数就可以解决此类问题!— 节省时间,安全可靠!模板编写
template <typename AnyType> // or class AnyType -- 没有逗号,没有分号
void Swap(AnyType& a, AnyType& b){
AnyType temp = a;
a = b;
b = a;
}
语句:
template <typename AnyType>
声明要建立一个模板,类型名为AnyType,类型名可以自己设置,其中的typename
可以用class
替代(两者等价)。模板本身不会创建函数,只是告诉编译器如何定义函数。
要让编译器知道程序需要一个特定形式的交换函数,只需在程序中使用Swap
函数即可,编译器将检查所使用的参数类型,并生成相应函数(类型事实上并没有显示地在调用函数时进行传递,编译器自己检查!)#include<iostream>
using namespace std;
template<typename T>
void swap1(T& a, T& b);
int main(){
int a = 10;
int b = 20;
double c = 1.5;
double d = 2.7;
cout << "before swap1: " << endl;
cout << "a: " << a << " b: " << b << " c: " << c << " d: " << d << endl;
swap1(a, b);
swap1(c, d);
cout << "a: " << a << " b: " << b << " c: " << c << " d: " << d << endl;
cout << "a: " << a << " b: " << b << endl;
}
template<typename T>
void swap1(T& a, T& b){
T temp = a;
a = b;
b = temp;
}
注意事项:
template<typename T>
在原型和函数定义之前都应该出现!重载的模板
需要对多个不同类型使用同一种算法时需要用到模板,但是并不是所有类型都是用同一种算法,重载模板很好地解决了这个问题。由于之前介绍过重载:特征标不同! ```cpp template
void Swap(T& a, T& b);
template
- 数组不能作为参数进行传递,数组之间不可以进行赋值;
```cpp
int a[3] = {1,2,3};
int b[3] = a; // Invalid
结构,类等可以相互之间进行赋值;
struct person{
int height;
int width;
int age;
}
person a = {1,2,3};
person b = a; // Valid
模板的局限性
其实模板的局限性在于:我们定义模板,首先会假设可执行那些操作,比如赋值操作等,但是对于特定的类型可能存在某些操作不能用的情况,从而使得当以某些类型作为参数传入模板函数时会报错!(模板适用性被压缩)其中的一个解决方法就是为特定的类型提供具体化的模板定义。
可能你会认为模板的重载能够解决这个问题,但是很多情况下其特征标是相同的,无法通过重载解决!显式具体化模板
对于给定函数名,可以有非模板函数、模板函数和显式具体化模板函数,以及它们的重载版本;
- 显式具体化的原型应以
template<>
打头,并通过名称来指出类型; 具体化优先于常规模板,而非模板优先于模板函数;
// job是一个结构体
// 非模板
void Swap(job& a, job& b);
// 常规模板
template <class T>
void Swap(T& a, T& b);
// 显示化模板
template <> void Swap<job>(job& a, job& b); // 也可以:template <> void Swap(job& a, job& b);
#include<iostream>
#include<new>
const int BUF = 512;
const int N = 5;
char buffer[BUF];
template <class T>
void display(T data);
template <> void display<int>(int data);
using namespace std;
int main(){
using namespace std;
display(100); // 会选择int类型的显示具体化模板
}
template <class T>
void display(T data){
cout << data << endl;
}
template <> void display<int>(int data){
cout << "this is int!" << endl;
}
那么如果要写成显示化模板,为何不直接用非模板呢?(貌似两者基本上没有不同)
实例化和具体化
代码中包含函数模板本身不会生成函数,它只是用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板的实例。比如像之前利用模板定义的函数进行运算,我们在调用函数时并没有指定模板接受参数的类型,编译器为我们自动实现了此操作,这即是隐式实例化。除了隐式实例化,还可以进行显示的实例化,这意味着可以命令编译器创建特定的实例。
template<typename T>
void swap1(T& a, T& b);
// 显示实例化函数
template void swap1<int>(int, int);
#include<iostream>
using namespace std;
template<typename T>
void swap1(T& a, T& b); // 注意这两行合成一句话!
// 指向模板的指针,指定了类型
void (*pf)(int& a, int& b) = swap1;
int main(){
int a = 10;
int b = 20;
double c = 1.5;
double d = 2.7;
cout << "before swap1: " << endl;
cout << "a: " << a << " b: " << b << " c: " << c << " d: " << d << endl;
(*pf)(a, b);
swap1(c, d);
cout << "after swap1: " << endl;
cout << "a: " << a << " b: " << b << " c: " << c << " d: " << d << endl;
}
template<typename T>
void swap1(T& a, T& b){
T temp = a;
a = b;
b = temp;
}
请注意显式实例化和显式具体化的差别!利用实例化函数进行运算:
// 隐式实例化
swap1(a,b);
// 显式实例化
swap1<int>(a,b);
swap1<int>(a,b);
即是一个实例化,利用整型实例化模板;swap1(a,b);
也是一个实例化,但是其为隐式的;编译器函数选择
对于函数重载、函数模板和函数模板重载,C++有一个选择策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时,这一过程称为重载解析。其大致步骤如下:
- 创建候选函数列表;
- 使用候选函数列表创建可行函数列表;这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。
- 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。
假设有如下的一个调用:may('B');
那么编译器首先寻找名称为may
的函数或者模板,找到之后进行参数数量的匹配,过滤掉需要必须要传入的参数与调用不同的函数或者模板,然后按照如下顺序进行参数类型筛选,选择最佳匹配:
- 完全匹配,但常规函数优先于模板;
- 提升转换(例如
char
和short
自动转换为int
,float
自动转换为double
); - 标准转换(例如
int
转为char
,long
转为double
); - 用户定义转换,如声明中定义的转换;
1. 完全匹配和最佳匹配
进行完全匹配时,C++允许某些“无关紧要的转换”(也即是说,参数类型并不是完完全全一样)。这类转换如下表所示,可以看出来这类转换和之前定义函数完成的自动转换一样,给我们的感觉就是的确可以这样(处理倒数第二列?)。
从实参 | 到形参 |
---|---|
Type | Type& |
Type& | Type |
Type[] | Type* |
Type(argument list) | Type(*)(argument list) |
Type | const Type |
Type | volatile Type |
Type* | const Type |
Type* | volatile Type* |
例如如下代码:
struct blot{int a; char b[10];}
void recycle(blot); // #1
void recycle(const blot); // #2
void recycle(blot&); // #3
void recycle(const blot&); // #4
// ...
blot ink = {25, "sports"};
// ...
recycle(ink)
recycle(ink)
存在多个完全匹配,前面的函数原型和它都是完全匹配,当存在多个完全匹配时,如果没有最佳匹配将会报错。
完全匹配之间的优先级:
- 指向非const数据的指针和引用优先与非const指针和引用参数匹配;(const只是在指针和引用时存在这种特性)
- 非模板函数优先于模板函数;
- 完全匹配的都是模板函数,则找最具体的模板作为最佳匹配;(最具体:指的是编译器推断出使用哪种类型时执行的转换最少)
基于以上特征:如果只存在#1和#2的定义,那么将会出现二义性;如果只定义了#3和#4,那么将会选择#3;
template <class Type> void recycle(Type t); // #1
template <class Type> void recycle(Type* t); // #1
struct blot{int a; char b[10];}
//...
blot ink = {25, "sports"};
//...
recycle(&ink)
显然上面的两个模板都和调用完全匹配,然而#1中的Type将会是:blot*
,儿#2中的Type将会是blot
,显然#2提供的信息更为具体。找出最具体模板的规则称为函数模板的部分排序规则。
2、关键字decltype和auto
decltype介绍
template <class T1, class T2>
void ft(T1 x, T2 y){
// ...
decltype(x + y) xpy = x + y;
// ...
}
由于此时x和y的类型未知,所以很难确切地写出x + y
的类型,decltype
关键字解决了这个问题。为了确定类型编译器必须遍历一个核对表,假设有如下申明:decltype(expression) var;
那么核对表的简化版如下:
第一步:如果expression是一个没有用括号括起的标识符,则var与标识符的类型相同,包括const等限定词;
double x = 20.0;
double* xp = &x;
decltype(xp) var; // 则var是double*类型
第二步:如果expression是一个函数调用,那么var类型和函数返回值类型相同;
double indeed(int);
decltype (indeed(3)) var; // var是double类型
注意:这个过程并不会实际调用函数。编译器通过查看函数的原型来获取返回类型,而无需实际调用函数。(这也是原型的好处之一罗)
第三步:如果expression是一个左值,则var为指向其的引用。为了与第一步区分开来,此时这个左值需要用括号进行区分;
double xx = 4.4;
decltype((xx)) r2 = xx; // r2是double &类型
decltype(xx) w = xx; // w是double类型
事实上括号并不会改变左值的值和左值性。
第四步:如果前面的条件都不满足,则var的类型与expression的类型相同;(表达式计算之后的类型)
int j = 3;
int& k = j;
decltype(j+6) var1; // var1是int型
decltype(k+7) var2; // var2是int型
auto使用
template <class T1, class T2>
auto ft(T1 x, T2 y) -> decltype(x + y)
{
// ...
return x + y;
}
不能将auto替换为decltype(x + y),因为该处的x和y还没有被声明;
- 这将返回类型移到了参数声明后面,
->double
被称为后置返回类型,其中的auto是占位符,表示后置返回类型提供的类型; - 由于
->double
在参数声明之后,所以可以使用他们;
事实上很多规则和性质,自己想一想就能明白其中的价值及原因。