C++入门:三、函数

这是我学习C++的第三篇笔记,函数。我的学习路径是

1. 变量和数据类型
2. 流程控制
3. 函数
4. 面向对象
5. 标准库

现在学习的是函数的声明、定义、调用等相关知识。

函数声明和定义

函数的声明包含返回类型,函数名字,0个或者多个形参,无函数体,通常在头文件中对函数进行声明。

返回类型 函数名称(参数类型1 参数1, 参数类型2 参数2);
// 例如声明一个求阶乘的函数
int fact(int val);

函数的定义包含返回类型,函数名字,0个或多个形参,以及函数体。

返回类型 函数名称(参数类型1 参数1, 参数类型2 参数2) {
    函数体
}

比如写一个求阶乘的函数,可以写成下面这样

int fact(int val)
{
    int ret = 1;
    while (val > 1) {
        ret *= val--; // ret乘val的值返回给ret,val再自减1
    }
    return ret;
}

写一些简单的函数大多数语言都差不多,不过可惜每种语言或多或少都有自己的特色,这是比较令人头秃的地方。

函数的参数

函数可以带有0或多个参数,每个参数都需要声明类型。参数传递可以传值和传引用。

如果形参是引用类型,那么它将绑定到对应的实参中,我们成为传引用。否则,将会把实参的值拷贝后赋值给形参,我们成为传值

传值调用的时候,函数内改变传递的变量,函数外该变量不会发生变化。例如

void passbyval(int val)
{
    val = 10;
    std::cout << val << std::endl;
}

int main() {
    int num = 1;
    std::cout << num << std::endl;
    passbyval(num);
    std::cout << num << std::endl;
    return 0;
}
// 输出
1
10
1

可以看到,参数传值,在函数内改变参数的值,不影响原值。

传引用的时候,函数内改变参数值相应会改原值,还是上面的例子,把参数改成引用试试


void passbyval(int &val)
{
    val = 10;
    std::cout << val << std::endl;
}

int main() {
    int num = 1;
    std::cout << num << std::endl;
    passbyval(num);
    std::cout << num << std::endl;
    return 0;
}
// 输出
1
10
10

可以看到,改变函数参数的值的时候原值也被更改。

C++中,建议使用引用形参代替指针,原因是拷贝大的容器或者变量效率低,并且有些类类型根本不支持拷贝,这时候智能通过引用传参。

指针作为参数

指针作为参数的时候传递的也是值,是指针的值。这个要怎么理解呢?可以看看下面的例子

// 传指针
void passptr(int *val)
{
    // 打印指针的地址
    std::cout << "函数内指针的地址:" << &val << std::endl;
    // 打印指针的值
    std::cout << "函数内指针的值:" <<  val << std::endl;

}

int main() {
    int num = 1;
    int *ptr = &num;
    std::cout << "函数外指针的地址:" << &ptr << std::endl;
    std::cout << "函数外指针的值:" << ptr << std::endl;
    passptr(&num);
    return 0;
}

// 输出
函数外指针的地址:0x7ffeec520760
函数外指针的值:0x7ffeec520768
函数内指针的地址:0x7ffeec520738
函数内指针的值:0x7ffeec520768

根据上面的运行结果可以看出,函数外指针的地址和函数内指针的地址不是一个地址,但是值是一样的,说明指针被复制了。因此传指针实际上传递的也是指针的值。

const 形参

const作为作为参数是很常见的做法。由于const不可变的特点,当使用const参数的时候,参数会非常安全(它不会被修改)。

参数中传入引用的目的很多时候是为了修改这个值,甚至把这个值当作返回值来用。因此,如果一个引用参数没有加const,给调用者隐含的意思就是这个值我会去修改它,它将是一个返回值。

数组参数

数组有个特殊的性质,不允许直接拷贝。因此我们不能将数组直接作为参数传递,当数组作为参数的时候,实际上需要传递的是数组的首元素的指针

void print(const int*)
void print(const int[]) //函数意图是传递一个数组参数
void print(const int[10])

上面函数声明虽然不同,但是实际上表达的意思都是一样的,当编译器处理这个函数调用的时候,只检查传入的参数是不是const int*。

如果我们传入的是一个数组,则会自动转换成数组第一个元素的指针。可以看看下面的代码

#include <iostream>

void print(int *arr) {
    std::cout << arr << std::endl;
}

int main() {
    int i = 0;
    print(&i); // 输出i的地址

    int arr[2] = {1,2};
    print(arr); // 输出首元素的地址
    return 0;
}

有默认值的参数

有些时候我们需要为函数定义多个参数,但大部分场景我们只需要用到其中的少部分参数,少部分场景才会用到所有的参数。比如文章标题过长,我们需要截断以便在设备商更好地显示,通常我们会定义一个默认的长度,只有特殊的情况才会去自定义长度。

// 带有固定参数
using std::string
string subtitle(string title, size_t pos = 0, size_t n = 10)
{
    return title.substr(pos, n);
}
string t = subtitle("hello world"); // t 为hello worl

函数的返回值

大多数类型都可以作为函数的返回类型,一种特殊的返回类型是void,表示函数不返回值。函数的返回类型不能是数组类型或者函数类型。

引用返回左值

如果函数返回的是一个引用,那么这个函数是一个左值,否则函数是一个右值。以下面代码为例

// 假设这里的idx是一个有效的下标
int &set(int arr[], int idx) {
    return a[idx];
}

int main() {
    int a[2] = {1,2};
    set(a, 1) = 10;
    std::cout << a[1] << std::endl;
    return 0;
}

// 将会输出 10

函数重载

如果同一个作用域内的几个函数名字相同但是行参不同,我们称为重载函数。比如

void print(int i)
{
    std::cout << "int: " <<  i << std::endl;
}

void print(double i)
{
    std::cout << "double: " << i << std::endl;
}

print(1);
print(1.122);

// 输出
// int: 1
// double: 1.122

函数重载能减少我们起名成本,不过给函数起不同的名字能让程序仍容易被人理解。

函数指针

当一个指针指向一个函数的时候,我们称这个指针为函数指针。其实和其他的指针一样,需要指向特定类型,函数指针指向的是函数类型,函数类型由返回值和参数类型共同决定,和函数名无关

因此如果想要声明一个指向函数的指针,只需要将指针名字替换函数名字即可。以求阶乘的函数为例子

int fact(int val);

// 声明一个可以指向fact函数的指针(括号是不可少的)
int (*pf)(int val) 

// 通常,声明指向函数的指针不写形参名,因此写成下面这样
int (*pf)(int)

如何使用

// 直接将函数名赋值给指向函数的指针
pf = fact;
// 调用
pf(3)

// 还可以这样赋值
pf = &fact; // 和pf=fact是等价的

赞赏

微信赞赏支付宝赞赏

发表评论

电子邮件地址不会被公开。 必填项已用*标注