C基础(一)

  1. 一、C程序结构
    1. 1.C程序主要包括
    2. 2.执行C程序
  2. 二、数据
    1. 1.数据类型
    2. 2.整数类型
    3. 3.浮点类型
    4. 4.void类型
    5. 5.变量
      1. 1.占位符
  3. 6.常量
    1. 1.整数常量
    2. 2.浮点常量
    3. 3.定义常量
  • 7.C存储类
    1. 1.auto 存储类(普通局部栈变量)
    2. 2.register 存储类
    3. 3.static 存储类
    4. 4.extern 存储类
    5. 5.总结
  • 8.运算符
    1. 1.异或
    2. 2.求余
  • 9.函数
    1. 1.函数体
    2. 2.函数参数
    3. 3.内部函数
    4. 4.外部函数
    5. 3.内联函数
    6. 4.main 函数
  • 10.枚举
    1. 1.先定义枚举类型,再定义枚举变量
    2. 2.定义枚举类型的同时定义枚举变量
    3. 3.省略枚举名称,直接定义枚举变量
  • 11.指针
    1. 1.指针的算术运算
    2. 2.复杂类型
    3. 3.指向函数的指针
    4. 4.提示
  • 12.字符串
  • 13.结构体
    1. 1.定义结构
    2. 2.结构体变量的初始化
    3. 3.访问结构成员
    4. 4.结构作为函数参数
    5. 5.指向结构的指针
    6. 6.位域
  • 14.共用体
    1. 1.定义共用体
    2. 2.访问共用体成员
  • 15.typedef
    1. typedef vs #define
  • define可以使用其他类型说明符对宏类型名进行扩展,但对 typedef 所定义的类型名却不能这样做。例如:
    1. 16.文件读写
      1. 1.打开文件
      2. 2.关闭文件
      3. 3.写入文件
      4. 4.读取文件
      5. 5.二进制 I/O 函数
  • 17.预处理器
  • define:定义宏
  • include:包含一个源代码文件
  • undef:取消已定义的宏
  • ifdef:如果宏已经定义,则返回真
  • ifndef:如果宏没有定义,则返回真
  • if:如果给定条件为真,则编译下面代码
  • else:#if 的替代方案
  • elif:如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
  • endif:结束一个 #if……#else 条件编译块
  • error:当遇到标准错误时,输出错误消息
  • pragma:使用标准化方法,向编译器发布特殊的命令到编译器中
    1. 1.预定义宏
    2. 2.预处理器运算符
      1. 1.宏延续运算符(\)
      2. 2.字符串常量化运算符(#)
      3. 3.标记粘贴运算符(##)
      4. 4.defined() 运算符
      5. 5.参数化的宏
  • 18.错误处理
    1. 1.errno、perror() 和 strerror()
  • 19.内存管理
  • 一、C程序结构

    1.C程序主要包括

    • 预处理器指令
    • 函数
    • 变量
    • 语句 & 表达式
    • 注释

    2.执行C程序

    1.键入 gcc xxx.c,回车后编译代码。
    2.没有错误则生成 a.out 可执行文件
    3.键入 ./a.out 执行程序

    二、数据

    1.数据类型

    整体氛围:

    • 1.基本类型:整数类型和浮点类型
    • 2.枚举类型:枚举类
    • 3.void类型:没有可用的值
    • 4.派生类型:指针类型、数组类型、结构类型、共用体和函数类型

    2.整数类型

    类型 存储大小 范围值
    char 1字节 -128到127或0到255
    unsigned char 1字节 0到255
    signed char 1字节 -128到127
    int 2或4字节 -32,768到32,767或-2,147,483,648到2,147,483,647
    unsigned int 2或4 字节 0 到 65,535或0到4,294,967,295
    short 2字节 -32,768到32,767
    unsigned short 2字节 0到65,535
    long 4字节 -2,147,483,648到2,147,483,647
    unsigned long 4字节 0到4,294,967,295

    注意,各种类型的存储大小与系统位数有关,但目前通用的以64位系统为主。
    可以使用 sizeof 运算符:

    #include <stdio.h>
    #include <limits.h>
    int main()
    {
    printf("int 存储大小:%lu \n", sizeof(int));
    return 0;
    }

    3.浮点类型

    类型 存储大小 范围值 精度
    float 4 字节 1.2E-38 到 3.4E+38 6 位小数
    double 8 字节 2.3E-308 到 1.7E+308 15 位小数
    long double 16 字节 3.4E-4932 到 1.1E+4932 19 位小数

    头文件 float.h 定义了宏,在程序中可以使用这些值和其他有关实数二进制表示的细节。下面的实例将输出浮点类型占用的存储空间以及它的范围值:

    #include <stdio.h>
    #include <float.h>
    int main()
    {
    printf("float 存储最大字节数 : %d \n", sizeof(float));
    printf("float 最小值: %E\n", FLT_MIN);
    printf("float 最大值: %E\n", FLT_MAX);
    printf("精度值: %d\n", FLT_DIG);
    return 0;
    }

    4.void类型

    • 1.函数返回为空:void add(int a, int b)
    • 2.函数参数唯恐:int rand(void)
    • 3.指针指向void:返回指向void的指针,可以转换为任何数据类型

    5.变量

    • 1、一种是需要建立存储空间的。例如:int a 在声明的时候就已经建立了存储空间。
    • 2、另一种是不需要建立存储空间的,通过使用extern关键字声明变量名而不定义它。 例如:extern int a 其中变量 a 可以在别的文件中定义的。
    • 除非有extern关键字,否则都是变量的定义。
      extern int i; //声明,不是定义
      int i; //声明,也是定义
    1.占位符
    • %d,%i 整数
    • %f 浮点
    • %s 字符串
    • %c char
    • %p 指针
    • %fL 长long
    • %e 科学计数
    • %g 小数或科学计数
    • %a,%A 读入一个浮点值(仅C99有效)
    • %c 读入一个字符
    • %d 读入十进制整数
    • %i 读入十进制,八进制,十六进制整数
    • %o 读入八进制整数
    • %x,%X 读入十六进制整数
    • %s 读入一个字符串,遇空格、制表符或换行符结束
    • %f,%F,%e,%E,%g,%G 用来输入实数,可以用小数形式或指数形式输入
    • %p 读入一个指针
    • %u 读入一个无符号十进制整数
    • %n 至此以读入值的等价字符数
    • %[] 扫描字符集合
    • %% 读 % 符号

    6.常量

    1.整数常量

    整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。

    整数常量也可以带一个后缀,后缀是 U 和 L 的组合,U 表示无符号整数(unsigned),L 表示长整数(long)。后缀可以是大写,也可以是小写,U 和 L 的顺序任意。

    212         /* 合法的 */
    215u /* 合法的 */
    0xFeeL /* 合法的 */
    078 /* 非法的:8 不是八进制的数字 */
    032UU /* 非法的:不能重复后缀 */

    85 /* 十进制 */
    0213 /* 八进制 */
    0x4b /* 十六进制 */
    30 /* 整数 */
    30u /* 无符号整数 */
    30l /* 长整数 */
    30ul /* 无符号长整数 */

    2.浮点常量

    浮点常量由整数部分、小数点、小数部分和指数部分组成。您可以使用小数形式或者指数形式来表示浮点常量。

    当使用小数形式表示时,必须包含整数部分、小数部分,或同时包含两者。当使用指数形式表示时, 必须包含小数点、指数,或同时包含两者。带符号的指数是用 e 或 E 引入的。

    3.14159       /* 合法的 */
    314159E-5L /* 合法的 */
    510E /* 非法的:不完整的指数 */
    210f /* 非法的:没有小数或指数 */
    .e55 /* 非法的:缺少整数或分数 */

    3.定义常量

    在 C 中,有两种简单的定义常量的方式:

    使用 #define 预处理器。
    使用 const 关键字。

    #define identifier value
    const type variable = value;

    7.C存储类

    存储类定义 C 程序中变量/函数的范围(可见性)和生命周期。这些说明符放置在它们所修饰的类型之前。下面列出 C 程序中可用的存储类:

    • auto
    • register
    • static
    • extern
    1.auto 存储类(普通局部栈变量)

    auto 存储类是所有局部变量默认的存储类。

    {
    int mount;
    auto int month;
    }

    上面的实例定义了两个带有相同存储类的变量,auto 只能用在函数内,即 auto 只能修饰局部变量。

    2.register 存储类

    register 存储类用于定义存储在寄存器中而不是 RAM 中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个词),且不能对它应用一元的 ‘&’ 运算符(因为它没有内存位置)。

    {
    register int miles;
    }

    3.static 存储类

    static 存储类指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。

    4.extern 存储类

    extern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当您使用 ‘extern’ 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。
    extern 修饰符通常用于当有两个或多个文件共享相同的全局变量或函数的时候,

    // 文件 main.c
    #include <stdio.h>

    int count ;
    extern void write_extern();

    int main()
    {
    count = 5;
    write_extern();
    }

    // 文件 support.c
    #include <stdio.h>

    extern int count;

    void write_extern(void)
    {
    printf("count is %d\n", count);
    }

    5.总结

    从作用域看:

    1、全局变量具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。
    2、静态局部变量具有局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。
    3、局部变量也只有局部作用域,它是自动对象(auto),它在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。
    4、静态全局变量也具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。

    从分配内存空间看:

    1、全局变量,静态局部变量,静态全局变量都在静态存储区分配空间,而局部变量在栈里分配空间
    2、全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。这两者在存储方式上并无不同。这两者的区别虽在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。

    1)静态变量会被放在程序的静态数据存储区(全局可见)中,这样可以在下一次调用的时候还可以保持原来的赋值。这一点是它与堆栈变量和堆变量的区别。
    2)变量用static告知编译器,自己仅仅在变量的作用范围内可见。这一点是它与全局变量的区别。
    从以上分析可以看出, 把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。因此static 这个说明符在不同的地方所起的作用是不同的。应予以注意。

    8.运算符

    1.异或

    异或用来交换两个数的值

    unsigned int a=60;  //0011 1100
    unsigned int b=13; //0000 1101
    a=a^b; //a=a^b=0011 0001
    b=a^b; //b=a^b=0011 1100 相当于b1=(a^b)^b
    a=a^b; //a=a^b=0000 1101 相当于a1=(a^b)^((a^b)^b)

    2.求余

    如果 % 左边是正数,那么余数也是正数;
    如果 % 左边是负数,那么余数也是负数;

    9.函数

    1.函数体

    函数定义

    return_type function_name (parameter list) 
    {
    ...
    return data;
    }

    函数声明

    return_type function_name (parameter list);

    2.函数参数
    调用类型 描述
    传值调用 该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。
    引用调用 通过指针传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。
    3.内部函数

    如果一个函数只能被本文件中其他函数所调用,它称为内部函数。在定义内部函数时,在函数名和函数类型的前面加 static,即

    static 类型名 函数名 (形参表)

    内部函数又称静态函数。使用内部函数,可以使函数的作用域只局限于所在文件。即使在不同的文件中有同名的内部函数,也互不干扰。提高了程序的可靠性。

    4.外部函数

    如果在定义函数时,在函数的首部的最左端加关键字 extern,则此函数是外部函数,可供其它文件调用。

    如函数首部可以为

    extern int max (int a,int b)

    C 语言规定,如果在定义函数时省略 extern,则默认为外部函数。

    3.内联函数

    内联函数是指用inline关键字修饰的函数。在类内定义的函数被默认成内联函数。内联函数从源代码层看,有函数的结构,而在编译后,却不具备函数的性质。
    内联扩展是用来消除函数调用时的时间开销。它通常用于频繁执行的函数,对于小内存空间的函数非常受益。
    使用内联函数的时候要注意:

    • 递归函数不能定义为内联函数
    • 内联函数一般适合于不存在while和switch等复杂的结构且只有1~5条语句的小函数上,否则编译系统将该函数视为普通函数。
    • 内联函数只能先定义后使用,否则编译系统也会把它认为是普通函数
    • 对内联函数不能进行异常的接口声明。
    4.main 函数
    int main( int argc, char *argv[] )

    argc 和 argv 是 main 函数的形式参数。
    命令行字符串将作为实际参数传递给main函数。具体为:

    • 可执行文件名称和所有参数的个数之和传递给 argc;
    • 可执行文件名称(包括路径名称)作为一个字符串,首地址被赋给 argv[0],参数1也作为一个字符串,首地址被赋给 argv[1],… …依次类推。

    10.枚举

    enum 枚举名 {枚举元素1,枚举元素2,...};

    注意:第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。我们在这个实例中把第一个枚举成员的值定义为 1,第二个就为 2,以此类推。

    在C 语言中,枚举类型是被当做 int 或者 unsigned int 类型来处理的,所以按照 C 语言规范是没有办法遍历枚举类型的。
    不过在一些特殊的情况下,枚举类型必须连续是可以实现有条件的遍历。

    1.先定义枚举类型,再定义枚举变量
    enum DAY
    {
    MON=1, TUE, WED, THU, FRI, SAT, SUN
    };
    enum DAY day;
    2.定义枚举类型的同时定义枚举变量
    enum DAY
    {
    MON=1, TUE, WED, THU, FRI, SAT, SUN
    } day;
    3.省略枚举名称,直接定义枚举变量
    enum
    {
    MON=1, TUE, WED, THU, FRI, SAT, SUN
    } day;

    11.指针

    每一个变量都有一个内存位置,每一个内存位置都定义了可使用连字号(&)运算符访问的地址,它表示了在内存中的一个地址。通过使用一元运算符 * 来返回位于操作数所指定地址的变量的值。
    指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。

    在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。
    NULL 指针是一个定义在标准库中的值为零的常量。

    1.指针的算术运算

    每增加一次,它都将指向下一个整数位置,即当前位置往后移 4 个字节。

    #include <stdio.h>

    const int MAX = 3;

    int main ()
    {
    int var[] = {10, 100, 200};
    int i, *ptr;

    /* 指针中的数组地址 */
    ptr = var;
    for ( i = 0; i < MAX; i++)
    {

    printf("存储地址:var[%d] = %x\n", i, ptr );
    printf("存储值:var[%d] = %d\n", i, *ptr );

    /* 移动到下一个位置 */
    ptr++;
    }
    return 0;
    }

    2.复杂类型
    • int p; — 这是一个普通的整型变量
    • int *p; — 首先从 p 处开始,先与*结合,所以说明 p 是一个指针, 然后再与 int 结合, 说明指针所指向的内容的类型为 int 型。所以 p 是一个返回整型数据的指针。
    • int p[3] — 首先从 p 处开始,先与[] 结合,说明 p 是一个数组, 然后与 int 结合, 说明数组里的元素是整型的, 所以 p 是一个由整型数据组成的数组。
    • int *p[3]; — 首先从 p 处开始, 先与 [] 结合, 因为其优先级比 高,所以 p 是一个数组, 然后再与 结合, 说明数组里的元素是指针类型, 然后再与 int 结合, 说明指针所指向的内容的类型是整型的, 所以 p 是一个由返回整型数据的指针所组成的数组。
    • int (*p)[3]; — 首先从 p 处开始, 先与 * 结合,说明 p 是一个指针然后再与 [] 结合(与”()”这步可以忽略,只是为了改变优先级), 说明指针所指向的内容是一个数组, 然后再与int 结合, 说明数组里的元素是整型的。所以 p 是一个指向由整型数据组成的数组的指针。
    • int **p; — 首先从 p 开始, 先与 结合, 说是 p 是一个指针, 然后再与 结合, 说明指针所指向的元素是指针, 然后再与 int 结合, 说明该指针所指向的元素是整型数据。由于二级指针以及更高级的指针极少用在复杂的类型中, 所以后面更复杂的类型我们就不考虑多级指针了, 最多只考虑一级指针。
    • int p(int); — 从 p 处起,先与 () 结合, 说明 p 是一个函数, 然后进入 () 里分析, 说明该函数有一个整型变量的参数, 然后再与外面的 int 结合, 说明函数的返回值是一个整型数据。
    • int (*p)(int); — 从 p 处开始, 先与指针结合, 说明 p 是一个指针, 然后与()结合, 说明指针指向的是一个函数, 然后再与()里的 int 结合, 说明函数有一个int 型的参数, 再与最外层的 int 结合, 说明函数的返回类型是整型, 所以 p 是一个指向有一个整型参数且返回类型为整型的函数的指针。
    • int *(*p(int))[3]; — 可以先跳过, 不看这个类型, 过于复杂从 p 开始,先与 () 结合, 说明 p 是一个函数, 然后进入 () 里面, 与 int 结合, 说明函数有一个整型变量参数, 然后再与外面的 结合, 说明函数返回的是一个指针, 然后到最外面一层, 先与[]结合, 说明返回的指针指向的是一个数组, 然后再与 结合, 说明数组里的元素是指针, 然后再与 int 结合, 说明指针指向的内容是整型数据。所以 p 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数。
    3.指向函数的指针

    代码和数据是一样的,都需要占据一定内存,那当然也会有一个基地址,所以我们可以定义一个指针来指向这个基地址,这就是所谓的函数指针。

    double func(int a,char c);
    double (*p)(int a,char c);
    p=&func;

    double s1=func(100,'x');
    double s2=(*p)(100,'x');

    4.提示
    • 内存是线性的,内存以地址空间的形式呈现给我们看的,所以可以说所谓的地址空间也是线性的,指针存放的是内存地址,所以你可以对地址做 ++,或者 — 这样的运算。
    • 两个指针不赋 NULL,是坏习惯
    • 初始化指针不赋 NULL,因为这样的指针会指向一片未知的区域,这样的指针不是空指针,但指向一片访问受限制的内存区域,你无法使用它,这样的情况下的指针,业界给了它一个形象的名字:“野指针”,而且难以调试,在许多编译器单步 debug 会出现奇怪的错误,但经常看见的 “Segmentation Fault” 这样的错误,实测当代码多的时候,这是一个非常蛋疼的错误,野指针就是成因之一,所以看到这样的错误,首先是想想,是否有某些指针没有初始化引起的
    • free() 后指针不赋 NULL,为指针分配内存后,指针便可以指向一片合法可使用的内存,但使用 free() 释放那片内存时,指针依旧存放着那片内存的地址,也就是依旧指向那片内存,但这片内存已经释放,不可访问,这时若不小心使用了这个指针,便会内存错误,又是会有奇怪的 bug ,代码几百行多点就会难以调试,业界给这样的指针也有个统称:“悬空指针”,为了避免这种蛋疼的情况出现,一定要释放内存后,给指向这片内存的指针,都赋值为 NULL,从中也可以看出,free() 这个函数释放内存时跟指向这片内存的指针并没有什么卵关系,不会连着把指针一起搞定掉的! 珍爱生命,远离 “野指针” 与 “悬空指针” !
    • 多级指针,指向指针的指针,有时人们也管它叫多维指针。既然指针变量是一个变量,指针变量能存变量的内存的地址。

    12.字符串

    在 C 语言中,字符串实际上是使用 null 字符 ‘\0’ 终止的一维字符数组。

    char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};

    依据数组初始化规则,您可以把上面的语句写成以下语句:
    char greeting[] = "Hello";

    C 中有大量操作字符串的函数:

    • strcpy(s1, s2); 复制字符串 s2 到字符串 s1。
    • strcat(s1, s2); 连接字符串 s2 到字符串 s1 的末尾。
    • strlen(s1); 返回字符串 s1 的长度。
    • strcmp(s1, s2); 如果 s1 和 s2 是相同的,则返回 0;如果 s1\s2 则返回大于 0。
    • strchr(s1, ch); 返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。
    • strstr(s1, s2); 返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。

    strlen 与 sizeof的区别:
    strlen 是函数,sizeof 是运算操作符,二者得到的结果类型为 size_t,即 unsigned int 类型。
    sizeof 计算的是变量的大小,不受字符 \0 影响;
    而 strlen 计算的是字符串的长度,以 \0 作为长度判定依据。

    13.结构体

    C 数组允许定义可存储相同类型数据项的变量,结构是 C 编程中另一种用户自定义的可用的数据类型,它允许您存储不同类型的数据项。

    1.定义结构

    为了定义结构,您必须使用 struct 语句。struct 语句定义了一个包含多个成员的新的数据类型,struct 语句的格式如下:

    struct tag { 
    member-list
    member-list
    member-list
    ...
    } variable-list ;

    tag 是结构体标签。
    member-list 是标准的变量定义,比如 int i; 或者 float f,或者其他有效的变量定义。
    variable-list 结构变量,定义在结构的末尾,最后一个分号之前,您可以指定一个或多个结构变量。下面是声明 Book 结构的方式:
    struct Books
    {
    char title[50];
    char author[50];
    char subject[100];
    int book_id;
    } book;

    在一般情况下,tag、member-list、variable-list 这 3 部分至少要出现 2 个。

    //此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
    //同时又声明了结构体变量s1
    //这个结构体并没有标明其标签
    struct
    {
    int a;
    char b;
    double c;
    } s1;

    //此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
    //结构体的标签被命名为SIMPLE,没有声明变量
    struct SIMPLE
    {
    int a;
    char b;
    double c;
    };
    //用SIMPLE标签的结构体,另外声明了变量t1、t2、t3
    struct SIMPLE t1, t2[20], *t3;

    //也可以用typedef创建新类型
    typedef struct
    {
    int a;
    char b;
    double c;
    } Simple2;
    //现在可以用Simple2作为类型声明新的结构体变量
    Simple2 u1, u2[20], *u3;

    在上面的声明中,第一个和第二声明被编译器当作两个完全不同的类型,即使他们的成员列表是一样的,如果令 t3=&s1,则是非法的。
    结构体的成员可以包含其他结构体,也可以包含指向自己结构体类型的指针,而通常这种指针的应用是为了实现一些更高级的数据结构如链表和树等。

    如果两个结构体互相包含,则需要对其中一个结构体进行不完整声明,如下所示:

    struct B;    //对结构体B进行不完整声明

    //结构体A中包含指向结构体B的指针
    struct A
    {
    struct B *partner;
    //other members;
    };

    //结构体B中包含指向结构体A的指针,在A声明完后,B也随之进行声明
    struct B
    {
    struct A *partner;
    //other members;
    };

    2.结构体变量的初始化
    struct Books
    {
    char title[50];
    char author[50];
    char subject[100];
    int book_id;
    } book = {"C 语言", "RUNOOB", "编程语言", 123456};
    3.访问结构成员

    为了访问结构的成员,我们使用成员访问运算符(.)。成员访问运算符是结构变量名称和我们要访问的结构成员之间的一个句号。您可以使用 struct 关键字来定义结构类型的变量。下面的实例演示了结构的用法:

    4.结构作为函数参数
    /* 函数声明 */
    void printBook( struct Books book );
    5.指向结构的指针
    struct Books *struct_pointer;

    现在,您可以在上述定义的指针变量中存储结构变量的地址。为了查找结构变量的地址,请把 & 运算符放在结构名称的前面,如下所示:

    struct_pointer = &Book1;

    为了使用指向该结构的指针访问结构的成员,您必须使用 -> 运算符,如下所示:
    struct_pointer->title;

    6.位域

    有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有 0 和 1 两种状态,用 1 位二进位即可。为了节省存储空间,并使处理简便,C 语言又提供了一种数据结构,称为”位域”或”位段”。

    位域定义与结构定义相仿,其形式为:

    struct 位域结构名 
    {

    位域列表

    };

    struct bs{
    int a:8;
    int b:2;
    int c:6;
    }data;

    说明 data 为 bs 变量,共占两个字节。其中位域 a 占 8 位,位域 b 占 2 位,位域 c 占 6 位。

    注意:

    • 一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域。也可以有意使某位域从下一单元开始。
      // 在这个位域定义中,a 占第一字节的 4 位,后 4 位填 0 表示不使用,b 从第二字节开始,占用 4 位,c 占用 4 位。
      struct bs{
      unsigned a:4;
      unsigned :4; /* 空域 */
      unsigned b:4; /* 从下一单元开始存放 */
      unsigned c:4
      }
    • 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。如果最大长度大于计算机的整数字长,一些编译器可能会允许域的内存重叠,另外一些编译器可能会把大于一个域的部分存储在下一个字中。
    • 位域可以是无名位域,这时它只用来作填充或调整位置。无名的位域是不能使用的。
      struct k{
      int a:1;
      int :2; /* 该 2 位不能使用 */
      int b:3;
      int c:2;
      };

    14.共用体

    共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。

    结构体变量所占内存长度是各成员占的内存长度之和。每个成员分别占有其自己的内存单元。
    共用体变量所占的内存长度等于最长的成员变量的长度。

    1.定义共用体

    为了定义共用体,您必须使用 union 语句,方式与定义结构类似。union 语句定义了一个新的数据类型,带有多个成员。

    union [union tag]
    {
    member definition;
    member definition;
    ...
    member definition;
    } [one or more union variables];

    union Data
    {
    int i;
    float f;
    char str[20];
    } data;

    union tag 是可选的,每个 member definition 是标准的变量定义,在共用体定义的末尾,最后一个分号之前,您可以指定一个或多个共用体变量,这是可选的。

    2.访问共用体成员

    使用 union 关键字来定义共用体类型的变量。为了访问共用体的成员,我们使用成员访问运算符(.)。

    15.typedef

    C 语言提供了 typedef 关键字,您可以使用它来为类型取一个新的名字。下面的实例为单字节数字定义了一个术语 BYTE:

    typedef unsigned char BYTE;

    可以使用 typedef 来为用户自定义的数据类型取一个新的名字。

    typedef struct Books
    {
    char title[50];
    char author[50];
    char subject[100];
    int book_id;
    } Book;

    typedef vs #define

    define 是 C 指令,用于为各种数据类型定义别名,与 typedef 类似,但是它们有以下几点不同:

    • typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。
    • typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。
    • define可以使用其他类型说明符对宏类型名进行扩展,但对 typedef 所定义的类型名却不能这样做。例如:

      #define INTERGE int
      unsigned INTERGE n; //没问题
      typedef int INTERGE;
      unsigned INTERGE n; //错误,不能在 INTERGE 前面添加 unsigned
    • 在连续定义几个变量的时候,typedef 能够保证定义的所有变量均为同一类型,而 #define 则无法保证。例如:
      #define PTR_INT int *
      PTR_INT p1, p2; //p1、p2 类型不相同,宏展开后变为int *p1, p2;
      typedef int * PTR_INT
      PTR_INT p1, p2; //p1、p2 类型相同,它们都是指向 int 类型的指针。

    16.文件读写

    1.打开文件

    可以使用 fopen( ) 函数来创建一个新的文件或者打开一个已有的文件,这个调用会初始化类型 FILE 的一个对象,类型 FILE 包含了所有用来控制流的必要的信息。下面是这个函数调用的原型:

    FILE *fopen( const char * filename, const char * mode );

    访问模式 mode 的值:

    r:打开一个已有的文本文件,允许读取文件。
    w:打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,则该会被截断为零长度,重新写入。
    a:打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。
    r+:打开一个文本文件,允许读写文件。
    w+:打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。
    a+:打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。
    如果处理的是二进制文件,则需使用下面的访问模式来取代上面的访问模式:

    "rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"

    2.关闭文件

    为了关闭文件,请使用 fclose( ) 函数。函数的原型如下:

    int fclose( FILE *fp );

    如果成功关闭文件,fclose( ) 函数返回零,如果关闭文件时发生错误,函数返回 EOF。这个函数实际上,会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。EOF 是一个定义在头文件 stdio.h 中的常量。

    3.写入文件

    下面是把字符写入到流中的最简单的函数:

    int fputc( int c, FILE *fp );

    函数 fputc() 把参数 c 的字符值写入到 fp 所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误,则会返回 EOF。

    int fputs( const char *s, FILE *fp );

    函数 fputs() 把字符串 s 写入到 fp 所指向的输出流中。如果写入成功,它会返回一个非负值,如果发生错误,则会返回 EOF。您也可以使用 int fprintf(FILE *fp,const char *format, …) 函数来写把一个字符串写入到文件中。

    4.读取文件

    下面是从文件读取单个字符的最简单的函数:

    int fgetc( FILE * fp );

    fgetc() 函数从 fp 所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回 EOF。

    char *fgets( char *buf, int n, FILE *fp );

    函数 fgets() 从 fp 所指向的输入流中读取 n - 1 个字符。它会把读取的字符串复制到缓冲区 buf,并在最后追加一个 null 字符来终止字符串。如果这个函数在读取最后一个字符之前就遇到一个换行符 ‘\n’ 或文件的末尾 EOF,则只会返回读取到的字符,包括换行符。

    5.二进制 I/O 函数

    下面两个函数用于二进制输入和输出:

    size_t fread(void *ptr, size_t size_of_elements,
    size_t number_of_elements, FILE *a_file);

    size_t fwrite(const void *ptr, size_t size_of_elements,
    size_t number_of_elements, FILE *a_file);

    17.预处理器

    C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。

    所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。下面列出了所有重要的预处理器指令:

    • define:定义宏

    • include:包含一个源代码文件

    • undef:取消已定义的宏

    • ifdef:如果宏已经定义,则返回真

    • ifndef:如果宏没有定义,则返回真

    • if:如果给定条件为真,则编译下面代码

    • else:#if 的替代方案

    • elif:如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码

    • endif:结束一个 #if……#else 条件编译块

    • error:当遇到标准错误时,输出错误消息

    • pragma:使用标准化方法,向编译器发布特殊的命令到编译器中

    1.预定义宏

    ANSI C 定义了许多宏。在编程中您可以使用这些宏,但是不能直接修改这些预定义的宏。

    • DATE:当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。
    • TIME:当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。
    2.预处理器运算符

    C 预处理器提供了下列的运算符来帮助您创建宏:

    1.宏延续运算符(\)

    一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符(\)。

    #define  message_for(a, b)  \
    printf(#a " and " #b ": We love you!\n")

    2.字符串常量化运算符(#)

    在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#)。在宏中使用的该运算符有一个特定的参数或参数列表。

    #include <stdio.h>

    #define message_for(a, b) \
    printf(#a " and " #b ": We love you!\n")

    int main(void)
    {
    message_for(Carole, Debra);
    return 0;
    }

    3.标记粘贴运算符(##)

    宏定义内的标记粘贴运算符(##)会合并两个参数。它允许在宏定义中两个独立的标记被合并为一个标记。

    #include <stdio.h>

    #define tokenpaster(n) printf ("token" #n " = %d", token##n)

    int main(void)
    {
    int token34 = 40;

    // 输出40
    tokenpaster(34);
    return 0;
    }

    4.defined() 运算符

    预处理器 defined 运算符是用在常量表达式中的,用来确定一个标识符是否已经使用 #define 定义过。如果指定的标识符已定义,则值为真(非零)。如果指定的标识符未定义,则值为假(零)。

    #include <stdio.h>

    #if !defined (MESSAGE)
    #define MESSAGE "You wish!"
    #endif

    int main(void)
    {
    printf("Here is the message: %s\n", MESSAGE);
    return 0;
    }

    5.参数化的宏

    CPP 一个强大的功能是可以使用参数化的宏来模拟函数。

    // 参数列表是括在圆括号内,且必须紧跟在宏名称的后边。宏名称和左圆括号之间不允许有空格。
    #define square(x) ((x) * (x))
    #define MAX(x,y) ((x) > (y) ? (x) : (y))
    #define SWAP2(x,y) {x=x^y;y=x^y;x=x^y;}

    18.错误处理

    在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。您可以在 errno.h 头文件中找到各种各样的错误代码。

    1.errno、perror() 和 strerror()

    C 语言提供了 perror() 和 strerror() 函数来显示与 errno 相关的文本消息。

    • perror() 函数显示您传给它的字符串,后跟一个冒号、一个空格和当前 errno 值的文本表示形式。
    • strerror() 函数,返回一个指针,指针指向当前 errno 值的文本表示形式。
      #include <stdio.h>
      #include <errno.h>
      #include <string.h>

      extern int errno ;

      int main ()
      {
      FILE * pf;
      int errnum;
      pf = fopen ("unexist.txt", "rb");
      if (pf == NULL)
      {
      errnum = errno;
      fprintf(stderr, "错误号: %d\n", errno);
      perror("通过 perror 输出错误");
      fprintf(stderr, "打开文件错误: %s\n", strerror( errnum ));
      }
      else
      {
      fclose (pf);
      }
      return 0;
      }

    19.内存管理

    • void *calloc(int num, int size);
      在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是0。

    • void free(void *address);
      该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。

    • void *malloc(int num);
      在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。

    • void *realloc(void *address, int newsize);
      该函数重新分配内存,把内存扩展到 newsize。

    注意:void 类型表示未确定类型的指针。C、C++ 规定 void 类型可以通过类型转换强制转换为任何其它类型的指针。