首页 > 编程语言 >C++笔记(2)——函数

C++笔记(2)——函数

时间:2023-07-17 18:35:08浏览次数:56  
标签:函数 形参 笔记 C++ 数组 类型 实参 定义

六. 函数

6.1 函数基础

一个典型的函数(function)定义包括:返回类型(return type)、函数名字,由0或多个形参(parameter)组成的列表以及函数体。我们通过调用运算符来执行函数,形式为"()"。

函数调用完成两项工作: 一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数。此时,主调函数 (calling function)的执行被暂时中断,被调函数(called function)开始执行。

如代码所示:

int function(int parameter) { 
    // include name, parameter and return type
    int result;
    return result; // return value
}

实参与形参

实参是形参的初始值。第一个实参初始化第一个形参,第二个实参初始化第二个形参,以此类推。尽管实参与形参存在对应关系,但是并没有规定实参的求值顺序。编译器能以任意可行的顺序对实参求值。

实参的类型必须与对应的形参类型匹配,这一点与之前的规则是一致的,我们知道在初始化过程中初始值的类型也必须与初始化对象的类型匹配。函数有几个形参,我们就必须提供相同数量的实参。因为两数的调用规定实参数量应与形参数量一致,所以形参一定会被初始化。

函数类型

大多数类型都能用作函数的返回类型。一种特殊的返回类型是void,它表示函数不返回任何值。函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。

6.1.1 局部对象

在C++语言中名字有作用域,对象有生命周期(lifetime) ,理解这两个概念非常重要。

  • 名字的作用域是程序文本的一部分,在其中随处可见。
  • 对象的生命周期是程序执行过程中该对象的存在的一段时间。

如我们所知,函数体是一个语句块。块构成一个新的作用域,我们可以在其中定义变量。形参和函数体内部定义的变量统称为局部变量(local variable)。它们对函数而言是“局部”的,仅在函数的作用域内可见,同时局部变量还会隐藏(hide) 在外层作用域中同名的其他所有声明中。在所有函数体之外定义的对象存在于程序的整个执行过程中。此类对象在程序启动时被创建,直到程序结束才会销毁。局部变量的生命周期依赖于定义的方式。

Tips: 内置类型的末初始化局部变量将产生未定义的值

局部静态对象

某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成static类型从而获得这样的对象。局部静态对象(local static object) 在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毀,在此期间即使对象所在的函数结束执行也不会对它有影响。

6.1.2 函数声明

和其他名字一样,函数的名字也必须在使用前声明。类似于变量,函数只能定义一次,但可以声明多次。如果一个函数永远也不会被我们用到,那么它可以只有声明没有定义。

函数的三要素(返回类型、函数名、形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称作函数原型(function prototype)

Tips: 在头文件中进行函数声明,含有函数声明的头文件应该被包含到定义函数的源文件中。

6.1.3 分离式编译

随着程序越来越复杂,我们希望把程序的各个部分分别存储在不同文件中。为了允许编写程序时按照逻辑关系将其划分开来,C++语言支持所谓的分离式编译(separate compilation)。分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。

编译和链接多个文件

举个例子,假设fact函数的定义位于一个名为fact.cc的文件中,它的声明位于名为Chapter6.h的头文件中。显然与其他所有用到fact函数的文件一样,fact.cc应该包含chapter6.h头文件。另外,我们在名为factMain.cc的文件中创建main函数,main函数将调用fact函数。要生成可执行文件(executable file),必须告诉编译起我们用到的代码在哪里。对于上述几个文件来说,编译的过程如下所示:

$ CC factMain.cc fact.cc # generates factMain.exe or a.out
$ CC factMain.cc fact.cc -o main # fenerates main or main.exe

其中,CC是编译器的名字,$是系统提示符,#后面是命令行下的注释语句。接下来运行可执行文件,就会执行我们定义的main函数。

如果我们修改了其中一个源文件,那么只需重新编译那个改动了的文件。大多数编译器提供了分离式编译每个文件的机制,这个过程通常会产生一个后缀名是.obj(Windows)或.o(UNIX)的文件,后缀名的含义是该文件包含对象代码(object code)

接下来编译器负责把对象文件链接在一起形成可执行文件。在我们的系统中,编译的过程如下所示:

$ CC -c factMain.cc # generates factMain.o
$ CC -c fact.cc # generates fact.o
$ CC factMain.o fact.o # generates factMain.exe or a.out
$ CC factMain.o fact.o -o main # generates main or main.exe

6.2 参数传递

如前所述,每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。

Tips: 形参初始化的机理与变量初始化一样

当形参是引用类型时,我们说它对应的实参被引用传递(passed by reference)或者函数被传引用调用(called by reference)。引用形参是它对应的实参的别名。

当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递(passed by value) 或者两数被传值调用(called by value)

6.2.1 传值参数

当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响初始值。

指针的行为和其他非引用类型一样。 当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后两个指针是不同的指针。因为指针使我们可以间接地访 问它所指的对象,所以通过指针可以修改它所指对象的值。

6.2.2 传引用参数

拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内) 根本就不支持拷贝操作。 当某种类型不支持持贝操作时,函数只能通过引用形参访问该类型的对象。

Tips: 当两数无须修改引用形参的值时最好使用常量引用

使用引用形参返回额外信息

一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。

该如何定义函数使得它能够既返回位置也返回出现次数呢? 一种方法是定义一个新的数据类型,让它包含位置和数量两个成员。还有另一种更简单的方法,我们可以给函数传入一个额外的引用实参。

6.2.3 const形参和实参

形参的初始化方式和变量的初始化方式是 一样的,所以回顾通用的初始化规则有助于理解本节知识。我们可以使用非常量初始化 一个底层const 对象,但是反过来不行:同时一个普通的引用必须用同类型的对象初始化。

尽量使用常量引用

把函数不会改变的形参定义成(普通的)引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制两数所能接受的实参类型。就像刚刚看到的,我们不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参。

6.2.4 数组形参

数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组以及使用数组时(通常)会将其转换成指针。因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。

尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式:

void print(const int*);
void print(const int[]);
void print(const int[10]);

尽管表现形式不同,但上面的三个函数是等价的:每个函数的唯一形参都是const int* 类型的。当编译器处理对print函数的调用时,只检查传入的参数是否是const int* 类型。

WARN: 和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。

因为数组是以指针的形式传递给函数的,所以一开始两数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术。

使用标记指定数组长度

管理数组实参的第一种方法是要求数组本身包含一个结束标记,使用这种方法的示例是C风格字符串(const char*类型)。C风格字符串存储在字符数组中,并且在最后一个字符后面跟着一个空字符。函数在处理C风格字符串时遇到空字符停止。

void print(const char *cp) {
    if(cp)
        while(*cp)
            cout << *cp++;
}

这种方法适用于那些有明显结束标记且不会与普通数据混淆的情况。

使用标准库规范

管理数组的第二种技术是传递数组首位元素指针,这种方法受到了标准库技术的启发。使用该方法,我们可以按照如下形式输出元素内容。

void print(const int *beg, const int *end) {
    while(beg != end)
        cout << *beg++ << endl;
}

为了调用这个函数,我们需要传入两个指针: 一个指向要输出的首元素,另一个指向为尾元素的下一位置。

int j[2] = {0, 1};
print(begin(j), end(j));

只要调用者能正确计算指针所指的位置,那么上述代码就是安全的。在这里,我们使用标准库begin和end函数提供所需指针。

显式传递一个表示数组大小的形参

第三种管理数组实参的方法是专门定义一个表示数组大小的形参,在C程序和过去的C++程序中常常使用这种方法。使用该方法,可以将print 函数重写成如下形式:

void print(const int ia[], size_t size) {
    for(size_t i = 0;i != size; ++i) {
        cout << ia[i] << endl;
    }
}

这个版本的程序通过形参size的值确定要输出多少个元素,调用print函数时必须传入这个表示数组大小的值:

int j[] = {0, 1};
print(j, end(j) - begin(j));

只要传递给函数的size值不超过数组实际的大小,函数就是安全的。

6.2.5 main: 处理命令行选项

有时我们确实需要给main传递实参,一种常见的情况是用户通过设置 一组选项来确定函数所要执行的操作。例如,假定main函数位于可执行文件prog之内,我们可以向程序传递下面的选项:

prog -d -o ofile data0

这些命令行选项通过两个(可选的)形参传递给main函数:

int main(int argc, char **argv[]) { ... }

WARN: 当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名宇,而非用户输入。

6.2.6 含有可变形参的函数

表6. 1: initializer_list 提供的操作:

代码 注释
initializer_list lst; 默认初始化; T类型元素的空列表
initializer_list lst {a,b,c...}; lst的元素数量和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const
lst2(lst) 或者 lst2 = lst 拷贝或赋值一个initializer list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素
lst.size() 列表中的元素数量
lst.begin() 返回指向lst中首元素的指针
lst.end() 返回指向lst中尾元素下一位置的指针

和vector一样,initiaizer_list也是一种模版类型。定义initializer_list对象时,必须说明列表中所含元素的类型.

和vector不一样的是,initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。

6.3 返回类型和return语句

return 语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。 return语句有两种形式:

return;
return expression;

不要返回局部对象的引用或指针

const_cast可以转换函数中常量类型和非常量类型,用来重载函数

constexpr函数

constexpr 函数(constexpr function)是指能用于常量表达式的函数。定义constexpr函数的方法与其他函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句:

内联函数和constexpr函数通常定义在头文件中。

6.5.3 调试帮助

(别问我为什么没有其他页的笔记,因为我不想记)

assert 预处理宏

assert 是一种预处理宏(preprocessor marco)。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert 宏使用一个表达式作为它的条件:

assert(expr);

首先对expr求值,如果表达式为假(即0),assert 输出信息并终止程序的执行。如果 表达式为真(即非0), assert什么也不做。assert宏定义在cassert头文件中。和预处理变量一样,宏名字在程序内必须唯一。

assert宏常用于检查“不能发生”的条件。例如,一个对输入文本进行操作的程序 可能要求所有给定单词的长度都大于某个网值。此时,程序可以包含一条如下所示的语句:

assert(word.size() > threshold);

NDEBUG 预处理变量

assert 的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG, 则assert什么也不做。默认状态下没有定义NDEBUG,此时assert 将执行运行时检查。我们可以使用一个#define语句定义NDEBUG,从而关闭调试状态。

定义NDEBUG能避免检查各种条件所需的运行时开销,当然此时根本就不会执行运行时检查。因此,assert应该仅用于验证那些确实不可能发生的事情。我们可以把assert当成调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查。

变量__func__输出当前调试的函数的名字。编译器为每个函数都定义了__func__,它是const char的一个静态数组,用于存放函数的名字。

除了C++编译器定义的__func__之外,预处理器还定义了另外4个对于程序调试很有用的名字:

  • _ _ FLIE _ _ 存放文件名的字符串字面值。
  • _ _ LINE _ _ 存放当前行号的整型字面值。
  • _ _ TIME _ _ 存放文件编译时间的字符串字面值。
  • _ _ DATE _ _ 存放文件编译日期的宇符串字面值。

6.7 函数指针

函数指针指向的函数数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。

标签:函数,形参,笔记,C++,数组,类型,实参,定义
From: https://www.cnblogs.com/BaiShun09/p/17560892.html

相关文章

  • C++中的异常处理详细说明
    看代码的过程中,经常看到try{}catch{}语句块,而且还经常性的看到这样的语句try{//保护代码}catch(...){//处理任何异常的代码}刚开始我对catch(...)非常困惑,因为C#中并没有这样的用法.所以,特意来了解学习一下C++中的异常处理方式通常来说,try{}catch{}块中,try......
  • mysql 笔记
    行转列: namecoursegradezhangsanjava20zhangsanc#60zhangsanpython40lisijava109lisic#30lisipython20wangwujava33 selectname,sum(casewhencourse='java'thengradeend)as'java',sum(casewhen......
  • 单分派泛函数
    当你的函数想根据不同的参数类型,做不同的操作的时候。python无法做重载,根据参数调用对应的签名函数。一般情况下只能if/elif/else来判断,时间久了,分支会特别多。使用functools.singledispatch装饰器可以把整体方案拆分成多个模块。甚至可以为你无法修改的类提供专门的函数。使用@s......
  • 字典,元组,元组内置方法、相关面试题 、 集合的内置方法 、字符编码 、文件操作 、函数
    字典的内置方法1.定义方式 d={'usernamne':"kevin"}#定义空字典d={}info=dict(username='kevin',age=18)#{'username':'kevin','age':18} print(info) #dic={#'name':�......
  • 文件内指针的移动 、内数据的修改 、函数(次函数非数学中的函数)(非常重要)
    文件的操作模式"""1.如果是t模式,read里面写的数字代表的是读取的字符个数2.如果是b模式,read里面写的数字代表的是读取的字节个数3.一个字节代表一个英文字符4.一个中文字符使用三个字节保存"""#withopen('a.txt','r',encoding='utf8')asf:#......
  • 小红书获得小红书笔记详情 API 返回值说明
    ​ item_get_video-获得小红书笔记详情 注册开通smallredbook.item_get_video公共参数名称类型必须描述keyString是调用key(必须以GET方式拼接在URL中)secretString是调用密钥api_nameString是API接口名称(包括在请求地址中)[item_search,item_get,item_s......
  • Learning hard C#学习笔记——读书笔记 03
    C#是面向对象的语言,每次到这里就会有一个问题,什么是对象,其实一句话就可以解释,那就是——万物皆是对象,这句话就像“如来”一样抽象,其实,我们无须在这上面耗费太大的精力,我们随着学习的深入,对象的概念自然会深入到脑海中所有面向对象的编程语言都有以下三个基础特征封装——把客......
  • 凸优化5——凸函数的定义
    本节对应凌青老师9,10两课,主要讲了凸函数的四种定义及相关证明凸函数的四种等价定义-知乎(zhihu.com)ConvexOptimization——凸函数-知乎(zhihu.com)具体可参考这两篇注意,凸函数的前提是,该函数的定义域是凸集......
  • Learning hard C#学习笔记——读书笔记 02
    每每说到类,不得不介绍的就是类的定义,它是一个抽象的概念,它是一个模板,制造对象的模板1.定义一个类classPreson{//类的成员变量}默认情况下,class关键字没有显式的使用internal修饰符来定义类,但是没有必要这样做,默认的修饰符就是internal除了internal这个权限修饰......
  • Reactjs学习笔记
    本篇是关于React的简介ReactJS是Facebook推出的一款前端框架,2013年开源,提供了一种函数式编程思想,拥有比较健全的文档和完善的社区,在React16的版本中对算法进行了革新,称之为ReactFiber。开发环境搭建需要nodeJS解析器,以及npm(node的包管理工具)如何引用React1.使用.js来引入......