程设课的 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>
?
scanf
和 printf
虽然性能好,但毕竟是 C 库函数,有几处和 C++ 习惯不符:
scanf
通过指针进行写入,忘记取地址符&
会变得不幸😅。- 这两个函数并不支持
string
(string
并没定义到char*
的隐式类型转换),需要手动调用.c_str()
。 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
。