目录
[1.1 函数的基本概念](#1.1 函数的基本概念)
[1.2 C 语言函数和 C++ 函数的区别](#1.2 C 语言函数和 C++ 函数的区别)
[2.1 函数定义语法](#2.1 函数定义语法)
[2.2 函数调用语法](#2.2 函数调用语法)
[2.3 函数声明和函数定义的区别](#2.3 函数声明和函数定义的区别)
[(5)为什么需要 extern?](#(5)为什么需要 extern?)
[3.1 有返回值函数](#3.1 有返回值函数)
[3.2 无返回值函数](#3.2 无返回值函数)
[3.3 auto 自动推导返回值](#3.3 auto 自动推导返回值)
[3.4 return 只能返回一个值](#3.4 return 只能返回一个值)
[3.5 返回多个值的三种方法](#3.5 返回多个值的三种方法)
[(3)使用 vector、pair、tuple 等类型。](#(3)使用 vector、pair、tuple 等类型。)
[4.1 形参和实参的关系](#4.1 形参和实参的关系)
[4.2 main 函数中的 w 和 h](#4.2 main 函数中的 w 和 h)
[4.3 SetSize 函数中的 w 和 h](#4.3 SetSize 函数中的 w 和 h)
[4.4 为什么两个地址不一样](#4.4 为什么两个地址不一样)
[4.5 栈区进栈和出栈过程](#4.5 栈区进栈和出栈过程)
[5.1 完整代码:](#5.1 完整代码:)
[5.2 局部变量](#5.2 局部变量)
[5.3 局部静态变量](#5.3 局部静态变量)
[5.4 全局变量](#5.4 全局变量)
[5.5 静态全局变量](#5.5 静态全局变量)
[5.6 四种变量对比总结](#5.6 四种变量对比总结)
文章摘要
本篇文章进入 C++ 第五章函数学习,主要围绕以下内容展开:
- 为什么需要函数
- 函数如何定义和调用
- 函数参数和返回值如何传递
- 函数调用时栈区内存如何变化
- 局部变量和全局变量的作用域与生命周期
通过 View、SetSize、GetPosX、TestVer 等代码示例,分析函数在程序中的执行过程,并重点解释形参和实参的复制关系、函数返回值、局部静态变量、全局变量和静态全局变量的区别,为后续多文件编译和模块化开发打基础。


一、为什么需要函数
当程序变复杂以后,如果所有代码都写在 main 函数中,会出现几个问题:
- 代码越来越长,可读性变差;
- 相同功能反复写,重复代码太多;
- 修改一个功能时,容易影响其他代码;
- 不方便后续进行多文件编译和工程化开发。
所以从第五章开始,就要引入一个非常重要的思想:把重复代码封装成函数,把复杂程序拆分成多个模块。
1.1 函数的基本概念
函数可以理解为:把一段代码封装起来,并给这段代码起一个名字,后面需要使用时直接调用这个名字即可。
例如:
cpp
bool View(int index)
{
if (index < 0)
return false;
cout << "call View(" << index << ")" << endl;
return true;
}
这个函数的名字叫 View,它的作用是接收一个整数 index,然后判断并输出信息,最后返回一个 bool 类型的结果。
以后只要写:
auto re = View(1024);
程序就会跳转到 View 函数内部执行。
1.2 C 语言函数和 C++ 函数的区别
(1)C语言
在C 语言中,一般情况下,同一个作用域内函数名不能重复。
例如下面这种写法在 C 语言中是不允许的:
void Test(int x)
{
}
void Test(double x)
{
}
因为它们名字都叫 Test。
(2)C++
但是在 C++ 中,这种写法是允许的,只要函数的参数类型或者参数数量不同即可。这叫做:
函数重载
例如:
cpp
void Test(int x)
{
cout << "Test(int)" << endl;
}
void Test(double x)
{
cout << "Test(double)" << endl;
}
void Test(int x, int y)
{
cout << "Test(int, int)" << endl;
}
调用时:
cpp
Test(10); // 调用 Test(int)
Test(3.14); // 调用 Test(double)
Test(10, 20); // 调用 Test(int, int)
虽然它们在代码中都叫 Test,但是 C++ 编译器在底层会根据参数类型、参数数量等信息生成不同的函数符号名。也就是说,表面上名字相同,底层并不是完全一样的名字。
这也是 C++ 支持函数重载的重要原因。
(3)类和对象及成员函数简述
后面学习类和对象时,还会看到**成员函数**,例如:
cpp
class Student
{
public:
void Show()
{
cout << "Student Show" << endl;
}
};
我们看到的函数名是 Show,但在底层,它会和类名 Student 关联起来。也就是说,机器最终识别的并不只是一个简单的 Show,而是能映射到具体类和具体方法。
二、函数的定义、声明与调用
2.1 函数定义语法
函数定义的基本格式如下:
bash
返回值类型 函数名(形参列表)
{
函数体;
}
例如:
cpp
bool View(int index)
{
if (index < 0)
return false;
cout << "call View(" << index << ")" << endl;
return true;
}
其中:
bool
表示函数返回值类型。
View
表示函数名称。
int index
表示函数参数,也叫形参。
{
...
}
里面的内容就是函数体。
2.2 函数调用语法
函数定义好以后,可以通过函数名调用:
auto re = View(1024);
这里的**1024 就是实参**。
函数调用以后,程序会进入 View 函数内部执行,执行完后再回到调用位置继续往下执行。
完整示例:
auto re = View(1024);
cout << re << endl;
执行结果类似:
call View(1024)
1
因为 View(1024) 返回的是 true,而 cout 输出 bool 类型时,默认会把 true 输出为 1,把 false 输出为 0。
2.3 函数声明和函数定义的区别
(1)函数声明
函数声明只是告诉编译器:这个函数存在,它的返回值类型、函数名和参数是什么。
例如:
cpp
bool View(int index);
这就是函数声明。
(2)函数定义
函数定义则是真正写出函数内部代码:
cpp
bool View(int index)
{
if (index < 0)
return false;
cout << "call View(" << index << ")" << endl;
return true;
}
(3)区别
bash
声明:告诉编译器有这个函数,不生成具体函数代码
定义:真正实现函数功能,会生成函数对应的代码
(4)变量的声明与定义
变量也是类似:
cpp
extern int gcount; // 声明,不分配内存
int gcount = 0; // 定义,分配内存
int gcount = 100; // 定义:真正创建变量,并分配内存
声明就是提前告诉编译器:
- 这个名字存在,它是什么类型,你先别报错。
- 有一个叫
gcount的全局变量,它的类型是int,但是它的真正定义不在这里,可能在别的.cpp文件里。
声明不真正分配变量存储空间,定义才会真正分配内存空间。
(5)为什么需要 extern?
因为项目中经常会有多个 .cpp 文件。
比如一个文件专门放全局变量:
cpp
// global.cpp
int gcount = 100;
另一个文件想使用它:
cpp
// main.cpp
extern int gcount;
int main()
{
gcount++;
}
如果没有 extern,main.cpp 不知道 gcount 是什么,就会报错。
三、函数返回值与参数实战
下面是本节主要代码:
cpp
#include <iostream>
using namespace std;
// 函数定义
// 返回值类型 函数名(形参类型 变量名, ...){函数代码块}
bool View(int index)
{
if (index < 0)
return false;
cout << "call View(" << index << ")" << endl;
return true;
}
// 函数的参数可以是 0 个到多个
void TestVoid(void)
{
return; // 终止函数的运行
}
auto GetPosX()
{
return 1.3;
}
void SetSize(int w, int h)
{
w += 1;
cout << w << ":" << h << endl;
cout << (long long)&w << ":"
<< (long long)&h << endl;
}
3.1 有返回值函数
例如:
cpp
bool View(int index)
{
if (index < 0)
return false;
cout << "call View(" << index << ")" << endl;
return true;
}
这个函数有返回值,返回值类型是 bool。
如果 index < 0,函数返回 false。
否则输出内容,并返回 true。
调用:
cpp
auto re = View(1024);
cout << re << endl;
这里 auto 会自动推导 re 的类型。
因为 View(1024) 返回的是 bool,所以:
cpp
auto re
等价于:
cpp
bool re
3.2 无返回值函数
cpp
void TestVoid(void)
{
return;
}
void 表示函数没有返回值。
这里的:
cpp
return;
不是返回某个具体值,而是直接结束函数。
也可以写成:
cpp
void TestVoid()
{
cout << "TestVoid" << endl;
}
在 C++ 中:
cpp
void TestVoid(void)
和:
cpp
void TestVoid()
都表示无参数函数。
3.3 auto 自动推导返回值
cpp
auto GetPosX()
{
return 1.3;
}
这里函数返回值类型写的是 auto。
因为:
cpp
return 1.3;
1.3 是一个 double 类型的小数,所以编译器会自动推导出:
cpp
auto GetPosX()
等价于:
cpp
double GetPosX()
调用:
cpp
auto x = GetPosX();
此时 x 的类型也是 double。
3.4 return 只能返回一个值
在 C++ 中,普通函数的 return 一次只能返回一个值。
例如:
cpp
int GetWidth()
{
return 1920;
}
这是可以的。
但是不能这样写:
cpp
return 1920, 1080;
3.5 返回多个值的三种方法
如果想返回多个值,常见方法有三种:
(1)通过参数返回:
cpp
void GetSize(int& w, int& h)
{
w = 1920;
h = 1080;
}
(2)使用结构体:
cpp
struct Size
{
int w;
int h;
};
Size GetSize()
{
return {1920, 1080};
}
(3)使用 vector、pair、tuple 等类型。
对于初学阶段,先理解:return 本身只能返回一个结果,如果需要多个结果,要借助参数或者自定义类型。
四、函数参数传递与栈区内存分析
4.1 形参和实参的关系
先看代码:
cpp
void SetSize(int w, int h)
{
w += 1;
cout << w << ":" << h << endl;
cout << (long long)&w << ":"
<< (long long)&h << endl;
}
int main()
{
int w = 1920;
int h = 1080;
cout << (long long)&w << ":"
<< (long long)&h << endl;
SetSize(w, h);
cout << "w=" << w << " h=" << h << endl;
return 0;
}
输出结果类似:

注意:**地址每次运行可能不同,不同编译器、不同系统下也可能不同。**我们重点看现象。
4.2 main 函数中的 w 和 h
在 main 函数中:
cpp
int w = 1920;
int h = 1080;
这两个变量是在 main 函数的栈区中申请的****局部变量。
然后打印地址:
cpp
cout << (long long)&w << ":"
<< (long long)&h << endl;
输出类似:
cpp
6422072:6422068
这表示**main 函数中的 w 和 h各自有自己的内存地址。**
4.3 SetSize 函数中的 w 和 h
调用函数:
cpp
SetSize(w, h);
这里传入的是 main 函数中的 w 和 h。
但是 SetSize 函数定义是:
cpp
void SetSize(int w, int h)
这里的 w 和 h 是形参。
重点来了:
普通参数传递时,形参和实参是复制关系。
也就是说,调用 SetSize(w, h) 时,会把 main 函数中 w 和 h 的值复制一份,传给 SetSize 函数中的形参 w 和 h。
所以在 SetSize 中:
cpp
w += 1;
修改的是 SetSize 自己的形参 w,不是 main 函数中的 w。
因此函数内部输出:
cpp
1921:1080
但是函数结束后,回到 main 函数:
cout << "w=" << w << " h=" << h << endl;
输出依然是:
w=1920 h=1080
这说明 main 中的 w 没有被改变。
4.4 为什么两个地址不一样
main 函数中打印的地址类似:
6422072:6422068
SetSize 函数中打印的地址类似:
6422032:6422040
两个地址不一样,说明它们不是同一块内存。
也就是说:
main 中的 w/h
和:
SetSize 中的 w/h
虽然名字一样,值一开始也一样,但是它们是两个不同函数栈空间中的变量。
这就是普通值传递的本质:
实参的值复制给形参,形参在函数内部单独申请栈区空间。
4.5 栈区进栈和出栈过程
函数调用时,栈区大致可以这样理解:
bash
进入 main 函数
申请 main 函数局部变量 w、h 的空间
调用 SetSize(w, h)
进入 SetSize 函数
申请 SetSize 函数形参 w、h 的空间
执行 w += 1
打印 SetSize 内部的 w、h 和地址
SetSize 函数结束
SetSize 函数栈空间释放
形参 w、h 被释放
回到 main 函数
main 函数中的 w、h 仍然存在
所以说:
进栈时申请函数所需空间,出栈时释放函数内部申请的局部变量和形参空间。
这里要注意一个细节:
形参在代码定义阶段只是一个名字和类型的描述,例如:
void SetSize(int w, int h)
程序没有调用 SetSize 时,运行时不会为这次调用的形参单独申请栈空间。
只有真正执行:
SetSize(w, h);
进入函数调用时,才会为本次调用创建形参对应的栈空间。
五、函数与变量作用域分析
5.1 完整代码:
cpp
#include <iostream>
using namespace std;
// 函数定义
bool View(int index)
{
if (index < 0)
return false;
cout << "call View(" << index << ")" << endl;
return true;
}
void TestVoid(void)
{
return;
}
auto GetPosX()
{
return 1.3;
}
void SetSize(int w, int h)
{
w += 1;
cout << w << ":" << h << endl;
cout << (long long)&w << ":"
<< (long long)&h << endl;
}
// 全局变量:进入 main 函数前申请空间
int gcount = 0;
// 静态全局变量:作用域仅限本文件
static int scount = 0;
int TestVer(int x, int y)
{
int tmp = x + y;
// 局部静态变量:第一次运行到此代码时申请空间
static int count = 0;
gcount++;
scount++;
count++;
cout << "gcount = " << gcount << endl;
cout << "scount = " << scount << endl;
cout << "count = " << count << endl;
return tmp;
}
int main()
{
auto re = View(1024);
cout << re << endl;
int w = 1920;
int h = 1080;
cout << (long long)&w << ":"
<< (long long)&h << endl;
SetSize(w, h);
cout << "w=" << w << " h=" << h << endl;
auto x = GetPosX();
{
TestVer(100, 200);
TestVer(200, 300);
auto re = TestVer(300, 400);
cout << "re = " << re << endl;
}
return 0;
}
5.2 局部变量
例如:
cpp
int TestVer(int x, int y)
{
int tmp = x + y;
return tmp;
}
这里的:
x
y
tmp
都属于局部变量。
其中 x 和 y 是函数参数,也可以看作****函数内部的局部变量。
tmp 是函数体内部定义的普通局部变量。
它们的特点是:
- 作用域:只在 TestVer 函数内部有效
- 生命周期:函数调用时创建,函数结束时释放
- 存储位置:通常在栈区
所以在函数外部不能直接访问:
cpp
cout << tmp << endl; // 错误,tmp 只在 TestVer 内部有效
但是:
cpp
return tmp;
- 是可以的。
- 因为这里返回的是
tmp的值,而不是返回tmp本身的地址。- 函数结束后,
tmp会被释放,但是它的值已经复制给了调用者。
5.3 局部静态变量
代码中有一行:
cpp
static int count = 0;
它定义在 TestVer 函数内部,所以它叫:
局部静态变量
它和普通局部变量最大的区别是:不会随着函数结束而销毁。
例如:
cpp
int TestVer(int x, int y)
{
static int count = 0;
count++;
cout << "count = " << count << endl;
return x + y;
}
如果连续调用三次:
cpp
TestVer(100, 200);
TestVer(200, 300);
TestVer(300, 400);
输出会类似:
cpp
count = 1
count = 2
count = 3
这说明 count 的值被保留下来了。
局部静态变量的特点是:
- 作用域:只在定义它的函数内部有效
- 生命周期:程序运行期间一直存在
- 初始化时机:第一次执行到该语句时初始化
- 存储位置:静态存储区
所以 count 虽然只能在 TestVer 函数内部访问,但是它的生命周期并不是函数调用期间,而是接近整个程序运行期间。
这就是局部静态变量的特殊之处:
作用域很小,但是生命周期很长。
5.4 全局变量
代码中:
cpp
int gcount = 0;
这是全局变量。
它定义在所有函数外面。
因此在当前文件中,下面的函数都可以访问它:
cpp
int TestVer(int x, int y)
{
gcount++;
cout << "gcount = " << gcount << endl;
return x + y;
}
全局变量的特点是:
- 作用域:从定义位置开始,到当前文件结束都可以访问
- 生命周期:程序开始运行前创建,程序结束时释放
- 存储位置:静态存储区
如果后续进入多文件编译,全局变量还可以通过 extern 在其他 .cpp 文件中声明后访问。
例如在 test.cpp 中定义:
cpp
int gcount = 0;
在其他文件中声明:
cpp
extern int gcount;
这样其他文件也可以访问这个全局变量。
但是全局变量有一个问题:大型工程中容易重名,也容易被多个地方修改,导致程序不好维护。
所以全局变量要谨慎使用。
5.5 静态全局变量
代码中:
cpp
static int scount = 0;
这是静态全局变量。
它也是定义在函数外部,但是前面加了 static。
它和普通全局变量的区别是:
- 普通全局变量:其他文件可以通过 extern 访问
- 静态全局变量:只能在当前 .cpp 文件内部访问
也就是说:
static int scount = 0;
把变量的作用域限制在当前文件中。
这就避免了大型项目中多个文件出现同名全局变量时发生冲突。
静态全局变量的特点:
- 作用域:仅限当前文件
- 生命周期:程序开始运行前创建,程序结束时释放
- 存储位置:静态存储区
所以它既具有"全局变量生命周期长"的特点,又限制了访问范围,减少了命名冲突。
静态全局变量具有全局生命周期,但作用域仅限当前文件。
5.6 四种变量对比总结
| 变量类型 | 示例 | 作用域 | 生命周期 | 存储区域 | 特点 |
|---|---|---|---|---|---|
| 局部变量 | int tmp = x + y; |
函数内部或代码块内部 | 进入作用域创建,离开作用域销毁 | 栈区 | 最常见,函数结束后释放 |
| 函数参数 | int x, int y |
函数内部 | 函数调用时创建,函数结束时释放 | 栈区 | 本质上也是函数内部变量 |
| 局部静态变量 | static int count = 0; |
函数内部 | 程序运行期间一直存在 | 静态存储区 | 只能函数内访问,但值会保留 |
| 全局变量 | int gcount = 0; |
当前文件及可被其他文件 extern 访问 |
程序运行期间一直存在 | 静态存储区 | 作用范围大,容易产生耦合 |
| 静态全局变量 | static int scount = 0; |
当前文件内部 | 程序运行期间一直存在 | 静态存储区 | 限制在本文件,减少重名冲突 |
六、本节重点总结
本节主要学习了函数的基础使用和底层执行逻辑:
(1)函数可以把重复代码封装起来,是后续模块化开发的基础。
(2)函数定义格式为:
返回值类型 函数名(形参列表)
{
函数体;
}
(3)普通参数传递是值传递,形参和实参是复制关系,地址不同。
(4)函数调用时会创建自己的栈空间,函数结束后栈空间释放。
(5)return 一次只能返回一个值,如果想返回多个值,可以通过引用参数、结构体、vector 等方式。
(6)局部变量在函数内部有效,函数结束后释放。
(7)局部静态变量作用域在函数内部,但生命周期贯穿程序运行期间。
(8)全局变量生命周期长,但作用范围大,容易造成重名和维护问题。
(9)静态全局变量生命周期长,但作用域限制在当前文件,更适合在单个 .cpp 文件内部保存共享状态。
(10)C++ 支持函数重载,同名函数可以通过不同参数列表区分,这是 C++ 相比 C 语言更灵活的地方。