Greenwicher's WikiAccelerated C++ 2016-04-19
【更新日期:
】第0章 开始学习C++
Hello, World!
//一个小的C++程序 |
Hello, World!
注释
//
:注释从//
之后到行尾的所有内容/*...*/
: 注释/*
和*/
之间的内容
#include 指令
- 标准库不属于C++语言核心,因此使用时需加载
iostream
为标准头文件,表示对顺序或流输入/输出的支持,不过它不支持随机存储或图形输入/输出
主函数main
- 每一个C++程序都必须包含一个名为main的函数,当我们请求C++实现运行程序时,它就会调用这个函数来响应我们的请求
int
定义了main()
函数的返回值,返回值为0表示成功,其他表示失败main()
函数的括号里括住了函数从编译器中接受到的参数main()
可以没有返回语句,这样编译器默认返回值为0
花括号
- 在C++中,花括号
{}
告诉编译器把出现在它们之间的所有内容当做一个单元来处理,并且按照语句的先后顺序来执行他们 - 就算函数体只有一条语句,也必须用花括号来扩住它
使用标准库进行输出
- 名字空间:这里的
std
称为名字空间,是一个相关名称的集合,定义了标准库中所有定义的名称 - 作用域运算符:
::
- 标准输出流:
std::cout
,指明和程序文件相联系的一个窗口 - 输出运算符:
<<
- 控制器:
std::endl
,实现对流的控制
返回语句
- 函数将在返回语句出现的位置终止函数的执行,并把一个值返回给调用这个函数的程序
- 返回值类型必须符合函数的定义
- 有可能在多个位置上合理地终止程序
表达式
- 表达式会让编译器对某些事物进行运算
- 运算会产生一个结果,同时还可能会具有副作用。也就是说副作用不是结果的直接部分,但它会影响程序或系统环境的状态。
- 表达式包含操作数和运算符
- Hello, World!例子里
<<
即为运算符,std::cout
,std::endl
, "Hello, World" 则是操作数 - 操作数:具有一定的类型,定义了该操作数的数据结构和对该数据结构的合理操作
- 运算符:运算符的行为取决于操作数的类型,这里的
<<
是左结合的
- Hello, World!例子里
作用域
- 名字空间
- 花括号
程序结构 / 自由风格
- 只是在防止相邻的符号混在一起的时候,才必须使用空白符(如空白格、换行符)
- 通过合理安排空白符,来提高程序的可读性
- 不能具有自由风格的实体
- 字符串直接量:用双引号括住的字符,不可跨行
- #include 名称:必须在单独的一行中出现(注释除外)
- //注释://后面可以跟着任何东西,结束于当前行的行尾
小结
- 程序结构
- 类型
- 名字空间
- 字符串直接量
- 定义和头文件
- 主函数
- 花括号和分号
- 输出
习题
0-0
#include <iostream> |
Hello, world!
0-1
这条语句试图计算3+4的值,返回值被放弃,并且没有任何副作用。
0-2
#include <iostream> |
This (") is a quote, and this (\) is a backlash.
0-3
#include <iostream> |
a bb ccc dddd eeeee ffffff
0-4
#include <iostream> |
#include <iostream> int main(){ std::cout<<"Hello, world!"<<std::endl; return 0; }
0-5
#include <iostream> |
该程序无效,因为即使函数体内仅有一条语句,也必须用花括号括住。
0-6
该程序有效,花括号定义了函数作用域,并且 main()
函数可以没有返回语句。
0-7
该程序无效,因为 /*
为始的注释结束于第一个相邻的 */
。
0-8
该程序有效,因为 //
之后直至行尾的所有内容都被注释掉。
0-9
int main(){} |
0-10
#include <iostream> |
第1章 使用字符串
输入
//请求某人输入其姓名,然后向这个人发出问候 |
Please enter your first name: Hello, Greenwicher!
- 对象:是计算机中的一段具有类型的内存空间,可以有名称,也可能没有
- 变量:是一个具有名称的对象
- 局部变量:只存在相应的作用域内,一旦程序运行到作用域外,改变量所占用的内存就会被系统回收
- 定义:定义变量名称和类型
- 接口:隐含在对象类型中的还有其接口,即可实现的操作的集合,如初始化操作
- 标准输入以及
>>
- 略去输入开始时碰到的空白字符(空白、制表键、回退键或换行符)直至遇到了另一个空白字符或文件结束标记为止
- 副作用:产生一个出现在计算机输出装置上的提示
- 缓冲区:输入/输出库会把它的输出保存在一个缓冲区的内部数据结构中
- 何时刷新
- 缓冲区已满
- 请求库从标准输入流中读数据
- 明确要求刷新
- 何时刷新
为姓名装框
//请求用户输入姓名,然后产生一个带框架的问候语句 |
Please enter your first name: *********************** * * * Hello, Greenwicher! * * * ***********************
const std::string greeting = "Hello, " + name + "!";
- 赋值: 变量初始化时,可通过
=
对其赋值,并且变量类型将进行转换 - 运算符重载: 对于不同类型的操作数来说具有不同的含义
- 常量:
const
指明了该变量在其生存期内不可变,因此必须在初始化的时候赋值 - 运算符左结合:同
<<
一样,+
也是左结合的
- 赋值: 变量初始化时,可通过
const std::string spaces(greeting.size(), ' ');
- 成员函数:名为greeting的对象有一个被称为size的成员,并且该成员为函数,表达greeting所包含的字符个数
- 字符直接量:通过单引号括起来,内建类型为
char
,代表一个字符 - 自表达式的变量:通过括号来构建变量,其具体构建方式则取决于变量的类型
小结
- 类型
char
:普通字符wchar_t
:款字符string
:字符串,包含了一连串的零个或多个字符std:string s;
:定义变量,初始值为空std:string t=s;
:定义变量,初始值为s
std:string z(n,c);
:定义变量,初始值为将字符c
复制n
次后的字符串os<<s;
:写入输出流,返回值为os
is>>s;
:读入输入流,返回值为is
s+t
:字符串叠加s.size()
: 求字符串中字符个数
- 变量定义的形式
std::string hello="Hello";
:明确的初始化值std::string stars(100, '*');
:根据类型和表达式来构造变量std::string name;
:初始值取决于变量类型
- 局部变量
- 常量
- 输入
习题
1-0
见前述本章程序。
1-1
#include <iostream> |
Hello, world!
该定义有效。
1-2
#include <iostream> |
该定义无效,因为运算符 +
无法作用于两个字符串直接量(实际上,为类型为 const char
的数组)。
1-3
#include <iostream> |
a string another string
该程序有效,主要考量了作用域的知识。该程序将输出如上两个字符串。
1-4
#include <iostream> |
a string another string
该程序有效,同1-3分析。
1-5
#include <iostream> |
a string a string, really
该程序无效,正如注释所说,变量 x
在作用域外被引用。更正后的程序如上所示。
1-6
#include <iostream> |
What is your name?Hello, Samuel And what is yours?Hello, Beckett; nice to meet you too!
考察缓冲区以及输入符的特性
第2章 循环和计数
问题
- 1.2节中的程序要求每一行输出都需要和程序的某一部分(并且还有一个变量)相对应,因此如果需要对输出格式进行修改,则需要改动较大
- 我们可以分别产生每一个输出字符,并且没必要把他们存储在变量当中
程序的整体结构
输出数目未知的行
#include <iostream> |
while语句
- 语句形式
while (condition) statement
其中statement语句被称为while循环体。while语句检测condition的值,若为真,则执行循环体,一直到条件为假;否则退出循环体。
- statement后无分号
- 若statement为普通语句,在这条语句的末尾会有它自己的分号
- 若statement为块语句,则块的
}
符号标志着语句的结束,那么也没必要用分号
- condition语句
- 不等式运算符的表达式
- 布尔类型
- 增量运算符:
++
设计while语句
- 理解while语句
- while语句结束时,条件必须为假
- 循环不变式:每当while语句要测试它的条件时,我们都能断言这个特性为真
输出一行
- 定义列数
const std::string::size_type cols = greeting.size() + pad * 2 + 2;
- 逐字符输出
std::string::size_type c = 0;
//不变式,到目前为止,在当前行中我们已经输出了c个字符
while (c != cols){
//输出一个(或多个)字符
//修改c的值,保持不变式的真实性
}
输出边界字符
//不变式,到目前为止,在当前行中我们已经输出了c个字符 |
- if语句
- 语句形式
if (condition)
statement
if (condition)
statement1
else
statement2
- 语句形式
- 逻辑运算符
==
:检测是否相等,关系运算符的优先级比算术运算符的低||
逻辑或,运算符的优先级低于关系运算符,并且左结合,更有短路求值(short-circuit evaluation)特性
输出非边界字符
if (r == pad + 1 && c == pad + 1){ |
- 逻辑运算符
&&
:逻辑与,左结合,并且短路求值
- 复合赋值运算符:
+=
完整的程序框架
略去重复使用的 std::
using
声明
使用for语句来缩短程序
for (init-statement condition; expression){ |
- 第一行称为for语句头,而其后的语句被称为for循环体
压缩检测
完整的框架程序
#include <iostream> |
Please enter your first name: *********************** * * * Hello, Greenwicher! * * * ***********************
计数
- 不对称区间的好处
- [m,n)的元素个数恰好为n-m,而对称区间相对不明显
- 表示空区间的优势[n,n)而非[n,n-1]
- 有利于循环不变式的表达
- 循环终止条件将变得更简明
小结
- 表达式
- 操作数的组合方式:依据运算符的优先级和结合性控制
- 结合性
- 赋值运算符和只有一个操作数的运算符是右结合的
- 其他大多数运算符都是左结合的
- 优先级
运算符优先级.png
- 结合性
- 操作数是如何转换成其他类型的
- 普通算术转换规则:会尽量保持精度
- 操作数的运算次序
- 操作数的组合方式:依据运算符的优先级和结合性控制
- 类型
- 半开区间
- 条件
- 语句
using
- 定义以及初始化
- 执行表达式并产生它的副作用
- 块语句
- while语句
- for语句
- if语句
习题
2-0
见本章前述程序。
2-1
只需令 pad
变量的值为0即可。
2-2
只需修改 pad
变量的值即可。
2-3
只需添加 int pad; std::cin>>pad;
即可
2-4
只需修改为 std::string blank(2*pad+greeting.size(),' '); std::cout<<blank;
2-5
- 正方形
//绘制正方形
#include <iostream>
#include <string>
using namespace std;
int main(){
//定义正方形边长
int a = 8;
//定义首行和尾行
string template1(a,'*');
string template2(a-2,' ');
for(int i=0;i!=a;++i){
if (i==0 || i==a-1){
cout<<template1<<endl;
} else {
cout<<"*"+template2+"*"<<endl;
}
}
return 0;
}******** * * * * * * * * * * * * ********
- 长方形
//绘制长方形
#include <iostream>
#include <string>
using namespace std;
int main(){
//定义长方形的长和边
int a = 8;
int b = 32;
//定义首行和尾行
string template1(b,'*');
string template2(b-2,' ');
for(int i=0;i!=a;++i){
if (i==0 || i==a-1){
cout<<template1<<endl;
} else {
cout<<"*"+template2+"*"<<endl;
}
}
return 0;
}******************************** * * * * * * * * * * * * ********************************
- 三角形
//绘制等腰直角三角形
#include <iostream>
#include <string>
using namespace std;
int main(){
//定义等腰直角三角形的腰长
int a = 8;
for(int i=0; i!=a; ++i){
if (i==0 || i==a-1){
string row(i+1,'*');
cout<<row<<endl;
} else {
string row(i,' ');
cout<<"*"+row+"*"<<endl;
}
}
return 0;
}* * * * * * * * * * * * * ********
2-6
该程序将输出1-10序列
2-7
#include <iostream> |
10 9 8 7 6 5 4 3 2 1 0 -1 -2 -3 -4 -5
2-8
#include <iostream> |
362880
2-9
#include <iostream> |
Please enter two numders 20
2-10
using std::cout
这里是指用cout
来表示名字空间std
中定义的cout
,并且其作用域仅在while语句的块语句中std::cout
这里是指名字空间std
中类型为ostream
的变量cout
,即标准输出流std::endl
这里是指名字空间std
中的控制流
第3章 使用批量数据
计算学生成绩
#include <iomanip> |
Please enter your first name: Hello, Greenwicher! Please enter your midterm and final exam grades: Enter all your homework grades, followed by end-of-file: Your final grade is 95.733
#include <ios>
:该库定义了类型streamsize
,输入/输出库用这个类型来表示长度#include <iomanip>
:该库定义了控制器setprecision
,用于指明输出所包含的有效位数cin >> midterm >> final;
:输入运算符左结合,并返回左操作数作为结果- 如果两个以上的字符串直接量仅仅是被空白符分隔开的话,那么这些字符串直接量就会被自动连接到一起
- 初始化以及缺省初始化
while (cin >> x)
:只要读取成功就会继续读取下去cout.precision()
:获取流在进行浮点数输出时所使用的精度setprecision()
:改变cout
中所有后继输出的精度streamsize prec = cout.precision(3)
:把精度设为3,并返回原先的精度
检测输入
cin >> x
:首先把一个数据读到x
中,然后返回cin
的值cin
的值被转化为bool
类型- 流数据读取何时失败
- 到达了输入文件的结尾
- 输入和要读取的变量的类型不一致
- 在输入装置中检测到一个硬件问题
循环不变式
用中值代替平均值
把数据集合存储在向量中
double x; |
vector<double>
:向量是一个存储数值集合的容器,其中所有数值都具有相同的类型,尖括号内制定了对象的类型homework.push_back(x)
:vector
中的成员函数push_back()
将添加值x
到该向量的尾部
产生输出
typedef vector<double>::size_type vec_sz
:typedef
用于定义特定类型的一个替代名vec_sz size = homework.size()
:vector
具有成员函数size()
,用以返回向量的长度sort(homework.begin(), homework.end())
:对向量homework
中的元素进行非递减排序median = size % 2 == 0 ? (homework[mid] + homework[mid-1]) / 2 : homework[mid]
:条件运算符- 向量的索引:每个向量中的任意一个元素都有一个被称为索引的整数与之相联,该元素未命名,且其类型同存储容器中的类型相同
#include <iomanip> |
Please enter your first name: Hello, Greenwicher! Please enter your midterm and final exam grades: Enter all your homework grades, followed by end-of-file: Your final grade is 95.8
一些更为深入的观察
- 向量不会检查索引是不是在其值域中取值,这样的检查是由用户来完成的
- 无符号整数类型
- C++标准对库的执行性能的要求很高
小结
- 类型定义
- 未定义的数值只能用作赋值运算的左侧操作数
- 关于向量的定义以及成员函数
- 其他库工具
习题
3-0
请见本章的前述程序。
3-1
显然,若向量为奇数,则丢掉向量中间的那个数;若向量为偶数,则丢掉向量中间的那两个数。丢弃之后,新的向量的中值很大程度不等同于原向量的中值。
3-2
#include <iostream> |
223 98 94 89 78 67 67 45 45 41 33 32 23 23 23 21 18 14 12 11 9 0 -10 -100
3-3
#include <iostream> |
apple: 5 banana: 1 durian: 1 lemon: 1 mango: 1 orange: 2
3-4
#include <iostream> |
sort max while precision setprecision streamsize 3 12
3-5
#include <iomanip> |
Please enter your first name: Hello, Alex! Please enter your midterm and final exam grades: Enter all your homework grades, followed by end-of-file: Your final grade is 95 Please enter your first name: Hello, Bob! Please enter your midterm and final exam grades: Enter all your homework grades, followed by end-of-file: Your final grade is 89.6 Please enter your first name: Hello, Wilson! Please enter your midterm and final exam grades: Enter all your homework grades, followed by end-of-file: Your final grade is 75
第4章 组织程序和数据
- 库工具的特性
- 都能解决某些特定类型的问题
- 与其他的大多数工具都相互独立
- 都具有一个名称
- C++中组织大型程序的方法
- 函数(也被称为子程序)
- 数据结构
- 类是函数以及数据结构的结合
- 将程序划分为可以独立编译并且在编译后能组合在一起的文件
组织计算
- 使用函数的好处
- 减少代码量,增强复用
- 更方便的修改函数所代表的行为
- 通过命名将函数看做黑箱,用更抽象的方式去思考更大的问题
- 计算成绩的函数
double grade(double midterm, double final, double homework){
return 0.2 * midterm + 0.4 * final + 0.4 * homework;
}- 返回值类型
- 函数名
- 参数列表
- 函数体
- 函数调用
- 参数要按顺序以及定义罗列
- 调用的过程是对参数进行复制,而非直接指向函数的参数
查找中值
//计算一个vector<double>类型的变量的中值 |
异常
:如果一个程序抛出了一个异常,那么这个程序就会在抛出异常的地方终止执行并转移到程序的另一部分,并向这部分提供一个异常对象
,异常对象中含有调用程序可以用来处理异常的信息domain_error
:函数参数的取值是函数所不能接受的- 参数复制:参数初始化仅仅是复制而非直接引用,因此函数不会影响传递参数的值
重新制定计算成绩的策略
//根据期中、期末考试成绩和保存家庭作业的向量来计算学生的总成绩 |
const vector<double>&
:对参数类型为double
的向量常量的引用,或者称为双精度向量常量引用引用
:是指这个名称是另一个特定对象的另一个名称,也就是替代名、别名重载
:不同的几个函数具有相同的函数名,系统环境依据被调用函数的参数形式来确定究竟是哪个函数if (hw.size() == 0)
:尽管若向量hw
为空,median
函数会报错,但仍不够具体。因此在这里提前做一步检查。
读家庭作业成绩
//从输入流中将家庭作业的成绩读入到一个vector<double>中 |
- 通过参数引用来增加函数返回值,该参数必须为左值参数,而非表达式
hw.clear()
:因为该变量来自外部作用域,因此可能包含其他先前学生的成绩in.clear()
:让库忽略任何可能导致输入尝试失败的情况,通过该成员函数来清楚其内部的错误状态- 貌似函数返回类型应该为
istream&
三种函数参数
- 参数复制
- 常量参数引用
- 非常量参数引用
#include <iomanip> |
Please enter your first name: Hello, Greenwicher! Please enter your midterm and final exam grades: Enter all your homework grades, followed by end-of-file: Your final grade is 95.8
使用函数来计算学生的成绩
try
和catch
:程序首先执行try
后花括号中的语句。如果发生catch
中指明的错误异常话,那么便停止try
中的语句执行而去执行catch
后花括号中的语句;否则跳过catch
花括号的语句。- 每当编写一条
try
语句时,都要考虑副作用以及副作用发生的时刻 - 要保证一条语句中的副作用个数不会超过一个
组织数据
把一个学生的所有数据放置在一起
struct
结构体
struct Student_info{ |
处理学生记录
- 读取学生记录
istream& read(istream& is, Student_info& s){
//读入并存储学生的姓名以及期中、期末考试成绩
is >> s.name >> s.midterm >> s.final;
read_hw(is, s.homework);
} - 计算学生成绩
double grade(const Student_info& s){
return grade(s.midterm, s.final, s.homework);
}- 函数参数通过常量引用而避免不必要的复制开销
- 异常处理的合理位置
- 按照姓名排序
bool compare(const Student_info& x, const Student_info& y){
return x.name < y.name;
}
sort(students.begin(), students.end(), compare)sort()
函数的第三个参数称为谓词,其为一个函数,返回布尔值,作为两两元素『大小』比较的标准string
类定义了<
运算符通过字典序来比较两个字符串大小
生成报表
int main(){ |
max()
函数要求两个参数的类型一致setw()
控制器让库把下一个输出项填充成有特定数目的字符集(通过给输出项添加空格来完成),并且对宽度的修改是短暂的e.what()
描述导致抛出异常的问题
把各部分代码连接到一起
跟许多其他语言一样,C++为了降低复杂度而分块编译的概念提供了支持,这个概念允许我们把程序放进不同的文件中并独立的编译这些文件中的每一个。
median()
函数定义的源文件//median函数的源文件
#include <algorithm> //获取sort的声明
#include <stdexcept> //获取domain_error的声明
#include <vector> //获取vector的声明
using std::domain_error; using std::sort; using std::vector;
//计算一个vector<double>类型的对象的中值
double median(vector<double> vec){
typedef vector<double>::size_type vec_sz;
vec_sz size = vec.size();
if (size == 0)
throw domain_error("median of an empty vector");
sort(vec.begin(), vec.end());
vec_sz mid = size/2;
return size % 2 == 0 ? (vec[mid] + vec[mid-1])/2 : vec[mid];
}median()
函数的头文件 (为了对其他用户也可用)#ifndef GUARD_median_h
#define GUARD_median_h
//median.h的最终版本
#include <vector>
double median(std::vector<double>);
#endif- 用一个分号代替这个函数的函数体,同时我们还可以去掉参数的名称,因为在没有函数体的情况下它们是不相干的
- 还需要把生命本身所用到的所有名称都包含进去
- 头文件应该只声明必要的名称,先定了包含在头文件中的名称之后,我们就可以为我们的用户保留最大限度的灵活性了。因此,头文件应该使用完整限定名而不是使用
using
声明 - 通过预处理程序来保证多次对头文件包含的安全性
#ifndef
指令检查其后的变量是否呗定义- 复制头文件中内容到程序中
#include "median.h"
把计算成绩的程序分块
- 将和某些特定结构相关的函数写在同一个头文件里进行声明,因为只有当该结构被定义时,那些函数才会有被调用的可能性
- 有必要在源文件中包含相应的头文件,以给编译器提供机会检查声明与定义是否一致
- 源文件中的
using
声明并没有问题,它只不过是个局部决策,而在头文件中并不是 - 将重载函数的声明放在一起会让这些可供选择的函数更易查找
修正后的计算成绩的程序
小结
- 程序结构
- 系统头和自定义头文件的引用
- 利用
ifndef
防止头文件的重复包含 - 避免在头文件中声明它们无需用到的名称,比如说
using
声明
- 类型
T&
:可修改的引用,参数必须为左值const T&
:不可修改的引用,避免额外内存开销
- 结构
- 包含0个或多个成员的类型
- 应该出现在一个有适当防护措施的头文件中
- 函数
- 函数声明与定义
- 重载
inline
与内联子过程
- 异常处理
- 异常类
- 库工具
习题
4-0
请见本章前述程序。
4-1
max
函数要求其参数类型必须一致,因此该段代码存在潜在问题。可改为如下代码,
Student_info s; |
4-2
#include <iostream> |
1 1 2 4 3 9 4 16 5 25 6 36 7 49 8 64 9 81 10 100
4-3
若忘记修改 setw()
的参数,则整数及其平方之间将不存在空格。完备程序请见4-2。
4-4
不太清楚和4-3之间的区别,难道是要控制精度么,但觉得没太大意义啊。
#include <iostream> |
1 1 2 4 3 9 4 16 5 25 6 36 7 49 8 64 9 81 10 100
4-5
#include <iostream> |
apple 3 banana 1 dragonfruit 1 durian 2 grape 1 lemmon 3 lime 1 orange 2 passionfruit 2 pineapple 1 watermelon 1
4-6
struct Student_info{ |
4-7
#include <iostream> |
The average of the vector is 5.5
4-8
f的返回类型应该是数组/向量。
4-9
string spaces(maxlen+1-students[i].name.size(), ' '); |
第5章 使用顺序容器并分析字符串
按类别来区分学生
//第一次尝试:把及格和不及格的学生记录分离开来 |
就地删除元素
- 除非是特别需要,否则我们都不希望同个数据有多个副本,以致内存浪费
- 从向量中删除元素将会耗费大量的时间
- 采用更加合适的数据结构
- 使用更加聪明的算法
//第二次尝试:函数正确,不过可能会相当慢 |
vector
类型有成员函数erase
来依据索引指明删除特定的向量元素,并影响向量的索引
顺序存取与随机存取
- 不同类型的容器会有不同的性能特性,并且会支持不同的操作
- 如果仅仅需要顺序访问,那么随机访问的特性就会变得冗余了
- 迭代器类型,允许我们以库能够控制的方式来访问数据结构,这个控制保证了库的高效实现
迭代器
- 一个
迭代器
(iterator
) 是一个值,它能够- 识别一个容器以及容器中的一个元素
- 让我们检查存储在这个元素中的值
- 提供操作来移动存储在这个元素中的值
- 采用对用于容器所能够有效处理的方式来对可用的操作进行约束
迭代器类型
container_type::const_iterator
:读操作
vector<Student_info>::const_iterator iter = students.begin(); |
container_type::iterator
:读写操作
迭代器操作
for (vector<Student_info>::const_iterator iter = students.begin(); iter!= students.end(); ++iter) { |
- 两个迭代器是否相等的判断
- 增量运算符对迭代器的重载
- 间接引用运算符
*
(其优先级低于.
)
一点语法知识
(*iter).name
iter->name
(用于上面的代替形式)
students.erase(students.begin() + i)的含意
- 随机访问和顺序访问
用迭代器来代替索引
//版本3:用迭代器来代替索引;效率可能还是不高 |
erase()
返回被删除元素后的那个元素
重新思考数据结构以实现更好的性能
- 在向量末尾添加元素 vs 在向量内部插入或删除元素
- 更好的数据结构:可以不支持索引的随机访问,但更方便的允许在容器任何位置插入和删除元素
list类型
vector
支持索引和快速随机访问,而list
提供了在容器中任何位置快速插入和删除元素
//版本4:用list来代替vector |
一些重要的差别
- 对
vector
进行元素的删除或添加会导致其他元素位置(或迭代器)的重新分配 - 对
list
进行元素的删除或添加并不会导致其他元素位置(或迭代器)的变化 list
类型有自带的sort
成员函数
一个恼人的话题
- 根据问题,选择适当的数据结构,提高程序性能
分割字符串
vector<string> split(const string& s){ |
<cctype>
头文件定义了isspace
函数string
类的substr()
成员函数
测试split函数
getline(std::cin, string)
以及split()
函数while(std::cin>>s)
连接字符串
为图案装框
纵向连接
- 方法1:
push_back()
- 方法2
ret.insert(ret.end(), bottom.begin(), bottom.end())
横向连接
小结
- 容器与迭代器
顺序容器和 string 类型支持的操作 |
解释 |
---|---|
container<T>::iterator |
容器的可读写迭代器类型 |
container<T>::const_iterator |
容器的只读迭代器类型 |
container<T>::size_type |
最大实例长度的类型名 |
c.begin() |
容器第一个元素位置的迭代器 |
c.end() |
容器最后一个元素的下面一个位置的迭代器 |
c.rbegin() |
容器最后一个元素位置的逆向迭代器 |
c.rend() |
容器第一个元素之前的那个位置的逆向迭代器 |
container<T> c; |
定义容器 c |
container<T> c(c2); |
容器 c 是 c2 的一个复件 |
container<T> c(n); |
定义一个有n个元素的容器 c |
container<T> c(n,t); |
定义一个有n个元素的容器 c , c 的元素是t的复件 |
container<T> c(b,e); |
创建容器,并保存位于区间[b,e)的迭代器所指示元素的一个复件 |
c=c2; |
用容器 c2 的一个复件来替换容器 c 的内容 |
c.size() |
返回 c 的元素个数 |
c.empty() |
判定 c 是否存在元素 |
c.insert(d,b,e) |
复制 [b,e) 迭代器所指示的元素至 c 中位于d之前的位置中 |
c.erase(it) |
删除it指示的元素 |
c.erase(b,e) |
删除[b,e)指示的元素 |
c.push_back(t) |
在 c 的末尾添加一个元素,其值为 t |
支持随机访问容器和 string 类型支持的操作 |
解释 |
---|---|
c[n] |
从容器中取出位于位置n的元素数值 |
- 迭代器操作
习题
第6章 使用库算法
分析字符串
- 利用循环来连接两幅字符图案
- 利用迭代器逐一访问并取出
- 利用
vector.insert(out.end, begin, end)
- 利用
copy(begin, end, out)
- 泛型算法:是一个不属于任何特定类别容器的算法,相反,它会从它的参数类型获得关于如何访问它所使用数据的提示。
- 迭代器适配器:在
<iterator>
中定义,最常用的迭代器适配器是back_inserter
,它用一个容器作为它的参数并产生一个迭代器,在生成的迭代器被用作一个目的地的时候,它会向容器末端添加数值。
另一个实现split的方法
// 如果参数是空白区域,返回true |
回文
bool is_palindrome(const string& s){ |
查找URL
#include<cctype> |
static
:静态存储类别说明符,具有全局寿命isalnum()
isalpha()
find()
search()
对计算成绩的方案进行比较
处理学生记录
container.empty()
:判断容器是否为空
分析成绩
transform()
- 函数本身作为参数被调用时的重载问题
- 异常的捕获
- 如何定义函数作为其他函数的输入参数
void
:返回类型为空
计算基于家庭作业平均成绩的总成绩
accumulate()
上交家庭作业的中值
remove_copy()
对学生进行分类并回顾一下我们的问题
一种两次传递的解决方案
remove_copy_if()
remove_if()
container.erase()
一种一次传递的解决方案
partition()
stable_partition()
算法、容器以及迭代器
- 算法仅在容器的元素上操作,而不影响容器本身。
- 容器的成员函数会影响容器本身
- 算法以及容器成员对迭代器的影响
小结
- 类型修饰符
- 类型
- 迭代器适配器
back_inserter()
front_inserter()
inserter()
- 算法
accumulate()
find()
find_if()
search()
copy()
remove_copy()
remove_copy_if()
remove_if()
remove()
transform()
partition()
stable_partition()
习题
6-0
6-1
6-2
6-3
6-4
6-5
6-6
6-7
6-8
6-9
第7章 使用关联容器
支持高效查找的容器
- 关联容器:自动将元素安排在一个序列中,该序列依赖于元素本身的值,最常见的一种关联数据结构存储了『键-值』对
- C++中的关联容器主要是
map
(映射表),其在<map>中定义
计算单词数
int main(){ |
- 声明映射表:
map<string, int>
- 读取映射表:通过数对(pair)来存储映射表中每一个元素的键和值
产生一个交叉引用表
map<string, vector<int> > xref(istream& in, vector<string> find_words(const string&) = split){ |
- 注意上段代码中函数返回参数的类型定义中用了『> >』,而不是『>>』(容易被编译器理解为>>运算符)
- 函数参数列表中的缺省参数声明
生成句子
表示规则
读入文法
生成句子
选择一个随机元素
关于性能的一点说明
- 映射表同散列表的对比
小结
- do while语句
- 数值初始化
rand()
pair<k, v>
map<k, v>
习题
7-0
7-1
7-2
7-3
7-4
7-5
7-6
7-7
7-8
7-9
第8章 编写泛型函数
泛型函数是什么?
- 泛型函数:其参数以及返回类型事先不知道,直到调用了才知道的函数类型
- 在调用函数时,C++内部会检查相应参数类型是否兼容函数定义中的操作
- 标准库对其函数参数的约束条件所采用的组织方式(如迭代器类型)
- 模板函数:实现了泛型函数的语言特征的函数,不同函数的差异取决于模板参数
未知类型的中值
template <class T> |
- 类型参数:表示类型而不是变量
typename
:对系统环境声明一个依赖于模板参数的类型
模板实例化
泛型函数和类型
- 模板的参数类型是依据实例化后的参数类型而被推断出来的
数据结构独立性
- 三种函数构建思路
- find(c.begin(), c.end(), val),能处理容器中任意连续的一部分
- c.find(val),find()仅仅是数据结构c的一个成员,因此别的数据结构得重新定义
- find(c, val),只能处理容器的整体部分
算法与迭代器
- 不同种类的迭代器提供不同种类的操作
- 依据算法来看对不同迭代器的需求
- 库定义了五种迭代器
顺序只读访问 / 输入迭代器
template <class In, class X> |
顺序只写访问 / 输出迭代器
template <class In, class Out> |
顺序读-写访问 / 正向迭代器
template <class For, class X> |
可逆访问 / 双向迭代器
template <class Bi> void reverse(Bi begin, Bi end){ |
随机访问 / 随机迭代器
template <class Ran, class X> |
迭代器区间和越界值
- 在库中有一个几乎是通用的约定,算法要用两个参数来指定区间。第一个参数指向区间的第一个元素,第二个参数则指向了紧位于区间最后的元素后面的那个位置。
- 为什么c.end()是最后元素紧接着的位置?
- 确定开放区间上界
- 如果区间根本没有元素
- 利用迭代器是否相等来顺序遍历区间
- 一种自然的方式来表示『区间之外』
输入输出迭代器
- 标准库提供了能被连接到输入和输出流的迭代器
用迭代器来提高适应性
template <class Out> |
小结
- 模板函数
- 迭代器
- 作为算法与容器之间的『粘合剂』,从而获得数据结构的独立性
- 迭代器的五个种类
习题
8-0
8-1
8-2
8-3
8-4
8-5
8-6
8-7
8-8
第9章 定义新类型
回顾一下Student_info
- 建立使用约定
- 潜在的假定
- 整合的接口
自定义类型
成员函数
struct Student_info{ |
- 函数名的不同:Student_info::read
- 函数参数的隐含操作对象:不需要把Student_info作为参数来传递
- 对象可以被直接访问:在函数内部,可以直接使用Student_info的数据元素
- 常量成员函数:因为操作对象被隐含进参数里,因此对其常量化只能在参数列表外进行
非成员函数
- 如果函数会改变一个对象的状态,那它就应该作为这个对象的成员函数
保护
class Student_info{ |
class
:同struct
的差别在于对第一个保护标识符之前成员访问属性的缺省处理方式不同(class
定义为私有的,struct
刚好相反)- 保护标识符:
public
以及private
- 数据隐藏机制
- 公有的(public):可被类型的所有用户访问
- 私有的(private):对用户不可访问
存取器函数
class Student_info{ |
- 存取器函数:对私有的数据提供接口的函数
检查对象是否为空
class Student_info{ |
Student_info类
class Student_info{ |
构造函数
- 构造函数:定义了对象的初始化,在对象被定义时自动调用,而不能被显式地调用;若不存在,则由编译程序合成一个构造函数。
- 构造函数的初始化方式
- 如果对象属于一种自定义类型:借助相应定义的构造函数
- 如果对象属于内部类型:数值初始化方式把它设为0,而缺省初始化方式会给它一个未定义值
- 否则,按照数值初始化或者缺省初始化进行
- 构造函数的特点
- 名称和类本身相同
- 没有返回类型
class Student_info{ |
缺省构造函数
Student_info::Student_info(): midterm(0), final(0) {} |
- 构造函数初始化程序:显示初始化内部类型或有自定义初始值的数据元素
带参数的构造函数
Student_info::Student_info(istream& s) {read(is); } |
使用Student_info类
int main() { |
小结
- 用户自定义类型
- 保护标识符
- 成员函数
- 构造函数
- 构造函数初始化程序列表
习题
9-0
9-1
9-2
9-3
9-4
9-5
9-6
9-7
第10章 管理内存和低级数据结构
指针和数组
指针
指针
:一个存放对象地址的值- 求址运算符:假设x是一个对象,&x就是该对象的地址。&: x -> p
- 间接引用算符:假设p是一个对象的地址,*p就是该对象的值。 *: p -> x
- 定义
- int p 与 int p (*p本身是一个声明符)
- int* p, q的陷阱
#include<iostream> |
x=5 x=6
- 对象与指针的关系:将对象理解成一个只包含该对象一个元素的容器,而将指向该对象的指针理解成为一个指向一个容器中唯一元素的迭代器
指向函数的指针
- 在程序中无法创建或修改一个函数,只有编译器才可以这样做。一个程序对一个函数的所有操作只有调用该函数或得到它的地址。
- 定义指向函数的指针
- int (*fp)(int):指向参数与返回均为整型的函数指针
- 等价获得函数地址语句
- fp = &next
- fp = next
- next为fp指向函数类型的函数
- 等价获得函数值的语句
- i = (*fp)(i)
- i = fp(i)
- 返回指向函数指针的函数
- typedef double (*analysis_fp) (const vector<Student_info>&);
- analysis_fp get_analysis_ptr();
- double (*get_analysis_prt()) (const vector<Student_info>&);
数组
- 数组元素的个数必需在编译的时候确定
- <cstddef> 定义了无符号类型size_t,其大小可以装下任何对象
- 数组名保存了该数组的首地址
指针算法
- 访问数组中的其他非首位元素
- 越界
索引
- p[n] 与 *(p+n)
数组初始化
- 避免显式定义数组大小
再看字符串常量
- 字符串常量是一个字符常量数组,该数组的大小是字符串的长度加一(多了一个空字符)
初始化字符串指针数组
static
sizeof()
- "?\?\?"
main函数的参数
- 一个整型参数与一个指向字符指针的指针参数
argc
:argv指向的数组中指针的个数argv
:指向一个指针数组首元素地址的指针,其元素为字符串参数
文件读写
标准错误流
cerr
:及时反映错误clog
:生成异常信息日志,适当的时候才输出
处理多个输入输出文件
ifstream
与ofstream
- 文件名要为指向一个以空字符结尾的字符数组的首元素的指针
int main(int argc, char ** argv){ |
三种内存分配方法
为一个对象分配/释放内存
- T *p = new T(args);
- delete p;
int* pointer_to_dynamic(int num){ |
为一个数组分配/释放内存
- T *p = new T[n];
- delete[] p;
小结
- 指针
- 主函数
- 输入/输出
- 内存分配
习题
10-0
10-1
10-2
10-3
10-4
10-5
10-6
第11章 定义抽象数据类型
Vec类
- 按照使用者的潜在需求来定义类的接口
vector<Student_info> vs; |
实现Vec类
- 用于函数的模板技巧同样适用于类
template <class T> class Vec{ |
内存分配
- 内存分配类
构造函数
- 三类构造函数
- 仅仅是新建一个对象
- 声明对象的大小
- 声明对象的大小和初始值
explicit
:显式与隐式的调用构造函数
类型定义
- 为常量与非常量迭代器类型以及我们用来表示Vec的大小的类型提供类型定义(typedef)
索引与大小
- 运算符的重载
- 指针的差
返回迭代器的操作
- 函数重载
复制控制
复制构造函数
- 显式以及隐式复制
copy constructor
template <class T> class Vec{ |
赋值运算符
template <classs T> class Vec{ |
- 自我复制
- 模板参数在头文件里是隐式的,不需要复写;而在头文件外面,必须声明模板参数的类型
this
:仅在成员函数内部有效,代表指向函数操作的对象的指针
赋值不是初始化
- 赋值与初始化的区别
- 赋值总是删除一个旧的值,而初始化没有这步操作
- 构造函数时钟只控制初始化操作;operator=成员函数只控制赋值操作
析构函数
destructor
:定义如何删除该类的一个对象实例,函数名为在类的名字前加一波浪线前缀(~),析构函数不带任何参数,而且没有返回值
template <class T> clas Vec{ |
默认操作
- 编译器自动为类生成构造函数、赋值运算符函数以及析构函数,若这些函数没有被显示的定义
『三位一体』规则
- 三位一体:复制构造函数、析构函数、赋值运算符函数
动态的Vec类型对象
- 为随机存储类对象分配比实际需要更多的内存,只有在使用完预分配的内存时,才可以申请分配更多的内存
template <class T> class Vec { |
灵活的内存管理
<memory>
:allocator<T>
最后的Vec类
- 类不变式
- create()函数
- uncreate()函数
小结
- 模板类
- 复制控制
- 自动生成的操作
- 重载运算符函数
习题
11-0
11-1
11-2
11-3
11-4
11-5
11-6
11-7
11-8
11-9
第12章 使类对象像一个数值一样工作
一个简单的String类
class Str{ |
- 显示定义默认构造函数
- 作为模板函数的构造函数
- 一般来说,一个不需要析构函数的类也不需要显示的定义复制构造函数或赋值运算符函数
自动转换
Str s("hello"); //构造s |
- 在类中定义类型转换
- 其他类型转换成该类类型
- 该类类型转换成其他类型
Str操作
索引运算符
class Str{ |
输入输出运算符
- 输入输出函数不能作为Str类的成员函数
- 输出函数不改变对象的状态
- 尽管输入函数会改变对象的状态,但是『对于一个二元运算符函数,其左操作数必须作为函数的第一个参数,右操作数作为函数第二个参数;并且如果该运算符函数是成员函数,那么第一个参数(也就是左操作数)总是默认的传递给该成员函数。』,因此如果将输入函数作为Str类的成员函数,那么
//以下两个语句等价
s.operator>>(cin);
s >> cin;
- 必要的声明以及函数定义
//在Str.h头文件中加入输入/输出运算符函数的声明
std::istream& operator>>(std::istream&, Str&);
std::istream& operator<<(std::ostream&, const Str&)
ostream& operator<<(ostream& os, const Str& s){
for(Str::size_type i = 0; i != s.size(); ++i)
os<<s[i];
return os;
}
class Str{
public:
size_type size() const {return data.size(); }
//其他同前
};
友员函数
//这些代码还不能被编译通过 |
- 友员函数拥有和成员函数一样的访问权力
class Str{
friend std::istream& operator>>(std:istream&, str&);
//其余同前
}
其他二元运算符
class Str{ |
混合类型表达式
- 混合类型的连接操作需要消耗大量内存
- 标准库中的
string
类是利用重载加号操作数函数,为每一种可能的操作数类型的连接定义一个版本
设计二元运算符
- 如果一个类支持转换,那么把二元运算符定义成非成员函数是一个良好的习惯,这样可以保证两个操作数的对称性。
有些转换是危险的
- 把构造函数定义为
explicit
类型的好处 - 一般来说,我们总是把定义将要被构造对象的结构(而不是内容)的构造函数声明为
explicit
类型转换操作函数
- 定义如何把一个对象从原来的类型转换成一个希望得到的类型
- 转换操作函数的函数名为
operator
加上目标类型名
类型转换与内存管理
小结
- 类型转换
- 带单个参数的非
explicit
的构造函数 operator type_name()
- 带单个参数的非
- 友员函数
- 作为类成员的模板函数
string
类中的操作s.c_str()
s.data()
s.copy(p, n)
习题
12-0
12-1
12-2
12-3
12-4
12-5
12-6
12-7
12-8
12-9
12-10
12-11
12-12
12-13
12-14
12-15
第13章 使用继承与动态绑定
一个简单的string类
class Core{ |
- 在一个类与另一个类比较起来,除了一些扩充以外其余部分完全相同的情况下,我们考虑使用
继承
特性。 - 基类(Core)与派生类(Grad):派生类继承了基类的所有成员,并且可以二次修改但不可删除;自然可以单独添加自己的成员
回顾保护类型
protected
:允许派生类访问基类中的成员
操作函数
继承与构造类函数
多态和虚拟函数
在不知道对象类型的情况下获得对象的值
- 虚函数:
virtual
动态绑定
- 动态绑定与静态绑定:指在运行的时候才决定调用什么函数;在编译的时候决定调用什么函数
- 多态性:在要求一个指向基类对象的指针或引用的地方,我们却可以用一个指向派生类的指针或引用来代替
简单回顾
用继承来解决我们的问题
容纳(实际上)未知类型的容器
虚拟析构函数
一个简单的句柄类
读取句柄
复制句柄对象
使用句柄类
微秒之处
继承与容器
需要哪一个函数?
小结
- 继承
- 动态绑定
- 覆盖
- 虚拟析构函数
- 构造函数与虚拟函数
- 静态成员
习题
13-0
13-1
13-2
13-3
13-4
13-5
13-6
13-7
13-8
13-9
第14章 近乎自动地管理内存
- 接口类与类指针类
- 在复制一个对象的时候程序实际做了什么?
用来复制对象的句柄
- 指针可能引发的问题
- 两个指针指向同一个对象
- 删除一个指针,而不释放所指对象占用的内存
- 删除一个对象,而没有删除指向该对象的指针
- 定义了一个指针,但为对其进行初始化
一个通用句柄类
template <class T> class Handle { |
使用一个通用句柄
引用计数句柄
- 引用计数:记录有多少个对象指向同一个底层对象
template<classs T> class Ref_handle{ |
可以让你决定什么时候共享数据的句柄
template<class T> class Ptr{ |
可控句柄的一个改进
复制我们不能控制的类型
- 所有的问题都可以通过引入一个额外的间接层来解决
复制在什么时候是必要的
小结
- 模板特化
习题
14-0
14-1
14-2
14-3
14-4
14-5
14-6
第15章 再探字符图形
设计
使用继承来模拟结构
- 定义基类、派生类以及接口类
Pic_base类
- 定义接口
- =0: 该虚函数没有其他定义,相应的不会有这个类本身的对象;这样的虚函数被称为纯虚拟函数,相应的类为抽象基类
派生类
复制控制
实现
实现用户接口
String_Pic类
补齐输出结果
VCat_Pic类
HCat_Pic类
Framew_Pic类
不要忘了友员类声明
小结
- 抽象基类
- 前置声明
习题
15-0
15-1
15-2
15-3
15-4
15-5
15-6
第16章 今后如何学习C++
- 重新阅读本书,把没做完的习题做完
- 编程风格以及编程技巧:《Ruminations on C++》
好好地利用你已经掌握的知识
- 想要更加熟悉已经学过的知识,我们只有不断地练习、练习、再练习!
学习更多的东西
- 《The C++ Programming Language》
- 《Generic Programming and the STL》
- 《Ruminations on C++》
- http://www.accu.org