程设 C++ 笔记

程设课的 C++ 笔记。上半学期是语法及 STL,下半学期是算法基础。

函数及其类型

C++ 11 lambda 表达式作为返回值

C++ 提供了 auto 关键字作自动类型推断,但在 C++ 11 中不能自动推断函数的返回值类型,必须通过 decltype() 指明。而 lambda 表达式没有具名类型,因此需要通过包装在函数对象function)中来解决。

#include <functional> // `std::function` 在 <functional> 头文件里
using namespace std;
function<int (int)> foo(int a) {
    return [=](int b){ return a + b; };
}

function<> 类模板接收一个参数,函数的类型。仿照普通函数的类型(return_type(arg_type,...))写即可。

当然,在 C++ 14 及以后可以直接写成:

auto foo(int a) {
    return [=](int b){ return a + b; };
}

函数指针

一个参数表是 (int, int),返回值是 void 的函数,它的指针类型写作 void(*)(int, int)。如果函数的形参是函数指针类型,需要写成:

void foo(void(*fun)(int, int)) {} // <- 形参名 fun 在类型的里面!

当然,可以使用函数模板:

template<class F>
void foo(F fun) {}

构造函数

复制构造函数

形如 C(const C& a) 的构造函数。在没有声明的情况下,编译器会自动生成一个,执行按位拷贝(浅拷贝)。在以下三种情况会调用:

  • 使用另一个同类对象初始化
  • 初始化函数的形参
  • 函数以值的形式返回对象*

C++ 17 及以上要求实现返回值优化,如果返回值是纯右值(一个直接调用构造函数生成的临时对象),不会执行复制构造函数。此外还有具名返回值优化,可以参考谷雨同学的知乎回答

以上操作的特点是,都发生了“在内存中开辟了一块新的空间,需要用一个同类对象初始化它”的事件,而在赋值操作中,赋值号左侧的左值并不是一块新的空间,因此并不调用复制构造函数。另一种理解是,赋值操作过程中不涉及初始化 ——而构造函数本质上都是在做初始化操作。

析构函数

通常情况下(比如,排除 new 分配的空间没有用 delete 释放的情况),每次调用构造函数,在将来的某个时间点都会发生一次对应的析构。注意,这里的构造函数包括所有种类的构造函数:无参构造函数、类型转换构造函数、复制构造函数、移动构造函数等。

#include <iostream>
using namespace std;
class A {
public:
    int n;
    A(const int a): n(a) {
        cout<<"int, "<<n<<", @"<<this<<endl;
    }
    A(const A& a) {
        cout<<"&,<-"<<a.n<<",->@"<<this<<endl;
        n=a.n;
    }
    ~A(){ cout<<"~, "<<n<<", @"<<this<<endl; }
    A& operator=(const A& a) {
        cout<<"=, "<<n<<" <- "<<a.n<<", ->@"<<this<<endl;
        n = a.n;
        return *this;
    }
};
int main(){
    A a{12};
    cout<<"--1--"<<endl;
    a = 9;
    cout<<"--2--"<<endl;
}

/*
int, 12, @0x64fe08 // 类型转换构造函数
--1--
int, 9, @0x64fe0c // 隐式类型转换
=, 12 <- 9, @0x64fe08 // 赋值
~, 9, @0x64fe0c // 临时对象析构
--2--
~, 9, @0x64fe08 // a 析构
*/

移动

复制构造函数中执行开销极大的深拷贝操作,而有时我们只是想把值从变量传到另外一个变量,之后丢弃原变量,这时不必进行拷贝,可以使用移动操作。

vector<int> a{1};
vector<int> b{std::move(a)};

std::move() 标注该左值为“可移动的”,即亡值 (可以理解为丧失其值的所有权)。亡值会优先绑定到右值引用类型上,因此会优先调用移动构造函数移动赋值运算符

std::move() 本质上只是类型转换,表示可以进行移动操作,不一定实际移动了(实际移动操作在构造函数和运算符重载中进行)。之后该左值是合法未定义的。

移动构造函数和移动赋值运算符的参数是右值引用类型(T&&),注意没有 const 修饰符,因为移动操作会修改右值引用。

举例来说,在类初始化列表中初始化成员对象时,使用 std::move 移动形参对象可以减少开销。

内联函数

无论是显式声明 inline 还是隐式(成员函数函数体直接写在类定义内),编译器都不一定将其内联。经试验,gcc-O0似乎 不会执行内联,而在 -O1 下则 似乎 会——断点调试器中找不到显示或隐式函数了。所以内联就是安慰剂

成员函数即使内联,也不会计入实例化的对象的存储空间

输入输出流

<cstdio> 还是 <iostream>

scanfprintf 虽然性能好,但毕竟是 C 库函数,有几处和 C++ 习惯不符:

  1. scanf 通过指针进行写入,忘记取地址符 & 会变得不幸😅。
  2. 这两个函数并不支持 stringstring 并没定义到 char* 的隐式类型转换),需要手动调用 .c_str()
  3. scanf("%c",...) 会把空白字符读入 char,而 cin 会跳过所有空白字符。如果缓冲区里还留着上一行的换行符,它会被读入 char。可以使用 " %c" ,跳过缓冲区里所有空白字符

输入数据量特别大的情况下,可以考虑用 scanf。而 cin 慢与它和 scanf 同步缓冲区有关,可以使用 (std::)ios::sync_with_stdio(false) 关闭,之后性能和 scanf 差不多,但不再支持两个函数混用

cin 还有一个坑,读取之后,后接的空白字符留在缓冲区里,包括cin.getline()。这时候再用 scanf 就可能出现混乱。

另外,<cstdio> 读入一个字符的函数是 getchar(),而 <iostream>cin.get(),丢弃字符可以直接使用 cin.ignore()

格式化

printf 用来保留三位小数的语法是 "%.3f",而 cout 需要用 <iomanip> 库:

cout << setiosflags(ios::fixed) << setprecision(3) << MY_DOUBLE << resetiosflags(ios::fixed) << endl;

输出十六进制可以使用 %x 或者 cout << hex << MY_INT