常见笔试题目之函数实现

题目:
已知strcpy函数的原型是:
char * strcpy(char * strDest,const char * strSrc);
1.不调用库函数,实现strcpy函数。
2.解释为什么要返回char *。
解说:
1.strcpy的实现代码
char * strcpy(char * strDest,const char * strSrc)
{
if ((strDest==NULL)||(strSrc==NULL)) //[1]
throw "Invalid argument(s)"; //[2]
char * strDestCopy=strDest; //[3]
while ((*strDest++=*strSrc++)!='/0'); //[4]
return strDestCopy;
}
错误的做法:
[1]
(A)不检查指针的有效性,说明答题者不注重代码的健壮性。
(B)检查指针的有效性时使用((!strDest)||(!strSrc))或(!(strDest&&strSrc)),说明答题者对C语言中类型的隐式转换没有深刻认识。在本例中char *转换为bool即是类型隐式转换,这种功能虽然灵活,但更多的是导致出错概率增大和维护成本升高。所以C++专门增加了bool、true、false三个关键字以提供更安全的条件表达式。
(C)检查指针的有效性时使用((strDest==0)||(strSrc==0)),说明答题者不知道使用常量的好处。直接使用字面常量(如本例中的0)会减少程序的可维护性。0虽然简单,但程序中可能出现很多处对指针的检查,万一出现笔误,编译器不能发现,生成的程序内含逻辑错误,很难排除。而使用NULL代替0,如果出现拼写错误,编译器就会检查出来。
[2]
(A)return new string("Invalid argument(s)");,说明答题者根本不知道返回值的用途,并且他对内存泄漏也没有警惕心。从函数中返回函数体内分配的内存是十分危险的做法,他把释放内存的义务抛给不知情的调用者,绝大多数情况下,调用者不会释放内存,这导致内存泄漏。
(B)return 0;,说明答题者没有掌握异常机制。调用者有可能忘记检查返回值,调用者还可能无法检查返回值(见后面的链式表达式)。妄想让返回值肩负返回正确值和异常值的双重功能,其结果往往是两种功能都失效。应该以抛出异常来代替返回值,这样可以减轻调用者的负担、使错误不会被忽略、增强程序的可维护性。
[3]
(A)忘记保存原始的strDest值,说明答题者逻辑思维不严密。
[4]
(A)循环写成while (*strDest++=*strSrc++);,同[1](B)。
(B)循环写成while (*strSrc!='/0') *strDest++=*strSrc++;,说明答题者对边界条件的检查不力。循环体结束后,strDest字符串的末尾没有正确地加上'/0'。
2.返回strDest的原始值使函数能够支持链式表达式,增加了函数的“附加值”。同样功能的函数,如果能合理地提高的可用性,自然就更加理想。 链式表达式的形式如:
int iLength=strlen(strcpy(strA,strB));
又如:
char * strA=strcpy(new char[10],strB);
返回strSrc的原始值是错误的。其一,源字符串肯定是已知的,返回它没有意义。其二,不能支持形如第二例的表达式。其三,为了保护源字符串,形参用const限定strSrc所指的内容,把const char *作为char *返回,类型不符,编译报错。

------------------------------------------------------------------------------------------华丽丽的分割线------------------------------------------------------------------------------------------------------------------------

在程序员面试的时候,面试官通常会让你实现一个或几个C语言里的库函数,以此来检查你的编程功底。类似的函数实现有atoi.itoa.atof.strcmp.strcpy.memset.memcpy等等。

在平时的编程中,我们可能极少有机会要自己实现这些函数。但是面试官却很喜欢用这些东西考察你。实际上所谓的"考察",永远是个伪命题。面试者其实不是用这个小函数的实现来考察"你行",而大部分寄希望于用这个小函数来"你不行"。随便在网上搜搜,就会发现很多讨论这个的帖子,其中大部分都有一些解答(用来证明你不行的理由),仔细琢磨就会发现其中的有一些还蛮有道理,但有一些解释实在是有点牵强。

说实话,这些函数实现的考察,作用还是有的,但是就我自己感觉,作用还不在单纯的编程能力上,在本文的最后,我会发表我自己的一点看法。还是先说说这些函数的一些实现,都是我自己比较认可的版本,肯定也不一定完全符合面试官的要求,但无论如何,如果我碰到面试官这样考我,我肯定拿下面的答案应付了:)。

首先来看看xtox系列的函数,这类函数涉及数类型和字符类型之间的转换,考察的一个知识点是如何将一个数字转化为对应的字符或者如何将对应的字符转化为数字(以下的代码演示了这些转换方法),在我知道这个方法以前,我都是写个函数,建立字符和数字的对应查询关系(相当于建立了一个数字和字符的查询快表)来匹配两者的对应关系。代码应该来说比较简单,就不加注释了,后文会提到需要额外说明的几点。(特别说明:为篇幅计,下面的代码出现了两行代码出现在一行的情况,真实的代码中可能不希望这样)

-------------------------------------------------------atoi----------------------------------------------------------

int atoi(const char* str)
{
int sign = 0,num = 0;
assert(NULL != str);
while (*str == ' ')
{
str++;
}
if ('-' == *str)
{
sign = 1; str++;
}
while ((*str >= '0') && (*str <= '9'))
{
num = num*10 + (*str - '0'); //就是这一行,将对应字符转化为数字

str++;
}
if(sign == 1)
return -num;
else
return num;
}

-------------------------------------------------atof------------------------------------------------------------

double atof(const char* str)
{
double val = 0.0,power = 0.0;
int sign = 0;
assert(NULL != str);
while (*str == ' ')
{
str++;
}
sign = (*str == '-')? -1 : 1;
if ('-' == *str || '+' == *str)
{
str++;
}
while ((*str >= '0')&&(*str <= '9'))
{
val = val* 10.0 + (*str - '0'); str++;
}
if ('.' == *str)
{
str++;
}
power = 1.0;
while ((*str >= '0')&&(*str <= '9'))
{
val = val* 10.0 + (*str - '0');
power *= 10; str++;
}
return sign*val/power;
}

---------------------------------------------itoa------------------------------------------------------------------------

char* itoa(int val,char* buf,unsigned int radix)
{
char *bufptr;
char *firstdig;
char temp;
unsigned int digval;
assert(buf != NULL);
bufptr = buf;
if (val < 0)
{
*bufptr++ = '-'; val = (unsigned int)(-(int)val);
}
firstdig = bufptr;
do
{
digval =(unsigned int) val % radix; val /= radix;
if (digval > 9)
{
*bufptr++ = (char)(digval - 10 + 'a');
}
else
{
*bufptr++ = (char)(digval + '0');
}
} while(val > 0);
*bufptr-- = '/0';//设置字符串末尾,并将指针指向最后一个字符
do //反转字符
{
temp = *bufptr; *bufptr = *firstdig; *firstdig = temp;
--bufptr; ++firstdig;
} while(firstdig < bufptr);
return buf;
}

----------------------------------------------itoa.end-------------------------------------------------------------------

下面是strxxx和memxxx系列,下面的一些实现,有些我到现在还抱有一些疑问,比如说strcmp函数,为什么要强制转换成unisigned以及为什么要用*dst来判断循环终止而不用*src,还没有找到更好的答案或者彻底弄清楚这些问题,以后弄清楚了再补上。

-------------------------------------------------------strcmp------------------------------------------------------------

int strcmp(const char* src,const char* dst)
{
int ret = 0;
if (src == dst)
{
return 0;
}
assert(NULL != src);//期待源字符串不为空
if (dst == NULL)
{
return -1;
}
while (!(ret = *(unsigned char*)src - *(unsigned char*)dst)&& *dst)
{
++src,++dst;
}
if (ret < 0)
{
ret = -1;
}
else if (ret > 0)
{
ret = 1;
}
return ret;
}

-------------------------------------------strcpy------------------------------------------------------------------------

char* strcpy(char* dst,const char* src)
{
char* strDst = dst;
assert(src != NULL && dst != NULL);//拷贝空串被认为是没有意义的,使用assert检查
while ((*dst++ = *src++) != '/0')
{
NULL;
}
return strDst;
}

--------------------------------------memcpy-------------------------------------------------------------------------

void* memcpy(void* dst,const void* src,size_t count)
{
char* pbTo = (char*)dst;
char* pbFrom = (char*)src;
assert(dst!= NULL && src != NULL);
assert(pbTo >= pbFrom+count || pbFrom >= pbTo + count);//防止内存重叠(overlap)
while (count-- > 0)
{
*pbTo++ = *pbFrom++;
}
return dst;
}

--------------------------------------memmove---------------------------------------------------------------------

void* memmove(void* dst,const void* src,size_t count)
{
char* pbTo = (char*)dst;
char* pbFrom = (char*)src;
assert(dst != NULL && src != NULL);
if (dst <= src || pbTo >= pbFrom + count)//没有overlap的情况,直接拷贝
{
while (count-- > 0)
{
*pbTo++ = *pbFrom++;
}
}
else
{
pbTo = pbTo + count -1;//overlap的情况,从高位地址向低位拷贝
pbFrom = pbFrom + count -1;
while (count-- > 0)
{
*pbTo-- = *pbFrom--;
}
}
return dst;
}

--------------------------------------memset-------------------------------------------------------------------------

void* memset(void* buf,int c,size_t count)
{
char* pvTo = (char*)buf;
assert(buf != NULL);
while (count-- >0)
{
*pvTo++ = (char)c;
}
return buf;
}

--------------------------------------memset.end---------------------------------------------------------------------

这些函数的代码都很短小,但是面试官对你这几行短小的代码抱有很高的期望。

首先,正确性!实现得都不正确,那还搞毛啊,其他的小问题肯定谈都不用谈了,直接out!正确性要注意的地方,每个函数的功能起码要了解(memmove等),边界的检查不能出错;返回值也是要注意的地方。

其次,assert不能少!对指针有效性的检查是非常必要的,特别是在memcpy中,存在两个assert,分别检查指针的有效性以及内存是否交叠。针对第二个assert还要加上必要注释(因为代码的维护者并不是一眼就能看出这个assert的涵义,搞不好可能直接在维护代码中删掉这么重要的一个assert检查)。

然后要注意的有:指针的有效性检验,最好是与NULL进行比较。最后,空语句(strcpy)、大括号一个都不能少。

我自己的想法,其实这些函数(包含其他小的函数),难度不算太大,当然要写得完全正确也非常不易。但是,在正确性的基础上,面试官期望从你的代码中发现你身上作为程序员的素质和态度!当我们编程的时候,我们的脑海里真的想的是手里敲的代码吗?当我们正在实现一个函数或者一个类的时候,我们真的认真考虑了它应该怎样被实现吗?针对这些小函数(当然,大函数更一样了),参数的有效性检查是always必要的(不要相信任何输入!),边界的有效性检查是很容易出错的!每一行代码都要想清楚为什么要这么实现,我想,只要你脑子所想是你手上所要做的,我手"写"我心,自然是不会出错了,起码也应该知道自己错在哪里,下次改正就好。《微软C编程精粹》最后一句话话告诉我们:成功地书写无错代码的关键可以总结为一个总的原则,即绝不允许同样的错误出现两次!

最后,推荐一些参考书,就我自己的阅读经验来看,这些参考书对如何写好这些小函数还是很有借鉴意义的,按重要性排序:

1.《编程精粹—微软编写优质无错C程序秘诀》,Writing Clean Code——Microsoft Techniques for Developing Bug-free C Programs;

2.《高质量C/C++编程指南》,一本书,也是林锐编的,网上广为流传的同名小册子算是精简版;

3.《程序员面试宝典》;

提供1和2(小册子)的电子版,以后有时间的时候上传上来。等不及的email知会一声:[email protected]

-------------------------------------------------------------------------------------------------------华丽丽的分割线-----------------------------------------------------------------------------------------------------------

题目:编写类String的构造函数、析构函数和赋值函数 (c++笔试题)
已知类String的原型为:
class String
{
public:
String(const char *str = NULL); // 普通构造函数
String(const String &other); // 拷贝构造函数
~ String(void); // 析构函数
String & operate =(const String &other); // 赋值函数
private:
char *m_data; // 用于保存字符串
};
请编写String的上述4个函数。
参考答案
// String的析构函数
String::~String(void) // 3分
{
delete [] m_data;
// 由于m_data是内部数据类型,也可以写成 delete m_data;
}

// String的普通构造函数
String::String(const char *str) // 6分
{
if(str==NULL)
{
m_data = new char[1];
*m_data = ‘0’;
}
else
{
int length = strlen(str);
m_data = new char[length+1];
strcpy(m_data, str);
}
}

// 拷贝构造函数
String::String(const String &other) // 3分
{
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
}

// 赋值函数
String & String::operate =(const String &other) // 13分
{
// (1) 检查自赋值 // 4分
if(this == &other)
return *this;

// (2) 释放原有的内存资源 // 3分
delete [] m_data;

// (3)分配新的内存资源,并复制内容 // 3分
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);

// (4)返回本对象的引用 // 3分
return *this;
}

你可能感兴趣的:(常见笔试题目之函数实现)