C语言进阶

十七、堆内存

是进程的一个内存段(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[ ]:每个命令字符串的首地址

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值