C语言摘要

407 阅读31分钟

3.变量和数据类型

3.1 大话C语言变量和数据类型

变量

  • 因为a的值可以改变,所以我们给它起了一个形象的名字,叫做变量
  • int a;创造了一个变量,我们把这个过程叫做变量定义
  • a = 123;把123交给了变量a,我们把这个过程叫做给变量赋值

数据类型

说明字符型短整型整形长整型单精度浮点型双精度浮点型无类型
数据类型charshortintlongfloatdoublevoid

连续定义多个变量

int a, b, c;
  • 连续定义的多个变量以逗号分隔,并且要拥有相同的数据类型;变量可以初始化,也可以不初始化

数据的长度

  • 是指数据占用多少个字节

3.2 在屏幕上输出各种类型的数据

  • 函数:putsprintf
  • %是格式控制符的开头
  • \n代表换行
  • //后面的是注释

3.3 C语言中的整数

  • 类型:shortintlong

整形的长度

  • short 至少占用 2 个字节
  • int 建议为一个机器字长。32 位环境下机器字长为 4 字节,64 位环境下机器字长为 8 字节
  • short 的长度不能大于 int,long 的长度不能小于 int

sizeof操作符

  • 样式:
#include <stdio.h>
int main()
{
    short a = 10;
    int b = 100;
   
    int short_length = sizeof a;
    int int_length = sizeof(b);
    int long_length = sizeof(long);
    int char_length = sizeof(char);
   
    printf("short=%d, int=%d, long=%d, char=%d\n", short_length, int_length, long_length, char_length);
   
    return 0;
}

不同整形的输出

  • %hd:short int类型
  • %d: int类型
  • %ld: long int类型

3.4 二进制、八进制、十六进制

  1. 二进制
  • 二进制由 0 和 1 两个数字组成,使用时必须以0b0B(不区分大小写)开头
  1. 八进制
  • 八进制由 0~7 八个数字组成,使用时必须以0开头(注意是数字 0,不是字母 o)
  1. 十六进制
  • 十六进制由数字 0-9、字母 A-F 或 a-f(不区分大小写)组成,使用时必须以0x0X(不区分大小写)开头 4.十进制
  • 十进制由 0~9 十个数字组成,没有任何前缀

3.5 C语言中的正负数及其输出

  • C语言规定,在符号位中,用 0 表示正数,用 1 表示负数
  • 如果不希望设置符号位,可以在数据类型前面加上 unsigned 关键字,例如:
unsigned short a = 12;
unsigned int b = 1002;
unsigned long c = 9892320;

意味着,使用了 unsigned 后只能表示正数,不能再表示负数了。

无符号数的输出

严格来说,格式控制符和整数的符号是紧密相关的,具体就是:

  • %d 以十进制形式输出有符号数;
  • %u 以十进制形式输出无符号数;
  • %o 以八进制形式输出无符号数;
    • %x 以十六进制形式输出无符号数。

3.6 C语言中的小数(float,double)

小数的输出

  • %f以十进制形式输出float类型
  • %lf以十进制形式输出double类型
  • %e以指数形式输出float类型,输出结果中的e小写
  • %E以指数形式输出float类型,输出结果中的E大写
#include <stdio.h>
#include <stdlib.h>
int main()
{
    float a = 0.302;
    float b = 128.101;
    double c = 123;
    float d = 112.64E3;
    double e = 0.7623e-2;
    float f = 1.23002398;
    printf("a=%e \nb=%f \nc=%lf \nd=%lE \ne=%lf \nf=%f\n", a, b, c, d, e, f);
   
    return 0;
}

数字的后缀

  • 一个数字,是有默认类型的:对于整数,默认是 int 类型;对于小数,默认是 double 类型。

小数和整数相互赋值

  • 将一个整数赋值给小数类型,在小数点后面加 0 就可以,加几个都无所谓。
  • 将一个小数赋值给整数类型,就得把小数部分丢掉,只能取整数部分,这会改变数字本来的值。注意是直接丢掉小数部分,而不是按照四舍五入取近似值。
#include <stdio.h>
int main(){
    float f = 251;
    int w = 19.427;
    int x = 92.78;
    int y = 0.52;
    int z = -87.27;
   
    printf("f = %f, w = %d, x = %d, y = %d, z = %d\n", f, w, x, y, z);

    return 0;
}

运行结果:
f = 251.000000, w = 19, x = 92, y = 0, z = -87

由于将小数赋值给整数类型时会“失真”,所以编译器一般会给出警告,让大家引起注意。

3.7 在C语言中使用英文字符

字符的表示

  • 字符类型由单引号''包围,字符串由双引号""包围

字符的输出

  • 使用专门的字符输出函数 putchar
  • 使用通用的格式化输出函数 printfchar 对应的格式控制符是 %c
  • putchar函数每次只能输出一个字符,输出多个字符需要调用多次

字符与整数

  • 无论在哪个字符集中,字符编号都是一个整数,从这个角度考虑,字符类型和整数类型本质上没有什么区别
  • 我们可以给字符类型赋值一个整数,或者以整数的形式输出字符类型。反过来,也可以给整数类型赋值一个字符,或者以字符的形式输出整数类型。

3.8 C语言转义字符

  • 字符集为每个字符分配了唯一的编号,我们不妨将它称为编码值。在C语言中,一个字符除了可以用它的实体(也就是真正的字符)表示,还可以用编码值表示。这种使用编码值来间接地表示字符的方式称为转义字符
  • 转义字符既可以用于单个字符,也可以用于字符串,并且一个字符串中可以同时使用八进制形式和十六进制形式。
#include <stdio.h>
int main(){
    puts("\x68\164\164\x70://c.biancheng.\x6e\145\x74");
    return 0;
}

运行结果:
http://c.biancheng.net\

3.9 C语言标识符、关键字、注释、表达式和语句

标识符

  • 由字母,数字,下划线组成,第一个不能是数字

关键字

  • 规范的字符串,例如int

注释

  • 提示或解释

表达式或语句

表达式(Expression)和语句(Statement)的概念在C语言中并没有明确的定义:

  • 表达式可以看做一个计算的公式,往往由数据、变量、运算符等组成,例如3*4+5a=c=d等,表达式的结果必定是一个值;
  • 语句的范围更加广泛,不一定是计算,不一定有值,可以是某个操作、某个函数、选择结构、循环等。

重点:

  • 表达式必须有一个执行结果,这个结果必须是一个值,例如3*4+5的结果 17,a=c=d=10的结果是 10,printf("hello")的结果是 5(printf 的返回值是成功打印的字符的个数)。
  • 以分号;结束的往往称为语句,而不是表达式,例如3*4+5;a=c=d;等。

3.10 C语言加减乘除运算

对除法的说明

  • 整除整得整,非整去小数得整
  • 分子或分母为小数则得小数,且是double类型

对取余运算的说明

  • 要求两边为整数
  • 如果%左边是正数,那么余数也是正数
  • 如果%左边是负数,那么余数也是负数

加减乘除运算的简写

a = a # b
a #= b

3.11C语言的自加(++)和自减(--)

  • ++ 在前面叫做前自增(例如 ++a)。前自增先进行自增运算,再进行其他操作
  • ++ 在后面叫做后自增(例如 a++)。后自增先进行其他操作,再进行自增运算

3.12C语言数据类型转化

自动类型转换

  1. 将一种类型的数据赋值给另外一种类型的变量时就会发生自动类型转换,例如:
float f = 100;
  • 100 是 int 类型的数据,需要先转换为 float 类型才能赋值给变量 f。再如:
int n = f;
  • f 是 float 类型的数据,需要先转换为 int 类型才能赋值给变量 n
  1. 在不同类型的混合运算中,编译器会自动转换参与的数据为同一类型进行计算,然后再进行计算。转换的规则如下:
  • 转换按数据长度增加的方向进行,以保证数值不失真,或者精度不降低。例如,int 和 long 参与运算时,先把 int 类型的数据转成 long 类型后再进行运算。
  • 所有的浮点运算都是以双精度进行的,即使运算中只有 float 类型,也要先转换为 double 类型,才能进行运算。
  • char 和 short 参与运算时,必须先转换成 int 类型。

强制类型转换

  • 强制类型转换是程序员明确提出的、需要通过特定格式的代码来指明的一种类型转换。换句话说,自动类型转换不需要程序员干预,强制类型转换必须有程序员干预

类型转换只是临时的

无论是自动类型转换还是强制类型转换,都只是为了本次运算而进行的临时性转换,转换的结果也会保存到临时的内存空间,不会改变数据本来的类型或者值。请看下面的例子:

#include <stdio.h>
int main(){
    double total = 400.8;  //总价
    int count = 5;  //数目
    double unit;  //单价
    int total_int = (int)total;
    unit = total / count;
    printf("total=%lf, total_int=%d, unit=%lf\n", total, total_int, unit);

    return 0;
}

运行结果:
total=400.800000, total_int=400, unit=80.160000

注意看第 6 行代码,total 变量被转换成了 int 类型才赋值给 total_int 变量,而这种转换并未影响 total 变量本身的类型和值。如果 total 的值变了,那么 total 的输出结果将变为 400.000000;如果 total 的类型变了,那么 unit 的输出结果将变为 80.000000。

自动类型转换和强制类型转换

可以自动转换的类型一定能够强制转换,但是,需要强制转换的类型不一定能够自动转换

4. C语言输入输出

4.1 C语言数据输出大汇总以及轻量进阶

  • puts(): 只能输出字符串
  • putchar(): 只能输出单个字符
  • printf(): 可以输出各种类型的数据
格式控制符说明
%c输出一个单一的字符
%hd、%d、%ld以十进制、有符号的形式输出 short、int、long 类型的整数
%hu、%u、%lu以十进制、无符号的形式输出 short、int、long 类型的整数
%ho、%o、%lo以八进制、不带前缀、无符号的形式输出 short、int、long 类型的整数
%#ho、%#o、%#lo以八进制、带前缀、无符号的形式输出 short、int、long 类型的整数
%hx、%x、%lx %hX、%X、%lX以十六进制、不带前缀、无符号的形式输出 short、int、long 类型的整数。如果 x 小写,那么输出的十六进制数字也小写;如果 X 大写,那么输出的十六进制数字也大写。
%#hx、%#x、%#lx %#hX、%#X、%#lX以十六进制、带前缀、无符号的形式输出 short、int、long 类型的整数。如果 x 小写,那么输出的十六进制数字和前缀都小写;如果 X 大写,那么输出的十六进制数字和前缀都大写。
%f、%lf以十进制的形式输出 float、double 类型的小数
%e、%le %E、%lE以指数的形式输出 float、double 类型的小数。如果 e 小写,那么输出结果中的 e 也小写;如果 E 大写,那么输出结果中的 E 也大写。
%g、%lg %G、%lG以十进制和指数中较短的形式输出 float、double 类型的小数,并且小数部分的最后不会添加多余的 0。如果 g 小写,那么当以指数形式输出时 e 也小写;如果 G 大写,那么当以指数形式输出时 E 也大写。
%s输出一个字符串

printf()的高级用法

%[flag][width][.precision]type
  1. type 表示输出类型,比如 %d、%f、%c、%lf,type 就分别对应 d、f、c、lf;再如,%-9d中 type 对应 d。
  2. width 表示最小输出宽度,也就是至少占用几个字符的位置;例如,%-9d中 width 对应 9,表示输出结果最少占用 9 个字符的宽度。

    当输出结果的宽度不足 width 时,以空格补齐(如果没有指定对齐方式,默认会在左边补齐空格);当输出结果的宽度超过 width 时,width 不再起作用,按照数据本身的宽度来输出。 对输出结果的说明:
  • n 的指定输出宽度为 10,234 的宽度为 3,所以前边要补上 7 个空格。
  • f 的指定输出宽度为 12,9.800000 的宽度为 8,所以前边要补上 4 个空格。
  • str 的指定输出宽度为 8,"http://c.biancheng.net" 的宽度为 22,超过了 8,所以指定输出宽度不再起作用,而是按照 str 的实际宽度输出。


3. .precision表示输出精度,也就是小数的位数

  • 位数大于precision时,四舍五入去掉多余的
  • 位数小于precision时,在后面补0;
    也可以用于整数或字符串,功能相反
  • 用于整数时,.precision表示最小输出宽度。与width不同的是,整数的宽度不足时会在左边补0;而不是补空格
  • 用于字符串时,.precision表示最大输出宽度,或者说截取字符串。当字符串的长度大于precision时,会截掉多余的字符;当字符串的长度小于precision时,.precision就不再起作用 寄
  1. flag 是标志字符。例如,%#x中 flag 对应 #,%-9d中 flags 对应-。下表列出了 printf() 可以用的 flag:
标志字符含  义
--表示左对齐。如果没有,就按照默认的对齐方式,默认一般为右对齐。
+用于整数或者小数,表示输出符号(正负号)。如果没有,那么只有负数才会输出符号。
空格用于整数或者小数,输出值为正时冠以空格,为负时冠以负号。
#- 对于八进制(%o)和十六进制(%x / %X)整数,# 表示在输出时添加前缀;八进制的前缀是 0,十六进制的前缀是 0x / 0X。
  • 对于小数(%f / %e / %g),# 表示强迫输出小数点。如果没有小数部分,默认是不输出小数点的,加上 # 以后,即使没有小数部分也会带上小数点。 |

\

4.2 C语言scanf:读取从键盘输入的数据

  • scanf(): 和prentf()类似,scanf()可以输入多种类型的数据。
  • getchar()、getche()、getch():这三个函数都用于输入单个字符
  • gets():获取一行数据,并作为字符串处理

scanf()函数

  • &称为取地址符
  1. scanf()对输入空格不严格要求
  2. 输入的数以逗号分隔
  3. 要求整数之间以is bigger than分隔

连续输入

输入其它数据

  • 除了输入整数,scanf()还可以输入单个字符、字符串、小数等 寄

scanf()格式控制符汇总

格式控制符说明
%c读取一个单一的字符
%hd、%d、%ld读取一个十进制整数,并分别赋值给short、int、long类型
%ho、%o、%lo读取一个八进制整数,并分别赋值给short、int、long类型
%hx、%x、%lx读取一个十六进制整数,并分别赋值给short、int、long类型
%hu、%u、%lu读取一个无符号整数,并分别赋值给unsigned short、unsigned int、unsigned long类型
%f、%lf读取一个十进制形式的小说,并分别赋值给float、double类型
%e、%le读取一个指数形式的小数,并分别赋值给float、double类型
%g、%lg既可以读取一个十进制形式的小数,也可以读取一个指数形式的小数,并分别赋值给float、double类型
%s读取一个字符串

4.3 C语言输入字符和字符串

输入单个字符

  1. getchar()(通用)
  • 就是scanf()的一个简化版本
  1. getche()(Windows)
  • 输入一个字符后会立即读取,不用按下回车键
  1. getch()(Windows)
  • 输入一个字符后会立即读取,不用按下回车键,没有回显,无法看到输入的字符

输入字符串

gets()和scanf()的主要区别是:

  • scanf()读取字符串时以空格为分隔,遇到空格就认为当前字符串结束了,所以无法读取含有空格的字符串
  • gets()认为空格也是字符串的的一部分,只有遇到回车键才认为字符串输入结束。
  • scanf()可以一次性读取多份类型相同或者不同的数据,getchar()、getche()、getch()和gets()每次只能读取一份特定类型的数据,不能一次性读取多份数据

5.C语言循环结构和选择结构

三大结构:

  • 顺序结构就是让程序按照从头到尾的顺序依次执行每一条C语言代码,不重复执行任何代码,也不跳过任何代码
  • C语言选择结构也称分支结构,就是让程序有选择性的执行代码
  • C语言循环结构就是让程序不断重复执行同一段代码

5.1 C语言if else语句详解

if(判断条件){
    语句块1
}else{
    语句块2
}

只使用if语句

if(判断条件){
   语句块
}

多个if else语句

if(判断条件){
   语句块1
} else if(判断条件2){
   语句块2
} else if(判断条件3){
   语句块3
} else if(判断条件m){
   语句块m
} else {
   语句块n
}

if语句的嵌套

#include <stdio.h>
int main(){
    int a,b;
    printf("Input two numbers:");
    scanf("%d %d",&a,&b);
    if(a!=b){  //!=表示不等于
        if(a>b) printf("a>b\n");
        else printf("a<b\n");
    }else{
        printf("a=b\n");
    }
    return 0;
}

5.2 C语言关系运算符详解

  • 专门用在判断条件中,让程序决定下一步操作,称为关系运算符
  • 都是双目运算符,结合性均为左结合
  • 关系运算符的优先级低于算术运算符,高于赋值运算符

5.3 C语言逻辑运算符详解

  1. 与运算(&&)
  2. 或运算(||)
  3. 非运算(!)

优先级

  • 赋值运算符(=) < &&和|| < 关系运算符 < 算术运算符 < 非(!) 详情寄

5.4 C语言switch case语句详解

switch(表达式){
    case 整型数值1: 语句 1;
    case 整型数值2: 语句 2;
    ......
    case 整型数值n: 语句 n;
    default: 语句 n+1;
}
  • break是C语言中的一个关键字,专门用于跳出switch语句
  • 由于default是最后一个分支,匹配后不会再执行其他分支,所以也可以不添加break;语句
  1. case后面必须是一个整数,或者是结构为整数的表达式,但不能包含任何变量 例:
case 10: printf("..."); break;  //正确
case 8+9: printf("..."); break;  //正确
case 'A': printf("..."); break;  //正确,字符和整数可以相互转换
case 'A'+19: printf("..."); break;  //正确,字符和整数可以相互转换
case 9.5: printf("..."); break;  //错误,不能为小数
case a: printf("..."); break;    //错误,不能包含变量
case a+10: printf("..."); break;  //错误,不能包含变量
  1. default不是必须的。当没有default时,如果所有case都匹配失败,那么就什么都不执行

5.5 C语言?和:详解,C语言条件运算符详解

语法格式:

表达式1 ? 表达式2 : 表达式3
  • 规则为:如果表达式1的值为真,则以表达式2的值作为整个条件表达式的值,否则以表达式3的值作为整个条件表达式的值 详:
if(a>b){
    max = a;
}else{
    max = b;
}

化为条件表达式:

max = (a>b) ? a : b;
  1. 条件运算符的优先级低于关系运算符和算数运算符,但高于赋值符因此可以去掉括号(上)
  2. 条件运算符?和:是一对运算符,不能分开单独使用
  3. 条件运算符的结合方向是自右向左

5.6 C语言while循环和do while循环详解

while循环

while(表达式){
   语句块
}

do-while循环

do{
   语句块
}while(表达式);

5.7C语言for循环(for语句)详解

for(初始化语句;循环条件;自增或自减){
   语句块
}

5.8 C语言break和continue用法详解(跳出循环)

  • break关键字:用于while、for循环时,会终止循环而执行整个循环语句后面的代码
#include <stdio.h>
int main(){
    int i=1, j;
    while(1){  // 外层循环
        j=1;
        while(1){  // 内层循环
            printf("%-4d", i*j);
            j++;
            if(j>4) break;  //跳出内层循环
        }
        printf("\n");
        i++;
        if(i>4) break;  // 跳出外层循环
    }

    return 0;
}
  • continue语句:作用是跳过循环体中剩余的语句而强制进行下一次循环。只用在while、for循环中,常与if条件语句一起使用,判断条件是否成立
#include <stdio.h>
int main(){
    char c = 0;
    while(c!='\n'){  //回车键结束循环
        c=getchar();
        if(c=='4' || c=='5'){  //按下的是数字键4或5
            continue;  //跳过当次循环,进入下次循环
        }
        putchar(c);
    }
    return 0;
}

5.9 C语言循环嵌套详解

  • 一条语句里面还有另一条语句
#include <stdio.h>
int main()
{
    int i, j;
    for(i=1; i<=4; i++){  //外层for循环
        for(j=1; j<=4; j++){  //内层for循环
            printf("i=%d, j=%d\n", i, j);
        }
        printf("\n");
    }
    return 0;
}

6. C语言数组

6.1 什么是数组?C语言数组的基本概念

数组的概念和定义

  • arrayName[index]
  • dataType arraName[length]

6.2 二维数组的定义、初始化、赋值

  • dataType arrayName[length1][length2]
  • C语言中,二维数组是按行排列的
  • 可以分段赋值,可以连续赋值

6.3 C语言判断数组中是否包含某个元素

对无序数组的查询

用循环遍历数组的每一个元素

#include <stdio.h>
int main(){
    int nums[10] = {1, 10, 6, 296, 177, 23, 0, 100, 34, 999};
    int i, num, thisindex = -1;
   
    printf("Input an integer: ");
    scanf("%d", &num);
    for(i=0; i<10; i++){
        if(nums[i] == num){
            thisindex = i;
            break;
        }
    }
    if(thisindex < 0){
        printf("%d isn't  in the array.\n", num);
    }else{
        printf("%d is  in the array, it's index is %d.\n", num, thisindex);
    }

    return 0;
}

对有序数组的查询

#include <stdio.h>
int main(){
    int nums[10] = {0, 1, 6, 10, 23, 34, 100, 177, 296, 999};
    int i, num, thisindex = -1;
   
    printf("Input an integer: ");
    scanf("%d", &num);
    for(i=0; i<10; i++){
        if(nums[i] == num){
            thisindex = i;
            break;
        }else if(nums[i] > num){
            break;
        }
    }
    if(thisindex < 0){
        printf("%d isn't  in the array.\n", num);
    }else{
        printf("%d is  in the array, it's index is %d.\n", num, thisindex);
    }

6.4 C语言字符数组和字符串详解

  • 字符数组实际上是一系列字符的集合,也就是字符串
  • C语言规定,可以将字符串直接赋值给字符数组,可以不指定数组长度,例:
char str[30] = {"c.biancheng.net"};
char str[30] = "c.biancheng.net";  //这种形式更加简洁,实际开发中常用
  • 字符数组只有在定义时才能将整个字符串一次性地赋值给它,一旦定义完了,就只能一个字符一个字符地赋值了

字符串结束标志

  • 在C语言中,字符串总是以'\0'作为结尾,所以'\0'也被称为字符串结束标志,或者字符串结束符。
  • " "包围的字符串会自动在末尾添加'\0'
  • 需要注意的是,逐个字符地给数组赋值并不会自动添加'\0',例
char str[] = {'a', 'b', 'c'};
  • 当用字符数组存储字符串时,要特别注意'\0',要为'\0'留个位置;这意味着,字符数组的长度至少要比字符串的长度大 1。请看下面的例子:
char str[7] = "abc123";

字符串长度

所谓字符串长度,就是字符串包含了多少个字符(不包括最后的结束符'\0'

6.5 C语言字符串的输入和输出

字符串的输入

  • puts():输出字符串并自动换行,该函数只能输出字符串
  • printf():通过格式控制符%s输出字符串,不能自动换行。除了字符串,printf() 还能输出其他类型的数据

字符串的输入

  • scanf():通过格式控制符%s输入字符串。除了字符串,scanf() 还能输入其他类型的数据
  • gets():直接输入字符串,并且只能输入字符串

6.6 C语言字符串处理函数

字符串连接函数 strcat()

strcat 是 string catenate 的缩写,意思是把两个字符串拼接在一起,语法格式为:

strcat(arrayName1, arrayName2);
  • strcat() 将把 arrayName2 连接到 arrayName1 后面,并删除原来 arrayName1 最后的结束标志'\0'。这意味着,arrayName1 必须足够长,要能够同时容纳 arrayName1 和 arrayName2,否则会越界(超出范围)

字符串复制函数 strcpy()

strcpy 是 string copy 的缩写,意思是字符串复制,也即将字符串从一个地方复制到另外一个地方,语法格式为:

strcpy(arrayName1, arrayName2);

字符串比较函数 strcmp()

strcmp 是 string compare 的缩写,意思是字符串比较,语法格式为:

strcmp(arrayName1, arrayName2);

6.7 C语言对数组进行排序

算法总结及实现

#include <stdio.h>
int main(){
    int nums[10] = {4, 5, 2, 10, 7, 1, 8, 3, 6, 9};
    int i, j, temp;

    //冒泡排序算法:进行 n-1 轮比较
    for(i=0; i<10-1; i++){
        //每一轮比较前 n-1-i 个,也就是说,已经排序好的最后 i 个不用比较
        for(j=0; j<10-1-i; j++){
            if(nums[j] > nums[j+1]){
                temp = nums[j];
                nums[j] = nums[j+1];
                nums[j+1] = temp;
            }
        }
    }
   
    //输出排序后的数组
    for(i=0; i<10; i++){
        printf("%d ", nums[i]);
    }
    printf("\n");
   
    return 0;
}

优化算法

#include <stdio.h>
int main(){
    int nums[10] = {4, 5, 2, 10, 7, 1, 8, 3, 6, 9};
    int i, j, temp, isSorted;
   
    //优化算法:最多进行 n-1 轮比较
    for(i=0; i<10-1; i++){
        isSorted = 1;  //假设剩下的元素已经排序好了
        for(j=0; j<10-1-i; j++){
            if(nums[j] > nums[j+1]){
                temp = nums[j];
                nums[j] = nums[j+1];
                nums[j+1] = temp;
                isSorted = 0;  //一旦需要交换数组元素,就说明剩下的元素没有排序好
            }
        }
        if(isSorted) break; //如果没有发生交换,说明剩下的元素已经排序好了
    }

    for(i=0; i<10; i++){
        printf("%d ", nums[i]);
    }
    printf("\n");
   
    return 0;
}

7.C语言函数详解

  • 函数就是一段封装好的,可以重复使用的代码,它使得我们的程序更加模块化,不需要编写大量重复的代码。
  • 函数可以提前保存起来,并给它起一个独一无二的名字,只要知道它的名字就能使用这段代码。函数还可以接收数据,并根据数据的不同做出不同的操作,最后再把处理结果反馈给我们

7.1什么是函数?

  • 允许我们将常用的代码以固定的格式封装(包装)成一个独立的模块,只要知道这个模块的名字就可以重复使用它,这个模块就叫做函数(Function)

参数

函数的一个明显特征就是使用时带括号( ),有必要的话,括号中还要包含数据或变量,称为参数(Parameter)。参数是函数需要处理的数据,例如:

  • strlen(str1)用来计算字符串的长度,str1就是参数。
  • puts("C语言中文网")用来输出字符串,"C语言中文网"就是参数。

返回值

所谓返回值,就是函数的执行结果。例如:

char str1[] = "C Language";
int len = strlen(str1);

7.2C语言函数定义

  • 将代码段封装成函数的过程叫做函数定义

C语言无参函数的定义

如果函数不接收用户传递的数据,那么定义时可以不带参数。如下所示:

dataType  functionName(){\
    //body\
}
  • dataType 是返回值类型,它可以是C语言中的任意数据类型,例如 int、float、char 等。
  • functionName 是函数名,它是标识符的一种,命名规则和标识符相同。函数名后面的括号( )不能少。
  • body 是函数体,它是函数需要执行的代码,是函数的主体部分。即使只有一个语句,函数体也要由{ }包围。
  • 如果有返回值,在函数体中使用 return 语句返回。return 出来的数据的类型要和 dataType 一样。

C语言有参函数的定义

如果函数需要接收用户传递的数据,那么定义时就要带上参数。如下所示:

dataType  functionName( dataType1 param1, dataType2 param2 ... ){\
    //body\
}

dataType1 param1, dataType2 param2 ...是参数列表。函数可以只有一个参数,也可以有多个,多个参数之间由,分隔。参数本质上也是变量,定义时要指明类型和名称。与无参函数的定义相比,有参函数的定义仅仅是多了一个参数列表。

函数不能嵌套定义

强调一点,C语言不允许函数嵌套定义;也就是说,不能在一个函数中定义另外一个函数,必须在所有函数之外定义另外一个函数。main() 也是一个函数定义,也不能在 main() 函数内部定义新函数。\

7.3C语言函数的形参和实参

  • 在函数定义中出现的参数可以看做是一个占位符,它没有数据,只能等到函数被调用时接收传递进来的数据,所以称为形式参数,简称形参
  • 函数被调用时给出的参数包含了实实在在的数据,会被函数内部的代码使用,所以称为实际参数,简称实参

形参和实参的区别和联系

  1. 形参变量只有在函数被调用时才会分配内存,调用结束后,立刻释放内存,所以形参变量只有在函数内部有效,不能在函数外部使用。

  2. 实参可以是常量、变量、表达式、函数等,无论实参是何种类型的数据,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参,所以应该提前用赋值、输入等办法使实参获得确定值。

  3. 实参和形参在数量上、类型上、顺序上必须严格一致,否则会发生“类型不匹配”的错误。当然,如果能够进行自动类型转换,或者进行了强制类型转换,那么实参类型也可以不同于形参类型。

  4. 函数调用中发生的数据传递是单向的,只能把实参的值传递给形参,而不能把形参的值反向地传递给实参;换句话说,一旦完成数据的传递,实参和形参就再也没有瓜葛了,所以,在函数调用过程中,形参的值发生改变并不会影响实参

  5. 形参和实参虽然可以同名,但它们之间是相互独立的,互不影响,因为实参在函数外部有效,而形参在函数内部有效。

7.4C语言函数的返回值

return 语句的一般形式为:

return 表达式;

对C语言返回值的说明:

  1. 没有返回值的函数为空类型
  2. return 语句可以有多个,可以出现在函数体的任意位置,但是每次调用函数只能有一个 return 语句被执行,所以只有一个返回值
  3. 函数一旦遇到 return 语句就立即返回,后面的所有语句都不会被执行到了。从这个角度看,return 语句还有强制结束函数执行的作用

7.5C语言函数的调用

函数调用(Function Call) ,就是使用已经定义好的函数。函数调用的一般形式为:

functionName(param1, param2, param3 ...);
  • functionName 是函数名称,param1, param2, param3 ...是实参列表。实参可以是常数、变量、表达式等,多个实参用逗号,分隔

函数的嵌套调用

  • 函数不能嵌套定义,但可以嵌套调用,也就是在一个函数的定义或调用过程中允许出现对另外一个函数的调用

7.6函数声明及函数原型

  • 声明(Declaration) ,就是告诉编译器我要使用这个函数,你现在没有找到它的定义不要紧,请不要报错,稍后我会把定义补上。
  • 函数声明给出了函数名、返回值类型、参数列表(重点是参数类型)等与该函数有关的信息,称为函数原型(Function Prototype)

7.7全局变量和局部变量

  • 定义在函数内部的变量称为局部变量(Local Variable) ,它的作用域仅限于函数内部, 离开该函数后就是无效的,再使用就会报错
  • 在所有函数外部定义的变量称为全局变量(Global Variable) ,它的作用域默认是整个程序,也就是所有的源文件,包括 .c 和 .h 文件

7.8C语言变量的作用域

  • 在函数内部定义的变量,它的作用域也仅限于函数内部,出了函数就不能使用了,我们将这样的变量称为局部变量
  • C语言允许在所有函数的外部定义变量,这样的变量称为全局变量

关于变量的命名

C语言规定,在同一个作用域中不能出现两个名字相同的变量,否则会产生命名冲突;但是在不同的作用域中,允许出现名字相同的变量,它们的作用范围不同,彼此之间不会产生冲突。这句话有两层含义:

  • 不同函数内部可以出现同名的变量,不同函数是不同的局部作用域;
  • 函数内部和外部可以出现同名的变量,函数内部是局部作用域,函数外部是全局作用域。
  1. 不同函数内部的同名变量是两个完全独立的变量,它们之间没有任何关联,也不会相互影响
  2. 函数内部的局部变量和函数外部的全局变量同名时,在当前函数这个局部作用域中,全局变量会被“屏蔽”,不再起作用。也就是说,在函数内部使用的是局部变量,而不是全局变量

7.9C语言块级变量

  • 代码块,就是由{ }包围起来的代码。代码块在C语言中随处可见,例如函数体、选择结构、循环结构等。不包含代码块的C语言程序根本不能运行,即使最简单的C语言程序(上节已经进行了展示)也要包含代码块

  • C语言允许在代码块内部定义变量,这样的变量具有块级作用域;换句话说,在代码块内部定义的变量只能在代码块内部使用,出了代码块就无效了

在 for 循环条件里面定义变量

  • 遵循 C99 标准的编译器允许在 for 循环条件里面定义新变量,这样的变量也是块级变量,它的作用域仅限于 for 循环内部

单独的代码块

作用域

  • 每个C语言程序都包含了多个作用域,不同的作用域中可以出现同名的变量,C语言会按照从小到大的顺序、一层一层地去父级作用域中查找变量,如果在最顶层的全局作用域中还未找到这个变量,那么就会报错

7.10C语言递归函数

  • 一个函数在它的函数体内调用它自身称为递归调用,这种函数称为递归函数。执行递归函数将反复调用其自身,每调用一次就进入新的一层,当最内层的函数执行完毕后,再一层一层地由里到外退出。

递归的进入

  1. 求 5!,即调用 factorial(5)。当进入 factorial() 函数体后,由于形参 n 的值为 5,不等于 0 或 1,所以执行factorial(n-1) * n,也即执行factorial(4) * 5。为了求得这个表达式的结果,必须先调用 factorial(4),并暂停其他操作。换句话说,在得到 factorial(4) 的结果之前,不能进行其他操作。这就是第一次递归

  2. 调用 factorial(4) 时,实参为 4,形参 n 也为 4,不等于 0 或 1,会继续执行factorial(n-1) * n,也即执行factorial(3) * 4。为了求得这个表达式的结果,又必须先调用 factorial(3)。这就是第二次递归

  3. 以此类推,进行四次递归调用后,实参的值为 1,会调用 factorial(1)。此时能够直接得到常量 1 的值,并把结果 return,就不需要再次调用 factorial() 函数了,递归就结束了

递归的退出

当递归进入到最内层的时候,递归就结束了,就开始逐层退出了,也就是逐层执行 return 语句

  1. n 的值为 1 时达到最内层,此时 return 出去的结果为 1,也即 factorial(1) 的调用结果为 1

  2. 有了 factorial(1) 的结果,就可以返回上一层计算factorial(1) * 2的值了。此时得到的值为 2,return 出去的结果也为 2,也即 factorial(2) 的调用结果为 2

  3. 以此类推,当得到 factorial(4) 的调用结果后,就可以返回最顶层。经计算,factorial(4) 的结果为 24,那么表达式factorial(4) * 5的结果为 120,此时 return 得到的结果也为 120,也即 factorial(5) 的调用结果为 120,这样就得到了 5! 的值

递归的条件

每一个递归函数都应该只进行有限次的递归调用,否则它就会进入死胡同,永远也不能退出了,这样的程序是没有意义的

要想让递归函数逐层进入再逐层退出,需要解决两个方面的问题:

  • 存在限制条件,当符合这个条件时递归便不再继续。对于 factorial(),当形参 n 等于 0 或 1 时,递归就结束了
  • 每次递归调用之后越来越接近这个限制条件。对于 factorial(),每次递归调用的实参为 n - 1,这会使得形参 n 的值逐渐减小,越来越趋近于 1 或 0

8.C语言预处理命令

8.1C语言预处理命令是什么?

  • #号开头的命令称为预处理命令
  • 在编译之前对源文件进行简单加工的过程,就称为预处理
#include <stdio.h>

//不同的平台下引入不同的头文件
#if _WIN32  //识别windows平台
#include <windows.h>
#elif __linux__  //识别linux平台
#include <unistd.h>
#endif

int main() {
    //不同的平台下调用不同的函数
    #if _WIN32  //识别windows平台
    Sleep(5000);
    #elif __linux__  //识别linux平台
    sleep(5);
    #endif

    puts("http://c.biancheng.net/");

    return 0;
}

8.2 C语言#include的用法详解

#include叫做文件包含命令,用来引入对应的头文件(.h文件)。#include 也是C语言预处理命令的一种

#include 的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同

#include 的用法有两种,如下所示:

#include <stdHeader.h> #include "myHeader.h"

使用尖括号< >和双引号" "的区别在于头文件的搜索路径不同:

  • 使用尖括号< >,编译器会到系统路径下查找头文件;
  • 而使用双引号" ",编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找

8.3 C语言#define的用法,C语言宏定义

#define 叫做宏定义命令,它也是C语言预处理命令的一种。所谓宏定义,就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就全部替换成指定的字符串

8.4 C语言带参数的宏定义

C语言允许宏带有参数。在宏定义中的参数称为“形式参数”,在宏调用中的参数称为“实际参数”,这点和函数有些类似。 对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参。

带参宏定义的一般形式为:

#define 宏名(形参列表) 字符串

在字符串中可以含有各个形参。

带参宏调用的一般形式为:

宏名(实参列表);

8.5 C语言带参宏定义和函数的区别

9.指针

9.1 C语言指针是什么?

  • 我们将内存中字节的编号称为地址(Address)或[指针](Pointer)

9.2 C语言指针变量的定义和使用

定义指针变量

定义指针变量与定义普通变量非常类似,不过要在变量名前面加星号*,格式为:

datatype *name;

或者

datatype *name = value;
  • *表示这是一个指针变量,datatype表示该指针变量所指向的数据的类型
  • *是一个特殊符号,表明一个变量是指针变量,定义 p1、p2 时必须带*。而给 p1、p2 赋值时,因为已经知道了它是一个指针变量,就没必要多此一举再带上*,后边可以像使用普通变量一样来使用指针变量。也就是说,定义指针变量时必须带*,给指针变量赋值时不能带*

通过指针变量取得数据

指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:

*pointer;

这里的*称为指针运算符,用来取得某个地址上的数据

  • 就是说,使用指针是间接获取数据,使用变量名是直接获取数据,前者比后者的代价要高
  • *在不同的场景下有不同的作用:*可以用在指针变量的定义中,表明这是一个指针变量,以和普通变量区分开;使用指针变量时在前面加*表示获取指针指向的数据,或者说表示的是指针指向的数据本身

关于 * 和 & 的谜题

*&a可以理解为*(&a)&a表示取变量 a 的地址(等价于 pa),*(&a)表示取这个地址上的数据(等价于 *pa),绕来绕去,又回到了原点,*&a仍然等价于 a。

对星号*的总结

在我们目前所学到的语法中,星号*主要有三种用途:

  • 表示乘法,例如int a = 3, b = 5, c;  c = a * b;,这是最容易理解的。
  • 表示定义一个指针变量,以和普通变量区分开,例如int a = 100;  int *p = &a;
  • 表示获取指针指向的数据,是一种间接操作,例如int a, b, *p = &a;  *p = 100;  b = *p;

9.3 C语言指针变量的运算(加法、减法和比较运算)

[指针]变量保存的是地址,而地址本质上是一个整数,所以指针变量可以进行部分运算,例如加法、减法、比较等

9.4 C语言数组指针(指向数组的指针)详解

  • 数组(Array)是一系列具有相同类型的数据的集合,每一份数据叫做一个数组元素(Element)。数组中的所有元素在内存中是连续排列的,整个数组占用的是一块内存
  • 定义数组时,要给出数组名和数组长度,数组名可以认为是一个[指针],它指向数组的第 0 个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址
  • 如果一个指针指向了数组,我们就称它为数组指针(Array Pointer) 引入数组指针后,我们就有两种方案来访问数组元素了,一种是使用下标,另外一种是使用指针。

1) 使用下标

也就是采用 arr[i] 的形式访问数组元素。如果 p 是指向数组 arr 的指针,那么也可以使用 p[i] 来访问数组元素,它等价于 arr[i]。

2) 使用指针

也就是使用 *(p+i) 的形式访问数组元素。另外数组名本身也是指针,也可以使用 *(arr+i) 来访问数组元素,它等价于 *(p+i)

9.5 C语言字符串指针# (指向字符串的指针)详解

  • 字符数组归根结底还是一个数组,上节讲到的关于指针和数组的规则同样也适用于字符数组
  • 字符串中的所有字符在内存中是连续排列的,str 指向的是字符串的第 0 个字符;我们通常将第 0  个字符的地址称为字符串的首地址。字符串中每个字符的类型都是char,所以 str 的类型也必须是char *

到底使用字符数组还是字符串常量

  • 在编程过程中如果只涉及到对字符串的读取,那么字符数组和字符串常量都能够满足要求;如果有写入(修改)操作,那么只能使用字符数组,不能使用字符串常量

获取用户输入的字符串就是一个典型的写入操作,只能使用字符数组,不能使用字符串常量

9.6 C语言指针变量作为函数参数

在C语言中,函数的参数不仅可以是整数、小数、字符等具体的数据,还可以是指向它们的[指针]。用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁

用数组作函数参数

数组是一系列数据的集合,无法通过参数将它们一次性传递到函数内部,如果希望在函数内部操作数组,必须传递数组指针

9.7 C语言指针作为函数返回值

C语言允许函数的返回值是一个[指针](地址),我们将这样的函数称为指针函数

  • 用指针作为函数返回值时需要注意的一点是,函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误

9.8 C语言二级指针(指向指针的指针)详解

  • 指针变量也是一种变量,也会占用存储空间,也可以使用&获取它的地址。C语言不限制指针的级数,每增加一级指针,在定义指针变量时就得增加一个星号*。p1 是一级指针,指向普通类型的数据,定义时有一个*;p2 是二级指针,指向一级指针 p1,定义时有两个*

9.9 C语言指针数组(数组每个元素都是指针)详解

如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。指针数组的定义形式一般为:

dataType *arrayName[length];

[ ]的优先级高于*,该定义形式应该理解为:

`[ ]`的优先级高于`*`,该定义形式应该理解为:

括号里面说明arrayName是一个数组,包含了length个元素,括号外面说明每个元素的类型为dataType *

9.10 C语言二维数组指针(指向二维数组的指针)详解

  • [二维数组]在概念上是二维的,有行和列,但在内存中所有的数组元素都是连续排列的,它们之间没有“缝隙”

指针数组和二维数组指针的区别

指针数组和二维数组指针在定义时非常相似,只是括号的位置不同:

1.  int *(p1[5]); //指针数组,可以去掉括号直接写作 int *p1[5];
1.  int (*p2)[5]; //二维数组指针,不能去掉括号

9.11 C语言函数指针(指向函数的指针)详解

  • 一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。我们可以把函数的这个首地址(或称入口地址)赋予一个[指针]变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针
    函数指针的定义形式为:
returnType (*pointerName)(param list);

returnType 为函数返回值类型,pointerName 为指针名称,param list 为函数参数列表。参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称,这一点和函数原型非常类似。 注意( )的优先级高于*,第一个括号不能省略,如果写作returnType *pointerName(param list);就成了函数原型,它表明函数的返回值类型为returnType *

10. C语言结构体详解

  • C语言结构体(Struct)从本质上讲是一种自定义的数据类型,只不过这种数据类型比较复杂,是由 int、char、float 等基本类型组成的

10.1 C语言结构体详解,C语言struct用法详解

在C语言中,可以使用结构体(Struct) 来存放一组不同类型的数据。结构体的定义形式为:

struct 结构体名{
    结构体所包含的变量或数组;
};

结构体变量

既然结构体是一种数据类型,那么就可以用它来定义变量。例如:

struct stu stu1, stu2;

定义了两个变量 stu1 和 stu2,它们都是 stu 类型,都由 5 个成员组成。注意关键字struct不能少
你也可以在定义结构体的同时定义结构体变量:

struct stu{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在学习小组
    float score;  //成绩
} stu1, stu2;

将变量放在结构体定义的最后即可

成员的获取和赋值

结构体和数组类似,也是一组数据的集合,整体使用没有太大的意义。数组使用下标[ ]获取单个元素,结构体使用点号.获取单个成员。获取结构体成员的一般格式为:

结构体变量名.成员名;

除了可以对成员进行逐一赋值,也可以在定义时整体赋值

10.2 C语言结构体数组详解

所谓结构体数组,是指数组中的每个元素都是一个结构体 例如:

struct stu{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组 
    float score;  //成绩
}class[5] = {
    {"Li ping", 5, 18, 'C', 145.0},
    {"Zhang ping", 4, 19, 'A', 130.5},
    {"He fang", 1, 18, 'A', 148.5},
    {"Cheng ling", 2, 17, 'F', 139.0},
    {"Wang ming", 3, 17, 'B', 144.5}
};

10.3 C语言结构体指针

当一个指针指向结构体时,我们就称它为结构体指针。C语言结构体指针的定义形式一般为:

struct 结构体名 *变量名;

注意,结构体变量名和数组名不同,数组名在表达式中会被转换为数组指针,而结构体变量名不会,无论在任何表达式中它表示的都是整个集合本身,要想取得结构体变量的地址,必须在前面加&,所以给 pstu 赋值只能写作:

struct stu *pstu = &stu1;

获取结构体成员

通过结构体指针可以获取结构体成员,一般形式为:

(*pointer).memberName

或者:

pointer->memberName

第一种写法中,.的优先级高于*(*pointer)两边的括号不能少。如果去掉括号写作*pointer.memberName,那么就等效于*(pointer.memberName),这样意义就完全不对了。

第二种写法中,->是一个新的运算符,习惯称它为“箭头”,有了它,可以通过结构体指针直接取得结构体成员;这也是->在C语言中的唯一用途。

上面的两种写法是等效的,我们通常采用后面的写法,这样更加直观

结构体指针作为函数参数

结构体变量名代表的是整个集合本身,作为函数参数时传递的整个集合,也就是所有成员,而不是像数组一样被编译器转换成一个指针。如果结构体成员较多,尤其是成员为数组时,传送的时间和空间开销会很大,影响程序的运行效率。所以最好的办法就是使用结构体指针,这时由实参传向形参的只是一个地址,非常快速

10.4 C语言枚举类型(C语言enum用法)详解

以每周七天为例,我们可以使用#define命令来给每天指定一个名字:

#include <stdio.h>

#define Mon 1
#define Tues 2
#define Wed 3
#define Thurs 4
#define Fri 5
#define Sat 6
#define Sun 7

int main(){
    int day;
    scanf("%d", &day);
    switch(day){
        case Mon: puts("Monday"); break;
        case Tues: puts("Tuesday"); break;
        case Wed: puts("Wednesday"); break;
        case Thurs: puts("Thursday"); break;
        case Fri: puts("Friday"); break;
        case Sat: puts("Saturday"); break;
        case Sun: puts("Sunday"); break;
        default: puts("Error!");
    }
    return 0;
}

#define命令虽然能解决问题,但也带来了不小的副作用,导致宏名过多,代码松散,看起来总有点不舒服。C语言提供了一种枚举(Enum)类型,能够列出所有可能的取值,并给它们取一个名字 枚举类型的定义形式为:

enum typeName{ valueName1, valueName2, valueName3, ...... };

enum是一个新的关键字,专门用来定义枚举类型,这也是它在C语言中的唯一用途;typeName是枚举类型的名字;valueName1, valueName2, valueName3, ......是每个值对应的名字的列表。注意最后的;不能少。 例如,列出一个星期有几天:

enum week{ Mon, Tues, Wed, Thurs, Fri, Sat, Sun };

可以看到,我们仅仅给出了名字,却没有给出名字对应的值,这是因为枚举值默认从 0 开始,往后逐个加 1(递增);也就是说,week 中的 Mon、Tues ...... Sun 对应的值分别为 0、1 ...... 6。

我们也可以给每个名字都指定一个值:

enum week{ Mon = 1, Tues = 2, Wed = 3, Thurs = 4, Fri = 5, Sat = 6, Sun = 7 };

更为简单的方法是只给第一个名字指定值:

enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };

这样枚举值就从 1 开始递增,跟上面的写法是等效的

10.5 C语言共用体

共用体(Union) ,它的定义格式为:

union 共用体名{\
    成员列表\
};
  • 结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员
  • 结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉