C++零基础到工程实战(5.1):初识函数—定义调用、参数返回值、栈区内存与变量作用域分析

目录

文章摘要

一、为什么需要函数

[1.1 函数的基本概念](#1.1 函数的基本概念)

[1.2 C 语言函数和 C++ 函数的区别](#1.2 C 语言函数和 C++ 函数的区别)

(1)C语言

(2)C++

(3)类和对象及成员函数简述

二、函数的定义、声明与调用

[2.1 函数定义语法](#2.1 函数定义语法)

[2.2 函数调用语法](#2.2 函数调用语法)

[2.3 函数声明和函数定义的区别](#2.3 函数声明和函数定义的区别)

(1)函数声明

(2)函数定义

(3)区别

(4)变量的声明与定义

[(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 返回多个值的三种方法)

(1)通过参数返回:

(2)使用结构体:

[(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++ 第五章函数学习,主要围绕以下内容展开:

  • 为什么需要函数
  • 函数如何定义和调用
  • 函数参数和返回值如何传递
  • 函数调用时栈区内存如何变化
  • 局部变量和全局变量的作用域与生命周期

通过 ViewSetSizeGetPosXTestVer 等代码示例,分析函数在程序中的执行过程,并重点解释形参和实参的复制关系、函数返回值、局部静态变量、全局变量和静态全局变量的区别,为后续多文件编译和模块化开发打基础。


一、为什么需要函数

当程序变复杂以后,如果所有代码都写在 main 函数中,会出现几个问题:

  1. 代码越来越长,可读性变差;
  2. 相同功能反复写,重复代码太多;
  3. 修改一个功能时,容易影响其他代码;
  4. 不方便后续进行多文件编译和工程化开发。

所以从第五章开始,就要引入一个非常重要的思想:把重复代码封装成函数,把复杂程序拆分成多个模块。

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; // 定义:真正创建变量,并分配内存

声明就是提前告诉编译器:

  1. 这个名字存在,它是什么类型,你先别报错。
  2. 有一个叫 gcount 的全局变量,它的类型是 int,但是它的真正定义不在这里,可能在别的 .cpp 文件里。

声明不真正分配变量存储空间,定义才会真正分配内存空间。

(5)为什么需要 extern

因为项目中经常会有多个 .cpp 文件。

比如一个文件专门放全局变量:

cpp 复制代码
// global.cpp
int gcount = 100;

另一个文件想使用它:

cpp 复制代码
// main.cpp
extern int gcount;

int main()
{
    gcount++;
}

如果没有 externmain.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)使用 vectorpairtuple 等类型。

对于初学阶段,先理解: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 函数中的 wh各自有自己的内存地址。**

4.3 SetSize 函数中的 w 和 h

调用函数:

cpp 复制代码
SetSize(w, h);

这里传入的是 main 函数中的 wh

但是 SetSize 函数定义是:

cpp 复制代码
void SetSize(int w, int h)

这里的 wh 是形参。

重点来了:

普通参数传递时,形参和实参是复制关系。

也就是说,调用 SetSize(w, h) 时,会把 main 函数中 wh 的值复制一份,传给 SetSize 函数中的形参 wh

所以在 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

都属于局部变量

其中 xy函数参数,也可以看作****函数内部的局部变量

tmp函数体内部定义的普通局部变量

它们的特点是:

  1. 作用域:只在 TestVer 函数内部有效
  2. 生命周期:函数调用时创建,函数结束时释放
  3. 存储位置:通常在栈区

所以在函数外部不能直接访问:

cpp 复制代码
cout << tmp << endl; // 错误,tmp 只在 TestVer 内部有效

但是:

cpp 复制代码
return tmp;
  1. 是可以的。
  2. 因为这里返回的是 tmp 的值,而不是返回 tmp 本身的地址。
  3. 函数结束后,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 的值被保留下来了。

局部静态变量的特点是:

  1. 作用域:只在定义它的函数内部有效
  2. 生命周期:程序运行期间一直存在
  3. 初始化时机:第一次执行到该语句时初始化
  4. 存储位置:静态存储区

所以 count 虽然只能在 TestVer 函数内部访问,但是它的生命周期并不是函数调用期间,而是接近整个程序运行期间。

这就是局部静态变量的特殊之处:

复制代码
作用域很小,但是生命周期很长。

5.4 全局变量

代码中:

cpp 复制代码
int gcount = 0;

这是全局变量。

定义在所有函数外面。

因此在当前文件中,下面的函数都可以访问它:

cpp 复制代码
int TestVer(int x, int y)
{
    gcount++;
    cout << "gcount = " << gcount << endl;
    return x + y;
}

全局变量的特点是:

  1. 作用域:从定义位置开始,到当前文件结束都可以访问
  2. 生命周期:程序开始运行前创建,程序结束时释放
  3. 存储位置:静态存储区

如果后续进入多文件编译,全局变量还可以通过 extern 在其他 .cpp 文件中声明后访问。

例如在 test.cpp 中定义:

cpp 复制代码
int gcount = 0;

在其他文件中声明:

cpp 复制代码
extern int gcount;

这样其他文件也可以访问这个全局变量。

但是全局变量有一个问题:大型工程中容易重名,也容易被多个地方修改,导致程序不好维护。

所以全局变量要谨慎使用。

5.5 静态全局变量

代码中:

cpp 复制代码
static int scount = 0;

这是静态全局变量。

它也是定义在函数外部,但是前面加了 static

它和普通全局变量的区别是:

  1. 普通全局变量:其他文件可以通过 extern 访问
  2. 静态全局变量:只能在当前 .cpp 文件内部访问

也就是说:

复制代码
static int scount = 0;

把变量的作用域限制在当前文件中。

这就避免了大型项目中多个文件出现同名全局变量时发生冲突。

静态全局变量的特点:

  1. 作用域:仅限当前文件
  2. 生命周期:程序开始运行前创建,程序结束时释放
  3. 存储位置:静态存储区

所以它既具有"全局变量生命周期长"的特点,又限制了访问范围,减少了命名冲突。

复制代码
静态全局变量具有全局生命周期,但作用域仅限当前文件。

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 语言更灵活的地方。

相关推荐
阿文的代码库2 小时前
如何在C++中使用标准库的智能指针
开发语言·c++·算法
郝学胜-神的一滴2 小时前
Qt 高级开发 008: 使用QSetting记住上次打开路径
开发语言·c++·qt·开源软件
kyle~3 小时前
机器人感知 --- 多相机传感时间误差分析
linux·c++·数码相机·机器人·ros2·传感器
周末也要写八哥3 小时前
C++变参模板之空参包的特殊情况
java·开发语言·c++
蝈理塘(/_\)大怨种3 小时前
c++ 入门基础
开发语言·c++
weixin_386468963 小时前
openharmony 6.0编译rk3568过程记录
c语言·c++·git·python·vim·harmonyos·openharmony
雪度娃娃3 小时前
转向现代C++——优先选用别名声明,而非 typedef
开发语言·c++
梓䈑4 小时前
【Linux网络】构建UDP网络服务:从Echo到聊天室的线程池架构演进
linux·网络·c++·udp