C++
参考资料
- C++ Primer 大而不全 (CPP 没法全), 叙述顺序很棒 5
开始
istream
类的对象 cin
,ostream
类的对象 cout cerr clog
, 用处顾名思义
std::endl
结束行, 刷新缓冲区, 在流中叫操纵符
istream
对象作为条件, 流未错误时 true
,EOF
或无效输入返回 false
, 原理是重载 bool
转换
- 另外, 遇见错误时
cin
对象会设置错误状态 (强转 bool
会返回这个值), 后续输入无效
基础
变量和基本类型
算术类型
- C++ 规定算术类型的最小尺寸, 注意
wchar_t
,char16_t
是 16 位 _tchar32_t
是 32 位 (后两种应对 unicode
)
- 对浮点型仅要求最小精度, 6/10/10 (一般来说 7/16 , 32bit / 64bit),
bool
值未定义
- 字节是可寻址的最小内存块, 存储的基本单元为字
整数类型
- 除
bool
与拓展 char
外, 其他整数类型都有符号和无符号版本, 分别用 signed
/unsigned
修饰
- 除
char
外, 其他算术类型本身就是 signed
, 但 char
有三种, 而其本身到底是 signed
还是 unsigned
则取决于编译器
- 强制类型转换, 赋
signed
一个区间外的值会产生未定义行为, 而赋 unsigned
则会产生模运算, 值为初始值对容量取模,-1 对 256 取模为 255
- 混用 signed 和 unsigned,signed 会自动转换为 unsigned
- 八进制与十六进制字面量, 类型为可以容纳的最小类型 (int/long/long long 以及它们的无符号版本), 注意没有 short
- 但十进制字面量的类型都是有符号的, 然而字面量本身不会为负数,-n 本质是 n 的取反
浮点类型
- 字符型字面量的类型为 char, 如 'a'
- 浮点型字面量的类型为 double, 可用科学计数法表示
- 关于指定字面量类型, 除后缀外, 还可以用前缀, 如 L,u8,u,U 分别是拓展 char
变量
- C 的初始化器变成了 C++ 的初始化列表, 但
int a{0}; int a={0};
都可以
- 特殊的, 初始化列表造成数据丢失编译器会报错, 而 = 或者 () 则不会
- extern int a 强调这是一个声明 (显示初始化会退化为定义), 在函数体内显示初始化 extern 的变量会报错
- 作用域与 C 一致
复合类型
- 左值引用即别名, 它必须被初始化 (绑在一个对象上)
- nullptr 是一种特殊类型的字面量, 可以转换为任何指针类型,NULL 是值为 ((void*) 0) 的宏
- 指针可以有引用
const
- const 对象必须初始化
- const 内部类型对象默认为内部链接, 原因是有初始值的常量使用时会被直接替换,cpp 为保证能找到它最好将它放在头文件中, 为避免重复定义默认内部链接
- 但常量也有不知道初始值的, 这时应该使用 extern const, 用初始化与否决定声明 / 定义
特性
- 对 const 的引用也是不可修改值的 (注意引用类型必须也是 const)
- 但反之,const 引用可以绑定到非 const 对象上, 也不能通过引用修改对象的值
- 特殊的 const 引用可以绑定到其它类型上 (可以转换就行), 会产生一个临时对象
- 顶层 const 与底层 const
constexpr
- 常量表达式 (甚至常量函数) 可以在编译期确定, 但自动判断常量很困难, 你可以声明一个 constexpr (特殊的 const) 变量, 它必须是被常量表达式初始化, 否则会报错
- 其必须是字面值类型,constexpr 指针也必须初始化为 nullptr/0 / 某个固定地址
处理类型
- using/typedef 可以为类型起别名, 再用声明符时, 优先级小于别名内部的
- auto 自动判断类型, 忽略顶层 const (引用不被认为是类型的一部分), 但底层 const 则保留
- anto & 时会保留顶层 const (即使不显示 const)
decltype()
可以得到表达式的类型, 但不使用表达式的值, 保留一切类型 (包括引用, 惟独这时引用会被当作类型的一部分, 所以可以 a+0
的形式去掉引用)
- 注意
*p
的类型是是一种引用,(p) 与 p 的类型是不同的,(p) 是表达式, 类型为引用, 原因是 (p) 是可以做左值的特殊表达式
自定义数据结构
- C++11 规定类内数据成员可以指定初始值, 否则会默认初始化
- 头文件保护符
字符串, 向量和数组
命名空间的 using 声明
using namespace::name;
就可以自由使用 name, 头文件显然不应使用
标准库类型 string
- 头文件
<string>
- C++ 可直接初始化 / 拷贝初始化 (只能提供一个值), 可以构造临时对象强行拷贝初始化
- getline (is,s) 读取一整行, 遇到换行符结束, 返回 is (直接 cin>> 是空白就停)
- 标准库类型大多有配套类型, 如 string::size_type 是无符号类型
C 风格字符串
- 显然字符串字面值是 C 风格 (字符数组)
- C 标准库 name.h 依旧可用, 但建议使用 cname 头文件, 因为它们属于 std 命名空间
- cctype 中有一堆判断字符的函数
范围 for
for(type name:range)
范围 for 循环, 遍历 range 中的每个元素, 并将其赋值给 name, 注意 元素本身过大是应使用引用
- 显然若想修改元素, 则 name 必须为引用类型
标准库类型 vector
- 模版不是类或函数, 是编译器生成类或函数的一份说明, 需要实例化
- 在类的数据成员声明处进行初始化 (类内初始值), 不可使用 ()
- 大多标准库类都可以 (n,v) 构造一个含 n 个 v 的对象, 注意可以省略 v, 会调用默认初始化 (若没有, 则必须给值)
- 空 vector 动态添加元素,
v.push_back(a)
, 非常快, 没必要预留空间
- for 范围循环体内不应改变 range 的大小
- 注意, 标准库模板类的伴生类型, 也是模板,
vector<int>::size_type
- 下标操作必须在合法范围内, 否则会产生未定义行为 (尽可能使用范围 for)
迭代器介绍
- 所有标准库容器都有迭代器, 但只有少数容器支持下标操作
- begin 和 end 返回迭代器,end 指向尾元素的下一位置, 当空时 begin==end
- 迭代器支持
*
和 ->
以及增 / 减 /==/!= 操作, 因此 for 循环判断条件是 it!=vec.end()
- 迭代器的类型有两种, 可修改元素以及不可修改元素, 如
vector<int>::iterator
和 vector<int>::const_iterator
- begin 和 end 返回的迭代器类型由容器是否是 const 决定,cbegin 和 cend 强制返回的是
const_iterator
- 任何可能改变 vector 对象容量的操作都会使迭代器失效
- 迭代器的距离的类型是
difference_type
, 是带符号的
数组
- 数组维度必须是常量表达式, 且类型不能用 auto
- 数组的元素必须是对象, 故不可是引用
- 数组下标通常声明为
size_t
类型,cstddef 中定义了 size_t
类型, 是无符号与机器相关的类型
- 数组与指针的关系基本与 C 一致,auto 推导为指针, 但 decltype 推导为数组
- begin 和 end 函数可以返回数组首 / 尾后指针 (利用模板与类型推导)
- 指针的距离是
ptrdif_t
类型, 机器相关的有符号类型
- 同一容器对象内的迭代器比较大小 / 运算才是有意义的
- 数组下标运算的索引可以是有符号的
- string 有一个
c_str
成员函数, 返回指向 c 风格字符串的指针 (当 string 对象发生改变时, 指针可能会失效, 想用时最好拷贝一份)
- vector 可以用数组初始化,
vector<int> v(begin(arr),end(arr));
多维数组
- 二维数组用范围 for, 外层必须是引用 (避免退化为指针)
- 用 typedef 简化多维数组的类型
表达式
- 重载的运算符无法改变优先级和结合律, 操作数个数, 求值顺序
- 左值与右值, 基本与 C 一致, 但也有许多区别, 如 decltype 等
- 求值顺序是不确定的, 不要在表达式使用副作用 (修改对象) 同时再次使用该对象, 只有
&& || ?: ,
的求值顺序是定义的
算术运算符
- 所有运算对象会被转换为同一类型
- C++11 规定, 除法运算一律趋零截断
- 同时 m%n 的符号与 m 一致
逻辑与关系运算符
- 不要
if(a==true)
(提升) 类型转换导致错误
赋值运算符
- 列表初始化时, 花括号内可以为空, 自动构造值初始化 (内置类型为 0, 类类型为默认初始化)
- 列表初始化匹配不到构造函数时, 会尽力执行类似 C 初始化器的行为
递增与递减运算符
- 前置版本返回对象 (左值), 后置版本返回对象原始值的副本 (右值)
- 所以多用前置版本
*p++
是个好习惯
成员访问运算符
->
返回的是左值, 而.
返回与对象本身一致 (左值 / 右值)
条件运算符
位运算符
- 位运算符的短整型运算对象会被提升为 int, 位运算符号位是未定义的 (最好只用在无符号类型上)
sizeof 运算符
sizeof obj.name
在 C++11 中可以用 sizeof class::name
代替
- 注意 sizeof 一个表达式并不会计算表达式的值
逗号运算符
类型转换
- 两类型可以互相转换, 即关联
- 大多情况, 短整型运算对象会被提升为 int (包括大 char)
- 条件中非 bool 类型的运算对象会被转换为 bool 类型
- 初始化与赋值中, 初始值 / 右值会被转换为变量的类型
- 算术运算 (按最大的, 因为补码的原因, 无需考虑负数) 与关系运算中, 运算对象会被转换为同类型
- 函数调用中, 参数会被转换
- 数组会自动转换为首元素的指针 (但有许多例外, 如取数组的引用)
- 0/nullptr 可被转换为任意指针类型, 任意指针类型可被转换为 (const) void*
- 非常量可被转换为常量, 但反过来不行 (底层 const 同理)
// 显示转换
cast-name<type>(expression); // 若type是引用,则结果是左值
static_cast<type>(expression); // 只要不包含底层const即可
const_cast<type>(expression); // 只能改变运算对象的底层const
reinterpret_cast<type>(expression); // 通常为运算对象的位模式提供较低层次上的重新解释
// 如将int*转换为char*
// 关于类的按下不表
// 旧的c风格转换依旧可以用,但语义不明确
语句
简单语句
- 空语句
;
记的写注释
- 表达式语句 (没副作用的话没意义)
- 复合语句
语句作用域
条件语句
- switch 语句内部定义一个被初始化 (显示 / 隐式) 变量, 在其作用域内, 不可被使用
- 反之, 不初始化可以, 或者用块分离
迭代语句
- 范围 for 语句要求 range 类有 begin 和 end 方法返回迭代器
for(declaration:expression) statement
==for(begin=d.begin(),end=d.end();begin!=end;begin++) statement
跳转语句
- 与 switch 语句类似,goto 不可同作用域跳过变量定义
- 其它与 C 一致, 包括标签的函数作用域
try 语句块和异常处理
- throw 表达式引发异常
- try catch 处理异常
- 异常类传递信息
- 异常类都有一个 what 成员函数, 返回 const char*
- 嵌套的 try 块中, 异常如果没有对应 catch 则会继续向外层 try 块查找, 全没有则转到 terminate 函数 (终止程序)
- throw 后的语句不会被执行, 因此要考虑保证异常安全 (如释放资源)
try {
// 程序代码
if(err){
throw errClass(info); // 抛出匿名异常对象
}
} catch (errClass e1) {
// 处理e1的代码
} catch (errClass e2) {
// 处理e2的代码
} catch (errClass e3) {
// 处理e3的代码
// exception头文件
throw exception(); // 无信息
// stdexcept头文件
throw runtime_error("info"); // 只有运行时才能检查的错误
throw logic_error("info"); // 程序逻辑错误
throw domain_error("info"); // 参数对应的结果值不存在
throw invalid_argument("info"); // 无效参数
throw length_error("info"); // 试图创建一个超出该类型最大长度的对象
throw out_of_range("info"); // 使用一个超出有效范围的值
throw range_error("info"); // 结果超出合理范围
throw overflow_error("info"); // 算术运算上溢
throw underflow_error("info"); // 算术运算下溢
函数
函数基础
- 实参的求值顺序未定义
- 形参可以匿名, 但这意味它无法被使用
- 块的自动生命周期
- 自动对象会默认初始化
- 全局对象会值初始化 (包括成员)
- C++ 支持分离式编译
参数传递
- 本质是实参初始化形参, 值传递, 引用传递
- 尽可能使用常量引用, 可以不拷贝, 不修改, 传递常量实参
- 引用参数可以当返回值用
- 对于传递数组退化为指针, 可以判断 '\0'(仅 char), 传递首尾指针, 传递 size
- 形参为数组的引用是可选项, 但会固定数组大小 (等着学模版)
- 若参数的类型一致, 可以传递
initializer_list
类型实现可变参数列表 (模版亦可)
initializer_list
类型的列表元素是 const 的, 并且拷贝构造是浅拷贝, 有 begin,end
- 拥有这些特性的目的是传递
{"123",1,1.0}
这样的参数, 使用范围 for
- C 语言的可变参数列表可用 (便于 C++ 访问 C 代码), 但仅可用 C++/C 共同类型
返回类型与 return 语句
- void 返回值函数可以隐式 return (int main 也行)
- 或
return;
亦可 return return_void_fun()
- C++ 要求函数必能 return,return 的对象可以转换为返回类型即可
- 返回值初始化主调函数的一个临时对象 (返回引用无需, 但不应返回局部变量的引用 / 指针)
- 返回引用的函数可以作为左值
- 可以返回列表初始化, 相当于列表初始化返回类型的临时变量
- main 函数可以返回 cstdlib 头文件中的
EXIT_SUCCESS
与 EXIT_FAILURE
宏
type fun()==auto fun()->type
函数重载
- 在同一作用域内, 函数名相同但参数列表不同
- 注意顶层 const 与否依旧属于同一参数, 底层 const 反之
- 使用常量引用的函数可以
const_cast
转换, 作为非常量引用函数重载的返回值
- 函数匹配 / 重载确定时可能发生最佳匹配, 无匹配错误, 二义性调用错误 (多个可匹配, 但都不完美)
- 注意局部函数声明 / 变量定义覆盖 所有 同名全局函数 (C++ 总是先查找名字)
特殊用途语言特性
- 默认实参 (注意参数排布顺序)
- 默认实参可以分开写在声明 / 定义中 (但是不可以覆盖)
- 非局部变量的任意表达式可以作为默认实参
- 内联函数 (inline), 注意许多编译器不支持内敛递归, 且编译器不一定响应 inline 请求
- constexpr 函数是特殊的 inline (编译期求值), 所有参数与返回值都是字面值类型, 函数体中有且只有一条 return 语句
- 但是, 我们可以在 constexpr 函数返回一个非常量 (此时显然不可再用于需常量位置)
- inline 函数和 constexpr 函数应定义在头文件中
- cassert 头文件 assert 宏, 断言表达式真值为 1
- NDEBUG 宏用于要求不调试 (可禁止 assert), 我们可以借用定义自己的 debug 行为
// 存放名字的const char数组
__FILE__ // 文件名
__LINE__ // 行号
__TIME__ // 编译时间
__DATE__ // 编译日期
__func__ // 函数名
函数匹配
- 首先确定候选函数
- 然后进行可行函数 (数量够, 类型可转换), 否则无匹配错误
- 之后依次检查参数匹配, 其中有每个参数不劣于其他可行函数且至少一个好于其他可行函数的唯一可行函数为最佳匹配, 否则二义性错误
// 1 类型相同,数组转换指针
// 1.5 顶层const // 都是精确匹配
// 2 底层const转换 //只能用于非const转换为const
// 3 类型提升
// 4 算术类型转换(除了提升)与指针转换(0/nullptr/void*)
// 5 类类型转换
函数指针
类
定义抽象数据类型
- 成员函数必须在类内声明, 但可以在类内定义 (隐式 inline), 也可以在类外定义
- this 是一个隐式的 const 指针, 指向调用该成员函数的对象 (不可自定义 this)
- 将 const 放在参数列表后, 声明常量成员函数, 本质上是声明隐式的 this 指针的底层 const
- 编译器处理类会先编译成员变量, 再编译成员函数 (无所谓顺序, 一律可以使用)
- 类外定义的成员函数需要加上 classname::
- this 可以返回,
*this
是引用
- 把非成员函数的接口与类置于同一头文件中
- 构造函数显然没有返回值, 且不可为 const
- 合成的默认构造函数, 类内初始值 + 默认初始化,
classnsme()=default;
即可
classnsme(...):name(...),name2(...){}
初始值列表
- 合成的拷贝, 赋值与析构函数会对每个成员进行拷贝, 赋值与销毁
访问控制与封装
- class 的默认访问权限是 private,struct 是 public, 这是唯一区别
- 类内可以声明友元, 使其访问类的 private 成员 (这不是函数声明, 函数需要再声明)
类的其它特性
- 类内可以定义类型别名
- 类内可以声明 mutatic 成员, 无论对象 / 函数是 const, 它都可被修改
- 显然 const 函数 return *this 是常量引用
- 类可前向声明, 为定义前是不完全类型, 以供类内加入自己指针类型的成员等情况
- 类可以声明类友元, 友元函数可以声明在类内 (隐式内联)
- 注意, 声明友元的其它类的成员函数, 必须在类内前向声明友元函数, 声明类, 声明友元, 定义函数
类的作用域
- 在类外定义函数, 加上类名:: 后, 其它参数不需要再加, 但返回类型在它前面, 需要加 (如果是类内类型)
- 先处理成员变量的声明, 后函数体
- 类内类型名应在开始处, 否则可能与外界类型名冲突
- 成员变量不怕冲突, 与其它位置的规则类似 (当然我们不建议)
::name
代表全局的 (反遮蔽)
构造函数再探
- 构造函数初始化或者赋值, 影响取决于成员的默认初始化
- const, 引用, 无默认初始化的成员必须通过构造函数初始化列表进行初始化
- 若构造函数的每个形参都有默认实参, 则构成了默认构造函数
- 委托构造函数
classname(...):classname(调用另一个构造){}
默认构造:
块作用域内定义无初始值的非静态变量
类使用构造函数时,没初始化该成员,会对其进行默认初始化
注意默认构造的定义应为
classname name;
而非
classname name();// 函数声明
值初始化:
数组初始化时,提供的值比成员少,则剩余成员进行值初始化
无初始值局部静态变量
classname name();
- 能仅使用一个实参的构造函数定义了隐式转换
- 这种转换只能隐式进行一次 (不可连续)
- 使用 explicit 关键字修饰函数阻止隐式转换, 同时也阻止了拷贝初始化 (使用 =)
- 但可以使用强制类型转换
- 聚合类即为没有构造函数, 成员都是 public, 无类内初始值, 无基类, 无虚函数的类
- 聚合类大抵只能 {} 初始化, 它显然有许多的缺陷
- 但聚合类是字面值类型
- 非聚合类的字面值常量类应满足, 有 constexpr 构造函数, 所有成员为字面值类型, 若有初始值应为字面值常量 /constexpr 构造, 必须使用析构函数的默认定义
- constexpr 构造函数是 constexpr 的, 构造函数的, 显然对成员递归的
类的静态成员
- 静态成员函数不可使用 this, 定义与其它成员函数一致 (类外定义不可有 static)
type classname::name = ...(可以是成员函数)
- 静态成员变量可以有类内初始值, 但必须是常量表达式且 type 是字面值类型 (还是得定义一下)
- 静态成员可以作默认实参
- 静态成员可以是不完全类型 (前向声明)
- 静态成员的类型可以是其类类型
C++ 标准库
IO 库