【MOOC浙大翁恺】C语言学习笔记

第一章 C语言介绍

目录

第一章 C语言介绍

1.1 基本的程序框架

 1.2 printf

1.2.1 输出

1.2.2 计算

1.3 scanf

1.4 变量

1.4.1 变量定义的格式

1.4.2 变量的数据类型

1.4.3 变量的名称

 1.4.4 常量

1.5 表达式 

1.5.1 定义

1.5.2 运算符

1.6 注释

1.7 代码tips

 第二章 判断语句

2.1 判断

2.1.1 if语句

2.1.2 关系运算符

2.2 分支

2.2.1 if嵌套

2.2.2 级联的if-else if语句

2.2.3 switch-case多路分支

第三章 循环语句

3.1 三种循环

3.1.1 while循环

3.1.2 do-while循环

3.1.3 for循环

3.1.4 三种循环的用法

3.2 循环控制

3.2.1 break和continue

3.2.2 goto

3.3 循环应用

3.3.1 猜数游戏

3.2.2 整数的分解

3.2.3 整数逆序

3.2.4 分数求和(1)

 3.2.5 分数求和(2)

 3.2.6 正序分解整数

3.2.7 求最大公约数

第四章 数据类型

4.1 基本类型介绍

4.1.1 基本类型

4.1.2 不同类型之间的区别

4.1.3 sizeof

4.2 整数型

4.2.1 内存占据

4.2.2 整数在计算机内部的表达

4.2.3 整数的输入输出

4.2.4 选择整数类型

4.3 浮点数型

4.3.1 浮点类型

4.3.2 浮点数的输入输出

4.3.3 超过范围的浮点数(inf & nan)

4.3.4 浮点运算的精度

 4.3.5 浮点数的内部表达

 4.3.6 选择浮点类型

4.4 字符型

4.4.1 字符类型

4.4.2 字符的输入输出

4.4.3 字符计算

4.4.4 大小写转换

4.5 逻辑型

4.5.1 bool介绍

4.5.2 bool运算

4.6 逃逸字符

4.6.1 逃逸字符的类型

4.6.2 回退一格:\b

4.6.3 到下一个表格位:\t

 4.6.4 回车\r和换行\n

4.7 类型转换

4.7.1 自动类型转换

4.7.2 强制类型转换

4.8 逻辑运算

4.8.1 逻辑运算符介绍

 4.8.2 逻辑运算符的优先级

4.8.3 短路

4.9 条件运算

4.9.1 条件运算符

4.9.2 条件运算的优先级

4.10 逗号运算

4.10.1 逗号运算符

4.10.2 逗号运算的优先级

4.10.3 在for中使用

第五章 函数

5.1 函数的定义和使用

5.1.1 函数的定义

5.1.2 调用函数

5.1.3 从函数中返回值

5.1.4 没有返回值的函数

5.2 函数的参数与变量

5.2.1 函数原型

5.2.2 参数传递

5.2.3 本地变量(local varible)

5.2.4 函数的其他细节

第六章 数组

6.1 数组的定义和使用

6.1.1 数组的定义

6.1.2 数组的使用范例:投票统计

6.2 数组的运算

6.2.1 数组的集成初始化

6.2.2 遍历数组

6.2.3 查找数组中的元素

6.3 示例:判断素数

6.4 二维数组

6.4.1 二维数组介绍

6.4.2 二维数组的初始化

6.4.3 二维数组的遍历

第七章 指针

 7.1 指针介绍

7.1.1 取址运算符:&

7.1.2 指针

7.1.3 访问地址上的变量的运算符:*

7.2 指针的使用

7.2.1 交换两个变量的值

7.2.2 函数需要返回多个值

7.2.3 函数需要返回运算的状态,结果需要通过指针返回

7.2.4 指针最常见的错误

7.4 指针与数组

7.4.1 传入函数的数组

7.4.2 数组变量是特殊的指针

7.5 指针与const // C99 ONLY

7.5.1 指针是const

7.5.2 指针所指的是const

7.5.3 判断const的位置

7.5.4 转换

7.5.5 const数组

7.6 指针计算

7.6.1 指针计算介绍

7.6.2 指针运算

 7.6.3 指针的类型

7.6.4 指针的作用

7.7 动态内存分配

7.7.1 malloc函数

第八章 字符串

8.1 字符串介绍

8.1.1 字符串

8.1.2 字符串变量

8.1.3 字符串常量

8.2 字符串的使用

8.2.1 字符串输入与输出

8.2.2 常见错误

8.2.3 空字符串

8.2.4 字符串数组

8.2.5 程序参数

8.3 字符输入与输出函数

8.3.1 putchar

8.3.2 getchar

8.4 标准库中的字符串函数 string.h

8.4.1 strlen 求字符串长度

8.4.2 strcmp 比较字符串大小

8.4.3 strcpy 拷贝字符串

8.4.4 strcat 连接字符串

8.4.5 安全问题

8.4.6 strchr 字符串中找字符

8.4.7 strstr 字符串中找字符串

第九章 结构类型

9.1 enum 枚举

9.1.1 枚举介绍

9.1.2 枚举的使用

9.1.3 自动计数的枚举

9.1.4 枚举量

9.2 struct 结构

9.2.1 声明结构类型

9.2.2 声明结构的形式

9.2.3 结构变量

9.2.5 结构与函数

9.2.6 结构中的结构 

9.3 union 联合

 9.3.1 typedef 类型定义

9.3.2 union 联合


1.1 基本的程序框架

#include <stdio.h>

int main(){
    
//我们所写的内容

    return 0;
}

 1.2 printf

1.2.1 输出

printf("Hello World!\n");
  • printf 是一个函数
  • " " 里面的内容叫做字符串
  • \n 表示换行

1.2.2 计算

  1. 示例
    printf ("%d\n", 1+1);
  2. 其他四则运算
    C符号意义
    +

    -
    *
    /
    %取余
    ()括号

1.3 scanf

int a = 0;
int b = 0;
scanf ("%d %d", &a, &b);
  •  scanf 是一个函数
  • "%d" 表示要读入整数
  • 注意后面的开头必须写 &

1.4 变量

1.4.1 变量定义的格式

<变量的数据类型> <变量的名称>

1.4.2 变量的数据类型

  • 详见第四章
  1. int 整型
    scanf: %d
    printf: %d
    整数和整数的运算只会得到整数,若有小数部分会直接被舍弃。但当浮点数和整数放到一起运算时,C会把整数转换为浮点数,然后进行浮点数运算
    int a = 10;
    printf("%f", a/2.0*0.333);
  2. float 单精度浮点数型
    scanf: %f
    printf: %f
  3. double  双精度浮点数型
    scanf: %lf
    printf: %f

1.4.3 变量的名称

  • 变量的名称是一种标识符。标识符有其构成的规则,基本原则是:只能由字母、数字和下划线组成。数字不可以出现在第一个位置上,C语言的关键字不能作为标识符。1.4.2 变量的赋值
  • a=b:将右边的值交给左边的变量
    (注:不是a和b相等的意思)
  • 初始化:
    1. 当赋值发生在定义变量的时候,就是变量的初始化
    2. <类型名称> <变量名称> = <初始值>;
    3. 组合变量定义时,也可在这个定义中单独给单个变量赋值,如:
    int price = 0, amount = 100;

 1.4.4 常量

  • 定义常量
    const int a = 100;

     const是一个修饰符,用来给变量加上一个不变的属性。表示这个变量一旦被初始化,就不可再修改。

1.5 表达式 

1.5.1 定义

⼀个表达式是⼀系列运算符和算子的组合,用来计算⼀个值。

  • 运算符:进行运算的动作,比如加法运算符“+”,减法运算符 “-”
  • 算子:是指参与运算的值,这个值可能是常数,也可能是变量,还可能是⼀个方法的返回值

1.5.2 运算符

  1. 运算符的优先级

    单目运算符:只有⼀个算子的运算符。单目运算的+和-表示取整数或取负数 
  2. 复合赋值
    • 5个算术运算符“ + ”、“ - ”、“ * ”、“ / ”、“ % ”(加、减、乘、除、取余),可以和“ = ”结合起来,形成复合赋值运算符:“ += ”、“ -= ”、“ *= ”、“ /= ”、“ %= ”
    • total += 5;
    • total = total + 5;
    • 注意两个运算符中间不要有空格
  3. 递增递减运算符
    “ ++ ”和“ -- ”是单目运算符,算子必须是变量,作用是给这个变量+1或者-1
     • count++;
     • count += 1;
     • count = count + 1;
    前缀形式:++和--放在变量的前面
    后缀形式:++和--放在变量的后面
    区别:

    示例:
    int a = 0;
    
    //测试a++的情况
    a=10;
    printf ("a++ = %d", a++);
    printf ("a = %d", a);
    //此时a++为10,a为11
    
    //测试++a的情况
    a = 10;
    printf ("++a = %d", ++a);
    printf ("a = %d", a);
    //此时++a为11,a为11
    

1.6 注释

单行注释:// 需要注释的内容

多行注释:/* 需要注释的内容 */

1.7 代码tips

  1. 单一出口原则

 第二章 判断语句

2.1 判断

2.1.1 if语句

  • 普通形式
    if ( /*通过关系运算判定条件是否成立*/ ){
        //执行内容 该语句前缩进一个tab的位置
    }
  • 与else
    if ( /*通过关系运算判定条件是否成立*/ ){
        //执行内容
    } else { 
       //若不满足if内的条件 则执行此内容
    }
  •  去掉{ }
    if (/*判断条件*/)
        /*执行内容*/ ; //该句在此分号结束
    
    //else如下同理
    if (/*判断条件*/)
        /*执行内容*/ ;
    else
        /*执行内容*/ ;

2.1.2 关系运算符

  • 关系运算:计算两个值之间的关系
  • 关系运算符
    关系运算符意义
    ==相等
    !=不相等
    >大于
    <小于
    <=小于或等于
    >=大于或等于
  • 当两个值的关系符合关系运算符的预期时, 关系运算的结果为整数1,否则为整数0
    printf("%d\n", 5>3);
    //显示结果为1
    
    printf("%d\n", 5<3);
    //显示结果为0
  • 所有关系运算符的优先级低于算术运算符,但是高于赋值运算符;其中,==和!=的优先级低于其他关系运算符
    //算术运算符
    7 >= 3+4
    //计算右侧3+4的结果 再与左侧7作比较
    
    //赋值运算符
    int r = a>0
    //计算a>0的结果(1或0) 将值赋给r
    

2.2 分支

2.2.1 if嵌套

  1. 当if的条件满足或者不满足时,要执行的语句也是一条if或if-else语句,称为嵌套
  2. 其中,else一般和最近的if匹配,见写法2(除非前面的if语句有大括号,见写法3)
  3. 缩进格式不能暗示else的匹配(见写法4)
  4. 建议:即使只有一条语句,if和else后面也要用大括号;大括号内的语句缩进一个tab的位置
//写法1
if (code == READY){
    if (count < 20){
        printf("OK\n");
    }else{
        printf("NO\n");
   }
}

//写法2 (else和最近的if匹配)
if (code == READY)
    if (count < 20)
        printf("OK\n");
    else //此时这个else与上一个if匹配
        printf("NO\n");

//写法3 (else与大括号)
if (code == READY)
    if (count < 20){
        printf("OK\n");
    }else //此时这个else与最外面的if匹配
        printf("NO\n"); 

//写法4(else缩进)
if (code == READY)
    if (count < 20)
        printf("OK\n");
else //此时这个else依然与上一个if匹配,不会因为缩进就和最外面的if匹配
    printf("NO\n");
   

2.2.2 级联的if-else if语句

  • 形式
if (/*exp1*/){
    /*st1*/ ;
}else if (/*exp2*/){
    /*st2*/ ;
}else{
    /*st3*/ ;
}

2.2.3 switch-case多路分支

switch (/*控制表达式*/){
case /*常量*/:
    /*语句*/;
    break; //根据情况考虑是否加入break,一般都要加
    ……
case /*常量*/:
    /*语句*/;
    break;
default:
    /*语句*/;
    break;
    ……
}

//示例
int type;
scanf ("%d", &type);

switch ( type ) {
case 1: 
    printf("你好");
    break;
case 2:
    printf("早上好");
    break;
case 3:
    printf("晚上好");
    break;
case 4:
    printf("再⻅");
    break;
default:
    printf("啊,什么啊?");
    break;
}
  • 控制表达式只能是整数型的结果
  • 常量可以是常数,也可以是常数计算的表达式
  • 根据表达式的结果,寻找匹配的case,并执行case后面的语句,一直到break为止
  • 如果所有的case都不匹配,那么就执行default后面的语句
    如果没有default,那么就什么都不做
  • switch语句可以看作是⼀种基于计算的跳转,计算控制表达式的值后,程序会跳转到相匹配的case(分支标号)处。 分支标号只是说明switch内部位置的路标,在执行完分支中的最后一条语句后, 如果后面没有break,就会顺序执行到下面case中的内容去(即使控制表达式与下一个case的常量不匹配),直至遇到break,否则会到switch结束为止

第三章 循环语句

3.1 三种循环

3.1.1 while循环

  • 基本形式

    while (/*循环条件*/){
        /*循环体语句*/;
    }
  • 如果把while翻译作“当”,那么一个while循环的意思就是:当条件满足时,不断地重复循环体内的语句

  • 循环执行之前判断是否继续循环,所以有可能循环⼀次也没有被执行

  • 条件成立是循环继续的条件

3.1.2 do-while循环

  • 基本形式
    do 
    {
        /*循环体语句*/;
    } while (/*循环条件*/);
  • 在进入循环的时候不做检查,而是在执行完⼀轮循环体的代码之后,再来检查循环的条件是否满足,如果满足则继续下⼀轮循环,不满足则结束循环
  • 与while循环的区别
    do-while循环是在循环体语句执行结束时才判断条件。也就是说,无论如何,循环体语句都会执行少一遍,然后再来判断条件

3.1.3 for循环

  • 基本形式
    for (/*初始动作*/; /*条件*/; /*每轮的动作*/) {
        //循环体语句
    }
    
    //示例
    for (i=0; i<100; i++){
        printf("%d", i);
    }
    //读作:对于⼀开始的i=0,当i<100时,重复做循环体,每⼀轮循环在做完循环体内语句后,使得i++
  • for中的每一个表达式都是可以省略的
    //例
    for (; /*条件*/; ) == while (/*条件*/)
  • for循环像⼀个计数循环:设定一个计数器,初始化它,然后在计数器到达某值之前,重复执行循环体,而每执行一轮循环,计数器值以一定步进进行调整,比如加1或者减1
  • 小技巧:
    做求和的程序时,记录结果的变量应该初始化为0,而做求积的变量时,记录结果的变量应该初始化为1
  • 循环次数:
    for (i=0; i<n; i++)
    在上例中,循环的次数是n,而循环结束以后,i的值也是n
    循环的控制变量i从0开始或是从1开始,判断i<n或是i<=n,对循环的次数和循环结束后变量的值都有影响

3.1.4 三种循环的用法

  • 如果有固定次数,用for循环
  • 如果必须执行一次,用do-while循环
  • 其他情况用while循环

3.2 循环控制

3.2.1 break和continue

  • break:跳出循环
  • continue:跳过本轮循环剩下的语句,进入下一轮
  • 在循环嵌套中,break和continue只能对其所在的那一层循环做
  • 接力break:
    在循环嵌套中使用break时,如果想用break跳出所有循环,可以使用接力break的方法
    //接力break示例
    int x;
    int one, two, five;
    int exit = 0; //用于判断接力break
    
    scanf("%d", &x);
    for (one=1; one < x*10; one++){
        for (two=1; two < x*10/2; two++){
            for (five=1; five < x*10/5; five++){
                if (one + two*2 + five*5 == x*10){
                    printf("用%d个1角加%d个2角加%d个五角得到%d元\n", one, two, five, x);
                    exit = 1; //用于执行接力break
                    break; //跳出最内层循环
                }
            }
            if (exit == 1) break; //利用接力break跳出第二层循环
        }
        if (exit == 1) break; //利用接力break跳出最外层循环
    }

3.2.2 goto

格式:

goto /*自己设立的标号*/; //开始跳转

/*自己设立的标号*/: /*跳转的位置*/ //利用自己设立的标号跳转到想要的位置

//示例
goto out;

out: 

可以在需要离开整个循环的位置放一个goto语句代替上述提及的接力break

//goto示例
int x;
int one, two, five;

scanf("%d", &x);
for (one=1; one < x*10; one++){
    for (two=1; two < x*10/2; two++){
        for (five=1; five < x*10/5; five++){
            if (one + two*2 + five*5 == x*10){
                printf("用%d个1角加%d个2角加%d个五角得到%d元\n", one, two, five, x);
                goto out; //goto语句
            }
        }
    }
}
out: //直接从循环内跳到此处
return 0;

3.3 循环应用

3.3.1 猜数游戏

题目:让计算机来想⼀个数,然后让用户来猜,用户每输入一个数,就告诉它是大了还是小了,直到用户猜中为止,最后还要告诉用户它猜了多少次。

代码实现:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(){
    srand(time(0));
    int number = rand()%100 + 1;
    // 上述语句中,rand()表示生成随机整数;x%n的结果是[0, n-1]的⼀个整数
    int count = 0;
    int a = 0;
    printf("我已经想好了一个1-100之间的数\n");
    do{
        printf("请猜这个1到100之间数:\n");
        scanf("%d", &a);
        count++;
        if ( a > number ){
            printf("你猜的数大了\n");
        }else if ( a < number ){
            printf("你猜的数小了\n");
        }
    }while (a != number);
    printf("太好了,你用了%d次就猜到了答案\n", count);

    return 0;
}
  • rand()表示生成随机整数
  • x%n的结果是[0, n-1]的一个整数

3.2.2 整数的分解

  • 对⼀个整数做%10的操作,就得到它的个位数
  • 对⼀个整数做/10的操作,就去掉了它的个位数
  • 然后再对第二步的结果做%10,就得到原来数的十位数了
  • 之后依此类推

3.2.3 整数逆序

要求:输入⼀个正整数,输出逆序的数(注意对输出0的要求

情况一:当有0时,不保留0。例如700,输出7。

#include <stdio.h>

int main(){
    int x;
    scanf("%d", &x);
    int digit; //表示每一位的数字
    int ret = 0; //存放结果 
    while(x>0){
    	digit = x%10;
    	x /= 10;
    	ret = ret*10 + digit;
    }
    printf("%d", ret);	

    return 0;
}

情况二:当有0时,保留0。例如700,输出007。

#include <stdio.h>

int main(){
    int x;
    scanf("%d", &x);
    int digit; //表示每一位的数字
    while(x>0){
    	digit = x%10;
    	x /= 10;
    	printf("%d", digit);	
    }

    return 0;
}

3.2.4 分数求和(1)

题目:

 代码实现:

#include <stdio.h>

int main(){
    int n;
    int i;
    double ret = 0.0;

    scanf("%d", &n);
    for (i=1; i<=n; i++){
    	ret += 1.0/i; //因为结果是浮点数,而i是整型,所以要写1.0来进行转换 
    }
    printf("%f\n", ret); 

    return 0;
}

 3.2.5 分数求和(2)

题目:

 代码实现:

#include <stdio.h>

int main(){
    int n;
    int i;
    double ret = 0.0;
    int sign = 1;

    scanf("%d", &n);
    for (i=1; i<=n; i++){
    	ret += 1.0*sign/i; //因为结果是浮点数,而i是整型,所以要写1.0来进行转换
    	sign = -sign; 
    }
    printf("%f\n", ret); 

    return 0;
}

 3.2.6 正序分解整数

题目:输入⼀个非负整数,正序输出它的每一位数字

代码实现:

#include <stdio.h>

int main(){
    int x;
    scanf("%d", &x);

    int mask = 1;
    int t = x;
    while(t>=10){
    	t /= 10;
    	mask *= 10;
    }
    do{
    	int d = x / mask;
    	printf("%d", d);
    	if (mask >= 10){
    		printf(" ");
    	}
    	x %= mask;
    	mask /= 10;
    }while (mask>0);
    printf("\n");

    return 0;
}

3.2.7 求最大公约数

题目:输入两个数a和b,输出它们的最大公约数

  • 辗转相除法
    1. 如果b等于0,计算结束,a就是最大公约数
    2. 否则,计算a除以b的余数,让a等于b,而b等于那个余数
    3. 回到第一步

代码实现:

#include <stdio.h>

int main(){
    int a,b;
    int t;

    scanf("%d %d", &a, &b);
    while(b != 0){
    	t = a%b;
    	a = b;
    	b = t;
    }
    printf("%d\n",a);

    return 0;
}

第四章 数据类型

4.1 基本类型介绍

4.1.1 基本类型

  • 整数型
    char、short、int、long、long long
  • 浮点数型
    float、double、
    long double
  • 逻辑(可以放到整数型中)
    bool
  • 指针
  • 自定义类型

注:紫色表示C99的类型

4.1.2 不同类型之间的区别

  • 类型名称:int、long、double
  • 输入输出的格式:%d、%ld、%lf
  • 表达的数的范围:char < short < int < float < double
  • 内存中所占据的大小:1个字节到16个字节
  • 内存中的表达形式:二进制数(补码)、编码

4.1.3 sizeof

  • 是⼀个运算符,给出某个类型或变量在内存中所占据的字节数
  • 是静态运算符,它的结果在编译时刻就决定了
  • 不要在sizeof的括号里做运算,这些运算不会做的

4.2 整数型

4.2.1 内存占据

  • char:1字节(8比特)
  • short:2字节
  • int:取决于编译器(CPU),表达计算机的字长,也就是用来表达寄存器宽度的
  • long:取决于编译器(CPU),表达计算机的字长
  • long long:8字节

 计算机的字长:

CPU与内存RAM之间有一个总线,而在CPU中有寄存器,计算机的字长指这个寄存器的宽度,换言之,是指这个寄存器是多少比特的。同时也是在说,在CPU和RAM之间传递数据的时候,每一次所传的数据大小是计算机的字长(目前常见的是32位或者64位的系统)。

4.2.2 整数在计算机内部的表达

  • 计算机内部一切都是二进制的表达

① 补码:计算机如何表达负数

  • 拿补码和原码相加可以得到一个溢出的“零”。
  • 实际上,从数学来看,负数意味着某个数与它的负数相加为零,即a+(-a)=0。
  • 用补码思想体现的话,以一个字节(8位)为例,二进制数11111111与00000001相加,可以得到100000000,由于是八位,首位的1会被舍去,即是(1)00000000,括号里的1去掉,则得到00000000,即溢出的“零”。
  • 因此,00000001的补码就是11111111。在11111111被当作纯二进制看待时,是十进制的255,被当作补码看待时则是-1。
  • 同理,对于-a,其补码就是0-a,实际是2^n-a,n是这种类型的位数

② 整数的范围

  • 数的范围
    对于一个字节(8位),可以表达的二进制数有00000000至11111111
    其中,00000000表示十进制的0
    11111111至10000000表示十进制的-1至128
    00000001至01111111表示十进制的1至127
  • 整数的范围
    1、char:1字节(8-bit):能表达的范围是-128到127
    2、short:2字节(16-bit):能表达的范围是-32768到32767
    3、int:取决于编译器(CPU),如32位,则是-2^(32-1)到2^(32-1)-1
    4、long:取决于编译器(CPU)
    5、long long:8字节

③ unsigned

  • 如果一个整数型不以补码的形式表示负数,它没有负数,只有0和正整数部分,就是unsigned。它可以使表达的范围在正数部分扩大一倍再加1,但不能表达负数了。
    例如在char当中,如果char a = 255(char是8位),在计算机内部会转换为11111111,即实际上这种情况下,a的值其实是-1,而不是255。但如果是unsigned char a = 255,由于取消了补码的形式,a就是255。原本char表达的范围是-128到127,在unsigned char下就会变成0到255(即127*2+1)。
  • 如果⼀个字面量常数想要表达的是unsigned,可以在后面加u或U,如255U。
  • 加l或L可以表示字面量是long (long)
  • *unsigned的初衷并非是扩展数能表达的范围,而是为了做纯二进制运算,主要是为了移位

④ 整数越界

整数是以纯二进制进行计算的,因此:

  • 11111111 + 1 —> 100000000 —> 0
  • 01111111 + 1 —> 10000000 —> -128
  • 10000000 - 1 —> 01111111 —> 127

4.2.3 整数的输入输出

  • 整数的输入输出(scanf、printf)
    %d:int、char、short
    %ld:long long
    %u:unsigned
    %lu:unsigned long long
  • 8进制和16进制
    1、8进制:一个以0开始的数字,其字面量是8进制
         16进制:一个以0x开始的数字,其字面量是16进制
    2、%d表示我们想要输入输出的是十进制数,如果我们想要输入输出8进制,则用%o,想要16进制则用%x。
    3、8进制和16进制只是如何把数字表达为字符串,与计算机内部如何表达数字无关。实际上,计算机内部只有二进制,我们写代码输入十进制数后,编译器会帮我们转换为二进制,8进制和16进制也是同理。
    4、16进制很适合表达二进制数据,因为4位二进制正好是一个16进制。
    例如二进制的00010010表达为16进制时,前四位0001转换为16进制是1,后四位0010转换为16进制是2,刚好00010010转换为16进制就是12(在0001010转换为十进制是17,0x12转换为十进制也是17)。因此现在程序员喜欢用16进制做一些二进制的工作,比较方便转换。
    5、8进制的一位数字正好表达3位二进制(因为早期计算机的字长是12的倍数,而非8,那时候喜欢用8进制)。

4.2.4 选择整数类型

为了准确表达内存和做底层程序的需要,C语言中整数有很多种类型。但如今,若没有特殊需要(例如做底层面对硬件时),就选择int。由于现在的CPU的字长普遍是32位或64位,一次内存读写就是一个int,一次计算也是⼀个int,选择更短的类型不会更快,甚至可能更慢。此外,现代的编译器⼀般会设计内存对齐,所以更短的类型实际在内存中有可能也占据⼀个int的大小(虽然sizeof告诉你更小),而unsigned与否只是输出的结果不同,内部计算是一样的。

4.3 浮点数型

4.3.1 浮点类型

类型字长范围有效数字
float32±(1.20*10^-38 ~ 3.40*10^38), 0, ±inf,nan7
double64±(2.2*10^-308 ~ 1.79*10^308), 0, ±inf,nan15
  • inf表示无穷大
  • nan表示非有效数字
  • 有效数字表示只有x个数字是有效的,多出来的数字就不准确了

4.3.2 浮点数的输入输出

类型scanfprint
float%f%f, %e
double%lf%f, %e
  • 科学计数法(%e)
  • 输出精度
    在%和f之间加上.n可以指定输出小数点后n位,这样的输出是做4舍5入的。
    printf("%.3f\n", -0.0049);
    //输出-0.005
    
    printf("%.30f\n", -0.0049);
    //输出0.004899999999999999841793218991
    /*输出上述是因为计算机内部实际无法精确表达-0.0049,
    因为数学上数是连续的,但计算机内部是离散的。
    这个数和实际的0.0049的距离其实就是浮点数的误差*/
    
    printf("%.3f\n", -0.00049);
    //输出-0.000

    4.3.3 超过范围的浮点数(inf & nan)

  • printf输出inf表示超过范围的浮点数:±∞
  • printf输出nan表示不存在的浮点数
  • 示例
    printf("%f\n", 12.0/0.0);
    //输出inf
    
    printf("%f\n", -12.0/0.0);
    //输出-inf
    
    printf("%f\n", 0/0);
    //输出nan

4.3.4 浮点运算的精度

  • 浮点数的运算实际是没有精度的。
    两个浮点数直接用“==”来判断是否相等可能会失败,所以应该求两个浮点数之间的差是否小于一个很小的数。
    通常是求两数之差的绝对值,即fabs(f1-f2) < 1e^-8或者1e^-12
  • 带小数点的字面量是double而非float,float需要用f或者F后缀来表明身份
    float a, b;
    a = 1.169f;
    b = 1.573f;

 4.3.5 浮点数的内部表达

  • 浮点数在计算时是由专用的硬件部件实现的
  • 计算double和float所用的部件是⼀样的

 4.3.6 选择浮点类型

  • 如果没有特殊需要,只使用double
  • 现代CPU能直接对double做硬件运算,性能不会比float差,在64位的机器上,数据存储的速度也不比float慢

4.4 字符型

4.4.1 字符类型

char是⼀种整数,也是⼀种特殊的类型:字符。这是因为:

  1. 我们可以用单引号来表示的字符字面量,我们把单引号括起来的一个部分叫做一个字符。
    例如'a'和'1'也是字符(''也是一个字符)
  2. 我们可以在printf和scanf里用%c来输入和输出字符

4.4.2 字符的输入输出

  • 如何输入'1'这个字符给char c?
    见下例
    '1'的ASCII编码是49,所以当c==49时,它代表'1'
    例1:
    char c;
    scanf("%c", &c); //输入1
    printf ("%d\n", c);
    //输出49
    prinf ("%c/n", c);
    //输出1
    
    例2:
    char c;
    int i;
    scanf("%d", &i); //输入49
    c = i;
    printf ("%d\n", c);
    //输出49
    prinf ("%c/n", c);
    //输出1
    
  • 混合输入
    scanf("%d %c", &i, &c);//有空格
    //在输入过程中间加空格的前提下,可以正确读到两个数
    
    scanf("%d%c", &i, &c);//无空格
    //在输入过程中间加空格的前提下,字符会直接读空格

4.4.3 字符计算

  • 一个字符加一个数字得到ASCII码表中那个数之后的字符
    char c = 'A';
    c+=1;
    printf("%c\n", c);
    //输出B
  • 两个字符相减,得到它们在表中的距离
    int i = 'Z' - 'A';
    printf("%d\n", i);
    //输出25

4.4.4 大小写转换

  • 字母在ASCII表中是顺序排列的
  • 大写字母和小写字母是分开排列的,并不在一起
  • 'a'-'A'可以得到两段之间的距离,因此:
    a+'a'-'A'可以把⼀个大写字母变成小写字母
    a+'A'-'a'可以把一个小写字母变成大写字母

4.5 逻辑型

4.5.1 bool介绍

  • #include <stdbool.h>
  • 之后就可以使bool和true、false
    #include <stdio.h>
    #include <stdbool.h>
     
    int main()
    {
        bool a = true;
        bool b = false;
        bool c = 1 > 0;
        bool d = 1 < 0;
    
        printf("%d %d %d %d", a, b, c, d);
     
        return 0;
    }
    
    //输出:1 0 1 0

4.5.2 bool运算

  • bool实际上还是以int的手段实现的,所以可以当作int来计算,也只能当作int来输入输出
  • 任何非零值都将被视为真实值

4.6 逃逸字符

4.6.1 逃逸字符的类型

  • 用来表达无法印出来的控制字符或特殊字符,它由⼀个反斜杠“\”开头,后面跟上另一个字符,这两个字符合起来,组成了一个字符
字符意义字符意义
\b回退一格

\"

双引号
\t到下一个表格位

\'

单引号
\n换行\\反斜杠本身
\r回车

4.6.2 回退一格:\b

让下一个输出的东西回到前一个的位置,(例1);但如果回退一格的后面不输出东西,那就什么也没有了(例2)。\b通常做的事情是回去但不删除,但部分终端软件可能会把\b解释为删除,看具体的软件。

//例1
printf("123\bA\n456\n");
/*输出样式为:
12A
456 */

//例2
printf("123\b\n456\n");
/*输出样式为:
123
456 */

4.6.3 到下一个表格位:\t

制表位是行当中某些固定的位置(参考tab键),\t的作用是到一行中具体的某个位置上,而不是固定数量的字符。

  • 每行的固定位置
  • 一个\t使得输出从下一个制表位开始
  • 用\t才能使得上下两行对齐
printf("123\t456\n");
printf("12\t456\n");
/*输出样式为:
123 456
12  456  */

 4.6.4 回车\r和换行\n

回车和换行起源于早期的打字机,回车是把输出的位置调到行首,换行是直接从当前位置换到下一行。
但是现在的shell一般会直接把\n翻译为要做回车和换行这两个动作。

4.7 类型转换

4.7.1 自动类型转换

  • 当运算符的两边出现不一致的类型时,会自动转换成较大的类型,大的意思是能表达的数的范围更大
  • char => short => int => long => long long
    在整数中,char会转换为short,short转换为int,以此类推
  • int => float => double
    当整数遇到浮点数,int会转换为float,而两种浮点数相遇,float会转换为double
  • printf:任何小于int的类型会被转换成int;float会被自动转换成double
  • scanf:需要明确知道后面变量的大小。例如要输入short,需要%hd

4.7.2 强制类型转换

  • 把一个量强制转换成另一个类型(通常是更大的数的类型转换为小的类型)
  • 格式:(类型名称) 值;
    (int) 10.2;
  • 注意这时候的安全性,小的变量不总能表达大的量,例如:

    (short) 32768;
    //这时候已经超过了short能表达最大范围,输出结果可能会是-32768
  • 强制类型转换只是从那个变量计算出了⼀个新的类型的值,并不改变那个变量,无论是值还是类型都不改变

    int i = 32768;
    short s = (short) i;
    printf = ("%d\n", i);
    //此时输出的结果仍然是32768,i的类型和值并没有变化
  • 强制类型转换的优先级高于四则运算

    double a = 1.0;
    double b = 2.0;
    int i = (int) a / b;
    /*此时是把a转换为int然后除以b
      由于b是double,这时候运算的结果仍然是double
      然后把这个double的值赋给i */
    
    int i = (int) (a / b)
    //这时候才是把a/b的值转换为了int

4.8 逻辑运算

4.8.1 逻辑运算符介绍

  • 逻辑运算是对逻辑量进行的运算,结果只有0或1
  • 逻辑量是关系运算或逻辑运算的结果

示例:

/*例1:如果要表达数学中的区间,如:x ∈(4,6),应该如何写C的表达式?*/

//错误写法
4 < x < 6; // 因为4<x的结果是一个逻辑值(0或1)

//正确写法
x > 4 && x < 6; 


/*例2:如何判断⼀个字符c是否是⼤写字⺟?*/

//正确写法
c >= 'A' && c<= 'Z';

 4.8.2 逻辑运算符的优先级

  • 顺序:! > && > ||
  • 示例:!age<20
    先算!age,若age是0,!age为1;若age非零,!age为0
    因此0或1和20作比较,!age<20始终是1
    如果想表达不是age<20,应该写!(age<20)

 

4.8.3 短路

  • 逻辑运算是自左向右进行的,如果左边的结果已经能够决定结果了,就不会做右边的计算
  • 对于&&,左边是false时就不做右边了
  • 对于||,左边是true时就不做右边了
  • 不要把赋值,包括复合赋值组合进表达式

4.9 条件运算

4.9.1 条件运算符

  • ?前面是条件
  • ?后面是条件满足时的值
  • :后面是条件不满足时的值
  • 示例
    int count;
    count = (count>20) ? count-10 : count+10;
    
    //等价于如下
    if (count>20){
        count = count-10;
    }else{
        count = count+10;
    }
    

4.9.2 条件运算的优先级

  • 条件运算符的优先级高于赋值运算符,但是低于其他运算符
  • 条件运算符是自右向左结合的
  • 嵌套条件表达式
    int count;
    count = (count > 20) ? (cout < 50) ? count - 10 : count - 5 : (count < 10 ) ? count + 10 : count + 5;
    会使程序阅读太复杂太困难,不建议使用

4.10 逗号运算

4.10.1 逗号运算符

逗号用来连接两个表达式,并以其右边的表达式的值作为它的结果

4.10.2 逗号运算的优先级

  • 所有的运算符中最低的,所以它两边的表达式会先计算
  • 逗号的组合关系是自左向右,所以左边的表达式会先计算
  • 右边的表达式的值会留下来作为逗号运算的结果
int i = 3+4, 5+6;
//i的结果会是7,因为逗号运算符优先级最低,会先做i=3+4

int i = (3+4, 5+6);
//i的结果会是11,因为括号的优先级更高,会先算括号里的表达式,而逗号表达式的结果是逗号右边的值

4.10.3 在for中使用

示例:

for (i=0, j=10; i<j; i++, j--)
//表示为i赋值0,j赋值10,之后每一轮i++,j--

第五章 函数

5.1 函数的定义和使用

5.1.1 函数的定义

  • 函数是一块代码,接收零个或多个参数,做一件事情,并返回零个或一个值

5.1.2 调用函数

  • 函数名(参数值);
  • ()起到了表示函数调用的重要作用
  • 即使没有参数也需要()
  • 如果有参数,则需要给出正确的数量和顺序
  • 这些值会被按照顺序依次用来初始化函数中的参数

5.1.3 从函数中返回值

① return介绍

  • return语句做两件事:1、停止函数的执行;2、送回一个值
  • return语句两种写法:1、return;(后面什么也不加);2、return 表达式;
  • 一个函数里可以出现多个return语句(但不推荐这样写,因为不符合单一出口的原则)
  • 如果函数有返回值,必须使用带值的return
    #include <stdio.h>
    
    int max(int a, int b)
    {
        int ret;
        if (a > b){
        ret = a;
        }else{
        ret = b;
        }
        return ret;
    }
    
    int main(){
        int a, b;
        scanf("%d %d", &a, &b);
        printf("%d\n", max(a,b));
    
        return 0;
    }

② 返回值的作用

  • 可以赋值给变量
  • 可以再传递给函数
  • 也可以直接丢弃,因为有的时候要的是函数执行过程中的一些其他作用,不一定非要返回值

5.1.4 没有返回值的函数

  • void 函数名(参数表)
  • 不能使用带值的return,可以没有return
  • 调用的时候不能做返回值的赋值
    void sum(int begin, int end)
    {
        int i;
        int sum = 0;
        for (i=begin; i<=end; i++){
            sum += 1;
        }
        printf("%d到%d的和是%d\n", begin, end, sum);
    }
    
    //在本例中,求和函数sum不需要返回值,我们只需要它执行printf

5.2 函数的参数与变量

5.2.1 函数原型

① 函数的先后关系

  • 像这样把sum()写在上面,是因为C的编译器是自上而下的顺序分析代码
  • 在看到下面的sum(1,10)的时候,它需要知道 sum()的样子
  • 也就是sum()要几个参数,每个参数的类型如何,返回什么类型
  • 这样它才能检查你对sum()的调用是否正确
  • 如果把上面的部分整体写到下面,旧标准会先假设你所调用的函数的参数都是int,返回值也是int,但如果你实际在下面写的不是int,那就会产生error

② 函数的原型/声明

5.2.2 参数传递

① 调用函数

如果函数有参数,调用函数时必须传递给它数量和类型都正确的值

可以传递给函数的值是表达式的结果,这包括:

  • 字面量
  • 变量
  • 函数的返回值
  • 计算的结果

如果给的值和参数类型不匹配调用函数时给的值与参数的类型不匹配是C语言传统上最大的漏洞,C的编译器总是悄悄替你把类型转换好,但是这很可能不是你所期望的 ,在后续的语言,C++/Java在这方面很严格

② 传值

C语言在调用函数时,永远只能传值给函数

#include<stdio.h>

void swap (int a, int b);

int main(){
    int a = 5;
    int b = 6;
   
    swap (a, b);
    printf("a=%d, b=%d\n", a, b);

    return 0;
}

void swap (int a, int b){
    int t = a;
    a = b;
    b = t;
}

//这样的代码并不能交换a和b的值
  • 每个函数有自己的变量空间,参数也位于这个独立的空间中,和其他函数没有关系
  • 过去,对于函数参数表中的参数,叫做“形式参数”,调用函数时给的值,叫做“实际参数”
  • 由于容易让初学者误会实际参数就是实际在函数中进行计算的参数,误会调用函数的时候把变量而不是值传进去了,所以我们不建议继续用这种古老的方式来称呼它们
  • 我们认为,它们是参数和值的关系

5.2.3 本地变量(local varible)

① 本地变量介绍

  • 函数的每次运行,就产生了一个独立的变量空间,在这个空间中的变量,是函数的这次运行所独有的,称作本地变量
  • 定义在函数内部的变量就是本地变量
  • 参数也是本地变量

② 变量的生存期和作用域

  • 变量的生存期:从变量出现到变量消亡
  • 作用域:在(代码的)什么范围内可以访问这个变量 (这个变量可以起作用)
  • 对于本地变量而言,它的生存期和作用域都在大括号{ }内,我们把这个部分称为“块”

③ 本地变量的规则

  • 本地变量是定义在块内的:它可以是定义在函数的块内,也可以定义在语句的块内,甚至可以随便设立一对大括号来定义变量
  • 程序运行进入这个块之前,其中的变量不存在,离开这个块,其中的变量就消失了
  • 块外面定义的变量在里面仍然有效
  • 块里面若定义了和外面同名的变量,则在块内会掩盖外面的
  • 不能在同一个块内定义多个同名的变量
  • 本地变量不会被默认初始化
  • 参数在进⼊函数的时候被初始化了

5.2.4 函数的其他细节

① 当函数没有参数时

  • void f(void):括号内写void,即明确告诉编译器该函数不接收任何参数
  • void f( ):括号内什么都不写的情况,在传统C中,它表表示f函数的参数表未知,并不表示没有参数

② 逗号运算符

  • 调用函数时,圆括号内的逗号是标点符号,不是运算符
  • 调用函数时的逗号和逗号运算符的区分:
    f(a,b):标点符号
    f((a,b)):逗号运算符

③  C语言不允许函数嵌套定义

我们可以在一个函数里放另一个函数的声明,但不能放另一个函数的定义(body)

④ 其他写法(不建议这样写)

  • int i,j,sum(int a, int b);
    定义i和j,声明sum函数
  • return (i);
    这个圆括号没有什么意义,可以这样写,但可能会让人误解return是个函数

⑤ main函数

  • int main()也是一个函数,如果你觉得main不要任何参数,可以写成int main(void)
  • return的0是有意义的,会返回给调用main函数的地方,来检查main并报告给操作系统
  • 传统上,如果一个应用程序如果返回0,表示正常运行结束;返回非0则表示出现错误

第六章 数组

6.1 数组的定义和使用

6.1.1 数组的定义

① 基本定义

  • <类型> 变量名称[元素数量];,例如:
    int grades[100]; //说明gardes里面每一个单元都是int,它有100个这样的int
    double weight[20]; //说明gardes里面每一个单元都是double,它有20个这样的double
  • 元素数量必须是整数
  • C99之前,元素数量必须是编译时刻确定的字面量。但从C99开始就可以是变量了

② 数组的特点

  •  其中所有的元素具有相同的数据类型
  • 一旦创建,不能改变数组的大小
  • 数组中的元素在内存中是连续依次排列的

③ 示例:int a[10];

  • 一个int的数组
  • 10个单元:a[0],a[1],…,a[9]
  • 连续排列
  • 每个单元就是一个int类型的变量
  • 每个单元都可以出现在赋值的左边或右边
  • 在赋值左边的叫做左值

④ 数组的单元

  • 数组的每个单元就是数组类型的一个变量
  • 使用数组时放在[]中的数字叫做下标或索引,下标从0开始计数,数组最大的下标是数组的个数减1

⑤ 有效的下标范围

  • 编译器和运行环境都不会检查数组下标是否越界,无论是对数组单元做读还是写
  • 一旦程序运行,越界的数组访问可能造成问题,导致程序崩溃(segmentation fault),但是也可能运气好,没造成严重的后果
  • 所以这是程序员的责任来保证程序只使用有效的下标值:[0, 数组的大小-1]

⑥ 长度为0的数组

  • int a[0];
  • 可以存在,但是无用

6.1.2 数组的使用范例:投票统计

题目:写一个程序,输入数量不确定的[0,9]范围内的整数,统计每一种数字出现的次数, 并输入-1表示结束

const int num = 10; //决定数组的大小 
	
int x; 
int cnt[num]; //定义数组 
int i;
	
for (i=0; i<num; i++){
	cnt[i] = 0;
} //初始化数组 
	 
scanf("%d", &x);
while ( x != -1){
	if (x>=0 && x<=9){
		cnt[x]++; //数组参与运算 
	}
	scanf("%d", &x);
}

for (i=0; i<num; i++){
	printf("%d出现了%d次\n", i, cnt[i]);
}

6.2 数组的运算

6.2.1 数组的集成初始化

① 集成初始化的形式

int a[] = {6, 3, 7, 23, 67, 12, 1, 2, 15, 4};
  • 直接用大括号给出数组的所有元素的初始值
  • 不需要给出数组的大小,编译器会替你数数
int a[10] = {2}; //第一个元素是2,后面的都是0
int a[10] = {0}; //第一个元素和后面的都是0
  • 如果给出了数组的大小,但是后面的初始值数量不足, 则其后的元素被初始化为0
  • 可以用来对数组进行初始化

② 集成初始化时的定位

int a[10] = {[0] = 2, [4] = 3, 6}; //C99 ONLY

int i;
for (i=0; i<10; i++){
    printf("%d\t", a[i]);
} 
//输出为:2  0  0  0  3  6  0  0  0  0

  • 在大括号内用[n]在初始化数据中给出定位
  • 没有定位的数据接在前面位置的后面,其他位置的值补零
  • 可以不给出数组大小,让编译器算,数组下标的最大值是大括号内具体给出的定位的值
  • 特别适合初始数据稀疏的数组

③ 数组的大小

sizeof(a)/sizeof(a[0])

/*  sizeof(a)计算整个数组的大小,sizeof(a[0]计算单个元素的大小
    相除就可以得到数组内元素的个数 */
  • sizeof给出整个数组所占据的内容的大小,单位是字节
  • sizeof(a[0])给出数组中单个元素的大小,于是相除就得到了数组的单元个数
  • 这样的代码,一旦修改数组中初始的数据,不需要修改遍历的代码

6.2.2 遍历数组

① 数组的赋值

int a[] = {2, 3, 4, 5, 6};
int b[] = a;
//这样并不能将数组a的值赋给数组b
  •  数组变量本身不能被赋值
  • 要把一个数组的所有元素交给另一个数组,必须采用遍历

② 数组的遍历赋值

for (i=0; i<length; i++){
    b[i] = a[i];
}
  • 通常都是使用for循环,让循环变量i从0到小于数组的长度,这样循环体内最大的i正好是数组最大的有效下标
  • 常见错误是:
    循环结束条件是<=数组长度
    离开循环后,继续用i的值来做数组元素的下标

6.2.3 查找数组中的元素

#include <stdio.h>

int search(int key, int a[], int length); 

int main()
{
	int a[] = {2, 4, 6, 7, 1, 3, 5, 9, 11, 12, 23};
	int key; //我们需要查找的数
	int loc;
	printf("输入一个待查找的数字:");
	scanf("%d", &key);
	loc = search(key, a, sizeof(a)/sizeof(a[0]));
	if (loc != -1){
		printf ("%d在第%d个位置上\n", key, loc);
	}else{
		printf("%d不存在\n", key);
	}

    return 0;
}

int search(int key, int a[], int length)
{
	int ret = -1;
	int i;
	for (i=0; i < length; i++){
		if (a[i] == key){
			ret = i;
			break;
		}
	}
	return ret;
}

数组作为函数的参数时: 

  • 不能在[]中给出数组的大小,是没有意义的
  • 不能再利用sizeof来计算数组的元素个数,往往必须再用另一个参数来传入数组的大小

6.3 示例:判断素数

思路:

  • 构造n以内的素数表
  • 令x为2
  • 将2x、3x、4x直至ax<n的数标记为非素数
  • 令x为下一个没有被标记为非素数的数,重复上一步,直到所有的数都已经尝试完毕

伪代码:

  • 开辟isPrime[n],初始化其所有元素为1,isPrime[x]为1表示x是素数,prime[x]为0则表示不是素数
  • 令x=2,如果x是素数,则对于(i=2; x*i<n; i++),即所有的x的倍数prime[i*x],赋值为0
  • 令x++,如果x<n,重复上一步,否则结束
	const int num;
	printf("请问您要查找多少以内的素数:");
	scanf("%d", &num);	
	int isPrime[num]; 
	int i;
	int x;
	for (i=0; i<num; i++){
		isPrime[i] = 1; 
	}//将数组的每个单元初始化为1
	
	for (x=2; x<num; x++){
		if(isPrime[x]){
			for (i=2; i*x<num; i++){
				isPrime[i*x] = 0;
			}
		}
	} //将不是素数的单元下标标记为0

	for (i=2; i<num; i++){
		if (isPrime[i]){
			printf("%d\t", i);
		}//从下标为2开始(因为2是已知的第一个素数),若数组的元素为1,则该单元的下标为素数,将其输出
	}

6.4 二维数组

6.4.1 二维数组介绍

  • 示例
    int a[3][5];
    通常理解为a是一个3行5列的矩阵
a[0][0]a[0][1]a[0][2]a[0][3]a[0][4]
a[1][0]a[1][1]a[1][2]a[1][3]a[1][4]
a[2][0]a[2][1]a[2][2]a[2][3]a[2][4]
  • 其中,每一个元素a[i][j]是一个整数
  • a[i][j]表示第i行,第j列的单元

6.4.2 二维数组的初始化

  • 列数是必须给出的,行数可以由编译器来数
  • 每行一个{},用逗号分隔
  • 最后的逗号可以存在,有古老的传统,如果省略,表示补零
  • 也可以用定位的初始化(C99 ONLY)
    int a[][5] = {
        {0,1,2,3,4},
        {2,3,4,5,6},
    };
    

6.4.3 二维数组的遍历

示例:tic-tac-toe游戏

  • 读入一个3X3的矩阵,矩阵中的数字为1则表示该位置上有一个X,为0表示为O
  • 程序判断这个矩阵中是否有获胜的一方, 输出表示获胜一方的字符X或O,或输出无人获胜
	const int size = 3;
	int board[size][size];
	int i, j;
	int numX; //X的数量 
	int mumO; //O的数量 
	int result = -1; // -1:无人获胜 1:X赢 0:O赢
	
	//读入board矩阵
	for (i=0; i<size; i++){
		for (j=0; j<size; j++){
			scanf("%d", &board[i][j]);
		}
	} 
	
	//检查行
	for (i=0; i<size && result == -1, i++){
		numO = numX = 0;
		for (j=0; j<size; j++){
			if (board[i][j] == 1){
				numX++;
			}else{
				numO++;
			}
		}
		if (numO == size){
			result = 0;
		}else if (numX == size){
			result = 1;
		}
	} 
	
	//检查列
	if (result == -1){
		for (j=0; j<size && result == -1; j++){
			numO = numX =0;
			for (i=0; i<size; i++){
				if (board[i][j] == 1){
					numX++;
				}else{
					numO ++;
				}
			}
			if (numO == size){
				result = 0;
			}else if (numO == size){
				result = 1;
			}
		}
	}
	 
	 // 检查正对角线
	 numO = numX = 0;
	 for (i=0; i<size; i++){
	 	if (board[i][i] == 1){
	 		numX++;
		 }else{
		 	numO++;
		 }
	 } 
	 if (numO == size){
	 	result = 0;
	 } else if (numX == size){
	 	result = 1;
	 }
	 
	 // 检查反对角线
	  numO = numX = 0;
	 for (i=0; i<size; i++){
	 	if (board[i][size-i-1] == 1){
	 		numX++;
		 }else{
		 	numO++;
		 }
	 } 
	 if (numO == size){
	 	result = 0;
	 } else if (numX == size){
	 	result = 1;
	 }

第七章 指针

 7.1 指针介绍

7.1.1 取址运算符:&

  • 取得变量的地址,它的操作数必须是地址
  • 地址的大小与int的大小是否相同取决于编译器、取决于是32位架构还是64位架构
  • 查看变量i的地址:
    int i;
    printf(“%p”,&i); //以16进制的形式输出i的地址
  • &不能对没有地址的东西取地址

7.1.2 指针

① 概述

  • 指针实际就是保存地址的变量
    int* p = &i; //p是i的指针 
  • *号靠近int和靠近p都可以,*p是一个int,于是p是一个指针。我们是把*交给了p,而不是交给int,因此在下面两行代码中,都表示p是指针,而q只是一个int型的变量
    int* p,q;
    int *p,q;

② 指针变量

  • 变量的值是内存的地址
  • 普通变量的值是实际的值
  • 指针变量的值是具有实际值的变量的地址

③ 作为参数的指针

  • 当我们把一个指针作为参数时,当我们调用函数时,我们需要交给它一个地址(用&符号),而不能交一个变量
    void f(int *p);
    
    int i = 0;
    f(&i); //不可以写f(i);
  • 在函数里面可以通过这个指针访问外面的这个i

7.1.3 访问地址上的变量的运算符:*

  • *是一个单目运算符,用来访问指针的值所表示的地址上的变量
  • 可以做右值也可以做左值
    int k = *p;
    *p = k+1;

左值之所以叫做左值:

  • 是因为出现在赋值号左边的不是变量,而是值,是表达式计算的结果
  • a[0] = 2;
  • *p = 3;
  • 是特殊的值,所以叫做左值

指针的运算符 & 和 *:

  • 互相反作用
  • *&yptr -> * (&yptr) -> * (yptr的地址)-> 得到那个地址上的变量 -> yptr
  • &*yptr -> &(*yptr) -> &(yptr) -> 得到yptr的地址 -> yptr

7.2 指针的使用

7.2.1 交换两个变量的值

void swap(int *pa, int *pb)
{
    int t = *pa;
    int *pa = *pb;
    int *pb = t;
{

7.2.2 函数需要返回多个值

  • 因为函数最多只能带回一个值,所以某些值就只能通过指针返回
  • 这种情况下,传入的部分参数的作用,实际上是把需要保存的结果带回来
    //示例:求某个数组中最小和最大的元素
    
    void minmax(int a[], int len, int *min, int *max);
    
    int main()
    {
        int a[] = {1, 3, 4, 6, 7, 9, 11, 12, 18, 21};
        int min, max;
        minmax(a, sizeof(a)/sizeof(a[0]), &min, &max);
        printf("min=%d, max=%d\n", min, max);
    
        return 0;
    }
    
    void minmax(int a[], int len, int *min, int *max)
    {
        int i;
        *min = *max = a[0];
        for (i=1; i<len; i++){
            if (a[i] < *min){
                *min = a[i];
            }
            if (a[i] > *max){
                *max = a[i];
            }
    }
    

7.2.3 函数需要返回运算的状态,结果需要通过指针返回

  • 常用的套路是让函数返回特殊的不属于有效范围内的值来表示出错:-1或0(在文件操作会看到大量的例子)
  • 但是当任何数值都是有效的可能结果时,就得分开返回了
  • 后续的语言(C++,Java)采用了异常机制来解决这个问题
    //做除法
    
    int divide(int a, int b, int *result);
    
    int main()
    {
        int a;
        int b;
        scanf("%d %d", &a, &b);
        int result;
        if (divide(a, b, &result)){
            printf("%d/%d=%d\n", a, b, result);
        }
    
        return 0;
    }
    
    int divide(int a, int b, int *result)
    {
        int ret = 1;
        if (b == 0){
        ret = 0; //如果除数为0, 则表示除法不成立 
        }else{
            *result = a/b;
        }
        return ret;
    }

7.2.4 指针最常见的错误

  • 定义了指针变量,还没有指向任何变量,就开始使用指针

7.4 指针与数组

7.4.1 传入函数的数组

  • 函数参数表中的数组实际上是指针
  • sizeof(a) == sizeof(int*)
  • 但是可以用数组的运算符[]进行运算

数组参数

以下四种函数原型是等价的:

  • int sum(int *ar, int n);
  • int sum(int *, int);
  • int sum(int ar[], int n);
  • int sum(int [], int);

7.4.2 数组变量是特殊的指针

  • 数组变量本身表达地址
    int a[10]; int*p=a; // ⽆需用&取地址
  • 数组的每个单元表达的是变量,需要用&取地址
    a == &a[0];
  •  []运算符可以对数组做,也可以对指针做
    实际上,如果我们把一个指针p指向某个变量,从数组的角度看,也可以视作该变量的地址存在于数组p,而p[0]这个单元里就是这个变量,因此*p和p[0]表达的都是这个变量的值
    int min = 2;
    int *p = &min;
    printf("*p=%d\n", *p); //输出p=2
    printf("p[0]=%d\n", p[0]); //输出p=2
  • *运算符可以对指针做,也可以对数组做
    int a[0] = {1, 3, 5, 7};
    printf("%d\n", *a); //输出1
  • 数组变量是const的指针,所以数组之间不能相互赋值
    int b[];
    //可以看作为:
    int * const b;

7.5 指针与const // C99 ONLY

7.5.1 指针是const

  • 表示一旦得到了某个变量的地址,不能再指向其他变量
    int * const q = &i; //q是const,也就是i的地址不能被改变了,q不能再指向其他变量了
    *q = 26; // OK,可以修改变量i的地址
    q++; //ERROR
    

7.5.2 指针所指的是const

  • 表示不能通过这个指针去修改那个变量(并不能使得那个变量成为const)
    const int *p = &i;
    *p = 26; //ERROR,(*p)是const,不能通过p堆对变量进行赋值
    i = 26; //OK,i本身是可修改的,除非i也是const
    p = &j; //OK

7.5.3 判断const的位置

  • 判断哪个被const了的标志:const在*的前面还是后面
    int i;
    const int* p1 = &i; //*p1不可修改
    int const* p2 = &i; //*p2不可修改
    int *const p3 = &i; //p3不可修改

7.5.4 转换

  • 我们总是可以把一个非const的值转换成const的
  • 当要传递的参数的类型比地址大的时候,这是常用的手段:既能用比较少的字节数传递值给参数,又能避免函数对外面的变量进行修改
    void f(const int* x);
    int a = 15; 
    f(&a); // ok
    const int b = a; 
    f(&b); // ok
    b = a + 1; // Error!

7.5.5 const数组

  • const int a[] = {1,2,3,4,5,6,};
  • 数组变量已经是const的指针了,这里的const表明数组的每个单元都是const int
  • 所以必须通过初始化进行赋值

保护数组值:

  • 因为把数组传入函数时传递的是地址,所以那个函数内部可以修改数组的值 
  • 为了保护数组不被函数破坏,可以设置参数为
    const int sum(const int a[], int length);

7.6 指针计算

7.6.1 指针计算介绍

  • 给一个指针加1,表示要让指针指向下一个变量
    int a[10];
    int *p = a;
    // *(p+1)表示指针p从a[0]指向了a[1]
  • 如果指针不是指向一片连续分配的空间,如数组,则这种运算没有意义
    例如一个指向int型数组的指针,在32位系统上,a[0]和a[1]的地址相差4个字节,即从a[0]指向a[1]时,地址应该+4,但如果我们直接修改指针p指向的地址,把地址+2、+3的话,就是没有意义的

7.6.2 指针运算

这些算术运算可以对指针做:

  • 给指针加、减某个整数(+, +=, -, -=)
    表示将指针向前或者向后移动某几个单元
  • 递增递减(++/--)
  • 两个指针相减
    表示这两个指针之间还有几个单元

① *p++

  • p+1,但p++不变
  • 取出p所指的那个数据来,完事之后顺便把p移到下一个位置去
  • *的优先级虽然高,但是没有++高
  • 常用于数组类的连续空间操作
  • 在某些CPU上,这可以直接被翻译成一条汇编指令
    //*p++常用于数组遍历
    char a[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1}; //将-1设为停止的标志
    char *p = &a[0];
    while( *p!=-1 ){
        printf("%d\t", *p++);
    }

② 指针比较

  • <,<=, ==,>, >=, != 都可以对指针做
  • 实际是比较它们在内存中的地址
  • 数组中的单元的地址肯定是线性递增

 ③ 0地址

  • 现在的操作系统都是多进程的,基本的管理单元叫做进程(例如你双击一个东西,它运行起来就是一个进程)。对于进程来说,操作系统会给它一个虚拟的内存空间。也就是说,所有的程序运行起来时,都以为是从0开始的一片连续空间,如果是32位的话,那个顶就是4G。
  • 当然你的内存中有0地址,但是0地址通常是个不能随便碰的地址。所以你的指针不应该具有0值 。
  • 因此可以用0地址来表示特殊的事情:
    1、返回的指针是无效的
    2、指针没有被真正初始化(先初始化为0)
    3、NULL是⼀个预定定义的符号,表示0地址(有的编译器不愿意你用0来表示0地址)

 7.6.3 指针的类型

  • 无论指向什么类型,所有的指针的大小都是一样的,因为都是地址
  • 但是指向不同类型的指针是不能直接互相赋值的
  • 这是为了避免用错指针

① 指针的类型转换

  • void* 表示不知道指向什么东西的指针(计算时与char*相同、但不相通)
  • 指针也可以转换类型
    int *p = &i; 
    void*q = (void*)p;
    这并没有改变p所指的变量的类型,而是让后人用不同的眼光通过p看它所指的变量(不再当作是int,而是把它当作void)

7.6.4 指针的作用

  1. 需要传入较大的数据时用作参数(例如传入数组后对数组做操作)
  2. 函数返回不止一个结果,需要用函数来修改不止一个变量
  3. 当需要动态申请内存的时候

7.7 动态内存分配

7.7.1 malloc函数

  • 头文件:#include <stdlib.h>
  • void* malloc(size_t size);
  • 向malloc申请的空间的大小是以字节为单位的
  • 返回的结果是void*,需要类型转换为自己需要的类型
    (int*)malloc(n*sizeof(int))
    #include <stdio.h>
    #include<stdlib.h> 
    //malloc的头文件
     
    int main(){
    	int number;
    	int *a;
    	int i;
    	printf("请输入数组元素数量:");
    	scanf("%d", &number);
    	a = (int*)malloc(number*sizeof(int)); //申请一块内存空间
    	for(i=0; i<number; i++){
    		scanf("%d", &a[i]); //此时申请来的空间可以当数组使用
    	} 
    	for(i=number-1; i>=0; i--){
    		printf("%d ", a[i]); //逆序输出数组
    	}
    	free(a); //释放空间
     
    	return 0;
    }

① 假如没空间

  • 如果申请失败则返回0,或者叫做NULL
    #include <stdio.h>
    #include<stdlib.h>
     
    int main(void){
    	void *p;
    	int cnt = 0;
    	while ((p=malloc(100*1024*1024))){ //100*1024*1024表示100M 
    		cnt++;
    	}
    	//如果p得到地址不是0,那么cnt++
    	//如果p得到的地址是0,那么循环终止
    	 
    	printf("分配了%d00MB的空间\n", cnt);
    
        free(p);
    
    	return 0;
    }

② free()

  • 把申请得来的空间还给“系统”
  • 申请过的空间,最终都应该要还
  • 只能还申请来的空间的首地址

③ 常见的问题

  • 申请了没free—>长时间运行内存逐渐下降
    1、新生:忘了
    2、老手:找不到合适的free的时机
  • free过了再free
  • 地址变过了,直接去free

第八章 字符串

8.1 字符串介绍

8.1.1 字符串

  • 字符串指以0(整数0)结尾的一串字符。写成0或‘\0’是一样的,但是和‘0’(这是个字符)不同
  • 0标志字符串的结束,但它不是字符串的一部分(计算字符串长度的时候不包含这个0)
  • 字符串以字符数组的形式存在,以数组或指针的形式访问(更多的是以指针的形式)。
  • 不能用运算符对字符串做运算,通过数组的方式可以遍历字符串
  • 唯一特殊的地方是字符串常量(双引号括起来的东西)可以用来初始化字符数组
  • 头文件string.h里有很多处理字符串的函数

8.1.2 字符串变量

① 字符串变量的书写

//注意是双引号
char *str = "Hello";
char word[] = "Hello";
char line[10] = "Hello"; //实际占了6个字节的位置,因为还有结尾的0(编译器会生成结尾的0)

8.1.3 字符串常量

  • "Hello"这样一个被双引号括起来的东西就叫做字符串常量,或者字符串的字面量
  • "Hello"会被编译器变成一个字符数组放在某处,这个数组的长度是6,结尾还有表示结束的0
  • 两个相邻的字符串常量会被自动连接起来,行末的\表示下一行还是这个字符串常量
    printf("请输入你的号码,"
            "比如12345"); //这两行会被自动连接起来
    
    printf("请输入你的号码,\
    比如12345"); \\上面的那个反斜杠会把这两行连在一起
  • char *s和char s[]
    char *s = "Hello"; 
    /*s是⼀个指针,初始化为指向⼀个字符串常量。
    由于这个常量所在的地方是一个只读的代码段,
    所以实际上s是const char* s 。
    但是由于历史的原因,编译器接受不带const的写法。
    如果试图对s所指的字符串做写⼊会导致严重的后果。*/
    
    char s[] = "Hello";
    //如果需要修改字符串,应该⽤数组。

指针或数组的选择:

  • 数组:表示这个字符串就在此处
            作为本地变量,空间会被自动回收
  • 指针:这个字符串不知道在哪里,只读即可
            处理参数
            动态内存分配
  • 如果要构造一个字符串:数组
    如果要处理一个字符串:指针

char *:

  • 字符串可以表达为char*的形式,但char*不一定是字符串
  • char *本意是指向字符的指针,可能指向的是字符的数组(就像int*一样)
  • 只有它所指的字符数组有结尾的0,才能说它所指的是字符串

8.2 字符串的使用

char *t = "title";
char *s;
s = t;
  • 并没有产生新的字符串,只是让指针s指向了t所指的字符串,对s的任何操作就是对t做的

8.2.1 字符串输入与输出

char s[10];

scanf("%9s", s); //读⼊一个单词(到空格/tab/回车为止)
//写成scanf("%s", s);是不安全的,因为不知道要读⼊的内容的⻓度

printf("%s", s);
  • 在%和s之间的数字表示最多允许读入的字符数量,这个数字应该比数组的大小小1

8.2.2 常见错误

char *string;
scanf("%s", string);
  •  以为char*是字符串类型,定义了一个字符串类型的变量string就可以直接使用了
  • 由于没有对string初始化为0,所以不一定每次运行都出错

8.2.3 空字符串

char buffer[100]="";
//这是⼀个空的字符串,buffer[0] == '\0'

char buffer[] = "";
//这个数组的⻓度只有1,这个里面放不下任何字符串

8.2.4 字符串数组

  • a是一个指针,指向另一个指针,那个指针指向一个字符(串)
    char **a
  • a是一个二维数组,第⼆个维度的大小不知道,不能编译
    char a[][]
  • a是一个二维数组,a[x]是一个char[10]
    char a[][10]
  • a是一个一维数组,a[x]是一个char*
    char *a[]

8.2.5 程序参数

int main(int argc, char const *argv[])
//argv[0]是命令本⾝
//当使⽤Unix的符号链接时,反映符号链接的名字

8.3 字符输入与输出函数

8.3.1 putchar

int putchar(int c);
  • 向标准输出(即那个黑窗口)写一个字符
  • 返回写了几个字符,EOF(-1)表示写失败

8.3.2 getchar

int getchar(void);
  • 从标准输入读入一个字符
  • 返回类型是int,是为了返回EOF(-1)

8.4 标准库中的字符串函数 string.h

8.4.1 strlen 求字符串长度

  • size_t strlen(const char *s);
  • 返回s的字符串长度(不包括结尾的0)
    #include <stdio.h>
    #include <string.h>
     
    int main(){
    	char line[] = "Hello";
    	printf("strlen=%lu\n", strlen(line));
    
    	return 0;
    }
  • 自己用代码实现strlen
    #include <stdio.h>
    
    size_t strlen(const char *s);
     
    int main(){
    	char line[] = "Hello";
    	printf("strlen=%lu\n", strlen(line));
    
    	return 0;
    }
    
    size_t strlen(const char *s){
    	int cnt = 0;
    	while (s[cnt] != '\0'){
    	 	cnt++;
    	}
    	return cnt; 
    }

8.4.2 strcmp 比较字符串大小

  • int strcmp(const char *s1, const char *s2);
  • 比较两个字符串:
    返回0:s1==s2
    返回>0:s1>s2
    返回<0:s1<s2
  • 比较的是两个字符串中不相同字符的ASCII码,可能返回的是ASCII码的差值,也可能只是用0、1、-1来表示比较的结果
    #include <stdio.h>
    #include <string.h> 
     
    int main(){
    	char s1[] = "abc";
    	char s2[] = "Abc";
    	printf("%d\n", strcmp(s1, s2));
    
    	return 0;
    }
  • 自己用代码实现
#include <stdio.h>

int strcmp(const char *s1, const char *s2);
 
int main(){
	char s1[] = "abc";
	char s2[] = "Abc";
	printf("%d\n", strcmp(s1, s2));

	return 0;
}

//写法1
int strcmp(const char *s1, const char *s2){
	int idx = 0;
	while (s1[idx] == s2[idx] && s1[idx]!='\0'){
		idx ++;
	}
	return s1[idx] - s2[idx];
}

//写法2

int strcmp(const char *s1, const char *s2){
	while (*s1 == *s2 && *s1!='\0'){
		s1++;
		s2++;
	}
	return *s1-*s2;
}

8.4.3 strcpy 拷贝字符串

  • char *strcpy (char *restrict dst, const char *restrict src);
  • 把src的字符串拷贝到dst
  • restrict表明src和dst不重叠(C99)
  • 返回dst(为了能链起代码来)
  • 常用于需要复制记录下某个字符串的情况
char *dst = (char*)malloc(strlen(src)+1); 
strcpy(dst, src);
  • 自己用代码实现
//写法1
int strcpy(char *dst, const char *src){
	int idx = 0;
	while(src[idx]){ //括号内实际是src[idx] != '0' 
		dst[idx] = src[idx];
		idx++;
	}
	dst[idx] = '\0';
	return dst;
} 

//写法2
int strcpy(char *dst, const char *src){
	char *ret = dst;
	while(*src){ //括号内实际是*src != '\0' 
		*dst++ = *src++;
	} //也可以直接写为 while(*dst++ = *src++);
	*dst = '\0';
	return ret;
}

8.4.4 strcat 连接字符串

  • char * strcat(char *restrict s1, const char *restrict s2);
  • 把s2拷贝到s1的后面,接成一个长的字符串,然后返回s1
  • s1必须具有足够的空间

8.4.5 安全问题

  • strcpy和strcat都可能出现安全问题:目的地没有足够的空间
  • 安全版本:
    char *strncpy (char *restrict dst, const char *restrict src, size_t n);
    char *strncat (char *restrict s1, const char *restrict s2, size_t n);
    int strncmp (const char *s1, const char *s2, size_t n);
    
    //函数名和参数表中,多出来的n表示能用的最多的字符数
    //strncmp中的n表示需要判断前n个字符
    //n要写具体的数字

8.4.6 strchr 字符串中找字符

  • char * strchr(const char *s, int c);
    从左边开始找所查找的字符第一次出现的位置
  • char * strrchr(const char *s, int c);
    从右边开始找所查找的字符第一次出现的位置
  • 返回NULL表示没有找到
#include <stdio.h>
#include <string.h>
 
int main(){
	char s[] = "hello";
	char *p = strchr(s, 'l');
	printf("%s\n", p); //结果是llo
	
	//如果想找到l并把l后面的东西复制到另一个字符串
	char *t = (char*)malloc(strlen(p)+1);
	strcpy(t, p);
	printf("%s\n", t); //结果是llo
	free (t); 
	
	//如果想找到l并要l前面的那一段he
	char c = *p;
	*p = '\0'; //把s中的l改为了\o,于是现在s只剩下了he 
	char *t1 = (char*)malloc(strlen(s)+1);
	strcpy(t, s);
	printf("%s\n", t1); //结果是he
	*p = c; //把p恢复 
	
	//如果想要找第二个l
	p = strchr(p+1, 'l');
	printf("%s\n", p); //结果是lo

	return 0;
}

8.4.7 strstr 字符串中找字符串

  • char *strstr (const char *s1, const char *s2);
    用来在字符串中寻找字符串
  • char * strcasestr(const char *s1, const char *s2); 
    寻找字符串的过程中忽略大小写

第九章 结构类型

9.1 enum 枚举

9.1.1 枚举介绍

  • 枚举是一种用户定义的数据类型,它用关键字enum以如下语法来声明:
    enum 枚举类型名字 {名字0, …, 名字n} ;
  • 枚举类型名字通常并不真的使用,要用的是在大括号里的名字, 因为它们就是就是常量符号,它们的类型是int,值则依次从0 到n
  • 创建三个常量,red的值是0,yellow是1,而green是2
    enum colors { red, yellow, green };
  • 当需要一些可以排列起来的常量值时,定义枚举的意义就是给 了这些常量值名字

9.1.2 枚举的使用

#include <stdio.h>

enum color {red, yellow, green}; //声明新的数据类型 color 

void f(enum color c); //参数是enum类型的color

int main(){
	enum color t = red;
	scanf("%d", &t);
	f(t);

	return 0;
}

void f(enum color c){
	printf("%d\n", c);
  • 枚举量可以作为值赋给变量
  • 枚举类型可以跟上enum作为类型
  • 但是实际上C语言内部,enum就是以整数int来做计算的,所以一个枚举的变量是可以当作int来进行输入和输出的

9.1.3 自动计数的枚举

#include <stdio.h>

enum color {red, yellow, green, numcolors}; 
//因为枚举中的各个量的值是从0开始依次递增的,所以numcolors的值就是前面数字个数的计数 

int main(int argc, char const *argv[]){
	int color = -1;
	char *colornames[numcolors] = {
		"red", "yellow", "green",
	};
	char *colorname = NULL; //NULL必须是大写 
	
	printf("输入你喜欢的颜色的代码:");
	scanf("%d", &color);
	if (color >=0 && color <numcolors){
		colorname = colornames[color];
	} else {
		colorname = "unknown";
	}
	printf("你喜欢的颜色是%s\n", colorname);
	
	return 0;
}
  • 这样需要遍历所有的枚举量,或者需要建立一个用枚举量做下标的数组的时候就很方便了

9.1.4 枚举量

#include <stdio.h>

enum COLOR {red = 5, yellow, green = 5}; //声明枚举量的时候可以指定值,可以是离散的 
int main(int argc, char const *argv[]){
	enum COLOR color = 0;
	printf("code for green is %d\n", green); //输出5 
	
    printf("and color is %d\n", color);//枚举量只是int 
    //虽然COLOR里面没有0,但上述代码在如今的编译器里是可以实现的(以前不行,需要做类型转换) 
	
	return 0;
}
  • 即使给枚举类型的变量赋不存在的整数值也没有任何warning或error
  • 虽然枚举类型可以当作类型使用,但是实际上很(bu)少(hao)用
  • 除非是需要排比的符号量,用枚举比const int方便
  • 枚举比宏(macro)好,因为枚举有int类型

9.2 struct 结构

9.2.1 声明结构类型

//写法1:在函数内部的声明结构类型
//和本地变量⼀样,在函数内部声明的结构类型只能在函数内部使用

#include <stdio.h>

int main(int argc, char const *argv[])
{
    struct date{
		int month;
		int day;
		int year;
	}; //记得这里有一个分号
	
	struct date today;
	today.month = 10;
	today.day = 31;
	today.year = 2021;
	
	printf("Today's date is %i-%i-%i.\n",today.year, today.month, today.day);
	
	return 0;
}

//写法2:在函数外部的声明结构类型
//通常在函数外部声明结构类型,这样就可以被多个函数所使用了
#include <stdio.h>

struct date{
	int month;
	int day;
	int year;
};
int main(int argc, char const *argv[])
{
    struct date today;
	today.month = 10;
	today.day = 31;
	today.year = 2021;
	
	printf("Today's date is %i-%i-%i.\n",today.year, today.month, today.day);
	
	return 0;
}

9.2.2 声明结构的形式

  • p1和p2都是point里面有x和y的值
    //TYPE 1
    struct point
    {
    	int x;
    	int y;
    };
    struct point p1, p2; //使用结构的时候前面要写上struct,后面要跟上变量的名字
  •  p1和p2都是一种无名结构,里面有x和y
    //TYPE 2
    struct
    {
    	int x;
    	int y;
    } p1, p2;
  •  p1和p2都是point,里面有x和y的值t
    //TYPE 3
    struct point
    {
    	int x;
    	int y;
    }; p1, p2;

对于第一和第三种形式,都声明了结构point。但是第⼆种形式没有声明point,只是定义了两个变量。

9.2.3 结构变量

① 定义结构类型的变量

struct date today;
today.month = 10;
today.day = 31;
today.year = 2021;

② 结构的初始化

struct date
{
	int month;
	int day;
	int year;
};

struct date today = {07, 31, 2014};//依次赋值给month、day、year
struct date thismonth = {.month = 7, .year = 2014}; //此时day初始化为0

③ 结构成员

  • 结构和数组有点像
  • 数组用[]运算符和下标访问其成员
    a[0] = 10;
  • 结构用.运算符和名字访问其成员
    today.day
    student.firstName
  • p1.x
    p1.y

④ 结构运算

  • 要访问整个结构,直接用结构变量的名字
  • 对于整个结构,可以做赋值、取地址,也可以传递给函数参数
    p1 = (struct point){5, 10}; // 进行一个强制类型转换,相当于p1.x = 5; p1.y = 10
    p1 = p2; // 相当于p1.x = p2.x; p1.y = p2.y

⑤ 结构指针

  • 和数组不同,结构变量的名字并不是结构变量的地址,必须使用&运算符
    struct date *pDate = &today;

9.2.5 结构与函数

① 结构作为函数参数

int numberofdays(struct date d);
  • 整个结构可以作为参数的值传入函数
  • 这时候是在函数内新建一个结构变量,并复制调用者的结构的值
  • 也可以返回一个结构
  • 这和数组完全不同

② 输入结构

由于没有直接的办法可以一次性scanf一个结构,因此我们可以做一个函数来读入结构

结构指针:用->表示指针所指的结构变量中的成员

*p.month = 12;
//可写作
p->month = 12;
  • 结构指针参数:相比直接传入结构(占空间),用结构指针作为参数,就只需要传入传出一个指针的大小
    如果需要保护传入的结构不被函数修改,可以const struct point *p
    #include <stdio.h>
    
    struct point{
    	int x;
    	int y;
    };
    
    struct point *getstruct(struct point *);
    void output(struct point);
    void print(const struct point *p);
    
    int main(int argc, char const *argv[])
    {	
    	struct point y = {0, 0};
    	getstruct(&y);
    	output(y);
    	output(*getstruct(&y));
    	print(getstruct(&y));
    }
    
    struct point *getstruct(struct point *p)
    {
    	scanf("%d", &p->x);
    	scanf("%d", &p->y);
    	printf("%d, %d", p->x, p->y);
    	return p;
    }
    
    void output(struct point p)
    {
    	printf("%d, %d", p.x, p.y);
    }
    
    void print(const struct point *p) 
    {
    	printf("%d, %d", p->x, p->y);
    }
    
    

9.2.6 结构中的结构 

① 结构数组

struct date dates[100]; //数组中的每一个元素是date类型的结构变量
struct date dates[] = {
{4,5,2005} {2,4,2005}
};
  •  示例(计时器)
#include <stdio.h>

struct time {
	int hour;
	int minutes;
	int seconds;
};

struct time timeupdate(struct time now);

int main (void)
{
	struct time testtimes[5] = {
		{11,59,59}, {12,0,0},{1,29,59},{23,59,59},{19,12,27}
	};
	int i;

	for (i=0; i<5; ++i){
		printf("time is %.2i:%.2i:%.2i\n", 
			testtimes[i].hour, testtimes[i].minutes, testtimes[i].seconds );

		testtimes[i] = timeupdate(testtimes[i]);

		printf("one second later it's %.2i:%.2i:%.2i\n",
			testtimes[i].hour, testtimes[i].minutes, testtimes[i].seconds);
		}

		return 0;
}

struct time timeupdate(struct time now)
{
	++now.seconds;
	if (now.seconds == 60){
		now.seconds = 0;
		++now.minutes;

		if (now.minutes == 60){
			now.minutes = 0;
			++now.hour;

			if (now.hour == 24){
				now.hour = 0;
			}
		}
	}
	return now;
}

② 结构中的结构

struct dateAndTime{
    struct date sdate;
    struct date stime;
} //结构里面的成员变量可以是另外的结构

③ 嵌套的结构

  •  用两个点坐标表示矩形
struct point //用point表达点
{
	int x;
	int y;
};
struct rectangle{ //用rectangle表达矩形
	struct point pt1;
	struct point pt2;
};

// 因此如果有变量
struct rectangle r;
// 就可以有
// r.pt1.x和r.pt1.y
// r.pt2.x和r.pt2.y

//如果有变量定义
struct rectangle r, *rp;
rp = &r;

//那么下面的四种形式是等价的
r.pt1.x
rp->pt1.x
r.pt1.x
rp->pt1.x
//但是没有rp->pt1->x(因为pt1不是指针)
  • 结构中的结构的数组
#include <stdio.h>

struct point{
int x;
int y;
};

struct rectangle {
struct point p1;
struct point p2;
};

void printRect(struct rectangle r)
{
printf("<%d, %d> to <%d, %d>\n", r.p1.x, r.p1.y, r.p2.x, r.p2.y);
}

int main(int argc, char const *argv[])
{
 int i;
 struct rectangle rects[ ] = {
 {{1, 2}, {3, 4}}, 
 {{5, 6}, {7, 8}}
 }; // 结构中的结构的数组
    // 最外层的大括号表达的是数组
    // 第二层的大括号表达的是数组的单元
    // 2 rectangles

 for(i=0;i<2;i++) printRect(rects[i]);
    
}

9.3 union 联合

 9.3.1 typedef 类型定义

typedef int Length;
// 使得Length成为int类型的别名
// Length这个名字可以代替int出现在变量定义和参数声明之类的地方
// 如下:
Length a, b, len;
Length numbers[10];
  •  当用于结构中时
typedef struct Adate{ //重载已有的类型名字;新名字的含义更清晰,具有可移植性
    int month;
    int day;
    int year;
} Date; //Date可以替代struct Adate

/*eg.*/Date d = {9, 1, 2005};

typedef int Length; // Length就等价于int类型

typedef *char[10] Strings; // Strings 是10个字符串的数组的类型

//用aNode代替struct node
//写法1
typedef struct node {
int data;           
struct node *next; 
 } aNode;   
//写法2
typedef struct node aNode;

9.3.2 union 联合

union AnElt{ 
 int i; 
 char c; 
} elt1, elt2; 
elt1.i = 4; 
elt2.c = ’a’; 
elt2.i = 0xDEADBEEF;
  • union在形式上和struct非常相似
  • 但union是一种选择:
    成员是一个int i
    或者成员是一个char c
  • sizeof(union …) = sizeof(每个成员)的最大值
  • 存储
    1、所有的成员共享⼀个空间
    2、同一时间只有一个成员是有效的
    3、union的大小是其最大的成员
  • 初始化:对第一个成员做初始化

① 联合的用处

#include <stdio.h>

typedef union{
    int i;
    char ch[sizeof(int)];
} CHI;

int main(int argc, char const *argv[])
{
    CHI chi;
    int i;
    chi.i = 1234;
    for (i=0; i<sizeof(int); i++){
        printf("%02hhX", chi.ch[i]);
    }
    printf("\n");

    return 0;
}
  • 通过这种方式得到一个整数之类的内部的字节
  • 文件操作、当要把一个整数以二进制的方式写到文件里的时候,可以作为读写中间的媒介

  • 100
    点赞
  • 468
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值