C++笔记(2)——函数
六. 函数
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++笔记(2)——函数的更多相关文章
- Matlab学习笔记 figure函数
Matlab学习笔记 figure函数 matlab中的 figure 命令,能够创建一个用来显示图形输出的一个窗口对象.每一个这样的窗口都有一些属性,例如窗口的尺寸.位置,等等.下面一一介绍它们. ...
- matlab学习笔记 bsxfun函数
matlab学习笔记 bsxfun函数 最近总是遇到 bsxfun这个函数,前几次因为无关紧要只是大概看了一下函数体去对比结果,今天再一次遇见了这个函数,想想还是有必要掌握的,遂查了些资料总结如下. ...
- Python:笔记(2)——函数与模块
Python:笔记(2)——函数与模块 Python函数 关于函数 1.我们可以使用Help来查看函数的帮助信息 2.调用函数的时候,如果传入的参数数量或者类型不符合均会报错. 3.函数名其实就是一个 ...
- matlab学习笔记13_1 函数返回值
一起来学matlab-matlab学习笔记13函数 13_1 函数返回值 觉得有用的话,欢迎一起讨论相互学习~Follow Me 参考文献 https://blog.csdn.net/qq_36556 ...
- swift学习笔记2——函数、闭包
之前学习swift时的个人笔记,根据github:the-swift-programming-language-in-chinese学习.总结,将重要的内容提取,加以理解后整理为学习笔记,方便以后查询 ...
- iOS 阶段学习第七天笔记(函数、递归)
iOS学习(C语言)知识点整理笔记 一.函数 1)概念:具有特定功能的代码块的封装 2)函数的定义: 函数类型+函数名(形参列表) 函数类型 函数名(形参类型1 形参名1,形参类型2 形参名2 ...
- haskell学习笔记_函数
一开始学习函数式编程语言就被告知函数式编程语言是一种“定义式”的语言,而不是一种命令式的语言,在学习haskell的函数语法时,此感觉更加强烈,haskell的函数定义倾向于一种类似C++里面的swi ...
- C语言深度解剖读书笔记(6.函数的核心)
对于本节的函数内容其实就没什么难点了,但是对于函数这节又涉及到了顺序点的问题,我觉得可以还是忽略吧. 本节知识点: 1.函数中的顺序点:f(k,k++); 这样的问题大多跟编译器有关,不要去刻意追求 ...
- 第2章KNN算法笔记_函数classify0
<机器学习实战>知识点笔记目录 K-近邻算法(KNN)思想: 1,计算未知样本与所有已知样本的距离 2,按照距离递增排序,选前K个样本(K<20) 3,针对K个样本统计各个分类的出现 ...
- es6学习笔记-async函数
1 前情摘要 前段时间时间进行项目开发,需求安排不是很合理,导致一直高强度的加班工作,这一个月不是常说的996,简直是936,还好熬过来了.在此期间不是刚学会了es6的promise,在项目有用到pr ...
随机推荐
- Windows 与 虚拟机VirtualBox 共享挂载
在自己的电脑上安装了虚拟机后,经常会有需要把Windows这边的文件或文件夹拷贝到虚拟机上,简单记录一下. 如下图,设备--共享文件夹 然后在Windows上创建共享文件夹 执行命令 sudo mkd ...
- Appuploader证书申请教程
转载:http://kxdang.com/topic/appuploader/certification.html IOS证书制作教程 点击苹果证书 按钮 点击新增 输入证书密码,名称 这个密码不是账 ...
- python工程师-day83
1.drf 的用户认证组件 (1)models from django.db import models# Create your models here.class User(models.Mode ...
- MySQL相关操作(实用函数和sql语法)
1.时间函数 当前时间 select current_timestamp(); 当前时间戳 select UNIX_TIMESTAMP(NOW()); 当前时间戳精确到毫秒 select REPLAC ...
- hibernate及SpringBoot集成Jpa实现对数据库操作
首先使用Maven工程和junit完成hibernate对数据库的简单操作,完成之后在使用SpringBoot集成Jap完成hibernate对数据库的操作.本文仅供新手学习查看,具体线上使用需要对代 ...
- 2022-12-23:portainer是docker的web可视化工具。如果根据docker部署去写yaml,默认local是k8s,而不是docker,这不符合需求,需要修改yaml。请问部署在
2022-12-23:portainer是docker的web可视化工具.如果根据docker部署去写yaml,默认local是k8s,而不是docker,这不符合需求,需要修改yaml.请问部署在 ...
- Redis 高级特性 Redis Stream使用
Redis Stream 简介 Redis Stream 是 Redis 5.0 版本新增加的数据结构. Stream从字面上看是流类型,但其实从功能上看,应该是Redis对消息队列(MQ,Messa ...
- 蓝桥杯真题 平面切分(Set自定义去重)
题目详情 资源限制 内存限制:256.0MB C/C++时间限制:1.0s Java时间限制:3.0s Python时间限制:5.0s 问题描述 平面上有 N 条直线,其中第 i 条直线是 y=Ai⋅ ...
- Python数据类型 - 元祖
介绍 元祖和列表都是有序数列,列表是用 [ ],元祖使用() 元祖不同的地方在于创建后不能修改 注意:当元祖中只有一个元素的时候,要加上逗号(一个括号会被当成运算符使用) 比如: (123, ) ...
- 2023 5.14 虚拟环境安装Linux
1.安装配置VM虚拟机 vmare workstation 虚拟机是一款桌面计算机虚拟软件 让用户能够在单一主机上同事运行多个操作系统 1.每个虚拟操作系统的硬盘与数据都是独立 2.多台虚拟机可以构建 ...