十七、堆内存
是进程的一个内存段(text、date、bss、heap、stack),由程序员手动管理
特点:足够大,缺点:使用麻烦
为什么使用堆内存:
1、随着程序的复杂,数据量变多
2、其他内存段的申请和释放不受控制,堆内存的申请和释放受控制
如何使用堆内存:
注意:C语言中没有控制堆内存的语句,只能使用C标准库提供的函数
#include <stdlib.h>
void *malloc(size_t size);
//功能:从堆内存中申请size个字节的内存,申请到的的内存中储存的是什么内容不确定
//返回值:成功时返回申请到的内存的首地址,失败返回NULL
void free(void *ptr);
//功能:释放一块堆内存,可以释放NULL,但是不能重复释放和释放非法内存
//注意:释放的知识使用权,里面的数据不会特意的全部清理
void *calloc(size_t nmemb, size_t size);
//功能:从堆内存中申请nmemb块大小为size的字节的内存,申请到的内存块会被初始化为0
//注意:申请到的还是一块完整的内存块
void *realloc(void *ptr, size_t size);
//功能:改变已有内存块的大小,size表示调整后的大小,在原有的基础上调大或调小
//返回值:是调整后的内存块的首地址,一定要重新接受,因为可能不是在原有的内存块的基础上调整
/*如果无法在原有内存块的基础上调整:
1、申请一块符合大小要求的内存块
2、把原内存的数据拷贝过去
3、把原内存块释放,返回新内存块的首地址*/
malloc的内存管理机制:
当首次向malloc申请堆内存是,malloc会向操作系统申请内存,操作系统会直接分配33页(1页=4096字节),交给malloc管理。
但不意味着可以越界,因为malloc会把使用权分配给“其他人”,这样就产生了脏数据
每个内存块之间都有空隙(4~12字节),这些空隙一些是为了内存对齐,其中4字节记录了malloc的维护信息,这些维护信息决定了下次分配内存块的位置,可以通过借助这个信息计算出每个内存块的大小,当这些信息被破坏时,会影响接下来的malloc和free的调用
使用堆内存要注意的问题:
内存泄露:
内存无法再使用,也无法释放,再次使用时只能重新申请,然后重复以上过程,是日积月累导致系统中可用的内存越来越少
注意:程序一旦结束,属于它的所有资源都会被操作系统回收
如何尽量避免内存泄露:
谁申请的谁释放,谁知道该释放谁释放
如何判断定位泄露:(搜一下)
1、查看内存的使用情况
2、分析代码,使用代码分析工具检查malloc使用情况
3、包装malloc、free申请、释放的信息记录到日志中
内存碎片:
已经释放但无法再继续使用的内存叫做内存碎片,是由申请和释放的时间不协调导致,而且无法避免,只能尽量减少
如何减少内存碎片:
1、尽量使用栈内存
2、不要频繁申请和释放内存
3、尽量申请大块的内存自己管理
内存清理数据:
#include <strings.h>
void bzero(void *s, size_t n);
//功能:把一块内存清理为0
//s:内存块的首地址
//n:内存块的字节数
#include <string.h>
void *memset(void *s, int c, size_t n);
//功能:把内存块按字节设置为c
//s:内存块的首地址
//c:想要设置的ASCII码值
//n:内存块的字节数
//返回值:设置成功后的内存首地址
堆内存定义二维数组:
指针数组:定义n * m二维数组
类型* arr[n];
类型* arr[n] = {};
for(int i=0;i<n;i++)
{
arr[i] = malloc(sizeof(类型)*m);//容易产生内存碎片
}
可以申请不规则的二维数组
数组指针:
类型 (arrp)[m] = malloc(sizeof(类型) * nm);//对内存要求高
注意:所谓的多维数组都是用一位数组模拟出来的
十八、字符串
字符:
字符就是符号和图案,在计算机中字符是以整数形式存在的,当需要使用是会根据ASCII码表中的对应关系来显示相应的符号或图案。
0 '\0'
48 '0'
65 'A'
97 'a'
串:
是一种数据结构,由一组连续的若干个相同类型的数据组成。
末尾有一个结束标志。
对于串型结构的处理都是批量性的,从开头位置直到遇到结束标志停止
字符串:
由字符组成的串型结构,结束标志是‘\0’
字符串的输入:
scanf %s 地址
缺点:不接收空格
char *gets(char *s);
功能:输入字符串,并且可以接收接收空格
返回值:链式调用(把一个函数的返回值作为参数传递给另一个函数)
char *fgets(char *s, int size, FILE *stream);
功能:可以设置输入的字符串的长度size-1,超出部分不接收,强制在末尾为‘\0’余留位置
字符串的输出:
printf %s 地址
int puts(const char *s)
功能:输出一个字符串,会在末尾自动加\n
返回值:成功输出的字符个数
字符串的存在形式:
字符数组:char arr[10] = {‘1’,‘2,‘3’};
由char类型组成的数组
注意:主动为’\0’预留位置
使用的是栈内存,数据可以修改
字符串字面值
“由双引号包含的若干个字符”,默认末尾加伤\0
字符串字面值是以地址形式存在的,储存在代码段,如果强行修改就会产生段错误
const char* str = “字符串字面值”;
sizeof(“strstr”)输出 字符个数+1
注意:两个一模一样的字符串字面值在代码段中只有一份
常用方式:
字符串数组[ ] = “字符串字面值”;
char str[20] = “hello world!”;
会自动为\0预留位置,而且可以修改值
赋值完后字符串存在两份,一份储存在代码段,另一份在栈内存(这份可以修改)
十九、预处理指令
程序员所编写的代码并不能被真正的编译器编译,需要一段程序把代码翻译一下。
翻译的过程叫做预处理,被翻译的代码叫做预处理指令,以#开头的都是预处理指令。
查看预处理的结果:
gcc -E code.c 把预处理的结果打印到终端上
gcc -E code.c -o code.i 把预处理的结果储存到code.i文件中
预处理指令的分类:
#include 文件包含
#include <> 从系统指定的路径下查找并导入头文件
#include " " 先从当前路径下查找,如果找不到再从系统指定路径查找并导入头文件
操作系统通过设置环境变量来指定头文件的查找路径,或者通过设置编译参数 -I/path
#define 定义宏
宏常量:#define MAX 100
优点:提高代码拓展性、提高可读性、提高安全新、可以用在case后面。
注意:末尾不要加分号,一般宏名全部大写
【全局变量:手字母大写 局部变量:全部小写】
【函数名:小写加下划线 指针:p 数组:arr 字符串:str】
预定好的宏:
_func_ //获取当前函数名
_FILE_ //获取当前文件名
_LINE_ //获取当前行号
_DATE_ //获取当前日期
_TIME_ //获取当前时间
宏函数:带参数的宏
#define sum(a,b) a+b
//1、把代码使用的宏函数替换成宏函数后面的代码
//2、把宏函数代码中的参数,替换为调用者提供的参数
不是真正的函数,没有函数传参,不检查参数类型,没有返回值,只有计算结果
宏的二义性:
由于宏代码所处的位置,参数的不同导致宏有不同的功能。
如何避免二义性:
1、宏函数整体加小括号,每个参数也都加小括号
2、使用宏函数时,不要提供带自运算符的变量作为参数
常见的笔试面试题:(C语言中指针相关的知识点有哪些)
#define INT int
typedef int INT //如果是普通类型,他们的功能上没有任何区别
#define INTP int* //p1是指针 p2 p3时int
typedef int* INTP //p1 p2 p3都是指针
宏函数交换数值
#define swap(a,b) {a=a+b;b=a-b,a=a-b;} //不能超出范围
#define swap(a,b) {a=a^b;b=a^b,a=a^b;} //不能是用一个参数
#define swap(a,b,typedef) {typedef t=a;a=b,b=t;}
#define swap(a,b) {typeof(a) t=a;a=b,b=t;}
宏函数与普通函数的区别?
它们是什么?
宏函数:不是真正的函数,是代码的替换,只是用法与函数很像
普通函数:一段具有某项功能的代码的集合,会被编译成二进制的执行储存在代码段,函数名就是函数的首地址,函数有独立的命名空间、栈内存
有什么不一样:
函数:有返回值、类型检查、安全、入栈、出栈、速度慢、跳转
宏函数:运算结果、通用、危险、替换、速度快、冗余
条件编译:
根据条件决定某些代码是否参与最终的编译
版本控制 #if 0做注释
#if
#elif
#else
#end
防止头文件被重复包含
#ifndef
#define
#endif
#ifdef
#endif
用于定义调试宏函数 编译时如果需要调试,则加入-DDEBUG参数
#ifdef DEBUG
#define debug(...) printf(__VA_AGRS__)
#else
#define debug(...)
#endif
#define error(...) printf(stdout,"%s:%s:%d %s: %m %s %s\n",__FILE__,__func__,__LINE__,__VA_AGRS__,__DATE__,__TIME__)
二十、头文件
问题:头文件可能被任何源文件包含,意味着头文件的内容会在多个目标文件(.o)存在,合并时不能冲突。
重点:在头文件中只编写声明语句,而不能有定义语句。
全局变量的声明
函数声明
宏常量
宏函数
typedef 类型重定义
结构、联合、枚举的类型声明
头文件的编写原则:
1、为每个.c文件写一份.h文件,.h文件是对.c文件的说明。
2、如果需要用到某个.c文件中的变量、函数、宏、结构…时,只需要把它的头文件导入即可
3、.c文件也要导入自己的.h文件,目的是让声明与定义一致
头文件的互相包含:
加入a.h中包含了b.h,b.h又需要a.c,这种情况在编译的时候就会出错
解决方法:就是把a.h中需要的内容和b.h中需要的内容提取出来,另外再编写一个c.h
未知的类型名’xxxx’,一般都是因为头文件相互包含
未知的类型名‘xxxx’,一般都是因为头文件相互包含导致的。(复制头文件卫士时未修改)
二十一、复合结构类型
结构
结构是由程序员自己设计的一种数据类型,用于描述一个事物的各项数据,由若干个不同的基础类型组成。
设计:
struct 结构体名
{
类型1 成员名1;
类型2 成员名2;
...
};
定义结构变量:
struct 结构体名 结构变量名;
注意:定义结构体变量时,struct不能省略
定义结构变量初始化:
struct 结构名 结构变量名 = {v1,v2,v3};
只初始化某些成员
注意:同类型的结构变量可以直接复制 变量名1=变量名2
访问结构成员:结构体名.成员名;
结构变量作形参时:
由于结构变量的字节数比较大,值传递的效率比较低,因此都是传递结构体的地址,当使用的是结构体指针时,那么使用->来访问成员。如果不需要修改结构变量的值,可以使用const保护。
结构指针名->成员名;
typedef重定义结构类型:
typedef struct 结构类型名 结构类型名;
之后就可以不使用struct关键字
typedef struct 结构类型名
{
类型 成员名;
...
}结构类型名;
注意:一般使用堆内存存放结构体变量
如何计算结构体的字节数:
结构体的成员的顺序会影响会影响它的总字节数,如果在设计结构体时能够合理地安排成员的顺序可以大大节省内存。
内存对齐
假定第一个成员从零开始,储存每个成员的地址编号必须能被它的字节数整除则填充空字节
内存补齐:
结构体的总字节数,必须是它最大成员的字节数的整数倍,如果不是整数倍则在末尾填充空字节。
在Linux系统下计算结构体的对齐和补齐时,如果成员的字节数超过4字节则按4字节算。windows下是按实际情况计算
#pragma pack(n)设置补齐、对齐的最大字节数 n<=4
联合:union
联合与结构的使用方法基本一致,与结构的区别就是所有的成员
公用一块内存,一个成员的值发生变化,其他所有成员的值也会随着改变。
联合就是使用少量的内存对应多个标识符,来达到节约内存的目的,现在已经基本不使用了。
联合常考的笔试题:
union Data
{
char ch[10];
int num=0x01020304;
};
注意:计算联合体时要考虑内存补齐。
如何判断系统是大端还是小端?
如果十六进制整数0x01020304 储存在以0x0A为起始地址的4字节内存中。
高位数据储存在高位地址(0A:04 0B:03 0C:02 0D:01)–小端
高位数据储存在低位地址(0A:01 0B:02 0C:03 0D:04)–大端
个人计算机系统一般都是小端系统,而UNIX服务器和网络设备都是大端系统,网络字节也是大端模式的数据
序列化和反序列化(sprintf、xml、json)
枚举:enum
枚举就是一种数据类型把可能出现的值全部罗列出来,取一个有意义的名字,除此之外,该类型的变量再次再出现别的数值就是非法的(愿望)。
枚举可以看做是值受限的int类型,但编译器为了效率并不会检查,所有c语言中枚举都可以当做int类型变量使用
enum Direction
{
UP = 183,
DOWN = 184,
RIGHT = 185,
LEFT = 186,
};
如果不给成员值,枚举的值默认从0开始,逐渐+1,如果设置了值,后面没设置的会在它的基础上+1
为什么要使用枚举:
为没有意义的值取一个有意义的名字,提高代码的可读性和安全性
二十二、文件读写
文件的分类
文本文件:储存的是ASCII码的二进制 ‘2’ ‘5’ ‘ 5’
二进制文件:储存的是数据的补码 11111111
文件IO
FILE *fopen(const char *path, const char *mode);
功能:打开或者创建文件
path:文件路径
mode:打开模式
r:以只读权限打开文件,文件如果不存在则打开失败。
r+:在r的基础上加入写权限。
w:以只写权限打开文件,如果文件存在则清空写入,如果文件不存在则创建。
w+:在w的基础上加入读权限。
a:以只写权限打开文件,如果文件存在则在末尾追加写入,如果文件不存在则创建。
a+:在a的基础上加入读权限。
返回值:结构指针,不需要关心它的成员,只需要知道它是操作文件的凭证,也叫做文件指针
二进制方式读写
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
功能:把内存中的数据写入到文件中
ptr:内存的地址
size:一次写入size字节
nmemb:写入多少次
stream:文件指针,fopen的返回值
返回值:成功写入的次数
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:从文件中读取数据到内存中
ptr:内存的地址
size:一次读取size字节
nmemb:读取多少次
stream:文件指针,fopen的返回值
返回值:成功读取的次数
文本方式读写
int fprintf(FILE *stream, const char *format, ...);
功能:以文本形式写入到文件中
stream:文件指针,fopen的返回值
format:写入的内容、占位符
…:变量名
返回成功写入的字节数
int fscanf(FILE *stream, const char *format, ...);
功能:从文件中以文本形式读取数据到变量中
stream:文件指针,fopen的返回值
format:读取的内容、占位符
…:变量的地址
返回成功读取的变量个数
关闭函数
int fclose(FILE *fp);
关闭文件
文件位置指针
每打开一个文件都会有一个指针记录着操作的位置,它会随着读写函数的执行而移动,r、r+、w、w+打开的位置指针都在文件开头,以a、a+方式打开时位置指针在末尾。
如果想要随机读取文件的任何位置的数据,需要手动设置文件的位置指针
int fseek(FILE *stream, long offset, int whence);
功能:设置文件位置指针的位置
stream:文件指针,fopen的返回值
offset:偏移值
whence:基础位置
SEEK_SET:文件开头
SEEK_CUR:当前位置
SEEK_END:文件末尾
返回值:成功返回0,失败返回-1
long ftell(FILE *stream);
功能:获取文件位置指针的位置
返回值:第几个字节
void rewind(FILE *stream);
功能:把文件位置指针设置到开头
文件相关的函数
int feof(FILE *stream);
功能:检查文件位置是否到达末尾
返回值:非0说明到达文件末尾
char *fgets(char *s, int size, FILE *stream);
功能:从文件中读取一行字符串
int fputs(const char *s, FILE *stream);
功能:写入一个字符串到文件中,并且会在末尾自动添加\n
返回值:成功写入的字符个数
int fgetc(FILE *stream);
功能:从文件中读取一个字符
int putc(int c, FILE *stream);
功能:从文件中写入一个字符到文件
int remove(const char *pathname);
功能:删除文件
返回值:成功返回0 失败返回-1
int rename(const char *oldpath, const char *newpath);
功能:重命名文件
返回值:成功返回0 失败返回-1
main函数的参数:
是为了获取命令行附加的参数
argc:命令行附加参数的个数
argv[ ]:每个命令字符串的首地址