C++零基础到工程实战(5.1):初识函数—定义调用、参数返回值、栈区内存与变量作用域分析
目录文章摘要一、为什么需要函数1.1 函数的基本概念1.2 C 语言函数和 C 函数的区别1C语言2C3类和对象及成员函数简述二、函数的定义、声明与调用2.1 函数定义语法2.2 函数调用语法2.3 函数声明和函数定义的区别1函数声明2函数定义3区别4变量的声明与定义5为什么需要 extern三、函数返回值与参数实战3.1 有返回值函数3.2 无返回值函数3.3 auto 自动推导返回值3.4 return 只能返回一个值3.5 返回多个值的三种方法1通过参数返回2使用结构体3使用 vector、pair、tuple 等类型。四、函数参数传递与栈区内存分析4.1 形参和实参的关系4.2 main 函数中的 w 和 h4.3 SetSize 函数中的 w 和 h4.4 为什么两个地址不一样4.5 栈区进栈和出栈过程五、函数与变量作用域分析5.1 完整代码5.2 局部变量5.3 局部静态变量5.4 全局变量5.5 静态全局变量5.6 四种变量对比总结六、本节重点总结文章摘要本篇文章进入 C 第五章函数学习主要围绕以下内容展开为什么需要函数函数如何定义和调用函数参数和返回值如何传递函数调用时栈区内存如何变化局部变量和全局变量的作用域与生命周期通过View、SetSize、GetPosX、TestVer等代码示例分析函数在程序中的执行过程并重点解释形参和实参的复制关系、函数返回值、局部静态变量、全局变量和静态全局变量的区别为后续多文件编译和模块化开发打基础。一、为什么需要函数当程序变复杂以后如果所有代码都写在main函数中会出现几个问题代码越来越长可读性变差相同功能反复写重复代码太多修改一个功能时容易影响其他代码不方便后续进行多文件编译和工程化开发。所以从第五章开始就要引入一个非常重要的思想把重复代码封装成函数把复杂程序拆分成多个模块。1.1 函数的基本概念函数可以理解为把一段代码封装起来并给这段代码起一个名字后面需要使用时直接调用这个名字即可。例如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 函数的区别1C语言在C 语言中一般情况下同一个作用域内函数名不能重复。例如下面这种写法在 C 语言中是不允许的void Test(int x) { } void Test(double x) { }因为它们名字都叫Test。2C但是在 C 中这种写法是允许的只要函数的参数类型或者参数数量不同即可。这叫做函数重载例如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; }调用时Test(10); // 调用 Test(int) Test(3.14); // 调用 Test(double) Test(10, 20); // 调用 Test(int, int)虽然它们在代码中都叫Test但是 C 编译器在底层会根据参数类型、参数数量等信息生成不同的函数符号名。也就是说表面上名字相同底层并不是完全一样的名字。这也是 C 支持函数重载的重要原因。3类和对象及成员函数简述后面学习类和对象时还会看到成员函数例如class Student { public: void Show() { cout Student Show endl; } };我们看到的函数名是Show但在底层它会和类名Student关联起来。也就是说机器最终识别的并不只是一个简单的Show而是能映射到具体类和具体方法。二、函数的定义、声明与调用2.1 函数定义语法函数定义的基本格式如下返回值类型 函数名(形参列表) { 函数体; }例如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函数声明函数声明只是告诉编译器这个函数存在它的返回值类型、函数名和参数是什么。例如bool View(int index);这就是函数声明。2函数定义函数定义则是真正写出函数内部代码bool View(int index) { if (index 0) return false; cout call View( index ) endl; return true; }3区别声明告诉编译器有这个函数不生成具体函数代码 定义真正实现函数功能会生成函数对应的代码4变量的声明与定义变量也是类似extern int gcount; // 声明不分配内存 int gcount 0; // 定义分配内存 int gcount 100; // 定义真正创建变量并分配内存声明就是提前告诉编译器这个名字存在它是什么类型你先别报错。有一个叫gcount的全局变量它的类型是int但是它的真正定义不在这里可能在别的.cpp文件里。声明不真正分配变量存储空间定义才会真正分配内存空间。5为什么需要extern因为项目中经常会有多个.cpp文件。比如一个文件专门放全局变量// global.cpp int gcount 100;另一个文件想使用它// main.cpp extern int gcount; int main() { gcount; }如果没有externmain.cpp不知道gcount是什么就会报错。三、函数返回值与参数实战下面是本节主要代码#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 有返回值函数例如bool View(int index) { if (index 0) return false; cout call View( index ) endl; return true; }这个函数有返回值返回值类型是bool。如果index 0函数返回false。否则输出内容并返回true。调用auto re View(1024); cout re endl;这里auto会自动推导re的类型。因为View(1024)返回的是bool所以auto re等价于bool re3.2 无返回值函数void TestVoid(void) { return; }void表示函数没有返回值。这里的return;不是返回某个具体值而是直接结束函数。也可以写成void TestVoid() { cout TestVoid endl; }在 C 中void TestVoid(void)和void TestVoid()都表示无参数函数。3.3 auto 自动推导返回值auto GetPosX() { return 1.3; }这里函数返回值类型写的是auto。因为return 1.3;1.3是一个double类型的小数所以编译器会自动推导出auto GetPosX()等价于double GetPosX()调用auto x GetPosX();此时x的类型也是double。3.4 return 只能返回一个值在 C 中普通函数的return一次只能返回一个值。例如int GetWidth() { return 1920; }这是可以的。但是不能这样写return 1920, 1080;3.5 返回多个值的三种方法如果想返回多个值常见方法有三种1通过参数返回void GetSize(int w, int h) { w 1920; h 1080; }2使用结构体struct Size { int w; int h; }; Size GetSize() { return {1920, 1080}; }3使用vector、pair、tuple等类型。对于初学阶段先理解return 本身只能返回一个结果如果需要多个结果要借助参数或者自定义类型。四、函数参数传递与栈区内存分析4.1 形参和实参的关系先看代码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函数中int w 1920; int h 1080;这两个变量是在main函数的栈区中申请的局部变量。然后打印地址cout (long long)w : (long long)h endl;输出类似6422072:6422068这表示main函数中的w和h各自有自己的内存地址。4.3 SetSize 函数中的 w 和 h调用函数SetSize(w, h);这里传入的是main函数中的w和h。但是SetSize函数定义是void SetSize(int w, int h)这里的w和h是形参。重点来了普通参数传递时形参和实参是复制关系。也就是说调用SetSize(w, h)时会把main函数中w和h的值复制一份传给SetSize函数中的形参w和h。所以在SetSize中w 1;修改的是SetSize自己的形参w不是main函数中的w。因此函数内部输出1921:1080但是函数结束后回到main函数cout w w h h endl;输出依然是w1920 h1080这说明main中的w没有被改变。4.4 为什么两个地址不一样main函数中打印的地址类似6422072:6422068SetSize函数中打印的地址类似6422032:6422040两个地址不一样说明它们不是同一块内存。也就是说main 中的 w/h和SetSize 中的 w/h虽然名字一样值一开始也一样但是它们是两个不同函数栈空间中的变量。这就是普通值传递的本质实参的值复制给形参形参在函数内部单独申请栈区空间。4.5 栈区进栈和出栈过程函数调用时栈区大致可以这样理解进入 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 完整代码#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 局部变量例如int TestVer(int x, int y) { int tmp x y; return tmp; }这里的x y tmp都属于局部变量。其中x和y是函数参数也可以看作函数内部的局部变量。tmp是函数体内部定义的普通局部变量。它们的特点是作用域只在 TestVer 函数内部有效生命周期函数调用时创建函数结束时释放存储位置通常在栈区所以在函数外部不能直接访问cout tmp endl; // 错误tmp 只在 TestVer 内部有效但是return tmp;是可以的。因为这里返回的是tmp的值而不是返回tmp本身的地址。函数结束后tmp会被释放但是它的值已经复制给了调用者。5.3 局部静态变量代码中有一行static int count 0;它定义在TestVer函数内部所以它叫局部静态变量它和普通局部变量最大的区别是不会随着函数结束而销毁。例如int TestVer(int x, int y) { static int count 0; count; cout count count endl; return x y; }如果连续调用三次TestVer(100, 200); TestVer(200, 300); TestVer(300, 400);输出会类似count 1 count 2 count 3这说明count的值被保留下来了。局部静态变量的特点是作用域只在定义它的函数内部有效生命周期程序运行期间一直存在初始化时机第一次执行到该语句时初始化存储位置静态存储区所以count虽然只能在TestVer函数内部访问但是它的生命周期并不是函数调用期间而是接近整个程序运行期间。这就是局部静态变量的特殊之处作用域很小但是生命周期很长。5.4 全局变量代码中int gcount 0;这是全局变量。它定义在所有函数外面。因此在当前文件中下面的函数都可以访问它int TestVer(int x, int y) { gcount; cout gcount gcount endl; return x y; }全局变量的特点是作用域从定义位置开始到当前文件结束都可以访问生命周期程序开始运行前创建程序结束时释放存储位置静态存储区如果后续进入多文件编译全局变量还可以通过extern在其他.cpp文件中声明后访问。例如在test.cpp中定义int gcount 0;在其他文件中声明extern int gcount;这样其他文件也可以访问这个全局变量。但是全局变量有一个问题大型工程中容易重名也容易被多个地方修改导致程序不好维护。所以全局变量要谨慎使用。5.5 静态全局变量代码中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函数调用时会创建自己的栈空间函数结束后栈空间释放。5return一次只能返回一个值如果想返回多个值可以通过引用参数、结构体、vector 等方式。6局部变量在函数内部有效函数结束后释放。7局部静态变量作用域在函数内部但生命周期贯穿程序运行期间。8全局变量生命周期长但作用范围大容易造成重名和维护问题。9静态全局变量生命周期长但作用域限制在当前文件更适合在单个.cpp文件内部保存共享状态。10C 支持函数重载同名函数可以通过不同参数列表区分这是 C 相比 C 语言更灵活的地方。