What & How & Why

这是本文档旧的修订版!


表达式

第 6 章笔记


函数基础

封装了代码,用于反复利用

  • 函数头
    • 需要是一个合法的标识符
    • 形式参数:函数输入的参数(parameter)
    • 返回类型:函数执行完成后得到的结果
  • 函数体:语句(必须带大括号),包含具体的计算逻辑

函数的声明和定义

  • 声明:只有函数头,没有函数体
  • 定义:两者都有
  • 声明可出现多次,定义只能出现一次(编译器无法选择哪段对应的汇编代码)
  • 编译执行顺序为从头到尾,因此需要函数的前置声明(告诉编译器该函数存在,在最后引入函数的定义)
  • 通常将声明加入 header 中,使用时包含即可
  • 只有声明没有定义会出现链接错误,这是因为链接期会寻找函数的定义。

函数定义出现多次的例外:inline

函数的调用

  • 需要函数名与实际参数(argument)
  • 实际参数会通过拷贝初始化交给形式参数进行参数传递
  • 返回值会通过拷贝初始化进行返回
  • 拷贝过程可能会被省略(涉及到优化及C++的新feature)
Stack Frame

调用函数时,函数内部的变量(局部变量)是通过 stack frame 的结构进行组织的。当牵涉到多重调用时,越往后调用的函数越处于 stack 的上方。

int Add(int x, int y)
{....}
int main()
{
    int x = 1;
    int y = 2;
    // Add 在 stack 中处于 main() 的上方
    // 从下到上,内存地址是逐渐减小的
    Add(1,2); 
}

  • 保存顺序:实参→返回地址→本地变量

函数的外部链接

以下面的程序为例:

int Add(int x, int y)
{
    return x + y;
}

int Sub(int x, int y)
{
    return x - y;
}

int main(int argc, char const *argv[])
{
    /* code */
    return 0;
}
# list all external link of demo
nm demo
# 需要关注的内容
# main
0000000140001476 T main
# add & sub
# 注意是 mangling 的名称
# 编译器友好形式
0000000140001450 T _Z3Addii
0000000140001464 T _Z3Subii

#demangle
nm demo | c++filt -t

# 结果,链接中实际保存了参数的信息
# 阅读友好形式
0000000140001450 T Add(int, int)
0000000140001464 T Sub(int, int)

  • 函数需要函数名和参数信息(参数的数量和类型)来确定,主要用于函数的重载。
  • C 不支持该类信息。如果需要支持 C 的调用,需要声明 extern “C”,请求编译器将该函数声明为 C 友好的外部链接形式
    • 声明后该函数唯一,不再支持重载。

函数详解

参数

  • 函数的形参可以是 0 到多个,参数列表为 0 时可以用 void 代替
  • 函数形参可以没有参数名称,但必须要有确定的参数类型
    • 函数类型用于编译器检查
    • 名称用于传入数值
  • 形参的变化不会引入新的函数
    • 编译器只检查函数类型
  • 初始化的顺序与形参的顺序无关
  • C++ 17 会强制省略复制临时对象

// 0参数 等价形式
void fun(){};
void fun(void){};

// 无参数名称的函数
void fun(int, int y) {}

// 名称不影响函数在编译器内的匹配
// redefination
void fun(int, int y) {};
void fuin(int a, int b) {};

// C++ 17 强制省略复制临时对象
struct Str
{
    Str() = default;
    Str(const Str&) {};
}
void fun(Str) {};
int main() 
{
    // 拷贝构造
    Str a;
    fun(a);
    // 临时对象不会拷贝构造
    fun(Str{});
}

值传递 & 地址(引用)传递

都是拷贝初始化的过程,但是行为不同:

  • 值传递:将值复制后进行传递
  • 地址(指针)传递:解引用→找到内存地址→修改内存内容
  • 引用传递:引用绑定了内存地址→修改内存内容
参数传递过程中会发生退化
  • 数组传递中,参数会由数组类型退化为指向数组首地址的指针类型
  • 数组长度无法传递
  • 多维数组的最高位才会产生退化

// 注意这里的函数参数
// int (*)[] 指参数类型是指向元素为一维数组的数组指针
void func2(int (*par)[]) { }
// 等价写法
// 注意 [4],元素数组的长度必须与 argument 匹配
void func2(int [][4]) {}
int main()
{
    int a[3][4];
    func2(a);
    return 0;
}

  • 如果需要阻止类型退化,以引用的形式作为参数

// 注意引用和指针的区别
// 此时不会退化,因此以二维数组的作为实参的形参必须是二维数组的引用
// 该方法可以传递数组长度
void func4(int (&ref)[3][4]) {}
int main()
{
    int a[3][4];
    func4(a);
    // error: invalid initialization of reference of type
    int b[2][4];
    func4(b);
}

initializer List
  • 需要使用 <initializer_list>
  • 所有参数必须类型相同

init list 维护了两个指针。其拷贝只牵涉到了指针的拷贝,因此不需要特地传引用。

函数的缺省参数

// 自定义缺省参数
void fun(int x = 0) {};
// 调用 fun(0)
fun();

  • 多参数情况下,若某个参数有缺省参数,其右边的所有参数都必须有缺省参数
  • 同个翻译单元中,缺省参数只能定义一次

// 声明中可以进行缺省参数默认值的定义
// 但同单元只能定义一次

void fun(int x, int y = 2, int z = 3);

//error: previous specification 
void fun(int x, int y = 2, int z = 3)
{
    std::cout << x + y + z << std::endl;
}

int main()
{
    fun(1);
    return 0;
}

  • 如果函数存在多个声明,只要缺省参数的定义不重复,可以组合到一起使用

// ok
void fun(int x, int y = 2, int z = 3);
void fun(int x = 1, int y, int z);

void fun(int x, int y, int z)
{
    std::cout << x + y + z << std::endl;
}

int main(int argc, char const *argv[])
{
    fun();
    return 0;
}

这种情况主要适用于多个翻译单元同时存在的情况下:编译器会根据不同的翻译单元在编译期将不同的默认参数赋予当前的翻译单元。存在这种机制主要是为了允许我们对默认参数的分级控制。比如存在如下三个文件:

  • header.h 定义了 z 的初始值
  • source 定义了 y 的初始值
  • main 定义了 x 的初始值

假设 Header 被 source 和 main 都引用,那么:

  • z 在 header 中被改动时,source / main 中的 z 都会被改变
  • 然而,source 和 main 中的默认参数是独立的,改变 source 中的 y 并不会影响 main 中的 y;同理,main 中的 x 的改变不会影响 source 中的 x。

通常,缺省值不太被改动的参数,都会放到函数的最后面。此类默认缺省值的参数设定一般都至于比较靠近根部的翻译单元中(header)

  • 缺省参数为变量时,编译器会将默认参数视作该变量

int x = 3;
void fun(int y = x){}

// 调用 fun(x),而不是fun(3)
fun();

main() 的带参数版本

int main(int argc, char const *argv[])
{
    for(int i =0; i < argc; ++i)
    {
        std::cout << argv[i] << "\n";
    }
    return 0;
}

# args 
# 1 为 argc = 0; 存储于 argv[0]
# ...
# 4 存储于 argv[3]
demo 1 2 3 4

  • argv[0] 一定是指当前程序名,可以用其表示当前程序名

std::cerr << "Usage: " << argv[0] << "detals ....";

函数体

隐式返回与显式返回
  • void 函数通常是隐式返回
  • main() 也支持隐式返回,返回类型为 int,正常结束的程序返回值为 0
  • 有返回类型的函数通常需要显式返回
    • 使用 return + 语句 / 表达式 / 初始化列表

// void 的显式返回
// 可以用作跳出当前函数体的手段
// 如果有返回值类型,那么返回值必须匹配该类型
return;

返回初始化列表

std::vector<int> fun2()
{
    return {1,2,3,4,5};
}

返回自动对象,指针的注意事项
  • 自动对象(临时,局部变量)被返回时一定要注意其生存周期
  • 不要返回局部变量的引用和指针
    • 静态局部变量的生存周期不受函数影响,因此可以使用
返回值优化
  • 分为具名和非具名返回值
  • C++ 17 会进行强制性的临时对象返回优化

# 初始化和返回默认情况下都会进行拷贝构造
# 系统会开辟内存->拷贝到内存->初始化
# 将拷贝的行为替换为直接在申请内存上直接构造,省略拷贝的过程
# 关闭返回值优化
# not work on C++ 17
fno-elide-constructors

函数重载和解析

其他内容