在C++中,函数是一种重要的编程构造,可将代码组织成可重用的模块,从而提高代码的可读性和可维护性。
(1)函数的定义
C++函数定义的基本形式如下:
返回类型 函数名(参数列表){
// 函数体
return 返回值;
}
各个部分的含义如下:
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);
函数调用实际上是对栈空间的操作过程
函数的参数传递可以使用传值调用(Pass by Value)、地址调用(Pass by Address)和引用调用(Pass by Reference)三种方式。
(1)传值调用
传值调用中,函数参数通过复制的方式传递给函数。即,在函数调用时,实参的值被复制到形参,函数内部对形参的修改不会影响实参的值。
e.g.
#include
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
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
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。
函数的递归调用是指函数调用自身的过程。在函数内部,通过调用自身来解决更小规模的问题,最终达到解决原始问题的目的。常用于解决求最大公约数、阶乘计算、斐波那契数列、树的遍历、链表操作等问题。
在递归调用过程中,系统使用堆栈(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); // 递归调用计算斐波那契数列
}
}
C++的内联函数是一种函数声明方式,用于对函数进行优化以提高程序的执行效率。通过使用内联函数,编译器会将函数的代码插入到调用该函数的每个位置,而不是像普通函数那样进行函数调用和返回。
使用内联函数可以减少函数调用的开销,因为它避免了函数调用时的栈帧保存和恢复操作。内联函数适用于执行时间较短、被频繁调用的函数,例如简单的数学计算或者访问对象的成员函数或成员变量。
C++中使用inline
关键字来声明内联函数。在函数定义或者函数原型前加上inline
关键字,告诉编译器将该函数进行内联展开。需要注意的是,编译器对于是否真正将函数内联展开有权决定,它可能根据一些规则(例如函数体的大小)来判断是否进行内联展开。同时,内联函数的定义必须放在头文件中,以便在调用处进行内联展开。
e.g.
#include
// 内联函数的定义
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
函数的地方,而不是进行函数调用。这样可以减少函数调用的开销,提高程序的执行效率。
函数重载(Function Overloading)是指在同一个作用域内,定义多个具有相同名称但参数列表不同的函数。通过函数重载,可以根据不同的参数类型或参数个数来调用不同的函数,相同的函数名对于不同的参数会被编译器当作不同的函数进行处理。
函数重载的条件:
函数重载的优点:
e.g.
#include
// 重载函数的声明和定义
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
函数被重载了三次,分别接受整数、浮点数和字符串作为参数。根据调用时提供的参数类型,编译器会选择调用合适的重载函数进行输出。
标识符作用域(Identifier Scope)指的是标识符(如变量名、函数名、类名等)在程序中可见和有效的范围。
通过标识符作用域,可以更好地组织代码结构、避免名称冲突,并能够灵活应用各种作用域规则来编写更加清晰、可读性高的程序。
C++中存在以下几种作用域:
作用域规则:
e.g.
#include
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
C++ 中的变量存储类用于指定变量在内存中的存储方式和生命周期。
存储类提供了灵活的方式来管理变量的生命周期和存储方式。根据需求选择合适的存储类可以提高程序的执行效率和内存管理。
C++ 提供了以下四种存储类:
(1)auto(自动类)
auto
是 C++ 中默认的存储类。auto
类型。(2)register(寄存器类)
register
关键字用于请求将变量存储于 CPU 寄存器中,以便更快地访问变量。register
变量的地址是不可获取的,也无法对其应用运算符 &
。(3)static(静态变量)
static
关键字用于声明静态变量。(4)extern(外部变量)
extern
关键字用于声明外部变量或函数。extern
可以用于引用其他源文件中定义的全局变量或函数。extern
声明一个全局变量时,该变量实际上是在其他源文件中定义的,编译器会在链接时解析其引用。e.g.
#include
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