C语言 查漏笔记

内存分区

可执行未运行

  1. bss段:全局未初始化数据
  2. data段:全局初始化数据
  3. txt代码段

可执行运行中

  1. 堆区(wr):使用malloc、calloc、realloc和free动态分配和释放
  2. 栈区(wr):局部变量/数组、函数形参、函数返回值 >4B(<4B寄存器)
  3. 全局区(wr):全局变量、static静态变量
  4. 文字常量区(r):字符串常量、符号常量
  5. 代码区(r):二进制代码

变量

系统对于变量的定义,必须要知道变量的空间大小,如char一字节;
系统不知道void类型的变量需要开辟多大的空间,所以void不能用来定义变量;

普通局部变量

  1. 定义形式:在函数内定义
  2. 作用范围:最近的{}内(复合语句中)
  3. 生存周期:最近的{}内有效,离开后系统自动回收
  4. 存储区域:栈区
  5. 特点
    a.普通变量不初始化,内容不确定
    b.普通变量同名,就近原则

普通全局变量

  1. 定义形式:在函数外定义
  2. 作用范围:
    a.当前源文件都有效,最好加extern声明一下
    b.其他源文件使用时,必须加extern声明,如extern int data;
  3. 生存周期:整个进程都有效
  4. 存储区域:全局区
  5. 特点
    a.全局变量不初始化,内容为0(bss段自动补0)
    b.其他源文件使用,必须在所使用的源文件中加extern声明
    c.全局变量和局部变量同名,在{}内优先使用局部变量

静态局部变量

  1. 定义形式:局部变量加static修饰
  2. 作用范围:最近的 {}内(复合语句中)
  3. 生存周期:整个进程,程序结束才被释放
  4. 存储区域:全局区
  5. 特点
    a.静态局部变量只能被初始化一次(定义一次)
    b.静态局部变量不初始化,内容为0

静态全局变量

  1. 定义形式:全局变量加static修饰
  2. 作用范围:仅当前源文件
  3. 生存周期:整个进程
  4. 存储区域:全局区
  5. 特点
    a.静态全局变量不初始化,内容为0
    b.静态全局变量仅在当前源文件使用(可使用指针对未释放的内存强行使用)

函数

普通函数(全局函数)

特点:其他源文件可以使用,必须加extern声明

静态函数(局部函数)

特点:其他源文件不能直接使用,可以封装在同源文件的全局函数中使用

预处理

c语言的编译过程

  1. 预处理:头文件包含,宏替换,条件编译,删除注释,不做语法检查
  2. 编译:将预处理过的文件,生成汇编文件,语法检查
  3. 汇编:将汇编文件生成二进制文件
  4. 链接:将众多二进制文件+库+启动代码,生成可执行文件

头文件包含(#include)

  1. #include<库>:从系统指定的目录下去找库
  2. #include"库":先从源文件目录去找,找不到再到系统指定目录去找
  3. 使用#include去包含文件,在预处理的时候,会将包含的文件整体替换到源代码中

宏定义(#define)

  1. 宏展开:在预处理阶段,编译器将所有的宏替换为原来的字符信息
  2. 宏只在宏定义的文件中有效
  3. 限制宏作用范围:#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)
缩写规则:+左边放在[]左边,+右边放在[]里面

数组名 :

  1. 作为地址,代表首元素的地址,即&arr[0]
  2. 作为类型,代表整个数组,sizeof(数组名)===整个数组的大小
  3. 对数组名取地址,代表数组的首地址
  4. 首元素地址(arr)是第一个元素的地址,首地址(&arr)是整个数组的地址
  5. arr和&arr的地址编号一样,但类型不一样
  6. arr+1:跳过第一个元素
  7. &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] )

  1. 一维数组作形参时,编译器会将其优化为一级指针
  2. 多维数组作形参时,编译器会将其优化为数组指针
    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)(可以用于强转)
本质:函数名就是函数的入口地址,所以可以使用指针去代替函数名
注意:

  1. 函数指针所能指的函数,必须是参数类型,数量一样,且返回值也必须一样
  2. 对函数指针取*是无效的,即编译器会自动删除函数指针前的 *

堆区

由于堆区的内存需要手动分配和释放,所以不要随意改变指向堆区内存的指针的指向如果指向一块堆区空间的所有指针全部改变了指向,那这块内存就没有办法去释放(进程结束前),则就造成了内存泄漏

分配

malloc函数

原型:void * malloc(unsigned int size);
形参:size为所需空间的大小,单位为字节,一般使用 n*sizeof(类型)

返回值:由于返回值是void*,所以使用时需要进行强转

  1. 成功返回空间的起始地址
  2. 失败返回NULL

空间内容:由malloc分配的空间内容是随机的,所以一般使用memset函数清空
多次调用:多次调用malloc所分配的空间之间,不一定是连续的

calloc函数

原型:void * calloc(unsigned int numb,unsigned size);
形参:

  1. numb分配内存的块数
  2. size每块的大小(字节)
  3. calloc分配的空间总大小为numb*size

返回值:仍然需要强转

  1. 成功,返回内存空间首地址
  2. 失败,返回NULL

空间内容:calloc分配的空间内容会自动清0

realloc函数

malloc和calloc函数所分配的空间也是固定的,分配完后不能增加或减少;
realloc函数可以实现内存的追加和减少

原型:void * realloc(void* s, unsigned int newsize);
形参:

  1. s原先内存空间的首地址(指针)
  2. newsize追加后总空间的大小(字节),一般使用n*sizeof(类型)

返回值:

  1. 原先空间后有足够的内存,返回值与s一样
  2. 原来空间后没有足够内存,则系统会拷贝原来空间的内容到新的空间,再释放原来的空间

注意:
当原来空间后没有足够的内存,系统会释放掉原来的空间,原来的指针s将会指向没有权限的空间,所以为了安全,一般使用s去接收realloc的返回值,即:
s=(强转)realloc(s,大小);

释放

free函数

原型:void free(void * p)
释放p所指的空间,不需要指定大小,由系统自动确认

防止重复释放

由于重复释放同一指针所指的空间是会报错的,所以为防止重复释放,所有释放语句都使用如下语句:

if(NULL!= p)
{
	free(p);
	p=NULL;
}

字符串处理函数(遇到\0结束)

  1. strlen函数
  2. strcpy/strncpy函数
  3. strcat/strncat函数 拼接
  4. strcmp/strncmp函数 比较
  5. strchr函数 匹配字符
  6. strstr函数 匹配字符串
  7. atoi/atol/atof函数 字符串转整型/长整型/浮点型
  8. strtok函数 切割
  9. sprintf函数 向字符数组格式化输出
  10. 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. 变量名1=变量名2 如:lucy=bob;
  3. 使用memset函数 如:memset(&lucy,&bob,sizeof(struct stu) );

typedef

typedef 的作用是给数据类型取别名,别名一般用大写

步骤:

  1. 用数据类型定义一个变量 如:int (*p)(int ,int )
  2. 用别名代替变量名 如:int (*FUN_P)(int ,int )
  3. 在最前面加上typedef 如:typedef int (*FUN_P)(int ,int )
  4. 使用别名定义变量 如:FUN_P fun;

结构体指针

对于一个结构体指针取 * ,代表的是结构体整体
指针可以使用成员符(->)直接访问成员,也可以使用( * 和.)访问成员

当函数要对结构体进行操作时,最好是传递结构体指针作为参数,这样可以节省空间

结构体的内存对齐

内存对齐原因

原因:32位的cpu一次性取4字节的数据,为提升cpu效率,则需要内存对齐

对齐

对齐规则

步骤:

  1. 确定分配单位(最大基础类型值)
  2. 成员起始位置的偏移量=自身的基础类型的整数(0~n)倍
  3. 内存中成员的相对位置与结构体中一致
  4. 结构总大小为成员中分配单位的整数倍

对齐示例

强制对齐规则

强制对齐规则指定的是分配单位,其值只能是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

对于嵌套的结构体,系统也是会进行内存对齐
步骤:

  1. 确定分配单位(最大基础类型值)
  2. 普通成员起始位置的偏移量=自身的基础类型的整数(0~n)倍
  3. 结构体成员起始位置的偏移量=被嵌套结构体内最大类型的整数(0~n)倍
  4. 被嵌套结构体的内部成员的位置偏移量以被嵌套结构体为准
  5. 内存中成员的相对位置与结构体中一致
  6. 结构总大小为分配单位的整数倍
  7. 被嵌套结构体大小为被嵌套结构体内部最大基础类型的整数倍

嵌套结构体内存

位段

位段是特殊的结构体,用于数据位的操作

定义形式:

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_CUR1文件指针当前位置
SEEK_END2文件末尾
SEEK_SET0文件开头

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)
浮点数

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值