编程范式(斯坦福大学)学习笔记《四》

斯坦福大学开放课程--编程范式(四)

综述

本节课的主要内容是关于泛型数据的拷贝,虽然是使用C语言实现,并且没有用到C++中的模板这种泛型编程技术,但是效果却非常好。本节内容紧接上节所将的字节位拷贝的知识,充分利用了字节拷贝技术。

笔记

由于内容和例子不断深入,实际核心内容则比较集中,因此这里只进行总结讨论。

引例

本节所有的例子都是针对于数据交换来进行的,从最简单的例子开始,不断深入。 开始是关于一个最简单的整数数据的交换实例:

void swap(int a, int b){
        int tmp = a;
        a = b;
        b = tmp;
}

此例子非常简单,只需要构造一个简单的中间临时变量tmp用来存放a的值,并且交换赋值相关的数据,就可以达到交换的目的。

但是,此实例有一个缺陷,就是值传递,而不是引用传递,这样,传递的值虽然改变,但是只想原始变量的单元却没有改变,具体来说就是:

a = 23;
b = 34
swap(a, b);
 
 

执行上面一段语句会发现,其实a,b的值并没有交换,原因和C/C++的参数的值传递以及指针传递有关系。函数调用的时候,只会拷贝a,b的值,因此调用swap的时候,交换的是形参,实际参数的值并没有改变。

要实现真正参数传递的效果,需要用指针的形式来实现:

void swap(int *vp1, int *vp2) {
    int a = *vp1;
    *vp1 = *vp2;
    *vp2 = *vp1;
}
 
 

再次调用swap(&a, &b)的时候,就会修改掉原来的值,因为这里传递过去的就是指针,所以,对vp1,vp2的操作 就是对指向单元a,b的操作,所以能够修改对应的值。

泛型交换与拷贝

上面的例子,只是对某种特定的类型进行交换,比如int类型,如果想对double类型等进行交换,只需要修改其类型为double即可,其他类型类似。 但是考虑到需要对多种不同类型进行交换,是否存在一种通用的方法呢? 在C++中,可以用模板template技术,然而这里,回想起上节课中讲到的字节操作,能否利用字节的拷贝来实现呢?答案是肯定的。

void swap(void *vp1, void *vp2, int size){
    char buffer[size];
    memcpy(buffer, vp1, size);
    memcpy(vp1, vp2, size);
    memcpy(vp2, buffer, size);
}
 
 

调用的时候,字号需要给定某个类型,即可实现。比如,通过:

double a = 23.0, b = 34.0;
swap(&a, &b, sizeof(double));
 
 

当然,对于结构体也可以通过这种形式来进行拷贝。

关于上面例子的几点说明:

  1. 这里声明数组的方式,使用的大小size是可变的,这只在某些编译器中支持,这里只是为了说明字符拷贝的方式,例子的重点在于交换。当然可以使用malloc或者new来动态分配大小可变的空间。
  2. 这里使用memcpy(dest, src, size)这个函数来进行内存单元的拷贝,注意此函数并不关心你的数据类型,单纯的进行单元的拷贝而已,所以虽然编译可能通过,但是还需要自己进行判断和控制。
  3. 这个例子的亮点就在于void*的使用,通过它能够实现泛型,即针对于int,short,char,struct等类型都能够保证能够拷贝交换成功。
  4. template和这里的区别和优缺点。注意到使用模板的话,编译后,会为每种类型都生成一种代码,比如int对应的,float对应的,这样如果调用次数很多的话,代码体积会增大,冗余过多。而这里编译出来就一套代码,更加简洁。
  5. 这里传递一个参数大小size是因为,由于泛型指针void*的存在,所以编译器并不知道要拷贝多少个字节,所以,需要你手动控制并指定一个大小。

存在的问题

由于编译器会很容易的放过void*带来的错误,所以如果两个类型不同的数据调用此函数,就会出现问题:

double a = 23.0;
    int b = 345;
    swap(&a, &b, sizeof(double));
 
 

这里,double和int类型占据的数据空间的大小是不同的,因此,如果单纯的直接调用这个函数,就会出错,简单的结果就是,截断拷贝或者多拷贝数据。 比如,int类型拷贝到double数据空间的时候,只有前面2个字节拷贝了,后面的原来double数据的两个字节仍然保留了;或者说double拷贝到int的时候,可能会多拷贝两个字节到int后面的数据,造成出错。具体的方式,与后面一个参数sizeof(double)或者sizeof(int)有关系。

初学者容易犯的错误

  1. 使用void * tmp = vp1,而不是前面锁讲到的char buffer[size],这个错误的原因是由于不理解void不是一个类型,不像int,double等属于一个类型,所以错误。void 只能用作函数参数,返回值才可行。但是可以使用 void * tmp = (int )&a这类的用法,因为具体的类型即可以赋值给一般的类型,你只有给定了一个具体的类型,才能让编译器知道规则,才能编译通过。
  2. 指针的拷贝,何时使用引用地址的问题。一个简单的例子出发,

char * husband = strdup("Fred"); char * wife = strdup("Wilma");

如果想交换两人所指向的空间内容,正确的做法是:

swap(&hustband, &wife, sizeof(char *))
 
 

也就是说,这里要交换的是指针的地址,交换之后,husband的内容发生了变化,内容变成了原来wife的内容,由于本身是地址,所以内容变了,实际上所只想的地址也变了,现在husband指向原来wife所指向的地址,而wife指向原来husband指向的地址。

一个错误的例子就是,swap(husband, wife, &sizeof(char *)),这样,交换的实际上是他们锁指向的内容,即存放Fred和Wilama的单元中的内容会交换,而且,由于char *是四个字节,因此交换的就只有四个字节的内容。 为何会如此呢?因为上面的例子,比如要交换a,b单元的内容,传入的就是a,b的地址&a, &b,同样,这里我直接传入指针,当然交换的是他们指向的单元的内容,即两个字符串。 所以要交换两个指针的内容,就要交换他们的地址,即指针的地址,指针的指针。

另外一个例子

思考一个下面线性搜索的例子,

int * lsearch(int key ,int* array, int size){
    for (int i = 0; i < size; i++)  
    {
        if(array[i] == key)
            return i;
    }
}
 
 

上面的这段代码,直接返回的就是找到索引的那个下标。

利用位比较的方式来实现

同样,为了应用上面我们学到的知识,这里想要泛型比较,搜索,如何实现? 例如,对于这里的int,能否用一个struct,一个double或者其他类型。 答案仍然是肯定的,只不过,我们需要对其中编译器的工作,比较的大小进行控制而已。

int *lsearch(void *key, void *base, int size, int elementSize){
    for (int i = 0; i < size; i++) {
      void * elemeAddr = (char *)base + i * elementSize; 
      if (memcmp(key, elemeAddr, elementSize) == 0)
        return elemeAddr;
    }       
}
 
 

这里的几个说明点就是,首先,传入参数的size就是要比较的数组的大小,类型我们不知道,就用void *类型,然后要传入每一个类型的大小,elementSize,这个标记了每一个数组成员的大小,正因为有这个我们才可以精准的定位到具体的单元,利用for循环来比较每一个单元和key的关系。而这里比较用的memcmp来进行,比较的字节数就是elementSize,传入两个指针即可,而比较的指针就是数组的每一个单元的地址,即elemeAddr而已。


猜你喜欢

转载自blog.csdn.net/dakin_/article/details/80979467