①C++内联函数
1.介绍
对于常规函数的调用,程序会在函数调用后立马存储该指令的内存地址,并将函数参数复制到堆栈中,跳到标记函数起点的内存单元,执行函数代码,然后跳回到地址被保存的指令处,来回跳跃的并记录条约位置意味着需要一定的时间成本。
内联函数则是使用相应的函数代码替换函数调用,无需进行程序的跳转,因此,内联函数的运行速度比常规函数快,但代价是需要占用更多内存。所以应该有选择地使用内联函数。如果代码执行时间很短并且函数经常被调用,此时在使用内联函数。
2.内联函数的使用:
在函数声明前加上关键字 inline;在函数定义前加上关键字inline。
通常的做法是省略原型,将整个定义放在本应该提供原型的地方。
程序员请求内联函数时,编译器不一定会满足这种要求,它认为函数过大或者自己调用自己(内联函数不能递归)时,不将其作为内联函数。
#include<iostream>
inline double square(double x) { return x * x; } //内联函数
int main()
{
using namespace std;
double a, d;
double c = 13;
a = square(5.0);
d = square(3.0 + 2.0);
cout << "a = " << a << endl;
cout << "d = " << d << endl;
return 0;
}
内联函数与常规函数一样,同样是值传递来进行参数的传递。
3.内联函数与宏定义区别
宏定义则是通过文本替换,而不是值传递,如果使用C语言的宏执行了类似函数的功能,应考虑将他们转换为C++内联函数。
②引用变量
1.介绍
引用的已定义的变量的别名,引用变量的主要用途是作为函数的形参,通过将引用变量作为参数,函数将使用原始数据,而不是其副本。这样除指针之外,引用也为函数处理大型结构提供了一种非常方便的途径。
2.引用变量的创建
int rats;
int & rodents = rats; //引用变量的创建
int *prats = &rats;
&不是地址运算符,而是类型标识符的一部分,int & 指的是指向int的引用,即rats 和 rodents指向相同的值和内存单元,将rodents加1将影响两个变量。
rodents = rats = *prats; prats = &rats =&rodents;
引用变量必须在声明引用时将其初始化(同const类型),而不能像指针那样,先声明,再赋值。
int rats =101;
int & rodents = rats; //引用变量的创建
int *prats = &rats;
int bunnies = 50;
rodents = bunnies; //可以吗?
rodents = bunnies 本质上是 rats = bunnies;不管怎么样,rodents永远效忠于rats.
3.将引用用作函数参数
引用经常被用作函数参数,使得函数中的变量名称为调用层序中的变量的别名,这种传递参数的方法称为按引用传递。按引用传递的函数能够访问调用函数中的变量。换句话说,传递引用时,函数可以使用原始数据。
#include<iostream>
void swap(int& a, int& b); //引用传递
int main()
{
using namespace std;
int a = 1;
int b = 10;
swap(a, b);
cout << "a = " << a << endl
<< "b = " << b << endl;
return 0;
}
void swap(int& a, int& b)
{
int temp;
temp = a;
a = b;
b = temp;
}
在使用引用参数时,一定要记得参数信息被修改了,(值传递不会有这个问题)因此,如果程序员的意图是让函数使用传递给它的信息,而不对这些信息进行修改,同时又想使用引用,则应使用常量引用,在函数原型中函数头使用const;
void swap(const int& a,const int& b)
这样声明后,如果编译器发现修改了a和b的值,将生成错误信息。
此外,如果要编写使用基本数值类型作为参数的函数,应采用按值传递的方式,而不要采用按引用传递的方式。当数据量比较大(如结构体和类)时,引用参数会很有用。
按值传递的函数,可使用多种类型的实参(左值,非左值,表达式),而对于引用传递的函数,传递引用的限制很严格(因为引用是一个变量的别名,则实参应该是变量)。如果实参和引用参数不匹配,C++将生成临时变量(当且仅当参数为const引用时才会这样),或者生成错误信息。
当实参的类型正确,但不是左值(不是变量);实参的类型不正确,但是可以转换为正确的类型(是变量,但是数据类型不匹配)。
左值:可被引用的数据对象(变量,数组元素,结构成员,引用和解引用的指针等),非左值包括字面常量(用括号引起来的字符串除外,它们由其地址表示)和包含多项的表达式。
编译器生成一个临时匿名变量,并让引用参数指向它,这些临时变量只在函数调用期间存在,此后编译器便将其删除,但是临时变量的存在导致参数的修改修改的是匿名变量的值,而不是传递进去的变量值,所以只有const引用变量才会有创建临时匿名变量的情况,否则会生成错误信息。
将引用参数声明应该尽可能的使用const:
1.使用const可以避免无意修改数据的编程错误;
2.能够处理const和非const实参,否则只能接受非const数据;
3.能够使函数正确生成并使用临时变量。
C++11新增了右值引用,是使用&&声明的,第18章讨论如何使用右值引用来实现移动语义。对于&声明的引用称为左值引用。
4.将引用用于结构
1.介绍:
引用非常适合用于结构和类,使用结构引用参数的方式与使用基本变量的引用相同,只需要在生命结构参数前使用引用运算符即可。
void display(const free_throws& ft)
{
std::cout << "name:" << ft.name << std::endl
<< "Made:" << ft.made << std::endl
<< "Attempts:" << ft.attempts << std::endl
<< "Percent" << ft.percent << std::endl;
}
void set_pc(free_throws& ft)
{
if (ft.attempts != 0)
ft.percent = 100.0f * (float)(ft.made) / (ft.attempts);
else
ft.percent = 0;
}
free_throws& accumulate(free_throws& target, const free_throws& source)
{
target.attempts += source.attempts;
target.made += source.made;
set_pc(target);
return target; //返回指向结构体的引用
}
2.返回引用
accumulate(dup,five) = four(同类型结构体)
accumulate(dup,five);
dup = four;
这条语句将值付给函数调用,这是可行的,因为函数返回了一个引用。本质相当于底下的两条代码。
为什么要返回引用?
如果不返回引用,同样的返回值需要复制到一个临时位置,调用程序使用的是临时值,返回引用则直接把数据给调用程序使用,提高了效率。
返回引用需要注意的问题:
不能返回临时变量的引用(注意引用创建临时变量),所以引用的类型和左值必须都要对应,为了避免这种情况的发生,将作为参数的引用进行返回。另一种方法是用new来分配新的存储空间。
注意:是不能返回指向临时变量的引用,而不是不能返回临时变量。临时变量被return后才销毁,所以是可以的,但是指向临时变量的引用,本体都没有了,指向它的复制体怎么可以存在呢?
为什么要将const用于引用返回类型?
假设要引用返回值,但又不允许像上边赋值情况的出现,只需要将返回类型声明为const引用。
5.将引用用于类对象
string version1(const string & s1, const string & s2)
{
string temp;
temp = s2 + s1 + s2;
return temp; //对的,先返回,返回被调用程序保存了,后销毁
}
string& version2(const string & s1, const string & s2)
{
string temp;
temp = s2 + s1 + s2;
return temp; //错的,因为temp指向的内存被释放了,temp的存在没有了意义
}
C-风格字符串用作string对象引用参数:
如果形参类型是 const string &,再调用函数时,使用的实参可以为 string对象 或C风格字符串(字符串字面量、以\0结尾的char数组,指向char的指针变量)。
6.对象、继承和引用
继承:将特性从一个类传递给另一个类的语言特性被称为继承,简单地说,ostream是基类,ofstream是派生类,派生类继承了基类的方法。
继承的另一个特性是,基类引用可以指向派生类对象,而无需进行强制类型转换。现实意义是,可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。
7.何时使用引用参数
使用引用参数的主要原因有两个:
①程序员能够修改调用函数中的数据对象
②通过引用传递而不是值传递,可以提高程序运行速度(数据对象较大的时候,第二个原因比较重要)
指导原则:
①对于传递的值而不作修改的函数:
1.数据对象很小,如内置类型或小型结构: 按值传递;
2.数据对象是数组:唯一选择,指向const数组的指针;
3.数据对象是较大的结构: const指针或const引用;
4.类对象: const引用(传递类对象参数的标准方式是按引用传递)
②对于修改调用函数中的数据:
1.数据对象很小: 使用指针,因为很明显提醒这是传递地址,变量内容会修改;
2.数据对象是数组:唯一选择,指针;
3.数据对象是结构:指针或引用;
4,类对象:引用
③默认参数
默认参数指的是函数调用中省略了实参时自动使用的一个值。如果有实参输入,实参会覆盖掉默认值。必须通过函数原型来设置默认值,这是因为编译器通过查看原型来了解函数所使用的参数数目。
char* left(const char*str,int n = 1);
对于带参数的列表,必须从右向左添加默认值。也就是说,要为某个参数设置默认值,则必须为它右边的所有参数提供默认值。
实参从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数。
只有原型指定了默认值,函数定义与没有默认参数时完全相同。
#include<iostream>
char* left(const char* str, int n = 1);
int main()
{
const int SIZE = 80;
using namespace std;
char str[SIZE];
char* pt;
cout << "Enter a string:\n";
cin.get(str, SIZE);
pt = left(str, 4); //给定参数
cout << pt << endl;
pt = left(str); //默认参数
cout << pt << endl;
delete[] pt;
return 0;
}
char* left(const char* str, int n)
{
int len = strlen(str);
n = (len > n)?n: len; //防止浪费空间
char* pt = new char [n + 1];
int i = 0;
while (i < n && str[i])
{
pt[i] = str[i];
i++;
}
pt[i] = '\0';
return pt;
}
//结果
//Enter a string:
//forthcoming
//fort
//f
④函数重载
默认参数能够让我们使用不同数目的参数调用同一个函数,而函数多态(函数重载)能够使用多个同名函数。可以通过函数重载来设计一系列函数——他们完成相同的工作,但使用不同的参数列表,C++使用上下文来确定所要使用的重载函数版本。
函数重载的关键是函数的参数列表——也成为函数的特征标。如果两个函数参数数目、类型和参数的顺序相同,则他们的特征标相同。是特征标使得函数可以重载,而不是函数类型(函数返回类型)。
使用被重载的函数时,需要在函数调用中使用正确的参数类型。如果没有匹配的原型并不会自动停止使用其中的某个函数,因为C++将尝试使用标准类型转换强制进行匹配。如果有多个参数列表都能够强制类型转换其对应的数据类型,C++将拒绝这种函数调用,并将其视为错误(我可以勉强匹配一个,有好多个我都能勉强匹配,那我就不干了)。
函数重载中,类型引用和类型本身视为同一特征标,并且 const和非const变量是两种不同的特征标(const标可以接受const 和 非const数据,非const标只能接受非const数据)。
如果参数都能适配多个函数,系统则会调用最匹配的版本。
当且仅当函数基本执行相同任务,但使用不同形式的数据时,才应该采用函数重载。而且不要忘了默认参数(默认参数相比函数重载,只需编写一个函数,程序也只需为一个函数请求内存,并且修改函数时,也只需要修改一个);然而,如果需要使用不同类型的参数,默认参数不管用,应该使用函数重载。
C++通过编辑器进行的名称修饰(名称矫正)来跟踪每一个重载函数。
#include<iostream>
char* left(const char* str, int n = 1);
unsigned long left(unsigned long num, unsigned n);
int main()
{
const int SIZE = 80;
using namespace std;
char str[SIZE];
char* pt;
cout << "Enter a string:\n";
cin.get(str, SIZE);
pt = left(str, 4); //给定参数
cout << pt << endl;
pt = left(str); //默认参数
cout << pt << endl;
delete[] pt;
unsigned long num = 123456;
unsigned long out;
out = left(num, 2);
cout << out << endl;
return 0;
}
char* left(const char* str, int n)
{
int len = strlen(str);
n = (len > n)?n: len; //防止浪费空间
char* pt = new char [n + 1];
int i = 0;
while (i < n && str[i])
{
pt[i] = str[i];
i++;
}
pt[i] = '\0';
return pt;
}
unsigned long left(unsigned long num, unsigned int n)
{
unsigned temp = num;
if (num == 0 || n == 0)
return 0; //空
unsigned int i = 1;
while (temp /= 10) //不能为num,不然就为0 了
i++;
if (i > n) //小于数字位数
{
n = i - n;
while (n--)
num /= 10;
return num;
}
else
return num;
}
⑤函数模板
1.介绍:
函数模板使用泛型来定义函数,其中的泛型可用具体的类型(如int 或 double)替换。通过将类型作为参数传递给模板,可使得编译器生成该类型的函数。
模板的建立:
template<typename T> //<class T>
void Swap(T& a, T& b) //开始编写函数 用 T代替数据类型的位置
{
T temp;
temp = a;
a = b;
b = temp;
}
注:函数模板不能缩短可执行程序。调用两次函数模板,最终仍将由两个独立的函数定义,最终的代码不包含任何的模板,而只包含了为程序生成的实际函数。更常见的情形是将模板放在头文件中,并在需要使用模板的文件中包含头文件。
2.模板的重载:
需要对多个不同类型使用同一种算法的函数时,可使用模板。并非所有的类型都使用相同的算法。为满足这种需求,可以像重载常规函数定义那样重载模板定义,被重载的模板的函数特征标必须不同,并非所有的模板参数都必须是模板参数类型,也可以是确定的参数类型。
#include<iostream>
template<typename T> //模板一定在前边
void Swap(T& a, T& b)
{
T temp;
temp = a;
a = b;
b = temp;
}
template<typename T> //模板的重载
void Swap(T ar1[], T ar2[], int n)
{
for (int i = 0; i < n; i++)
{
T temp;
temp = ar1[i];
ar1[i] = ar2[i];
ar2[i] = temp;
}
}
template<typename T>
void Show(T ar1[], int n)
{
for (int i = 0; i < n; i++)
std::cout << ar1[i] << " ";
std::cout << std::endl;
}
int main()
{
using namespace std;
const int SIZE = 10;
int a, b;
double c, d;
int a1[SIZE] = { 1,2,3,4,5,6,7,8,9,10 };
int b1[SIZE] = { 11,21,31,41,51,61,71,81,91,100 };
double c1[SIZE] = { 1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0 };
double d1[SIZE] = { 11.0,21.0,31.0,41.0,51.0,61.0,71.0,81.0,91.0,101.0 };
a = 1;
b = 10;
c = 2.0;
d = 20.0;
cout << "a = " << a << ",b = " << b << endl << "c = " << c << ",d = " << d << endl;
cout << endl;
Swap(a, b);
cout << "exchange:\n";
cout << "a = " << a << ",b = " << b << endl << "c = " << c << ",d = " << d << endl;
Show(a1, SIZE);
Show(b1, SIZE);
cout << "exchange:\n";
Swap(a1, b1, SIZE);
Show(a1, SIZE);
Show(b1, SIZE);
Show(c1, SIZE);
Show(d1, SIZE);
cout << "exchange:\n";
Swap(c1, d1, SIZE);
Show(c1, SIZE);
Show(d1, SIZE);
return 0;
}
3.模板的局限性:
函数模板中,通常假定了可执行哪些操作。比如代码规定了赋值操作、乘法操作等,但是对于结构体,数组来说,该函数并不成立,也就是说,编写的模板很可能无法处理某些类型,解决方案是为特定的类型提供具体化的模板。
4.显示具体化:
函数模板无法使用Swap()函数来交换结构体中某些成员的值,可以提供一个具体化函数定义——显示具体化,其中包含所需的代码。当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。
如果有多个原型,编译器在选择原型时,非模板优先于显示具体化和模板版本,而显式具体化优先于使用模板生成的版本。
struct job
{
char name[40];
double salary;
int floor;
};
//非模板函数
void Swap(job& a1,job& a2) //可引用,可指针,这里是引用
//模板函数
template<typename T>
void Swap(T & a, T & b);
//显式具体化
template<> void Swap<job>(job & a1,job & a2); //<job>可选,也就是说 显式具体化比非模板函数多了template<>
5.实例化和具体化
5.1实例化
在代码中包含函数模板本身并不会生成函数定义,它只是用于生成函数定义的方案。当我使用模板,为模板传入数据类型参数时,编译器使用模板为我写类型生成函数定义,这时得到的是模板实例。实例化有两种形式,分别为显式实例化和隐式实例化。
隐式实例化(implicit instantiation)
使用模板(进行调用),生成的函数定义就是隐式实例化。
显式实例化(explicit instantiation)
显式实例化意味着在调用前,直接命令编译器创建特定的实例,而不是等传入参数才知道要为模板传入什么数据类型(从而生成相关函数定义)。显示实例化声明方式:
比如存在这么一个模板函数
template <typename T>
void Swap(T &a, T &b)
第一种方式是声明所需的种类,用<>符号来指示类型,并在声明前加上关键词template,如下:
template void Swap<int>(int &, int &);
第二种方式是直接在程序中使用函数创建,如下:
Swap<int>(a,b);
显式实例化直接使用了具体的函数定义,而不是让程序去自动判断。
#include<iostream>
struct job
{
char name[40];
double salary;
int floor;
};
template<typename T>
void Swap(T& a, T& b);
void show(const job j);
template<>void Swap<job>(job& a1, job& a2);
int main()
{
using namespace std;
int i = 10, j = 20;
cout << "i = " << i << " j = " << j << endl;
Swap(i, j);
cout<<"now i = " << i << " j = " << j << endl;
job Sue = { "Sue",675555.7,9 };
job sidney = { "sidney",1111.1,3 };
show(Sue);
show(sidney);
Swap(Sue, sidney);
cout << "now\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& a1, job& a2) //显示具体化
{
double s;
int f;
s = a1.salary;
a1.salary = a2.salary;
a2.salary = s;
f = a1.floor;
a1.floor = a2.floor;
a2.floor = f;
}
void show(const job j)
{
std::cout << j.name << ": $ " << j.salary << " on floor " << j.floor << std::endl;
}
5.2具体化
显式具体化将不会使用Swap()模板来生成函数定义,而应使用专门为该特定类型显式定义的函数类型。
区分: template<> ——具体化 没有<> ——实例化
警告:试图在同一个文件(或转换单元)中使用同一种类型的显式实例和显式具体化将出错。
6.编译器选择使用哪个版本
对于函数重载、模板重载和函数模板重载,C++需要一个定义良好的策略,来决定为函数调用选择使用哪一个函数定义,这个过程称为重载解析。大致过程如下:
①创建候选函数列表。其中包含与被调函数名称相同的函数和模板函数;
②使用候选函数列表创建可行函数列表(参数数目正确,隐式转换不行的拿掉(char int float可以隐式转换的也要留下));
③确定是否有最佳可行函数。
如何确定最佳可行函数:
①完全匹配>②提升转换(char short转换为int, float转换为double)>③标准转换(int 转换为char,long转换为double)>④用户自定义转换(如类声明中的定义转换)
什么是完全匹配?
进行完全匹配的时候,C++允许某些无关紧要的转换,表8.1,Type可以是任何类型(引用,指针等),如果有多个匹配的原型,编译器无法完成重载解析,没有最佳的可行函数,编译器会生成一条错误信息。然而,有时候,即使两个函数都完全匹配,仍可完成重载解析。
1.对于指针和引用,指向非const数据的指针和引用优先于非const指针和引用参数匹配;
2.非模板函数优先于模板函数,如果两个都是模板函数,较具体的优先。最具体并不意味着显式具体化,而是指编译器推断使用哪种类型时执行的转换最少。如果有多个同样合适的非模板或模板函数,但没有一个函数比其他函数更具体,则函数调用是不确定的,因此是错误的,如果不存在匹配的韩式,则也是错误的。
创建自定义选择:
在有些情况下,可通过编写合适的函数调用,引导编译器做出希望的选择。
#include<iostream>
template<typename T>
T lesser(T a, T b);
int lesser(int a, int b);
int main()
{
using namespace std;
int m = 20, n = -30;
double x = 15.5;
double y = 25.9;
cout << lesser(m, n) << endl; //调用int
cout << lesser(x, y) << endl; //模板并且传入double
cout << lesser<>(m, n) << endl; //<>表明用模板
cout << lesser<int>(x, y) << endl; //显示实例化,强制转换为int
return 0;
}
template<typename T>
T lesser(T a, T b)
{
return a < b ? a : b;
}
int lesser(int a, int b)
{
a = a < 0 ? -a : a;
b = b < 0 ? -b : b;
return a < b ? a : b;
}
对于多个参数的函数调用与有多个参数的原型进行匹配的时候,编译器必须考虑所有参数的匹配情况。一个函数比其他函数都合适,所有参数的匹配情况都必须不比其他函数差,同时至少有一个参数的匹配程度比其他函数都高。
7.模板函数的发展
问题:
template<class T1,class T2>
void ft(T1 X, T2 Y)
{
xpy = x + y;
}
请问 xpy是什么类型呢?
解决方案(C++11)
1.关键字decltype:
int x;
decltype(x) y; //使y有着和x一样的类型
给decltype提供的参数可以是表达式,也可以将语句合二为一:
int x;
decltype(x) y; //使y有着x一样的数据类型
decltype (x + y) xpy;
xpy = x + y;
//合并到一起
decltype (x + y) xpy = x + y;
decltype比这些示例岩石的复杂些,为确定类型,编译器必须便利一个核对表,假设有如下声明:
decltype(expression) var;
第一步: 如果expression是一个没有用括号括起来的标识符,则var的类型与该标识符类型相同,包括const等限定符;
第二步:(第一步不满足)如果expression是一个函数调用,则var的类型与函数的返回类型相同(不需要调用,编译器看一眼函数原型就知道了);
第三步:(一二步都不满足)如果expression是一个左值,则 var为指向其类型的引用
注:expression是一个用括号括起来的标识符:
double xx = 4.4;
decltype((xx)) r2 = xx;
括号并不会改变表达式的值和左值性。
第四步:(前边三步都不满足)则var的类型与expression类型相同:
decltype(100L) i1; //i1为long类型
2.另一种函数声明语法(C++11后置返回类型)
有一个问题是decltype无法解决的。
void ft(T1 X, T2 Y)
{
...
return x + y;
}
此时未声明x和y,无法使用decltype,为此,C++新增了一种声明和定义函数的方法。
double h(int x, float y); //原来
auto h(int x,float y) -> double; //新增
double是后置返回类型,auto是一个占位符,表示后置返回类型提供的类型。可用于函数定义,也可通过结合使用这种语法和decltype指定返回类型。
auto ft(T1 X, T2 Y) ->decltype(X+Y)
{
...
return x + y;
}