-
函数定义
- 语法格式
- 函数定义包括函数头和函数体。函数头包含返回类型、函数名和参数列表。函数体是用花括号
{}
括起来的代码块,用于实现函数的功能。例如,定义一个计算两个整数之和的函数:
这里int add(int a, int b) { return a + b; }
int
是返回类型,表示函数返回一个整数;add
是函数名;(int a, int b)
是参数列表,说明函数接受两个整数参数a
和b
;{ return a + b; }
是函数体,实现了将两个参数相加并返回结果的功能。 - 函数定义包括函数头和函数体。函数头包含返回类型、函数名和参数列表。函数体是用花括号
- 函数体中的变量作用域
- 在函数体内部定义的变量具有局部作用域,它们只在函数内部有效。例如:
这里void function() { int localVariable = 10; std::cout << localVariable << std::endl; } int main() { function(); // 在这里无法访问localVariable return 0; }
localVariable
在function
函数内部定义,所以只能在function
函数内部使用,在main
函数或者其他函数中无法访问。
- 语法格式
-
函数原型(声明)
- 作用和必要性
- 函数原型主要用于告诉编译器函数的名称、返回类型和参数类型等信息,使得编译器在编译调用该函数的代码时能够进行正确的类型检查。这样可以将函数的定义放在调用它的代码之后,或者放在其他文件中。例如,如果有一个函数定义在另一个文件中,在调用这个函数的文件中就需要提供函数原型。
- 语法格式
- 函数原型的语法格式为:返回类型 函数名(参数类型列表);。例如,
int add(int a, int b);
是前面定义的add
函数的原型。注意,函数原型的末尾需要有一个分号。
- 函数原型的语法格式为:返回类型 函数名(参数类型列表);。例如,
- 函数原型与函数定义的区别
- 函数原型只是函数的声明,不包含函数体,它主要用于编译器的类型检查。而函数定义包含了函数体,是函数功能的具体实现。例如,在一个大型项目中,可以先在头文件中提供函数原型,然后在源文件中实现函数定义。
- 作用和必要性
-
函数重载
- 概念和目的
- 函数重载是指在同一个作用域内,可以定义多个同名函数,只要它们的参数列表不同(参数个数、参数类型或者参数顺序不同)。函数重载的目的是为了方便程序员使用相似功能的函数,根据不同的参数情况执行不同的操作。例如,定义两个
add
函数,一个用于计算两个整数相加,另一个用于计算两个浮点数相加:
int add(int a, int b) { return a + b; } double add(double a, double b) { return a + b; }
- 函数重载是指在同一个作用域内,可以定义多个同名函数,只要它们的参数列表不同(参数个数、参数类型或者参数顺序不同)。函数重载的目的是为了方便程序员使用相似功能的函数,根据不同的参数情况执行不同的操作。例如,定义两个
- 编译器如何区分重载函数
- 编译器通过检查函数调用时的实际参数类型和个数来确定调用哪一个重载函数。例如,在
int result1 = add(3, 5);
中,编译器会根据参数3
和5
是整数,调用int add(int a, int b)
函数;而在double result2 = add(3.0, 5.0);
中,编译器会调用double add(double a, double b)
函数。
- 编译器通过检查函数调用时的实际参数类型和个数来确定调用哪一个重载函数。例如,在
- 重载函数的匹配规则和注意事项
- 当调用一个重载函数时,编译器首先会寻找完全匹配的函数,如果没有完全匹配的,会尝试进行一些隐式类型转换来找到合适的函数。但是,如果存在二义性(即有多个函数都可以匹配,但编译器无法确定唯一的一个),则会导致编译错误。例如,有一个函数
void func(int a);
和另一个函数void func(double a);
,在调用func(3.5f)
(3.5f
是单精度浮点数)时,编译器可能会因为不知道是将3.5f
转换为整数还是双精度浮点数而产生二义性错误。
- 当调用一个重载函数时,编译器首先会寻找完全匹配的函数,如果没有完全匹配的,会尝试进行一些隐式类型转换来找到合适的函数。但是,如果存在二义性(即有多个函数都可以匹配,但编译器无法确定唯一的一个),则会导致编译错误。例如,有一个函数
- 概念和目的
-
默认参数
- 概念和语法格式
- 默认参数是指在函数定义或声明时,可以为参数指定一个默认值。在调用函数时,如果没有提供该参数的值,就会使用默认值。语法格式为:返回类型 函数名(参数类型 参数名 = 默认值);。例如:
这里int multiply(int a, int b = 2) { return a * b; }
b
是有默认值的参数,默认值为2
。 - 默认参数的使用规则和注意事项
- 默认参数必须从右向左连续定义,不能间隔。例如,
int func(int a = 1, int b, int c = 3);
这样的定义是错误的。在调用有默认参数的函数时,可以省略默认参数的值,例如,int result1 = multiply(3);
这里会使用b
的默认值2
,计算结果为6
;也可以提供新的值,例如,int result2 = multiply(3, 4);
这里b
的值为4
,计算结果为12
。同时,在函数的声明和定义中,如果同时出现默认参数,建议在声明中指定默认参数,定义中可以不用再次指定(如果指定,必须与声明中的默认参数一致),以避免重复定义带来的不一致问题。
- 默认参数必须从右向左连续定义,不能间隔。例如,
- 概念和语法格式
-
栈帧的创建与销毁
- 栈帧创建过程
- 当一个函数被调用时,系统会在程序的栈空间中为该函数创建一个栈帧。首先,会将函数的返回地址(即调用该函数的下一条指令的地址)压入栈中,这确保函数执行完后能回到正确的位置继续执行后续代码。然后,根据函数参数的类型和数量,将参数的值(如果是值传递)或引用(如果是引用传递)或指针(如果是指针传递)依次压入栈中。最后,为函数内部定义的局部变量分配内存空间。
- 例如,有函数
void func(int a, int b)
,当调用func(3, 4)
时,系统会先将返回地址压入栈,然后将3
和4
压入栈作为参数a
和b
的值,接着为func
函数内部可能定义的局部变量预留空间。
- 栈帧销毁过程
- 当函数执行结束(遇到
return
语句或者函数体的最后一个花括号)时,栈帧会被销毁。首先,会释放函数内部局部变量所占用的内存空间。然后,根据函数的返回值类型(如果有返回值),将返回值复制到一个临时存储位置(如果是基本数据类型)或者通过移动语义(如果是对象)将返回值传递给调用者。最后,将栈顶指针恢复到调用该函数之前的位置,这样就相当于销毁了这个栈帧,同时将返回地址从栈中弹出,程序继续从返回地址处执行。
- 当函数执行结束(遇到
- 栈帧创建过程
-
参数传递方式的细节
- 值传递深入理解
- 复制过程:在值传递中,实际参数的值会被完整地复制到函数的形式参数中。对于基本数据类型,这是一个简单的字节复制过程。例如,传递一个
int
类型的参数,会将该int
值的字节序列复制到函数参数对应的内存位置。对于自定义结构体等复杂类型,会递归地复制每个成员变量的值。 - 对原始参数的影响:由于是复制了一份新的值给函数参数,所以在函数内部对参数的修改不会影响到原始的实际参数。例如,对于函数
void modify(int num)
,在函数内部num = 10
,但如果在函数外部有int original_num = 5; modify(original_num);
,original_num
的值依然是5
。
- 复制过程:在值传递中,实际参数的值会被完整地复制到函数的形式参数中。对于基本数据类型,这是一个简单的字节复制过程。例如,传递一个
- 引用传递深入理解
- 引用的本质:引用在底层实现上可以看作是一个指针常量,它总是指向被引用的对象。当进行引用传递时,实际上传递的是对象的地址,但是在语法上使用起来就像使用原始对象一样。例如,
int& ref = original_num;
,ref
和original_num
在内存中指向同一个位置。 - 对原始参数的影响:因为引用和原始对象共享同一块内存空间,所以在函数内部通过引用对参数进行操作,实际上就是对原始对象进行操作。例如,函数
void modifyByReference(int& num)
,在函数内部num = 10
,如果在函数外部有int original_num = 5; modifyByReference(original_num);
,original_num
的值会变为10
。
- 引用的本质:引用在底层实现上可以看作是一个指针常量,它总是指向被引用的对象。当进行引用传递时,实际上传递的是对象的地址,但是在语法上使用起来就像使用原始对象一样。例如,
- 指针传递深入理解
- 指针的操作方式:指针传递是把变量的地址传递给函数。在函数内部,通过解引用指针(使用
*
操作符)来访问和修改指针所指向的变量的值。例如,函数void modifyByPointer(int* ptr)
,当传递&original_num
作为参数时,在函数内部通过*ptr = 10
来修改original_num
的值。 - 与引用传递的区别:虽然指针传递和引用传递都可以在函数内部修改原始变量的值,但指针传递需要显式地解引用指针来访问变量,而引用传递在语法上更简洁,直接使用引用变量就可以访问和修改原始变量。另外,指针可以在函数内部重新赋值指向其他对象,而引用一旦初始化就不能再引用其他对象。
- 指针的操作方式:指针传递是把变量的地址传递给函数。在函数内部,通过解引用指针(使用
- 值传递深入理解
-
函数返回值传递机制的细节
- 基本数据类型返回值传递
- 复制返回值:当函数返回一个基本数据类型(如
int
、double
等)的值时,函数会将返回值复制到一个临时存储位置。这个临时存储位置可能是一个寄存器或者栈中的某个位置,具体取决于编译器和硬件架构。例如,对于函数int add(int a, int b) { return a + b; }
,当调用add
函数时,计算a + b
的结果会被复制到这个临时位置,然后这个值再被赋值给接收返回值的变量(如int result = add(3, 5);
中的result
)。
- 复制返回值:当函数返回一个基本数据类型(如
- 对象返回值传递
- 返回值优化(RVO):当函数返回一个对象时,C++编译器可能会应用返回值优化。在没有返回值优化的情况下,函数会先创建一个临时对象,将函数内部的对象复制到这个临时对象中(通过调用复制构造函数),然后返回这个临时对象。但是,通过返回值优化,编译器可以直接将函数内部的对象构造到接收返回值的对象的内存空间中,避免了不必要的复制操作。例如,对于函数
MyClass createObject()
,如果MyClass
是一个自定义类,在合适的条件下,编译器会直接将createObject
函数内部构造的MyClass
对象构造到接收返回值的MyClass
对象中,而不是先复制到一个临时对象再进行赋值。 - 移动语义(Move Semantics):如果编译器没有进行返回值优化,除了复制构造函数外,C++还提供了移动构造函数来更高效地处理对象返回值。移动构造函数允许将一个对象的资源(如动态分配的内存)“移动”到另一个对象中,而不是进行复制。例如,对于一个包含动态分配数组的类,移动构造函数可以将数组的指针从一个对象转移到另一个对象,避免了重新分配内存和复制数组元素的开销。当函数返回对象时,编译器可能会优先调用移动构造函数(如果定义了)来提高效率。
- 返回值优化(RVO):当函数返回一个对象时,C++编译器可能会应用返回值优化。在没有返回值优化的情况下,函数会先创建一个临时对象,将函数内部的对象复制到这个临时对象中(通过调用复制构造函数),然后返回这个临时对象。但是,通过返回值优化,编译器可以直接将函数内部的对象构造到接收返回值的对象的内存空间中,避免了不必要的复制操作。例如,对于函数
- 基本数据类型返回值传递