c基础自问自答

最近由于工作的需要也由于希望能够更加的理解PHP的底层实现机制,所以开始了边看PHP源码边拾起c的旅程,前前后后两个星期,重新翻看了《C Primer Plus》和最近反响不错的Head First出品的《嗨翻C语言》。由于大学时期也弄了不少C程序,但看书过程中还是收获到了很多的知识,所以在这里加以整理。这篇东西整理的并不系统,仅仅属于个人杂记,所以谨慎观看。

1.各种不同的变量是存储在内存的哪个部分呢?
简介:首先,让我们看看计算机的内存是怎样规划的,如图,计算机按照从上到下的顺序将内存划分为栈、堆、全局量、常量段和代码段这几个部分。
内存管理
答:
①在函数内声明的变量,俗话说的局部变量通常保存在栈中
②在函数外声明的变量,俗话说的全局变量通常保存在全局量中
③通过malloc()动态创建的变量通常保存在堆中
④char *name = “Wenzhi Xu”;”Wenzhi Xu”会存放在常量区,而name这个变量会指向”Wenzhi Xu”的首地址。注意,既然是常量区,就不能去更改它的值
④char name[] = “Wenzhi Xu”;这种以字符数组的方式创建字符串的方式比较特殊,”Wenzhi Xu”会首先存放在常量区,然而还没结束,毕竟name是一个数组,所以之后会在栈上去创建这个数组

2.使用scanf()的时候要小心什么?
简介:看下面这个例子,就是定义了一个book,然后输入一个书名而已

#include<stdio.h>
int main(int argc, char *argv[]){
    char book[20];
    printf("输入你最喜欢的一本书:");
    scanf("%s", book);
    printf("\n你最喜欢的一本书是:%s\n", book);
    return 1;
}

乍眼一看是没什么问题的,但是请看,我的这个执行结果,爆出了segmentation fault错误,因为用户的输入始终是不确定的,然而我们的代码一定要考虑不安全的因素。
F2CCED1F-7DE9-466C-9369-C9DCF45A4F5E
答:
①在使用scanf的时候,可以加一个参数,比如上面的代码中在scanf改成scanf(“%19s”, book);即可
②不使用scanf就行啦,换成fgets(),将scanf()改成 fgets(book, sizeof(book), stdin);

3.一个int值为100000转换成short会发生什么?
简介:这涉及到int和short所占的字节数,通常情况下,int占4字节(32位),而short占2字节(16字节)。
答:十进制 100000 -> 二进制 0001 1000 0110 1010 0000,当将其强转为short时,会从右向左截取16位,也就是 1000 0110 1010 0000,所以再将1000 0110 1010 0000转换为十进制,发现最高位为1,则会以负数处理,所以结果是-31072

4.gcc的-c 和 -o参数有什么用?
答:
①-c 只编译不链接
②-o 链接代码

5.Makefile是可移植的吗?
答:如果在windows上写了个Makefile,无法保证能够在Linux和Mac上使用,因为Makefile会调用操作系统的命令。

6.如果将结构变量赋值给另一个结构变量时会发生什么?
答:计算机会创建一个全新的结构副本。

7.结构体的各个字段在内存中是紧挨着排放的吗?
答:不是,而且各个字段之间可能会有缝隙,因为对于字长是32位的机器来说,就不希望某个变量跨域32位的边界保存。

8.字长是什么?
答:我们通常所说的32位操作系统或者64位操作系统就是这么来的,字长是CPU能并行处理的二进制位数。

9.一种在结构体中节省空间的办法
简介:当我想在结构体中加一个标识字段的时候,比如男和女的时候,通过下面的方式可以以位来存储一个变量

struct Coder{
    unsigned int sex:1;
};

10.在free二叉树存储空间的时候要如何free?
简介:一个二叉树那么多节点,在free的时候一定要把每一个节点和叶节点都需要free掉,因为二叉树都是通过malloc动态创建的空间,如果不free掉的话那些空间别人就永不了啦,具体可以通过递归的方式全部free掉

void release(BinaryTree *t){
    if(t){
        if(t->left){
            release(t->left);
        }
        if(t->right){
            release(t->right);
        }
        free(t);
    }
}


11.创建一个函数的时候发生了什么?

void sayHello(){
printf("Hello World!\n");
}

当编译器看到这里定义了一个函数时,看看上面第一个图,所以这段代码被存放在了代码区,而又发现,这里定义了一个sayHello()函数,创建sayHello()函数的同时,也在常量区中创建了一个同名函数的指针,所以当在main()中调用这个sayHello()时,实际上是使用函数指针。

12.头文件的使用方法都有哪些?
答:
①在Unix或者Linux下,将头文件放在/usr/local/include下就可以直接include头文件了
②使用绝对路径的方式,在写include时,直接写绝对路径
③gcc -I,来应用头文件,例如 gcc -I/my_header test.c -o test

13.对多个.o文件进行存档或者说打包成静态库
为什么要存档或者打包?
我们经常会有一些公共的类库和方法是共享的,也就是说,这些公共模块有可能在项目1中用也在项目2中用,所以打包将是一个很方便的做法,同时也缩短了这些公共类库的编译时间。

简介:运行一个C语言程序之前,我们做的是先把test.c代码写好,然后gcc test.c -o test啪啪啪就生成了一个可执行文件,然而这个gcc的过程其实可以分为很多部,包括:编译,链接等过程,那么这个.o文件是在哪个过程中出现的呢?也就是在编译之后还没有链接时出现的中间文件。

存档方法:
比如我现在有两个已经编译的但没有链接的.o文件 c1.o 和 c2.o

ar -rcs libc12.a c1.o c2.o

这时就会在同级目录下出现一个libc12.a文件,注意:.a文件一定要以lib开头

同时我们也可以通过nm命令来查看.a文件是由哪些.o文件生成的,以及内部的详细信息

nm libc12.a

8FB6979C-1902-4234-A5C0-FF9C46B3AF2E

那么,既然.a文件已经生成了,怎么去使用呢?
比如说,我现在有个小项目由c1.c,c2.c和c3.c组成,而我已经降c1.c和c2.c存档为libc12.a了,那么就不需要去再浪费的编译他们了,让我们直接使用libc12.a吧,只需要这样编译即可

gcc c3.c -la12.a -o c123


注意
1.c3.c一定要放在最前面
2.-la12.a好像有点蒙的感觉,实际上可以拆成两部分看,-l和a12.a,-l参数意味需要引一个.a文件,而a12.a实际上是liba12.a,这也就是为什么.a文件一定要以lib开头的原因
3.最后生成了可执行文件c123

14.系统调用exec系
简介:当我们的项目中有一项功能是需要查看我们的系统(Linux)根目录下有哪些文件时(看起来挺危险的),可以直接在终端中敲上 ls / ,就能得到结果了,但是如果我们的c项目怎么去执行ls /呢?当然就需要使用exec系函数,之所以说exec系的意思是exec家族有太多兄弟姐妹,比如有execl(),execle(),execlp(),execv(),execvp()和execve()。
已经特么蒙了,咋回事?这一堆一堆的长得还都差不多,这是要闹哪样,请看下图
新文档 2_1
来,先走一个execl()试试,execl()第一个参数为需要执行的命令,而后面都是执行这个命令所需要的参数,而最后一个参数绝对是NULL。

#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, const char * argv[]) {
        if(execl("/bin/ls", "/bin/ls", "/", NULL) == -1){
                fprintf(stderr, "can't run execl 0:%s\n", strerror(errno));
        }
        printf("HERE!!");
        return 0;
}

编译并执行这个c程序,会发现得到了系统根目录下的文件列表,但是最后的HERE!!为什么没出来?
答:因为当程序执行到execl()时,会抛开程序后面的代码,也就是说程序在执行完execl()就结束。

15.fork进程时岂不是很慢?
为了让fork()进程更快,操作系统不会真的复制父进程的数据,而是让子进程享用父进程的数据。然而操作系统也不会让子进程去修改共享的数据,一旦发现则会单独为子进程复制一份数据,这种技术叫“写时复制”。

16.进程莫名地被干掉了怎么办?
简介:大家无论在使用Mac,windows或者Android的时候会遇到一种情况,软件莫名的崩溃了,然后弹出一个提示框提示你,“是否反馈崩溃信息,让我们提供更好的体验?”之类之类的吧,那么既然软件崩溃了,又是怎么捕获到软件崩溃了呢?这时候就需要使用sigaction来捕捉信号。
栗子:

#include<stdio.h>
#include<stdlib.h>
//使用信号需要引入signal.h
#include<signal.h>
void die(){
    puts("\n是否提交反馈信息,让我们提供更好的体验?");
    exit(1);
}
int catch_signal(int sig, void (*handler)(int)){
    struct sigaction action;
    action.sa_handler = handler;
    sigemptyset(&action.sa_mask);
    action.sa_flags = 0;
    return sigaction(sig, &action, NULL);
}
int main(){
    //信号类型为SIGINT,当然还有其他类型的信号
    if(catch_signal(SIGINT, die) == -1){
        fprintf(stderr, "can't map the handler");
        exit(2);
    }
    char name[20];
    printf("输入你的名字:");
    fgets(name, 20, stdin);
    printf("Hello %s\n", name);
    return 0;
}

让我们来看看究竟它是怎么捕获程序莫名终止的,执行下看看,当程序在运行当中的时候,我按下CTRL+C来终止程序,然后sigaction捕获到了终止信号,调用了die()函数。而后我们就可以在die()这里做很多文章。
2FC71964-4023-456B-B88A-0B8976A73BC2

17.register寄存器变量
简介:通过register定义的变量,会建议编译器将该变量直接存储在寄存器中(注意是建议,编译器有可能并不会那么做),由于寄存器在CPU内部,且计算一般在寄存器中完成,这样CPU就无需到内存中将变量拷贝进寄存器中计算,这样就省下了从内存到寄存器的时间,会大大提高运算能力。然而何种变量适合使用register定义呢?通常是那种经常需要使用的变量。还有一点是寄存器变量只能定义简单的数值类型,不能定义指针和数组等变量。

register int num;

18.函数指针
简介: 是的,指针也是可以指向函数的,在编译期间,如果发现函数指针的赋值,会将函数块的首地址赋给指针。函数指针有点类似匿名函数的感脚,活学活用绝对是有好处的,下面放了个小例子~
注意:函数指针的定义一定要将 * 和 函数名 用括号括起来,比如 “ 返回类型 (*函数名)(参数列表)

#include <stdio.h>
void func1()
{
    printf("hello I'm here!");
}
void test()
{
    void (*p)(void);
    p = func1;
    p();
}
int main()
{
    test();
    return 0;
}

19.获取可变数目参数的函数
简介: 有的时候函数的参数真的是不固定的,所以C函数支持可变数目参数,下面是一个简单的例子

#include <stdio.h>
#include <stdarg.h>
int total(int num, ...)
{
    int i = 0, total = 0;
    //声明va_list变量
    va_list arg_ptr;

    //初始化参数指针
    va_start(arg_ptr, num);

    //从可变参数列表中取回参数
    for (;i < num;i++) {
        total += va_arg(arg_ptr,int);
    }
    //执行清理
    va_end(arg_ptr);
    return total;
}
int main()
{
    int a = 1, b = 2, c = 3;
    printf("%d", total(3, a ,b, c));
    return 0;
}

20.处理时间

#include <stdio.h>
#include <time.h>
int main()
{
    time_t now;
    time_t *p = &now;
    struct tm *tM;   //定义tm结构体变量
    char buffer[100];//存储时间格式字符串
    now = time(p);   //返回时间戳

    printf("时间戳为:%ld", now);
    tM = localtime(&now);//将time_t 结构转成 tm结构
    strftime(buffer, 100, "Today is %A", tM);//使用strftime设置时间格式
    puts(buffer);//打印时间格式
    return 0;
}

21.使用ASSERT库调试程序
简介:assert()可以用来调试程序,比如下面的这段代码在编译执行后会提示错误信息,但这是建立在编译器开启调试模式的情况下出现的。

#include <stdio.h>
#include <assert.h>
int main()
{
    int x = 0;
    assert(x != 0);
    return 0;
}

22.malloc()、calloc()和realloc()分配内存
简介:这3个函数都可以用于实现动态分配内存的功能,当然还是有区别的。malloc()和calloc()的功能是一致的,唯一的不同点在于两个函数的参数不同。
使用malloc()和calloc()动态分配一个int类型的空间例子

#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
int main()
{
    int int_size = sizeof(int32_t);
    int32_t *malloc_num, *calloc_num;
    malloc_num = (int32_t *) malloc(int_size);//malloc()接收需要申请的总字节数
    calloc_num = (int32_t *) calloc(1, int_size);//calloc()第一个参数为特定变量类型的个数,第二个参数为特定变量类型所占的字节数
    //也就是说malloc参数int_size 是 calloc()参数中两个变量的乘积,即 1 * int_size。
    *malloc_num = 1;
    *calloc_num = 2;

    printf("%d\n", *malloc_num);
    printf("%d\n", *calloc_num);
    return 0;
}

realloc()可以说是malloc()和calloc()基础上的函数

#include <stdio.h>
#include <stdlib.h>
#define STU_NUM 10
int main()
{
    int i;
    int *student;
    //动态申请10个大小的数组
    student = (int *) malloc(STU_NUM * sizeof(int));
    if(!student){
        printf("malloc() failed!\n");
    }
    for (i=0; i < STU_NUM; i++) {
        student[i] = 100;//每个学生都是一百分
    }
    //突然来了一个新生,怎么办? 这就需要realloc()登场,它可以在原有内存空间的基础再次申请内存
    student = realloc(student, (STU_NUM+1) * sizeof(int));
    if(!student){
        printf("realloc() failed!\n");
    }
    printf("end.");
    return 0;
}

由于各种各样的场景,就会导致使用realloc()时发生不同的事情。

  • 如果realloc()第一个参数为NULL,会像malloc()一样,返回第二个参数大小的内存空间
  • 如果当前位置有足够的内存供分配,则会在原位置申请足够的空间
  • 如果当前位置无足够的内存供分配,则会将原位置这块儿的数据做一份拷贝,找到一块儿足够大小的内存分配并将拷贝复制到此处
  • 如果realloc()第二个参数为0,则释放此处内存,并返回NULL

23.memset()、memcpy()和memmove()
简介:memset()主要做内存块的初始化操作,类似于类似于数组的初始化工作;memcpy()用于将源内存数据拷贝到目标内存,但需要考虑内存重叠状况,不过这个函数的功能已经被memmove()覆盖;memmove()则可以移动内存数据。

c基础自问自答
Tags:             

发表评论

电子邮件地址不会被公开。 必填项已用*标注