美文网首页程序员
C语言知识点(上)

C语言知识点(上)

作者: __bba3 | 来源:发表于2020-05-01 20:45 被阅读0次

    1.源码文件如何变成可执行文件(*)

    需要以下4个步骤:
    (1)预处理阶段:预处理器根据以#开头的指令,修改主要包括#include、#define和条件编译三个方面,修改源码内容,比如如果源码中有 #include<stdio.h> 则预处理器会读取文件 stdio.h 文件中的内容,并将其直接插入到原来的源码文件中,通常另存为以 .i 为扩展名的文件。(gcc -E hello.c -o hello.i)
    (2)编译阶段:编译器读取 .i 文件中的内容,并将其翻译为以 .s 为扩展名的汇编语言文件。(gcc -S hello.c -o hello.s)
    (3)汇编阶段:汇编器将 .s 文件翻译成机器码,并保存为 .o为扩展名的文件。
    (gcc -c hello.s -o hello.o)
    (4)链接阶段:链接器将不同的 .o 文件合并到一起,组成最终的可执行文件;比如我们的程序里调用了 printf 函数,作为一个C标准函数,printf 单独存在于一个 printf.o 的文件中,那么链接器将会找到这个 printf.o 文件,将其中的内容合并到我们自己的 .o 文件中,生成可以被加载到内存中执行的文件。

    C语言主要分为几个版本:Old Style C、C89、C99和C11。其中,C89、C99和C11是标准语言规范,现在广泛使用的是C99。

    2.面向过程和面向对象的区别(*)

    (1)面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
    (2)面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为
    比如:五子棋
    面向过程:1.开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。
    面向对象:1.黑白双方,2、棋盘系统,负责绘制画面,3、规则系统,负责判定诸如犯规、输赢等。

    特点
    • 面向过程(蛋炒饭)
      优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
      缺点:没有面向对象易维护、易复用、易扩展
    • 面向对象(盖浇饭)
      优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
      缺点:性能比面向过程低

    3.基本数据类型

    (1)数值类型

    数值类型分为整型(整数)和浮点型(小数)。按照表示数字范围的从大到小。

    <1>整型

    整数分为五类:字符型(char)、短整型(short)、整型(int)、长整型(long)和长长整型(long long)

    <2>浮点型

    浮点型分三类:单精度型(float)、双精度型(double)和长双精度型(long double)

    (2)获取类型的大小

    关键字sizeof:查看变量或者类型大小。


    (3)字节

    sizeof获得数据的单位是Byte(字节)。Byte(字节)是计量存储容量的一种计量单位,一个字节是8位二进制,可容纳256个数字。一个ASCII字符就是一个字节。

    用B表示Byte(字节),用b表示bit(比特/位).

    (4)输入输出格式化

    double的输入占位符必须是%lf,输出占位符可以是%f。

    (5)整数类型

    <1>表示范围

    类型的表示范围与类型大小存在如下关系:

    n表示的是bit位,比如:char是一个字节,8位,int是4个字节,32个二进制位,由于存在负数,所以数的大小会减半。

    <2>无符号整型

    在一些特殊情况下,数据只用0和整数,不存在负数,这时可以使用无符号整型unsigned。无符号整型只是在原来的整型前加上关键字unsigned。因为没有负数,所以数值的表示范围扩大了一倍。


    总之,类型的表示范围与类型大小存在如下关系:

    <3>整数类型的选择
    • 大多数情况下使用int。
    • 如果int范围不够,使用long long。
    • 避免使用long。
    • 谨慎使用unsigned。

    (5)浮点类型

    <1>浮点数的范围
    示例 输出 含义
    1.0/0.0 inf 表示正无穷大
    -1.0/0.0 -inf 表示负无穷大
    0.0/0.0 nan 不存在
    <2>浮点数的精度

    注意:浮点类型没有无符号unsigned类型。

    如何比较浮点数?使用最小误差。

       float a = 10.2;  
       float b = 9;  
       float c = a - b;
       if(fabs(c - 1.2) < 0.000001){
           printf("%f == 1.2\n",c);//1.200000 == 1.2
    }
    

    在误差范围内认为相等。即绝对值差小于精度最小值。
    float浮点数误差通常为1-6(6位有效数字)。
    double浮点数误差通常为1-15(15位有效数字)。

    <3>浮点类型选择
    • 大多数情况下使用double。
    • 尽量不要使用float。
    • 过程运算可以使用long double。

    既然浮点数这么不准确,为什么还需要?
    浮点数通过损失精度,获取更大的表示范围。

    (6)字符类型

    字符类型是一个特殊类型,是整型的一种。使用单引号表示字符字面量,例如:字母'a'、数字'1'、空字符''、转义字符\n。

    • 通常使用%c作为格式化占位符输入输出,有时也可以使用%d输出字符对应ASCII编码。(C语言中字符即数字。)
    <1>ASCII编码

    ASCII编码使用7 位二进制数(剩下的1位二进制为0)来表示所有的大写和小写字母,数字0 到9、标点符号, 以及在美式英语中使用的特殊控制字符。

    • ASCII表的特点:
      1.字母在ASCII表中是顺序排列的。
      2.大写字母和小写字母是分开排列的。
    <2>运算
    • 字符类型可以像整型一样参与运算。
      1.一个字符加上一个数字得到ASCII表中对应新字符。
      2.两个字符相减,得到这两个字符在表中的距离
    <3>转义字符/逃逸字符

    在ASCII表中,除了常见的字符(如:大小写字母、数字等),还包含一些无法打印出来的控制字符或特殊字符。这些字符以反斜线\开头,后面接着一个字符,这种字符被称作转义字符/逃逸字符。

    (7)布尔类型

    在C99中新增bool类型表示逻辑值,它只有两种值(true和false)。使用前需要加上头文件stdbool.h

    (8)数据类型转换

    <1>自动类型转换

    当运算符左右两边操作数的类型不一致时,会自动转换成较大类型。

    • 整型:charshortintlonglong long
    • 浮点型:intfloatdoublelong double
    <2>强制类型转换

    当把一个较大的类型转换成较小的类型,需要强制转换。
    强制转换语法:

    (转换后的类型)值
    
    • 浮点数转整数采用的是截断方式。

      printf("%d\n",(int)3.14);//3
      
    • 整型转浮点型也需要强制转换。

      printf("%f\n",(double)2);//2.000000
      

    4.运算符

    (1)算术运算符

    + 、-、*、 /(取整)、 %(取余)
    

    (2)关系运算符

    ==、!=、>、 <、 >=、 <=
    

    (3)逻辑运算符

    && 、  || 、   !
    

    闰年判断:year%4==0&&year%100!=0 || year%400==0(year能被4整除 and 不能被100整除 or year能被400整除 )

    (4)复合赋值运算符

    +=、-=、*=、/=、%=
    

    (5)自增、自减运算符

    自增运算符与自减运算符优先级高于算术运算符。

    <1>前缀自增/自减
    ++a、--a
    
    <2>后缀自增/自减

    (6)运算的优先级顺序

    • 自增/自减的优先级大于算数运算符的优先级;
    • 自增/自减的优先级大于解引用*的优先级
    • 中括号[ ]的优先级大于解引用*的优先级
    • 结构体中用点.的优先级大于取地址&的优先级
    • 结构体中用点.的优先级大于接引用*

    5.变量

    初始化和赋值:

    初始化时在生成变量时放入数值,赋值是在已经生成变量时放入数值。

    6.控制语句

    (1)条件判断

    <1>if-else

    代码块与if之间使用空格或者Tab缩进,不影响编译和执行,只是为了提高代码可读性。

    if(condition1){
    
    }else if(condition2){
    
    }else{
    
    }
    
    <2>switch case
    switch(表达式){
        case 整型常量1:
           /* 表达式等于整型常量1执行的代码 */
           break; /* 可选的 */
        case 整型常量2:
           /* 表达式等于整型常量2执行的代码 */
           break; /* 可选的 */
        default : /* 可选的 */
           /* 表达式不等于上面所有情况执行的代码 */
    }
    

    (2)循环

    <1>while语句
    while(条件){
       /* 如果条件为真将重复执行的语句 */
    }
    
    <2>do-while
    do {
       /* 如果表达式为真将重复执行的语句 */
    }while(条件);//注意while()后的分号;。
    

    do-while循环是先循环后判断,循环体至少执行一次;while循环是先判断后循环,循环体可能一次也不执行。

    <3>for
    for (初始值;条件;递增或递减){
       /* 如果条件为真将重复执行的语句 */
    }
    

    在while和for循环中,break是结束一个循环体;continue是结束单次循环。

    (3)简化

    <1>省略大括弧

    如果if语句、while语句、for语句中只有一个执行语句,可以省略大括弧。

    <2>三元运算符:?

    如果if-else语句只有单个执行语句,可以使用三元运算符:?。

    7.进制

    (1)转换

    <1>十进制转R进制

    十进制转任何进制用短除法。从下往上来统计。

    <2>R进制转十进制

    加权和。

      0x2A=2*16^1+A*16^0=32+10=42
    

    代码:

    2进制数1011转10进制:1
    (1*2)+0)*2+1)*2+1)=11
    int res=0;
    res=res*2+每一位
    

    (2)C语言中的进制

    <1>进制常量表示

    C语言不能直接表示二进制常量。八进制数字以0开头,十六进制数字以0x或0X开头。

    <2>进制打印(输出)

    进制的输出其实与字符输出是一样的,根据占位符的不同输出不同。

    • 十进制:%d
    • 八进制:%#o
    • 十六进制:%#x
    char a = 'a';
    printf("%c\t%d\t%#o\t%#x\n",a,a,a);//a,97,0141,0x61
    
    <3>进制输入
    • 10进制:%d
    • 8进制:%o
    • 16进制:%x
    scanf("%o",&n);//010
    printf("%d\n",n);//8
    scanf("%x",&n);//0xa
    printf("%d\n",n);//10
    
    <4>%i

    %i 可以匹配八进制、十进制、十六进制表示的整数。
    例如: 如果输入的数字有前缀 0(018),%i将会把它当作八进制数来处理,如果有前缀0x (0x54),它将以十六进制来处理。

    scanf("%i",&n);//0xa
    printf("%d\n",n);//10
    

    8.文件操作

    (1)文件输入输出

    • 使用printf()和命令行重定向>实现文件输出;
    • 使用scanf()和命令行重定向<实现文件输入。
    <1>实例
    • hello.c
    char name[256];
    scanf("%s",name);
    printf("Hello %s\n",name);
    
    • 编译

      gcc hllo.c -o hello
      
    • 执行

    echo zhangsan > namefile   //把张三重定向到namefile文件中
    ./hello < namefile > output   //把输入定向到./hello 中,然后把执行结果定向到output中。
    

    (2)文件的打开和关闭fopen和fclose

    • 头文件
      <stdio.h>
    <1>打开文件fopen
    FILE *fopen(const char *pathname, const char *mode);
    
    • 参数
    No. 参数 作用
    1 filename 需要打开的文件
    2 mode 文件打开方式
    • 返回值
    No. 类型 说明
    1 成功 返回值是指向这个文件流的文件指针
    2 失败 NULL
    • 打开方式
    No. 打开方式 含义
    1 r(read)
    2 w(write) 写,不存在创建,存在清空写入(w),追加(a)
    3 a(append) 追加
    4 +(plus) 读或写,主要是配合r、w、a使用
    5 t(text) 文本文件(默认)
    6 b(binary) 二进制文件
    <2>关闭文件fclose
    int flcose(FILE* stream);
    
    • 参数
      stream文件指针。
    • 返回值
      如果成功释放,返回0; 否则返回EOF(-1).

    (3)文本读写fprintf()fscanf()

    <1>函数原型
    int printf(const char *format, ...);
    int fprintf(FILE *stream, char *format, argument...);
    int fscanf(FILE *stream, char *format, argument... );
    

    fprintf()/fscanf()printf()/scanf()使用非常相似,区别在于fprintf()/fscanf()第一个参数stream是文件描述符。

    <2>用法示例
    • 从文件中读出数据
    int i ;
    float f ;
    char c;
    char str[10];
    fscanf(fp, "%d %f %c %s\n", &i, &f, &c, str); //字符串是不需要加&
    
    • 将数据写入文件
    int i = 10;
    float f = 3.14;
    char c = 'C';
    char str[10] = "haha";
    fprintf(fp, "%d %f %c %s\n", i, f, c, str);
    

    如果不需要从文件里面写入字符串,那么就可以用逗号或者其他符号来分隔;如果文件里需要写入字符串,那么字符串与其他数据之间只能用空格和回车来分隔。

    <3>实例
    • 将多个学生信息写入文件并读出。
    struct Student {
        char  name[32];   //姓名
        int  age;     //年龄
        float  score;   //成绩
    };
    int main() {
        //打开文件
        FILE* fp =fopen("./info.txt","r");
        if(NULL==fp) {
            perror("文件fp打开失败");
            return 1;
        }
        //从文件中读取数据
        int n;
        fscanf(fp,"%d",&n);
        struct Student student[n];
        for(int i=0; i<n; ++i) {
            fscanf(fp,"%s %d %f",student[i].name,&student[i].age,&student[i].score);
        }
        //将数据写入文件
        FILE* fq=fopen("./res.txt","w");
        if(NULL==fq) {
            perror("fq打开失败");
            return 1;
        }
        for(int i=0; i<n; ++i) {
            fprintf(fq,"%s\t%d\t%f\n",student[i].name,student[i].age,student[i].score);
        }
        //关闭文件
        fclose(fq);
        fclose(fp);
    }
    

    (4)二进制读写:fread()和fwrite()

    对于二进制文件的数据读取和写入是一对,只有以二进制的方式写入到文件才能读出来,不能从文本文件中读取数据。

    <1>函数原型
    size_t fread(void *ptr, size_t size, size_t count, FILE* stream);
    size_t fwrite(void *ptr, size_t size, size_t count, FILE* stream);
    
    • 参数
    No. 参数 作用
    1 ptr 一个指针,在fread()中是从文件里读入的数据存放的地址;在fwrite()中是写入到文件里的数据存放 的地址。
    2 size 每次要读写的字节数
    3 count 读写的次数
    4 stream 文件指针
    • 返回值
      成功读取/写入的字节数。
    <2>用法示例
    • 从文件中读出字符串
    char str[100];   
    fread(str, sizeof(str), 1, fp);//每次读取sizeof(str)个字节,读一次
    或者:
    fread(str,1,sizeof(str),fp);//每次读取1个字节,读取sizeof(str)次,和上面一样。
    
    • 将字符串写入文件
    char str[] = "Hello World";
    fwrite(str, sizeof(str), 1, fp);
    或者:
    fwite(str,1,sizeof(str),fp);
    
    <3>实例
    • 往文件中写数据(结构体)
    typedef struct {
        char  name[32];   //姓名
        int  age;     //年龄
        float  score;   //成绩
    }Student;
    int main(){
        FILE* fp = fopen("./1.txt","wb");
        if(NULL==fp){
            perror("open failed");
            return 1;
        }
        Student student[2];
        strcpy(student[0].name,"张三");//结构体中的字符串赋值要使用strcpy
        student[0].age=20;
        student[0].score=92.8;
        strcpy(student[1].name,"李四");
        student[1].age=22;
        student[1].score=76.2;
        for(int i=0;i<2;++i){
            fwrite(&student[i],sizeof(Student),1,fp);
        }
        fclose(fp);
    }
    
    • 从文件中读数据(结构体)
    int main() {
        //打开文件
        FILE* fp =fopen("./1.txt","rb");
        if(NULL==fp) {
            perror("文件fp打开失败");
            return 1;
        }
        Student student;//由于不知道读取的个数,所以使用while循环
        while(fread(&student,sizeof(Student),1,fp)){//读到数据就显示,否则就退出
            printf("%s\t%d\t%f\n",student.name,student.age,student.score);
        }
        fclose(fp);
    }
    

    (1)写操作fwrite()后必须关闭流fclose()。
    (2)不关闭流的情况下,每次读或写数据后,文件指针都会指向下一个待写或者读数据位置的指针。
    (3)sizeof和strlen的区别,否则就会读不到数据(在读数据时,使用strlen(buf))或者读到一半数据(写数据时,使用sizeof(msg)=8)。
    (4)对于二进制文件的写入和读取是同时存在的,不能从普通文件中读取数据。
    (5)其他类型数据的读取:https://www.cnblogs.com/xudong-bupt/p/3478297.html

    <4>二级制文件和文本文件
    比较 文本 二进制
    优势 便于人类读写,跨平台 文件较小,机器读写比较快
    劣势 文件较大,机器读写比较慢 不便人类读写,不跨平台
    配置 Unix用文件 Windows用注册表
    • 说明:
      (1)Unix喜欢用文本文件来做数据存储和程序配置。
      (2)windows喜欢用二进制文件。
      (3)数据量较多使用数据库
      (4)多媒体使用二进制
      (5)通常使用第三方库读写文件,很少直接读写二进制文件。

    (5)文件定位:ftell()fseek()

    <1>函数原型
    // 获取位置
    long ftell(FILE* stream);
    // 设置位置
    int fseek(FILE* stream,long offset,int whence);
    
    • 参数
    No. 参数 含义
    1 stream 文件指针
    2 offset 偏移量,基于起始点偏移了offset个字节
    3 whence 起始点
    No. whence 数值 含义
    1 SEEK_SET 0 从头开始
    2 SEEK_CUR 1 从当前开始
    3 SEEK_END 2 从结束开始
    • 返回值
      ftell()返回文件指针当前位置,基于文件开头的偏移字节数。
    <2>示例:
    fseek(stream, 0, SEEK_END);
    // 将文件指针指向文件结尾,并偏移了 0 个字节,也就是直接将文件指针指向文件结尾
    fseek(stream, -10, SEEK_CUR);
    // 将文件指针指向当前位置,并偏移了 -10 个字节,也就是将文件指针往前移动10个字节
    
    应用

    获取文件大小。

        char path[1024];
        scanf("%s",path);
        FILE* fp = fopen(path,"r");
        if(fp){
            fseek(fp,0,SEEK_END);
            long size = ftell(fp);
            printf("%ldB\n",size);
        }
    

    (6)文件结尾判断feof()

    <1>函数原型
    int feof(FILE* stream);
    
    • 返回值
      一旦文件指针指向文件结尾,就返回一个真值;否则返回非真值。

    (7)返回开头rewind()

    <1>函数原型
      void rewind(FILE* stream);
    
    <2>举例
    FILE *fp = fopen("./text.txt", "r+");
    fseek(fp, 0, SEEK_END);   // 将文件指针指向文件结尾
    long len = ftell(fp);     // 获取文件指针位置,得到文件的大小(Byte)
    rewind(fp);               // 将文件指针重新指向文件开头
    

    (8) 清空数据流fflush()

    <1>函数原型
    void fflush(FILE* stream);
    
    <2>使用

    在使用多个输出函数连续进行多次输出时,有可能发现输出错误。因为下一个数据再上一个数据还没输出完毕,还在输出缓冲区中时,下一个printf就把另一个数据加入输出缓冲区,结果冲掉了原来的数据,出现输出错误。 在 prinf();后加上fflush(stdout); 强制马上输出,避免错误。

    (9)文件重名名rename

     int rename(const char *old_filename, const char *new_filename);
    

    (10)文件删除remove

    int remove(char * filename);
    

    (11)putcgetc

    <1>函数原型
    int getc ( FILE * stream );//从stream中获取一个字符
    int putc ( int character, FILE * stream );//将字符写入到stream中
    
    <2>实现cp命令
    int main(int argc,char* argv[]){
        if(3!=argc){
            perror("argc error");
            return 1;
        }
        FILE* srcfp=fopen(argv[1],"rb");
        if(NULL==srcfp){
            perror("srcfile error");
            return 1;
        }
        FILE* dstfp=fopen(argv[2],"wb");
        if(NULL==dstfp){
            perror("dstfile error");
            return 1;
        }
        while(!feof(srcfp)){//到达文件末尾返回true,否则返回flase
            putc(getc(srcfp),dstfp);
        }
        fclose(srcfp);
        fclose(dstfp);
    }
    

    9.宏定义

    (1) 宏定义是什么?

    宏是用来表示一段代码的标识符。

    宏也是标识符,也要满足标识符的规则。但通常习惯使用大写字母和下划线命名

    (2)宏定义怎么用?

    <1>宏定义通常有三种用法:
    • 当作常量使用。
    • 当作函数使用。
    • 编译预处理。
    <2>宏定义常量
    • 预定义宏
      ANSI C标准有定义好的宏定义,称为预定义宏。这些宏定义以双下划线__开头结尾。
    printf("%s:%d",__FILE__,__LINE__);//源文件名:当前语句所在的行号
    printf("%s:%s",__DATE__,__TIME__);//月 日 年 时间
    
    • 自定义宏
      除了使用标准定义的宏,可以使用#define指令用来定义一个宏。

    (1)语法

      #define 标识符 值
      #define PI 3.1415926
    

    (2)说明

    • 注意没有结尾的分号,因为不是C的语句。

    • 名字必须是一个单词,值可以是各种东西。

    • 在C语言的编译器开始之前,编译预处理程序会把程序中的名字换成值,是完全的文本替换。

    • 如果一个宏的值有其他宏的名字,也会被替换

      #define PI_2 2*PI
      
    • 如果一个宏的值超过一行,最后一行之前行末需要加\

      #define PI_2 2 \
                   * \
                   PI
      
    • 宏的值后面出现的注释不会被当做宏的值的一部分。

      #define PI_2 2*PI  // 二倍的PI
      
    <3>带参数的宏

    宏可以带参数,使用上有些像函数。这种宏称为带参数的宏。

    • 语法
    #define 标识符(参数...) 代码
    示例:
    #define square(x) ((x)*(x))
    #define cube(x) ((x)*(x)*(x))
    
    • 错误示范
    #define square(x) x*x
    square(10);//100
    square(10+1);//10+1*10+1=21
    
    • 说明
      上面因为缺少括号导致错误,称为宏定义边际效应,所以带参数的宏需要在以下两个位置加上括号:
      (1)参数出现的每个地方都要加括号。
      (2)整个值要加括号
    • 参数的宏也可以有多个参数
    #define MIN(a,b) ((a)<(b)?(a):(b))
    

    swap函数:

    #define SWAP(m,n) {\
        int t=m;\
        m=n;\
        n=t;\
    }
    

    尽量避免使用宏定义。

    <4>编译预处理

    有时我们会使用没有值的宏,这种宏用于条件编译的,#ifdef #ifndef用于检查宏是否被定义过。控制代码的编译。

    #define TEST
    
    #ifdef TEST
        printf("Test\n");//如果前面定义了:#define TEST,则执行这个,否则执行后面的。
    #else
        printf("No Test\n");
    #endif
    

    (3)宏展开

    宏的本质是指编译前(编译预处理阶段),用定义中的值或者代码完全替换宏的标识符。
    只替换条件编译中的宏。使用下面指令来查看宏的展开。

        gcc -E hello.c -o hello.i
    

    (4)编译预处理指令

    #开头的都是编译预处理指令。除了宏定义,还有文件包含#include和条件编译指令#if、#ifdef #ifndef、#else、#elif、#endif,一般写在.h文件中。

    • 文件包含#include,把文件内容包含到代码中。
    • 条件编译指令,根据编译条件,选择编译或者编译某段代码。
    • 格式
    #ifndef __HELLO_H
    #define __HELLO_H
    函数声明(函数原型)
    #endif //__HELLO_H
    

    10.头文件

    (1)经验

    编写小的程序可以把代码写在一个文件中,当编写大程序中,需要把代码分在多个文件中。

    • 多个源代码文件
      (1)main()里面代码太长适当分成几个函数。
      (2)一个源代码文件太长适当分成几个文件。
      (3)两个独立的源代码文件不能编译成可执行文件。

    (2)头文件概念

    <1>#include指令

    把函数原型放到一个头文件.h,在需要调用这个函数的源代码文件.c,使用#include指令包含这个头文件,使编译器在编译的时候知道函数的原型。

    <2>头文件作用

    头文件主要用于编译器的编译阶段,告诉编译器在代码中使用了这么一个函数。只是告诉编译器函数的原型,保证调用时参数类型和个数正确

    (3)头文件的使用

    在使用和定义函数的地方都要#include头文件,#include指令不一定要放在.c文件的最前面,但是通常习惯这样做。

    #include指令分类

    #include指令有两种形式:
    (1)#include <>:编译器到指定目录查找,主要用于查找标准库的头文件。
    (2)#include "":编译器先到当前目录查找(.c文件所在目录),如果没有再到指定目录查找。

    #include指令是一个编译预处理指令,和宏一样,在编译之前就处理了。它会把指定的文件原封不动的插入到它所在的地方。

    (4)头文件怎么写

    头文件通常用来存放所有对外公开的函数的原型和全局变量的声明
    通常任何.c文件都有对应同名的.h文件

    <1>声明
    • 常见的声明
      • 函数声明
      • 变量声明
      • 结构体声明
      • 宏声明
      • 枚举声明
      • 类型声明

    通常声明只能可以放在头文件中,否则,编译器连接会出现重名函数错误。

    • 重复声明
      在一个编译单元中,不允许重复声明,尤其是结构体声明。为了防止头文件不被多次#include导致重复声明。
    • 定义与声明
      声明是不产生代码的语句。定义是产生代码的语句。
    int i;// 变量的定义,在.c文件中
    extern int i; // 变量的声明,在.h文件中
    
    <2>标准头文件结构

    避免头文件多次包含,必须使用标准头文件结构。

    #ifndef _文件名_H__
    #define _文件名_H__
    // 声明
    #endif
    

    使用条件编译和宏,保证头文件在一个编译单元中只会#include一次。

    #pragma once指令也起到相同作用,但是并不是所有编译器支持。

    • 分析



      在cord.cpp 中第一次遇到coordin.h的文件的时候,它的内容会被读取,找到#ifndef COORDIN_H_,并且会给COORDIN_H_设定一个值,之后再次看到coordin.h头文件时,COORDIN_H_就已经被定义了,coordin.h的内容就不会再次被读取了。

    • 误区:
      (1)#include不是用来引入库的,是告诉编译器函数声明,
      (2)头文件只有函数原型,函数实现在.a(Unix)或者.lib(Windows)中。
      (3)现代的C语言编译器会默认引入所有的标准库。

    11. 变量的作用域和生存周期

    (1)作用域

    <1>作用域是什么?

    在什么范围内可以访问这个变量。

    <2>作用域怎么用?

    局部变量的作用域在变量定义的大括号以内。

    (2)生存周期

    <1>生存周期是什么?

    变量什么时候出现到什么时候灭亡。对于局部变量,生存期与作用域一致。

    <2>使用

    不要返回局部变量的地址(注意是地址,不是值),返回局部变量的值是可以的。

    • 认识
    string& test_str(){
        string str = "test";
        return str;
    }
    int main(){
        string& str_ref = test_str();
        cout << str_ref << endl;
        return 0;
    }
    
    A.编译警告
    B.返回局部变量的引用,运行时出现未知错误
    C.正常编译且运行
    D.把代码里的&都去掉之后,程序可以正常运行
    
    • 分析:
      ABD。
      (1)在C语言中,局部变量是分配在栈空间上的, 当函数调用结束后,由编译器释放.
      (2)通过调用test_str得到了他的局部变量的内存地址, 然而在main函数中调用函数时,这个内存地址被”破坏”了,类似于野指针。在c语言中,一种典型的错误就是将一个指向局部变量的指针作为函数的返回值
      (3) 如果返回指针(变量地址),应该返回堆区或者全局区的地址(全局变量或者静态变量)

    (3)同名隐藏

    在相同作用域中,同名变量会报错;在不同的作用域中,内部变量会隐藏外部变量,

    int main() {
        int n = 1;
        {
            printf("n = %d\n",n);//1
            int n=10;
            printf("n = %d\n",n);//10
            n = 20;
        }
        printf("n = %d\n",n);//1
    }
    

    12.变量分类

    (1)本地变量/局部变量

    <1>概念

    在大括号内定义的变量就是本地变量/局部变量。

    <2>特点
    • 1.本地变量是定义在代码块内的,可以定义在函数的块内,可以定义在语句的块内(for),可以定义在一个随意的大括弧里面。
    • 2.程序进入块前,内部变量不存在,离开时失效。
    • 3.块外定义的变量,块内仍然有效。(反过来不行)

    函数的每次运行,都会产生一个独立的变量空间,在这个空间中的变量,是函数这次运行独有的。
    1.定义在函数内部的变量就是本地变量;2.参数也是本地变量

    <3>初始化
    • 1.本地变量不会默认初始化
    • 2.参数在进入函数时被初始化。

    本地变量/局部变量的生存期和作用域都是在大括号内。

    (2)全局变量

    <1>定义

    定义在函数外面的变量称为全局变量。

    int n;//全局变量
    int main(){
        int m;//局部变量
    }
    
    <2>特点

    全局变量有全局的生存周期和作用域。

    • 1.不属于任何函数。
    • 2.所有函数内部都可以使用。
    <3>初始化
    • 没有初始化的全局变量会自动初始化为0。
    • 只能用编译时刻已知的值初始化全局变量。(只能用常量来初始化全局变量)
    • 初始化发生在main()前。
    <4>同名隐藏

    如果函数内部存在与全局变量同名的变量,则全局变量被隐藏。

    全局变量有全局的生存周期和作用域。

    (3)局部静态变量

    <1>定义

    在本地变量定义时加上static变成静态本地变量。(只初始化1次)

    <2>特点

    当函数离开时,静态局部变量会继续存在并保存其值。

    int inc(){
        static int n = 1;
        n = n + 1;
        return n;
    }
    int main(){
        printf("%d\n",inc());//2
        printf("%d\n",inc());//3
        printf("%d\n",inc());//4
    }
    

    (1)静态本地变量的初始化在第一次进入函数时执行,以后进入函数会保持离开的值。
    (2)静态本地变量是特殊的全局变量,具有全局生存周期和局部作用域。

    (4)全局静态变量

    <1>定义

    在全局变量前加上关键字static。

    <2>全局变量与全局静态变量区别
    • 1.若程序由一个源文件构成时,全局变量与全局静态变量没有区别。
    • 2.若程序由多个源文件构成时:
      非静态的全局变量的作用域是整个源程序,非静态的全局变量在各个源文件中都是有效的,而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。
    • 3.非静态全局变量具有外部链接的静态,可以在所有源文件里调用,除了本文件,其他文件可以通过extern的方式引用。
    extern int a,b;//在.h文件中全局变量的声明
    

    (5)变量的作用域和生命期总结

    • 作用域
      变量或函数在运行时候的有效作用范围
    • 生命期
      变量或函数在运行时候的没被销毁回收的存活时间。


    (6)static关键字小结

    static在C语言里面既可以修饰变量,也可以修饰函数。

    <1>static变量
    • 静态局部变量:在函数中定义的,生命周期是整个源程序,但是作用域和局部变量没区别。只能在定义这个变量的函数范围内使用,而且只在第一次进入这个函数时候被初始化,之后的初始化会跳过,并保留原来的值。退出这个函数后,尽管这个变量还在,但是已经不能使用了。
    • 静态全局变量:全局变量本身就是静态存储的,但是静态全局变量和非静态全局变量又有区别:
      1.全局变量:变量的作用域是整个源程序,其他源文件也可以使用,生命周期整个源程序。
      2.静态全局变量:变量的作用域范围被限制在当前文件内,其他源文件不可使用,生命周期整个源程序。

    静态变量的生命周期是整个源程序,而且只能被初始化一次,之后的初始化会被忽略。(如果不初始化,数值数据将被默认初始化为0, 字符型数据默认初始化为NULL)。

    <2>static函数(内部函数)

    只能被当前文件内的其他函数调用,不能被其他文件内的函数调用,主要是区别非静态函数(外部函数)。

    1.在函数前面加上static就使它成为只能所在编译文件中使用的函数。
    2.在全局变量前加上static使它成为只能所在编译文件中使用的全局变量

    (7)实践经验

    <1>不要返回本地变量的指针
    • 返回本地变量的地址是危险的。
    • 返回全局变量或静态本地变量的地址是安全的。
    • 返回函数内的动态内存是安全的,但注意要记得释放。
    • 最好的做法是返回传入的指针。(常用)
    <2>慎用静态变量
    • 不要使用全局变量在函数间传递参数和结果。
    • 尽量避免使用全局变量。
    • 使用全局变量和静态本地变量的函数是不可重入的,是线程不安全的。

    13.内存

    (1) 结构体字节对齐

    在C语言里,结构体所占的内存是连续的,但是各个成员之间的地址不一定是连续的。所以就出现了"字节对齐"。

    字节对齐默认原则
    • 结构体变量的大小,一定是其最大的数据类型的大小的整数倍,如果某个数据类型大小不够,就填充字节。
    • 结构体变量的地址,一定和其第一个成员的地址是相同的。
    struct Box{
        int height;
        char a[10];
        double width; 
        char type;
    };
    int main(void) {
        struct Box box;
        printf("box = %p\n", &box);
        printf("box.height = %p\n", &box.height);
        printf("box.a = %p\n", box.a);
        printf("box.width = %p\n", &box.width);
        printf("box.type = %p\n", &box.type);
        printf("box = %ld\n", sizeof(box));
    }
    

    (2)内存四区

    image.png
    <1>栈区(stack)

    由编译器自动分配和释放,主要是存放函数参数的值,局部变量的值。
    比如:int a; int *p; 这儿的a和p都存放在栈中

    <2>堆区(heap)

    由程序员自己申请分配和释放,需要malloc()、calloc()、realloc()函数来申请,用free()函数来释放如果不释放,可能出现指针悬空/野指针。

    函数不能返回指向栈区的指针,但是可以返回指向堆区的指针。

    <3> 数据区(data)

    ** 数据区在程序结束后由操作系统释放**

    • 存放常量。
      包含字符串常量和其他常量。 char *p = "I love u"; 指针p指向的这块内存属于常量区。
    • 存放全局变量和静态变量。
      初始化的全局变量和静态变量在一块区域(data段);未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,称作BSS(Block Started by Symbol:以符号开始的块);
    <4>代码区(code)

    用于存放编译后的可执行代码,二进制码,机器码。

      int a; //申请栈区内存
      a = 4; //指向的代码,放在代码区。
    

    (3)堆和栈的区别(重要)

    No. 比较方面
    1 管理方式 由系统自动管理,以执行函数为单位 由程序员手动控制
    2 空间大小 空间大小编译时确定(参数+局部变量) 具有全局性,总体无大小限制。
    3 分配方式 函数执行,系统自动分配;函数结束,系统立即自动回收 使用new/malloc()手动申请;使用delete/free()手动释放
    4 优点 使用方便,不需要关心内存申请释放。 可以跨函数使用。(可以返回指向堆区的指针)
    5 缺点 只能在函数内部使用。 容易造成内存泄露。

    (4)显示目标文件区段大小

    • size命令:

    dec与hex是前面三个区域的和,dec是十进制,hex是十六进制。

    • 各区段的含义
    No. 区段 名称 含义
    1 text 代码段(code segment/text segment) 存放程序执行代码的内存区域。该区域的大小在运行前已确定,且通常属于只读。可能包含一些只读的常数变量,例如字符串常量等。
    2 data 数据段(data segment) 存放程序中已初始化的全局变量的内存区域。数据段属于静态内存分配。
    3 bss BSS段(bss segment) 存放程序中未初始化的全局变量的内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。
    • 没有显示的区段
    No. 区段 含义
    1 栈(stack) 存放程序临时创建的局部变量,也就是函数括弧{}中定义的变量(不包括static声明的变量)。在函数被调用时,参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。
    2 堆(heap) 存放程序动态分配的内存段,大小并不固定,可动态扩张或缩减。当调用malloc()等函数分配内存时,堆被扩张;当调用free()等函数释放内存时,堆被缩减。

    14.二进制

    (1)位运算

    位运算说穿了,就是直接对整数在内存中的二进制位进行操作.

    <1>按位运算
    No. 操作符 功能
    1 & 按位与
    2 | 按位或
    3 ~ 按位取反
    4 ^ 按位异或(相异为真)
    <2>运算规则
    <3>按位与

    1.让某一位或某些位为0(清零)

    int n = 0xFFFF;
    n = n & 0x0010;//0x10
    

    2.取一个数中某些指定位:
    比如a=23,我想取a的二进制的后面4位数,那么可以找一个后4位是1其余位是0的数b,即b=0x0f(十六进制,转换为二进制为00001111),a&b就得到了a的后四位。

    a:00010111
    b:00001111
    a&b:00000111
    

    3.保留指定位:
    比如a=23(用8bit表示),我想保留其二进制的第4和第6位(最左边为第1位),其余位置0。那么可以找一个第4和第6位是1其余位是0的数b与a进行按位与运算.

    a:00010111
    b:00010100
    a&b:00010100
    

    4.应用:

    • 判断某一位是否为1;
      设置一个只有某一位是1的数,其余为是0,然后和判断的数进行位与。
    • 判断一个数是否是偶数;
      与1进行位与,如果其二进制的最末尾是0表示偶数,为1表示奇数。

    结论:任何二进制位与0能实现置0;与1保持原值不变.

    <4>按位或

    1.让某一位或某些位为1,其余位不变。

    int n = 0x0000;
    n = n | 0x0010;
    

    2.拼接两个二进制数。

    int a = 0xab00;
    int b = 0x0012;
    int c = a|b;//0xab12
    
    <5>按位取反

    1,得到全部为1的数字~0

    int n = ~0;// 等同于0xFFFF
    

    2.使数字的部分清零x& ~7。

    int n = 0xFFFF;
    n = n & ~7;
    
    <6>按位异或

    1.两个相等数异或结果为0。

    int n = 0x1234;
    n = n^n;
    

    2.对同一个变量两次异或相同值,变回原值。

    int a = 0x1234;
    int b = 0x1357;
    a = a^b;//0x163
    a = a^b;//0x1234
    

    3.0和任何数字(或字符)异或都为任何数(或字符)

    int n=21;
    n = n^0;//21
    

    4.应用:

    • 把一个数据的某些位翻转,即1变为0,0变为1
      如要把a的奇数位翻转,可以对a和b进行“按位异或”运算,其中b的奇数位置为1,偶数位置为0。
    • 交换两个值,不用临时变量(***)
    x ^= y;
    y ^= x;
    x ^= y;
    
    • 加密解密
      加密程序(a^b),解密程序是加密程序的逆过程,这里的加密和解密程序是完全相同的,原因是(a^b)^b=a。

    5.例题

    [136.只出现一次的数字]:
    思路:每个数字全部异或,相同的会为0,直到最后一个数字。
    [389.找不同]:
    思路:字符也可以异或,相同字符异或为0.

    逻辑运算与按位运算
    1.逻辑运算结果只有0和1两种值,按位运算有多种值。
    2.逻辑运算相当于把所有的非零值都变成1,再按位运算。

    (2)移位运算

    在移动过程中相当于操作二进制数

    No. 操作符 功能
    1 << 左移
    2 >> 右移
    <1>左移

    i<<j表示i中所有位向左移动j个位置,右边填入0
    左移一位相当于乘以2,两位乘以4。

    1<<1;//2
    1<<2;//4
    1<<3;//8
    
    <2>右移

    i>>j表示i中所有位向右移动j个位置,对于unsigned类型,左边填入0;对于signed类型,左边填入符号位。
    右移一位相当于除以2,再取整。

    14>>1;//7
    14>>2;//3
    14>>3;//1
    14>>4;//0
    

    1.应用:

    • 循环移位的实现:
      将一个无符号整数x的各位进行循环左移n位的运算,即把移出的高位填补在空出的低位处。

      b=(a<<n) | (a>>(16-n)) ;
      
    • 求x的绝对值

     y = x >> 31 ;//二进制最高位
     return (x^y)-y ; //or: (x+y)^y 
    
    <3>位移运算与乘除运算
    <4>综合

    求一个无符号数的二进制中1的个数。

    //就是判断最低位是否为1(最右边)
    int numof1(int n){
        int count=0;
        while(n){
            if(n & 1){
                ++count;
            }
            n = n >> 1;
        }
        return count;
    }
    

    (3)位域

    <1>定义

    位域是又称作位段,是把一个字节中的二进位划分为几个不同的区域。

    <2>作用

    节省空间,有些信息不需要占用一个完整的字节。

    <3>使用
    • 1.定义位域
      定义位域与结构定义相仿。
    struct 位域结构名{ 
        类型 位域名:位域长度;
    };
    

    为了保证位域的可以移植性,成员类型通常为unsigned intint,C99可以使用bool
    示例:

    struct Byte{
      unsigned int b1:1;
      unsigned int b2:1;
      unsigned int b3:1;
      unsigned int b4:1;
      unsigned int b5:1;
      unsigned int b6:1;
      unsigned int b7:1;
      unsigned int b8:1;
     };
    
    • 2.位域变量
      定义和使用位域变量与结构体相同。每个域有一个域名,允许在程序中按域名进行操作。

      struct Byte a;
      
    • 3.位域大小
      整个结构体的总大小为最宽基本类型成员大小的整数倍。

    相关文章

      网友评论

        本文标题:C语言知识点(上)

        本文链接:https://www.haomeiwen.com/subject/uenfghtx.html