12个有趣的C语言问答(详解)

本文参照博文《12个有趣的C语言问答》,在原文的基础上增加来对应的知识点的详细介绍。

1 gets()方法

Q:下面的代码有一个被隐藏的问题,你能找到它吗?

 1 #include <stdio.h>
 2 
 3 int main(void)
 4 {
 5     char buff[10];
 6     memset(buff, 0, sizeof(buff));
 7     gets(buff);
 8     printf("%s
", buff);
 9 
10     return 0;
11 }

A:这个不显眼的问题就是使用了gets()方法,其函数原型如下:

char* gets(char *s);

此方法接受一个字符数组参数,但是却没有检查此数组是否有足够的空间来拷贝数据。gets()函数是不安全的,不推荐使用,一般情况下编译器也会给出警告提示:the `gets' function is dangerous and should not be used。gets()不检查预留存储区是否能够容纳实际输入的数据。多出来的字符简单的溢出到相邻的存储区,可能会导致错误。

所以,这里我们一般用fgets()方法更好,函数原型如下:

char* fgets(char *s, int n, FILE *stream); 

一般使用fgets()函数,都是读取文件当中的n-1个字符到s中,其实,此函数还有一个很好的用处就是从标准输入流中读取字符串,而且不用担心输入的字符个数超出了字符数组的大小而导致溢出的问题!要怎样做呢?如下:

char str[10];  
fgets(str, siezof(str), stdin); 

值得注意的是:谨记fgets()只读取n-1个字符所以,fgets()读取到换行符、文件尾或读完n-1个字符便会进行返回。

2 strcpy()方法

Q:密码防护是很基本的功能,看看能够搞定下面这一段代码?

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

说明:该程序通过在运行时携带一个密码参数,然后程序会将用户输入的密码参数值与真实的密码比较,如果两者相等就输出cracked信息,否则输出incorrect提示。

3 main()函数的返回类型

Q:请问下面这段代码能否通过编译?如果能的话,那么这段代码中隐含什么问题吗?

#include <stdio.h>
#include <stdlib.h>
void main(void)
{
    char *ptr = (char *)malloc(10);
    if (NULL == ptr){
        printf("
 Malloc failed 
");
        return;
    }else{
        //Do some processing
        free(ptr);
    }
    return;
}

A:代码能通过编译,但是会留下针对main()函数返回值类型的警告。main()函数的真正返回值类型应该是int而不是void,这是因为int返回类型可以返回程序运行的状态值,尤其是当这段程序作为其他应用的附属程序时这个状态值将更加重要。

mainret.c:3:6: warning: return type of ‘main’ is not ‘int’ [-Wmain]
 void main(void)
      ^

4 内存泄漏

Q:请问,以下代码有内存泄漏吗?

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    char *ptr = (char*)malloc(10);
    if (NULL == ptr){
        printf("
 Malloc failed 
");
        return -1;
    }else{
        //Do some processing
    }

    return 0;
}

A:不会,虽然上面的代码没有对指针ptr进行内存释放,但实际上即使是程序结束也不会造成内存泄漏,因为当程序结束时所有一开始被占据的内存就全部清空了。但是,如果上面分配内存这段代码是在while循环里面那将会造成严重的问题。

5 free()方法

Q:以下代码,当用户输入'freeze'时会崩溃,而如果输入'zebra'则运行正常,为什么?

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <memory.h>
 4 int main(int argc, char *argv[])
 5 {
 6     char *ptr = (char *)malloc(10);
 7 
 8     if (NULL == ptr){
 9         return -1;
10     }
11     if (argc == 1){
12         printf("
 Usage: add a string 
");
13     }else {
14         memset(ptr, 0, 10);
15         strncpy(ptr, argv[1], 9);
16         while (*ptr != 'z'){
17             if (*ptr == ' ') break;
18             else ptr++;
19         }
20         if (*ptr == 'z'){
21             printf("
 String contains 'z' 
");
22             //Do some more processing
23         }
24         free(ptr);
25     }
26 
27     return 0;
28 }

A:问题的根源是因为代码在while循环中改变了 ptr 指针的地址。当输入为'zebra'时,while循环甚至在执行第一遍前就结束了,所以free()释放的内存地址就是一开始malloc()分配的地址。但是当输入'freeze'时, ptr记录的地址在while循环中被更改,因此将会使错误的地址传递到free()方法中引起崩溃。

注意:调用free()方法释放内存时,参数必须要么是NULL,要么是先前从malloc/calloc或者realloc返回的地址,不能将一次动态申请的内存的部分释放。

6 atexit()和_exit()

Q:以下代码中的atexit()方法并没有被调用,直到为什么吗?

#include <stdio.h>
#include <unistd.h>

void func(void)
{
    printf("
 Clean up function called 
");
}

int main(void)
{
    int i = 0;

    atexit(func);
    for (; i < 0xFFFF; i++);
    _exit(0);
}

A:这是因为使用了 _exit() 方法。此方法并没有调用清除数据相关的方法,比如 atexit()等。exit和_exit都是用来正常终止一个进程的,主要区别是_exit会立刻进入内核,而exit先执行一些清除工作(包括执行各种终止处理程序,关闭所有标准I/O等,一旦关闭了IO,例如printf等函数就不会输出任何东西了),然后才进入内核。这两个函数会对父子进程有一定的影响,当用vfork创建子进程时,子进程会先在父进程的地址空间运行(这跟fork不一样),如果子进程调用了exit就会把父进程的IO给关掉。

补充:还有一种在程序退出前执行相应函数的方法,就是调用<stdlib.h>中提供的_onexit()回调函数,用法如下:

 1 #include <iostream>
 2 #include <string>
 3 #include <stdlib.h>
 4 
 5 using namespace std;
 6 
 7 int fun(void){
 8     cout << "Exit Function
";
 9     return 0;
10 }
11 
12 int main()
13 {
14     _onexit(fun);
15     cout << "Finished.
";
16     return 0;
17 }

需要注意的是,回调函数要求返回值必须是int类型,否则会报错!

7 void*与C结构体

Q:能够设计一个方法接受任意类型的参数然后返回整数?同时,是否有办法传递多个这样的参数?

A:一个能接受任意类型参数的方法像下面这个样子:

int func(void *ptr)

如果需要传递多个参数,那么我们可以传递包含这些参数的结构体。

8 *与++运算符

Q:以下代码将输出什么?为什么?

#include <stdio.h>
int main(void)
{
    char *ptr = "Linux";
    printf("
 [%c] 
", *ptr++);
    printf("
 [%c] 
", *ptr);
    return 0;
}

A:程序的输出结果如下:

 [L] 

 [i] 

因为++与 * 的优先级一样,所以 *ptr++ 将会从右向左操作。按照这个逻辑,ptr++ 会先执行然后执行*ptr。所以第一个结果是'L'。也因为 ++ 被执行了,所以下一个printf() 结果是'i'。

9 Making changes in code segment

Q:以下代码运行时一定会崩溃,你能说出原因吗?

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

A:这是因为字符串常量“Linux”是以只读的形式存储的,而通过*ptr='T'语句,此代码尝试更改只读内存存储的字符串内容,此操作当然行不通,所以才会导致崩溃。

10 Process that changes its own name

Q:你能否写一个程序,在它运行时修改它的名称?

A:以下的代码可以:

 1 #include <stdio.h>
 2 #include <memory.h>
 3 
 4 int main(int argc, char *argv[])
 5 {
 6     int i = 0;
 7     char buff[100];
 8 
 9     memset(buff, 0, sizeof(buff));
10     strncpy(buff, argv[0], sizeof(buff));
11 
12     memset(argv[0], 0, strlen(buff));
13     strncpy(argv[0], "NewName", 7);
14     //Simulate a wait. Check the process name at this point
15     for (; i < 0xFFFFFFFF; i++);
16 
17     return 0;
18 }

可以通过下面的方法测试

$ gcc chname.c -o chname
$ ./chname &
[1] 4677
$ ps 4677
  PID TTY      STAT   TIME COMMAND
 4677 pts/11   R      0:08 NewName 

11 局部变量的返回地址

Q:下面的代码有问题吗?如果有,如何修改?

 1 #include <stdio.h>
 2 int* inc(int val)
 3 {
 4     int a = val;
 5     a++;
 6     return &a;
 7 }
 8 
 9 int main(void)
10 {
11     int a = 10;
12     int *val = inc(a);
13     printf("
 Increamented value is equal to [%d] 
", *val);
14 
15     return 0;
16 } 

A:虽然上面的代码有时运行会很好,但是在方法 inc() 中有很严重的隐患,因为它返回了局部变量的地址。当inc()方法执行后,再次使用局部变量的地址就会造成不可估量的结果。解决之道就是传递变量a的地址给main()。PS:我觉得最后一句的说法有问题。

12 处理printf()参数

Q:请问以下代码的输出是什么?

#include<stdio.h>
 
int  main( void )
{
    int  a = 10, b = 20, c = 30; 
    
    printf ("
 %d..%d..%d 
", a+b+c, (b = b*2), (c = c*2));
    return  0;  
}

A:程序的输出如下:

 110..40..60 

这是因为参数都是从右向左处理的,然后打印出来却是从左向右。

原文地址:https://www.cnblogs.com/xiaomanon/p/4321907.html