C++ Primer Plus第八章 函数探幽

目录

内联函数

目录

一、内联函数

1.1什么是内联函数?

1.2怎么使用内联函数?

1.3 内联函数、普通函数和预处理宏的区别与联系

二、引用变量

2.1什么是引用变量        

2.2 引用的属性和特别之处

2.3将引用用于结构 

2.4何时使用引用?

三、函数重载

3.1 默认参数

 3.2 函数多态

3.3何时使用函数重载

四、函数模板

4.1 函数模板

4.2 模板实例化和具体化

一、内联函数

        欢迎来到第八章!!!手动鼓掌👏 👏 👏 话不多说,继续学习。

1.1什么是内联函数?

首先,书上是这么讲的:

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

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

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

        总之简单地说:内联函数是一种采用展开函数体的方式减少函数调用开销的技术。可以理解为将其它函数调用的函数体直接嵌入到调用处,省去了调用时的参数传递、挂起和返回的开销,从而提高了程序的执行效率。在C++中,可以使用inline关键字来声明内联函数。(注:内联函数不能递归

1.2怎么使用内联函数?

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

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

        通常的做法是省略原型,将整个定义(即函数头和所有函数代码)放在本应提供原型的地方。下面给段原书代码示范案例:

// inline.cpp -- using an inline function
#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";
    // cin.get();
    return 0;  
}

        注:输出表明,内联函数和常规函数一样,也是按值来传递参数的。但是宏不能按值传递,即使使用新的定义也不行。

1.3 内联函数、普通函数和预处理宏的区别与联系

内联函数与普通函数区别: 

1)内联函数的函数体要比普通函数的函数体
        这是因为内联函数会在调用处展开,如果函数体过长则展开后的代码会很长,反而增加了代码量,降低了可维护性。

2)内联函数会在编译时进行替换,而普通函数则需要在运行时进行调用
        因此,内联函数比普通函数的执行速度要快。但是,如果内联函数被频繁调用,可能会对代码的体积造成一定影响,因为每次调用都会生成相应的代码。

3)内联函数不允许递归调用
        这是因为内联函数的展开式在编译的时候就确定了,而递归调用需要在程序运行时进行,两者无法兼容。

二、引用变量

2.1什么是引用变量        

书上原话是这么说的:

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

int rats;
int & rodents = rats;  //创建引用

        请务必注意,这里的&不是地址运算符,而是类型标识符的一部分。就像声明中的char*指的是指向char的指针一样,int &指的是指向int的引用。上述引用声明允许将rats和rodents互换——它们指向相同的值和内存单元。

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

int rat;
int &rodent;
rodent = rat;//错误用法

2.2 引用的属性和特别之处

临时变量、引用参数和const:
        如果实参与引用参数不匹配,C++将生成临时变量。当前,仅当参数为const引用时,C++才允许这样做,但以前不是这样。
生成临时变量的情况:

  •  实参的类型正确,但不是左值;
  • 实参的类型不正确,但可以转换为正确的类型。
  • 左值参数是可以被引用的数据对象,如变量、数组元素、结构成员、引用和解除引用的指针。
  • 非左值包括字面常量(用引用括起的字符串除外,它们由其地址表示)和包含多项的表达式。
  • 常规变量属于可修改的左值,而const变量属于不可修改的左值

应尽可能使用const
        将引用参数声明为常量数据的引用的理由有三个:

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

2.3将引用用于结构 

        引用非常适合用于结构和类(C++的用户定义类型),使用结构引用参数的方式与使用基本变量引用相同,只需在声明结构参数时引用运算符&即可。 

引用与传统返回机制的不同
        传统返回机制与按值传递参数类似:计算关键字return后面的表达式,并将结果返回给调用函数。从概念上说,这个值被复制到一个临时位置,而调用程序将使用这个值。
        **注意:**返回引用的函数实际上是被引用的变量的别名。
返回引用时需要注意的问题:
        返回引用时最重要的一点是,应避免返回函数终止时不再存在内存单元引用。避免编写下面这样的代码

为了避面这种问题,以下两种方法:

  • 返回一个作为参数传递给函数的引用。作为参数的引用将指向调用函数使用的数据,因此返回的引用也将指向这些数据。
  • 用new来分配新的存储空间。(这种方法存在一个问题:在不再需要new分配的内存时,应使用delete来释放它们。调用clone()隐藏了对new的调用,这使得以后很容易忘记使用delete来释放内存。

为何将const用于引用返回类型

        要使用引用返回值,但又不允许执行像给accumulate()赋值这样的操作,需要将返回类型声明为const引用。

2.4何时使用引用?

这里就用书上原话来回答吧,使用引用参数的主要原因有两个:

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

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

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

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

  • 如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码(其中x是int),则很明显,该函数将修改x。
  • 如果数据对象是数组,则只能使用指针。
  • 如果数据对象是结构,则使用引用或指针。
  • 如果数据对象是类结构,则使用引用。

2.5 对象,引用,继承

  • 对象:在C++中,对象是类的实例。类是定义对象属性和行为的蓝图,而对象则是这个蓝图的具体实现。你可以想象对象就像工厂里生产出来的汽车,而类就是汽车的设计图纸。
  • 继承:继承是一种创建新类的方式,它允许新类(子类)继承另一个类(父类)的属性和方法。这就像孩子从父母那里继承特征一样。
  • 引用:引用是另一种类型的变量,它实际上是对另一个变量的别名。你可以把它想象成指向某个变量的指针,但使用起来更像变量本身。

三、函数重载

3.1 默认参数

        在C++中,你可以为函数的参数指定一个默认值。如果调用函数时没有提供这个参数的值,那么编译器会自动使用这个默认值。简单点讲,就是假如你有5个参数的函数需要反复调用,且其中3个参数反复调用都是同样的值,而你不想全部自己手动输上去,那么就可以采用默认值去简便。

//定义默认参数:在函数声明时,你可以直接在参数列表中为参数指定默认值
void myFunction(int a, int b = 10) {
    // 函数体
}



//使用默认参数:当你调用这个函数时,如果只提供了一个参数,那么b 将自动使用默认值。
myFunction(5); // a = 5, b = 10
myFunction(5, 20); // a = 5, b = 20

        注:在默认参数赋值的时候,参数必须从右向左开始默认赋值,否则将会报错。

 3.2 函数多态

        函数多态(函数重载)让您能够使用多个同名的函数。直观上讲,“多态”指的是有多种形式,因此函数多态 允许函数可以有多种形式。因而可以通过函数重载来设计一系列函数——它们完成相同的工作,但使用不同的参数列表。

        说白话,就是看下面的图就懂了,也就是函数名可以一致,但是参数不同,可以函数重载。但是匹配函数时,并不区分const和非const变量,举个例子也就是参数类型为char *和const char*看成是同一种类型变量。

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

        左值引用参数r1与可修改的左值参数(如double变量)匹配;const左值引用参数r2与可修 改的左值参数、const左值参数和右值参数(如两个double值的和)匹配;最后,左值引用参数 r3与左值匹配。注意到与r1或r3匹配的参数都与r2匹配。这就带来了一个问题:如果重载使用 这三种参数的函数,结果将如何?答案是将调用最匹配的版本:

3.3何时使用函数重载

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

        使用一个带默认参数的函数要简单些。只需编写一个函数(而不是 两个函数),程序也只需为一个函数(而不是两个)请求内存;需要修改函数时,只需修改一个。然而,如果需要使用不同类型的参数,则默认参数便不管用了,在这种情况下,应该使用函数重载 。

四、函数模板

4.1 函数模板

         函数模板,也就是C++的一大重要特性,也就是泛型编程。那么什么时候可以考虑使用模板?答:当你有一个程序的写法,算法都是一样的,但是用到的变量不一样,比如第一次你用了int,第二次你又想用double,那么这个时候就可以考虑函数模板。

#include <iostream>

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

int main() {
    int x = 5;
    int y = 10;
    std::cout << "Before swap: x = " << x << ", y = " << y << std::endl;

    // 调用模板函数,交换两个整数的值
    swap(x, y);

    std::cout << "After swap: x = " << x << ", y = " << y << std::endl;

    double a = 3.14;
    double b = 2.71;
    std::cout << "Before swap: a = " << a << ", b = " << b << std::endl;

    // 调用模板函数,交换两个双精度浮点数的值
    swap(a, b);

    std::cout << "After swap: a = " << a << ", b = " << b << std::endl;

    return 0;
}

        当然,函数模板也是有他的局限性。就比如类型的限制。比如你为数组名定义了运算符>,但是因为数组名为地址,可能得到的结果就不是你想要的,当然后面也可以考虑重载运算符,但是起码他也是有自己的局限的。

4.2 模板实例化和具体化

模板实例化:模板实例化是模板机制中的一个关键概念,它涉及到如何根据提供的参数类型来创建模板的具体版本。

  • 隐式实例化:编译器自动根据函数调用时提供的参数类型来实例化模板。
  • 显式实例化:开发者可以强制编译器实例化特定类型的模板版本,这有助于优化和控制模板的编译过程。

模板具体化:允许为特定的模板参数提供特定的实现。

  • 函数模板具体化:为特定类型提供不同的函数实现,对于处理特殊类型时非常有用。
  • 类模板部分具体化:为类模板的某些模板参数提供特定的定义,有助于优化特定类型的性能。
  • 类模板完全具体化:为所有模板参数提供完全特定的实现,这在需要完全控制特定类型行为时非常有用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值