面试题1


  试题1:

void test1()
{
 char string[10];
 char* str1 = "0123456789";
 strcpy( string, str1 );
}


  试题2:

void test2()
{
 char string[10], str1[10];
 int i;
 for(i=0; i<10; i++)
 {
  str1[i] = 'a';
 }
 strcpy( string, str1 );
}


  试题3:

void test3(char* str1)
{
 char string[10];
 if( strlen( str1 ) <= 10 )
 {
  strcpy( string, str1 );
 }
}


  解答:

 

我对这几道题目的分析如下:
1、   字符串和字符数组char str1[]的异同点:
通过实例说明:string str=”0123456789”; char str1[]={0,1,2,3,4,5,6,7,8,9}
(1) 实际上字符串str也是由一个个的字符构成的,每一个字符都占据一个字节的空间,这一点和字符数组str1是一样的,但是字符串str后面会有一个追加的空字符’/0’来表示字符串的结尾,而字符数组是不用的。图示如下:
str字符串

 

0
1
2
3
4
5
6
7
8
9
/0

 

str1字符数组

 

0
1
2
3
4
5
6
7
8
9

 

                           
(2) 但是我们在使用strlen()函数,获取字符串或字符数组长度时,都是10,因为在计算字符串长度时,并没有将字符串的最后一个空字符’/0’计算在内,这一点可以参照下面的strlen()函数的具体实现。
2、   关于字符串拷贝函数strcpy():
这个函数需要两个字符指针char*参数,第一个参数表示目的字符串,第二个参数表示源字符串。但是需要注意的就是第二个字符指针必须指向一个字符串,而不能是一个字符数组,因为strcpy()函数具体实现里面有一个for循环,而这个for循环的结束条件就是碰到字符串中的空字符’/0’,如果第二个字符指针指向字符数组的话,将会导致for循环得不到正常结束,从而导致拷贝错误。Strcpy()函数也可参照下面的具体实现代码。
3、   字符串指针char*或称做字符指针的初始化问题(关键要习惯这种初始化格式):
(1) 将char*看作字符串指针,使用字符串初始化:
string str=”12345”;
char* ch=str;//错误,不能将str型赋给char*,因为str是一个string类对象,类型不同不能赋值。
但是,下面的初始化就是正确的:
char ch="12345";//"12345"根本就是一个字符串,也就是一个追加了空字符的字符数组而已,它的内存模型和string类对象根本就是两码事,这里初学者很容易搞混。
(2) 将char*看作字符串指针,使用字符数组初始化:
char str[]={1,2,3,4,5};
char* ch=str;//实际就是将字符数组中的第一个字符地址赋给ch
上一语句相当于:char* ch=&str[0];
实际上,上面的字符数组str可以直接使用字符串初始化,这也是可以的,即:
char str[]="12345";
(3) 将char*看作字符指针,使用某个字符的地址初始,但这种初始化方式的实际意义并不大:
char str=’A’;
char* ch=&str;


  试题1字符串str1需要11个字节才能存放下(包括末尾的’/0’),而string只有10个字节的空间,strcpy会导致数组越界;

  对试题2,如果面试者指出字符数组str1不能在数组内结束可以给3分;如果面试者指出strcpy(string, str1)调用使得从str1内存起复制到string内存起所复制的字节数具有不确定性可以给7分,在此基础上指出库函数strcpy工作方式的给10分;

  对试题3,if(strlen(str1) <= 10)应改为if(strlen(str1) < 10),因为strlen的结果未统计’/0’所占用的1个字节。

  剖析:

  考查对基本功的掌握:

  (1)字符串以’/0’结尾;

  (2)对数组越界把握的敏感度;

  (3)库函数strcpy的工作方式,如果编写一个标准strcpy函数的总分值为10,下面给出几个不同得分的答案:

  2分

void strcpy( char *strDest, char *strSrc )
{
  while( (*strDest++ = * strSrc++) != ‘/0’ );
}


  4分

void strcpy( char *strDest, const char *strSrc ) 
//将源字符串加const,表明其为输入参数,加2分
{
  while( (*strDest++ = * strSrc++) != ‘/0’ );
}


  7分

void strcpy(char *strDest, const char *strSrc) 
{
 //对源地址和目的地址加非0断言,加3分
 assert( (strDest != NULL) && (strSrc != NULL) );
 while( (*strDest++ = * strSrc++) != ‘/0’ );
}


  10分

//为了实现链式操作,将目的地址返回,加3分!

char * strcpy( char *strDest, const char *strSrc ) 
{
 assert( (strDest != NULL) && (strSrc != NULL) );
 char *address = strDest; //必须将目的字符串的目的地址保存,以防下面的while之后,无法返回目的字符串的地址,即第一个字符的地址
 while( (*strDest++ = * strSrc++) != ‘/0’ ); 
  return address;
}


  从2分到10分的几个答案我们可以清楚的看到,小小的strcpy竟然暗藏着这么多玄机,真不是盖的!需要多么扎实的基本功才能写一个完美的strcpy啊!

  (4)对strlen的掌握,它没有包括字符串末尾的'/0'。

  读者看了不同分值的strcpy版本,应该也可以写出一个10分的strlen函数了,完美的版本为: int strlen( const char *str ) //输入参数const

{
 assert( strt != NULL ); //断言字符串地址非0
 int len;
 while( (*str++) != '/0' ) 
 { 
  len++; 
 } 
 return len;
}


  试题4:

void GetMemory( char *p )
{
 p = (char *) malloc( 100 );
}

void Test( void ) 
{
 char *str = NULL;
 GetMemory( str ); 
 strcpy( str, "hello world" );
 printf( str );
}


  试题5:

char *GetMemory( void )

 char p[] = "hello world"; 
 return p; 
}

void Test( void )

 char *str = NULL; 
 str = GetMemory(); 
 printf( str ); 
}


  试题6:

void GetMemory( char **p, int num )
{
 *p = (char *) malloc( num );
}

void Test( void )
{
 char *str = NULL;
 GetMemory( &str, 100 );
 strcpy( str, "hello" ); 
 printf( str ); 
}


  试题7:

void Test( void )
{
 char *str = (char *) malloc( 100 );
 strcpy( str, "hello" );
 free( str ); 
 ... //省略的其它语句
}


  解答:

  试题4传入中GetMemory( char *p )函数的形参为字符串指针,在函数内部修改形参并不能真正的改变传入形参的值,执行完

char *str = NULL;
GetMemory( str );


  后的str仍然为NULL;

        实际上,上面的参数传递是数值传递,不是地址传递,如果传入的指针地址是有效的,那么修改形参的指针值是不影响实参的,但如果修改形参指针指向的具体数据的话,那么实参指针指向的数据也就会改变的,因为形参和实参是地址相同的两个指针,他们指向同一个内存单元。

  试题5中

char p[] = "hello world"; 
return p;


  的p[]数组为函数内的局部自动变量,在函数返回后,内存已经被释放。这是许多程序员常犯的错误,其根源在于不理解变量的生存期。实际上,字符数组p存放在栈中的地址返回了,但是退出函数后,指针p指向的栈空间被释放,也就是说p指向的内存空间已经在退出函数时变成无效内存了

  试题6: 该题稍微麻烦的地方是使用的指向指针型数据的指针:char **p这里我们就务必要清楚指针p的具体指向模型,否则无法做该题。

 

GetMemory避免了试题4的问题,传入GetMemory的参数为字符串指针的指针,但是在GetMemory中执行申请内存及赋值语句

*p = (char *) malloc( num );


  后未判断内存是否申请成功,应加上:

if ( *p == NULL )
{
 ...//进行申请内存失败处理
}


  试题7存在与试题6同样的问题,在执行

char *str = (char *) malloc(100);


  后未进行内存是否申请成功的判断;另外,在free(str)后未置str为空,导致可能变成一个“野”指针,应加上:

str = NULL;


  试题6的Test函数中也未对malloc的内存进行释放。

  剖析:

  试题4~7考查面试者对内存操作的理解程度,基本功扎实的面试者一般都能正确的回答其中50~60的错误。但是要完全解答正确,却也绝非易事。

  对内存操作的考查主要集中在:

  (1)指针的理解;

  (2)变量的生存期及作用范围;

  (3)良好的动态内存申请和释放习惯。

  再看看下面的一段程序有什么错误:

swap( int* p1,int* p2 )
{
 int *p;
 *p = *p1;
 *p1 = *p2;
 *p2 = *p;
}


  在swap函数中,p是一个“野”指针,有可能指向系统区,导致程序运行的崩溃。在VC++中DEBUG运行时提示错误“Access Violation”。该程序应该改为:

swap( int* p1,int* p2 )
{
 int p;
 p = *p1;
 *p1 = *p2;
 *p2 = p;
}

 

2.内功题 
 
试题1:分别给出BOOL ,int ,float,指针变量 与“零
值”比较的 if 语句(假设变量名为var ) 
 
  解答: 
 
   BOOL 型变量:if(!var) 
 
   int 型变量: if(var==0) 
 
   float型变量: 
 
   const float EPSINON = 0.00001; 
 
   if ((x >= - EPSINON) && (x <= EPSINON) 
 
   指针变量:  if(var==NULL) 
 
  剖析: 
 
  考查对0值判断的“内功”,BOOL 型变量的0判断完全可以写
成if(var==0) ,而int 型变量也可以写成if(!var) ,指针变量的
判断也可以写成if(!var) ,上述写法虽然程序都能正确运行,但
是未能清晰地表达程序的意思。 
 
 
 
 
  一般的,如果想让if 判断一个变量的“真”、“假”,应直接使
用if(var)、if(!var) ,表明其为“逻辑”判断;如果用if 判断一个
数值型变量(short、int 、long等),应该用if(var==0) ,表明
是与0进行“数值”上的比较;而判断指针则适宜用if
(var==NULL) ,这是一种很好的编程习惯

浮点型变量并不精确,所以不可将float变量用“==” 或“!
=”与数字比较,应该设法转化成“>=” 或“<=” 形式。如果写成
if (x == 0.0) ,则判为错,得0分。 

试题2:以下为Windows NT下的32位C++程序,请计算
sizeof 的值 
void Func ( char str[100] ) 

 sizeof( str ) = ? 

 
void *p = malloc( 100 ); 
sizeof ( p ) = ?
 
  解答: 
 
sizeof( str ) = 4 
sizeof ( p ) = 4
 
  剖析: 
 
  Func ( char str[100] )函数中数组名作为函数形参时,
在函数体内,数组名失去了本身的内涵,仅仅只是一个指针;在
失去其内涵的同时,它还失去了其常量特性,可以作自增、自减
等操作,可以被修改。 
 
  数组名的本质如下: 
 
  (1)数组名指代一种数据结构,这种数据结构就是数组; 
 
  例如: 
 
char str[10]; 
cout << sizeof(str) << endl;
 
  输出结果为10,str指代数据结构char[10]。 
 
  (2)数组名可以转换为指向其指代实体的指针,而且是一
个指针常量,不能作自增、自减等操作,不能被修改; 
 
char str[10];  
str++; // 编译出错,提示str不是左值 
 
  (3)数组名作为函数形参时,沦为普通指针。 
 
  Windows NT 32位平台下,指针的长度(占用内存的大
小)为4字节,故sizeof( str ) 、sizeof ( p )  都为4。

4. 嵌入式系统中经常要用到无限循环,你怎么样用C编写死循环呢?
这个问题用几个解决方案。我首选的方案是:
while(1) { } 
一些程序员更喜欢如下方案:
for(;;) { } 
这个实现方式让我为难,因为这个语法没有确切表达到底怎么回事。如
果一个应试者给出这个作为方案,我将用这个作为一个机会去探究他们
这样做的
基本原理。如果他们的基本答案是:“我被教着这样做,但从没有想到过
为什么。”这会给我留下一个坏印象。
第三个方案是用goto 
Loop: 
... 
goto Loop; 

4. static有什么用途?(请至少说明两种) 
1.限制变量的作用域 
2.设置变量的存储域 

7. 引用与指针有什么区别? 
1) 引用必须被初始化,指针不必。 
2) 引用初始化以后不能被改变,指针可以改变所指的对象。 
2) 不存在指向空值的引用,但是存在指向空值的指针

9. 全局变量和局部变量在内存中是否有区别?如果有,是什么区别? 
全局变量储存在静态数据库,局部变量在堆栈

内存分配方式有三种:
  
  (1)从静态存储区域分配。内存在程序编译的时候就已经分配好了,这块内存在程序的整个运行期间都存在。例如全局变量,static.
  
  (2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中。效率很高,但是分配的内存容量有限。
  
  (3)从堆上分配,也成动态分配内存。程序在运行的时候用malloc/new申请任意多少的内存,程序员自己负责在何时用free/delete释放内存。内存分配的生存期由我们决定,使用非常灵活,但是问题也很多。

11. 堆栈溢出一般是由什么原因导致的? 
没有回收垃圾资源 

3.不能做switch()的参数类型是: 
switch的参数不能为实型


内存溢出指你申请了10个字节的空间,但是你在这个空间写入11或以上字节的数据,就是溢出
内存泄漏指你用malloc或new申请了一块内存,但是没有通过free或delete将内存释放,导致这块内存一直处于占用状态
 

extern可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。另外,extern也可用来进行链接指定。

 

void *memset(void *s, char ch,  size_t n);
函数解释:将s中前n个字节 (typedef unsigned int size_t)用 ch 替换并返回 s 。
memset:作用是在一段内存块中填充某个给定的值,它是对较大的 结构体数组进行清零操作的一种最快方法 [1]
  1. char passwd[10]; 
  2.  
  3.     memset(passwd,0,sizeof(passwd)); 

2.strcpy()函数

问:下面是一个简单的密码保护功能,你能在不知道密码的情况下将其破解吗?

   
   
  1. #include<stdio.h> 
  2.  
  3. int main(int argc, char *argv[]) 
  4.     int flag = 0; 
  5.     char passwd[10]; 
  6.  
  7.     memset(passwd,0,sizeof(passwd)); 
  8.  
  9.     strcpy(passwd, argv[1]); 
  10.  
  11.     if(0 == strcmp("LinuxGeek", passwd)) 
  12.     { 
  13.         flag = 1; 
  14.     } 
  15.  
  16.     if(flag) 
  17.     { 
  18.         printf("\n Password cracked \n"); 
  19.     } 
  20.     else 
  21.     { 
  22.         printf("\n Incorrect passwd \n"); 
  23.  
  24.     } 
  25.     return 0; 

答:破解上述加密的关键在于利用攻破strcpy()函数的漏洞。所以用户在向“passwd”缓存输入随机密码的时候并没有提前检查“passwd”的容量是否足够。所以,如果用户输入一个足够造成缓存溢出并且重写“flag”变量默认值所存在位置的内存的长“密码”,即使这个密码无法通过验证,flag验证位也变成了非零,也就可以获得被保护的数据了。例如:

   
   
  1. $ ./psswd aaaaaaaaaaaaa 
  2.  
  3. Password cracked 

虽然上面的密码并不正确,但我们仍然可以通过缓存溢出绕开密码安全保护。

要避免这样的问题,建议使用 strncpy()函数

.void*和C结构体

问:你能设计一个能接受任何类型的参数并返回interger(整数)结果的函数吗?

答:如下:

 
 
  1. int func(void *ptr) 

如果这个函数的参数超过一个,那么这个函数应该由一个结构体来调用,这个结构体可以由需要传递参数来填充。

问:下面的代码段有错,你能指出来吗?

 
 
  1. #include<stdio.h> 
  2.  
  3. int main(void
  4.     char *ptr = "Linux"
  5.     *ptr = 'T'
  6.  
  7.     printf("\n [%s] \n", ptr); 
  8.  
  9.     return 0; 

答:这是因为,通过*ptr = ‘T’,会改变内存中代码段(只读代码)“Linux”的第一个字母。这个操作是无效的,因此会造成seg-fault或者崩溃。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值