ThankNeko's Blog ThankNeko's Blog
首页
  • 操作系统

    • Linux基础
    • Linux服务
    • WindowsServer笔记
    • Ansible笔记
    • Shell笔记
  • 容器服务

    • Docker笔记
    • Kubernetes笔记
    • Git笔记
  • 数据库服务

    • MySQL笔记
    • ELK笔记
    • Redis笔记
  • 监控服务

    • Zabbix笔记
  • Web服务

    • Nginx笔记
    • Tomcat笔记
  • 数据处理

    • Kettle笔记
  • Python笔记
  • Bootstrap笔记
  • C笔记
  • C++笔记
  • Arduino笔记
  • 分类
  • 标签
  • 归档
  • 随笔
  • 关于
GitHub (opens new window)

Hoshinozora

尽人事,听天命。
首页
  • 操作系统

    • Linux基础
    • Linux服务
    • WindowsServer笔记
    • Ansible笔记
    • Shell笔记
  • 容器服务

    • Docker笔记
    • Kubernetes笔记
    • Git笔记
  • 数据库服务

    • MySQL笔记
    • ELK笔记
    • Redis笔记
  • 监控服务

    • Zabbix笔记
  • Web服务

    • Nginx笔记
    • Tomcat笔记
  • 数据处理

    • Kettle笔记
  • Python笔记
  • Bootstrap笔记
  • C笔记
  • C++笔记
  • Arduino笔记
  • 分类
  • 标签
  • 归档
  • 随笔
  • 关于
GitHub (opens new window)
  • Python笔记

  • C笔记

  • C++笔记

    • C++基本介绍
    • C++基本介绍
    • C++字符串与向量与数组
    • C++字符串与向量与数组
    • C++语句
    • C++函数
      • 函数
        • 介绍
        • 使用函数
        • 全局对象
        • 局部对象
        • 函数声明
        • 参数传递
        • 指针或引用形参的const
        • 数组形参
        • 数组引用形参
        • initializer_list类型
        • return语句
        • main函数参数
        • 函数重载
        • 默认实参
        • 内联函数
        • 函数指针
    • C++类
    • C++标准库
  • Arduino笔记

  • Dev
  • C++笔记
Hoshinozora
2024-05-24
目录

C++函数

# 函数

# 介绍

函数是一个命名的代码块,通过调用函数可以执行对应的代码,函数还可以有零个或多个参数,且通常还会有一个结果(返回值)。函数可以重载,也就是说同一个名称可以被多个函数使用。

一个典型的函数定义包括返回值类型、函数名称、零个或多个形参、函数体,函数体即调用函数时会执行的语句块。 调用函数需要使用调用运算符"()"一对圆括号,该运算符作用于函数或指向函数的指针,圆括号内可以有零个或多个实参,实参用于初始化函数的形参。调用表达式的类型就是函数的返回类型。

函数调用时,会使用实参初始化函数对应的形参,并将控制权从主调函数转移给被调函数,此时主调函数的指向被暂时中止,被调函数开始执行。

编程中,形参一般使用(param)eter表示,实参一般使用(arg)ument表示。

# 使用函数

格式:

// 定义函数
return-type name(param-type name,...){
    function-statements
    return expression;
}

// 调用函数
function-name(arg-value,...);
// 调用函数并接收函数返回值
return-type name = function-name(arg-value,...);
1
2
3
4
5
6
7
8
9
10

例如:

int addon(int a, int b){
    return a + b;
}
int main(){
    // 输出5
    cout << addon(2, 3) << ends;
    return 0;
}
1
2
3
4
5
6
7
8

# 全局对象

在C++中,名称有作用域,名称仅在作用域内可见。对象有生命周期,生命周期是程序执行过程中该对象存在的一段时间。 在所有函数体之外定义的对象会作用于整个程序,因此这些对象被称为全局对象,在整个程序中可见。这些全局对象随着程序的执行而创建,直到程序结束才会销毁。

# 局部对象

# 介绍

函数体是一个语句块,块会构成一个新的作用域,因此函数形参和函数内部定义的变量被称为局部变量,局部变量对应着局部对象,仅在函数作用域内可见。

局部对象又分为自动对象和局部静态对象,两者的区别是生命周期不同。

# 自动对象

自动对象是只存在于语句块执行期间的对象,当语句块执行结束后,块中的自动对象就会自动销毁。 形参对应的就是自动对象,函数开始时为其申请内存空间,函数终止时进行销毁。 普通局部变量也是自动对象,在变量定义时进行创建,函数终止时进行销毁。

# 局部静态对象

局部静态对象可以令局部变量的生命周期延长到程序结束才销毁,但仍只能作用于所在作用域中。 定义局部静态对象需要使用static关键字,来将局部变量定义为static类型,定义格式:static type name; 例如:

int count_call() {
    // 给定初始值则使用初始值进行初始化,否则使用默认初始化。
    static int count = 1;
    // 每次函数结束返回count,然后count局部变量进行自增。
    return count++;
}

int main() {
    for (int i = 0; i < 10; i++) {
        // 每次调用,其中count的值都不会重置,因为count是局部静态对象。
        cout << count_call() << endl;
    }
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 函数声明

# 介绍

函数声明又称函数原型,用于描述函数的接口所需的全部信息,也就是函数的三要素:返回类型、函数名、形参类型。 函数只能定义一次,但可以声明多次。函数声明和函数定义非常相似,区别是函数声明无需函数体,而函数定义需要函数体。

虽然函数声明写在源代码效果一样,但如果将函数声明放在头文件中,声明就能被多个源文件共享使用,可以提高可复用性、减少代码冗余,尤其是在大型项目中。

另外不将函数直接定义在头文件中,是因为如果将函数定义写在头文件中,该头文件中的函数定义会被包含进所有用到它的源文件,编译时间可能会显著增加。

例如:

void print(vecor<int>::const_iterator beg, vecor<int>::const_iterator end);
1

尽管函数声明中形参名不是必须的,但仍然可以写上形参名以供使用者能够更好理解寒素功能。

# 分离式编译

函数声明的作用会在分离式编译时有所体现。在C++中编译器是分离式编译的,在编译一个源文件时,它并不需要知道所调用函数的具体实现,只需要知道函数的接口信息即可。

函数声明的目的就是向编译器提供这些接口信息,使编译器能在编译调用函数的代码时进行类型检查和参数匹配。

分离式编译时,源代码A会通过链接器来找到源代码B的函数定义,链接时链接器会在所有目标文件中查找符号表,以找到源代码A所调用的函数名的函数定义,具体例子:

  1. 我们先将函数声明在functions.h头文件中。
#pragma once
int add(int a, int b);
1
2
  1. 然后将函数定义在functions.cpp源文件中。
// 函数定义
int add(int a, int b) {
    return a + b;
}
1
2
3
4
  1. 然后在其他源文件中调用函数即可。
#include <iostream>
// 导入函数声明,以便编译器能够进行类型检查
#include "functions.h"

int main() {
    int r = add(3, 4);
    std::cout << r << std::endl;
    return 0;
}
1
2
3
4
5
6
7
8
9

# 参数传递

每次调用函数时,都会重新创建它的形参,形参可以是引用类型,形参名将绑定到对应实参上,调用时称实参为引用传递或引用调用。 如果形参不是引用类型则会将传入的实参值拷贝给形参进行初始化,调用时称实参为值传递或传值调用。

拷贝较大的类型对象或容器对象时,传递效率是比较低的,并且有些类型根本不支持拷贝操作。此时就可以使用引用而不通过拷贝传递对象,还能提高传递效率。 另外如果函数无需改变引用形参的值,最好将其声明为常量引用。 例如:

bool isShorter(const string &s, const string &2){
    return s1.size() < s2.size();
}
1
2
3

# 指针或引用形参的const

我们可以使用在声明指针或引用形参时使用const,此时会将形参初始化成底层const对象。也就是可以改变指针本身,但不能改变指针所指对象的值。

# 数组形参

因为数组不允许拷贝、且使用数组时会将其转换成指针,所以我们无法以值传递的方式使用数组参数。所以我们传递数组给函数,实际上传递指向数组首元素的指针。 尽管不允许将数组作为值传递,但形参仍然可以写成数组的形式:

// 以下三种方式完全一样,都是接收数组的首指针
void print(const int* arrPtr);
// 表示要一个数组
void print(const int arrPtr[]);
// 表示要一个数组,期待里面有10个元素,并不做实际约束
void print(const int arrPtr[10]);
1
2
3
4
5
6

传递数组参数一般有如下三种方式:

// 第一种方式:使用结束标记迭代数组。
// 要求数组本身包含一个结束标记作为结尾,例如char的空字符\0标记。
void print(const char* cp) {
    // 如果cp为空,则直接返回
    if (!cp) { return; }
    // 解引用指针,如果指针不是\0空字符,则循环输出遇到空指针。
    while (*cp) {
        cout << *cp++;
    }
}
int main() {
    char content[] = "Hello, World!";
    print(content);
    return 0;
}


// 第二种方式:使用标准库函数传递数组首指针和尾后指针。
void print(const int *beg, const int *end) {
    while (beg != end){
        cout << *beg++ << endl;
    }
}
int main() {
    int a[] = {1, 2, 3, 6, 9, 20};
    print(std::begin(a), std::end(a));
    return 0;
}


// 第三种方式:传递一个表示数组大小的形参。
void print(const int arr[], size_t size) {
    for (size_t i = 0; i != size; i++) {
        cout << arr[i] << endl;
    }
    return;
}
int main() {
    int a[] = { 1, 2, 3, 6, 9, 20 };
    print(a, std::end(a) - std::begin(a));
    return 0;
}
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
40
41
42

# 数组引用形参

形参的引用传递也可以用在数组上,将引用绑定到数组实参,但形参必须指定大小,例如:

// 形参是数组的引用,维度是类型的一部分,因此必须传入10大小的数组。
void print(const int (&arr)[10]){
    // 因为是数组的引用所以可以像数组一样使用范围for循环。
    for(auto i : arr){
        cout << i << endl;
    }
}
int main() {
    int a[] = { 1, 2, 3, 6, 9, 20, 33, 44, 88, 99 };
    print(a);
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12

# initializer_list类型

initializer_list类型是初始化列表,它是一种标准库类型,用于表示某种特定类型值的数组,该类型一旦被初始化,其中的元素值就不可变,它的底层实际就是常量数组。

如果一个函数的某个形参要传入的实参数量不定但实参类型都相同,就可以使用initializer_list类型。如果实参数量不定且实参类型也不同,则可以使用可变参数模板。

它定义在initializer_list头文件的std命名空间中,使用方式如下:

// 定义一个名为lst、元素类型为T的空列表。
initializer_list<T> lst;
// 列表初始化。
initializer_list<T> lst{a,b,c...};
// 元素拷贝初始化。
initializer_list<T> lst2(lst);
// 赋值会使原始列表lst和副本列表lst2共享元素
initializer_list<T> lst2 = lst;
// 返回列表元素数量
lst.size();
// 返回列表首元素的指针
lst.begin();
// 返回列表的尾后指针
lst.end();
1
2
3
4
5
6
7
8
9
10
11
12
13
14

使用例子:

#include <iostream>
#include <string>
#include <initializer_list>

using std::cout;
using std::endl;
using std::string;
using std::initializer_list;

// initializer_list类型可以直接作为参数传递,不需要使用指针或引用方式。
void print_msgs(initializer_list<string> il) {
    // 因为是直接作为参数传递,所以可以使用它的begin()和end()方法,因此可以使用范围for循环。
    for (const string &i : il) {
        cout << i << endl;
    }
}

int main() {
    // 可以直接传递序列,使用花括号扩起。
    print_msgs({ "Hello", "World!" });
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

注意:initializer_list不能作为返回类型,试了会报错,要想返回列表可以使用vector类型。

# return语句

# 返回介绍

return语句会终止当前正在执行的函数,并将控制权返回给调用该函数的地方。

执行函数时会在调用点生成一个临时量,函数执行返回的结果会用于初始化该临时量。

return语句的格式:

// 没有返回值的return只能用在返回类型是void的函数中
// void类型函数如果没有写,则会隐式在最后添加"return;"
return;
// 返回的表达式结果的类型,必须与返回类型相同。
return expression;
1
2
3
4
5

注意:不要返回局部对象的引用或指针,函数结束后局部对象会被释放,因此指向他们的引用或指针将变得无效。

# 返回引用

如果返回值的是引用,则可以将函数调用作为赋值表达式的左值使用,此时会将引用的对象的值,修改为右值。例如:

char& first(string& val) {
    return val[0];
}
int main() {
    string s = "123";
    first(s) = 'A';
    // 输出A23
    cout << s << endl;
    return 0;
}
1
2
3
4
5
6
7
8
9
10

注意:返回的引用的对象不能是常量。

# 返回值列表

返回值还可以是列表类型,此时我们可以使用vector作为返回类型。例如:

vector<string> func(){
    return {"Hello", "World!"};
}
int main(){
    for (auto i : func()){
        cout << i << endl;
    }
    return 0;
}
1
2
3
4
5
6
7
8
9

# 返回数组

C++不能直接返回数组,但我们可以返回数组的指针,以下方式可以实现:

  1. 使用类型别名,指定别名的数组类型、长度。
// 定义类型别名,同等于using arrT = int[5];
typedef int IntArray[5];
// 表示返回类型是一个int[5]数组的指针。
IntArray* createArray() {
    // 函数返回时数组被销毁,所以数组要定义为静态对象。
    static int arr[5] = {1, 2, 3, 4, 5};
    return &arr;
}
int main() {
    // 调用函数并使用返回的数组。
    IntArray* arr = createArray();
    for (int i = 0; i < 5; ++i) {
        // 因为是数组指针,所以要解引用。
        std::cout << (*arr)[i] << std::endl;
    }
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  1. 函数直接声明返回的是数组类型,格式:arr_type (*func_name(parm_list))[arr_length];
// 表示返回类型是一个int[5]数组的指针。
int(*createArray(int i))[5] {
    static int arr[5] = { 1, 2, 3, 4, 5 };
    arr[4] = i;
    return &arr;
}
int main() {
    int (*arr)[5] = createArray(9);
    for (int i = 0; i < 5; ++i) {
        std::cout << (*arr)[i] << " ";
    }
    std::cout << std::endl;
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. 使用置尾返回类型,将返回类型声明在形参列表后面的方式被称为置尾返回类型,该方式适用于所有返回类型,但对于返回类型较为复杂的函数最有效,格式:auto func_name(parm_list) -> arr_type(*)[arr_length];
// 表示返回类型是一个int[5]数组的指针。
auto createArray(int i) -> int(*)[5] {
    static int arr[5] = { 1, 2, 3, 4, 5 };
    arr[4] = i;
    return &arr;
}
int main() {
    int (*arr)[5] = createArray(9);
    for (int i = 0; i < 5; ++i) {
        std::cout << (*arr)[i] << " ";
    }
    std::cout << std::endl;
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# main函数参数

# 接收参数

在我们需要给main传递实参时,需要让main函数接收两个形参,第一个形参表示数组中字符串的数量,第二个形参是一个字符串类型的数组。声明两个形参后,我们就能通过命令行传递实参给main函数了。 argv的第一个元素必然指向程序文件名或空字符串,后面的元素才是传入的实参。 格式:

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

# 返回值

main函数是唯一可以不指定return的非void函数,main函数中如果没有return语句,则会在末尾隐式插入一条返回0的return语句。虽然如此,但还是建议书写上更直观。

一般main函数返回0表示程序正常执行,返回非0为执行不成功。

# 函数重载

# 介绍

如果在同一作用域中的几个函数名称相同,但形参列表不同,我们称之为重载函数。在调用重载函数时,编译器会根据我们传入的实参类型来推断我们想要调用的函数。

函数重载可以减轻程序员起名字、记名字的负担。

注意:

  1. main函数不能重载,一个项目中只能有一个main函数,用作程序入口。

  2. 函数重载不允许两个函数的形参列表的类型、数量、位置完全一致。

  3. 给函数起不同的名称可以使程序更易理解,所以对于功能不同的函数不应该使用重载。

# 定义

例子一:一个函数,多种输出方式。

void print(const int *beg, const int *end);
void print(const string s);

// 会调用上面第一个print函数定义
int a[3] = {1, 2, 3};
print(begin(j), end(j));
// 会调用上面第二个print函数定义
print("Hello");
1
2
3
4
5
6
7
8

例子二:数据库查询,根据传入数据类型执行不同的数据查询。

// 根据Account查找记录
Record lookup(const Account&);
// 根据Account查找记录
Record lookup(const Phone&);

Account acct;
Phone phone;
Record r1 = lookup(acct);
Record r2 = lookup(phone);
1
2
3
4
5
6
7
8
9

# 重载与作用域

如果在内层作用域中定义了外层中的函数名,则外层的函数名会被隐藏,在内层中仅使用内层的名称。

void print(string);
void print(double);
void print(int);
{
    // 调用外层的void print(string);
    print("hello");
    
    // 新作用域:隐藏外层的print名称
    void print(int);
    
    // 会报错,因为外层的print被隐藏了,内层没有匹配string类型的print函数
    print("hello");
    
    // 正确,能匹配内层的print(int)
    print(1);
    
    // 正确,能匹配内层的print(int),值会被隐式转换
    print(3.14);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 默认实参

默认实参用于定义形参的默认值,定义了默认值的形参在函数调用时,可以传入指定实参,也可以忽略不传入,不传入时会使用默认值作为实参。

默认实参也是定义在形参列表中,如果某个形参定义了默认实参,则该形参之后的形参也必须指定默认值。默认值不能是局部变量,但可以是表达式,只要表达式结果是形参类型即可。

调用函数会按照位置进行传参,不能跳过传入某个实参使用默认值,只能忽略尾部的实参。

例如:

// 定义
int add(int a, int b = 1, int c = 2);

// 调用,形参对应为a=5,b=1,c=2
add(5);
// 调用,形参对应为a=5,b=4,c=2
add(5,4);
// 错误,不能跳过传入某个实参使用默认值,只能忽略尾部的实参
add(1,,3);

// 定义,默认值可以是表达式
int add(int a = 1 + 2, int b = get_a());
1
2
3
4
5
6
7
8
9
10
11
12

# 内联函数

调用函数会有开销,在调用时计算机会操作寄存器保存调用时位置、返回调用时位置。而内联函数可以避免该类开销。编译器会将内联函数代码直接在调用代码处进行展开。

但很多编译器都不支持内联函数,且函数过大的话,内联函数也不太好展开。因此一般仅用于较为简单的函数使用。

使用例子:

// 在函数定义开头使用inline关键字即可
inline int big(int a, int b){
    return a > b ? a : b;
}
int main(){
    cout << big(1,2) << endl;
    // 以上会被编译器编译成如下
    cout << (1 > 2 ? 1 : 2) << endl;
    return 0;
}
1
2
3
4
5
6
7
8
9
10

constexpr函数

constexpr函数是指能用于常量表达式的函数,该类函数的返回类型以及所有形参类型都必须是字面值类型,且函数体必须有且只能有一条return语句。

constexpr函数的返回值不一定要是常量,但是如果将constexpr函数用在常量表达式,则必须返回常量。

字面值类型也就是算数类型、引用、指针、字符串序列等类型。

// 定义一个constexpr函数
constexpr int new_sz() {return 233;}
// constexpr表达式能够使用constexpr函数进行初始化。
constexpr int foo = new_sz();
1
2
3
4

# 函数指针

指向函数的指针被称为函数指针,函数指针指向函数而非对象。

函数指针的指针类型由函数的返回类型决定。另外还需要指定所指函数的形参列表,形参列表可以用来区别要使用哪个重载函数。

定义格式:func_ptr_type (*name)(param_list);

例如:

bool less(int a,int b){return a < b;}
// 定义一个未初始化的返回布尔类型的函数的指针,它需要两个int参数。
bool (*fp)(int,int);
// 初始化函数指针,&取地址符可选,写不写都行。
fp = less;
// 通过函数指针调用函数,*解引用符可选,写不写都行。
fp(1,2);
1
2
3
4
5
6
7

函数指针也可以作为参数,作为参数时可以直接显式书写在形参列表,也可以使用typedef为函数指针声明类型别名,然后形参写类型别名。

// 作为参数显式书写时可以写(*fp)()也可以直接写fp()
void useFunc(bool fp(int a,int b));

// 使用类型别名创建函数类型别名
typedef bool Func(int,int);
void useFunc(Func fp);

// 使用类型别名创建函数指针类型别名
typedef bool (*FuncP)(int,int);
void useFunc(FuncP fp);
1
2
3
4
5
6
7
8
9
10

函数指针也可以作为返回值,使用类型别名即可。

// 返回函数指针的函数。
auto Func(int) -> int(*)(int,int);
// 给返回的函数指针类型创建别名。
typedef bool (*FuncP)(int,int);
// 使用函数指针类型别名作为类型接收返回值
FuncP f1(int);
1
2
3
4
5
6
#C++#函数
C++语句
C++类

← C++语句 C++类→

最近更新
01
二〇二五年四月十七日随笔
04-17
02
二〇二五年四月十六日随笔
04-16
03
二〇二五年四月九日随笔
04-09
更多文章>
Theme by Vdoing | Copyright © 2022-2025 Hoshinozora | MIT License
湘ICP备2022022820号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式