内存分区
可执行未运行
- bss段:全局未初始化数据
- data段:全局初始化数据
- txt代码段
可执行运行中
- 堆区(wr):使用malloc、calloc、realloc和free动态分配和释放
- 栈区(wr):局部变量/数组、函数形参、函数返回值 >4B(<4B寄存器)
- 全局区(wr):全局变量、static静态变量
- 文字常量区(r):字符串常量、符号常量
- 代码区(r):二进制代码
变量
系统对于变量的定义,必须要知道变量的空间大小,如char一字节;
系统不知道void类型的变量需要开辟多大的空间,所以void不能用来定义变量;
普通局部变量
- 定义形式:在函数内定义
- 作用范围:最近的{}内(复合语句中)
- 生存周期:最近的{}内有效,离开后系统自动回收
- 存储区域:栈区
- 特点
a.普通变量不初始化,内容不确定
b.普通变量同名,就近原则
普通全局变量
- 定义形式:在函数外定义
- 作用范围:
a.当前源文件都有效,最好加extern声明一下
b.其他源文件使用时,必须加extern声明,如extern int data; - 生存周期:整个进程都有效
- 存储区域:全局区
- 特点
a.全局变量不初始化,内容为0(bss段自动补0)
b.其他源文件使用,必须在所使用的源文件中加extern声明
c.全局变量和局部变量同名,在{}内优先使用局部变量
静态局部变量
- 定义形式:局部变量加static修饰
- 作用范围:最近的 {}内(复合语句中)
- 生存周期:整个进程,程序结束才被释放
- 存储区域:全局区
- 特点
a.静态局部变量只能被初始化一次(定义一次)
b.静态局部变量不初始化,内容为0
静态全局变量
- 定义形式:全局变量加static修饰
- 作用范围:仅当前源文件
- 生存周期:整个进程
- 存储区域:全局区
- 特点
a.静态全局变量不初始化,内容为0
b.静态全局变量仅在当前源文件使用(可使用指针对未释放的内存强行使用)
函数
普通函数(全局函数)
特点:其他源文件可以使用,必须加extern声明
静态函数(局部函数)
特点:其他源文件不能直接使用,可以封装在同源文件的全局函数中使用
预处理
c语言的编译过程
- 预处理:头文件包含,宏替换,条件编译,删除注释,不做语法检查
- 编译:将预处理过的文件,生成汇编文件,语法检查
- 汇编:将汇编文件生成二进制文件
- 链接:将众多二进制文件+库+启动代码,生成可执行文件
头文件包含(#include)
- #include<库>:从系统指定的目录下去找库
- #include"库":先从源文件目录去找,找不到再到系统指定目录去找
- 使用#include去包含文件,在预处理的时候,会将包含的文件整体替换到源代码中
宏定义(#define)
- 宏展开:在预处理阶段,编译器将所有的宏替换为原来的字符信息
- 宏只在宏定义的文件中有效
- 限制宏作用范围:#undef 宏名,使后面的语句中的宏无效
不带参数的宏
示例:
#define N 40
在预处理阶段,编译器会将所有的N替换为40,即宏展开
带参数的宏
示例:
#define N(x) x*x
N(3+5)==3+5*3+5==23
注:宏只能原样替换,不能计算
相对于函数来说,宏节省了时间,浪费了空间
条件编译
对于条件编译,编译器会将不满足的语句注释掉,不会进行编译,即选择性编译
测试宏不存在(#ifndef)
#ifndef N
printf("没有定义宏N \n");
#else
printf("定义了宏N \n");
#endif
测试宏存在(#ifdef)
#ifdef N
printf("定义了宏N \n");
#else
printf("没有定义宏N \n");
#endif
其他(#if)
#if 表达式
printf("表达式为真 \n");
#else
printf("表达式为假 \n");
#endif
防止头文件重复包含
由于使用#include包含头文件,编译器会用整个头文件内容来替换include语句,不会去检查是否有重复内容,如果有重复包含的头文件,则可能出现重复的函数和变量,导致错误!
处理方法:
方式一:在所有头文件第一行,加上 #pragma once
方式二:c/c++标准制定
#ifndef __文件名大写_H__ //例如:头文件名为a.h,则 宏名为 __A_H__
#define __文件名大写_H__ //由于同一工程中,头文件名不能重复,所以使用头文件名延展的宏名,就不会出现重复
头文件内容
#endif
对比:
#pragma 主要由编译器决定,强调文件名,对于一些古老的编译器不支持,对于编译器不支持的#pragma语句,编译器会跳过
#ifndef 由c/c++标准制定,强调宏,而不是文件
补码
原码 ==> 符号位不变,其余取反 ==> 反码 ==> +1 ==> 补码
补码 ==> 符号位不变,其余取反 ==> 反码 ==> +1 ==> 原码
定义 | 示例(一字节) | |
---|---|---|
原码 | 数据的二进制码 | -4的原码:1000 0100 |
反码 | 对原码除符号位外,全部取反 | -4的反码:1111 1011 |
补码 | 在反码的基础上,+1 | -4的补码:1111 1100 |
对于无符号数、正数,原码,反码和补码是相同的
重要:计算机对于一个变量的值的储存,先看对其赋值的数是正数还是负数,如果是正数,则将原码储存;如果是负数,则将其转换为补码,在储存在变量空间中;如果越界,直接截断
例:
//vs下运行结果,vs下会将其强转为int型,所以截断后,会自动补为4字节
char a = 709;
printf("%X",a); // FFFFFFC5 原码为FFFFF2C5 先截断,再将符号位扩展
char b = -254;
printf("%X",a); //2
char c = 4;
printf("%X",a); //4
补码的意义
统一了0的储存
如果没有补码:-0的原码:1000 0000 +0的原码:0000 0000
有补码后:
-0的储存:1000 0000 == >1111 1111 ==> 0000 0000
+0的储存:0000 0000
统一0的储存的意义:
若没有统一:
一字节有符号的范围为 -127 ~ -0 和 +0~ 127 ,共255个值
一字节无无符号的范围 0 ~ 255 ,共256个值
将减法运算变成加法运算
示例:6+(-10) = -4
若没有补码:
使用补码储存:
指针
关于内存
每个进程可以分配的内存大小,与计算机的寻址能力有关;
对于32位的计算机,可以寻找0x 0000 0000 ~ 0x FF FF FF FF个地址编号,每一个地址编号为1字节,即可以寻找232个地址编号,即232个字节,为4GB.
对于C语言程序,指针操作的是虚拟内存,而不是实际的物理内存,虚拟内存是由物理内存映射的。
由系统决定的存储方式:
对于多字节数据的存储,计算机有两种方式,高地址= => 低地址 和 低地址 = => 高地址
指针变量
由于32位计算机的地址编号都是4字节的,所以任何类型的指针变量的大小都为4字节
指针变量由起始地址和步长组成
步长由指针本身的类型决定,为固定值,如int*、char * 等,与所指的数据类型无关
跨度:* (p+n),跨度为 (n*步长),跨度为步长的整数倍
写:在储存数据的时候,使用&符,取得数据的起始地址,并储存在指针的地址空间中
读:在读取数据的时候,先取出指针空间中的内容(起始地址),在向后读取m个字节,m的数值与步长相同
void* 指针(万能指针)
虽然系统不知道void的大小,但是系统是知道void * 的大小(32位平台4字节),所以,可以使用 void * 来定义指针变量;
但是,由于指针的步长由类型决定,而void * 的步长系统未知,则void*定义的指针变量不能直接使用,需要强制类型转换,来确定变量的步长
int a=10;
void * p;
p=&a;//存储时,直接存储
printf("%d",*(int*)p);//使用时必须进行类型转换
NULL == ((void*)0)
void * 可以用来储存任何类型的一级指针变量
指针与数组
一维数组
在使用的时候,[]的本质是 * ()的缩写;
所以:arr[1] === 1[arr] === * (arr+1) === * (1+arr)
缩写规则:+左边放在[]左边,+右边放在[]里面
数组名 :
- 作为地址,代表首元素的地址,即&arr[0]
- 作为类型,代表整个数组,sizeof(数组名)===整个数组的大小
- 对数组名取地址,代表数组的首地址
- 首元素地址(arr)是第一个元素的地址,首地址(&arr)是整个数组的地址
- arr和&arr的地址编号一样,但类型不一样
- arr+1:跳过第一个元素
- &arr+1:跳过整个数组
int arr[5]={10,20,30,40,50};
int *p =arr;
printf("%d",*p++); //10 ++和*同等优先级,++在右,先使用再自增
printf("%d",*(p)++); //20
printf("%d",*(p++)); //21
指针数组
形式:int *p[3];
本质:数组,存放内容为指针
p+1:跳过一列,即p[1]
数组指针
形式:int (*p)[4];
([]中的值不能省略)
本质:指针变量,指向数组的首地址(非首元素地址)
p+1:跳过整个数组,跳过的字节数跟[]中的值 和 数组类型有关
int(*p)[4] 和 int(*p)[5]中,p的类型不一样,指针的步长不同
对于一个指向n维数组的数组指针,其类型为n+1级指针,其可以转换为n+1级数组
例如:
int arr[4]={0,1,2,3};
int (*p)[4]=NULL;
p=&arr;
printf("%d",*(*p+2));//*(arr+2)==arr[2]==2
printf("%d",p[0][2]);//一维数组指针,转换为二维数组
二维数组
对于二维数组,也可以使用*()替换[];
例如:a[2][3]===> * ( * (a+2)+3) 代表第1行第2列的元素值
数组名:首行的行地址,+1跳过1行
列地址:对行地址取*,就是当前行的第0列的列地址,列地址+1,跳过一列,列地址也是元素地址
任何维度的数组在物理存储上,都是一维的,所以可以使用一维数组的方式访问任何维度的数组
指针与函数
形参
当要在函数内部修改外部变量的值时,需要将外部变量的地址作为参数传递给函数
一维数组:当函数的形参为数组(a[]),编译器会自动将其优化为一级指针变量(*a)
多维数组:如果形参是多维数组(a[][4]),则会被优化成数组指针变量( (*a)[4] )
- 一维数组作形参时,编译器会将其优化为一级指针
- 多维数组作形参时,编译器会将其优化为数组指针
int a[2]-------------------->int * a
int a[2][3]----------------->int (*a)[3]
int a[2][3][4]------------->int (*a)[3][4]
int a[2][3][4][5]---------->int (*a)[3][4][5]
所以,当数组作为函数的形参时,直接将数组名传递给函数就行
指针作为返回值
注意,当指针作为函数的返回值时,不要将指向局部变量的指针返回,因为指向局部变量的指针,所对应的内存权限,在函数结束后,会被收回,在函数外操作没有权限的空间,是非法的,且其值不确定
函数指针
形式:int (*p)(int a,int b);
:类型为int (*)(int ,int)(可以用于强转)
本质:函数名就是函数的入口地址,所以可以使用指针去代替函数名
注意:
- 函数指针所能指的函数,必须是参数类型,数量一样,且返回值也必须一样
- 对函数指针取*是无效的,即编译器会自动删除函数指针前的 *
堆区
由于堆区的内存需要手动分配和释放,所以不要随意改变指向堆区内存的指针的指向,如果指向一块堆区空间的所有指针全部改变了指向,那这块内存就没有办法去释放(进程结束前),则就造成了内存泄漏
分配
malloc函数
原型:void * malloc(unsigned int size);
形参:size为所需空间的大小,单位为字节,一般使用 n*sizeof(类型)
返回值:由于返回值是void*,所以使用时需要进行强转
- 成功返回空间的起始地址
- 失败返回NULL
空间内容:由malloc分配的空间内容是随机的,所以一般使用memset函数清空
多次调用:多次调用malloc所分配的空间之间,不一定是连续的
calloc函数
原型:void * calloc(unsigned int numb,unsigned size);
形参:
- numb分配内存的块数
- size每块的大小(字节)
- calloc分配的空间总大小为numb*size
返回值:仍然需要强转
- 成功,返回内存空间首地址
- 失败,返回NULL
空间内容:calloc分配的空间内容会自动清0
realloc函数
malloc和calloc函数所分配的空间也是固定的,分配完后不能增加或减少;
而realloc函数可以实现内存的追加和减少
原型:void * realloc(void* s, unsigned int newsize);
形参:
- s原先内存空间的首地址(指针)
- newsize追加后总空间的大小(字节),一般使用n*sizeof(类型)
返回值:
- 原先空间后有足够的内存,返回值与s一样
- 原来空间后没有足够内存,则系统会拷贝原来空间的内容到新的空间,再释放原来的空间
注意:
当原来空间后没有足够的内存,系统会释放掉原来的空间,原来的指针s将会指向没有权限的空间,所以为了安全,一般使用s去接收realloc的返回值,即:
s=(强转)realloc(s,大小);
释放
free函数
原型:void free(void * p)
释放p所指的空间,不需要指定大小,由系统自动确认
防止重复释放
由于重复释放同一指针所指的空间是会报错的,所以为防止重复释放,所有释放语句都使用如下语句:
if(NULL!= p)
{
free(p);
p=NULL;
}
字符串处理函数(遇到\0结束)
- strlen函数
- strcpy/strncpy函数
- strcat/strncat函数 拼接
- strcmp/strncmp函数 比较
- strchr函数 匹配字符
- strstr函数 匹配字符串
- atoi/atol/atof函数 字符串转整型/长整型/浮点型
- strtok函数 切割
- sprintf函数 向字符数组格式化输出
- sscanf函数 从字符数组格式化输入
const关键字
const关键字修饰符,作用是将变量修饰为只读
const int * p 修饰 *
将*p修饰为只读,不能通过 *p去改变p所指的地址的变量值
即:*p只读,p可读可写
int * const p 修饰 p
将p修饰为只读,不能更改p的指向,但可以修改*p的值
即:p只读,*p可读可写
结构体
一次性结构体:
struct //没有结构体名,无法使用结构体名去定义变量,所以是一次性的
{
int num;
char name[20];
int age;
} lucy;
清空结构体变量:memset函数
使用结构体变量给结构体变量赋值:
- 逐个成员赋值
- 变量名1=变量名2 如:lucy=bob;
- 使用memset函数 如:memset(&lucy,&bob,sizeof(struct stu) );
typedef
typedef 的作用是给数据类型取别名,别名一般用大写
步骤:
- 用数据类型定义一个变量 如:int (*p)(int ,int )
- 用别名代替变量名 如:int (*FUN_P)(int ,int )
- 在最前面加上typedef 如:typedef int (*FUN_P)(int ,int )
- 使用别名定义变量 如:FUN_P fun;
结构体指针
对于一个结构体指针取 * ,代表的是结构体整体
指针可以使用成员符(->)直接访问成员,也可以使用( * 和.)访问成员
当函数要对结构体进行操作时,最好是传递结构体指针作为参数,这样可以节省空间
结构体的内存对齐
内存对齐原因
原因:32位的cpu一次性取4字节的数据,为提升cpu效率,则需要内存对齐
对齐规则
步骤:
- 确定分配单位(最大基础类型值)
- 成员起始位置的偏移量=自身的基础类型的整数(0~n)倍
- 内存中成员的相对位置与结构体中一致
- 结构总大小为成员中分配单位的整数倍
强制对齐规则
强制对齐规则指定的是分配单位,其值只能是2n,最后具体的分配单位是默认分配单位和指定的值中较小的一个,即min(默认,指定)
指定方式:在文件开头,加上 #pragma pack(值)
结构体嵌套
对于嵌套的结构体,访问数据要访问到最底层
typedef struct
{
int x;
int y;
}DATA1;
typedef struct
{
char a;
int b;
DATA1 c;
}DATA2;
DATA2 m;
m.c.x=15;//访问x
m.c.y=10;//访问y
对于嵌套的结构体,系统也是会进行内存对齐
步骤:
- 确定分配单位(最大基础类型值)
- 普通成员起始位置的偏移量=自身的基础类型的整数(0~n)倍
- 结构体成员起始位置的偏移量=被嵌套结构体内最大类型的整数(0~n)倍
- 被嵌套结构体的内部成员的位置偏移量以被嵌套结构体为准
- 内存中成员的相对位置与结构体中一致
- 结构总大小为分配单位的整数倍
- 被嵌套结构体大小为被嵌套结构体内部最大基础类型的整数倍
位段
位段是特殊的结构体,用于数据位的操作
定义形式:
typedef struct
{
unsigned char a:2; // a占2位
unsigned char :2; //用于占位
unsigned char b:1; //b占1位
unsigned char :0; //另起一个位段
unsigned int c:3; //c占3位
}DATA;
对于位段来说,着重点是位的操作,所以位段的类型一般仅为unsigned char(1字节)或unsigned int (4字节)
相邻位段,如果大小没超过类型的大小,则会压缩在同一个类型字节中
由于位段结构中,会有占位的位,所以最好在是用前将内存清空(memset)
共用体(union)
共用体的特点为,共用体内的成员是共用同一块内存地址的
共用体的定义形式与结构体相同:
typedef union data
{
int a;
char b;
short c;
}DATA;
//由于共用体成员共用一块内存,所以DATA类型只占最大的4字节
文件
文件函数
fopen/fopen_s函数
原型:FILE * fopen(const char * filename ,const char * mode);
原型:errno_t fopen_s( FILE** pFile, const char *filename, const char *mode );
原型:errno_t _wfopen_s(FILE** pFile,const wchar *filename,const wchar *mode);
功能:按指定模式打开文件
mode | 作用 |
---|---|
r | 只读模式 |
w | 只写模式 |
a | 追加模式 |
b | 以二进制打开 |
t | 以文本打开 |
+ | 读写模式 |
返回值:fopen成功返回文件指针,失败返回NULL;而fopen_s成功返回0,失败返回错误值,根据错误值可以查找错误原因
fputc函数
原型:int fputc(int c,FILE * stream);
功能:写一个字符到流中
返回值:成功返回字符的ASCII码,失败返回EOF(-1)
fgetc函数
原型:int fgetc(FILE *stream);
功能:从流中读取一个字符
返回值:返回读取得字符得ASCII码值,遇到读错误和文件末尾时,返回EOF(-1)
fputs函数
原型:int fputs(const char* str,FILE *stream);
功能:将字符串写入流中
返回值:成功返回非0值,失败返回EOF
fgets函数
原型:char * fgets(char* str,int numChar,FILE *stream);
功能:从流中读取字符串
返回值:成功返回字符串的首地址,失败返回NULL
fread/fread_s函数
原型:size_t fread(void *buffer,size_t size,size_t count,FILE *stream);
原型:size_t fread_s(void *buffer,size_t buffsize,size_t elementSize,size_t count,FILE *stream);
功能:从给定输入流stream读取最多count个对象到数组buffer中
返回值:已读取到缓冲区的完整项,遇到错误或文件尾,会小于count
参数 | 意义 |
---|---|
buffer | 保存读取数据的数组 |
size | 项目大小 |
count | 项的最大数量 |
buffersize | 目标缓冲区大小 |
elementSize | 要读取的项的大小 |
fwrite函数
原型:size_t fwrite(const void* buffer,size_t size,size_t count,FILE *stream);
功能:从buffer数组内存中,将原样数据写入到指定流中
返回值:实际写入完整项的数量,如果发生错误,则会小于count
fseek函数
原型:int fseek(FILE * stream,long offset,int origin);
功能:将流指针移动到指定位置
定位方式:从origin位置,向前/后移动offset个字节,前移负数,后移正数
返回值:成功返回0,失败返回非0
origin | 值 | 意义 |
---|---|---|
SEEK_CUR | 1 | 文件指针当前位置 |
SEEK_END | 2 | 文件末尾 |
SEEK_SET | 0 | 文件开头 |
rewind函数
原型:void rewind(FILE *stream);
功能:重置流指针的位置
ftell函数
原型:long ftell(FILE* stream);
功能:查询当前流指针相对于开头位置的偏移量(字节数)
fprintf函数
原型:int fprintf(FILE *stream,const char * format);
功能:将字符串格式化输出到流中,使用方法类似于printf
fscanf函数
原型:int fscanf(FILE *stream,const char * format);
功能:从流中格式化读取字符串,使用方法类似于scanf
feof函数
原型:int feof(FILE *stream);
功能:检测流中文件是否结束,结束返回非0,未结束返回0
fclose函数
原型:void fclose(FILE *stream);
功能:关闭文件指针所指的文件
浮点数的存储
浮点数的存储在计算机的内存中分为3部分:符号位,阶码,尾数
符号位:最高位,1位
阶码:由二进制浮点数位移产生,右移增大,左移减小,基数跟类型有关,float 8位价码,基数127,double 11位阶码,基数1023
尾数:经过位移后,二进制浮点数的小数部分
例:32.625(float)