基本概念-函数_预处理¶
函数传参¶
pass by value¶
对象被复制,并赋值给方法 f(T x) 的输入参数
优点: - 在函数内对参数进行的更改不会影响到实参
缺点: - 如果复制的参数很大(例如,包含多个数据成员的结构体),将导致性能损失
何时使用: - 内置数据类型和小对象(≤ 8字节)
何时不使用: - 固定大小的数组,会退化成指针。低效,且不会保存数组的尺寸信息 - 大型对象
pass by pointer¶
变量的地址被复制,并赋值给方法 f(T* x) 的输入参数
优点: - 允许函数改变实参的值 - 实参未被复制(速度快)
缺点: - 实参可能是空指针 - 解引用指针比直接访问值慢
何时使用:
- 原始数组(如果是只读,使用 const T*)
何时不使用: - 其他所有情况
pass by reference¶
变量的引用被复制,并赋值给方法 f(T& x) 的输入参数
优点: - 允许函数改变实参的值(与指针相比,可读性更好) - 实参未被复制(速度快) - 引用必须初始化(没有空指针) - 避免了隐式转换(非 const T& 的情况)
何时使用: - 除原始指针外的所有情况
何时不使用: - 通过值传递可能带来性能优势,并且在内置数据类型和小型对象(容易复制的对象)使用时,可以提高可读性
struct MyStruct;
void f1(int a); // pass by-value
void f2(int& a); // pass by-reference
void f3(const int& a); // pass by-const reference
void f4(MyStruct& a); // pass by-reference
void f5(int* a); // pass by-pointer
void f6(const int* a); // pass by-const pointer
void f7(MyStruct* a); // pass by-pointer
void f8(int*& a); // pass a pointer by-reference
//--------------------------------------------------------------
char c = 'a';
f1(c); // ok, pass by-value (implicit conversion)
// f2(c); // compile error different types
f3(c); // ok, pass by-value (implicit conversion)
函数签名与重载¶
函数签名 (signature) 定义了(特定的)函数的输入类型,以及模板函数的输入和输出类型。
一个函数签名包括参数的数量、参数的类型以及参数的顺序。
- C++标准禁止仅在返回类型上有差异的函数声明
- 具有不同签名的函数声明可以有不同的返回类型
函数重载 (overloading) 允许使用相同名称但具有不同签名的多个不同函数
void f(int a, char* b); // signature: (int, char*)
// char f(int a, char* b); // compile error same signature
// but different return types
void f(const int a, char* b); // same signature, ok
// const int == int
void f(int a, const char* b); // overloading with signature: (int, const char*)
int f(float); // overloading with signature: (float)
// the return type is different
在 C++ 中,=delete 关键字用于显式地删除某个函数,以禁止使用某些函数重载,增强代码安全性和清晰度。
void g(int) {}
void g(double) = delete;
g(3); // ok
g(3.0); // compile error
#include <cstddef> // std::nullptr_t
void f(int*) {}
void f(std::nullptr_t) = delete;
f(nullptr); // compile error
函数的默认参数¶
默认参数是一个具有默认值的函数参数。
- 如果用户没有为这个参数提供值,将使用默认值
- 所有默认参数必须是最右边的参数
- 默认参数只能声明一次
- 默认参数可以提高编译时间并避免冗余代码,因为它们避免了定义其他重载函数
void f(int a, int b = 20); // 声明
//void f(int a, int b = 10) { ... } // 编译错误,b的默认值已在声明中设置
void f(int a, int b) { ... } // 定义,b的默认值已经设置
f(5); // b 是 20
C 中的函数指针¶
标准C通过函数指针的概念实现了泛型编程能力和可组合性。
一个函数可以作为指针传递给另一个函数,并作为“间接调用”执行。
#include <stdlib.h> // qsort
int descending(const void* a, const void* b) {
return *((const int*) a) > *((const int*) b);
}
int array[] = {7, 2, 5, 1};
qsort(array, 4, sizeof(int), descending);
// 数组变为:{ 7, 5, 2, 1 }
int eval(int a, int b, int (*f)(int, int)) {
return f(a, b);
}
// 类型:int (*)(int, int)
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
cout << eval(4, 3, add); // 输出 7
cout << eval(4, 3, sub); // 输出 1
问题: - 安全性:在泛型情况下没有对参数类型的检查(例如,qsort) - 性能:任何操作都需要对原始函数进行间接调用。不可能进行函数内联
函数对象¶
函数对象 (function object),或称为仿函数 (functor),是一种可以作为参数处理的可调用对象。函数对象是实现了 operator() 的任何对象。这个操作符允许对象像普通函数那样被调用。
#include <algorithm> // for std::sort
struct Descending { // <-- 函数对象
bool operator()(int a, int b) { // 函数调用操作符
return a > b;
}
};
int array[] = {7, 2, 5, 1};
std::sort(array, array + 4, Descending{});
// 数组变为:{ 7, 5, 2, 1 }
优点:
- 安全性:始终可以进行参数类型检查。可能涉及模板。
- 性能:编译器将
operator()嵌入目标函数的代码中,并编译该程序。操作符内联是标准行为。 - C++11 通过提供称为 lambda 表达式的较少冗余的函数对象简化了这一概念。例如,可以将
Descending函数对象用 lambda 表达式替换如下:[](int a, int b) { return a > b; }。
lambda 表达式¶
定义¶
C++11 的 lambda 表达式是一个内联的 (inline) 局部作用域 (local-scope) 函数对象 (function object):
auto x = [capture clause] (parameters) { body }
- 捕获子句([capture clause])标记了 lambda 的声明以及如何捕获局部作用域参数(按值捕获、按引用捕获等)。例如,
[=]表示捕获所有外部变量通过值,[&]表示捕获所有外部变量通过引用。 - 参数列表(parameters)是 lambda 的普通函数参数(可选的)。
- 函数体(body)是一个普通的函数体。
等号右侧的表达式是 lambda 表达式,通过该表达式创建的运行时对象 x 是闭包 (closure)。在 C++ 中,一个 lambda 表达式的实例被称为闭包。闭包不仅包括函数逻辑,还包括捕获的环境(即通过捕获子句指定的外部变量)。
# include <algorithm> // for std::sort
int array[] = {7, 2, 5, 1};
auto lambda = [](int a, int b){ return a > b; }; // named lambda
std::sort(array, array + 4, lambda);
// array: { 7, 5, 2, 1 }
// in alternative, in one line of code: // unnamed lambda
std::sort(array, array + 4, [](int a, int b){ return a > b; });
// array: { 7, 5, 2, 1 }
捕获¶
Lambda 表达式通过两种方式捕获在 lambda 体内使用的外部变量:
- 按值捕获
- 按引用捕获(可以修改外部变量的值)
捕获列表可以按以下方式传递:
- [] 不捕获任何变量
- [=] 按值捕获所有变量。注意, lambda 表达式按值捕获的时候, 捕获的是 const value, 想要在 body 中修改,需要加上
mutable. 比如auto lambda = [=] () mutable {var =3;} - [&] 按引用捕获所有变量
- [var1] 仅按值捕获
var1 - [&var2] 仅按引用捕获
var2 - [var1, &var2] 按值捕获
var1并按引用捕获var2 - [=, &var1] 捕获 lambda 体内使用的所有变量按值捕获,除了
var1按引用捕获。 - [&, var1] 捕获 lambda 体内使用的所有变量按引用捕获,除了
var1按值捕获。 - 一个 lambda 表达式可以在不捕获的情况下读取一个变量,如果该变量是 constexpr
// GOAL: find the first element greater than "limit"
# include <algorithm> // for std::find_if
int limit = ...
auto lambda1 = [=](int value) { return value > limit; }; // by-value
auto lambda2 = [&](int value) { return value > limit; }; // by-reference
auto lambda3 = [limit](int value) { return value > limit; }; // "limit" by-value
auto lambda4 = [&limit](int value) { return value > limit; }; // "limit" by-reference
// auto lambda5 = [](int value) { return value > limit; }; // no capture
// compile error
int array[] = {7, 2, 5, 1};
std::find_if(array, array + 4, lambda1);
constexpr int limit = 5;
int var1 = 3, var2 = 4;
auto lambda1 = [](int value){ return value > limit; };
auto lambda2 = [=, &var2]() { return var1 > var2; };
-
C++14 Lambda 表达式参数可以自动推导
auto x = [](auto value) { return value + 4; }; -
C++14 Lambda 表达式参数可以初始化
auto x = [](int i = 6) { return i + 4; }; -
Lambda 表达式可以组合
auto lambda1 = [](int value) { return value + 4; }; auto lambda2 = [](int value) { return value * 2; }; auto lambda3 = [&](int value) { return lambda2(lambda1(value)); }; // 返回 (value + 4) * 2 -
一个函数可以返回一个 lambda(也可以进行动态分派)
auto f() { return [](int value) { return value + 4; }; } auto lambda = f(); cout << lambda(2); // 输出 "6" -
Lambda 捕获是按 const 值
int var = 1; auto lambda1 = [&]() { var = 4; }; // ok lambda1(); cout << var; // 输出 '4' // auto lambda2 = [=]() { var = 3; }; // 编译错误 // lambda 的 operator() 是 const 的 auto lambda3 = [=]() mutable { var = 3; }; // ok lambda3(); cout << var; // 输出 '4', lambda3 按值捕获 -
[this] 捕获当前对象(
*this)按引用捕获(在 C++17 中这是隐式的) - [x = x] 捕获当前对象成员
x按值捕获(C++14) - [&x = x] 捕获当前对象成员
x按引用捕获(C++14) - [=] 默认捕获
this指针按值已在 C++20 中被弃用
class A {
int data = 1; // 类成员变量
void f() {
int var = 2; // 局部变量
auto lambda1 = [=]() { return var; }; // 按值捕获局部变量,返回 2
auto lambda2 = [=]() { int var = 3; return var; }; // 在新作用域定义 var,返回 3
auto lambda3 = [this]() { return data; }; // 按引用捕获 `this`,返回成员 data 的值 1
auto lambda4 = [*this]() { return data; }; // 按值捕获当前对象(C++17),返回成员 data 的值 1
// auto lambda5 = [data]() { return data; }; // 编译错误:'data' 在此作用域不可见
auto lambda6 = [data = data]() { return data; }; // 按值捕获成员 data,返回 1
}
};
预处理¶
预处理指令 (preprocessor directive) 是任何以井号 hash (#) 开头的行,它告诉编译器在编译之前如何解释源代码。
宏是预处理指令,它会用替换文本替换代码中任何出现的标识符。
- 宏是不好用的: 不要使用宏扩展!! 或尽可能少地使用
- 宏无法直接调试
- 宏扩展可能会有意想不到的副作用
- 宏没有命名空间或作用域
预处理器 (preprocessors) 会处理所有以 # 开头的语句:
- preprocessor可以看作是一个程序。preprocessor运行时,从上到下扫描程序文件,查找预处理指令(preprocessor directives)。预处理指令就是 # 开头的指令,并且结尾一定没有分号;。
- #include "my file.h" 插入代码到当前文件
- #define MACRO <expression> 定义一个新的宏
- #undef MACRO 取消定义一个宏 (出于安全原因,宏应尽早被取消定义)
- 多行预处理:在行末使用反斜杠 \
当使用 #include files 预处理指令时,preprocessor会把#include files 这个预处理指令替换为相应的.h文件内容(用来声明变量)。然后在编译结束之后,在链接阶段把链接到的函数添加进去。“”引用的.h表明.h文件是自己写的,编译器将会优先在本地的文件夹寻找.h文件。<>引用的.h文件表明.h文件不是自己写的,编译器将会跑到include directories里去寻找.h文件,include directories是project settings/IDE settings/complier settings的一部分。总之,只要不是在当前文件中的引用,而是来自compiler,IDE,第三方库,操作系统等等的.h文件,就用<>引用。如果是linux系统,就会去 /usr/include/文件夹里面找 <> 的头文件。
总之,用 <> 的地方可以用引号,但是用引号的地方不能用 <>。这样 <> 可以保证不会把当前目录的内容引入。
预处理指令只在当前文件中有效。预处理指令的语法是独立于C++正式程序的另一套语法。预处理指令只和自己相对位置敏感,和C++程序没有关系。
条件编译¶
#if <condition>
code
#elif <condition>
code
#else
code
#endif
条件编译指令(#if, #ifdef, #ifndef, #else, #elif, #endif)
- #if defined(MACRO) 等同于 #ifdef MACRO
- #elif defined(MACRO) 等同于 #elifdef MACRO(C++23) 检查宏是否已定义
- #if !defined(MACRO) 等同于 #ifndef MACRO
- #elif !defined(MACRO) 等同于 #elifndef MACRO(C++23) 检查宏是否未定义