文章

02.C++函数

02.C++函数

C++ 函数

C++ 函数基础

函数的定义

函数的定义一般主要有 5 个步骤:

1、返回值类型

2、函数名

3、参数表列

4、函数体语句

5、return 表达式

语法:

1
2
3
4
5
返回值类型 函数名 (参数列表)
{
       函数体语句
       return表达式
}
  • 返回值类型 :一个函数可以返回一个值。在函数定义中
  • 函数名:给函数起个名称
  • 参数列表:使用该函数时,传入的数据
  • 函数体语句:花括号内的代码,函数内需要执行的语句
  • return 表达式: 和返回值类型挂钩,函数执行完后,返回相应的数据

示例:

1
2
3
4
5
6
//函数定义 定义一个加法函数,实现两个数相加
int add(int num1, int num2)
{
	int sum = num1 + num2;
	return sum;
}

函数的调用

功能: 使用定义好的函数

语法: 函数名(参数)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//函数定义
int add(int num1, int num2) // 定义中的num1,num2称为形式参数,简称形参
{
	int sum = num1 + num2;
	return sum;
}

int main() {

	int a = 10;
	int b = 10;
	// 调用add函数
	int sum = add(a, b);// 调用时的a,b称为实际参数,简称实参
	cout << "sum = " << sum << endl;

	a = 100;
	b = 100;

	sum = add(a, b);
	cout << "sum = " << sum << endl;

	system("pause");

	return 0;
}

函数的常见样式

常见的函数样式有 4 种

  1. 无参无返
  2. 有参无返
  3. 无参有返
  4. 有参有返

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//函数常见样式
//1、 无参无返
void test01()
{
	//void a = 10; //无类型不可以创建变量,原因无法分配内存
	cout << "this is test01" << endl;
	//test01(); 函数调用
}

//2、 有参无返
void test02(int a)
{
	cout << "this is test02" << endl;
	cout << "a = " << a << endl;
}

//3、无参有返
int test03()
{
	cout << "this is test03 " << endl;
	return 10;
}

//4、有参有返
int test04(int a, int b)
{
	cout << "this is test04 " << endl;
	int sum = a + b;
	return sum;
}

函数的声明

作用: 告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。

  • 函数的声明可以多次,但是函数的定义只能有一次

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//声明可以多次,定义只能一次
//声明
int max(int a, int b);
int max(int a, int b);
//定义
int max(int a, int b)
{
	return a > b ? a : b;
}

int main() {

	int a = 100;
	int b = 200;

	cout << max(a, b) << endl;

	system("pause");

	return 0;
}

函数的分文件编写

作用: 让代码结构更加清晰

函数分文件编写一般有 4 个步骤

  1. 创建后缀名为.h的头文件
  2. 创建后缀名为.cpp的源文件
  3. 在头文件中写函数的声明
  4. 在源文件中写函数的定义

示例:

步骤 1:定义头文件

1
2
3
4
5
6
7
//swap.h文件
#include<iostream>
using namespace std;

//实现两个数字交换的函数声明
void swap(int a, int b);

步骤 2:编写 cpp 源文件

1
2
3
4
5
6
7
8
9
10
11
12
//swap.cpp文件
#include "swap.h"

void swap(int a, int b)
{
	int temp = a;
	a = b;
	b = temp;

	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
}

步骤 3:main 函数

1
2
3
4
5
6
7
8
9
10
11
//main函数文件
#include "swap.h"
int main() {

	int a = 100;
	int b = 200;
	swap(a, b);

	system("pause");
	return 0;
}

函数参数

函数默认参数

在 C++ 中,函数的形参列表中的形参是可以有默认值的。

语法:返回值类型 函数名 (参数= 默认值){}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int func(int a, int b = 10, int c = 10) {
	return a + b + c;
}

// 1. 如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
// 2. 如果函数声明有默认值,函数实现的时候就不能有默认参数
int func2(int a = 10, int b = 10);
int func2(int a, int b) {
	return a + b;
}

int main() {
	cout << "ret = " << func(20, 20) << endl; // ret = 50
	cout << "ret = " << func(100) << endl; // ret = 120
	return 0;
}

函数占位参数

C++ 中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置

语法: 返回值类型 函数名 (数据类型) {}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 函数占位参数 ,占位参数也可以有默认参数
void func(int a, int) {
	cout << "this is func" << endl;
}

int main() {

	func(10, 10); //占位参数必须填补

	system("pause");

	return 0;
}

在 C++ 中,函数的占位参数是不能被直接使用的,它仅仅是用来占位置的;这里第二个参数是一个占位参数,没有变量名。在 func 函数内部,你无法获取到第二个参数的值,因为它没有被命名。占位参数通常用于特定的编操作统中,比如与某些 API 兼容性、函数签名匹配或者在未来的版本中保留参数位置。

C++ 可变参数

cstdarg 实现可变参数

C++ 中,可以使用 <cstdarg> 头文件中定义的宏,比如 va_startva_argva_end,来定义接受可变数量参数的函数。这些宏用于操作 va_list 类型的对象,它代表开始处理可变参数列表之前的参数。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <cstdarg>
int sum(int n, ...)
{
    int total = 0;
    va_list args;
    va_start(args, n);
    for (int i = 0; i < n; i++)
    {
        total += va_arg(args, int);
    }
    va_end(args);
    std::cout << total << std::endl;
    return total;
}
void test_var()
{
    sum(3, 1, 2, 3);             // 6
    sum(5, 1, 2, 3, 4, 5);       // 15
    sum(7, 1, 2, 3, 4, 5, 6, 7); // 28
}
可变参数模板 (C++ 11)

从 C++11 开始,C++ 引入了可变参数模板,它允许你为模板定义任意数量和任意类型的参数。

可变参数模板使用省略号 来定义参数包,实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

template<typename T, typename... Args>
void print(T firstArg, Args... args) {
   std::cout << firstArg << std::endl; // 打印第一个参数
   if constexpr(sizeof...(args) > 0) { // 如果还有剩余参数
		print(args...); // 递归调用,打印剩余参数
   }
}
int main() {
   print(1, 2.5, "hello", 'a'); // 打印多种类型的参数
   return 0;
}
可变参数宏 (C++ 11)

C++11 也支持可变参数宏,它允许定义接受可变数量参数的宏,通过使用省略号 VA_ARGS

下面是一个宏的例子,用于打印传入的参数:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#define PRINT_PARAMS(...) print(#__VA_ARGS__, __VA_ARGS__)

void print(const char* names, ...) {
   // 处理参数的实现
   // ...
}
int main() {
   PRINT_PARAMS(x, y, z); // 使用宏传递参数
   return 0;
}

C++ 函数参数传递

C++ 中,函数参数可以通过几种不同的方式传递,这些方式决定了是否会生成新的对象:

根据实际情况选择合适的参数传递方法,在不损失性能的同时实现所需的功能至关重要。

  • 对于大型或复杂对象,通常推荐使用引用指针传递,特别是当你不打算修改传入对象时,那么常量引用将是最好的选择。
  • 当你希望在函数内部拥有一个独立副本,或是在处理基本数据类型(如 int,double 等)时,传值则可能更加合适。
  • 在 C++11 及以后版本中,移动语义和右值引用的引入使得在某些情况下可以更高效地传递对象,尤其是当对象支持移动操作时。

如果可以的话,参数传递与返回值的传递尽量by reference

参数传递的三种方式,设计类成员函数时,要提前考虑好那些函数的数据会改变,如果不改变请加上 const

  • pass by value
  • pass by reference
  • pass by reference to const (推荐!)

返回值传递的时候,如果可以,建议使用 return by reference。C++11 的右值引用也要优先考虑

传值(Pass by Value)

当函数参数是按值传递的时候,会创建该参数的一个新副本。这个过程涉及调用对象的拷贝构造函数来生成新的对象。这意味着对于复杂数据类型或大型对象,传值由于拷贝操作的开销会导致性能问题。

1
2
3
void functionName(MyClass valueParameter) {
   // 在这里,valueParameter 是实参的一个拷贝
}

传值在函数内的改动不会影响原始对象。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass {
public:
    MyClass() {
        // 默认构造函数的实现
    }
    MyClass(const MyClass& other) {
        // 拷贝构造函数的实现
        // 这里会被调用来创建传值参数的副本
    }
};
void myFunction(MyClass byValueParam) {
    // 当调用 myFunction 时,byValueParam 是实参的拷贝,
    // 由 MyClass 的拷贝构造函数创建。
}
int main() {
    MyClass myObj;
    myFunction(myObj); // 这里调用 myFunction 时,myObj 被拷贝
    return 0;
}

在上述代码中,myFunction 以值的方式接收 MyClass 类型的参数 byValueParam。当 main 函数中的 myObj 实例传递给 myFunction 时,该对象的拷贝构造函数会被调用以创建 byValueParam 的副本。这个副本是一个完全独立的对象,与原始 myObj 对象在内存中位于不同的位置。

这使得按值传递在处理大型对象或有大量成员的对象时可能会影响性能,因为拷贝这样的对象可能非常昂贵。在这些情况下,通常推荐使用引用或指针传递,特别是可以使用 const 引用来避免不必要的拷贝,同时防止函数内部修改对象。

在 C++11 和更新的版本中,还可以使用移动语义来优化临时对象的按值传递,其通过移动构造函数而不是拷贝构造函数来创建对象的副本,从而避免了不必要的拷贝,提高了效率。

传引用(Pass by Reference

当函数参数是引用传递时,不会创建新的对象。相反,引用作为实参的别名存在,任何对引用的修改都会影响到原始对象。这种方式既避免了拷贝的开销,又允许函数内部修改实参的状态。

1
2
3
void functionName(MyClass& referenceParameter) {
   // referenceParameter 是对实参的引用,不会产生新对象
}
传常量引用(Pass by Const Reference

这种方式既避免了拷贝,又保护了实参不被修改。通常在不需要修改传入对象同时又想避免拷贝成本时,常量引用是更好的选择。

1
2
3
void functionName(const MyClass& constReferenceParameter) {
   // constReferenceParameter 是对实参的只读引用,不会产生新对象
}

传指针(Pass by Pointer

类似于传引用,传指针也不会创建新的对象,因为指针只是指向原始对象的地址。传指针允许函数内部修改指针指向的对象,但需要注意空指针悬空指针的风险。

1
2
3
void functionName(MyClass* pointerParameter) {
   // pointerParameter 是指向实参的指针,不会产生新对象
}

传右值引用(Pass by Rvalue Reference)(C++11 及以后)

右值引用用于实现移动语义完美转发。它允许函数接收一个临时对象(即将被销毁的对象)的所有权,这通常涉及选择性地转移资源而无须进行拷贝。

1
2
3
void functionName(MyClass&& rvalueReferenceParameter) {
   // rvalueReferenceParameter 可以访问实参的资源而无需进行拷贝
}

# 指针、引用值和值作为形参互相传递

在 C++ 中,函数参数可以是值、指针或引用。以下是如何在不同类型的参数之间进行传递的示例:

  • 从指针到引用

如果你有一个指针并且想传递给一个需要引用的函数,你可以解引用指针 (*) 来传递对象的引用。

1
2
3
4
5
6
7
8
9
void funcByRef(int& ref) {
    // 使用 ref...
}
void example() {
    int value = 10;
    int* ptr = &value;

    funcByRef(*ptr); // 解引用 ptr 并传递引用
}
  • 从引用到指针

如果你有一个引用并且想传递给一个需要指针的函数,你可以使用取地址运算符 (&) 来获取引用对象的地址。

1
2
3
4
5
6
7
8
9
10
void funcByPtr(int* ptr) {
    // 使用 ptr...
}
void example() {
    int value = 10;
    int& ref = value;

    funcByPtr(&ref); // 获取 ref 的地址并传递指针
}

  • 值到指针或引用

如果你有一个变量值并且想作为指针或引用传递,你需要获取它的地址或者直接传递变量。

1
2
3
4
5
void example() {
    int value = 10;
    funcByRef(value); // 直接传递引用
    funcByPtr(&value); // 传递值的地址
}

需要注意的是,在 C++ 中,传递引用是安全的,因为引用必须被初始化且不能是 nullptr。然而,传递指针时,应当确保指针不是空指针,且指向的是有效的内存地址,否则在函数内部解引用无效指针将会导致未定义行为(包括程序崩溃)。确保你的代码在解引用之前检查了指针的有效性。

函数重载

作用: 函数名可以相同,提高复用性

函数重载满足条件:

  • 同一个作用域下
  • 函数名称相同
  • 函数参数类型不同或者个数不同或者顺序不同

注意: 函数的返回值不可以作为函数重载的条件

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 函数重载需要函数都在同一个作用域下
void func()
{
	cout << "func 的调用!" << endl;
}
void func(int a)
{
	cout << "func (int a) 的调用!" << endl;
}
void func(double a)
{
	cout << "func (double a)的调用!" << endl;
}
void func(int a ,double b)
{
	cout << "func (int a ,double b) 的调用!" << endl;
}
void func(double a ,int b)
{
	cout << "func (double a ,int b)的调用!" << endl;
}

//函数返回值不可以作为函数重载条件
//int func(double a, int b)
//{
//	cout << "func (double a ,int b)的调用!" << endl;
//}


int main() {
	func();
	func(10);
	func(3.14);
	func(10,3.14);
	func(3.14 , 10);
	return 0;
}

示例 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 函数重载注意事项
// 1、引用作为重载条件

void func(int &a)
{
	cout << "func (int &a) 调用 " << endl;
}

void func(const int &a)
{
	cout << "func (const int &a) 调用 " << endl;
}


//2、函数重载碰到函数默认参数

void func2(int a, int b = 10)
{
	cout << "func2(int a, int b = 10) 调用" << endl;
}

void func2(int a)
{
	cout << "func2(int a) 调用" << endl;
}

int main() {
	
	int a = 10;
	func(a); // 调用无const
	func(10); // 调用有const


	// func2(10); // 碰到默认参数产生歧义,需要避免

	system("pause");

	return 0;
}

lambda

lambda 定义

官方参考网站:https://en.cppreference.com/w/cpp/language/lambda

lambda 本质上是一个普通的函数,只是它不像普通函数这样声明,它是我们的代码在过程中生成的,用完即弃的函数,不算一个真正的函数,是匿名函数 。

格式:

1
2
[] ({形参表}) {函数内容}
// 中括号表示的是捕获,作用是如何传递变量 lambda使用外部(相对)的变量时,就要使用捕获。

lambda 捕获

如何使用捕获?

  • 添加头文件: #include <functional>
  • 修改相应的函数签名 std::function <void(int)> func 替代 void(*func)(int)
  • 捕获 [] 使用方式:

[=],则是将所有变量值传递到 lambda 中 [&],则是将所有变量引用传递到 lambda 中 [a],是将变量 a 通过值传递,如果是 [&a] 就是将变量 a 引用传递,它可以有 0 个或者多个捕获

示例 1:C 语言式函数指针接收 lambda:没有带捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "LambdaDemo.h"
#include <vector>
#include <iostream>
#include <functional>

void for_each(const std::vector<int>& values, void(*function)(int))
{
	for (int temp : values) {
		function(temp); // 调用lambda函数
	}
}


void LambdaDemo::test_lambda()
{
	std::vector<int> values = { 1,2,3,4,5 };

	auto lambda = [](int val) {std::cout << val << std::endl; }; // ok
	
	// 使用 std::function 替代 auto
	std::function<void(int)> lambda = [](int val) {std::cout << val << std::endl; }; // error,不存在从std::function<void(int)>到void(*function)(int)的适当转换函数

	for_each(values, lambda);

}

示例 2:捕获外部变量

lambda 可以使用外部(相对)的变量,而 [] 就是表示打算如何传递变量,要用捕获就必须要用 C++ 新的函数指针,不用用 C 语言的函数指针了,否则 lambda 转不了函数指针,编译出错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <vector>
#include <iostream>
#include <functional>
void ForEach2(const std::vector<int>& values, const std::function<void(int)>& func) {
	for (int temp : values) {
		func(temp);
	}
}
void test2() {
	std::vector<int> values = { 1, 2, 3, 4, 5 };
	//注意这里的捕获必须要和C++新带的函数指针关联起来!!!
	int a = 5;  //如果lambda需要外部的a向量
	//则在捕获中写入a就好了
	auto lambda = [a](int val) { std::cout << a << std::endl; };
	ForEach2(values, lambda);
}

我们有一个可选的修饰符mutable,它允许 lambda 函数体修改通过拷贝传递捕获的参数。若我们在 lambda 中给 a 赋值会报错,需要写上 mutable

1
2
3
4
auto lambda = [a](int val) mutable {
	a = 5;
	std::cout << a << std::endl;
	};

lambda 使用场景

使用 lambda 的场景 find_if

find_if 是一个搜索类的函数,区别于 find 的是:它可以接受一个函数指针来定义搜索的规则,返回满足这个规则的第一个元素的迭代器。这个情况就很适合 lambda 表达式的出场了

1
2
3
4
5
6
7
8
9
10
#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> values = { 1, 5, 3, 4, 2 };
    //下面就用到了lambda作为函数指针构成了find_it的规则
    auto it = std::find_if(values.begin(), values.end(), [](int value) { return value > 3; });  //返回第一个大于3的元素的迭代器 
    std::cout << *it << std::endl;  //将其输出
}

C++ 中如何处理多返回值

方法一:通过函数参数传引用或指针的方式

把函数定义成 void,然后通过参数引用传递的形式 “ 返回 “ 两个字符串,这个实际上是修改了目标值,而不是返回值,但某种意义上它确实是返回了两个字符串,而且没有复制操作,技术上可以说是很好的。但这样做会使得函数的形参太多了,可读性降低,有利有弊。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "ReturnMultiDemo.h"
#include <iostream>
using namespace std;

void GetUserAge(const string& user_name, bool& work_status, int& age)
{
	if (user_name.compare("xiaoli") == 0)
	{
		work_status = true;
		age = 18;
	}
	else
	{
		work_status = false;
		age = -1;
	}
};
void ReturnMultiDemo::testReturnMulti0()
{
	bool work_status = false;
	int age = -1;
	GetUserAge("xiaoli", work_status, age);
	std::cout << "查询结果:" << work_status << "    " << "年龄:" << age << std::endl;
	getchar();

}

方法二: 通过函数的返回值是一个 array(数组)或 vector

当然,这里也可以返回一个 vector,同样可以达成返回多个数据的目的。

不同点是 Array 是在栈上创建,而 vector 会把它的底层储存在堆上,所以从技术上说,返回 Array 会更快

但以上方法都只适用于相同类型的多种数据的返回

1
2
3
4
5
6
7
8
9
10
11
//设置是array的类型是stirng,大小是2
std::array<std::string, 2> ChangeString() {
    std::string a = "1";
    std::string b = "2";

    std::array<std::string, 2> result;
    result[0] = a;
    result[1] = b;
    return result;
    // 也可以return std::array<std::string, 2>(a, b);
}

方法三:使用 std::pair 返回两个返回值

可以返回两个不同类型的数据返。
使用 std::pair 这种抽象数据结构,该数据结构可以绑定两个异构成员。这种方式的弊端是只能返回两个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
std::pair<bool, int> GetUserAge(const std::string& user_name)
{
    std::pair<bool, int> result;
    if (user_name.compare("xiaoli") == 0)
    {
        result = std::make_pair(true, 18);
    }
    else
    {
        result = std::make_pair(false, -1);
    }
    return result;
}
int main()
{
    std::pair<bool, int> result = GetUserAge("xiaolili");
    std::cout << "查询结果:" << result.first << "   " << "年龄:" << result.second << std::endl;
    getchar();
    return 0;
}

方法四:使用 std::tuple 返回三个或者三个以上返回值

std::tuple 这种抽象数据结构可以将三个或者三个以上的异构成员绑定在一起,返回 std::tuple 作为函数返回值理论上可以返回三个或者三个以上的返回值。
tuple 相当于一个类,它可以包含 x 个变量,但他不关心类型,用 tuple 需要包含头文件 #include <tuple>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <tuple>

std::tuple<bool, int,int> GetUserAge(const std::string& user_name)
{
    std::tuple<bool, int,int> result;
    if (user_name.compare("xiaoli") == 0)
    {
        result = std::make_tuple(true, 18,0);
    }
    else
    {
        result = std::make_tuple(false, -1,-1);
    }
    return result;
}

int main()
{
    std::tuple<bool, int,int> result = GetUserAge("xiaolili");
    bool work_status;
    int age;
    int user_id;

    std::tie(work_status, age, user_id) = result;
    std::cout << "查询结果:" << work_status << "    " << "年龄:" << age <<"   "<<"用户id:"<<user_id <<std::endl;
    getchar();
    return 0;
}

方法五:返回一个结构体 (推荐)

结构体是在栈上建立的,所以在技术上速度也是可以接受的
而且不像用 pair 的时候使用只能 temp.first, temp.second,这样不清楚前后值是什么,可读性不佳。而如果换成 temp.str, temp.val 后可读性极佳,永远不会弄混

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
struct result {
    std::string str;
    int val;
};
result Function () {
    return {"1", 1};//C++新特性,可以直接这样子让函数自动补全成结构体
}
int main() {
    auto temp = Function();
    std::cout << temp.str << ' ' << temp.val << std::endl;
}

#include <iostream>
using namespace std;

struct Result
{
    int add;
    int sub;
};

Result operation(int a, int b)
{
    Result ret;
    ret.add = a + b;
    ret.sub = a - b;
    return ret;
}

int main()
{
    Result res;
    res = operation(5, 3);
    cout << "5+3=" << res.add << endl;
    cout << "5-3=" << res.sub << endl;
}

拓展:

C++ 函数:std::tie 详解

方法六:C++ 的结构化绑定

C++17 引入的新特性

本文由作者按照 CC BY 4.0 进行授权