C++函数探幽

1. 函数与结构

(1)与数组名就是数组第一个元素的地址不同的是,结构名只是结构的名称,要获得结构的地址,必须使用地址运算符&。

2. 函数指针

(1)函数的地址是存储其机器语言代码的内存的开始地址。

(2)获取函数的地址:使用函数名即可。要将函数作为参数进行传递,必须传递函数名。

(3)声明函数指针:声明指向某种数据类型的指针时,必须指定指针指向的类型。声明应指定函数的返回类型以及函数的特征标(参数列表)。

double pam(int);

double (*pf)(int);   //(*pf)是函数,pf是函数指针(注意:[]优先级比*高)

pf = pam;  //将函数地址赋值给函数指针

(4)使用指针来调用函数:

double pam(int);
double (*pf)(int);
pf = pam;

double x = pam(4);
double y = (*pf)(5);

double y = pf(5):       //注意C++也允许这样使用函数指针(概念上一点点冲突但是可用)

(5)函数指针示例

// fun_ptr.cpp

#include <iostream>

double betsy(int);
double pam(int);

void estimate(int lines, double (*pf)(int));

int main()
{
    using namespace std;
    int code;
       
    cout << "How many lines of code do you need?";
    cin >> code;
    cout << "Here's Betsy's estimate:\n";
    estimate(code, betsy);
    cout << "Here's Pam's estimate:\n";
    estimate(code, pam):
    return 0;
}

double betsy(int lns)
{
    return 0.05*lns;
}

double pam(int lns)
{
    return 0.03*lns + 0.0004*lns*lns;
}

void estimate(int lines, double (*pf)(int))
{
    using namespace std;
    cout << lines << " lines will take ";
    cout << (*pf)(lines) << " hour(s)\n";
}

(6)函数指针数组

const double* (*pf)(const double*, int);

const double* (*pa[3])(const double*, int) = {f1, f2, f3};  //包含三个函数指针的数组

auto pb = pa; //由于数组名是指向第一个元素的指针,因此pa和pb都是指向函数指针的指针

const double* px = pa[0](av, 3);
const double* py = (*pb[1])(av, 3);

double x = *pa[0](av, 3);
double y = *(*pb[1])(av, 3);

指向函数指针数组的指针并不少见。

(7)使用typedef进行简化

typedef const double* (*p_fun)(const double*, int);

p_fun p1 = f1;

//使用别名简化代码:
p_fun pa[3] = {f1, f2, f3};
p_fun (*pd)[3] = &pa;

注意,学会使用typedef,可减少输入量,也可以在编写代码时不容易犯错,让程序更容易理解。熟练使用typedef。

3. 引用变量

(1)引用看上去很像伪装的指针,实际上他们有很多不同之处,除了表示法不用外,还有:

必须在声明引用时将其初始化,而不能像指针那样,先声明再赋值 (引用更接近于const指针,必须在创建时进行初始化,一旦与某个变量关联起来,就一直效忠于它);

引用经常被用作函数参数,这种传递参数的方法称为按引用传递。按引用传递允许被调用的函数能够访问调用函数中的变量。(注意,由于定义引用变量时需要对其初始化,因此函数调用使用实参初始化形参,因此函数的引用参数被初始化为函数调用传递的实参。

(2)如果不想修改传递给函数的信息,又想使用引用,则应使用常量引用。不过一般这样的建议直接用按值传递。

double refcube(const double& ra);

(3)函数调用的时候,C++什么时候创建临时变量?

如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现。解决方法是,禁止创建临时变量,现在的C++标准正是这样做的。如果看到了编译器提示的有关临时变量的警告,记得不要忽略。

现在,对于形参为const引用的C++函数,如果实参不匹配,C++将创建临时变量来存储值。

应尽可能将引用形参声明为const。

(4)左值引用和右值引用

C++11新增右值引用(使用&&声明),让库设计人员能够提供有些操作的更有效实现。平常用不到就别随便用。以前的引用(使用&声明的引用)现在被称为左值引用。

(5)将const用于引用返回类型

如果要使用引用返回值,又不希望执行类似于accumulate(dup, five) = four;这样的操作,只需将返回类型声明为const引用:

const free_throws& accumulate(free_throws& target, const free_throws& source);

通过省略const,其含义更模糊。因此写代码时注意,如无需修改,就将其设为返回const引用。

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

string version1(const string& s1, const string& s2)
{
    string temp;
       
    temp = s2 + s1 + s2;
    return temp;
}

//调用version1:
string input;
string result = version1(input, "***");

        形参为const string&,实参"***"类型为const char*,程序怎么能够接受将char指针赋给string引用呢?

        首先,string类定义了一种char*和string的转换功能,这使得可以使用C-风格字符串来初始化string对象。

        其次,如果形参为const引用,假设实参的类型与引用参数类型不匹配,但可被转换为引用类型,程序将创建一个正确类型的临时变量,使用转换后的实参值来初始化它,然后传递一个指向该临时变量的引用。同样,也可以将实参char*或const char*传递给形参const string&。

4. 函数模板

函数模板使用泛型来定义函数。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于类型是用参数表示的,因此模板特性有时也被称为参数化类型(parameterized types)。

函数模板允许以任意类型的方式来定义函数。模板并不创建函数,只是告诉编译器如何定义函数。

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

如果需要多个将同一种算法用于不同类型的函数,记得使用模板。

//函数模板练习1 -- funtemp.cpp - using a function template
#include <iostream>
//函数原型
template <typename T>
void Swap(T& a, T& b);

int main()
{
    using namespace std;
    int i = 10;
    int 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";

    double x = 24.5;
    double y = 81.7;
    cout << "x, y = " << x << ", " << y << ".\n";
    cout << "Using compiler-generated int swapper:\n";
    Swap(x, y);
    cout << "Now x, y = " << x << "," << y << ".\n";
    // cin.get();
    return 0;
}

// 函数模板定义
template<typename T>
void Swap(T& a, T& b)
{
    T temp;
    temp = a;
    a = b;
    b = temp;
}

注意,函数模板不能缩短可执行程序。最终的代码不包含任何模板,而只包含了为程序生成的实际函数,使用模板的好处是,它使生成多个函数定义更简单、更可靠。一般,将模板定义放在头文件中,并在需要使用模板的文件中包含头文件。

知识点一:重载的模板

并非所有的类型都使用相同的算法,因此可以像重载常规函数定义那样重载模板定义。被重载的模板的函数特征标必须不同。

//函数模板练习2 -- twotemps.cpp - using overloaded template functions
#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()
{
    // ...
}

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

知识点二:模板的局限性

编写的模板函数很可能无法处理某些类型。解决方案:C++允许重载运算符,以便能够将其用于特定的结构或类;或者为特定类型提供具体化的模板定义。

知识点三:显式具体化(explicit specialization)

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

C++标准定义的形式(ISO/ANSI C++标准):

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

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

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

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

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

//模板原型
template <typename T>
void Swap(T&, T&);

//显式具体化
template <> void Swap<job>(job&, job&);
//注意其中的<job>可选,所以显式具体化也可以写为:
template <> void Swap(job&, job&);

知识点四:实例化和具体化

什么是实例:编译器使用模板为特定类型生成函数定义时,得到的是模板实例(instantiation)。编译器根据模板自动生成实例为隐式实例化(implicit instantiation)。C++允许显式实例化(explicit instantiation)。

//显式实例化语法:声明所需的种类——用<>符号指示类型,并在声明前加上关键字template
template void Swap<int>(int, int);

//下述为显式具体化:
template <> void Swap<int>(int&, int&);
template <> void Swap(int&, int&);

显式实例化的声明意思是“使用Swap()模板生成int类型的函数定义”。

显式具体化的声明意思是“不要使用Swap()模板来生成函数定义,而应使用专门为int类型显式地定义的函数定义”。这些原型必须有自己的函数定义。

注意:试图在同一个文件中使用同一种类型的显式实例和显式具体化将出错。

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

template <typename T>
T Add(T a, T b)
{
    return a+b;
}

//...

int m = 6;
double x = 10.2;
cout << Add<double>(x, m) << endl;    //显式实例化

知识点五:编译器选择使用哪个函数版本

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

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

第2步:使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。

第3步:确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。

编译器为使函数调用参数与可行的候选函数的参数匹配所需要进行的转换,从最佳到最差的顺序:

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

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

标准转换(例如,int转换为char, long转换为double)。

用户定义的转换,如类声明中定义的转换。

完全匹配和最佳匹配:

进行完全匹配时,C++允许某些“无关紧要的转换”。

完全匹配允许的无关紧要转换
从实参到形参
TypeType&
Type&

Type

Type[]*Type
Type(argument-list)Type(*)(argument-list)
Typeconst Type
Typevolatile Type
Type*const Type*
Type*volatile Type*

通常,有两个函数完全匹配是一种错误,会导致二义性(ambiguous),但这一规则有两个例外:

指向非const数据的指针和引用优先与非const指针和引用参数匹配。注意const和非const之间的区别只适用于指针和引用指向的数据。

如果其中一个是模板函数,则非模板函数将优先于模板函数(包括显式具体化)。如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先。用于找出最具体的模板的规则被称为函数模板的部分排序规则(partial ordering rules)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值