内联函数
内联函数是C++为了提高程序运行速度所做的一项改进。常规函数和内联函数之间的主要区别不在于编写方式,而在于C++编译器任何将他们组合到程序中。要了解内联函数与常规函数之间的区别,必须深入程序内部。
编译过程的最终产品是可执行程序——由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存地址。计算机随后将逐步执行这些指令。有时(条件语句,循环语句)将跳过这些指令,向前或向后跳到特定地址。
常规函数调用也使程序跳到另一个地址,并在函数结束时返回。执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈,跳到标记函数起点的内存单元,执行函数代码,然后跳回到地址被保存的指令处。来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。
内联函数提供了另一种更好的选择,内联函数的编译代码与其他程序代码“内联”起来了。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置执行代码,再跳回来。因此内联函数运行速度更快,但需要占用更多内存。
如果程序在10个不同的地方调用同一个内联函数,则该程序将包含该函数代码的10个副本。
要使用这项特性,必须采取下述措施之一:
(1)在函数声明前加上关键字 inline ;
(2)在函数定义前加上关键字 inline ;
通常的做法是省略原型,将整个定义放在本应提供原型的地方。
// inline.cpp -- using an inline function
#include<iostream>
// an inline function definition
inline double square(double x)
{
return x * x;
}
int main()
{
using namespace std;
double a,b;
double c = 13.0;
a = square(5.0);
b = square(4.5 + 7.5);
cout << a << " " << b << endl;
cout << square(c++) << endl;
return 0;
}
尽管程序没有提供独立的原型,但 C++ 原型特性仍在起作用。这是因为在函数首次使用前出现的整个函数定义充当了原型。这意味着可以给square()传递 int 或 long 值,将值传递给函数前,程序自动将这个值强制转换为double 类型。
内联和宏
inline 工具是 C++ 新增的特性。C语言使用预处理器语句 #define 来提供宏——内联代码的原始实现 。
例如:下面是一个计算平方的宏:
#define SQUARE( X ) X * X
注意 : 这并不是通过传递参数实现的,而是通过文本替换实现的—— X 是 " 参数 " 的符号标记。
但宏不能按值传递。
引用变量
引用变量是复合类型,引用是已定义的变量的别名。通过将引用变量用作参数,函数将使用原始数据,而不是其副本。
之前的 & 是用来指示变量地址的。现在还可以用其来声明引用。
例如:要将bro 作为 wuhu 变量的别名:
int wuhu;
int & bro = wuhu
// int & 指的是指向 int 的引用
若将bro 加 1 将同时影响这两个变量,更准确的说,bro++操作将一个有两个名称的变量加 1 .
所以 现在使用 & 是 指向 某变量 的引用或者是指针呢?
例如:创建yep的引用和指针:
int yep = 1;
int & yyds = yep; // 引用
int * YYDS = &yep; // 指针
必须在声明引用变量时进行初始化,而不能像指针那样,先声明再赋值。虽然可以通过初始化声明来设置引用,但不能通过赋值来设置。
引用更接近 const 指针:
int & bro = wuhu;
// 这实际上是下述代码的伪装表示:
int * const pr = &wuhu;
将引用作为函数参数
引用常被用作函数参数,使得函数中的变量名成为调用程序中的变量的别名,这种传递参数的方法称为按引用传递。按引用传递允许被调用的函数能够访问调用函数中的变量。
现在通过一个常见的问题——交换两个变量的值,来比较一下引用和指针;再加上一种不可行的方法。
示例:
#include<iostream>
void swapr(int & a,int & b) // 使用引用
{
int temp;
temp = a;
a = b;
b = temp;
}
void swapp(int * px,int * py) // 使用指针
{
int temp;
temp = *px;
*px = *py;
*py= temp;
}
void swapt(int a,int b) // 不可行的方法
{
int temp;
temp = a;
a = b;
b = temp;
}
int main()
{
using namespace std;
int m = 520;
int n = 1314;
cout << m << " " << n << endl;
cout << "使用引用后:" << endl;
swapr(m,n);
cout << m << " " << n << endl;
m = 520;
n = 1314;
cout << "使用指针后:" << endl;
swapp(&m,&n);
cout << m << " " << n << endl;
m = 520;
n = 1314;
cout << "使用第三种方法后:" << endl;
swapt(m,n);
cout << m << " " << n << endl;
return 0;
}
// 运行结果如下:
// 520 1314
// 使用引用后:
// 1314 520
// 使用指针后:
// 1314 520
// 使用第三种方法后:
// 520 1314
引用的属性和特别之处
#include<iostream>
double cube(double a);
double refcube(double &b);
int main()
{
using namespace std;
double x =3.0;
cout << cube(x) << endl;
cout << x << endl;
cout << refcube(x) << endl;
cout << x << endl;
return 0;
}
double cube(double a)
{
a *= a * a;
return a;
}
double refcube(double &b)
{
b *= b * b;
return b;
}
// 运行结果
// 27
// 3
// 27
// 27
refcube() 函数修改了 main() 中 x 的值,这就是按引用传递的特殊之处。如果我们的意图是让函数使用传递给它的信息,而不对这些信息进行修改,同时又想使用引用,则应使用常量引用,可以在函数原型或函数头使用const。
const double refcube(double &b);
// 这样做,编译器发现代码修改了 b的值时会生成错误信息
如果b是一个变量的别名,则实参应该是该变量:
double t = refcube(x + 1.0)
// 这是错的,因为表达式 x + 1.0 不是变量
临时变量 引用参数 和 const
C++的临时变量是编译器在需要的时候自动生成的临时性变量,它们并不在代码中出现.但是它们在编译器生成的二进制编码中是存在的,
如果实参与引用参数不匹配, C++ 将生成临时变量。首先,什么时候将创建临时变量呢? 如果引用参数是 const ,则如下两种情况将生成临时变量 :
(1)实参的类型正确,但不是左值 ;
(2)实参类型不正确,但可以转换为正确的类型。
应尽可能的使用 const:
将引用参数声明为常量数据的引用理由如下:
(1)使用 const 可以避免无意中修改数据的编程错误;
(2)使用 const 使函数能够处理 const 和 非 const 实参,否则将只能接受 非 const 的数据;
(3)使用 const 引用使函数能够正确生成并使用临时变量。
现在的引用都是左值引用,后面的章节会介绍右值引用·。
引用用于结构
引用很适合用于结构和类。
假设有如下结构定义:
struct free_throws
{
std::string name;
int made;
int attempts;
float percent;
};
则可以这样编写函数原型,在函数中将指向该结构的引用作为参数:
void set_pc(free_throw & ft);
如果不希望函数修改传入的结构,可使用const:
void set_pc(const free_throw & ft);
为何要返回引用
传统返回机制与按值传递函数参数相似:计算return 后面的表达式,并将结果返回给调用函数
从理论上来说,这个值被复制到一个临时的位置,而调用程序将使用这个值。
看下面的语句:
dup = accumulate(team,five);
如果accumulate()返回一个结构,而不是指向结构的引用,将把整个结构复制到一个临时位置,再将这个拷贝复制给dup;如果返回值为引用时,将直接把team 复制到 dup,效率更高。
注意:返回引用的函数实际上是被引用的变量的别名。
返回引用时需要注意的问题
返回引用时最重要的一点是,应避免返回函数终止时不再存在的内存单元引用;避免返回指向临时变量的指针。
为了避免这种问题,最简单的方法是,返回一个作为参数传递给函数的引用·。作为参数的引用将指向调用函数使用的数据,因此返回的引用也将指向这些数据。
另一种方法是用 new 来分配新的存储空间。下面是使用引用来完成类似工作的方法:
const free_throw & clone(free_throw & ft)
{
free_throw *pt;
*pt = ft;
return *pt;
}
第一条语句创建了一个无名的 free_throw 结构,并让指针 pt 指向该结构,因此 *pt 就是该结构。这样似乎会返回该结构,但实际上返回的是这个结构的引用。
引用用于类对象
将类对象传递给函数时,C++ 通常的做法是使用引用。例如,可以通过使用引用,让函数将类string,ostream,istream,ofstream,ifstream类的对象作为参数。
下面看一个示例:
#include<iostream>
#include<string>
using namespace std;
string versional(const string & s1,const string & s2)
{
string temp;
temp = s2 + s1 + s2;
return temp;
}
int main()
{
string input,result;
cout << "Enter a string: ";
getline(cin,input);
result = versional(input,"***");
cout << "Your string enhanced: " << result << endl;
return 0;
}
在这种情况下,s1 和 s2 为string对象。使用引用效率更高,因为函数不需要创建新的string对象,并将在原来对象中的数据复制到新对象中。const符指出,该函数将使用原来的string对象,但不会修改它。
temp是一个新的string对象,只在函数versional()中有效,该函数执行完毕后,它将不存在,因此返回指向temp的引用不可行,因此该函数的返回类型是string。这意味着temp的内容将被复制到一个临时存储单元中,然后在main()中,该存储单元的内容被复制到一个名为 result 的string中。
对象,继承和引用
ostream he ofstream 类凸显了引用的一个有趣属性。ofstream 对象可以使用ostream类的方法,这使得文件输入输出的格式与控制台的相同。使得能够将特性从一个类传递给另一个类的语言特性被称为继承。ostream是基类,ofstream是派生类。派生类继承了基类的方法。
继承的另一个特征是,基类引用可以指向派生类对象,而无需进行强制类型转换。这种特征的一个实际结果是,可以定义一个接受基类引用作为参数的函数,在调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。
何时使用引用参数
使用引用参数的原因:
(1)程序员能够修改调用函数中的数据对象;
(2)通过传递引用而不是整个数据对象,可以提供程序运行速度。
引用参数实际上是基于指针的代码的另一个接口。那什么时候使用引用,什么时候使用指针,什么时候按值传递呢?
对于使用传递的值而不做修改的函数
(1)如果数据对象很小,如内置数据类型或小型结构,则按值传递 .
(2)如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针.
(3)如果数据对象是较大的结构,则使用const 指针或const 引用,提高程序效率。这样可以节省复制结构所需要的时间和空间。
(4)如果数据对象是类对象,则使用const 引用。类设计的语义常常要求使用引用,这是C++新增这项新特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。
对于修改调用函数中数据的函数:
(1)如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码,则很明显,该函数将修改x。
(2)如果数据对象是数组,只能使用指针。
(3)如果数据对象是结构,使用引用和指针。
(4)如果数据对象是类对象,使用引用。
默认参数
默认参数指的是当函数调用中省略了实参时自动使用的一个值
直接看示例:
#include<iostream>
const int ArSize = 80;
char * left(const char * str,int n = 1);
int main()
{
using namespace std;
char sample[ArSize];
cout << "Enter a string:\n";
cin.get(sample,ArSize);
char *ps = left(sample,4);
cout << ps << endl;
delete [] ps;
ps = left(sample);
cout << ps << endl;
delete [] ps;
return 0;
}
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;
}
函数重载
函数多态是C++ 在 C 的基础上新增的功能。默认参数让您能够使用不同数目的参数调用同一个函数,而函数多态(函数重载)让你能够使用多个同名的函数。可以通过函数重载来设计一系列函数——他们完成相同的工作,但使用不同的参数列表。形象点说,重载函数就像是有多种含义的动词。
函数重载的关键是函数的参数列表。例如,可以定义一组原型如下的 print() 函数:
void print(const char * str,int width); // 1
void print(double d,int width); // 2
void print(long l,int width); // 3
void print(int i,int width); // 4
void print(const char *str); // 5
使用 print() 函数时,编译器将根据所采取的用法使用有相应特征标的原型:
print("Pancakes",15); // 1
print("Syrup"); // 5
print(1990.0,10); // 2
print(1099,12); // 4
print(199L,15); // 3
使用被重载的函数时,需要在函数调用中使用正确的参数类型。例如:
unsigned int year = 2022;
print(year,6);
它不与任何原型匹配,但C++ 尝试使用标准类型转换强制进行匹配。但是如果有多个将数字作为第一个参数的原型,这种情况下,C++ 拒绝这种函数调用。
一些看起来彼此不同的特征标是不能共存的。例如,编译器在检查特征标时,将把类型引用和类型本身视为同一个特征标。
另外,在匹配函数时,要区分const 和 非 const 变量。
请记住,是特征标,而不是函数类型使得可以对函数进行重载。例如,下面两个声明是互斥的:
long gronk (int n,float m);
double gronk (int n,float m);
返回类型可以不同,但特征标也必须不同:
long gronk (int n,float m);
double gronk (float n,int m);
重载引用参数
下面看一个示例:
#include<iostream>
unsigned long left(unsigned long num,unsigned ct);
char * left(const char * str,int n = 1);
int main()
{
using namespace std;
char * trip = "Hawaii!";
unsigned long n = 12345678;
int i;
char * temp;
for(i = 1 ; i < 10 ; i++)
{
cout << left(n, i) << endl;
temp = left(trip, i);
cout << temp << endl;
delete [] temp;
}
return 0;
}
unsigned long 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;
}
// 运行结果
// 1
// H
// 12
// Ha
// 123
// Haw
// 1234
// Hawa
// 12345
// Hawai
// 123456
// Hawaii
// 1234567
// Hawaii!
// 12345678
// Hawaii!
// 12345678
// Hawaii!
函数模板
函数模板是通用的函数描述,也就是说,他们使用泛型来定义函数,其中的泛型可用具体的类型(如 int 或 double)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。
函数模板允许以任意类型的方式来定义函数。例如,可以建立这样一个交换模板:
template <typename AnyType>
void swap(AnyType &a, AnyType &b)
{
AnyType temp;
temp = a;
a = b;
b = temp;
}
第一行指出,要建立一个模板,并将类型命名为AnyType. 关键字 template 和 typename 是必需的,除非可以用关键字 class 代替 typename 。
模板并不创建任何函数,只是告诉编译器如何定义函数。需要交换int 的函数时,编译器将按模板创建这样的函数,并用int 代替 double。
提示:如果需要将同一种算法用于不同类型的函数,请使用模板。
重载的模板
需要多个对不同类型使用同一算法的函数时,可使用模板。然鹅,并非所有的类型都使用相同的算法。为了满足这种需求,可以像重载常规函数定义那样重载模板定义。被重载的模板的函数特征标必须不同。
示例:
template <typename T> // original template
void swap(T &a,T &b);
template <typename T> // new template
void swap(T *a,T *b,int n);
模板的局限性
假设有如下模板函数:
template <typename T>
void f(T a,T b)
{
...
}
通常,代码假定可执行哪些操作。例如,下面的代码假定定义了赋值,但如果T 为数组,这种假设不成立 : a = b;
同样,当 T 为结构 ,该假设不成立: a > b;
解决方案是,为特定类型提供具体化的模板。
显示具体化
下面看一个示例来看看显示具体化的工作方式:
#include<iostream>
template <typename T>
void swap(T &a,T &b);
struct job
{
char name[40];
double salary;
int floor;
};
template <> void swap <job>(job &j1, job &j2);
void show(job &j);
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";
cout << "Using compiler-genrated int swapper:\n";
swap(i,j);
cout << "Now i, j = " << i << ", " << j << ".\n";
job sue = {"Susan",73000.60,7};
job sidney = {"Sidney Paul",78544.80,9};
cout << "Before job swapping:\n";
show(sue);
show(sidney);
swap(sue,sidney);
cout << "After job swapping:\n";
show(sue);
show(sidney);
return 0;
}
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 t1;
int t2;
t1 = j1.salary;
j1.salary = j2.salary;
j2.salary = t1;
t2 = j1.floor;
j1.floor = j2.floor;
j2.floor = t2;
}
void show(job &j)
{
using namespace std;
cout << j.name << ": $" << j.salary
<< " on floor" << j.floor << endl;
}