第五章 遇到的问题及解决方案
@H_403_21@
5.1 “奇怪”的段错误
5.1.1
问题后果及前因
@H_404_54@在修改编码之初,我先在一个数据结构里添加了一个变量,几天后,我添加了一个极为简单的函数,这个函数会访问那个数据结构新添加的变量。编译和连接都没有问题,make install也成功了。但在运行时竟然出现段错误。由于函数是一个非常简单的函数,可以确信不会产生任何越界或非法访问之类的操作。但问题是什么呢?
@H_404_54@这让我伤透了脑筋,调试的时候也是出现此错误,而且还找不到原因。
@H_404_54@后来发现是因为系统时间的问题而导致make的错误。由于在修改数据结构时的时间竟然是比原始的修改前的文件的时间还要早,因此其实没有重新编译。而后来操作此变量的函数所在的文件可能又重新编译了。这样的结果当然是,此函数的语句在引用一个内存中根本没有出现的变量,不会出错才怪呢。
5.1.2
问题解决之道
@H_404_54@解决的方法极为简单,修改系统时间,重新修改修改过的文件并保存,最后重新编译系统。一切OK!
5.1.3
经验 结论 技巧
@H_404_54@结论:
² make只是管理代码关连的工具,它也会犯错,但这种错误是源于程序员本身。
² 请关注开发时对版本的控制。
5.2 不能Debug出来的宏错误
5.2.1
问题后果及前因
@H_404_54@我遇到的宏错误问题主要是swap引起的,由于Postgresql在qsort_arg中没有用memcpy,而是用了几个宏来实现字节的拷贝。相关代码和测试如下:
//qsort.c
static void swapfunc(char *,char *,size_t,int);
//真正的拷贝函数
#define swapcode(TYPE,parmi,parmj,n) /
do { /
size_t i = (n) / sizeof (TYPE); /
TYPE *pi = (TYPE *)(void *)(parmi); /
TYPE *pj = (TYPE *)(void *)(parmj); /
do { /
TYPE t = *pi; /
*pi++ = *pj; /
*pj++ = t; /
} while (--i > 0); /
} while (0)
// 调用swap前一定要使用此宏来初始化!!
#define SWAPINIT(a,es) swaptype = ((char *)(a) - (char *)0) % sizeof(long) || /
(es) % sizeof(long) ? 2 : (es) == sizeof(long)? 0 : 1;
static void
swapfunc(a,b,n,swaptype)
char *a, *b;
size_t n;
int swaptype;
{
if (swaptype <= 1)
swapcode(long,a,n);
else
swapcode(char,n);
}
//实现任意数据结构的交换
#define swap(a,b) /
if (swaptype == 0) { /
long t = *(long *)(void *)(a); /
*(long *)(void *)(a) = *(long *)(void *)(b); /
*(long *)(void *)(b) = t; /
} else /
swapfunc(a,es,swaptype)
// 测试swap
typedef struct student{
char * name;
char * id;
long year;
long day;
}student;
int main()
{
int swaptype;
student a ;
student b;
int es = sizeof(a);
a.day=1;
b.day=2;
SWAPINIT(&a,es)
swap(&a,&b);
return 0;
}
|
5.2.2
问题解决之道
@H_404_54@解决的办法是:用宏时一定要用括号。或者,最好不用宏。
5.2.3
经验 结论 技巧
@H_404_54@从中我们可以看到宏的巨大缺点,同时也是它的优点——都是简单的字符串替换策略导致。它的优点是可以不用检查类型,而这正是现在高级语言所避免的,现在无论是Java,还是C#都是强语言类型,就连哪怕int a=1;if(a==true)之类的语句都不能通过。这有效的防止了编译时通过,而运行时出现的“莫明其妙”的问题。加之我们的痛苦经验,我们可以得出一个结论:
5.3 结构体成员访问错误
5.3.1
问题后果及前因
@H_404_54@在修改Tuplesortstate结构体后,在ExecSort中添加代码以访问此结构体的成员时,遇到这个错误,编译器提示说无法访问结构体成员。我一时无法理解,明明我在Tuplesortstate结构体内添加了三个成员变量,而且也已经编译通过了,在ExecSort函数所在的nodeSort.c的开始,也已经把Tuplesortstate的头文件包含进来了。声明一个Tuplesortstate *指针也可以,为什么却不能访问其变量?
5.3.2
问题解决之道
@H_404_54@解决的方法是把Tuplesortstate结构的定义从tuplestore.c中一份到nodeSort.c即可。
5.3.3
经验 结论 技巧
@H_404_54@问题的实质在于:要定义一个类型的变量或引用此类型变量(或指针)的成员时,必须知道这个类型的内存布局,而不只是一个名字。关于类型、定义与内存布局在下一节中有更为详细的阐述。
5.4 “防不胜防”的下标出错
5.4.1
问题后果及前因
@H_404_54@在算法编写完成并编译通过后,我用了几乎相当于编写算法的时间来调试程序,而程序中最难调试也是错误最大的错误便是数组下标错位!这些代码虽然占了大概不到20%的代码,但这种错误却几乎占掉了调试的80%的时间精力,或许这也是一种另类的20-80原则?
5.4.2
问题解决之道
@H_404_54@这种问题的解决办法,除了写程序时的脑袋保持清醒外,几乎没有什么全能的办法可以杜绝。倒是有一些习惯或叫技巧可以借鉴。
5.4.3
经验 结论 技巧
我们也可以有一个这样的结论:正确的下标操作=清晰的思路+良好的习惯+纯熟的经验+耐心。
或者,我们也可以写这样的等式:好的程序员 >=(一定有拥有) 清晰的思路+良好的习惯+纯熟的经验+耐心。
5.5 “不可思议”的死循环
5.5.1
问题后果及前因
@H_404_54@在编写Select(S,K)算法之后,编译、调试并运行都已经通过,且结果很正确。当我在表里再添加一些元组,运行同样的语句,竟然就出现“server closed the connection unexpectedly”。而且在修改select的count或offset参数时,这种错误有时出现,有时又不出现,很像内存访问的问题,但一时也让我百思不得其解。
图表 29:“不可思议”的死循环BUG的跟踪观察结果
5.5.2
问题解决之道
@H_404_54@注意,由于要处理元组排序码相等的情况,所以我们把和pTemp相同的元素也一并放入到pLarger中,也包括pTemp本身。也正是因为这个原因,只需要修改最后一步,让其循环找到pTemp后,将其和pLarger[0]交换,递归规模也减少1,问题也就解决了。
//qsort_arg.c
/*ouyang 12.11*/
// 如果恰好有smallerInder比pTemp小,则pTemp即为解.
if(k==(smallerInder+1))
{
}
// 否则缩小子问题,递归调用求解.
else if(k<(smallerInder+1))
{
pTemp = select_s_k(pSmaller,k,smallerInder,cmp,arg,select_count,select_offset);
}
else
{
if(smallerInder>0)
{
pTemp = select_s_k(pLarger,k-smallerInder,largerInder,select_offset);
}
else//!!ouyang 12-15找到一个巨大的BUG,当smallerInder为时,即所选的pTemp刚好就是最小无元素之时,如果不加这个分支,会导致死循环!!!!
//由于largerInder中包含和pTemp相等的元素,故不可能为,不用考虑。
{
for(i=0;i<largerInder;i++)
{
if(pLarger[i]==pTemp)
{
//swap to position 0
pTemp= pLarger[0];
pLarger[0]=pLarger[i];
pLarger[i]=pTemp;
break;
}
}
pTemp = select_s_k(&pLarger[1],k-smallerInder-1,largerInder-1,select_offset);
}
}
// 释放内存
free(pMid);
free(pSmaller);
free(pLarger);
return pTemp;
}
|
5.5.3
经验 结论 技巧
@H_404_54@经验结论:边界的处理,最少的代码却可能隐藏着最大的错误。
第六章 心得体会
@H_403_21@
6.1 版本控制 提纲挈领
6.1.1
版本控制 事关重大
@H_404_54@前一章提到的make因为系统时间的问题而导致一些错误的行为,其实都是因为版本的控制而导致的。由于Postgresql的文件达一千多个,文件夹也有200多个。如果修改错了某个文件,过了一段时间,都有可能不知修改了什么文件,在什么文件夹下,可见版本控制 事关重大。
@H_404_54@在企业,这个问题也会被极大的放大,因此一般都会采用CVS或SVN之类的方式来解决版本控制问题。但由于我们这个实习虽然面对的文件却很多,但修改的地方比较少,同组的人也只有一两个,当然就不会兴师动众的何用SVN之类的解决方案了。
@H_404_54@在本实习中,我采用了以下的方式来控制版本,以达到提纲挈领的作用。
6.1.2
统一注释方便搜索
在每处修改之处加统一的注释,方便搜索。比如,本项目所有修改的地方都有ouyang和修改的日期做注释。
6.1.3
建立映像文件夹 只保存修改文件
新建一个文件夹,采用和Postgresql相同的文件夹结构,但只把修改过的文件拷贝进来,只针对此文件夹查找和跳转。如果要转入到Postgresql文件夹下,只须删除路径中的那个新文件段即可。如home/postgresqlChanged12-16/postgresql-
6.2 小议内存 指针乃王道
6.2.1
程序员与内存
@H_404_54@在Postgresql中,我们应用到了大量的指针排序,也遇到了关于内存的种种问题,这促使我们撰写此节,来探究关于内存的更多话题。
6.2.2
类型与变量 指针变量与内存布局
@H_404_54@类型是定义好的一种内存布局,变量是按照这个定义好的类型和布局去开辟定量的内存空间。而指针也是一个变量,此变量存储在一个四个字节的内存区域中,存储的内容是某个变量的地址或者是NULL。
// ouyang 12-16
// 内存与布局的测试
#include <iostream>
using namespace std;
typedef struct student{
int day;
int year;
}student;
typedef struct teacher {
int teacher_day;
//int year;
}teacher;
int main()
{
student a;
a.day =1;
teacher *b;
b = (teacher*)(void*)(&a);
cout<<"b->teacher_day: "<<b->teacher_day;//可以正常输出
return 0;
}
|
6.2.3
类型与函数 定义与声明
@H_404_54@类与函数,从不同的角度实现了代码的复用。你或许可以很轻易的指出两者的不同,比如类是对相同概念的一种包装,而函数是对算法的共同步骤进行的包装。但是,反应到内存角度,他们有什么区别?
6.3 面向对象 大势所趋
6.3.1
面向过程与面向对象
@H_404_54@在阅读Postgresql源码的时候,一个最大的收获就是使用一个面向过程的C语言实现了面向对象的种种特性。两种编程思想的特点在N多的文章或书籍中都有很好的注解。比较经典的表述是:(我不知道是不是可以称为经典,因为这两句话深存于脑海中已多年):
6.
@H_502_3358@
3.2
C与伪继承
@H_404_54@Postgresql中所有的结点类都采用了此技巧。比如所有的类都继承自Node,所有的计划状态类都继承自PlanState类。下面总结一下此技术的关键几点:
基于以上的原因,C可以很简单的实现伪继承。此技术相对下面要讨论的多态还比较简单,下面就剖析一下用C来实现多态的技术。
6.
3.3
C与多态
@H_404_54@Postgresql中没有真正实现多态的技术,而只是用了switch—case+伪继承来实现“不同类型的不同动作”。但是switch—case结构无论出现在什么地方,都意味着很有可能这段代码的可扩展性就大大的降低。因为用这种技术来分支,都是在编译时就已经定下来了,如果未来要增加一种类型,就必须得修改所有地方的代码(包括类、switch—case还有类型枚举标记)并重新编译。这显然有违软件工程中的简单性和可扩展性原则。下面就简要讨论多态实现的几个关键技术。
² virtual table:每一个类都会有一个Virtual table与之相联,并且每个类都至少有一个虚函数,那就是它的虚析构函数。Virtual table其实只是一个函数指针数组。一个类的Virtual table由在这个类中所声明的以及由这个类继承来的虚函数的地址所构成。对于继承来的虚函数,仅仅那些没有被覆写的才会被加进去。
² 内存布局:在上一小节中已经描述。不再赘述。
我之前实现过一个简单的模拟,但没有找到代码,等找到之后再补充此点。
6.4 编程经验积累
6.4.1
编译与运行 静态与动态
@H_404_54@C++的很多哲学是:能在编译时确定的事情就一定在编译时确定,而Java的设计中,却经常出现如果在运行才真正确定,那就等运行时刻再定吧。比如C/C++中不允许数组在运行时候指定长度,而Java则在运行时可以动态的改变数组的长度。
² 预编译,如宏替换,include之类。
² 类的结构与接口
² 继承
² 数组
² 变量换名
² 变量定义
运行时确定的事情有:
² 动态申请内存
² 多态以决定类的行为
² 类中的引用(如指针)所指的真正类型及行为
可能还有更多。这里只是罗列了几类。
无论如何,区分静态与动态,编译与运行,并把可能的错误尽量的在编译时找到,而不是运行时,是正确的编程,并编写出正确的程序所必须了解的事情。
6.4.2
继承与组合 统一与灵活
@H_404_54@继承是类的静态结构,而组合是获得类的功能的动态方法。比如在PlanState中,既继承自Node,又包含Plan和Estate引用,他因此获得三者的功能。
² 继承是类的静态行为,在编译时就确定了不会变;而组合是对象的动态行为,在运行时才确定。
² 使用对象的组合来设计可以更灵活,更有弹性。这是因为你只需要调用组合类的接口即可完成接口的“包装”,更有弹性。虽然与继承相比,它多了一个对象的引用,但只是一个对象的引用而已,这可以使得在运行时使这个引用指向不同的子类,而获得在运行时确定行为时改变行为的弹性。
² 使用类的继承可能会增加大量的类,导致管理不太方便。
² 继承与组合的共同点是都可以代码复用的产生新的行为,只是前者可能局限于“覆盖重写”,而后者则是接口调用重新包装。
当然,这是有关设计模式的东西,我们不做深谈。重要的是:我们应该时刻牢记,我们的设计应该尽量的统一和灵活,当两者不能同时具备时,只能根据我们的需要做一种折衷权衡。
6.5 一份耕耘 一分收获
终于写到本文的最后了,这篇长达50页的报告中,汇聚了我们的心血,也充满了我们的收获。也因此,我们更加相信:一份耕耘@H_449_4030@ 一分收获!
原文链接:https://www.f2er.com/postgresql/197432.html