C++——函数探幽(10000字)

内联函数

常规函数和内联函数之间的主要区别不在于编写方式,而在于C++编译器如何将他们组合搭配程序中。

运行过程时,操作系统将这些指令载入到计算机内存中,因此每一条指令都有特定的内存地址。

执行到函数调用命令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码,然后跳回到地址被保存的指令处。来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。

C++内联函数提供了另外一种选择。

内联函数的编译代码与其他程序代码“内联”起来了。也就是说,编译器将使用相应的函数代码代替换函数调用。

对于内联函数,程序无需跳到另一个位置执行后代码,再调回来。因此,内联函数的运行速度比常规函数稍快。

要使用这项特性,必须采取以下措施之一:

  • 在函数声明前加上关键字inline;

  • 在函数定义前加上关键字inline;

程序员请求将函数作为内联函数时,编译器并不一定会满足这种要求。他可能认为该函数过大或注意到函数调用了自己(内联函数不可以递归),因此不将其作为内联函数;

内联与宏

inline工具是C++新增的特性。C语言使用预处理器语句#define来提供宏——内联代码的原始实现。

例如,下面是一个计算平方的宏:

#define SQUARE(X) X*X

这并不是通过传递参数实现的,而是通过文本替换实现的——X是“参数”的符号标记。

a=SQUARE(5.0);
b=SQUARE(4.5+7.5);
d=SQUARE(C++);

上述示例中只有第一个能够正常工作。可以通过括号来进行改进

#define SQUARE(x) ((x)*(x))

但仍存在这样的问题,即宏不能按值传递。即使使用心得定义,SQUARE(C++)仍将c递增两次,但是使用内联函数计算结果,传递他,以计算其平方值,然后将c递增一次。

这里的目的不是演示如何编写C宏,而是要指出,如果C语言的宏执行了类似函数的功能,应考虑将他们转换为C++内联函数。

引用变量

C++新增了一种复合类型——引用变量。

引用是已定义的变量的别名(另一个名称)

引用变量的主要用途是用作函数的形参。

创建引用变量

C和C++使用&符号来指示变量的地址。C++给&赋予了另一个含义,将其用来声明引用。

例如,要将rodents作为rats变量的别名,可以这样做:

int rats;
int & rodents=rats;

其中,&不是取地址运算符,而是类型标识符的一部分。

上述引用声明允许将rats和rodents互换——即他们指向相同的内存单元和值。

对于C语言用户来说,首次接触到引用时可能也会有些困惑,因为这些用用户很自然的想到指针,但他们之间还是有区别的。例如,可以创建指向rats的引用和指针:

int rats=10;
int & rodents=rats;
int *prats=&rats;

实际上,引用还是不同于指针的。除了表示法不同外,还有其他的差别。例如,差别之一是,必须在声明引用时将其初始化,而不是像指针那样,先声明再赋值:

int rats;
int &rodents;
rodents=rat;

上述代码是错误的!!

引用常被用做函数参数,是得函数中的变量名成为调用程序中的变量的别名。这种传递参数的方法为按引用传递。

按引用传递允许被调用的函数能够访问调用函数的变量。

引用的属性和特别之处

临时变量、引用参数和const

如果实参与引用参数不匹配,C++将生成临时变量。当前,仅当参数为const引用时,C++才允许这样做。

首先,什么时候将会创建临时变量呢?如果引用参数是const,则编译器将在下面两种情况生成临时变量:

  • 实参的类型正确,但不是左值;

  • 实参的类型不正确,但可以转换为正确的类型;

左值是什么呢?

左值参数是可被引用的数据对象,例如,变量、数据元素、结构成员、引用和接触引用的指针都是左值。

非左值包括字面常量(用引号括起的字符串除外,他们由其地址表示),和包含多项的表达式。

现在,常规变量和const变量都可视为左值,因为可通过地址访问他们。但常规变量属于可修改的左值,而const变量属于不可修改的左值。

假设我们定义了refcube(),使其接受一个常量引用参数:

double refcube(const double &ra)
{
    return ra*ra*ra;
}
​
double side=3.0;
double *pd=&side;
double &rd=side;
long edge=5L;
double lens[4]={2.0,5.0,10.0,12.0};
double c1=refcube(side);
double c2=redcube(len[2]);
double c3=refcube(rd);
double c4=refcube(*pd);
double c5=refcube(edge);
double c6=refcube(7.0);
double c7=refcube(size+10.0);

参数side、len[2]、rd和*pd都是有名称的、doble类型的数据对象,因此可以为其创建引用,而不需要临时变量

然而,edge虽然是变量,类型却不正确,double引用不能指向long。另一方面,参数7.0和side+10.0的类型都正确,但没有名称,在这些情况下,编译器都将生成一个临时匿名变量,并让ra指向它。

这些临时变量只在函数调用期间存在,此后编译器便可以随意删除。

如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将组织这种意图的实现。

如果声明将引用指定为const,C++将在必要时生成临时变量。实际上,对于形参为const引用的C++函数,如果实参不匹配,则其行为类似于按值传递,为确保原始数据不被修改,将使用临时变量来存储值。

***注意:如果函数调用的参数不是左值或与相应的const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数引用该变量。

尽可能使用const

将引用参数声明为常量数据的引用的理由有三个:

  • 使用const可以避免无意中修改数据的编程错误;

  • 使用const使函数呢能够处理const和非const实参,否则将只能接受非const数据;

  • 使用const引用使函数能够正确生成并使用临时变量。

C++11新增了另一种引用——右值引用。这种引用可指向右值,是使用&&声明的:

double &&rref=std::sqrt(36.00);
double j=15.0;
double && jref=2.0*j+18.5;

对象、继承和引用

什么是继承?

使能够将特性从一个类传递给另一个类的语言特性被称为继承。

ostream是基类,而ofstream是派生类。派生类继承了基类的方法,这意味着ofstream对象可以使用基类的特性。

继承的另一个特征

基类引用可以指向派生类对象,而无需进行强制类型转换。

这种特征的一个实际结果,可以定义一个接受基类引用作为参数的函数。调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。

例如:参数类型为ostream &的函数可以接受ostream对象(如cout)或您声明的ofstream对象作为参数。

何时使用引用参数

使用引用参数的主要原因有两个

  • 程序员能够修改调用函数中的数据对象。

  • 通过传递引用而不是整个数据对象,可以提高程序的运行速度。

什么时候使用引用?什么时候使用指针呢?什么时候应该按值传递呢?

  • 如果数据对象很小,如内置数据类型或小型结构,则按值传递。

  • 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为只想const的指针

  • 如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率。这样还可以节省复制结构所需的时间和空间。

  • 如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用。

  • 如果数据对象是内置数据类型,则使用指针。

  • 如果数据对象是数组,则只能使用指针。

  • 如果数据对象是结构,则使用引用或指针。

  • 如果数据对象是类对象,则使用引用。

默认参数

什么是默认参数?

默认参数指的是当函数调用中省略了实参时,自动使用的一个值。

如果将void wow(int n)设置成n有默认值为1,则函数调用wow()相当于wow(1)。

如果设置默认值?

必须通过函数原型。

由于编译器通过查看原型来了解函数所使用的参数数目,因此函数原型也必须将可能的默认参数告知程序。方法是将值赋给原型中的参数。

例如,left()的原型如下:

char * left(const char * str,int n = 1);

实参按从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数。

函数重载

默认参数让您能够使用不同数目的参数调用同一个函数,而函数多态(函数重载)让您能够使用多个同名的函数。属于”多态“指的是有多种形式,因此函数多态允许函数可以有多种形式。类似地,术语 ” 函数重载 “ 指的是可以有多个同名的函数,因此对名称进行了重载。这两个术语是一回事,但我们通常使用函数重载。可以通过函数重载来设计一系列函数——他们完成相同的工作,但使用不同的参数列表。

函数重载的关键就是函数的参数列表——也成为函数特征标(function signature)

如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则他们的特征标相同,而变量名是无关紧要的。

C++允许定义名称相同的函数,条件是他们的特征标不同。如果参数数目和/或参数类型不同,则特征标不同。、

一些看起来彼此不同的特征标是不能共存的。例如下面:

double cube(double x);

double cube(double &x);

编译器在检查函数特征标时,将把类型引用和类型本身视为同一个特征标。

匹配函数时,要区分const和非const变量。

void dribble(char  *bits);
​
void dribble(const  char * cbits);
​
void dabble(char *bits);
​
void drivel(const char  * bits);

下面列出了各种函数调用对应的原型:

const char p1[20]="How's the weather?";
char p2[10]="How's business?";
dribble(p1);   //dribble(const char * );
dribble(p2);  //dribble(char *);
dabble(p1);  //no match
dabble(p2);  //dabble(char *);
drivel(p1);  //drivel(const char *);
drivel(p2);  //drivel(const char *);

drivle()和dabble()之所以在行为上有这种差别,主要是因为将非const值赋给const变量是合法的,但反之则是非法的。

返回类型可以不同,但特征标也必须不同。

何时使用函数重载?

虽然函数重载很吸引人,但也不要滥用。仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应采用函数重载。

函数模板

函数模板是通用的函数描述,也就是说,他们使用泛型来定义函数,其中的泛型可用具体的类型(如int或double型)替换,通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型(而不是具体类型)的方式编写程序,因此有时也被称为通用编程。由于类型是由参数表示的,因此模板特性有时也被称为参数化类型(parameterized types),下面介绍为何需要这种特性以及其工作原理。

函数模板允许以任意类型的方式来定义函数。例如,可以这样建立一个交换模板:

template <typename  AnyType>
void Swap(AnyType &a,AnyType &b)
{
    AnyType temp;
    temp=a;
    a=b;
    b=temp;
}

第一行指出,要建立一个模板,并将类型命名为AnyType。关键字template和typename是必需的,除非可以使用关键字class代替typename。另外,必须使用尖括号。类型名可以任意选择(这里为AnyType),只要遵守C++命名规则即可;很多程序员都是用简单的名称,如T。

模板并不创建任何函数,而只是告诉编译器该如何定义函数。需要交换int的函数时,编译器将按模板模式创建这样的函数,并用int 代替AnyType 。同样,需要交换double的函数时,编译器将按模板模式创建这样的函数,并用double代替AnyType。

注意:函数模板不能缩短可执行程序。最终的代码不包含任何模板,而至包含了为程序生成的实际函数。

使用模板的好处是他是生成多个函数定义简单、更可靠。

重载的模板

和常规重载一样,被重载的模板的函数特征标必须不同。例如,下面程序新增了一个交换模板,用于交换两数组中的元素。原模版特征标为(T &,T&),而新模板的特征标为(T[],T[],int)。注意,在后一个模板中最后一个参数类型为具体类型(int),而不是泛型。并非的所有的模板参数都必须是模板参数类型。

#include<iostream>
using namespace std;
template <typename T>
void Swap(T& a, T& b);
​
template <typename T>
void Swap(T* a, T* b,int n);
void Show(int a[]);
const int Lin = 8;
int main()
{
    int i = 10, j = 20;
    cout << "i,j= " << i << "  " << j << ".\n";
    cout << "Using compiler-gernertated int swapper:\n";
    Swap(i, j);
    cout << "Now i,j = " << i << ", " << j << " .\n";
    
    int d1[Lin] = { 0,7,0,4,1,7,7,6 };
    int d2[Lin] = { 0,7,2,0,1,9,6,9 };
    cout << "Original array:\n";
    Show(d1);
    Show(d2);
    Swap(d1, d2, Lin);
    cout << "Swpped array:\n";
    Show(d1);
    Show(d2);
    return 0;
}
template <typename T>
void Swap(T& a, T& b)
{
    T temp = a; a = b; b = temp;
}
​
​
template <typename T>
void Swap(T* a, T* b, int n)
{
    T temp;
    for (int i = 0; i < n; i++)
    {
        temp = a[i];
        a[i] = b[i];
        b[i] = temp;
    }
}
​
void Show(int a[])
{
    cout << a[0] << a[1] << "/";
    cout << a[2] << a[3] << "/";
    for (int i = 4; i < Lin; i++)
        cout << a[i];
    cout << endl;
}

以下是程序的输出结果:

i,j= 10 20. Using compiler-gernertated int swapper: Now i,j = 20, 10 . Original array: 07/04/1776 07/20/1969 Swpped array: 07/20/1969 07/04/1776

模板的局限性

假设有如下模板函数:

template<class T>//class可替换为 typename;
void f(T a,T b);

通常代码假定可执行哪些操作。例如,下面的代码假定定义了赋值,但如果T为数组,这种假设将不成立。

a=b;

总之,编写的模板函数很可能无法处理某些类型。另外,有时候通用化是有意义的,但C++语法不允许这样做。

例如,将两个包含位置的坐标的结构相加是有意义的,虽然没有为结构定义运算符+。

一种解决方案是,C++允许您重载运算符+,以便能够将其用于特定的结构或类。这样使用运算符+的模板便可处理重载了运算符+的结构。另一种解决方案时,为特定类型提供具体化的模板定义。下面就来介绍这种解决方案。

显示格式化

假设定义了如下结构:

struc job
{
    char name[40];
    double salary;
    int floor;
};

由于C++允许将一个结构赋给另外一个结构,可以交换两个结构。

然而假设只想交换salary和floor成员,而不交换name成员,则需要使用不同的代码,但Swap()的参数将保持不变(两个job结构的引用),因此无法使用模板重载来提供其他的代码。

然而,可以提供一个具体化函数定义---成为显式具体化(explicit specialization),其中包括所需的代码。

当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。

C++98标准选择了下面的方法。

  • 对于给定的函数名,可以有非模板函数、模板函数、和显式具体化模板函数以及他们的重载版本。

  • 显式具体化的原型和定义应以template<>打头,并通过名称来指定类型。

  • 具体化优于常规模板,而非模板函数优先于具体化和常规模板。

下面是用于交换job结构的非模板函数、模板函数、和具体化的原型:

void Swap(job&.job&);//非模板函数;
​
template <typename T>
void Swap(T&,T&);//模板函数;
​
template<> void Swap<job>(job&,job&);//具体化
​

正如前面所指的,如果有多个原型,则编译器在选择原型时,非模板版本优先于显式具体化和模板版本,而显示具体化优先于使用模板生成的版本.

这是显式具体化的函数定义.

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;
}

实例化和具体化

为进一步了解模板,必须理解术语实例化和具体化.

记住,在代码中包含函数模板并不会生成函数定义,他只是一个用于生成函数定义的方案.

编译器使用模板为特定类型生成函数定义时,得到的是模板实例(instantiation).

函数调用Swap(i,j)导致编译器生成Swap()的一个实例,该实例使用int类型。

模板并非函数定义,但使用int的模板实例是函数定义.这种实例化方法没成为隐式实例化(implicit instantiation).

最初,编译器只能通过隐式实例化,来使用模板生成函数定义,但现在C++还允许显式实例化(explicit instantiation)。这意味着可以直接命令编译器生成函数定义,如Swap<int>()。其语法是,声明所需的种类——用<>符号指示类型,并在声明前加上关键字template.

template void Swap<int>(int ,int );

实现了这种特性的编译器看到上述声明后,将使用Swap()模板生成一个使用int类型的实例。也就是说,该声明的意思是“使用Swap()模板生成int类型的函数定义”。

与显式实例化不同的是,显式具体化使用下面两个等价的声明之一:

template<> void Swap<int>(int& ,int&);
template <>void swap(int &,int&);

区别在于,这些声明的意思是,“不要使用Swap()模板来生成函数定义,而应使用专门为int类型显示地定义的函数定义”。

这些原型必须有自己的函数定义。显式具体化声明的关键字template后包含<>,而显示实例化没有。

还可通过在程序中使用函数来创建显式实例化,例如:

template<class T>
T Add(T a,T b)
{
    return a+b;
}
...
int m=6;
double x=10.2;
cout<<Add<double>(x,m)<<endl;

这里的模板与函数调用Add(x,m)不匹配,因为该模板要求两个函数参数的类型相同。但通过使用Add<double>(x,m),可强制为double类型实例化,并将参数m强制转换为double类型,以便与函数Add<double>(double,double)的第二个参数匹配。

如果对Swap()做类似的处理,结果将如何?

int m=5;
double x=14.3;
Swap<double>(m,x);

这将为类型double生成一个显式实例化。不幸的是,这些代码不管用,因为第一个形参的类型为double&,不能指向int变量m

隐式实例化、显式实例化和显示具体化统称为具体化。他们的相同之处在于,他们表示的都是是使用具体类型好的函数定义,而不是通用描述。

引入显示实例化后,必须使用新的语法——在声明中使用前缀template和template<>,以区分显式实例化和显示具体化。通常来说,功能越多,语法规则也越多。下面的代码片段总觉了这些概念:

template<class T>
void Swap(T&, T&);
​
template<> void Swap<job>(job&, job&);
int main()
{
    template void Swap<char>(char&, char&);
    short a, b;
    ...
    Swap(a,b);
    job n,m;
    ...
    Swap(n,m);
    char g,h;
    ...
    Swap(g,h);
    ...
}

编译器看到char的显示实例后,将使用模板定义生成Swap()的char版本。对于其他Swap()调用,编译器格局函数调用中实际的参数,生成相应的版本。例如,当编译器看到函数调用Swap(a,b)后,将生成生成Swap()的short的版本,因为两个参数的类型都是short。当编译器看到Swap(n,m)后,将使用为job类型提供的独立定义(显式具体化)。当编译器看到Swap(g,h)后,将使用处理显式实例化时生成的模板具体化。

编译器选择使用那个函数版本

对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义、尤其是有多个参数时,这个过程被称为重载解析(overloading resolution)。我们先大致了解一下这个过程时如何进行的。

  • 第一步:创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。

  • 第二步:使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应形参类型完全匹配的情况。例如,使用float参数的函数调用可以将该参数转换为double,从而与double形参匹配,而模板可以为float生成一个实例。

  • 第三步:确定是否有最佳的可行函数。如果有,就用它,没有该函数调用出错。

考虑只有一个函数参数的情况,如下面的调用:

may('B');

首先,编译器将寻找候选者,即名称为may()的函数和函数模板。然后寻找哪些可以用一个参数调用的函数。例如,下面的函数如何要求,因为其名称与被调用的函数相同,且可只给他们传递一个参数:

void may(int);                           //#1
float may(float,float=3);                //#2
void may(char);                          //#3
char* may(const char *);                 //#4
char may(const char&);                   //#5
template<class T> void may(const T &);   //#6
template<class T> void may(T*);          //#7

注意,只考虑特征标,而不考虑返回类型。其中的两个候选函数(#4和#7)不可行,因为整数类型不能被隐式地转换(即没有显示强制类型转换)为指针类型。剩余的一个模板可用来生成具体化,其中T被替换为char类型。这样剩下的5个函数,其中的每一个函数,如果他是声明的唯一一个函数,都可以被使用。

接下来,编译器必须确定哪一个可行函数是最佳的。他查看为使函数调用参数与可行的候选函数的参数匹配所需要进行的转换。通常,从最佳到最差的顺序如下所示:

  1. 完全匹配,但常规函数优先于模板

  2. 提升转换(例如,char和short自动转换为int,float自动转换为double)

  3. 标准转换(例如,int转换为char,long转化为double)

  4. 用户定义的转换,如类声明中定义的函数。

例如,函数#1优于函数#2,因为char到int的转换使提升转换,而char到float的转换为标准转换。

函数#3、函数#5和函数#6都优于函数#1和函数#1,因为他们都是完全匹配的。

3和#5由于#6,因为#6是函数模板。

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值