学习笔记总结篇(一)

文章目录

在这里插入图片描述

0.前言

这里是一篇零零散散的笔记总结而成,可能有一些问题是笔者没有注意到点,还要文章里面的代码全部没有区别中文跟英文,有可能地方有bug没有修好的地方。

一.c语言的概述

1.c语言是一门通用的程序设计语言,它具有以下6种特点:

(1.)高效性:因为它继承了低级语言(如机器语言等等),代码的运行效率高,具有良好的可读性与编写性。

(2.)灵活性:c语言的语法不拘一格,可以在原有的基础上自行定义,给了程序员更多的创作性与想象力。

(3.)功能丰富:除了基础语法外,c语言还具有很多的操作符与自定义类型来表达复杂的数据类型。

(4.)表达能力强:c语言的语法形式与人们的自然语言相似,书写自由,结构规范,可以通过简单的控制语句就可以完成复杂的控制程序流程。

(5.)移植性好:在不同的操作系统下,只需要改改甚至不需要修改就可以进行跨平台开发。

 (6.)   学习了c语言也可以养成属于自己的编程思维,以后无论学什么语言都事半功倍。

以上都是c语言的介绍。

二.数据的类型

1.什么叫做关键字💕

 关键字(Keywords),又被叫保留字,是c语言中的规定具有特殊意义的字符串。c语言一共有32个关键字。(分别为:
**auto  double  int  struct  break  else  long  switch
case   enum   register   typedef   char   extern  return  union
const  float  short  unsigned  continue  for  signed  void
default  goto  sizeof  volatile  do  if  while  static)**

2.什么是标识符🤍

就是为了在程序运行中可以使用变量,常量,函数,数组,指针等等,那就要给他们设计一个名字,这个名字被我们称为标识符。标识符不可以是关键字,其他都可以,只要不违规就行了。

3.数据类型概述💜

(1.)基本的数据类型:整形(int),字符型(char),浮点型(float),枚举类型(enum)

(2.)构造类型:当基本类型不满足使用时需要时,设计者可以进行构造所需要的数据类型,其中包括有数组类型(这个后面会列举),结构体类型(struct),联合体(union)。

4.常量与变量🧡

 1.常量就是在程序运行中不可以改变的量,包括有3类:数值型常量(包括整形常量和实型常量),字符型常量和符号常量。

5.整形的常量与变量💚

1.整形:就是整形整形常数,如9878,-564等等这些数据,它们分别有2进  制,10进制,16进制。

2.整形可以是基本整形(int),长整型(long int),短整型(short int)

也可以是有符号整形(signed)或者无符号整形(unsigned)。

整形定义:singned int(有符号整形),unsingned int(无符号整形),短整形(short int)

长整型(lomg int)等等。如int a=10;

6.浮点型常量与变量🧡

浮点型由小数与整形组合而成的,表示的方法也有2种:小数与指数的形式。

1.小数的形式:小数的表示方法就是用十进制来描述,如1.23,3.14等等

2.指数的形式(科学计数法):当浮点数特别大的时候,可以使用指数的型式来表示。使用字母e或者E来表示,如:45e2(也就是45*10的二次方),45e-2(也就是45*10的负二次方)等等这些。

在书写浮点数的时候可以在后面加上f(表示float类型)或者加上l(表示long double)的类型,如4.15f,15.23l等等。

浮点数定义:单精度类型(float),双精度类型(double)等等,如float a=3.14;

7.字符型常量与变量❤

字符型与其他的类型不一样,需要使用定界符来进行定义,如char i=‘a’;等等

1.字符:只是使用一对‘’来进行定界的就叫字符,如‘a’,‘b’等等字符。字符是区分大小写的,大写与小写的意思是不一样的。‘’代表的是定界符,不属于字符的一部分。

2.字符串:是使用一对“”括起来的诺干字符的序列,它与字符的区别是:

   (1.)定界符的不一致。
   (2.)长度不同。
   (3.)存储的方式不一样。

字符的定义只有一个关键字:char a=‘j’; char c=“abcde”;

8.转义字符💙

1.转义字符在字符常量中是一种特殊的字符,通常以反斜杠“\”开头,后面跟一个字符或多个字符。如:\n(换行) \t(制表) \v(竖向跳格) \b(退格) \r(回车) \f(走纸换页)等等。

在这里插入图片描述

9.存储类型💝

内存一般分为栈区,堆区,静态区。

 1.auto变量:用于定义一个局部变量为自动变量,存储在内存的动态存储区,这意味着每次程序执行到该地址就会产生一个新的变量,并重新初始化。如:auto int c=10;

2.static变量:表示为静态变量,存储在内存的静态区域中。相对于其他变量便于修改,static变量始终会保持它的值。如:static int c=0;,它的值会一直保留在内存中,除非进行运算或者关闭电脑。

3.register变量:又被称为寄存器存储类变量,通过它,可以请求编译器把需要频繁访问的局部变量放到寄存器里面去,而不是内存,这样的好处就是提高程序的运行速度。

要想有效的利用寄存器关键字register关键字,就必须了解处理器的内部结构,知道可以用于存放变量寄存器的数量,种类,工作方式等等。如register int c=100;

4.extern变量:称为外部声明,用于不在当前源文件的声明但是需要使用到的外部数据时。

习题:1.输出地图常用地点。(要求输出家庭住址与公司住址。)

#include <stdio.h>


//设置百度地图常用地点
int main()     //建立主函数,一个c语言不能没有主函数
{
	printf("=======请设置常用地点==========\n");//由于没有要求需要其他的东西,所以我们可以直接使用printf进行输出即可,
	printf("我的家:xxxxxxxx\n");
	printf("我的公司:pppppppp\n");
	return 0;
}
       2计算5的幂(要求使用static函数来进行输出)。
//利用静态函数static进行计算5的幂
#include <stdio.h>
int ci()   //因为需要调用函数
{
	static int c = 5;   //因为是计算5的幂,需要定义静态变量5;
	c *= c;    //这里计算幂3
	printf("%d\n", c);
	return 0;
}   

int main()    //老规矩建立主函数
{
	ci();   //调用3次看看结果对不对
	ci();
	ci();
	return 0;
}   //这题主要就是理解静态函数的用法。

       4.引用外部数据进行使用
//调用外部数据
//文件1
#include <stdio.h>
int main()
{
	int b = 150;
	extern int c;      //声明c是外部数据,需要使用extern进行修饰才可以使用数据
	int sum = b + c;
	printf("%d ", sum);   //打印结果看看行不行
	return 0;
}
//文件2
#include <stdio.h>
int c = 100;  //这是全局变量
       5.输出字符串i love you。
#include <stdio.h>
int main()
{
	char p[20] = { "i love you" };   //字符串需要使用““进行修饰,字符是使用‘’进行修饰的
	printf("%s\n", p);   //打印字符使用%c进行打印,打印字符串则使用%s。
	char a = 'x';     
	printf("%c", a);     //打印字符。
	return 0;
}

三.常用的输入与输出函数

1.字符数据的输入与输出

   1.字符输出函数:输出字符常用的是putchar,作用是输出一个字符到显示设备上。格式:使用前需要添加头文件<stdio.h>,putchar(‘a’);

   2.字符输入函数:字符数据使用的输入函数是getchar,作用是从键盘上获取(输入)一个字符。格式:要添加头文件<stdio.h>,char c=getchar(‘a’);

2.字符串的输入与输出

   1.字符串输出函数:字符串输出的函数为puts函数,作用是输出一个字符串到显示设备上。格式:使用前要添加头文件<stdio.h>,puts(“i love me”);

  2.字符串输入函数:字符串输入使用的是gets函数,作用是读取一个字符串(自己输入的)保存在变量中。格式:使用前要添加头文件<stdio.h>,char c=0;gets(c);

3.格式输出函数

 1.格式输出函数:格式输出函数为printf函数,作用是向设备输出诺干任意类型的数据。格式:使用前要添加头文件<stdio.h>,printf(格式控制(%d,%s,%c等等)输出列表)。

  2.格式输入函数:格式输入函数scanf函数,作用是按照指定的格式接受键盘上输入的数据,但是\n不会给显示出来,还是会留在缓冲区里面,多次使用scanf的时候需要使用一个getchar来接收这个\n,**在版本高的vs开发环境里面,scanf会报错,需要使用scanf_s或者在前面加上#define _CRT_SECURE_NO_WARNINGS 1这个数据。**格式:使用前要添加头文件<stdio.h>,scanf(格式控制,地址列表(也就是你需要使用那个类型来接收))。

四.操作符

1.表达式

1.表达式由操作符(+.-.*.\等等)和操作数(可以是字符,数字等等)组成。

2.运算符与表达式

   1.赋值表达式:在声明变量时可以为其赋一个初始值,就是将一个常数或者表达式的结果赋予一个变量,这就是赋值,而赋值所使用的符号就是=号。如:int a=10;int c=a+10;char a=‘c’;等等。

3.强制类型转换概念

1.在自动类型转换可以知道,如果数据类型不一致,系统就会根据情况进行转换,但是会产生警告,如果我们使用一下强制类型转换就不会发生警告的问题。如:float  c=3.14f;int a=(int)c;
  
  2.什么叫自动类型转换
  
   1.数据通常有很多种类型组成,但是它们的长度与特性各不相同;例如,把较短的数据变量赋给较长的数据类型时,较短的数据会自动升级成较长的数据类型;产生的数据不会丢失;但是把较长的值赋给较短的数据类型时,将会发生数据丢失,也就是会产生截断。如,把char的值赋给int,数据不会丢失,但是,反过来就会发生数据截断,产生数据丢失的后果,

4.运算符

  1.算术运算符:+ - * \  %(取模(出来的结果是余数))其中,+ -(这里的代表的是正负)被称为单目运算符,而+ - (这里的是加减)* \ %被称为双目运算符。

2.自增与自减

1.c语言中有两个特殊的运算符。就是++(自增),--(自减),自增就是变量加1,而自减就变量减一,++a被称为前缀自增,a++被称为后缀自增,自剪也是一样。

 2.前缀与后缀的区别:前置则是先自增或者自减在进行使用,后置的是先使用后自加或者自减。但如果只有一个操作数的情况,无论前置还是后置都会进行自增或者自减操作。尽量让它们单独出现,不要与其他操作符混在一起。

3.关系运算符与关系表达式

     1.关系运算符:<   >    =    <=    >=    ==(等于,因为单独的=是代表赋值的意思)  !=(不等于)。

      2.关系表达式:就是利用关系运算符对2个或多个数据进行比较,若为真(1,或者以上),则表示指定关系成立,为假(0),则关系不成立。 例如:int c=10;int a=10,诺c==a,则关系成立。

     3.优先级与结合性:关系运算符是自左向右结合的。使用关系运算符的世界,需要注意它们的结合性。如:if(c=a !=10),则会!=先进行运算,因为不等于的优先级高于=。

 4.逻辑运算符与逻辑表达式和位操作符

      1.逻辑运算符:在c语言中,逻辑运算符一共有3种,分别为逻辑与(&&),逻辑或(||),逻辑非(!)作用为:&&:逻辑与(也是并且的意思)(用于判断2个即2个以上的表达式可以使用)  ||:逻辑或(也是或者的意思)(用于多个表达式时,只要一个为真即可进行下一步)  逻辑非:作用就是按位取反      。单独的&&或者||叫做位操作符。

       2.位操作符:位操作符:3种符号都是按照二进制位进行的     与 &(公式:有0为0,全1为1)  ^或 (公式:有1为1,全0为0)   | 异或 :对应的二进制位进行异或,(公式:相同则位0,不同也就是相异则为1).注意;它们的操作数都必须是整数。
 任何2个相同的整数异或都是0,0异或上任何整数也是本身。

      3.逻辑表达式:使用逻辑操作符对操作数进行比较的时候就叫逻辑表达式。如:if(a<c&&c>b),这两个条件需要同时满足才可以进行if里面的语句,为假就会跳过这个if,
      || 这个就是2个条件只要满足一个就可以进行下面的语句块。
if(a>100 || a==100//为了避免读者直接复制代码导致编译不过我先说明下,有时候笔者写着写着就会忘记改成英文符号使用中文的,在编译器使用中文符号是编译不过的。
{
(//语句块)
}
      4.优先级与结合性:&& ||是双目操作符,需要2个操作数,结合性为左向右,!为单目,则从右向左。它们的优先级从高到底为:! && ||。

5.逗号运算符与表达式

1.逗号运算符:在c语言中,把,称为逗号运算符,作用:可以起到分隔语句的作用。

2.逗号表达式:也就是使用,来隔开表达式,相左到右顺序进行,但是,最后结果是最后一个表达式的结果。如int c=(a+b,c+m,n+q)的结果显示是n+q的值。

6.复合赋值

 1.复合赋值:是c语言中独有的,实际上这属于一种缩写,可以是代码的描述更加简洁。如

c=c+a跟c+=a是一样的。

7.其他操作符:

  1.下标引用,函数调用,结构成员:下标引用操作符:[]使用:一个数组名+一个索引值
 函数调用操作符:(用于函数传参的也就函数调用操作符())接受一个或者多个操作数
  第一个操作数是函数名,剩余的操作数是传给函数的的参数。
结构成员操作符:有2种
结构体变量名.结构体.成员名(传值调用)

结构体地址->结构体指针->成员名(传址调用)

2.*与&(同时都具有双重意思)是一对冤家:&取地址操作符是提取目标的地址,而*       (单独的*叫做解引用操作符,与类型连用则是变成指针。例如int * pa)则是一个指针,可以让指向一个地址,
  只有一个操作数时:&就叫取地址操作符,2个以上是则叫按位与.

8.移位操作符

 1.左移左移操作符与右移操作符:左边丢弃,右边补          0    , 这些是以二进制的方式进行的。
 2.右移操作符:它有2种情况 1.算术右移:右边丢     弃,左边补原符号位(原符号是:正数也是补0 负数补1)

逻辑移动:右边丢弃,左边补0;可以通俗来讲就是都补0,(也可以这么理解左移*2,右移/2)
原符号:正为0,负数为1,只有正整数的情况才是通常采用右移算术移动。

习题:1.加密字符,要求:所有标准输入的数字,输出方式一样,但是字符需要改成与它距离为13的字母。

#include <stdio.h>
int main()
{ 
	char a = getchar();      //获取标准输入字符
	int b = 0;
	int c = 0;
	scanf("%d%d", &b, &c);     //输入数字
	printf("原文为%c %d %d\n",a,b,c);    //未进行加密的原文
	printf("%c %d  %d", a + 13,b,c);    //经过加密后的
	return 0;
}

2.编写一个函数,输出25从左到右交换的值是多少。(25是无符号数)

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
//25交换左边与右边的值
unsigned int reverse(unsigned int z) 
{
	z = z << 27;  //进行移位。
	return z;
}
int main()
{
	unsigned int c = 25;   //定义25;
	//25进制位:0000 0000 0000 0000 0000 0000 0001 1001
	unsigned int o=reverse(c);   //调用函数进行移位
	printf("%u ", o);      //输出结果为3355443200
	return 0;
}

3统计剩余车位数量

//统计剩余车位数量
#include <stdio.h>
int main()
{
	int  a = 70;   //假设车位有70个;每一次有人进来就减少,这里需要用到循环与选择语句。下面会写到
	while (a)
	{
		a--;      //有一辆车进来就是减少车位
		if (a>65)      //利用if语句进行判断;
		{
			printf("车位还有%d\n", a);
		}
	}
	return 0;
}

五.选择结构

1.if选择语句

    1.在 if 语句中首先是进行表达式的判断,然后根据出来的结果(真或假)决定后续的流程,if语句一共有3种形式,分别为:if  if.....else  和else if  3种形式。

           (1.)if语句的形式:if (表达式)语句。其中小括号里面是需要进行判断的条件,语句则是结果为真的时候将要执行的对应操作。else则是结果为假时执行的,一般来说else与最近的if匹配
if(//判断条件,如果为真则执行下面语句)
{
printf("hehe\n");
}
else
{
printf("haha\n");    \\这里是为假的情况
}
          (2)else if:这个语句时对一系列条件相斥的条件进行校验,从而解决复杂的问题。else if的形式为:
if//第一个条件)
{//语句块1)
}
else if//表达式2)
{
(语句块2//这里面是不需要加()我这样是为了方便阅读
}//这里不举这么多了
else
{//语句块3)
}
注意:else if(表达式2)(18<=age<26,这种写法是错误的,,因为它会先判断前面的,诺为真则下面的那个也是真。正确的写法是:age>=18 && age<26)

2.条件运算符:?:

条件运算符属于双目运算符,它可以对一个表达式进行校验,然后根据结果返回另外2个表达式中的一个,真值则返回:前,反之就后面一个。
if(a>b)
{
max=a;
}
else
{
max=b
}//类似这样的语句可以使用条件运算符进行简化
max=(a>b)?a:b;//它们与上面的表达式是等价的;

3.switch语句

 switch语句:常用于多路分支的选择,当然也可以使用if语句来完成多分枝,但是if语句多了也会影响到代码的可读性,所以就有了新的选择,switch语句。一般形式为:
switch//表达式)
{
case 1:
(//语句块);
break//switch是不会自己跳出的,所以需要加上break来进行跳出。
.......//后面的形式都差不多了就不写了。
}

4.if 语句与switch语句的区别

    (1.)语法上:if 需要使用else进行配合,switch也需要使用case进行配合,区别在于,if 先进行判断,而switch则是后进行判断。
 ( 2.)效率上:if 语句适用于少量的校验,判断速度上也更快。但是随着校验量的增大,if 语句的速度会逐渐下降,并且不容易进行扩大与添加。
 switch语句则是除了default(这个是用于除外固定情况外的其他情况因素,可以放在任何位置)的默认情况外,对于每一项case的校验速度都是一样的,default的速度比case要快点。
 (3.)少量校验情况使用if 语句是最好的,通常在3到4个左右条件下,高于5个以上,建议使用switch语句比较好。

习题:1.利用if语句划分学生的成绩

//利用学生成绩的划分
#include <stdio.h>
int main()
{
	double a = 0;       //这样设定是因为学生成绩不可能只有整数
	printf("请输入学生的成绩:\n");
	scanf("%lf", &a);
	if (a>90 && a<100)     //这里判断大于90而小于100的;
	{
		printf("优秀");
	}
	else if (a>=80 && a<=90)  //判断大于80小于90的
	{
		printf("良好");
	}
	else if (a>=60 && a<=80)   //判断大于60小于80的
	{
		printf("合格");
	}
	else                    //60以下的
	{
		printf("不合格");
	}
	return 0;
}
       2.利用switch语句进行抽奖模拟
#include <stdio.h>
int main()
{
	int a = 0;   //这里代表的是抽奖号码;
	printf("请输入你的抽奖号码\n");
	scanf("%d", &a);
	switch (a)
	{
	case 1:
		printf("恭喜你的中了特等奖");   
		break;
	case 2:
		printf("恭喜你中了1等奖");
		break;
	case 3:
		printf("恭喜你中2等奖");
		break;
	case 4:
		printf("恭喜你中3等级");
			break;
	default:
		printf("很遗憾,你没有中奖");
	}
	return 0;
}
       3.今天要喝什么饮料,可乐,咖啡还是牛奶?
//今天喝什么饮料
#include <stdio.h>
int main()
{
	int a = 0;
	printf("请输入你需要喝的饮料代号,1:可乐,2:咖啡,3:牛奶\n");   //这里是进行编号
	scanf("%d", &a);
	if (a==1)                    //利用if进行判断选择,也可以使用switch。
	{
		printf("喝可乐");
	}
	else if (a==2)
	{
		printf("喝咖啡");
	}
	else if (a == 3)
	{
		printf("喝牛奶");
	}
	else
	{
		printf("没有该代码");
	}
	return 0;
}

4.校园网收费标准:校园网是1天一元收费,如果购买超过30天则按照每天0.75元收费,否则就按原价收费。

//判断校园网的收费
#include <stdio.h>
int main()
{
	int i = 1;   //这里是不满30天的收费
	double o = 0.75;  //这里是满30的收费
	int a = 0;   //这里代表是购买的天数;
	printf("请输入你需要购买的天数\n");
	scanf("%d", &a);
	double c = (a < 30) ? i : o;   //这类使用条件运算符会快点。
	printf("%.2lf", c);   //这里.2f是代表保留2位浮点数。
	return 0;
}

请添加图片描述

六.循环控制

1.c语言的循环结构有如下几种:

       1.while循环:使用这个while循环可以解决某个条件满足时反复执行的问题。形式如下:
 但是初学者可能会犯一个错误那就是可能会在while后面部分添加上;,记住:选择语句(if,switch)和循环语句(while,do....while,for);
      使用while循环的时候,有时候可能会犯差一错误(就是循环体少执行了1次,这种错误往往河很难发现的,因为它在编译器中不会报错),就是把例如:a<=20写成a<20;这样就会导致差一错误。
while(表达式)
{//要是为真则执行里面的语句块,一直循环到为假的时候跳出循环)//这是第一个循环结构
}
 2. do....while语句:这个循环体就是不论条件有没有满足都是会执行一次语句在进行判断是否继续执行,如果为真则继续,为假则跳出。
与while循环想比,do....while是无论如何都会执行一次,而while是需要满足条件才可以进行。
do
{//语句块)//这里面是不需要加()我这样是为了方便阅读
}while(这里放的是表达式,为真则返回继续执行,为假则跳出);//这里是需要加;的
 3.for循环:for循环可以只控制一个循环,并且每次进行循环时修改循环变量。在3种循环语句,for循环时最灵活的,也是最常用的。
for//表达式1(初始化条件(也就是初值)):表达式2(判断条件);表达式3(进行修改循环变量);)//这是往复循环,直到表达式2为假为止,一般来说表达式1只执行一次,剩下都是2与3在往复循环。
{
(语句块)//这里面是不需要加()我这样是为了方便阅读
}
//为了避免我上面的文字太多
for(int i=0//赋予初始变量);i<=10(//进行判断);i++(//改变初始变量,如何返回表达式2进行判断))
{
printf("haha");//按照这样循环,编译器上就会执行11次;
}
//第二种赋值方式,上面的例子是最新的标准,为了避免有些读者使用的是老的编译器
int i=0;//这里就是在外面先进行定义,然后在放入循环中
for(i=0;i<20;i++//
{
printf("haha");//这里就会打印20次;
}

for循环的变体:一共有如下3情况:
   1.for语句中省略表达式1:
      在上面我们知道表达式1代表的是对循环体设置初始值,如果对它进行省略会发生什么呢?

在这里插入图片描述
在这里插入图片描述

//求n!的值(省略表达式1的情况)
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
	int n=0, i = 1, ret = 1;   //这里定义了变量。
	printf("输入n的值\n");//这里我输入的是5;
	scanf("%d", &n);
	for (; i <= n; i++)   //可以知道这样写也是正确,编译器并不会报错
	{
		ret *= i;
	}
	printf("%d", ret);
	return 0;
}
由此可见省略表达式1也是可行,但是不可以省略表达式1的;由程序运行可以知道,在定义i的时候已经直接给定了初始值,这样在使用for循环的时候就可以省略表达式1了。
       2.for语句中省略表达式2:表达式2代表是判断条件,如果省略就会导致代码值恒为真,一直循环下去,例如

在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
	int n = 0, i = 1, ret = 1;
	for (i=1; ; i++)   //可以知道这样写也是正确,编译器并不会报错,但还是尽量不要这么写。
	{
		printf("haha\n");
	}
	return 0;
}

       3.for语句中省略表达式3:它是用于改变循环体变量的,可以省略,但是没必要,因为你省略还需要加上类似的条件,不然代码也会一直进行下去。

在这里插入图片描述
这是加了条件的情况·
在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
	int n = 0, i = 1, ret = 1;
	for (i = 1; i<=10;)   //可以知道这样写也是正确,编译器并不会报错
	{
		printf("haha\n");  //没加条件的情况,会陷入死循环
	}
	return 0;
}
//加了条件的情况
int main()
{
	int n = 0, i = 1, ret = 1;
	for (i = 1; i<=10;)   //可以知道这样写也是正确,编译器并不会报错
	{
		i += i;   //没什么必要省略表达式3.
		printf("haha\n");
	}
	return 0;
}

4.循环嵌套模式:
   3种结构的互相嵌套。如下面的嵌套是可以进行的
while//表达式)  //第一层
{
//语句块
    while       //第二层
   {
     //语句块
   }
}

do…while的循环嵌套:

do    //第一层
{
//语句块
    do    //第二层
    {
       //语句块
    }while//条件)
}while(//条件)

for循环的嵌套:

for(表达式1;表达式2;表达式3//第一层
{
//语句块
    for123//第二层
    {
    //语句块
    }
}

以上循环体都是可以进行互相嵌套的,下面我写2道例题来加深印象
1.百钱卖百鸡的问题
在这里插入图片描述

//百钱买百鸡
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
	int c = 0, o = 0, b = 0;  //这里代表的是大鸡(5),小鸡(4)与鸡苗(4)。
	for ( c = 0; c < 20; c++)    //这里代表了大鸡可以买20次;
	{
		for (o=0;o<25;o++)       //这里代表了小鸡可以买25次
		{
			b = 100 - c - o;    //这里是指鸡苗的数量,3种满足才可以100鸡
			if (5*c+4*o+b/4.0==100)   //这类判断3鸡一共花100钱
			{
				printf("大鸡=%d,母鸡=%d,鸡苗=%d\n", c,o,b);   //为真输出结果
			}
		}
	}
	return  0;
}

2.九九口决表
在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
//99口诀表
int main()
{
	int x = 1, c = 1;    //这里是指x为行,c为列。
	for ( x = 1; x <=9; x++)    //这是行循环
	{
		for (c = 1; c <= x;c++)   //这里是列循环
		{
			printf("%2d*%2d =%2d", x, c, x * c);   //打印结果
		}
		printf("\n");     //进行换行
	}
	return  0;
}

2.转移语句

   5.goto语句:
        它是无条件转移语句,可以使程序立即跳转到想要的位置,它后面带有有一个标识符,该标识符是同一个函数内的某条语句的标号。由于不经常使用,这里就不解释了,有兴趣的读者可以去百度看看。
int	b=0;
int c=0;
for(b=0;b<=100;b++)
{
for(c=0;c<=100;c++)
{
printf("haha\n");
if(c==50)
{
goto nex;  //利用goto可以直接跳出到标记点上。
}
}
nex:   //这是标志点
printf("退出");
return 0;
}
   6.break语句:有时候会遇到不管检验条件如何都会进行运行的语句,这个时候就需要强制退出语句了。它用于终止并跳出循环。
break;//这是它的一般形式。使用如下
//例如:
while1{
printf("haha\n");  //如果没有break的话,这个循环就会一直跑下去,现有了break,执行完一次就会强制跳出了
break;
//注意一点:循环体的break与switch里面是不一样的。在循环体用于终止循环并跳出,switch里面的则是执行后跳出。
}
 7.continue语句:这个用于程序需要返回循环体重新执行的时候而不是跳出循环的情况就要用到它了。一般形式为
continue;//作用就是结束本次循环,回到循环头前继续执行循环。写法与上面一样,都在循环体后面加上。

七.数组

1.一维数组

定义:数组实际上就是一组相同类似的元素的集合。其定义语法如下
//一维数组由类型说明符 数组名 [常量表达式(下标)]组成 如
int iarr[10]

一维数组的引用:

 数组在进行定义后,可以通过其中的数组元素进行引用,方式则是:数组名[下标]
//例如
int	arr[10]={1,2,3,4,5,6,7,8,9,10};
//则可以通过这样来引用其中的一个值:arr[8],则对应的数据就是9;

一维数组的初始化:

(1.)定义的同时进行初始化
int arr[5]={1,2,3,4,5};
(2.)如果只是给定一部分的值,则后面未赋值的默认为0;
int arr[10]={1,2,3,4,5,6};
 (3.)当给全部数组元素进行赋值时,可以不指定它的长度
int arr[]={1,2,3,4,5,6};

一维数组的应用:

     1.给5个同学排号,但是后面又来一个学生插入。代码如下
#include <stdio.h>

int main()
{
	//定义数组
	int arr[5] = { 1,2,3,4,5 };
	int arr1[6] = { 1,2,3,4,5,6 };
	int i = 0;//这里是插入的排序,也有更好的方法,这个是比较简单的
	int j = 0;
	for ( i = 0; i < 5; i++)   //利用循环进行遍历数组输出
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	printf("插入后为\n");
	for ( j = 3; j < 6; j++)
	{
		printf("%d ", arr1[j]);
	}
	return 0;
}

2.二维数组

定义:二维数组是一个特殊的一维数组,但是各元素任然是一个数组。其定义形式如下:
//与一维数组差不多:数据类型 数组名[常量表达式1][常量表达式2]
int arr[10][5]  //代表里面有10行5列的元素,但是这不是唯一,也可以是10列5行,具体要看编译器怎么定义。

初始化:

 (1.)将所有数据写在一起,按照数组元素排列顺序进行赋值
int arr[2][2]={1,2,3,4};
(2.)为所有元素赋初值时,可以省略行标,但是不可以省略列标
int arr[][3]={1,2,3,4,5,6};
(3.) 分行给数组分值。
int	arr[2][3]={1,2,3},{4,5,6};
(4.) 直接赋值(一般不这么使用,)
int arr[2][3];
arr[0][0]=1;
arr[0][1]=2;......

二维数组的应用:
行列替换(在vs运行不出来行列替换的效果)

int main()
{
	int arr[2][3] = { 12,23,45,69,55,78 };   //建立2个二维数组,用于存储初始数组与替换后的数据。
	int arr1[3][2] = { 0 };
	int  i = 0, j = 0;    //定义行列
	for ( i = 0; i < 2; i++)
	{
		for (j = 0; j < 3; j++)
		{
			printf("置换前:%d\t",arr[i][j]);   //显示二维数组里面的元素
		}
		printf("\n");
	}
	for ( i = 0; i < 2; i++)      
	{
		for (j = 0; j < 3; j++)
		{
			arr1[j][i] = arr[i][j];
			printf("置换后%d\t", arr1[j][i]);
		}
		printf("\n");
	}
	return 0;
}

3.字符数组

定义与引用:

     定义:字符数组的定义如:
char  数组标识符[常量表达式]
char arr[5];
    引用:字符数组也可以采取下标引用的方式
arr[0]='H';
arr[1]='L'
......//等等。

字符数组的初始化:

(1.)逐个赋值
char arr[5]={'h','e','l','l','o'};
(2.)定义的同时进行初始化,此时可以省略数组长度
char arr[]={'h','e','l','l','o'};
 (3.)利用字符串给字符数组赋值;
char arr[5]={"hello"};//或者
char arr[5]="hello";

字符数组的结束标志:

    在c语言中,使用字符数组保存字符串时,系统会自动补上字符串的结束标志 \0 作为结束标志,
    例如上面方法4的字符数组,它在内存实际上是这样存储的
   在内存中:    h e l l o \0 

\0是系统自己添加上去的.因此等价于:

char arr[]={'h','e','l','l','o','\0'};//一般来说,系统都会自己添加上\0,但是前提就是你声明了字符数组的长度。

字符数组的输入与输出:

字符数组输入与输出可以使用2种格式字符:%s(字符串输入)与%c(字符输入);

字符数组应用:
计算字符数组的长度

int main()
{
	char arr[] = "Hello wold";   //定义字符数组
	int i = 0;
	int s = 0;
	for ( i = 0; arr[i]!='\0'; i++)   //利用循环进行遍历输出,利用一个变量记录数据,
	{
		s++;
	}
	printf("%d", s);   //最后输出结果。
	return 0;
}

4.多维数组

 多维数组的声明与二维数组相同,只是下标更多了而已
数据类型 数组名[常量表达式1][常量表达式2][常量表达式3][常量表达式4]....
[常量表达式n]
int arr[2][3][5];//意思就是2排3行5列。2排里面有3行元素,3行元素里面又包含了5个元素。
5.冒泡排序法:
        基本思想:每次比较相邻的2个数据,将小的数据排在大的数据前面,可以实现数组的小到大排序。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

void pai(int arr[])
{
    int i = 0;
    int j = 0;
    int t = 0;
    for (i = 1; i <= 9; i++)  //需要走的趟数
    {
        for (j = i + 1; j <= 10; j++)   //进行比较
            if (arr[i] > arr[j])    //如果前一个数比后一个数大,则利用中间变量t实现两值互换
            {
                t = arr[i];
                arr[i] = arr[j];
                arr[j] = t;
            }
    }
    printf("排序后的顺序是:\n");
    for (i = 1; i <= 10; i++)
        printf("%5d", arr[i]);    //输出排序后的数组
}


int main()
{
     int arr[11]={11,89,74,54,15,45,22,13,17,45,17};    //定义变量及数组为基本整型
     pai(arr);
    return 0;
}

实现小游戏:三子棋

//头文件
#pragma once
#include <stdio.h>
#include <time.h>
#include<stdlib.h>


#define ROWS 3   //行
#define COLS 3   //列




void init_board(char board[ROWS][COLS], int rows, int cols);
void display_board(char board[ROWS][COLS], int rows, int cols);
void computer_move(char board[ROWS][COLS], int rows, int cols);
void player_move(char board[ROWS][COLS], int rows, int cols);
int check_win(char board[ROWS][COLS], int rows, int cols);
//主函数
#define _CRT_SECURE_NO_WARNINGS 1
#include "get.h"


void menu()
{
	printf("*******************************\n");
	printf("*****1.paly    2.exit *********\n");
	printf("*******************************\n");
}


void game()
{
	int ret = 0;
	char board[ROWS][COLS] = { 0 };
	init_board(board, ROWS, COLS);
	display_board(board, ROWS, COLS);
	while (1)
	{
		printf("电脑走:\n");
		computer_move(board, ROWS, COLS);
		display_board(board, ROWS, COLS);
		ret = check_win(board, ROWS, COLS);
		if (ret != 'q')
		{
			break;
		}


		printf("玩家走:\n");
		player_move(board, ROWS, COLS);
		display_board(board, ROWS, COLS);
		ret = check_win(board, ROWS, COLS);
		if (ret != 'q')
		{
			break;
		}
	}


	if (ret == '*')
	{
		printf("玩家赢了!\n");
	}
	else if (ret == 'x')
	{
		printf("电脑赢了!\n");
	}
	else if (ret == ' ')
	{
		printf("平局!\n");
	}
}


int main()
{
	int input = 0;
	srand((unsigned)time(NULL));
	do
	{
		menu();
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 2:
			return 0;
			break;
		default:
			printf("输入错误,请重新输入!\n");
			break;
		}
	} while (input);


	system("pause");
	return 0;
}

//主体
#define _CRT_SECURE_NO_WARNINGS 1
#include "get.h"




void init_board(char board[ROWS][COLS], int rows, int cols)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < rows; i++)
	{
		for (j = 0; j < cols; j++)
		{
			board[i][j] = ' ';
		}
	}
}
void display_board(char board[ROWS][COLS], int rows, int cols)                    //显示棋盘
{
	int i = 0;
	for (i = 0; i < rows; i++)
	{
		printf("  %c |  %c |  %c \n", board[i][0], board[i][1], board[i][2]);
		if (i <= rows - 1)
		{
			printf("----|----|----\n");
		}
	}
}


void computer_move(char board[ROWS][COLS], int rows, int cols)           //电脑走
{
	while (1)
	{
		int x = rand() % 3;
		int y = rand() % 3;
		if (board[x][y] != '*' && board[x][y] != 'x')
		{
			board[x][y] = 'x';
			return;
		}


	}
}


void player_move(char board[ROWS][COLS], int rows, int cols)   //玩家走
{
	int x = 0;
	int y = 0;
	printf("玩家请输入坐标《》:");
	do
	{
		scanf("%d %d", &x, &y);
		if (board[x - 1][y - 1] == 'x')
		{
			printf("该坐标已经被占用,请重新输入《》:\n");
		}
		else if (((x - 1) > 3) || ((y - 1) > 3) || (x - 1 < 0) || (y - 1 < 0))
		{
			printf("输入坐标错误,请重新输入《》:\n");
		}
		else
		{
			board[x - 1][y - 1] = '*';
			break;
		}
	} while (1);
}


static int is_full(char board[ROWS][COLS], int rows, int cols)    //判断棋盘满没有
{
	int i = 0;
	int j = 0;
	for (i = 0; i < rows; i++)
	{
		for (j = 0; j < cols; j++)
		{
			if (board[i][j] == ' ')
			{
				return 0;
			}
		}
	}
	return 1;
}


int check_win(char board[ROWS][COLS], int rows, int cols)     //判断输赢
{
	int k = 1;
	int i = 0;
	for (i = 0; i < rows; i++)
	{
		if ((board[i][0] == board[i][1]) && (board[i][1] == board[i][2]) && board[i][0] != ' ')
		{
			return board[i][1];
		}
	}
	for (i = 0; i < cols; i++)
	{
		if ((board[0][i] == board[1][i]) && (board[1][i] == board[2][i]) && (board[0][i] != ' '))
		{
			return board[0][i];
		}
	}
	if ((board[0][0] == board[1][1]) && (board[1][1] == board[2][2]) && (board[0][0] != ' '))
	{
		return board[1][1];
	}
	if ((board[0][2] == board[1][1]) && (board[1][1] == board[2][0]) && (board[0][2] != ' '))
	{
		return board[0][2];
	}
	if (is_full(board, rows, cols))
	{
		return ' ';
	}
	return 'q';
}

杨辉三角问题

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main(void)
{
	int data[9][9];
	for (int i = 0; i < 9; i++)
	{
		for (int j = 0; j < 9; j++)
		{
			data[i][j] = 1;
		}
	}
	for (int i = 2; i < 9; i++)
	{
		for (int j = 1; j < i; j++)
		{
			data[i][j] = data[i - 1][j] + data[i - 1][j - 1];
		}
	}


	int i, j;
	for (int i = 1; i < 9; i++)
	{
		for (j = 1; j < 9 - i; j++)
		{
			printf(" ");
		}
		for (int j = 0; j <= i; j++)
		{
			printf("%3d", data[i][j]);
		}
		printf("\n");
	}
	return 0;
}

猜凶手问题:这里使用的是穷举法
日本某地发生了一件谋杀案,警察通过排查确定杀人凶手必为4个嫌疑犯的一个。

以下为4个嫌疑犯的供词:

A说:不是我。

B说:是C。

C说:是D。

D说:C在胡说

已知3个人说了真话,1个人说的是假话。

现在请根据这些信息,写一个程序来确定到底谁是凶手。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
char finr() 
{
	char killer = 'a';   //这里是使用字符代表abcd4个人
	for (; killer <= 'd'; killer++) 
	{
		if (((killer != 'a') + (killer == 'c') + (killer == 'd') + (killer != 'd')) == 3) 
		{
			printf("凶手是: %c\n", killer);
		}
	}
}
int main()
{
	finr();
	return 0;
}

猜名次问题:穷举法
5位运动员参加了10米台跳水比赛,有人让他们预测比赛结果:

A选手说:B第二,我第三;

B选手说:我第二,E第四;

C选手说:我第一,D第二;

D选手说:C最后,我第三;

E选手说:我第四,A第一;

比赛结束后,每位选手都说对了一半,请编程确定比赛的名次。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>

int main()
{
	int A = 0;
	int B = 0;
	int C = 0;
	int D = 0;
	int E = 0;
	for (A = 1; A <= 5; A++)
		for (B = 1; B <= 5; B++)
			for (C = 1; C <= 5; C++)
				for (D = 1; D <= 5; D++)
					for (E = 1; E <= 5; E++)
						if (((B == 2) + (A == 3) == 1 && (B == 2) + (E == 4) == 1 && (C == 1) + (D == 2) == 1 && 
							(C == 5) + (D == 3) == 1 && (E == 4) + (A == 1) == 1) == 1)
							if (120 == A * B * C * D * E)
								printf("A=%d  B=%d  C=%d  D=%d  E=%d\n", A, B, C, D, E);
	return 0;
}

八.函数

1.函数

   0.构成c程序的基础单元就是函数,函数中包含了可执行代码块、
 
  1.函数的声明与定义:
       一个函数的定义如下:
int//函数的返回值类型 
app//函数的名字 int x,int c)//函数的参数列表,列表里面多少个数据就需要传多少参数进去
int app(int  x,int c)  //完整的组成
{
return 0; //里面的代码块又叫做函数体。
}
 定义函数的特殊情况:
     1.无参函数:就是代表没有任何参数的函数
     2.空函数:就是没有任何内容与功能的函数
void//(无返回类型) crr()  //调用时里面没有传数据进来的函数就叫无参函数
void arr()  //这种就叫做空函数
{
}
   3.函数内部是不可以定义另一个函数的,但是可以进行函数的调用。
int main()
{
void arr()    //这样写是错误的,不可以在一个函数的内部进行函数的定义的
{
return 0;
}
}
函数的声明:就是告诉编译器有这个函数,顺带告诉它这个函数功能有什么作用,常用的声明方式有2中:
  1.就是先声明,后定义
int arr(int a);  //进行声明,声明时需要带;


int  main()  //主函数
{
int x=100; 
arr(x);    //调用函数arr把数据x传入
}



int arr(int a)   //进行定义,不需要带;
{
printf("%d",a);
return 0;
}
  2.声明的时候就进行定义(个人推荐第二种,因为这样可以使代码看起来更简洁)
int arr(int a)   //进行声明同时进行定义。
{
printf("%d",a);
return 0;
}


int  main()  //主函数
{
int x=100; 
arr(x);    //调用函数arr把数据x传入
}

2.函数的参数

 1.形式参数与实际参数
        1.形式参数:声明与定义函数的时候,里面的参数被称为形式参数,简称形参。
int arr(int a)   //a被称作形式参数
{
printf("%d",a);
return 0;
}
       2.实际参数:调用函数时,函数名后面括号中的参数叫做实际参数。简称实参。
int  main()  //主函数
{
int x=100; 
arr(x);    //x被叫做实际参数。
}
  2.数组元素作为函数的参数
       数组元素作为函数的实参,与普通变量作为函数参数是一样的,是单向的值传递
void ar(int c)  //数组元素作为函数参数
{
printf("%d",c);  //进行数组的输出
}


int main()
{
int arr[10]={1,2,3,4,5,6,7,8,9,10}  //给定数组的元素
int  i = 0;
for(i=0;i<10;i++)
{
ar(arr[i]);   //利用循环把数组元素一一传进去。
}
return 0;
}
2.数组名作为函数的参数
   在c语言里面,通常数组名表示的是数组的第一个元素的地址,
   但是有2种情况例外:1.siezof,在这里表示的是整个数组。
                                     2.  &地址也是取出整个数组的地址。
void ar(int c[])  //数组首元素作为函数参数
{
int	i = 0;
for(i=0;i<10;i++)
{
printf("%d",c[i]);  //进行数组的输出
}
}

int main()
{
int arr[10]={1,2,3,4,5,6,7,8,9,10}  //给定数组的元素
ar(arr);   //把数组首元素传进去。
return 0;
}

3…递归

 1.什么是递归:就是程序调用自身的技巧就叫递归。核心思想就是:把大事化小。

一些例题:
1.递归实现次方

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int po(int n, int k)
{
	if (k <= 0)    //这里为跳出的条件。
		return 1;
	else
		return n * po(n, k - 1);   //不满足则继续进行递归
}
int main()
{
	int n = 0;    
	int k = 0;
	scanf("%d%d", &n, &k);
	int ret = po(n, k);
	printf("%d\n", ret);    //打印返回的结果。
	return 0;
}

2.递归实现斐波那契

int fib(int n)
{
	if (n == 1 || n == 2)
	{
		return 1;
	}
	else
	{
		return fib(n - 1) + fib(n - 2);
	}
}
int main()
{
	int n = 0;
	int ret = 0;
	scanf("%d", &n);
	ret = fib(n);
	printf("ret=%d", ret);
	return 0;
}

3.计算分离出来数字的合(递归实现)

int DigitSum(int n)
{
	int d = 0;
	int i = 0;
	if (n>=1)
	{
		d=DigitSum(n / 10)+(n%10);    //实现递归分离

	}

	return d;
}

int main()
{
	int add = 0;
	scanf("%d", &add);
	int c= DigitSum(add);
	printf("%d", c);
	return 0;
}

4.利用递归实现字符逆序排列

char reverse_string(char* string)
{
	if (*string != '\0')
	{
		 reverse_string(string+1);
	}
	printf("%c",*string);
	return 0;
 }

char reverse_string(char* string)
{

}

int main()
{
	char arr[] = "abcdef";
    reverse_string(arr);
	return 0;
}

4.内外部函数之分

内部函数:定义了一个函数,该函数只能在该源文件的内部使用,那么该函数为内部函数。而内部函数又被称为静态函数。在一个函数面前加上关键字static就可以将函数指定为内部函数;内部函数的定义:
static(修饰为内部函数的关键字) int(返回值类型) add(该函数的名字)(int abc,int abn)(参数列表)
外部函数:它是可以被外部源文件调用的函数,在一个函数前面加上关键字extern就可以将该函数指定为外部函数:定义如下:
extern int add(int abc,int abv)

5.一些经典的函数

数学函数库(math.h)(部分):1.abs函数:它的功能是求出一个数字的绝对值;形式如下:
int abs(int i)
//使用:int i=-12;
int c=abs(i);//该结果就是12;
 2.labs函数:它的功能是求出长整形数的绝对值;用法与上面相同,返回的都是一个整形。
 3.fbs函数:功能是求出浮点数数的绝对值;用法与上同;

简单例子:求出2人相差几岁:

#include <stdio.h>
#include <math.h>

int	main()
{
int	i=0;
int c=0;
int b=0;//这里是记录它们相差的数据。
int h=0;
printf("输入它们的年龄:\n");
scanf("%d %d",&i,&c);
b=i-c;
h=abs(b);
printf("2个人相差的数据为:%d",h);
return 0;
}
还有一些求三角函数的:4.sin函数:求正弦:形式为:
double sin(double x)
5.cos函数:求余弦,形式为:
double x(double i)
 6.tan函数:求正切:形式为:
double i(double z)

字符函数

需要使用头文件ctype.h
 1.isalpha函数:检测某个字符是否为字母
 2.isdigit函数:检测某个字符是否为数字
 3.isalnum函数:检测某个字符是否为字母或者数字
 它们的使用形式都是一样的:
char c;
scanf("%c",&c);
isalpha(c);

字符串处理函数
下面函数的使用方法可以在一个网站中找到它们包含的头文件与参数类型:

https://cplusplus.com/

1.strlen函数:计算字符串的个数,以\0为结束标记:,它的返回值是无符号类型:

2.strcpy函数:长度不受限制字符串函数:功能:把字符数组2的内容拷贝放到字符数组1去

3.strcat函数:长度不受限制字符串函数:功能:把字符串2连接到字符串1的末尾,使其组成一个新的字符串,

4.strcmp函数:长度不受限制字符串函数:功能:将一个字符串1与另一个字符串2进行比较,按照ASCII表的顺序进行比较。如果字符串1=字符串2,则返回0:字符串1>字符串2,则返回1,字符串1<字符串2,则返回-1;

5.strncpy函数:长度受限制字符串函数:把字符数组2的内容拷贝n个字节放到字符数组1去,但是与前面相比,它多了个参数的限制,就是说,它不是无限制的拷贝过去。

6.strncat函数:长度受限制字符串函数:在字符串1的末尾追加n个字节的字符串2的内容。

7.strncmp函数:长度受限制字符串函数:将一个字符串1与另一个字符串2进行n个字节的内容比较,按照ASCII表的顺序进行比较。如果字符串1=字符串2,则返回0:字符串1>字符串2,则返回1,字符串1<字符串2,则返回-1;

8.strstr函数:字符串查找函数:功能:判断字符串2是否为字符串1的子串,如果是,则返回字符串2从字符串1中出现的位置开始到字符串1的结尾,否则返回null。

9.strtok函数:字符串查找函数:功能:分割字符串的内容。

10.strerror函数:错误报告函数:库函数调用失败之后,该函数可以将错误码转换为错误信息。

11.memcpy函数:内存操作函数:功能:内存拷贝函数,从源字符串地址的起始位置考试拷贝n个字节的内容到目标空间地址中;

12.memmove函数 :内存操作函数:拷贝函数;上面的内存拷贝函数没有考虑到内存空间数据重叠的情况,它可以保证源字符串在被覆盖区域的之前将重叠的字节拷贝到目标区域,但是后面的内容会被修改。
例如:1,2,3,4,5,6,7,我要修改前面把123复制到456的位置就会变成:1,2,3,1,2,3,7这样的数据;

13.memset函数:内存操作函数:初始化函数,将一块的内存空间的数据全部进行初始化为指定的数据。

14.memcmp:内存操作函数:把内存空间的字符串1与字符串2的前n给字节的内容进行比较,如果字符串1=字符串2,则返回0:字符串1>字符串2,则返回1,字符串1<字符串2,则返回-1。

一些小练习:1.打印x图案

#include<stdio.h>

int main()
{
    int n,i,j;
    while(scanf("%d",&n)!=EOF)
    {
        for(i=0;i<n;i++)
        {
            for(j=0;j<n;j++)
            {
                if(j==i||j==n-i-1)
                {
                    printf("*");
                }
                else
                {
                    printf(" ");
                }
            }
            printf("\n");
        }
    }

2.获得月份的天数

#include<stdio.h>


int main()
{
	int year,month;
	while(scanf("%d %d",&year,&month)!=EOF)
    {
            if(month==1||month==3||month==5||month==7||month==8||month==10||month==12)
{
				printf("31\n");
			}
			if(month==4||month==6||month==9||month==11){
				printf("30\n");
			}
			if(month==2){
				if(year%4==0){
					printf("29\n");
				}
				else{
					printf("28\n");
				}
			}
	}
	return 0; 
} 

3.完成3角型的判断

#include <stdio.h>


    int main()
    {
        int a=1, b=1, c=1;


        while (scanf("%d %d %d", &a, &b, &c) != EOF)
        {
            if (a + b > c && a + c > b && b + c > a)
            {
                if (a == b && b == c)
                {
                    printf("Equilateral triangle!\n");
                }
                else if (a == b || b == c || a == c)
                {
                    printf("Isosceles triangle!\n");
                }
                else
                {
                    printf("Ordinary triangle!\n");
                }
            }
            else
            {
                printf("Not a triangle!\n");
            }
        }
        return 0;
    }

九.指针

1.指针

 概述:想要看明白指针,那就必须要要数据在系统是怎么存的,通常来说,系统会对每一个内存单元进行编号,这些内存单元内又会有其他带有编号的小房间,简单来说,想要学会灵活的利用指针,就需要知道房间的编号。
 我们已经在前面已经知道了&这个符号的作用,它是用来取出变量在内存中的地址。如图:i所在的内存空间的数据,i是4个字节的内容,所以占据了4个小房间。

在这里插入图片描述
变量与指针

   变量的地址是变量和指针之间的连接纽带,所谓的指向就是通过地址来体现的。因为指针变量通常指向了一个变量的地址,将该变量的地址赋给了指针变量,该指针就指向了变量的第一个内容。如图:把i的地址存到了指针变量p中,在对p进行*操作就得到了i里面的内容了。

在这里插入图片描述

计算机的存取流程:根据变量名与地址的对应关系,找到某个变量的地址,然后从它的地址中开始读取里面的数据存放到cpu里面去。在低级语言(汇编)中一般直接通过地址来访问内存单元,高级语言一般采用变量名的形式。

指针变量:一个变量的地址称为该变量的指针,有一个变量专门存放另一个变量的地址,它就是指针变量。它的定义形式如下:
类型说明 (如int char double float等等)*(表示该变量是指针变量) 变量名
int* arr;//这是一个整形指针。
对指针变量的赋值:指针变量如同普通变量一样,使用前需要对其进行赋值,未经赋值的指针变量不可以使用,给指针变量赋值只可以赋地址,而不能是其他数据。
int a;
int* p=&a;
//第二种
int a;
int* b;
b=&a
指针变量的引用:引用指针变量是对变量进行间接访问的一种形式,引用指针变量的形式为*指针变量。含义是引用指针变量所指向的值。

*与&运算符:它们都属于单目运算符,&是取地址运算符,返回一个操作数的地址,*则是指针运算符,用于返回指定地址存放的变量值。

&*与*&的区别:它们的关系是同级的,按自右向左方向结合,&*p先进行的是*运算在进&运算,则拿出来的是*p的地址,*&a,是先进行&运算,在进行*运算,则拿出来的就是a的地址。

指针的++与--运算:指针的++与--运算不同于普通变量的运算,它不是按照1+1这样相加的,而是根据类型的长度进行++与--操作,也就是说:int*类型数据进行++或者--运算时,是一次性增加4个字节的内容,也就是跳过一个整形的数据。

指针+-运算:指针+-整数就是跳过它的数据类型,如整形指针+1则跳过4个字节内容,char指针类型+1则跳过1字节内容。

野指针问题:野指针是指针指向的位置不可知,是随机的,没有明确方向的。
它成形的原因是”:1.指针未进行初始化(没有指向的地址)2.指针越界访问
3.指针指向的空间释放。
如何规避野指针:1.指针初始化。2.小心指针越界。3.指针指向空间释放时设置为指向null。4.避免返回局部空间。5.指针使用前检测有效性。

2.数组与指针

一维数组与指针:定义一维数组时,系统会为其分配一块连续的内存空间,数组名在一般情况下在内存中就是首地址。诺定义一个指针变量,并把数组的首地址传给该指针,则该指针指向该数组的首地址。
int a[10]={1,2,3,4,5,6,7,8,9,10}
int* p=a;
 这里的a是数组名,也是数组的首地址,把a赋给了*p,也就是将数组a的首地址给了p。

 一些小原理:还是以上面的例子:p+n与a+n都是代表了数组元素a【n】的地址,既&a【n】。这个数组a有10个元素,则n的取值为0~9,则数组元素可以表示为p+0~9或者a+0~9;
 *(p+n)与*(a+n)都用于表示数组元素a【n】。语句:printf(“%5d”,*(p+i)表示的是数组a的元素。
int arr[m][n];//这是一个二维数组
int* a=NULL;
 二维数组与指针:1.a表示二维数组的首地址,也表示数组第一行的首地址,a+1表示第二行的地址,a+m表示第m+1行的地址。
 2.a【0】+n表示数组第一行第n+1个元素的地址,a【m】+n表示第m+1行第n+1个元素的地址。
 3.&a【0】表示数组第一行的首地址,&a【m】表示第m+1行的首地址;
 4.&a【0】【0】可以表示数组第一行第一个元素的首地址,也可以表示整个二维数组的首地址,&a【m】【n】就是第m+1行n+1列的元素。
 指针也可以表示地址,因此可以通过指针引用二维数组的元素。
 5.*(*(a+m)+n)和*(a【m】+n)含义相同,都是表示第m+1行第n+1列的元素。
 图解:

在这里插入图片描述

指针数组: 就是一个数组元素全是指针的数组。每个元素都是指针类型的数组。
     
     数组指针:就是指向数组的指针被称为数组指针。
  指针数组与数组指针的区别就在于,前者是先于类型结合,后者则是以与数组名先进行结合
 指针数组:int* arr【5】(数组里面存放都是指针类型)
 数组指针:int (*arr)【5】(一般用于二维数组,它是指向一个数组的指针)
 数组指针一般多用于二维数组
 int (*pf)【10】等价于int p【10】【10】
 *((*2)+i)+j 等价于 int p【10】【10】
 int *p 【10】=&arr(代表的数组)  跳40字节  也就是&地址,取走整个数组的地址,
 int *p=arr       跳4字节     这里只是代表了首地址。

  字符与指针:访问字符串有2种形式,一是字符数组的形式,另一种就是字符指针;其形式为:
char* arr=“abcde”;
记住这个方法不是把整个字符串存入到字符指针里面去,而是把第一个字符的地址赋给了该指针变量。
图解:

在这里插入图片描述

 指向指针的指针:一个可以作为指向整形,浮点型,字符型等等的变量,当然也可以指向一个指针啦。此时,这个指针叫做指向指针的指针(多级指针)。
int C=10;
int* p=&c;
int** o=&p; //这个叫二级指针(指向指针的指针)

如图:

在这里插入图片描述

  指针变量作为函数的参数:整形,浮点型,字符型,数组名,数组元素等等都可以作为函数的参数,此外,指针也可以作为函数的参数;
  下面有一些简单的代码可以看看指针作为函数参数的效果;
#include <stdio.h>

void swnp(int* c,int* q) //因为传递过来的是地址,需要使用指针来接受,通过指针会修改原来的东西,因为地址指向它那块的地址(可以想象为传过来的实参)
{
int t=0;
t=*c;   //因为是指针变量,需要解引用得到具体的数据。
*c=*q;
*q=t;
}

int main()
{
int x=0;
int y=0;
scanf("%d %d",&x,&y);
swnp(&x,&y);
printf("%d %d", x, y);  
return 0;
}

通过上面例子可以知道,运用指针来传递参数可以节省值传递带来的开销,也可以使函数调用不产生值传递。
下面再来一个练习:运用指针完成冒泡排序:
它的基本思想是:如果对于n个数据进行冒泡排序,则需要进行n-1论的比较,在第一轮里面要从后到前进行n-1次两两比较,在j轮里面需要进行n-j次的比较。

#include <stdio.h>
void pai(int* a, int* c)
{
    int i = 0; //趟数
    int j = 0; //轮数
    int t = 0; //交换
    for (i = 0; i < *c - 1; i++)
    {
        for (j = 0; j < *c - 1 - i; j++)
        {
            if (*(a + j) > *(a + j + 1))  //符合条件进行交换
            {
                t = *(a + j);
                *(a + j) = *(a + j + 1);
                *(a + j + 1) = t;
            }
        }
    }
    printf("排序好的数组为:");
    for (i = 0; i < *c; i++)
    {
        printf("%d ", *(a + i));
    }
}

int	main()
{
    int a[5] = { 1,7,9,4,3 };
    int c = 5;
    pai(a, &c);
    return 0;
}

通过上面的例子可以知道:1.数组名是数组的首地址
2.当形参为数组时,实参也可以时指针变量:
3.数组名在2种情况下不代表首地址而是整个数组:&与sizeof。如:&arr与sizeof arr,前者时拿出数组的整个地址,后者时计算整个数组的大小。

数组与指针传参问题:下面有几种指针的传参问题,我们来看看行不行:

//一维数组
void ar(int arr[]?  //通过编译发现这个是可以的
void ar(int arr[10]) ? //这个也是可以的 
void ar(int *arr) ?    //利用一级指针接受数组名也是可以的
void ar(int *arr[10])? //这是数组指针的传值方式,不可以的
void ar(int **arr)  ?  //这里二级指针不行。

//二维数组
void arr(int ar[3][5]? //二维数组传参利用二维数组接受,可以
void arr(int ar[][]?  //这个不行,因为他找不到行数与列数里面的元素有多少在哪里。
void arr(int ar[][5]? //这个可以,因为二维数组可以省略第一个[]的数字
//由上得知:1.二维数组传参,可以省略第一个[]的数字。2.一个二维数组可以不知道多少行,但是一定要知道有一行有多少元素。

void arr(int *ar)?// 不行,因为它只找到了行
void arr(int* ar[5]?//不行,因为它是代表了指针数组(一维)。
void arr(int*ar)[5]?//这个可以,*先于ar结合找到第一行地址。
void arr(int **ar) ? //二级不行,因为不知道先找到哪一个

//二级指针
void tee(char **p)
{}

int main()
{
char c='x';
char *p=&c;
char **v=&p;
char* a[10];
tee(&p);
tee(v);
tee(arr); ?//这个不行,因为它只是字符指针数组,而不是多维数组。
return 0;
}

函数指针:它是指向函数的指针变量。而且函数是有地址的,所以它也可以给利用指针变量来进行保存。我们看一段代码

void tee()
{
printf("jijiji");
}
void (*p)();
void *p();
//以上2种数据,哪个可以存储&tee呢?
//答案是:第一个:因为p先与*结合,说明它是一个指针变量,指针指向一个函数,指向了无参函数,它的返回类型是void。

函数指针数组:它是存放函数指针的一种数组;里面的数据类型全部都是函数指针;下面来看看函数指针数组的写法形式:

//这里有3种定义形式
int (*par[10])();//这个形式是par先于[]结合,说明是数组,后剩下是int (*)()类型。
int *par1[10]();//这个形式是先于[]结合,说明为数组,而数组的元素是*par1的指针数组,返回类型为int类型。
int*)()par3[10];//这是全部独立的东西。
//所以,函数指针数组的写法就是par这种写法。

函数指针数组一般多用于:转移表。(计算器)

#include <stdio.h>
int add(int a,int b)
{
return a+b;
}

int sub(int a,int b)
{
return a-b;
}

int mul(int a,int b)
{
return a*b;
}

int div(int a,int b)
{
return a/b;
}

int main()
{
int x,y;
int input=1;
int ret=0;
int (*p[5])(int x,int y)={0,add,sub,mul,div};
whlie(input)
{
printf("1.add,2.sub,3.mul,4.div\n");
printf("请选择:");
scanf("%d",&input);
if((input<=4&&input>=1))
{
printf("输入数据\n");
scanf("%d %d",&x,&y);
ret=(*p[input]);
}
else
{
printf("输入有误\n");
printf("ret=%d",ret);
}
return 0}

指向函数指针数组的指针:它是一个指针,,指针指向了一个数组,数组的元素都是函数指针。
定义:

void tee(const char* str);
{
printf("%s\n",str);
}
int main()
{
//函数指针p
void (*p)(const char*)=tee;
//函数指针的数组p1
void (*p1[5])(const char* str);
p1[0]=tee;
//指向函数指针数组p1的指针p2
void (*(p2)[5])(const char*)=&p1;
return 0;
}

回调函数:它是一个通过函数指针调用的函数。如果你把函数的指针作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就叫该函数为回调函数,回调函数不是由该函数的实现方式直接调用的,而是在特定的条件下由另一方调用的用于对该事件的响应。
这里就需要介绍下一个函数qsort函数:看下关于qsort函数的代码。它的原型:
void qsort (void* base, size_t num, size_t size,
int (compar)(const void,const void*));

#include <stdio.h>

int cmp(const void* p1,const void* p2)//这里是排序规则,因为qosrt没有提供排序规则。
{
return (*(int*)p1-*(int*)p2);
}

int main()
{
int arr[]={1,3,5,7,9,2,4,6,8,0};
int i=0;
qsort(arr,sizeof(arr)/sizeof(arr)[0],sizeof(int),cmp);//第一个参数是需要排序的数组,第二个参数是数据的大小,第三个数据是代表一个数据的大小,第四个数据是排序规则。
for(i=0;i<sizeof(arr)/sizeof(arr)[0];i++)
{
printf("%d ",arr[i]);
}
printf("\n");
return 0;
}

通过上面的例子知道了qsort 的用法。我们现在可以尝试下利用这个函数来完成一些排序算法的实现;

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
void Swap(char* buf1, char* buf2, int width)
{
	int i = 0;
	for (i = 0; i < width; i++)
	{
		char temp = *buf1;
		*buf1 = *buf2;
		*buf2 = temp;
		buf1++;
		buf2++;
	}
}
int cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}
void bubbleSort(void* base, size_t num, size_t width, int (*cmp)(const void*, const void*))
{
	int i = 0;
	for (i = 0; i < num - 1; i++)
	{
		int j = 0;
		for (j = 0; j < num - 1 - i; j++)
		{
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			{
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
		}
	}
}
void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}
void test109()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
}
struct Stu
{
	char name[20];
	int age;
};
int cmp_name(const void* e1, const void* e2)
{
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
int cmp_age(const void* e1, const void* e2)
{
	return (((struct Stu*)e1)->age, ((struct Stu*)e2)->age);
}

void test110()
{
	struct Stu s[3] = { {"zhangsan",15},{"lisi",17},{"wangwu",20} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_age);
}
void test111()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubbleSort(arr, sz, sizeof(arr[0]), cmp_int);
	print_arr(arr, sz);
}
void test112()
{
	struct Stu s[3] = { {"zhangsan",15},{"lisi",17},{"wangwu",20} };
	int sz = sizeof(s) / sizeof(s[0]);
	bubbleSort(s, sz, sizeof(s[0]), cmp_age);
}
int main()
{
	test111();
	return 0;
}

2.模拟实现部分字符函数

1.模拟实现strlen

//模拟strlen
#include <stdio.h>
int My_strlen(const char* arr)
{
	int j = 0;
	while (*arr++!='\0')
	{
		j++;
	}
	return j;
}

int main()
{
	char arr[20]="abcdef";
	char arr1[20]="cdefgh";
	int  arr2 = My_strlen(arr);
	printf("%d", arr2);
	return 0;
}

2.模拟strcpy

//模拟strcpy
#include <stdio.h>
char* My_strcpy(char* arr, const char* arr2)

{
	assert(*arr && *arr2);
	char* sss = arr;
	while (*arr++ = *arr2++)
	{
		;
	}
	return sss;
}


int main()
{
	char arr[20] ;
	char arr1[20] = "abcdef";
	//int  arr2 = My_strlen(arr);
	printf("%s", My_strcpy(arr, arr1));
	return 0;
}

3.模拟实现strcat

#include <stdio.h>
char* My_strcat(char* arr, const char* arr1)
{
		char* ret = arr;
		assert(arr != NULL);
		assert(arr1 != NULL);
		while (*arr)
		{
			arr++;
		}
		while ((*arr++ = *arr1++))
		{
			;
		}
		
		return ret;
}

int main()
{
	char arr[20] ="jklo";
	char arr1[20] = "abcdef";
	printf("%s", My_strcat(arr, arr1));
	return 0;
}

4.模拟库函数strcmp

//模拟库函数strcmp;
#include <stdio.h>
int My_strcat(const char* arr,const char* arr1)
{
	assert(arr!=NULL);
	assert(arr1 != NULL);
	int c=0;
	while (*arr)
	{
		if (*arr++ == *arr1++)
		{
			c = 0;
			continue;
		}
		 if (arr < arr1)
		{
			c = 1;
			break;
		}
		 if (arr>arr1)
		{
			c = -1;
			break;
		}
	}
	return c;
}

int main()
{
	char arr[20] ="abcdlf";
	char arr1[20] = "abcdef";
	int ret = My_strcat(arr,arr1);
	if (ret< 0)
	{
		printf("arr<arr1");
	}
	else if (ret==0)
	{
		printf("arr==arr1");
	}
	else
	{
		printf("arr>arr1");
	}
	return 0;
}

5.模拟实现strstr

//模拟实现strstr
#include <stdio.h>
char* My_strstr(const char* arr, const char* arr1)
{
	assert(arr && arr1);
	const char* r = arr;
	const char* r2 = arr1;
	const char* r3 = arr;
	while (*r3)
	{
		r = r3;
		r2 = arr1;
		while (*r!='\0' && *r2 != '\0'&& *r==*r2 )
		{
			r++;
			r2++;
		}
		if (*r2=='\0')
		{
			return (char*)r3;
		}
		r3++;
	}
	return NULL;
}

int main()
{
	char arr[20] ="lkopii";
	char arr1[20] = "abcdef";
	printf("%s", My_strstr(arr, arr1));
	return 0;
}

6.模拟实现memcpy

//模拟实现memcpy
void* My_memcpy(void* mou,const void* op, int k)  //这里是因为用户不知道会放什么类型的数据;
{
	char* ret = mou;     //记录字符串
	assert(mou && op);
	while (k--)
	{
		*(char*)mou = *(char*)op; //这里是因为不知道用户会传多少字节内容进来;
		mou=(char*)mou+1;
		op = (char*)op + 1;
	}
	return (ret);
}

int main()
{
	char arr[20] ="ooooo";
	char arr1[20] = "abcdef";
	char* iu = My_memcpy(arr, arr1, 4);
	printf("%s", iu);
	return 0;
}

7.模拟实现memmove

#include <stio.h>
#include <assert.h>
void* my_memmove(void* dest, const void* src, int n)
{
	char* pdest = (char*)dest;
	const char* psrc = (const char*)src;
	assert(dest);
	assert(src);
	if (pdest <= psrc && pdest >= psrc + n)
	{
		while (n--)
		{
			*pdest = *psrc;
		}
	}
	else 
	{
		while (n--)
		{
			*(pdest + n) = *(psrc + n);
		}
	}
	return dest;
}

int main()
{
	char arr[10] = "abcdef";
	char arr0[10] = "abcdef";
	char arr1[10] = { 0 };
	my_memmove(arr + 2, arr, 4);
	my_memmove(arr1, arr0, 4);
	printf("内存覆盖情况:%s\n", arr + 2);
	printf("正常情况:%s\n", arr1);
	return 0;
}

8.模拟实现strncpy

#include <stdio.h>

char* my_strncpy(char* dest, const char* src, size_t count)
{
    char* start = dest;
    while (count && (*dest++ = *src++)) 
    {
        count--;
    }
    if (count) 
    {
        while (--count)
        {
            *dest++ = '\0';
        }
    }
    return start;
}
int main()
{
    char arr1[] = "abcdefghi";
    char arr2[] = "ibe";
    my_strncpy(arr1, arr2, 6);
    printf("%s\n", arr1);
}

9.模拟实现strncat

#include <stdio.h>
#include<string.h>
#include<assert.h>

void my_strncat(char* arr, const char* arr1, int n)
{
	char* p = arr;
	char* q = (char*)arr1;
	assert(arr);
	assert(arr1);
	while (*p)
	{
		p++;
	}
	while (n--)
	{
		*p = *q;
		p++;
		q++;
	}
	*p = '\0';
	printf("%s", arr);
	printf("\n");
}

int main()
{
	char arr[32] = "I am a ";
	const char arr1[] = "goooo";
	int numuu;
	printf("请输入要拼接字符长度(num<%d):", strlen(arr1) + 1);
	scanf("%d", &numuu);
	my_strncat(arr, arr1, num);
	return 0;
}

10.模拟实现atoi

#include<stdlib.h>
#include<assert.h>
#include<ctype.h>
#include<limits.h>
enum State
{
	INVALID,
	VALID
};
enum State state = INVALID;

int my_atoi(const char* s)
{

	if (NULL == s)
	{
		return 0;
	}
	if (*s == '\0')
	{
		return 0;
	}
	while (isspace(*s))
	{
		s++;
	}
	int flag = 1;
	if (*s == '+')
	{
		flag = 1;
		s++;
	}
	else if (*s == '-')
	{
		flag = -1;
		s++;
	}
	long long n = 0;
	while (isdigit(*s))
	{
		n = n * 10 + flag * (*s - '0');
		if (n > INT_MAX || n < INT_MIN)
		{
			return 0;
		}
		s++;
	}
	if (*s == '\0')
	{
		state = VALID;
		return (int)n;
	}
	else
	{
		return (int)n;
	}
}
int main()
{

	char* p = "1234";
	int ret = my_atoi(p);
	if (state == VALID)
	{
		printf("正常转换:%d\n", ret);
	}
	else
	{
		printf("非法转换:%d\n", ret);
	}
	return 0;
}

十.结构体与联合体

1.结构体:

 有时候总会由一些复杂的数据类型,数组,整形等等变量无法解决的数据,所以就出现了结构体这种类型,专门用来封装一些复杂的数据的类型(structure)
 以此来表示新的数据类型。
 结构体声明:
struct S(最好使用大写,这是结构体的名字)
{
char arr;
int arri;
//这里是成员列表
}x;(这是结构体变量。)

这里有一些要点:关键字struct声明这是一个结构体,结构体名表示创建的新类型名,大括号里面的是成员列表,。注意,结构体变量后面的;不能缺少。
例如,我需要声明一个商品,如图:
在这里插入图片描述

结构体变量的引用:
定义了结构体之后我们就可以进行引用:它的引用形式为结构体变量名.成员名

x.arr='a';
x.arri=10;
还有需要注意的是,结构体不能直接把把一个结构体当成一个整体进行输入和输出,
需 要引用这个结构体的成员名进行输入与输出。如果一个结构体里面的某个成员为结构体,
则需要先找到外层结构体的数据,在进行引用内层结构体的数据进行输入与输出。
结构体变量是也可以进行各种运算的,例如:
X.arr=X.arr+50;
X。arr=X.arr++;
结构体的初始化:结构体与其他类型基本一样,也可以在定义结构体的时候对其进行赋值。如
struct Xarr
{
double arr;
int  arr1;
float arr2;
short arr3;
char arr4;
}structt={"3.14,10,3.1f,1,'w'"};//这是在定义的时候直接赋值,还有一种就是先定义后赋值也是可以的。
结构体大小的计算规则:结构体的计算与其他类型的计算不一样,在不同的编译器里面,还有不同对齐规则。以下就是结构体的对齐规则。
1.第一个成员在偏移量为0的地址处
2.其他成员变量要对齐到某个数字(也就是对齐数)的整数倍处地址处
3.对齐数:编译器默认的一个对齐数与该成员大小的较小值。
4.结构体总大小为最大对齐数的整数倍
5.如果存在嵌套的情况,则嵌套结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所以最大的对齐数(含嵌套结构体的对齐数)的整数倍。
为什么会存在内存对齐这个概念:1.为了保证可移植性
2.性能问题:因为数据结构(堆)尽可能会自然边界对齐。最主要就是,为了对未对齐的内存,处理器需要进行2次访问,而对齐的内存只需要一次。(整个结构体对齐数是取那个类型最大的)
修改默认对齐数的预处理指令:(但没事还是不要去修改)#pragma 。
#pragma pack(10) //修改默认对齐数为10;
#pragma pack() //还原默认对齐数
 结构体数组:当需要定义多个结构体时,我们就可以采用数组的形式。结构体数组与普通数组的区别就是:前者的元素都是结构体,后者的元素都为普通类型。它定义方式与单个结构体相同,不同的时在变量名上加入了数组;
//定义结构体数组
struct Xarr
{
char arr[10];
int nian;
}xy[5];//这里表示有一个数组,它的元素类型时结构体,它的初始化与普通数组一样,都是一个一个数据进行初始化。

如图:

在这里插入图片描述

 结构体指针:它是一个指向结构体变量的指针,指向的是结构体变量的初始地址,除此之外,它还可以指向结构体数组与数组中的元素。
 它的定义形式如下:
//struct X(结构体类型) *p(指针名);
指针指向的是结构体变量的地址,因此可以使用指针来访问结构体里面的成员,方式有2种:. 与 ->;
//第一种方式
(*p).arr=10000;//这是第一种,先解引用找到这个结构体,最后里面.进行操作。
//第二种
p->arr=10000;//这是第二种,相比第一种的麻烦,第二种显然方便了很多。

根据以上可以知道结构体的3种引用方式:1.p.成员名
2.(*p).成员名
3.p->成员名。

指向结构体数组的指针:结构体指针变量指向结构体数组时,指针变量的值就是结构体数组的首地址。还可以直接指向结构体数组中的元素,这时指针变量的值就是该结构体数组元素的首地址。如
struct stu* ps;//这是一个结构体指针
ps=xy;  //它指向了上面的结构体数组。(这只是例子)
结构体做函数参数:使用某个函数时,其参数也可以是结构体变量。其形式有3种:
1.使用结构体变量作为函数参数:此时作为函数实参时,它采取的是值传递的方式。
2.使用指向结构体变量的指针作为函数参数:在使用结构体变量作为函数参数占的内存和开销过于大,那有什么办法,这时,结构体指针就大大的派上用场了。因为传指针的时候,只是把首地址进行传递,并没有把副本数据也一并传输。
3.使用结构体变量成员作为函数参数:其实这个方式与第一种一样没什么好讲的。
void dis(struct Stu stul) //使用结构体变量作为函数参数
void dis(struct Stu* stul) //使用结构体指针作为函数参数
void dis( Stu.fs[0] )  //使用结构体变量的成员作为函数参数

结构体自引用的形式:

struct Node
{
int data;
struct Node* next;
};

2.链表的概述

   链表:它是一种常见的数据结构,链表中有一个头指针,指向一个变量,这个变量叫元素。在链表里,每个元素包括数据部分与指针部分。数据部分用来存放数据,指针部分用来存放指向下一个元素的地址。最后一个元素的指针指向null。

在这里插入图片描述
注意:链表这种数据需要使用指针来实现,因此链表中所以节点都会包含一个指针变量,以保存下一个节点的地址。

 创建动态链表:在我们创建之前需要认识下几个函数
   1.malloc函数:它用于在内存中动态分配一块size大小的内存空间。原型为
   :void* malloc (size_t size);它会返回一个指针,该指针指向开辟好的内存空间。开辟失败则返回null;
   2.calloc函数:用于在内存中分配n个长度为size的连续内存空间,原型为:
   void* calloc (size_t num, size_t size);  它会返回一个指针,该指针指向开辟好的一块连续内存空间。开辟失败则返回null。
   3.free函数:它用于释放指针ptr指向的内存空间。原型为:void free (void* ptr);
   ferr无返回值,ptr指向的是最近一次调用calloc或者malloc函数。
   下面看一个代码。
#include <stdio.h>
#include <stdlib.h>

struct Student  //建立一保存学生信息的结构体
{
char cName[20];
int  iNumber;
struct Student* pNext;   //指向下一个节点的指针
};

int iCount     //全局变量表示链表长度
 

struct Student* Create()   //创建链表函数
{
struct Student* pHead=NULL;  //初始化链表,指向为空。
struct Student* pEnd;    
struct Student* pEew;
iCont=0;   //开始初始化链表长度
pEnd=pEew=(struct Student*)malloc(sizeof(struct Student));
if(pEew==NULL)
{
return 1;
}
printf("请输入姓名,学号\n");
scanf("%s",&pNew->cName);
scanf("%s",&pNew->iNumber);
whlie(pNew->iNumber!=0)    
{
iCount++;
if(iCount==1)
{
pNew->pNEXT=pHead;   //使得指向为空
pEnd=pNew;          //跟踪新加入的节点          
pHead=pNew;         //头指针指向首节点
}
else
{
pNew->pNext=NULL;   //新节点的指针为空
pEnd->pNext=pNew;   //原来的尾巴指向新的节点
pEnd=pNew;          //*pEnd指向新节点
}
pNew=(struct Student*)malloc(sizeof(struct Student)); //再次分配内存空间
if(pNew==NULL)
{
return 1;
}
scanf("%s",&pNew->cName);
scanf("%s",&pNew->iNumber);
}
free(pNew);
pNew=NULL;
retrun pHead;
}

void Print(struct Student* pHead)  //输出节点函数
{
struct Student *pTemp;  //循环所使用的临时指针;
int ilndex=1;     //表示链表中节点的序号
printf("有 %d 名成员\n",iConut);
printf("\n");
pTemp=pHead;  //获取首节点的地址
while(pTemp!=NULL)
{
printf("NO:%d 号成员:\n",ilndex);   //开始输出
printf("姓名:%s\n ",pTemp->cName);
printf("姓名:%s\n ",pTemp->iNumber);
printf("\n");
pTemp=pTemp->pNext;  //临时指针移动到下一节点
ilndex++;  //进行自加运算
}
}

struct Student* lnsert(struct Student* pHead)  //插入节点函数
{
struct Student* pEew;   //指向新分配的空间;
printf("插入信息\n");  //指向新空间
pNew=(struct Student*)malloc(sizeof(struct Student));  //分配空间
if(pNew==NULL)
{
return 1;
}
printf("姓名:%s\n ",pTemp->cName);
printf("姓名:%s\n ",pTemp->iNumber);
pNew->pNext=pHead;   //新节点指针指向原来的首节点
pHead=pNew;  //头指针指向新节点
iCount++;  //增加节点数
retrun pHead  ;
}

void Delete(struct Student* pHead,int ilndex)
{
int i; 
struct Student* pTemp;  //临时指针
struct Student* pPre;  //表示需要删除的前节点。
pTemp=pHead;  //得到头节点
pPre=pTemp;
printf("确认删除NO.%d 成员\n",ilndex);  //提示
for(i=1;i<ilndex;i++)  //利用循环得到需要删除的节点。
{
pPre=pTmep;
pTemp=pTemp->pNext;
}
pPre->pNext=pTemp->pNext;  //连接删除节点两边的节点
free(pTemp);  //释放要删除的节点。
pTemp=NULL;
iCount--;  //减少链表里面的元素。
}

int main({
struct Student* pHead;//定义头节点
pHead=Create();  //创建节点
pHead=insert();  //插入节点
Delete(pHead,2);//删除第二个节点
Print(pHead); //输出链表
return 0;
}

3.共用体(联合体)

  共用体:它使几种不同的数据类型放到同一段内存单元里面。它在某一时刻只可以使用一个值,由于所有类型共用一块内存,所以它的大小是那个类型最大的。
  它的形式为:
union Dis(这是共用体名)
{
int arr;
char arr1;
float arr2;
(它们是成员列表)
}vs(这是共用体变量);
共用体变量的引用:当它定义完成之后就可以引用它里面其中一个值,形式为:共用体变量.成员名。与结构体相似。
共用体的初始化:在定义的同时可以同步进行初始化。注意:对共用体进行初始化只需要对它里面的一个成员赋值就行,其类型必须和共用体的第一个成员的类型一致,如果共用体第一个成员是结构体,则初始化就可以包含多个用于初始化的该结构体的表达式。
共用体的大小计算:共用体的大小至少是最大成员的大小。当最大成员大小不是最大对齐数的整数倍时候,就要对齐到最大对齐数的整数倍。
//共用体的使用
#include <stdio.h>
union data
{
int i;
char c;
};
int main()
{
union data U={97};
printf("i:%d ",union.i);
printf("c:%c ",union.c);
return 0;
}

 注意事项:1.同一内存虽然可以用来存放不同类型的成员,但每次只能存放一种类型数据,而不能同时存放所有类型,也就是,共用体只有一个成员起作用,其他成员不管用。
 2.共用体变量里,起作用的永远是最后存放的成员数据,存入新成员后,原有的成员将会失去作用。
 3.共用体变量与各个成员的地址是一样的
 4.不能对共用体变量名赋值,也不能企图引用变量名来得到一个值。

4.枚举

 枚举:它是一种基本的数据类型,而且不可以给分拆。一个枚举变量包含一组祥光的标识符,每一个标识符都对应一个整数值,称为枚举常量。枚举里面的标识符必须都是每个唯一的,而且不可以当成关键字或当前作用域内其他相同的标识名。
 枚举的定义形式:
enum Day //星期
{
Mon;
Tues;
Wed;
Thur;
Fri;
Sat;
Sun;
};
enum Sex //性别
{
MALE;
FEMAEL;
SECRET;
};
枚举常量中第一个数据对应的0,依次递增(这个值不是固定的,可以修改的但是建议不要。)
它的优点:枚举相比于#define哟拥有很多优势:1.增加了代码的可读性与可维护性
2.枚举有类型检查,更加的严谨。
3.防止命名污染(封装)
4.便于调试
5.使用方便,一次可以定义多个常量。

5.位段

 位段:是一种特殊的结构体类型,其所有的成员长度均已二进制位位单位进行定义,结构体的成员被称为位段,一般定义如下:
struct 结构体名
{
类型 变量名:长度;
........
}
一个位段必须是:int,unsigned int或者signegd int类似的数据。
一些相关说明:1.位段类型和位段变量的定义,以及位段成员的引用,均已结构体相同。
2.位段后面的数字代表的是占多少二进制位的数据。
3.不一定是按照顺序存储的,我们可以更改它存储的顺序。
4.可以让位段占满一个字节,也可以不用占满。
5.一个位段必须存储在一个存储单元(通常位1字节)中,不可以跨字节存放。如果本单元不足以容纳下一个数据,则会跳过去。
6.可以以格式字符输出整数数据。
7.在数值表达式引用位段时,系统会自动提升位整形数。
8.位段是不可以跨平台的,注重移植性应该避免使用位段。
为什么跨平台性差:1.int在别的地方被当初有符号还是无符号这个不确定
2.位段的最大数目不确定(如16位机器最大是16,32最大为32等等)如在16机器运行了32机器的位段,会出大问题。
3.位段的分配顺序未定义。不知道左开始还是右开始
4.当一个结构体包含2个位段,第二个位段成员比较大,无法容纳第一个位段剩余时,是舍弃还是利用也是不确定的。
最后:与结构相比,位段可以达到同样效果,可以很好节省空间,但是有跨平台的问题存在。

一个小小的练习,实现一个通讯录

//通讯录
#include"yxl.h"
//打印菜单
menu()
{
	printf("*****************************************\n");
	printf("********1.添加联系人  2.删除联系人*******\n");
	printf("********3.查找联系人  4.修改联系人*******\n");
	printf("********5.显示联系人  6.清空联系人*******\n");
	printf("********7.排序联系人  0.退出      *******\n");
	printf("*****************************************\n");
}
enum  Option
{
	EXIT,
	ADD,
	DELE,
	SEACH,
	XIUGAI,
	DISPALY,
	DISPEM,
	SORT,
};
int main()
{
	int input;//功能选择
	Conpeo conpeo;//创建通讯录变量
	Initpeo(&conpeo);

	do
	{
		menu();
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case EXIT:
			printf("操作完成,请推出!");
			break;
		case ADD:
			Addpeople(&conpeo);
			break;
		case DELE:
			Delepeo(&conpeo);
			break;
		case SEACH:
			seach_dispalupeo(&conpeo);
			break;
		case XIUGAI:
			Xiugaipeo(&conpeo);
			break;
		case DISPALY:
			Dispalypeo(&conpeo);
			break;
		case DISPEM:
			Dispempeo(&conpeo);
			break;
		case SORT:
			sort_peo(&conpeo);
			break;
		default:
			printf("选择错误,请重新选择!");
			break;
		}
	} while (input);
	return 0;
}
//头文件部分


#pragma once

#include <stdio.h>
#include <assert.h>
#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include<string.h>
#include<stdlib.h>

#define N 1000//s数组中包含的人数
#define NAME 20//名字中的字符个数
#define SEX 10//性别的字符个数
#define TELE 11 //电话号码中的位数
#define DRESS 30//地址中的字符个数

//创建一个结构体,包含一个学生的信息
typedef struct People
{
	/*个人信息姓名、性别、年龄、电话、住址*/
	char name[NAME];
	char sex[SEX];
	int age;
	char tele[TELE];
	char strdess[DRESS];
}Peo;

//创建一个通讯录结构提,用包含学生数组和人数
typedef struct contact
{
	Peo arrpeo[N];
	int num;
}Conpeo;

//初始化联系人
void Initpeo(Conpeo* str);

//添加联系人信息
void Addpeople(Conpeo* str);

//显示所有联系人信息
void Dispalypeo(Conpeo* str);

//删除联系人信息
void Delepeo(Conpeo* str);

//修改指定联系人信息
void Xiugaipeo(Conpeo* str);

//查找并显示指定联系人的信息
void seach_dispalupeo(Conpeo* str);

//清空所有联系人
void Dispempeo(Conpeo* str);

//按姓名排序联系人
void sort_peo(Conpeo* str);

//实现
#define _CRT_SECURE_NO_WARNINGS 1


#include"yxl.h"
/*函数功能的实现*/

//初始化
void Initpeo(Conpeo* str)
{
	str->num = 0;//个数为0
	//对结构体内的数据进行初始化时,用memset函数,因为数组的大小是不知道的
	memset(str->arrpeo, 0, sizeof(str->arrpeo));
}

//添加联系人信息
void Addpeople(Conpeo* str)
{
	//联系人已满
	if (str->num == N)
	{
		printf("通讯录已满,不能增加");
		return;
	}
	//联系人未满
	else
	{
		printf("请输入联系人姓名:");
		scanf("%s", str->arrpeo[str->num].name);
		printf("请输入联系人性别:");
		scanf("%s", str->arrpeo[str->num].sex);
		printf("请输入联系人年龄:");
		scanf("%d", &(str->arrpeo[str->num].age));
		printf("请输入联系人电话:");
		scanf("%s", str->arrpeo[str->num].tele);
		printf("请输入联系人地址:");
		scanf("%s", str->arrpeo[str->num].strdess);
	}
	str->num++;
}

//显示联系人信息
void Dispalypeo(Conpeo* str)
{
	//列表为空时
	if (str->num == 0)
	{
		printf("列表为空,没有联系人!\n");
	}
	else
	{
		printf("%-20s %-5s %-5s %-12s %-20s\n", "姓名", "性别", "年龄", "电话", "地址");
		for (int i = 0; i < str->num; i++)
		{
			printf("%-20s %-5s %-5d %-12s %-20s\n", str->arrpeo[i].name, str->arrpeo[i].sex, str->arrpeo[i].age, str->arrpeo[i].tele, str->arrpeo[i].strdess);
		}
	}
}

//查找指定联系人
int Seachpeo(Conpeo* str, char name[])
{

	//没有联系人
	if (str->num == 0)
	{
		printf("列表为空,没有联系人\n");
		return -1;
	}
	else
	{
		for (int i = 0; i < str->num; i++)
		{
			//字符串的比较
			if (strcmp(str->arrpeo[i].name, name) == 0)
			{
				//输出所查找的联系人,所在下标
				return i;
			}
		}
		return -1;
	}
}

//查找并显示指定联系人的信息
void seach_dispalupeo(Conpeo* str)
{
	char name[NAME] = { 0 };
	printf("请输入需要查找的联系人姓名:");
	scanf("%s", name);
	if (str->num == 0)
	{
		printf("联系人列表是空的。\n");
	}
	int pos = Seachpeo(str, name);
	if (pos == -1)
	{
		printf("该联系人不存在!\n");
	}
	else
	{
		printf("查找的联系人,信息如下:\n");
		printf("%-20s %-5s %-5s %-12s %-20s\n", "姓名", "性别", "年龄", "电话", "地址");
		printf("%-20s %-5s %-5d %-12s %-20s\n", str->arrpeo[pos].name, str->arrpeo[pos].sex, str->arrpeo[pos].age, str->arrpeo[pos].tele, str->arrpeo[pos].strdess);
	}
}

//删除联系人信息
void Delepeo(Conpeo* str)
{
	//没有联系人
	if (str->num == 0)
	{
		printf("联系人列表为空,无法删除。\n");
		return;
	}
	//有联系人
	else
	{
		printf("请输入要删除人的姓名。\n");
		char name[NAME] = { 0 };
		scanf("%s", name);
		if (Seachpeo(str, name) == -1)
		{
			printf("该联系人不存在\n");
		}
		else
		{
			for (int i = Seachpeo(str, name); i < str->num; i++)
			{
				//删除的该联系人,从左向右,后面的数据往前移
				str->arrpeo[i] = str->arrpeo[i + 1];
			}
			printf("该联系人已删除\n");
			str->num--;
		}
	}
}

//修改指定联系人信息
void Xiugaipeo(Conpeo* str)
{

	//没有联系人
	if (str->num == 0)
	{
		printf("没有联系人\n");
		return;
	}
	else
	{
		char name[NAME];
		printf("请输入要修改的联系人姓名\n");
		scanf("%s", name);
		int pos = Seachpeo(str, name);
		if (pos == -1)
		{
			printf("该联系人不存在\n");
			return;
		}
		//修改联系人的信息
		printf("请输入修改后的联系人信息:\n");
		printf("请输入姓名:\n");
		scanf("%s", str->arrpeo[pos].name);
		printf("请输入性别:\n");
		scanf("%s", str->arrpeo[pos].sex);
		printf("请输入年龄:\n");
		scanf("%d", &str->arrpeo[pos].age);
		printf("请输入电话:\n");
		scanf("%s", str->arrpeo[pos].tele);
		printf("请输入地址:\n");
		scanf("%s", str->arrpeo[pos].strdess);
		printf("修改成功!\n");
	}
}

//清空所有联系人
void Dispempeo(Conpeo* str)
{
	str->num = 0;
	printf("已经清空联系人列表,现在该列表为空!\n");
}

//按姓名排序联系人
void sort_peo(Conpeo* str)
{
	int i = 0;
	int j = 0;

	for (i = 0; i < str->num; i++)
	{
		for (j = 0; j < str->num - 1 - i; j++)
		{
			if (strcmp(str->arrpeo[j].name, str->arrpeo[j + 1].name) > 0)
			{
				Peo tem = str->arrpeo[j];
				str->arrpeo[j] = str->arrpeo[j + 1];
				str->arrpeo[j + 1] = tem;
			}
		}
	}
	printf("排序成功!\n");
}

十一.存储管理

1.内存

 数据的存放方式:计算机程序一般经过4个逻辑段:1.可执行代码段.2.静态数据。3.动态数据(堆)和栈。
 1.可执行代码和静态数据存储在固定位置。
 2.动态数据需要系统动态分配内存,一般存放在堆区的内存池。
 3.局部数据对象,函数参数,以及调用函数和被调用函数的连续,存放在栈区的内存池。
堆区与栈区:
1.堆:内存的全局存储空间里,程序可以动态分配和释放的内存块被称为自由存储空间,也称为堆、
c程序里面:使用malloc与free函数来进行分配与释放动态内存。
2.栈:程序不会像处理堆那样,在栈中显示的分配内存。。当程序调用函数或者声明局部变量时,系统会自动分配内存。栈是一个后进先出的压入弹出式的数据结构。

2.动态管理

1.malloc函数:是在内存里面动态分配一块size大小的内存空间。使用它时,要包含头文件,stdlib.h。分配成功它会返回指向开好空间的指针,如开辟失败则返回null。
2.calloc函数:它的功能时在内存里面分配n个长度为size的连续内存空间。开辟成功,它会返回指向这块连续内存空间的首地址,开辟失败则返回null.
3.realloc函数:它将ptr指针指向的内存空间大小改为size,它的返回值是指向新地址的指针,如果出现错误,则返回null;
4.free函数:它的功能是释放指针ptr指向的内存区域,使的该内存区域能被其他变量所使用。它无返回值。
以上函数,它们的原型如下:
//malloc
void* malloc(unsigned int size);
//使用它分配一块内存空间
int* ptr=(int*)malloc(sizeof(int)); //这里需要强制转换是因为它返回了的空间是void*类型,使用的时候需要把这块空间强转成对应的类型。
if(ptr==NULL) //申请之后应该进行判断看看它有没有成功分配内存,如果开辟失败还继续执行后面代码,那代码就会崩溃。
{
    return 1;//如果内存开辟失败,则不执行后面的代码,开辟成功则继续执行。
}
//calloc函数
void* calloc(unsigned n,unsigned size);//原型
//使用它分配一块连续的内存空间
int* ptr=(int*)calloc(3,sizeof(int));
if(ptr==NULL)  //进行检查保证安全性
{
return 1;
}

//realloc函数
void* realloc(void* ptr,size_t size);
//使用
double* ptr=(double*)malloc(sizeof(double));
if(ptr==NULL)   //进行检查保证安全性
{
return 1;
}
int* rrr=realloc(ptr,sizepf(int))
if(rrr==NULL)   //进行检查保证安全性
{
return 1;
}  //把上面的浮点型空间改为整形空间

//free函数
void* free(void* ptr); //原型,它的使用也很简单。
free(ptr); //里面这个ptr指的是最近一次调用内存开辟函数。
ptr==NULL; //使用完之后,为了保证指针不乱跑,最好就把它置为空;
//

3.内存泄漏与丢失

内存泄漏问题:在使用完动态开辟没有及时将其释放就叫内存泄漏,内存泄漏问题,严重的话还可能导致系统崩溃。free函数是实时回收动态内存的,在程序结束后系统也会自动回收,但是在大型项目里面,如果没有及时的释放,后果会很严重,就假如,在程序中里面可能需要分别1w次的动态内存,就算每次分配的很少,问题就是每次都没释放呢?会极大的造成内存的损失,影响到系统的性能,严重的话就可能导致整个项目的崩溃。
所以,每次使用完动态内存之后都应该及时释放掉。当再次需要的时就重新申请。
内存丢失问题:所谓内存丢失就是一个需要使用的那块内存以及被释放了。下面可以看看内存丢失的例子;
old=(int*)malloc(sizeof(int));  
new=(int*)malloc(sizeof(int));
//这里创建了2块内存,下面进行这样的操作
old=new;
free(old);
//选择old指向的空间时原来的new空间指向的,于是这块空间被释放了,但是old原来的空间没有被释放,(因为已经没有人找的到那块空间了),所以这块空间就造成了丢失。
常见的动态内存错误:
1.对NULL指针进行解引用。
2.对动态内存开辟的空间进行越界访问
3.对非动态内存进行free释放
4.使用free释放一块动态内存的一部分。
5.对同一块内存进行多次释放
6.动态内存忘记进行释放(内存泄漏)

4.柔性数组:

  柔性数组;它是c99标准里面定义的,它允许结构中最后一个元素是未知数,这就叫柔性数组成员。例如:typedef struct st_type
  {
  int i;
  int a[0]  //柔性数组成员;
  }type_a;
  第二种形式
  typedef struct st_type
  {
  int i;
  int a[]  //柔性数组成员;
  }type_a;
  特点:1.结构中的柔性数组成员前必须有一个其他成员
  2.sizeof返回这种结构的大小不包括柔性数组的内存。
  3.包含柔性数组成员的结构用malloc()函数进行开辟动态内存分配,应该大于结构体的大小,以使用柔性数组的预期大小。

柔性数组的使用

//1.
  typedef struct st_type
      {
      int i;
      int a[]  //柔性数组成员;
      }type_a;
int i=0;
type_a *p=(type_a*)malloc(sizeof(type_a)+100*sizeof(int))
if(type_a *p==NULL)
{
 retrun 1;
}
p->i=100;
for(i=0;i<100;i++)
{
p->a[i]=i;
}
free(p);
p=NULL;

柔性数组的优势:1.方便内存释放:如果我们的代码是给另一个人使用的函数里面,你在里面做了二次分配,并把整个结构体返回用户,用户调用free可以释放结构体,但是用户不知道这个结构体里面的成员也需要free,所以你不能指望用户会帮你发现这个问题,因此,我们需要把结构体内存以及其成员要的内存一次性分配好,并给用户返沪一个结构体指针,这样用户做一个free就可以了。
2.有利于访问速度:连续的内存有利于提高访问速度,减少内存碎片。

十二.文件

1.文件的概述

文件是一组相关的数据的有序集合,这个数据集有个名字叫文件。
通常情况下,使用计算机主要就是在使用文件,输入与输出都是从标准输入设备输入,输出都是由标准输出设备输出。
所有的文件都是以流的形式进行输入与输出,与文本流和二进制流对应,文件可以分为文本文件与二进制文件。
按文件内容看,可以分为源文件(.c),目标文件(win环境.obj),可执行文件(win环境.exe),头文件和数据文件等等。
按用户视角来看,又可以分为:普通文件:留在磁盘或其他外部介质的一个有序数据集。
设备文件:指与主机相连的各种外部设备,设备也可以看成一个文件进行管理,把他们输入与输出等同于对文件的读和写。
文件名:它是文件的标识,便于用户识别与发现
包含3部分:文件路径+文件名主干+文件后缀
例如:c:\cfio\test.txt

2.文件的基本操作:

文件指针:它是一个指向文件有关信息,这些信息包括文件名,状态和当前位置,保证在一个结构体变量里面。在c语言里面,该名称是FILE型。
它声明为:
typedef struct
{
short level;
unsigned flags;
char fd;
unsigned char hold;
short bsize;
unsigned char *buffer;
unsigned ar *curp;
unsigned istemp;
short token;
}FILE;

上面使用typedef定义了一个FILE类型的结构体,在编写程序可以直接使用FILE类型来定义变量,但定义变量不用将里面的内容全部给出,只需要写成二如下形式:
不可以通过定义FILE类型变量来操作文件,FILE型数据对象位置都是由库函数确定,c语言里面只可以FILE指针来操作文件。

FILE* fp;

文件的打开:

 fopen函数:它可以用来打开一个文件,打开文件的操作其实就创建一个流。它的原型就定义在stdio.h里面。如果打开形式失败则会返回一个null。调用形式为:
FILE* fp;
fp=fopen(文件名,使用文件的方式);

在这里插入图片描述

例如:以只读文件的形式打开一个文件;

FILE *fp;
fp=fopen("123.txt","r";  //每次打开文件时判读下有没有打开成功。
if(fp==NULL)
{
printf("打开失败")
return 1;
}

打开失败的原因:1.指定的盘符不存在或路径不存在。
2.文件名含有无效字符
3.以r的形式打开一个不存在的文件。

文件的关闭

  在 文件使用完毕后,应该使用文件关闭函数:fclose进行关闭。它的原型也在stdio.h里面。调用形式为:
fclose(文件指针)
if(文件指针!=0//判断文件是否关闭成功
{
printf("文件关闭失败";//这里还有上面最好使用错误码返回的形式。
}

正常的文件关闭之后,这个函数会返回0;否则就返回eof。
在程序结束之前,应该关闭所有文件,以防造成数据损失。

3.文件读写操作

打开文件之后就可以对文件进行读写操作。c语言提供了很多文件操作函数。

1.fputc函数:用于把一个字符写入到文件里面。它的形式为:

ch=fputc(ch(需要写入的字符),fp(文件指针));//ch是需要写入的字符,fp是文件指针变量,如果写入成功则返回写入的字符,失败则返回EOF;

2.fgetc函数:用于从指定文件(文件指针指向的文件)中读取一个字符赋予ch(某个变量);其形式为:

ch=fgets(fp);
这里需要注意的是:文件必须是以只读或者读写的形式打开。遇到函数结束时,会返回文件结束标志eof。
 
 fgets与gets的区别:后者函数读完之后回车换行,并不存储它,前者函数存储换行符并在其后写一个null character(‘\0’).不可以简单的把fgets函数理解为gets函数限制字符数目的stdin加强版。在改用fgest函数之后,应该注意把字符串末尾的\0删去。

3.fputs函数:它与fputc函数类似,用于向指定的文件写入一个字符串。其形式为:

fputs(字符串,文件指针);//这里可以时字符串常量,字符数组名,指针或者变量

4.fgets函数:它与fgetc类似,用于从指定文件里面读取一个字符串到一个字符数组里面 形式为:

fgets(字符数组名,n,文件指针); //这里的n代表的时需要读取多少个字符(包括\0)

5.fprintf函数:它与前面的printf函数类似,都是属于格式化读写函数,不同的时,前者用于终端,后者用于磁盘文件。一般形式为:

ch=fprintf(文件指针,格式字符(%d,%s等等),输出列表(需要输出的字符等等))

6.fscanf函数:它与前面的scanff函数类似,都是属于格式化读写函数。它的形式为:

fscanf(文件指针,格式字符,输入列表)

7.fread与fwrite函数:它们实现的时对整块进行读写的功能函数。
fread函数的功能:从文件指针指向的文件里面读取count次,每次读取size字节的内容,读取的信息保存到buffer地址里面。其形式为:

fread(buffer,size,conut,fp);

fwrite函数功能:将buffer地址开始的信息输出count次,每次写size字节到文件指针指向的文件里面。

fwrite(buffer,size,count,fp);

1.buffer:一个指针;对于fwrite来说,是待输出数据的地址(起始地址),对于fread函数来说,它是待读取数据存放的地址。
2.size:需要读取的字节数
3.count:需要读多少个size字节的数据项
4.fp:文件指针。
例如:从fp指向的文件中读取2个字节保存在数组a里面,连读3次。

fread(a,23,fp);

4.文件的定位:

1.fseek函数:它是移动文件内部的位置指针。一般形式为:

fseek(文件指针,位移量,起始点);

位移量是指需要移动的字节数,一般为long型数据。
起始点:从何处开始计算位移量。一般文件首,文件当前位置,文件尾。
文件首:SEEK—SET
文件当前位置:SEEK-----CUR
文件尾:SEEK—END
例如:将指针从当前位置向后退20字节;

fseek(fp,-20l1

注意:fseek一般用于二进制文件,在文本文件中使用,由于需要使用转换,计算位置会出现偏差。
文件的随机读写在移动指针之后进行,然后就可以使用任意的读写函数进行读写。
rewind函数:它用于将位置指针返回到文件开头,没有返回值;形式为:

int rewind(文件指针);

ftell函数:它可以得到流式文件中的当前位置,并使用相对于文件头的位移量表示,一般形式为:
当这个函数的返回值为-1时,表示这个函数出错。

long ftell(文件指针)
 文本文件:要求以ASCII码的形式存储,则需要在存储前转换。以ASCLL码形式存储的文件就叫文本文件
 二进制文件:数据在内存里面以二进制形式存储,如果不加以转换输出到外存,那么它就是二进制文件。
 文件读取结束判定:
 被错误使用feof函数:正在文件读取的过程里面,不可以直接使用feof函数的返回值来判断文件是否结束。而是应用于当文件读取结束的时候,判断是读取失败还是遇到文件末尾结束。
 1.文本文件是否读取结束:判断的返回值是否为EOF(fgetc),或者null(fgets)
 2.二进制文件:它的读取判断是根据判断返回值是否小于实际要读的个数
 文件缓冲区概念:ANSIC标准采用的是缓冲文件系统处理数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟出一块文件缓冲区,从内存向磁盘输出数据会先送到内存的缓冲区里,装满缓冲区后才一起送到磁盘上。如果磁盘向计算机读入数据,则从磁盘文件里面读取数据输入到内存缓冲区,然后才从缓冲区里面逐个送到程序数据区。缓冲区的大小由编译系统决定。
//文本文件结束判断
#include <stdio.h>
#include <stdlib.h>
int main()
{    
int c;   // 注意:int,非char,要求处理EOF   
 FILE*fp=fopen("test.txt", "r");    
 if(!fp) 
 {      
   perror("File opening failed");       
     returnEXIT_FAILURE;   
  } //fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF    
       while ((c=fgetc(fp)) !=EOF) // 标准C I/O读取文件循环   
  {     
     putchar(c);   
  }
  //判断是什么原因结束的    
  if (ferror(fp))        
    puts("I/O error when reading");    
  else if (feof(fp))
  puts("End of file reached successfully");
  fclose(fp);
  }
//二进制结束文件
#include <stdio.h>
enum { SIZE=5 };
int main()
{   
 doublea[SIZE] = {1.,2.,3.,4.,5.};   
 FILE* fp=fopen("test.bin", "wb"); // 必须用二进制模式   
  fwrite(a, sizeof*a, SIZE, fp); // 写 double 的数组   
   fclose(fp);    
   doubleb[SIZE];   
   fp=fopen("test.bin","rb");    
   size_tret_code=fread(b, sizeof*b, SIZE, fp); // 读 double 的数组   
    if(ret_code==SIZE) 
    {        
    puts("Array read successfully, contents: ");       
     for(intn=0; n<SIZE; ++n) printf("%f ", b[n]);      
       putchar('\n');   
       } 
       else            //进行判断
       {     
       if (feof(fp))          
       printf("Error reading test.bin: unexpected end of file\n");     
       }  
       else if (ferror(fp))
        {          
         perror("Error reading test.bin");       
        }
        fclose(fp);   
        }   
//文件缓冲区存在示例代码
#include <stdio.h>
#include <windows.h>
int main()
{
 FILE* pf=fopen("toxxst.txt", "w"); 
fputs("123456f", pf);//先将代码放在输出缓冲区 
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n"); 
Sleep(10000); 
printf("刷新缓冲区\n"); 
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘) 
//注:fflush 在高版本的VS上不能使用了 
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n"); 
Sleep(10000); 
fclose(pf); //注:fclose在关闭文件的时候,也会刷新缓冲区
 pf=NULL;
  return0;
 }   

十三.预处理

1.程序的翻译环境与执行环境

  在ANSL C的任何一种实现,都存在2种不同的环境:1.翻译环境,在这里,我们所编辑的代码将会转换成可执行的机器指令。2.执行环境:它用于实际执行代码。

2.编译与链接

 1.翻译环境:1.组成一个程序的每个源文件都会通过编译器编译过程转换未目标代码
 2.每个源文件都会通过链接器(Linker)捆绑在一起,形成一个完整且单一的可执行程序.
 3.链接器同时也会引入标准库里面的任何被程序使用到的函数,而且它可以搜索程序员个人定义的程序库,将需要的函数也引入到程序里面。
 步骤:1.源文件进行预编译与预处理状态。
 2.进行编译(按照规定进行:语法分析,词法分析,语义分析,符号汇总)
 3.进行汇编(生成重定位目标文件.o,形成符号表。)
 4.链接:合并段表,符号合并于重定位。

3.运行环境问题

程序在执行过程中:1.程序必须载入内存,在有操作 系统的环境里面:一般这个动作由操作系统完成,在独立的环境里面,程序的载入必须手工安排,也可能是通过可执行代码置入只读内存里面。
2.程序执行开始,接着调用main函数。
3.开始执行代码。这个时候程序就会使用一个运行堆栈,存储函数的局部变量和返回地址。程序同时使用静态内存,存储在静态内存的变量在程序整个执行过程一直保留它们的值。
4.终止程序。正常终止main函数,也可能意外终止。

4.预定义符号概述

_ FILE _:进行编译的源文件
_LINE _: 当前文件行号
_ DETE _: 文件被编译的日期
_ TIME _: 文件被编译的时间
_ STDC _: 如果编译器遵循标准c规则,其值为1;否则是未定义;
例子:

printf("file:%s line: %d\n",_FILE_,_LINE_);

5.#define与宏定义

1.定义标识符:形式为:

#define name stuff

这里需要注意一点就是:利用#define定义的常量或标识符时候,最好不要带上;。

  宏定义:是预处理命令的一种,它提供了一种可以替换源代码里面的字符串机制。
  注意:c语言没有规定#define必须写在函数外面,只是规定这条命令必须独占一个完整的逻辑行,起作用的范围就是#define出现的位置到源文件执行结束,或找到相应的#undef指令。如果需要#define只某个函数有效,完全可以写在函数里面。

1.不带参数的宏定义:形式为
#define 宏名 字符串
1.#代表这是一条预处理指令
2.宏名是一个标识符,必须符合c语言对应标识符的限定。宏名需要简单且意义明确,一般习惯为都采用大写字母表示,以区别变量名。
3.字符串可以是常数,或者表达式,格式字符串等等。
4.在字符串出现宏名的情况下是不进行替换的,如:
5.如果字符串多于一行,可以使用|进行续行。
6.#define命令出现在函数外,则它的有效范围就会定义命名之后到源文件结束。
7.可以使用#undef结束一个#define命令。
8.宏定义是一种预处理命令,不同于变量的定义,它只做字符替换,而不分配内存空间。
9.宏不可以进行调试与递归。
10.宏的参数与类型无关,只要对参数操作是合法的就可以适用于任意类型。
下面看看一些简单宏定义形式

#include <stdio.h>
#define PI  3.14159  //这是以常量作为数值。
#define SIDE 5   //边长
#define PERIMETER 4*SIDE  //正方形周长计算
#define ARAE  SIDE*SIDE  //正方形面积计算
#define STANDARD "ccccccccc" //相比于数字,计算公式,宏还可以使用字符串的形式。
#define 
#define
#define

int main()
{
char [50]="STANDARD is you STANDARD";  //这种情况宏是不会产生替换的。     
printf(STANDARD);
#undef STANDARD;   //这里结束后,前面那个宏定义就会失效。
}
带有参数的宏定义:它不仅仅完成字符串替换,还要完成参数替换。其形式为:
#define 宏名(参数名) 字符串
下面看一个简单的例子:
#include <stdio.h>
#define MAX(a,b) ((a)*(b)+(b))  //这里带括号为了保证计算顺序不会搞错,因为宏的本质就是替换。

int main()
{
int x=5,y=9
printf("x,y:%d %d\n",x,y);
printf("%d",MAX()x,y);//调用换这个宏,里面的数据就会变成:((a)*(b)+(b)) 就会按照这个方式进行计算。
return 0;
}

通过上面知道:在一些简单的例子使用宏,比使用函数要方便的多,但是它不是没有缺点,在过复杂的计算里面,使用宏将会多出很多不必要的支出。
注意事项:1.宏定义时,参数需要带括号,如果不加括号可能会导致运算顺序的失调,增加了结果的不确定性。
2.宏扩展必须使用括号来保护表达式中优先级低的操作数,以保证结果能完美。
3.对带参数的宏进行展开,只是使用宏名括号内的实参字符串代替#define命令行的形参。
4.宏定义时,宏名于带参数的括号之间不要加空格,否则会将空格后面的字符都作为替代字符串的一部分。
5.在带参宏定义里面,形式参数不分配内存单元,因此可以不用做类型定义。

6.#include与头文件

#include命名:它的用法大家都熟悉,这里就不写了:
对于它来说:()与“”是有一定区别的。():它是系统的标准方式,在预处理会先从系统提高库目录去寻找需要的文件内容,其次才是去找自定义文件。
“”:它就会先去寻找用户目录(自定义文件)寻找需要的文件,然后再是系统目录里面寻找。

 :经常使用文件头位置被包含的文件叫做头文件。一般为.h后缀。一般情况我们可以把以下内容放到头文件里面:
 1.宏定义。
 2.结构体,联合,枚举声明。
 3.typedef(重命名)声明。
 4.外部函数声明。
 5.全局变量声明。
 注意:1.一个#include只能包含一个头文件。
 2.文件是可以嵌套的,即在一个被包含的头文件里面可以包含另一个头文件。
 3.age.c里面包含了头文件age.h,则预编译之后它们会合成一个文件。如果在age.h中有全局静态变量,则该全局静态变量在在age.c里面也有效,这时不需要extern声明。

7.条件编译

#if命令:如果它参数表达式为真,则编译#if到#endif之间的程序段,否则跳过这段程序·.
 #else的形式:它的作用是为#if为假时提供的另一种选择,作用与之前普通else 相近。
 #elif命令:用来建立如果,或者如果这样阶梯式的操作选择,它与else if差不多。
 #ifdef命令以及#ifndef命令:在if条件编译里面,需要判断符合常量具体的值,但有时候并不需要判断具体值,只需要判断这个符号常量是否给定义,这时候就可以采用#ifdef以及#ifndef,它们的含义是:如果有定义和如果无定义.
  #undef命令:可以事先删除 定义好的宏。
  #line命令:它用于显示_LINE_与_FINE_的内容。
  #pragma命令:它用于设定编译器状态,或指示编译器完成一些特定状态。
  1.message参数:在编译器信息窗口输出相应信息;
  2.code_seg参数:设置程序里面的函数代码存放的代码段。
  3.once参数:保证头文件只被编译一次。

它们的形式为:

//#if与#endif,#else
#define NUM 500;
int i=10;
#if NUM>50  //如果满足条件则执行i++;
i++;
#elif NUM==50  //或者如果满足。
i+10;
 #else   //不满足执行i--;
 i--;
 #endif  //表示使用结束;
 
//#ifdef与#ifndef
#difine SCR "神零"
#ifdef SCR  //如果前面已被定义则输出信息。
printf(SCR);
#ifndef CCC //如果没被定义,则输出下面语句段
printf(CCC);
#endif  //表示结束。

//#nudef
#define MAX 1000
#undef MAX  //这遇到这个命令之前,宏都是一值有效的。

//#line
#line 行号["文件名"]
//这里的行号可以是任意正整数,可选任意有效的文件标识符,行号为源程序当前的行号,文件名为当前源文件名。如:
#line 100 "13.9.c"
#include <stdi.h>
int main()
{
printf("当前行号为:%的\n",_LINE_);
return 0;
}

//#pragma 
#pragma once
#pragma message
#pragma code_seg

一些小细节问题

函数的栈帧问题:函数的栈帧的创建与销毁
main函数也是给别的函数调用的。
函数栈帧的创立与销毁在每一个编译器的逻辑是相同的,但是大体上回略有不同,因为越高级的编译器对与栈的封装就越严谨。
理解函数的栈帧就需要理解ebp与esp这2个寄存器的概念
里面存放的地址是用来维护函数栈帧的
ebp:通常这个寄存器的名字 :栈底指针
esp: 通常这个寄存器的名字 : 栈顶指针
以上的2个寄存器工作状态就是当一个函数在运行的时候对该函数栈帧进行维护的,也就是说
这2个寄存器维护的是在运行(处于调用)状态的栈帧。
指向方式:由低地址在高地址上面压栈。就是由高到低。
压栈:给栈顶放一个元素(push)汇编指令
出栈:取走栈顶的一个元素(pop)汇编指令
1.局部变量是怎么创建的
局部变量的创建就是在开辟的栈帧空间中进行创建,以压栈的形式创建。
2.为什么局部变量的值是随机值
因为在创建栈帧的时候会进行随机设置,没有赋值的情况下会全部进行随机化。
3.函数是怎么传参的,顺序又是什么
传参在内存就是先进行内存块(main函数中就已经进行拷贝)的创建(压栈)临时变量(形参)
4.形参和实参的关系是什么
形参在销毁的时候不会影响到实参,因为它就是实参的一份拷贝。
5.函数调用是怎么做的
先进行内存的申请,就是利用栈顶与栈底指针进行栈帧的维护,然后进行调用。
6.函数调用结束后怎么返回的
把需要返回的值存放到一个寄存器中,然后在函数栈帧的销毁的时候不会影响到返回值,因为它已经存到了寄存器区域了。

数组名传的都是首地址,但是有2个例外一个是sizof与&地址的情况列外,它们都是代表整个数组。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值