在C++中,函数是一种重要的编程构造,可将代码组织成可重用的模块,从而提高代码的可读性和可维护性。
1. 函数的定义与声明
(1)函数的定义
C++函数定义的基本形式如下:
返回类型 函数名(参数列表){ // 函数体 return 返回值; }
各个部分的含义如下:
- 返回类型: 指定了函数返回值的数据类型。如果函数不需要返回值,则返回类型为void。
- 函数名: 这是对函数的一种标识,用于调用该函数。
- 参数列表: 指定了函数的参数类型和名称。如果该函数没有输入参数,则参数列表为空。
- 函数体: 这是函数的主体部分,包含了具体的实现逻辑,可以访问函数的参数以及其它变量。
返回值: 函数执行完毕后返回的结果。
e.g.
int sum(int a, int b) { return a + b; }
该函数的名称为sum
,接受两个整数类型的参数a
和b
,并返回a
和b
的和。函数返回类型指定了函数返回值的数据类型为int
。
(2)函数的声明
函数的声明是指在代码中提前声明函数名、参数列表和返回值类型等信息,但不实现具体的函数功能。这样做可以让编译器在编译时知道有该函数的存在,方便后续调用,一般出现在头文件中。
需要注意的是,如果函数需要被其他文件调用,则必须进行函数声明。同时,函数声明与函数定义中的参数必须保持一致,否则编译会出错。
e.g.
// 函数声明 int sum(int a, int b);
2. 函数的调用
函数调用实际上是对栈空间的操作过程
- 建立被调用函数的栈空间
- 保护调用函数的运行状态和返回地址
- 传递函数实参给形参
- 执行被调用函数函数体内语句
- 将控制权和返回值交给调用函数
函数的参数传递可以使用传值调用(Pass by Value)、地址调用(Pass by Address)和引用调用(Pass by Reference)三种方式。
- 传值调用适用于只需用实参的副本进行计算而不影响实参本身的场景
- 地址调用适用于需要通过指针进行进一步操作的情况
- 引用调用在写代码时更方便和直观,同时也能提高程序的效率,常用于需要修改实参值的情况,也可避免拷贝大型对象的开销。
(1)传值调用
传值调用中,函数参数通过复制的方式传递给函数。即,在函数调用时,实参的值被复制到形参,函数内部对形参的修改不会影响实参的值。
e.g.
#include <iostream> void square(int num) { num *= num; std::cout << "Inside square function: " << num << std::endl; } int main() { int number = 5; square(number); std::cout << "After function call: " << number << std::endl; return 0; }
上述示例定义了一个名为 square()
的函数,该函数接受一个整型参数 num
。在 main()
函数中,调用 square()
函数并将变量 number
的值作为实参传递给函数。在函数内部,对形参 num
进行平方运算,并输出结果。但是在函数调用后,打印 number
的值时,发现 number
的值没有改变。这是因为值调用只是将实参的值复制给了形参,函数内部的修改不会影响到实参本身。
(2)传址调用
传址调用中,函数通过传递变量的地址或指针来访问或修改实参的值。
#include <iostream> void incrementByOne(int *numPtr) { (*numPtr)++; } int main() { int number = 5; incrementByOne(&number); std::cout << "After function call: " << number << std::endl; return 0; }
上述示例定义了一个名为incrementByOne()
的函数,该函数接受一个整型指针参数 numPtr
。在 main()
函数中,定义了一个变量 number
并将其地址传递给incrementByOne()
函数。在函数内部,我们使用指针间接修改实参 number
的值。因此,在函数调用后,打印 number
的值时,发现其值已经被增加1。
(3)引用调用
引用调用中,函数通过引用参数来访问或修改实参的值。与地址调用类似,但是使用引用可以更直接地操作变量。
#include <iostream> void incrementByOne(int &numRef) { numRef++; } int main() { int number = 5; incrementByOne(number); std::cout << "After function call: " << number << std::endl; return 0; }
上述示例中定义了一个名为 incrementByOne()
的函数,该函数接受一个整型引用参数 numRef
。在 main()
函数中,定义了一个变量 number
,并将其作为实参传递给 incrementByOne()
函数。在函数内部,直接操作引用 numRef
,实际上就是操作了实参 number
。因此,在函数调用后,打印 number
的值时,发现其值已经被增加1。
3. 函数的递归调用
函数的递归调用是指函数调用自身的过程。在函数内部,通过调用自身来解决更小规模的问题,最终达到解决原始问题的目的。常用于解决求最大公约数、阶乘计算、斐波那契数列、树的遍历、链表操作等问题。
在递归调用过程中,系统使用堆栈(stack)来保存每个函数的局部变量、返回地址和其他信息。每次递归调用时,系统将创建一个新的栈帧(stack frame)以保存当前函数的状态,并将其推入堆栈顶部。当递归结束时,系统将按照先进后出的顺序弹出栈帧,恢复之前的函数状态。递归调用可以使代码更加简洁和易读,但也可能导致栈溢出等问题。
e.g. 求最大公约数
// 函数定义 int gcd(int a, int b) { if (b == 0) { return a; // 当第二个数为0时,第一个数即为最大公约数 } else { return gcd(b, a % b); // 递归调用欧几里得算法 } }
gcd
使用欧几里得算法(辗转相除法)来求解最大公约数
e.g. 计算阶乘
int factorial(int n) { if (n == 0) { return 1; } else { return n * factorial(n - 1); } }
factorial
函数计算一个整数n的阶乘,它使用了递归调用的方式。当n等于0时,函数返回1;否则,函数返回n和factorial(n-1)
的乘积。在递归调用过程中,每次调用都会将n的值减1,直到n等于0为止。
e.g. 斐波那契数列
int fibonacci(int n) { if (n <= 1) { return n; // 当 n <= 1 时,直接返回 n } else { return fibonacci(n - 1) + fibonacci(n - 2); // 递归调用计算斐波那契数列 } }
4. 内联函数
C++的内联函数是一种函数声明方式,用于对函数进行优化以提高程序的执行效率。通过使用内联函数,编译器会将函数的代码插入到调用该函数的每个位置,而不是像普通函数那样进行函数调用和返回。
使用内联函数可以减少函数调用的开销,因为它避免了函数调用时的栈帧保存和恢复操作。内联函数适用于执行时间较短、被频繁调用的函数,例如简单的数学计算或者访问对象的成员函数或成员变量。
C++中使用inline
关键字来声明内联函数。在函数定义或者函数原型前加上inline
关键字,告诉编译器将该函数进行内联展开。需要注意的是,编译器对于是否真正将函数内联展开有权决定,它可能根据一些规则(例如函数体的大小)来判断是否进行内联展开。同时,内联函数的定义必须放在头文件中,以便在调用处进行内联展开。
e.g.
#include <iostream> // 内联函数的定义 inline int add(int a, int b) { return a + b; } int main() { int x = 5; int y = 3; // 调用内联函数 int sum = add(x, y); std::cout << "Sum: " << sum << std::endl; return 0; }
在上面的例子中,add
函数被声明为内联函数。编译器会将函数体的代码插入到调用add
函数的地方,而不是进行函数调用。这样可以减少函数调用的开销,提高程序的执行效率。
5. 函数重载
函数重载(Function Overloading)是指在同一个作用域内,定义多个具有相同名称但参数列表不同的函数。通过函数重载,可以根据不同的参数类型或参数个数来调用不同的函数,相同的函数名对于不同的参数会被编译器当作不同的函数进行处理。
函数重载的条件:
- 函数名称必须相同。
- 参数列表必须不同,包括参数类型、参数顺序或参数个数的不同。
- 返回值类型可以相同也可以不同。
函数重载的优点:
- 提供更直观的接口:使用相同的函数名字来表示功能相似但参数不同的函数,可以提供更直观和易懂的接口。
代码复用:通过函数重载,可以避免定义多个类似- 功能的函数,提高代码的复用性。 - 增加代码可读性:函数重载使得程序代码更加清晰、易读,提高了代码的可维护性。
e.g.
#include <iostream> // 重载函数的声明和定义 void print(int n) { std::cout << "Integer: " << n << std::endl; } void print(double x) { std::cout << "Double: " << x << std::endl; } void print(const char* str) { std::cout << "String: " << str << std::endl; } int main() { int a = 10; double b = 3.14; const char* c = "Hello"; // 调用不同版本的print函数 print(a); // 调用print(int) print(b); // 调用print(double) print(c); // 调用print(const char*) return 0; }
上述例子中,print
函数被重载了三次,分别接受整数、浮点数和字符串作为参数。根据调用时提供的参数类型,编译器会选择调用合适的重载函数进行输出。
6. 标识符作用域
标识符作用域(Identifier Scope)指的是标识符(如变量名、函数名、类名等)在程序中可见和有效的范围。
通过标识符作用域,可以更好地组织代码结构、避免名称冲突,并能够灵活应用各种作用域规则来编写更加清晰、可读性高的程序。
C++中存在以下几种作用域:
- 全局作用域(Global Scope):在任何函数或代码块之外定义的标识符具有全局作用域。全局作用域中的标识符在整个程序中都是可见和有效的。
- 标识符作用域:C++中的命名空间提供了一种逻辑上的作用域封装机制。通过将相关的实体(如变量、函数、类等)放置在同一个命名空间内,可以限定它们的作用域范围。命名空间作用域中的标识符在该命名空间内是可见和有效的。
- 类作用域(Class Scope):类作用域指的是类的成员变量、成员函数和嵌套类的作用域。
类作用域中的标识符在类定义内部是可见和有效的。 - 局部作用域(Local Scope):局部作用域指的是在函数、代码块或循环等内部定义的标识符的范围。局部作用域中的标识符只在其所在的函数、代码块或循环等内部是可见和有效的。
作用域规则:
- 名称屏蔽(Name Hiding):在内部作用域中定义了与外部作用域相同名称的标识符时,会隐藏外部作用域中的标识符。当需要访问外部作用域的标识符时,可以使用作用域解析运算符(::)来显式指定。
- 嵌套作用域(Nested Scope):在一个作用域内部可以嵌套其他作用域,内部作用域中的标识符优先级更高。
- 生命周期(Lifetime):标识符的有效期取决于其作用域。一旦超出了其作用域,该标识符就不再可见,其内存空间可能被释放或重新分配。
e.g.
#include <iostream> int globalVar = 10; // 全局作用域 namespace MyNamespace { int localVar = 20; // 命名空间作用域 class MyClass { public: static int classVar; // 类作用域 void func() { int count = 5; // 局部作用域 std::cout << "count: " << count << std::endl; } }; } int MyNamespace::MyClass::classVar = 30; // 类静态成员的定义与初始化 int main() { int localVar = 15; // 局部作用域 std::cout << "globalVar: " << globalVar << std::endl; // 访问全局作用域下的变量 std::cout << "localVar: " << localVar << std::endl; // 访问局部作用域下的变量 std::cout << "namespace localVar: " << MyNamespace::localVar << std::endl; // 访问命名空间作用域下的变量 std::cout << "classVar: " << MyNamespace::MyClass::classVar << std::endl; // 访问类作用域下的变量 MyNamespace::MyClass obj; obj.func(); // 调用类作用域下的成员函数 return 0; }
输出结果:
globalVar: 10
localVar: 15
namespace localVar: 20
classVar: 30
count: 5
7. 存储类
C++ 中的变量存储类用于指定变量在内存中的存储方式和生命周期。
存储类提供了灵活的方式来管理变量的生命周期和存储方式。根据需求选择合适的存储类可以提高程序的执行效率和内存管理。
C++ 提供了以下四种存储类:
(1)auto(自动类)
auto
是 C++ 中默认的存储类。- 自动存储类(auto storage class)用于指定局部变量的存储方式。
- 自动存储类的变量的生命周期与其所在的作用域相同。
- 在函数内部定义的局部变量默认为
auto
类型。
(2)register(寄存器类)
register
关键字用于请求将变量存储于 CPU 寄存器中,以便更快地访问变量。- 由于寄存器数量有限,编译器可以忽略 register 请求,而将变量视为 auto 变量。
register
变量的地址是不可获取的,也无法对其应用运算符&
。
(3)static(静态变量)
static
关键字用于声明静态变量。- 静态变量在程序执行期间一直存在,其生命周期从首次被初始化开始,直到程序结束。
- 静态变量在内存中分配一次,并且初始化只会执行一次。
- 静态局部变量在函数内部定义,但生命周期延长到函数调用结束后。
- 静态全局变量在文件作用域内定义,对整个文件都是可见的。
(4)extern(外部变量)
extern
关键字用于声明外部变量或函数。extern
可以用于引用其他源文件中定义的全局变量或函数。- 在一个源文件中使用
extern
声明一个全局变量时,该变量实际上是在其他源文件中定义的,编译器会在链接时解析其引用。
e.g.
#include <iostream> int globalVar; // 外部链接的全局变量 void func() { int localVar = 10; // 自动变量(默认) register int regVar = 20; // 寄存器变量 static int staticVar = 30; // 静态局部变量 globalVar++; // 访问全局变量 std::cout << "localVar: " << localVar << std::endl; std::cout << "regVar: " << regVar << std::endl; std::cout << "staticVar: " << staticVar << std::endl; localVar++; // 自动变量的值可以改变 regVar++; // 寄存器变量的值可以改变 staticVar++; // 静态局部变量的值可以改变 } int main() { globalVar = 100; // 初始化全局变量 for (int i = 0; i < 5; i++) { func(); } return 0; }
输出结果:
localVar: 10
regVar: 20
staticVar: 30
localVar: 10
regVar: 20
staticVar: 31
localVar: 10
regVar: 20
staticVar: 32
localVar: 10
regVar: 20
staticVar: 33
localVar: 10
regVar: 20
staticVar: 34