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是等价的

你可能还喜欢下面这些文章

Go入门:五、goroutine和channel

这是我Go学习的第五篇笔记,学习的是go的语言的其他特性,这些特性是其他语言所不具备的。这次主要学习的是goroutine和channel。我的语言学习过程一般分为下面几个:1. 变量和数据类型2. 流程控制方法3. 函数声明和调用4. 面向对象5. 语言特性6. 常用标准库goroutine介绍和使用Go语言中,每个并发执行的单元称为goroutine(可类比线程)。当一个程序启动时候,main函数在一个main goroutine中运行。如果想要创建新的goroutine,使用go关键字!语法创建一个新的 goroutinechannel是goroutine的通信机制,比如创建一个能够接收

linux shell 入门

从程序员的角度来看, Shell本身是一种用C语言编写的程序,从用户的角度来看,Shell是用户与Linux操作系统沟通的桥梁。用户既可以输入命令执行,又可以利用 Shell脚本编程,完成更加复杂的操作。在Linux GUI日益完善的今天,在系统管理等领域,Shell编程仍然起着不可忽视的作用。深入地了解和熟练地掌握Shell编程,是每一个Linux用户的必修 功课之一。Linux的Shell种类众多,常见的有:Bourne Shell(/usr/bin/sh或/bin/sh)、Bourne Again Shell(/bin/bash)、C Shell(/usr/bin/csh)、K Shel

Go入门:六、常用标准库

这是我的Go学习的第六篇笔记,也是Go入门的最后一篇笔记。在大多数语言中,了解了变量和数据类型,流程控制,函数,面向对象,再加上标准库,就可以用这门语言去写一些项目了。首先让我想想,在工作中通常会用语言频繁处理什么问题或者处理什么数据?最常见的应该是各种字符串操作,日期和时间,读写文件、socket等IO相关的操作!字符串处理 — StringsString提供了一组处理字符串的操作,常用的有:判断一个字符串是否在另一个字符串中分割字符串为[]string和组合[]string为一个字符串字符串替换…太多了,就不一一列举了,这里列出一些常用的字符串操作。字符串判断字符串分割与合并字符串转换

Execl宏教程:从hello world开始入门

Excel宏使用的是vba,基本上就是运行在Excel里面的vb。所以学习vba和学习一门编程语言没有什么区别。所以我们最开始需要学的的就是一些基础语句。为了不让学习显得太枯燥,我们从一个hello world开始。首先需要打开Microsoft Excel,找到开发工具->宏,输入一个宏名称,点击创建创建了新的宏之后,就会出现一个编辑器界面Sub test()End Sub使用一个弹窗弹出hello worldSub test()MsgBox(“hello world”)End Sub到这里,一个简单的宏就创建完成了,虽然它现在什么也不能做,但是别着急,后面宏会为你做很多很多的事情,能

Go入门:四、面向对象

这是我的Go学习笔记的第四篇,面向对象!现代语言几乎都会面向对象进行了支持!当然,Go也具备面向对象的特性!我的语言学习过程一般分为下面几个:1. 变量和数据类型2. 流程控制方法3. 函数声明和调用4. 面向对象5. 语言特性6. 标准库Go语言中的面向对象有点特殊。在Go语言里面,没有显式的class、extends等面向对象语言经常使用的关键词,但是却有面向对象的特性。看看Go怎么实现的把!创建一个类按照我的理解,类实际上就是某种模板,这个模板里面含有有限多个属性和方法。在Go里面,定义这个模板的语法使用type来实现!比如单个int类型可以构成一个类(没错,你甚至可以在int数据类型上

赞赏

微信赞赏支付宝赞赏

发表回复

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