欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C语言学习笔记3

程序员文章站 2022-07-15 08:38:01
...

指针

指针是一种数据类型,使用它可以用来定义变量,指针变量存储的是整数,代表了内存的编号(地址)

为什么要使用指针?

  1. 函数之间是相互独立,但是有些时候需要共享变量
    传参是值传递
    全集变量容易命名冲突
    使用数组还需要传递长度
    命名空间是独立的,但地址空间是同一个,所以指针可以解决这个问题;

  2. 由于函数之间的传参是值传递(内存拷贝),对于字节数比较多的变量,值传递效率较低,如果传递变量的地址只需要4|8字节;

  3. heap堆内存无法取名,不能像bss、data、stack让变量名与内存建立联系,只能使用指针记录堆(heap)内存;

如何使用指针?
定义: 类型* 变量名_p;

  1. 指针变量与普通变量的用法有很大区别,建议在取名以p结尾加以区分
  2. 指针变量与普通变量一样默认值是随机的。一般初始值是NULL
  3. 指针的类型决定了他可以访问多少个字节
  4. 一个 "*"只能定义一个指针变量
    int *p1,p2,p3;定义了三个指针
    赋值:变量名_p = 地址;
    指向栈内存:
    int
    p=#
    指向堆内存
    int
    p= malloc(4);
    解引用: *p
    通过指针变量中记录的内存编号去访问内存,该过程可能产生段错误,原因是里面存储的内存编号是非法的。
    访问的字节数由指针变量的类型决定

使用指针注意的问题

空指针:值为NULL的指针变量叫做空指针,强行解引会产生段错误NULL
如果一个函数的返回值类型是指针当函数执行出错就返回NULL
如何避免空指针带来的段错误:

  1. 不使用来历不明的指针做一判断
  2. 函数的参数是指针,别人传给你的就有可能是空指针
  3. 从函数获取的返回值也可能是空指针
    if(NULL == p)//判断空指针方法
    在大多数系统中是0个别系统中是1

野指针:指向不确定的内存空间
解引用野指针的后果:

  1. 段错误(指向非法的内存

  2. 脏数组(指向别的数据

  3. 一切正常
    野指针比空指针的危害更严重,无法判断出来,而且可以使隐藏型错误短时间内不暴露出来
    所有的野指针都是程序员自己制造出来的、如何避免产生野指针

    	1 定义指针变量时一定初始化,一般初始化为空
    	2 函数不返回栈内存的地址(不要返回函数内的指针)
    	3 指针指向的内存被销毁后,指针变量要及时置空 
    

指针的运算
指针变量中存储的是整数,理论上整形数据可以使用的运算符它都可以使用,但大多数运算都是无意义
指针+n:加了整个类型的宽度n(如 int是4) 前进n个元素
指针-n:减掉宽度
n 后退n个元素
指针-指针 得到指针减指针除以宽度(相同类型的指针)计算出两个指针之间相隔多少个元素

const与指针

当我们为了提高传参效率而使用指针时,传参的效率提高了,但是也有被修改的风险
使用const可以保护指针所指向的内存
const int* p; 保护 指针 所指向的内存,不能通过解引用去修改内存中的数据
int const p;同上
int * const p;指针变量不可被修改
int const
const p;同下
const int* p;指针 变量 和 指针 缩指向的内存都不可修改

指针数组与数组指针

指针数组:就是由指针组成的数组,他的成员是指针变量。
类型* arr[长度];
数组指针:专门指向数组的指针
类型 (*arr)[长度];

指针与数组

数组名是一种特殊的指针,它是常量,不修改它的值,它与数组的内存是映射关系
数组名字==&数组名
指针变量有自己的存储空间,如果他存储的是数组的首地址,指针可以当数组使用,数组名也可以当指针使用

数组名[i] == (等价于) *(数组名+i)
数组作为函数的参数时蜕变成了指针(所以函数使用数组时长度会丢失)
(函数内int arr[] <==> int * arr)

二级指针

指向指针的指针,里面存储的是指针变量的地址。
定义:类型** 变量_pp
赋值:变量名_pp = &指针;
解引用:
*变量名_pp <=> 指针
**变量名_pp <=> *指针

函数指针

函数指针就是指向函数指针,它里面存储的是函数在代码段中所处的位置(函数名)
函数名就是个地址(整数),它代表函数在代码段所处的位置
回调函数
返回值 (*函数名)(参数类型1 参数名1,参数类型1 参数名1,…);
*如int(compar)(const void p)
typedef 返回值 (函数名)(参数类型1 参数名1,参数类型1 参数名1,…);
指针名 变量名
“day09”
int cmp(const void
p1,const void
p2)
{
return (const 类型)p1 > (const 类型)p2;
}

sizeof对于不知道的类型返回1
time_t sec = 0;
tiem_t s =time(&sec);效果一样的

堆内存

堆内存是进程的一个内存段(text、data、bss、heap、stack),由程序员手动管理,使用麻烦
为什么使用堆内存?

  1. 随着程序的复杂数量变多
  2. 其他内存段的申请和释放不受控制
    如何使用堆内存?
    C语言中内有控制堆内存的语句,只能使用C标准库提供的函数
    #include <stdlib.h>
    char str[255]
  • void malloc(size_t size);
    功能:从堆内存申请size个字节的内存,申请内存中存储是什么内存不确定
    返回值:成功则返回申请到的内存首地址,失败返回NULL
    void
    在C++编译中不能自动转换成其他类型的指针,要想兼容要需要强制类型转换
    int* p = (int*)malloc(4)

  • void free(void *ptr);
    功能:释放一块堆内存,可以释放NULL,但不能重复释放和非法地址
    注意:释放的是使用权,里面的数据不会特意清零
    上面两个速度更快

    常见的面试题:

    • 堆内存与栈内存的区别
      谁管理 大小 使用 安全 优点 缺点
    • 堆内存越界的后果
      超过33页产生段错误
      破坏malloc的维护信息,malloc/free会出错
      脏数据
    • 什么是内存泄露,如何定位内存泄露
      使用完毕的堆内存没有释放,当再次需要时再次申请且没释放,长时间积累导致可用内存变少
      通过工具分析或者通过任务管理器查看内存使用(windows),用PS命令或者通过GDB查看堆栈使用情况(linux)
      封装malloc、free函数,记录申请内存和释放内存的过程
    • 什么是内存碎片,如何避免内存碎片
      尽量使用栈内存
      不要频繁申请释放内存
      尽两申请大块内存自己管理
  • void *calloc(size_t nmemb,size_t size);
    功能:从堆内存中申请nmemb块size个字节的内存

  • void *realloc(void *ptr,size_t size);
    size 是调整后的大小
    功能:改变已经有内存块的大小,在原有的基础上调大调小
    返回值:调整后的首地址,返回值一定要重新接收,可能不是在原内存
    如果无法再原内存块上进行调整
    1 申请一块新的符合要求的内存块
    2 把原内存块上的内容拷贝过去
    3 把原内存块释放掉返回新内存块的首地址

malloc的内存管理机制:

当首次向malloc申请内存时,malloc会向操作系统申请内存,操作系统会直接分配33页的内存给malloc管理(1页等于4096字节)

但这不意味着可以<越界>访问,因为malloc把使用分配给"其他人",这样会产生脏数据

每个内存块之间会有些空隙(4~12字节),这些空隙一些是为了内容对齐,其中有4个字节记录着malloc维护信息,也可以借助计算出每个内存块的大小,当这些信息被破坏时会影响malloc和free函数的调用

使用堆内存要注意的问题
内存泄露:
内存无法再使用,也无法被释放这种叫内存泄露(函数每次调用每次泄露,会累积导致系统中可用的内存越来越少 重启可解决部分问题)

谁申请谁释放,谁知道该释放谁释放
程序一但结束属于它的资源都会被操作系统回收
如何判断内存泄露:

  • 查看内存的使用情况,windows用任务管理器 linux用ps
  • 使用代码分析工具
  • 分析代码 包装malloc、free,记录申请、释放的信息到日志中

内存碎片:
已经释放了但是不能继续用的,申请和释放的时间不协调导致的,无法避免,尽量减少
内存之间的空隙也是内存碎片(4个字节的空隙)
如何减少内存碎片

  • 尽量使用栈内存
  • 不要频繁的申请释放内存
  • 尽量申请大块字节自己管理

内存与清理函数
#include

void bzero(void *s,size_t n);
功能:把一块内存清理为0
s:内存块的首地址
n:内存块的字节数

void *memset(void *s,int c,size_t n);
功能: 把内存块按字节设置为c
s:内存块的首地址
c:想设置的的ASCII码值
n:内存块的字节数
返回值:设置成功的内存首地址

堆内存定义二维数组
指针数组:零散的内存也可以
类型* arr[n];

for(int i=0;i<n;i++)
{
	arr[i]=malloc(sizeof(类型)*m);
}
	注意:每行的M的值可以不同,这就是不规则二维数组
数组指针: 对内存要求更高
	类型(*arr)[n] = malloc(类型*m*n);
	注意:所谓的多维数组都是用一维数组模拟的

ASCII字符
\0 0
0 48
A 65
a 97
字符的输入:
scanf("%c", &ch);
ch = getchar();
**串:**是一种数据结构,由一组连续的若干个类型相同的数据组成,有一个结束标志
字符串:

由字符组成的串形结构,结束标志就是'\0'
sizeof("xixihaha")结果是9
字符串的字面值:
	"由双引号包含的若干个字符",以地址形式存在,数据存储在代码段,如果修改会产生段错误
	const char* str="1232";
	const char* str="字符串字面值";
	注意:他的结束标志隐藏在末尾
	用sizeof("strstr")来求结果是字符个数加1(\0也是一个字符)
	由char类型组成的数组,要为'\0'预留位置
常用方式:字符数组[]="字符串字面值";
	自动为\0预留位置
	赋值完成后字符串存在两份,一份存储在代码段,另一份存储在栈内存可被修改

字符串的输入:
char str[255];

scanf %s str;

scanf %s 地址
	缺点:不能输入空格

char* gets(char *s);
	功能:输入字符串,并且可以接受空格
	返回值:链式调用(把一个函数的返回值,作为另一个函数的参数)
char *fgets(char *s,int size, stdin);
功能:可以设置输入的字符串的长度size-1字符,超出部分不接收,它会为'\0'预留位置
	输入部分不足size-1部分会接受回车(\n)

字符串的输出
printf %s 地址
int puts(const char *s);
功能:输出一个字符串,会在末尾自动添加一个\n
返回值:成功输出的字符串个数

输出缓冲区
程序输入的程序并不能立即显示在屏幕上,而是先存储在输出缓冲区中,满足一些条件后才显示出来

  • 遇到\n后立即把缓冲区中的数据显示在屏幕上
  • 遇到输入语句立即显示
  • 当输出缓冲区满4k立即显示
  • 当程序结束时输出缓冲区中的数据
  • 手动刷新 fflsuh(stdout) //把缓冲区的强制显示出来 在linux下使用
    缓冲区机制可以提高数据的读写速率

输入缓冲区
程序并不立即获取到屏幕上输入的数据,而按下回车键后程序才从输入缓冲区中读取数据

  1. 当读取整形或浮点型数据时,缓冲区中的数据是字母或符号,此时将读取失败,并且会影响接下来所有的数据读取

    清空缓存区,重新输入
    scanf("%*[^\n]");//清空缓存区
    scnaf("%c");
    
  2. fgets可以制定读取size-1个字符,如果有多余的会残留在输入缓冲区中
    判断读取的字符串最后一个字符是否是\n,如果不是则说明缓冲区中有残留数据
    char arr[20]={};
    fgets(arr,20,stdin);

  3. 当先输入整型、浮点型数据,在输入字符、字符串时前一次会残留一个’\n’,影响字符、字符串的输入
    scanf(" %c", &ch);

    方法1:必须确定缓冲区中有垃圾数据,否则程序就就停下等待一个\n
    scanf("%*[^\n]");
    scnaf("%*c");//拿走一个字符

    方法2:把输入缓冲区的当前位置指针移动到末尾,只能在linux系统下使用
    stdin->_IO_read_ptr = stdin->_IO_read_end;//注意书写格式
    缓存区当前位置↑ 缓存区末尾↑
    字符串的常用操作
    #inlcude <string.h>

  • size_t strlen(const char *s);
    功能:计算字符串长度,结果 不包括’\0’

  • char *strcpy(char *dest, congst char *src);
    功能:把src拷贝到dest,相当于给字符串赋值
    返回值:dest(链接调用)

  • char *strcat(char *dest, const char *src);
    功能:把字符src追加到dest的末尾,相当于 +=

  • int strcmp(const char *s1, const char *2);
    功能:比较两个字符串,按照字典序,谁在前谁小
    返回值:
    s1 > s2 正数 bbc > abc
    s1 < s2 负数
    s1 == s2 0

    更多函数:memcpy memmove sscanf sprintf

    int sprintf(char *str, const char *format,…);
    功能:把各种类型的数据拼接 输出到str中
    返回值:字符串str长度

    int sscanf(const char *str, const char *format, …);
    功能:从str中解析数据
    返回值:成功读取到的变量个数

    void *memcpy(void *dest, const void *src,size_t n);
    功能:从src拷贝n个字节到dest中

    void *memove(void *dest, const void *src,size_t n);
    功能:从src拷贝n个字节到dest中
    与memcopy区别dest与src如果有重叠也能正常工作,他会比较dest和src的前后关系
    dest < src 从后往前拷
    dest > src 从前往后拷

预处理指令:

程序员缩编写的代码并不能真正的被编译器所编译,需要一段程序 把它翻译一下,翻译的过程叫预处理,被翻译的代码叫预处理,以#开头的都是预处理指令
查看预处理的结果:
gcc _E code.c 把预处理的结果显示在屏幕上
gcc _E code.c -o code.i 把预处理的结果存储到code.i
预处理指令的分类
#include //文件包含
#include <>从系统指定的路径查找并导入头文件
#include ““先从当前路径下查找,如果没有再从系统制定的路径查找并导入头文件
操作系统通过设置环境变量来制定头文件的查找路径,或者设置编译器参数 - I /path
#define 定义宏
宏常量:#define MAX 100//常量
优点:方便批量修改、提高可读性、安全性高、还可用在case后
末尾不要带分号,宏名全部大写
预定义的宏://使用 如: printf(”%s”, DATE);
__ func __ //获取函数名
__ FILE __ //文件名
__ LINE __ //行号(%d)
__ DATE __ //日期
__ TIME __ //时间

宏函数:其实就是带参的宏
不是真正的函数,不检查参数类型,没有传参,没有返回值
#define dfunc(a,b,c) (a+b+c)/2
1 把代码中使用到宏函数替换为宏函数后面的代码
2 把宏函数中使用的参数替换为调用者提供的参数
定义宏常量和宏函数不能换行,可以使用续行符号-> (每行都要加)
或者用大括号保护代码

实现一个交换两个变量的宏函数,能够通用
宏的二义性:
由于代码所处位置、参数不同导致宏有不同功能
如何避免二义性:宏函数整体要加小括号,每个参数都要加小括号
注意:容易出现选择题,如:那个宏具有二义性
注意:使用宏函数时提供带自变运算符的变量作为参数
常见笔试面试题:
如果替换普通类型,他们功能没有区别
#define INTP int*
INTP p1,p2,p3 //只有p1是指针p2 p3是int类型变量//指针不能连续定义
typedef int* INTP;//typedef类型定义
INTP p1,p2,p3 //p1,p2,p3都是指正

宏函数与普通的区别?

他们是什么?

  • 宏函数不是真正的函数,而是代码替换。只是使用起来像是函数

  • 函数 是一段具有某项功能的代码,会被编译成二进制指令存储在代码段中函数名就是它的首地址,有独立的命名空间、栈内存
    有什么不一样

  • 函数 返回值 类型检查 安全 压栈、出栈 速度慢 跳转(相比较而言)

  • 宏函数 返回结果 通过 危险 替换 速度快 冗余
    优缺点

  • 运算符:
    # 把宏函数的参数变成字符串
    ## 合并两个参数变成参数符

条件编译:
根据条件让代码是否参与编译

  •   	判断编译语言
      	#if __cplusplus
      		//C++
      	#else
      		//C
      	#endif//__cplusplus
    
  •   	判断系统位数
      	#if __x86_84__
      		printf("64位系统");
      	#elif __i386__
      		pfintf("32位系统");
      	#endif
    
  •    版本控制(功能)
      	#if 	
      	#elif
      	#else 
      	#endif
    
  •   	头文件卫士,防止头文件被重复包含
      	#ifndef	//if n def	写头文件这几个是必要的
      	#else
      	#endif
    
  •   	判断调试或上线//调试的时候显示上线的时候不显示
      	#ifdef 宏名 是否存在
      	#else
      	#endif
      			
      			#ifdef DEBUG
      				#define debug(...) printf(__VA_ARGS__)
      			#else
      				#define debug(...)
      			#endif     			
      			//固定格式	需要时拷贝
      			 					
      			#define error(...) fprintf(stdout,"%s:%s:%d %s %m %s %s\n",__FILE__,__func__,__LINE__,__VA_ARGS__,__DATE__,__TIME__)
      			//固定格式	需要时拷贝
      			
      			#if 1//注释大段代码
      			#endif
    

    常考面试题
    定义一个宏函数一年有多少秒
    #define YEAR_sec (3652436000u)//u无符号
    在类型重定义时#define与typeof的区别

    在定义常量时间#define与const的区别

    宏函数与普通函数的区别

头文件中应该写什么
问题1 头文件可能被任何源文件包含 意味着头文件中的内存会在多个目标文件中存在,合并时不能冲突
重点:头文件中只编写声明语句,不能有定义语句
全局变量声明
函数声明
宏常量
宏函数
typedef 类型重定义
结构、枚举、联合的声明
头文件的编写原则:

  •    对每个.c文件写一份.h文件//.h文件是对.c文件的说明	
    
  •    如果需要用到某个.c文件的变量、函数、宏,只需要把它的头文件导入。
    
  •    .c文件也要导入它的.h 摸底是让声明和定义一致
    

    头文件的互相包含
    假如a.h包含了b.h,b.h由包含了a.h, 这种情况就是编译出错
    解决:用c.h包含a.h b.h公共的部分
    错误:位置的类型’xxxx’ 一般都是由于头文件互相包含导致

Makefile

Makefile是由一系列编译指令组成的可执行文本文件,也叫编译脚本
在命令执行make命令会自动执行Makedile脚本的编译指令,他可以根据文件的修改时间来判断哪些文件需要编译,哪些不需要,从而提高编译效率

**编译规则:**
  •   1 如果此项目没有编译过则编译所有.c文件,并链接成可执行程序
      2 如果此项目编译过仅仅某个.c文件被修改,则此次只编译被修改的.c文件,并链接
      3 如果某个.h文件被修改,依赖它的所有.c都要重新编译,并链接
    

    一个最简单的Makefile编译脚本:
    执行目标:依赖
    编译指令

      被依赖的目标1:依赖的文件		//依赖的文件被修改 目标才执行
      	编译指令
      
      被依赖的目标2:依赖的文件
      	编译指令
      	
      ...
      
      负责清理的执行目标:
      	rm ... 	
    

如果要删除执行文件和所有的中间目标文件,只要执行一下 make clean 就可以了。

gcc main.o phone_book.o tools.o
vi Makefile	
	有自动推导(每行之间无间隔)	
	main.o:tools.h phone_book.h	//而main.o没有.h所以单独写
		$(CC) -std=gnu99 -c main.c		
	%.o.:%.c %.h		//.o依赖他的.c.h
		$(CC) -std=gnu99 -c $< -o [email protected]
							↑源文件 ↑目标文件	

结构

就够是由程序员自己设计的一种数据类型,用于描述一个事物的各项数据,由若干个不同基础类型组成

设计:
	struct 结构体名字		//	一般使用堆内存
	{
		类型 成员名;
		...
	};//<-不能少
	
	定义结构变量:
	struct 结构名 变量名;
	注意:定义结构变量时,struct不能省略但是可以用typedef重新定义
	
	定义结构变量初始化:
		struct 结构名 变量名 = {v1,v2,v3...};
		要根据成员顺序初始化
		struct 结构名 变量名 = {.成员名=v1,.成员名=v2,.成员名=v3,...};
		同类型的结构变量可以直接赋值
		

	
	写在函数外,(结构体名大写)
	struct Student也可以在这里定义 typedef Student{} Student; 
	{
		char name[20];
		char sex;
		long id;
		float score;
	};//main函数之外,  也就在};之间写变量
	
	main函数内:
		struct Student stu;
		//struct Student stu={"hehe"};//可以少顺序不能乱
		//struct Student stu={".di=9574,.score=53"};//没赋值为0
		strcpy(stu.name,"hehe");//或者 sprintf(stu.name,"hehe");//把hehe打印到stu.name中
		stu.sex='m';
		stu.id=9527;
		stu.score=750;
		
		printf("%s %c %ld %g \n", stu.name,stu.sex......)

	访问成员:
	结构变量名.成员名
	
	成员变量作形参时:
		由于结构变量的字节数都比较大,值传递的效率比较低,因此都传递结构变量的地址,如果不需要修改结构变量的值,可以使用const保护
		结构指针->成员名;
	typedef重定义结构类型
		typedef struct Teacher Teacher;//之后可以不使用struct
		
	结构体的成员顺序会影响它的总字节数,在设计结构体时如果成员的顺序合理可以大大节约内存
	内存对齐:
		假定第一个成员从零地址开始,存储每个成员地址编号必须能被它的字节数整除,不能整除则在末尾填充空字节//访问速度会快
	内存补齐:
		结构体的总字节数,必须是他最大成员的整数倍,如果不是则填充空字节
	在linux系统下计算结构体的对齐和补齐时如果成员的字节数字超过了4字节则按4字节算
	不同系统分别计算	#pragma pack(2)//设置补齐对齐的最大字节超过两字节按两字节算2的n次方 只降不加 n<=默认的最大字节数 windows中超过多少按多少来算
	
	位域
	typedef struct 结构体名 
	{
		类型 成员名:n;//表示该成员只占n个字节
	}结构体名;//使用位域后也要考虑补齐

联合:
联合与结构的使用方法基本一致,与结构的区别是所有成员共用一块内存。一个成员的值改变其他成员的值也会随之变化//节约 用少量的内存定义多个变量名(很少使用)
用少量的内存对应多个标识符,来达到节约内存的目的//现在基本不再使用
union Data
{
char ch;
int num;
};
联合常考的笔试题:
union Data
{
char str[10];
int num;
};
问 总字节数是多少? 答案是12//联合不需要对齐需要补齐
如何判断系统的大小端? 可以用联合判断计算机的大小端
高位数据存储在高位地址-> 小
高位数据存储在低位地址-> 大

#include <stdio.h>
//	如何判断系统的大小端?
union Data
{
	char sh;//0A~0D
	int num;//
};
int main(int argc,const char* argv[])
{
	union Data da;
	//da.sh=;
	da.num=2;
	printf("%d ",da.sh);
}
个人计算机都是小端系统,而UNIX服务器和网络设备都是大端,网络字节序也就是大端
序列化和反序列化

枚举:
枚举就是把一种数据类型可能出现的值全部罗列出来,取一个有意义的名字除此之外该类型的变量再等于其他值就是非法的(愿望)
枚举可以看做是值受限的int类型
为什么使用枚举:
为无意义的值去定义一个有意义的名字,提高代码可读性

enum Direction				//数据类型
{
	UP=183,//枚举(常量)//如果不给值从0开始加(从183开始后面就是184)
	DOWN=184,
	RIGHT=185,
	LEFT=186,
};	

enum Direction n = UP|DOWN|RIGHT|LEFT;