C语言小总结

本文是C语言的学习总结,涵盖了void类型的使用规则、结构体内存对齐、数组指针与指针数组的区别、文件读写的关键概念,以及指针函数和递归的解析。强调了在文件读写中的缓冲区管理和EOF的重要性,同时探讨了结构体的深拷贝与浅拷贝。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言

🐷如果但凡有用请点赞,因为我看不到活人头,我也算是明白为啥
好多up都说你们的点赞是我最大的动力这句话了。确实点赞才是最大的动力
希望,你们好好学习然后给我点赞!

对于有些极为难以接受的概念,让我学习C语言也很头疼,但是我想把我在学习C语言中对其深层次的思考分享给你们,让你们更加清晰,少走弯路把。第一次写博客,可能有笔误,因为没时间细看,我大致的思路我尽可能表达给你,你看着有些很多,其实大多都是比喻,我用我认为最明白的语言,希望然你理解在那些大佬理解了然后一笔带过的东西把!

1.关于void的小总结

规则一:
当函数参数想要为空时,一般还是用 fun(void)更加的好,这样在编译阶段如果 fun(3)传递参数就会出错,当然定义函数时用fun(), 在实际应用时是可以用fun(3),并且通过编译但是相较前者更好。

void fun1(void){}
void fun2(){}
int main(){
	fun1(3); //err
	fun2(3); //ok
}

规则二:
由于空指针,指向的类型为空因此,在做指针±等运算时,编译器并不知道+±-的实际指针的步长。不过可以通过强制类型转换即可

void* pvoid;
pvoid++;//err
pvoid+=1; /err
(char*)pvoid++;//ok

规则三:
void* 做万能指针!也就是说如果用void* 做函数参数任何指针类型都可以作为实际参数传递给形式参数(也就是定义那里的模板参数)这种方式在系统中十分常用!!因为他的健壮性,由于C语言绝大多数都是在用指针因此万能指针又显得格外重要。

int n=3;
int *pn=&n;
void fun(void* p,int n)
{
	*(int*)p=n;
}
int main(){
	fun(pn,3);
}

规则四:
不要把void 当作所谓的类型定义变量!如

void a;//err
void * p;//ok

因为void 英语翻译是虚无的,所以他所定义的内存空间是无,所以定义不了变量。为什么void * 可以呢,因为所有只要带上(数据类型)*变量 都是指针变量!!!!,指针类型规定为4个字节 在32,而64位则为8个字节,所以它是可以定义的,所有类型只要可以查到开辟内存的字节数,说明就可以定义变量。就void除外!因此千万不要用void 定义变量,这是很愚蠢的选择啦!


2.关于结构体在内存中的对齐方式

对齐规则

对齐规则
以最大类型为对齐单位找到结构体中最大数据类型其中包括在子结构体的最大数据类型比较得出最大的数据类型,比如int、double ,其中double 最大对齐单位为8字节!
结构体作为成员对齐如果有子结构体,则在子结构体之前和之后都按照 最大类型对齐规则而子结构体的起始内部数据填充起始时按照子结构体最大数据类型对齐,然后依次时后来的数据。

注:结构体内部数据的定义先后顺序会影响结构体的大小,因此建议先大后小这样整体相对小一些在很多情况

例题

struct
{
	int a;
	short b;
}A;

**

1.首先观察其中最大类型:int、short 其中int 为4字节因此
int最大,因此对齐大小应该为4字节!
变量存放方式计算方法

①看定义 先定义a 先放a, a为int 四个字节,放在4*0 的位置,也就是0,因为0没有放东西。就放入4个字节大小的a。

②然后看b,同样的方法,b为short两个字节,将其放在20,但是a已经存放了4个字节,因此改变21,放在第2+1也就是第三个字节位置,发现,还是a的存储位置,那就继续增加2*2,放在第4+1也就是第五个字节位置。此时为空,放入两个字节大小。

aaaa
bb**

从上面可以看到,最终如果放不下了就自动填充不管还有没有数据
综上sizeof(A)为8个字节,其中每个格子代表一个字节


3.数组指针与指针数组

  1. 简述指针数组和数组指针的区别?
    指针数组是数组,用来存放指针类型的数组!
    数组指针是指针,是指向数组类型的指针!
    其中,其实在C语言看到的复合数组类型如 数组 int a[10] ,异或者是 其他 函数名称等
    其实都是可以当作数据类型来看,只不过写的方式不同罢了,其中数组如果这么写会更加
    清晰,它是一种数据类型是自定义类型,int [10] a, 这样看是不是更加清晰数组?,其实
    数组是一种自定义类型,类似结构体,只不过它的类型决定于两个因素,第一个是数组
    存放的数据类型,第二就是数组的维数以及每个维数的个数,这几个因素共同构建了一个
    自己定义的数据类型。
2. 如何定义一个指向 int a[10] 类型的指针变量(数组指针)(使用3种方法)?
①typedef int A [10];
   A* a;typedef int (*A)[10]
   A a ;int (*a)[10]
3. 
	int a[10];
	int b[5][10];
	int (*p)[10];
	p = &a;	//为何加 &
	p = b;	//为何不用加 & 
①为什么 第一个要加& 其实可以这么来看上面的式子!
	int [10] a;
	int [10] * p;
	p=&a   这就是好像
	int a;
	int *p;
	p=&a;
②为什么第二个不用加呢
	int [10] b[5];
	int [10] *p;
	好比
	int a[5];
	int *p;
	p=a;
	
4. int a[3][5] = { 0 };
	a -> a + 0   		     
	a + i				     
	*(a+i) -> a[i]		 
	*(a+i)+j -> &a[i][j]   
	*(*(a+i)+j ) -> a[i][j]
	
深刻理解数组的含义(个人见解):
	首相 就从最简单的来最容易理解:
		int a[10];
	①理解a的意思
		(1)数组类型变量名
		(2)该数组的首元素地址
		一定要明白,它既是名字,又有另一层含义也就是首元素的地址。
		关于a大家理解都是首元素地址呢?其实这与指针相关数组依次访问
		每个元素的时候其实都是,*(a+i) 其中a为数组名称,i为偏移量喽!
		因此把数组名定为第一个元素再合适不过!
	②理解&a的意思
		int a;
		&a;
		所以? &a是什么意思呢?不就是 a的地址吗?
		因此这回就是数组变量a的地址吗?也就是大家常说的整个数组的地址
		这里有一件事很有趣,&a=a ,首地址都是一样的区别只是指针的步长
		罢了。
		
	③理解sizeof(b)|sizeof(int [10])sizeof(&b)
		好!让我们解决一直困惑我们的 sizeof
		①我们先类比最基本的sizeof 的用法!
			int a;
			sizeof(a);//里头是sizeof(变量名)求出变量占的内存
			int b[10];//这样写你还是会忘记数组是变量!
			int [10] b; //你看这不就清楚了吗
			sizeof(a); //不久清楚了吗?
			如果你还记得,sizeof(a)=sizeof(int) 对吧?
			所以sizeof(b)=sizeof(int [10])这不久搞定了吗
		②接下来就是sizeof(&b)
			一句话:一个变量取地址不就是为了给指针吗?
			但是需要类型匹配的指针类接受哦!不然内存和指针步长
			对不上, 
			接下来给你看看:&b 是给谁的.
			int b[10];
			typedef int Bp[10] ;//定义一个 int [10] int类型
								//一维数组并且有10个元素的数组类型!
								//是类型!
			Bp* p=NULL;			//定义数组类型的指针,就可以指向
			p=&b;				//数组变量了。
			类比
			int a;
			int *p;
			p=&a;

			ok! 所以sizof(&b)==sizeof(p); 注意哦所以只要是指针
			类型都是4字节!!!so,sizeof(&b)=4!
			
				


指针数组与数组指针作为函数参数传递
引:2022.1.15本来打算写一个copy的程序,想到要用argc,与argv
这时候就想到了,数组指针,指针数组作为函数参数时,用法到底时怎么样的,当然我结合上面我想对,指针数组和数组指针作为函数参数传递做一个小的总结
我就以char类型作为代表做解释了。
一般数组指针不做为函数传递,数组指针是为二维数组做准备的
当然如果你

类型指针数组数组指针
实参char* A[10]char (*B)[20]
形参形式1char* *A(数组退化指针)char
形参形式2char* A[10] (原封不动也对)

  1. int main(int argc, char *argv[]);
    argc, argv分别代表什么?

    argc 为参数个数 ,默认有一个,这个就是程序名!
    argv 就是指针数组,由于数组作为参数传递会退化为指针,因此写为 char** argv
    同样正确。


4.结构体对齐

#include<stdio.h>


#pragma pack(1)//按照最小对齐
typedef struct hard
{
	int a:4;//后面冒号代表a变量占的位数!由于不够32位所以后面还可以放
	int b:5;//b、c、d、e都已放进去不过要注意,必须连续才可以。
	int c:6;
	int d:7;
	int e:8;
}hard;

typedef struct test
{
	int a;
	char b;
	double c;
}test;
int main()
{
	printf("hard:%ld\n",sizeof(hard));
	printf("test:%ld\n",sizeof(test));
	return 0;

}



结构体中的深拷贝浅拷贝

**1.同类型结构体变量是否可以相互赋值?会存在什么风险?**

可以!
存在深浅拷贝的问题,浅拷贝导致的实质是由于结构体中的数据类型存在
指针类型,而且其指向的内存空间为堆空间,因此在做如下操作时

浅拷贝发生的条件
1.结构体中有指针
2.指针指向的内存为堆
3.只使用A=B来赋值
解决方式
重新为新的变量分配堆空间,避免指向相同内存块
struct A
{
	char * name;
};
struct A  a;
a.name=(char*)malloc(sizeof(sizeof(char)*100);//开辟堆空间
假如 a.name=0x1111;
struct A b=a;
拷贝后 b.name=0x1111;
然而并没有重新再开辟新的堆,只是共用同一个空间,因此称为浅拷贝。
要想避免给呢,就需要人为重新分配!
b.name=(char*)malloc(sizeof(sizeof(char)*100);
此时b.name=0x2222; 也就OK了完成定义变量开辟,来开辟新的内存空间啦!
两个变量没有相互交集了,这才是该有的样子。


5.文件读写!

引言:跌跌撞撞来到文件读写,虽然容易却又需要辨析很多东西。

文件分类按照硬件类型分类
设备文件普通文件
文件分类按照文件存储方式
文本文件二进制文件

①设备文件:其实就是如键盘、鼠标这些硬件所对应抽象得来的文件(万物皆文件)。
②普通文件:就是磁盘、硬盘啊,那种稳定存储设备中存储的文件。
⭐(接下来比较重要,因为关系到C语言文件的读写了)
③文本文件:文本文件说白了就是我们写入存储起来是按照ASCII码的方式存储的,比如你再文本中输入了一个‘a’,它在内存中就会存放一个’97‘ 的二进制数,一共占1个字节!
(1)文本文件中的char 是非负的也就是0-255,按照ASCII 一一对应,也没有什么压缩。很直观,比如存了你输入 ab ,它就存了 ‘a’ ->97,‘b’->‘98‘。
(2)文本文件的 结尾叫做 EOF 是个宏定义 就是个别名 它的值是-1
其实就是EOF=-1,类比与字符串的’\0’ 当看到这个时我们和系统都知道字符串到结尾了,而我们和系统看到EOF也就是-1时也就知道到结尾了,说来也有意思。
注:计算机存储的方式都是补码!!
补充:与EOF类似的有一个 库函数也就是 feof(FILE* p); 这个时判断文件到没到结尾的,当然想要知道没有到结尾时应该这么用!feof§,因为如果到结尾它返回1,没到结尾i它返回0,有时候在用条件判断时,其实要判断文件没有到结尾,因此这么用。

💊ok ,那么你了解了所有类型了,不就是读写了嘛? 其实在学习文件读写,其实让我看来最重要的不过就是问什么读写文件, 为什么它这么重要?

只要是计算机,只有两个最最重要的部分💀,一定要记住啊!
①计算器,ALU也就是负责计算
②存储器,存储数据
注:计算机无非就做两件事一件叫计算一件叫存储,存完继续算。

所以你想想,读写文件多重要了把?文件物理层面上就是存储,不过是
有区别的
,但是我们明白了读写文件其实就是做了整个计算机做的半壁江山差不多。

其实你知道这些之后,读写文件其实就很简单了
读写文件其实就做三件事
①打开文件
②读/写文件
③关闭文件

在C语言中每一个过程都对应着相应的函数来个笼统划分把

打开文件fopen
读写文件fread/fwrite
关闭文件fclose

读写过程是很简单,但是还有一个在读写过程中很重要的概念也就是缓冲区咱们常用的buffer。

BUFFER(缓冲区):
缓冲区做的事情其实就是,你本来可以一口气搬完的东西,你非得搬N趟,这部浪费时间?所以内存也是这么做,差不多了就一口气把内存的东西写道外存也就是硬盘!也可以说菜鸟驿站啥的哈哈,总之提升了效率。
缓冲区产生写延迟:
有了缓冲区,那你只有关闭了文件内容才会被写入,不然你在程序执行还没,fclose的时候它的内容没有更新,或者C语言系统给他分配的缓冲区满了,它自己就会刷新,也就是写道硬盘,或者用fflush函数想冲厕所一样,把所谓的缓冲区这个文件流水冲进马桶(也就是硬盘存储空间)。
解决方法:
①等待缓冲区满
②手动fflush(主动冲厕所)!

fopen的参数问题
其实,最主要的就是 t、和b 这两个,如果不用的画,其实fopen会智能匹配
,但是如果有时候没成功记得人为添加这两个参数。
对应关系如表:

文本文件(txt)二进制文件
tb

注:二进制文件就是机器码了都是01011111这种
补充:文本文件,是按照ASCII码来的所以每一个字符都是8位,相比较二进制文件它可以用更少的位表示,举例

文本文件二进制文件
abab
1000001、1000010(分开的)1000001+10000010(合并)=100000011(最终)

老大劲找的,这个应该比较清楚啦!
在这里插入图片描述

显然,一个表示ab 用了16位,另一个就是用9位,这数据量一目了然。
这个只能用于理解,不完全正确,但是逻辑肯定是对的!


fgets /fputs 的爱恨情仇

在函数中fputc 和putc 以及fgetc和getc 他们都可以说一模一样没有什么难度
要说有点蹩脚的那就是fgets!
先说fputs
前提是输入文本文件
①fputs 注意事项:鄙人看来,其实它做了一件事那就是将字符串的结尾’\0’去了,然后写进了文件,然后再文件的结尾添加了-1也就是EOF。后面再放入字符串,如果没有带上\n不会换行。

②fgets 则是在给定大小的缓冲区中读出字符串,
(1)最多可以度 size-1个字符,最后一位用来放\0因为输入的时候去了
(2)遇到\n会把这个字符读入缓冲区,并且以’\n’为结束标志。

注:如果想一个fputs 读一个fgets ,一定要再fputs 中的缓冲区中加上\n
不然,连续的fputs 会依次连接在一起类似strcat一样,中间的’\0’已经被去掉了。


文件读写应用中的总结!
1.首先是scanf 录入字符串的时候记得,以‘\n’换行为结束,但是这个
'\n’仍然在stdin的缓冲区中,因此正常来说加一个getchar(),把这个多余的字符吃了。
2.然后就是在C语言中一般不能对文件进行删除,那怎么办呢?
目前已知的一种方法就是
①逐行读取内容到,buf 的缓冲区,然后你不要的内容,通过fseek移动光标来避免读改行内容,然后将原文件清空,再重新写入。
详情请看我C作业,那里有一个250行的程序也就是一个关于读写配置文件的程序。


6.指针函数与函数指针

①指针函数,是返回值为指针的函数
int * fun(int a)
②函数指针,是指向函数的指针,

int (*pFun)(int);//函数指针

重点是函数指针,接下来好好讨论一下,函数指针这个问题
函数指针的用法

1.首先用于指向函数
int fun(int a)
{
	return 1;
}
int(*pFun)(int);
pFun=fun;
pFun(1);
指向了该函数就可以当函数名使用,因为拥有了函数的如可地址,并且给了
参数。
2.(重点)
作为形参使用(回调函数)
int  add(int a, int b)
{
	return a+b;
}
int  minus( int a,int b)
{
	return a-b;
}
int fun( int a, int b, int (*pFun)(int ,int ))
{
	pFun(a,b);
}
我们可以这样做
fun(1,2,add);
fun(1,2,minus);
只需要传入的函数名,就可达到,一个函数可以拥有多种功能,也称多态。
3.
函数指针数组
int (*pFun[3])(int ,int );
这是一个 类型为 函数指针 的 数组
这样就可以,在该数组存放多个函数名(即函数入口地址)

其实抽象来看,函数也是一种数据类型罢了,只不过它相比于普通函类型多了,让返回值参数,传入参数类型,以及参数类型,算是自定义类型
因此有能指向 函数类型的指针也是合理的。


7.宏定义

1.不带参数的宏定义

#define  MAX  10
宏定义说白了就 你写 MAX  最后就会被替换成 10
这个过程是在预处理阶段处理的

2.带参数的宏定义

其实可以算个函数了
但是还是等价替换,不过有参数,形参会和实参结合的
#define mutlipe(a,b) a*b
不过要注意,它只会把 a 与b 直接替换,并不会考虑运算优先级,
加入你调用 mutlipe(1+2,3) ->  1+2*3  而不是 (1+2*3
因此常加括号
如下
#define mutlipe(a,b)  (a)*(b)  这样比较稳妥

这就是宏定义函数

8.递归

递归说白了,就是套娃,镜子里反射镜子,里头镜子里头还有镜子这样子。

**函数不仅可以调用别的函数,还可以调用自身,而且在自己还没定义完全**

int fun(int a)
{
	fun(a-1);
	return 0;
}
自己call 自己,也就是递归
递归就可以做很多了
调用顺序
fun(a)->fun(a-1)->fun(a-2)-> *****
当然你要给它确定好结束 所以来个条件 return 就很重要
不然就是程序永远递归,就死了
ok

请添加图片描述

收工
先这样,先偷会懒晚哈哈

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱小李的小潘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值