C++ Primer
Categories:
C++基础知识?
C++ Primer
- 单文件编译
g++ -o 输出文件名 待编译文件名
- 读取、输出到文件
程序名 < 输入文件
程序名 > 输出文件
数据类型
- 选用的经验:
数值不为负选择无符号型(unsigned)
整数运算选用int
,超过表示范围的话选用long long
浮点数运算选用double
- 不要混用有符号类型和无符号类型,带符号数会自动转换为无符号数,等于初始值对无符号类型所能表示的数值总数取模后的余数
- 字符型字面值常量的类型可以通过前缀指定,整型、浮点型可以通过后缀指定
变量
- 初始化和赋值存在本质上的巨大区别
- 使用列表初始化
{}
,在可能的数据丢失时会使编译器给出警告 - 声明(declaration)和定义(definition)是不同的,声明使名字为程序所知,定义则为变量申请存储空间或赋初值
- 使用
extern
关键字来声明变量 - 变量能且只能被定义一次,但可以被多次声明
- C++是静态语言——在编译阶段检查类型
- 全局变量在块作用域内用
::
前缀可以显式访问(屏蔽块作用域内的局部变量) - 局部变量最好不要同全局变量同名
复合类型
- 通过在变量名前加
&
来定义引用类型,引用必须初始化 - 引用即别名
- 引用只能绑定在对象上
- 指针存放对象的地址,使用取地址符
&
获取地址 - 利用指针访问对象,使用解引用符
*
访问对象 &
和*
出现在声明和表达式中的含义截然不同- 初始化空指针(=nullptr、=0、=NULL)
- 赋值永远改变的是等号左侧的对象
void*
可以存放任意对象的地址- 类型修饰符(
*
和&
)仅修饰其后的第一个变量标识符
const限定符
- 常量引用是对const的引用
- 在初始化常量引用时允许使用任意表达式作为初始值,只要该表达式的结果能够转换成引用的类型即可
- 对const的引用可能引用一个并非const的对象
- 所谓指向常量的指针或引用不过是指针或引用**“自以为是”**地认为自己指向常量,所以自觉地不去改变所指对象的值
- 弄清声明的含义最有效的方法是从右向左阅读
*
放在const之前说明指针是一个常量-不变的是指针本身的值而非指向的那个值- 指针本身是一个常量并不意味着不能通过指针修改其所指向对象的值
- 非常量可以转换为常量 反之则不行
- 常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式
- 一个对象是不是常量表达式由它的数据类型和初始值共同决定
- 顶层const 表示指针本身是个常量;底层const 表示指针所指的对象是个常量(仅示例,顶层和底层const适用于各种类型)
- 当执行对象的拷贝操作时,顶层const和底层const区别明显
处理类型
- 类型别名:
typedef 前者 后者
—— 后者是前者的同义词 - 注意typedef中使用
*
的情况(并不是简单的替换关系),const
是对给定类型的修饰 - 别名声明:
using 前者 = 后者
—— 前者是后者的同义词 auto
根据初值自动推断数据类型(仅保留底层const)顶层const需要在auto前加以修饰*
和&
只从属于某个声明符而非基本数据类型的一部分decltype
推断表达式类型而不使用其作为初值(保留变量的全部类型)- 如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型
- 如果表达式的内容是解引用操作,则decltype将得到引用类型
- 对于decltype所用的表达式来说,在变量名上加括号与不加括号得到的类型会有不同
decltype((变量))
的结果永远是引用,decltype(变量)
只有在变量本身是引用时才是引用
自定义数据结构
struct 类名 类体 ;
- 预处理器保证头文件多次包含仍能安全工作——头文件保护符
#define
把一个名字设定为预处理变量#ifdef
当且仅当变量已定义时为真#ifndef
当且仅当变量未定义时为真#endif
检查结果为真时执行后续操作直到出现此命令- 一般将预处理变量名全部大写以保证其唯一性
- 头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明
using声明
using namespace::name;
- 头文件中不应包含using声明
string
std::string
可变长字符序列- 在执行读取操作时,string对象会自动忽略开头的空白(空格符、换行符、制表符等)并从第一个真正的字符开始读起,直到遇见下一处空白为止
- 常用操作:
getline(a,b)
——从a中读取一行(以换行符为界)赋给bs.empty()
——判断s是否为空s.size()
- size函数的返回值是一个无符号整型数(类型为
string::size_type
) 注意避免int和unsigned混用 - string的比较:1.字符相同时,较短string小于较长string;2.字符相异时,第一对相异字符的比较
- 当string对象和字符/字符串字面值混在一条语句中时,必须确保每个+两侧的运算对象至少有一个是string
- 字符串字面值与string是不同的类型
- 使用C++版本的C标准库头文件
ctype.h
=>cctype
- cctype中包含一系列字符的判断和处理函数
- 基于范围的for语句
for (declaration : expression)
类似于python中的for语句 - string中的字符可以通过下标访问
- 始终注意检查下标的合法性(是否在正确的范围内)
vector
std::vector
表示对象的集合(所有对象的类型相同),也被称为容器- vector是一个类模板而非类型
vector<类型> 容器名;
- vector丰富的初始化方式:列表(
vector<T> v5{a,b,c...}
或vector<T> v5={a,b,c...}
)、拷贝(vector<T> v2(v1)
或vector<T> v2 = v1
)、构造(vector<T> v3(n,val)
或vector<T> v3(n)
)… push_back(值)
将值作为vector的尾元素压到vector的尾端- vector能高效地快速添加元素(没有必要为其指定容量)
- 如果循环体内部包含有向vector添加元素的语句,则不能使用范围for循环
- empty和size函数与string的类似
- size函数的返回值也是属于vector的特殊类型
size_type
但需要指出vector的元素类型 - vector的比较法则也与string类似
- vector不能使用下标添加元素,只能使用下标访问已存在的元素
迭代器
- 有迭代器的类型同时拥有返回迭代器的成员
begin()
返回指向第一个元素的迭代器end()
返回指向尾元素下一位置**(尾后)**的迭代器- 一般来说,我们不清楚迭代器的准确类型(使用
auto
来定义变量) *iter
返回迭代器所指元素的引用iter->mem
++iter
/--iter
指示容器的下一个/上一个元素- 泛型编程:所有标准库容器的迭代器都定义了
==
和!=
,所以在for循环中使用!=
而非<
,因为这种编程风格在标准库提供的所有容器中都有效 const_iterator
只能读元素,不能写元素->
即为解引用和成员访问的结合,it->mem等价于(*it).mem- 任何一种可能改变vector容量的操作,都会使该vector的迭代器失效
- 凡是使用了迭代器的循环体,都不要向迭代器所属的容易添加元素
- 两个迭代器相减的结果为
difference_type
(带符号整型数)
数组
- 与vector类似的是存放类型相同的对象的容器;不同的是数组的大小确定不变,不能随意向其增加元素
- 数组中元素的个数也是数组类型的一部分,所以需要为常量表达式
- 数组的初始化:列表初始化,不允许拷贝
- 字符数组可以使用字符串字面值初始化,但要注意字符串字面值结尾处自带一个空字符
- 默认情况下,类型修饰符从右向左依次绑定;但对于数组而言,由(括号)内向外阅读更有意义
- 在使用数组下标时,通常将其定义为
size_t
类型 - 使用数组类型的对象其实是使用一个指向该数组首元素的指针
- 当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组;但是decltype不会发生上述转换
- 数组可以使用下标索引尾元素后那个并不存在的元素
begin(数组名)
/end(数组名)
能安全地返回首元素指针/尾后元素指针- 两个指针相减的结果为
ptrdiff_t
- 如果两个指针分别指向不相关的对象,则不能比较
- 内置的下标运算符所用的索引值不是无符号类型,这与vector和string是不同的
- C风格字符串存放在字符数组中并以空字符(
\0
)结束 - 头文件
cstring
中定义的函数可以操作C风格字符串 - 使用标准库string比使用C风格字符串更安全高效
- 尽量使用标准库类型而非数组
多维数组
- 严格来说,C++中没有多维数组,通常所说的多维数组其实是数组的数组
- 使用
{}
括起来的一组值初始化多维数组,花括号嵌套与否完全等价(嵌套只是为了更清晰地阅读) - 可以仅初始化部分元素,其它元素执行默认初始化
- 使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型
- 当程序使用多维数组的名字时,会自动将其转换成指向数组首元素的指针,即指向第一个内层数组的指针
表达式基础
- 左值和右值:左值可以位于赋值语句的左侧,右值则不能(在C++中并非如此简单)
- 当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)
- 赋值运算符需要一个非常量左值作为其左侧运算符,得到的结果也为左值
- 取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,该指针为右值
- 解引用运算符、下标运算符的求值结果为左值
- 复合表达式中,括号无视优先级与结合律
- 求值顺序在大多数运算符中没有明确规定,除了
&&
、||
、?:
、,
外
运算符
- 整数相除的商值无论正负一律向0取整(舍弃小数部分)
(-m)/n
和m/(-n)
都等价于-(m/n)
,m%(-n)
等价于m%n
,(-m)%n
等价于-(m%n)
- 除非必须,否则不用递增递减运算符的后置版本
ptr->mem
等价于(*ptr).mem
P.S.解引用运算符的优先级低于点运算符- 条件运算符(
cond?expr1:expr2
)可嵌套 最好不超过两到三层 - 条件运算符的优先级非常低,通常需要在它两端加括号
- 仅将位运算符用于处理无符号类型
- 移位运算符(IO运算符)的优先级不高不低:低于算术运算符,高于关系运算符、赋值运算符、条件运算符
sizeof
返回一条表达式或一个类型名字所占的字节数sizeof (type)
和sizeof expr
sizeof
并不实际计算其运算对象的值- 对char或类型为char的表达式执行sizeof运算结果为1
- sizeof运算不会把数组转换成指针来处理,等价于对数组中所有元素执行一次sizeof运算并将所得结果求和
- 对string或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间
- 逗号运算符真正的结果是右侧表达式的值
类型转换
- 算术转换:一种算术类型->另一种算数类型,如运算符的运算对象将转换成最宽的类型,整数值将转换成浮点类型
- 整型提升:小整数类型->较大的整数类型,如
bool
、char
、short
提升为int
、long
等 - 强制类型转换
cast-name<type>(expression)
- 任何具有明确定义的类型转换,只要不包含底层const,都可以使用
static_cast
- 当需要把一个较大的算术类型赋值给较小的类型时,
static_cast
非常有用 static_cast
对于编译器无法自动执行的类型转换也非常有用const_cast
只能改变运算对象的底层const,只有const_cast
能改变表达式的常量属性- 使用
reinterpret_cast
非常危险 - 要尽量避免强制类型转换
- 旧式的强制类型转换
type (expr)
和(type) expr
,不够清晰明了,追踪困难
运算符优先级
条件语句
else
与离它最近的未匹配的if
匹配,使用花括号可以强制匹配case
标签必须是整形常量表达式- 如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳转到后一处的行为为非法行为
迭代语句
- 传统for循环的执行流程:首先执行init-statement;接下来判断condition;条件为真,执行循环体;最后执行expression
- for语句中的init-statement可定义多个对象,但只能有一条声明语句,所以所有变量的基础类型必须相同
- 在范围for语句中,当范围变量为引用类型时,才能对元素执行写操作
- 在范围for语句中,使用
auto
可以保证类型相容 - 范围for语句的等价传统for语句(不能用范围for语句增加vector对象或其他容器的元素)
do while
与while
十分相似,只是先执行循环体后检查条件
跳转语句
break
负责终止离它最近的while
、do while
、for
、switch
语句,并从这些语句后的第一条语句开始继续执行continue
用于终止离它最近的for
、while
、do while
循环中的当前迭代并立即开始下一次迭代goto label;
label是用于标识一条语句的标示符
try语句块和异常处理
- 程序的异常检测部分使用
throw
表达式引发一个异常 try
块后跟随一个或多个catch
子句,由try
中抛出的异常来选中对应的catch
子句- C风格字符串(
const char*
) - 异常中断了程序的正常流程,那些在异常发生期间正确执行了“清理”工作的程序被称作异常安全的代码
stdexcept
头文件定义了几种常用的异常类,另外几种异常类型:exception
、bad_alloc
、bad_cast
- 后三种异常只能以默认初始化的方式初始化,不允许为这些对象提供初始值;其它异常类型使用string对象或者C风格字符串初始化这些类型的对象,不允许使用默认初始化的方式
- 异常类型只定义了一个成员函数
what
,该函数没有参数,返回一个指向C风格字符串的const char*
,提供关于异常的信息
函数基础
- 函数形参列表中的形参通常用逗号隔开,其中每个形参都含有一个声明符的声明,即使两个形参的类型一样,也必须把两个类型都写出来
- 形参名是可选的,当函数确实有个别形参不会被用到时,此类形参通常不命名以表示在函数体内不会使用它
- 函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针
- 名字有作用域,对象有生命周期
- 形参和函数体内部定义的变量为局部变量,仅在函数的作用域内可见,还会隐藏在外层作用域中同名的其他所有声明中
- 只存在于块执行期间的对象成为自动对象
- 局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并直到程序终止才被销毁——
static
- 函数名需要在使用前声明,函数只能定义一次,但可以声明多次,函数声明无需函数体,用
;
替代即可 - 函数声明也称作函数原型
- 含有函数声明的头文件应该被包含到定义函数的源文件中
- 分离式编译允许程序分散在几个文件中,每个文件独立编译
参数传递
- 引用传递和值传递
- C++中建议使用引用类型的形参代替指针类型访问函数外部的对象
- 使用引用来避免拷贝
- 当函数无须修改引用形参的值时最好使用常量引用
- 使用引用形参返回额外信息:一个函数只能返回一个值,但有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效途径
- 用实参初始化形参时会忽略掉顶层const(作用于对象本身)
- 在C++中允许定义若干具有相同名字的函数,前提是不同函数的形参列表有明显的区别
- 可以用非常量初始化一个底层const对象,但反之不行;一个普通的引用必须用同类型的对象初始化
- C++允许使用字面值初始化常量引用
- 尽量使用常量引用,把函数不会改变的形参定义为普通引用是一种常见错误
- 数组的特殊性:不允许拷贝数组,使用数组时会将其转换成指针
- 尽管不能以值传递的方式传递数组,但可以把形参写成类似数组的形式,本质上传递的还是指向数组首元素的指针
- 管理指针形参(数组实参)的三种技术:1.使用标记指定数组长度;2.使用标准库规范(传递指向数组首元素和尾后元素的指针);3.显式传递一个表示数组大小的形参
- 形参也可以是数组的引用,如
int (&arr)[10]
,数组大小是构成数组类型的一部分 - 传递多维数组,如
int matrix[][10]
,编译器会忽略第一个维度,最好不要把它包括在形参列表内。matrix的声明看起来是一个二维数组,实际上形参是指向含有10个整数的数组的指针 int main(int argc, char *argv[]) {...}
第二个形参是一个数组,其元素是指向C风格字符串的指针;第一个形参表示数组中字符串的数量int main(int argc, char **argv) {...}
等价于上述代码- 当使用argv中的实参时,可选的实参从
argv[1]
开始,argv[0]
保存程序名 - 如果函数的实参数量未知但全部实参的类型都相同,可以使用
initializer_list
类型的形参,使用方式类似于vector
initializer_list
中的元素永远是常量值,无法改变initializer_list
对象中元素的值- 省略符形参是为了便于C++程序访问某些特殊的C代码(使用了C标准库varargs)而设置的,形如
void foo(parm_list, ...);
和void foo(n...)
返回类型和return语句
- 返回void的函数不必须要有return,因为在最后总会隐式执行。若想让函数提前退出,可以使用return。
- 在含有return语句的循环后也应该有一条return语句,否则程序是错误的且难以被编译器发现
- 不要返回局部对象的引用或指针
- 引用返回左值:调用一个返回引用的函数得到左值,其他返回类型得到右值
- C++11规定,函数可以返回
{}
包围的值的列表(返回类型为vector<类型>
) - 头文件
cstdlib
中定义了两个预处理变量EXIT_FAILURE
、EXIT_SUCCESS
,可以作为main函数的返回值 - 递归:函数调用自身(main函数不能调用自己)
- 函数不能返回数组但可以返回数组的指针或引用
- 要想定义一个返回数组的指针或引用可以使用类型别名,如
typedef int arrT[10]
、等价写法using arrT = int[10]
,此时,arrT* func(int i)
中的func函数即返回一个指向含有10个整数的数组的指针 - 除了类型别名,返回数组指针的函数形式如
Type (*function(parameter_list))[dimension]
- 还可以使用尾置返回类型,如
auto func(int i) -> int(*)[10]
- 或者使用
decltype(数组名)*
来声明函数
函数重载
- 重载函数:同一作用域内的几个函数名相同但形参列表不同(main函数不能重载)
- 重载函数的返回类型需要一致,不允许同名函数返回不同的类型
- 顶层const形参并不区分重载函数而底层const(指针、引用)可以区分重载函数
- 最好只重载非常相似的操作
const_cast
在重载函数的情景中最有用- 函数匹配也叫重载确定,在调用重载函数时可能的三种结果:最佳匹配、无匹配、二义性调用
- 在C++中,名字查找发生在类型检查之前
特殊用途语言特性
- 一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值
- 默认实参负责填补函数调用缺少的尾部实参
- 当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,让那些经常使用默认值的形参出现在后面
- 在给定的作用域中一个形参只能被赋予一次默认实参,函数的后续声明只能为之前没有默认值的形参添加默认实参,且该形参右侧的所有形参必须都有默认值
- 应在函数声明中指定默认实参,并将该声明放在合适的头文件中
- 只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参;用作默认实参的名字在函数声明所在的作用域内解析,而这些名字的求值过程发生在函数调用时
- 内联函数可以避免函数调用的开销
- 在函数返回类型前加上
inline
就可将其声明为内联函数 - 内联说明只是向编译器发出请求,编译器可以选择忽略这个请求
- 内联机制一般用于优化规模较小、流程直接、频繁调用的函数
- constexpr函数是指能用于常量表达式的函数,函数的返回类型及所有形参的类型都为字面值类型且函数体中有且只有一条retuen语句
- 在编译过程中,constexpr函数被隐式指定为内联函数
- 允许constexpr函数的返回值并非一个常量
- 内联函数和constexpr函数通常放在头文件中
assert
预处理宏,用法:assert (expr)
,对expr求值,若为假,输出信息并终止运行;若为真,什么也不做- 预处理名字由预处理管理器而非编译器管理,应直接使用预处理名字而无需using声明
assert
的行为依赖于NDEBUG
预处理变量的状态,当#define NDEBUG
时,assert
什么也不做- 编译器定义了一些局部静态变量用于程序调试,
_ _func_ _
、_ _FILE_ _
、_ _LINE_ _
、_ _TIME_ _
、_ _DATE_ _
函数匹配
- 候选函数:同名函数、声明可见
- 可行函数:形参数量相等、形参类型相同
- 寻找最佳匹配
- 如果没有一个函数脱颖而出,编译器会因调用具有二义性而拒绝请求
- 调用重载函数应尽量避免强制类型转换。如果在实际应用中确需强制类型转换,则说明设计的形参集合不合理。
- 实参类型转换的等级:1.精确匹配;2.通过const转换实现的匹配;3.通过类型提升实现的匹配;4.通过算数类型转换或指针转换实现的匹配;5.通过类类型转换实现的匹配
- 内置类型提升和转换可能在函数匹配时产生意想不到的结果
- 所有算数类型转换的级别都一样
函数指针
- 函数指针指向的是函数而非对象
- 函数
bool lengthCompare(const string &, const string &);
,声明一个指向该函数的指针,bool (*pf)(const string &, const string &);
pf = lengthCompare;
等价于pf = &lengthCompare
bool b = pf("hello","goodbye");
等价于bool b = (*pf)("hello","goodbye");
等价于bool b = lengthCompare("hello","goodbye");
- 与数组类似,虽然不能定义函数类型的形参,但形参可以是指向函数的指针,形参看起来是函数类型,实际上当作指针;可以直接把函数作为实参使用,它也会自动转换为指针
- 使用类型别名和
decltype
可以简化使用函数指针的代码,如typedef decltype(lengthCompare) Func;
定义了函数类型,typedef decltype(lengthCompare) *FuncP;
定义了函数指针 using F = int(int*, int);
定义了函数类型F,using PF = int(*)(int*, int);
定义了指向函数类型的指针PF- 当
decltype
作用于函数时,它返回函数类型而非指针类型,需要显式地加上*
来表示需要返回指针
定义抽象数据类型
- 类=数据抽象+封装;数据抽象=接口+实现
- 定义在类内部的函数是隐式的
inline
函数 - 成员函数的声明必须在类的内部,它的定义既可以在类的内部也可以在类的外部;作为接口组成部分的非成员函数,它们的定义和声明都在类的外部
- 成员函数通过一个名为
this
的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this
- 因为
this
的目的总是指向“这个”对象,所以this
是一个常量指针,不允许改变this
中保存的地址 - 当把
const
关键字放在成员函数的参数列表之后,紧跟在参数列表后的const
的作用是修改隐式this
指针的类型,表示this
是一个指向常量的指针=>这样使用const
的成员函数称为常量成员函数 - 常量成员函数不能改变调用它的对象的内容
- 常量对象,以及常量对象的引用或指针都只能调用常量成员函数
- 编译器首先编译成员的声明,然后才轮到成员函数体。所以,成员函数体可以随意使用类中的其它成员而无需在意这些成员出现的次序
- 成员函数的定义必须与它的声明匹配,同时,类外部定义的成员的名字必须包含它所属的类名
return *this
返回调用该函数的对象,函数的返回类型应为对应类型的引用- 如果非成员函数是类接口的组成部分,这些函数的声明应该与类在同一个头文件内
- 构造函数不能被声明成
const
的 - 编译器创建的构造函数又称合成的默认构造函数
- 只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数
- 如果类包含有内置类型或者符合类型的成员,则只有当这些成员全都被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数
= default
要求编译器生成默认构造函数- 构造函数初始值列表在
函数名(参数列表):
后,在{}
函数体之前 - 构造函数初始值列表是成员名的一个列表,每个名字后紧跟
()
括起来的成员初始值,不同成员的初始化通过逗号分隔 - 当某个数据成员被构造函数初始值列表忽略时,它将以与合成默认构造函数相同的方式隐式初始化
- 一般来说,编译器生成的拷贝、赋值和析构操作将对对象的每个成员执行拷贝、赋值和销毁操作
- 很多需要动态内存的类应该使用
vector
对象或string
对象管理必要的存储空间,使用vector
或string
能避免分配和释放内存带来的复杂性 - 如果类包含
vector
或string
成员,则其拷贝、赋值和销毁的合成版本能正常工作
访问控制与封装
- 使用访问说明符(
public
、private
)加强类的封装性 class
和struct
关键字唯一的区别在于其默认访问权限不太一样,可以用任意一个来定义类struct
:第一个访问说明符前的成员是public
的;class
:第一个访问说明符前的成员是private
的- 类可以允许其它类或函数访问其非公有成员->友元
- 友元声明只需要在类内增加一条以
friend
关键字开始的函数声明语句即可 - 友元声明适用于类的接口组成部分非成员函数
- 友元声明仅仅指定访问权限,而非通常意义上的函数声明。如果我们希望类的用户能够调用某个友元函数,必须在友元声明外再专门声明一次函数。
- 通常在类的头文件中,除了类内部的友元声明外独立声明友元函数(在类外)
- 最好在类定义开始或结束前的位置集中声明友元
类的其它特性
- 用来定义类型的成员必须先定义后使用,因此,类型成员通常出现在类开始的地方
- 最好只在类外部定义的地方说明
inline
,可以使类更易理解 - 只要函数在参数的数量和/或类型上有所区别,就可以重载成员函数
- 当我们希望能修改类的某个数据成员,即使是在一个
const
成员函数内,可以通过在变量声明中加上mutable
实现 - 当我们提供一个类内初始值时,必须以
=
或{}
表示 - 一个
const
成员函数如果以引用的形式返回*this
,那么它的返回类型将是常量引用 - 通过区分成员函数是否是
const
的,可以对其进行重载 - 建议:对于公共代码使用私有功能函数->避免在多处使用同样的代码
- 我们可以仅仅声明类而暂时不定义它(类似于函数),如
class Screen;
->前向声明 - 在 “声明之后” “定义之前” 的类类型是一个不完全类型
- 前向声明适用于当类的成员包含指向它自身类型的引用或指针
- 如果一个类指定了友元类
friend class 类名
,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员 - 每个类负责控制自己的友元类或友元函数,友元关系不存在传递性
- 当把一个成员函数声明成友元时,必须明确指出该成员函数属于哪个类,如
类名::成员函数名
- 要想令某个成员函数作为友元,我们必须仔细组织程序的结构以满足声明和定义的彼此依赖关系
- 如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明
类的作用域
- 一个类就是一个作用域的事实很好地解释为什么当我们在类的外部定义成员函数时必须同时提供类名和函数名
- 一旦遇到类名,定义的剩余部分就在类的作用域之内了
- 返回类型必须指明它是哪个类的成员(返回类型中使用的名字都位于类的作用域之外)
- 编译器处理完类中的全部声明后才会处理成员函数的定义
- 在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字
- 当类的成员被隐藏时,可以通过加上类的名字或显式地使用
this
指针来强制访问成员,如this->成员变量名
或类名::成员变量名
- 建议不要把成员名作为参数或其它局部变量使用
- 当外部作用域的对象被隐藏时,可以使用作用域运算符访问它
构造函数再探
- 初始化和先定义后赋值在一些情况下有很大不同
- 如果成员是
const
或引用或属于某种类类型而该类没有定义默认构造函数时,必须将其初始化 - 应养成使用构造函数初始值的习惯
- 成员的初始化顺序与它们在类定义中出现的顺序一致,最好令构造函数初始值的顺序与成员声明的顺序一致,如果可能的话尽量避免使用某些成员初始化其他成员
- 如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数
- 委托构造函数使用它所属类的其它构造函数执行其自身的初始化过程,其成员初始值列表只有唯一的入口,即类名,如
Sales_data(): Sales_data("", 0, 0){函数体}
- 当对象被默认初始化或值初始化时自动执行默认构造函数,类必须包含一个默认构造函数以便在这些情况下使用
- 如果定义了其他构造函数,最好也提供一个默认构造函数
- 编译器只会自动地执行一步类类型转换
explicit
可以用来抑制构造函数的隐式转换,只能在类内声明构造函数时使用,explicit
只对一个实参的构造函数有效,需要多个实参的构造函数不能用于隐式转换,无须为其指定- 当使用
explicit
关键字声明构造函数时,它将只能以直接初始化的形式使用,且编译器不会在自动转换过程中使用该构造函数 - 尽管编译器不会将
explicit
构造函数用于隐式转换,但我们可以用这样的构造函数显式地强制转换,如static_cast
- 聚合类:1.所有成员
public
2.未定义任何构造函数 3.无类内初始值 4.无基类,也无virtual
函数 - 聚合类可以使用由花括号括起来的成员初始值列表初始化,如
Data val1 = { 0, "Anna"}
,初始值的顺序必须与声明的顺序一致,若初始值列表的元素个数少于类的成员数,则靠后的成员被值初始化 - 数据成员都是字面值类型的聚合类是字面值常量类
- 如果
- 数据成员都是字面值类型;
- 类至少含有一个
constexpr
构造函数; - 如果一个数据成员含有类内初始值,则内置类型成员的初始值是一条常量表达式,或者如果成员属于某种类类型,则初始值使用成员自己的
constexpr
构造函数; - 类必须使用析构函数的默认定义 则它也是一个字面值常量类
constexpr
构造函数体一般是空的,使用前置关键字就可以声明一个constexpr
构造函数
类的静态成员
- 使用
static
关键字使得成员与类本身直接相关,而不是与类的各个对象保持关联 - 类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据
- 静态成员函数不与任何对象绑定在一起,不包含
this
指针,无法声明为const
- 可以使用作用域运算符直接访问静态成员,也可以使用类的对象、引用或指针来访问静态成员,成员函数不用通过作用域运算符就能直接使用静态成员
- 既可以在类内也可以在类外定义静态成员函数,在类外定义时不能重复
static
关键字 - 必须在类的外部定义和初始化每个静态成员
- 从类名开始,一条定义语句的剩余部分就都位于类的作用域之内了
- 静态数据成员的类型可以是它所属的类类型,而非静态数据成员只能声明为它所属类的指针或引用
- 静态成员和普通成员的另一个重要区别是我们可以使用静态成员作为默认实参,但非静态成员不行,因为它的值本身属于对象的一部分
IO类
iostream
、fstream
、sstream
三个头文件分别定义了用于读写流、命名文件、内存string对象的类型- 标准库通过继承机制使我们能忽略这些不同类型的流之间的差异
- 不能拷贝或对IO对象赋值
- IO类的条件状态,确定一个流对象的状态最简单的方法是将其作为一个条件使用,如
while
循环检查>>
表达式返回的流的状态 - 流对象的
rdstate
成员返回一个iostate
值,对应流的当前状态 - 流对象的
clear
成员可以复位所有错误标志位(无参数)或设置流的新状态(有参数) - 缓冲刷新:数据真正写到输出设备或文件,导致缓冲刷新的原因有很多
endl
完成换行并刷新缓冲区工作;flush
刷新缓冲区但不输出任何额外字符;ends
向缓冲区插入一个空字符并刷新缓冲区- 如果程序崩溃,输出缓冲区不会被刷新
cout<<unitbuf;
告诉流在每次写操作后进行一次flush
;cout<<nounitbuf;
恢复为正常的缓冲区刷新机制- 当一个输入流关联到一个输出流时,从输入流读取数据的操作会先刷新关联的输出流,
cout
和cin
就是关联在一起的 x.tie(&o)
将流x关联到输出流o- 既可以将
istream
关联到ostream
,也可将ostream
关联到ostream
- 每个流同时最多关联到一个流,但多个流可以同时关联到同一个
ostream
文件输入输出
- 头文件
fstream
定义了三个类型来支持文件IO:ifstream
-读;ofstream
-写;fstream
-读写 fstream
的各类特有操作ifstream in(ifile);
构造一个ifstream
并打开给定文件- 调用
open
可以将空文件流与文件相关联,例如ofstream out;
、out.open(ofile);
。使用if (out)
可以判断open
是否成功。 - 为了将文件流关联到另一文件,必须先关闭已关联文件,例如
in.close()
、in.open(ifile)
。 - 当一个
fstream
对象被销毁时,close
会自动被调用。 - 文件模式:
in
-读方式;out
-写方式;app
-每次写操作前定位到文件末尾;ate
-打开文件后定位到文件末尾;trunc
-截断文件;binary
-以二进制方式进行IO - 以
out
模式(ofstream
的默认模式)打开文件会丢弃已有数据 - 阻止一个
ofstream
清空给定文件内容的方法是同时指定app
模式,例如ofstream app("file", ofstream::out| ofstream::app);
- 每次打开文件时,都要设置文件模式,否则使用默认值
string流
istringstream
-读;ostringstream
-写;stringstream
-读写stringstream
的各类特有操作istringstrean
和ostringstream
的使用- string流在拆分从文件中读入的字符串时非常有用
顺序容器概述
vector
-可变大小数组;deque
-双端队列;list
-双向链表;forward_list
-单向链表;array
-固定大小数组;string
-与vector
相似的容器,但专门用于保存字符string
和vector
将元素保存在连续的内存空间中:由元素下标计算地址非常快速,但在中间位置添删元素非常耗时list
和forward_list
令容器任何位置的添删操作都很快速,但不支持元素的随机访问,额外内存开销很大deque
更为复杂,支持快速的随机访问,在中间位置添删元素代价很高,但在两端添删元素速度很快array
大小固定,不支持添删元素和改变容器大小- 现代C++程序应该使用标准库容器,而不是原始的数据结构,如内置数组
- 除非有很好的理由选择其他容器,否则使用
vector
- 如果程序有很多小元素,且空间额外开销很重要,不要使用
list
或forward_list
- 如果程序要求随机访问元素,应使用
vector
或deque
- 如果程序要求在容器中间添删元素,应使用
list
或forward_list
- 如果程序需要在头尾添删元素,但不需要在中间添删元素,使用
deque
- 如果程序只有在读取输入时才需要在容器中间插入元素,随后需要随机访问元素:首先确定是否真的需要在容器中间插入元素,在处理输入数据时,可以很容易地向
vector
追加数据,再调用标准库的sort
函数来重排容器内元素,从而避免在中间插入元素;如果必须在中间位置插入元素,考虑在输入阶段使用list
,一旦输入完成,将list
中内容拷贝到vector
中。 - 不确定使用何种容器,可以在程序中只使用
vector
和list
的公共操作:使用迭代器,不使用下标操作,避免随机访问。这样在必要时选择使用vector
或list
都很方便
容器库概览
-
每个容器都定义在一个头文件中,文件名与类型名相同。容器均定义为模板类,大部分容器都需要额外提供元素类型信息。
-
顺序容器几乎可以保存任意类型的元素
-
容器操作:类型别名、构造函数、赋值与swap、大小、增删元素、获取迭代器、反向容器的额外成员
-
迭代器范围由一对迭代器表示,
[begin,end)
左闭右开区间,需要保证end不在begin之前,且指向同一个容器的元素或尾后元素 -
借助类型别名,可以在不了解容器中元素类型的情况下使用它,这在泛型编程中非常有用
-
begin
和end
操作生成指向容器中首元素和尾后元素的迭代器,形成一个包含容器中所有元素的迭代器范围 -
begin
和end
有多个版本:list<string> a = {"Milton", "Shakespeare", "Austen"}; auto it1 = a.begin(); // list<string>::iterator auto it2 = a.rbegin(); // list<string>::reverse_iterator auto it3 = a.cbegin(); // list<string>::const_iterator auto it4 = a.crbegin(); // list<string>::const_reverse_iterator
-
当不需要写访问时,应使用
cbegin
和cend
-
容器的定义和初始化:默认构造函数,拷贝初始化
c1(c2)
或c1=c2
,列表初始化c{a,b,c...}
或c={a,b,c...}
。只有顺序容器的构造函数才能接受大小参数seq(n,t)
,关联容器并不支持 -
拷贝初始化:1.拷贝整个容器;2.拷贝由一个迭代器对所指定的元素范围
-
当将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同
-
顺序容器提供的构造函数可以接受一个容器大小和一个元素初始值,例如
vector<int> ivec(10,-1);
-
定义一个
array
时,除了指定元素类型,还必须指定大小,例如array<int, 42>
-
虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但
array
并无此限制 -
容器的赋值运算可用于所有容器
-
赋值运算将左边容器的全部元素替换为右边容器元素的拷贝
-
swap
(交换元素)通常比直接拷贝快得多,如swap(c1,c2)
、c1.swap(c2)
-
assign
(替换元素)仅适用于顺序容器,不支持关联容器和array
,如seq.assign(b,e)
、seq.assign(il)
、seq.assign(n,t)
-
赋值相关运算会导致容器内部的迭代器、引用和指针失效,但
swap
不会 -
除
array
外,swap
不对任何元素进行拷贝删除或插入,可以保证在常数时间完成 -
除
string
外,指向容器的迭代器、引用和指针在swap
操作后仍指向swap
操作前所指向的元素 -
交换两个
array
所需时间与array
中的元素数目成正比 -
统一使用非成员版本的
swap
是一个好习惯 -
容器大小操作:
size
、empty
、max_size
-
比较两个容器实际上进行元素的逐对比较,与
string
的关系运算类似:大小相同元素相等则相等;大小不同元素相等则小容器小于大容器;大小不同元素不等则取决于第一个不等元素的比较结果
顺序容器操作
- 添加元素:
push_back(t)
或emplace_back(args)
、push_front(t)
或emplace_front(args)
、insert(p,t)
或emplace(p,args)
及多种insert
操作 - 向一个
vector
、string
、deque
插入元素会使所有指向容器的迭代器、引用和指针失效 - 除
array
和forward_list
外,每个顺序容器都支持push_back
- 当我们用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器的是对象值的一个拷贝,而不是对象本身。
list
、forward_list
、deque
还支持push_front
,deque
像vector
一样提供随机访问元素的能力,但它提供了vector
所不支持的push_front
,deque
保证在容器收尾插入和删除元素的操作都只花费常数时间,与vector
一样,在deque
首尾之外的位置插入元素会很耗时- 将元素插入到
vector
、deque
、string
中的任何位置都是合法的,但可能很耗时 insert
的返回值是指向新插入元素的迭代器- 理解
emplace
:c.emplace_back("978-0590353403", 25, 15.99)
等价于c.push_back(Sales_data("978-0590353403", 25, 15.99))
emplace
在容器中直接构造元素。传递给emplace
的参数必须与元素类型的构造函数相匹配。front
和back
分别返回首元素和尾元素的引用,注意区分其与begin
和end
的区别(后者是迭代器)- 访问成员函数返回的是引用,如果容器是一个
const
对象,则返回值是const
的引用;如果容器不是const
的,则返回值是普通引用,我们可以用来改变元素的值 - 如果使用
auto
变量来保存和改变元素的值,必须将变量定义为引用类型 - at和下标操作只适用于
string
、vector
、deque
和array
,每个顺序容器都有一个front
成员函数,除forward_list
外所有顺序容器都有一个back
成员函数。 - 删除
deque
中除首尾外的任何元素都会使迭代器、引用、指针失效;指向vector
或string
中删除点之后位置的迭代器、引用、指针都会失效 pop_front
和pop_back
分别删除首元素和尾元素,vector
和string
不支持pop_front
,forward_list
不支持pop_back
erase
可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定范围内的所有元素,返回指向删除元素之后位置的迭代器forward_list
插入删除操作:before_begin()
、cbefore_begin()
返回指向链表首元素之前不存在的元素的迭代器(首前迭代器);insert_after()
在迭代器p之后的位置插入元素;emplace_after
使用args在p指定的位置后创建一个元素;erase_after
删除p指向的位置后的元素- 顺序容器改变大小:
c.resize(n)
、c.resize(n,t)
;vector
、string
、deque
进行resize
可能导致迭代器、指针和引用失效,在缩小容器时指向被删除元素的迭代器、引用、指针会失效 - 由于向迭代器添加元素和从迭代器删除元素的代码可能会使迭代器失效,因此必须保证每次改变容器的操作之后都正确地重新定位迭代器,这对
vector
、string
和deque
尤为重要。 - 程序必须保证每个循环步中都更新迭代器、引用或指针,使用
insert
和erase
是一个好的选择,因为它们会返回操作后的迭代器(分别指向新添加元素和删除元素之后) - 不要保存
end
返回的迭代器,而是反复调用它
vector对象是如何增长的
vector
将元素连续存储,为了降低添加元素时所带来的内存分配和释放开销,vector
和string
会预分配更大的内存空间来避免内存空间的重新分配- 管理容量的成员函数:
c.capacity()
-不重新分配内存空间的话c可以保存多少元素;c.shrink_to_fit()
-将capacity()
减少为与size()
相同大小;c.reserve(n)
-分配能容纳n个元素的内存空间 - 调用
reserve
永远不会减少容器占用的内存空间
额外的string操作
- 构造string的其他方法:
string s(cp,n)
、string s(s2,pos2)
、string s(s2,pos2,len2)
s.substr(pos,n)
子字符串操作string
还定义了额外的insert
和erase
版本(接受下标的版本):s.insert(s.size(), 5, 'i');
、s.erase(s.size()-5, 5);
string
还提供了接受C风格字符数组的insert
和assign
C++特性
unique_ptr<类型>
代表一种不共享的指针,不能复制只能移动(std::move
),可以通过make_unique<类型>(参数)
来创建。归属于头文件<memory>
,属于C++标准库。
参考 How to: Create and use unique_ptr instances
inline
一种关键字,表示内联。在程序编译过程中对内联部分的代码调用直接替换代码段。仅适用于简单函数,仅对编译器进行建议,必须与实际实现的函数体放在一起才有意义(仅作用于声明则无效)。
const
一种关键字,表示常量。被修饰的对象或变量无法被修改。
const对象必须初始化,仅在文件内有效。
如果想在多个文件中共享const对象,必须在变量的定义之前添加extern
关键字。
允许一个常量引用绑定非常量对象,但无法通过常量引用改变非常量对象。类似的,允许一个常量指针绑定非常量对象。
参考 const (C++)
constexpr
用来修饰编译器常量。由编译器来验证变量的值是否是一个常量表达式。
在C++ 11中,表示“常量”可以用constexpr,表示“只读”时才用const。
constexpr在修饰指针时仅对指针有效,与指针所指的对象无关。(将其定义的对象置为了顶层const)
memcpy
memcpy(a,b,c)
从b处拷贝c个字节至a
属于标准库的cstring
override
override
关键字用于派生类中需要重写的函数后,如果这些函数未被重写,编译器会报错
防止直接继承基类成员函数的接口和缺省实现