基于 _C++ Primer Fifth _ 编写

不愧是 C++,这才只是入门,只到 C++ 11

开始 Getting Started

万恶之源:

#include <iostream>
int main() {
std::cout << "Hello, world!" << std::endl;
return 0;
}

文件的后缀名有很多,最常见的应该是 .cc.cpp

编译器也有很多,如使用 g++ 编译:

g++ -o prog1 prog1.cc
  • 添加支持 c++ 11 标准:-std=c++11
  • -Wall 生成更多警告信息

输入输出:

  • 这里使用了 iostream 库中的标准输入流和标准输出流,该库中还有标准错误流 cerrclog
  • << 是输出操作符,>> 是输入操作符
  • endl 是一种控制符 manipulator,有结束当前行和刷新缓冲区的作用

std:: 表示使用命名空间 namespace 的函数等,用于区分

一个读入的例子:

std::cin >> v1 >> v2;

>> 左边是一个 istream,右边是一个对象,并返回左边的操作数为结果

注释方面依旧是两种:///* */

控制流:whilefor

读入未知数量的输入:while(std::cin >> value),即当输入流读到 EOF 或无效输入时,会无效,导致条件为 false

键盘中的 EOF 是输入 Ctrl + z

if 语句

C++ 支持类

变量与基础类型 Variables and Basic Types

初级内置类型

算术类型:整数和浮点数

对于 unicode 字符,有 char16_tchar32_t

选择使用的类型的原则:

  • 如果非负,使用 unsigned
  • 整数运算一般使用 int,如果不够,考虑 long long
  • charbool 用于表示特殊的东西
  • 浮点数运算用 double

注意 signed 和 unsigned 参与运算时会发生隐式转换

字面值:后面可以加 ulll 等表示整数,fl 表示浮点数,前面加 uU 表示 16 位、32 位 Unicode,u8 表示 utf-8 字符串

特殊的 e

编译器自动对每个字符串字面值末尾加 \0

可以用转义字符 excape sequence 表示一些不可打印的字符,如 \n;也有泛化的表示,如 \40 表示空格

变量

定义时初始化:int i = 0;,注意这里的 = 和赋值无关,而是表示初始化 initialization

还有另外的初始化方法——列表初始化 list initializationint i = {0};int i{0}

如果没有初始化值,则是默认初始化

函数内的变量未初始化

为了支持分开编译,声明 declaration 使名字被程序直到,定义 definition 则创建了关联的实体,例如:

extern int i; // 声明但不定义
int j; // 声明且定义

标识符,作用域

复合类型

复合类型 compound type 是以另一种类型定义的类型,如引用和指针

引用 reference 相当于对象的别名,如

int ival = 1024;
int &refVal = ival;

引用定义时绑定到一个对象上,且不可更改

指针 pointer 指向另一个类型 int *ip1;

指针保持了另一个对象的地址,通过取址运算符 & 获取一个对象的地址:int *p = &ival;

注意指针类型和指向的对象类型必须匹配

使用解引用运算符 * 来访问对象:

*p = 0;
cout << *p;

注意 *& 在表达式和声明时表达的含义不同

空指针 null pointer 没有指向任何对象:int *p1 = nullptr;

void* 指针可以存任何对象的地址

当然,还有指向指针的指针:int **ppi = &pi;

const 标识符

const 定义常量,必须在定义时初始化,然后不可变

变量可以赋值给常量,常量也可以赋值给变量,因为复制对象并不会改变对象

const int &ri = i; 表示对常量的引用,其不可用于改变值,注意 i 可以是非 const 的

类似的,const int *cpi = &i; 表示对常量的指针,不可用该指针改变其指向的变量的值

const 指针表示其不可改变指向,类似于引用:int *const curErr = &errNumb

总的来说,指向 const 对象的叫低级 const,该对象本身就是 const 的叫顶级 const

  • 低级 const 只会出现在复合类型中
  • 当复制对象时,高级 const 被忽略

常量表达式 constant expression 表示可以在编译时确定的表达式,如 constexpr int limit = mf + 1;

注意,constexpr 指针暴露的顶级 const:constexpr int *q = nullptr;

处理类型

使用 typedef 给类型起别名:typedef double wages;

还有另一种方法:using wages = double;

auto 自动推断类型

同样的,对于复合类型,忽略顶级 const,如有需要,必须手动加上:const auto f = &i;

decltype 返回操作数的类型,如 decltype(f()) sum = x;,注意这里编译器并不会调用函数 f(),而是依据其返回值确定类型

注意,decltype((variable)) 始终是一个引用类型

自定义数据结构

定义一个可以直接访问数据元素的“类”:

struct Sales_data {
std::string bookNo;
double revenue = 0.0;
};

这里有数据成员

为了防止被多处使用的头文件被反复包含而多次编译,使用预处理器 preprocessor 来定义 header 保护,如:

#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data {
std::string bookNo;
double revenue = 0.0;
};
#endif

字符串,向量和数组 Strings, Vectors, and Arrays

命名空间 using 声明

为了防止每次都要写 std::cin,可以使用 using 声明:using std::cin;

string 类型

string 初始化除了之前说过的,还有

string s2 = "value"; // 直接初始化
string s3("value"); // 复制初始化
string s4(10, 'c'); // 10 个 c

注意末尾的 \0 不会被复制到字符串中

字符串的操作:<<>>getline(is, s)s.empty()s1 + s2s1 = s2s1 == s2s1 < s2

字符串可以直接连接,但是为了兼容 C,字符串字面值不能直接连接

cctype 头文件中有很多处理字符的函数,如 islower(c)tolower(c)

c 开头的库表示是 C++ 版本的 C 语言库

一种 for 循环的写法:for (auto c : str)

[] 用于索引,范围是 0 <= index < s.size()

vector 类型

vector 是一个模板 templatevector<int> 等才是类型

初始化同理

添加元素:push_back()

.size() 返回的是 vector<int::size_type

介绍 Iterator

除了下标以外,还有一种访问方法——迭代器 iterator

.begin() 返回指向第一个元素的迭代器,.end() 返回指向最后一个元素的下一位的迭代器

迭代器可以解引用,自增或自减,判断是否相等

注意因为 .end() 返回的迭代器不指向某个元素,所以不可以自增或自减

一个使用迭代器迭代的写法:for (auto it = s.begin(); it != s.end(); ++it)

类似指针,vector<int>::const_iterator it; 只可读,不可写

如果操作的对象是 const 的,则 .begin() 等返回的是 const_iterator,如

const vector<int> cv;
vector<int>::const_iterator it = cv.begin();

为了能够显式指明想要的是 const_iterator,可以使用 .cbegin().cend()

string 和 vector 的迭代器同样支持加减、大小比较,这种运算叫做迭代器算术 iterator arithmetic

特别的,两个迭代器相减的返回值的类型是 difference_type

数组

数组相当于固定大小的 vector

对数组取下标的变量的类型是 size_t,其定义在 cstddef

数组和指针紧密相连

数组有类似于容器的两个迭代器函数:int *pbeg = begin(arr), *pend = end(arr);

两个指针相减的返回值的类型是 ptrdiff_t

对于 C++ 来说,最好不要使用 C 风格的 string,故这里不作介绍

多维数组

多维数组也可以使用 for 循环,但是除了最内层以外都要使用引用:

for (const auto &row : ia)
for (auto col : row)
cout << col << endl;

表达式 Expressions

基础

单元、二元、三元运算符

运算符重载

左值与右值

优先级与结合性

表达式中的计算是没有顺序的,如 int i = f1() * f2(); 是无法得知哪个函数先调用的;cout << i << ++i << endl; 也是一个未定义的行为

有定义计算循序的运算符只有 &&||? :,

算术运算符

在计算中,小的整数类型被提升为较大的整数类型

逻辑与关系运算符

赋值运算符

返回左操作数,优先级低

自增/自减

一种常见的写法:*iter++

成员访问函数

-> 返回的是一个 lvalue;如果对象是 lvalue,则 . 返回的是一个 lvalue,,反之则是 rvalue

位运算

由于在位运算中符号位如何处理是未知的,所以推荐只对 unsigned 变量进行位运算

sizeof(类型) 返回 size_t,还有一种用法:sizeof 表达式,注意表达式不会被求值

类型转换

隐式类型转换

显式类型转换:

  • static_cast
  • const_cast 改变了低级的 const,在重载函数中有用
  • reinterpret_cast 基于操作数的 bit 的低级重新解释

总之,要尽可能避免类型转换

语句 Statements

空语句应该有注释,以免被人忽略

块与块作用域

控制语句

对应 switch,最好一定要定义一个 default,即使是空的

定义在 while 里的变量在每次迭代时都会创建和销毁

stdexcept 中定义了通用的异常类:exceptionruntime_errorrange_erroroverflow_errorunderflow_errorlogic_errodomain_errorinvalid_argumentlength_errorout_of_range

函数 Functions

参数传递

值参数传递 vs 引用参数传递

对于较大的对象,按值传递复制的开销较大,但又不想要改变该对象,可以使用 const 引用

因为在赋值时顶层 const 被忽略,所以 void fcn(const int i)void fcn(int i) 是冲突的

可以传递对数组的引用:f(int (&arr)[10])

二维数组的传递:

void print(int (*matrix)[10], int rowSize);
// 或
void print(int matrix[][10], int rowSize); // 第一维会被编译器忽略,所以最好不要包含

命令行参数传递:

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

其中 argv[0] 为程序名称

initializer_list 类似于数组,是一个模板类型,可以用来接收未知数量的参数:

void error_msg(initializer_list<string> il) {
for (auto beg = il.begin(); beg != il.end(); ++beg)
cout << *beg << " ";
cout << endl;
}
error_msg({"functionX", "okay"});

返回类型与返回语句

一种简单的返回 vector 的方法:

vector<string> process() {
return {"functionX", "okay"};
}

如果 main() 没有返回值,则隐式返回 0,在 cstdlib 中预处理的变量表示成功和失败:EXIT_FAILUREEXIT_SUCCESS

对于一个返回一个数组的函数,可以使用尾随返回类型 trailing return type,如

auto func(int i) -> int (*)[10];

特殊用途的特色

inline 可以让编译器将函数展开,但编译器可以拒绝这么做

constexpr 函数隐式 inline 了,同时返回值不一定是 const

以上两者一般定义在头文件中

assert(expr);:若 expr 为假,终止程序,用于检查不会发生的条件

在编译时添加参数 -D NDEBUG 来关闭 debug 模式

结合一些宏,可以提高调试效率:

#ifndef NDEBUG
cerr << __func__ << endl;
#endif
  • __func__ 输出函数名称
  • __FILE__ 文件名
  • __LINE__ 当前行数

函数指针

函数指针类似于函数原型:

bool (*pf) (const string &, const string &);

使用起来类似于一个函数别名:

bool b = pf("hello", "goodbye");

可以将其指向其他的函数,但必须函数原型一致

函数指针也可以作为参数,可以像函数一样写,也可以显式写成函数指针的形式:

void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));

传递参数时直接使用函数名:

useBigger(s1, s2, lengthCompare);

当然,也可以返回一个函数指针:

auto f1(int) -> int (*)(int*, int);

类 Classes

在类内部定义的成员函数隐式 inline;如果在外部定义,则要指定是哪个类:Sales_data::print(const string& s) {...}

const 成员函数实际上相当于:const Sales_data *const this,即指定了 this 的 const 性质——不改变该对象

如果没有构造函数,编译器会自动生成一个默认构造函数,也可以显式指定:Sales_data() = default;。当然,这只针对内置的类型。

构造函数初始化列表:Sales_data(const string &s): bookNo(s) { }

一个类可以通过友元 friend 使类或函数访问到其私有成员

友元声明只指定了访问,还需要另外声明友元声明的函数,该函数通常和类声明在同一个头文件中

我们可以在类中定义一个类型成员:

class Screen {
public:
using pos = string::size_type;
}

const 成员函数返回的 *this 是一个 const 对象

每个类实际上定义了自己的新作用域,因此,对于定义在类外部的成员,要使用类名称指定:

class Window_mgr {
public:
ScreenIndex addScreen(ScreenIndex i);
}
Window_mgr::ScreenIndex Window_mgr::addScreen(ScreenIndex i);

注意区分初始化 ConstRef::ConstRef(int ii): i(ii) { } 和赋值 ConstRef::ConstRef(int ii) { i = ii; }

其中初始化顺序一定是被声明的顺序,而不是定义中写的顺序,故最好两者顺序一致

指派 delegating 构造函数使用了该类的另一个构造函数执行初始化,如:

class Sales_data {
public:
    // nondelegating constructor initializes members from corresponding arguments
Sales_data(std::string s, unsigned cnt, double price):
            bookNo(s), units_sold(cnt), revenue(cnt*price) { }
// remaining constructors all delegate to another constructor
Sales_data(): Sales_data("", 0, 0) { }
}

Sales_data obj(); 是一个函数声明Sales_data obj2; 才是默认构造对象

单个参数的构造函数可以隐式调用构造函数转换,如:

item.combine(string("9-999")); // 隐式调用 Sales_data(const std::string &s) 转换为 Sales_data

为了防止这种转换,可以禁用隐式转换:

explicit Sales_data(const std::string &s): bookNo(s) { }

这样子做了之后也不可用于复制形式的初始化:

Sales_data item1(null_book); // 允许,直接初始化
Sales_data item2 = null_book; // 不允许

集合体 aggregate

  • 所有的数据成员都是 public
  • 没有定义任何构造函数
  • 没有类内部的初始化
  • 没有基类或虚函数

静态成员:

class Account {
public:
static double rate() { return interestRate; } // 声明
static void rate(double newRate);
private:
static double interestRate;
}

// 定义
void Account::rate(double newRate) {
interestRate = newRate;
}

// 使用
double r = Account::rate();

静态 static const 成员可以在类内部初始化

IO 库 The IO Library

IO 类

IO 库的头文件:iostreamfstreamsstream

为了支持操作 wchar_t 类型的数据,定义了 wistreamwostream 等类型,对应的有 wcinwcout

IO 对象不可复制或赋值

stream 有四种情况:eoffailbadgood

.rdstate() 返回该 stream 的 iostate

.clear() 有两种重载:

  • 无参数的:清空所有情况
  • 有一个参数的:cin.clear(cin.rdstate, ~cin.failbit & ~cin.badbit);

io 的 buffer 刷新的情况:

  • 程序正常结束
  • buffer 满了
  • 显式使用如 endlflush 的操作符
  • 使用 unitbuf 操作符,使得每次输出操作都会刷新 buffer
  • 输出流被绑定到另一个流,如 cin 被绑定到 cout,故读 cin 会刷新 cout 的 buffer
    • 解除绑定的方法:cin.tie(nullptr);

文件输入和输出

创建一个 fstream:ifstream input("input.txt");

文件模式:

  • inout

  • app 每次写之后 seek 到结尾

  • ate 在打开后 seek 到结尾

  • trunc 截断文件

  • binary 以二进制模式执行 IO 操作

保持文件内容的输出:ofstream app("file2", ofstream::out | ofstream::app)

string

当我们想要对一整行的独立字做一些工作时,istringstream 很有用

顺序容器 Sequential Containers

顺序容器类型:vectordequelistforward_listarraystring

rbegin()rend()

.assign()

支持比较大小

pushinsert 一个成员时,我们传递的对象类型的容器的元素类型,且会被复制到容器中;但使用 emplace 等直接在容器管理的空间中使用构造函数来创建元素,如 c.emplace_back("999", 25, 15.99);

一个使用循环来插入元素的方法:

while (begin != v.end()) {
++begin;
begin = v.insert(begin, 42);
++begin;
}

如果我们已经不再需要多余的空间了,可以使用 shrink_to_fit() 来返回未利用的内存,但实际实现可以忽略该请求

string 的搜索函数返回的是无符号的 string::size_type,如果没找到,返回的 string::npos 是 -1,即最大的 string 大小

将字符串转化为数字:stoi(s)stod(s)

容器适配器 container adaptor

泛型算法 Generic Algorithms

初见算法

算法不会执行容器操作,而是基于迭代器操作,故算法从来不会改变底层容器的大小

算法通常接受由首尾迭代器指明的范围

只读算法:findaccumulate,如 string sum = accumulate(v.cbegin(), v.cend(), string(""));

有的算法可以接受两个序列,如 equal(roster1.cbegin(), roster1.cend(), roster2.cbegin())

注意第二个序列只接受了一个迭代器,我们需要确保第二个序列长度不小于第一个序列

写容器元素的算法:fill

back_inserter 接受对容器的引用并返回一个插入迭代器,当我们通过这个迭代器赋值时,会自动调用 push_back

不少算法都有 _copy 的版本,如 replace_copy(ilst.cbegin(), ilst.cend(), back_inserter(ivec), 0, 42);

重新排列的算法,如去重的算法:

sort(words.begin(), words.end());
auto end_unique = unique(word.begin(), words.end());
words.erase(end_unique, words.end());

自定义操作

lambda 表达式代表可调用的代码单元,可以被视为匿名、内联函数,有如下形式:[捕获列表] (参数列表) -> 返回值类型 { 函数体 }

尽管 lambda 出现在函数的内部,其只有在捕获列表中指明了变量,才能使用函数内部的局部变量

for_each() 函数接受一个可调用对象并调用输入范围内的每个对象

当我们定义了一个 lambda 时,编译器生成了一个新的匿名的类。

当然,可以捕捉值,也可以捕捉引用

默认情况下,lambda 不能改变按值捕获的变量的值,但可以加上 mutable 关键字,使得可以改变这个值:

size_t v1 = 42;
auto f = [v1] () mutable { reutrn ++v1; };
v1 = 0;
auto j = f(); // j = 43;

fucntional 头中的 bind 可以接受可调用函数和参数,并将参数应用于该函数,形式为:auto 新可调用对象 = bind(可调用对象, 参数列表)

例如:

using namespace std::placeholders; // _1 是占位符,在命名空间 std::placeholders 中
auto check6 = bind(check_size, _1, 6); // _1 表示其是新可调用对象的第 1 个参数

注意 bind 是将参数复制过去,对于不能复制的,可以使用 refcref 引用:

for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' '));

重探迭代器

迭代器的类型:insertstreamreversemove

insert 迭代器:

  • back_inserter
  • front_inserter
  • inserter

iostream 迭代器:

  • istream_iterator<T>in(is) 从输入流 is 中读数据
  • istream_iterator<T>end istream_iterator 的尾迭代器

输出迭代器同理:

ostream_iterator<int> out_iter(cout, " ");
for (auto e : vec)
out_iter = e; // 等价于 *out_iter++ = e;
cout << endl;

可以通过提供反向迭代器降序排序:sort(vec.rbegin(), vec.rend());

五类迭代器

算法的结构

算法的几种结构:

  • alg(beg, end, 其他参数)
  • alg(beg, end, dest, 其他参数)
  • alg(beg, end, beg2, 其他参数)
  • alg(beg, end, beg2, end2, 其他参数)

注意带 _if_copy 后缀的算法

优先使用成员函数版本的算法