第六章 函数
6.1 函数基础
编写函数
c++中函数主要由以下部分组成:返回类型,函数名,形参(0个或多个)及函数体组成,我们最开始写的helloworld就可以看做是一个函数(主函数)。
int main()
{
printf("helloworld\n");
return 0;
}
其中int
是函数返回值类型,main
是函数名,花括号中间是函数体,该函数形参为空。
调用函数
那么如果我们要调用应该怎么做呢?
我们给定一个函数比如一个加法计算函数add
int add(int a,int b)
{
return a+b;
}
观察不难发现,这个函数返回值是整型,形参为两个整型的值,因此我们调用的时候需要赋予这两个形参实参以供其运行。
调用过程如下
int main()
{
cout << add(1,2);
return 0;
}
结果显而易见会输出3。
这个过程发生了什么事呢?其实在这个过程中,主函数main
的执行暂时被中断,其运行逻辑被add函数所接管,执行完add
函数后,其替换为其返回值为主函数所用。
形参和实参
计算机就是喜欢将简单问题复杂化,形参就是形式参数,实参就是实际参数。形式参数是一个占位符,代表着这一位参数需要是什么样的类型,实参就是实际输入的参数,实参的输入遵循类型转化规则如下图。
函数的参数列表
参数列表可以为空,可以不写或者显式写出为void如下
int func1(void){}
int func2(){}
这两种写法都合法
参数列表有多个同一类型形参也需要都标出返回值类型。
int add(int a,int b){return a+b;}
比如上述add
函数
6.1.1 分离式编译
函数可能调用和实现和声明不在一个文件中,比如我声明函数在一个头文件header.hpp
,函数实现在cal.cpp
,调用在calMain.cpp
,那么应该怎么办呢。
c++提供了分离式编译的功能
此时我们就可以使用这行指令
g++ calMain.cpp cal.cpp -o main
来联合编译代码,在calMain.cpp中必须包含header.hpp 头文件
6.3 参数传递
如果实参是引用类型则说其被引用传递或者该函数被传引用调用,说人话就是函数能直接修改引用的值本身,而如果实参被拷贝则不会印象愿值。
举个例子
void add1_copy(int val) {
val++; // 对传入的参数进行增加操作,但不影响原始变量
}
void add1_ref(int& val) {
val++; // 直接对传入的参数进行增加操作,影响原始变量
}
int main() {
int a = 10; // 定义一个整数变量 a,赋值为 10
add1_copy(a); // 通过值传递方式调用函数,传入变量 a 的拷贝
cout << "After add1_copy: " << a << endl; // 输出变量 a 的值,预期输出为 10,因为传递的是拷贝
add1_ref(a); // 通过引用传递方式调用函数,传入变量 a 的引用
cout << "After add1_ref: " << a << endl; // 输出变量 a 的值,预期输出为 11,因为直接修改了引用的变量
return 0;
}
6.2.1传值参数
这就是之前说的只拷贝的情况,无论传递的是指针或者是直接传值都是直接进行拷贝而不改变原本变量值。
6.2.2传引用参数
通过传递引用参数可以实现对变量值进行直接修改。
我们可以在实际操作中尽量使用引用以避免拷贝,因为有些值本身是很大或者复杂的,引用比拷贝节省空间。而且使用引用形参还能一次性返回多个值,下面是一个例子。
int findWord(string str, char target, int& totalTimes) {
int firstOccurrence = -1; // 初始化第一次出现位置为 -1
totalTimes = 0; // 初始化目标字符出现总次数为 0
// 遍历字符串
for (int i = 0; i < str.length(); ++i) {
if (str[i] == target) {
// 如果当前字符等于目标字符
totalTimes++; // 总次数加一
if (firstOccurrence == -1) {
// 如果是第一次出现,记录位置
firstOccurrence = i;
}
}
}
return firstOccurrence; // 返回第一次出现的位置
}
6.2.3 const形参和实参
这部分和变量很像,回顾一下,顶层const指的是直接应用于对象本身的 const 限定符,而不是对象的引用或指针。也就是说类似const int a = 1;
这种,在拷贝时会忽略const(很好理解,毕竟拷贝不改变其值嘛)但是引用时因为其无法改变的特性因此无法被赋值。
我们可以用一个非常量初始一个底层const对象但是反过来不行。
刚刚说到了尽量使用引用而非拷贝,但是拷贝不会影响本身变量的值但是引用会影响,怎么办呢,因此我们需要多用const引用,比如在字符串相关操作的时候
void use_string(string& str) {
// 可能修改 str
}
void process(const string& str) {
// 不修改 str
use_string(str); // 错误:use_string 需要非 const 引用
}
使用常量引用有助于我们更好的使用str的值。
6.2.4数组形参
数组有几个性质:
1.不允许拷贝数组
2.使用数组时常写作指针的形式
由于这两个性质,当需要将数组写作形参时我们可以用以下写法
void print(const int*);
void print(const int[]);
void print(const int[10]);
这三种表现方式等价,有聪明的小伙伴就会说了,不对,第三个规定了有10个元素,而第二个没有规定,怎么会是等价的呢?实际上译器会忽略这个大小,并将 int[10] 解释为 int*,这样和第一二个写法也是一致的。
我们也可以使用标记来指定数组长度,在C风格字符串中,最后一个字符跟着一个空字符,因此可以用作结束条件。
并且由于受到标准库中begin
和end
的启发,我们可以用如下实例辅助理解
#include <iostream>
// print函数不变,接受两个const int*类型的参数
void print(const int *beg, const int *end) {
while (beg != end) {
std::cout << *beg++ << " "; // 输出当前元素并递增指针
}
std::cout << std::endl;
}
// begin函数使用模板参数推导,返回指向数组首元素的指针
template <std::size_t N>
const int* begin(const int (&arr)[N]) {
return arr; // 返回指向数组首元素的指针
}
// end函数使用模板参数推导,返回指向数组尾后元素的指针
template <std::size_t N>
const int* end(const int (&arr)[N]) {
return arr + N; // 返回指向数组尾后元素的指针
}
int main() {
int j[] = {0, 1, 2, 3, 4, 5}; // 定义一个整型数组
// 调用print函数,使用begin和end函数来获取数组的开始和结束指针
print(begin(j), end(j));
return 0;
}
还有一些,首先是可以显式地传递一个数组的大小在形参之中,这点在之前的函数中经常使用,也可以将数组设置为引用形参。并且在c++中其实没有真正的多维数组,多维数组其实是数组的数组,当多维数组传递给函数时真正传递的是指向数组首元素的指针。
6.2.5 main函数的形参
看这一页的时候我可头大了,短短一页说的内容和天书一样
后来才发现没那么复杂,argc是指令数量,一般情况下不用我们自己定义,而由程序自己统计,而argv就是我们代码输入的部分,比如我们规定一个程序为prog
,那么第一份argv
(argv[0]
)的值就是prog
,然后后面每输入一个由空格隔开的参数都会被记录为argv[n]
。
6.2.6 拥有可变形参的情况
有时候我们不知道需要输入多少个形参,此时可以用可变形参这个概念
首先第一个方法是initializer_list形参,此方法适用于实参数量未知但是类型相同的方式
它和vector一样是模板类型得指定类型,和vector不一样的是其中都是常量不可改变。
6.3 返回类型与return语句
6.3.1 无返回值的函数
void类型的函数无返回值,可以在最后加
return;
或者不做任何处理
6.3.2 有返回值的函数
这种函数将会通过
return 返回值;
返回其值,返回一个值的方式和初始化一个变量或者形参的方式完全一致:它会产生一个临时量,其值就是该函数调用的结果。
要注意:不要返回局部变量的指针或者引用!
在我们之前的章节有提到作用域问题,当函数生命周期结束局部变量也会被销毁,此时引用或使用其指针都会有访问到非法内存的风险。
6.3.3 返回数组指针
因为数组无法被拷贝,因此无法返回数组,但是可以返回数组指针。在这里这本书提供了一个方法,就是以起别名的方式进行简化这个任务例如:
typedef int arrT[10];
using arrT = int[10];
arrT* func(int i);
但是其实我感觉没那么麻烦,我们只要简单的区分返回静态数组或者动态数组就行。
如果返回值是静态数组则可以这样:
#include <iostream>
// 定义一个返回静态数组指针的函数
int (*returnStaticArray())[5] {
static int arr[5] = {1, 2, 3, 4, 5};
return &arr;
}
int main() {
int (*ptr)[5] = returnStaticArray();
// 使用指针访问数组元素
for (int i = 0; i < 5; ++i) {
std::cout << (*ptr)[i] << " ";
}
std::cout << std::endl;
return 0;
}
如果是动态数组则需要这样
#include <iostream>
// 定义一个返回动态分配数组指针的函数
int* returnDynamicArray(int size) {
int* arr = new int[size];
for (int i = 0; i < size; ++i) {
arr[i] = i + 1;
}
return arr;
}
int main() {
int size = 5;
int* ptr = returnDynamicArray(size);
// 使用指针访问数组元素
for (int i = 0; i < size; ++i) {
std::cout << ptr[i] << " ";
}
std::cout << std::endl;
delete[] ptr; // 记得释放动态分配的内存
return 0;
}
6.4 函数重载
简单来说就是名字相同但是作用域不同的几个函数就叫做函数重载。
比如:
#include<iostream>
using namespace std;
string FindName(int id);//通过id寻找
string FindName(string addr);//通过地址寻找
string FindName(int age ,string addr);
string FindName(int id)
{
string Name = "id";
return Name;
}
string FindName(string addr)
{
string Name = "addr";
return Name;
}
string FindName(int age ,string addr)
{
string Name = "mix";
return Name;
}
int main()
{
cout << FindName(1) << endl;
cout << FindName("dishai") << endl;
cout << FindName(1,"khdjkask") << endl;
return 0;
}
这个代码的执行结果是
id
addr
mix
几个注意的点:
- 只有返回值不同的函数不是函数重载是错误函数
- 主函数不能重载
- 重载函数返回值、形参类型、形参数量至少有一个不同并且函数名一致
- 一个有顶层const的形参和没有它的函数无法区分。 Record lookup(Phone* const)和 Record lookup(Phone*)无法区分。相反,是否有某个底层const形参可以区分。 Record lookup(Account*)和 Record lookup(const Account*)可以区分
- 重载和作用域:若在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体,在不同的作用域中无法重载函数名
6.5 特殊用途语言特性
6.5.1默认实参
这没啥好说的
普通实参如下
int add(int a,int b);
默认实参如下·
int add(int a = 0, int b = 0);
第一个函数调用时必须输入值,但是有默认实参的形参可以不输入值,不输入值则为默认实参,也就是说假如我直接调用add();
将输出0而不报错。
6.5.2 内联函数和constexpr函数
内联函数inline
看代码
#include <iostream>
// 声明一个内联函数
inline int add(int a, int b) {
return a + b;
}
int main() {
int result = add(3, 4); // 编译器会在这里展开 add 函数的代码
std::cout << "Result: " << result << std::endl;
return 0;
}
- 普通函数的缺点:调用函数比求解等价表达式要慢得多。
- inline函数可以避免函数调用的开销,可以让编译器在编译时内联地展开该函数。
- inline函数应该在头文件中定义。
constexpr函数
在c11标准中提出了新的函数constxpr函数用于计算常量表达式,下面是一个示例:
#include <iostream>
// 定义一个 constexpr 函数,计算阶乘
constexpr int factorial(int n) {
return (n <= 1) ? 1 : (n * factorial(n - 1));
}
int main() {
constexpr int result = factorial(5); // 编译期间计算阶乘结果
std::cout << "Factorial of 5 is: " << result << std::endl;
return 0;
}
几个要注意的点:
- 函数的返回类型及所有形参类型都要是字面值类型。
- constexpr函数应该在头文件中定义
6.5.3 调试帮助
这将涉及两个点,一个是assert宏,一个是NDEBUG宏。
assert宏
这是一个常见的断言工具,主要用于在运行时检查条件是否满足,如果条件为假(即表达式的值为 0 或者假),则输出错误消息并终止程序的执行。它通常用于在开发阶段检查程序中的逻辑错误或不变条件是否被满足,属于断言(assertion)的一种形式。
举个例子
#include <cassert>//assert的头文件
#include <iostream>
int divide(int a, int b) {
// 断言 b 不为零
assert(b != 0);
return a / b;
}
int main() {
int x = 10, y = 0;
std::cout << divide(x, y) << std::endl; // 这里会触发断言失败,程序终止
return 0;
}
NDEBUG宏
NDEBUG 是一个预处理宏,用于控制 assert 宏的行为。如果定义了 NDEBUG,则 assert 宏将被禁用,相当于在程序中所有的 assert 语句都被移除。这样可以在发布版本中去掉调试代码,提高程序的性能和减小可执行文件的大小。
由于在直接的执行代码中无法被体现,因此我们显式输出代码
#include <cassert>
#include <iostream>
int divide(int a, int b) {
#ifndef NDEBUG
std::cout << "Debugging message: divide(" << a << ", " << b << ")" << std::endl;
#endif
assert(b != 0);
return a / b;
}
int main() {
int x = 10, y = 0;
std::cout << divide(x, y) << std::endl; // 在发布版本中,assert 会被禁用,不会输出调试信息
return 0;
}
6.6 函数匹配
这一板块本来应该和函数重载一起讲的,就是在重载的过程中可能会出现多个相似项目,这样的话就需要参考函数匹配规则了。
主要就三点:1.候选函数;2.可行函数;3.寻找最佳匹配。
首先是查看候选函数:选定本次调用对应的重载函数集,集合中的函数称为候选函数(candidate function)。
然后在看可行函数:考察本次调用提供的实参,选出可以被这组实参调用的函数,新选出的函数称为可行函数(viable function)。
最后寻找最佳匹配:基本思想:实参类型和形参类型越接近,它们匹配地越好。
给看一个极端的例子:
#include<iostream>
using namespace std;
int PNUm(int a);
int PNum(float a);
int PNum(double a,int a2 = 1);
int PNUm(int a)
{
return 1;
}
int PNum(float a)
{
return 2;
}
int PNum(double a,int a2)
{
return 3;
}
int main()
{
cout<<PNum(1.1)<<endl;
return 0;
}
大家可以执行一下,我的执行结果是3。
6.7 函数指针
最后一个点了,函数指针指向的是函数而非对象
给代码
#include <iostream>
using namespace std;
// 函数:打印整数
void printInteger(int num) {
cout << "Integer: " << num << endl;
}
// 函数:打印平方
void printSquare(int num) {
cout << "Square: " << num * num << endl;
}
// 函数:执行函数指针指向的函数
void executeFunction(int value, void (*funcPtr)(int)) {
funcPtr(value); // 通过函数指针调用传入的函数
}
int main() {
int number = 5;
// 声明一个函数指针变量,指向 printInteger 函数
void (*ptrPrintInteger)(int) = printInteger;
// 使用函数指针调用 printInteger 函数
executeFunction(number, ptrPrintInteger);
// 声明一个函数指针变量,指向 printSquare 函数
void (*ptrPrintSquare)(int) = printSquare;
// 使用函数指针调用 printSquare 函数
executeFunction(number, ptrPrintSquare);
return 0;
}
注意
- 指针两端括号必不可少
- 形参中使用函数定义或者函数指针定义效果一样
- 使用类型别名或者decltype
- 返回指向函数的指针:1.类型别名;2.尾置返回类型