C语言知识点(上)

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.位域大小
    整个结构体的总大小为最宽基本类型成员大小的整数倍。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,667评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,361评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,700评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,027评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,988评论 5 361
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,230评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,705评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,366评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,496评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,405评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,453评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,126评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,725评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,803评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,015评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,514评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,111评论 2 341