C面试常考的函数和坑

一 前言

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

二 容易出差的函数

2.1 snprintf返回值

对C程序来说缓冲区溢出攻击发生的代码,多是使用没有带长度的api,比如使用strcpy,sprintf,作为替代,常常可以使用strncpy和snprintf 替代,但是这两个函数也是有坑的。

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

测试方法:

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,这个地方很奇怪吧。

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

常常使用snprintf 做字符串的连接,挺好用的,只是要注意下返回值不是打印多少就返回多少的。

2.2 strncpy

函数原型:

       #include 
       char *strcpy(char *dest, const char *src);
       char *strncpy(char *dest, const char *src, size_t n);

strncpy说明:

     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,则函数不会补全。
可能的实现是这样的:

  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。
举个例子:

#include 
#include 
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,几次测试都是,不知道是不是巧合。

我们来改下,改成如下:

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的,调试看看:

(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了。
通常的安全用法:

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

这样写的话,如果buf不够长,仍然可以正常工作,但是str会被截断长度为buflen-1.

2.3 fwrite返回值

函数原型:

 #include 
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含义如下:

       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。
例子:

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是多少,结果如下:

test fwrite:5

返回值不是写入的字节数,而是写入的数据项的个数。

2.4 malloc和realloc、free

原型说明:

       #include 

       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);

这两个是内存分配和释放的函数比较熟悉吧,那么来看看下面代码:

 #include 
#include 
#include 
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的说明:

     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两次会怎么样?

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);
}

结果发生异常,如下:

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

那么如果内存free后,设置为null那,看下:

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

测试了下正常的,来看下解释:

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是个好习惯。

还要注意到释放的指针必须是malloc等函数返回的改动不行,看看下面代码:

#include 
#include 
#include 
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,如下:

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

realloc 也是个奇葩,它实现的功能不单一,看看解释:

       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);

简单说来:

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

看看以下代码:

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

如果realloc失败,则返回NULL,而参数ptr并未释放,被我们清空了,导致内存无法释放了,这个隐藏的挺深的,不容易看出来。

正常处理方式:

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

通过重新新定义个变量临时保存就好了。

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

这里面容易引起的问题就是没有判断是否为空,从而导致了问题。

你可能感兴趣的:(C面试常考的函数和坑)