函数声明

函数在调用前应先声明或定义

声明给出了函数原型 ( Function Prototype ),即函数名、返回值类型、参数列表

1
ret_type function_name( parameter_list );
  • 原型中的参数名是可选的,只需指出类型

函数定义

1
2
3
4
5
ret_type function_name( parameter_list )
{
// statements
return value;
}
  • 返回值类型为 void 的函数无返回值,可以没有 return 语句,或者使用 return;

  • 形参和实参

    • 形参 ( parameter ): 函数定义中参数列表中的参数,是函数中的局部变量,对外不可见

    • 实参 ( argument ): 函数调用时实际给函数传入的参数

  • 为什么要使用函数原型

    假设不使用函数原型,而是直接使用函数定义,当一个源文件中的一组函数存在互相调用关系时,就必然导致至少一个函数在调用前未定义。例如:

    1
    2
    3
    4
    5
    6
    函数1(){
    if( 条件a ) 调用函数2; // 错误,函数二调用前未定义
    }
    函数2(){
    if( 条件b ) 调用函数1;
    }

对于单个源文件的程序,通常将函数声明放在 main 函数之前,函数定义放在 main 函数之后。对于大型的程序,则要考虑分文件编写

函数的分文件编写

  1. 创建后缀名为.h的头文件

    在头文件中包含函数需要包含的头文件

    在头文件中写函数的声明

  2. 创建后缀名为.cpp的源文件

    在源文件中包含对应的头文件

    在源文件中写函数的定义

函数调用

传值调用、传址调用、引用调用

  • 引用注意事项:

    引用在定义时必须初始化 int& b = a;

    引用初始化后,不能改变

  • 区别:

    1. 传值

      实参和形参是两个不同的地址空间

    2. 传址

      实参是变量的地址,形参是一个指针,函数内可以通过指针修改其所指的值

      1
      2
      3
      void fun(int* a){...;}

      fun(&b);

      将数组名传递给函数实际传递该数组首元素的地址,函数定义中形参 char s[]char *s 等价

      传递多维数组时,需要指明除第一维之外其他维的长度

    3. 传引用

      形参是实参的别名,对形参的操作就是对实参的操作

      1
      2
      3
      void fun(int& a){...;}

      fun(b);
  • 引用的本质: 常指针

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //发现是引用,转换为 int* const ref = &a;
    void func(int& ref){
    ref = 100; // ref是引用,转换成 *ref = 100;
    }
    int main(){
    int a = 10;

    //转换为 int* const ref = &a;
    int& ref = a;
    ref = 20; // 转换为 *ref = 20;

    func(a);

    return 0;
    }

返回引用

  1. 注意: 不要返回局部变量的引用

    1
    2
    3
    4
    5
    6
    7
    8
    int& fun(){
    /*以下是错误的
    int a = 10;
    return a;
    */
    static int a = 10; //静态变量
    return a;
    }
  2. 函数的调用可以作为左值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    int vals[] = {1,2,3,4,5};

    int& setValues(int i){
    return vals[i]; // 返回第i个元素的引用
    }

    int main() {
    // 返回引用的函数可以做左值
    setValues(1) = 7;
    setValues(3) = 9; //vals: 1,7,3,9,5
    }

常量引用

用来修饰形参,防止误操作

1
2
3
4
5
//打印数据函数
void showValue(const int& val){
//val = 1000; //不允许修改
cout<<"val = "<<val<<endl;
}
1
2
3
4
5
6
7
int& ref = 10;  //引用本身需要一个合法的内存空间,因此这行错误

//常量左值引用既可以操作左值,又可以操作右值,但是不允许修改
const int& ref = 10;

int&& a = 10; //右值引用,还能修改 a 的值
a = 20;

函数递归

待更新…

函数指针

函数的二进制代码存放在代码段,函数的地址是它在内存中的起始地址。如果把函数的地址作为参数传递给函数,就可以在函数中灵活的调用其他函数

函数指针就是一个指向函数首地址的指针

声明函数指针

声明函数时,要提供函数的类型,即返回值参数列表

1
ret_type (*func_pointer)( parameter_list );

参数列表只需指明类型,不用写变量名

例如:int Max(int a, int b); 的函数指针: int (*pf)(int, int);

void printData(int (*pf)(int, int), int a, int b); 的函数指针: void (*pD)(int (*)(int, int), int, int);

使用自动类型推导创建函数指针:

1
2
const double* f3(const double*, int);
auto p3 = f3;

给函数指针赋值

函数名就是函数的地址,用函数名给函数指针赋值

1
2
3
double pam(int);
double (*pf)(int) = nullptr;
pf = pam;

通过函数指针调用函数

两种方式调用

  • pf(1);
  • (*pf)(1);

回调函数

回调函数:Callback 即 call then back. 回调函数是指通过参数将函数传递到其它代码的,某一块可执行代码的引用。

例一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 三个回调函数
int Max(int a, int b)
return a > b ? a : b;
}
int Min(int a, int b){
return a > b ? b : a;
}
int Sum(int a,int b){
return a + b;
}

void print(int (*Callback)(int, int), int a, int b){
cout << Callback(a,b);
}

int main(){
print(Max,1,2);
print(Min,1,2);
print(Sum,1,2);
}

例二:多线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <thread>
#include <iostream>
using namespace std;

void testThread1(){
cout << "线程1" << endl;
}
void testThread2(){
cout << "线程2" << endl;
}

int main(){

thread t1(testThread1);
thread t2(testThread2);

t1.join();
t2.join();
}

typedef 和函数指针

1
2
3
4
5
6
7
8
9
int Max(int a, int b){
return a > b ? a : b;
}

typedef int(*PFUNC)(int, int);

void print2(PFUNC pf, int a, int b){
printf("%d", pf(a, b));
}

如何理解:把 typedef 后面内容中的 PFUNC 替换成 pf, 然后整体拿到等号左边

万能指针充当函数指针

  • 万能指针:空类型的指针 void*
  • 万能指针能操作任何类型,在使用前必须做强制类型转换
1
2
3
4
5
6
7
void* p = nullptr;
int a = 0;
p = &a;
printf("%d", *(int*)p);
float fNum = 0.0f;
p = &fNum;
printf("%f", *(float*)p)

万能指针做函数指针的例子:

1
2
3
4
5
6
7
8
void print(){
printf("万能指针做函数指针")
}

void* p = nullptr;

((void(*)())p)();
(*(void(*)())p)();

自动类型推导

auto

  • auto 可用于推导变量的类型,使用 auto 声明的变量必须初始化
  • auto的自动类型推导发生在编译期,不会造成运行效率的降低
1
2
3
vector<int> vec {1,2,3,4,5};
for(auto i = vec.begin(); i != vec.end(); i++)
cout << *i << endl;

注:

  • auto 可以和 &, const 结合使用
  • auto 不能用于推导数组类型

从 C++ 14 开始,auto 可以用于推导函数的返回类型

1
2
3
4
template<typename T, typename U>
auto add( T x, U y ){
return x + y;
}

从 C++ 20 开始,auto 可以用于函数形参

1
2
3
auto add( auto x, auto y){
return x + y;
}

decltype

待更新…

Lambda 表达式

使用 lambda 表达式可以就地封装短小的功能闭包,使代码简洁,增强可读性

Lambda 的类型是无名的非联合非聚合类类型,称为闭包类型(closure type)。这个类是具有 operator() 的类,使用方式类似于函数调用

语法:

[ capture ]( params ) -> ret_type { body };

  • 返回值类型可省略,由编译器推导
  • 如果要在其他地方使用,可以使用 function 存储 Lambda 表达式,例如 std::function<int(int)> func = [](int x){return ++x;}; 或者 auto func = [](int x){return ++x;};
  • 捕获列表:
    • [=] 表示按值捕获所有变量
    • [n] 表示按值捕获变量n
    • [&] 表示按引用捕获所有变量
    • [&n] 表示按引用捕获 n
    • C++ 14 起允许在捕获列表中定义新的变量,不写类型,类型由编译器自动推导

 

捕获列表为空的 lambda 表达式会生成转换函数,可以隐式转换为函数指针类型

1
2
int(*p1)() = [] {return 0;};
auto p2 = +[] {return 0;};

 
 

lambda 表达式的 operator() 默认是 const 的,这意味着按值捕获的变量在其中不可被修改。

 

若在声明时使用 mutable 关键字,则 operator() 是非 const 的,按值捕获的变量可以在 lambda 表达式内部被修改,但不会影响外界变量

1
2
3
4
int n = 0;

[n]()mutable{n++; std::cout << n; }(); // 输出 1
std::cout << n; // 输出 0

 

按引用捕获的变量可以被修改,相当于修改外界变量

例如:

1
2
3
int n = 0;
[&n]{n++;}();
// 此时 n 的值为 1

相当于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int n = 0;

class __lambda_4_3{

public:
// 函数调用运算符()
inline /*constexpr */ void operator()() const{
n++;
}

private:
// 引用成员
int & n;

public:
// 初始化列表
__lambda_4_3(int & _n)
: n{_n} {}

} __lambda_4_3{n};

__lambda_4_3();

 
 

例1:

1
2
auto add_10 = [num = 10](int x){return num + x;};
std::cout << add_10(5) << std::endl;

例2:使用std::sort ( 定义于头文件 <algorithm> ) 对数组元素进行排序

1
2
int a[10] = {1,5,2,8,4,9,8,0,5,3};
std::sort(a,a+10,[](int a, int b){return a > b;}); // 降序

泛型 Lambda

从 C++ 14 开始,Lambda 函数的形参可以使用 auto 来产生泛型

1
auto is_bigger = [](auto a, auto b){ return a > b;};

内联函数 (inline function)

内联函数与宏(macro) 的比较:

  • 宏的缺点1:容易出错

    1
    2
    3
    4

    #define MYADD(x,y) ((x)+(y))
    // 若 x+y 处不加括号,那么会先计算乘法,ret = 10 + 20 * 20
    int ret = MYADD(10,20) * 20;

    即使加了足够多的括号,有些情况依然与预期效果不符

    1
    2
    3
    4
    5
    #define MYCOMPARE(a,b) (((a)<(b))?(a):(b))

    int a = 10, b = 20;
    //宏函数展开时,有两次++a
    int ret = MYCOMPARE(++a,b); //ret的值为12
  • 宏的缺点2:不可调试

  • 内联函数的优点:

    1. 将函数代码在调用处展开,避免了函数调用的开销,以空间换时间
    2. 可调试:定义为内联的函数在程序的 Debug 版本中不会内联,像普通函数一样便于调试

说明符:inline (加在函数定义之前)

1
2
3
4
5
6
7
// 声明前不需要加 inline
int myCompare(int a, int b);
// 定义前加 inline
inline int myCompare(int a, int b)
{
return a<b?a:b;
}

通常对较短的函数使用内联,可在函数声明处直接写函数的定义

 

在类内部定义的函数自动成为内联函数

 

内联函数应在头文件中定义。编译器在调用点内联展开函数的代码时,必须能够找到内联函数的定义才行,因此头文件中仅有函数声明是不够的。

 

如果头文件仅有声明,那么在每个使用到该内联函数的源文件中,都要对该内联函数进行定义,而且每个源文件里的定义必须完全相同。

 

在函数定义前加 inline 仅仅是给编译器的一个优先内联的建议,编译器不一定会接受这种建议,而没有声明为内联函数的,编译器也可能自动将此函数做内联编译。一个好的编译器会内联小的、简单的函数

默认参数 (default argument)

  • 函数的默认参数通常设置在函数原型中

    int add(int, int = 0, int = 0)

  • 默认参数的值必须是常数值或常量

  • 在参数列表中,带默认参数的形参必须放在后面,即假如某个参数有默认值,那么其后所有参数都必须有默认值

  • 在函数调用时,实参只能从右往左省略,避免出现二义性

函数的默认参数应在函数名称最早出现时设置,如果不使用函数原型,那么也可以在函数定义的函数头中设置

1
2
3
4
5
6
7
8
9
int add(int a, int b=0, int c=0){
return a+b+c;
}

int main(){
add(1); // 省略 b, c
add(1,2); // 省略 c
add(1,2,3);
}

占位参数

1
2
3
4
5
6
7
8
//函数占位参数
void func(int a,int){
cout<<"this is func"<<endl;
}
int main(){
func(10,10); //占位参数必须填补
return 0;
}
1
2
3
4
5
6
7
8
//占位参数也可以有默认参数
void func(int a,int = 10){
cout<<"this is func"<<endl;
}
int main(){
func(10); //占位参数可以省略
return 0;
}

函数重载 (overload)

作用: 函数名可以相同,提高复用性

函数重载满足条件:

  • 同一个作用域下
  • 函数名称相同
  • 函数参数类型不同,或者个数不同,或者顺序不同

注意: 函数的返回值不可以作为函数重载的条件

注意事项:

  1. 引用作为重载的条件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void func(int& a){
    cout << "func(int& a)调用" << endl;
    }
    void func(const int& a){
    cout << "func(const int& a)调用" << endl;
    }
    int main(){
    int a = 10;
    func(a); // 优先调用无const: func(int& a)
    func(10); // 调用有const: func(const int& a)
    }
  2. 函数重载碰到默认参数

    应避免出现二义性的情况

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    int add(int a){
    return a;
    }
    int add(int a, int b = 0){
    return a + b;
    }

    int main(){
    cout << add(1); // 错误,有多个匹配,函数调用有歧义 ( ambiguous )
    cout << add(1,2); // 调用第二个版本
    }