重学C++笔记之(六)函数探幽

1. 内联函数

1.1 基础知识

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

  • 常规函数:使用跳转地址的形式进行函数访问,函数访问结束后返回。
  • 内联函数:它编译代码与其他程序内联起来。也就是说编译器将使用相应的函数代码替换函数调用,从而无需跳转。

内联函数比常规函数稍快,但是代价是需要占用更多的内存。

内联函数的使用必须采取下列措施之一

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

下面简单举例进行说明,可以看出它与常规函数的使用没什么区别。

#include<iostream>
using namespace std;

inline double square(double x)
{
    
    return x * x;}

int main()
{
    
    
    double a,b;
    double c = 13.0;
    a = square(5.0);
    b = square(4.5 + 7.5);
    cout<<"a: "<<a<<endl;
    cout<<"b: "<<b<<endl;
    return 0;
}

输出结果:
在这里插入图片描述

1.2 内联函数与宏

inline工具是C++新增的特性。C语言使用预处理器语句#define来提供宏,它属于内联函数的原始实现。例如:

#define SQUARE(X) X*X

我们知道宏定义是通过本文替换来实现的,所以上面的X表示“参数”。

2. 引用变量

引用是已定义变量的别名。引用变量的主要用途是做函数的形参。通过将引用变量用作参数,函数将使用原始数据,而不是其副本。

2.1 引用变量

引用的使用:

int rats;
int &rodents = rats;

rodents将作为rats的别名。这里&不是地址运算符,而是类型标识符。就像char*一样,是指向char的指针一样。引用必须在定义的时候就将它初始化。 引用更接近于const,必须在创建的时候初始化,一旦与某个变量关联起来,就一致效忠于它。也就是说:

int &rodents = rats;

实际上面的代码块是下述代码的伪装表示:

int * const pr = &rats;

2.2 将引用用作函数参数

引用经常用作函数参数,使得函数中的变量名称为调用程序中的变量的别名。这种传递参数的方法称为按引用传递。我们知道C语言只能进行值传递,它属于一种拷贝。

下面使用引用交换两个变量:

#include<iostream>
using namespace std;

void swap(int& a, int& b);
int main()
{
    
    
    int wallet1 = 300;
    int wallet2 = 350;
    cout<<"wallet1: "<<wallet1<<endl;
    cout<<"wallet2: "<<wallet2<<endl;
    swap(wallet1,wallet2);
    cout<<"After using references to swap"<<endl;
    cout<<"wallet1: "<<wallet1<<endl;
    cout<<"wallet2: "<<wallet2<<endl;
    return 0;
}
void swap(int& a, int& b)
{
    
    
    int temp;
    temp = a;
    a = b;
    b = temp;
}

输出:
在这里插入图片描述
如果想要使用引用,但是又不想在调用的函数中修改变量的值,那么可以在形参的前面加const。

double refcube(const double &ra);//ra将不会被改变。

如果传递的参数比较简单,使用按值传递就可以了,但是如果传递的数据比较大(如结构和类)时,引用参数将很有用,因为引用是一种别名,而不是拷贝。

2.3 将引用用于结构

引用非常适合用于结构和类,而且引用的引入主要是为了用于这些类型的。

参考代码:

#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 Brance", 13, 14};
    free_throws two = {
    
    "Andor Knott", 10, 16};
    free_throws team = {
    
    "Throwgoods", 0, 0};

    set_pc(one);
    display(one);
    accumulate(team, one);
    display(accumulate(team, two));
    accumulate(accumulate(team, one), two);
    return 0;
}
void display(const free_throws& ft)
{
    
    
    cout<<"Name: "<<ft.name<<endl;
    cout<<"Made: "<<ft.made<<endl;
    cout<<"Attempts; "<<ft.attempts<<endl;
    cout<<"Percent: "<<ft.percent<<endl;
}
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;
}

输出:
在这里插入图片描述
上面的程序中,accumulate返回的是引用,它返回的是target的对象。如果不是引用则返回拷贝,意味着需要重新拷贝一个临时位置,然后在拷贝到需要的位置去。

2.4 将引用用于类对象

这里以string对象举例:

#include<iostream>
#include<string>
using namespace std;
string version1(const string& s1,const string& s2);
const string& version2(string& s1, const string& s2);
const string& version3(string& s1, const string& s3);

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<<"result: "<<result<<endl;
    cout<<"Original string: "<<input<<endl;

    result = version2(input,"###");
    cout<<"result: "<<result<<endl;
    cout<<"Original: "<<input<<endl;

    result = version3(input,"@@@");
    cout<<"result: "<<result<<endl;
    cout<<"Original: "<<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)
{
    
    
    s1 = s2 + s1 + s2;//s1是引用传递,它的值会被改变
    return s1;
}
const string& version3(string& s1,const string& s2)
{
    
    
    string temp;
    temp = s2 + s1 + s2;
    return temp;//temp是临时变量,如果返回它的引用不可行!!!
    //程序崩溃
}

version3不会被被正常执行,因为它返回的是一个临时变量,临时变量在函数结束后将被销毁,而函数的返回值类型为引用。所以它的输出为:
在这里插入图片描述

2.5 对象、继承和引用

ostream是基类,ofstream是派生类,这意味着ofstream对象可以使用基类的特性,如格式化方法percision()和setf()。
基类的另一个特性,基类引用可以指向派生类,而无需进行强制类型转换。例如,参数类型ostream&的函数可以接受ostream对象或您声明的ofstream对象作为参数。

参考代码等复习文件的时候补上!

2.6 何时使用引用参数

引用参数有两个主要原因

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

这些同样是使用指针参数的原因。因为引用参数实际上是基于指针的代码的另一个接口。

关于何时使用引用,下面有一些指导原则。
对于使用传递的值不作修改的函数:

  • 如果数据对象很小,用值传递;
  • 如果数据类型是数组,则用指针,且将指针申明为const指针。
  • 如果数据对象较大,则使用const指针或const引用;来提高效率。
  • 如果数据对象是类对象,则使用const引用。

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

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

3. 默认参数

默认参数必须通过函数原型告知程序。因为编译器通过查看原型来了解函数所使用的的参数数目,而和函数定义无关。比如:

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

对于形参列表的函数原型,必须从右向左添加默认值。这样函数调用的时候就方便设置值了!

int harpo(int n,int m= = 4,int j = 5);//有效
int chico(int n, int m = 6, int j);//无效
int groucho(int k = 1, int m = 2, int n = 3);//有效

对于harpo函数,我们可以提供1个、2个或3个参数,实参从左至右赋值给形参:

beeps = harpo(2);//harpo(2, 4, 5);
beeps = harpo(1, 8);//harpo(1, 8, 5);
beeps = harpo(8, 7, 6);//没有默认参数

程序举例:

#include<iostream>
using namespace std;

const int ArSize = 80;
char* left(const char* str,int n = 1);
int main()
{
    
    
    char sample[ArSize];
    cout<<"Enter a string:\n";
    cin.get(sample, ArSize);
    char *ps = left(sample, 4);//只要前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 = 0;
    for(i = 0;i < n&&str[i];i++)//str[i]防止n输入超出范围
        p[i] = str[i];
    while(i <= n)
        p[i++] = '\0';
    return p;
}

4. 函数重载(多态)

4.1 概念

函数多态是C++在C语言的基础上新增的函数功能,它可以使程序使用多个同名的函数。函数重载的关键是函数的参数列表——也称为函数特征标。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名无关紧要。C++允许定义名称相同的函数,条件是它们的特征标不同。

下列两个函数不能进行重载,因为对于编译器来说,无法判断使用哪个。

double cube(double x);
double cube(double &x);//这两个不能进行函数重载

下列函数也不能进行重载,返回值不同,不能作为重载的标志。

long gronk(int n, float m);
double gronk(int n, float m);//编译器无法判断调用哪个

如果都是引用类型,它属于函数重载,根据函数调用的匹配程度来决定调用哪个。

#include<iostream>
using namespace std;

void stove(double& r1);
void stove(const double & r2);
void stove(double && r3);
int main()
{
    
    
    double x = 5.5;
    const double y = 32.0;
    stove(x);//r1
    stove(y);//r2
    stove(x + y);//r3
}

void stove(double &r1)
{
    
    
    cout<<r1<<endl;
}
void stove(const double &r2)
{
    
    
    cout<<r2<<endl;
}
void stove(double &&r3)
{
    
    
    cout<<r3<<endl;
}

输出:
在这里插入图片描述

4.2 一个示例

#include<iostream>
using namespace std;

unsigned long left(unsigned long num, unsigned ct);
char* left(const char* str,int n = 1);
int main()
{
    
    
    char* trip = "Hawaii!!!";
    unsigned long n = 12345678;
    int i;
    char * temp;
    for(i = 1; i < 10; i++)
    {
    
    
        cout<<left(n,i)<<endl;
        temp = left(trip, i);
        cout<<temp<<endl;
        delete [] temp;
    }
    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;
    }
    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];
    while(i <=n)p[i++]='\0';
    return p;
}

输出:
在这里插入图片描述

5. 函数模板

5.1 定义

函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中泛型可用具体的类型(int或double)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。有时候也称通用编程,模板特性有时候也称为参数化类型。

比如一个交换函数的函数模板:

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

这里的T可以换成任意变量形式,它只是一个符号形式。
关键字typename也可换成class,它表示类型。

下面看看它的具体使用:

#include<iostream>
using namespace std;

template <typename T> //or class T
void Swap(T &a, T &b);

int main()
{
    
    
    int i = 10,j = 20;
    cout<<"i, j = "<<i<<","<<j<<endl;
    cout<<"After Swap(i,j):\n";
    Swap(i,j);//这里为int版本
    cout<<"i, j = "<<i<<","<<j<<endl;
    return 0;
}
template<typename T>
void Swap(T &a, T &b)
{
    
    
    T temp;
    temp = a;
    a = b;
    b = temp;
}

输出:
在这里插入图片描述
使用模板并不能缩短可执行程序。调用多少个类型,就会生成多少个函数,它只是使得生成多个函数的定义更简单、更可靠。

5.2 重载模板

并不是所有类型都是用相同模板的算法,所以我们可以对模板进行重载,以满足不同的需求。下列代码有两个交换模板,一个可以交换常规变量,一个用于交换数组类型。

#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 Lim = 8;
int main()
{
    
    
    int i = 10,j = 20;
    cout<<"i, j = "<<i<<","<<j<<endl;
    cout<<"After Swap(i,j):\n";
    Swap(i,j);//这里为int版本
    cout<<"i, j = "<<i<<","<<j<<endl;

    int d1[Lim] = {
    
    0, 7, 0, 4, 1, 7, 7, 6};
    int d2[Lim] = {
    
    1, 2, 3, 4, 5, 6, 7, 8};
    cout<<"Original arrray:\n";
    Show(d1);
    Show(d2);
    Swap(d1, d2, Lim);//数组类型
    cout<<"After Swap array:\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[])
{
    
    
    for(int i = 0; i < Lim; i++)
        cout<<a[i]<<" ";
    cout<<endl;
}

输出:
在这里插入图片描述

5.3 显示具体化和显示实例化

加入显示具体化之后,函数就有三种类型:非模板函数、模板函数和具体化的原型,他们被调用的优先级为:

非模板函数 > 显示具体化模板函数 > 模板函数

下列分别非三种类型的原型,可以不指明变量名。

void Swap(job &,job &);//非模板函数,不同函数
template <typename T>//模板函数
void Swap(T &, T &);
template<> void Swap<job>(job &,job &);//显示具体化模板函数
//其中具体化为job类型
template<> void Swap(job &, job &);//显示具体化模板函数,第二种方法

注意与显示实例化的区别,显示实例化指在程序执行过程,显示的表明生成的函数类型。如下面的Add函数,需要手动确定调用哪一种类型。

template <class T>
T Add(T a, T b)
{
    
    return a + b;}
int m = 6;
double x = 10.2;
Add<double>(x, m);//m为int型,需要显示实例化,这里利用了强制转换。

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

上一例中的Add之所以可以运行,是因为可以进行强制类型转换,但是如果使用引用类型,则行不通(没有进行声明)。为了区分显示实例化,使用显示具体化的时候需要进行声明(手动确定使用哪一种类型),它的声明方式和调用方式如下:

template void Swap<char>(char&, char&);//显示实例化声明,将使用char类型
char a,b;
Swap(a, b);//显示实例化调用

函数重载、函数模板和函数模板重载的出现,导致出现很多种类型的函数,这就涉及到函数的调用选择,设计的内容太多,感兴趣可以查看书中的8.5.5章节。

5.4 decltype结合auto的使用

C++11新增关键字decltype,可以指明变量类型:

int x;
decltype (x) y;//y的类型和x类型相同,也就是int类型

对于函数模板声明,如果无法知道函数的返回类型,可以使用auto进行占位,具体调用的时候再确定返回类型。

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

总览目录
上一篇:(五)函数的使用
下一篇:(七)内存模型和名称空间


文章参考:《C++ Primer Plus第六版》

猜你喜欢

转载自blog.csdn.net/QLeelq/article/details/112392664