【嵌入式入门学习笔记】-- 六、Linux C语言高级编程

嵌入式入门学习系列笔记索引

一、Linux简介

二、deb软件包管理

三、shell中的特殊字符

四、Linux常用命令

五、shell编程

六、Linux C语言高级编程


1.gcc编译器

1.1 GNU工具

编译工具:把一个源程序编译为一个可执行程序

调试工具:能对执行程序进行源码或汇编级调试

软件工程工具:用于协助多人开发或大型软件项目的管理,如make、CVS、Subvision

其他工具:用于把多个目标文件链接成可执行文件的链接器,或者用作格式转换的工具。

1.2 GCC简介

全称为GNU CC ,GNU项目中符合ANSI C标准的编译系统  

编译如C、C++、Object C、Java、Fortran、Pascal、Modula-3和Ada等多种语言

GCC是可以在多种硬体平台上编译出可执行程序的超级编译器,其执行效率与一般的编译器相比平均效率要高20%~30%

一个交叉平台编译器 ,适合在嵌入式领域的开发编译

1.2.1 gcc所支持后缀名

.c                C原始程序

.C/.cc/.cxx       C++原始程序

.h                预处理文件(头文件)

.i                已经过预处理的C原始程序

.ii               已经过预处理的C++原始程序

.s/.S             汇编语言原始程序

.o                目标文件

.a/.so            编译后的库文件

1.2.2 编译器的主要组件

分析器:分析器将源语言程序代码转换为汇编语言。因为要从一种格式转换为另一种格式(C到汇编),所以分析器需要知道目标机器的汇编语言。

汇编器:汇编器将汇编语言代码转换为CPU可以执行字节码。

链接器:链接器将汇编器生成的单独的目标文件组合成可执行的应用程序。链接器需要知道这种目标格式以便工作。

标准C:核心的C函数都有一个主要的C库来提供。如果在应用程序中用到了C库中的函数,这个库就会通过链接器和源代码连接来生成最终的可执行程序。

1.2.3 GCC的基本用法和选项

-c     只编译,不连接成为可执行文件,编译器只是由输入的.c等源代码文件生成.o为后缀的目标文件,通常用于编译不包含主程序的子程序文件。 

-o     output_filename,确定输出文件的名称为output_filename,同时这个名称不能和源文件同名。如果不给出这个选项,gcc就给出预设的可执行文件a.out。

-g     产生符号调试工具(GNU的gdb)所必要的符号资讯,要想对源代码进行调试,我们就必须加入这个选项。
 
-O     对程序进行优化编译、连接,采用这个选项,整个源代码会在编译、连接过程中进行优化处理,这样产生的可执行文件的执行效率可以提高,但是,编译、连接的速度就相应地要慢一些。

-O2    比-O更好的优化编译、连接,当然整个编译、连接过程会更慢。

-I     dirname,将dirname所指出的目录加入到程序头文件目录列表中,是在预编译过程中使用的参数。

-L     dirname,将dirname所指出的目录加入到程序函数档案库文件的目录列表中,是在链接过程中使用的参数。

1.2.4 GCC的错误类型及对策

 第一类∶C语法错误

       错误信息∶文件source.c中第n行有语法错误(syntex errror)。有些情况下,一个很简单的语法错误,gcc会给出一大堆错误,我们最主要的是要保持清醒的头脑,不要被其吓倒,必要的时候再参考一下C语言的基本教材。

 第二类∶头文件错误

          错误信息∶找不到头文件head.h(Can not find include file head.h)。这类错误是源代码文件中的包含头文件有问题,可能的原因有头文件名错误、指定的头文件所在目录名错误等,也可能是错误地使用了双引号和尖括号。

 第三类∶档案库错误

        错误信息∶链接程序找不到所需的函数库(ld: -lm: No such file or directory )。这类错误是与目标文件相连接的函数库有错误,可能的原因是函数库名错误、指定的函数库所在目录名称错误等,检查的方法是使用find命令在可能的目录中寻找相应的函数库名,确定档案库及目录的名称并修改程序中及编译选项中的名称。

 第四类∶未定义符号

        错误信息∶有未定义的符号(Undefined symbol)。这类错误是在链接过程中出现的,可能有两种原因∶一是使用者自己定义的函数或者全局变量所在源代码文件,没有被编译、连接,或者干脆还没有定义;二是未定义的符号是一个标准的库函数,在源程序中使用了该库函数,而连接过程中还没有给定相应的函数库的名称,或者是该档案库的目录名称有问题,这时需要使用档案库维护命令ar检查我们需要的库函数到底位于哪一个函数库中,确定之后,修改gcc连接选项中的-l和-L项。

1.2.5 GCC编译过程

分为四个步骤: (四个步骤分别实现了哪些功能?)

①预处理(Pre-Processing),将头文件(.h)进行展开

②编译(Compiling)

③汇编(Assembling)

④链接(Linking)

【嵌入式入门学习笔记】-- 六、Linux C语言高级编程_第1张图片

【嵌入式入门学习笔记】-- 六、Linux C语言高级编程_第2张图片

1.2.6 生成预处理代码

$ gcc –E test.c -o test.i 
   用wc命令,查看这两个阶段代码大小:
     $ wc test.c test.i
     9     16     127   test.c
     842 1934   16498 test.i
     851 1950   16625 总用量
  test.i比test.c增加了很多内容,这些内容主要是放在系统提供的include文件中的,.i文件将.c文件中的头文件进行了展开。

1.2.7 生成汇编代码

检查语法错误,并生成汇编文件
$ gcc –S test.i –o test.s

1.2.8 生成目标代码

方法一,用gcc直接从C源代码中生成目标代码:
$ gcc –c test.s –o test.o

方法二,用汇编器从汇编代码生成目标代码:
$ as test.s –o test.o 

1.2.9 生成可执行程序

将目标程序链接库资源,生成可执行程序
$ gcc  test.o –o test
./test

2.gdb调试工具

2.1 gdb调试流程

首先使用gcc对test.c进行编译,注意一定要加上选项‘-g’ 
# gcc -g test.c -o test 
# gdb test   //开始调试

2.2 gdb调试命令

查看文件         (gdb) l
设置断点         (gdb) b 6      //在第6行设置断点
查看断点情况     (gdb) info b   //b是断点b
运行代码         (gdb) r
查看变量值       (gdb) p n      //查看变量n的值
单步运行         (gdb) n  //按n就是单步,在单步到函数调用行时,按s进入函数:(gdb) s 进入函数
恢复程序运行     (gdb) c    //后面若没有断点,则程序会一直跑下去
帮助            (gdb) help [command] 
退出gdb         (gdb) q

2.3 gdb的使用切记点

在gcc编译选项中一定要加入‘-g’。

只有在代码处于“运行”或“暂停”状态时才能查看变量值。

设置断点后程序在指定行之前停止。

3.条件编译和结构体

3.1 条件编译

编译器根据条件的真假决定是否编译相关的代码,

常见的条件编译有两种方法:

3.1.1 根据宏是否定义

其语法如下:

  #ifdef 

     ……

  #else

     ……

  #endif

#include 

int main(int argc, const char *argv[])
{
#ifdef _DEBUG_
    printf("hello world\n");
#else
    printf("welcome to Beijing\n");
#endif
    return 0;

}

//打印  welcome to Beijing
#include 

#define _DEBUG_

int main(int argc, const char *argv[])
{
#ifndef _DEBUG_
    printf("hello world\n");
#else
    printf("welcome to Beijing\n");
#endif
    return 0;

}

//打印  welcome to Beijing

3.1.2 根据宏的值

其语法如下:

  #if 

     ……

  #else

     ……

  #endif

#include 

#define _DEBUG_ 1

int main(int argc, const char *argv[])
{
#if _DEBUG_
    printf("hello world\n");
#else
    printf("welcome to Beijing\n");
#endif
    return 0;

}

//打印  hello world

3.2 结构体

3.2.1 结构体的赋值方式

#include 

//student不能省略
struct student      
{
    int num;
    char name[10];
    float grade;

};  //这里的分号要记得加


int main(int argc, const char *argv[])
{
    struct student stu1;
    stu1.num = 1;
  //  stu1.name = "zhangsan"; 这是错误的

#if 0
    stu1.name[0] = 'z';
    stu1.name[1] = 'h';    // 效率太低
    stu1.name[2] = 'a';
#endif
    strcpy(stu1.name,"zhangsan");   //用字符串拷贝提高效率,strcpy函数要用到string.h头文件
    
    stu1.grade = 85.5;

    printf("%d %s %f\n",stu1.num,stu1.name,stu1.grade);
    
    return 0;
}

3.2.2 定义结构体变量时可以直接赋值

#include 

//student不能省略
struct student                
{
    int num;
    char name[10];
    float grade;

};  //这里的分号要记得加


int main(int argc, const char *argv[])
{
    struct student stu1,stu2 = {2,"lisi",90};   //定义的时候就可以赋值

    printf("%d %s %f\n",stu2.num,stu2.name,stu2.grade);
    
    return 0;
}

3.2.3 也可以在定义结构体时创建变量并赋值

#include 

//student可以省略
struct student
{
    int num;
    char name[10];
    float grade;

}stu3 = {3,"zhouwu",95};   //分号要加 


int main(int argc, const char *argv[])
{

    printf("%d %s %f\n",stu3.num,stu3.name,stu3.grade);
    
    return 0;
}

3.2.4 在定义结构体时创建多个变量并赋值,要用逗号隔开

#include 

//student可以省略
struct student   
{
    int num;
    char name[10];
    float grade;

}stu3 = {3,"zhouwu",95},stu4 = {4,"wangliu",70};   //分号要加 


int main(int argc, const char *argv[])
{

    printf("%d %s %f\n",stu4.num,stu4.name,stu4.grade);
    
    return 0;
}

3.2.5 测试结构体大小

#include 

//student可以省略
struct student   
{
    int num;              //  ****
    char name[10];        //  ****  **** **
    float grade;          //  ****

}stu3 = {3,"zhouwu",95},stu4 = {4,"wangliu",70};   //分号要加 


int main(int argc, const char *argv[])
{

  //  printf("%d %s %f\n",stu4.num,stu4.name,stu4.grade);
    
    printf("%ld %ld\n",sizeof(struct student),sizeof(stu1));   
    
    return 0;
}

//打印结果均为20 
//口算结构体大小: int 4字节 + char 10字节 + float 4字节 = 18字节  这不对

3.2.6 字节对齐

#include 

//student可以省略
struct student   
{
    int num;              //  ****
    char name[10];        //  ****  **** **** 这里因为字节对齐补充了两个字节
    float grade;          //  ****

}stu3 = {3,"zhouwu",95},stu4 = {4,"wangliu",70};   //分号要加 


int main(int argc, const char *argv[])
{

  //  printf("%d %s %f\n",stu4.num,stu4.name,stu4.grade);
    
    printf("%ld %ld\n",sizeof(struct student),sizeof(stu1));   
    
    return 0;
}

//打印结果均为20 
//口算结构体大小: int 4字节 + char 12字节 + float 4字节 = 20字节

4.结构体数组和结构体指针

4.1 结构体数组的定义与初始化

#include 
#include 

struct student      
{
    int num;
    char name[10];
    float grade;

};  //这里的分号要记得加


int main(int argc, const char *argv[])
{
    struct student s[4];
    s[0].num = 1;

#if 0
    stu1.name[0] = 'z';
    stu1.name[1] = 'h';    // 效率太低
    stu1.name[2] = 'a';
#endif
    strcpy(s[0].name,"zhangsan");   //用字符串拷贝提高效率,strcpy函数要用到string.h头文件
    
    s[0].grade = 85.5;

    printf("%d %s %f\n",s[0].num,s[0].name,s[0].grade);   // 1  zhangsan  85.5

    
    return 0;
}

初始化

#include 
#include 

struct student      
{
    int num;
    char name[10];
    float grade;

};  //这里的分号要记得加


int main(int argc, const char *argv[])
{
    struct student s[4] = {{1,"zhangsan",85.5}};  // 对结构体数组中的第一个成员进行了初始化
或是
    struct student s[4] = {{1,"zhangsan",85.5},{2,"lisi",80}};  // 用逗号隔开

    return 0;
}
#include 
#include 

struct student      
{
    int num;
    char name[10];
    float grade;

}s1[2] = {{3,"zhouwu",90},{4,"wangliu",98}};  


int main(int argc, const char *argv[])
{
    return 0;
}

4.2 结构体指针

#include 
#include 

struct student      
{
    int num;
    char name[10];
    float grade;

};  //这里的分号要记得加


int main(int argc, const char *argv[])
{
    struct student *p;
    struct student stu1 = {1,"lisi",90};

    p = &stu1;

    printf("%d %s %f\n",(*p).num,(*p).name,(*p).grade);   // 1  lisi  90
    printf("%d %s %f\n",p->num,p->name,p->.grade);    // 1  lisi  90

    return 0;
}
#include 
#include 

struct student      
{
    int num;
    char name[10];
    float grade;

};  //这里的分号要记得加


int main(int argc, const char *argv[])
{
    struct student *p;
    struct student stu1 = {1,"lisi",90};
    
    p->num = 1; /* 这样写会报错,因为指针p只是定义了,并没有给它赋值,p是野指针,并不确切的知道它指向了哪,对这样的指针进行读写,会报错 */
    p->name = "zhangsan";  // 这样写会报错
    p->grade = 90;    // 这样写会报错



    printf("%d %s %f\n",(*p).num,(*p).name,(*p).grade);   // 1  lisi  90
    printf("%d %s %f\n",p->num,p->name,p->.grade);    // 1  lisi  90

    return 0;
}

5.共用体及typedef

 5.1 共用体

        在C语言中,不同数据类型的数据可以使用共同的存储区域,这种数据构造类型称为共用体,简称共用,又称联合体。共用体在定义、说明和使用形式上与结构体相似。两者本质上的不同仅在于使用内存的方式上。

#include 

union gy
{
    int a;
    char b;
};

int main(int argc,const char *argv[])
{
    union gy n;
    n.a = 0x12345678;
    printf("%ld\n",sizeof(union gy));   //  4

    printf("%#x\n",n.a);   // 0x12345678
    printf("%#x\n",n.b);   // 0x78

    n.b = 'a';
    printf("%#x\n",n.a);    //  0x12345661  a的数值也改变了
    printf("%#x\n",n.b);    //  0x61
    
    return 0;


}

嵌套情况

#include 

union gy
{
    int a;
    char b;
    struct
    {
        int c;
        float d;
    }f;
};

int main(int argc,const char *argv[])
{
    union gy n;
   // 用n.f.c去访问共用体中的结构体中的成员

    return 0;
}

5.2 typedef

 在C语言中,允许使用关键字typedef定义新的数据类型

其语法如下:

        typedef   <已有数据类型>   <新数据类型>;

如:

        typedef   int  INTEGER;

        这里新定义了数据类型INTEGER, 其等价于int

#include 
#include 

struct student      
{
    int num;
    char name[10];
    float grade;

};  //这里的分号要记得加


int main(int argc, const char *argv[])
{
    // typedef int INTENG;
    typedef struct student STU;  // STU就等价于struct student  
    STU stu1;
    stu1.num = 1;


#if 0
    stu1.name[0] = 'z';
    stu1.name[1] = 'h';    // 效率太低
    stu1.name[2] = 'a';
#endif
    strcpy(s[0].name,"zhangsan");   //用字符串拷贝提高效率,strcpy函数要用到string.h头文件
    
    stu1.grade = 85.5;

    printf("%d %s %f\n",stu1.num,stu1.name,stu1.grade);   // 1  zhangsan  85.5

    
    return 0;
}
#include 
#include 

typedef struct student      
{
    int num;
    char name[10];
    float grade;

}STU1;  //这里的分号要记得加


int main(int argc, const char *argv[])
{
    // typedef int INTENG;
    // typedef struct student STU;  // STU就等价于struct student  
    STU1 stu1;
    stu1.num = 1;


#if 0
    stu1.name[0] = 'z';
    stu1.name[1] = 'h';    // 效率太低
    stu1.name[2] = 'a';
#endif
    strcpy(s[0].name,"zhangsan");   //用字符串拷贝提高效率,strcpy函数要用到string.h头文件
    
    stu1.grade = 85.5;

    printf("%d %s %f\n",stu1.num,stu1.name,stu1.grade);   // 1  zhangsan  85.5

    
    return 0;
}
#include 
#include 

typedef struct student      
{
    int num;
    char name[10];
    float grade;

}STU1,*STU_P;  //这里的分号要记得加


int main(int argc, const char *argv[])
{
    STU1 *p;
    STU_P q;  // q等价于p,typedef int* INT_P 也是指针类型

    // typedef struct student STU;  // STU就等价于struct student  
    STU1 stu1;
    q = &stu1;
    p = &stu1;
    stu1.num = 1;

#if 0
    stu1.name[0] = 'z';
    stu1.name[1] = 'h';    // 效率太低
    stu1.name[2] = 'a';
#endif
    strcpy(s[0].name,"zhangsan");   //用字符串拷贝提高效率,strcpy函数要用到string.h头文件
    stu1.grade = 85.5;

    printf("%d %s %f\n",stu1.num,stu1.name,stu1.grade);   // 1  zhangsan  85.5
    printf("%d %s %f\n",q->num,q->name,q->grade);   // 1  zhangsan  85.5
    printf("%d %s %f\n",p->num,p->name,p->grade);   // 1  zhangsan  85.5

    
    return 0;
}

6.内存管理

C/C++定义了4个内存区间:

        代码区/全局变量与静态变量区/局部变量区,即栈区/动态存储区,即堆区

6.1 存储分配方式

6.1.1 静态存储分配

        通常定义变量,编译器在编译时都可以根据该变量的类型知道所需内存空间的大小,从而系统在适当的时候为他们分配确定的存储空间。

        在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

6.1.2 动态内存分配

        有些操作对象只有在程序运行时才能确定,这样编译器在编译时就无法为他们预定存储空间,只能在程序运行时,系统根据运行时的要求进行内存分配,这种方法称为动态内存分配。

        所有动态存储分配都在堆区中进行。

        从堆上分配,亦称动态内存分配。程序在运行的时候用malloc申请任意多少的内存,程序员自己负责在何时用free释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

堆内存的分配与释放

        当程序运行到需要一个动态分配的变量或对象时,必须向系统申请取得堆中的一块所需大小的存贮空间,用于存贮该变量或对象。当不再使用该变量或对象时,也就是它的生命结束时,要显式释放它所占用的存贮空间,这样系统就能对该堆空间进行再次分配,做到重复使用有限的资源。

        堆区是不会自动在分配时做初始化的(包括清零),所以必须用初始化式(initializer)来显式初始化。

#include 
#include 

int main(int argc, const char *argv)
{
    char *p = NULL;
    p = (char *)malloc(10);
    strcpy(p,"hello");
    printf("%s\n,p");   // hello
    free(p);    //  和malloc成对出现
    return 0;
}
#include 
#include 

int main(int argc, const char *argv)
{
    char *p = NULL;
    p = (int *)malloc(sizeof(int)); // 不知道应该分配多少空间时,就用sizeof

    free(p);    //  和malloc成对出现
    return 0;
}

6.2 malloc与free

        void * malloc(size_t num)
        void   free(void *p)
        ①malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。
        ②malloc申请到的是一块连续的内存,有时可能会比所申请的空间大。其有时会申请不到内存,返回NULL。
        ③malloc返回值的类型是void *,所以在调用malloc时要显式地进行类型转换,将void * 转换成所需要的指针类型。
        ④如果free的参数是NULL的话,没有任何效果。
        ⑤释放一块内存中的一部分是不被允许的。

注意事项

        删除一个指针p(free(p);):实际意思是删除了p所指的目标(变量或对象等),释放了它所占的堆空间,而不是删除p本身(指针p还在,只不过p指向的东西没了),释放堆空间后,p成了空悬指针。

        动态分配失败。返回一个空指针(NULL),表示发生了异常,堆资源不足,分配失败。

        malloc与free是配对使用的: free只能释放堆空间。如果malloc返回的指针值丢失,则所分配的堆空间无法回收,称内存泄漏,同一空间重复释放也是危险的,因为该空间可能已另分配,所以必须妥善保存malloc返回的指针,以保证不发生内存泄漏,也必须保证不会重复释放堆内存空间。

        动态分配的变量或对象的生命期:无名对象的生命期并不依赖于建立它的作用域,比如在函数中建立的动态对象在函数返回后仍可使用。我们也称堆空间为自由空间(free store)就是这个原因。但必须记住释放该对象所占堆空间,并只能释放一次,在函数内建立,而在函数外释放是一件很容易失控的事,往往会出错。

        野指针:不是NULL指针,是指向“垃圾”内存的指针。“野指针”是很危险的。

“野指针”的成因主要有两种:

        ①指针变量没有被初始化。

        ②指针p被free之后,没有置为NULL,让人误以为p是个合法的指针。指针操作超越了变量的作用范围。这种情况让人防不胜防。

#include 
#include 

int main(int argc, const char *argv)
{
    char *p = NULL;
    p = (char *)malloc(10);

    if(NULL == p)
    {
        printf("malloc failed\n");
        return -1;
    }

    strcpy(p,"hello");
    printf("%s\n,p");   // hello
    free(p);    //此时p是野指针,free和malloc成对出现

    p = NULL;   // free后要置为NULL

    return 0;
}

你可能感兴趣的:(嵌入式学习,c语言,嵌入式)