第二章 面向对象的编程风格(调用函数)

来源:互联网 发布:java退格符处理 编辑:程序博客网 时间:2024/05/20 11:22

2.2调用函数

 

程序:用冒泡排序法实现对一个vector内的整数值加以排序。冒泡排序两种理解方式:

 

第一种:

主要是由两个嵌套的for循环实现的。外层的for循环以ix的值遍历vector内的元素,ix的值由0递增到size-1,当外循环的每次迭代完成时,由ix索引出来的元素就会被放入到合适的位置。当ix为0时,vector中的最小元素就会被找到,并被放置于位置0处。当ix的值为1时,次小的元素就会被找到放置1的位置,依次内推,放置元素的操作由内层的for循环完成。jx从ix+1依次递增到size-1;它比较位于ix处以及jx处的两个元素,如果jx的元素比较小,就将两元素互换。

void display(vector<int>vec)
{
  for(int ix=0;ix<vec.size();++ix)
  cout<<vec[ix]
      <<'';
  cout<<end1;
}

void swap(int vall,int val2)
{
  int temp=val1;
val1=val2;
val2=temp;
}

void bubble_sort(vector<int>vec)
{
  for(int ix=0;ix<vec.size();++ix)
  for(int jx=ix+1;jx<vec.size();++jx)
  if(vec[ix]>vec[jx])
  swap(vec[ix],vec[jx]);
}

int main()
{
  int ia[8]={8,34,3,13,1,21,5,2};
  vector<int>vec(ia,ia+8);        //容器中装入八个数组元素,跟数组的用法一样,需要首地址和元素个数。
  cout<<"vector before sory:";
  display(vec);
  bubble_sort(vec);
  cout<<"vector after sort:";
  display(vec);
}

 

第二种是形如:

经典排序算法 - 冒泡排序Bubble sort

原理是临近的数字两两进行比较,按照从小到大或者从大到小的顺序进行交换,

这样一趟过去后,最大或最小的数字被交换到了最后一位,

然后再从头开始进行两两比较交换,直到倒数第二位时结束,其余类似看例子

例子为从小到大排序,

原始待排序数组| 6 | 2 | 4 | 1 | 5 | 9 |


第一趟排序(外循环)

第一次两两比较6 > 2交换(内循环)

交换前状态| 6 | 2 | 4 | 1 | 5 | 9 |

交换后状态| 2 | 6 | 4 | 1 | 5 | 9 |

 

第二次两两比较,6 > 4交换

交换前状态| 2 | 6 | 4 | 1 | 5 | 9 |

交换后状态| 2 | 4 | 6 | 1 | 5 | 9 |

 

第三次两两比较,6 > 1交换

交换前状态| 2 | 4 | 6 | 1 | 5 | 9 |

交换后状态| 2 | 4 | 1 | 6 | 5 | 9 |

 

第四次两两比较,6 > 5交换

交换前状态| 2 | 4 | 1 | 6 | 5 | 9 |

交换后状态| 2 | 4 | 1 | 5 | 6 | 9 |

 

第五次两两比较,6 < 9不交换

交换前状态| 2 | 4 | 1 | 5 | 6 | 9 |

交换后状态| 2 | 4 | 1 | 5 | 6 | 9 |

 

第二趟排序(外循环)

第一次两两比较2 < 4不交换

交换前状态| 2 | 4 | 1 | 5 | 6 | 9 |

交换后状态| 2 | 4 | 1 | 5 | 6 | 9 |

 

第二次两两比较,4 > 1交换

交换前状态| 2 | 4 | 1 | 5 | 6 | 9 |
交换后状态| 2 | 1 | 4 | 5 | 6 | 9 |

 

第三次两两比较,4 < 5不交换

交换前状态| 2 | 1 | 4 | 5 | 6 | 9 |
交换后状态| 2 | 1 | 4 | 5 | 6 | 9 |

 

第四次两两比较,5 < 6不交换

交换前状态| 2 | 1 | 4 | 5 | 6 | 9 |

交换后状态| 2 | 1 | 4 | 5 | 6 | 9 |

 

第三趟排序(外循环)

第一次两两比较2 > 1交换

交换后状态| 2 | 1 | 4 | 5 | 6 | 9 |

交换后状态| 1 | 2 | 4 | 5 | 6 | 9 |

 

第二次两两比较,2 < 4不交换

交换后状态| 1 | 2 | 4 | 5 | 6 | 9 |
交换后状态| 1 | 2 | 4 | 5 | 6 | 9 |

 

第三次两两比较,4 < 5不交换

交换后状态| 1 | 2 | 4 | 5 | 6 | 9 |
交换后状态| 1 | 2 | 4 | 5 | 6 | 9 |

 

第四趟排序(外循环)无交换

第五趟排序(外循环)无交换


排序完毕,输出最终结果1 2 4 5 6 9

 

同样是最N-1次循环,只是理解不一样。本题不是此种理解。

 

传值与传址

 

这个程序的错误在于swap()函数被调用时候,传人的vec[ix]与vec[jx]与swap()函数的两个参数val1与val2,两者的关联只是拥有相同值,当swap()改变val1与val2的时候,vec[ix]与vec[jx]的值并没有交换。是的,其实传给swap()的对象被复制了一份,原对象和副本之间根本没有什么联系。当我们调用一个函数的时候,会在内存中建立一块特殊区域,称为程序堆栈。这块特殊区域提供了每个函数参数的存储空间。它也提供了函数岁定义的每个对象的内存空间,我们将这些对象称为local object(局部对象),一旦函数完成,内存就会被释放掉,或者说是从程序堆栈中pop掉。

传值:

当我们将vec[ix]这样的对象传入函数,默认情况下其值会被复制一份,成为参数的局部性定义。这种方式称为传值。我们在bubble_sort()内传给swap()的对象和我们在swap()内操作的对象,其实是没有任何关系的两组对象。

程序改动:

我们需要令swap()的参数和传入的实际对象产生关联。这就是所谓的传址。最简单的做法是将参数声明为一个reference:
void swap3(int & vall,int & val2)
{
  /*swap()的函数代码并没有任何改变,改变的是它的参数与传入对象的之间的关系*/
int temp=val1;
val1=val2;
val2=temp;
}
所以函数参数的传递应该是传址而不是传值,所以我们要将vec改为一个reference.
将void bubble_sort(vector<int>vec)
改成void bubble_sort(vector<int>&vec)

pass by reference的语义

int ival=1024;//对象,类型为int
int *pi=&ival;//pointer(指针),指向一个Int对象
int &rval=ival;//reference(引用),代表一个int对象

当我们这么写:
int jval=4096;
rval=jval;
便是将jval的值赋值给rval所代表的对象(也就是ival)我们无法令rval转而代表jval,因为c++不允许我们改变reference所代表的对象,它们必须从一而终。

当我们写:pi=&rval;其实是将ival(此为rval所代表的对象)的地址赋值给pi。我们并未令pi指向rval。

注意,重点是,面对reference的所有操作都和面对“reference所代表的对象”所进行的操作一般无二。

 

reference的意义有两点
1.reference将对象作为函数参数传入时候,对象本身并不会复制为另一份。复制的是对象的地址。函数中对该对象的任何操作,都相当于是对传入对象进行的间接操作。
2.可以直接对所传入的对象进行修改,降低复制大型对象的额外负担。

 

程序:显示的vector以传值的方式传入display()中。每次进行显示操作,vector的所有元素就会被复制。但是如果直接传入vector的地址,速度会进行的更快。将vector的函数声明为reference,便可达到这个目的。
void display(const vector<int>&vec)  
{
 for(int ix=0;ix<vec.size();++ix)
{
 cout<<vec[ix]<<' ';
 cout<<end1;
}
}
我们声明了一个 reference to const vector,因为函数之中并不会更改Vectorde 内容,少了const并不会造成错误。但加上const会让阅读的人了解,我们以传址的方式来传递vector,为的是避免复制操作,而不是在函数之中对它进行修改。

vector也可以以pointer形式传递。这和reference的传递效果相同:传递的是对象地址,而不是整个对象的副本。唯一差别在于reference和pointer的用法不同。例如:

void display(const vector<int>*vec)
{
 if(!vec)                
//当我们提领pointer时,一定要先确定其值并非0.至于refenrence

{
 cout<<"display():vector pointer is 0\n";
return;
}
for(int ix=0;ix<vec->size();++ix)
cout<<(*vec)[ix]<<' ';
cout<<end1;
}
int main()
{
 int ia[8]={8,32,3,13,1,21,5,2};
vector<int>vec(ia,ia+8);
cout<<"vector before sort:";
display(&vec);//传入地址
//.....

pointer参数和reference参数之间更重要的差异是,pointer可能(也可能不)指向某个实际对象,当我们提领pointer时,一定要先确定其值并非0.至于refenrence,则必定会代表某个对象,所以不需做此检查。

一般来说,除非你希望在函数内更改参数值,就像前一节fibon_elem一样。bool fibon_elem(int pos,int &elem);否则我建议在传递内置类型时,不要使用传址方式。传址机制主要用于传递class object。

 

总程序:

 

#include<iostream>
using namespace std;
int main()
{
 int ia[8]={8,34,3,13,1,21,5,2};
 vector<int>vec(ia,ia+8);
 cout<<"vector before sort";
 display(vec);
 bubble_sort(vec)
 cout<<"vector after sort";
 display(vec);

}

void swap(&ival1,&ival2)
{

temp=ival1;
ival1=ival2;
ival2=temp;


}
void bubble_sort(vector<int>&vec)
{
for(int ix=0;ix<vec.size;++ix)
for(int jx=ix+1;jx<vec.size;++jx)
if(vec[ix]>vec[jx])
swap(vec[ix],vec[jx]);

}

void display(vector<int>vec)

{
for(ix=0;ix<vec.size;++ix)
cout<<vec[ix]
    <<' ';
cout<<endl;
}

 


作用域以及范围

 

除了static,函数定义的对象,只存在于函数的执行期间。如果将这些所谓的局部对象的地址返回,会导致运作错误。

函数是暂时位于程序的栈堆(内存内的一块特殊区域)之上。局部对象就放在这块区域中。当函数执行完毕,这些区域的内存就会被舍弃。于是局部对象不复存在。

一般而言,对于根本不存在的对象进行寻址,是很不好的习惯。举个例子,fibon_seq()将Fibonacci数列以vector返回:
   vector<int> fibon_seq(int size)
{
if(size<=0||size>1024)
{
cerr<<"warning:fibon_seq():<<size<<"not supported--resetting to 8\n";
size=8;
}
vector<int>elem(size);
for(int ix=0;ix<size;++ix)
if(ix==0||ix==1)
elems[ix]=1;
else elems[ix]=elems[ix-1]+elems[ix-2];
return elems;

}

不论以pointer形式还是reference将elems返回,都不正确。因为elems在fibon_seq()执行完毕时已不复存在。

如果将elems以值的形式传回,便不会产生任何问题;因为返回的是对象的副本,它在函数之外依然存在。

 

 

 

为对象分配的内存,其存活时间称为存储期(storage duration)或范围(ectent)

每次fibon_seq()执行,都会为elems分配内存,每当fibon_seq()结束就会释放。我们称此对象有局部性范围(locai extent)函数参数(如上列size)便具有局部范围。

对象在程序内的存活区域称为该对象scope(作用域)。

我们说size和elems在fibon_seq()函数内拥有local scope,如果某个对象仅具有local scope,其名称在local scope之外不可见。

 

 

 

对象如果在函数以外声明,具有所谓的file scope。对象如果拥有file scope,从其声明点至文件末尾都是可见的。file scope内的对象也具有local scope(局部作用域),意即该对象的内存在main()开始执行时候已经分配好了,可以一直存在函数结束。

内置类型的对象,如果定义在file scope之内,必定被初始化为0.但如果它们被定义在local scope之内,那么除非程序员制定其初值,否则不会被初始化。


 

 

 

动态内存管理
不论local scope或file scope,对我们而言,都是由系统自动管理。

第三种存储期形式称为dynamic extent(动态范围)其内存是由程序的空闲空间(free store)分配而来,有时也称heap memory(堆内存)。

这种内存必须由程序员自行管理,其分配通过new表达式来完成,而其释放通过delete表达式完成。

 

 

new表达式的形式如下:
new type:
此处的type可为任意内置类型,也可以是程序知道的class类型。

 

 

new表达式可以写:
new type(initial_value);
例如:
int *p;
pi=new int;
便是先由heap分配出来的一个类型为int的对象,再将其地址赋值给pi.默认情况下,由heap分配的对象,皆未经过初始化。New表达式的另一种形式允许我们指定初值,例如:
pi=new int(1024);
同样是先由heap分配出一个类型为Int对象,再将其地址赋值给pi,但这个对象的值会被初始化为1024.

 

如果要从heap中分配数组,可以这么写:
int *pia=new int[24];
上述程序代码会从heap分配一个数组,拥有24个整数。pia会被初始化为数组第一个元素的地址。数组中的每个元素都未经初始化。c++没有提供任何语法让我们得以从heap分配数组的同时为其元素设定初值。
 

从heap分配的对象,被称为具有dynamic extent,因为它们是在运动时通过new表达式分配来的,因此可以持续存活,直到delete表达式加以释放为止。下面delete表达式会释放pi所指的对象:
delete pi;
如果要删除数组的所有对象,必须在数组指针和delete表达式之间,加一个空的下标运算符:
delete pi;
如果要删除数组中的所有对象,必须在数组指针和delete表达式之间,加上一个空的下标运算符:
delete[]pia;
注意,无需检验pi是否为0:
if(pi!=0)//多此一举,编译器会替我们检查
delete pi;
编译器会自动检查这项,由于某种原因,程序员不想使用delete表达式,由Heap分配而来的对象就永远不会被释放。这称为memory leak(内存泄露)。第6章我们会查看程序设计过程中什么时机应该采用动态内存分配。

第七章讨论异常处理时,也会谈到如何预防memory leak的发生。

0 0
原创粉丝点击