//数组在初始化时数组大小只能写常量
int arr[10] = { 0 }; //arr有10位,并且全为0
int arr[n] = {0}; //不合法
//但可以这样
int* arr = new int[n] //开辟一个n个int空间的地址,让arr作为指针来接
int* arr = {0,1,2,3}; //不合法,只能接一个0
char* arr = {"hello"}; //不合法,"hello"这时一个常量,不能用指针指向
const char* arr = {"hello"} //合法,规定指针不改变常量的值
//"hello"是一个常量,执行这句是"hello"被分成'h','e','l','l','e'放在例如0x00,0x01,0x02,0x03,0x04这五个地址上,arr作为一个指针指向0x00('h'),编译器会理解arr = "hello"
char arr[] = {"hello"} //合法,是一个字符数组
//即使是c风格的字符串(字符数组),在末尾也会加上\0,(int *arr or int arr[])
当数组作为形参时:
一.引用传递
1. type fun(int *&arr....) arr作为左值引用,当函数被调用时值传递为
int *&arr = array (左右类型都是int*,左值是右值的别名)
2. (int (&arr)[10],.....) &arr要阔起来因为优先级不够,[]中必须填写一个常量
二.指针传递
3.(int arr[], ....) 一维数组传递,虽然数组不允许拷贝和复制,但是在形参中数组退化成为一个指针
4. (int * arr .....) 指针传递,arr指向数组首地址
右左原则:首先找到变量名,先往右看,当右边没有的话或者出现“)”的时候往左看,当左边没有的话或者出现“(”,循环这个过程,直至读完变量或者函数的声明:
int a; //a是一个int类型的变量
int *a; //a是一个指针,指向int型
int a(); //a是一个函数,返回值是int
int (*a)(); //a是指向一个返回int型的函数的函数指针
int a[5]; //a是一个数组,有5个空间,每一个空间大小(类型)是int
int *a[5]; //a是一个数组,有5个空间,每一个空间是一个指向int类型的指针(a是一个存储5个指针的数组)
int (*a)[5]; //a是一个指针,有5个空间,每一个空间大小是int(a一个是指向一个5个int类型的数组的指针)
int (*a[])(); //a是一个数组,数组装着指针,每个指针指向一个返回值为int型的函数
int (*(*a)[])(); //a是一个指针,指向一个数组,数组里装的都是指针,每个指针指向一个返回类型为int的函数(a是一个指向int型函数指针组成的数组的指针)
int const a; //a是一个int类型是的常量
const int a; //int const a 等价与 const int a
int const *a; //a是一个指向一个int常量的指针,意味着a不是常量,可以给a赋值让a指向其他地方,*a(解引用)才是常量
int *const a; //a是一个常量指针,指向一个int类型,意味着指针a不可改变,在初始化后就一直指向这个地址,但是指向的int类型数据(*a)可以改变,
两种写法:
返回值 operator运算符(左参数,右参数){
实现体
}
返回值 operator运算符(运算符右边参数的引用){
实现体
}
第二种写法一般写在类里面,因为可以用this来指代左边的参数
命名的强制类型转换:
name-cast(表达式);
double a;
const char* p;
static_cast(a); //不支持不想关类型转换,比如一个类转换成一个int等,其他都支持即使是把子类强转为父类,(但是不安全,可能会越界)
const_cast(p); //只能去掉const和加上const
dynamic_cast (new Son); //只支持安全的类型转换,即把子类转换成父类,其他的都不支持
reinterpret_cast; //最不安全的,不支持基本类型转换,其他的都支持
#define NUM 10
const int NUM = 10;
用#define NUM 10 的时候在预处理的时候NUM已经被替换成10了,NUM就没有被编译器看到过,当我们得出一些编译错误信息的时候,可能会带来一些困惑,如果这个NUM不是写在你的头文件里你你们不知道这个10代表什么意思,解决方法是用常量替代上面的宏
const有类型,可进行编译器类型安全检查,#define无类型,不可进行类型检查
const有作用域,而#define不重视作用域,宏不能作为命名空间,结构体,类的成员
namespace A{
#define NUM 100 //无效,宏不能作为结构体,命名空间,类多成员
const int NUM1 = 100;
}
cout<
左值:可以放在等号左边的,能够取地址,具有名字
int a = 0; //变量名,a是左值
void func(int &a) //左值引用的函数调用,a是左值
++a;--a; //前置自增或自减,因为++a是先算a+1,赋值给a,这个a是可以取地址的,所有是左值
++a = 10; //所有此式可以通过编译
(a = 9) = 100; //赋值运算或者复合赋值运算也可以当作左值
A *a = &p;
*a = xxx; //解引用也是左值
右值:只能放在右边的值,不能取地址,不具备名字
a = 9 //字面值,右值
a = Max(i,j); //返回非引用函数调用,右值
a++;a--; //后置自增或自减,右值,因为是先算a+1,然后把a+1这个值返回出去,而这个值无地址无名是个右值
a = b - c; //算术表达式,右值
a = b && c; //逻辑表达式,右值
当右值被作为拷贝函数参数时,将被作为将亡值,如果类中有移动拷贝构造时优先调用移动拷贝构造,否则调用拷贝构造
#include
using namespace std;
class A {
public:
A() {
cout << "AStruct " << this << endl;
}
~A() {
cout << "~A" << endl;
}
A(const A&) { //拷贝构造
cout << "cpStruct" << this << endl;
};
//A(A&&) { //移动拷贝构造,参数列表中写的A&&是右值引用
// cout << "moveStruct" << this << endl;
//}
};
A test() {
A temp;
return temp;
}
int main() {
A a = test(); //函数调用返回值是一个右值,此时A类中移动拷贝构造被注释,此时调用的是拷贝构造
return 0;
}
运行结果如下
AStruct 000000A466EFFBA4
cpStruct000000A466EFFCE4
~A
~A
如果把注释消掉,运行结果如下:
AStruct 000000CA73F1F804
moveStruct000000CA73F1F944
~A
~A
此时调用的是移动构造
用右值来调用拷贝构造时,右值被称为将亡值,目的是触发移动构造或移动赋值构造,并进行资源转移,之后调用析构函数(资源转移后调用析构,所以被称为将亡值)
int a = 100;
int& b = a;
const int& c = 10;
int&& d = 10; //右值引用,指向右值,但是d是一个左值
d++;
cout << d << endl;
int&& e = move(a); //右值引用,运用move把左值a变成一个右值,但是e是一个左值
e++;
cout << e << endl;
运行结果如下:
11
101
常量也可以通过右值引用进行修改
声明出来的左值引用或者右值引用都是左值
左值引用时对左值的引用,右值引用(c++11新特性)是对右值的引用
也有特例,左值引用也可以引用右值:const 左值引用 可以引用右值,但是这个局限,不能修改这个值(但是我们用引用的目的是通过引用修改变量,所以这个方法没什么用,所以c++11引入了右值引用来解决这个问题,让右值也可以被修改),
右值引用也可以这样引用左值:用std::move(lvalue)函数,这个函数可以把左值转化成右值,如果这个左值的类型实现了移动构造或者移动赋值构造,则这个右值即使一个将亡值
左值引用避免了对象的拷贝,比如再函数传参用引用起别名来代替值传递
右值引用实现了移动语义和实现完美转发
移动语义:
#include
using namespace std;
class A {
public:
int* p;
A() {
p = new int(10);
cout << "A():p=" << p << endl;
}
~A() {
delete p;
p = nullptr;
cout << "~A()" << endl;
}
A(const A& a) { //深拷贝,重新分配资源
//p = a.p; //当不适用拷贝构造直接进行赋值的话
//a.p = nullptr; //对a的p指针置空会报错,因为a是一个常量引用,不可改变,所以a.p会存在出现野指针的问题,会导致内存泄漏
p = new int(10);
memcpy(p, a.p, sizeof(int));
cout << "cpStruct:p=" << p << endl;
};
/*A(A&& a) {
this->p = a.p;
a.p = nullptr;
cout << "moveSturct:p=" << endl;
}*/
};
int main() {
A a;
A b(a);
return 0;
}
运行结果如下:
A():p=000001F7EA9A71F0
cpStruct:p=000001F7EA9A6AF0
~A()
~A()
这个时候a作为左值调用的是拷贝构造,在堆里new了一个与原对象一样的对象,是深拷贝,所以两个对象地址不一样
如果改成这样
#include
using namespace std;
class A {
public:
int* p;
A() {
p = new int(10);
cout << "A():p=" << p << endl;
}
~A() {
delete p;
p = nullptr;
cout << "~A()" << endl;
}
A(const A& a) {
p = new int(10);
memcpy(p, a.p, sizeof(int));
cout << "cpStruct:p=" << p << endl;
};
A(A&& a) {
this->p = a.p;
a.p = nullptr;
cout << "moveSturct:p=" << p << endl;
}
};
int main() {
A a;
A b(move(a)); //让a变成一个右值,实现右值引用,在类里会调用移动构造而不是拷贝构造
cout << a.p << endl;
return 0;
}
结果如下:
A():p=0000024A277E6CB0
moveSturct:p=0000024A277E6CB0
0000000000000000
~A()
~A()
此时b与之前的a完全一样,而a在调用完移动构造后销毁.
当用move把a变成右值是,a变成将亡值,会触发移动构造,即在消亡之前把a赋给b,完成后自己灭亡
相比与拷贝构造,移动构造性能更强,类似与两个冰箱,a冰箱有一只大象,冰箱也想要有一只大象,拷贝构造的做法是,在外界再找一个一摸一样的大象放在冰箱里,而移动构造的做法是,把a中大象移动到b冰箱去
移动语义的用法:
对象赋值时避免资源重新分配
stl容器应用:
vector vec;
vec.push_back(A());
A()作为一个右值,在push_back(A a)函数中形参进行值传递时 A a = A();触发了移动构造,直接把A()的值让vector容器接管,相比之前效率更高
完美转发:不仅能准确的转发参数的值,还能保证被转发的参数左右值属性不变
void func(int &n){
cout<<"lvalue="<
void revoke(T &&t){ //T &&t或者auto &&t是万能引用,可以使接左值也可以接右值
func(forward(t)); //forward(t)保持参数的左右值属性不变
}
int main(){
int i = 10;
revoke(i); //i作为一个左值调用revoke
revoke(10); //10作为一个右值调用revoke
return 0;
}
结果如下
lvalue=10
rvalue=10
如果是一个不具体的类型加上&&(此例中是 T &&t)那这个引用时万能引用,如果时具体的类型比如int &&t就是右值引用,forward()是保持这个值的左右值属性不变,如果参数是左值则会转换成T类型的左值,如果参数是右值,则会转换T类型的右值
在c++11之前,指针管理有几个困境
资源释放了,指针没有置空
1. 野指针:指针指向堆中一个地址,这个地址delete掉了,但是指针没有制空(赋nullptr)
悬挂指针:多个指针指向一个堆里的一个地址,其中一个指针把这个地址delete掉了,也置空了,但其他的指针不知道,其他指针就是悬挂指针
踩内存:当上面两种情况下,野指针或者悬挂指针指向的地址被其他进程重新使用,这些指针可以对意料之外的指向的地址操作,这叫做踩内存
没有释放资源产生内存泄漏
new与delete次数不匹配
次数匹配,但在运用多态时,用父类构建子类对象时,父类没有虚析构,析构函数就静态绑定了父类析构,子类生命周期结束时,只会调用父类析构,子类的属性就得不到释放造成了内存泄漏
#include
using namespace std;
class A {
int i;
public:
~A() {
cout << "父类析构调用" << endl;
}
};
class B :public A {
int j;
public:
~B() {
cout << "子类析构调用" << endl;
}
};
int main() {
A* b = new B();
delete b;
return 0;
}
运行结果如下:
父类析构调用
应改成虚析构,在父类析构前加入virtual,让他在运行时绑定子类析构,因为子类析构结束后会调用父类析构
#include
using namespace std;
class A {
int i;
public:
virtual ~A() {
cout << "父类析构调用" << endl;
}
};
class B :public A {
int j;
public:
~B() {
cout << "子类析构调用" << endl;
}
};
int main() {
A* b = new B();
delete b;
return 0;
}
子类析构调用
父类析构调用
重复释放资源,引发coredump
解决方案:智能指针:
string库内部的声明如下
//构造函数
string(); //创建一个空的字符串
string(const string& str); //拷贝构造,用一个string对象初始化
string(const char* s); //用一个字符串初始化
string(int n,char c); //初始化字符串为n个字符c
//基本赋值操作
string& operator=(const char* s); //重载=号,结果返回这个这个对象的引用,所有可以链式操作,就像cout一样在这里是(s = "1234") = "asd";不报错,因为完成等号后的值返回的是一个引用,是个左值
string& operator=(const string& s);
string& operator=(char c);
string& assign(const char* s); //字符串赋值赋值
string& assign(const string &s);
string& assign(const char* s ,int n); //把s字符串前n个赋给此对象
string& assign(int n ,int c); //用n个字符c赋值给此对象
string& assing(const string&s,int start,int n); //将s的从start开始的n个字符赋值给对象
//字符串的存取操作
char& operator[](int n); //通过[]来取字符,这个可能会数组越界
char& at(int n); //通过at方法获取字符,内涵异常处理不会越界
//字符串的拼接操作
string& operator+=(const string& str); //重载+=,实现拼接
string& operator+=(const char* s);
string& operator+=(char c);
string& append(const char*s); //将s追加到该字符串尾部
string& append(const char*s,int n); //把s的前n个字符追加到字符串尾部
string& append(const string*s);
string& append(const string*s,int pos,int n);//把s从pos开始的n个字符追加到字符串上
string& append(int n ,char c); //追加n个字符c
//字符串的查找替换
int find(const string& str,int pos = 0) const; //查找第一次出现str的位置,pos为参数默认为0,,结尾加cosnt代表这是个只读函数,不会改变数据成员,可以提高程序健壮性
int find(const char* s,int pos = 0) const;
int find(const char* s,int pos = 0, int n) const; //查找前n个字符第一次出现s的位置
int find(char c,int pos = 0) const; //查找字符c第一次出现的为
int rfind(const string& str,int pos = npos) const; //逆序找
int rfind(const char* s,int pos = npos) const;
int rfind(const char* s,int pos =npos, int n) const;
int rfind(char c,int pos = npos) const;
string& replace(int pos,int n,const string& str); //在字符串从pos开始后n位变成str
string& replace(int pos,int n,const char* str);
//大小比较
bool operator>(const string&s); //> < == != 都可用
......
int compare(const string &s)const; //按字符编码排序,大于返回1小于返回-1,=返回0
int compare(const char* s)const;
//提取子串
string substr(int pos = 0, int n = nops) const; //返回前n个字符组成的字符串
//插入和删除
string& insert(int pos, const char* s); //插入字符串
string& insert(int pos, const string& s);
string& insert(int pos, int n, char c); //在指定位置插入n个字符c
string& erase(int pos, int n = npos); //删除从pos开始的n个字符
vector
在stl中除了string之外的所有容器都是类模板
vector是动态数组,每当size()的返回值(即容器的大小)即将超过容器容量时,capactiy()返回值(容量)会自增一倍(0,1,2,4,8,16,32…),当这个容器后面的空间没有需要动态增加的大小时,它的做法是找一个更大的内存空间,然后把原数据拷贝到新空间,并释放原空间
vector v;
vector::iterator it; //it是存放int类型vector容器的迭代器
int i;
v.push_back(i);
v.pop_back(i);
v.front();
v.back();
v.begin(); //得到容器的起始迭代器指向首元素
v.end(); //得到容器的尾迭代器指向尾元素的下一个位置,
it++; //迭代器重载了++ --表明迭代器到下一个迭代器的位置
*it //代表it迭代器指向的对象
函数API
//构造函数
vector v;
vector(v.begin(),v.end());//可以这样int arr[] = {1,2,3};vector;v(arr,arr+sizeof(arr)/sizeof(int));
vector(n,elem); //拷贝n个elem给自身
vector(const vector &vec);
//赋值
assign(beg,end); //将[beg,end)区间的数据赋值自身
assign(n,elem);
vector& operator=(const vector &vec); //重载=
swap(vec); //将vec与本容器内容互换
size(); //元素个数
empty(); //是否为空
resize(int num); //重新指定长度为num,容器变短则删除超出的,变长则填默认值
resize(int num ,int elem); //如上,elem是默认值
capacity(); //容器的容量
reserver(int len); //直接预留len个长度,预留位置不初始化
//存取操作
at(int idx);
operator[];
front(); //返回第一个数据元素
back();
//插入和删除
insert(const iterator pos,int count,ele); // pos代表插入位置的迭代器,n为个数,ele是哪个数
push_back(ele);
pop_back();
erase(const iterator start,const iterator end);//删除迭代器从start到end'之间的元素
erase(const iterator pos); //删除迭代器指向的元素
clear(); //删除容器中所有
deque
双端动态数组,其也实现了迭代器,但是复杂程度和vector不是一个量级的,尽可能的使用vector,升值进行排序可以先把数据赋值到vector中在vector中排完序后再赋回来还更快
deque的实现原理:
有一个连续的地址被称为中控器,每个地址储存一个指针,指针指向缓冲区一小段连续的空间,deque的数据存在缓冲区中其中一小段连续空间中的一个空间,当这一小段空间填满了,中控器会再这个指针的上放或者下方再创建一个指针,指向缓冲区中另一个连续的小段地址,如果从头端压入,则数据放在这段连续地址从右往前数第一个空地址,如果从后方压入,则放在连续地址的从前往后数的第一个空地址
deque的API与vector大同小异,多了push_front(ele)和pop_front(ele);
stack
栈 :先进后出的容器,没有迭代器,不支持遍历
pop(); //压出
push(ele); //压入元素
top(); //栈顶元素
empty(); //判断堆栈是否为空
size(); //栈大小
queue
队列容器:先进先出,出数据一方叫对头,入数据较队尾,没有迭代器,不支持遍历行为
API与stack大同小异
list
链表:储存非连续非顺序的一种数据结构,是一个双向链表,拥有双向迭代器,不用管具体细节,能用就像
API与deque大同小异,因为sort排序定义在algorithm头文件里,只支持随机访问容器不支持链表容器,所有list提供了内部sort成员函数
sort(l.begin(),l.end());
set
集合:只有键值,所有元素的键值会自动排序,且不允许有两个相同的键值,有迭代器,但是是只读迭代器,不能中途改变,因为改变键值可能会导致set从有序变为无序,set和multiset底层都是用红黑树完成的是平衡二叉树
multiset与set完全相同,除了运行键值重复
API
swap(st); //交换两个集合容器
insert(ele); //插入ele
erase(pos); //删除pos迭代器所指的元素,返回下一个元素的迭代器
erase(beg,end); //删除[beg,end)的所有元素
erase(ele); //删除值为ele的元素
find(key); //查找key的个数
//更改set的排序规则
set s1; //排序规则是在尖括号里面是一个类型,所有只能通过仿函数定义规则(仿函数重载())
//如下
class MyCompare{
public:
bool operator()(int v1,int v2){ //重载(),当调用MyCompare()的时候,执行这个操作
return v1>v2; //改成从小到大
}
}
set s1; //当实例化的时候,类MyCompare实例化构造函数触发仿函数,实现排序规则的改变
//再更改set的排序规则的对象是自定义结果类型的时候,可以设置这个仿函数的类设置为友元类定义在这个自定义结构里
//比如
class Person{
friend class MyCompare;
.....
}
set::const_iterator ret = s1.find(ele); //find返回一个迭代器,因为set所有迭代器都是只读,所以用const_iterator来接,如果没找到这个值,返回s1.end();
if (ret != s1.end()){
cout<<*ret<::const_iterator,set::const_iterator> p;
p = s1.equal_range(keyele);
if (p .first != s1.end()){
cout<<"下限为"<<*(p.first)<
pair
队组:将一对值组合成一个值,可以有不同的数据类型,通过first,second访问
make_pair(type1,type2)返回一个队组
map
//suanfa.h
//constexpr auto N = 10;
//auto randArr(int max) -> int(*)[N];
int* randArr(int size, int max);
void print(int* (*pf)(int size, int max), int size, int max);
void print(int* arr, int size);
void swap(int& a, int& b);
void Bubble_sort(int* arr, int size);
void Select_sort(int* arr, int size);
void Insert_sort(int* arr, int size);
void Merge_sort(int* arr, int L ,int R);
void Merge(int* arr, int L,int M, int R);
void Quick_sort(int* arr, int L, int R);
int Quick(int* arr, int L, int R);
int Fenzhi_get_max(int* arr, int L, int R);
void Hanoi(int nums, char a, char b, char c);
//main.cpp
#include
#include"suanfa.h"
using namespace std;
int main() {
int size,max;
cin >> size >> max;
int * arr = randArr(size, max);
time_t t1 = time(0);
Merge_sort(arr, 0, size - 1);
time_t t2 = time(0);
cout << t2 - t1 << endl;
//print(arr, size);
//cout << "-----------" << endl;
//cout << Fenzhi_get_max(arr,0,size - 1);
//int n;
//cin >> n;
//Hanoi(n, 'a', 'b', 'c');
return 0;
}
//suanfa.cpp
#include
#include"suanfa.h"
#include
using namespace std;
//constexpr auto N = 10;
//auto randArr(int max) -> int(*)[N]
//{
// srand(time(0));
// int(*arr)[N] = (int(*)[N]) new int[N]();
// for (int i = 0; i < N; i++) {
// (*arr)[i] = rand() % max;
// }
// return arr;
//}
int* randArr(int size, int max) {
srand(time(0));
int* arr = new int[size]();
for (int i = 0; i < size; i++) {
arr[i] = rand() % max;
}
return arr;
}
void print(int* (*pf)(int size, int max), int size, int max) {
for (int i = 0; i < size; i++) {
cout << pf(size, max)[i] << endl;
}
}
void print(int* arr, int size)
{
for (int i = 0; i < size; i++) {
cout << arr[i] << endl;
}
}
void swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
void Bubble_sort(int* arr, int size)
{
for (int i = 0; i < size - 1; i++) {
for (int j = 0 ; j < size - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]);
}
}
}
}
void Select_sort(int* arr, int size)
{
for (int i = 0; i < size - 1; i++) {
int min = i;
for (int j = i + 1; j < size ; j++) {
if (arr[j] < arr[min]) {
min = j;
}
}
swap(arr[i], arr[min]);
}
}
void Insert_sort(int* arr, int size)
{
int ok = 1;
for (int i = 0; i < size - 1; i++) {
for (int j = i + 1; j > 0 && ok; j--) {
if (arr[j] < arr[j - 1]) {
swap(arr[j], arr[j - 1]);
}
else {
ok == 0;
}
}
}
}
void Merge_sort(int* arr, int L, int R)
{
if (L == R) return;
int M = L + (R - L) / 2;
Merge_sort(arr, L, M);
Merge_sort(arr, M + 1, R);
Merge(arr, L, M, R);
}
void Merge(int* arr, int L,int M, int R)
{
int* temp = new int[R - L + 1];
int i = 0;
int p1 = L;
int p2 = M + 1;
while (p1 <= M && p2 <= R) {
temp[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= M) {
temp[i++] = arr[p1++];
}
while (p2 <= R) {
temp[i++] = arr[p2++];
}
for (int c = 0; c < R - L + 1; c++) {
arr[c + L] = temp[c];
}
delete[] temp;
}
void Quick_sort(int* arr, int L, int R)
{
if (R - L <= 0) return;
else {
int M = Quick(arr, L, R);
Quick_sort(arr, L, M - 1);
Quick_sort(arr, M + 1, R);
}
}
int Quick(int* arr, int L, int R)
{
int point = R;
int p1 = L , p2 = R - 1;
while (1) {
while (arr[p1] < arr[point] && p1 < point) {
p1++;
}
while (arr[p2] > arr[point] ) {
p2--;
}
if (p1 >= p2) {
break;
}
else {
swap(arr[p1], arr[p2]);
p1++;
p2--;
}
}
swap(arr[p1], arr[point]);
return p1;
}
int Fenzhi_get_max(int* arr,int L , int R)
{
if (R < L) return -1;
if (R == L) return arr[L];
if (R - L == 1) return arr[L] > arr[R] ? arr[L] : arr[R];
int M = L + (R - L) / 2;
int last_L = Fenzhi_get_max(arr, L, M);
int last_R = Fenzhi_get_max(arr, M + 1, R);
return last_L > last_R ? last_L : last_R;
}
void Hanoi(int nums, char a, char b, char c)
{
static int i = 1;
if (nums == 1) {
cout << "第" << i << "次" << a << "->" << b << endl;
i++;
}
else {
//把nums-1个圆盘从起始盘搬到辅助盘上去
Hanoi(nums - 1, a, c, b);
//把最大的圆盘从起始盘搬到目标盘去
cout << "第" << i << "次" << a << "->" << c << endl;
i++;
//把nums-1个圆盘从辅助盘搬到目标盘上去
Hanoi(nums - 1, c, b, a);
}
}