《C++ Primer Plus》第八章总结

一、C++内联函数

内联函数是C++为提高程序运行速度所做的一项改进。常规函数和内联函数之间的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中。

编译过程的最终产品是可执行程序,由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存地址。计算机随后将逐步执行这些指令。有时(如有循环或分支语句时),将跳过一些指令,向前或向后跳到特定地址。常规函数调用也使程序跳到另一个地址(函数的地址),并在函数结束时返回。

执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入到寄存器中),然后跳回到地址被保存的指令处(这与阅读文章时停下来看脚注,并在阅读完脚注后返回到以前阅读的地方类似)。来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。

C++内联函数提供了另一种选择。内联函数的编译代码与其他程序代码“内联”起来了。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。如果程序在10个不同的地方调用同一个内联函数,则该程序将包含该函数代码的10个副本。

应有选择地使用内联函数。如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小一部分。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。另一方面,由于这个过程相当快,因此尽管节省了该过程的大部分时间,但节省的时间绝对值并不大,除非该函数经常被调用。

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

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

通常的做法是省略原型,将整个定义放在本应提供原型的地方。程序员请求将函数作为内联函数时,编译器并不一定会满足这种要求。它可能认为该函数过大或注意到函数调用了自己(内联函数不能递归),因此不将其作为内联函数;而有些编译器没有启用或实现这种特性。

#include <iostream>
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); // can pass expressions
    cout << "a = " << a << ", b = " << b << "\n";
    cout << "c = " << c;
    cout << ", c squared = " << square(c++) << "\n";
    cout << "Now C = " << c << "\n";
    return 0;
}

输出表明,内联函数和常规函数一样,也是按值来传递参数的。如果参数为表达式,则函数将传递表达式的值。这使得C++的内联功能远远胜过C语言的宏定义。

尽管程序没有提供独立的原型,但C++原型特性仍在起作用。这是因为在函数首次使用前出现的整个函数定义充当了原型。这意味着可以给square()传递int或Iong值,将值传递给函数前,程序自动将这个值强制转换为 double类型。

内联与宏

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

例如,下面是一个计算平方的宏:
#define SQUARE (X) X*X

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

a = SQUARE (5.0); is replaced by a = 5.0*5.0;
b = SQUARE (4.5+7.5); is replaced by b = 4.5 + 7.5 * 4.5+ 7.5;
d = SQUARE (c++); is replaced by d = c++*c++;

上述示例只有第一个能正常工作。可以通过使用括号来进行改进:
#define SQUARE (X) ((X)*(X))

但仍然存在这样的问题,即宏不能按值传递。即使使用新的定义,SQUARE(C++)仍将c递增两次,但是程序中的内联函数square()计算c的结果,传递它,以计算其平方值,然后将c递增一次。如果使用C语言的宏执行了类似函数的功能,应考虑将它们转换为C++内联函数。

二、引用变量

C++新增了一种复合类型——引用变量。引用是已定义的变量的别名(另一个名称)。引用变量的主要用途是用作函数的形参。通过将引用变量用作参数,函数将使用原始数据,而不是其副本。这样除指针之外,引用也为函数处理大型结构提供了一种非常方便的途径,同时对于设计类来说,引用也是必不可少的。

1、创建引用变量

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

例如,要将rodents作为rats变量的别名,可以这样做:
int rats;
int & rodents = rats;//rodents是rats的别名

其中,&不是地址运算符,而是类型标识符的一部分。就像声明中的char*指的是指向char的指针一样,int&指的是指向int的引用。

#include <iostream>
int main()
{
    using namespace std;
    int rats = 101;
    int &rodents = rats;
    cout << "rats = " << rats;
    cout << ", rodents = " << rodents << endl;
    rodents++;
    cout << "rats = " << rats;
    cout << ", rodents = " << rodents << endl;
    cout << "rats address = " << &rats;
    cout << ", rodents address = " << &rodents << endl;
    return 0;
}

可以创建指向rats 的引用和指针:
int rats = 101;
int & rodents = rats;
int * prats = &rats;

表达式rodents和*prats都可以同rats互换,而表达式&rodents和 prats都可以同&rats互换。从这一点来说,引用看上去很像伪装表示的指针。实际上,引用还是不同于指针的。除了表示法不同外,还有其他的差别。

差别之一是,必须在声明引用时将其初始化,而不能像指针那样,先声明,再赋值;

引用更接近 const 指针,必须在创建时进行初始化,一旦与某个变量关联起来,就将一直效忠于它。也就是说:int & rodents = rats;实际上是下述代码的伪装表示:int * const pr = &rats;其中,引用rodents扮演的角色与表达式*pr相同。

#include <iostream>
int main()
{
    using namespace std;
    int rats = 101;
    int &rodents = rats;
    cout << "rats = " << rats;
    cout << ", rodents = " << rodents << endl;
    cout << "rats address = " << &rats;
    cout << ", rodents address = " << &rodents << endl;
    int bunnies = 50;
    rodents = bunnies;
    cout << "bunnies = " << bunnies;
    cout << ", rats = " << rats;
    cout << ", rodents = " << rodents << endl;
    cout << "bunnies address = " << &bunnies;
    cout << ", rodents address = " << &rodents << endl;
    return 0;
}

最初,rodents引用的是rats,但随后程序试图将rodents 作为 bunnies的引用:
rodents = bunnies;

这种意图暂时是成功的,因为rodents的值从101变为了50,但rats也变成了50,同时rats和rodents的地址相同,而该地址与bunnies的地址不同。由于rodents是rats的别名,因此上述赋值语句与下面的语句等效:
rats = bunnies;

简而言之,可以通过初始化声明来设置引用,但不能通过赋值来设置。

2、将引用用作函数参数

引用经常被用作函数参数,使得函数中的变量名成为调用程序中的变量的别名。这种传递参数的方法称为按引用传递。按引用传递允许被调用的函数能够访问调用函数中的变量。按值传递导致被调用函数使用调用程序的值的拷贝。

#include <iostream>
void swapr(int &a, int &b);
void swapp(int *p, int *q);
void swapv(int a, int b);
int main()
{
    using namespace std;
    int wallet1 = 300;
    int wallet2 = 350;
    cout << "walletl = $" << wallet1;
    cout << " wallet2 = $" << wallet2 << endl;
    cout << "Using references to swap contents: \n";
    swapr(wallet1, wallet2);
    cout << "wallet1 = $" << wallet1;
    cout << " wallet2 = $" << wallet2 << endl;
    cout << "Using pointers to swap contents again: \n";
    swapp(&wallet1, &wallet2); // pass addresses of variables
    cout << "wallet1 = $" << wallet1;
    cout << " wallet2 = $" << wallet2 << endl;
    cout << "Trying to use passing by value: \n";
    swapv(wallet1, wallet2); // pass values of variables
    cout << "walletl = $" << wallet1;
    cout << " wallet2 = $" << wallet2 << endl;
    return 0;
}
void swapr(int & a, int & b)
{
    int temp = a;
    a = b;
    b = temp;
}
void swapp(int * p, int *q)
{
    int temp = *p;
    *p = *q;
    *q = temp;
}
void swapv(int a, int b)
{
    int temp = a;
    a = b;
    b = temp;
}

3、引用的属性和特别之处

#include <iostream>
double cube(double a);
double refcube(double &ra);
int main()
{
    using namespace std;
    double x = 3.0;
    cout << cube(x);
    cout << "=cube of " << x << endl;
    cout << refcube(x);
    cout << "= cube of " << x << endl;
    return 0;
}
double cube(double a)
{
    a *= a * a;
    return a;
}

double refcube(double &ra)
{
    ra *= ra * ra;
    return ra;
}

如果ra是一个变量的别名,则实参应是该变量。

double z = refcube (x + 3.0); // 代码不合理,因为表达式x+3.0并不是变量

x + 3.0 = 5.0;//不能将值赋给该表达式

如果试图使用像refcube(x+3.0)这样的函数调用,在现代的C++中,这是错误的,大多数编译器都将指出这一点,而有些较老的编译器将发出警告。这是由于早期的C++确实允许将表达式传递给引用变量。有些情况下,仍然是这样做的。这样做的结果如下:由于x+3.0不是double类型的变量,因此程序将创建一个临时的无名变量,并将其初始化为表达式x±3.0的值。然后,ra将成为该临时变量的引用。

临时变量、引用参数和const

如果实参与引用参数不匹配,C++将生成临时变量。当前,仅当参数为const引用时,C++才允许这样做。首先,如果引用参数是const,则编译器将在下面两种情况下生成临时变量:

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

左值参数是可被引用的数据对象,例如,变量、数组元素、结构成员、引用和解除引用的指针都是左值。非左值包括字面常量(用引号括起的字符串除外,它们由其地址表示)和包含多项的表达式。在C语言中,左值最初指的是可出现在赋值语句左边的实体,但这是引入关键字const之前的情况。现在,常规变量和 const变量都可视为左值,因为可通过地址访问它们。但常规变量属于可修改的左值,而const变量属于不可修改的左值。

如果参数不是const引用,即如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现。解决方法是,禁止创建临时变量,现在的C++标准正是这样做的。因此,如果声明将引用指定为 const,C++将在必要时生成临时变量。实际上,对于形参为const引用的C++函数,如果实参不匹配,则其行为类似于按值传递,为确保原始数据不被修改,将使用临时变量来存储值。

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

尽可能使用const的原因:

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

  • 使用const可以避免无意中修改数据的编程错误;
  • 使用const使函数能够处理const和非const实参,否则将只能接受非 const数据;
  • 使用const引用使函数能够正确生成并使用临时变量。

C++11新增了另一种引用——右值引用。这种引用可指向右值,是使用&&声明的。新增右值引用的主要目的是,让库设计人员能够提供有些操作的更有效实现。以前的引用(使用&声明的引用)称为左值引用。而一些函数返回的是一些数据的拷贝时,这种拷贝是一个右值,不能被赋值。

4、将引用用于结构体

#include <iostream>
#include <string>
using namespace std;
struct free_throws
{
    string name;
    int made;
    int attempts;
    float percent;
};
void display(const free_throws &ft);
void set_pc(free_throws &ft);
free_throws &accumulate(free_throws &target, const free_throws &source);

int main()
{
    free_throws one = {"Ifelsa Branch", 13, 14};
    free_throws two = {"Andor Knott", 10, 16};
    free_throws three = {"Minnie Max", 7, 9};
    free_throws four = {"Whily Looper", 5, 9};
    free_throws five = {"Long Long", 6, 14};
    free_throws team = {"Throwgoods", 0, 0};
    free_throws dup;
    set_pc(one);
    display(one);
    accumulate(team, one);
    display(team);
    display(accumulate(team, two));
    accumulate(accumulate(team, three), four);
    display(team);
    dup = accumulate(team, five);
    std ::cout << "Displaying team;\n";
    display(team);
    std::cout << "Displaying dup after assignment: \n";
    display(dup);
    set_pc(four);
    accumulate(dup, five) = four;
    std::cout << "Displaying dup after ill-advised assignment :\n";
    display(dup);
    return 0;
}
void display(const free_throws &ft)
{
    using std::cout;
    cout << "Name: " << ft.name << '\n';
    cout << " Made; " << ft.made << '\t';
    cout << "Attempts:" << ft.attempts << '\t';
    cout << "Percent: " << ft.percent << '\n';
}
void set_pc(free_throws &ft)
{
    if (ft.attempts != 0)
        ft.percent = 100.0f * float(ft.made) / float(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;
}

(1)返回引用和传统返回机制的不同

传统返回机制与按值传递函数参数类似,计算关键字return后面的表达式,并将结果返回给调用函数。从概念上说,这个值被复制到一个临时位置,而调用程序将使用这个值。返回引用的函数实际上是被引用的变量的别名。

(2)返回用时需要注意的问题

应避免返回函数终止时不再在在的内存单元引用。同样,也应避免返回指向临时变量的指针。为避免这种问题,最简单的方法是,返回一个作为参数传递给函数的引用。作为参数的引用将指向调用函数使用的数据,因此返回的引用也将指向这些数据。另一种方法是用new来分配新的存储空间。

const free_throws & clone(free_throws & ft)

{

        free_throws * pt;
        *pt = ft;
        return *pt;

}

第一条语句创建一个无名的free_throws结构,并让指针pt指向该结构,因此*pt就是该结构。上述代码似乎会返回该结构,但函数声明表明,该函数实际上将返回这个结构的引用。这样,便可以使用该函数:
free_throws & jolly = clone(three) ;

这使得jolly成为新结构的引用。这种方法存在一个问题:在不再需要new分配的内存时,应使用 delete来释放它们。调用clone()隐藏了对new的调用,这使得以后很容易忘记使用delete来释放内存。

(3)为何将const用于引用返回类型

在赋值语句中,左边必须是可修改的左值。也就是说,在赋值表达式中,左边的子表达式必须标识一个可修改的内存块。在accumulate (dup, five) = four;这条语句中,函数返回指向dup的引用,它确实标识的是一个这样的内存块,因此这条语句是合法的。

另一方面,常规(非引用)返回类型是右值——不能通过地址访问的值。这种表达式可出现在赋值语句的右边,但不能出现在左边。其他右值包括字面值和表达式。显然,获取字面值的地址没有意义,这种返回值位于临时内存单元中,运行到下一条语句时,它们可能不再存在。

假设要使用引用返回值,但又不允许执行像给accumulate()赋值这样的操作,只需将返回类型声明为const 引用。通过省略const,可以编写更简短代码,但其含义也更模糊。通常,应避免在设计中添加模糊的特性,因为模糊特性增加了犯错的机会。

5、将引用用于类对象

将类对象传递给函数时,C++通常的做法是使用引用。例如,可以通过使用引用,让函数将类string、ostream、istream、ofstream和ifstream等类的对象作为参数。

#include <iostream>
#include <string>
using namespace std;
string version1(const string &sl, const string &s2);
const string &version2(string &sl, const string &s2); // has side effect
const string &version3(string &sl, const string &s2); // bad design
int main()
{
    string input;
    string copy;
    string result;
    cout << "Enter a string: ";
    getline(cin, input);
    copy = input;
    cout << "Your string as entered: " << input << endl;
    result = version1(input, "***");
    cout << "Your string enhanced: " << result << endl;
    cout << "Your original string. " << input << endl;

    result = version2(input, "###");
    cout << "Your string enhanced: " << result << endl;
    cout << "Your original string: " << input << endl;
    cout << "Resetting original string.\n";
    input = copy;
    result = version3(input, "@d@");
    cout << "Your string enhanced: " << result << endl;
    cout << "Your original string: " << input << endl;
    return 0;
}

string version1(const string &s1, const string &s2)
{
    string temp;
    temp = s2 + s1 + s2;
    return temp;
}
const string &version2(string &s1, const string &s2) // has side effect
{
    s1 = s2 + s1 + s2;
    return s1;
}
const string &version3(string &sl, const string &s2)
{
    string temp;
    temp = s2 + sl + s2;
    // unsafe to return reference to local variable
    return temp;
}

将C-风格字符串用作string对象引用参数

首先,string类定义了一种char*到string的转换功能,这使得可以使用C-风格字符串来初始化string对象。其次是假设实参的类型与引用参数类型不匹配,但可被转换为引用类型,程序将创建一个正确类型的临时变量,使用转换后的实参值来初始化它,然后传递一个指向该临时变量的引用。

这种属性的结果是,如果形参类型为const string&,在调用函数时,使用的实参可以是string对象或C-风格字符串,如用引号括起的字符串字面量、以空字符结尾的char数组或指向char的指针变量。

第三个版本有一个巨大的缺陷,返回一个指向 version3()中声明的变量的引用。这个函数能够通过编译,但当程序试图执行该函数时将崩溃。具体地说,问题是由result = version3 (input, "@@@");赋值语句引发的。程序试图引用已经释放的内存。

6、对象、继承和引用

ostream 和ofstream类凸现了引用的一个有趣属性。ofstream对象可以使用 ostream类的方法,这使得文件输入/输出的格式与控制台输入/输出相同。使得能够将特性从一个类传递给另一个类的语言特性被称为继承。简单地说,ostream是基类(因为ofstream是建立在它的基础之上的),而ofstream是派生类(因为它是从ostream派生而来的)。派生类继承了基类的方法,这意味着 ofstream对象可以使用基类的特性,如格式化方法precision()和 setf()。

继承的另一个特征是,基类引用可以指向派生类对象,而无需进行强制类型转换。这种特征的一个实际结果是,可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。

#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;
void file_it(ostream &os, double fo, const double fe[], int n);
const int LIMIT = 5;
int main()
{
    ofstream fout;
    const char *fn = "data.txt";
    fout.open(fn);
    if (!fout.is_open())
    {
        cout << "Can't open " << fn << ". Bye. \n";
        exit(EXIT_FAILURE);
    }
    double objective;
    cout << "Enter the focal length of your "
            "telescope objective in mm: ";
    cin >> objective;
    double eps[LIMIT];
    cout << "Enter the focal lengths, in mm, of " << LIMIT
         << " eyepieces: \n";
    for (int i = 0; i < LIMIT; i++)
    {
        cout << "Eyepiece #" << i + 1 << ": ";
        cin >> eps[i];
    }
    file_it(fout, objective, eps, LIMIT);
    file_it(cout, objective, eps, LIMIT);
    cout << "Done\n";
    return 0;
}
void file_it(ostream &os, double fo, const double fe[], int n)
{
    ios_base ::fmtflags initial;
    initial = os.setf(ios_base ::fixed); // save initial formatting state
    os.precision(0);
    os << "Focal length of objective: " << fo << " mm\n";
    os.setf(ios::showpoint);
    os.precision(1);
    os.width(12);
    os << "f.l. eyepiece";
    os.width(15);
    os << "magnification" << endl;
    for (int i = 0; i < n; i++)
    {
        os.width(12);
        os << fe[i];
        os.width(15);
        os << int(fo / fe[i] + 0.5) << endl;
    }
    os.setf(initial);
}

对于该程序,最重要的一点是,参数os(其类型为ostream &)可以指向 ostream 对象(如cout),也可以指向 ofstream 对象(如fout)。方法setf()让我们能够设置各种格式化状态。例如,方法调用setf(ios_base::fixed)将对象置于使用定点表示法的模式;setf(ios_base :: showpoint)将对象置于显示小数点的模式,即使小数部分为零。方法precision()指定显示多少位小数(假定对象处于定点模式下)。所有这些设置都将一直保持不变,直到再次调用相应的方法重新设置它们。方法width()设置下一次输出操作使用的字段宽度,这种设置只在显示下一个值时有效,然后将恢复到默认设置。默认的字段宽度为零,这意味着刚好能容纳下要显示的内容。

函数file_it()使用了两个有趣的方法调用:

ios_base :: fmtflags initial;
initial = os.setf(ios_base :: fixed); // save initial formatting state

......

os.setf(initial);

方法setf()返回调用它之前有效的所有格式化设置。ios_base::fmtflags是存储这种信息所需的数据类型名称。因此,将返回值赋给initial将存储调用file_it()之前的格式化设置,然后便可以使用变量initial作为参数来调用setf(),将所有的格式化设置恢复到原来的值。因此,该函数将对象回到传递给file_it()之前的状态。每个对象都存储了自己的格式化设置。因此,当程序将cout传递给file_it()时,cout的设置将被修改,然后被恢复;当程序将fout传递给file_it()时,fout的设置将被修改,然后被恢复。

7、何时使用引用参数

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

  • 程序员能够修改调用函数中的数据对象;
  • 通过传递引用而不是整个数据对象,可以提高程序的运行速度。

当数据对象较大时(如结构和类对象),第二个原因最重要,这些也是使用指针参数的原因,引用参数实际上是基于指针的代码的另一个接口。

对于使用传递的值而不作修改的函数:

  • 如果数据对象很小,如内置数据类型或小型结构,则按值传递。
  • 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针。
  • 如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率。这样可以节省复制结构所需的时间和空间。
  • 如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用,这是C++新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。

对于修改调用函数中数据的函数:

  • 如果数据对象是内置数据类型,则使用指针或引用。
  • 如果数据对象是数组,则只能使用指针。
  • 如果数据对象是结构,则使用引用或指针。
  • 如果数据对象是类对象,则使用引用。

三、默认参数

默认参数指的是当函数调用中省略了实参时自动使用的一个值,这极大地提高了使用函数的灵活性。必须通过函数原型来设置默认参数。由于编译器通过查看原型来了解函数所使用的参数数目,因此函数原型也必须将可能的默认参数告知程序,方法是将值赋给原型中的参数。

对于带参数列表的函数,必须从右向左添加默认值。也就是说,要为某个参数设置默认值,则必须为它右边的所有参数提供默认值。实参按从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数。默认参数并非编程方面的重大突破,而只是提供了一种便捷的方式。在设计类时将,通过使用默认参数,可以减少要定义的析构函数、方法以及方法重载的数量。

#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]; // copy characters
    while (i <= n)
        p[i++] = '\0'; // set rest of string to '\0'
    return p;
}

四、函数重载

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

函数重载的关键是函数的参数列表——也称为函数特征标。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的。C++允许定义名称相同的函数,条件是它们的特征标不同。

没有匹配的原型并不会自动停正使用其中的某个函数,因为C++将尝试使用标准类型转换强制进行匹配。而如果有多个可以进行转换的原型,则C++将拒绝这种函数调用,并将其视为错误。

一些看起来彼此不同的特征标是不能共存的。如果某个参数与多个原型可以匹配,则此编译器无法确定究竟应使用哪个原型。为避免这种混乱,编译器在检查函数特征标时,将把类型引用和类型本身视为同一个特征标。

匹配函数时,const修饰符考虑在函数签名中,区分 const和非 const变量。

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

dribble()函数有两个原型,一个用于const指针,另一个用于常规指针,编译器将根据实参是否为const来决定使用哪个原型。dabble()函数只与带非const 参数的调用匹配,而drivel()函数可以与带const或非const参数的调用匹配。drivel()和dabble()之所以在行为上有这种差别,主要是由于将非const值赋给const变量是合法的,但反之则是非法的。

是特征标,而不是函数类型使得可以对函数进行重载。返回类型可以不同,但特征标也必须不同。

1、重载引用参数

类设计和STL经常使用用参数,因此知道不同引用类型的重载很有用。

void sink(double & r1);
void sank(const double & r2); 
void sunk(double && r3) ;

左值引用参数r1与可修改的左值参数匹配,const左值引用参数r2与可修改的左值参数、const左值参数和右值参数匹配;最后,左值引用参数r3与左值匹配。注意到与r1或r3匹配的参数都与r2匹配。如果重载使用这三种参数的函数,函数将调用最匹配的版本。

2、重载示例

#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!!";  // test value
    unsigned long n = 12345678; // test value
    int i;
    char *temp;
    for (i = 1; i < 10; i++)
    {
        cout << left(n, i) << endl;
        temp = left(trip, i);
        cout << temp << endl;
        delete[] temp; // point to temporary storage
    }
    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]; // copy characters
    while (i <= n)
        p[i++] = '\0'; // set rest of string to '\0'
    return p;
}

3、何时使用函数重载

仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应采用函数重载。如果功能可以使用默认参数来实现,那么使用一个带默认参数的函数要简单些。只需编写一个函数(而不是两个函数),程序也只需为一个函数(而不是两个)请求内存;需要修改函数时,只需修改一个。然而,如果需要使用不同类型的参数,则默认参数便不管用了,在这种情况下,应该使用函数重载。

4、名称修饰

C++跟踪踪每一个重载函数的方法是给这些函数指定了秘密身份,使用C++开发工具中的编辑器编写和编译程序时,C++编译器将执行一些神奇的操作——名称修饰或名称矫正,它根据函数原型中指定的形参类型对每个函数名进行加密。对原始名称进行的表面看来无意义的修饰或矫正,将对参数数目和类型进行编码。添加的一组符号随函数特征标而异,而修饰时便用的约定随编译器而异。

五、函数模板

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

函数模板允许以任意类型的方式来定义函数。

例如,可以这样建立一个交换模板:

template <typename AnyType>
void Swap (AnyType &a, AnyType &b)

{

        AnyType  temp;
        temp = a;
        a = b;
        b = temp;

}

第一行指出,要建立一个模板,并将类型命名为AnyType。关键字template和typename是必需的,除非可以使用关键字class代替typename。 另外,必须使用尖括号。类型名可以任意选择,只要遵守C++命名规则即可。余下的代码描述了交换两个AnyType值的算法。模板并不创建任何函数,而只是告诉编译器如何定义函数。需要交换int的函数时,编译器将按模板模式创建这样的函数,并用int代替AnyType。同样,需要交换 double的函数时,编译器将按模板模式创建这样的函数,并用double 代替AnyType。在标准C++98添加关键字typename之前,C++使用关键字class来创建模板,即使用class来替换掉typename。typename关键字使得参数AnyType 表示类型这一点更为明显;然而,有大量代码库是使用关键字class开发的。在这种上下文中,这两个关键字是等价的。

如果需要多个将同一种算法用于不同类型的函数,使用模板。如果不考虑兼容的问题,并愿意键入较长的单词,则声明类型参数时,应使用关键字typename 而不使用class。

#include <iostream>
// function template prototype
template <typename T> // or class T
void Swap(T &a, T &b);
int main()
{
    using namespace std;
    int i = 10;
    int j = 20;
    cout << "i, j = " << 1 << ", " << j << "\n";
    cout << "Using compiler-generated int swapper:\n";
    Swap(i, j); // generates void Swap(int &, int &)
    cout << "Now i, j=" << i << "," << j << ".\n";
    double x = 24.5;
    double y = 81.7;
    cout << "x, y = " << x << "," << y << ".\n";
    cout << "Using compiler-generated double swapper:\n";
    Swap(x, y); // generates void Swap(double &, double &)
    cout << "Now x, y = " << x << ", " << y << ".\n";
    return 0;
}
template <typename T> // or class T
void Swap(T &a, T &b)
{
    T temp;
    temp = a;
    a = b;
    b = temp;
}

程序中的第一个Swap()函数接受两个int参数,因此编译器生成该函数的int版本。也就是说,用int替换所有的T,程序员看不到生成的函数,但编译器确实生成并在程序中使用了它们。函数模板不能缩短可执行程序。对于该程序,最终仍将由两个独立的函数定义,就像以手工方式定义了这些函数一样。最终的代码不包含任何模板,而只包含了为程序生成的实际函数。使用模板的好处是,它使生成多个函数定义更简单、更可靠。更常见的情形是,将模板放在头文件中,并在需要使用模板的文件中包含头文件。

1、重载的模板

需要多个对不同类型使用同一种算法的函数时,可使用模板。然而,并非所有的类型都使用相同的算法。为满足这种需求,可以像重载常规函数定义那样重载模板定义。和常规重载一样,被重载的模板的函数特征标必须不同。其中并非所有的模板参数都必须是模板参数类型。

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

2、模板的局限性

编写的模板函数很可能无法处理某些类型。另一方面,有时候通用化是有意义的,但C++语法不允许这样做。例如,将两个包含位置坐标的结构相加是有意义的,虽然没有为结构体定义运算符+。一种解决方案是,C++可以重载运算符+,以便能够将其用于特定的结构或类。这样使用运算符+的模板便可处理重载了运算符+的结构。另一种解决方案是,为特定类型提供具体化的模板定义。

3、显式具体化

可以提供一个具体化函数定义——称为显式具体化,其中包含所需的代码。当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。具体化机制随着C++的演变而不断变化。

(1)第三代具体化(ISO/ANSI C++标准)

试验其他具体化方法后,C++98标准选择了下面的方法:

  • 对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本。
  • 显式具体化的原型和定义应以template<>打头,并通过名称来指出类型。
  • 具体化优先于常规模板,而非模板函数优先于具体化和常规模板。

// 非模板函数
void Swap(job &, job &) ;

//模板函数

template <typename T>
void Swap(T &, T &) ;

//具体化

template <> void Swap<job>(job &, job &);

如果有多个原型,则编译器在选择原型时,非模板版本优先于显式具体化和模板版本,而显式具体化优先于使用模板生成的版本。Swap<job>中的<job>是可选的,因为函数的参数类型表明,这是job的一个具体化。因此,该原型也可以这样编写:
template <>void Swap(job &, job &) // simpler form

(2)显式具体化示例

#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-generated int swapper: \n";
    Swap(i, j); // generates void Swap(int &, int.&).
    cout << "Now i, j = " << i << " ," << j << ".\n";
    job sue = {"Susan Yaffee", 73000.60, 7};
    job sidney = {"sidney Taffee", 78060.72, 9};
    cout << "Before job swapping: \n";
    Show(sue);
    Show(sidney);
    Swap(sue, sidney); // uses void Swap(job &, job &)
    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;
}

4、实例化和具体化

在代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例。模板并非函数定义,但使用int的模板实例是函数定义。这种实例化方式被称为隐式实例化,因为编译器之所以知道需要进行定义,是由于程序调用函数时提供了int参数。

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

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

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

与显式实例化不同的是,显式具体化使用下面两个等价的声明之一:
template <>void Swap<int>(int &, int &); // explicit specialization
template <> void Swap(int &,int&) ;//explicit specialization
区别在于,这些声明的意思是“不要使用Swap()模板来生成函数定义,而应使用专门为int类型显式地定义的函数定义”。这些原型必须有自己的函数定义。显式具体化声明在关键字template后包含<>,而显式实例化没有。

试图在同一个文件(或转换单元)中使用同一种类型的显式实例和显式具体化将出错。

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

template<class T>

T add(T a, T b)

{

    return a + b;

}

......

int m = 6;
double x = 10.2;
cout << add<doubIe>(x, m) << endI;

这里的模板与函数调用 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 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);
    job n, m;
    Swap(n, m);
    char g, h;
    Swap(g, h); // use explicit template instantiation for char
}

编译器看到char的显式实例化后,将使用模板定义来生成Swap()的char版本。对于其他Swap()调用,编译器根据函数调用中实际使用的参数,生成相应的版本。

5、编译器选择使用哪个函数版本

对于函数重载、函数模板和函数模板重载,C++需要一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析。

  • 第1步:创建候选函数列表,其中包含与被调用函数的名称相同的函数和模板函数。
  • 第2步:使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。
  • 第3步:确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。

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

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和shorts 自动转换为 int,float自动转换为double)。
  3. 标准转换(例如,int转换为char,long转换为double)。
  4. 用户定义的转换,如类声明中定义的转换。

函数#1优于函数#2,因为char到int的转换是提升转换,而char到float的转换是标准转换。函数#3、函数#5和函数#6都优于函数#1和#2,因为它们都是完全匹配的。#3和#5 优于#6,因为#6函数是模板。

如果两个函数(如#3和#5)都完全匹配,将如何办?通常,有两个函数完全匹配是一种错误,但这一规则有两个例外。

(1)完全匹配和最佳匹配

进行完全匹配时,C++允许某些“无关紧要的转换”。下表列出了这些转换——Type表示任意类型。Type可以是char&这样的类型,因此这些规则包括从char&到const char &的转换。Type(argument-list)意味着用作实参的函数名与用作形参的函数指只要返回类型和参数列表相同,就是匹配的。

完全匹配允许的无关紧要转换

从实参

到形参
Type Type &
Type & Type
Type [ ] * Type
Type (argument-list) Type (*)(argument-list)
Type const Type
Type volatile Type
Type * const Type
Type * volatile Type *

如果有多个匹配的原型,则编译器将无法完成重载解析过程;如果没有最佳的可行函数,则编译器将生成一条错误消息,该消息可能会使用诸如“ambiguous(二义性)”这样的词语。

然而,有时候,即使两个函数都完全匹配,仍可完成重载解析。首先,指向非 const 数据的指针和引用优先与非const指针和引用参数匹配。然而,const和非const之间的区别只适用于指针和引用指向的数据,对于常规变量还是会报错的。

一个完全匹配优于另一个的另一种情况是,其中一个是非模板函数,而另一个不是。在这种情况下,非模板函数将优先于模板函数(包括显式具体化)。如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先。这意味着显式具体化将优于使用模板隐式生成的具体化。

术语“最具体”并不一定意味着显式具体化,而是指编译器推断使用哪种类型时执行的转换最少。

例如,请看下面两个模板:
template <class Type> void recycle (Type t); // #I
template <class Type> void recycle (Type * t); // #2
假设包含这些模板的程序也包含如下代码:
struct blot {int a; char b[10]; };
blot ink = {25, "spots"};

......

recycle (&ink); // address of a structure

recycle(&ink)调用与#1模板匹配,匹配时将Type解释为blot*。recycle(&ink)函数调用也与#2模板匹配,这次Type被解释为ink。因此将两个隐式实例——recycle<blot*>(blot*)和recycle<blot>(blot*)发送到可行函数池中。

在这两个模板函数中,recycle<blot*>(blot*)被认为是更具体的,因为在生成过程中,它需要进行的转换更少。#2模板已经显式指出,函数参数是指向Type的指针,因此可以直接用 blot标识Type;而#1模板将Type作为函数参数,因此Type必须被解释为指向blot的指针。也就是说,在#2模板中,Type已经被具体化为指针,因此说它“更具体”。

用于找出最具体的模板的规则被称为函数模板的部分排序规则。和显式实例一样,这也是C++98新增的特性。

(2)部分排序规则示例

#include <iostream>
template <typename T>
void ShowArray(T arr[], int n);
template <typename T>
void ShowArray(T *arr[], int n);
struct debts
{
    char name[50];
    double amount;
};
int main()
{
    using namespace std;
    int things[6] = {13, 31, 103, 301, 310, 130};
    struct debts mr_E[3] =
        {
            {"Ima Wolfe", 2400.0},
            {"Ura Foxe", 1300.0},
            {"Iby Stout", 1800.0}};
    double *pd[3];
    for (int i = 0; i < 3; i++)
        pd[i] = &mr_E[i].amount;
    cout << "Listing Mr. E's counts of things: \n";
    ShowArray(things, 6); // uses template A
    cout << "Listing Mr. E's debts:\n";
    ShowArray(pd, 3);
    return 0;
}
template <typename T>
void ShowArray(T arr[], int n)
{
    using namespace std;
    cout << "template A\n";
    for (int i = 0; i < n; i++)
        cout << arr[i] << ' ';
    cout << endl;
}
template <typename T>
void ShowArray(T *arr[], int n)
{
    using namespace std;
    cout << "template B\n";
    for (int i = 0; i < n; i++)
        cout << *arr[i] << ' ';
    cout << endl;
}

重载解析将寻找最匹配的函数。如果只存在一个这样的函数,则选择它;如果存在多个这样的函数,但其中只有一个是非模板函数,则选择该函数;如果存在多个适合的函数,且它们都为模板函数,但其中有一个函数比其他函数更具体,则选择该函数。如果有多个同样合适的非模板函数或模板函数,但没有一个函数比其他函数更具体,则函数调用将是不确定的,因此是错误的;当然,如果不存在匹配的函数,则也是错误。

(3)自己选择

在有些情况下,可通过编写合适的函数调用,引导编译器做出自己希望的选择。

#include <iostream>
template <class 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;
}
int main()
{
    using namespace std;
    int m = 20;
    int n = -30;
    double x = 15.5;
    double y = 25.9;
    cout << lesser(m, n) << endl;
    cout << lesser(x, y) << endl;
    cout << lesser<>(m, n) << endl;
    cout << lesser<int>(x, y) << endl;
    return 0;
}

第一个函数调用与模板函数和非模板函数都匹配,因此选择非模板函数。

第二个函数调用与模板匹配(T为double)。

第三个函数调用中lesser<>(m,n)中的<>指出,编译器应选择模板函数,而不是非模板函数;编译器注意到实参的类型为int,因此使用int替代T对模板进行实例化。

第四个函数调用要求进行显式实例化(使用int替代T),将使用显式实例化得到的函数。x和y的值将被强制转换为int,该函数返回一个int值。

(4)多个参数的函数

将有多个参数的函数调用与有多个参数的原型进行匹配时,情况将非常复杂。编译器必须考虑所有参数的匹配情况。如果找到比其他可行函数都合适的函数,则选择该函数。一个函数要比其他函数都合适,其所有参数的匹配程度都必须不比其他函数差,同时至少有一个参数的匹配程度比其他函数都高。这些规则只是为了让任何一组函数原型和模板都存在确定的结果。

6、模板函数的发展

(1)是什么类型

在C++98中,编写模板函数时,一个问题是并非总能知道应在声明中使用哪种类型。

template<class T1, class T2>
void ft (T1 x, T2 y)

{        ......

        ?type? xpy = x + y;

        ......

}

由于不知道ft()将如何使用,因此无法预知xpy是什么类型。正确的类型可能是T1、T2或其他类型,因为我们无法确定是否会发生类型提升或者运算符重载。因此,在C++98中,没有办法声明xpy的类型。

(2)关键字decltype(C++11)

C++11 新增的关键字decltype提供了解决方案。可这样使用该关键字:

int x;
decltype(x) y; // make y the same type as x

给decltype提供的参数可以是表达式。

decltype(x + y) xpy; // make xpy the same type as x + y
xpy = x + y;
另一种方法是,将这两条语句合二为一:
decltype (x +y) xpy = x + y;

decltype一般比这些示例演示的要复杂些。为确定类型,编译器必须遍历一个核对表。

decltype (expression) var ;

则核对表的简化版如下:

  1. 如果expression是一个没有用括号括起的标识符,则var的类型与该标识符的类型相同,包括const 等限定符;如double x = 5.5;decltype (x) w;
  2. 如果expression是一个函数调用,则var的类型与函数的返回类型相同;并不会实际调用函数,编译器通过查看函数的原型来获悉返回类型,而无需实际调用函数。
  3. 如果expression是一个左值,则var为指向其类型的引用。要进入第三步,expression不能是未用括号括起的标识符。
  4. 如果前面的条件都不满足,则var的类型与expression的类型相同。

注意:如果需要多次声明,可结合使用typedef和decltype。

(3)另一种函数声明语法(C++11后置返回类型)

有一个相关的问题是decltype 本身无法解决的。

template<class T1, class T2>
? type? gt(T1 x, T2 y)

{

        return x+y;

}

同样,无法预知将x和y相加得到的类型,好像可以将返回类型设置为decltype(x+y),但是,此时还未声明参数x和y,它们不在作用域内(编译器看不到它们,也无法使用它们)。必须在声明参数后使用decltype,为此,C++新增了一种声明和定义函数的语法。

double h(int x, float y);

使用新增的语法可编写成这样:
auto h(int x, float y) -> double;

这将返回类型移到了参数声明后面。->double 被称为后置返回类型。其中auto是一个占位符,表示后置返回类型提供的类型,这是C++11给auto新增的一种角色。

通过结合使用这种语法和decltype,便可给gt()指定返回类型

template<class T1, class T2>
auto gt(T1 x, T2 y) -> decltype (x+ y)

{

        return x+y;

}

现在,decltype在参数声明后面,因此x和y位于作用域内,可以使用它们。

相关推荐

  1. django

    2024-04-07 06:58:03       37 阅读
  2. 、设计模式

    2024-04-07 06:58:03       19 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-04-07 06:58:03       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-07 06:58:03       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-07 06:58:03       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-07 06:58:03       18 阅读

热门阅读

  1. JVM总结

    2024-04-07 06:58:03       14 阅读
  2. MQTT面试题

    2024-04-07 06:58:03       11 阅读
  3. leetcode热题HOT 23. 合并 K 个升序链表

    2024-04-07 06:58:03       12 阅读
  4. [高考] 数理化

    2024-04-07 06:58:03       12 阅读
  5. centos 安装 stable-diffusion 详细流程

    2024-04-07 06:58:03       11 阅读
  6. QT智能指针

    2024-04-07 06:58:03       18 阅读
  7. 【工具或平台】Gem5编译

    2024-04-07 06:58:03       13 阅读
  8. vue指令v-model

    2024-04-07 06:58:03       14 阅读