C语言归总学习2

21 文件(文件不只有数据还需要协议)

所谓文件(file)一般指存储在外部介质上数据的集合,比如我们经常使用的txt、 bmp、jpg、exe、rmvb等等。这些文件各有各的用途,我们通常将它们存放在磁盘或者可移动盘等介质中
**文件无非就是一段数据的集合,这些数据可以是有规则的集合,也可以是无序的集合。操作系统也就是以文件(block)为单位对数据进行管理的。**也就是说,要访问外部介质上的数据,必须先按照文件名进行查找,然后从该文件中读取数据。要想写数据到外部介质,必须得建立一个文件,然后再写入。因此,你眼前的文件只是数据的集合。

文件─般包括三要素:文件路径、文件名、后缀。
由于在C语言中‘'一般是转义字符的起始标志,故在路径中需要用两个’'表示路径中目录层次的间隔,也可以使用’/‘’作为路径中的分隔符。

数据的输入和输出几乎伴随着每个C语言程序,所谓输入就是从“源端”获取数据,所谓输出可以理解为向“终端”写入数据这里的源端可以是键盘、鼠标、硬盘、光盘、扫描仪等输入设备,终端可以是显示器、硬盘、打印机等输出设备。在C语言中,把这些输入和输出设备也看作“文件”。(站在应用程序的角度)

1.数据流:(缓冲区(I/O流)是由两个缓存组成的。)

I/O设备的多样性及复杂性,给程序设计者访问这些设备带来了很大的难度和不便。为此,ANSIC的I/O系统即标准I/O系统,把任意输入的源端或任意输出的终端,都抽象转换成了概念上的“标准Ⅳ/O设备”或称“标准逻辑设备”。程序绕过具体设备,直接与该“标准逻辑设备”进行交互,这样就为程序设计者提供了一个不依赖于任何具体I/o设备的统一操作接口,通常把抽象出来的“标准逻辑设备”或“标准文件”称作“流”。 其他的工作交由OS来进行,屏蔽了硬件之间的差异。

把任意I/O 设备,转换成逻辑意义上的标准I/O设备或标准文件的过程,并不需要程序设计者感知和处理,是由标准I/O系统自动转换完成的。故从这个意义上,可以认为任意输入的源端和任意输出的终端均对应一个“流”.
**流按方向分为:输入流和输出流。**从文件获取数据的流称为输入流,向文件输出数据称为输出流。
流按数据形式分为:文本流和二进制流。文本流是 ASCIl码字符序列(针对C),而二进制流是字节序列。

tip:文本流配置错误会出现乱码。

**流是一种抽象的概念,负责在数据的产生者和数据的使用者之间建立联系,并管理数据的流动。**图示:
在这里插入图片描述
指程序与数据的交互是以流的形式进行的。进行C语言文件的存取时,都会先进行“打开文件”操作,这个操作就是在打开数据流,而“关闭文件”操作就是关闭数据流。

缓冲区(Buffer):
指在程序执行时,所提供的一块存储空间(在内存中),可用来暂时存放做准备执行的数据。它的设置是为了提高存取效率,因为内存的存取速度比磁盘驱动器快得多。
C语言的文件处理功能依据系统是否设置“缓冲区”分为两种:一种是设置缓冲区,另一种是不设置缓冲区。由于不设置缓冲区的文件处理方式,必须使用较低级别的I/O函数(包含在头文件io.h和fcntl.h中)来直接对磁盘存取,这种方式的存取速度慢,并且由于不是C的标准函数,跨平台操作时容易出问题。所以一般利用第一种处理方式,即设置冲区的文件处理方式,即CPU一般情况下只能对内存读写,不能访问外设。外设要输入输出数据,也只能写入内存或从内存读取。(一切设备都只能和内存直接打交道)
当使用标准I/O函数(包含在头文件stdio.h中)时,系统会自动设置缓冲区,并通过数据流来读写文件。当进行文件读取时,不会直接对磁盘进行读取,而是先打开数据流,将磁盘上的文件信息拷贝到缓冲区内,然后程序再从缓冲区中读取所需数据,如下图所示:
在这里插入图片描述在这里插入图片描述
ps:文件的读写,主存有一个缓冲区,磁盘还有一个缓存。(由于磁盘与内存之间的读写速度严重不匹配) 磁盘缓存的延迟主要是由于磁盘读写慢的原因。所以是该缓冲区(I/O流)是由两个缓存组成的。

当需要读取文件时(即CPU的数据没有在内存中找到,需要去硬盘读取),会进行系统调用,通过OS来管理,告知磁盘将数据写入到磁盘内的缓存中,当缓存满或数据写完时,告知OS将数据写入主存中的缓冲区缓存,然后写入主存,并产生中断告知CPU,数据准备好了。
写入类似,先将数据写入主存的缓冲区缓存,然后当写入完毕或缓冲区满时,通过系统调用写入磁盘的磁盘缓冲区(buff,很快一下就存入该缓存),然后再慢慢写入磁盘。

2.为什么需要缓冲区(函数库CRT管理,写入或读入时交由OS):

缓冲最为常见于IO系统中,设想一下,当希望向屏幕输出数据的时候,由于程序逻辑的关系,可能要多次调用printf函数,并且每次写入的数据只有几个字符,如果每次写数据都要进行一次系统调用,让内核向屏幕写数据,就明显过于低效了,因为系统调用的开销是很大的,它要进行上下文切换、内核参数检查、复制等,如果频繁进行系统调用,将会严重影响程序和系统的性能
一个显而易见的可行方案是将对控制台连续的多次写入放在一个数组里,等到数组被填满之后再一次性完成系统调用写入,实际上这就是缓冲最基本的想法。当读文件的时候,缓冲同样存在。我们可以在CRT中为文件建立一个缓冲,当要读取数据的时候,首先看看这个文件的缓冲里有没有数据,如果有数据就直接从缓冲中取。如果缓冲是空的,那么CRT就通过操作系统一次性读取文件一块较大的内容填充缓冲。这样,如果每次读取文件都是一些尺寸很小的数据,那么这些读取操作大多都直接从缓冲中获得,可以避免大量的实际文件访问。

除了读文件有缓冲以外,写文件也存在着同样的情况,而且写文件比读文件要更加复杂,
因为当我们通过fwrite向文件写入一段数据时,此时这些数据不一定被真正地写入到文件中,而是有可能还存在于文件的写缓冲里面,那么此时如果系统崩溃或进程意外退出时,有可能导致数据丢失,于是CRT 还提供了一系列与缓冲相关的操作用于弥补缓冲所带来的问题。

fflush()当输出时会将在剩余缓冲区的内存写入到文件中,而当读入时,只是清空了缓冲区,即此时需要接受新的数据。

3.文件类型:

分为文本文件和二进制文件两种。
文本文件是以字符编码的方式进行保存的。二进制文件将内存中数据原封不至文件中,适用于非字符为主的数据。如果以记事本打开,只会看到一堆乱码。
其实,除了文本文件外,所有的数据都可以算是二进制文件。二进制文件的优点在于存取速度快,占用空间小,以及可随机存取数据。适用于音频和视频
在这里插入图片描述
文本文件怎么转换为字符的呢?

itoa(int value,char *string,int base);
atoi//将字符串转换成整形

4.文件存取方式:

包括顺序存取方式和随机存取方式两种。
顺序读取也就是从上往下,一笔一笔读取文件的内容。保存数据时,将数据附加在文件的末尾。这种存取方式常用于文本文件,而被存取的文件则称为顺序文件。
随机存取方式多半以二进制文件为主。它会以一个完整的单位来进行数据的读取和写入,通常以结构为单位。

文件对象(所以对文件的操作都是经过文件对象对文件的操作)

C语言文件系统中的类型:
FILE:对象类型,足以保存控制C I/O 流所需的全部信息
fpos_t:非数组完整对象类型,足以唯一指定文件的位置和多字节剖析状态每个FILE对象直接或间接保有下列信息:(由文件流来管理
(C95)字符宽度:未设置、窄或宽。
(C95)多字节与宽字符间转换的分析状态 ( mbstate _t类型对象)
缓冲状态:无缓冲、行缓冲、全缓冲。
缓冲区,可为外部的用户提供缓冲区所替换。
**I/O模式:**输入、输出或更新(兼具输入与输出)。
二进制/文本模式指示器
文件尾指示器
错误状态指示器。
文件位置指示器,可作为fpos_t类型对象访问,对于宽流包含剖析状态。(C11)在多个线程读、写、寻位或查询流时避免数据竞争的再入锁。

宏常量

EOF  // int 类型的负值整数常量表达式  #define EOF (-1)  一般用于判断文本文件的(末尾)结束

EOF是文本文件结束的标志。在文本文件中,数据是以字符的ASCⅡ代码值的形式存放,普通字符的ASCⅡ代码的范围是32127(十进制),EOF16进制代码为0xFF(十进制为-1),因此可以用EOF作为文件结束标志。
当把数据以二进制形式存放到文件中时,就会有-1值的出现,因此不能采用EOF作为二进制文件的结束标志。为解决这一个问题,ASCI C提供一个feof函数,用来判断文件是否结束。feof函数既可用以判断二进制文件又可用以判断文本文件。

FOPEN_MAX //能打开的最大文件数        #define FOPEN_MAX  20
FILENAME_MAX //能最大包含文件名的数组 #define FILENAME_MAX 260
BUFSIZ //setbuf() 可以设置缓冲区的大小#define BUFSIZ 512

在这里插入图片描述
ps:EOF 和定位宏最重要

6.C语言提供的标准文件(可以重定位)
通常把显示器称为标准输出文件,printf 就是从stdout读取数据后向这个文件输出数据;
通常把键盘称为标准输入文件,通过键盘输入到stdin后scanf 从其中读取数据。
在这里插入图片描述
在默认情况下,stdout是行缓冲的,他的输出会放在一个buffer里面,只有到换行的时候,才会输出到屏幕而stderr是无缓冲的,会直接输出。

int feof(FILE *stream);其功能是检测流上的文件结束符,如果文件结束,则返回非0值,否则返回0

void perror ( const char * str );参数 s 所指的字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量errno的值来决定要输出的字符串。在库函数中有个errno变量,每个errno值对应着以字符串表示的错误类型。当你调用"某些"函数出错时,该函数已经重新设置了errno的值。perror函数只是将你输入的一些信息和errno所对应的错误一起输出。

5.文件操作(https://zh.cppreference.com/w/c/io)

C语言操作文件分为三步,1)打开文件,2)读写文件,3)关闭文件。
在这里插入图片描述
文件名可为绝对路径也可为相对路径。 但LINUX文件系统与WINDOWS文件系统不同,WINDOWS可以根据盘符来存放文件,而LINUX没有盘符的概念,需要目录(存放在哪个目录),不过其实都是挂载

ps:r不会创建文件,尽量不要以w打开文件
在这里插入图片描述

ps:
1.打开的文件必须关闭,不然其内容可能将不会写入磁盘,造成数据不一致。
2.打开文件时,生成了一个文件结构,该结构由操作系统管理。在关闭时需要销毁该文件结构。

FILE *fp=NULL;
fp=fopen("D:\\sqh.txt","w"); // \\转义  绝对路径
if(fp==NULL)
{
	printf("open file failure \n ")
	exit(EXIT_FAILURE);  //stdlib.h
}

fclose(fp);//将缓冲区的内容写入硬盘,该缓冲区由主存管理。
fp=NULL;//类似malloc free操作过程 

return 0;

文本文件操作(文本文件的操作是靠格式符来控制的,如空格,制表,换行等,冒号)

6 sprintf pirntf fprintf fputc putchar puts 返回值都为转换后字符串的长度
//将格式控制符转换后的字符串写入到buff中
int x=sprintf(buff,"a=%d b=%d \n",a,b);//每个字符占1个字节

float ft=12.23;//"12.23";
sprintf(buff,"%f",ft); //以字符串的形式写入buff
sprintf(buff,"%o",a);//以8进制存放,可惜不能以2进制存放

//将格式控制符转换后的字符串写入到stdout中,(stdout其实也是个缓存,只不过是行缓存,只有遇到\n 才会打印)
int x=printf("a=%d b=%d \n",a,b);//其底层调动的是fprintf(stdout,"a=%d b=%d \n",a,b); 即此时不像文件写入,而是向标准输出设备写入

//将格式控制符转换后的字符串写入到FILE类型对象fp中,其实fp对象是一个结构体,fp的成员有一个base缓存,所以也是写入到缓存
int x=fprintf(fp,"a=%d b=%d \n",a,b); //可以将数组一个一个放进去。

int putchar(int char)  //其功能是把参数 char 指定的字符(一个无符号字符)写入到标准输出 stdout 中

int fputc (int c, File *fp)// 将字符c写到文件指针fp所指向的文件的当前写指针的位置

 int fputs(const char *s,FILE * stream);//函数将字符串指针s所指向的字符串中的内容写到流stream中。标志结束的空字符(NULL)不写。函数也不另外增加一个换行符。只是输出字符串中的所有字符。 函数操作成功时返回值为0,否则返回非0值。
 
int puts(const char *string);//puts()函数用来向标准输出设备(屏幕)输出字符串并换行,具体为:把字符串输出到标准输出设备,将'\0'转换为回车换行。其调用方式为,puts(s);其中s为字符串字符(字符串数组名或字符串指针)。

7 sscanf(当字符串能被按特定符号等分时,可提取字符串) fscanf scanf fgets fgetc
//返回值为正确读取了多少个字符
int x=sscanf("12 34",%d %d,&a,&b)//从字符串里获取数据  返回值为正确读取了多少个字符
int x=sscanf("192.128.0.4",%d.%d.%d.%d,&a,&b,&c,&d)//从字符串里获取数据

scanf("%d",&ar[i]);  //从标准输入流中读取。  打在打印时需要回车所以在缓冲区内多添加了\n,当循环时,需要fflush刷新

//从文件流内读取
fscanf(fp,"%d",&ar[i]);//读取后给ar[i],相当于一个一个从文件流里读出来,遇到空格和换行就会结束读取

char *fgets(char *str, int n, FILE *stream); //从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止  会将\n读取到buff

int fgetc(FILE *stream);//读取一个字节后,光标位置后移一个字节,该字符作为一个无符号字符读取,并被转换成为一个整型值如果到了文件的结尾或遇到读错误,将返回EOF。由于EOF是一个有效的整型值,当你操作二进制文件时,必须用feof函数进行文件结束检测。同样也必须使用ferror函数进行出错检查。  

在这里插入图片描述

二进制文件操作(二进制文件在内存中是按固定大小划分的,一个数据占类型大小*字节)

二进制文件操作与文本文件类似。只是打开模式不同。此外文件名的后缀名对文件的内容没有影响。但是二进制写文件操作为fwrite、读操作为fread
在这里插入图片描述在这里插入图片描述
tip:一般不会以二进制来进行文件读写,格式控制及其复杂,加密文件一般也是以文本文件进行加密。

int fseek(FILE *stream, long offset, int fromwhere);//函数设置文件指针stream的位置。文件指针指向文件/流。位置指针指向文件内部的字节位置,随着文件的读取会移动,文件指针如果不重新赋值将不会改变或指向别的文件。  offset是1字节 需要转换成类型

fseek函数一般用于二进制文件,也可以用于文本文件

fseek(fp,0,SEEK_END)//将文件定位符放在末尾  
fpos_t set=0;
fgetpos(fp,&set);//读取当前文件定位符的位置,即此时在末尾,可以读出文件的大小 但是对文件文本无效
//int ar[]{12,15,34,56,780};// c  e
//若是以16进制存放,则存放内容以存入内容一致
int ar[]{0x12,0x23,0x34,0x56,0x780};

int n=sizeof(ar)/sizeof(ar[0]);
FILE *fp=NULL;
fp=fopen("D:\\sqh.date","wb"); // \\转义  绝对路径
if(fp==NULL)
{
	printf("open file failure \n ")
	exit(EXIT_FAILURE);  //stdlib.h
}
fwrite(ar,sizeof(int),n,fp);
//fread(ar,sizeof(int),n,fp);//可以多个数据读入
fclose(fp);//将缓冲区的内容写入硬盘,该缓冲区由主存管理。
fp=NULL;//类似malloc free操作过程 

return 0;

8 文件文件不只有数据还需要协议

如当链表(树)的数据读入文件后,当读出时如何还原链表(树)以前的结构等,此时就需要用户之间的协议。
即序列化和反序列化

9 缓冲和非缓冲区文件系统

在ANSI C标准中,使用的是“缓冲文件系统"。所谓缓冲文件系统指系统自动地在内存为每一个正在使用的文件名开辟一个缓冲区,从内存向磁盘输出数据必须先送到内存中的缓冲区,装满后再一起送到磁盘去。反向也是如此

//vs2012 stdio.h中的FILE结构体:

在这里插入图片描述

tip:4K是由OS分配的,OS管理内存是根据页的方式进行管理(内存管理:固定内存管理,可变内存管理,段内存管理,页内存管理,段页内存管理),对于OS来说,程序的代码段数据段是透明的,OS只是负责将内存进行管理,而程序的代码段分配到哪几页,数据段分配到哪几页,OS是不关心的,即页写数据与OS无关,OS只管分配页,而分配的页为什么是4K呢?(虚拟内存),因为磁盘分配的页是4K,需要对应。

22 关键字 typedef sizeof extern static const bool

ps:为什么需要使用关键字,首先是为了程序能更加便于书写,其次是为了使得填充编译器和CPU造成的一些怪异问题。

typedef是在计算机编程语言中用来为复杂的声明定义简单的别名。它本身是一种存储类的关键字,与auto、extern、mutable、static、register(存储类别关键字)等关键字不能出现在同一个表达式中。
typedef 将一切合法的变量定义转变为类声明型

与宏的区别:

#define	SINT int *//将SINT 替换成int *
typedef int* TINT;//将合法的定义变为类型
#if 0
int main()
{
	SINT a, b;  // int * a, int b 宏是替换
	TINT c, d;  // int * c ,int * d;  TINE是一个新类型声明  即c,d是TINT类型
	int * e, f; // int * e, int f   *与e结合  即 int (*e) ,f;
	return 0;
}

sizeof() 计算类型的大小或变量的大小,也就字节个数,其在编译阶段就将类型大小计算好了
PS:strlen()是一个函数 ,其计算除‘\0’外,字符串中具有的字符个数,不能传入NULL

extern 用在全局变量或函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用"。
ps 需保证一个特定名称的所有外部定义在每一个目标模板中都有相同的类型,不然可能发生存储空间方式不同
尽管在某些上下文环境,数组与指针非常类似,但他们毕竟不同。尽管在一个语句中引用filename的值将得到指向该数组起始元素的指针,但第一个filename的类型是“字符数组”,而不是“字符指针”。第二个filename被声明为一个指针。这两个对filename的声明使用的存储空间方式不同,只是在操作数组时,数组会退化而已,不代表数组就是指针,毕竟存储空间方式不同;
如char filename[]=”/etc/passwd”; //file1.cpp

extern char * filename; //file2.cpp
static关键字
C语言中static关键字修饰变量和函数,C++有其他用法;
1.局部变量
2全局变量
3.函数
静态变量(static)都在数据区(.data)分配,不会随着函数的退出而释放,且该局部变量未初始化会自动进行初始化为0
初始化不为0的全局变量、静态变量分配在.data区,修饰函数里的局部变量时其定义是在函数内定义的。它的作用域在是函数内的,其生命期在整个文件。

fun(int i)
{
	static x=10;//编译器直接将其放在.data区,不产生具体的代码
	static y=i;//编译器将检查其标志位置位
	
}

static修饰符是一个能减少命名冲突的有用工具。将局部变量的生存期扩大到全局,而将全局变量的作用域限制在了整个文件,使得其他文件不可见,其他文件可以定义与其同名的变量,两者互不影响。
函数变为静态函数也是限制了作用域,限制在一个源文件里,其他文件不能调用
常见的两种用途:
1)统计函数被调用的次数。
2)减少局部数组建立和赋值的开销,变量的建立和赋值是需要一定的处理器开销的,特别是数组等含有较多元素的存储类型。在一些含有较多的变量并且被经常调用的函数中,可以将一些数组声明为static类型,以减少建立或者初始化这些变量的开销。(主要用途)
若全局变量仅在单个C文件中访问,则可以将这个变量修改为静态全局变量,以降低模块间的耦合度;若全局变量仅由单个函数访问,则可以将这个变量改为该函数的静态局部变量,以降低模块间的耦合度;

const 使变量成为常变量
PS:const 变量必须初始化。

int main()
{
	const int a = 10;
	int b = 0;
	int *p = (int *)&a;//p的指向了a啊,即存了a的地址 
	//a = 100;
	*p = 100;
	b = a;
	cout << a << b << *p << endl;
	//*p 分两步 1.取的p所存a的地址,然后在根据a的地址找到对应空间修改
	return 0;
}

输出
在这里插入图片描述
在这里插入图片描述
神奇不,a=100的,但输出为10。const 使得a成为了常变量

bool类型
bool只有true和false;在C语言中0是false,其他情况(非0)都为true
在.c文件中需要引入头文件#include <stdbool.h>;在.cpp 文件中直接使用。

bool a=45;//编译时会根据确定类型、大小   转变了45为true

三目运算符
能进行简单的比较替代if
switch
switch是另外─种选择结构的语句,用来代替简单的、拥有多个分枝的if else 语句。

switch(整型变量表达式)//直线翻译: switch语句,即“切换”语句; case即“情况”。
{
case常量1:语句块1; break; 
case常量2∶语句块2; break;
case常量3∶语句块3; break;..
case常量n:语句块n; break;
default:语句; break;
}

1)只能针对基本数据类型中的整型类型使用switch,这些类型包括int、char等。对于其他类型,则必须使用if语句。
2) switch()的参数类型不能为浮点数,字符串。(必须是整型类型)。
3) case标签必须是常量表达式(constantExpression),如42或者’4’+20。
4) case标签必须是惟一常量;也就是说,不允许两个case具有相同的常量值。
5)default 不是必须的。当没有default时,如果所有case 都匹配失败,那么在switch中就什么都不执行。接着执行后续代码。

case &a+10 : printf("...."); break//错误的

&a是个常量呢还是变量呢?
一般我们都是这么写的

int *p=&a;   //即p指向a的地址 &a其实就是int *类型

23 类型转换

运行时,运算符两边的类型是一致的(无论是默认转还是强转)。
在这里插入图片描述
编译器在处理需要默认类型转换时,将<-的类型自动转为它对应左边的类型进行计算(),如
float 转为double 进行运算,而上箭头不会自动转,而是当运算符左右两边类型还不相同时才会进行转换
横向转换(无条件转换),如两个float运算时自动转为double,运算结束在返回为float,提高运算的精度
纵向转换,运算(比大小)时小的转为大的(往上),赋值是截断
其他转换没有优先级,有多种转换方式
由于计算机表示小数(包括float,double)都有误差,我们不能直接用==判断两个小数是否相等。而是利用两个小数差的绝对值,如小于0.0000001(.后6个0)
当进行运算时当一个操作数为unsigned时,另外一个也会转换

24 字符串

字符,实际代表一个整数,整数值对应于该字符在编译器采用的字符集中的序列值
字符串,代表的是一个指向无名数组起始字符的指针,该数组被双引号之间的字符以及一个额外的二进制值为零的字符"\0"初始化
数组定义的字符串,将字符串的拷贝放在了栈区,通过str下标可以修改字符串的值,且后面的空间用’\0’填充。


#include<stdio.h>
#include<string>
int main()
{	
	char stra[8] = { "tulun" };//将"tulun"被拷贝放在了栈区 通过str下标可以修改字符串的值,且后面的空间用'\0'填充
	// strlen(stra); 5
	//sizeof(stra);//?     8个char
	char strb[]={"tulun"};
	const char *spa = "tulun"; //"tulun"被放在了.data 区,是常性的 通过*spa不能修改字符串的值
	// strlen(spa); 5
	int a=10; //10放在哪里? 10被当做了代码,不可访问,10是立即数(立即数寻址)
	//sizeof(spa); 4
	//sizeof("tulun");//? 6 无名数组 数组开辟的内存个数
	const char *spb = "tulun";
	printf(" %d \n", (stra == strb));//0 数组名比较,每个数组都有自己开辟的数组空间,数组名指向了首地址
	printf(" %d \n", (spa == spb)); //1 指向字符串的指针比较,"tulun"字符串被放在了.data区,指针指向同一个地方
	printf(" %d \n", (strlen(stra)));
	printf(" %d \n", (sizeof(stra)));
	printf(" %d \n", (sizeof(strb)));//6 数组开辟的内存个数
	printf(" %d \n", (strlen(spa)));
	printf(" %d \n", (sizeof(spa)));
	printf(" %d \n", (sizeof("tulun")));//
	return 0;
}

ps:sizeof() //空间大小
在这里插入图片描述
ps:数组都开辟自己的空间且字符串被拷贝到栈区存放在了对应的数组里,数组名指向了各自的首地址,而指针指向的字符串常量指向的是同一处(编译器优化)(指针内保存的是字符串的地址,而不可能保存字符串),即一个无名数组的起始地址,在.DATA区

 
#include<iostream>
#include<string.h>
#include<stdio.h>
using namespace std;
int main()
{
	char stra[8] = { "tulun" };
	char strb[] = { 't','l','j' };
	const char *spa = "tulun"; 
	int a = 10; 
	const char *spb = "tulun";
	printf(" %s \n", stra);//正常打印,遇到'/0'结束	
	printf(" %s \n", strb);//直到遇到'/0'才结束,中间存在对齐
	printf(" %s \n", spa);//将spa指向的字符串正常打印
	cout << stra << endl;
	cout << strb << endl;
	cout << spa << endl;//同样的
	return 0;
}

在这里插入图片描述

25 分治策略与递归

关键字:问题 规模
分治策略:是将规模比较大的问题可分割成规模较小的相同问题。问题不变,规模变小。
分治法所能解决的问题一般具有以下四个特征:
1.该问题的规模缩小到一定的程度就可以容易地解决。

2.该问题可以分解为若干个规模较小的相同问题。

3.使用小规模的解,可以合并成,该问题原规模的解。

4.该问题所分解出的各个子规模是相互独立的。(不然导致内存浪费)

递归:在分治策略中递归地求解一个问题,在每层递归中应用如下三个步骤;
分解︰将问题划分成一些子问题,子问题的形式与原问题一样,只是规模更小。

解决︰递归地求解子问题。如果子问题的规模足够小,则停止递归,直接求解。

合并∶将小规模的解组合成原规模问题的解。

ps:
**1.递归有两个过程,一个递推,一个回退。**这两个过程由递归终止条件控制,即逐层递推,直至递归终止条件满足,终止递归,然后逐层回归。
2.递归由于借助于栈(栈空间有限,栈溢出)不适用于大数据量,所以如快排等适用于小数据量。
3.每次调用发生时都,首先判断递归终止条件。
递归调用同普通的函数调用一样,每当调用发生时,就要分配新的栈帧(形参数据,现场保护,局部变量);而与普通的函数调用不同的是,由于递推的过程是一个逐层调用的过程,因此存在一个逐层连续的分配栈帧过程,直至遇到递归终止条件时,才开始回归,这时才逐层释放栈帧空间,返回到上一层,直至最后返回到主调函数。
4.递归一般可以写成数学形式

26 栈对齐和结构体对齐 *

1 栈对齐:
栈对齐将栈划分了字节(4字节、8字节…),即若为4字节划分 int占4字节、char 占4字节 ,同时编译器还会进行编译优化,如

int a=1;
char b=1;
int c=1;
char d=1;//编译时,编译器会进行优化可能会将其变为 int a=1,char b=1,char d=1,int c=1;已节省栈空间

2 结构体对齐(定义顺序按类型大小从大到小定义(贪心算法,先放最大的,然后在接着放小的))
结构体内部定义的类型顺序一般不会被编译器优化。

由于存储变量地址对齐的问题,计算结构体大小的3条规则:
1.结构体变量的首地址,必须是结构体变量中的“最大基本数据类型成员所占字节数”的整数倍。
2.结构体变量中的每个成员相对于结构体首地址的偏移量,都是该成员基本数据类型所占字节数的整数倍。
3.结构体变量的总大小,为结构体变量中“最大基本数据类型成员所占字节数"的整数倍。

ps:

  1. 数组可拆分成一个一个基本类型的集合
  2. 预处理指令#pragma pack(n) 可以改变默认对齐数。n取值1,2,4,8,16 需要#pragma pack 结束 vs中默认值=8,gcc默认值=4;
  3. 结构体之间比较不能使用strcmp,由于存在对齐(空出来的空间0xcccc)
struct sdate
{
	int year;
	int month;
	int day;
};
struct Student1
{
	char name[20];//20
	char id[10];//20+10
	int age;//32+4
	float score;//36+4
	struct sdate binary;//40+12
	double grade;//54+8 =62 -->64
};

//sizeof 64

为什么要进行对齐
1)内存大小的基本单位是字节(byte),理论上来讲,可以从任意地址访问变量,但是实际上,cup并非逐字节读写内存,而是以2,4,或8的倍数的字节块来读写内存,因此就会对基本数据类型的地址作出一些限制,即它的地址必须是2,4或8的倍数。那么就要求各种数据类型按照一定的规则在空间上排列,这就是对齐。
2)有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。
3)由于不同平台对齐方式可能不同,如此一来,同样的结构在不同的平台其大小可能不同,在无意识的情况下,互相发送的数据可能出现错乱,甚至引发严重的问题。

27 缓冲区

void *类型
缓冲最为常见于IO系统中,设想一下,当希望向屏幕输出数据的时候,由于程序逻辑的关系,可能要多次调用printf 函数,并且每次写入的数据只有几个字符,如果每次写数据都要进行一次系统调用,让内核向屏幕写数据,就明显过于低效了,因为系统调用的开销是很大的,它要进行上下文切换、内核参数检查、复制等,如果频繁进行系统调用,将会严重影响程序和系统的性能。
一个显而易见的可行方案是将对控制台连续的多次写入放在一个数组里,等到数组被填满之后再一次性完成系统调用写入,实际上这就是缓冲最基本的想法。当读文件的时候,缓冲同样存在。我们可以在CRT中为文件建立一个缓冲,当要读取数据的时候,首先看看这个文件的缓冲里有没有数据,如果有数据就直接从缓冲中取如果缓冲是空的,那么CRT就通过操作系统一次性读取文件一块较大的内容填充缓冲。这样,如果每次读取文件都是一些尺寸很小的数据,那么这些读取操作大多都直接从缓冲中获得,可以避免大量的实际文件访问。

缓冲区(栈)溢出
C语言不进行边界检测如strlen、gets、scanf等函数,不知道缓冲区大小(不知道缓冲区极限,什么时候停止输入)则会导致数据污染(甚至改变主调函数的返回地址)
解决
1.使用安全的函数如gets使用fgets等
2.系统提供的栈随机化技术(ALSL)(但全局变量和代码本身的位置并没有改变)
3.系统将栈和堆进行保护,不可执行属性
4.系统提供使用cahary或其他机制检测代码中潜在的缓冲区溢出风险

28 动态内存管理

1. 动态内存

栈区︰我们知道栈区在函数被调时分配,用于存放函数的参数值,局部变量等值。在windows 中栈的默认大小是1M,在VS 中可以设置栈区的大小。在Liunx中栈的默认大小是10M,在gcc编译时可以设置栈区的大小。

堆区:程序运行时可以在堆区动态地请求一定大小的内存,并在用完之后归还给堆区(当有多个程序时,堆区可以共享,你用我也可以用,当一个程序用完后归还,另一个即可以申请,即充分利用有限的资源)。在Liunx系统中堆区的大小接近3G。windows下的大小?

ps:一般情况下我们需要大块内存或程序在运行的过程中才知道所需内存大小,我们就从堆区分配空间。(栈区分配的空间只有1M,且一般需要确定大小,所以都是大开小用,而堆区是通过需要的内存大小进行储存的)

2.动态内存分配函数(必须妥善保管其申请空间返回的地址)

C语言中动态内存管理的有四个函数:malloc, calloc,realloc,free,都需要引用stdlib.h文件

  1. malloc向堆区申请一块指定大小的连续内存空间
void * malloc (size _t size);// typedef unsigned int size_t
//若分配成功,则返回为任何拥有基础对齐的对象类型对齐的指针
//若size为零,则 malloc 的行为是实现定义的(在VS已经定了,就返回一个堆区的地址,只是用户无法使用)。例如返回空指针。亦可返回非空指针;但不应当解引用这种指针,而且应将它传递给free以避免内存泄漏。
//返回值:成功时,返回指向新分配内存的指针。为避免内存泄漏,必须用free()或 realloc()解分配返回的指针。失败时,返回空指针

malloc工作在user space(用户态),是以什么样的数据结构组织从内核中申请分配一大块内存?
内核态(资源都是由OS管理的)和用户态交互(调用系统API open、close、read、write )
malloc第一次向内核申请(申请堆内存的系统API)brk()(增加或减小brk指针相当于申请和回退申请的内存)和mmap()(将磁盘上的页面加载到虚拟地址空间上) 申请的单位是页面,申请后返回申请整个内存的起始地址,然后将用户申请的多少字节返回给用户,其他空间由malloc函数库(用户态)来管理,此后用户申请的小内存不需要要向OS申请。
malloc(0)也会申请内存资源
malloc(-10) 拒接不做任何处理

即malloc申请的空间是被malloc函数库管理(一次申请4K),下次malloc时,若是申请的空间很小,则直接从malloc函数库直接获取,而不用进行系统调用,而若是很大,超过4K则需要进行重新系统调用,然后交给函数库。

int *p = (int *)malloc(sizeof(int) * 10);
ifNULL==p)exit(1);//malloc申请之后,一定要记得检测其是否申请成功(多线程安全)!
free(p);//空悬指针

申请空间时,为其分配了40个字节的空间作为数据空间(即用户需要的),还分配了8个字节的越界标记,这个应该和栈的carray技术一样,检查越界的,所以发生越界错误时,是在free时才会检测出来错误,free也只是在调用free时才发现才去即时制止。同时还会分配头部空间的字节,来记录malloc分配的空间大小20字节 40+8+20=40+28,即系统还会多提供32字节的内存(头部空间大小不,一般int 20 char 24)

在这里插入图片描述
ps:因为申请空间时还会提供头部信息空间和越界标记空间,所以在反复申请小空间时,其申请的堆空间的使用比例较小,且容易内存的碎片问题。

  1. free 用来释放从 malloc , realloc, calloc成功获取到的动态内存分配的空间,会检测越界标记
void free( void* ptr );//返回无类型
//若ptr为空指针,则函数不进行操作,即不会出错,应该是有判断。
//若在 free()返回后通过指针t ptr访问内存,则行为未定义(除非另一个分配函数恰好返回等于ptr的值)。

ps:
1、 free只是将指针指向的堆区回收(通过标记位进行判断其是否被分配来回收)
2 、然后通过查看malloc生成的头部信息来查看分配的空间大小进行空间的回收
3 、再通过越界标记来检测是否发生数据填写越界。
4、 free释放后的指针变成了空悬指针,其仍指向释放的堆区,空悬指针需要变成空指针,指向NULL,因为堆区没有进行保护,是共享的(线程),若空悬指针没有变为空指针,则可能发生可怕的后果,其指针还能操作(修改、读取和释放)被释放的空间。

  1. calloc分配并使用零初始化连续内存空间
void *calloc(size_ num,size_t size);
if(P==NULL)exit(1;//还需要检测
//为num个对象(元素)的数组分配内存,并初始化所有分配存储中的字节为零。
//若分配成功,会返回指向分配内存块最低位(首位)字节的指针,它为任何类型适当地对齐。若size为零,则行为是实现定义的(可返回空指针,I或返回不可用于访问存储的非空指针)。

void *My_calloc(size_ num,size_t size)//与其等价
{
	void *p=malloc(num*size);
	//if(P==NULL)exit(1);
	if(P!=NULL{
	memset(p,0,num*size);
	}
	
	 return p;
}

calloc是线程安全的:它表现得如同只访问通过其参数可见的内存区域,而非任何静态存储。
令free或realloc解分配一块内存区域的先前调用,同步于令 calloc分配相同或部分相同的内存区域的识用。这种同步出现于任何解分配函数所做的内存访问之后,和任何calloc所做的内存访问之前。所有操作每块特定内存区域的分配及解分配函数拥有单独全序。

  1. realloc 扩充之前分配的内存块(重新分配内存)
void *realloc(void *ptr,size_t new_size);
//1.如其后还有足够的堆空间就直接移动越界标记和改写头部信息直接填充
//2.当其后没有足够的堆空间,需要重新申请足够大的堆内存,并进行拷贝先前的数据(只能用于内置类型),然后释放先前的空间。
//3.也可以进行堆空间的收缩(移动fd,改变头部信息),但极其容易发生内存碎片问题(外碎片)

void *My_realloc(void *ptr,size_t new_size)//与其等价伪代码
{
	void *newdata=malloc(size);
	memcpy(newdata,p,size);//p的空间大小未知,可能发生越界,不安全
	free(p);//原先的空间已经被释放了
	return p;
}

ps:realloc重新分配给定的内存区域。它必须是之前为malloc)、 calloc()或realloc()所分配,并且仍未被free或realloc的调用所释放。否则。结果未定义。

当使用realloc来扩容时注意不能使用其为循环队列扩容,不然会破坏了循环队列的结构,因为它是靠头尾指针来指向的,,而扩容是从队列的首地址扩容的,而头尾不一定在首地址。

//realloc重新分配时需注意
int *ip=(int *)malloc(sizeof(int)* 10000);
if(NULL==ip) exit(EXIT_FAILURE);
//ip=(int *)realloc(ip,sizeof(int *)*1000);错误写法,当分配失败时,返回NULL。会发生内存泄露。

int *p=(int *)realloc(ip,sizeof(int *)*1000);//需要指针p
if(p==NULL)//扩充失败
{
	free(ip);
	exit(EXIT_FAILURE);
}
else
{
	//free(ip);错误写法,一旦分配成功,ip已经被释放
	ip=p;
	
}

栈与堆的区别

栈区︰我们知道栈区在函数被调时分配,用于存放函数的参数值,局部变量等值。在windows 中栈的默认大小是1M,在VS 中可以设置栈区的大小。在Liunx中栈的默认大小是10M,在gcc编译时可以设置栈区的大小。

堆区:程序运行时可以在堆区动态地请求一定大小的内存,并在用完之后归还给堆区(当有多个程序时,堆区可以共享,你用我也可以用,当一个程序用完后归还,另一个即可以申请,即充分利用有限的资源)。在Liunx系统中堆区的大小接近3G

1、管理方式:栈由系统自动管理;堆由程序员控制,使用方便,但易产生内存泄露。
2、生长方向:栈向低地址扩展(即”向下生长”),是连续的内存区域(连续分配内存,如int a=4,int b=4;两个空间在内存里是连续的);堆一般向高地址扩展(即”向上生长”),是不连续的(申请的两个空间是不连续的)。
内存区域。这是由于堆区管理系统用链表来存储空闲内存地址,自然不连续,而链表从低地址向高地址遍历。
3、空间大小:栈顶地址(esp)和栈的最大容量由系统预先规定(通常默认1M或10M);堆的大小则受限于计算机系统中有效的虚拟内存,32位Linux系统中堆内存可达2.9G空间。
4、存储内容:栈在函数调用时,首先压入是函数实参,然后主调函数中下条指令(函数调用语句的下条可执行语句)的PC地址压入,最后是被调函数的局部变量。本次调用结束后,局部变量先出栈,指令地址出栈,最后栈平衡,程序由该点继续运行下条可执行语句。堆通常在头部用一个字节存放其大小,堆用于存储生存期与函数调用无关的数据,具体内容由程序员安排。
5、分配方式:栈可静态分配或动态分配。静态分配由编译器完成,如局部变量的分配。动态分配由alloca 函数在栈上申请空间,用完后自动释放不需要调动free函数。堆只能动态分配且手工释放。
6、分配效率:栈由计算机底层提供支持:分配专门的寄存器存放栈地址,压栈出栈由专门的指令执行,因此效率较高。堆由函数库提供,机制复杂,效率比栈低得多。
7、分配后系统响应:只要栈剩余空间大于所申请空间,系统将为程序提供内存,否则报告异常提示栈溢出。操作系统为堆维护一个记录空闲内存地址的链表(空闲链表)。当系统收到程序的内存分配申请时,会遍历该链表寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点空间分配给程序。若无足够大小的空间(可能由于内存碎片太多),有可能调用系统功能去增加程序数据段的内存空间,以便有机会分到足够大小的内存,然后进行返回。大多数系统会在该内存空间首地址处记录本次分配的内存大小,供后续的释放函数(如free/delete)正确释放本内存空间。
此外,由于找到的堆结点大小不一定正好等于申请的大小,系统会自动将多余的部分重新放入空闲链表中(由会造成内存碎片)。
8、碎片问题:栈不会存在碎片问题,因为栈是先进后出的队列,内存块弹出栈之前,在其上面的后进的栈内容已弹出。而频繁申请释放操作会造成堆内存空间的不连续,从而造成大量碎片,使程序效率降低。

可见,堆容易造成内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和内核态切换,内存申请的代价更为昂贵。所以栈在程序中应用最广泛,函数调用也利用栈来完成,调用过程中的参数、返回地址、栈基指针和局部变量等都采用栈的方式存放。所以,建议尽量使用栈,仅在分配大量或大块内存空间时使用堆。

最后使用栈和堆时应避免越界发生,否则可能程序崩溃或破坏程序堆、栈结构,产生意想不到的后果。

堆区在进程结束时,即会回收其资源,但是对于服务器而已,就存在内存泄露的威胁,因为服务器是长期不关机的,当长时间发生内存泄露时,当内存被占满,就会发生服务器宕机重启!!!

指针 void(泛型编程、抽象)

常见的指针变量定义
int *p   p为指向int类型的指针
int **p  p为二级指针,可以指向int *类型的指针  p+1代表的是+4 (一个指针的偏移)
int*p[n] p为一个拥有n个元素的数组,该数组的元素类型为int *   
int (*p)[n] p是指向一个数组的指针,该数组有n个元素,元素类型为int   p+1代表的是n个类型元素的偏移,一个数组的偏移
int *p();  p是一个函数,其返回值为int *类型
int (*p)() p是指向返回值为int类型函数的指针 函数指针
int (*p[n])() p是一个拥有n个元素的数组,且该数组元素的类型为函数指针,该函数的返回类型为int,参数类似为void
int (*(*p)[n])() p是一个指针,其指向一个函数指针数组  *p为一个数组,p指向该数组,且该数组元素类型为一个函数指针
int **p)[10]*p)[10] 是一个数组,(*p)是拥有n个元素的数组,其元素类型为int *,p指向该数组
void *p p是一个泛型指针,可以接受其他指针的赋值,但不能解引用,可以进行强转
int ar[10];
int (*p)[10]=&ar;//指向一个数组  &ar  int(*)[10]  ar int *
int (*p)[10]=ar; //error p是指向一个数组,而不是指向一个数组的首元素,虽然ar与&ar指向的位置相同,但其+1偏移不同

函数指针(函数有类型)

在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针
函数指针的定义方式为:

函数返回值类型(*指针变量名)(函数参数列表);  其类型由返回值类型和参数类型决定

“函数返回值类型”表示该指针变量可以指向具有什么返回值类型的函数;“函数参数列表”表示该指针变量可以指向具有什么参数列表的函数。这个参数列表中只需要写函数的参数类型即可。

函数指针的特点:
1)可以计算sizeof(pf);不能计算sizeof(*pf); 函数的大小是随意的
2)函数指针变量没有++和一运算,也不可以和整型数加减。

extern int Mul_Int(int a,int b);
extern int Add_Int(int a,int b);

int main()
{
	int x=10,y=20;
	//int (*pfun)(int,int)=NULL;
	//pfun=Add_Int;
	typedef int (*pfun)(int,int);
	pfun p=Add_Int;  
	//pfun p=&Add_Int;  //都是合法的,编译器处理时,当Add_Int没有括号,即认为是取其地址
	
	int z=(*p)(x,y);//明示指针函数调用  编译器会处理* 与 &  可以进行简写  针对的是函数名
	//z=p(x,y);//都是合法的,但下面这种写法有点混淆,不能明示出其是一个函数指针调用
	
	fun q=Mul_Int;
	int w=(*q)(x,y);
}

ps:当编译时,当使用函数调用时,即确定了此时函数调用对应到函数定义的地址

函数指针是指向函数的指针变量。因此"函数指针"本身首先应是指针变量,只不过该指针变量指向函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。如前所述,C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。

函数指针有两个用途:调用函数和做函数的参数。

1.转移表(转移表就是一个函数指针数组 void(*p[n])() switch 虚表 JVM

即可用来实现“菜单驱动系统”。系统提示用户从菜单中选择一个选项,每个选项由不同的函数提供服务。【若每个选项包含许多操作,用switch操作,会使程序变得很长,可读性差。这时可用转移表的方式】 且switch只能用于整形,而转移表适用范围更广

extern int Mul_Int(int a,int b);
extern int Add_Int(int a,int b);
extern int Div_Int(int a,int b);
extern int Sub_Int(int a,int b);

int main()
{
	int (*pfun[5])(int,int)={NULL,Mul_Int,Add_Int,Div_Int,Sub_Int};//但只能存放该函数指针类型的函数
	
	cin>>i;
	cin>>x>>y;
	int sum=pfun[i](x,y); //相当于int sum=(*pfun[i])(x,y);  pfun[1]-》Mul_Int
}

可以使用结构体使得其包含多个函数指针,已达到包含不同类型的函数

struct TableFun
{
	void (*fun)();  //函数指针
	int (*ipn)(int,int);
	void*pu)(int);
}//结构体,不同数据的集合!!!  这也是虚函数表的一种形式,数组可以放同一类的函数指针,结构体可以放不同类型的函数指针

TableFun a;  sizeof(a)=12  

ps:数组和结构体都是一种数据结构,其是数据的集合,数组可以存放相同类型的数据,而结构体可以存放不同类型的数据!!!且结构体进行重载下标,也可以进行像数组一样的操作

switch也会形成表驱动

int opt;
cin>>opt;
switch(opt)   //输入的值*偏移量+入口地址
{
	case 0:cout<<"0 \n";break;
	case 1:cout<<"1 \n";break;
	case 2:cout<<"2 \n";break;
	case 3:cout<<"3 \n";break;
	case 4:cout<<"4 \n";break;
	case 5:cout<<"5 \n";break;
}

ps:编译时,当其结构连续时,编译器会为switch准备一个跳表,即数组,该数组元素其实是一个函数,即函数指针数组,通过选择不同的下标来完成选择调用不同的函数,且需要break作为switch的终止
在这里插入图片描述

首先将opt的值放入eax,再将eax的值放入一个(内存)里,ecx*4+8278C0h 此时输入的是5
8278C0h就相当于一个数组,其数组存放着跳转的地址,其实相当于存放了一个函数的入口

当输入的数不连续,一般间隔大于5,就不会产生表驱动,就会被转换为if,每次都需要比较
在这里插入图片描述

2 回调函数(实现泛型)

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数**。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。** 如信号,声明时将signal的处理函数的参数传入了信号处理函数,当信号(事件)时,系统内核调用该信号处理函数。

void * Print_Init(void * input)
{
	int * put = (int *)input;
	cout << *put;
	return  put++;
}

void * Print_char(void * input)
{
	char * put = (char *)input;
	cout << *put;
	return  put++;
}

void * Print_double(void * input)
{
	double * put = (double *)input;
	cout << *put;
	return  put++;
}

void  Print(void * input,int n,void *((*fun))(void *))//该函数相当于接口,其他实现隐藏
{
	
	int i = 0;
	while (i < n);
	{
		input = fun(input);

	}
	cout << endl;
}
int main()
{
	const int n = 10;
	char br[n] = { 1,2,3,4,5,6,7,8,9,10 };
	double cr[n] = { 1,2,3,4,5,6,7,8,9,10 };
	
	Print(br, n, Print_Init);//后面改进,输入int 类型即输出int类型,输入char即输出char
	Print(cr, n, Print_double);

编译器对于局部函数指针的声明
void *(pfun)(void *)  //error,在局部声明函数指针时不能省略*,否则系统会将其看成是调用的函数
void *(*pfun)(void *)  //正确,声明一个函数指针pfun

但当函数指针作为函数参数时
void  Print(void * input,int n,void *(fun)(void *))  //正确,可以省略*,因为在函数形参不可能调用函数
void  Print(void * input,int n,void *((*fun))(void *)) //正确,但是一般最好这样写

编译不通过

在这里插入图片描述
将func(void (*p)())提到前面的括号里,编译成功,此时仍然是该函数返回类型是一个函数指针,其参数也是一个函数指针
在这里插入图片描述
分析时,先分析最里面的括号。

尽量使用typedef,使得程序易于理解
在这里插入图片描述

(*(void (*)() )0))();  //将0强转为函数指针,然后调动0地址的程序,即0的函数

上电时调动COMS 内嵌的代码,固件,然后通过该COMS程序引导磁盘调动其第一个扇区存放的boots程序,引导加载操作系统。CPU也是一个固件,其内嵌了指令集。

C语言中的算法

1. 快排

在这里插入图片描述

qsort函数对 compare_string 即对回调函数的两个形参进行了排序,当传入字符串时,实则传入的是二级指针被泛型指针接收,若强转时只进行了一级强转,则此时获得的是二级指针指向的一级指针的地址比较!!!
int compare_ints(const void * x,const void * y)//指向两个位置 一个指向头,一个指向尾
{
	int arg1 = *(const int *)x;//传入的是当前数组的值,而没有发生拷贝,是地址传递
	int arg2 = *(const int *)y;
	if (arg1 < arg2) return -1;
	if (arg1 > arg2) return 1;
	return 0;//返回看是否需要调换  //当两个负值进行相减时需要注意。,两个正值相加时需要注意
}
int compare_string(const void * x, const void * y)//此时接收的是指向两个字符串的指针,两个字符串想改变存储位置,需要二级指针
{
	const char * arg1 = *(const char **)x;
	//与 char * arg1 =(char *)x 是不同的 前者是指向字符串的指针的指针通过地址传递,传入
	//后者是通过值传递将指向字符串的指针拷贝进来。
	 char * arg2 = *( char **)y;
	return (strcmp(arg1, arg2));

	//return 0;//返回看是否需要调换  //当两个负值进行相减时需要注意。,两个正值相加时需要注意
}
int main()
{

	int s[]{ 34,12,67,889,23,45,100,78,90 };
	const char * s1[]{ "asds","weaf","bsds","csd","zsd","hdfd" };
	int size = sizeof(s) / sizeof(*s);
	int size1 = sizeof(s1) / sizeof(*s1);

	qsort(s, size, sizeof(int), compare_ints);//还将第三个参数调动的函数的参数进行了通过地址传递进行了交换
	qsort(s1, size1, sizeof(char*), compare_string);//该qsort函数对 compare_string的两个形参进行了排序

	for (int x:s)
	{
		cout << x<<" ";
	}
	for (int i = 0; i < size1; i++)
	{
		cout << s1[i] << " ";
	}
	
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值