内联函数
常规函数调用时,程序跳到函数的起始地址,并在函数结束后返回。
对于内联函数,程序无需跳到另一个位置处执行,再跳回来,因为编译器会将内联函数代码替换为函数调用。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。
如何使用内联函数:
1. 在函数声明前加关键字inline。
2. 在函数定义前加关键字inline。
3. 编译器满足程序员的请求。因为即使在函数声明和定义前加了关键字 incline,编译器也并不一定满足这种要求。当函数太长或递归调用自己时,编译器不将其作为内联函数。
内联函数示例:
// an inline function definition
inline double square(double x){return x * x;}
内联函数与宏的区别
内联函数是按值来传递参数的,而宏是通过文本替换来实现的。
例如定义宏:
#define SQUARE(X) X*X
square(4.5 + 7.5) 等于 12*12
SQUARE(4.5 + 7.5) 等于 4.5 + 7.5 * 4.5 + 7.5
引用变量
创建引用变量
C++用符号 & 来声明引用变量。
int rats;
int & rodents = rats; // make rodents an alias for rats
此时 & 不是地址运算符,而是类型标示符的一部分。就像声明中的char *指的指向 char 的指针一样,int & 指的是指向 int 的引用。
注意 1:必须在声明引用时将其初始化,而不能像指针那样,先声明,再赋值。
int rat;
int & rodent;
rodent = rat; // Error, we can't do this;
注意 2:在创建时一旦与某个变量关联起来,就会一直与其关联,而不会再与其他变量关联。
int rats = 101; ①
int & rodents = rats; ②
int bunnies = 50; ③
rodents = bunnies; ④
第 ①② 两条语句将引用变量 rodents 与变量 rats 关联,第 ③④ 条语句试图将 rodents 与 bunnies 关联。此时 rodents 的值变成了 50,似乎成功了,但此时的 rats 的值也变成了50,即 ④ 条语句是将 50 赋值给了rats 和 rodents 变量的地址。
将引用用作函数参数
函数参数的传递有三种:按值传递、按引用传递、按指针传递。其中引用传递和指针传递都可以在被调用函数里改写调用函数里的变量。
按值传递,会产生两个名称、两个变量:
int times = 20;
sneezy(times);
void sneezy(int x){....}
其中的 times 和 x 是两个不同的变量。
按引用传递,会产生两个名称、一个变量:
int times = 20;
sneezy(times);
void sneezy(int &x){....}
其中的 times 和 x 虽是两个不同的名称,但是指向同一个地址。
将引用用于结构
引用主要用于结构体和类。使用结构体引用参数的方式和使用基本变量引用相同,只需要在声明结构体参数时使用引用运算符 & 即可。
定义如下结构体:
struct free_throws
{
std::string name;
int made;
int attempts;
float percent;
};
void set_pc(free_throws & ft); // use a reference to a structure
void display(const free_throws & ft); //don't allow change to structure
free_throws & accumulate(free_throws & target, const free_throws &source); // return a reference
返回结构体与返回结构体引用的区别:
dup = accumulate(team, five);
如果上面的 accumulate 返回一个结构,而不是指向结构的引用,将把整个结构体复制到一个临时位置,再将这个拷贝复制给 dup。 但在返回值为引用时,将直接把 team 复制给 dup,其效率更高。
注意 1:返回引用的函数实际上是被引用的变量的别名
注意 2:应避免返回函数终止时不再存在的内存单元引用。为避免该问题,可返回一个作为参数传递给函数的引用。
将引用用于类对象
将类对象传递给函数时,C++通常的做法是使用引用。示例:
string version1(const string &s1, const string &s2); // good design
const string &version2(string &s1, const string &s2); // has side effect
const string & version3(string &s1, const string &s2); // bad desing
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;
}
version 1 传递的两个 string 类均为 const 引用,返回 string,不会改变传递的 string 类,是个好的设计。version 2 虽然能够成功,但 string1 的引用不是 const 类,函数内部会改变其值,可能会有 side effect。version 3 返回引用,但内部的 temp 在返回时已经不存在了,其引用也就不存在了,因此该设计会导致运行 error 或编译 warnning。
注意:基类引用可以指向派生类对象,而无需进行强制类型转换。例如,参数类型为 ostream & 的函数可以接受 ostream 对象(如 cout ),也可接受 ofstream 对象作为参数,因为 ostream 是基类(因为 ofstream 是建立在它的基础上的),而 ofstream 是派生类的(因为它是从 ostream 派生而来的)。
引用小结:
使用引用参数的主要原因有两个:
- 使用引用参数可以修改调用函数中的数据对象。
- 传递引用而不是整个数据对象,可以提高程序的运行速度。
如何选择使用按值传递、按引用传递、按指针传递的原则:
对于使用传递的值而不作修改的函数:
- 如果数据对象很小,如内置数据类型或小型结构,则按值传递。
- 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向 const 的指针。
- 如果数据对象是较大的结构,则使用 const 指针或 const 引用,以提高程序的效率。
- 如果数据对象是类对象,则使用 const 引用。
对于修改调用函数中数据的函数:
- 如果数据对象是内置数据类型,则使用指针。
- 如果数据对象是数组,则使用指针。
- 如果数据对象是结构,则使用引用或指针。
- 如果数据对象是类对象,则使用引用。
默认参数
默认参数指的是当函数调用中省略了实参时自动使用的一个值。
注意 1:设置函数的默认参数,必须通过函数原型,而不是函数定义。由于编译器通过查看原型来了解函数使用的参数数目,因此函数原型也必须将可能的默认参数告知程序,方法时将值赋给原型中的参数。
char *left(const char *str, int n = 1);
注意 2:对于带参数列表的函数,必须从右向左添加默认值。即要为某个参数设置默认值,则必须为它右边的所有参数提供默认值。
int harpo(int n, int m = 4, int j = 5); //VALID
int chico(int n, int m = 6; int j); // INVALID
int groucho(int k = 1, int m = 2, int n = 3); //VALID
注意 3:实参按从左到右的顺序依次被赋值给相应的形参,而不能跳过任何参数。
beeps = harpo(3, , 8); //invalid, doesn't set m to 4
通过使用默认参数,可以减少要定义的析构函数、方法以及方法重载的数量。
函数重载
函数重载指的是可以有多个同名的函数;函数多态允许函数可以有多种形式。重载是多态的一种形式。示例:
void print(const char *str, int width);
void print(double d, int width);
void print(long l, int width);
注意:有些看似重载的函数时不能共存的。
double cube(double x);
double cube(double &x);
参数 x 与 double 和 double & 原型都匹配,因此编译器无法确定究竟应使用哪个原型。
函数模板
函数模板是通用的函数描述,它们使用泛型来定义函数,其中的泛型可用具体的类型替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。
函数模板示例:
//function template prototype
template <typename AnyType>
void Swap(AnyType &a, AnyType &b);
//function template definition
template<typename AnyType>
void Swap(AnyType &a , AnyType &b)
{
AnyType temp;
temp = a;
a = b;
b = temp;
}
函数模板中关键字 template 和 typename 是必需的,或者可以用关键字 class 代替 typename;另外,,必需使用尖括号。
注意:模板并不创建任何函数,而只是告诉编译器如何定义函数。因此,函数模板不能缩短可执行程序,最终的可执行代码不包含任何模板,而只包含了为程序生成的实际函数。更常见的情形是,将模板放到头文件里,并在需要使用模板的文件中包含头文件。
函数的具体化包括:隐式实例化、显示实例化和显示具体化。其共同点是都是使用具体类型的函数定义,而不是通用描述。
template <class T>
void Swap(T &a, T&); // template prototype
template <> void Swap<job>(job &, job &); // explicit specialization for job
int main(void)
{
template void Swap<char>(char &, char &); //explicit instantiation for char
short a, b;
...
Swap(a, b); // implicit template instantiation for short
job n, m;
...
Swap(n, m); // use explicit specialization for job
char g, h;
...
Swap(g, h); //use explicit template instantiation for char
}
编译器选择使用哪个函数版本?
对于函数重载、函数模板和函数模板重载,C++需要一个定义良好的策略,来决定为函数调用使用哪一个函数定义。
1. 创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
2. 使用候选函数列表创建可行函数列表。
3. 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。