C++基础教学(含程序样例)

解决闪现问题
在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)

指针和其他地址表达式只能执行如下运算:

  1. 地址表达式±整数
  2. 整数±地址表达式
    新地址=旧地址+(整数*基类型大小)

++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

  1. 声明
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;
}

总结:

  1. 对象类型(即类)可以像其他任意类型(比如基元类型)那样作为返回类型,需将其放在函数声明的开头
  2. 从函数返回对象是,通常要调用类的构造函数
  3. 可声明并实例化数组,这种数组甚至可以放到其他类声明中,从而创建包含其他对象的实例
  4. 可直接用swap或random_shuffle算法而不必自己写,要使用C++STL提供的某个算法,需在程序开头添加
    #include <algorithm>
  5. vector模板
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值