解决闪现问题
在return 0; 的代码上方添加
system(“PAUSE”);
cout 代表“控制台输出”(console output),必须在代码行最左端
cout <<(左箭头流操作符)“XXXXXX”;
#include <iostream>
不加载iostream,就无法使用cout,cin
数学库支持
using namespace std;
using语句是为了简化程序,可直接引用cout这样的对象(还有cin等),而不必写std::cont
cout << "xxx" << endl;
endl是end line的简称,发音 end ELL,等于与"\n"(换码序列)
endl的全称是std::endl,using语句帮助少打一个std
cin表示控制台输入,用流操作符>>表明
cin >> xxx;
建议使用 double双精度浮点,8字节存储
int的范围:正负20亿
C++在执行计算时将所有数据转为双精度,若想要使用单精度,最好写成12.8f
C++源代码→编译→机器码→链接→可执行程序
注释://… 和/…/
变量规则:字母,数字,不能是关键字
首字母为字母,不能是数字,也可以是
忘记初始化局部变量,会自动用一些垃圾值初始化
i++:先返回当前值,i再递增加1
++i:先递增1,再返回结果
近年来,前缀版本更好使用
j=i++:i赋值给j,再递增1
++,-- 具有最高优先级
新的强制类型转换
static_cast<类型>(表达式)
2 判断语句
if-else
if-else if-else
switch-case
case n: 语句
可以添加一个default情况,如果与任何case都不匹配,就跳到default
某个case不写break,会直通到下一个case处
x=y=z=0表示x=(y=(z=0))
3 循环语句
while
do-while
for循环
可声明只在循环内部有效的变量
在循环语句中,continue会立即跳到下一次循环,可能会忽视递增++i;
在for语句中,continue会在跳转前执行递增
for-each(第17章)
4 被调用的函数
布尔(短路)逻辑
&&,||,!
flase&&any=false
true || any = true
按位操作符:&,|,^,~
Math库
#include <cmath>
sqrt(n):平方
随机数
1.设置随机数种子→系统时间
2.通过复杂的数学运算生成序列的下一个数
#include <cstdlib> //支持srand和rand函数
#include <ctime> // 支持ctime函数
srand(time(nullptr));
//(c++11开始支持nullptr关键字,即空指针)
//种子值无需多次设置
cout << rand()%n <<endl; //打印0到n-1
cout << rand()%n <<endl;
获得随机值的范围是无符号整数范围中任意一个
自定义函数
1.程序开头声明函数(也可以是定义)
2.定义函数
3.调用
返回类型 函数名 (参数列表){
语句
}
参数列表中要加正确的类型名称,以逗号分隔,可为空
递归函数:为了解决层数为n的一个常规问题,假定已解决了n-1层,至少指定一个中止
C++函数实参默认传值,函数接收的实参是原始值的拷贝,所有操作都只影响临时拷贝
若要修改原始值,使用指针
6 数组
C++默认将全局变量或数组初始化为零,但未初始化的局部变量将包含垃圾。
元素索引编号用于测量它到数组开头的距离
字符串和字符串数组
1.string类
#include<string>
二维数组:矩阵
7 指针
&:取址操作符
*:间接寻址操作符,表示指向的东西
地址本身不会改变
==&==在函数声明中代表引用,表示这是另一个变量的别名,引用参数对参数的操作是永久性的
为了允许函数操纵数据(传引用),需要传递一个地址
double_int(&n)
为了接收地址,声明指针类型的实参
void double_int(int *p)
数组名能单独使用,是一个常量,能转换为一个地址,可用它像指针变量赋值
int *p;
p=arr;->等价于 p=&arr[0]
p=arr+2;->等价于p=&arr[2]
c++将所有数组名都解释成地址表达式,如arr[2]被转换成:*(arr+2)
a[n]转换成指针引用*(a+n)
指针和其他地址表达式只能执行如下运算:
- 地址表达式±整数
- 整数±地址表达式
新地址=旧地址+(整数*基类型大小)
++p; //指向数组的下一个元素
在地址表达式上加减整数值,编译器自动使整数乘以指针基类型大小
在指针山加N,将生成距离原始指针N个元素的新地址
&arr[2]<&arr[3],相当于arr+2<arr+3,求值结果总是true
*p++=相当于*(p++)=0:先将*p设为0,再递增指针p来指向下一个元素
8 字符串:分析文本
字符\0是c++用于表示空(NULL)字符的方法,即ascii码值为0(0的ASCII值为48)
字符串处理函数,库cstring
strcpy(s1,s2):s2的内容拷贝到目标字符串s1
strcat(s1,s2):s2的内容连接到s1的末尾
strlen(s):返回字符串s的长度(不计空终止符)
strncpy(s1,s2,n):s2最多n个字符拷贝到s1
strncat(s1,s2,n):s2最多n个字符连接到s1末尾
getline方法获取整行输入,按Enter之前的输入的所有字符
cin.getline(指定字符串,最多要拷贝多少个字符(不计空终止符))
cin >> str1;
//最多获取第一个空白字符(空格,制表符,换行符)之前的数据
单字符和字符串
‘A’:单字符,将其替换为A的ASCII值,即转换为整数值65
”A“:长度为1的字符串,再数据区放两个字节:A的ASCII码值65,一个空终止字节,要转换为地址
单引号表达式在转换成ASCII码后被视为数值,不是数组
双引号表达式时字符数组,所以会转换为地址
strtok函数:分解字符串,token分隔符(定界符)
strtok(source_string,delims):根据由delims指定的定界符返回元字符串的第一个token
strtok(nullptr,delims):使用之前的strtok调用所指定的源字符串,获取下一个token。使用delims指定的定界符。
srotok通常返回指向token的指针,没有更多的token(子字符串)则返回空值
对于调用strtok报错如下:
C4996 ‘strtok’: This function or variable may be unsafe. Consider using strtok_s instead. To disable deprecation, use_CRT_SECURE_NO_WARNINGS. See online help for details.
解决办法:
解决This function or variable may be unsafe
string类
缺点是不兼容strtok函数,其只支持C字符串
#include <string> //支持新的string类
#inclde <cstring> //支持旧式字符串函数,例如strcpy()
string必须用std前缀来限定,如同cin/cout,除非添加using namespace std,否则要写成std::string
声明 string a;
初始化 string a(“afdshka”);
或者 a=“fsdajk”
string对象不需要调用库函数就能拷贝和比较
用==比较,返回true或false(比较C字符串则需要调用strcmp)
用=将一个string变量的数据拷贝给另一个,拷贝字符串内容而不是指针值
用+连接字符串
+能连接两个string变量,或连接一个string变量和一个C字符串,但不能连接两个C语言的字符串(字符串字面值仍是C语言的字符串),如:
string str=“the dog” + “is my friend”; 是错误的
getline(cin,指定字符串)
//读入string对象,不需要指定最大字符数,是全局函数而非成员
可以访问和C字符串一样的语法来访问string对象中的字符,string[index],使用基于0的索引
string对象使用size成员函数获取长度
调用string对象的c_str方法将string对象转换为C字符串
9 文件:电子存储
9.1 文件流对象
#include <fstream>
//创建文件流对象并和磁盘文件管理
ofstream fout(1.txt); //打开文件1.txt
fout << "hello";
fout.close();
ofstream | 文件输出流 |
---|---|
ifstream | 文件输入流 |
fstream | 泛化文件流(打开时必须指定输入或输出) |
引用磁盘文件
ofstream fout("C:\\users\\1.txt");
//使用了反斜杠记法,C++也支持用正斜杠(/)用作文件路径分隔符
char filename[FILENAME_MAX + 1];
//FILENAME_MAX是预定义常量,代表系统支持的文件名的最大长度(含路径名),分配FILENAME_MAX+1个字符保证字符串filename能容下任何游戏文件名
cout << "input filename:";
cin.getline(filename, FILENAME_MAX);
ofstream file_out(filename);
file_out.eof();//会在遇到文件尾时返回true
9.2 文本文件和二进制文件
文本文件 | 可像读写控制台那样读写这种文件。通常,写入文本文件的每个字节都是一个可打印的ASCII码。以文本模式打开文件,应使用与控制台通信一样的操作,这涉及到六操作符(<<,>>)和getline函数 |
---|---|
二进制文件 | 读写数据的实际数值,不涉及ASCII码。以二进制模式打开文件,只能使用成员函数read和write来传输数据(直接读/写操作)。 |
将255写入二进制文件时:11111111
写入文本模式:00110010 00110101 00110101,分别代表数字50,53,53,也就是字符2,5,5的ASCII码
fstream.read(addr,size) | 读 |
---|---|
fstream.read(addr,size) | 写 |
addr必须是char类型,需传递一个地址表达式(指针,数组名或是用&获得的地址),还需使用char强制类型转换来更改类型
例如,file1.write((char*) (&n), sizeof(n))
创建文件流对象时,可指定文本模式(默认)或二进制模式
如果使用文本模式,写入时每个换行符(ASCII 10)都转换为一对回车+换行符;读取时回车+换行符转换回换行符。
要以二进制随机访问模式打开为文件,需要使用ios::out和ios::binary,或者ios::in和ios::binary标志
随机访问模式允许直接跳至文件的任何位置,如果文件指针越过文件尾,文件自动增大
用seekp成员函数移动文件指针,该函数获取距离文件开头的偏移量(字节单位)作为参数
fbin.seekp(offset);
10 类和对象
面向对象编程 OOP
- 声明
class 类名{
int x,y;
};
类声明以分号结尾
类成员默认私有,不能从类的外部访问,若要可用,类至少包含一个公共成员(Public)
2.声明对象
引用对象:对象.成员
结构和类
C++语言的struct和class关键字等价,都可创建类
struct的成员默认公用
私有:仅成员可用(保护数据)
private
成员函数定义:
需要作用域前缀 类名::
类型 类名::函数名(参数列表){
语句
}
类外函数不能直接引用私有数据成员,但类内的成员函数可以,无论是否私有
引用成员函数:对象.函数(实参)
类必须先声明再使用。相反,函数定义可以放到程序的任何地方,但是必须放在类声明后面
Fraction
分数类Fraction。也称有理数类,改类存储两个数字来代表分子和分母
创建Fraction类时要限制对数据成员的访问,最起码要防止分母为零,也有必要对比值进行合理简化,确保每个有理数都有唯一表达式
内联函数
函数定义放到类声明中,即可使函数内联,这种函数定义不需要在末尾加分号,是成员声明
class Fraction{
private :
int num,den;
public:
void set(int n,int d){num=n;den=d;}
int get_num(){return num;}
int get_den(){return den;}
没有内联的函数仍需在程序内单独定义
内联函数 | 类的其他函数 |
---|---|
在类声明中就定义好了(而非仅是声明) | 在类声明外部定义,在类中给出原型 |
不需要作用域前缀 | 定义时要写作用域前缀 |
编译时函数主体就“内联”(插入)到代码中 | 运行时发出真正的函数调用,控制转至另一个代码位置 |
适合小函数 | 适合较长函数 |
有些限制,不可递归调用 | 无特殊限制 |
C++编译器每次遇到一个变量或函数名时:
1.在同一个函数中查找(如局部变量)
2.在同一个类中查找(如类的成员函数)
3.在函数或类的作用域中没有找到对应声明,就查找全局声明
包含自己项目文件中的声明要使用引号
#include "Fraction.h"
使用引号,C++编译器会先查找当前目录,其次才是查找标准include文件目录
如果函数返回类型是类,就必须返回该类的对象。可在函数定义中先成目该类的一个对象(作为局部变量),并在最后返回它
#include <cstdlib>
#include <string>
#include <iostream>
using namespace std;
class Fraction {
private:
int num, den; //num分子,den分母
public:
void set(int n, int d) { num = n; den = d; normalize(); }
int get_num() { return num; }
int get_den() { return den; }
Fraction add(Fraction other);
Fraction mult(Fraction other);
private:
void normalize(); //分数化简
int gcf(int a, int b); //gcf最大公因数
int lcm(int a, int b); //lcm最小公倍数
};
//没有内联的这三个私有函数仍需再程序某个地方单独定义
int main()
{
int a, b;
string str;
Fraction fract;
while (true) {
cout << "input num:";
cin >> a;
cout << "input den:";
cin >> b;
fract.set(a, b);
cout << "num is " << fract.get_num() << endl;
cout << "den is " << fract.get_den() << endl;
cout << "again (y or n)";
cin >> str;
if (!(str[0] == 'Y' || str[0] == 'y'))
break;
}
return 0;
}
//------------------------------------------------
//Fraction的成员函数
//Normalize(标准化):分数化简
void Fraction::normalize() {
//处理涉及0的情况
if (den == 0 || num == 0) {
num = 0;
den = 1;
}
//仅分子有符号
if (den < 0) {
num *= -1;
den *= -1;
}
//从分子分母中分解出gcf
int n = gcf(num, den);
num = num / n;
den = den / n;
}
int Fraction::gcf(int a, int b) {
if (b == 0)
return abs(a);
else
return gcf(b, a % b);
}
int Fraction::lcm(int a, int b) {
int n = gcf(a, b);
return a / n * b;
}
Fraction Fraction::add(Fraction other) {
Fraction fract;
int lcd = lcm(den, other.den);
int quto1 = lcd / den;
int quto2 = lcd / other.den;
fract.set(num * quto1 + other.num * quto2, lcd);
return fract;
}
Fraction Fraction::mult(Fraction other) {
Fraction fract;
fract.set(num * other.num, den * other.den);
return fract;
}
库cstdlib
atof():获取字符串输入并生成浮点(double)值
atoi():获取字符串输入并生成浮点int值
stoi():将字符串转换为整数
stof():将字符串转换为浮点数
库cctype
toupper©:c是小写就返回大写,否则原样返回
tolower©:c是大写就返回小写,否则原样返回
11 构造函数
构造函数本质是一个初始化函数
11.1 构造函数入门
类名(参数列表)
在类声明外面定义的构造函数:
类名::类名(参数列表){
语句
}
构造函数可以内联
多个构造函数(重载)
C++允许重用名称创建不用函数,用参数列表加以区别,构造函数也不例外
成员初始化
C++11:
每个构造函数都为指定数据成员分配指定的值,除非构造函数用自己的值覆盖
默认构造函数
每个类都应当有一个默认构造函数(即无参数构造函数),除非要求用户在创建对象时必须初始化
class Point {
private :
int x,y;
public:
Point(int new_x,int new_y){set(new_x,new_y);}
set(int new_x,int new_y);
int get_x();
int get_y();
};
该构造函数支持在声明对象的同时初始化:
Point a(1,2),b(1-,-20);
但现在声明对象而不提供参数就会出错
Point c; //错误!无默认构造函数!
11.2 引用变量和引用参数(&)
C++的引用(避免了使用指针语法):
int n;
int &r=n;
&在一个数据声明中使用,创建的是一个引用变量,该引用变量引用n,结果时改动r相当于改动n
引用和指针的共同点:建立了一种方式来引用现有数据项,而不是为新数据分配空间
11.3 拷贝构造函数
会自动调用拷贝构造函数的情况:
函数返回类类型的值
参数时类类型,会创建实参的拷贝并传给函数
使用一个对象初始化另一个对象,例如
Fraction a(1,20);
Fraction b(a);
声明拷贝构造函数:
类名(类名 const &来源)
const关键字确保实参不会被函数更改
上述语法使用了引用参数,函数获取对来源对象的引用,不是获取一个新的拷贝
class Point{
//....
public :
Point(Point const &src);
//...
};
Point::Point(Point const &src){
x=src.x;
y=src.y;
}
11.4 将字符串转换为分数的构造函数
Fraction ::Fraction(char *s){
int n=0;
int d=1;
char *p1=strtok(s,"/,");
char *p2=strtok(null,"/,");
if (p1)
n=atoi(p1);
ip(p2)
d=atoi(p2);
set(n,d);
12 两个完整的OPP例子
12.1 动态对象创建
指针还有一个用途:建立对象网络,这称为“动态内存分配”:在运行时请求内存,让程序判断何时分配新对象,而不是在程序运行前就固化内存需求
C++在运行分配内存最简单的方法时使用new关键字
ptr = new type;
type可以是内建类型(如int,double),也可以是用户自定义类型(比如类)。ptr是相应类型的指针。
假如Fraction类已声明好,以下语句创建Fraction对象并返回指向它的指针
Fraction *p = new Fraction;
对象本身无名(Fraction是类名而不是对象名)。这样通过指针操纵对象更为简单:
(*p).set(10,12);
(*p).set(2,7);
cout<<(*p).get_num();
cout<<(*p).get_den();
使用的语法
(*ptr).成员名
或是
ptr->成员名 //对ptr进行解引用类获得对象并访问指定成员
p->set(10,12);
p->set(2,7);
cout<<p->get_num();
cout<<p->get_den();
new 关键字有些变种,可指定实参来初始化对象,例如
Fraction *p= new Fraction(2,3);
该语句向匹配的构造函数传递实参2和3,找不到匹配的构造函数会报告语法错误
12.2 new和delete的其他用法
可用new创建一系列数据项,定义好的任何类型都允许,不管是内建类型还是用户自定义类型
int pInt = new int[10]; //分配10个int
Point pPt = new Point[50]; //分配50个Point
各种情况要指定大小,可以是常数或运行计算时计算的值(如变量)
分配好内存后,可通过由new返回并存储到指针中的地址来访问数据项,就像所有项都是某个数组的一部分
//初始化所有数据项
for (int i=0;i<10;++i){
pInt[i]=i;
}
for (int i=0;i<50;++i){
pPt[i].set(i,2);
}
最好显示回收请求的内存来防止内存泄漏。C++程序终止时,请求的所有内存都归还给系统。
delete关键字有两种形式,分配了多个项就用第二种。
每种形式都不是销毁指针,而是释放之前分配给该指针的内存
delete ptr;
delete [] ptr;
delete pNode; //删除一个节点
delete [] pInt; //删除全部10个int
12.3 二叉树应用
实例:如何获取一个名字列表并按字母顺序打印(有序二叉树)
C++标准模板库(STL)已在<set>和<map>模板中类中实现了二叉树,在实例中我们自己写
打印二叉树
打印子树所需的步骤(p指向根):
如指向的节点不为空,
打印左子树
打印当前节点的值
打印右子树
创建名字排序程序需设计并编码两个类:Bnode和Btree
Bnode类
建模节点的类,节点不含行动,是被动的
每个节点对象都需要三个公共成员:本身的字符串值以及指向左右两个子树的指针。
//二叉树的节点类
class Bnode {
public:
string val;
Bnode* pLeft;
Bnode* pRight;
Bnode(string s) { val = s; pLeft = pRight = nullptr; }
}
类不可以包含它自己的实例,但pLeft和pRight并非Bnode的实例,只是指向同一个类的其他对象的指针
该类无默认构造函数,用户不复制便不能创建节点
Bnode my_node("Emily"); //合法
该构造函数最大的好处是两个指针默认为空值(nullptr)
Btree类
二叉树类
// 二叉树类的声明,含辅助函数
class Btree {
public:
Btree() { root = nullptr; }
void insert(string s) { root = insert_at_sub(s, root); }
void print() { print_sub(root); }
private:
Bnode* root;
Bnode* insert_at_sub(string s, Bnode* p);
void print_sub(Bnode* p);
};
类的用户访问不了根,便无法直接访问任何节点
insert_at_sub和print_sub这两个辅助函数是递归的,不可内联,必须在类声明的外部定义
Bnode* Btree::insert_at_sub(string s, Bnode* p) {
if (!p)
{
return new Bnode(s);
}
else if (s < p->val) {
p->pLeft = insert_at_sub(s, p->pLeft);
}
else if (s > p->val) {
p->pRight = insert_at_sub(s, p->pRight);
}
return p;
}
void Btree::print_sub(Bnode* p) {
if (p)
{
print_sub(p->pLeft);
cout << p->val << endl;
print_sub(p->pRight);
}
}
int main() {
Btree my_tree;
string sPrompt = "input your name(Enter):";
string sInput = "";
while (true) {
cout << sPrompt;
getline(cin, sInput);
if (sInput.size() == 0)
{
break;
}
my_tree.insert(sInput);
}
cout << "排序后的名字:" << endl;
my_tree.print();
}
二叉树可以无限扩容,只受制于内存容量,在树中的访问时对数级增长
//核心:insert_at_sub函数,保证添加到书的字符串严格保持字母顺序
在p指向的子树中插入字符串所需要的步骤:
If p为NULL,
创造新节点并返回指向它的指针
Else If s “小于” 该节点的字符串:
在左子树插入s
Else If s “大于” 该节点的字符串:
在右子树插入s
返回p
如果目标字符串s此时按字母顺序既不大于也不小于当前节点的值,表面发现了一个匹配的字符串,函数直接返回,不创造新的节点
递归和迭代
迭代通常更高效
递归方案造成在每一级都发生一次函数调用,有时时解决问题的唯一实际方案,如汉诺塔问题
12.4 汉诺塔问题
一次只能移动一个盘,大盘不能在小盘上面
可创建对象(类的实例)数组
#include <iostream>
using namespace std;
#define MAX_LEVELS 10
//声明三个栈,每个栈都是包含圆盘大小编号的一个对象
//stacks[3]包含三个这样的对象
class Cstack {
public:
int rings[MAX_LEVELS]; //该数组容纳圆盘的大小编号
int tos; //栈顶索引
void populate(int size); //初始化栈
void clear(int size); //清除栈
void push(int n); //入栈
int pop(void); //出栈
}stacks[3];
void Cstack::populate(int size) {
for (int i = 0; i < size; i++) {
rings[i] = i + 1;
}
tos = -1;
}
void Cstack::clear(int size) {
for (int i = 0; i < size; i++) {
rings[i] = 0;
}
tos = size - 1;
}
void Cstack::push(int n) {
rings[tos--] = n;
}
int Cstack::pop(void) {
int n = rings[++tos];
rings[tos] = 0;
return n;
}
void move_stack(int n, int src, int dest, int other);
void move_a_ring(int source, int dest);
void print_stacks(void);
void pr_chars(int ch, int n);
int stack_size = 7;
int main()
{
stacks[0].populate(stack_size);
stacks[1].clear(stack_size);
stacks[2].clear(stack_size);
print_stacks();
move_stack(stack_size, 0, 2, 1);
return 0;
}
//移动栈,递归解题
//假设已经解决了N-1个盘的问题,在此前提下移动N个盘
//src=来源栈,dest=目标站
void move_stack(int n, int src, int dest, int other) {
if (n == 1) {
move_a_ring(src, dest);
}
else {
move_stack(n - 1, src, other, dest);
move_a_ring(src, dest);
move_stack(n - 1, other, dest, src);
}
}
//移动一个盘:从来源栈弹出盘,压入目标栈,打印新状态
void move_a_ring(int source, int dest) {
int n = stacks[source].pop();
stacks[dest].push(n);
print_stacks();
}
//打印栈,打印三个栈的每个物理层的圆盘
void print_stacks(void) {
int n = 0;
for (int i = 0; i < stack_size; i++) {
for (int j = 0; j < 3; j++) {
n = stacks[j].rings[i];
pr_chars(' ', 12 - n);
pr_chars('*', 2 * n);
pr_chars(' ', 12 - n);
}
cout << endl;
}
system("Pause");
}
void pr_chars(int ch, int n) {
for (int i = 0; i < n; i++) {
cout << (char)ch;
}
}
有时,应用程序需要输出大量数据,需暂停并提示用户继续,system(“PAUSE”);对于支持的系统很理想。如果不支持,或想写更好的可移植代码,可如下编码:
#include <string>
...
string dummy;
cout<<"按ENTER建继续.";
getline(cin,dummy);
13 用STL简化编程
C++——标准模板库STL
模板是可用来创建高级容器的泛化数据类型,例如,可用list模板创建整数、浮点甚至是自定义类型的链表
13.1 链表模板
STL提供了对 建立在其他类型基础上的集合(或容器)的广泛支持。
例如:
list<int> iList;
list<string> sList; //字符串列表
list<Fraction> bunch0Fract; //分数列表
基类型可以是任何基元类型(可以再代码中使用的最简单的构造就称为基元(例如int),其他构造都是它们复合而成的),也可以是自定义类型。
用list模板创建的链表能高效地执行插入和删除操作。
STL还支持其他泛化数据结构,包括vector(可无限增长的数组)以及set和map(基于二叉树构建)
所有STL名称都是std命名空间的一部分,意味着要添加std::
创建和使用列表类
使用列表模板前首先开启对它的支持:
#include <list>
using namespace std;
然后可以创建自己的链表类,用一下语法声明STL列表类:
list<类型> 列表名
//std::list<类型> 列表名
创建好的列表开始是空的,可用push_back函数在列表末端(back 的来历)添加元素,例如
list<string> LS;
LS.push_back("Able");
LS.push_back("Baker");
LS.push_back("Charily");
push_front成员函数则可以将元素添加到列表前端。
list<string> LS;
LS.push_front("Able");
LS.push_front("Baker");
LS.push_front("Charily");
两者添加完顺序不同
可以向数组那样,使用逗号分隔的列表来初始化包括列表在内的大多数STL容器
list<int> iList={1,2,3,4,5];
创建和使用迭代器
STL的许多模板都使用迭代器,从而一次访问一个列表元素(称为遍历)
迭代器外观和使用都像指针,使用++,–和*操作符(有区别)
声明迭代器
list<类型>::iterator 迭代器名
如下声明一个列表和对应的迭代器:
list<string>LS;
list<string>::iteractor iter;
现在就可以使用iter遍历LS列表,因其类型一致(string)
STL提供begin和end函数返回指向列表头尾的迭代器
list<string>::iteractor iter=LS.begin();
正确初始化的iter可像指针一样使用。递增操作符++使iter指向下一项
++iter; //在列表中前进一个元素
和指针一样,用间接寻址操作符(*)访问迭代器指向的数据:
cout << *iter<<endl; //打印指向的字符串
用一个循环可以打印所有列表元素
end成员函数生成的迭代器指向最后一个元素之后的位置,而非指向最后一个元素本身
只要没有抵达LS.end(),循环就继续
iter = LS.begin();
while (iter != LS.end()){
cout<<*iter<<endl;
++iter;
}
或使用for循环
for(iter = LS.begin();iter!=LS.end();++iter){
cout<<*iter<<endl;
}
使用基于范围的for打印列表
for (auto x:LS){
cout << x <<endl;
}
STL类提供了内建sort函数:
LS.sort(); //按字母顺序对列表排列
为支持列表的sort()和其他成员函数,列表的基类型必须为小于操作符(<)、=、==定义合理行为
连续排列函数
大型数据库的更好方案是始终维持数据的排序状态,每个新元素都添加到它的正确排序位置
每次添加时首先判断正确的字母顺序位置,然后,insert函数在迭代器指向的元素前插入一个新元素
for(iter=LS.begin();iter!=LS.end()&&s>*iter;){
++iter;
}
LS.insert(iter,s);
13.2 设计RPN计算器
Reverse Polish Notation(RPN,逆波兰记法):获取任意复杂度的一个输入行,分析它,并执行所有指定的计算
语法规则:
表达式→数值字面值
表达式→ 表达式 表达式 操作符
RPN等价于中缀记法
支持的操作符:+,-,*,/
为RPN使用栈
STL栈是一个典型后入先出(LIFO)机制
策略:
1.程序读取数字时把它压入栈顶(push)
2.程序读取操作符时,从占中弹出两值,计算结果,再将结果压回栈
常规STL栈类简介
使用栈模板,需添加如下指令开启对他的支持:
#include<stack>
然后就可以采用STL列表相似的语法创建一个常规的栈机制
stack <类型> 栈名
//std::
插入元素要用push成员函数
常用的stack成员函数
栈类函数 | 说明 |
---|---|
stack.push(data) | 将数据(具有栈的基础类型)压入栈顶 |
stack.top() | 从栈顶返回数据但不删除,删除要用pop |
stack.pop() | 删除栈顶项(但不返回他的值) |
stack.size() | 返回栈中当前所有项的数量 |
stack.empty() | 空栈返回true;否则返回false |
STL栈的设计将出战分为两步操作,为实现“返回栈顶项并删除”,需同时执行 top和pop两个操作
int n = stack_ints.top();//拷贝栈顶项
stack_ints.pop(); //删除栈顶项
13.3 正确解释尖括号
在C++中,两个右尖括号之间需插入空格,否则会解释为右移位操作符
list <stack <int> >list_of_stacks;
C++11和后续版本不需要添加空格,可以根据上下文正确解释两个右尖括号
对于空栈执行出栈操作时严重错误,所以务必先调用size或empty函数来检查
14 面向对象的三门问题
14.1 逻辑推理
蒙提霍尔悖论
两组数据:
大奖和安慰奖
门的状态,包括有大奖的那扇门的标识以及先打开哪扇门进而将其排除
创建2个类:
1.PrizeManager (奖品管理):包含一个构造函数和两个公共函数,奖品列表本身作为成员函数中的局部数据来维护
2.DoorManager (门管理):start_new_game判断三扇门中哪一扇是大奖(0,1,或2);set_sel_door函数登记用户的选择,据此判断备选门(altDoor)和“坏”门(badDoor);调用query_door函数判断该选择是否中了大奖
数据成员
winDoor:包含大奖门编号、DoorManager对象在每次新游戏前随机选择该编号
selDoor:用户最初选择的门
badDoor:指定“坏”门,用户做出最终选择前该门被打开以揭示安慰奖
altDoor:指定“备选”门。
#include <string>
#include <iostream>
#include <ctime>
#include <cstdlib>
using namespace std;
class PrizeManager {
public:
PrizeManager() { srand(time(nullptr)); }
string get_good_prize();
string get_bad_prize();
};
//const关键字确保实参不会被函数更改
string PrizeManager::get_good_prize() {
static const string prize_list[5] = {
"A new car",
"Many dollars",
"Travel Europe",
"An apartment",
"Tea with queen"
};
//获取prize_list的总大小,除以一个string对象的大小,即可得到元素数量
int sz = sizeof(prize_list) / sizeof(string);
return prize_list[rand() % sz];
}
string PrizeManager::get_bad_prize() {
static const string prize_list[8] = {
"Twice lunch",
"A box of Fish",
"A vist from xiaochou",
"Stay in school fornight",
"A ten-year-old VCR",
"A funny class",
"A analyze from xiaochou",
"One Day Happy in City"
};
int sz = sizeof(prize_list) / sizeof(string);
return prize_list[rand() % sz];
}
class DoorManager { //负责选择坏门
public:
DoorManager() { srand(time(nullptr)); }
void start_new_game();
void set_sel_door(int n);
int get_alt_door() { return altDoor + 1; }
int get_bad_door() { return badDoor + 1;}
bool query_door(int n) { return n == (winDoor+1); }
private:
int winDoor;
int selDoor, altDoor, badDoor;
};
void DoorManager::start_new_game() {
winDoor = rand() % 3;
}
void DoorManager::set_sel_door(int n) {
selDoor = n - 1;
if (selDoor == winDoor) {
if (rand() % 2) {
//随机true或false
altDoor = (selDoor + 1) % 3;
badDoor = (selDoor + 2) % 3;
}
else {
badDoor = (selDoor + 1) % 3;
altDoor = (selDoor + 2) % 3;
}
}
else {
//否则(选中的门不是大奖门)
//备选门是大奖门
altDoor = winDoor;
//{0,1,2}中不等于selDoor或altDoor的编号赋给badDoor
badDoor = 3 - selDoor - altDoor;
}
}
void play_game();
int get_number();
PrizeManager prize_mgr;
DoorManager door_mgr;
int main() {
cout << "Welcome TO Good Deal, Bad Deal!" << endl;
cout << "I am Month Schmall." << endl;
string s;
while (true) {
play_game();
cout << "again?(Y or N): ";
getline(cin, s);
if (s[0] == 'N' || s[0] == 'n') {
break;
}
}
return 0;
}
void play_game() {
string s;
cout << "choose a door between these three doors: " << "(1,2,3)?";
int n = get_number();
door_mgr.set_sel_door(n);
cout << "before I open this door," << "I want open a door that you not choose." << endl;
cout << "behind " << door_mgr.get_bad_door() << "th door is " << prize_mgr.get_bad_prize() << endl<<endl;
cout << "now, you want choose " << n << "th door" << " to " << door_mgr.get_alt_door() << "th door? (Y or N): ";
getline(cin, s);
if (s[0] == 'Y' || s[0] == 'y') {
n = door_mgr.get_alt_door();
}
cout << endl << "ok, u get it....";
if (door_mgr.query_door(n)) {
cout << prize_mgr.get_good_prize();
}
else {
cout << prize_mgr.get_bad_prize();
}
cout << endl<<endl;
}
int get_number() {
string sInput;
while (true) {
getline(cin, sInput);
int n = stoi(sInput);
if (n >= 1 && n <= 3) {
return n;
}
cout << "input number between 1-3, please reinput : ";
}
}
可用sizeof操作符让编译器判断数组大小,使得程序更容易维护,还消除了一个错误源
int sz = sizeof(prize_list) / sizeof(string);
prize_list[rand() % sz];
若是想要PrizeManager避免重复一样的奖品,则需要采用“洗牌模式”,即从奖品列表中选择元素,直到用完所有元素,此时PrizeManager对象将自动洗牌(重置列表)
洗牌算法:
For I = N-1 Down to 2
J = Random 0 to I
Swap array[I] and array[J]
算法在正确编码后,会包含从0到N-1的数字的一个数组
改进PrizeManager
shuffle函数对该索引数组进行随机化处理,然后用该数组从奖品列表中选择
class PrizeManager {
public:
PrizeManager() { srand(time(nullptr)); }
string get_good_prize();
string get_bad_prize();
private:
int good_array[5];
int bad_array[8];
int good_index;
int bad_index;
void shuffle(int* p, int n); //shuffle函数对该索引数组进行随机化处理,然后用该数组从奖品列表中选择
};
PrizeManager::PrizeManager() {
srand(time(nullptr));
for (int i = 0; i < 5; ++i) {
good_array[i] = i;
}
for (int i = 0; i < 8; ++i) {
bad_array[i] = i;
}
good_index = bad_index = 0;
shuffle(good_array, 5);
shuffle(bad_array, 8);
}
string PrizeManager::get_good_prize() {
if (good_index >= 5) {
shuffle(good_array, 5);
good_index = 0;
}
static const string prize_list[5] = {
"A new car",
"Many dollars",
"Travel Europe",
"An apartment",
"Tea with queen"
};
return prize_list[good_array[good_index++]];
}
string PrizeManager::get_bad_prize() {
if (bad_index >= 8) {
shuffle(bad_array , 8);
bad_index = 0;
}
static const string prize_list[8] = {
"Twice lunch",
"A box of Fish",
"A vist from xiaochou",
"Stay in school fornight",
"A ten-year-old VCR",
"A funny class",
"A analyze from xiaochou",
"One Day Happy in City"
};
return prize_list[bad_array[bad_index++]];
}
void PrizeManager::shuffle(int* p, int n) {
for (int i = n - 1; i > 1; --i) {
int j = rand() % (i + 1);
int temp = p[i];
p[i] = p[j];
p[j] = temp;
}
}
15面向对象的扑克牌游戏
15.1 规则
葫芦:三条加一对,如AAA55,比四条小
同花:五张牌皆属于同一花色,比葫芦小
顺子:五张牌连续,比同花小,比顺子大
两对:比一对大,比三条小
同花顺
同花大顺:AKQJ10,同一花色,除五条外最大的
开发类:
Deck(牌墩/一副牌):调用deal_a_card()(返回card对象)来创建5个card对象,然后向每个对象发出指令:打印自己;主要职责:在需要时洗牌,在类的用户前隐藏该细节,并在需要时发一张牌
Card(牌张/一张牌):rank(点数)和suit(花色),支持display函数,为数据结构赋予一定的智能
#include 支持swap函数,交换各个实参的值(类型相同)、
以及random_shuffle函数
random_shuffle(beg_range,end_range);
打乱某个元素的范围,beg_range指向集合(如数组)范围起点的一个迭代器或指针,end_range指向范围终点
15.2 vector模板
STL提供vector(向量),可以无限增大
声明向量后,可调用push_back函数来添加函数
可像数组那样对向量进行索引,使用size函数来获取长度,clear()函数清除内容
#include <string>
#include <iostream>
#include <ctime>
#include <cstdlib>
#include <algorithm> //算法,使用swap()
#include <vector>
using namespace std;
class Card {
public:
Card() {}
Card(int r, int s) { rank = r; suit = s; }
int rank;
int suit;
string display();
};
string Card::display() {
static const string aRanks[] = { "2","3","4","5","6","7","8","9","10","J","Q","K","A" };
static const string aSuits[] = { "梅花","方块","红桃","黑桃" };
return aSuits[suit] + aRanks[rank] + ".";
}
class Deck {
public:
Deck();
Card deal_a_card();
private:
int cards[52];
int iCard;
void shuffle();
};
Deck::Deck() {
srand(time(nullptr));
for (int i = 0; i < 52; ++i) {
cards[i] = i;
}
shuffle();
}
void Deck::shuffle() {
iCard = 0;
for (int i = 51; i > 0; --i) {
/*
int j = rand() % (i + 1);
swap(cards[i], cards[j]); */
random_shuffle(cards, cards + 52);
}
}
Card Deck::deal_a_card() {
if (iCard > 51) {
cout << endl << "正在重新洗牌..." << endl;
shuffle();
}
int r = cards[iCard] % 13;
int s = cards[iCard++] / 13;
return Card(r, s);
}
class Eval {
public:
Eval(Card* pCards);
string rank_hand();
private:
int rankCounts[13];
int suitCounts[4];
int has_reps(int n);
bool is_straight();
bool verify_straight(int n);
bool is_flush();
bool is_two_pair();
};
Eval::Eval(Card* pCards) {
for (int i = 0; i < 13; ++i) {
rankCounts[i] = 0;
}
for (int i = 0; i < 4; ++i) {
suitCounts[i] = 0;
}
for (int i = 0; i < 5; ++i) {
int r = pCards[i].rank;
int s = pCards[i].suit;
++rankCounts[r];
++suitCounts[s];
}
}
string Eval::rank_hand() {
string s;
if (is_straight() && is_flush()) {
if (rankCounts[12] && rankCounts[11] ){
s="你的牌是同花大顺(Royal Flush)! 奖金=800";
}
else {
s="你的牌是同花顺(Straight Flush)! 奖金=50";
}
}
else if (has_reps(4)) {
s="你的牌是四条(Four Of A Kind)! 奖金=25";
}
else if (has_reps(3) && has_reps(2)) {
s = "你的牌是葫芦(Full House)! 奖金=9";
}
else if (is_flush()) {
s = "你的牌是同花(Flush)! 奖金=6";
}
else if(is_straight()) {
s = "你的牌是顺子(Straight)! 奖金=4";
}
else if (has_reps(3)) {
s = "你的牌是三条(Three Of A Kind)! 奖金=3";
}
else if (is_two_pair()) {
s = "你的牌是一对(Two Pair). 奖金=2 ";
}
else if (has_reps(2)) {
s = "你的牌是一对(Pair). 奖金=1";
}
else {
s = "你的牌是无对的(No Pair). 奖金=0";
}
return s;
}
int Eval::has_reps(int n) {
for (int i = 0; i < 13; ++i) {
if (rankCounts[i] == n)
{
return true;
}
}
return false;
}
//判断是不是顺子
//查看牌点中的“单牌”,验证这张牌是不是开始一个顺子
bool Eval::is_straight() {
for (int i = 0; i <= 8; ++i) {
if (rankCounts[i] == 1) {
return verify_straight(i);
}
}
return false;
}
bool Eval::verify_straight(int n) {
for (int i = n + 1; i < n + 5; ++i) {
if (rankCounts[i] != 1) {
return false;
}
}
return true;
}
//同花
bool Eval::is_flush() {
for (int i = 0; i < 4; ++i) {
if (suitCounts[i] == 5) {
return true;
}
}
return false;
}
//两对
bool Eval::is_two_pair() {
int n = 0;
for (int i = 0; i < 13; ++i) {
if (rankCounts[i] == 2) {
++n;
}
}
return n == 2;
}
Deck my_deck;
Card aCards[5];
bool aFlags[5];
vector<int> selVec;
void play_game();
bool draw();
int main() {
string s;
while (true) {
play_game();
cout << "again?(Y or N):";
getline(cin, s);
if (s[0] == 'N' || s[0] == 'n') {
break;
}
}
return 0;
}
void play_game() {
for (int i = 0; i < 5; ++i) {
// Card crd=my_deck.deal_a_card();
aCards[i] = my_deck.deal_a_card();
aFlags[i] = false;
cout << i + 1 << ". ";
cout << aCards[i].display() << endl;
}
cout << endl;
//抽牌并重新展示
if (draw()) {
for (int i = 0; i < 5; ++i) {
cout << i + 1 << ". ";
cout << aCards[i].display();
if (aFlags[i]) {
cout << " *";
}
cout << endl;
}
cout << endl;
}
Eval my_eval(aCards);
cout << my_eval.rank_hand() << endl;
}
bool draw() {
string sInput;
selVec.clear();
cout << "输入要重抽的牌的编号:";
getline(cin, sInput);
if (sInput.size() == 0) {
return false;
}
//读取输入字符串,在selVec中为读取的每个数字都添加一个元素
for (int i = 0; i < sInput.size(); ++i) {
int n = sInput[i] - '0';
if (n >= 1 && n <= 5) {
selVec.push_back(n - 1);
}
}
//为selVec中的每个数字0-4重新抽取对应的牌
for (int i = 0; i < selVec.size(); ++i) {
int j = selVec[i];
aCards[j] = my_deck.deal_a_card();
aFlags[j] = true;
}
return true;
}
总结:
- 对象类型(即类)可以像其他任意类型(比如基元类型)那样作为返回类型,需将其放在函数声明的开头
- 从函数返回对象是,通常要调用类的构造函数
- 可声明并实例化数组,这种数组甚至可以放到其他类声明中,从而创建包含其他对象的实例
- 可直接用swap或random_shuffle算法而不必自己写,要使用C++STL提供的某个算法,需在程序开头添加
#include <algorithm>
- vector模板