目录

表达式

第 6 章笔记


函数基础

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

函数的声明和定义

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

函数的调用

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参数 等价形式
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

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

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

函数体

隐式返回与显式返回

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

返回初始化列表

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

返回自动对象,指针的注意事项
返回值优化

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

函数的返回类型

函数返回的方式

// 经典返回
int fun(int a, int b) {}
// 尾部返回 C++11
// 使用泛型编程的时候,参数的类型由模板参数决定,在后面写返回类型更方便
// 返回类的成员函数的自定义类型,在后面写更方便
// 尾部返回可以自动查找域
auto fun(int a, int b) -> int {}

// 自动推导返回 C++14
// 基于 return 语句进行推导
// 返回 int
auto fun(int a, int b) { return a + b; }
如果存在多个 return 语句,那么返回结果的类型必须一致,否则需要使用 constexpr if 来处理:
// 与运行期条件不同,constexpr 在编译器生效
// 通过 constexpr value 来舍弃某个分支的 return
// 实际上编译结果只存在一个 return 的类型
constexpr bool value = true;
auto fun()
{
    if constexpr (value) 
    { 
        return 1; 
    } 
    else 
    {
        return 3.14; 
    }
}

返回类型与结构化绑定

struct Str
{
    int x;
    int y;
};

Str fun()
{
    return Str{};
}

int main(int argc, char const *argv[])
{

    // C++ 17
    // v1 v2 实际上是引用
    // 使用 v1, v2 直接绑定了 x 和 y
    auto [v1, v2] = fun();
    v1;
    v2;

    //Str res = fun();
    return 0;
}

nodiscard 属性

// 使用 warning 提醒该返回值非常重要,不能忽略
[[nodiscard]] int fun(int a, int b) 
{
    return a + b;
}

int main()
{
    // 返回值没有用到
    // warning: ignoring return value
    fun(2, 3);
}

函数重载和解析

函数的重载

函数重载和 name mangling

重载函数会被 mangle 为以下的格式:

# 除了函数名,还有参数的信息
000000014000145c T _Z3fund
0000000140001450 T _Z3funi

name lookup

void fun()
{
    std::cout << "global fun\n";
}

namespace myNS
{
    void fun()
    {
        std::cout << "NS fun\n";
    }
    void g() 
    {
        fun();
    }
}

int main(int argc, char const *argv[])
{
    // 限定名称查找
    // 指定了查找的区域
    // 域可以由 Namespace,class(sub class) 形成
    ::fun();
    myNS::fun();

    // 非限定名称查找
    // 会从当前的域(myNS)开始查找
    // 一旦查找到名称,就会停止 name lookup
    // 调用 myNS::fun()
    myNS::g();

    // 如果当前域不存在 fun() 则会去上级查找
    // 查找的顺序从上到下,如果域内的 myNS::fun() 的定义在 g() 之后
    // 那么会调用 ::fun()
    // 可以提前声明 myNS::fun() 来告诉编译器本域中存在 myNS::fun() 的定义
    return 0;
}

名称隐藏
argument dependent lookup

重载解析

  1. 过滤不能被调用的版本(non-viable candidate)
    1. 参数数量不匹配
    2. 实参无法被转变为形参
    3. 实参不满足形参的限制条件
  2. Best match
    1. 单个参数
      1. 选择匹配等级高的版本
匹配等级

级别越低,匹配程度越高

涉及 low const 的情况

// 左值会优先选择非 const 版本
// 编译器会倾向选择保证对应变量的访问权限的函数
void fun(int& x){}
void fun(const int& x){}

// int& 
fun(x);
// const int&
fun(3);

多个形参的情况

其他内容

递归函数

内联函数

内联函数可以放到头文件中

constexpr 函数

// 在编译期和运行期均可调用
constexpr int fun(int x ){ return x};
// 编译器调用
constexpr int x = fun(3);
// 运行期调用
// 由于参数只能在运行期确定,因此是运行期调用
int y;
std::cin >> y;
fun(y);

consteval 函数

  • inline 的出发点是让函数内容在 main 中的调用处展开,因此兼容性最强
  • constexpr / consteval 更强调在编译期计算

函数指针

函数类型的定义

u,sing K = int(int);
// fun 被声明为 int(int) 类型的函数
K fun;
// 不能通过赋值的方式给出定义
K fun = { return 0; } // error

函数指针

#include <iostream>
#include <vector>
#include <algorithm>

using K = int(int);

int inc(int x)
{
    return x + 1;
}

// 将指向 inc 的函数指针作为参数
// 类型为 K*,也就是 int(*)(int)
// 好处:高阶函数逻辑不变,通过调用的函数来实现不同的功能
// 实例:泛型算法,很多泛型算法接收函数指针作为参数
int twiceInc(K* inc, int x)
{
    int temp = (*inc)(x);
    return temp * 2;
}


int main(int argc, char const *argv[])
{
    // 不是函数声明,是指针
    // 可接收 int(int) 类型的函数
    K* fun = &inc;

    // 通过指针调用函数
    // 解引用->再调用
    std::cout <<(*fun)(100) << std :: endl;
    std::cout << twiceInc(fun, 100) << std::endl;

    // 泛型算法实例
    std::vector<int> a{1,2,3,4,5};
    std::transform(a.begin(), a.end(), a.begin(), fun);

}

函数的退化

// fun 是 int(*)(int)
 auto fun = inc;

函数指针与重载

void fun(int) { std::cout << "single para;\n"; }
void fun(int, int) {std::cout << "double para;\n"; }

int main(int argc, char const *argv[])
{
    // 无法通过 auto 来推断 fun 的类型
    // fun 在此处包含了两个不同类型的函数
    // auto 无法确定选择哪一个 fun
    auto x = fun;

    // 需要显式的指定类型
    using K = void(int);
    K* x = fun; //int(int) type
    return 0;
}

函数指针作为返回值

int inc(int x) { return x + 1; }
int dec(int x) { return x - 1; }

auto fun(bool condition, int x)
{
    // 返回的实际上是函数的指针
    return condition ? inc(x) : dec(x);
}

int main(int argc, char const *argv[])
{

    std::cout << fun(true,1) << std::endl;
    return 0;
}