C语言面试常考函数和坑

195阅读模式

一 前言

思维还是比较奇怪的东西,面临未知的时候充满了谨慎,对于自己稍微熟悉的东西,又会犯自大的问题,从而有些理所当然了,所以任何时候谨慎是个好习惯,用任何函数的时候多读读API的文档说明,可以避免不少坑的。根据我的经验来说,任何自己忽略的事情,总会让自己付出代价,或早或晚,还是那句话,出来混,底子要扎实了,不然早晚会还的。文章源自懂站帝-http://www.sfdkj.com/13321.html

二 容易出差的函数

2.1 snprintf返回值

对C程序来说缓冲区溢出攻击发生的代码,多是使用没有带长度的api,比如使用strcpy,sprintf,作为替代,常常可以使用strncpy和snprintf 替代,但是这两个函数也是有坑的。文章源自懂站帝-http://www.sfdkj.com/13321.html

   #include <stdio.h>
    int snprintf(char *str, size_t size, const char *format, ...);

测试方法:文章源自懂站帝-http://www.sfdkj.com/13321.html

void test_snprintf(void)
{
   char abc[10] ={0};
   int len = snprintf(abc,sizeof(abc),"%s","def");
   printf("copy len:%d str:%s\n",len,abc);
  
   char * need_copy = "012345678910";
   int len2 = snprintf(abc,sizeof(abc),"%s",need_copy);
   printf("copy len2:%d str:%s\n",len2,abc);
}

snprintf 是按照格式要求,最多复制size个字符到str中(实际是size-1个,最后是结束符0)。 有三种返回值: 1、 负数: 表示出错; 2、 如果str足够大,那么返回的是实际打印的数值,比如我们的例子len为3. 3、 如果str不够大,比如第二种情况,abc的大小为10,而我们要复制的是12个字符, 那么复制了9个字符到str中,且在尾部补0,但是注意了,返回的是原始字符的长度,即12,这个地方很奇怪吧。文章源自懂站帝-http://www.sfdkj.com/13321.html

copy len:3 str:def
copy len2:12 str:012345678

常常使用snprintf 做字符串的连接,挺好用的,只是要注意下返回值不是打印多少就返回多少的。文章源自懂站帝-http://www.sfdkj.com/13321.html

2.2 strncpy

函数原型:文章源自懂站帝-http://www.sfdkj.com/13321.html

       #include <string.h>
       char *strcpy(char *dest, const char *src);
       char *strncpy(char *dest, const char *src, size_t n);

strncpy说明:文章源自懂站帝-http://www.sfdkj.com/13321.html

     The strncpy() function is similar, except that at most n bytes of src are copied.  Warning: If there is no null byte among the first  n  bytes  of  src,  the string placed in dest will not be null-terminated.

strncpy 一般用来替代strcpy的,比strcpy安全一点,即最多从src中copy n个字符到dst中,如果这n个字符没有包含null,则函数不会补全。 可能的实现是这样的:文章源自懂站帝-http://www.sfdkj.com/13321.html

  char * strncpy(char *dest, const char *src, size_t n)
 {
         size_t i;
         for (i = 0; i < n && src[i] != '\0'; i++)
                   dest[i] = src[i];
         for ( ; i < n; i++)
                   dest[i] = '\0';
               return dest;
 }

从上面的可能实现看出,n如果远大于src的长度,会对其余的部分补0。 举个例子:文章源自懂站帝-http://www.sfdkj.com/13321.html

#include <stdio.h>
#include <string.h>
void teststrncpy(void)
{
   char dst[8];
   char * src = "123456789";
   char * ndst = strncpy(dst,src,sizeof(dst));
   printf("%s\n",ndst);
}
int main(void)
{
  teststrncpy();
  return 0;
}

以上的bug可以看出来嘛,拷贝了8个字符到dst中,因为src长度是大于8的,所以拷贝过去的没有null结束符,如果dst没有初始化为0的话,会导致字符串使用异常了,我实际测试中倒是发现会在dst的下一个地址设置为null,几次测试都是,不知道是不是巧合。文章源自懂站帝-http://www.sfdkj.com/13321.html

我们来改下,改成如下:文章源自懂站帝-http://www.sfdkj.com/13321.html

void teststrncpy(void)
{
   char dst[18];
   memset(dst,0x1,18);
   char * src = "123456789";
   char * ndst = strncpy(dst,src,sizeof(dst));
   printf("%s\n",ndst);
}

按照strncpy的逻辑,是将大于src长度后面的空间设置为NULL的,调试看看:文章源自懂站帝-http://www.sfdkj.com/13321.html

(gdb) p  *dst@10
$8 = "\001\001\001\001\001\001\001\001\001\001"
(gdb) n
9          char * ndst = strncpy(dst,src,sizeof(dst));
(gdb) p sizeof(dst)
$9 = 18
(gdb) n
10         printf("%s\n",ndst);
(gdb) p *dst@10
$10 = "123456789"
(gdb) p /x dst
$11 = {0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 
  0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}

果然把我们原来设置的,给设置为NULL了。 通常的安全用法:文章源自懂站帝-http://www.sfdkj.com/13321.html

           if (buflen > 0) {
               strncpy(buf, str, buflen - 1);
               buf[buflen - 1]= '\0';
           }

这样写的话,如果buf不够长,仍然可以正常工作,但是str会被截断长度为buflen-1.文章源自懂站帝-http://www.sfdkj.com/13321.html

2.3 fwrite返回值

函数原型:文章源自懂站帝-http://www.sfdkj.com/13321.html

 #include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb,
                     FILE *stream);

fwrite含义如下:文章源自懂站帝-http://www.sfdkj.com/13321.html

       The function fwrite() writes nmemb items of data, each size bytes long, to the stream pointed to by stream, obtaining them from the location given by ptr.

简单地说是向stream写入数据项,这些数据项单个大小为size,数量为nmemb,数据项的首地址为ptr。 例子:文章源自懂站帝-http://www.sfdkj.com/13321.html

void test_fwrite(void)
{
  FILE * fp = fopen("abc.bin","w+");
  if (fp == NULL)  return;
  int  arrays[5] ={1,2,3,4,5};
  int len = fwrite(&arrays,sizeof(int),5,fp);
  fclose(fp);
  printf("test fwrite:%d",len);
}

猜下输出的len是多少,结果如下:文章源自懂站帝-http://www.sfdkj.com/13321.html

test fwrite:5

返回值不是写入的字节数,而是写入的数据项的个数。文章源自懂站帝-http://www.sfdkj.com/13321.html

2.4 malloc和realloc、free

原型说明:文章源自懂站帝-http://www.sfdkj.com/13321.html

       #include <stdlib.h>

       void *malloc(size_t size);
       void free(void *ptr);
       void *calloc(size_t nmemb, size_t size);
       void *realloc(void *ptr, size_t size);
       void *reallocarray(void *ptr, size_t nmemb, size_t size);

这两个是内存分配和释放的函数比较熟悉吧,那么来看看下面代码:文章源自懂站帝-http://www.sfdkj.com/13321.html

 #include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
   char * p = malloc(0);
   if (p == NULL) printf("Malloc return null\n");
   else printf("Malloc return not null\n");
   *p = 0;
}

请问下面的代码会core嘛,答案是不会,看看malloc的说明:文章源自懂站帝-http://www.sfdkj.com/13321.html

     The  malloc()  function  allocates size bytes and returns a pointer to the allo‐
       cated memory.  The memory is not initialized.  If size is 0, then  malloc()  re‐
       turns  either  NULL,  or  a  unique pointer value that can later be successfully
       passed to free().

看这个解释,如果size为0,则malloc要么返回NULL,要么返回一个后面可以被成功释放的非NULL指针,这里面系统还是会给p分配一个一个字节的空间的,所以不会有问题。 不过我们的代码忘记free了,我们free两次会怎么样?文章源自懂站帝-http://www.sfdkj.com/13321.html

int main(void)
{
   char * p = malloc(0);
   if (p == NULL) printf("Malloc return null\n");
   else printf("Malloc return not null\n");
   *p = 0;
   free(p);
   free(p);
}

结果发生异常,如下:文章源自懂站帝-http://www.sfdkj.com/13321.html

miao@ubuntu-lab:~/c-test$ ./a.out
Malloc return not null
free(): double free detected in tcache 2
Aborted (core dumped)

那么如果内存free后,设置为null那,看下:文章源自懂站帝-http://www.sfdkj.com/13321.html

free(p); p= NULL; free(p);

测试了下正常的,来看下解释:文章源自懂站帝-http://www.sfdkj.com/13321.html

The free() function frees the memory space pointed to by ptr,  which  must  have
been  returned  by  a previous call to malloc(), calloc(), or realloc().  Other‐
wise, or if free(ptr) has already been called before, undefined behavior occurs.
If ptr is NULL, no operation is performed.

如果释放了一个已经释放的指针结果是未定义的,如果释放NULL,则什么都不会发生。那,如果释放多次NULL是没有问题,不会core的,也不会引起异常。所以释放完毕内存后,设置指针为NULL是个好习惯。文章源自懂站帝-http://www.sfdkj.com/13321.html

还要注意到释放的指针必须是malloc等函数返回的改动不行,看看下面代码:文章源自懂站帝-http://www.sfdkj.com/13321.html

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
   char * p =(char*) malloc(4);
   if (p == NULL) printf("Malloc return null\n");
   else printf("Malloc return not null\n");
   *p =4;
   free(p+1);
}

我们把申请到p指针移动了一位释放,现在会怎么样,是core还是正常只是内存泄漏那,结果是core,如下:文章源自懂站帝-http://www.sfdkj.com/13321.html

miao@ubuntu-lab:~/c-test$ ./a.out
Malloc return not null
free(): invalid pointer
Aborted (core dumped)

realloc 也是个奇葩,它实现的功能不单一,看看解释:文章源自懂站帝-http://www.sfdkj.com/13321.html

       The realloc() function changes the size of the memory block pointed to by ptr to
       size  bytes.   The contents will be unchanged in the range from the start of the
       region up to the minimum of the old and new sizes.  If the new  size  is  larger
       than  the  old  size, the added memory will not be initialized.  If ptr is NULL,
       then the call is equivalent to malloc(size), for all values of size; if size  is
       equal  to  zero,  and ptr is not NULL, then the call is equivalent to free(ptr).
       Unless ptr is NULL, it must have been returned by an earlier call  to  malloc(),
       calloc(), or realloc().  If the area pointed to was moved, a free(ptr) is done.

       void *realloc(void *ptr, size_t size);

简单说来:文章源自懂站帝-http://www.sfdkj.com/13321.html

  1. 如果size的大小大于ptr原来分配的,则扩充ptr,原来部分内存内容保持不变,新增加的内存没有被初始化哦。
  2. 如果ptr为NULL,函数等同于malloc(size).
  3. 如果size为0,ptr不为null,则等同于free(ptr),当然这个ptr也必须是malloc等返回的,此时realloc返回为NULL。

看看以下代码:文章源自懂站帝-http://www.sfdkj.com/13321.html

void * ptr = realloc(ptr,size);
if (ptr != NULL) {
   // 业务处理
}else {
  // 错误处理
}

如果realloc失败,则返回NULL,而参数ptr并未释放,被我们清空了,导致内存无法释放了,这个隐藏的挺深的,不容易看出来。文章源自懂站帝-http://www.sfdkj.com/13321.html

正常处理方式:文章源自懂站帝-http://www.sfdkj.com/13321.html

void *ptmp = realloc(ptr,size);
if ( ptmp != NULL) {
    ptr = ptmp;
  // 业务处理
}else {
// 错误处理
}

通过重新新定义个变量临时保存就好了。文章源自懂站帝-http://www.sfdkj.com/13321.html

void * newp = realloc(oldp ,size);
// 此处要判断newp是否为空

这里面容易引起的问题就是没有判断是否为空,从而导致了问题。文章源自懂站帝-http://www.sfdkj.com/13321.html

懂站帝
  • 本文由 发表于 2022年5月22日 16:52:58
  • 版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至395045033@qq.com举报,一经查实,本站将立刻删除。