C++描述的数据结构和算法(一)

  • 参数传递方式(如传值、引用和常量引用)。
  • 函数返回方式(如返值、引用和常量引用)。
  • 模板函数。
  • 递归函数。
  • 常量函数。
  • 内存分配和释放函数:new与delete。
  • 在检查程序的时候我们应该关注以下这些点:

在程序开发过程中通常需要做到如下两点:一是高效地描述数据;二是设计好的算法。在程序的开发过程中,要重点关注这些信息:

  • 它正确吗?
  • 它容易读懂吗?
  • 它有完善的文档吗?
  • 它容易修改吗?
  • 它在运行时需要多大内存?
  • 它的运行时间有多长?
  • 它的通用性如何?能不能不加修改就可以用它来解决更大范围的问题?
  • 它可以在多种机器上编译和运行吗?或者说需要经过修改才能在不同的机器上运行吗?

上述问题的相对重要性取决于具体的应用环境。比如,如果正在编写一个只需运行一次即可丢弃的程序,那么主要关心的应是程序的正确性、内存需求、运行时间以及能否在一台机器上编译和运行。不管具体的应用环境是什么,正确性总是程序的一个最重要的特性。一个不正确的程序,不管它有多快、有多么好的通用性、有多么完善的文档,都是毫无意义的(除非它变正确了)。尽管我们无法详细地介绍提高程序正确性的技术,但可以为大家提供一些程序正确性的验证方法以及公认的一些良好的程序设计习惯,它们可以帮助你编写正确的代码。

算法的目标是如何开发正确的、精致的、高效的程序。

2 函数与参数

2.1 传值参数

考察函数func(),该函数用来计算表达式 a+b+b*c+(a+b-c)/(a+b) + 4,其中a,b和c是整数,结果也是一个整数。

int func(int a, int b, int c)
{
    return a+b+b*c+(a+b-c)/(a+b)+4;
}

形式参数a、b和c 都是传值参数(value parameter)。运行时,与传值形式参数相对应的实际参数的值将在函数执行之前被复制给形式参数,复制过程是由该形式参数所属数据类型的复制构造函数(copy constructor)完成的。如果实际参数与形式参数的数据类型不同,必须进行类型转换,从实际参数的类型转换为形式参数的类型,当然,假定这样的类型转换是允许的。

当函数运行结束时,形式参数所属数据类型的析构函数(destructor)负责释放该形式参数。当一个函数返回时,形式参数的值不会被复制到对应的实际参数中。因此,函数调用不会修改实际参数的值。

2.2 模板函数

编写一段通用的代码,将参数的数据类型作为一个变量,不明确指定其数据类型,而是由编译器来确定。利用模板函数计算一个表达式代码如下:

template
T Abc(T a, T b, T c)
{
    return a+b+b*c+(a+b-c)/(a+b)+4;
}

通过把T替换为int,编译器可以立即构造出func的整形版本,而把T替换为double或long,编译器又可以构造出函数func的双精度型版本和长整型版本。

2.3 引用参数

上述模板函数形式参数的用法会增加程序的运行开销。假定a, b 和c 是传值参数,在函数被调用时,类型T的复制构造函数把相应的实际参数分别复制到形式参数a、b和c之中,以供函数使用;而在函数返回时,类型T的析构函数会被唤醒,以便释放形式参数a,b和c。假定数据类型为用户自定义的类类型,那么它的复制构造函数将负责复制其所有元素,而析构函数则负责逐个释放每个元素,这都是非常大的消耗。

一种优化方案是,将a、 b和c改用引用参数。如果用语句func(x,y,z)来调用函数,其中x、y和z是相同的数据类型,那么这些实际参数将被分别赋予名称a,b和c,因此在函数func执行期间,x、y和z被用来替换对应的a,b和c。与传值参数的情况不同,在函数被调用时,本程序并没有复制实际参数的值,在函数返回时也没有调用析构函数。

template
T Abc(T& a, T& b, T& c)
{
    return a+b+b*c+(a+b-c)/(a+b)+4;
}

2.4 常量引用参数

更进一步,C++还提供了另外一种参数传递方式——常量引用(const reference),这种模式指出函数不得修改引用参数。

template
T func(const T& a, const T& b, const T& c)
{
    return a+b+b*c+(a+b-c)/(a+b)+4;
}

使用关键字const来指明函数不可以修改引用参数的值。对于简单数据类型,当函数不会修改实际参数值的时候我们可以
采用传值参数,对于其他复杂的数据类型(包括模板类型),当函数不会修改实际参数值的时候可以采用常量引用参数。

可以得到上述代码的一个更通用的版本。在新的版本中,每个形式参数可能属于不同的数据类型,函数返回值的类型与第一个参数的类型相同。

template
Ta func(const Ta& a, const Tb& b, const Tc& c)
{
    return a+b+b*c+(a+b-c)/(a+b)+4;
}

2.5 返回值

函数可以返回值、引用或常量引用。在前面的例子中,函数func返回的都是一个具体值,在这种情况下,被返回的对象均被复制到调用(或返回)环境中。对于函数func的所有版本来说,这种复制过程都是必要的,因为函数所计算出的表达式的结果被存储在一个局部临时变量中,当函数返回时,这个临时变量(以及所有其他的临时变量和传值形式参数)所占用的空间将被释放,其值当然也不再有效。为了避免丢失这个值,在释放临时变量以及传值形式参数的空间之前,必须把这个值从临时变量复制到调用该函数的环境中去。

如果需要返回一个引用,可以为返回类型添加一个前缀 &。如:

T& X(int i, T& z)

如果在函数名之前添加关键字const,那么函数将返回一个常量引用,例如:

const T& X (int i, T& z)

除了返回的结果是一个不变化的对象之外,返回一个常量引用与返回一个引用是相同的。

2.6 递归函数

递归函数(recursive function)是一个自己调用自己的函数。涉及数学的两个相关概念——数学函数的递归定义以及归纳证明。在数学中经常用一个函数本身来定义该函数。例如阶乘函数f(n)=n!的定义如下:

对于函数f(n)的一个递归定义,要想使它成为一个完整的定义,必须满足如下条件:

  • 定义中必须包含一个基本部分,返回值必须是直接定义的(即非递归)。
  • 在递归部分中,右侧所出现的所有f的参数都必须有一个比n小,以便重复运用递归部分来改变右侧出现的f,直至出现f的基本部分。

例程1 以下程序给出了一个计算阶乘n!的C++函数。

int Factorial (int n)
{
    //计算n!
    if (n<=1) return 1;
    else return n * Factorial(n-1);
}

例程2 模板函数Sum和RSum统计元素a[0]至a[n-1]的和,一种是传统的累加,一种是递归调用的方式。

template
T Sum(T a[], int n)
{ 
    //累加方式计算a[0: n-1]的和
    T tsum=0;
    for(int i = 0; i < n; i++)
        tsum += a[i];
    return tsum;
}

template
T Rsum(T a[], int n)
{
    //递归方式计算a[0: n-1]的和
    if (n > 0)
        return Rsum(a, n-1) + a[n-1];
    return 0;
}

例程3 以下使用递归函数生成排列

template
void Perm(T list[], int k, int m)
{ 
    //生成list [k: m ]的所有排列方式
    int i;
    if (k == m) {
        //输出一个排列方式
        for (i = 0; i <= m; i++)
            cout << list [i];
        cout << endl;
    }
    else // list[k: m ]有多个排列方式
        // 递归地产生这些排列方式
        for (i=k; i <= m; i++) {
            Swap (list[k], list[i]);
            Perm (list, k+1, m);
            Swap (list [k], list [i]);
        }
}

3 动态存储分配

3.1 操作符new

C++操作符new可用来进行动态存储分配,该操作符返回一个指向所分配空间的指针。例如,为了给一个整数动态分配存储空间,可以使用下面的语句来说明一个整型指针变量:

int *y ;

当程序需要使用该整数时,可以使用如下语法来为它分配存储空间:

y = new int;

操作符new分配了一块能存储一个整数的空间,并将指向该空间的指针返回给 y,y是对整数指针的引用,而*y则是对整数本身的引用。为了在刚分配的空间中存储一个整数值,可以使用如下语法:

*y = 10;

我们可以把上述三步进行适当的合并,如下例所示:

int *y = new int;

*y = 10;

int *y = new int (10);

int *y;

y = new int (10);

3.2 一维数组

使用一维或二维数组时,这些数组的大小在编译时可能是未知的,事实上,它们可能随着函数调用的变化而变化。因此,对于这些数组必须进行动态存储分配。

为了在运行时创建一个一维浮点数组x,首先必须把x说明成一个指向float的指针,然后为数组分配足够的空间。例如,一个大小为n的一维浮点数组可以按如下方式来创建:

float *x = new float [n];

操作符new分配n个浮点数所需要的空间,并返回指向第一个浮点数的指针。可以使用如下语法来访问每个数组元素:x[0], ..., x[n-1]。

3.4 操作符delete

动态分配的存储空间不再需要时应该被释放,所释放的空间可重新用来动态创建新的结构。可以使用C++操作符delete来释放由操作符new所分配的空间。下面的语句可以释放分配给 *y的空间以及一维数组x:

delete y;

delete [ ] x;

3.5 二维数组

虽然C++提供了多种机制用来说明二维数组,但其中的多数机制都要求在编译时明确地知道每一维的大小。而且,在使用这些机制时,很难编写出一个允许形式参数是一个第二维大小未知的二维数组的函数。之所以如此,是因为当形式参数是一个二维数组时,必须指定其第二维的大小。

例如,a[ ][10]是一个合法的形式参数,而a[ ][ ]不是。克服这种限制的一条有效途径就是对于所有的二维数组使用动态存储分配。当一个二维数组每一维的大小在编译时都是已知时,可以采用类似于创建一维数组的语法来创建二维数组。例如,一个类型为char的7×5数组可用如下语法来定义:

char c[7][5];

如果在编译时至少有一维是未知的,必须在运行时使用操作符new来创建该数组。一个二维字符型数组,假定在编译时已知其列数为 5,可采用如下语法来分配存储空间:

char (*c)[5];
try {
    c = new char [n][5];
}

catch (xalloc) {
    //仅当n e w失败时才会进入
    cerr << "Out of Memory" << endl;
    exit (1);
}

例程:为一个二维数组分配存储空间

template 
void Make2DArray ( T ** &x, int rows, int cols)
{
    //创建一个二维数组
    //创建行指针
    x = new T * [rows];
        
    //为每一行分配空间
    for(int i = 0; i < rows; i++)
        x[i] = new int [cols];
}

例程:按照申请空间的相反顺序释放空间

//释放二维数组的空间
template 
void Delete2DArray(T ** &x, int rows)
{
    //释放为每一行所分配的空间
    for (int i = 0; i < rows; i++)
            delete [] x[i];
    
    //删除行指针
        delete[] x;

    x = nullptr;
}

 

你可能感兴趣的:(数据结构与算法)