C语言基础知识(2)

C语言基础知识(2)

1. 常用的基础函数

1.1 字符输入输出函数

输入:getchar(); 输入一个字符
输出:putchar(); 输出一个字符

#include <stdio.h>

int main()
{
	char a;
	a = getchar();//函数给变量赋值,将输入的一个字符放到变量a中
	//等价于scanf("%c", &a);
	putchar(a); //输出a中的字符,一次只能输出一个字符
	//等价于printf("%c", a);
	putchar('\n');
	return 0;
}

在这里插入图片描述

遍历字符串,以 ‘\0’ 作为字符串的结尾

#include <stdio.h>

int main()
{
	char a[100] = "hello";// 使用字符串常量初始化数组a
	int i = 0;
	// 遍历字符串
	while(a[i] != '\0')
	{
		putchar(a[i]);
		i++ ;
	}
	putchar('\n');
	
	//或者
	for(i = 0; a[i] != '\0'; i++)
	{
		putchar(a[i]);	
	}
	putchar('\n');
	retrun 0;
}

1.2 字符串的输入输出函数

gets的参数是数组名
puts的参数是数组名或者字符串常量
%s是字符串的格式符
get string 输入: gets(); 允许输入空格,输入"xiao ming" 得到"xiao ming"; 相当于scanf(“%s”); 不允许输入空格,输入"xiao ming" 得到"xiao"
put string 输出:puts(); 默认会输出\n; 相当于printf(“%s”);自己写\n才会换行
在这里插入图片描述
计算字符串长度。char s[] = “Struggle for a better future”;
字符串长度不算\0,所以字符串长度为28

1.3 随机函数

计算机的随机都是假随机,所有的随机数都是根据一套算法计算出的看似随机的数

#include <stdlib.h>
// std standard标准 lib library 库 stdlib头文件中声明的内容很杂
#include <time.h>
srand(); //seed random 设置随机数的种子数 rand 生成的所有的随机数都是根据种子数计算出来的
// 没有返回值,有整型参数,就是作为种子数
rand();// random 生成一个随机数 没有参数,返回值是一个随机数
time();// time(0) 参数0是固定的,得到的是从1970年1月1日0:0:0到现在的秒数
在这里插入图片描述
-10~10 闭区间 区间内有21个数
rand()%21 得0~20
rand()%21-10 -10~10 之间的数
计算随机数
rand()%范围内数字的个数 + 范围内的最小值 可以得到你想要的范围内的随机数

例子:编写一个彩票机制,在1-35之间不能有重复数,输出最终中奖结果。每个人可以选择7个数;选中7个数,可以中1千万;选中6个数,可以中1百万;选中5个数可以中10万,选中4个数可以中100块钱,选中3位数可以中10块钱。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main ()
{
	srand(time(0));//设置随机的种子数
	int i,j;
	int user[7];
	int system[7];
	
	for(i = 0; i++ ; i < 7)
	{
		scanf("%d"&user[i]);
		if(user[i] < 1 || user[i] > 35 )
		{
		printf("超出范围\n");
		i--;
		continue;//使用 continue 语句跳过当前循环,直接进入下一次循环
		}
		for((j = 0;j < i;j++)
		{
			if(user[i] == user[j])
			{
				i--;
				printf("输入重复\n");
				break;
			}
		}
	}
	for(i = 0;i < 7;i++)
	{
		system[i] = rand()%35+1;
		for(j = 0;j < i;j++)
		{
			if(system[j] == system[i])
			{
				i--;
				break;
			}
		}

	}

	int count = 0;
	for(i = 0;i < 7;i++) //遍历user
	{
		for(j = 0;j < 7;j++){//遍历system

			if(user[i] == system[j])
			{	
				count++;
				break;//不写break;也对
			}
		}
	}

	switch(count)
	{
	case 7:
		printf("1 0000 0000\n");
		break;
	case 6:
		printf("10 0000\n");
		break;
	case 5:
		printf("1000\n");
		break;
	case 4:
		printf("100\n");
		break;
	case 3:
		printf("10\n");
		break;
	default:
		printf("谢谢参与\n");
		break;
	}

	//打印开奖数字
	for(i = 0;i < 7;i++)
	{
		printf("%d ",system[i]);
	}
	printf("\n");
	return 0;
}

2. 冒泡排序

为什么需要排序?排序方便我们分析数据。
分析: int a[7] = {85,72,65,79,53,95,87};

以升序排序为例:比较数组中所有相邻的两个数,前者大于后者则交换两个数。反复执行这个过程实现排序。【冒泡排序的原理】

  1. 冒一个泡,需要比较N-1次,N是数据的数量
    在这里插入图片描述
    由此可以看出,冒一次泡,可能无法完全排序完成
  2. 所以要进行N-1轮冒泡完成排序
    在这里插入图片描述
#include <stdio.h>

int main()
{
	int a[7] = {85,72,65,79,53,95,87};
	int i,j;
	// 冒泡,将最大的数放在最后,需要循环 7-1(N-1)轮冒泡完成排序
	for(j = 0; j < 7-1 ; j++)
	{
		int flag = 0;//表示在某一轮冒泡中没有发生过交换
		for(i = 0;i < 7-1-j; i++) //内层循环负责冒一次泡,-j的目的是为了比上一轮少比较一次
		{
			if(a[i] > a[i + 1])
			{
				int temp = a[i];
				a[i] = a[i+1];
				a[i+1] = temp;
				flag = 1; //一旦发生交换,flag值会变成1
			}
		}
		if(flag == 0) //在每一轮冒泡完成之后判断是否发生过交换,如果没有发生交换,说明数组已经有序,停止外层循环
		{
			break;
		}
	}
	//查看冒泡排序后的结果
	for(j = 0; j < 7;j++)
	{
		printf("%d ",a[j]);
	}
	printf("\n");
	return 0;
}

优化后的代码,用户在终端输入要排序的数组即可

#include <stdio.h>
#define MAX_SIZE 100

int mian()
{
	int a[MAX_SIZE];
	int i,j,n;
	printf(""Enter the number of elements:");
	scanf("%d",&n);
	for(i = 0; i < n; i++)
	{
		printf("Enter %d elements:\n", n);
		scanf("%d",&a[i]);
	}
	for(i = 0; i < n-1; i++)
	{
		int flag =0}
	return 0;
}

3. 二维数组

  1. 本质:在C语言中,二维数组是一种特殊的一维数组,它的每个元素也是一个一维数组。
  2. 使用二维数组的目的:批量的定义类型相同,逻辑相似的一维数组,就定义二维数组。
  1. int a[2][3];
    a 是二维数组的名称
    2 是数组a中有两个元素,即数组a中有两个一维数组
    3 是作为a的元素的一维数组的元素个数
    int 作为a的元素的一维数组的元素类型
    在这里插入图片描述
//不能直接给a[0]赋值,因为a[0]是一个一维数组,C语言没有给一维数组直接赋值的运算符
a[0][0] = 10;// 给a[0]数组的0元素赋值
a[1][2] = 10;// 给a[1]数组的2元素赋值

由于二维数组的每个元素都有两个角标,所以一般使用双重for循环遍历,一般使用外层循环的循环变量作为第一个角标,内层循环的循环变量作为第二个角标。

#include <stdio.h>

int main()
{
	int a[2][3];
	int i,j;
	for(i = 0; i < 2;i++) //外层循环遍历第一个角标
	{
		for(j = 0; j < 3;j++)//内层循环遍历第二个角标
		{
			a[i][j] = i * j;
			printf("%d\n",a[i][j]);
		}
	}
	return 0;
}
  1. 三种初始化的概念和一维数组是一样的。
    • 完全初始化:数组的数量和元素个数相等。
int a[2][3] = {{1,2,3},{4,5,6}};
int a[2][3] = {1,2,3,4,5,6};
  • 部分初始化:数组的数量小于元素的个数,其他元素默认补0。
int a[2][3] = {1,2,3};//a[0]:1 2 3 a[1]: 0 0 0
int a[2][3] = {{1},{2,3}};//a[0]:1 0 0 a[1]:2 3 0
  • 默认初始化:
    二维数组的第一个长度是二维数组的长度,是二维数组中包含的一维数组的个数,可以省略。第二个长度是元素的数组长度,是不能省的,属于二维数组的元素类型。
int a[][3] = {{1,2,3},{4,5,6}};
int a[][3] = {1,2,3};

例题

  1. 用二维数组表示一个68的平面直角坐标系,默认显示,输入对应的位置和一个字符,给二维数组中相应的位置赋值,以行列的形式打印二维数组
    我们经常使用二维数组来表示坐标系,其中二维数组的每一个一维数组用来表一行。
    • 用第一个角标表示Y轴,第二个角标表示X轴。
    • 坐标原点在左上角。X轴向右递增,Y轴向下递增。
      在这里插入图片描述
#include <stdio.h>

int main()
{
	char a[8][6];//表示宽x=6高y=8的坐标系
	int x,y;
	for(y = 0; y < 8;y++)
	{
		for(x = 0;x < 6;x++)
		{
			a[y][x] = '*';
		}
	}
	while(1)
	{
		//遍历打印二维数组
		for(y = 0; y < 8;y++)
		{
			for(x = 0;x < 6;x++)
			{
				printf("%c ",a[y][x]);
			}
		}
		int inputX,inputY;
		char inputC;
		scanf("%d%d %c",&inputX, &inputY, &inputC);
		//给坐标系中的点对应的二维数组的元素赋值
		a[inputY][inputX] = inputC;
	}
	return 0;
}
  1. 鞍点,判断数组中是否存在鞍点,在该行最大,并且在该列最大
    int a[3][4];
    a[0]: 1 2 3 4
    a[1]: 5 6 7 8
    a[2]:9 12 11 10
#include <stdio.h>

int main()
{
	int a[3][4] = {1,2,3,4,5,6,7,8,9,12,11,10};
	int x,y;
	for(y = 0; y < 3;y++)
	{
		int max = a[y][0]; // 假设a[y]数组的0元素是最大值
		int maxI = 0; // 假设最大值角标是0
		for(x =0; x < 4; x++)
		{
			if(a[y][x] > max)
			{
				max = a[y][x];
				maxI = x;
			}
		}
		printf("a[%d] max = %d maxI = %d\n", y, max, maxI);
		int i;
		//在循环中遍历的是i,maxI是固定的
		for(i = 0; i < 3;i++)
		{
			if(a[i][maxI] > max)
			{
				break;
			}
		}
		if(i == 3) //说明没有执行过break
		{
			printf("a[%d][%d] = %d\n", y, maxI, max);
		}
	}
	return 0;
}

在这里插入图片描述

  1. 请将杨辉三角形(7阶)保存在二维数组中,并输出。
    1 0 0 0 0
    1 1 0
    1 2 1 0
    1 3 3 1
    1 4 6 4 1
    1 5 10 10 5 1

int yh[7][7] = {{1},{1},{1},{1},{1},{1},{1}};
yh[y][0] 不需要计算
yh[0] 不需要计算
从yh[1][1] 开始计算
yh[y][x] = yh[y-1][x] + yh[y-1][x-1];

#include <stdio.h>

int main()
{
	int yh[7][7] = {{1},{1},{1},{1},{1},{1},{1}};
	int y,x;
	for(y =1; y < 7; y++)
	{
		for(x = 1; x < y+1; x++)
		{
			yh[y][x] = yh[y-1][x] + yh[y-1][x-1];
		}
	}
	for(y =0; y < 7; y++)
	{
		for(x = 0; x < y+1; x++)
		{
			//%3d 打印的整数至少占3个格,不够3位数左边补空格
			//大于3位数全部输出,此时输出会大于3个格
			printf("%3d", yh[y][x]);
		}
		printf("\n");
	}
	
	return 0;
}

在这里插入图片描述

4. 函数

函:包装信息
数:数据,数据的算法
函数主要用来封装逻辑

什么时候需要封装函数?

  • 固定的算法要封装 如冒泡排序 判断质数 找最大值…
  • 会反复使用的代码 比如打印数组
  • 如果一个函数写的太大了,它一定可以拆分成若干个小函数。【函数不能写太大】

系统函数:printf scanf putchar getchar puts gets strlen strcpy strcat strcmp 等等

4.1 自定义函数

对于完整的函数实现,需要包含四个部分:

返回值类型 函数名(形参列表[可以为空])
{
函数体
}

#include <stdio.h>
/*
函数名:function
形参列表:空
返回值类型:当函数没有返回值的时候,返回值类型要写成void
void 叫无类型,在C语言中不能定义void类型的变量  void a;不行
函数功能:打印4和9之间较小的值
*/
void function()
{
	int a = 4, b = 9;
	int min = a<b?a:b ;
	printf("min = %d\n",min);
}

int main()
{
	printf("hello world \n");
	function();
	printf("end\n");
	return 0;
}

在这里插入图片描述

4.1.1 使用要点
  1. 调用才执行。不调用的函数和没写是一样的。
  2. 先声明再使用。

主调函数和被调函数: A函数调用B函数,A是主调函数,B是被调函数。【被调函数定义在主调函数的下面,需要在调用之前先声明】;
调用函数的语法:函数名()

#include <stdio.h>
//当函数没有定义在主调函数上面时,需要在主调函数上面先对被调函数进行声明
void function();

int main()
{
	printf("hello world \n");
	function();
	printf("end\n");
	return 0;
}

void function()
{
	int a = 4, b = 9;
	int min = a<b?a:b ;
	printf("min = %d\n",min);
}
4.1.2 变量的作用域

变量的作用域就是变量的使用范围。
变量的作用域可以简单理解就是定义变量的{}。

4.1.3 形参列表

作用:主调函数给被调函数传数据。

注意要点

  1. 信息传递的方向:由主调函数 传递给 被调函数
  2. 形参与实参,个数要一致,类型要一致。

实参:主调函数传给被调函数的数据,实参可以是常量也可以是变量,实参的数量和类型需要和形参一一对应。
形参:在函数的()里定义的变量,就是形参。每个变量都必须有自己的类型,int a,b 这样是不可以的!每个形参之间使用逗号分割,数量没有上限。形参的作用域是它所在的函数。
在这里插入图片描述

驼峰命名法
每个单词的首字母大写
大驼峰:整个名称的首字母大写 ,其余字母均小写。 用于给类型命名,例如:FirstName、LastName、UserName等。
小驼峰:整个名称的首字母小写 用于给除类型以外的命名,后面的每个单词的首字母大写,其余字母均小写。例如:firstName、lastName、userName等。

4.1.4 返回值

作用:将被调函数中的数据传给主调函数

注意要点

  1. 信息传递的方向:被调函数 传给 主调函数
  2. 返回值类型:在定义函数的时候,函数名左边的类型,决定函数的返回值类型。void表示没有返回值,void类型不能定义变量。
  3. 返回值只能返回一个值,不可以返回数组,因为C语言的数组不能整体赋值。

return: 用于结束函数,return后面的表达式的结果将会传给主调函数。如果函数的返回值类型是void,那么return后面不能写表达式。
在这里插入图片描述
在这里插入图片描述

return 0; //main函数的return 0 是返回给系统,0 代表的意思只有自己,一般情况下main函数返回0表示正常结束
函数如果定义了有返回值,那么每一个结束函数的逻辑分支都要有返回值。

4.1.5 局部变量与全局变量

局部变量:定义在函数中的变量,就是局部变量,是有作用域限制的。
全局变量:定义在函数以外的变量,就是全局变量,它没有作用域的限制。

对比

  1. 对于全局变量,如果没有进行初始化操作,那么这个变量默认为0。局部变量默认没有值。
  2. 生命周期:
    • 全局变量从程序开始执行创建,到程序结束删除。
    • 局部变量从所在的{}开始执行创建,{}执行完删除。
  3. 作用域:
    • 全局变量程序的任何地方都可以用。
    • 局部变量只能在定义的{}里使用。
  4. 全局变量不能重名。局部变量在同一个作用域里也不能重名。
  5. 全局变量 与局部变量 重名的问题:
    当小作用域和大作用域变量重名时,优先使用小作用域的变量
    在这里插入图片描述

C语言是面向过程的语言,过程就是函数,C语言中我们的编码单位是函数
C++语言是面向对象的语言 对象就是类,C++语言中我们的编码单位是类

5. 指针

指针变量的本质是变量。
指针变量是存放地址的变量。
内存单元地址即为指针

简单的用我们生活的例子来说:顺丰快递将电脑这个物品送到联想大厦(地址)里面,顺丰快递就是变量就是存放地址的变量。
地址起到了什么作用?查找的作用
地址的作用是查找。地址指的是内存中的地址
内存的最小编址单位是字节
在这里插入图片描述
变量的运算,本质上是对变量中数据的运算
地址只有两种运算:地址偏移、间接运算

5.1 地址偏移

+、-

char 在内存中占1个字节,所以+1,差值是1
在这里插入图片描述
int在内存占4个字节,+1 差值是4
在这里插入图片描述

地址±N 是±N*地址类型大小个字节
地址是有类型的

5.2 间接运算

*单目运算

  1. 只能对地址运算
    2.它的运算结果不是数值,是地址对应的对象本身。对象可以是变量、可以是常量、可以是数组。

在这里插入图片描述

#include <stdio.h>

int main()
{
	int a;
	/*
	&a运算得到变量a的地址
	*&a运算得到&a地址对应的对象本身,就是变量a
	=10 将10 赋值给a
	*/
	*(&a) = 10;
	printf("%d\n", a); //10
	return 0;
}

5.3 定义指针变量

int *p;
*:说明p是指针变量
int 说明p中存放的地址是int类型
指针变量是存放地址的变量

在C/C++中,出现在声明和语句中的符号,都不是运算符。
能够出现在声明语句中的符号,在C语言中只有3个
*:指针变量
[]: 数组
(): 函数
int a; 整型变量
int a[10]; 数组
int a(); 函数

5.3.1 初始化

指针变量只能存放地址,地址也只能被指针变量存放。

int a;
int *p = &a; //使用&a初始化指针变量p。
这里"*p",是表示指针身份的符号。

5.3.2 赋值

在这里插入图片描述
输出指针变量
在这里插入图片描述
在这里插入图片描述
应用层的编码,使用的都是假地址。

5.3.3 指针变量的意义

指针变量常见的用法之一: 在被调函数中对主调函数中的变量赋值。【将被调函数的运算结果通过指针变量传给主调函数】

#include <stdio.h>

int main()
{
	int a;
	int* p = &a; //使用&a初始化指针p
	//当p中存放a的地址,我们说p指向a
	*p = 10; //指针变量的运算是对变量中地址的运算,*p运算得到a本身,=10 是给a赋值
	printf("%d\n",a);//10
	return 0;
}
#include <stdio.h>

void fun(int* b)//定义指针类型的形参
{
	*b = 10;
}
int main()
{
	int a;
	fun(&a); // 使用&a初始化形参b,此时形参b指向main中的变量a
	printf("%d\n",a);//10
	return 0;
}

指针变量常见的用法之二: 指针指向字符串常量

在这里插入图片描述
指针所占内存空间的大小:

  1. 一个字节有8位二进制
  2. 在32位系统中一个地址占4个字节,和地址类型无关,和谁的地址都无关,所以一个指针变量是4个字节;64位系统,指针变量是8个字节

在32位系统中,每个地址(指向内存中某个位置的指针)占用4个字节的内存空间。这意味着,无论指针的具体类型、指向的变量或函数的类型,或者指针指向的内存区域是在哪个进程或线程中,一个指针变量都需要4个字节的内存空间来存储它所指向的地址。
例如,在C语言中,一个指向整数类型int的指针变量,其实际上只是一个内存地址,用来存储该整数变量在内存中的地址。在32位系统中,这个指针变量需要4个字节的内存空间来存储它所指向的整数变量的地址。同样地,一个指向字符类型char的指针变量,也需要4个字节的内存空间。
因此,可以得出结论:在32位系统中,一个指针变量所占用的内存空间大小固定为4个字节,与它所指向的变量类型、指向的内存区域、以及指针本身的类型等都无关
这个规律同样适用于64位系统,只不过每个地址占用的内存空间变为8个字节。
在这里插入图片描述
在这里插入图片描述

两个指针变量之间赋值,就是修改指向

#include <stdio.h>

// 实参初始化形参  int* m = p  int* n = q;
//函数内部三行执行完,m指向q,n指向p
void fun(int* m,int* n)
{
	int* temp = m;
	m = n;
	n = temp;
}

int main()
{
	int a = 3, b = 5;
	int* p = &a, q = &b;
	fun(p,q);
	//函数中并没有写有关a b的值的语句,只是更改了指向,所以在此输出a b 的值并不变
	printf("a is %d    b is   %d\n", a, b); //  ?    ?    3  5
	return 0;
}

5.4 指针与数组的联系

  1. 数组名就是数组的首地址。数组名是常量,是数组的元素类型的地址。
  2. 数组中的元素在内存中连续存放。已知数组首地址,可以通过地址的偏移运算得到数组中的每个元素的地址。
#include <stdio.h>

int main()
{
	int a[5];
	int i;
	printf("a = %p\n", a); //指针变量的打印是%p,打印数组名,数组名是数组的首地址
	for(i = 0; i < 5; i++)
	{
		printf("%p\n",&a[i]);//打印每个元素的地址
	}
	return 0;
}

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

5.4.1 下标运算

下标运算:[], 是二元运算,左值是地址,右值是整数
运算规则:*(地址+整数);地址+整数得到一个新的地址
下标运算得到的运算结果不是数值,而是 新地址对应的对象本身。

将数值的首元素地址赋值给指针,我们叫指针指向数组。
定义一个指针指向一个数组,指针的类型应该是数组的元素类型。

#include <stdio.h>

int main()
{
	int a[10] = {1,2,3,4,5,6,7,8,9,10};
	int* p;//想要定义指针指向数组a;那么指针的类似应与数组a元素的类型一致
	p = a;// 等价于 p = &a[0]; 让p指向数组a
	int i;
	for(i = 0; i < 10; i++)
	{
	/*
	 因为p中存放数组a的首地址,所以p+i得到数组a的i元素的地址
	 *(p+i)运算得到a中i元素本身,等价于p[i]--下标运算
	*/
		printf("%d %d %p\n",p[i], *(p+i), p+i);
	}
	return 0;
}

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

5.4.2 字符数组
  1. 定义字符数组,将字符串放在数组中,定义指针指向数组,使用指针输出数组中保存的字符串。
  2. 字符串初始化一个字符数组
  3. %s需要的参数是 字符类型的地址,它是从所给的地址开始一个一个字符的打印,直到\0为止。
#include <stdio.h>

int main()
{
	char buf[] = "hello world";
	char* p = buf;
	printf("%s\n%s\n",p, buf);
	return 0;
}

在这里插入图片描述
C语言对字符串的处理有两个要素

  1. 字符串首地址作为字符串的开始
  2. ‘\0’作为字符串的结尾【C语言因为对于字符串的所有操作,都是从字符串的首地址开始,到’\0’结束。】

使用%s需要传给它字符串的首地址,它会从首地址开始输出,直到’\0’为止。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.5 空指针和野指针

野指针:当指针中存放的地址是无效地址,就是野指针。在编程时要避免野指针野指针非常危险,可能会造成不可预计的bug。
空指针 :当一个指针被赋值成NULL的时候,它是空指针,NULL表示没有地址,空指针也不能间接运算,空指针一旦进行间接运算程序必然崩溃。

  1. 当一个指针还不能确定指向的时候,应该赋值成空指针。
  2. 当定义一个指针不赋值的时候,它里面一定是无效地址,一定是野指针。
  3. 错误的三种级别:
    • 编译错误,编译器会提示
    • 程序运行崩溃,下面的错误叫做段(内存段)错误。
      在这里插入图片描述
    • 没有任何提示,就是得不到正确的结果。
      在这里插入图片描述

值传递和地址传递

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

函数传递值的方式

一个函数想要给调用值,传递值,有两种方式

  1. 返回值:return max;
  2. 参数上的地址传递 getMax(a,5,&max)

在这里插入图片描述

6. 内存分配

系统给内存分出好多区间,我们只关注存放变量的区间

6.1 内存分区(变量可能被分配的内存分区)

  • 栈(stack):普通的局部变量在栈空间,在作用域开始执行时自动创建,作用域执行完毕自动删除。栈空间的变量生命周期太短。

栈是一种线性数据结构,通常具有先进后出(Last-In-First-Out,LIFO)的特性。它是一种特殊的内存区域,用于存储函数调用和本地变量。当函数被调用时,栈会在内存中分配一段区域,用于存储函数的参数、返回地址和局部变量等数据。在函数返回时,这些数据会被从栈中弹出,栈的内存空间也会被释放。

  • 堆(heap):堆中的变量自己创建自己删除。堆中变量的生命周期自定义。

堆是一种动态分配内存的机制,其数据结构为树形结构。堆的内存空间是由程序员通过动态分配内存来管理的,即程序可以在运行时请求堆内存。堆中存储的数据可以在任何时候被访问和修改,而且不会随着函数的返回而被释放。堆通常用于动态创建对象和数据结构等场景。

  • 静态(bss:Block Started by Symbol和data):全局变量和静态局部变量在静态区。程序开始执行就自动创建,程序执行完毕自动删除。

静态(Static)是一种内存分配方式,通常用于存储全局变量、静态变量和常量等数据。静态分配的内存在程序编译时就已经分配好了,并且在程序运行期间保持不变。
在静态内存分配中,有两个主要的存储区域:BSS 和 Data。
除了全局变量和静态变量,还有以下数据类型可以使用静态内存分配:

  1. 常量(const):常量是一种不可修改的数据类型,可以使用静态内存分配来存储常量的值。
  2. 函数:函数也可以使用静态内存分配来存储,这样可以避免函数调用时的动态内存分配开销。
  3. 枚举类型(enum):枚举类型是一种由编译器分配的整数类型,可以使用静态内存分配来存储枚举类型的值\
  4. 结构体(struct)和联合体(union):结构体和联合体是一种复合数据类型,可以使用静态内存分配来存储它们的成员变量。
  5. 静态数据类型:C++中的静态数据类型(static)也可以使用静态内存分配来存储。

总之,静态内存分配可以用于存储各种类型的数据,包括全局变量、静态变量、常量、函数、枚举类型、结构体和联合体等。在使用静态内存分配时,需要注意避免内存泄漏和内存溢出等问题,以确保程序的正确性和效率。

  • 静态区分为有初始化和没有初始化两部分。我们称没有初始化的区间叫bss、有初始化的叫data

BSS(Block Started by Symbol)段用于存储未初始化的全局变量和静态变量,它通常包含一些程序中定义的未初始化的全局变量和静态变量,这些变量在程序运行之前会被自动初始化为0或者空指针。
Data 段用于存储已初始化的全局变量和静态变量,它通常包含一些程序中定义的已初始化的全局变量和静态变量,这些变量在程序运行之前就已经被初始化为具体的值了

静态变量是一种在程序编译时就已经分配好内存空间的变量,其生命周期与程序的生命周期相同,与函数的调用无关。通常,静态变量的值在程序运行期间不会发生变化,除非程序中显式地对它们进行修改。
在 C 语言中,静态变量可以在函数内部或函数外部定义,使用关键字 static 来标识。在函数内部定义的静态变量只能在函数内部访问,而在函数外部定义的静态变量可以在整个程序中访问。
静态变量与普通变量的区别在于它们不会在函数调用结束后被销毁,而是在程序运行期间一直存在,直到程序结束。因此,静态变量可以用于存储需要在多次函数调用之间保持不变的数据,比如计数器、缓存等。
另外,由于静态变量的作用域是整个程序,因此可以在多个函数之间共享数据,这对于一些需要共享数据的场景非常有用。

  • 三个内存分区对变量生命周期的管理不同

静态内存分配和动态内存分配是两种不同的内存管理方式,它们的主要区别在于内存的分配和释放方式不同。

  1. 静态内存分配是在程序编译时就已经分配好了内存空间,程序运行时不需要再进行内存分配和释放。静态内存分配通常用于存储全局变量、静态变量、常量和函数等数据,以及一些固定大小的数据结构。
  2. 动态内存分配是在程序运行时根据需要进行内存分配和释放。动态内存分配通常使用堆内存来分配内存空间,程序员可以通过调用 malloc() 或 new() 等函数来动态地分配内存空间,并通过调用 free() 或 delete() 等函数来释放已经分配的内存空间。
  3. 静态内存分配的主要优点是可以提高程序的运行效率,因为内存空间在编译时已经分配好了,不需要在程序运行时进行内存分配和释放操作。但是,静态内存分配的缺点是内存空间的大小在编译时就已经确定了,不能动态调整大小,因此无法应对一些动态变化的情况。
  4. 动态内存分配的主要优点是可以动态地分配和释放内存空间,可以根据程序的实际需要动态调整内存空间大小,因此更灵活。但是,动态内存分配的缺点是需要手动管理内存,容易出现内存泄漏和内存溢出等问题,需要程序员自己负责管理内存,增加了程序的复杂度。

在这里插入图片描述

6.2 堆空间内存的申请与释放

#include <stdlib.h>
6.2.1 申请内存

void *malloc(size_t size);
malloc :memory allocation
参数:size_t是malloc函数对int类型起的别名,size尺寸,_t type,表示要申请内存的大小,单位字节。
返回值: *表示返回地址,void表示地址的类型是无类型地址,返回的地址是申请内存的地址。比如申请了100个字节,返回的是地址最小的字节的地址。malloc申请的内存是连续的。如果申请内存失败返回NULL。

在这里插入图片描述
malloc申请的内存不会初始化

强制类型转换

(类型)变量 这是一个运算,运算的结果是将变量中的值转换成()里的类型后的值。
在这里插入图片描述
根据 C 语言标准,void* 可以自动转换为其他指针类型,所以int * p = (int * )malloc(10*sizeof(int)) 等价于 int * p = malloc(10 *sizeof(int))

6.2.2 释放内存

void free(void *p);
参数:是要释放内存的地址,必须是malloc申请的内存才能使用free释放。 free之后内存中的数据还在

6.2.3 使用注意事项
  1. malloc 和free 成对去写
    • 如果申请的内存没有释放,那么就算程序结束内存依然会被占用,重启电脑会清理掉。
    • 申请堆空间的内存却不释放,叫内存泄漏
  2. malloc 申请内存,会成功或失败,如果成功,则可以使用申请来的堆空间;如果失败,会返回NULL
  3. 在堆空间被释放掉之后,及时将指针p归零,以防出现使用这块已经被释放掉的内存,造成不必要的风险。
#include <stdio.h>
#include <stdlib.h>

int main()
{
	//使用malloc的正常用法
	//使用malloc创建数组我们叫动态创建数组,因为可以使用变量表示数组长度。
	//int *p = (int *)malloc(10*sizeof(int));
	
	/*
	在堆空间申请内存,40个字节,10*sizeof(int)为了提高可读性,表明空间用来做整型数组
	将返回的无类型地址放到指针变量pv中
	*/
	void* pv = malloc(10*sizeof(int));
	int *p = (int *)pv;
	if(p == NULL) //判读是否申请内存失败
	{
		printf("error\n");
		return 0;
	}
	int i;
	//循环输入,给每个元素赋值
	for(i = 0;i < 10;i++)
	{
		scanf("%d", &p[i]);//p指向数组首地址,所以p[i]是数组i元素
	}
	for(i = 0;i < 10;i++)
	{
		printf("%d", &p[i]);
	}
	free(p); //释放空间,但是不清空数据,释放之后p和pv都是野指针,因为虽然p和pv类型不一样,但是指向同一内存。
	//由于后面程序就结束了,这里不置空也行,主要是为了演示用法
	p = NULL; //将野指针置空
	pv = NULL;//将野指针置空
	return 0;
}

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

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


int main(int argc, const char *argv[])
{
    //在堆区申请空间
    int n;
    printf("请输入申请空间个数: \n");
    scanf("%d",&n);
    int *p = malloc(n*sizeof(int));
    if(p == NULL)//申请空间失败
    {
        printf("malloc failed\n");
        return -1; // -1代表失败
    }
    //申请成功,往空间里存值
    int i;
    for(i = 0;i < n; i++)
    {
        p[i] = i;
    }
    //打印空间里的值
     for(i = 0;i < n; i++)
    {
         printf("%d\n",p[i]);
    }
     printf("\n");
     free(p);//释放空间
     p = NULL; //将野指针置空
    return 0;
}

在这里插入图片描述

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

struct student 
{
    char name[20];
    int age;
    int score;
};

void setStudentInfo( struct student *p,int n)
{
    int i;
    for(i = 0;i < n; i++)
    {
         printf("请输入第%d个学生信息:姓名 年龄 成绩:\n",i+1);
         scanf("%s%d%d",p[i].name,&p[i].age,&p[i].score);
    }
}

void showStudentInfo( struct student *p,int n)
{
    int i;
    for(i = 0;i < n; i++)
    {
         printf("学生信息:姓名: %s   年龄%d  成绩%d\n",p[i].name,p[i].age,p[i].score);
    }
}
  

int main(int argc, const char *argv[])
{
    //在堆区申请空间
    struct student *p = NULL;
    int n; //班级人数
    printf("请输入班级人数: \n");
    scanf("%d",&n);
    p = malloc(n*sizeof( struct student));
    if(p == NULL)//申请空间失败
    {
        printf("malloc failed\n");
        return -1; // -1代表失败
    }
    setStudentInfo(p,n);
    showStudentInfo(p,n);
    free(p);//释放空间
    p = NULL; //将野指针置空
    return 0;
}

在这里插入图片描述

7. 位运算

位bit 位运算是对二进制位的运算

为什么使用位运算?

  1. 赋值运算符至少是给1个字节中的8个bit同时赋值,但是有些逻辑只想给变量中的某些二进制位赋值,此时需要使用位运算。
  2. 位运算就相当于一个组合拳,用一个变量值,通过位运算来表示好几种状态。
  3. 所有的位运算,只能针对整数运算 unsigned char short int (无符号数)
  4. 绝大部分情况下使用位运算都是对无符号整数进行位运算的。因为无符号数没有负数。
  5. 位运算的目标是变量中具体的二进制位
    在这里插入图片描述

7.1 左移和右移

7.1.1 左移

“<<” 二元运算,左值<<右值,将左值变量中的所有二进制位,向高位移动右值个位,高位溢出,低位补0。【溢出就是不要了】

在这里插入图片描述

7.1.2 右移

“>>” 二元运算 左值>>右值 将左值变量中的所有二进制位,向低位移动右值个位,低位溢出,高位补0,如果是负数,高位补1。

在这里插入图片描述

7.2 按位取反

~ 单目运算,将变量中所有的位,1变成0,0变成1

#include <stdio.h>

int main()
{
	unsigned char a = 1;//0000 0001
	a = ~a;//1111 1110
	//查看二进制,使用十六机制更方便
	printf("%#x\n", a); //0xFE
	return 0;
}

7.3 按位与

& 二元运算,对应的位,有一个位0结果就是0

#include <stdio.h>

int main()
{
	unsigned char a = 0xA5; //1010 0101
	unsigned char b = 0x8C; // 1000 1100
	unsigned char c = a&b; 
	/*
	1010 0101
	1000 1100
	--------------------
	1000 0100
	*/
	printf("%#x\n", c);//0x84
	return 0;
}

按位与的作用

  1. 给指定的二进制位写0

    unsigned char a;
    a = a&~(1<<n);//将变量a的n位写0,其他位不变 n>=0 0位是最低位

#include <stdio.h>

int main()
{
	 unsigned char a = 0xA5; //1010 0101
	 a = a &~(1<<5);
	 /*
	 0010 0000            1<<5
	 1101 1111             ~(1<<5)
	 
	 1010 0101
	 1101 1111             a&~(1<<5)
	 -----------------------
	 1000 0101             5位由1变位0
	 最低位读作0位
	 */
	 printf("%#x\n", a);
	 return 0;
	 
}
  1. 读取指定位的值
#include <stdio.h>

int main()
{
	unsigned char a = 0xA5;
	int i;
	for(i = 0; i < 8;i++)
	{
		if((a&1<<i) == 0)
		{
			printf("a的%d bit is 0 \n", i);
		}
		else //a&1<<i 就算不得0,也未必是1
		{
			printf("a的%d bit is 1\n", i);
		}	
	}
	/*
	1010 0101
	0000 0100
	----------------------
	0000 0100 
	*/
	return 0;
}

7.4 按位或

| 指定的二进制位,写1 ,有一个1结果就是1
a = a|1<<n; 给a的第n位写1 ,n>=0

#include <stdio.h>

int main()
{
	unsigned char a = 0xA5;
	a = a|1<<3;
	/*
	1010 0101
	0000 1000
	--------------------
	1010 1101           3位由0变为1
	*/
	printf("%#x\n", a); //0xad
	return 0;
}

7.5 按位异或

^ 指定位取反 对应的位,相同得0 ,不同得1
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

8. 宏定义

为什么使用宏定义?
int a = 1;
常量本身没有意义,我们在编程时需要让常量看起来有意义,此时可以使用宏定义。

8.1 无参数

#define 宏名(自己起名) 宏值
#预处理
define是宏定义的指令
宏名需要我们自己起名,一般都是全大写,为了在代码中容易和变量区分,宏名就是我们要给常量起的名字
宏值 是需要起名的常量

宏定义的原理是在预处理的时候,用宏值替代宏名
在这里插入图片描述
在这里插入图片描述
宏定义要是有多行的话,用\来链接宏的值
在这里插入图片描述

8.2 有参数

代码量比较小的,可以用宏定义带参数的

#include <stdio.h>
#define SQR(x) x*x

int main(int argc, const char *argv[])
{
    printf("res is %d\n",SQR(3)); 
     printf("res is %d\n",SQR(3 + 2));  // 3 + 2 * 3 + 2
    return 0;
}

在这里插入图片描述

#include <stdio.h>
#define SQR(x) (x)*(x)

int main(int argc, const char *argv[])
{
    
    printf("res is %d\n",SQR(3 + 2));  
    return 0;
}

在这里插入图片描述

8.3 带参宏与函数的区别

带参宏:
宏是在编译的时候替换
如果代码量小,调用频率又很高
带参宏会导致代码量大

函数:运行时调用函数,要分配栈空间

8.4 宏定义的优点

  1. 宏值不仅可以是常量,还可以是任意代码。宏定义的替换是无脑替,写什么就替换成什么,哪怕有语法错误
    在这里插入图片描述

  2. 使用宏定义方便后期的修改和维护

  3. 增加代码的可读性

  4. 提高程序的运行效率,使用带参宏可以完成函数调用功能,又能减少系统开销,提高运行效率

9. 结构体

为什么使用结构体?
为了将数据进行封装。
将数据封装成一个整体,形成一个新的类型,叫结构体类型
结构体的本质是一种类型

9.1 结构体类型的定义

/*
struct是定义结构体类型的关键字
student 是结构体类型的名字
{}; 注意:{}的结尾有; 这里的{}不是作用域,它里面不能写任何逻辑;里面写的代码不是在定义变量也不是在定义数组,只是在告诉编译器,一旦定义了结构体类型的变量,那么变量中包含这些内容。
*/
struct Student
{
char name[20]; //结构体的成员,或者叫属性
char number[20];//结构体的成员,或者叫属性
int grade; //结构体的成员,或者叫属性
float score; //结构体的成员,或者叫属性
}; //注意:{}的结尾有;

9.2 结构体变量的定义

struct Student
{
char name[20];
char number[20];
int grade;
float score;
};
int main()
{
/*
定义结构体变量首先需要使用关键字 struct ,后面跟结构体类型的名字,最后是结构体变量的名字
*/
struct Student a; //在这里结构体变量的名称叫a,定义结构体变量时结构体的成员才会被定义出来,结构体的成员都在结构体的变量中
printf(“%d\n”,sizeof(a)); //48,结构体变量这么大因为它得装得下所以的成员。
return 0;
}

9.3 访问成员变量

成员运算符 . 二元运算
左值是结构体变量,右值是成员名称
运算结果:是结构体变量中的成员对象本身,可以是变量还可以是数组

在这里插入图片描述

9.4 输出

不能直接把结构体变量格式化,只能一个一个属性单独进行格式化。

在这里插入图片描述

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

struct Student
{
	char name[20];     
	char major[20];
	int age;             
	float num;         
};    
// 因为函数的形参使用了结构体类型,所以函数的声明必须在结构体下面
void print_info(struct Student stu);

int main()
{
	struct Student a; 
	strcpy(a.name, "小明");
	strcpy(a.major, "数学");
	a.age = 18;
	a.num = 10086;
	
    struct Student b; 
	strcpy(b.name, "小明");
	strcpy(b.major, "英语");
	b.age = 38;
	b.num = 10000;
	print_info(a);
	print_info(b);
    return 0;
}      

void  print_info(struct Student stu)
{
	printf("%s %s %d %f\n",stu.name,stu.major,stu.age,stu.num);
}     

9.5 结构体指针

结构体指针,就是普通的指针变量。
在这里插入图片描述

9.6 结构体数组

在这里插入图片描述
参考:http://c.biancheng.net/view/245.html

9.7 结构体嵌套

参考:https://www.yiibai.com/cprogramming/nested-structure-in-c.html

9.8 总结

定义结构体,尽量把相同的类型的成员写在一起,尽可能的节省内存。