第08章 函数探幽

<c++ primer plus>第六版

8 函数探幽

8.1 c++内联函数

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

例: 一般定义内联函数时, 省略函数原型, 直接在原型处定义整个函数.
inline double square(double x) {return x*x;}

c语言中使用#define来提供宏, 可以实在原始的内联代码

#define SQUARE(X) X*X

a = SQUARE(5.0); //编译时替换为 a = 5.0*5.0, 相当于内联函数.

8.2 引用变量

引用变量是c++新增的一种复合类型.
引用是已定义变量的别名.
引用的主要用途是用途函数的形参.

int rats = 100;
int & rodents = rats;
//rodents 是 rats的别名.
//int & 可以认为是指向int的引用.
//&是类型标识运算符的一部分(不是地址运算符),
//rats和rodents的值和地址都相同.

cout << "rats    = " << rats << endl;
cout << "rodents = " << rodents << endl;
cout << "addr rats    = " << &rats << endl;
cout << "addr rodents = " << &rodents << endl;



int rats = 101;
int & rodents = rats; //引用, rodents相当于rats, &rodents相当于&rats.
int * rodents = &rats;//指针, *parts 相当于rats, prats   相当于&rats.

引用与指针区别:

  1. 引用在声明时必须初始化, 指针可以先声明再赋值. 可以认为引用接近于const指针.
int rats = 101;       //一个int型变量
int & rodents = rats; //rodents是rats的别名, 它的值是101.

int bunnies = 50;     //又一个int型变量
rodents = bunnies;    //什么效果? 1) [错]rodents变成了bunnies的引用 2) [对]rodents仍然是rats的引用, 仅仅是值变成了50, 相当于rats=bunnies.

8.2.2 将引用用作函数参数

引用用作函数参数, 使得函数中的变量名成为实际参数的别名, 这种方法称为按引用传递.

#include<iostream>
void swap_r(int &a, int &b); //a和b是引用
void swap_p(int *p, int *q); //p和q是指针
void swap_v(int  a, int  b); //a和b是新变量
int main()
{
    using namespace std;

    int wallet1 = 300;
    int wallet2 = 350;

    cout << "wallet1=" << wallet1 << ", wallet2=" << wallet2 << endl;

    //使用引用交换内容
    swap_r(wallet1, wallet2);
    cout << "wallet1=" << wallet1 << ", wallet2=" << wallet2 << endl;

    //使用指针交换内容
    swap_p(&wallet1, &wallet2);
    cout << "wallet1=" << wallet1 << ", wallet2=" << wallet2 << endl;

    //使用新变量交换内容
    swap_v(wallet1, wallet2); //交换失败
    cout << "wallet1=" << wallet1 << ", wallet2=" << wallet2 << endl;
}

void swap_r(int &a, int &b)
{
    int tmp = a;
    a = b;
    b = tmp;
}

void swap_p(int *p, int *q)
{
    int tmp = *p;
    *p = *q;
    *q = tmp;
}

void swap_v(int a, int b)
{
    int tmp = a;
    a = b;
    b = tmp;
}

8.2.3 引用的属性和特别之处

如下函数要求ra是一个引用, 所以现代c++不允许recube(x+3.0)这种调用, 因为x+3.0不是变量, 早期c++会创建一个临时就是, ra成为该临时变量的引用.

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

8.2.4 将引用用于结构

引用的主要作用是为了用于结构和类, 而不是基本的内置类型.

struct free_throws
{
    std::string name;
    int made;
    int attempts;
    float percent;
};

void set_pc(free_throws & ft); //函数参数是一个引用, 引用指向free_throws
void display(const free_throws & ft); //函数参数是一个常引用, 不允许修改数据
free_throws &accumulate(free_throws & target, const free_throws & source); //返回引用

传统返回: 计算return后面的表达式, 得到一个值复制到临时位置, 调用函数的程序使用这个临时值.
返回引用: 不需要将值复制到临时位置.

注意: 如果返回引用, 则该引用不能指向临时变量.

const free_throws & clone2(free_throws &ft)
{
    free_throws new_guy; //临时变量, 在函数终止时会释放
    new_guy = ft;
    return new_guy; //返回临时变量的引用, 会引发错误.
}

解决上述问题有两个方法:

  1. 将引用作为参数传递给函数, 然后返回它.
  2. 使用new来分配新的内存空间, 但后续要记得使用delete来释放该内存.
const free_throws & clone3(free_throws &ft)
{
    free_throws * pt; //分配新内存
    *pt = ft;
    return *pt; //返回对该内存的引用
}
free_throws & jolly = clone3(three); //jolly变成对函数中新分配内存的引用.

8.2.5 将引用用于类对象

给函数传递类对象时, c++通常做法是使用引用.

8.2.6 对象, 继承, 引用

继承的一个特性是: 基类引用可以指向派生类对象, 无需进行强制类型转换.
实际结果: 定义一个函数, 接受基类引用作为参数, 调用函数时可以传给它基类对象, 也可以传给它派生类对象.

#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 = "ep_data.txt";

    fout.open(fn);
    if (!fout.is_open())
    {
        cout << "Can't open " << fn << ". Bye." << endl;
        exit(EXIT_FAILURE);
    }

    double objective = 1800;
    double eps[LIMIT] = {30, 19, 14, 8.8, 7.5};

    file_it(fout, objective, eps, LIMIT); //第一个参数是ostream的引用, 可以传给它ofstream的引用
    file_it(cout, objective, eps, LIMIT);
}

void file_it(ostream &os, double fo, const double fe[], int n)
{
    ios_base::fmtflags initial;
    initial = os.setf(ios_base::fixed); //存好初始格式, 定点表示法模式.

    os.precision(0);
    os << "Focal length of objective: " << fo << "mm" << endl;

    os.setf(ios::showpoint); //显示小数点模式, 当前小数部分为0.
    os.precision(1); //显示多少位小数.
    os.width(12);    //下一次输出操作使用的字段宽度(然后恢复默认).
    os << "f.1. 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);
}

8.2.7 何时使用引用参数

使用引用参数的主要原因:

  1. 在函数中能够修改数据对象.
  2. 传递引用省去了复制数据的时间, 可以提高程序的运行速度.

指导原则: 只传递值而不做修改的函数中:

  1. 如果数据对象很小(内置数据类型或小型结构), 则按值传递.
  2. 如果数据对象是数组, 则使用指针传递, 并将指针声明为const指针.
  3. 如果数据对象是较大的结构, 则使用const指针或const引用, 节省复制结构所需时间和空间.
  4. 如果数据对象是类对象, 则使用const引用. 类设计的语义常常要求使用引用, 因此传递类对象参数的标准方式是按引用传递.

指导原则: 在函数中修改数据的函数中:

  1. 如果数据对象是内置数据类型, 则使用指针.
  2. 如果数据对象是数组, 则只能使用指针.
  3. 如果数据对象是结构, 则使用指针或引用.
  4. 如果数据对象是类对象, 则使用引用.

8.3 默认参数

char * left(const char * str, int n=1); //原型中, 指定参数n的默认值为1
char * left(const char * str, int n)    //函数定义中, n不指定默认值
{
    ...
}

8.4 函数重载(函数多态)

c++通过函数参数列表的不同来确定要使用的重载函数版本.
函数的参数列表也称为函数特征标(function signature).
函数的参数个数和参数类型可以作为特征标.
函数的返回值类型不能做为特征标.

double cube(double x)和 double cube(double &x)不能共存, 因为引用和类型本身视为同一特征标.
匹配函数时, 非const可以转为const, 反过来不行, 所以把一个const变量传给函数的非const参数会报错.

#include<iostream>

using namespace std;

void f0(char * bits);
void f0(const char * bits);

void f1(char * bits);
void f2(const char * bits);

int main()
{
    char c0[20] = "abcdefg";
    const char c1[20] = "hijklmn";

    f0(c0); //非const变量, 调用重载的非const参数函数.
    f0(c1); //const变量, 调用重载的const参数函数

    f1(c0);
    //f1(c1); //const变量不能传给非const参数, 报错: invalid conversion from "const char*" to "char*"

    f2(c0); //非const变量可以传给const参数
    f2(c1);

    return 0;
}

void f0(      char * bits) { cout << "calling f0-no-const" << endl; }
void f0(const char * bits) { cout << "calling f0-const" << endl; }
void f1(      char * bits) { cout << "calling f1-no-const" << endl; }
void f2(const char * bits) { cout << "calling f2-const" << endl; }

8.5 函数模板

函数模板是通用的函数描述, 也就是说他们使用泛型来定义函数, 其中的泛型可以用具体的类型替换.
由于类型是用参数表示的, 因此模板特性有时也称为参数化类型.
模板不能缩短可执行程序, 它只是使函数定义更简单可靠.

#include<iostream>

//新建一个模板, 将类型命名为T(可以任意选择), 其中template和typename是关键字.
template <typename T> //注意这一行没有分号, 这一行与下一行是一句, 不能分开.
void Swap(T &a, T &b)
{
    T temp;
    temp = a;
    a = b;
    b = temp;
}

int main()
{
    using namespace std;

    int i=10, j=20;
    Swap(i, j); //传给模板int类型, 编译器自动生成void Swap(int &, int &)
    cout << "i=" << i << ", j=" << j << endl;

    double x=24.5, y=81.7;
    Swap(x, y); //传给模板double类型, 编译器自动生成void Swap(double &, double &)
    cout << "x=" << x << ", y=" << y << endl;

    return 0;
}

模板重载.
如果需要对多个不同类型使用相同算法的函数, 则使用模板.
如果需要对多个不同类型使用不同算法的函数, 则使用模板重载.

#include<iostream>

//函数模板声明
template <typename T> void Swap(T &a, T &b);
template <typename T> void Swap(T * a, T * b, int n);

int main()
{
    using namespace std;

    int i=9, j=70;
    Swap(i, j);
    cout << "i=" << i << ", j=" << j << endl;

    double x[] = {1.2, 2.3, 3.4, 4.5, 5.6};
    double y[] = {9.8, 8.7, 7.6, 6.5, 5.4};
    Swap(x, y, 5);
    for (int i=0; i<5; i++)
    {
        cout << x[i] << ", " << y[i] << endl;
    }
}

//函数模板定义
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[n];
    for (int i=0; i<n; i++)
    {
        temp[i] = a[i];
        a[i] = b[i];
        b[i] = temp[i];
    }
}

模板局限性: 模板函数很可能无法处理某些类型. 一种解决方案, 使用重载运算符, 另一种解决方案, 为特定类型提供具体化的模板定义.

显式具体化

#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); //显式具体化, 交换job, 表示这是对交换job的一个具体工作方式.

void show_job(const job &j1);

int main()
{
    using namespace std;

    int i=8, j=72;
    Swap(i, j);
    cout << "i=" << i << ", j=" << j << endl;

    job jx = {"namex", 100.1, 5};
    job jy = {"namey", 200.1, 6};
    Swap(jx, jy);

    show_job(jx);
    show_job(jy);
}

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 tmp_salary;
    tmp_salary = j1.salary;
    j1.salary = j2.salary;
    j2.salary = tmp_salary;

    int tmp_floor;
    tmp_floor = j1.floor;
    j1.floor = j2.floor;
    j2.floor = tmp_floor;
}

void show_job(const job &j1)
{
    using namespace std;
    cout << "name  = " << j1.name << endl;
    cout << "salary= " << j1.salary << endl;
    cout << "floor = " << j1.floor << endl;
}

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

posted @ 2022-07-10 11:09  编程驴子  阅读(12)  评论(0编辑  收藏  举报