C++函数
# 函数
# 介绍
函数是一个命名的代码块,通过调用函数可以执行对应的代码,函数还可以有零个或多个参数,且通常还会有一个结果(返回值)。函数可以重载,也就是说同一个名称可以被多个函数使用。
一个典型的函数定义包括返回值类型、函数名称、零个或多个形参、函数体,函数体即调用函数时会执行的语句块。 调用函数需要使用调用运算符"()"一对圆括号,该运算符作用于函数或指向函数的指针,圆括号内可以有零个或多个实参,实参用于初始化函数的形参。调用表达式的类型就是函数的返回类型。
函数调用时,会使用实参初始化函数对应的形参,并将控制权从主调函数转移给被调函数,此时主调函数的指向被暂时中止,被调函数开始执行。
编程中,形参一般使用(param)eter表示,实参一般使用(arg)ument表示。
# 使用函数
格式:
// 定义函数
return-type name(param-type name,...){
function-statements
return expression;
}
// 调用函数
function-name(arg-value,...);
// 调用函数并接收函数返回值
return-type name = function-name(arg-value,...);
2
3
4
5
6
7
8
9
10
例如:
int addon(int a, int b){
return a + b;
}
int main(){
// 输出5
cout << addon(2, 3) << ends;
return 0;
}
2
3
4
5
6
7
8
# 全局对象
在C++中,名称有作用域,名称仅在作用域内可见。对象有生命周期,生命周期是程序执行过程中该对象存在的一段时间。 在所有函数体之外定义的对象会作用于整个程序,因此这些对象被称为全局对象,在整个程序中可见。这些全局对象随着程序的执行而创建,直到程序结束才会销毁。
# 局部对象
# 介绍
函数体是一个语句块,块会构成一个新的作用域,因此函数形参和函数内部定义的变量被称为局部变量,局部变量对应着局部对象,仅在函数作用域内可见。
局部对象又分为自动对象和局部静态对象,两者的区别是生命周期不同。
# 自动对象
自动对象是只存在于语句块执行期间的对象,当语句块执行结束后,块中的自动对象就会自动销毁。 形参对应的就是自动对象,函数开始时为其申请内存空间,函数终止时进行销毁。 普通局部变量也是自动对象,在变量定义时进行创建,函数终止时进行销毁。
# 局部静态对象
局部静态对象可以令局部变量的生命周期延长到程序结束才销毁,但仍只能作用于所在作用域中。
定义局部静态对象需要使用static关键字,来将局部变量定义为static类型,定义格式:static type name;
例如:
int count_call() {
// 给定初始值则使用初始值进行初始化,否则使用默认初始化。
static int count = 1;
// 每次函数结束返回count,然后count局部变量进行自增。
return count++;
}
int main() {
for (int i = 0; i < 10; i++) {
// 每次调用,其中count的值都不会重置,因为count是局部静态对象。
cout << count_call() << endl;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 函数声明
# 介绍
函数声明又称函数原型,用于描述函数的接口所需的全部信息,也就是函数的三要素:返回类型、函数名、形参类型。 函数只能定义一次,但可以声明多次。函数声明和函数定义非常相似,区别是函数声明无需函数体,而函数定义需要函数体。
虽然函数声明写在源代码效果一样,但如果将函数声明放在头文件中,声明就能被多个源文件共享使用,可以提高可复用性、减少代码冗余,尤其是在大型项目中。
另外不将函数直接定义在头文件中,是因为如果将函数定义写在头文件中,该头文件中的函数定义会被包含进所有用到它的源文件,编译时间可能会显著增加。
例如:
void print(vecor<int>::const_iterator beg, vecor<int>::const_iterator end);
尽管函数声明中形参名不是必须的,但仍然可以写上形参名以供使用者能够更好理解寒素功能。
# 分离式编译
函数声明的作用会在分离式编译时有所体现。在C++中编译器是分离式编译的,在编译一个源文件时,它并不需要知道所调用函数的具体实现,只需要知道函数的接口信息即可。
函数声明的目的就是向编译器提供这些接口信息,使编译器能在编译调用函数的代码时进行类型检查和参数匹配。
分离式编译时,源代码A会通过链接器来找到源代码B的函数定义,链接时链接器会在所有目标文件中查找符号表,以找到源代码A所调用的函数名的函数定义,具体例子:
- 我们先将函数声明在
functions.h
头文件中。
#pragma once
int add(int a, int b);
2
- 然后将函数定义在
functions.cpp
源文件中。
// 函数定义
int add(int a, int b) {
return a + b;
}
2
3
4
- 然后在其他源文件中调用函数即可。
#include <iostream>
// 导入函数声明,以便编译器能够进行类型检查
#include "functions.h"
int main() {
int r = add(3, 4);
std::cout << r << std::endl;
return 0;
}
2
3
4
5
6
7
8
9
# 参数传递
每次调用函数时,都会重新创建它的形参,形参可以是引用类型,形参名将绑定到对应实参上,调用时称实参为引用传递或引用调用。 如果形参不是引用类型则会将传入的实参值拷贝给形参进行初始化,调用时称实参为值传递或传值调用。
拷贝较大的类型对象或容器对象时,传递效率是比较低的,并且有些类型根本不支持拷贝操作。此时就可以使用引用而不通过拷贝传递对象,还能提高传递效率。 另外如果函数无需改变引用形参的值,最好将其声明为常量引用。 例如:
bool isShorter(const string &s, const string &2){
return s1.size() < s2.size();
}
2
3
# 指针或引用形参的const
我们可以使用在声明指针或引用形参时使用const,此时会将形参初始化成底层const对象。也就是可以改变指针本身,但不能改变指针所指对象的值。
# 数组形参
因为数组不允许拷贝、且使用数组时会将其转换成指针,所以我们无法以值传递的方式使用数组参数。所以我们传递数组给函数,实际上传递指向数组首元素的指针。 尽管不允许将数组作为值传递,但形参仍然可以写成数组的形式:
// 以下三种方式完全一样,都是接收数组的首指针
void print(const int* arrPtr);
// 表示要一个数组
void print(const int arrPtr[]);
// 表示要一个数组,期待里面有10个元素,并不做实际约束
void print(const int arrPtr[10]);
2
3
4
5
6
传递数组参数一般有如下三种方式:
// 第一种方式:使用结束标记迭代数组。
// 要求数组本身包含一个结束标记作为结尾,例如char的空字符\0标记。
void print(const char* cp) {
// 如果cp为空,则直接返回
if (!cp) { return; }
// 解引用指针,如果指针不是\0空字符,则循环输出遇到空指针。
while (*cp) {
cout << *cp++;
}
}
int main() {
char content[] = "Hello, World!";
print(content);
return 0;
}
// 第二种方式:使用标准库函数传递数组首指针和尾后指针。
void print(const int *beg, const int *end) {
while (beg != end){
cout << *beg++ << endl;
}
}
int main() {
int a[] = {1, 2, 3, 6, 9, 20};
print(std::begin(a), std::end(a));
return 0;
}
// 第三种方式:传递一个表示数组大小的形参。
void print(const int arr[], size_t size) {
for (size_t i = 0; i != size; i++) {
cout << arr[i] << endl;
}
return;
}
int main() {
int a[] = { 1, 2, 3, 6, 9, 20 };
print(a, std::end(a) - std::begin(a));
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 数组引用形参
形参的引用传递也可以用在数组上,将引用绑定到数组实参,但形参必须指定大小,例如:
// 形参是数组的引用,维度是类型的一部分,因此必须传入10大小的数组。
void print(const int (&arr)[10]){
// 因为是数组的引用所以可以像数组一样使用范围for循环。
for(auto i : arr){
cout << i << endl;
}
}
int main() {
int a[] = { 1, 2, 3, 6, 9, 20, 33, 44, 88, 99 };
print(a);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
# initializer_list类型
initializer_list类型是初始化列表,它是一种标准库类型,用于表示某种特定类型值的数组,该类型一旦被初始化,其中的元素值就不可变,它的底层实际就是常量数组。
如果一个函数的某个形参要传入的实参数量不定但实参类型都相同,就可以使用initializer_list类型。如果实参数量不定且实参类型也不同,则可以使用可变参数模板。
它定义在initializer_list头文件的std命名空间中,使用方式如下:
// 定义一个名为lst、元素类型为T的空列表。
initializer_list<T> lst;
// 列表初始化。
initializer_list<T> lst{a,b,c...};
// 元素拷贝初始化。
initializer_list<T> lst2(lst);
// 赋值会使原始列表lst和副本列表lst2共享元素
initializer_list<T> lst2 = lst;
// 返回列表元素数量
lst.size();
// 返回列表首元素的指针
lst.begin();
// 返回列表的尾后指针
lst.end();
2
3
4
5
6
7
8
9
10
11
12
13
14
使用例子:
#include <iostream>
#include <string>
#include <initializer_list>
using std::cout;
using std::endl;
using std::string;
using std::initializer_list;
// initializer_list类型可以直接作为参数传递,不需要使用指针或引用方式。
void print_msgs(initializer_list<string> il) {
// 因为是直接作为参数传递,所以可以使用它的begin()和end()方法,因此可以使用范围for循环。
for (const string &i : il) {
cout << i << endl;
}
}
int main() {
// 可以直接传递序列,使用花括号扩起。
print_msgs({ "Hello", "World!" });
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
注意:initializer_list不能作为返回类型,试了会报错,要想返回列表可以使用vector类型。
# return语句
# 返回介绍
return语句会终止当前正在执行的函数,并将控制权返回给调用该函数的地方。
执行函数时会在调用点生成一个临时量,函数执行返回的结果会用于初始化该临时量。
return语句的格式:
// 没有返回值的return只能用在返回类型是void的函数中
// void类型函数如果没有写,则会隐式在最后添加"return;"
return;
// 返回的表达式结果的类型,必须与返回类型相同。
return expression;
2
3
4
5
注意:不要返回局部对象的引用或指针,函数结束后局部对象会被释放,因此指向他们的引用或指针将变得无效。
# 返回引用
如果返回值的是引用,则可以将函数调用作为赋值表达式的左值使用,此时会将引用的对象的值,修改为右值。例如:
char& first(string& val) {
return val[0];
}
int main() {
string s = "123";
first(s) = 'A';
// 输出A23
cout << s << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
注意:返回的引用的对象不能是常量。
# 返回值列表
返回值还可以是列表类型,此时我们可以使用vector作为返回类型。例如:
vector<string> func(){
return {"Hello", "World!"};
}
int main(){
for (auto i : func()){
cout << i << endl;
}
return 0;
}
2
3
4
5
6
7
8
9
# 返回数组
C++不能直接返回数组,但我们可以返回数组的指针,以下方式可以实现:
- 使用类型别名,指定别名的数组类型、长度。
// 定义类型别名,同等于using arrT = int[5];
typedef int IntArray[5];
// 表示返回类型是一个int[5]数组的指针。
IntArray* createArray() {
// 函数返回时数组被销毁,所以数组要定义为静态对象。
static int arr[5] = {1, 2, 3, 4, 5};
return &arr;
}
int main() {
// 调用函数并使用返回的数组。
IntArray* arr = createArray();
for (int i = 0; i < 5; ++i) {
// 因为是数组指针,所以要解引用。
std::cout << (*arr)[i] << std::endl;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 函数直接声明返回的是数组类型,格式:
arr_type (*func_name(parm_list))[arr_length];
// 表示返回类型是一个int[5]数组的指针。
int(*createArray(int i))[5] {
static int arr[5] = { 1, 2, 3, 4, 5 };
arr[4] = i;
return &arr;
}
int main() {
int (*arr)[5] = createArray(9);
for (int i = 0; i < 5; ++i) {
std::cout << (*arr)[i] << " ";
}
std::cout << std::endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
- 使用置尾返回类型,将返回类型声明在形参列表后面的方式被称为置尾返回类型,该方式适用于所有返回类型,但对于返回类型较为复杂的函数最有效,格式:
auto func_name(parm_list) -> arr_type(*)[arr_length];
// 表示返回类型是一个int[5]数组的指针。
auto createArray(int i) -> int(*)[5] {
static int arr[5] = { 1, 2, 3, 4, 5 };
arr[4] = i;
return &arr;
}
int main() {
int (*arr)[5] = createArray(9);
for (int i = 0; i < 5; ++i) {
std::cout << (*arr)[i] << " ";
}
std::cout << std::endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# main函数参数
# 接收参数
在我们需要给main传递实参时,需要让main函数接收两个形参,第一个形参表示数组中字符串的数量,第二个形参是一个字符串类型的数组。声明两个形参后,我们就能通过命令行传递实参给main函数了。 argv的第一个元素必然指向程序文件名或空字符串,后面的元素才是传入的实参。 格式:
int main(int argc, char* argv[]) { ... }
# 返回值
main函数是唯一可以不指定return的非void函数,main函数中如果没有return语句,则会在末尾隐式插入一条返回0的return语句。虽然如此,但还是建议书写上更直观。
一般main函数返回0表示程序正常执行,返回非0为执行不成功。
# 函数重载
# 介绍
如果在同一作用域中的几个函数名称相同,但形参列表不同,我们称之为重载函数。在调用重载函数时,编译器会根据我们传入的实参类型来推断我们想要调用的函数。
函数重载可以减轻程序员起名字、记名字的负担。
注意:
main函数不能重载,一个项目中只能有一个main函数,用作程序入口。
函数重载不允许两个函数的形参列表的类型、数量、位置完全一致。
给函数起不同的名称可以使程序更易理解,所以对于功能不同的函数不应该使用重载。
# 定义
例子一:一个函数,多种输出方式。
void print(const int *beg, const int *end);
void print(const string s);
// 会调用上面第一个print函数定义
int a[3] = {1, 2, 3};
print(begin(j), end(j));
// 会调用上面第二个print函数定义
print("Hello");
2
3
4
5
6
7
8
例子二:数据库查询,根据传入数据类型执行不同的数据查询。
// 根据Account查找记录
Record lookup(const Account&);
// 根据Account查找记录
Record lookup(const Phone&);
Account acct;
Phone phone;
Record r1 = lookup(acct);
Record r2 = lookup(phone);
2
3
4
5
6
7
8
9
# 重载与作用域
如果在内层作用域中定义了外层中的函数名,则外层的函数名会被隐藏,在内层中仅使用内层的名称。
void print(string);
void print(double);
void print(int);
{
// 调用外层的void print(string);
print("hello");
// 新作用域:隐藏外层的print名称
void print(int);
// 会报错,因为外层的print被隐藏了,内层没有匹配string类型的print函数
print("hello");
// 正确,能匹配内层的print(int)
print(1);
// 正确,能匹配内层的print(int),值会被隐式转换
print(3.14);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 默认实参
默认实参用于定义形参的默认值,定义了默认值的形参在函数调用时,可以传入指定实参,也可以忽略不传入,不传入时会使用默认值作为实参。
默认实参也是定义在形参列表中,如果某个形参定义了默认实参,则该形参之后的形参也必须指定默认值。默认值不能是局部变量,但可以是表达式,只要表达式结果是形参类型即可。
调用函数会按照位置进行传参,不能跳过传入某个实参使用默认值,只能忽略尾部的实参。
例如:
// 定义
int add(int a, int b = 1, int c = 2);
// 调用,形参对应为a=5,b=1,c=2
add(5);
// 调用,形参对应为a=5,b=4,c=2
add(5,4);
// 错误,不能跳过传入某个实参使用默认值,只能忽略尾部的实参
add(1,,3);
// 定义,默认值可以是表达式
int add(int a = 1 + 2, int b = get_a());
2
3
4
5
6
7
8
9
10
11
12
# 内联函数
调用函数会有开销,在调用时计算机会操作寄存器保存调用时位置、返回调用时位置。而内联函数可以避免该类开销。编译器会将内联函数代码直接在调用代码处进行展开。
但很多编译器都不支持内联函数,且函数过大的话,内联函数也不太好展开。因此一般仅用于较为简单的函数使用。
使用例子:
// 在函数定义开头使用inline关键字即可
inline int big(int a, int b){
return a > b ? a : b;
}
int main(){
cout << big(1,2) << endl;
// 以上会被编译器编译成如下
cout << (1 > 2 ? 1 : 2) << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
constexpr函数
constexpr函数是指能用于常量表达式的函数,该类函数的返回类型以及所有形参类型都必须是字面值类型,且函数体必须有且只能有一条return语句。
constexpr函数的返回值不一定要是常量,但是如果将constexpr函数用在常量表达式,则必须返回常量。
字面值类型也就是算数类型、引用、指针、字符串序列等类型。
// 定义一个constexpr函数
constexpr int new_sz() {return 233;}
// constexpr表达式能够使用constexpr函数进行初始化。
constexpr int foo = new_sz();
2
3
4
# 函数指针
指向函数的指针被称为函数指针,函数指针指向函数而非对象。
函数指针的指针类型由函数的返回类型决定。另外还需要指定所指函数的形参列表,形参列表可以用来区别要使用哪个重载函数。
定义格式:func_ptr_type (*name)(param_list);
例如:
bool less(int a,int b){return a < b;}
// 定义一个未初始化的返回布尔类型的函数的指针,它需要两个int参数。
bool (*fp)(int,int);
// 初始化函数指针,&取地址符可选,写不写都行。
fp = less;
// 通过函数指针调用函数,*解引用符可选,写不写都行。
fp(1,2);
2
3
4
5
6
7
函数指针也可以作为参数,作为参数时可以直接显式书写在形参列表,也可以使用typedef为函数指针声明类型别名,然后形参写类型别名。
// 作为参数显式书写时可以写(*fp)()也可以直接写fp()
void useFunc(bool fp(int a,int b));
// 使用类型别名创建函数类型别名
typedef bool Func(int,int);
void useFunc(Func fp);
// 使用类型别名创建函数指针类型别名
typedef bool (*FuncP)(int,int);
void useFunc(FuncP fp);
2
3
4
5
6
7
8
9
10
函数指针也可以作为返回值,使用类型别名即可。
// 返回函数指针的函数。
auto Func(int) -> int(*)(int,int);
// 给返回的函数指针类型创建别名。
typedef bool (*FuncP)(int,int);
// 使用函数指针类型别名作为类型接收返回值
FuncP f1(int);
2
3
4
5
6