C++ Primer Plus 学习笔记(三)—— 函数

函数

函数是C++的编程模块,要使用函数,必须提供定义和原型,并调用该函数。

1 函数原型

原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型(如果有的话)以及参数的类型和数量告诉编译器。

函数原型不要求提供变量名,有类型列表就足够了。例如下面两个原型是等价的:

void test(int num);
void test(int);

在编译阶段进行的原型化被称为静态类型检查,它可以捕获许多在运行阶段非常难以捕获的错误。

2 函数与数组

函数形参声明为数组时,在传参时是传入数组首地址值,也就是说实际上数组类型会退化为指针类型

因此,以下两种声明方式是基本等价的,只是第一种方式会提示用户传入的是数组类型:

int add(int nums[], int len);
int add(int* nums, int len);

3 指针和const

  • 将常规变量的地址赋给常规指针,可以修改指针值及指针指向地址的值:
 void test1()
{
    int num1 = 10;
    int num2 = 20;
    int* np = &num1;

    *np = 1;
    cout << num1 << endl; // 输出:1

    np = &num2;
    cout << *np << endl; // 输出:20
}
  • 将常规变量的地址赋给指向const的指针,可以修改指针值,但不能修改指向地址的值:
void test2()
{
    int num1 = 10;
    int num2 = 20;
    const int* np = &num1;

    // *np = 1; // 编译报错
    cout << num1 << endl;

    np = &num2;
    cout << *np << endl;
}
  • 将const变量的地址赋给常规指针,属于非法操作,因为常规指针是允许修改指向值的,与const变量定义冲突:
void test3()
{
    const int num1 = 10;
    // int* np = &num1; // 编译报错
}
  • 将const变量的地址赋给指向const的指针,可以修改指针值,但不能修改指向地址的值:
void test4()
{
    const int num1 = 10;
    int num2 = 20;
    const int* np = &num1;

    // *np = 1; // 编译报错
    cout << num1 << endl;

    np = &num2;
    cout << *np << endl;
}
  • 将const变量的地址赋给指向const的const指针,不可以修改指针值和指向地址的值:
void test5()
{
    const int num1 = 10;
    int num2 = 20;
    const int* const np = &num1;

    // *np = 1; // 编译报错
    cout << num1 << endl;

    // np = &num2; // 编译报错
    cout << *np << endl;
}

当指针指向指针时,情况会更复杂:

  • 假如是一级间接关系,则将非const指针赋给指向const的指针是可以的:
void test6()
{
    int num1 = 10;
    int* np = &num1;
    const int* np2 = np;
    cout << *np2 << endl;
}
  • 在进入两级间接关系时,将const和非const混合的指针赋值方式将不再安全。比如下面这个例子,将非const地址(&p1)赋给了const指针(pp2),此时可以使用p1来修改const数据,因此编译器不允许这种操作:
void test7()
{
    const int** pp2;
    int* p1;
    const int n = 3;
    // pp2 = &p1; // 编译报错
    *pp2 = &n;
    *p1 = 10;
}

4 函数和二维数组

二维数组在定义时就要求指定列数(为了能让编译器确定每行长度),因此在作为形参时也需要指定列数,其实际传入的是一个指向指针(即指向一维数组的指针)的指针:

void test8(int (*arr)[2], int row);
void test9(int arr[][2], int row);
void test10(int** arr, int row, int col);

int main()
{
    int nums[][2] = { {1, 2}, {3, 4} };
    test8(nums, 2);
    test9(nums, 2);
    // test10(nums, 2); // 编译报错
    return 0;
}

5 函数指针

函数也有地址,可以编写将另一个函数的地址作为函数的参数。

int test11()
{
    return 1;
}

int test12()
{
    return 2;
}

void calc(int (*pf)())
{
    cout << pf() << endl;
}

int main()
{
    calc(test11); // 输出:1
    calc(test12); // 输出:2
    return 0;
}

6 内联函数

内联函数是C++为提高程序运行速度所做的一项改进,编译器会使用相应的函数代码替换函数调用。

  • 对比常规函数调用,优点是节省了处理函数调用的时间,缺点是需要占用更大的内存。

  • 对比C语言的宏实现,区别点在于内联函数是按值传递参数实现的,而宏是通过文本替换实现的。

#include <iostream>

using namespace std;

inline int Square(double x) { return x * x; }

#define SQUARE(x) (x) * (x)

int main()
{
    int num1 = 2;
    cout << "before: num = " << num1 << ", Square=" << Square(num1++) << ", after: num = " << num1 << endl;

    int num2 = 2;
    cout << "before: num = " << num2 << ", SQUARE=" << SQUARE(num2++) << ", after: num = " << num2 << endl;

    return 0;
}
// 输出:
// before: num = 2, Square=4, after: num = 3
// before: num = 2, SQUARE=6, after: num = 4

7 引用变量

7.1 引用概念

引用是已定义变量的别名,必须在声明引用时进行初始化。

而且,只能通过初始化声明来设置引用,但不能通过赋值来设置。

#include <iostream>

using namespace std;

int main()
{
    int num1 = 1;
    int& nr = num1;
    cout << "num1=" << num1 << ", nr=" << nr << endl;

    int num2 = 2;
    nr = num2; // 由于nr是num1的别名,因此实际上等于 num1 = num2
    cout << "num1=" << num1 << ", num2=" << num2 << ", nr=" << nr << endl;

    return 0;
}

// 输出:
// num1=1, nr=1
// num1=2, num2=2, nr=2

7.2 引用传递

引用在作为函数参数时,使得函数中的参数名成为调用程序中的变量的别名,这种传递参数的方法称为按引用传递。

在const引用作为函数参数时,为了能够更加通用,是允许传入与引用类型不匹配的实参的,此时将生成一个临时变量用于存储传入的值,并让函数形参指向它。

#include <iostream>

using namespace std;

double test(const double& x)
{
    return x * x;
}

int main()
{
    double d1 = 1.0;
    long l1 = 1;
    cout << test(d1) << endl; // 1 不会生成临时变量
    cout << test(1.2) << endl; // 1.44 会生成临时变量
    cout << test(l1) << endl; // 1 会生成临时变量
    cout << test(d1 + 1) << endl; // 4 会生成临时变量
    return 0;
}

7.3 返回引用

函数返回引用意味着调用程序将直接访问返回值,而不需要拷贝,通常用法是返回的引用将指向传递给函数的引用。

值得注意的是,如果将const用于引用返回类型,那么将意味着不能使用返回的引用来直接修改它指向的结构。

#include <iostream>

using namespace std;

struct TestStruct {
    int num;
    std::string name;
};

TestStruct& add1(TestStruct& ts)
{
    ts.num += 1;
    return ts;
}

const TestStruct& add2(TestStruct& ts)
{
    ts.num += 1;
    return ts;
}

int main()
{
    TestStruct ts1 = {
        .num = 1,
        .name = "test",
    };
    cout << add2(ts1).num << endl;
    add1(ts1).num = 4; // 可以更改返回引用的值

    TestStruct ts2 = {
        .num = 1,
        .name = "test2",
    };
    cout << add2(ts2).num << endl;
    // add2(ts2).num = 3; // 编译报错,无法更改const引用的值
    return 0;
}

8 函数重载

函数重载指的是可以拥有多个同名的函数,区分不同函数的关键在于函数的参数列表——也称为函数特征标。

在没有找到匹配的原型时,C++会尝试使用标准类型转换强制进行匹配,但是如果存在多种可行转换的话,编译器将无法判断该使用哪种原型,此时C++将拒绝这种调用。

类型引用和类型本身是会被编译器视为同一特征标的,因为编译器调用时时无法在判断要使用哪种原型的。

double test(double x);
double test(double& x);

double num = 2.0;
test(num); // 此时编译器无法判断该使用哪个原型

普通指针和常量指针则是两个不同的特征标,编译器将根据实参是否为const来决定使用哪个原型。

#include <iostream>

using namespace std;

void test(char* ch)
{
    cout << "test char*" << endl;
}

void test(const char* ch)
{
    cout << "test const char*" << endl;
}

int main()
{
    char ch[5] = "haha";
    char* ch1 = ch;
    const char* ch2 = "xixi";
    test(ch1); // test char*
    test(ch2); // test const char*
    return 0;
}

9 函数模板

函数模板使用通用类型来定义函数。函数模板本身并不创建任何函数,而只是告诉编译器如何定义函数,只有在实际调用传入具体类型时,编译器才会按模板模式创建该类型的函数即模板实例。

9.1 显式具体化

当在使用函数模板需要对某种类型做特殊处理时,可以提供一个具体化函数定义——称为显式具体化。

如果有多个原型,编译器在选择原型时,非模板版本将优先于显式具体化和模板版本。

// 非模板版本
void Swap(Job& j1, Job& j2);

// 模板版本
template<class T>
void Swap(T& j1, T& j2);

// 显式具体化版本
template<> Swap(Job& j1, Job& j2);

9.2 实例化和具体化

隐式实例化、显式实例化和显式具体化统称为具体化,相同点在于表示的都是使用具体类型的函数定义。

  • 在调用函数模板未显式声明类型时,此时编译器将通过隐式实例化,来使用模板生成函数定义。
template<class T>
void Swap(T&, T&);

int num1 = 1;
int num2 = 2;
Swap(num1, num2); // 通过判断传入类型为int,隐式生成了模板实例 void Swap(int&, int&);
  • C++还允许显式实例化,即可以直接命令编译器创建特定的实例。
template void Swap<int>(int&, int&);
  • 显式具体化和显式实例化区别点在于:1、声明方式不同;2、显式具体化必须提供函数定义,而显式实例化可以只提供函数原型使其显式生成模板实例。
template<> Swap(Job& j1, Job& j2);

10 重载解析

对于函数重载、函数模板和函数模板重载,C++需要决定为函数调用使用哪一个函数定义,这个过程称为重载解析。这个过程大致进行的步骤为:

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

编译器在确定哪个函数是最佳时,它会查看为使函数调用参数与可行的候选函数的参数匹配所需要进行的转换。通常,从最佳到最差的顺序如下:

  1. 完全匹配,但常规函数优于模板。
  2. 提升转换(例如,char和short自动转换为int)。
  3. 标准转换(例如,int转换为char,long转换为double)。
  4. 用户定义的转换,如类声明中定义的转换。

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

请添加图片描述

因此,下面所有的原型都是完全匹配的:

struct TestStruct {...};
TestStruct ts;
test(ts);

// 完全匹配的原型
void test(TestStruct);
void test(TestStruct&);
void test(const TestStruct);
void test(const TestStruct&);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值