这章再深入的讲些函数相关的。
1.C++内联函数
编译器对内联的函数处理就是将相应的函数的代码直接替换掉对应函数的调用。对于内联代码,程序无需跳到另一个位置处执行代码,再调回来。因此,内联函数的运行速度比常规函数调用稍快,但是更占用内存。如果程序在10个不同的位置调用同一个函数,则该程序将包含该函数代码的10个备份。
应有选择的地使用内联函数。如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间只占整个过程的很小一部分。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。另一方面,由于这个过程相当快,因此尽管节省了该过程的大部分时间,但节省的时间的绝对值并不大,除非该函数经常被调用。
要使用这项特别,必须采取如下措施之一:
- 在函数声明前加上关键字inline;
- 在函数定义前加上关键字inline。
通常的做法是省略原型,将整个定义放在本该提供原型的地方。
当函数过大或者函数有递归自己的时候,不应该将此函数声明为内联函数。
下面简单举个栗子:
inline int square(int a) {
return a * a;
}
int main() {
int aa = square(5);
cout << "aa:" << aa << endl;
return 0;
}
inline工具是C++新增的特性。C语言使用预处理语句#define来提供宏——内联代码的原始实现。例如:
#define SQUARE(X) X*X
这并不是通过传递参数实现的,而是通过文本替换来实现的。
a = SQUARE(5);// 实际是 a = 5*5
b = SQUARE(1+5);// 实际是 b = 1+5 * 1+5
c = SQUARE(a++);//实际是 a++ * a++
上面只有第一个能正常工作,即使通过改进宏定义
#define SQUARE(X) ((X)*(X))
对应第三个仍然是错误的。所以在c++中可以考虑使用将以前用宏定义的函数改为内联函数实现。
2.引用变量
2.1 创建引用变量
引用是已定义的变量的别名。使用方法如下
int a = 10;
int &b = a;//b是a变量的别名
其中&不是地址运算符,而是类型标识符的一部分。比如char*表示char类型的指针,int&表示int类型的引用。上述声明表示a和b指向相同的值和内存单元。举个详细的栗子:
int main() {
int a = 10;
int &b = a;
//通过b修改变量
b = 20;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
//通过a修改变量
a = 30;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
//查看他俩的地址
cout << "a addr:" << &a << endl;
cout << "b addr:" << &b << endl;
return 0;
}
输出为
a:20
b:20
a:30
b:30
a addr:0x7fff59892938
b addr:0x7fff59892938
引用变量与指针的差别之一是,必须在声明引用时将其初始化,如
int a = 10;
int *b;
b = &a;//可以
int &c = a;//可以
int &b;//不可以
b = a
因此引用更接近const指针,必须在创建时初始化,并且一旦关联起来,就一直效忠于它。
2.2 将引用用作函数参数
引用经常被用作函数参数,这种传递参数的方法被称为按引用传递。按引用传递允许被调用的函数访问调用函数中的变量。这种方式和C的按指针传递类似。举个例子:
#include <iostream>
using namespace std;
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
void swap(int &a, int &b) {
int tmp = a;
a = b;
b = tmp;
}
int main() {
int a = 10;
int b = 5;
//按指针传递
swap(&a, &b);
cout << "a:" << a << ",b:" << b << endl;
a = 16;
b = 25;
//按引用传递
swap(a, b);
cout << "a:" << a << ",b:" << b << endl;
return 0;
}
2.3 引用的属性和特别之处
首先如果意图是不允许函数修改参数,还想使用引用,则可以用常量引用,例子:
double refcube(const int & n);//n是一个引用,引用的是一个整形常量
上面只是个例子,对于数据比较小的情况,应该使用按值传递的方式,只有在数据较大的情况再使用常量引用(既不能通过引用修改原数据,又可以避免大量的数据创建拷贝)。
以下是Primer原文的总结,但在实际实验结果和描述不一致,可能是现代编译器更智能了吧。
当函数参数为常量引用时,如果实参与引用参数类型不匹配,C++会生成临时变量。注意的是参数必须是声明为常量引用的时候,且类型不匹配才会创建临时变量,否则编译器报错。例子:
#include <iostream>
using namespace std;
int square(const int &n) {
return n * n;
}
int square2(int &n) {
return n * n;
}
int main() {
long a = 10L;
square(a);//不报错,因为会创建临时变量,临时变量是long型的10转换为int型5
square2(a);//报错,类型不匹配
return 0;
}
创建临时变量的情况有
实参的类型正确,但不是左值;-
实参的类型不正确,但是可以转换为正确的类型。
什么是左值呢,很多,所以就说什么不是左值吧,如101、“string”这种字面量就不是左值,或者 1+1 这种表达式也不是左值。
那为什么只有常量引用才可以创建临时变量?还是例子:
#include <iostream>
using namespace std;
void swap(int &a, int &b) {
int tmp = a;
a = b;
b = a;
}
int main() {
long a = 10L;
long b = 12L;
swap(a, b);
cout << "a:" << a << ",b:" << b << endl;
return 0;
}
实际打印结果:
a:12,b:10
==先说下原文的意思:如果允许创建临时变量则引用就失效了,临时比那两交换不代表原变量交换。但是实际结果是的确交换了,所以这块还得再研究下。我的测试环境是64位Mac OS,IDE是CLion。==
应该尽可能使用常量引用,原因如下:
- 使用const引用可以避免无意中修改数据的编程错误;
- 使用const引用使函数能够处理const和非const实参,否则将只能接受非const实参;
- 使用const引用使函数能够正确生成并使用临时变量。
2.4 将引用用于结构
没啥太多说的,返回引用时要注意一点,应避免函数终止时不再存在内存单元引用。
2.5 何时使用引用参数
使用引用参数的主要原因有两个:
- 程序员能够修改调用函数中的数据对象;
- 通过传递引用而不是整个数据对象,可以提高程序的运行速度。
对于使用传递的值而不作修改的函数:
- 如果数据对象很小,如内置数据类型或小型结构,则按值传递。
- 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针。
- 如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率。这样可以节省复制结构所需的时间和空间。
- 如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用,这是C++新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。
对于修改调用函数中数据的函数:
- 如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码(其中x是int),则很明显,该函数将修改X。
- 如果数据对象是数组,则只能使用指针。
- 如果数据对象是结构,则使用引用或指针。
- 如果数据对象是类对象,则使用引用。
3.默认参数
栗子:
#include <iostream>
using namespace std;
int processInt(int a, int b = 1) {
return a * b;
}
int main() {
cout << "result1:" << processInt(10) << endl;
cout << "result2:" << processInt(10, 3) << endl;
return 0;
}
输出结果为:
result1:10
result2:30
需要注意的是:==对于带参数列表的函数,必须从右向左添加默认值==。
4.函数重载
概念不说了。函数重载的挂件是函数的参数列表——也成为函数特征标。跟返回值没关系。注意下两个函数原型
double process(int a,int b);
double process(int &a,int &b);
这两个函数是冲突的。
函数匹配时,并不区分const和非const变量。
5.函数模板
就是泛型的使用,栗子:
template<typename _T>
void swap(_T &a, _T &b);
int main() {
using std::cout;
using std::endl;
int a = 10, b = 5;
float c = 11.1f, d = 12.2f;
swap(a, b);
cout << "a:" << a << ",b:" << b << endl;
swap(c, d);
cout << "c:" << c << ",d:" << d << endl;
return 0;
}
template<typename _T>
void swap(_T &a, _T &b) {
_T tmp = a;
a = b;
b = tmp;
}
输出
a:5,b:10
c:12.2,d:11.1
==注意,在mac clion上使用的时候,不要在执行swap的之前写类似using namespace std;这种代码,因为std空间中自带了个swap的函数,参数列表也是两个引用类型的模板。==
5.1 重载的模板
很简单,看栗子:
#include <iostream>
template<typename _T>
void swap(_T &a, _T &b);
template<typename T>
void swap(T *a, T *b);
int main() {
using std::cout;
using std::endl;
int a = 10, b = 5;
float c = 11.1f, d = 12.2f;
swap(a, b);
cout << "a:" << a << ",b:" << b << endl;
swap(&c, &d);
cout << "c:" << c << ",d:" << d << endl;
return 0;
}
//传递引用的swap
template<typename _T>
void swap(_T &a, _T &b) {
_T tmp = a;
a = b;
b = tmp;
}
//传递指针的swap
template<typename T>
void swap(T *a, T *b) {
T tmp = *a;
*a = *b;
*b = tmp;
}
输出
a:5,b:10
c:12.2,d:11.1
5.2 模板的局限性
还是看上面swap的栗子:
template<typename _T>
void swap(_T a, _T b);
{……}
如果传入函数的是两个数组,则在函数能不能执行a=b这种,也不能执行a>b,a*b等等。
5.3 显示具体化
这个话题比较有意思,从来没想过,举个栗子,假设有如下结构体
struct Person{
int age;
char name[40];
char id[18];
}
例外需要有个交换的函数swap,则swap和之前写的一样,会交换两个Person的所有属性,但是如果我们希望有个函数值交换id和name,不交换age,则同样需要声明一样的函数swap(T &a,T &b),就产生冲突了。这个时候可以提供一个具体化函数定义——称为显式具体化,其中包含所需的代码。
C++98有如下定义和规则:
- 对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及他们的重载版本。
- 显式具体化的原型和定义应该以template<>开头,并通过名称来指出类型。
- 具体化优先于常规模板,非模板函数优先于具体化和模板。
举个栗子,都是原型:
void swap(Person &,Person &);//非模板函数
//模板函数
template <Typename T>
void swap(T &,T &);
//具体化模板函数
template <> void swap<Person>(Person &,Person &);
//具体化也可以这么写,省略函数名后的Person,因为参数列表已经表明了
template <> void swap(Person &,Person &);