【C++ Prime Plus】复习
前言
复习内容只针对自己遗忘的一些知识点以及一些特殊概念,已熟悉及理解的内容大部分不会列出
第1章 预备知识
计算机语言处理两个概念:数据和算法
C语言是结构化编程与自顶向下(控制结构与函数)
C++语言继承了C,并且添加了面向对象编程与泛型编程
面向对象编程强调的是数据与自下向上(基础类到程序)
泛型编程指创建独立于类型的代码
第2章 开始学习C++
C++对大小写敏感
在C语言中,省略返回类型相当于说返回类型为int
,但这一理念不适用于C++
main函数可以不用写return
,默认返回0,但是其他有着返回值的函数必须写return
.h
的头文件属于C风格的头文件,使用时无需使用命名空间,但C++风格的头文件在使用时需要使用命名空间
endl
会保证刷新输出,\n
则不保证刷新输出,所以有可能会出现输入内容之后才会显示提示信息的情况
声明语句可分为“定义声明语言”与“引用声明语句”
高级的cout
也能达到printf
所提供的细致的控制功能
C++提供了两种发送消息的方法:使用类方法或重新定义运算符(如cout
与cin
)
函数原型中形参只提供类型,可以不用命名
第3章 处理数据
以两个下划线或下划线和大写字母打头的名称被保留给实现使用,以一个下划线开头的名称被保留给实现,用作全局标识符
sizeof
返回类型或者变量的长度,单位为字节;类型名需要放在括号中,变量名的括号是可选的
C++可以使用括号赋值初始化,也可以使用大括号初始化器(可以使用等号,也可以不使用)
cout
的控制符有endl
换行dec
十进制hex
十六进制oct
八进制
使用后缀来表示常量的类型
- 整数
- 默认是
int
常量 - l或L表示
long
常量 - u或U表示
unsigned int
常量 - ul或UL表示
unsigned long
常量 - ll或LL表示
long long
常量 - ull或ULL表示
unsigned long long
常量
- 默认是
- 浮点数
- 默认是
double
常量 - f或F是
float
常量 - l或L是
long double
常量
- 默认是
转义序列中,\n
字符 \032
八进制 \x3a
十六进制
与int
不同,char
默认情况下既不是无符号,也不是有符号,是否有符号是由C++实现决定的
wchar_t
(宽字符类型)可以表示扩展字符集,wcin
和wcout
可以用于处理wchar_t
流,可以添上前缀L来指示宽字符常量和宽字符串,如wcout << L"tall" << endl;
C++新增了类型char16_t
和char32_t
,前缀u表示char16_t
字符常量和字符串常量,前缀U表示char32_t
常量
符号常量 | 表示 |
---|---|
CHAR_BIT | char的位数 |
CHAR_MAX | char的最大值 |
CHAR_MIN | char的最小值 |
SCHAR_MAX | signed char的最大值 |
SCHAR_MIN | signed char的最小值 |
UCHAR_MAX | unsigned char的最大值 |
SHRT_MAX | short的最大值 |
SHRT_MIN | short的最小值 |
USHRT_MAX | unsigned short的最小值 |
INT_MAX | int的最大值 |
INT_MIN | int的最小值 |
第4章 复合类型
数组
在声明数组的时候可以使用变量作为数组大小
如果只对数组一部分初始化,其他元素将会被设置为零;可以在大括号中不包含任何东西,来将所有元素初始化为零。
字符串
任何两个由空白(空格、制表符、换行符)分隔的字符串常量都将自动拼接为一个字符串
getline()
会丢弃掉读取到的换行符,而get()
会将读取到的换行符保留在输入流中,不过可以使用不带参数的get()
读取下一个字符(包括换行符)
cin.getline(str_arr, arr_size)
常用来读取C风格字符串,getline(cin, str)
常用来读取string字符串
C++允许将列表初始化用于C风格字符串和string对象
string
类可以使用+
将两个string
对象合并起来,还可用+=
将字符串附加到string
对象的末尾
C风格字符串可以使用strcpy()
来进行字符串的复制,strcat()
来进行字符串的拼接,字符串复制的时候由于数组长度的原因,使用strncpy()
更为安全
原始字符串用R作为前缀,在内部可以自由使用双引号与反斜杠,如cout << R"(text)";
,其使用"(
和)"
作为定界符,在定界符中加入任意字符串也可以生成特殊定界符,如cout << R"+=(text)+=";
结构 struct
C++允许在声明结构变量时省略struct
列表初始化也可以用于结构,等号是可选的,如果大括号内不包含任何东西,各成员将被设置为零
共用体 union
共用体能够存储不同的数据类型,但只能同时存储其中的一种类型,共用体的长度为其最大成员长度
枚举 enum
对于枚举,只定义了赋值运算符,默认首元素为0,后面的依次加1
枚举取值范围的上限,是大于这个最大值的最小的2的幂,并减1,如果所有值都大于0,下限为0,否则,方法与上限相同,但加上负号
指针
在声明指针时,对于每一个指针名,都要用*
变量在称为栈的内存区域中,而new出的对象在堆或自由存储区中
使用delete
释放new
出的单个对象,使用delete []
释放new
出的一组对象
可以使用数组访问法(即方括号)来使用指针,数组名与指针的区别是:数组名不可修改,指针可以修改;使用sizeof
时,数组名返回整个数组的长度,而指针只返回指向目标的长度
指针变量加一的时候,实际增加的值与指向的类型有关
普通变量名访问成员属性时使用点号.
,而指针访问成员属性的时候使用箭头->
数组的替代品
vector<typeName> vec(size);
array<tyepName, size> arr;
vector
是在堆中分配内存空间的,而array
是在栈中,因此效率更高
at()
方法虽然更慢,但可以判断索引的有效性
第5章 循环和关系表达式
循环
C++提供了三种基础循环:for循环,while循环,do...while循环。
C++有一个特殊的基于范围的for循环,如
int arr[5] = {1, 2, 3, 4, 5};
for (int x: arr)
{
cout << x << endl;
}
控制语句:在关键字,如for
,if
等之后加上一个空格再加括号,视觉上强化控制语句与函数调用之间的区别。
using声明与using编译指令using std:cout;
using namespace std;
前缀、后缀:前缀递增、前缀递减和解除引用运算符的优先级相同,以从右向左的方式进行结合。后缀递增和后缀递减的优先级相同,但比前缀运算符的优先级高,以从左到右的方式进行结合。
语句块:语句块中定义了与语句块外名字相同的变量时,在语句块中,新变量会隐藏旧变量。
逗号运算符:优先级是最低的,先计算第一个表达式,再计算第二个表达式,整体的值是第二部分的值。
字符串比较:C风格字符串使用strcmp()
函数来比较,左在右前,返回负值,左与右相同,返回零,左在右后,返回正值。
延时程序:clock()
返回程序开始执行后所用的系统时间,ctime
头文件中定义了一个符号常量CLOCKS_PER_SEC
,每秒所包含的系统时间单位。int second = 10; clock_t start = clock(); clock_t delay = second * CLOCKS_PER_SEC; while (clock() - start <= delay);
类型别名:typedef
和#define
都可以用来定义类型的别名,但是声明一系列指针变量的时候不能使用后者,因为它是基于宏替换的。
文本输入
cin
在读取单个字符时会忽略空格和换行符
cin.get(ch)
读取输入的下一个字符(即使是空格),并赋值给ch
cin >> ch; // 一次读取一个字符,跳过空格等空白符
cin.get(ch); // 一次读取一个字符,不跳过空白符,将读取到的值存储到ch中,返回一个istream对象
cin.get(); // 一次读取一个字符(包括换行符等),返回值是读取到的字符
cin.get(name, size); // 一次读取一行字符(如果不超过最大长度),但会将换行符留在输入流中
cin.get(name, size).get(); // 同上,不过最后会处理掉最后的换行符
文件尾条件:EOF检测文件尾,在检测到文件尾的时候,cin.eof()
和cin.fail()
将会返回true
,注意,eof()
与fail()
报告最近读取的结果,也就是说,它们在事后报告,因此,应将cin.eof()
与cin.fail()
测试放在读取后。
cin.clear()
可能清除EOF标记,使输入继续进行
当cin
出现在需要bool值的地方,会被转换为一个bool值(转换函数是由istream类提供的,可以将istream转换为bool值)
cin.get()
在读取到EOF时,会返回一个用符号常量EOF表示的特殊值
cin.get(ch);
while (cin.fail() == false) // 或 while (!cin.fail())
{
...
cin.get(ch);
}
while ((ch = cin.get()) != EOF)
{...}
第6章 分支语句与逻辑表达式
分支语句
if语句
if (test-condition)
statement
if (test-condition)
statement1
else
statement2
if (test-condition1)
statement1
else if (test-condition2) // 可以存在更多的 else if
statement2
else
statement3
switch语句
switch (integer-expression) // integer-expression 必须是一个结果为整数值的表达式
{
case label1: statement(s) // 每个标签都必须是整数常量表达式
case label2: statement(s) // 最常见的标签为int或char常量,也可以枚举量
...
default : statement(s)
}
如果既可以使用if else if语句,也可以使用switch语句,则当选项不少于三个时,应使用switch
逻辑运算符
||
逻辑OR:如果左侧子表达式为true,则不执行右侧的子表达式
&&
逻辑AND:如果左侧子表达式为false,则不执行右侧的子表达式
!
逻辑NOT
逻辑NOT的优先级高于所有的关系与算术运算符
逻辑OR和逻辑AND的优先级低于关系运算符
逻辑AND的优先级高于逻辑OR的优先级
在C++中,可以使用and
、or
与not
,C语言中如果要使用,需要在程序中添加头文件iso646.h
字符函数库 cctype
一个与字符相关的,非常方便的函数软件包,它可以简化诸如确定字符是否为大写字母、数字、标点符号等工作
函数名称 | 返回值 |
---|---|
isalnum() |
如果参数是字母数字,即字母或数字,则函数返回true |
isalpha() |
如果参数时字母,该函数返回false |
iscntrl() |
如果参数是控制字符,该函数返回true |
isdigit() |
如果参数是数字(0~9),该函数返回true |
isgraph() |
如果参数是空格之外的打印字符,该函数返回true |
islower() |
如果参数是小写字母,该函数返回true |
isprint() |
如果参数是打印字符(包括空格),该函数返回true |
ispunct() |
如果参数是标点符号,该函数返回true |
isspace() |
如果参数是标准空白字符,如空格、进纸、换行符、回车、水平制表符或者垂直制表符,该函数返回true |
isupper() |
如果参数是大写字母,该函数返回true |
isxdigit() |
如果参数是十六进制数字,即0~9、a~f或A~F,该函数返回true |
tolower() |
如果参数是大写字符,则返回其小写,否则返回该参数 |
toupper() |
如果参数是小写字符,则返回其大写,否则返回该参数 |
读取错误
当使用cin
读取时发生类型不匹配的情况时,将发生4种情况
- 变量的值保持不变
- 不匹配的输入将被留在输入队列中
cin
对象中的一个错误标记将被设置- 对
cin
方法的调用将返回false(如果被转换为bool类型)
处理这种情况,应采用三个步骤
- 重置
cin
以接受新的输入 - 删除错误输入
- 提示用户再输入
简单文件输入/输出
输出:添加头文件fstream
,创建了一个ofstream
对象,将ofstream
对象同文件关联起来,结束后关闭文件
打开已有的文件,以接受输出时,默认将它其长度截断为零,因此原来的内容将丢失。
如果忘记关闭文件,程序正常终止时,将会自动关闭它
输入:添加头文件fstream
,创建一个ifstream
对象,将ifstream
对象同文件关联起来,结束后关闭文件
从文件获取内容时,使用is_open()
或good()
方法检查文件是否成功打开
第7章 函数
基础知识
函数的返回类型不能为数组,但是可以为指针
提供函数原型可以在编译时捕获错误,在原型中可以不提供形参的名称
C++标准使用argument(参数)来表示实参,使用parameter(参量)来表示形参
在C++中,当且仅当用于函数头或函数原型时,int * arr
和int arr[]
的含义才是相同的。当指针指向数组的第一个元素时,建议使用数组表示法,而当指针指向一个独立的值时,使用指针表示法。
STL使用超尾概念来指定区间
指针与const
int x = 5;
int *p_x = &x; // 常规变量的地址赋给常规指针
int y = 5;
const int *p_y = &y; // 常规变量的地址赋给指向const的指针
const int z = 5;
const int *p_z = &z; // const变量的地址赋给指向const的指针
const int *p1 = &x; // 指针指向的值不可修改,指针本身可以修改,从而指向其他值
int * const p2 = &x; // 指针的值不可修改,但是指针指向的值可以修改
const int * const p3 = &x; // 指针的值和指针指向的值都不可以修改
函数指针
函数也有地址,函数的地址是存储其机器语言代码的内存的开始地址
函数名(不带括号)就是函数的地址
函数指针声明时指定返回值和参数,如double (*pf)(int) = pam; // *pf需要带括号,不带括号就是一个返回值为double*的名字为pf的函数
使用函数指针调用函数时有两种,第一种使用(*pf),如double y = (*pf)(5);
第二种像使用函数名一样使用pf,如double y = pf(5);
,第一种语义更明显
函数指针数组 double (*pa[3])(int);
函数指针数组指针 double (*(*pa)[3])(int);
使用typedef
简化函数指针
typedef double real;
typedef double (*p_fun)(int);
p_fun p1 = f1;
p_fun pa[3];
p_fun (*pa)[3];
第8章 函数探幽
内联函数
内联函数可以提高C++程序的运行速度,但代价是占用更多内存
编译器将使用相应的函数代码来替换函数调用
内联函数只需在函数声明或函数定义前加上inline
关键字即可
对于内联函数,通常的做法是,省略函数原型,直接将整个函数定义放在本应提供函数原型的地方inline double squre(double x) {return x * x;}
引用变量
引用是已定义变量的别名,通常用途是作为函数形参,这样便可以直接使用原始数据
在使用引用时,必须在声明引用的同时对其初始化
如果想要使用信息而不对其进行修改,可以使用常量引用
如果是常量引用,编译器在下列情况下会生成临时变量:实参的类型正确,但不是左值;实参的类型不正确,但可以转换为正确的类型
基类引用可以指向派生类对象
默认参数
必须通过函数原型来设置默认参数,默认值从右到左添加
函数重载
函数的参数列表,也称为函数特征标,C++允许定义名字相同的函数,条件是他们的特征标不同
编译器在检查函数的特征标时,将引用类型和类型本身视为同一个特征标,但把const
和非const
看作不同特征标
不要滥用函数重载,仅当函数基本上执行相同的任务,但使用不同的数据时,才应使用函数重载
函数模版
函数模版是通用的函数描述,使用泛型来定义函数,其中的泛型可用具体的类型替换。
通过将类型作为参数传递给模版,可使编译器生成该类型的函数
模版并不创建函数,只是告诉编译器如何定义函数
template <typename T>
void swap(T & a, T & b) {
T temp;
temp = a;
a = b;
b = temp;
}
并非所有的模版参数都必须是模版类型template <typename T> void swap(T * a, T * b, int n);
模版函数无法处理某些类型时,有两种方法:第一种,重载运算符使得目标类型支持对应的运算;第二种,提供具体化的模版定义
显式具体化:以template<>开头,还需要提供函数定义
template<> swap<job>(job a, job b);
template<> swap(job a, job b);
显式实例化:声明所需要的类型,并在声明前使用template,无需提供函数定义template swap<int>(int, int);
decltype:推导类型 decltype(expression) var;
- 如果expression是一个没有用括号括起来的标识符,则var的类型与该标识符的类型相同,包括const等限定符
double x = 5.5; double y = 7.9; double &rx = x; const double *pd; decltype(x) w; // w is type double decltype(rx) u; // u is type double & decltype(pd) v; // v is type const double *
- 如果expression是一个函数调用,则var的类型与函数的返回类型相同
- 如果expression是一个用括号括起来的标识符,则var为指向其类型的引用
double xx = 4.4; decltype((xx)) r2 = xx; // x is type double & decltype(xx) w = xx; // w is type double
- 如果前面的条件都不满足,则var的类型与expression的类型相同
int j = 3; int &k = j; int &n = j; decltype(j + 6) i1; // i1 is type int decltype(100L) i2; // i2 is type long decltype(k + n) i3; // i3 is type int
- 需要多次声明时,可以使用typedef与decltype结合使用
typedef decltype(x+y) xytype; xytype xpy = x + y; xytype &rxy = xpy;
后置返回类型:在返回类型会随着形参类型导致不确定的时候可以使用后置返回类型
double h(int x, float y);
auto h(int x, float y) -> double;
template<typename T1, typename T2>
auto gt(T1 x, T2 y) -> decltype(x+y)
{
...
}
第9章 内存模型和名称空间
多文件
为了有效地组织程序,可以将其分为三部分:头文件(包含结构声明的代码)、源代码文件(包含实现结构、函数的代码)、源代码文件(包含调用函数、结构的代码)
头文件常包含的内容有:函数原型、使用#define
或const
定义的符号常量、结构声明、类声明、模版声明、内联函数等。
在包含自己的头文件时,使用引号优先在当前目录中查找
头文件只能包含一次,为了放置多次包含,可以如下
#ifndef XXX_H_
#define XXX_H_
...
#endif
存储持续性、作用域和链接性
持续性:
- 自动存储持续性:执行函数或代码块时被创建,执行完将会自动释放。
- 静态存储持续性:在程序整个运行过程中都存在
- 线程存储持续性(本书不讨论)
- 动态存储持续性:使用new分配的内存
作用域:描述了名称在文件的多大范围内可见
链接性:描述了名称如何在多个文件之间共享
自动存储持续性
同名变量,内部新定义会隐藏掉之前的定义
自动变量通常使用栈来进行管理
静态存储持续性
默认初始化为0,持续存在于内存中
外部链接性静态变量:如int x; // 在代码块之外
- 定义声明会分配空间
int x; // 在代码块之外
- 引用声明不分配空间
extern int x;
内部链接性静态变量:如static int x; // 在代码块之外
无链接性静态变量:如static int x; // 在代码块内部
- 只在代码块内可用,但在代码块不活动时仍然存在
说明符与限定符
const:
- const修饰的变量名不能进行修改。
- const全局变量的链接性为内部的,也就是说,每个文件有一组属于它的常量。
- 外部链接性的常量:
extern const int max = 50
,所有要使用该常量的文件都要添加extern
,但只能有一个进行初始化。
volatile:不允许缓存,因为该变量对应的内容可能会被硬件改写,属于实时数据
mutable:用来指出,即使某个类或者结构变量为const,但内部被mutable修饰的成员也可以被修改
函数与链接性
所有的函数都是静态持续性的(因为不允许在函数内部定义函数)
默认函数是外部链接性的,可以用static修饰,从而使其变为内部链接性
动态存储持续性
编译器通常使用三块独立的内存,一块用于静态变量,一块用于静态变量,一块用于动态存储。
使用new运算符在堆中找到一个内存块,分配空间并初始化。
// 类型名+括号
double * pd = new double (99.99);
// 数组用大括号
int * ar = new int[4] {1, 2, 3, 4};
// 大括号初始化也可以用于单个元素
int * pi = new int {666};
包含了头文件new之后,就可以使用定位new特性,从你传递给它的地址处直接开始分配空间,使用定位new分配的空间无需使用delete进行释放。
char buffer[50];
int * p1 = new (buffer) int[10];
名称空间
声明区域是可以在其中进行声明的区域
潜在作用域是从声明点开始,到其声明区域的结尾
创建名称空间,可以是全局的,也可以位于其他名称空间中,但是不能位于代码块中
namespace Jack {
double pail;
void fetch();
}
namespace Jill {
double fetch;
namespace Bob {
int pail;
}
}
名称空间是开放的,即可以将名称加入到已有的名称空间中
// 之前已经有了Jack名称空间
namespace Jack {
int pal;
}
使用域解析运算符来使用名称空间中的变量Jack::pail = 99.99;
Jill::Bob::pail = 15;
using声明不允许存在重名的变量,会产生冲突
using编译指令的变量可以被覆盖
可以给命名空间创造别名 namespace Jk = Jack;
未命名的命名空间,从声明点到该区域末尾都可以使用,类似于内链接性的静态变量。
在名称空间中声明的函数的作用域是整个命名空间
第10章 对象和类
重要的OOP特性:抽象、封装、数据隐藏、多态、继承、代码的可重用性
过程性编程优先考虑步骤,面向对象编程优先考虑数据
类
类:将数据表示和操纵数据的方法组合在一起
要使用类,只需要了解其公共接口,要编写类,必须要创造其公共接口
数据隐藏:防止程序直接访问数据
封装:将实现细节放在一起并与抽象分开
C++中,类和结构唯一的区别是默认访问权限,类默认是private,结构默认是public
类方法:定义成员函数时,要用作用域运算符标识所属类。类方法可以访问类的private组件
内联方法:有两种实现形式,第一种,定义位于类声明中的函数;第二种,类实现中使用inline修饰方法
同一个类的所有对象属性独立,共用同一组方法
//类声明
class className
{
private:
data member declarations
public:
member function prototypes
};
// 实现类方法
return_type className::functionName(...)
{
...
}
构造和析构
构造函数和析构函数没有返回值与类型标识。
// 声明中直接实现
Stock(string name) {
...
}
~Stock() {
...
}
// 实现文件中再进行实现
Stock::Stock(string name) {
...
}
Stock::~Stock() {
...
}
构造函数不能通过对象使用,只能在创建对象时使用。创建对象时有两种形式:隐式调用构造函数:Stock food("hello")
,显式调用构造函数:Stock food = Stock("hello");
,当显式调用构造函数的时候可能会有两种实现方式,第一种是直接创建对象并初始化,第二种是创建一个临时对象,然后将它的所有属性都复制给原对象。
默认构造函数:当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。自己定义时有两种:第一种,给所有的参数都赋默认值;第二种,写重载的无参版本。当隐式调用默认构造函数时,不要带括号,否则容易被认为是函数,如Stock food();
,被视作是一个返回值为Stock对象的无参函数。
由于自动变量放在栈中,所以后创建的对象会先删除掉。
列表初始化:提供与某个构造函数的参数列表匹配的内容,如Stock jack {"hello"};
const:某个方法不会修改当前对象的属性,可以在括号后面添加const,如void show() const;
this指针
this指针指向调用对象,*this可以视作调用对象的别名。
对象数组
声明并初始化对象数组
Stock stocks[4] = {
Stock("nana", 12.5, 50),
Stock("nana"), // 可以使用不同的构造函数
Stock(),
}; // 未指出的将会自动使用默认构造函数进行初始化
类作用域
在类中定义的名称,作用域是整个类,只在该类中是已知的,在类外是不可知的
要使用对象来调用类的公有函数
类自己内部使用类方法时不需要使用修饰
常量:由所有对象共享的常量有两种方法:一种,使用枚举enum {months = 12};
;第二种,使用关键字static进行修饰static int months = 12;
枚举:普通的枚举容易冲突,如:enum egg{Small}; enum t_shirt {Samll}; // 会产生冲突
作用域为类的枚举:enum class egg {Small}; enum class t_shirt {Small}; egg choice = egg::Small;
第11章 使用类
运算符重载
运算符函数的格式operatorop(argument-list)
如:Time operator+(const Time & t) const;
成员函数运算符重载时,运算符左侧的对象是调用对象,运算符右侧的对象是作为参数被传递的对象。
运算符优先级:如t4 = t1 + t2 + t3;
转换为 t4 = t1.operator+(t2 + t3);
,先计算后面的
限制条件:
- 重载运算符至少有一个操作数时用户定义的类型,防止重载标准类型的运算符
- 使用运算符时不能违背原来的语法规则,比如二元运算符就不可以当做一元运算符重载
- 不能创建新的运算符,如试图定义
operator**();
来计算幂 - 不能重载下列运算符
sizeof
:sizeof运算符.
:成员运算符.*
:成员指针运算符::
:作用域解析运算符?:
:条件运算符- 更多见 P387
- 以下运算符只能使用成员函数进行重载
=
:赋值运算符()
:函数调用运算符[]
:下标运算符->
:通过指针访问类成员的运算符
在得到一个新对象的时候,尽可能的使用构造函数
可以对已经重载的运算符进行重载,比如-
既是一元运算符,又是二元运算符
友元函数
友元有三种:友元函数、友元类、友元成员函数
通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限
非成员函数由于没有使用对象调用,它所使用的所有值都是显式参数
创建友元:第一步,将函数原型加入到类声明中,并在前面加上关键字friend
,第二步,编写函数定义(不用在前面添加friend
)
友元函数虽然在类声明中声明,但它并不是成员函数,不能使用成员运算符来调用;但它拥有和成员函数相同的访问权限
重载<<运算符:C++是左到右处理输出的
ostream & operator<<(ostream & os, const c_name & obj) {
os << ... ;
return os;
}
随机数
cstdlib
中包含了srand()
和rand()
的原型
ctime
中包含了time()
的原型
rand()
返回一个0~Max之间的整数,具体取决于实现,常使用方法如:int x = rand() % 10;
time(0)
返回当前时间,通常为从某一个日期开始的秒数
srand()
设置新种子,从而启动一个新的随机数序列,如:srand(time(0));
类的自动转换
目标类型转换为当前类型:使用接受一个函数的构造函数(如果第二个参数有默认值也可以),使用关键字explicit
可以关闭隐式转换。
Time(int x); // 如果构造函数这样定义
Time t1;
t1 = 15; // 可以隐式转换
explicit Time(int x); // 这样不能隐式转换
Time t1, t2;
t1 = Time(15); // 两种显式转换的形式
t2 = (Time) 15;
当前类型转换为目标类型:使用转换函数,转换函数必须是类方法,不能指定返回类型,不能有参数,但有返回值。使用关键字explicit
可以关闭隐式转换
operator typename(); // 转换函数格式
Time::operator int() const {
return hour * mins_per_hour + min;
}
Time t1, t2, t3;
int time1 = t1; // 隐式转换,如果函数使用 explicit 修饰,这个就不能使用
int time2 = (int) t2;
int time3 = int(t3);
加法实现:
- 第一种方法:将加法定义为友元函数,里面的参数是当前类型,然后通过自动转换将普通变量转换为对应类型,再调用这个函数。
- 第二种方法:显式指出每一种类型,这种方法相较于第一种,代码量更多,但是减少了不必要的转换,时间和空间浪费较少。
第12章 类和动态内存分配
动态内存和类
静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化的时候不需要加static
在构造函数中使用new
来分配内存时,必须在相应的析构函数中使用delete
释放内存
对于空指针,可以使用C++新的关键字nullptr
C++自动提供的成员函数有(如果没有定义的话):
- 默认构造函数
- 默认析构函数
- 复制构造函数
- 赋值运算符
- 地址运算符
默认析构函数:
- 如果对象是动态变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数。
- 如果对象是静态变量,则在程序结束之后调用该对象的析构函数
- 如果对象使用
new
创建的,在显式使用delete
删除对象的时候才会调用析构函数
复制构造函数:复制构造函数用于初始化过程中,class_name(const class_name &);
常见的使用情况有:Time t2(t1);
Time t2 = t1;
Time t2 = Time(t1);
Time *pt = new Time(t1);
,每当程序生成了对象副本的时候,就会使用该函数,常见于按值传参和返回对象。默认构造函数是浅拷贝的,意味着对于指针类型的,它会直接复制地址,会导致出错,对于这种情况,需要深拷贝,分配内存并拷贝指向位置的值。
赋值运算符:class_name & class_name::operator=(const class_name &);
赋值运算符与复制构造函数的区别在于,当前对象可能已经分配了数据,需要先对原有的数据进行释放,然后再复制新的数据;应该避免出现赋值给自己的情况;返回一个指向调用对象的引用(用于连续赋值)。
c_name & c_name::operator=(const c_name & cn)
{
if (this == &cn)
return *this;
delete [] c_pointer;
c_pointer = new type_name[size];
...
return *this;
}
比较函数:一般将比较运算符和其他常见的二元运算符作为友元函数出现,这是因为当左值不是一个对象的时候,也能够通过自动类型转换成功完成操作
中括号运算符可以使用方法operator[]()
来重载,一般会同时定义一个应对const变量的版本,如字符串中const char & operator[](int x) const;
静态类成员函数:使用static
关键字修饰的成员函数。不能通过对象调用,可以通过类名和作用域解析运算符调用。静态类成员函数中只能使用静态数据成员
构造函数中的new应注意事项
- 如果在构造函数中使用
new
来初始化指针成员,则应该在析构函数中使用delete
释放掉它 new
和delete
必须互相兼容,new
对应于delete
,new []
对应于delete []
- 如果使用多个构造函数,则必须以相同的方式使用
new
,要么都带中括号,要么都不带。 - 应定义一个复制构造函数(深复制)
- 应定义一个赋值运算符(先释放,深复制)
有关返回对象的说明
返回指向const对象的引用:返回引用不会调用复制构造函数,引用指向的对象必须在调用函数执行时存在,形参传入的时候就是const引用。
返回指向非const对象的引用:常见的两种情况,重载赋值运算符;重载<<
运算符
返回对象:如果待返回的对象是个局部变量,返回对象时会调用复制构造函数
返回const对象:为了防止出现神奇的代码cout << t1 + t2 = t3 << endl;
使用指向对象的指针
初始化对象的几种形式:Time *pt = new Time(666);
Time *pt = new Time;
使用new
创建的指针对象一定要记得使用delete
删除对象
声明对象指针时,需要注意几点:
- 使用常规表示法来声明对象指针
Time * pt;
- 可以将指针初始化为已经存在的对象
Time * pt = &t1;
- 可以使用
new
来调用对应的构造函数初始化指针,同时创建一个新的对象Time * pt = new Time(666);
- 可以使用
->
运算符通过指针访问类方法pt->show();
- 可以对对象指针使用解除引用运算符(*)来获得对象
Time t2 = *pt;
定位new
运算符:因为定位new
运算符无需使用delete
进行释放,所以在作为对象指针的时候可能存在几个问题。后面的指针覆盖了前面的指针,导致前面的对象指针所指向的对象丢失,这种情况需要程序员来安全的使用定位new
运算符来确保不会出现覆盖;指针对象的析构函数需要手动调用,如:pt->~Time()
,同时,应该以相反的创建顺序来调用析构函数。
类设计
在设计类的时候,一般由其类型所具有的特征来设计公有的接口,然后考虑成员属性该如何实现,最后考虑公有接口的实现。
在类中可以嵌套结构或类声明,在类声明中声明的类、结构体、枚举等被称为嵌套在类中,其作用域为整个类
成员初始化列表:如果成员变量中有const变量或者引用变量,那么在构造函数代码块区域内无法进行初始化,只能使用成员初始化列表进行初始化。 Time::Time(): const_value1(value1), const_value2(value2) { ... }
这种格式只能用于初始化列表,真正被初始化的顺序与类声明中的顺序有关,与初始化器中的顺序无关
C++提供了类内初始化,可以在声明类的时候直接对其进行类内初始化,但是构造函数中的成员初始化列表会覆盖掉类内初始化
class c_name
{
int v1 = 10;
const v2 = 30;
...
}
对于懒得实现的复制构造函数或者赋值运算符重载,可以将其定义为私有的,这样就会在编译的时候找出错误。
第13章 类继承
继承
由于访问权限的原因,派生类构造函数必须使用基类构造函数来初始化基类的私有成员,使用成员初始化列表语法来完成这种工作。也可以对派生类成员使用成员初始化列表语法 derived::derived(type1 x, type2 y): base(x, y) {...}
创建派生类对象时,先调用基类构造函数,在调用派生类构造函数。派生类对象过期时,先调用派生类析构函数,再调用基类析构函数
基类指针或引用可以在不进行显式类型转换的情况下指向或引用派生类对象。但基类指针或引用只能调用基类方法。在形参为基类指针或者引用的地方也可以使用派生类对象。
多态公有继承
在派生类中重新定义基类的方法:一般只有在派生类中要修改一个方法的时候,才会进行声明
使用虚方法:经常在基类中将派生类会重新定义的方法声明为虚方法,实现虚方法时无需使用关键字virtual
在派生类方法中,使用作用域解析运算符来调用基类方法
向上强制类型转换:将派生类引用或者指针转换为基类引用或者指针,反之称之为向下强制类型转换
虚函数注意事项
构造函数不能是虚函数
基类的析构函数应为虚函数
友元等非成员函数不能是虚函数
派生类没有重新定义的虚函数将使用基类版本
派生类中重新定义的方法将隐藏掉基类的所有同名方法,与参数列表无关。如果重新定义基类的方法,应确保和原来的原型完全相同,但如果返回类型是基类指针或引用,则可以修改为指向派生类的指针或引用,这种特性被称之为返回类型协变
如果基类声明被重载了,则应该在派生类中重新定义所有的同名基类版本
纯虚函数声明的结尾处为=0
,包含纯虚函数的类不能实例化,只能用作基类
访问权限
protected
使用保护数据成员可以简化代码的编写工作,但存在设计缺陷
最好对类数据成员采用私有访问控制,同时提供基类方法使得派生类能够访问基类数据
保护访问可以使派生类访问类外不能访问的内部函数
继承与动态内存分配
派生类不使用new
时,可以不用显式定义各种函数
派生类使用了new
时,必须显式定义析构函数、复制构造函数、赋值运算符。复制构造函数可以在初始化列表中调用基类的复制构造函数,如:B::B(const B & b) : A(b) {...}
。赋值运算符可以使用作用域解析运算符,来调用基类的赋值运算符,如:B & B::operator=(const B & b) {A::operator(b); .. }
要想使用基类的友元函数,可以对派生类对象强制类型转化为基类,然后去调用基类的友元函数,如:
ostream & operator<<(ostream & os, B & b) {
os << (const A &) b << endl;
...
}
第14章 C++中的代码重用
实现has-a关系有两种方法,包含、组合或层次化;使用私有继承和保护继承。
使用一个类,需要了解构造函数和其他类方法
接口和实现:使用公有继承一般继承接口和实现,属于is-a关系;使用保护继承、私有继承或者包含时,只继承实现,不继承接口,属于has-a关系。
在类的私有部分使用typedef
来简化输入,如:typedef std::vector<int> vect;
C++和约束:使用explicit
可以防止单参数的隐式转换,使用const
可以防止修改数据
包含对象成员的类
初始化被包含的对象:使用成员初始化列表来初始化成员对象,使用成员名调用。初始化顺序取决于声明顺序,与成员初始化列表顺序无关。
// 假设A类中有个成员,是B类的对象,名为b_obj
// 假设B类有个构造函数 B(int x)
A::A(int x) : b_obj(x) {...}
使用被包含对象的接口:使用成员名调用成员的方法
访问子对象:直接使用成员名
私有继承
初始化基类组件:使用成员初始化列表来初始化基类组件,使用基类名调用。
// 假设A类私有继承B类
// B类有构造函数 B(int x)
A::A(int x) : B(x) {...}
访问基类方法:使用类名和作用域解析运算符来调用基类的方法。
访问基类对象:使用强制类型转换来访问基类对象,将自身转换为基类对象
// 假设A类私有继承B类
const B & A::get_b_obj() const
{
return (const B &) *this;
}
访问基类的友元函数:显式转换为基类来使用基类的友元函数
// 假设B类重载了 << 运算符
// A类私有继承了B类
ostream & operator<<(ostream & os, const A & a_obj) {
os << a_obj.val << endl; // 输出A类独有的数据
os << (const B &) a_obj << endl; // 输出B类的数据
}
在私有继承中,未进行显式类型转换的派生类引用或指针不能直接赋值给基类的引用或指针。
要使用基类的方法在派生类外面可用,有两种方法:
- 定义一个使用该基类方法的派生类方法
int A::func() { return B::func(); }
- 将函数调用包装在另一个函数调用中,即使用
using
声明,在A类的public部分,声明一句using B::func;
,注意:using
声明只使用成员名,不包含括号以及参数
与包含的区别:包含版本提供了显式命名的对象成员,而私有继承一般是无名称的子对象成员(可以通过多继承包含多个子对象);包含版本初始化使用对象名,私有继承一般使用基类名;包含版本调用子对象方法时使用对象名和成员运算符,私有继承调用子对象方法时使用基类名和作用域解析运算符。
使用包含还是私有继承:包含易于理解,能包含多个同类子对象;私有继承可以使用子对象的保护方法,可以重新定义只能在类中使用的虚函数;通常,应使用包含来建立has-a关系
多重继承(MI)
使用多个基类的继承被称之为多重继承,继承的时候,要限定每一个基类,不然默认是private
公有多重继承是is-a关系,保护、私有多重继承是has-a关系
多重继承时,在友元函数中容易出现无法确定转换为哪个基类的问题。
虚基类:从多个类派生出的对象,只含有一个基类 class B: virtual public A {...}
- 构造函数:使用虚基类时,构造函数不可以层层传递,除非只使用虚基类的默认构造函数,否则每一个派生类都要显式调用虚基类的构造函数
- 哪个方法:多重继承出现同名方法时,使用基类名加作用域解析运算符来确认使用哪个方法,或者重新定义该方法,然后在该方法内部确定使用哪一个方法
void C::func() { B::func(); }
void C::func() { A::func(); }
- 混合使用:当类分别通过多条虚途径和非虚途径继承某个特定的类时,该类将包含一个表示所有虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象
- 优先调用:如果某个名称优于其他所有名称,将不会产生二义性问题。规则是派生类中的名称优于直接或间接祖先类中的同名称。这种二义性规则与访问性无关,比如说某个最优的名称是private的,那么仍然会调用这个方法,尽管会调用失败。
类模板
定义类模板:
- 在类声明前加入模板声明,并且在类内使用定义的类型
- 使用模板函数成员函数替换掉原有的类方法
- 将类限定符从
ClassName::
改为ClassName<Type>::
- 如果在类声明中定义了方法(内联方法),则可以省略掉模板前缀和类限定符
- 不能将模板函数放在独立的实现文件中,最简单的方法是将所有模板信息放在一个头文件中
template <typename Type>
class ClassName
{
private:
Type a;
public:
ClassName();
void func();
ClassName & operator=(const ClassName & cn); // 注意,这里是 ClassName
}
template <typename Type>
ClassName<Type>::ClassName()
{
...
}
template <typename Type>
void ClassName<Type>::func()
{
...
}
template <typename Type>
ClassName<Type> & operator=(const ClassName<Type> & cn) // 注意,这里是 ClassName<Type>
{
...
}
使用模板类:必须显式地提供所需的类型 ClassName<int> cn;
非类型参数:模板头的参数可以是类型,也可以是非类型或表达式参数(可以为整形、枚举、引用或指针),模板代码不能修改参数的值,也不能使用参数的地址 template <typename T, int n>
,普通的构造函数中作为参数,使用new和delete管理的内存,都是在堆中,而这种是在栈中,更快,但是每个不同的n,都会生成自己的模板。
模板多功能性:
- 模板类可以用作基类、组件类、其他模板的类型参数
- 可以递归使用模板
vector< vector<int> > vec;
- 模板可以使用多个类型参数,并且还可以为类型参数提供默认值,模板函数不能为类型参数提供默认值,对于非类型参数,模板类和模板函数都可以为其提供默认值
- 模板(包括类和函数)可以作为结构、类或者模板类的成员
- 模板可以包含类型参数和非类型参数,模板还可以包含本来说是模板的参数。可以混合使用模板参数和常规参数
// 套娃
// 假设A是个模板类,类型参数名为T1 B是A的一个嵌套模板类,类型参数名为T2
// 假设 funt 是一个模板函数
template <typename T1>
class A
{
private:
class B;
public:
template <typename T3>
T3 funt();
}
template <typename T1>
template <typename T2>
class A<T1>::B
{
// B类的声明部分
public:
void func();
}
template <typename T1>
template <typename T2>
class A<T1>::B<T2>::func()
{
...
}
template <typename T1>
template <typename T3>
T3 A<T1>::funt()
{
...
}
// 模板包含一个本来就是模板的类型参数
template <template <typename T> class B>
class A
{
...
}
模板具体化:
- 隐式实例化:声明一个对象的时候,编译器自动生成一个具体的类
ClassName<int> cn;
- 显式实例化:虽然可能还没有对象,但是会生成一个具体的类
template class ClassName<int>;
- 显式具体化:一般需要针对某个特殊类型进行修改
template <> class ClassName<int> { ... }
- 部分具体化:部分具体化可以给类型参数之一指定具体的类型
template <typename T2> class ClassName<int, T2> { ... }
关键字template后的尖括号内声明的是没有被初始化的类型参数。 - 指针版:可以为指针提供特殊版本,如果提供类型是指针,会自动使用指针版的模板
template <typename T> class A { ... }
template <typename T *> class A { ... }
模板类和友元:
- 模板类和非模板友元函数:必须要为要使用友元具体化
template <typename T> class ClassName { public: friend void show(ClassName<T> & cn); // 友元函数 } void show(ClassName<int> & cn); void show(ClassName<double> & cn);
- 模板类和约束模板友元函数:使友元函数本身成为模板。第一步:在类定义的前面声明每一个模板函数;第二步:在函数中将模板函数声明为友元;第三步:为友元提供模板定义
template <typename T> void show(T &); // 声明模板函数 template <typename T> class ClassName { public: friend void show<>(ClassName<T> & cn); // 将模板函数声明为友元 } template <typename T> void show(T & t) // 为友元提供模板定义 { ... }
- 模板类和非约束模板友元函数:通过在类内部声明模板,可以创建非约束模板友元函数
template <typename T> class ClassName { public: template <typename TT> friend void show(TT &); } template <typename TT> void show(TT & t) { ... }
模板别名:可以使用typedef为模板指定别名 typedef vector<int> veci; veci temp;
,也可以使用using using veci = vector<int>;
如果是部分类型参数的话可以这样 template <typename T> using name = ClassName<T, int>;
,using这种也可以用于非模板,而且可读性比 typedef 更强
第15章 友元、异常和其他
友元
友元有友元函数、友元类、友元成员函数。
友元类的所有方法都可以访问原始类的私有成员和保护成员,但是哪些函数、成员函数或者类成为友元都是由类定义的,而不能从外部强加友情,所以这并不与面向对象的编程思想相悖。
友元类:友元声明位置不重要,一般是先定义原始类,后定义友元类。
class A {
public:
friend class B;
...
}
class B {
...
}
友元成员函数:仅让特定的类成员成为另一个类的友元,必须小心排列各种声明和定义的顺序。前向声明可以解决一部分问题。
class A; // 原始类的前向声明
class B { // 友元成员函数在这个类中
public:
void func(A &); // 因为可能使用到A类的方法,一般都会延迟实现
};
class A {
public:
friend void B::func(A &);
};
inline void B::func(A &) { // 如果需要成为内联函数,可以使用inline关键字
...
}
互为友元类:这种情况下,第一个声明的类不能包含实现,因为它里面可能需要用到第二个类的信息。
嵌套类
包含类的成员可以创建和使用嵌套类,嵌套类的访问权限控制等同于普通类,当前类只能访问嵌套类的public部分。
嵌套类声明在public部分,所有部分都能使用,但是必须使用作用域解析运算符。声明在protected部分,当前类和它的派生类可以使用。声明在private部分,仅当前类可以使用
class A {
public:
class B {
...
};
};
A::B x;
类声明的位置决定了类的作用域或者可见性。类可见后,访问控制规则(公有、保护、私有、友元)将决定程序对嵌套成员的访问权限。
模板中的嵌套类使用和普通情况下的嵌套类差不多,因为最后都是由编译器生成的。
template <typename T>
class A {
private:
class B {
public:
T val;
};
};
异常
abort()
:向标准错误流发送消息 abnormal program termination(程序异常终止),然后终止程序
错误码:一般使用函数返回值判断函数是否执行成功,这样的话,处理结果就需要使用参数来传递回去。
异常:是指对程序运行过程中发生的异常情况的一种响应。throw
关键字表示引发异常,紧随其后的值指出了异常的特征。try
块标识其中特定的异常可能被激活的代码块,它后面跟一个或多个catch
块。
int func(int a, b); // 函数原型
try {
z = func(a, b);
}
catch (const char * s) {
cout << s << endl;
}
int func(int a, b) {
if (a == -b) {
throw "bad argument: a = -b not allowd";
}
return 2.0 * a * b / (a + b);
}
将对象作为异常:优点是能够携带更多的信息
try {
...
}
catch (A & a) {
}
catch (B & b) {
}
栈解退:是指出现异常的时候,程序释放栈的空间,直到遇到第一个try catch块。
异常规范:double harm(double) throw(bad_thing);
如同这种,后面明确指出会抛出何种异常,最好不要使用该功能。但可以使用noexcept
指出函数不会引发异常,如void show() noexcept;
异常与返回:有两个主要区别,一个是返回到的位置不同,异常会一直返回到第一个try catch块,而普通的只返回到调用位置就好了;第二个区别是,异常会产生一个拷贝,而返回一般返回值是引用或者其他的话,会返回引用或者其他,因为异常会释放栈空间,所以引用一般会无效,才会使用拷贝。
异常引用参数:因为基类引用可以引用派生类对象,所以使用引用作为形参。
如果有一个异常类继承层次结构,应这样排列catch块,将捕获位于层次结构最下面的异常类的catch语句放在最前面,将捕获基类异常的catch语句放在最后面,使用省略号将会捕获所有的异常
exception类:可作为所有异常的基类,有一个what()
的虚方法,返回一个字符串
可以在异常类中使用嵌套异常类来组合异常,嵌套类也可以被继承,还可用作基类
class A {
public:
class B: public exception {
public:
const char * what();
}
}
const char * A::B::what() {
return "bad argument!\n";
}
未捕获异常:异常没有匹配到catch,会调用函数terminate(),而该函数默认调用abort()函数,可以修改默认调用的函数 set_terminate(my_func);
意外异常:只没有出现在异常规范中,但是却被抛出了。这种情况下会调用unexpected()函数,这个函数将调用terminate()函数,可以修改 set_unexpected(my_func);
异常规范不适用于模板,异常在与动态内存分配同时使用时,需要小心处理内存有关内容。
RTTI(运行阶段类型识别)
dynamic_cast
是否可以安全的将对象的地址赋值给特定类型的指针 B * pb = dynamic_cast<B *> pa;
,如果可以转换,将返回目标类型,如果无法转换,则返回空指针。如果将其用于引用的话,失败时将引发bad_cast异常
typeid
确认两个对象是否为同种类型,类似于sizeof,属于运算符 typeid(B) == typeid(b_obj);
type_info
是type_id
返回的结果的类型
dynamic_cast <type-name> (expression)
在类层次中进行向上转换
const_cast <type-name> (expression)
可以改变值为const或volatile A a; const A * paa = &a; A *pa = const_cast<A *>(paa); *pa = a_obj;
static_cast <type-name> (expression)
可以随意改变具有继承关系的类,如A->B,B->A
reinterpret_cast <type-name> (expression)
。。。
第16章 string类和标准模板库
string类
string
实际上是模板具体化basic_string<char>
的一个typedef。
string类将string::nops
定义为字符串的最大长度,通常为unsigned int
的最大值
string类重载了+
+=
=
[]
>=
>
<=
<
==
!=
等运算符
构造函数:
string(const char *s)
string(size_type n, char c)
包含n个元素的string对象,其中每个字符都被初始化为cstring(const string & str)
string()
string(const char * s, size_type n)
将string对象初始化为s指向的C-风格字符串的前n个字符,即使超过了C风格字符串长度template <class Iter> string(Iter begin, Iter end)
将string对象初始化为[begin, end)之间的字符string(const string & str, string size_type pos = 0, size_type n = npos)
初始化为从str的pos位置开始的n个字符string(string && str) noexcept
C++11的string(initializer_list<char> il)
初始化列表
string类输入
cin >> str;
读取一个单词,遇到的空格仍然留在输入流中getline(cin, str);
读取一行,有一个可选参数,用于指定使用哪个字符来确定输入的边界getline(cin, str, ':')
遇到冒号停止读取
string类方法:
size()
和length()
返回字符串中的字符数find()
方法:搜索给定的字符串或字符size_type find(const string & str, size_type pos = 0) const
从位置pos开始,查找子字符串,返回首次出现的时其首字符的索引,未找到返回string::nops
size_type find(const char * s, size_type pos = 0) const
size_type find(const char * s, size_type pos = 0, size_type n)
从位置pos开始,查找s的前n个字符组成的字符串出现的位置size_type find(char ch, size_type pos = 0) const
从位置pos开始,查找字符ch
- 查找相关的其他方法,重载函数特征标都与
find()
相同,rfind()
查找最后一次出现的位置,find_first_of()
查找参数中任何一个字符首次出现的位置find_last_of
查找参数中任何一个字符最后一次出现的位置find_first_not_of()
查找第一个不包含在参数中的字符find_last_not_of()
查找最后一个不包含在参数中的字符 capacity()
显示实际分配给字符串的空间大小,字符串长度一般都少于这个数值reserve(n)
手动为某个字符串分配至少n的空间,实际分配空间一定大于这个值c_str()
返回string对象的C-风格字符串
字符串种类
typedef basic_string<char> string;
typedef basic_string<wchar_t> wstring;
typedef basic_string<char16_t> u16string;
typedef basic_string<char32_t> u32string;
智能指针模板类
智能指针对象:拥有类似于指针的功能,但是在结束的时候自己会释放空间,所有的智能指针都只能显式声明,不能将普通指针隐式转换为智能指针(explicit修饰其构造函数)。所有的指针都不能指向非堆的内存区域(只能指向new分配出来的区域)
头文件:memory
名称空间:std
auto_ptr
auto_ptr<string> ps(new string);
C++98提供的方案,现在不建议使用。所有权转让之后原有的智能指针调用会导致程序崩溃。
unique_ptr
unique_ptr<double> pdu(new double);
建议使用,也是所有权的概念。除了临时右值,其余的智能指针都不能进行所有权转让。pdu = unique_ptr<double>(new double(15));
可以使用 move()
方法来安全赋值 pdu1 = move(pdu2);// pdu1未初始化 pdu2有效
shared_ptr
shared_ptr<string> pss(new string);
跟踪智能指针的数量,最后一个指针才会释放空间。
使用new
时可以使用任意一种智能指针,使用new []
时只能使用unique_ptr
需要多个指针指向同一个对象时,建议使用shared_ptr
,其余情况下使用unique_ptr
标准模板库
STL提供了一组表示容器、迭代器、函数对象和算法的模板
泛型编程:STL通过为每个类定义适当的迭代器,并以统一的风格设计类,能够对内部使用决然不同的容器,编写相同的代码
基于范围的for循环:for(double x: price) cout << x << endl;
基于范围的for循环可以选择将参数定义为引用对原始数据进行修改
vector:
push_back()
将元素添加到矢量末尾erase()
删除矢量中给定区间中的元素,接受两个迭代器参数。insert()
接受三个迭代器参数,第一个参数指定了插入元素的位置,第二个和第三个参数定义了被插入区间,通常是另一个容器的一部分。
STL函数:
for_each()
接受三个参数,第一个和第二个参数定义了区间,第三个参数是一个函数符,表示对每个元素执行的操作,被执行的函数不能对元素进行修改for_each(books.begin(), books.end(), show);
random_shuffle()
接受两个迭代器参数,指定区间,并随机排列区间中的元素random_shuffle(books.begin(), books.end());
sort()
接受三个参数,第一个和第二个迭代器参数定义了区间,第三个默认是内置的<运算符操作,可以使用自己定义的比较函数sort(vec.begin(), vec.end(), mysort);
copy()
接受三个参数,第一个和第二个迭代器参数表示要复制的范围,第三个迭代器参数表示要复制到的位置copy(arr, arr+10, vec.begin());
transform()
两个版本。第一个版本接受4个参数,前两个参数是指定容器区间的迭代器,第三个参数是指定将结果复制到哪里的迭代器,第四个参数是函数符,只有一个参数,它被应用于区间中的元素,生成结果中的新元素。第二个版本接受5个参数,前两个参数是指定容器区间的迭代器,第三个参数是第二个区间的起始位置,第四个函数是结果复制目标的迭代器,第五个参数是函数符,这里的函数符是双参数的transform(vec.begin(), vec.end(), out.begin(), sqrt);
tranform(v1.begin(), v1.end(), v2.begin(), v3.begin(), plus<int>());
next_permutation()
将区间内容转换为下一种排列方式,两个迭代器参数,如果处于最后序列的话,会返回false
迭代器
迭代器:是一个广义指针,可以对它进行解引用和递增,主要是为了提供一个统一的接口 vector<int>::iterator it;
自动类型判断在这里很有用 auto it = vec.begin();
++
运算符的重载,前缀无参数,后缀有个int标记 int operator++(); //前缀
int operator++(int) // 后缀
迭代器概念 (应具有的特征(简短)):
- 能够对迭代器进行解引用操作,以便访问它引用的值
- 能够将迭代器赋给另一个
- 迭代器应该能比较是否相等
- 应该能够使用迭代器遍历容器中的所有元素
迭代器分类
- 输入迭代器
InputIterator
可以被程序用来读取容器中的信息,能够单向访问容器中的所有值 - 输出迭代器
OutputIterator
被程序用来修改容器中的信息,单通行 - 正向迭代器
ForwardIterator
单方向,但是保证按照相同的顺序遍历一系列值,既能读取数据,也能修改数据 - 双向迭代器
BidirectionalIterator
具有正向迭代器的所有特征,同时支持递减运算符 - 随机访问迭代器
RandomAccessIterator
具有双向迭代器的所有特征,同时添加了支持随机访问的操作和用于对关系进行排序的关系运算符
预定义迭代器
ostream_iterator<int, char> out_iter(cout, " ");
第一个模板参数表示被发送给输出流的数据类型,第二个模板参数表示输出流使用的字符类型,构造函数第一个参数表示要使用的输出流,第二个字符串参数是在发送给输出流的每个数据项之后显示的分隔符。copy(vec.begin(), vec.end(), out_iter);
istream_iterator<int, char> in_iter(cin);
第一个模板参数指出要读取的数据类型,第二个模板参数指出输入流使用的字符类型,构造函数参数意味着使用由cin管理的输入流,省略构造函数则表示输入失败copy(istream_iterator<int, char>(cin), istream_iterator<int, char>(), vec.begin());
- 三种可以自动内存分配的插入迭代器:模板参数表示容器类型,构造函数类型表示实际的变量名
back_insert_iterator<vector<int>> back_iter(vec);
将元素插入到容器尾部front_insert_iterator<vector<int>> front_iter(vec);
将元素插入到容器的前端insert_iterator<vector<int>> insert_iter(vec, vec.begin());
将元素插入到第二个构造函数参数对应的元素之前
容器
容器概念指定了所有STL容器类必须满足的一系列要求,容器类型是具体的模板
一些基本的容器特征(参见P695)
STL容器的基本方法:
size()
返回容器中元素数目swap()
交换两个容器的内容begin()
返回一个指向容器中第一个元素的迭代器end()
返回一个表示超过容器尾的迭代器
容器种类:
- 序列:序列的要求(序列都有的成员函数)(见P697),序列的可选要求(部分有共同点的容器具有的成员函数)(见P697)
vector
数组的一种类表示,提供了自动内存管理;可以使用at()
来检查是否越界,可以将一个变量直接赋值给另一个变量。在尾部插入和删除的时间是固定的。deque
双端队列,支持随机访问,在开始位置插入和删除的时间是固定的list
双向链表,不支持随机访问和数组表示法,有自己比较独特的一些成员函数(见P699)forward_list
单向链表,不可反转queue
队列是一个适配器类,底层类默认是deque
,只包含队列的操作,不能从队首插入,队尾删除这种priority_queue
优先级队列是一个适配器类,底层类默认是vector
,只包含优先级队列的操作stack
栈是一个适配器类,底层类默认是vector
,只提供了栈接口array
大小固定的数组,可以将很多stl算法用于该对象
- 关联容器:将键和值关联在一起,并使用键来查找值
set
multiset
map
multimap
X::value_type
指出了容器的值类型X::key_type
指出了容器的键类型set<string> A;
第一个模板参数是存储的数据类型,第二个模板参数是可选的,是用于对键排序的函数符set_union(A.begin(), A.end(), B.begin(), B.end(), ostream_iterator<string, char>(cout, " ");
计算两个集合的并集,五个参数,第一个、第二个参数指定第一个集合的区间,第三个、第四个参数指定第二个集合的区间,第五个参数指定将结果集合复制到什么位置set_intersection()
set_difference()
计算两个集合的交集,差集,参数同上lower_bound()
将键作为参数返回第一个不小于键参数的成员(大于等于)upper_bound()
将键作为参数返回第一个大于键参数的成员 通常用来确定一个区间multimap<int, string> codes;
map使用pair<int, string>
的对象作为自己的成员,可以使用codes[5].first
和codes[5].second
来分别访问键和值count()
成员函数接受键作为参数,返回具有该键的元素数量lower_bound()
和upper_bound()
也可以使用equal_range()
成员函数接受键作为参数,返回两个迭代器,表示的区间与该键匹配。返回的对象是两个迭代器的pair对象pair<multimap<int, string>::iterator, multimap<int, string>::iterator> range = codes.equal_range(5);
- 无序关联容器:也是键值关系,不过是基于哈希表的
unordered_set
unordered_multiset
unordered_map
unordered_multimap
函数对象
函数对象(也叫函数符)是可以以函数方式与()
结合使用的任意对象。包括函数名、函数指针、重载了()运算符的类对象
函数符概念:
- 生成器是不用参数就可以调用的函数符
- 一元函数是用一个参数就能调用的函数符
- 二元函数是用两个参数就能调用的函数符
- 返回bool值的一元函数是谓词
- 返回bool值的二元函数是二元谓词
两个参数的函数,可以修改为一个参数的函数
// 两个参数 定义
bool tooBig(int n, int v) { return n > v; }
// 使用
if (tooBig(a, b)) {
cout << "a > b" << endl;
}
// 一个参数
class tooBig {
private:
int v;
public:
tooBig(const int & vv) : v(vv) {}
bool operator()(const int & n) { return n > v; }
};
// 使用
if (tooBig(b)(a)) { // b是传递给构造函数的,a是传递给匿名对象的
cout << "a > b" << endl;
}
预定义的函数符:(参见P711)
plus
相加plus<int> add;
minus
multiplies
divides
自适应函数符和函数适配器:
- 自适应生成器、自适应一元函数、自适应二元函数、自适应谓词、自适应二元谓词
- 使函数成为自适应的原因是,携带了表示参数类型和返回类型的typedef成员,如
plus<int>::result_type;
是int的typedef,还有first_argument_type
second_argument_type
- 函数符自适应的意义在于,函数适配器对象可以使用函数对象,并认为存在这些typedef成员
binder1st
binder2nd
这两个类可以将自适应二元函数转换为自适应一元函数binder1st(f2, val) f1;
之后f2(val, x)
等价于f1(x)
,第一个参数直接绑定为了val。可以使用bind1st()
方法来简化使用,如transform(v1.begin(), v1.end(), v2.begin(), bind1st(multiplies<double>(), 2.5));
可以完成乘2.5的操作,binder2nd
与其类似,不过绑定的是第二个参数
算法
对于算法函数设置,有两个主要的通用部分,首先都是用模板来提供泛型,其次,都使用迭代器来提供访问容器中数据的通用形式
STL将算法库分为四种:
- 非修改式序列操作:不修改容器的内容,如
find()
for_each()
等 - 修改式序列操作:可以修改容器的内容,如
transform()
random_shuffle()
copy()
等 - 排序和相关操作:如
sort()
等 - 通用数字运算:如计算累计,内部乘积各种
按照结果位置分类:
- 分为就地算法(将结果保存到原始数据的位置)和复制算法(将结果发送到另一位置)
- 有些算法有两个版本,复制版本一般以_copy结尾,如
replace_copy(...)
- 还有一种,根据将函数应用于容器元素得到的结果来执行操作,通常以_if结尾,可以理解为满足某种情况下执行操作
replace_if(...)
其他库
vector
valarray
array
的区别:三者都是数组,vector
是一个容器类和算法系统的一部分;valarray
是面向数值计算的;array
是为了替代内置数组设计的
slice
类对象可以作为数组索引,初始化三个整数分别为:起始索引、索引数和跨距 varint[slice(1,4,3)] = 10; // 1,4,7,10下标的元素都设置为10
初始化列表:模板initializer_list
,这个模板类包含成员函数begin()
end()
size()
初始化列表对象中单元素是不可修改的
double sum(const initializer_list<double> & il) {
double total = 0;
for (auto it = il.begin(); it != il.end(); it++) {
total += *it;
}
return total;
}
第17章 输入输出和文件
基础概念
C++没有将输入输出建立在语言中,而只是作为组件来使用
C++将输入输出看做字节流,流相当于一个桥梁,连接流源和流目标
通常使用缓冲区来高效处理输入输出
在程序中包含iostream
将自动创建8个对象,cin
cout
cerr
(没有缓冲) clog
(有缓冲) wcin
wcout
wcerr
wclog
输入输出可以重定向 program <in.txt >out.txt
命令行参数:int main(int argc, char * argv[])
argc
是命令行中参数的个数,包含命令本身 argv[0]
就是命令名
使用cout输出
<<
运算符经过重载,用于处理各种基本类型 (unsigned char
signed char
char
short
unsigned short
int
unsigned int
long
unsigned long
long long
unsigned long long
float
double
long double
)
<<
运算符在遇到char *
指针,将显示字符串,其他类型的指针,将视作 void *
,并打印地址
<<
运算符可以拼接输出,因为其返回值是ostream &
put()
方法,用于显示字符,可以拼接 cin.put('w').put('a');
write()
方法,可以显示字符串,第一个参数是要显示的字符串,第二个参数指出需要显示多少个字符串,遇到空字符也不会停止,直至打印够了指定数量的字符 cout.write(str, strlen(str));
flush
和endl
,都可以用来刷新缓冲区,后者多一个换行 cout << flush;
flush(cout);
格式化:
- 默认浮点类型被显示为6位
- 计数系统(进制),
dec
hex
oct
既是成员函数,也是控制符hex(cout);
cout << hex;
- 调整字段宽度,只影响下一项,
width()
成员函数,无参数时返回当前字段的宽度设置,传入参数时将宽度设置为x,并返回以前的宽度,如果设置宽度小于实际应显示宽度,将会自动增宽cout.width(5);
- 填充字符,
fill()
成员函数cout.fill('*');
- 浮点数精度:
precision()
成员函数 默认情况下,指的是显示的有效位数。cout.precision(8);
- 末尾的0和小数点:使用
setf()
,传入参数ios_base::showpoint
cout.setf(ios_base::showpoint);
setf()
方法:fmtflags setf(fmtflags);
参数见表格fmtflags setf(fmtflags, fmtflags);
参数见表格 第二参数清除一批相关的位,然后第一参数将其中一位设置为1,在定点和科学表示法中,精度指的是小数位数,并且都显示末尾的0
unsetf()
方法可以消除setf()
的效果- 控制符实现与
setf()
与unsetf()
同样的功能,见表格 iomanip
头文件中包含控制符setprecision()
精度setfill()
填充setw()
宽度
常量 | 含义 |
---|---|
ios_base::boolalpha | 输出bool值为true或false |
ios_base::showbase | 显示基数前缀,如十六进制的0x |
ios_base::showpoint | 显示小数点 |
ios_base::uppercase | 16进制输出使用大写字母 |
ios_base::showpos | 正数前显示+ |
第二个参数 | 第一个参数 | 含义 |
---|---|---|
ios_base::basefield | ios_base::dec | 使用基数10 |
ios_base::basefield | ios_base::oct | 使用基数8 |
ios_base::basefield | ios_base::hex | 使用基数16 |
ios_base::floatfield | ios_base::fixed | 使用定点计数法 |
ios_base::floatfield | ios_base::scientific | 使用科学计数法 |
ios_base::adjustfield | ios_base::left | 使用左对齐 |
ios_base::adjustfield | ios_base::right | 使用右对齐 |
ios_base::adjustfield | ios_base::internal | 符号或者基数左对齐,值右对齐 |
控制符 | 调用 |
---|---|
boolalpha | setf(ios_base::boolalpha) |
noboolalpha | unsetf(ios_base::boolalpha) |
showbase | setf(ios_base::showbase) |
noshowbase | unsetf(ios_base::showbase) |
showpoint | setf(ios_base::showpoint) |
noshowpoint | unsetf(ios_base::showpoint) |
showpos | setf(ios_base::showpos) |
noshowpos | unsetf(ios_base::showpos) |
uppercase | setf(ios_base::uppercase) |
nouppercase | unsetf(ios_base::uppercase) |
internal | setf(ios_base::internal, ios_base::adjustfield) |
left | setf(ios_base::left, ios_base::adjustfield) |
right | setf(ios_base::right, ios_base::adjustfield) |
dec | setf(ios_base::dec, ios_base::base-field) |
hex | setf(ios_base::hex, ios_base::base-field) |
oct | setf(ios_base::oct, ios_base::base-field) |
fixed | setf(ios_base::fixed, ios_base::floatfield) |
scientific | setf(ios_base::scientific, ios_base::floatfield) |
使用cin输入
>>
运算符经过重载,能够识别基本类型。读取数字时,可以用hex
oct
dec
控制符一起使用,来指定将输入解释为十六进制、八进制、十进制 cin >> hex >> x;
>>
运算符会返回调用对象的引用,因此可以拼接起来
>>
运算符会跳过空白(空格、换行符和制表符)
>>
运算符在遇到不满足要求的输入时,将导致cin >> input
的结果为false
流状态
eofbit
如果到达文件尾,则设置为1badbit
如果流被破坏,则设置为1,如文件读取错误failbit
如果输入操作未能读取预期的字符或输出操作没有写入预期的字符,则设置为1clear()
用于重置状态、将状态设置为它的参数,将清除其他的状态位clear()
clear(eofbit)
setstate()
只影响参数中设置的位exception()
可以决定哪些状态会引发异常cin.exception(badbit | eofbit);
引发的异常为ios_base::failure
,该异常派生自std::exception
包含一个what()
方法- 流状态异常时,将关闭输入与输出,直到状态良好,一般在之后会处理掉不合理的输入
get(char &)
单字符输入,返回调用对象的引用,可以拼接cin.get(c1).get(c2);
get(void)
单字符输入,返回字符对应的int值c1 = cin.get();
get(char *, int, char)
行输入,读取指定数量的字符或遇到分隔符(第三个参数),默认分隔符是\n
,并且会将分隔符留在输入流中cin.get(str, limit);
getline(char *, int, char)
行输入,不将分隔符留在输入流中cin.getline(str, limit);
ignore(int, char)
读取并丢弃指定数量的字符或遇到分隔符ignore(255, '\n');
如果没有读取任何字符get(char *, int)
会设置failbit
如果没有读取任何字符(换行被视为读取了一个字符)或读取了最大数目的字符,但行中还有值getline(char *, int)
会设置failbit
read()
函数读取指定数目的字节,并存储,不会在末尾加上空值字符,可以拼接cin.read(input, 144);
peek()
函数,返回输入的下一个字符,但不抽取输入流中的字符ch = cin.peek()
gcount()
方法返回最后一个非格式化抽取方法读取的字符数
putback()
方法将一个字符插入到输入流中的最前面cin.putback(ch);
文件输入与输出
ofstream
与ifstream
对象,对应头文件是fstream
,可以使用iostream
的相关方法,输入输出流对象过期时,将自动关闭文件连接或显式使用close()
关闭 ofstream fout("a.txt");
fout.close()
is_open()
方法,检查文件是否被打开
多个文件可以创建多个对象或者使用一个对象依次打开关闭
文件模式
- 文件模式常量见表格
ifstream
以ios_base::in
作为默认模式ofstream
以ios_base::out
作为默认模式fstream
不提供默认模式write()
成员函数以二进制存储数据fout.write((char *) p1, sizeof p1);
read()
成员函数可以从文件读取二进制信息fout.read((char *) p1, sizeof p1);
常量 | 含义 |
---|---|
ios_base::in | 打开文件,以便读取 |
ios_base::out | 打开文件,以便写入 |
ios_base::ate | 打开文件,并移到文件尾 |
ios_base::app | 追加到文件尾 |
ios_base::trunc | 如果文件存在,则截短文件 |
ios_base::binary | 二进制文件 |
随机存取
seekg()
将输入指针移动到指定位置,seekp()
将输出指针移动到指定位置,tellg()
与tellp()
返回当前位置seekg(streamoff, ios_base::seekdir);
streamoff
是偏移字节seekdir
有ios_base::beg
(相对文件开始)ios_base::cur
(相对当前位置)ios_base::end
(相对文件尾)fin.seekg(-1, ios_base::cur);
seekg(streampos)
streampos
表示绝对位置(从文件开始算起)
内核格式化
读取string对象中的格式化信息或将格式化信息写入string对象中被称为内核格式化
ostringstream
类,有str()
成员函数,返回一个被初始化为缓冲区内容的字符串对象
istringstream
类,可以用string对象进行初始化 istringstream instr(str);
总之,istringstream
和istringstream
类使得能够使用istream
和ostream
类的方法来管理存储在字符串中的字符数据
ostringstream outstr;
outstr << setw(15) << x;
string str = outstr.str();
第18章 探讨C++新标准
回顾之前的C++11新功能
新类型:long long
unsigned long long
char16_t
char32_t
原始字符串R"C:\Program Files"
统一的初始化:扩大了初始化列表的适用范围,使用时,等号是可选的 int x {5}
初始化列表语法也能使用在new
中,int *pi = new int[3] {1, 2, 3};
初始化列表语法也能用在创建对象中,如果有初始化列表构造函数,则使用初始化列表构造函数,如果没有初始化列表构造函数,将会使用参数类型匹配的普通带参的构造函数,关于初始化列表构造函数,可以参考第16章的initializer_list
声明:
auto
自动类型判断auto pf = &str;
decltype
将变量类型声明为表达式的类型decltype(3.15 + 2) result;
- 返回类型后置 一种函数声明语法,适用于不知道返回什么类型的情况(如一个模板函数,不清楚传入时哪个参数类型更大,可以使用这种方法)
auto func(int a, double b) -> double;
using=
模板别名,对于比较长的标识符,可以使用这种方式创建别名using vec_int_it = std::vector<int>::iterator;
这种方法相较于tyepdef
可以用于模板部分具体化template<typename T> using arr12 = std::array<T, 12>;
使用时可以arr12<double> a1;
nullptr
空指针
智能指针:auto_ptr
(不建议使用) unique_ptr
所有权转让 shared_ptr
记录指针数量 weak_ptr
不熟悉
异常规范:放弃了可能引发哪些异常的语法,但是保留了可以强调某个函数不会引发异常的语法 noexcept
作用域内枚举:enum class N1 {never, sometimes, often, always};
enum struct N2 {never, lever, sever};
N1::never
N2::never
对类的修改:加入了explicit
关键字,用于禁止一些隐式的自动转换。可以在类定义中初始化成员,可以使用等号或大括号,但不能使用圆括号版的初始化
class A {
private:
int val1 = 10;
double val2 {3.14};
char val3;
public:
A() {}
A(char ch): val3(ch) {}
A(int a, double b, char c): val1(a), val2(b), val3(c) {}
}
// 类内初始化类似于下面的结果
A::A(): val1(10), val2(3.14) {}
A::A(char ch): val1(10), val2(3.14), val3(ch) {}
模板与STL方面:
- 基于范围的for循环:
for (auto & x: vec) { ... }
- 新的STL容器:
forward_list
单向链表unordered_map
哈希实现的mapunordered_multimap
哈希实现的multimapunordered_set
哈希实现的setunordered_multiset
哈希实现的multiset 模板array
- 新的STL方法:
cbegin()
cend()
const迭代器crbegin()
crend()
const反向迭代器 valarray
- 摈弃了
export
- 尖括号,不用再担心与
>>
的混淆了 如:vector<vector<int>> vec;
右值引用:不能对其应用地址运算符的值,右值引用使用&&
,可以用来给某个临时右值续命 博客园相关文章:从4行代码看右值引用-qicosmos
移动语义和右值引用
移动语义:实际的文件位置未发生改变,只是更改了访问它的方式。如计算机中,可以很容易的将一个文件复制到另一个文件夹中,只是更改了位置标记,真正的存储位置一般不会变化。
常规复制构造函数:使用const左值引用作为参数
移动构造函数:使用右值引用作为参数,常见于将地址所有权转让出去,这种常被称之为窃取。
A::A(A && a) { // 常见的就是,将地址所有权转让出去
pi = a.pi;
a.pi = nullptr;
}
右值引用:右值引用可支持移动语义。首先,右值引用让编译器知道何时可以使用移动语义;第二步,编写移动构造函数,使其提供所需的行为
移动赋值运算符:也是将源对象的所有权装让给目标
强制移动:如果对于一些不是很重要的数据,想要强制移动,有两种方案。第一种,使用运算符static_cast<>
将对象的类型强制转换为 className &&
;第二种,使用头文件utility
的函数std::move()
整个表达式是一个右值,并不是说move语句负责实现 obj2 = std::move(obj1);
新的类功能
自动提供的函数有:默认构造函数、复制构造函数、复制赋值运算符、析构函数、移动构造函数、移动赋值运算符
默认方法的使用和禁用:如果想要使用默认版本,使用关键字default
显式声明(适合于编写了某个带参的构造方法,但是又不想写无参版本),在声明中可以这样写 className() = default;
。如果想要禁用某种自动生成的方法,可以使用关键字delee
禁止(适合于禁止对象之间的复制,但是编译器默认提供了复制赋值方法),在声明中这样写 className & operator=(const className &) = delete;
,这种也可以将声明放在private部分实现。default
只能用于6个特殊函数,delete
可以用于任何函数(重要一点:它只用来查找匹配函数,使用他们将会导致编程错误,如某个函数参数是double,int会被自动转换,你可以设计一个int并delete,这样就能禁止该函数使用int参数)
委托构造函数:在一个构造函数中使用另一个构造函数 A::A(int a, int b) {...}
A::A(int a): A(a, 15) {...}
继承构造函数:使用using,可用于普通函数,也可用于构造函数
- 普通函数:由于继承时,如果在派生类中重写了某个方法,则会覆盖掉基类的所有方法,可以使用using来继续使用基类的所有方法
class B: public A { public: using A::func; // 这样即使下面重写了基类的方法,基类的其他参数的方法也能正常使用 func(int) { ... }; // 只有参数也为int的时候,才会优先使用派生类的方法 }
- 构造函数:和普通函数同样的使用方法,不过主要在于基类的构造函数,只会初始化基类的成员,派生类的成员还应该自己初始化。
管理虚方法:为了避免使用虚方法时出现形参写错,而导致隐藏了基类虚方法的情况,引入了override
虚说明符,如果声明与基类不同,将会产生编译错误。 virtual void f(char * ch) const override {...}
。在某些情况下,不允许继承类再去覆写虚方法,可以使用final
虚说明符说明禁止进一步覆写 virtual void f(char * ch) const final {...}
这两个并不是关键字,平时可以用作变量名
lambda函数
lambda函数可以当做一个函数对象来使用,差异有两点,匿名和自动推断返回类型(完全由一条返回语句组成时有效)。
count = count_if(vec.begin(), vec.end(), [](int x){return x % 3 == 0;});
如果多条语句,lambda函数可以使用返回类型后置 [](double x)->double {int y = x; return x - y;}
lambad可访问域内的任何动态变量。要使用变量,可以将其放在中括号中。[z],按值访问变量,[&z],按照引用访问变量,[&],按照引用访问所有动态变量,[=],按值访问所有变量 [&, x],按值访问x,其他的都是引用访问。
在C++中引入lambda的主要目的是,让您能够将类似于函数的表达式用作接收函数指针或函数符的函数的参数
包装器
包装器也被称为适配器,如模板bind
可以绑定函数的某个参数,模板mem_fn
可以将成员函数作为常规函数进行传递,模板reference_wrapper
可以创建行为像引用但是可以被复制的对象,而包装器function
可以以统一的方式处理多种类似于函数的形式
调用特征标是有返回类型以及用括号括起来的参数类型列表定义的,如double(double)
function
可用于包装调用特征标相同的函数指针、函数对象或lambda表达式 std::function<double(char, int)> fdci;
function<double(double)> dd1 = func1; // 函数指针
function<double(double)> dd2 = fq(10); // 函数对象
function<double(double)> dd3 = [](double u) {return u * u}; // lambda函数
// 或者直接在使用的时候使用临时对象
typedef function<double(double)> fdd;
use_f(y, fdd(func1));
可变参数模板
需要理解几个要点:模板参数包,函数参数包,展开参数包,递归
template <typename ... Args> // Args 是一个模板参数包
void show_list(Args ... args) // args 是一个函数参数包
{
show_list(args...); // args... 是展开参数包,给函数参数包后面加上省略号
}
核心理念是:将函数参数包展开,对第一项进行处理,然后将余下的传递给递归调用,以此类推,直到列表为空
template <typename T, typename ... Args>
void show_list(const T & value, const Args & ... args)
{
cout << value << ", ";
show_list(args ...);
}
template <typename T>
void show_list(const T & value) {
cout << value << endl;
}
新增的其他功能
这里只列出了概念名称,没有看懂
并行编程:关键字thread_local
头文件thread
mutex
condition_variable
future
新增的库:头文件random
chrono
tuple
ratio
regex
低级编程:说明符 alignas
,运算符 alignof()
, constexpr机制
杂项:stdint.h
cstdint
assert
语言变化
主要描述了语言的发展和成长,只记录关键字
标准委员会 ANSI ISO/ANSI ISO/IEC JTC1/SC22/WG21
C++编程社区
Boost项目
TR1
接下来的任务
可以阅读一些介绍高级主题和面向对象编程的书
学习特定的类库,如windows类库,apple编程的类库等