命令行参数 #
早就知道在 c 中是可以在 main 中获取从命令行传入的参数的。但是对于解析是一窍不通,这里简单说一下如何获取整形。
不过,这些方法都仅仅适用于简单的字符串处理,而一个简单的带参 cli 程序,基本都要求参数能够无视顺序,可选参数,这些统统都不能实现…
std::stringstream
#
这个函数用于把字符串转换成流,然后再把流输出到整形。
std::stringstream convert{arcv[1]};
int num{};
if(convert>>num){
std::cout<<num;
}
std::stoi #
这个就是现代C++的方法了。
直接转换就行:
int num{std::stoi(argv[1])};
std::cout << num;
lambda表达式 #
lambda表达式看起来像这样子:
[<capture>](<parameter list>) -> <return type> {<body>};
参数列表可以省略,返回值也可以省略,会自动推断。
所以说,最简的 lambda 表达式:[]{}。
捕获 #
对于以下变量来说,不需要捕获也可使用:
- 全局变量,静态局部变量,线程局部变量
- 常量表达式(包括被编译器视为常量表达式的变量)
捕获语法是 lambda 表达式中一个不可缺少的部分,词法分析就是通过 [] 来发现这是一个表达式的。
lambda表达式不和{}一样,不能访问上级大括号中的局部变量。它只能访问静态和全局的变量,以及constexpr。
而捕获其实就是把原来应该能让 lambda 能够使用的局部变量让它使用。
捕获的时候可以声明新变量的,在 [] 内,可以使用当前块内能访问的变量来参与运算。
捕获有两种类型:默认捕获和单个捕获。
按对象捕获 #
假设有变量 a b:
- 传值:
[a,b]。默认传入变量的常量值,如果需要修改,需要在大括号前加上mutable。 - 传引用:
[&a,&b]
默认捕获 #
默认捕获会捕获 lambda 中提到的所有变量。
[=]:传入值[&]:传入引用
不允许同时使用默认捕获和按对象的捕获。
创建闭包 #
提到闭包这个词,就不得不提函数式编程了。简单来说,函数是“一等公民”,就像我们传统的命令式编程的变量(对象)一样,可以作为参数传递,可以被返回,可以运行时创建等等。
那么闭包就是保存了当前上下文中某些状态的函数。
在C++中,想实现一个闭包,还是得定义一个普通函数(虽然说跟一般的匿名函数印象不太一样):
std::function<int(int)> multiply(int factor) {
return [factor](int base) { return base * factor; };
}
int main() {
auto multiply_3 = multiply(3);
auto multiply_4 = multiply(4);
std::cout << multiply_3(3) << '\n';
std::cout << multiply_4(4);
}
multiply_3 和 multiply_4 就是一个闭包。
实现一个简单的函数 #
lambda 表达式可以作为 lambda 类型的变量存储。
#
auto square{[](int base) { return base * base; }};
std::cout << square(3);
也可以声明的时候就调用: #
[]() { std::cout << "Hello world!\n"; }();
// 前一个括号是形参列表,后一个括号表示调用函数
值得提的一点是,这其实不是真正的函数,实际上只是重载了 () 实现像函数一样的调用。
在算法库的应用 #
前面在 <algorithm> 中,最后一个参数是比较函数的地址,其实我们可以直接在这里写 lambda 函数。
lambda推断 #
对于实参推断(给出 (auto a, auto b) 的情况,只要函数体里面的能匹配,那么没有问题,因为编译器会自动生成重载的 lambda 表达式。
对于返回值,要求所有返回的地方一致才能推断,否则,请使用 -> 手动指定返回值类型。
带捕获的 lambda 拷贝
#
如果对带捕获的 lambda 表达式操作的时候,目标的类型是 std::function,那么会发生隐式类型转换,将 lambda 闭包进行深拷贝,使得多次使用 std::function 构造的多个对象出现独立的拥有原始闭包状态的副本。
在 lambda 构造的时候使用了 mutable 的值捕获,那么每次捕获的变量都是原始的捕获值。
解决方法是使用 std::ref 来包装 lambda 表达式。对于传入参数不一致的情况,它会返回一个 std::reference_wrapper 对象,保证内部存储的是对原始 lambda 对象的引用,从而保证每次调用的是同一个 lambda 表达式对象。
运算符重载 #
有三种办法重载:
友元函数:如果想要重载操作数是类对象的运算符,为了获取类内成员的访问权限,可以把此重载函数以友元的形式声明在类内。
普通函数:同时可以使用 getter 和 setter 来避免直接访问内部的成员变量。
类成员函数:仅适用于左侧操作数是类成员的情况,此时右操作数就作为参数。
一些选择建议:
- 重载赋值、下标(
[])、函数调用、成员选择(->) -> 类成员函数 - 重载一元运算符 -> 类成员函数
- 不修改左操作数 -> 普通/友元函数
- 左操作数是内置类型无法修改 -> 普通/友元函数
- 左操作数可以修改 -> 类成员函数
重载函数一般返回左侧的对象的引用(以便可以连续用 . 调用)或者返回一个同类型的对象(用于连续使用重载运算符比如 +=)。
重载一元+/- #
由于一元的 +/- 是类成员函数,而且没有参数,所以不会和二元的混淆。
重载自增/减 #
前自增/减参数列表没有参数,而后自增/减有一个 int 用于占位,从而将两者区分。
由于后自增/减要返回原来的值,所以需要一个临时变量用于返回,相比前自增/减多了开销,更倾向使用前自增/减。
下标访问重载 #
下标访问的重载返回值一般使用 auto&,从而作为左值被修改。
在C++23之前,如果定义类的时候使用 const,那么就需要添加一个成员重载函数:
const auto& operator[](T t) const;
这个函数被常量对象优先使用,被非常量对象次优先使用。
在C++23,用右值引用解决此问题。
飞船运算符 #
外来语言的符号,用于比较两个数大小关系。
返回下面类型:
strong_ordering: 有equivalent、equal、greater 和 less。
weak_ordering: 比 strong 少了相等,因为是弱,只有等效。
partial_ordering: 相比 weak 多了 unordered,用于表示无法比较的情况。
从上往下可以隐式转换。
类型转换重载 #
类型转换重载也是 operator 操作符,不过重载的目标其实有点像C的强制转换:
class MyClass{
//...
public:
operator int() {
// conversion
return <intType_obj>;
}
};
无须返回值,也无须参数,因为显而易见的返回目标类型,传入的是这个类对象,这也必须是一个类成员函数。
建议加上 explicit 关键字,因为我们不希望隐式转换带来的不确定性。
如何平衡类型转换重载和转换构造函数?
假设要把A转换成B:
A可改,B不可改。那么就定义一个类型转换的重载。
A不可改,B可改。那么就定义一个参数只含A的转换构造函数,让B去接收A。
A、B都不可改。那么定义一个普通函数实现转换吧。
复制运算符重载 #
在动态分配内存中,有时为了避免自己赋值给自己,用一个 if 来设置保护防止出现空指针问题。
在三/五/零原则中,如果不带指针,那么应该不应该定义这个复制运算符重载。(其他:析构、复制构造,移动运算符,移动构造函数)