C语言学习笔记

风尘

文章目录

  1. 1. 数据类型
  2. 2. 常量
  3. 3. 变量
  4. 4. 运算符
  5. 5. 流程控制
  6. 6. 函数
  7. 7. 预处理器
  8. 8. 指针
  9. 9. 结构
    1. 9.1. 类型定义
    2. 9.2. 联合
  10. 10. 输入与输出

数据类型

  • char 字符型,点一个字节。
  • int 整型,通常代表机器中整数的自然长度。
  • short int 短整型,通常为16位(int可以省略)。
  • long int 长整型,通常为32位(int可以省略)。
  • float 单精度浮点型
  • double 双精度浮点型
  • long double 高精度的浮点数

signedunsigned用于限定char类型和任何整型,unsigned类型的数值总是正值或0。

常量

语法:#define 常量名 常量值

  • 整数常量
    包含intlong类型常量。long类型以lL结尾。
    当一个整数无法用int表示时,也被当作long类型处理。
    无符号常量以uU结尾,无符号长整型使用ulUL结尾。
    前缀0表示八进制形式,0x表示十六进制形式。

  • 浮点数常量
    没有后缀的常量为double类型。
    后缀加fF表示float类型。
    后缀加lL表示long double类型。

  • 字符常量
    一个字符常量是一个整数,如'0'值为48,它与数值0无关。
    转义字符通常只表示一个字符,如'\013'
    字符常量'\0'表示值为0的字符,即空字符(null)。

  • 字符串常量
    与字符常量的区别是字符串常量用" "双引号括起来。其实就是字符数组,内部使用空字符('\0')作为结尾,因此,字符串常量占据的存储单元比双引号内的字符数大1。

  • 枚举常量
    语法:enum 枚举名 {枚举列表}
    枚举常量是另外一种类型常量,是一个常量整型值的列表,如:

    1
    enum boolean {NO, YES};

    未显示声明的枚举,第一个枚举名的值为0,第二值为1,依此类推。
    如果指定部分枚举值,未指定枚举值将向后递增。

    1
    2
    3
    4
    enum colors {WHITE=0,BLACK,RED,YELLOW}
    BLACK-->1
    RED-->2
    YELLOW-->3

常量表达式是仅仅包含常量的表达式。这种表达式只在编译时求值,而不在运行时求值。

变量

变量必须先声明再使用,一个变量声明只能指定一种类型,后面可以有一个或多个该类型变量。如:int lower,upper…;
任何

  • 外部变量
    定义在函数之外的变量叫做外部变量。由于定义在函数之外,因此可以在所有函数中使用。由于C语言不允许在一个函数中定义其它函数,因此函数本身是“外部的”。变量都可以使用const限定符限定为不可被修改变量。

    如果要在外部变量定义之前使用变量,或者外部变量的定义与变量的使用不在同一个源文件中,必须在相应变量声明中强制使用关键字extern。外部变量的定义中数组必须指定长度,但extern声明不一定要指定数组长度。

    文件a.c
    1
    2
    3
    #define MAXSIZE 10;
    int a;
    double b[MAXSIZE];
    文件b.c
    1
    2
    extern int a; //使用a.c文件中的变量a
    extern double b[]; //使用a.c文件中的b省略了数组大小
  • 自动变量
    定义在函数内的变量叫做“局部变量”,也叫“自动变量”。由于定义在函数之内,因此只可以函数内使用,多次调用函数不保留前次调用时的赋值。

  • 静态变量
    static修饰的变量,叫做静态变量。静态变量的存储方式与全局变量相同,都是静态存储方式。全局变量的作用域是整个源程序,即源程序源的所有文件中有效。静态变量作用域则是只在当前变量所在源文件中可以使用,其次静态变量的值在函数调用后一直保持不会消失。即使在函数中声明的,每次调用函数,其值都会保存上一次调用后值。

  • 寄存器变量
    使用register关键字声明的变量,叫做寄存器变量。register变量放在机器的寄存器中,这样可以使程序更小,执行速度更快。register声明只适用于自动变量或函数的形式参数形式:

    1
    2
    3
    4
    5
    test(register variA,register variB)
    {
    register int variC;
    ...
    }

    实际上,底层硬件环境对寄存器变量的使用会有一些限制。每个函数中只有很少的变量可以保存在寄存器中,且只允许某些变量类型的变量。编译器可以忽略过量的或不支持的寄存器变量声明,因此过量的寄存器变量声明并没有什么害处。但是注意,无论寄存器变量实际上是不是存放在寄存器中,它的地址都是不能访问的。不同的机器,对寄存器变量的限制不同。

运算符

  • 自述运算符
    +-*/%
    c/c++java语言中取模运算(%)就是取余运算,而python则有些不同

  • 关系运算符
    >>=<<===!=

  • 逻辑运算符
    ||&&!

  • 按位运算符
    按位运算就是将数值转换为二进制位,然后进行运算得到最终值:
    & 按位与(AND)

    运算规则是两个为真才为真 1&1=1, 1&0=0, 0&1=0, 0&0=0。如 3 二进制位是 0000 00115 的二进制位是 0000 0101,由按位与规则可得,001 & 101等于0000 0001,最终值为1

    求模运算时当被除数为 2 的 n 方时,可以用按位与运算替换更高效,公式为 a%2n=a&(2n1)a\%2^n=a\&(2^n-1) 即,

    14%8=14%23=14&(231)=614\%8=14\%2^3=14\&(2^3-1)=6

    | 按位或(OR)

    运算规则是一个为真则为真1|0=1, 1|1=1, 0|0=0, 0|1=1。如6二进制位是0000 01102 的二进制位是0000 0010,由按位或规则可得,110 | 010等于110,最终值为6

    ^ 按位异或(XOR)

    运算规则是如果两个值不相同,则为真,相同则为假1^0=1, 1^1=0, 0^1=1, 0^0=0。如5二进制位是0000 01019 的二进制位是0000 1001,由按位异或规则可得,0110 ^ 1001 等于1100,最终值为12

    <<左位移

    运算规则是将左侧数值的二进制位向左移动右侧数值位。移动后右边补0,正数左边第一位补 0,负数补1,结果相当于乘以 2 的 n 次方。如:5<<2,就是5的二进制位向左移2位,即0000 0101101向左移两位得到0001 0100,最终值为5乘以 2 得 2 次方,等于20

    >>右位移

    运算规则是将左侧数值的二进制位向右移动右侧数值位。移动后正数第一位补0,负数补1,结果相当于除以 2 的 n 次方。如:5>>2,就是5 的进制位向右移动2位,即0000 0101101右移两位后得到0000 0001,最终值为5 除以 2 得 2 次方,等于1

    ~按位求反(一元运算符)

    运算规则是取位数值相反值~0=1, ~1=0。 如5 二进制位是0000 0101,取反后为1111 1010,最终值为-6

  • 自增运算符
    ++ 可以作为前缀运算符,表示先作自增,后赋值;也可以作为后缀运算符,表示先赋值,再作自增。

    1
    2
    3
    4
    int x, n;
    n = 1;
    x = ++n; //x值为2,n为2
    x = n++; //x值为2,n为3
  • 自减运算符
    -- 用法同自增运算符

  • 三元运算符
    表达式 ? 表达式 : 表达式

流程控制

  • if…else 语句

    1
    2
    3
    4
    if (表达式)
    语句
    else
    语句
  • switch 语句

    1
    2
    3
    4
    5
    switch (表达式) {
    case 常量表达式:语句
    case 常量表达式:语句
    default:语句
    }

    注意,case后必须为整数值常量或常量表达式。

  • while 循环

    1
    2
    while(表达式)
    语句

    如果希望while循环体至少被执行一次可以使用do...while循环:

    1
    2
    3
    do 
    语句
    while (表达式);
  • for 循环

    1
    2
    for(表达式1;表达式2;表达式3)
    语句
  • break / continue 语句
    用于继续或结束循环语句。

  • goto 语句

    1
    2
    3
    4
    5
    6
    for ( ... )
    for ( ... ) {
    if (disaster) goto error;
    }

    error:

    大多数情况,使用goto语句比不使用goto语句程序段要难以理解和维护,少数情况除外。尽管该问题不太严重,但还是建议尽可能少的使用。

函数

函数定义:
返回值类型 函数名(参数列表){ 函数体 }

函数定义可以不带有返回类型,默认返回int类型。函数在源文件中出现的次序可以是任意的,只要保证一个函数不被分离到多个文件中即可。被调用函数通过return 表达式向调用者返回值,return后面表达式可以省略。

预处理器

预处理器是编译过程中单独执行的第一个步骤,最常用的预处理器指令是#include#define

  • 文件包含
    文件包含指令(#include)用于在编译期间把指定文件的内容包含进当前文件中。形式如下:
    #include "文件名"

    #include <文件名>

    当文件名用引号引起来(通常用于包含程序目录中的文件),则在源文件所在位置查找该文件;如果该位置没有该文件或者文件名用尖括号括起来(通常用于包含编译器的类库路径里面的头文件),则根据相应规则查找该文件,该规则同具体实现有关。如果某个包含文件内容发生了变化,那么所有依赖于该包含的文件的源文件都必须重新编译。

  • 宏替换
    宏替换指令(#define)用于用任意字符序列替代一个标记。形式如下:
    #define 标识符 记号序列

    替换文本前后空格会被忽略,两次定义同一标识符是错误的,除非两次记号序列相同(所有空白分割符被认为是相同的)。
    该定义的名字作用域从其定义点开始,到被编译的源文件末尾处结束。定义超过一行使用反斜杠(\)换行。

    替换的文本可以是任意的,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //为无限循环定义一个名字
    #define forever for(;;)

    //定义带参数宏
    #define max(A,B) ((A) > (B) ? < (A) : (B))
    #define square(x) x * x

    main(){
    int i, z;

    i = z = 2;
    max(++i,i++); // 结果为4
    square(z+1); // 结果为5
    }

    宏定义也有一些缺陷,如上面max,它对每个参数执行两次自增操作。square没有增加括号而导致计算次序错误。

    可以通过#undef取消名字的宏定义:

    1
    #undef max

    #运算符可以使得宏定义的实际参数替换为带引号的字符串:

    1
    2
    3
    #define dprint(expr) printf(#expr + "=%d")
    调用结果x = 4,y = 2:
    dprint(x/y) --> x/y=2

    ##运算符可以使得宏定义的实际参数相连接:

    1
    2
    3
    #define paste(x,y) x ## y
    调用结果:
    paste(1,2) --> 12
  • 条件编译

    语法 
    1
    2
    3
    4
    5
    6
    7
    #if 常量表达式
     文本
    #elif 常量表达式
     文本
    #else
     文本
    #endif

    当预处理器检测到常量表达式值为非0时,对相应表达式下面文本进行编译,后续表达式及文本将会被抛弃。常量表达式可以使用define 标识符define(标识符)表达式,当标识符已经定义时,其值为1,否则为0。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //检测HDR标识符,没有定义时将其定义
    #if !defined(HDR)
    #define HDR
    #endif

    等价于

    #ifdef HDR
    #define HDR
    #endif

    如上所示,可以使用#ifdef 标识符#ifndef 标识符控制指令替换#if define 标识符

  • 预定义标识符

    识符 说明
    __LINE__ 当前所在源文件行数的十进制常量
    __FILE__ 被编译的源文件名字的字符串
    __DATE__ 被编译的源文件编译日期的字符串,格式:“Mmm dd yyyy”
    __TIME__ 被编译的源文件编译时间的字符串,格式:“hh:mm:ss”
    __STDC__ 整型常量1(只有在遵循标准的实现中该标识符才被定义为1)
  • 其他预处理指令
    #line 常量 "文件名" 以十进制整型常量的形式定义下一行源代码的行号。其中"文件名"可以省略,表示设置当前编译的源文件。
    #error 信息 当预处理器遇到此指令时停止编译并输出定义的错误消息。通常与#if...#endif等指令一起使用。
    #pragam 记号序列 使处理器执行一个与具体实现相关的操作。无法识别的pragma(编译指示)将被忽略掉。
    # 空指令。预处理器行不执行任何操作。

指针

为了便于记忆,指针的声明形式是在变量声明的基础上加一个*间接寻址或间接引用运算符:

1
int *p; //声明一个int类型的指针*p

通过一元运算符&获取一个对象的地址:

1
2
3
int x = 1;
p = &x;
printf("%d",*p); --> 打印1

一元运算符*&的优先级比算术运算符优先级高,因此在进行算术运算时不需要加括号:

1
2
3
4
5
*p += 1;

++*p;

(*p)++;

语句(*p)++中的圆括号是必需的,否则,表达式将对p进行加一运算,而不是对ip指向的对象进行加一运算,原因在于一元运算符表达式遵循从右到左的顺序。

由于指针也是变量,所以可以直接使用,而不必通过间接引用的方法使用:

1
2
int *pp;
pp = ip; //通过变量的形式将指针pp指向指针ip指向的对象

同其他类型变量一样,指针也可以初始化,对指针有意义的初始化值只能是0或表示地址的表达式。C语言保证0永远不是有效的数据地址,因此,返回0可用来表示发生了异常事件。

指针与整数之间不能相互转换,但0是唯一的例外。常量0可以赋值给指针,指针也可以与常量0进行比较。程序中通常使用符号常量(NULL)代替常量0,其定义在stddef.h头文件中。

  • 指针与函数参数
    C语言是以传值的方式将参数值传递给被调用函数,因此,被调用函数不能直接修改主调函数中变量的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void swap(int x,int y){
    int temp;

    temp = x;
    x = y;
    y = temp;
    }
    调用: 
    swap(x,y);

    由于参数传递是传值方式,所以上述函数无法成功交换变量。可以通过将交换的变量的指针传递给被调用函数的方法实现该功能:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void swap(int *px,int *py){
    int temp;

    temp = *px;
    *px = * py;
    *py = temp;
    }
    调用: 
    swap(&x,&y);
  • 指针与数组
    数组其实是由N个对象组成的集合,这些对象存储在相邻的内在区域中。因此可以将指针变量指向数组的每个对象。

    1
    2
    3
    4
    int a[10];
    int *pa;

    pa = &a[0]; //将pa指向数组第0个元素

    根据指针运算的定义,pa+1指向数组下一个对象,pa+i指向pa所指向数组对象之后的第i个对象,pa-i指向pa所指向数组对象之前的第i个元素。
    由于数组名所代表的就是该数组最开始的一个元素的地址,因此下面两等式作用相同:

    1
    2
    3
    pa = &a[0];

    pa = a;

    由上面等式,对数组元素a[i]的引用也可以写成*(a+i)形式。实际上,在C语言计算a[i]元素时就是先将其转换成*(a+i)的形式,然后再求值。

    数组名和指针的不同之处在于,指针是一个变量,数组名却不是变量。因此语句pa=apa++是合法的,而a=paa++形式是非法的。

    当两个指针指向同一个数组的成员时,两个指针可以进行比较运算(===、!=、<、>=):

    1
    2
    3
    4
    5
    6
    7
    char c[] = "hello";
    char *pc1 = c;
    char *pc2 = &c[1];

    运算:
    pc2 > pc1 --> True //比较运算返回True
    pc2 - pc1 + 1 --> 2 //返回两指针指向的元素之间元素的数目

    由上面代码可知,指向数组元素位越置靠前的指针,指针值越大。但是,指向不同数组的元素的指针之间的算术或比较运算没有意义。

    根据指针上面的特性,可以写出返回字符串长度函数的两个指针实现版本:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    /* strlen函数,返回客串s的长度 */
    版本一:
    int strlen(char *s){
    int n;

    for(n = 0;*s != '\0';s++)
    n++;

    return n;
    }

    版本二:
    int strlen(char *s){
    char *p = s;

    while (*p != '\0')
    p++;

    return p - s;
    }
    • 指针算术运算具有一致辞性,如果处理的数据类型是比字符型占据更多的存储空间的浮点类型,并且p是一个指向浮点类型的指针,那么在执行p++后,p将指向下一个浮点数的地址。所有的指针运算都会自动考虑它所指向的对象长度。
    • 有效的指针运算包括相同类型指针之间的赋值运算;指针同整数之间的加减法运算;指向相同数组中元素的两个指针间的减法或比较去处;将指针赋值为0或与0之间的比较运算。其他所有形式的指针运算都是非法的。

    C语言数组可以使用花括号{}括起来初值表进行初始化。同时也支持多维数组,如果将二维数组作为参数传递给函数,函数的参数声明中可以不指定数组的行数,但必须指明数组的列数,因为,二维数组在内存中的排列方式是按行排列的,即第一行排完之后再排列第二行,依此类推。当给出数组的列数时,通过列数与行数的关系,即可找到对应的地址。

    1
    2
    3
    4
    5
    6
    7
    8
    f (int array[2][13]);
    //可以写成
    f (int array[][13]);
    //还可以写成
    f (int (*array)[13]);

    //错误写法
    f (int *array[13]); --> 因为[]的优先级高于*的优先级,如果声明时不使用()时,相当于声明了一个指向指针的一维数组。

    由于指针本身也是变量,所以它也可以像其他变量一样被存储在数组中。因为[]优先级高于*的优先级,所以上例中int *array[13];相当于声明了一个指向指针的一维数组。

    指针数组与二维数组的区别是,二维数组是分配了固定存储空间(行*列)的,而指针数组只是定义了指定个数的指针,而没有对它们初始化,它们的初始化必须以显式的方式进行。因此,指针数组优于二维数组的重要一点是,指针数组每一行长度可以不同。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //二维数组
    int array[10][20]; --> 固定1020列长度的二维数组
    //指针数组 
    char *monthName = {"January","February","March"}; --> 长度不固定的指针数组

    //调用
    array[0][0];

    monthName[0][0]; -->返回 J
    **monthName; -->等价于下标方式 返回 J
  • 命令行参数
    调用主函数数(main)时,有两个参数。第一个参数(argc)用于参数计数,表示运行程序时命令行中参数的个数;第二个参数(argv),是一个指向字符串数组的指针,其中每个字符串对应一个参数。另外,ANSI标准要求,argv[argc]的值必须为一个空指针。main函数返回值为0表示正常退出,返回非0值表示代表程序异常退出。如echo程序,它将命令行参数回显在屏幕上的一行中,其中命令行中各参数之间用空格隔开:

    1
    2
    3
    4
    echo hello, world

    # 打印输出
    hello, world
    echo程序实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 版本一:将argv看成是一个字符指针数组
    #include <stdio.h>

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

    for (i = 1; i < argc; i++)
    printf("%s%s", argv[i], (i < argc - 1) ? " " : "");

    printf("\n");
    return 0;
    }

    // 版本二:通过指针方式实现
    main(int argc,char *argv[]){

    while (--argc > 0)
    printf("%s%s", *++argv, (argc > 1) ? " " : "");

    printf("\n");
    return 0;
    }

    注意:printf的格式化参数也可以是表达式。如:
    printf((argc>1) ? "%s " : "%s", *++argv);
  • 指向函数的指针
    C语言中,函数本身不是变量,但可以定义指向函数的指针。这种类型的指针可以被赋值、存放在数组中、传递给函数以及作为函数的返回值。调用指向函数的指针时,它们是函数的地址,因为它们是函数,所以同数组名一样,前面不需要加&运算符。
    由于任何类型的指针都可以转换为void *类型,并且在将它转换回原来的类型时不会丢失信息,因此,函数指针数组参数的类型通常用void指针类型。定义形式:
    类型 (*指针变量名) (参数列表)

    注意上面(*指针变量名)括号不能省略。

    指向函数指针的常见用法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    /* 实现operate()函数传入不同函数指针实现相关函数功能 */ 

    #include <assert.h>
    #include <stdio.h>

    double operate(double *num, int len, double (*nump)(double *num, int len));
    double max(double *num, int len);
    double min(double *num, int len);
    double avg(double *num, int len);

    main(int argc, char *argv[]) {
    double num[] = {2.0, 2.0, 3.0, 1.5, 2.5, 5.3, 4.0, 2};
    int len = 8;
    printf("%f", operate(num, len, min)); --> 1.5
    printf("%f", operate(num, len, max)); --> 5.3
    printf("%f", operate(num, len, avg)); --> 2.7875
    }

    //获取数组最大值
    double max(double *num, int len) {
    double temp;

    temp = *num++;
    for (int i = 1; i < len; i++, num++)
    if (temp < *num)
    temp = *num;

    return temp;
    }

    // 获取数组最小值
    double min(double *num, int len) {
    double temp;

    temp = *num++;
    for (int i = 1; i < len; i++, num++)
    if (temp > *num)
    temp = *num;

    return temp;
    }

    //获取数组平均值
    double avg(double *num, int len) {
    assert(len != 0);
    double ti;

    ti = 0;
    for (int i = 0; i < len; i++, num++)
    ti += *num;

    return ti / len;
    }

    // 通过函数指针调用不同的方法
    double operate(double *num, int len, double (*nump)(double *num, int len)) {
    return nump(num, len);
    }
  • 指针别名(Pointer aliasing)
    指两个及以上的指针指向同一数据,即不同的名字指针指向同一内在地址,则称一个指针是另一个指针的别名。如:

    1
    2
    3
    4
    int i = 0;
    int j = 0;
    int *a = &i;
    int *b = &i; // 指针b是指针a别名
  • restrict指针限定符
    该关键字是 C99 标准中新引入的一个指针类型修饰符,它只可应用于限定和约束指针,主要作用是限制指针别名,表明当前指针是访问一个数据对象的唯一方式,所有修改该指针所指向的内存中内容操作都必须通过该指针来修改,而不能通过其他途径修改。这样做的用处是帮助编译器更好的优化代码,生成更有效率的汇编代码。如果该指针与另外一个指针指向同一对象,将会导致未定义行为。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 未加指针限定符的指针参数
    int add(int *a, int *b){
    *a = 10;
    *b = 12;
    return *a + *b;
    }
    // 添加指针限定符的指针参数
    int add2(int *restrict a, int *restrict b){
    *a = 10;
    *b = 12;
    return *a + *b;
    }

    // 调用两个方法
    int i,j;
    add(&i, &j); // 返回值22,编译器无法确定内存是否被其他指针别名修改(即函数指针参数未设置 restrict 限定符),无法作出优化
    add2(&i, &j); // 返回值22,生成的汇编代码会进行优化操作
    add2(&i, &i); // 返回值24,因为传递参数违反了 restrict 限定符对函数内部实现的约束(两个参数指向同一内存地址,导致互为指针别名),导致未定义行为。

结构

结构是一个或多个变量的集合,这些变量可以是不同的类型。ANSI标准定义了定义了结构的赋值操作————结构可以拷贝、赋值、函数参数,函数返回值。声明形式如下:
struct 结构标记 { 结构成员 }

结构成员、结构标记和普通变量(非成员)可以使用相同名字,而不会冲突,因为通过上下文分析可以对它们进行区分。

struct声明定义了一种数据类型。在标志结构成员表结束的右花括号之后可以跟一个变量表,这与其他基本类型的变量声明是相同的:

1
struct {...} x, y, z;

如果结构声明的后面不带变量表,则不会为它分配存储空间,它仅仅描述了一个结构的模板或轮廓。如果结构声明中带有标记,就可以使用该标记定义结构实例:
struct 结构标记 结构名 [ = {结构初始化值} ]

上面结构初始化值可以省略,在表达式中可以使用结构成员运算符(.)引用某个特定结构中的成员:
结构名.成员

1
2
3
4
5
6
7
8
9
10
11
// 声明结构
struct point {
int x;
int y;
}

// 定义结构实例
struct point pt = {100,200}; --> 可以通过花括号的方式进行初始化

// 引用结构成员
printf("%d",pt.x); --> 打印100

结构可以进行嵌套,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 声明嵌套结构
struct react {
struct point pt1;
struct point pt2;
}

// 定义结构实例,并初始化
struct rect screen = {
{100, 200}, --> 花括号可以省略,但不建议
{300, 400}
};

// 引用结构成员
printf("%d",screen.pt2.x); --> 打印300

结构类型的参数和其他类型的参数一样,都是值传递。因此,下面例子不会改变原结构 p1 的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>

struct point {
int x;
int y;
};

main() {
struct point addpoint(struct point p1, struct point p2);

struct point p1 = {100, 200};
struct point p2 = {200, 300};

struct point p = addpoint(p1, p2);
printf("\n%d\t%d", p.x, p.y); // 打印300 500
printf("\n%d\t%d", p1.x, p1.y); // 打印100 200
}

struct point addpoint(struct point p1, struct point p2) {
p1.x += p2.x;
p1.y += p2.y;

return p1;
}
  • 结构数组
    当有一组信息需要存储到结构体时,可以使用结构数组。结构数组和普通数组声明类似,就是在定义结构实例时增加一个中括号[数组大小]即可,如:

    1
    struct point pa[50]; // 定义结构数组pa

    当使非字符数组时,结尾不是以\0结束,所以不容易判断数组长度。通常有三种解决方法:

    • 手工计算,直接写入具体长度。缺点是不得扩展,当列表变更时,需要手动维护,不安全。
    • 在初值表的结尾处加上一个空指针,然后遍历循环,直到讲到尾部的空指针为止。
    • 使用编译时一元运算符sizeof 对象sizeof (类型名),它可以计算任一对象的长度,即指定对象或类型占用的存储空间字节数。因为数组的长度在编译时已经完全确定,它等于 数组项的长度 * 项数,因此,得出数组项数为 数组长度 / 数组项的长度

    sizeof反回一个无符号整型值,其类型为size_t,该类定义在头文件<stddef.h>中。一般用法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 预处理器中的应用,如返回上面结构数组的大小
    #define PA_LENGTH (sizeof(pa) / sizeof(pa[0]))
    或 
    #define PA_LENGTH (sizeof(pa) / sizeof(struct point)) --> 两者作用相同,但当类型改变时此种写法需要同步修改,因此,建议使用前者方法。


    // 同结构一样其他类型数组也可使用上面方法获取数组大小
    int a[] = {1,12,123,1234};

    sizeof(a) / sizeof(a[0]) --> 4

    sizeof(a) / sizeof(int) --> 4
  • 结构指针
    当传递给函数的结构很大时,使用结构指针方式的效率比复制整个结构的效率高。结构指针和普通指针声明类似,如:

    1
    2
    3
    4
    5
    6
    // 声明结构指针
    struct point *pp;

    // 访问结构成员
    (*pp).x;
    (*pp).y;

    上面示例,访问指针结构成员(*pp).x中的圆括号,是必需的。因为 结构成员运算符(.)的优先级高于 指针运算符(*)。

    结构指针使用频率非常高,为了用不用方便,C语言提供了另一种简写方式引用结构成员:
    p->结构成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //声明结构数组指针*ppa和结构指针*pp
    struct point *ppa, *pp;

    // 结构数组指针指向结构数组
    ppa = pa;

    // 结构指针指向结构数组第二项 
    pp = &pa[1];

    ppa->y; --> 200
    pp->y; --> 400

    // 运算符 . 和 -> 都是从左至右结合,所以下面表达式等价
    struct rect r, *rp = &r;

    r.pt1.x ;
    rp->pt1.x ;
    (r.pt1).x ;
    (rp->pt1).x ;

    在所有运算符中,结构运算符.->用于函数用的()用于下标的[] 优先级最高。

  • 自引用结构
    一个包含自身实例的结构是非法的,但将实例声明为指针是允许的。如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    struct tnode {
    struct tnode left; --> 非法声明
    }

    struct tnode {
    struct tnode *left; --> 合法声明
    }

    // 结构互相引用
    struct t {
    struct s *p;
    }

    struct s {
    struct t *q;
    }

类型定义

C语言可以通过typedef来建立新的数据类型名,形式如下:

typedef 类型 类型名

typedef声明的类型在变量名的位置出现,而不是紧接在关键字typedef之后。建议使用大写字母开头定义类型名,以示区分。
typedef声明并没有创建一个新类型,只是为某个已存在的类型增加一个新的名称而已。

1
2
3
4
5
6
7
8
9
// 定义一个 String 类型
typedef char *String;

// 定义一个结构类型
typedef struct point Point;

Point pp = {5,6};
pp.x; --> 5
pp.y; --> 6

typedef类似于#define语句,但typedef是由编译器解释的,因此它的文本替换功能要超过预处理器的能力。如:

1
2
3
4
5
6
7
8
9
// 定义类型`PFI`是一个指向函数的指针,该函数接收两个`char *`类型的参数,返回`int`类型
typedef int (*PFI) (char *, char *);

function func(char *,char *){
return 0;
}

PFI pfi = &func;
pfi(0,0); --> 返回0

typedef除了表达方式更简洁之外,使用它还有两个重要原因。一是它可以使程序参数化,以提高程序的可移植性。如声明的数据类型同机器有关,当程序需要移植到其他机器上时,只需改变typedef类型定义就可以了。另一个原因是它可以为程序提供更好的说明性。如 PFI 类型明显比一个指向复杂结构的指针更容易让人理解。

联合

联合实际上就是一个结构,只不过联合的不同成员都保存在同一个存储空间,也就是联合中所有成员相对于基地址的偏移量都为0,因此联合空间要大到足够容纳最“宽”的成员。

定义:union 联合标记 { 联合成员 }

联合可以给任何一个成员赋值,但每次的赋值将会覆盖上一次赋值,因此读取的类型必须是最近一次存入的类型,否则其返回结果取决于计算机的具体实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义联合u
union myunion {
int i;
float f;
char c;
char *pc;
} u;

// 赋值
u.i = 10;
printf("\n%d", u.i); --> 10

u.c = 'a';
printf("\n%c", u.c); --> a
printf("\n%d", u.i); --> 97 // 因为 u.i 被最后一次 u.c 赋值覆盖,所以字符 'a' 对应的ASCII整数值为 97

如同上示例,访问联合成员与访问结构成员方式相同:

联合.成员联合指针->成员

输入与输出

  • 格式化输出/输出

    输出函数:int printf(char *format,...)

    输入函数:int scanf(char *format,...)

    函数格式化参数以%开始,并以一个转换转换字符结束。在%和转换字符之间依次可以包含:

    • 负号,用于指定被转换的参数按照左对齐的形式输出。
    • ,用于指定最小字段宽度。
    • 小数点,用于将字段和精度分开。
    • ,用于指定精度,即要打印的最大字符数、浮点数点后的位数、整型最少最少输出的数字数目。
    • 字母hl,表示将整数作为short类型或long类型打印。
转换符 描述
d, i int 类型;十进制数
o int 类型,打印无符号八进制数(没有前导0)。
x, X int 类型,打印无符号十六进制数(没有前导0x或0X)。
u int 类型,打印无符号十进制数。
c int 类型,单个字符。
s char *类型,打印字符串。
f double 类型十进制小数,精度默认为6。
e, E double 类型,输入格式为指数形式,精度默认是6。如:m.dddddd e +/-。
g, G double 类型,尾部的0和小数不打印。
p void *类型。
% 打印 % 号。
  • 可变参数函数
    printf函数一样,函数参数的数量和类型是可变的。使用...定义可变参数。
    头文件<stdarg.h>中提供了va_list类型用于声明一个**参数指针(ap)**变量;宏va_startap针初始化为指向第一个无名参数的指针,参数表必须至少包括一个有名参数(如:char *format),va_start将最后一个有名参数作为起点。
    每次调用va_arg,该函数将返回一个参数,并将ap指向下一个参数。va_arg使用一个类型名来决定返回的对象类型、指针移动的步长。最后,在函数返回之前调用va_end以完成一些必要的清理工作。

    使用示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    void myprintf(char *fmt,...){
    va_list ap;
    char *p, *sval;
    int ival;
    double dval;

    va_start(ap, fmt);
    for (p = fmt; *p; p++) {
    if (*p != '%') {
    putchar(*p);
    continue;
    }

    switch (*++p) {
    case 'd':
    ival = va_arg(ap, int);
    printf("%d", ival);
    break;
    case 'f':
    dval = va_arg(ap, double);
    printf("%f", dval);
    break;
    default:
    putchar(*p);
    break;
    }
    }
    }