C++学习笔记之(对象复制的困惑)

本文通过火车调度问题,介绍了如何使用栈结构解决问题,并详细解释了栈类的设计、实现及对象复制过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


xmxoxo 2006-11-14 at XiaMen

 类的基础也学习了部份;现在正逐渐养成用类来描述程序和问题的习惯。
群里一位朋友的问题引发了类对象中的一个问题。

有一个T字型铁轨,标号为1,2,。。。,n的车厢位于铁轨的左边,
当所有车厢移动到铁轨的右边时,要求重新排序车厢的顺序。规则是支线上的
车厢可以不动,也可以移动到铁轨的右边。例如,如果n=3,车厢1,2,3在铁
轨的左边,依次进行如下的过程:3号车厢进入支线,2号车厢进入支线,2号车
厢进入铁轨的右边,3号车厢进入铁轨右边,1号车厢进入支线,1号车厢进入铁
轨右边。最后获得新的顺序1,3,2。
对于任意n值,请列出所有的可能排序。
 这应该是一道古老的题目了,记得在很久以前的某份报纸上看到这道题。
那时候还没学过数据结构,感觉可能性很难把握。使用栈结构应该是很方便描述
各种状态的。
 首先,使用单向链表来保存栈中的数据:

struct node
{
 
int dat;
 node 
*next;
};

 

然后定义一个栈类(stack),来描述栈对象,并将方法包装进去。
在stack类里,定义了两个private变量,m_length用来描述栈的大小;
top指针用来指向栈顶,也就是单向链表的头结点。
栈类的方法加了常用的这几个:
isempty()用来判断栈是否为空
length()用来返回栈的大小;
pop()用来出栈;
push(int)用来将数据压入栈.
同时添加了复制函数,这正是本文的重点,在下面会详细说明。

 

class stack  
{
private:
 
int m_length; //栈大小
 node *top;  //头结点指针

public:
 
bool push(int dat);
 
int pop();
 
int length();
 
bool isempty();
 stack();  
//构造函数
 
//stack(const stack &s); //复制函数
 virtual ~stack(); //析构函数

};

 

刚开始的时候并没有给类添加复制函数,于是stack.cpp内容如下:


// Stack.cpp: implementation of the Stack class.
// 栈类,实现出栈,进栈等 
// 版本:v1.0
// 日期:2006.11.14
// 作者:xmxoxo at XiaMen
//////////////////////////////////////////////////////////////////////

#include 
"stdafx.h"
#include 
"stack.h"
#include 
"stdlib.h"

//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////

stack::stack()
{
 m_length 
= 0//大小为0;
 top = NULL;  //初始化指针
}

stack::
~stack()
{
 
//terminate
 if (m_length!=0)
 {
  
while (m_length>0)
  {
   pop();
  }
 }
}



//judge stack is empty
bool stack::isempty()
{
 
if (top==NULL)
 {
  
return 1;
 }
 
else
 {
  
return 0;
 }
}

//return size of stack 
int stack::length()
{
 
return m_length;
}

//pop data
int stack::pop()
{
 
int ret=0;
 node 
*tmp;
 
if (m_length!=0)
 {
  
//pop node
  tmp=top;
  ret
=tmp->dat; 
  top 
= tmp->next;
  
//delete node
  delete tmp;
  
// size of 
  m_length--;
 }
 
return ret;
}

//push data

bool stack::push(int dat)
{
 
bool ret=0;
 node 
*tmp;
 tmp 
= new node;
 tmp
->dat = dat;
 tmp
->next = top;
 top
=tmp;
 
// size of stack ++
 m_length++;
 
return ret;
}


描述完栈类,接下来分析题目,根据题意,左边(用stack left来表示)的火车可以进入
支线(用stack tmp来表示),也可以到右边(用stack right来表示,注意,得到的结果
就是right里保存的内容,但在输出的时候直接pop后,顺序是颠倒的),当然,如果不用
栈对象,也可以使用字串或者数组进行处理,但是表达上可能更复杂,也不太好理解。

 现在我们得到三个栈对象,left,tmp,和right,来分析一下火车的行走情况,left
出栈的火车,有两种情况,一是进入支线tmp,我们把这个行为简单记为“进”;二是直接
到右边(right),把这个行为记为“右”;而在支线(tmp)出线的火车只有一种行为,就是
出栈后进入right,把这个行为记为“出";显然,“进”后马上“出”,就相当于“右”。
 现在先考虑1列火车的情况,根据上面的推理,1列火车时只有一种情况,就是“右”。
 再来考虑n列火车的情况,假设现在是第i列火车在left准备出栈,那么有这么几种
情况:
1、"右“。直接到right
2、“进”。根据上面的推理,“进”完后不能马上“出”了,否则就跟“右”是一样的了。
3、当tmp不为空时,left不“右”也不“进”,而是“出”,即支线火车进入右边。

枚举了所有的情况,就可以使用递归来进行编程了。递归的方法很简单,主程序如下:

'----------------------------------------------------------

// trainFoo.cpp : Defines the entry point for the console application.
//
// 火车排列问题
//作者: xmxoxo 2006.11.13 at xiamen
//当前版本 v1.0

#include 
"stdafx.h"
#include 
"stack.h"
#include 
"iostream.h"

//输出结果
void output(stack& p)
{
 
int len,i;
 len
=p.length();
 
for (i=0;i<len;i++)
 {
  cout
<<outtmp.pop();
  
if (i!=len-1)
   cout
<<",";
 }
 cout
<<endl;
}

//递归过程
//参数: 左栈,临时栈,输出栈
void foo(stack left,stack tmp, stack out)
{

 
int i=0;
 
if (left.length()==1)
 {
  
//如果左栈只有一个数,则输出这个数到输出栈
  
//cout<<"右   ";
  out.push(left.pop());
  
//临时栈全部出栈
  while (!tmp.isempty())
  {
   
out.push(tmp.pop());
  }
  
//并输出结果
  output(out);
 }
 
else
 {
  
//分情况

  i
=left.pop();
  
//cout<<"["<<i<<"]"<<"进,";
  
//1.左栈进临时栈
  tmp.push(i);
  
//递归,传值
  foo(left, tmp, out);

  
//恢复临时栈
  i=tmp.pop();
  
  
//2.左栈出栈到输出栈
  
//cout<<"右,";
  out.push(i);

  
//递归
  foo(left,tmp,out);

  
//3.临时栈出栈到输出栈
  if (!tmp.isempty ())
  {
   
out.push(tmp.pop());
   foo (left,tmp,
out);
  }

 }
}

//定义左栈
stack objleft;
//定义栈
stack objstack;
//定义输出栈
stack objout;

//主程序
int main(int argc, char* argv[])
{
 
int n;
 
int i;
 
//I/O 处理
 cout<<"Please Input Number:";
 cin
>>n;

 
//初始化
 
//建立左栈
 for (i=1;i<=n;i++)
 {
  objleft.push (i);
 }
 
 
//初始化结束

 
//开始处理
 foo(objleft,objstack,objout);


 
return 0;
}

'----------------------------------------------------------
程序根据N的情况进行了递归,但是运行后却发现结果不正确,经过跟踪发现
对象作为函数的参数传递的时候,传递得不正确。只有在主程序中的
foo(objleft,objstack,objout);这一句运行的时候,有正确的传递,而在
foo()函数里进行的递归调用时传递就错误了。foo函数定义的是值参,也就是
说,调用的参数会复制产生一个新的变量,在函数中使用。但是foo的三个参
数都是stack类的实例对象,在stack类中,我们定义了一个单向链表来保存栈
的数据,其中,node结构里使用到了一个指针next,正是在类中的指针导致了
对象实例在复制中的错误,在跟踪程序中发现,当递归引用foo函数时,程序
确实也复制了新的对象,连指针也复制了,但这个指针复制后,还是指向原来
的位置,而不是先复制出指针指向的空间,比如一个指针,值为0x00431D30,复制后
还是0x00431D30,还是指向同一个地址,而指向的地址并没有复制。所以无论尝试
foo(stack &left...)形参方式还是foo(stack left)值参方式,复制的新对象中
的指针仍然是指向相同的空间。
 查询了关于类的教程,原来在类里除了构造函数及析构函数,还有一个
复制函数。用于复制对象时调用,来产生新的对象,这个复制函数是在新的对象中
运行的,也就是要产生的那个对象。函数的原型是:stack::stack(const stack &s)
这里是使用形参的方式,并且使用了const前缀。根据思路,现在可以自定义出复制对象
的过程,要注意的是:在这个函数里可以访问&s这个形参的所有成员及成员函数。
(注:const前缀保证了不会对原有变量进行修改;而&s形参,则表示了引用,保证了
该函数不需要对s变量进行复制,否则,在调用复制函数时又调用复制了函数,进入了死
循环,摘自优快云)
 明白了对象的复制函数,就可以自己构造一个复制函数了,由于stack中的top指针
是头指针,在复制的过程中不能再使用push函数相同的方法来把数据加到链表顶端,而是
要加在链表的尾部,所以定义了一个临时的tail尾指针。

 

stack::stack(const stack &s)
{
 m_length 
= 0;
 top 
= NULL;
 node 
*tmp;
 node 
*tail; //尾指针
 node *newnode;
 tmp
=s.top;
 tail
=NULL;
 
while (tmp!=NULL)
 {

  newnode 
= new node;
  newnode
->dat = tmp->dat;
  newnode
->next =NULL;
  
if (top!=NULL)
  {
   tail
->next =newnode;
   tail
=newnode;
  }
  
else
  {
   top
=newnode;
   tail
=newnode;
  }
  
// size of stack ++
  m_length++;

  tmp
=tmp->next;
 }
}

加入了复制函数后,stack类可以正常工作了,主程序不需要修改。
以下是n=3及n=4的输出结果:

Please Input Number:3
3,2,1
3,1,2
1,3,2
2,1,3
1,2,3
Press any key to continue

Please Input Number:4
4,3,2,1
4,3,1,2
4,1,3,2
4,2,1,3
4,1,2,3
1,4,2,3
2,1,4,3
1,2,4,3
3,2,1,4
3,1,2,4
1,3,2,4
2,1,3,4
1,2,3,4
Press any key to continue

可以看出,输出的结果很有规律,正是按上面我们分析的“进”,“右”,“出”方式得到的结果。 

11.17 补记:

    上面的主程序在情况分析有误,情况1和情况2会出现重复,可以合并处理,另外,原先是判断n=1
时输出结果,也可以直接合并到递归里,判断左堆栈和临时栈同时为空即可输出结果。修改后得到了
以下的程序及结果:

// trainFoo.cpp : Defines the entry point for the console application.
//
// 火车排列问题
//作者: xmxoxo 2006.11.13 at xiamen
//当前版本 v1.0

#include 
"stdafx.h"
#include 
"stack.h"
#include 
"iostream.h"

//输出结果
void output(stack p)
{
    
int len,i;
    len
=p.length();
    
for (i=0;i<len;i++)
    {
        cout
<<p.pop();
        
if (i!=len-1)
            cout
<<",";
    }
    cout
<<endl;
}

//递归过程
//参数: 左栈,临时栈,输出栈
int foo(stack left,stack tmp, stack out)
{
    
static count;
    
int i=0;
    
//分情况

    
//1.左栈进临时栈
    if (!left.isempty())
    {
        tmp.push(left.pop());
        
//cout<<"进,";
        
//递归,传值
        foo(left, tmp, out);
        
//恢复左栈
        left.push(tmp.pop());
    }
    
//2.临时栈出栈到输出栈
    if (!tmp.isempty())
    {
        
//cout<<"出,";
        out.push(tmp.pop());
        foo (left,tmp,
out);
        
//恢复状态
        tmp.push (out.pop());
    }
    
if (left.isempty()&&tmp.isempty())
    {
        output(
out);
        count
++;
    }
    
return count;
}

//定义栈
stack objstack;
//定义左栈
stack objleft;
//定义输出栈
stack objout;

//主程序
int main(int argc, char* argv[])
{
    
int n;
    
int i;
    
int count=0;
    
//I/O 处理
    cout<<"Please Input Number:";
    cin
>>n;

    
//初始化
    
//建立左栈
    for (i=1;i<=n;i++)
    {
        objleft.push (i);
    }
    
    
//初始化结束
    
    
//开始处理
    count=foo(objleft,objstack,objout);
    cout 
<< "共 " << count << "个结果."<<endl;
    
return 0;
}

Please Input Number:3
3,2,1
3,1,2
1,3,2
2,1,3
1,2,3
共 5个结果.

Please Input Number:4
4,3,2,1
4,3,1,2
4,1,3,2
1,4,3,2
4,2,1,3
4,1,2,3
1,4,2,3
2,1,4,3
1,2,4,3
3,2,1,4
3,1,2,4
1,3,2,4
2,1,3,4
1,2,3,4
共 14个结果.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值