【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释

一 、C语言学习第一天

1.1 编写C语言代码:hello.c
#include
#include  //调用system系统函数需要包含的头文件

int main()
{
	printf("hello world\n");
	system("pause");
	return 0;
}
解决在运行程序的时候,黑窗口一闪而过的方法

1、通过system()函数解决,在return 0之前,添加system(“pause”);函数调用

2、借助VS工具解决:在项目上 —》右键 —》 属性 —》 配置属性 —》 链接器 —》 系统 —》 子系统 —》 在下拉框中选择“控制台 (/SUBSYSTEM:CONSOLE)”

1.2 编写helloworld程序的两种方式

1、借助VS编辑工具编写:创建项目 --》 创建 helloworld.c 源文件 --》 写 helloworld程序 --》Ctrl + F5 执行

2、借助笔记本、gcc编译工具编写

  • 在记事本中写 helloworld 程序
  • 使用记事本创建 helloworld.c 文件
  • 使用gcc编译工具 ,在记事本写的 helloworld.c 所在目录中,执行 gcc helloworld.c -o myhello.exe
  • 在终端(黑窗口)中,运行 : myhello.exe
gcc 、g++编译常用选项说明
选项 含义
-o file 指定生成的输出文件名为file
-E 只进行预处理
-S(大写) 只进行预处理和编译
-c(小写) 只进行预处理、编译和汇编
#include< > 与 #include " "的区别:
  • < > 表示系统直接按系统指定的目录检索
  • " " 表示系统先在 " " 指定的路径(没写路径代表当前路径)查找头文件,如果找不到,再按系统指定的目录检索
1.3 system函数

作用:执行系统命令,如: pause、cmd、calc、mspaint、notepad…

代码示例:

#include 
#include 

int main()
{
	//system("calc"); //windows平台
	system("ls"); //Linux平台, 需要头文件#include 

	return 0;
}
1.4 gcc编译的四个步骤【重点】

1、预处理的命令格式:gcc -E xxx.c -o xxx.i

预处理所完成的事情:

  • 头文件展开,不检查语法错误,可以展开任意文件
  • 宏定义替换,将宏名替换为宏值
  • 替换注释,将注释变成空行
  • 展开条件编译,根据条件来展开指令

2、编译的命令格式:gcc -S hello.i -o hello.s

编译所完成的事情:

  • 逐行检查语法错误【重点】,整个编译4步骤中最耗时的过程
  • 将C程序翻译成汇编指令,得到 .s汇编文件

3、汇编的命令格式:gcc -c hello.s -o hello.o

汇编所完成的事情:

  • 翻译:将汇编指令翻译成对应的二进制代码

4、链接的命令格式:gcc hello.o -o hello.exe

链接所完成的事情:

  • 数据段合并
  • 数据地址回填
  • 库引入
1.5 程序的调试

1、设置断点,F5启动调试

2、停止的位置,是尚未执行的指令

3、逐语句执行下一条(F11):进入函数内部,逐条执行跟踪

4、逐过程执行下一条(F10):不进入函数内部,逐条执行程序

5、添加监视:调试–》窗口–》监视:输入监视的变量名,自动监视变量值的变化

为程序添加行号的方法:

工具–》选项–》文本编辑器–》C/C+±-》行号 ,选中即可

1.6 VS2013的C4996错误

错误原因:由于微软在VS2013中不建议再使用C的传统库函数scanf,strcpy,sprintf等,所以直接使用这些库函数会提示C4996错误

解决办法:如果想要继续使用此函数,需要在源文件中添加一下指令就可以避免这个错误的提示

#define _CRT_SECURE_NO_WARNINGS     //这个宏定义最好要放到.c文件的第一行
#pragma warning(disable:4996)	//或者使用这个


二、C语言学习第二天(数据类型)

2.1 常量与变量
2.1.1 关键字

C语言中总共有32个关键字

数据类型关键字(12个):char , short , int , long , float , double , unsigned , signed , struct , union , enum , void

控制语句关键字(12个):if , else , switch , case , default , for , do , while , break , contiune , goto , return

存储类关键字(5个):auto,extern , register , static , const

其他关键字(3个):sizeof , typedef , volatile

2.1.2 数据类型

数据类型的基本作用:编译器预算对象(变量)分配的内存空间的大小
【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第1张图片

2.1.3 常量

特点:

  • 在程序运行过程中,其值不能被改变
  • 常量一般出现在表达式或者赋值语句中

示例:

1. “hello”、'A'-103.1415926(浮点常量)	

2. #define PI 3.1415	【强调】:没有分号结束标记。 【推荐】 定义宏: 定义语法: #define 宏名 宏值

3. const int a = 10;	定义语法:const 类型名 变量名 = 变量值。

		const关键字: 被该关键字修饰的变量,表示为只读变量。
2.1.4 变量
特点:
  • 在程序运行过程中,其值可以改变
  • 变量在使用前必须先定义,定义变量前必须有相应的数据类型
  • 变量在编译时为其分配相应的内存空间
  • 可以通过其名字和地址访问相应内存
标识符的命名规则:

1、标识符不能是关键字

2、标识符只能由字母、数字、下划线组成

3、第一个字符必须为字母或者下划线

4、标识符中字母区分大小写

声明和定义的区别
  • 声明变量不需要建立存储空间,如:external int a;
  • 定义变量需要建立存储空间,如:int b;

示例:

#include 

int main()
{
	//extern 关键字只做声明,不能做任何定义
	//声明一个变量a,a在这里没有建立存储空间
	extern int a;
	a = 10;	//error, 没有空间,就不可以赋值

	int b = 10;	//定义一个变量b,b的类型为int,b赋值为10

	return 0;
}

代码示例:

#include 
#define MAX 10 //声明了一个常量,名字叫MAX,值是10,常量的值一旦初始化不可改

int main()
{
	int a;	//定义了一个变量,其类型为int,名字叫a

	const int b = 10; //定义一个const常量,名为叫b,值为10
	//b = 11; //err,常量的值不能改变

	//MAX = 100;	//err,常量的值不能改变

	a = MAX;//将abc的值设置为MAX的值
	a = 123;

	printf("%d\n", a); //打印变量a的值

	return 0;
}

2.2 整形
2.2.1 整形变量的定义和输出
打印格式 含义
%d 输出一个有符号的10进制int类型
%o(字母o) 输出8进制的int类型
%x 输出16进制的int类型,字母以小写输出
%X 输出16进制的int类型,字母以大写输出
%u 输出一个10进制的无符号数

代码示例:

#include 

int main()
{
	int a = 123;	//定义变量a,以10进制方式赋值为123
	int b = 0567;	//定义变量b,以8进制方式赋值为0567
	int c = 0xabc;	//定义变量c,以16进制方式赋值为0xabc

	printf("a = %d\n", a);
	printf("8进制:b = %o\n", b);
	printf("10进制:b = %d\n", b);
	printf("16进制:c = %x\n", c);
	printf("16进制:c = %X\n", c);
	printf("10进制:c = %d\n", c);

	unsigned int d = 0xffffffff; //定义无符号int变量d,以16进制方式赋值
	printf("有符号方式打印:d = %d\n", d);
	printf("无符号方式打印:d = %u\n", d);
	return 0;
}

输出结果:

a = 123
8进制:b = 567
10进制:b = 375
16进制:c = abc
16进制:c = ABC
10进制:c = 2748
有符号方式打印:d = -1
无符号方式打印:d = 4294967295
请按任意键继续. . .
2.2.2 整形变量的输入

代码示例:

#include 

int main()
{
	int a;
	printf("请输入a的值:");

	//不要加“\n”
	scanf("%d", &a);

	printf("a = %d\n", a); //打印a的值

	return 0;
}

输出结果:

请输入a的值:10
a = 10
请按任意键继续. . .
2.2.3 short 、 int 、 long 、 long long类型

类型所占内存大小:

数据类型 占用空间
short(短整型) 2字节
int(整型) 4字节
long(长整形) Windows为4字节,Linux为4字节(32位),8字节(64位)
long long(长长整形) 8字节

打印输出格式:

打印格式 含义
%hd 输出short类型
%d 输出int类型
%ld 输出long类型
%lld 输出long long类型
%hu 输出unsigned short类型
%u 输出unsigned int类型
%lu 输出unsigned long类型
%llu 输出unsigned long long类型

代码示例:

#include 

int main()
{
	short a = 10;
	int b = 10;
	long c = 10l; //或者10L
	long long d = 10ll; //或者10LL

	printf("sizeof(a) = %u\n", sizeof(a));
	printf("sizeof(b) = %u\n", sizeof(b));
	printf("sizeof(c) = %u\n", sizeof(c));
	printf("sizeof(c) = %u\n", sizeof(d));

	printf("short a = %hd\n", a);
	printf("int b = %d\n", b);
	printf("long c = %ld\n", c);
	printf("long long d = %lld\n", d);

	unsigned short a2 = 20u;
	unsigned int b2 = 20u;
	unsigned long c2= 20ul; 
	unsigned long long d2 = 20ull; 

	printf("unsigned short a = %hu\n", a2);
	printf("unsigned int b = %u\n", b2);
	printf("unsigned long c = %lu\n", c2);
	printf("unsigned long long d = %llu\n", d2);

	return 0;
}

输出结果:

sizeof(a) = 2
sizeof(b) = 4
sizeof(c) = 4
sizeof(c) = 8
short a = 10
int b = 10
long c = 10
long long d = 10
unsigned short a = 20
unsigned int b = 20
unsigned long c = 20
unsigned long long d = 20
请按任意键继续. . .
2.2.4 有符号数和无符号数的区别
  • 有符号数最高位为符号为,0代表整数,1代表负数

代码示例:

#include 

int main()
{
	signed int a = -1089474374; //定义有符号整型变量a
	printf("%X\n", a); //结果为 BF0FF0BA

	//B       F      0        F       F     0        B	      A
	//1011 1111 0000 1111 1111 0000 1011 1010

	return 0;
}
  • 无符号数最高位不是符号位,而是数的一部分,无符号数不可能是负数

代码示例:

#include 

int main()
{
	unsigned int a = 3236958022; //定义无符号整型变量a
	printf("%X\n", a); //结果为 C0F00F46

	return 0;
}
2.3 sizeof关键字

1、sizeof不是函数,所以不需要包含任何头文件,它的功能是计算一个数据类型的大小,单位为字节

2、sizeof的返回值为size_t

3、size_t类型在32位操作系统下式unsigned int,是一个无符号整数

代码示例:

#include 

int main()
{
	int a;
	int b = sizeof(a);//sizeof得到指定值占用内存的大小,单位:字节
	printf("b = %d\n", b);

	size_t c = sizeof(a);
	printf("c = %u\n", c);//用无符号数的方式输出c的值

	return 0;
}

输出结果:

b = 4
c = 4
请按任意键继续. . .
2.4 字符型
2.4.1 字符变量的定义和输出

字符变量用于存储一个单一字符,在C语言中用char表示,其中每个字符变量都会占用一个字节。在给字符型变量赋值时,需要用一对英文半角格式的单引号(‘ ’)把字符括起来。字符变量实际上并不是把该字符本身放到变量的内存单元中去,而是将该字符对应的ASCLL编码放到变量的存储单元中。char的本质就是一个1字节大小的整形。

代码示例:

#include 

int main()
{
	char ch = 'a';
	printf("sizeof(ch) = %u\n", sizeof(ch));

	printf("ch[%%c] = %c\n", ch); //打印字符
	printf("ch[%%d] = %d\n", ch); //打印‘a’ ASCII的值

	char A = 'A';
	char a = 'a';
	printf("a = %d\n", a);		//97
	printf("A = %d\n", A);	//65

	printf("A = %c\n", 'a' - 32); //小写a转大写A
	printf("a = %c\n", 'A' + 32); //大写A转小写a

	ch = ' ';
	printf("空字符:%d\n", ch); //空字符ASCII的值为32
	printf("A = %c\n", 'a' - ' '); //小写a转大写A
	printf("a = %c\n", 'A' + ' '); //大写A转小写a

	return 0;
}

输出结果:

sizeof(ch) = 1
ch[%c] = a
ch[%d] = 97
a = 97
A = 65
A = A
a = a
空字符:32
A = A
a = a
请按任意键继续. . .
2.4.2 字符变量的输入

代码示例:

#include 

int main()
{
	char ch;
	printf("请输入ch的值:");

	//不要加“\n”
	scanf("%c", &ch);
	printf("ch = %c\n", ch); //打印ch的字符

	return 0;
}

输出结果:

请输入ch的值:A
ch = A
请按任意键继续. . .
ASCLL码大致由一下两部分组成:
  • ASCLL非打印控制字符:ASCII 表上的数字 0-31 分配给了控制字符,用于控制像打印机等一些外围设备
  • ASCII 打印字符:数字 32-126 分配给了能在键盘上找到的字符,当查看或打印文档时就会出现。数字 127 代表 Del 命令
2.4.3 转义字符
转义字符 含义 ASCII码值(十进制)
\a 警报 007
\b 退格(BS) ,将当前位置移到前一列 008
\f 换页(FF),将当前位置移到下页开头 012
\n 换行(LF) ,将当前位置移到下一行开头 010
\r 回车(CR) ,将当前位置移到本行开头 013
\t 水平制表(HT) (跳到下一个TAB位置) 009
\v 垂直制表(VT) 011
\ 代表一个反斜线字符"" 092
代表一个单引号(撇号)字符 039
" 代表一个双引号字符 034
? 代表一个问号 063
\0 数字0 000
\ddd 8进制转义字符,d范围0~7 3位8进制
\xhh 16进制转义字符,h范围09,af,A~F 3位16进制

注意:黄色字体标注的为不可打印字符

代码示例:

#include 

int main()
{
	printf("abc");
	printf("\refg\n"); //\r切换到句首, \n为换行键

	printf("abc");
	printf("\befg\n");//\b为退格键, \n为换行键

	printf("%d\n", '\123');// '\123'为8进制转义字符,0123对应10进制数为83
	printf("%d\n", '\x23');// '\x23'为16进制转义字符,0x23对应10进制数为35

	return 0;
}

输出结果:

efg
abefg
83
35
请按任意键继续. . .
2.5 实型(浮点型):float、 double

实型变量也可以称为浮点型变量,浮点型变量是用来存储小数数值的。在C语言中, 浮点型变量分为两种: 单精度浮点数(float)、 双精度浮点数(double), 但是double型变量所表示的浮点数比 float 型变量更精确。由于浮点型变量是由有限的存储单元组成的,因此只能提供有限的有效数字。在有效位以外的数字将被舍去,这样可能会产生一些误差。不以f结尾的常量是double类型,以f结尾的常量(如3.14f)是float类型。

代码示例:

#include 

int main()
{
	//传统方式赋值
	float a = 3.14f; //或3.14F
	double b = 3.14;

	printf("a = %f\n", a);
	printf("b = %lf\n", b);

	//科学法赋值
	a = 3.2e3f; //3.2*1000 = 3200,e可以写E
	printf("a1 = %f\n", a);

	a = 100e-3f; //100*0.001 = 0.1
	printf("a2 = %f\n", a);

	a = 3.1415926f;
	printf("a3 = %f\n", a); //结果为3.141593

	return 0;
}

输出结果:

a = 3.140000
b = 3.140000
a1 = 3200.000000
a2 = 0.100000
a3 = 3.141593
请按任意键继续. . .
2.6 进制
2.6.1 二进制

二进制是计算技术中广泛采用的一种数制。二进制数据是用0和1两个数码来表示的数。它的基数为2,进位规则是“逢二进一”,借位规则是“借一当二”。当前的计算机系统使用的基本上是二进制系统,数据在计算机中主要是以补码的形式存储的

术语 含义
bit(比特) 一个二进制代表一位,一个位只能表示0或1两种状态。数据传输是习惯以“位”(bit)为单位。
Byte(字节) 一个字节为8个二进制,称为8位,计算机中存储的最小单位是字节。数据存储是习惯以“字节”(Byte)为单位。
1b 1bit,1位
1B 1Byte,1字节,8位
1k,1K 1024B
1M(1兆) 1024k, 1024*1024
1G 1024M
1T 1024G
1Kb(千位) 1024bit,1024位
1KB(千字节) 1024Byte,1024字节
1Mb(兆位) 1024Kb = 1024 * 1024bit
1MB(兆字节) 1024KB = 1024 * 1024Byte
十进制转化二进制的方法:

用十进制数除以2,分别取余数和商数,商数为0的时候,将余数倒着数就是转化后的结果
【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第2张图片

十进制的小数转换成二进制

十进制的小数转换成二进制:小数部分和2相乘,取整数,不足1取0,每次相乘都是小数部分,顺序看取整后的数就是转化后的结果
【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第3张图片

2.6.2 八进制

八进制,Octal,缩写OCT或O,一种以8为基数的计数法,采用0,1,2,3,4,5,6,7八个数字,逢八进1。一些编程语言中常常以数字0开始表明该数字是八进制。八进制的数和二进制数可以按位对应(八进制一位对应二进制三位),因此常应用在计算机语言中。
【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第4张图片

十进制转换八进制的方法

用十进制数除以8,分别取余数和商数,商数为0的时候,将余数倒着数就是转化后的结果
【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第5张图片

2.6.3 十六机制

十六进制(英文名称:Hexadecimal),同我们日常生活中的表示法不一样,它由0-9,A-F组成,字母不区分大小写。与10进制的对应关系是:0-9对应0-9,A-F对应10-15。十六进制的数和二进制数可以按位对应(十六进制一位对应二进制四位),因此常应用在计算机语言中。
【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第6张图片

十进制转化十六进制的方法

用十进制数除以16,分别取余数和商数,商数为0的时候,将余数倒着数就是转化后的结果
【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第7张图片

2.6.4 C语言如何表示相应进制数
十进制 以正常数字1-9开头,如123
八进制 以数字0开头,如0123
十六进制 以0x开头,如0x123
二进制 C语言不能直接书写二进制数

代码示例:

#include 

int main()
{
	int a = 123;		//十进制方式赋值
	int b = 0123;		//八进制方式赋值, 以数字0开头
	int c = 0xABC;	//十六进制方式赋值

	//如果在printf中输出一个十进制数那么用%d,八进制用%o,十六进制是%x
	printf("十进制:%d\n",a );
	printf("八进制:%o\n", b);	//%o,为字母o,不是数字
	printf("十六进制:%x\n", c);

	return 0;
}

输出结果:

十进制:123
八进制:123
十六进制:abc
请按任意键继续. . .
2.7 计算机内存数值的存储方式
2.7.1 原码

一个数的原码(原始的二进制码)有以下特点:

1、最高位做为符号位,0表示正,为1表示负

2、其它数值部分就是数值本身绝对值的二进制数

3、负数的原码是在其绝对值的基础上,最高位变为1

十进制数 原码
+15 0000 1111
-15 1000 1111
+0 0000 0000
-0 1000 0000

说明:原码表示法简单易懂,与带符号数本身转换方便,只要符号还原即可,但当两个正数相减或不同符号数相加时,必须比较两个数哪个绝对值大,才能决定谁减谁,才能确定结果是正还是负,所以原码不便于加减运算。

2.7.2 反码

反码的特点:

1、对于正数,反码与原码相同

2、对于负数,符号位不变,其它部分取反(1变0,0变1)

十进制数 反码
+15 0000 1111
-15 1111 0000
+0 0000 0000
-0 1111 1111

说明:反码运算也不方便,通常用来作为求补码的中间过渡

2.7.3 补码

在计算机系统中,数字一律用补码来存储

补码的特点:

1、对于正数,原码、反码、补码相同

2、对于负数,其补码为它的反码加1

3、补码符号为不动,其他位取反,最后整个数加1,得到原码

十进制数 补码
+15 0000 1111
-15 1111 0001
+0 0000 0000
-0 0000 0000

代码示例:

#include 

ints main()
{
	int  a = -15;

	printf("%x\n", a);
	//结果为 fffffff1
	//fffffff1对应的二进制:1111 1111 1111 1111 1111 1111 1111 0001
	//符号位不变,其它取反:1000 0000 0000 0000 0000 0000 0000 1110
	//上面加1:1000 0000 0000 0000 0000 0000 0000 1111  最高位1代表负数,就是-15

	return 0;
}
2.7.4 补码的意义

示例1:用8位二进制数分别表示+0和-0

十进制数 原码
+0 0000 0000
-0 1000 0000
十进制数 反码
+0 0000 0000
-0 1111 1111

说明:不管以原码方式存储,还是以反码方式存储,0也有两种表示形式。为什么同样一个0有两种不同的表示方法呢?

但是如果以补码的方式存储,补码统一了零的编码:

十进制数 补码
+0 0000 0000
-0 10000 0000由于只用8位描述,最高位1丢弃,变为0000 0000

示例2:计算9-6的结果

以原码的方式相加:

十进制数 原码
9 0000 1001
-6 1000 0110

结果为-15,不正确

以补码方式相加:

十进制数 补码
9 0000 1001
-6 1111 1010

【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第8张图片
最高位的1溢出,剩余8位二进制表示的是3,结果正确

在计算机系统中,数值一律使用补码来存储,主要原因是:

1、统一了零的编码

2、将符号位和其他位统一处理

3、将减法运算转变为加法运算

4、两个用补码表示的数相加时,如果最高位(有符号)有进位,则进位被舍弃

2.7.5 数值溢出

当超过一个数据类型能够存放最大的范围时,数值会溢出

有符号位最高位溢出的区别:符号位溢出会导致数的正负发生改变,但最高位的溢出会导致最高位丢失

数据类型 占用空间 取值范围
char 1字节 -128到 127(-27 ~ 27-1)
unsigned char 1字节 0 到 255(0 ~ 28-1)

代码示例:

#include 

int main()
{
	char ch;

	//符号位溢出会导致数的正负发生改变
	ch = 0x7f + 2; //127+2
	printf("%d\n", ch);
	//	0111 1111
	//+2后 1000 0001,这是负数补码,其原码为 1111 1111,结果为-127

	//最高位的溢出会导致最高位丢失
	unsigned char ch2;
	ch2 = 0xff + 1; //255+1
	printf("%u\n", ch2);
	//	  1111 1111
	//+1后 10000 0000, char只有8位最高位的溢出,结果为0000 0000,十进制为0

	ch2 = 0xff + 2; //255+1
	printf("%u\n", ch2);
	//	  1111 1111
	//+1后 10000 0001, char只有8位最高位的溢出,结果为0000 0001,十进制为1

	return 0;
}

输出结果:

-127
0
1
请按任意键继续. . .
2.8 类型限定符
限定符 含义
extern 声明一个变量,extern声明的变量没有建立存储空间。 extern int a;//变量在定义的时候创建存储空间
const 定义一个常量,常量的值不能修改。 const int a = 10;
Volatile 防止编译器优化代码
register 定义寄存器变量,提高效率。register是建议型的指令,而不是命令型的指令,如果CPU有空闲寄存器,那么register就生效,如果没有空闲寄存器,那么register无效。
2.9 字符串格式化输出和输入
2.9.1 字符串常量

1、字符串是内存中一段连续的char空间,以’\0’(数字0)结尾

2、字符串常量是由双引号括起来的字符序列,如“china”、“C program”,“$12.5”等都是合法的字符串常量

字符常量与字符串常量的不同

【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第9张图片
每个字符串的结尾,编译器会自动的添加一个结束标志位’\0’,即 “a” 包含两个字符’a’和’\0’

2.9.2 printf函数和putchar函数

printf是输出一个字符串,putchar输出一个char

printf格式字符:

打印格式 对应数据类型 含义
%d int 接受整数值并将它表示为有符号的十进制整数
%hd short int 短整数
%hu unsigned short 无符号短整数
%o unsigned int 无符号8进制整数
%u unsigned int 无符号10进制整数
%x,%X unsigned int 无符号16进制整数,x对应的是abcdef,X对应的是ABCDEF
%f float 单精度浮点数
%lf double 双精度浮点数
%e,%E double 科学计数法表示的数,此处"e"的大小写代表在输出时用的"e"的大小写
%c char 字符型。可以把输入的数字按照ASCII码相应转换为对应的字符
%s char * 字符串。输出字符串中的字符直至字符串中的空字符(字符串以’\0‘结尾,这个’\0’即空字符)
%p void * 以16进制形式输出指针
%% % 输出一个百分号

printf附加格式:

字符 含义
l(字母l) 附加在d,u,x,o前面,表示长整数
- 左对齐
m(代表一个整数) 数据最小宽度
0(数字0) 将输出的前面补上0直到占满指定列宽为止不可以搭配使用-
m.n(代表一个整数) m指域宽,即对应的输出项在输出设备上所占的字符数。n指精度,用于说明输出的实型数的小数位数。对数值型的来说,未指定n时,隐含的精度为n=6位。

代码示例:

#include 
int main()
{
	int a = 100;
	printf("a = %d\n", a);//格式化输出一个字符串
	printf("%p\n", &a);//输出变量a在内存中的地址编号
	printf("%%d\n");

	char c = 'a';
	putchar(c);//putchar只有一个参数,就是要输出的char
	long a2 = 100;
	printf("%ld, %lx, %lo\n", a2, a2, a2);

	long long a3 = 1000;
	printf("%lld, %llx, %llo\n", a3, a3, a3);

	int abc = 10;
	printf("abc = '%6d'\n", abc);
	printf("abc = '%-6d'\n", abc);
	printf("abc = '%06d'\n", abc);
	printf("abc = '%-06d'\n", abc);

	double d = 12.3;
	printf("d = \' %-10.3lf \'\n", d);

	return 0;
}

输出结果:

a = 100
0046F834
%d
a100, 64, 144
1000, 3e8, 1750
abc = '    10'
abc = '10    '
abc = '000010'
abc = '10    '
d = ' 12.300     '
请按任意键继续. . .
2.9.3 scanf函数与getchar函数

1、getchar是从标准输入设备读取的一个char

2、scanf通过%d转义的方式可以得到用户通过标准输入设备输入的数据

代码示例:

#include 

int main()
{
	char ch1;
	char ch2;
	char ch3;
	int a;
	int b;

	printf("请输入ch1的字符:");
	ch1 = getchar();
	printf("ch1 = %c\n", ch1);

	getchar(); //测试此处getchar()的作用

	printf("请输入ch2的字符:");
	ch2 = getchar();
	printf("\'ch2 = %ctest\'\n", ch2);

	getchar(); //测试此处getchar()的作用
	printf("请输入ch3的字符:");
	scanf("%c", &ch3);//这里第二个参数一定是变量的地址,而不是变量名
	printf("ch3 = %c\n", ch3);

	printf("请输入a的值:");
	scanf("%d", &a);
	printf("a = %d\n", a);

	printf("请输入b的值:");
	scanf("%d", &b);
	printf("b = %d\n", b);

	return 0;
}

输出结果:

请输入ch1的字符:a
ch1 = a
请输入ch2的字符:b
'ch2 = btest'
请输入ch3的字符:c
ch3 = c
请输入a的值:10
a = 10
请输入b的值:12
b = 12
请按任意键继续. . .


三、C语言学习第三天运算符与表达式

3.1 常用运算符分类
运算符类型 作用
算术运算符 用于处理四则运算
赋值运算符 用于将表达式的值赋给变量
比较运算符 用于表达式的比较,并返回一个真值或假值
逻辑运算符 用于根据表达式的值返回真值或假值
位运算符 用于处理数据的位运算
sizeof运算符 用于求字节数长度
3.2 算术运算符
运算符 术语 示例 结果
+ 正号 +3 3
- 负号 -3 -3
+ 10 + 5 15
- 10 - 5 5
* 10 * 5 50
/ 10 / 5 2
% 取模(取余) 10 % 3 1
++ 前自增 a=2; b=++a; a=3; b=3;
++ 后自增 a=2; b=a++; a=3; b=2;
前自减 a=2; b=–a; a=1; b=1;
后自减 a=2; b=a–; a=1; b=2;
3.3 赋值运算符
运算符 术语 示例 结果
= 赋值 a=2; b=3; a=2; b=3;
+= 加等于 a=0; a+=2; a=2;
-= 减等于 a=5; a-=3; a=2;
*= 乘等于 a=2; a*=2; a=4;
/= 除等于 a=4; a/=2; a=2;
%= 模等于 a=3; a%2; a=1;
3.4 比较运算符

C语言中的比较运算符,“真”用数字“1”表示,“假”用数字“0”表示

运算符 术语 示例 结果
== 相等于 4 == 3 0
!= 不等于 4 != 3 1
< 小于 4 < 3 0
> 大于 4 > 3 1
<= 小于等于 4 <= 3 0
>= 大于等于 4 >= 1 1
3.5 逻辑运算符
运算符 术语 示例 结果
! !a 如果a为假,则!a为真; 如果a为真,则!a为假。
&& a && b 如果a和b都为真,则结果为真,否则为假。
|| a || b 如果a和b有一个为真,则结果为真,二者都为假时,结果为假。
3.6 运算符优先级
优先级 运算符 名称或含义 使用形式 结合方向 说明
1 [] 数组下标 数组名[常量表达式] 左到右
() 圆括号 (表达式)/函数名(形参表)
. 成员选择(对象) 对象.成员名
-> 成员选择(指针) 对象指针->成员名
2 - 负号运算符 -表达式 右到左 单目运算符
~ 按位取反运算符 ~表达式
++ 自增运算符 ++变量名/变量名++
自减运算符 –变量名/变量名–
***** 取值运算符 *指针变量
& 取地址运算符 &变量名
! 逻辑非运算符 !表达式
(类型) 强制类型转换 (数据类型)表达式
sizeof 长度运算符 sizeof(表达式)
3 / 表达式/表达式 左到右 双目运算符
***** 表达式*表达式
% 余数(取模) 整型表达式%整型表达式
4 + 表达式+表达式 左到右 双目运算符
- 表达式-表达式
5 << 左移 变量<<表达式 左到右 双目运算符
>> 右移 变量>>表达式
6 > 大于 表达式>表达式 左到右 双目运算符
>= 大于等于 表达式>=表达式
< 小于 表达式<表达式
<= 小于等于 表达式<=表达式
7 == 等于 表达式==表达式 左到右 双目运算符
!= 不等于 表达式!= 表达式
8 & 按位与 表达式&表达式 左到右 双目运算符
9 ^ 按位异或 表达式^表达式 左到右 双目运算符
10 | 按位或 表达式|表达式 左到右 双目运算符
11 && 逻辑与 表达式&&表达式 左到右 双目运算符
12 || 逻辑或 表达式||表达式 左到右 双目运算符
13 ?: 条件运算符 表达式1? 表达式2: 表达式3 右到左 三目运算符
14 = 赋值运算符 变量=表达式 右到左
/= 除后赋值 变量/=表达式
*= 乘后赋值 变量*=表达式
%= 取模后赋值 变量%=表达式
+= 加后赋值 变量+=表达式
-= 减后赋值 变量-=表达式
<<= 左移后赋值 变量<<=表达式
>>= 右移后赋值 变量>>=表达式
&= 按位与后赋值 变量&=表达式
^= 按位异或后赋值 变量^=表达式
|= 按位或后赋值 变量|=表达式
15 逗号运算符 表达式,表达式,… 左到右
3.7 类型转换

数据有不同的类型,不同类型数据之间进行混合运算时必然涉及到类型的转换问题

转换的方法有两种:

1、自动转换(隐式转换):遵循一定的规则,由编译系统自动完成

2、强制类型转换:把表达式的运算结果强制转换成所需的数据类型

类型转换原则:

类型转换的原则:占用内存字节数少(值域小)的类型,向占用内存字节数多(值域大)的类型转换,以保证精度不降低

【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第10张图片

3.7.1 隐式类型转换

代码示例:

#include 

int main()
{
	int num = 5;
	printf("s1=%d\n", num / 2);
	printf("s2=%lf\n", num / 2.0);

	return 0;
}

输出结果:

s1=2
s2=2.500000
请按任意键继续. . .
3.7.2 强制类型转换

说明:强制类型转换指的是使用强制类型转换运算符,将一个变量或表达式转化成所需的类型,其基本语法格式如下所示:(类型说明符) (表达式)

代码示例:

#include 

int main()
{
	float x = 0;
	int i = 0;
	x = 3.6f;

	i = x;			//x为实型, i为整型,直接赋值会有警告
	i = (int)x;		//使用强制类型转换

	printf("x=%f, i=%d\n", x, i);

	return 0;
}

输出结果:

x=3.600000, i=3
请按任意键继续. . .


四、C语言学习第四天-程序流程结构

4.1 概述

C语言支持最基本的三种程序运行结构:顺序结构、选择结构、循环结构

1、顺序结构:程序按顺序执行,不发生跳转

2、选择结构:依据是否满足条件,有选择的执行相应的功能

3、循环结构:依据条件是否满足,循环多次执行某段代码

4.2 选择结构
4.2.1 if语句

【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第11张图片
代码示例:

#include 

int main()
{
	int a = 1;
	int b = 2;

	if (a > b)
	{
		printf("%d\n", a);
	}

	return 0;
} 
4.2.2 if…else语句

【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第12张图片
代码示例:

#include 
int main()
{
	int a = 1;
	int b = 2;

	if (a > b)
	{
		printf("%d\n", a);
	}
	else
	{
		printf("%d\n", b);
	}
	return 0;
}
4.2.3 if…else if …else

【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第13张图片
代码示例:

#include 

int main()
{
	unsigned int a;
	scanf("%u", &a);

	if (a < 10)
	{
		printf("个位\n");
	}
	else if (a < 100)
	{
		printf("十位\n");
	}
	else if (a < 1000)
	{
		printf("百位\n");
	}
	else
	{
		printf("很大\n");
	}

	return 0;
}
4.2.4 三目运算符
#include 

int main()
{
	int a = 10;
	int b = 20;
	int c;

	if (a > b)
	{
		c = a;
	}
	else
	{
		c = b;
	}
	printf("c1 = %d\n", c);

	a = 1;
	b = 2;
	c = ( a > b ? a : b );
	printf("c2 = %d\n", c);

	return 0;
}
4.2.5 switch语句
#include 

int main()
{
	char c;
	c = getchar();

	switch (c) //参数只能是整型变量
	{
	case '1':
		printf("OK\n");
		break;//switch遇到break就中断了
	case '2':
		printf("not OK\n");
		break;
	default://如果上面的条件都不满足,那么执行default
		printf("are u ok?\n");
	}
	return 0;
}
4.3 循环结构
4.3.1 while语句

【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第14张图片
代码示例:

#include 

int main()
{
	int a = 20;
	while (a > 10)
	{
		scanf("%d", &a);
		printf("a = %d\n", a);
	}

	return 0;
}
4.3.2 do…while语句

【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第15张图片
代码示例:

#include 

int main()
{
	int a = 1;
	do
	{
		a++;
		printf("a = %d\n", a);
	} while (a < 10);

	return 0;
}
4.3.3 for语句

代码示例:

#include 

int main()
{
	int i;
	int sum = 0;
	for (i = 0; i <= 100; i++)
	{
		sum += i;

	}

	printf("sum = %d\n", sum);

	return 0;
}

输出结果:

sum = 5050
请按任意键继续. . .
4.3.4 嵌套循环

循环语句之间可以相互嵌套

代码示例:

#include 

int main()
{
	int num = 0;
	int i, j, k;
	for (i = 0; i < 10; i++)
	{
		for (j = 0; j < 10; j++)
		{
			for (k = 0; k < 10; k++)
			{
				printf("hello world\n");
				num++;
			}
		}
	}

	printf("num = %d\n", num);

	return 0;
}
4.4 跳转语句break 、 continue 、 goto
4.4.1 break语句

在switch条件语句和循环语句中都可以使用break语句:

1、当它出现在switch条件语句中时,作用是终止某个case并跳出switch结构

2、当它出现在循环语句中,作用是跳出当前内循环语句,执行后面的代码

3、当它出现在嵌套循环语句中,跳出最近的内循环语句,执行后面的代码

代码示例:

#include 

int main()
{
	int i = 0;
	while (1)
	{
		i++;
		printf("i = %d\n", i);

		if (i == 10)
		{
			break; //跳出while循环
		}
	}

	int flag = 0;
	int m = 0;
	int n = 0;

	for (m = 0; m < 10; m++)
	{
		for (n = 0; n < 10; n++)
		{
			if (n == 5)
			{
				flag = 1;
				break; //跳出for (n = 0; n < 10; n++)
			}
		}

		if (flag == 1)
		{
			break; //跳出for (m = 0; m < 10; m++)
		}
	}

	return 0;
}
4.3.2 continue语句

在循环语句中,如果希望立即终止本次循环,并执行下一次循环,此时就需要使用continue语句

代码示例:

#include

int main()
{
	int sum = 0;           //定义变量sum

	for (int i = 1; i <= 100; i++)
	{
		if (i % 2 == 0)   //如果i是一个偶数,执行if语句中的代码
		{
			continue;      //结束本次循环
		}
		sum += i;          //实现sum和i的累加
	}

	printf("sum = %d\n", sum);

	return 0;
}
4.3.3 goto语句(无条件跳转,尽量少用)

代码示例:

#include 

int main()
{
	goto End; //无条件跳转到End的标识
	printf("aaaaaaaaa\n");

End:
	printf("bbbbbbbb\n");

	return 0;
}


五、C语言学习第五天之数组和字符串

5.1 概述

数组就是在内存中连续的相同类型的变量空间。同一个数组所有的成员都是相同的数据类型,同时所有的成员在内存中的地址是连续的

数组属于构造数据类型:

1、一个数组可以分解为多个数组元素:这些数组元素可以是基本数据类型或构造类型

int a[10];  
struct Stu boy[10];

2、按数组元素类型的不同,数组可分为:数值数组、字符数组、指针数组、结构数组等类别

int a[10];
char s[10];
char *p[10];

说明:通常情况下,数组元素下标的个数也称为维数,根据维数的不同,可将数组分为一维数组、二维数组、三维数组、四维数组等。通常情况下,我们将二维及以上的数组称为多维数组

5.2 一维数组
5.2.1 一维数组的定义和使用

1、数组名字符合标识符的书写规定(数字、英文字母、下划线)

2、数组名不能与其它变量名相同,同一作用域内是唯一的

3、方括号[]中常量表达式表示数组元素的个数

int a[3]表示数组a有3个元素
其下标从0开始计算,因此3个元素分别为a[0],a[1],a[2]

4、定义数组时[]内最好是常量,使用数组时[]内即可是常量,也可以是变量

代码示例:

#include 

int main()
{
	int a[10];//定义了一个数组,名字叫a,有10个成员,每个成员都是int类型
	//a[0]…… a[9],没有a[10]
	//没有a这个变量,a是数组的名字,但不是变量名,它是常量
	a[0] = 0;
	//……
	a[9] = 9;

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		a[i] = i; //给数组赋值
	}

	//遍历数组,并输出每个成员的值
	for (i = 0; i < 10; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");

	return 0;
}
5.2.2 一维数组的初始化

在定义数组的同时进行赋值,称为初始化。全局数组若不初始化,编译器将其初始化为零。局部数组若不初始化,内容为随机值。

代码示例:

int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//定义一个数组,同时初始化所有成员变量
	int a[10] = { 1, 2, 3 };//初始化前三个成员,后面所有元素都设置为0
	int a[10] = { 0 };//所有的成员都设置为0
	
	 //[]中不定义元素个数,定义时必须初始化
	int a[] = { 1, 2, 3, 4, 5 };//定义了一个数组,有5个成员
5.2.3 数组名

数组名是一个地址的常量,代表数组汇总首元素的地址

代码示例:

#include 

int main()
{
	int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//定义一个数组,同时初始化所有成员变量

	printf("a = %p\n", a);
	printf("&a[0] = %p\n", &a[0]);

	int n = sizeof(a); //数组占用内存的大小,10个int类型,10 * 4  = 40
	int n0 = sizeof(a[0]);//数组第0个元素占用内存大小,第0个元素为int,4

	int i = 0;
	for (i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");

	return 0;
}

输出结果:

a = 0029FDF4
&a[0] = 0029FDF4
12345678910
请按任意键继续. . .
5.2.4 一维数组的强化训练
一维数组的最值

代码示例:

#include 

int main()
{
	int a[] = {  1, -2, 3,- 4, 5, -6, 7, -8, -9, 10 };//定义一个数组,同时初始化所有成员变量

	int i = 0;
	int max = a[0];
	for (i = 1; i < sizeof(a) / sizeof(a[0]); i++)
	{
		if (a[i] > max)
		{
			max = a[i];
		}
	}
	printf("数组中最大值为:%d\n", max);

	return 0;
}

输出结果:

数组中最大值为:10
请按任意键继续. . .
一维数组的逆置
#include 

int main()
{
	int a[] = { 1, -2, 3,-4, 5, -6, 7, -8, -9, 10 };//定义一个数组,同时初始化所有成员变量

	int i = 0;
	int j = sizeof(a) / sizeof(a[0]) - 1;
	int temp;

	while (i < j)
	{
		temp = a[i];
		a[i] = a[j];
		a[j] = temp;

		i++;
		j--;
	}

	for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	{
		printf("%d  ", a[i]);
	}
	printf("\n");



	return 0;
}

输出结果:

10  -9  -8  7  -6  5  -4  3  -2  1
请按任意键继续. . .
冒泡排序

代码示例:

#include 

int main()
{
	int a[] = { 1, -2, 3,-4, 5, -6, 7, -8, -9, 10 };//定义一个数组,同时初始化所有成员变量

	int len = sizeof(a) / sizeof(a[0]);

	int temp;

	printf("排序前的数组:");
	for (int i = 0; i < len; i++)
	{
		printf("%d  ", a[i]);
	}
	printf("\n");


	for (int i = 0; i < len - 1; i++)
	{
		for (int j = 0; j < len - i - 1; j++)
		{
			//从小到大排序
			if (a[j] > a[j + 1])
			{
				temp = a[j];
				a[j] = a[j + 1];
				a[j + 1] = temp;
			}
		}
	}

	printf("排序后的数组:");
	for (int i = 0; i < len; i++)
	{
		printf("%d  ", a[i]);
	}
	printf("\n");



	return 0;
}

输出结果:

排序前的数组:1  -2  3  -4  5  -6  7  -8  -9  10
排序后的数组:-9  -8  -6  -4  -2  1  3  5  7  10
请按任意键继续. . .
5.3 二维数组
5.3.1 二维数组的定义和使用

二维数组定义的一般形式是:

类型说明符 数组名[常量表达式1][常量表达式2]

其中常量表达式1表示第一维下标的长度,常量表达式2 表示第二维下标的长度

二维数组的说明:

1、命名规则同一维数组

2、定义了一个三行四列的数组,数组名为a其元素类型为整型,该数组的元素个数为3×4个,即:
【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第16张图片
二维数组a是按行进行存放的,先存放a[0]行,再存放a[1]行、a[2]行,并且每行有四个元素,也是依次存放的

3、二维数组在概念上是二维的:其下标在两个方向上变化,对其访问一般需要两个下标

4、在内存中并不存在二维数组,二维数组实际的硬件存储器是连续编址的,也就是说内存中只有一维数组,即放完一行之后顺次放入第二行,和一维数组存放方式是一样的

代码示例:

#include 

int main()
{
	//定义了一个二维数组,名字叫a
	//由3个一维数组组成,这个一维数组是int [4]
	//这3个一维数组的数组名分别为a[0],a[1],a[2]
	int a[3][4];

	a[0][0] = 0;
	//……
	a[2][3] = 12;

	//给数组每个元素赋值
	int i = 0;
	int j = 0;
	int num = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 4; j++)
		{
			a[i][j] = num++;
		}
	}

	//遍历数组,并输出每个成员的值
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 4; j++)
		{
			printf("%d, ", a[i][j]);
		}
		printf("\n");
	}

	return 0;
}

输出结果:

0, 1, 2, 3,
4, 5, 6, 7,
8, 9, 10, 11,
请按任意键继续. . .
5.3.2 二维数组的初始化

代码示例:

//分段赋值 	int a[3][4] = {{ 1, 2, 3, 4 },{ 5, 6, 7, 8, },{ 9, 10, 11, 12 }};
	int a[3][4] = 
	{ 
		{ 1, 2, 3, 4 },
		{ 5, 6, 7, 8, },
		{ 9, 10, 11, 12 }
	};

	//连续赋值
	int a[3][4] = { 1, 2, 3, 4 , 5, 6, 7, 8, 9, 10, 11, 12  };

	//可以只给部分元素赋初值,未初始化则为0
	int a[3][4] = { 1, 2, 3, 4  };

	//所有的成员都设置为0
	int a[3][4] = {0};

	//[]中不定义元素个数,定义时必须初始化
	int a[][4] = { 1, 2, 3, 4, 5, 6, 7, 8};
5.3.3 数组名

数组名是一个地址的常量,代表数组中首元素的地址

代码示例:

#include

int main()
{
	//定义了一个二维数组,名字叫a
	//二维数组的本质上还是一维数组,此一维数组有3个元素
	//每个元素又是一个一维数组int[4]
	int a[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
	
	//数组名为数组首元素地址,二维数组的第0个元素为一维数组
	//第0个一维数组的数组名为a[0]

	printf("a = %p\n", a);
	printf("a[0] = %p\n", a[0]);

	//测二维数组所占内存空间,有3个一维数组,每个一维数组的空间为4*4
	//sizeof(a) = 3 * 4 * 4 = 48
	printf("sizeof(a) = %d\n", sizeof(a));

	//测第0个元素所占内存空间,a[0]为第0个一维数组int[4]的数组名,4*4=16
	printf("sizeof(a[0]) = %d\n", sizeof(a[0]));

	//测第0行0列元素所占内存空间,第0行0列元素为一个int类型,4字节
	printf("sizeof(a[0][0]) = %d\n", sizeof(a[0][0]));

	//求二维数组行数
	printf("i = %d\n", sizeof(a) / sizeof(a[0]));

	// 求二维数组列数
	printf("j = %d\n", sizeof(a[0]) / sizeof(a[0][0]));

	//求二维数组元素的总个数,行 * 列总数
	printf("n = %d\n", sizeof(a) / sizeof(a[0][0]));

	return 0;
}

输出结果:

a = 004EF858
a[0] = 004EF858
sizeof(a) = 48
sizeof(a[0]) = 16
sizeof(a[0][0]) = 4
i = 3
j = 4
n = 12
请按任意键继续. . .
5.3.4 二维数组强化训练

代码示例:

#include

int main()
{
	//二维数组:五行、三列
	//行代表人:老大到老五
	//列代表科目:语文、数学、英语

	float a[5][3] = {
		{80,75,76},

		{59,65,71},
		{59,63,70},
		{85,45,90},
		{76,77,45}
	};

	int i, j, person_low[3] = { 0 };
	float s = 0, lesson_aver[3] = { 0 };

	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++) 
		{
			s = s + a[j][i];
			if (a[j][i] < 60)
			{
				person_low[i]++;
			}
		}

		lesson_aver[i] = s / 5;
		s = 0;
	}

	printf("各科的平均成绩为:\n");
	for (int i = 0; i < 3; i++)
	{
		printf("%.2f\n", lesson_aver[i]);
	}

	printf("各科不及格的人数:\n");
	for (int i = 0; i < 3; i++)
	{
		printf("%d\n", person_low[i]);
	}

	return 0;
}

输出结果:

各科的平均成绩为:
71.80
65.00
70.40
各科不及格的人数:
2
1
1
请按任意键继续. . .
5.5 字符数组和字符串
5.5.1 字符数组和字符串的区别

1、C语言中没有字符串这种数据类型,可以通过char的数组来替代

2、字符串一定是一个char的数组,但char的数组未必是字符串

3、数字0(和字符‘\0’等价)结尾的char数组就是一个字符串,但如果char数组没有以数字0结尾,那么就不是一个字符串,只是普通字符数组,所以字符串是一种特殊的char的数组

代码示例:

#include 

int main()
{
	char c1[] = { 'c', ' ', 'p', 'r', 'o', 'g' }; //普通字符数组
	printf("c1 = %s\n", c1); //乱码,因为没有’\0’结束符

	//以‘\0’(‘\0’就是数字0)结尾的字符数组是字符串
	char c2[] = { 'c', ' ', 'p', 'r', 'o', 'g', '\0'}; 
	printf("c2 = %s\n", c2);

	//字符串处理以‘\0’(数字0)作为结束符,后面的'h', 'l', 'l', 'e', 'o'不会输出
	char c3[] = { 'c', ' ', 'p', 'r', 'o', 'g', '\0', 'h', 'l', 'l', 'e', 'o', '\0'};
	printf("c3 = %s\n", c3);

	return 0;
}

输出结果:

c1 = c prog烫烫烫J刣?
c2 = c prog
c3 = c prog
请按任意键继续. . .
5.5.2 字符串的初始化

代码示例:

#include 

// C语言没有字符串类型,通过字符数组模拟
// C语言字符串,以字符‘\0’, 数字0
int main()
{
	//不指定长度, 没有0结束符,有多少个元素就有多长
	char buf[] = { 'a', 'b', 'c' };
	printf("buf = %s\n", buf);	//乱码
								
	char buf2[100] = { 'a', 'b', 'c' };//指定长度,后面没有赋值的元素,自动补0
	/*char buf[1000] = { “hello” };*/
	printf("buf2 = %s\n", buf2);

	//所有元素赋值为0
	char buf3[100] = { 0 };

	//char buf4[2] = { '1', '2', '3' };//数组越界

	char buf5[50] = { '1', 'a', 'b', '0', '7' };
	printf("buf5 = %s\n", buf5);

	char buf6[50] = { '1', 'a', 'b', 0, '7' };
	printf("buf6 = %s\n", buf6);

	char buf7[50] = { '1', 'a', 'b', '\0', '7' };
	printf("buf7 = %s\n", buf7);

	//使用字符串初始化,编译器自动在后面补0,常用
	char buf8[] = "agjdslgjlsdjg";

	//'\0'后面最好不要连着数字,有可能几个数字连起来刚好是一个转义字符
	//'\ddd'八进制字义字符,'\xdd'十六进制转移字符
	// \012相当于\n
	char str[] = "\012abc";
	printf("str == %s\n", str);

	return 0;
}

输出结果:

buf = abc烫烫跳?赭$
buf2 = abc
buf5 = 1ab07
buf6 = 1ab
buf7 = 1ab
str ==
abc
请按任意键继续. . .
5.5.3 字符串的输入输出

由于字符串采用了 ‘\0’标志,字符串的输入和输出将变得简单方便

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include 

int main()
{
	char str[100];

	printf("input string1 : \n");
	scanf("%s", str);
	printf("output:%s\n", str);

	return 0;
}

输出结果:

input string1 :
wangjiabo
output:wangjiabo
请按任意键继续. . .
1、gets()函数

函数介绍:

#include 
char *gets(char *s);
功能:从标准输入读入字符,并保存到s指定的内存空间,直到出现换行符或读到文件结尾为止。
参数: 
	s:字符串首地址
返回值:
	成功:读入的字符串
	失败:NULL

gets(str)与scanf(“%s”,str)的区别:

1、gets(str)允许输入的字符串含有空格

2、scanf(“%s”,str)不允许含有空格

注意:由于scanf()和gets()无法知道字符串s大小,必须遇到换行符或读到文件结尾为止才结束输入,因此容易导致字符数组越界(缓冲区溢出)的情况。

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include "stdio.h"

int main()
{
	char str[100];
	printf("请输入str: ");
	gets_s(str);
	printf("str = %s\n", str);

	return 0;

}

输出结果:

请输入str: wang jia bo
str = wang jia bo
请按任意键继续. . .

注意:VS2015使用的是新C标准,也就是C11,在新标准中,应该是用gets_s代替gets

gets_s的具体用法是:

gets_s(char *buff,size)
2、fgets()函数

函数介绍:

#include 
char *fgets(char *s, int size, FILE *stream);
功能:从stream指定的文件内读入字符,保存到s所指定的内存空间,直到出现换行字符、读到文件结尾或是已读了size - 1个字符为止,最后会自动加上字符 '\0' 作为字符串结束。
参数:
	s:字符串
	size:指定最大读取字符串的长度(size - 1)
	stream:文件指针,如果读键盘输入的字符串,固定写为stdin
返回值:
	成功:成功读取的字符串
	读到文件尾或出错: NULL

说明:fgets()在读取一个用户通过键盘输入的字符串的时候,同时把用户输入的回车也做为字符串的一部分。通过scanfgets输入一个字符串的时候,不包含结尾的==“\n”==,但通过fgets结尾多了“\n”fgets()函数是安全的,不存在缓冲区溢出的问题。

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include "stdio.h"

int main()
{
	char str[100];
	printf("请输入str: ");
	fgets(str, sizeof(str), stdin);
	printf("str = \"%s\"\n", str);

	return 0;
}

输出结果:

请输入str: wang jia bo
str = "wang jia bo
"
请按任意键继续. . .
3、puts()函数

函数介绍:

#include 
int puts(const char *s);
功能:标准设备输出s字符串,在输出完成后自动输出一个'\n'。
参数:
	s:字符串首地址
返回值:
	成功:非负数
	失败:-1

代码示例:

#include 

int main()
{
	printf("hello world");
	puts("hello world");

	return 0;
}

输出结果:

hello worldhello world
请按任意键继续. . .
4、fputs()函数

函数介绍:

#include 
int fputs(const char * str, FILE * stream);
功能:将str所指定的字符串写入到stream指定的文件中, 字符串结束符 '\0'  不写入文件。 
参数:
	str:字符串
	stream:文件指针,如果把字符串输出到屏幕,固定写为stdout
返回值:
	成功:0
	失败:-1

注意:fputs()是puts()的文件操作版本,但fputs()不会自动输出一个’\n’

代码示例:

#include 

int main()
{
	printf("hello world");
	puts("hello world");
	fputs("hello world", stdout);

	return 0;
}

输出结果:

hello worldhello world
hello world请按任意键继续. . .
5、strlen()函数

函数介绍:

#include 
size_t strlen(const char *s);
功能:计算指定指定字符串s的长度,不包含字符串结束符‘\0’
参数:
s:字符串首地址
返回值:字符串s的长度,size_t为unsigned int类型

代码示例:

#include 
#include

int main()
{
	char str[] = "abc\0defg";
	int n = strlen(str);
	printf("n = %d\n", n);

	return 0;
}

输出结果:

n = 3
请按任意键继续. . .
5.5.4 字符串数组强化:字符串追加

代码示例:

#include 

int main()
{
	char str1[] = "abcdef";
	char str2[] = "123456";
	char dst[100];

	int i = 0;
	while (str1[i] != 0)
	{
		dst[i] = str1[i];
		i++;
	}

	int j = 0;
	while (str2[j] != 0)
	{
		dst[i + j] = str2[j];
		j++;
	}
	dst[i + j] = 0;  //字符串结束符

	printf("dst = %s\n", dst);


	return 0;
}

输出结果:

dst = abcdef123456
请按任意键继续. . .


六 、C语言学习第六天之函数

6.1 概述
6.1.1 函数分类

C 程序是由函数组成的,我们写的代码都是由主函数 main()开始执行的。函数是 C 程序的基本模块,是用于完成特定任务的程序代码单元。

从函数定义角度看,函数可以分为系统函数和用户定义函数两种:

1、系统函数,即库函数:这是有编译系统提供的,用户不必自己定义这些函数,可以直接使用它们,例如我们常用的打印函数printf()

2、用户定义函数:用以解决用户的专门需求

6.1.2 函数的作用

1、函数的使用可以省去重复代码的编写,降低代码的重复率

代码示例:

// 求两数的最大值
int max(int a, int b)
{
	if (a > b){
		return a;
	}
	else{
		return b;
	}
}

int main()
{
	// 操作1 ……
	// ……
	int a1 = 10, b1 = 20, c1 = 0;
	c1 = max(a1, b1); // 调用max()

	// 操作2 ……
	// ……
	int a2 = 11, b2 = 21, c2 = 0;
	c2 = max(a2, b2); // 调用max()

	// ……

	return 0;
}

2、函数可以让程序更加模块化,从而有利于程序的阅读、修改和完善

假如我们编写一个实现以下功能的程序:读入一行数字;对数字进行排序;找到它们的平均值;打印出一个柱状图。如果我们把这些操作直接写在main()里,这样可能会给用户感觉代码会有点凌乱。但,假如我们使用函数,这样可以让程序更加清晰、模块化:

代码示例:

#include 

int main()
{
	float list[50];

	// 这里只是举例,函数还没有实现
	readlist(list, 50);
	sort(list, 50);
	average(list, 50);
	bargraph(list, 50);

	return 0;
}
6.1.3 函数的调用:产生随机数

当调用函数时,需要关心5要素:

1、头文件:包含指定的头文件

2、函数名字:函数名字必须和头文件声明的名字一样

3、功能:需要知道此函数的作用后才进行调用

4、参数:参数类型要匹配

5、返回值:根据需要接收返回值

代码示例:

#include 
time_t time(time_t *t);
功能:获取当前系统时间
参数:常设置为NULL
返回值:当前系统时间, time_t 相当于long类型,单位为毫秒

#include 
void srand(unsigned int seed);
功能:用来设置rand()产生随机数时的随机种子
参数:如果每次seed相等,rand()产生随机数相等
返回值:无

#include 
int rand(void);
功能:返回一个随机数值
参数:无
返回值:随机数
#include 
#include 
#include 

int main()
{
	time_t tm = time(NULL);//得到系统时间
	srand((unsigned int)tm);//随机种子只需要设置一次即可

	int r = rand();
	printf("r = %d\n", r);

	return 0;
}
6.2 函数的定义
6.2.1 函数定义的格式
返回值类型 函数名(形参列表)
{
    数据定义部分;
    执行语句部分;
}
6.2.2 函数名字、形参、函数体、返回值
1、函数名

理论上是可以随意起名字,最好起的名字见名知意,应该让用户看到这个函数名字就知道这个函数的功能。注意,函数名的后面有个圆换号(),代表这个为函数,不是普通的变量名

2、形参列表

在定义函数时指定的形参,在未出现函数调用时,它们并不占内存中的存储单元,因此称它们为形式参数或者虚拟参数,简称形参,表示它们并不是实际存在的数据,所以,形参里的变量是不能赋值

代码示例:

void max(int a = 10, int b = 20) // error, 形参不能赋值
{
}

在定义函数时指定的形参,必须是,类型+变量的形式

代码示例:

//1: right, 类型+变量
void max(int a, int b)
{
}

//2: error, 只有类型,没有变量
void max(int, int)
{
}

//3: error, 只有变量,没有类型
int a, int b;
void max(a, b)
{
}

在定义函数时指定的形参,可有可无,根据函数的需求来设计,如果没有形参,圆括号内容为空,或者写一个void关键字

代码示例:

// 没形参, 圆括号内容为空
void max()
{
}

// 没形参, 圆括号内容为void关键字
void max(void)
{
}
3、函数体

花括号{ }里的内容即为函数体的内容,这里为函数功能实现的过程,这和以前的写代码没太大区别,以前我们把代码写在main()函数里,现在只是把这些写到别的函数里

4、返回值

函数的返回值是通过函数中的return语句获得的,return后面的值也可以是一个表达式

返回值的几个注意点:

  • 尽量保证return语句中表达式的值和函数返回类型是同一类型
int max() // 函数的返回值为int类型
{
	int a = 10;
	return a;// 返回值a为int类型,函数返回类型也是int,匹配
}

  • 如果函数返回的类型和return语句中表达式的值不一致,则以函数返回类型为准,即函数返回类型决定返回值的类型。对数值型数据,可以自动进行类型转换
double max() // 函数的返回值为double类型
{
	int a = 10;
	return a;// 返回值a为int类型,它会转为double类型再返回
}

提醒:如果函数返回的类型和return语句中表达式的值不一致,而它又无法自动进行类型转换,程序则会报错

  • return语句的另一个作用为中断return所在的执行函数,类似于break中断循环、switch语句一样
int max()
{
	return 1;// 执行到,函数已经被中断,所以下面的return 2无法被执行到
	return 2;// 没有执行
}
  • 如果函数带返回值,return后面必须跟着一个值,如果函数没有返回值,函数名字的前面必须写一个void关键字,这时候,我们写代码时也可以通过return中断函数(也可以不用),只是这时,return后面不带内容( 分号“;”除外)
void max()// 最好要有void关键字
{
	return; // 中断函数,这个可有可无
}
6.3 函数的调用

定义函数后,我们需要调用此函数才能执行到这个函数里的代码段。这和main()函数不一样,main()为编译器设定好自动调用的主函数,无需人为调用,我们都是在main()函数里调用别的函数,一个 C 程序里有且只有一个main()函数

6.3.1 函数执行流程
#include 

void print_test()
{
	printf("this is for test\n");
}

int main()
{
	print_test();	// print_test函数的调用

	return 0;
}

1) 进入main()函数

2) 调用print_test()函数

​ a. 它会在main()函数的前寻找有没有一个名字叫“print_test”的函数定义;

​ b. 如果找到,接着检查函数的参数,这里调用函数时没有传参,函数定义也没有形参,参数类型匹配;

​ c. 开始执行print_test()函数,这时候,main()函数里面的执行会阻塞( 停 )在print_test()这一行代码,等待print_test()函数的执行。

3) print_test()函数执行完( 这里打印一句话 ),main()才会继续往下执行,执行到return 0, 程序执行完毕。

6.3.2 函数的形参和实参

1、形参出现在函数定义中,在整个函数体内都可以使用,离开该函数则不能使用

2、实参出现在主调函数中,进入被调函数后,实参也不能使用

3、实参变量对形参变量的数据传递是“值传递”,即单向传递,只由实参传给形参,而不能由形参传回来给实参

4、在调用函数时,编译系统临时给形参分配存储单元。调用结束后,形参单元被释放

5、实参单元和形参单元是不同的单元。调用结束后,形参单元被释放,函数调用结束后返回主调函数后则不能再使用该形参变量。实参单元仍保留并维持原值。因此,在执行一个被调函数时,形参的值如果发生改变,并不会改变主调函数中实参的值

6.3.3 无参函数调用

如果是调用无参函数,则不能加上“实参”,但在调用时括号不能省略

// 函数的定义
void test()
{
}

int main()
{
	// 函数的调用
	test();	// right, 圆括号()不能省略
	test(250); // error, 函数定义时没有参数

return 0;
}
6.3.4 有参函数调用
1、如果实参列表包含多个实参,则各个参数之间用逗号隔开
// 函数的定义
void test(int a, int b)
{
}

int main()
{
	int p = 10, q = 20;
	test(p, q);	// 函数的调用

	return 0;
}
2、实参与形参的个数应相等,类型应该匹配(相同或赋值兼容)。实参与形参的顺序对应,一对一地传递数据
3、实参可以是常量、变量或者表达式,无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传递给形参。 所以,这里的变量是在圆括号()外面定义、赋好值的变量。
/ 函数的定义
void test(int a, int b)
{
}

int main()
{
	// 函数的调用
	int p = 10, q = 20;
	test(p, q);	// right
	test(11, 30 - 10); // right

	test(int a, int b); // error, 不应该在圆括号里定义变量

	return 0;
}
6.3.5 函数返回值
1、如果函数定义没有返回值,函数调用时不能写void关键字,调用时也不能接收函数的返回值

代码示例:

// 函数的定义
void test()
{
}

int main()
{
	// 函数的调用
	test(); // right
	void test(); // error, void关键字只能出现在定义,不可能出现在调用的地方
	int a = test();	// error, 函数定义根本就没有返回值

	return 0;
}
2、如果函数定义有返回值,这个返回值我们根据用户的需求可用可不用,但是,假如我们需要使用这个函数的返回值,我们需要定义一个类型匹配的变量来对函数的返回值进行接收

代码示例:

// 函数的定义, 返回值为int类型
int test()
{
}

int main()
{
	// 函数的调用
	int a = test(); // right, a为int类型
	int b;
	b = test();	// right, 和上面等级

	char *p = test(); // 虽然调用成功没有意义, p为char *, 函数返回值为int, 类型不匹配

	// error, 必须定义一个匹配类型的变量来接收返回值
	// int只是类型,没有定义变量
	int = test();	
	
	// error, 必须定义一个匹配类型的变量来接收返回值
	// int只是类型,没有定义变量
	int test();
	
	return 0;
}
6.4 函数的声明

如果使用用户自己定义的函数,而该函数与调用它的函数(主调函数)不在同一文件中,或者函数定义的位置在主调函数之后,则必须在调用此函数之前对被调用的函数作声明。所谓函数声明,即使在函数尚未定义的情况下,事先将该函数的有关信息通知编译器,相当于告诉编译器,函数在后面定义,从而使得编译器能后正常运行

注意:一个函数只能被定义一次,但可以声明多次
#include 

int max(int x, int y); // 函数的声明,分号不能省略
// int max(int, int); // 另一种方式

int main()
{
	int a = 10, b = 25, num_max = 0;
	num_max = max(a, b); // 函数的调用

	printf("num_max = %d\n", num_max);

	return 0;
}

// 函数的定义
int max(int x, int y)
{
	return x > y ? x : y;
}
函数定义和声明的区别

1、定义是指对函数功能的确立,包括指定函数名、函数类型、形参及其类型、函数体等,它是一个完整的、独立的函数单位

2、声明的作用则是把函数的名字、函数类型以及形参的个数、类型和顺序(注意,不包括函数体)通知编译系统,以便在对包含函数调用的语句进行编译时,据此对其进行对照检查(例如函数名是否正确,实参与形参的类型和个数是否一致)

6.5 main函数与exit函数

在main函数中调用exit和return结果是一样的,但在子函数中调用return只是代表子函数终止了,在子函数中调用exit,那么程序终止

代码示例:

#include 
#include 

void fun()
{
	printf("fun\n");
	//return;
	exit(0);
}

int main()
{
	fun();
	while (1);

	return 0;
}
6.6 多文件(分文件)编写
6.6.1 分文件编程

1、 把函数声明放在头文件xxx.h中,在主函数中包含相应头文件

2、在头文件对应的xxx.c中实现xxx.h声明的函数

6.6.2 防止头文件重复包含

当一个项目比较大时,往往都是分文件,这时候有可能不小心把同一个头文件 include 多次,或者头文件嵌套包含

代码示例:

a.h 中包含 b.h

#include "b.h"

b.h 中包含 a.h

#include "a.h"

main.c 中使用其中头文件:

#include "a.h"

int main()
{
	return 0;
}

编译上面的例子,会出现如下错误:
【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第17张图片

为了避免同一个文件被include多次,C/C++中有两种方式,一种是 #ifndef 方式,一种是 #pragma once 方式

方法一代码示例:

#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__

// 声明语句

#endif

方法二代码示例:

#pragma once

// 声明语句


七 、C语言学习第七天之指针

7.1 概述
7.1.1 内存
内存的含义:

1、存储器:计算机的组成中,用来存储程序和数据,辅助CPU进行运算处理的重要部分

2、内存:内部存储器,暂存程序/数据–掉电丢失:SRAM、 DRAM、 DDR、DDR2 、DDR3

3、外存:外部存储器,长时间保存程序/数据–掉电不丢失 :ROM 、ERRROM、 FLASH(NAND 、NOR)、硬盘、光盘

内存是沟通CPU和硬盘的桥梁:

1、暂时存放CPU中的运算数据

2、暂存与硬盘等外部存储器交换的数据

7.1.2 物理存储器和存储地址空间
物理存储器:实际存在的具体存储器芯片

1、主板上装插的内存条

2、显示卡上的显示RAM芯片

3、各种适配卡上的RAM芯片和ROM芯片

存储地址空间:对存储器编码的范围,在软件上常说的内存是指这一层含义

1、编码:对每个物理存储单元(一个字节长度)分配一个号码

2、寻址:可以根据分配的号码找到相应的存储单元,完成数据的读写

7.1.3 内存地址

1、将内存抽象成一个很大的一维字符数组

2、编码就是对内存的每一个字节分配一个32位或者64位的号码(与32为或者64位处理器相关)

3、这个内存编码我们称之为内存地址

内存中的每一个数据都会分配相应的地址
7.1.4 指针和指针变量

1、内存中的每一个字节都有一个编号,这就是“地址”

2、如果在程序中定义了一个变量,在对程序进行编译或者运行时,系统就会给这个变量分配内存单元,并确定它的内存地址(编号)

3、指针的实质就是内存“地址”。指针就是地址,地址就是指针

4、指针是内存单元的编号,指针变量就是存放地址的变量

5、通常我们叙述时会把指针变量简称为指针,实际它们的含义并不一样

7.2 指针基础知识
7.2.1 指针变量的定义和使用

1、指针也是一种数据类型,指针变量也是一种变量

2、指针变量指向谁,就把谁的地址赋值给指针变量

3、“*”操作符操作的是指针变量指向的内存空间

代码示例:

#include 

int main()
{
	int a = 0;
	char b = 100;
	printf("%p, %p\n", &a, &b); //打印a, b的地址

	//int *代表是一种数据类型,int*指针类型,p才是变量名
	//定义了一个指针类型的变量,可以指向一个int类型变量的地址
	int *p;
	p = &a;//将a的地址赋值给变量p,p也是一个变量,值是一个内存地址编号
	printf("%d\n", *p);//p指向了a的地址,*p就是a的值

	char *p1 = &b;
	printf("%c\n", *p1);//*p1指向了b的地址,*p1就是b的值

	return 0;
}

输出结果:

0025F71C, 0025F713
0
d
请按任意键继续. . .
注意:&可以取得一个变量在内存中的地址,但是,不能取寄存器变量,因为寄存器变量不在内存里,而在CPU里面,所以是没有地址的。
7.2.2 通过指针间接修改变量的值

代码示例:

#include

int main()
{
	int a = 0;
	int b = 11;
	int *p = &a;

	*p = 100;
	printf("a = %d, *p = %d\n", a, *p);

	p = &b;
	*p = 22;
	printf("b = %d, *p = %d\n", b, *p);

	return 0;
}

输出结果:

a = 100, *p = 100
b = 22, *p = 22
请按任意键继续. . .
7.2.3 指针大小

1、使用sizeof()测量指针的大小,得到的总是:4或者8

2、sizeof()测的是指针变量指向存储地址的大小

3、 在32位平台,所有的指针(地址)都是32位(4字节)

4、在64位平台,所有的指针(地址)都是64位(8字节)

代码示例:

#include

int main()
{
	int *p1;
	int **p2;
	char *p3;
	char **p4;
	printf("sizeof(p1) = %d\n", sizeof(p1));
	printf("sizeof(p2) = %d\n", sizeof(p2));
	printf("sizeof(p3) = %d\n", sizeof(p3));
	printf("sizeof(p4) = %d\n", sizeof(p4));
	printf("sizeof(double *) = %d\n", sizeof(double *));


	return 0;
}

输出结果:

sizeof(p1) = 4
sizeof(p2) = 4
sizeof(p3) = 4
sizeof(p4) = 4
sizeof(double *) = 4
请按任意键继续. . .
7.2.4 野指针和空指针

指针变量也是变量,是变量就可以任意赋值,不要越界即可(32位为4字节,64位为8字节),但是,任意数字赋值给指针变量是没有意义,因为这样的指针就变成了野指针,此指针指向的区域是未知(操作系统不允许操作此指针指向的内存区域)。所以,野指针不会直接引发错误,操作野指针的内存区域才会出问题

#include

int main()
{
	int a = 100;
	int *p;
	p = a; //把a的值赋值给指针变量p,p为野指针, ok,不会有问题,但没有意义

	p = 0x12345678; //给指针变量p赋值,p为野指针, ok,不会有问题,但没有意义

	*p = 1000;  //操作野指针指向未知区域,内存出问题,err

	return 0;
}

但是,野指针和有效指针保存的都是数字,为了标志此指针么有指向任何变量(空闲可用),C语言中,可以把NULL赋值给此指针,这样就标志此指针为空指针,没有任何指针。

int *p = NULL;

NULL是一个值为0的宏常量;

#define NULL    ((void *)0)
7.2.5 万能指针void *

void * 指针可以指向任意变量的内存空间:

#include

int main()
{
	void *p = NULL;

	int a = 10;
	p = (void *)&a; //指向变量时,最好转换为void *

					//使用指针变量指向的内存时,转换为int *
	*((int *)p) = 11;
	printf("a = %d\n", a);

	return 0;
}
7.2.6 const修饰的指针变量

代码示例:

#include

int main()
{
	int a = 100;
	int b = 200;

	//指向常量的指针
	//修饰*,指针指向内存区域不能修改,指针指向可以变
	const int * p1 = &a; //等价于int const *p1 = &a;
	//*p1 = 111; //err
	p1 = &b; //ok

	//指针常量
	//修饰p1,指针指向不能变,指针指向的内存可以修改
	int * const p2 = &a;
	//p2 = &b; //err
	*p2 = 222; //ok

	return 0;
}
在编辑程序时,指针作为函数参数,如果不想修改指针对应内存空间的值,需要使用const修饰指针数据类型
7.3 指针和数组
7.3.1 数组名

数组名字是数组的首元素地址,但它是一个地址常量:

#include

int main()
{
	int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	printf("a = %p\n", a);
	printf("&a[0] = %p\n", &a[0]);

	//a = 10; //err, 数组名只是常量,不能修改


	return 0;
}

输出结果:

a = 0037F858
&a[0] = 0037F858
请按任意键继续. . .
7.3.2 指针操作数组元素

代码示例:

#include

int main()
{
	int a[] = { 1,2,3,4,5,6,7,8,9 };
	int i = 0;

	int n = sizeof(a) / sizeof(a[0]);

	for (int i = 0; i < n; i++)
	{
		printf("%d  ", *(a + i));
	}
	printf("\n");

	int *p = a;  //定义一个指针变量保存a的地址
	for (int i = 0; i < n; i++)
	{
		p[i] = 2 * i;
	}

	for (int i = 0; i < n; i++)
	{
		printf("%d  ", *(p + i));
	}

	return 0;
}

输出结果:

1  2  3  4  5  6  7  8  9
0  2  4  6  8  10  12  14  16  请按任意键继续. . .
7.3.3 指针加减法运算
加法运算

1、指针计算不是简单的整数相加

2、如果是一个int * , +1的结果是增加一个int的大小

3、如果是一个char * ,+1的结果是增加一个char的大小

代码示例:

#include 

int main()
{
	int a;
	int *p = &a;
	printf("%d\n", p);
	p += 2;//移动了2个int
	printf("%d\n", p);

	char b = 0;
	char *p1 = &b;
	printf("%d\n", p1);
	p1 += 2;//移动了2个char
	printf("%d\n", p1);

	return 0;
}

输出结果:

4324012
4324020
4323991
4323993
请按任意键继续. . .

通过改变指针指向操作数组元素:

代码示例:

#include

int main()
{
	int a[] = { 1,2,3,4,5,6,7,8,9 };
	int i = 0;
	int n = sizeof(a) / sizeof(a[0]);

	int *p = a;

	for (i = 0; i < n; i++)
	{
		printf("%d  ", *p);
		p++;
	}

	printf("\n");

	return 0;

}

输出结果:

1  2  3  4  5  6  7  8  9
请按任意键继续. . .
减法运算

代码示例1:

#include

int main()
{
	int a[] = { 1,2,3,4,5,6,7,8,9 };
	int i = 0;
	int n = sizeof(a) / sizeof(a[0]);

	int *p = a + n - 1;
	for (i = 0; i < n; i++)
	{
		printf("%d  ", *p);
		p--;
	}

	printf("\n");

	return 0;
}

输出结果:

9  8  7  6  5  4  3  2  1
请按任意键继续. . .

代码示例2:

#include 

int main()
{
	int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	int *p2 = &a[2]; //第3个元素地址
	int *p1 = &a[1]; //第2个元素地址
	printf("p1 = %p, p2 = %p\n", p1, p2);

	int n1 = p2 - p1; //n1 = 1
	int n2 = (int)p2 - (int)p1; //n2 = 4
	printf("n1 = %d, n2 = %d\n", n1, n2);
	
	return 0;
}

输出结果:

p1 = 0040F88C, p2 = 0040F890
n1 = 1, n2 = 4
请按任意键继续. . .
7.3.4 指针数组

指针数组,它是数组,数组中的每个元素都是指针类型

代码示例:

#include 

int main()
{
	//指针数组
	int *p[3];
	int a = 1;
	int b = 2;
	int c = 3;
	int i = 0;

	p[0] = &a;
	p[1] = &b;
	p[2] = &c;

	for (i = 0; i < sizeof(p) / sizeof(p[0]); i++)
	{
		printf("%d, ", *(p[i]));
	}
	printf("\n");

	return 0;
}
7.4 多级指针

1、C语言中允许多级指针存在,在实际应用中一级指针最常用,其次是二级指针

2、二级指针就是指向一个一级指针变量地址的指针

3、三级指针就是指向一个二级指针变量地址的指针,三级指针基本用不着,但是考试会考

int a = 10;
	int *p = &a; //一级指针
	*p = 100; //*p就是a

	int **q = &p;
	//*q就是p
	//**q就是a

	int ***t = &q;
	//*t就是q
	//**t就是p
	//***t就是a
7.5 指针和函数
7.5.1 函数形参改变实参的值

代码示例:

#include 

void swap1(int x, int y)
{
	int tmp;
	tmp = x;
	x = y;
	y = tmp;
	printf("x = %d, y = %d\n", x, y);
}

void swap2(int *x, int *y)
{
	int tmp;
	tmp = *x;
	*x = *y ;
	*y = tmp;
}

int main()
{
	int a = 3;
	int b = 5;
	swap1(a, b); //值传递
	printf("a = %d, b = %d\n", a, b);

	a = 3;
	b = 5;
	swap2(&a, &b); //地址传递
	printf("a2 = %d, b2 = %d\n", a, b);

	return 0;
}

输出结果:

x = 5, y = 3
a = 3, b = 5
a = 5, b = 3
请按任意键继续. . .
7.5.2 数组名做函数参数

数组名做函数参数,函数的形参会退化为指针

代码示例:

#include 

void printArrary(int *a, int n)
{
	int i = 0;
	for (i = 0; i < n; i++)
	{
		printf("%d, ", a[i]);
	}
	printf("\n");
}

int main()
{
	int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	int n = sizeof(a) / sizeof(a[0]);

	//数组名做函数参数
	printArrary(a, n);
	return 0;
}

输出结果:

1, 2, 3, 4, 5, 6, 7, 8, 9,
请按任意键继续. . .
7.5.3 指针作为函数的返回值

代码示例:

#include 

int a = 10;

int *getA()
{
	return &a;
}


int main()
{
	*( getA() ) = 111;
	printf("a = %d\n", a);

	return 0;
}

输出结果:

a = 111
请按任意键继续. . .
7.6 指针和字符串
7.6.1 字符指针

代码示例:

#include 

int main()
{
	char str[] = "hello world";
	char *p = str;
	*p = 'm';
	p++;
	*p = 'i';
	printf("%s\n", str);  //millo world

	p = "mike jiang";
	printf("%s\n", p);  
	char *q = "test";
	printf("%s\n", q);  

	return 0;
}

输出结果:

millo world
mike jiang
test
请按任意键继续. . .
7.6.2 字符指针做函数参数

代码示例:

#include

void mystrcat(char * dest, char * src)
{
	int len1 = 0;
	int len2 = 0;

	while (dest[len1])
	{
		len1++;
	}
	while (src[len2])
	{
		len2++;
	}

	for (int i = 0; i < len2; i++)
	{
		dest[len1 + i] = src[i];
	}

}

int main()
{
	char dest[100] = "hello mike";
	char src[] = "123456";

	mystrcat(dest, src);
	printf("dest = %s\n", dest);

	return 0;	
}

输出结果:

dest = hello mike123456
请按任意键继续. . .
7.6.3 const修饰的指针变量

代码示例:

#include 
#include 
#include 

int main(void)
{
	//const修饰一个变量为只读
	const int a = 10;
	//a = 100; //err

	//指针变量, 指针指向的内存, 2个不同概念
	char buf[] = "aklgjdlsgjlkds";

	//从左往右看,跳过类型,看修饰哪个字符
	//如果是*, 说明指针指向的内存不能改变
	//如果是指针变量,说明指针的指向不能改变,指针的值不能修改
	const char *p = buf;
	// 等价于上面 char const *p1 = buf;
	//p[1] = '2'; //err
	p = "agdlsjaglkdsajgl"; //ok

	char * const p2 = buf;
	p2[1] = '3';
	//p2 = "salkjgldsjaglk"; //err

	//p3为只读,指向不能变,指向的内存也不能变
	const char * const p3 = buf;

	return 0;
}
7.6.4 指针数组作为main函数的形参
int main(int argc, char *argv[]);

1、main函数是操作系统调用的,第一个参数标明argc数组的成员数量,argv数组的每个成员都是char *类型

2、argv是命令行参数的字符串数组

3、argc代表命令行参数的数量,程序名字本身算一个参数

代码示例:

 #include 

//argc: 传参数的个数(包含可执行程序)
//argv:指针数组,指向输入的参数
int main(int argc, char *argv[])
{

	//指针数组,它是数组,每个元素都是指针
	char *a[] = { "aaaaaaa", "bbbbbbbbbb", "ccccccc" };
	int i = 0;

	printf("argc = %d\n", argc);
	for (i = 0; i < argc; i++)
	{
		printf("%s\n", argv[i]);
	}
	return 0;
}

输出结果:

argc = 1
E:\VS2015\C语言基础学习复习\Debug\C语言基础学习复习.exe
请按任意键继续. . .
7.7.5 项目开发中常用的字符串应用模型
1、strstr中的while和do-while模

利用strstr标准库函数找出一个字符传中substr出现的个数

a) 、while模型

代码示例:

#include
#include
#include

int main()
{
	char *p = "11abcd111122abcd333abcd3322abcd3333322qqq";
	int n = 0;

	while ((p = strstr(p, "abcd")) != NULL)
	{
		//能进来肯定有匹配的子串
		//重新设置起点的位置
		p = p + strlen("abcd");
		n++;

		if (*p == 0)
		{
			break;
		}
	}

	printf("n = %d\n", n);

	return 0;
}

b) 、do-while模型

代码示例:

#include
#include
#include

int main()
{
	char *p = "11abcd111122abcd333abcd3322abcd3333322qqq";
	int n = 0;

	do
	{
		p = strstr(p, "abcd");
		if (p != NULL)
		{
			n++; //累计个数

			//重新设置查找的起点
			p = p + strlen("abcd");
		}
		else
		{
			break;
		}
	} while (*p != 0);  //循环终止条件

	printf("n = %d\n", n);

	return 0;
}
2、 两头堵模型

案例:求非空字符串元素的个数

代码示例:

#include
#include
#include

int fun(char *p, int *n)
{
	if (p == NULL || n == NULL)
	{
		return -1;
	}

	int begin = 0;
	int end = strlen(p) - 1;

	//从左边开始,如果当前字符为空,而且还没有结束
	while (p[begin] == ' ' && p[begin] != 0)
	{
		begin++;
	}

	//从右往左移动
	while (p[end] == ' ' && p[end] > 0)
	{
		end--;  //往左移动
	}

	if (end == 0)
	{
		return -2;
	}

	//非空元素的个数
	*n = end - begin + 1;

	return 0;
}



int main()
{
	char *p = " jijlsdiffk ";
	int ret = 0;
	int n = 0;

	ret = fun(p, &n);
	if (ret != 0)  
	{
		return ret;
	}

	printf("非空字符串元素个数:%d\n", n);  

	return 0;
}
3、字符串反转模型(逆置)

【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第18张图片

代码示例:

#include
#include
#include

int inverse(char *p)
{
	if (p == NULL)
	{
		return -1;
	}


	int begin = 0;
	int end = strlen(p) - 1;
	char temp;

	while (begin < end)
	{
		//交换元素
		temp = p[begin];
		p[begin] = p[end];
		p[end] = temp;

		begin++;
		end--;
	}

	return 0;

}



int main()
{
	//char *str = "abcdefg";  //文件区常量,内容不允许修改
	char str[] = "abcdefg";



	int ret = inverse(str);
	if (ret != 0)  //查看没有查询到非空字符时的返回值
	{
		return ret;
	}

	printf("str =  %s\n", str);

	return 0;
}
7.7.6 字符串处理函数
1、strcpy()

函数介绍:

#include 
char *strcpy(char *dest, const char *src);
功能:把src所指向的字符串复制到dest所指向的空间中,'\0'也会拷贝过去
参数:
	dest:目的字符串首地址
	src:源字符首地址
返回值:
	成功:返回dest字符串的首地址
	失败:NULL

注意: 如果参数dest所指的内存空间不够大,可能会造成缓冲溢出的错误情况

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

int main()
{
	char dest[20] = "123456789";
	char src[] = "hello world";
	strcpy(dest, src);
	printf("%s\n", dest);

	return 0;
}

输出结果:

hello world
请按任意键继续. . .
2、strncpy()

函数介绍:

#include 
char *strncpy(char *dest, const char *src, size_t n);
功能:把src指向字符串的前n个字符复制到dest所指向的空间中,是否拷贝结束符看指定的长度是否包含'\0'。
参数:
	dest:目的字符串首地址
	src:源字符首地址
	n:指定需要拷贝字符串个数
返回值:
	成功:返回dest字符串的首地址
	失败:NULL

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
//#include
#include


int main()
{
	char dest[20];
	char src[] = "hello world";

	strncpy(dest, src, 5);
	printf("%s\n", dest);

	dest[5] = '\0';
	printf("%s\n", dest);

	return 0;
}

输出结果:

hello烫烫烫烫烫烫烫烫烫蘲慿厁?
hello
请按任意键继续. . .
3、strcat()

函数介绍:

#include 
char *strcat(char *dest, const char *src);
功能:将src字符串连接到dest的尾部,‘\0’也会追加过去
参数:
	dest:目的字符串首地址
	src:源字符首地址
返回值:
	成功:返回dest字符串的首地址
	失败:NULL

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

int main()
{
	char str[20] = "123";
	char *src = "hello world";
	printf("%s\n", strcat(str, src));

	return 0;
}

输出结果:

123hello world
请按任意键继续. . .
4、strncat()

函数介绍:

#include 
char *strncat(char *dest, const char *src, size_t n);
功能:将src字符串前n个字符连接到dest的尾部,‘\0’也会追加过去
参数:
	dest:目的字符串首地址
	src:源字符首地址
	n:指定需要追加字符串个数
返回值:
	成功:返回dest字符串的首地址
	失败:NULL

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

int main()
{
	char str[20] = "123";
	char *src = "hello world";
	printf("%s\n", strncat(str, src, 5));

	return 0;
}

输出结果:

123hello
请按任意键继续. . .
5、strcmp()

函数介绍:

#include 
int strcmp(const char *s1, const char *s2);
功能:比较 s1 和 s2 的大小,比较的是字符ASCII码大小。
参数:
	s1:字符串1首地址
	s2:字符串2首地址
返回值:
	相等:0
	大于:>0 在不同操作系统strcmp结果会不同   返回ASCII差值
	小于:<0

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

int main()
{
	char *str1 = "hello world";
	char *str2 = "hello mike";

	if (strcmp(str1, str2) == 0)
	{
		printf("str1==str2\n");
	}
	else if (strcmp(str1, str2) > 0)
	{
		printf("str1>str2\n");
	}
	else
	{
		printf("str1);
	}

	return 0;
}

输出结果:

str1>str2
请按任意键继续. . .
6、strncmp()

函数介绍:

#include 
int strncmp(const char *s1, const char *s2, size_t n);
功能:比较 s1 和 s2 前n个字符的大小,比较的是字符ASCII码大小。
参数:
	s1:字符串1首地址
	s2:字符串2首地址
	n:指定比较字符串的数量
返回值:
	相等:0
	大于: > 0
	小于: < 0

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

int main()
{
	char *str1 = "hello world";
	char *str2 = "hello mike";

	if (strncmp(str1, str2, 5) == 0)
	{
		printf("str1==str2\n");
	}
	else if (strcmp(str1, "hello world") > 0)
	{
		printf("str1>str2\n");
	}
	else
	{
		printf("str1);
	}


	return 0;
}

输出结果:

str1==str2
请按任意键继续. . .
7、sprintf()

函数介绍:

#include 
int sprintf(char *str, const char *format, ...);
功能:根据参数format字符串来转换并格式化数据,然后将结果输出到str指定的空间中,直到出现字符串结束符 '\0'  为止。
参数:
	str:字符串首地址
	format:字符串格式,用法和printf()一样
返回值:
	成功:实际格式化的字符个数
	失败: - 1

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

int main()
{
	char dst[100] = { 0 };
	int a = 10;
	char src[] = "hello world";
	printf("a = %d, src = %s", a, src);
	printf("\n");

	int len = sprintf(dst, "a = %d, src = %s", a, src);
	printf("dst = \" %s\"\n", dst);
	printf("len = %d\n", len);

	return 0;
}

输出结果:

a = 10, src = hello world
dst = " a = 10, src = hello world"
len = 25
请按任意键继续. . .
8、sscanf()

函数介绍:

#include 
int sscanf(const char *str, const char *format, ...);
功能:从str指定的字符串读取数据,并根据参数format字符串来转换并格式化数据。
参数:
	str:指定的字符串首地址
	format:字符串格式,用法和scanf()一样
返回值:
	成功:参数数目,成功转换的值的个数
	失败: - 1

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

int main()
{
	char src[] = "a=10, b=20";
	int a;
	int b;
	sscanf(src, "a=%d,  b=%d", &a, &b);
	printf("a:%d, b:%d\n", a, b);

	return 0;
}

输出结果:

a:10, b:20
请按任意键继续. . .
9、strchr()

函数介绍:

#include 
char *strchr(const char *s, int c);
功能:在字符串s中查找字母c出现的位置
参数:
	s:字符串首地址
	c:匹配字母(字符)
返回值:
	成功:返回第一次出现的c地址
	失败:NULL

代码示例:

p = a123abcd
请按任意键继续. . .
10、strstr()

函数介绍:

#include 
char *strstr(const char *haystack, const char *needle);
功能:在字符串haystack中查找字符串needle出现的位置
参数:
	haystack:源字符串首地址
	needle:匹配字符串首地址
返回值:
	成功:返回第一次出现的needle地址
	失败:NULL

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

int main()
{
	char src[] = "ddddabcd123abcd333abcd";
	char *p = strstr(src, "abcd");
	printf("p = %s\n", p);

	return 0;
}

输出结果:

p = abcd123abcd333abcd
请按任意键继续. . .
11、strtok()

函数介绍:

#include 
char *strtok(char *str, const char *delim);
功能:来将字符串分割成一个个片段。当strtok()在参数s的字符串中发现参数delim中包含的分割字符时, 则会将该字符改为\0 字符,当连续出现多个时只替换第一个为\0。
参数:
	str:指向欲分割的字符串
	delim:为分割字符串中包含的所有字符
返回值:
	成功:分割后字符串首地址
	失败:NULL

说明:

1、在第一次调用时:strtok()必需给予参数s字符串

2、往后的调用则将参数s设置成NULL,每次调用成功则返回指向被分割出片段的指针

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

int main()
{
	char a[100] = "adc*fvcv.ebcy*hghbdfg$casdert";
	char *s = strtok(a, ".*$");//将"*"分割的子串取出
	while (s != NULL)
	{
		printf("%s\n", s);
		s = strtok(NULL, "*");
	}

	return 0;
}

输出结果:

adc
fvcv.ebcy
hghbdfg$casdert
请按任意键继续. . .
12、atoi()

函数介绍:

#include 
int atoi(const char *nptr);
功能:atoi()会扫描nptr字符串,跳过前面的空格字符,直到遇到数字或正负号才开始做转换,而遇到非数字或字符串结束符('\0')才结束转换,并将结果返回返回值。
参数:
	nptr:待转换的字符串
	返回值:成功转换后整数

功能相似的函数有:

  • atof():把一个小数形式的字符串转化为一个浮点数
  • atol():将一个字符串转化为long类型

代码示例:

#include
#include

int main()
{
	char str1[] = "          -10";
	int num1 = atoi(str1);
	printf("num1 = %d\n", num1);

	char str2[] = "0.123";
	double num2 = atof(str2);
	printf("num2 = %lf\n", num2);

	char str3[] = "123L";
	long num3 = atol(str3);
	printf("num3 = %ld\n", num3);

	return 0;
}

输出结果:

num1 = -10
num2 = 0.123000
num3 = 123
请按任意键继续. . .
7.8 指针小结
定义 说明
int i 定义整形变量
int *p 定义一个指向int的指针变量
int a[10] 定义一个有10个元素的数组,每个元素类型为int
int *p[10] 定义一个有10个元素的数组,每个元素类型为int*
int func() 定义一个函数,返回值为int
int *func() 定义一个函数,返回值为int *
int **p 定义一个指向int的指针的指针,二级指针


八、C语言学习第八天之内存管理

8.1 作用域

C语言变量的作用域分为:

1、代码块作用域(代码块是{}之间的一段代码)

2、函数作用域

3、文件作用域

8.1.1 局部变量

局部变量也叫auto自动变量(auto可写可不写),一般情况下代码块{}内部定义的变量都是局部变量(自动变量),它有如下特点:

1、在一个函数内定义,只在函数范围内有效

2、在复合语句中定义,只在复合语句中有效

3、随着函数调用的结束或复合语句的结束局部变量的生命周期也结束

4、如果没有赋初值,内容为随机

代码示例:

#include 

void test()
{
	//auto写不写是一样的
	//auto只能出现在{}内部
	auto int b = 10; 
}

int main(void)
{
	//b = 100; //err, 在main作用域中没有b

	if (1)
	{
		//在复合语句中定义,只在复合语句中有效
		int a = 10;
		printf("a = %d\n", a);
	}

	//a = 10; //err离开if()的复合语句,a已经不存在
	
	return 0;
}
8.1.2 静态(static)局部变量

1、static局部变量的作用域也是在定义的函数内有效

2、static局部变量的生命周期和程序运行周期一样,同时static局部变量的值只初始化一次,但可以赋值多次

3、static局部变量若未赋予初值,则由系统自动赋值:数值型变量自动赋初值为0,字符型变量赋空字符

代码示例:

#include 

void fun1()
{
	int i = 0;
	i++;
	printf("i = %d\n", i);
}

void fun2()
{
	//静态局部变量,没有赋值,系统赋值为0,而且只会初始化一次
	static int a;
	a++;
	printf("a = %d\n", a);
}

int main(void)
{
	fun1();
	fun1();
	fun2();
	fun2();
	
	return 0;
}

输出结果:

i = 1
i = 1
a = 1
a = 2
请按任意键继续. . .
8.1.3 全局变量

1、在函数外定义,可被本文件及其它文件中的函数所共用,若其它文件中的函数调用此变量,须用extern声明

2、全局变量的生命周期和程序运行周期一样

3、不同文件的全局变量不可以重名

8.1.5 静态(static)全局变量

1、在函数外定义,作用范围被限制在所定义的文件中

2、不同文件静态全局变量可以重名,但作用域不冲突

3、static全局变量的生命周期和程序运行周期一样,同样static全局变量的值只初始化一次

8.1.5 extern全局变量声明

extern int a;声明一个变量,这个全局变量在别的文件中已经定义了,这里只是声明,而不是定义

8.1.6 全局函数和静态函数

在C语言中函数默认都是全局的,使用关键字static可以将函数声明为静态,函数定义为static意味着这个函数只能在定义的这个函数的文件中使用,而在其他文件中不能进行调用,即使在其他文件中声明这个函数都没用

提醒:对于不同文件的static函数名称是可以相同的

注意:

1、允许在不同的函数中使用相同的变量名,它们代表不同的对象,分配不同的单元,互相不干扰

2、同一源文件中,允许全局变量和局部变量同名,在局部变量的作用域内,全局变量不起作用

3、所有的函数默认都是全局的,意味着所有的函数都不能重名,但如果是static函数,那么作用域是文件级的,所以不同的文件static函数名是可以相同的

8.1.7 总结
类型 作用域 生命周期
auto变量 一对{}内 当前函数
static局部变量 一对{}内 整个程序运行期
extern变量 整个程序 整个程序运行期
static全局变量 当前文件 整个程序运行期
extern函数 整个程序 整个程序运行期
static函数 当前文件 整个程序运行期
register变量 一对{}内 当前函数
全局变量 整个程序 整个程序运行期
8.2 内存布局
8.2.1 内存分区

C代码经过预处理、编译、汇编、链接之后生成一个可执行程序。在没有运行程序之前,也就是说程序没有加载到内存之前,可执行程序内部已经分好3段信息,分别为代码区(text)、数据区(data)和未初始化数据区(bass)3个部分(有些人直接把data和bss合起来叫做静态区或者全局区)

1、代码区

存放CPU执行的机器指令。通常代码区是共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁别执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。

2、全局初始化数据区/静态数据区(又叫bss区)

该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)

3、为初始化数据区(又叫bss区)

存入的是全局为初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为0或者空(NULL)

==说明:==程序在加载到内存前,==代码区和全局区(data和bss)的大小是固定的,程序运行期间不能改变。==然后,运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出的代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区(向下增长)和堆区(向上增长)

【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第19张图片

代码区(text segment)

加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的

未初始化数据区(BSS)

加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。

全局初始化数据区/静态数据区(data segment)

加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程

栈区(stack)

栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间

堆区(heap)

堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

8.2.2 存储类型总结
类型 作用域 生命周期 存储位置
auto变量 一对{}内 当前函数 栈区
static局部变量 一对{}内 整个程序运行期 初始化在data段,未初始化在BSS段
extern变量 整个程序 整个程序运行期 初始化在data段,未初始化在BSS段
static全局变量 当前文件 整个程序运行期 初始化在data段,未初始化在BSS段
extern函数 整个程序 整个程序运行期 代码区
static函数 当前文件 整个程序运行期 代码区
register变量 一对{}内 当前函数 运行时存储在CPU寄存器
字符串常量 当前文件 整个程序运行期 data段

代码示例:

#include 
#include 

int e;
static int f;
int g = 10;
static int h = 10;
int main()
{
	int a;
	int b = 10;
	static int c;
	static int d = 10;
	char *i = "test";
	char *k = NULL;

	printf("&a\t %p\t //局部未初始化变量\n", &a);
	printf("&b\t %p\t //局部初始化变量\n", &b);

	printf("&c\t %p\t //静态局部未初始化变量\n", &c);
	printf("&d\t %p\t //静态局部初始化变量\n", &d);

	printf("&e\t %p\t //全局未初始化变量\n", &e);
	printf("&f\t %p\t //全局静态未初始化变量\n", &f);

	printf("&g\t %p\t //全局初始化变量\n", &g);
	printf("&h\t %p\t //全局静态初始化变量\n", &h);

	printf("i\t %p\t //只读数据(文字常量区)\n", i);

	k = (char *)malloc(10);
	printf("k\t %p\t //动态分配的内存\n", k);

	return 0;
}

输出结果:

&a       0025FEB4        //局部未初始化变量
&b       0025FEA8        //局部初始化变量
&c       00AC946C        //静态局部未初始化变量
&d       00AC903C        //静态局部初始化变量
&e       00AC9464        //全局未初始化变量
&f       00AC9468        //全局静态未初始化变量
&g       00AC9034        //全局初始化变量
&h       00AC9038        //全局静态初始化变量
i        00AC6B30        //只读数据(文字常量区)
k        0050C830        //动态分配的内存
请按任意键继续. . .
8.2.3 内存操作函数
1、memset()

函数介绍:

#include 
void *memset(void *s, int c, size_t n);
功能:将s的内存区域的前n个字节以参数c填入
参数:
	s:需要操作内存s的首地址
	c:填充的字符,c虽然参数为int,但必须是unsigned char , 范围为0~255
	n:指定需要设置的大小
返回值:s的首地址

代码示例:

#include 
#include 
#include

int main()
{
	int a[10];

	memset(a, 0, sizeof(a));
	memset(a, 97, sizeof(a));
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%c\n", a[i]);
	}


	return 0;
}

输出结果:

a
a
a
a
a
a
a
a
a
a
请按任意键继续. . .
2、memcpy()

函数介绍:

#include 
void *memcpy(void *dest, const void *src, size_t n);
功能:拷贝src所指的内存内容的前n个字节拷贝到dest所指的内存地址上。
参数:
	dest:目的内存首地址
	src:源内存首地址,注意:dest和src所指的内存空间不可重叠,可能会导致程序报错
	n:需要拷贝的字节数
返回值:dest的首地址

代码示例:

#include 
#include 
#include

int main()
{
	int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int b[10];

	memcpy(b, a, sizeof(a));
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d, ", b[i]);
	}
	printf("\n");

	//memcpy(&a[3], a, 5 * sizeof(int)); //err, 内存重叠

	return 0;
}

输出结果:

1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
请按任意键继续. . .
3、memmove()

函数介绍:

memmove()功能用法和memcpy()一样,区别在于:dest和src所指的内存空间重叠时,memmove()仍然能处理,不过执行效率比memcpy()低些
4、memcmp()

函数介绍:

#include 
int memcmp(const void *s1, const void *s2, size_t n);
功能:比较s1和s2所指向内存区域的前n个字节
参数:
	s1:内存首地址1
	s2:内存首地址2
	n:需比较的前n个字节
返回值:
	相等:=0
	大于:>0
	小于:<0

代码示例:

#include 
#include 
#include

int main()
{
	int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int b[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

	int flag = memcmp(a, b, sizeof(a));
	printf("flag = %d\n", flag);

	return 0;
}

输出结果:

flag = 0
请按任意键继续. . .
8.2.4 堆区内存的分配和释放
1、malloc()

函数介绍:

#include 
void *malloc(size_t size);
功能:在内存的动态存储区(堆区)中分配一块长度为size字节的连续区域,用来存放类型说明符指定的类型。分配的内存空间内容不确定,一般使用memset初始化。
参数:
	size:需要分配内存大小(单位:字节)
返回值:
成功:分配空间的起始地址
失败:NULL

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include  
#include 
#include 

int main()
{
	int count, *array, n;
	printf("请输入要申请数组的个数:\n");
	scanf("%d", &n);

	array = (int *)malloc(n * sizeof(int));
	if (array == NULL)
	{
		printf("申请空间失败!\n");
		return -1;
	}
	//将申请到空间清0
	memset(array, 0, sizeof(int)*n);

	for (count = 0; count < n; count++) /*给数组赋值*/
		array[count] = count;

	for (count = 0; count < n; count++) /*打印数组元素*/
		printf("%2d", array[count]);

	free(array);

	return 0;
}

输出结果:

请输入要申请数组的个数:
5
 0 1 2 3 4请按任意键继续. . .
2、free()

函数介绍:

#include 
void free(void *ptr);
功能:释放ptr所指向的一块内存空间,ptr是一个任意类型的指针变量,指向被释放区域的首地址。对同一内存空间多次释放会出错。
参数:
ptr:需要释放空间的首地址,被释放区应是由malloc函数所分配的区域。
返回值:无
8.3 内存分区代码分析
1、返回栈区地址

代码示例:

#include 
int *fun()
{
	int a = 10;
	return &a;//函数调用完毕,a释放
}

int main(int argc, char *argv[])
{
	int *p = NULL;
	p = fun();
	*p = 100; //操作野指针指向的内存,err

	return 0;
}
2、返回data区地址
#include 

int *fun()
{
	static int a = 10;
	return &a; //函数调用完毕,a不释放
}

int main(int argc, char *argv[])
{
	int *p = NULL;
	p = fun();
	*p = 100; //ok
	printf("*p = %d\n", *p);

	return 0;
}
3、值传递1

代码示例:

#include 
#include 

void fun(int *tmp)
{
	tmp = (int *)malloc(sizeof(int));
	*tmp = 100;
}

int main(int argc, char *argv[])
{
	int *p = NULL;
	fun(p); //值传递,形参修改不会影响实参
	printf("*p = %d\n", *p);//err,操作空指针指向的内存

	return 0;
}
4、值传递2

代码示例:

#include 
#include 

void fun(int *tmp)
{
	*tmp = 100;
}

int main(int argc, char *argv[])
{
	int *p = NULL;
	p = (int *)malloc(sizeof(int));

	fun(p); //值传递
	printf("*p = %d\n", *p); //ok,*p为100

	return 0;
}
5、返回堆区地址

代码示例:

#include 
#include 

int *fun()
{
	int *tmp = NULL;
	tmp = (int *)malloc(sizeof(int));
	*tmp = 100;
	return tmp;//返回堆区地址,函数调用完毕,不释放
}

int main(int argc, char *argv[])
{
	int *p = NULL;
	p = fun();
	printf("*p = %d\n", *p);//ok

	//堆区空间,使用完毕,手动释放
	if (p != NULL)
	{
		free(p);
		p = NULL;
	}

	return 0;
}


九、C语言学习第九天之复合类型(自定义类型)

9.1 结构体
9.1.1 概述

数组:描述一组具有相同类型数据的有序集合,用于处理大量相同类型的数据运算

有时我们需要将不同类型的数据组合成一个有机的整体,如:一个学生有学号/姓名/性别/年龄/地址等属性。显然单独定义以上变量比较繁琐,数据不便于管理

C语言中给出了另一种构造数据类型——结构体

【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第20张图片

9.1.2 结构体变量的定义和初始化
定义结构体变量的方式:

1、先声明结构体类型再定义变量名

2、在声明类型的同时定义变量

3、直接定义结构体类型变量(无类型名)

代码示例:

//定义结构体变量方式一
struct stu
{
    成员表列;
};
struct stu Mike;

//定义结构体变量的方式二
struct stu
{
    成员表列;
}Mike,Bob;

//定义结构体变量的方式三
struct{
    成员表列;
}Mike,Bob;
结构体类型和结构体变量关系

1、结构体类型:指定了一个结构体类型,它相当于一个模型,但其中并无具体数据,系统对之也不分配实际内存单元

2、结构体变量:系统根据结构体类型(内部成员状况)为之分配内存空间

代码示例:

//结构体类型的定义
struct stu
{
    char name[50];
    int age;
};

//先定义类型,再定义变量(常用)
struct stu s1 = {"mike",18};

//定义类型时同时定义变量
struct stu
{
    char name[50];
    int age;
}s2 = {"lily",22};

//直接定义结构体类型变量(无类型名)
struct
{
    char name[50];
    int age;
}s3 = {"yuri",25};
9.1.3 结构体成员的使用

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

//结构体类型的定义
struct stu
{
	char name[50];
	int age;
};

int main()
{
	struct stu s1;

	//如果是普通变量,通过点运算符操作结构体变量
	strcpy(s1.name, "abc");
	s1.age = 18;
	printf("s1.name = %s, s1.age = %d\n", s1.name, s1.age);

	//如果是指针变量,通过->操作接头体成员
	strcpy((&s1)->name, "test");
	(&s1)->age = 18;
	printf("(&s1)->name = %s, (&s1)->age = %d\n", (&s1)->name, (&s1)->age);

	return 0;
}

输出结果:

s1.name = abc, s1.age = 18
(&s1)->name = test, (&s1)->age = 18
请按任意键继续. . .
9.1.4 结构体数组

代码示例:

#include

//统计学生成绩
struct stu
{
	int num;
	char name[20];
	char sex;
	float score;
};

int main()
{
	//定义一个含有5个元素的结构体数组并将其初始化
	struct stu boy[5] = {
		{ 101, "Li ping", 'M', 45 },
		{ 102, "Zhang ping", 'M', 62.5 },
		{ 103, "He fang", 'F', 92.5 },
		{ 104, "Cheng ling", 'F', 87 },
		{ 105, "Wang ming", 'M', 58 }
	};

	int c = 0;
	float ave, s = 0;
	for (int i = 0; i < 5; i++)
	{
		s += boy[i].score;  //计算总分
		if (boy[i].score < 60)
		{
			c++;  //统计不及格人数
		}
	}

	printf("s = %f\n", s);  //打印总分
	ave = s / 5;
	printf("average = %f\ncount = %d\n", ave, c);

	for (int i = 0; i < 5; i++)
	{
		printf(" name = %s score = %f\n", boy[i].name, boy[i].score);
		// printf(" name=%s,  score=%f\n", (boy+i)->name, (boy+i)->score);
	}

	return 0;
}

输出结果:

s = 345.000000
average = 69.000000
count = 2
 name = Li ping score = 45.000000
 name = Zhang ping score = 62.500000
 name = He fang score = 92.500000
 name = Cheng ling score = 87.000000
 name = Wang ming score = 58.000000
请按任意键继续. . .
9.1.5 结构体嵌套结构体

代码示例:

#include 

struct person
{
	char name[20];
	char sex;
};

struct stu
{
	int id;
	struct person info;
};

int main()
{
	struct stu s[2] = { 1, "lily", 'F', 2, "yuri", 'M' };

	int i = 0;
	for (i = 0; i < 2; i++)
	{
		printf("id = %d\tinfo.name=%s\tinfo.sex=%c\n", s[i].id, s[i].info.name, s[i].info.sex);
	}

	return 0;
}

输出结果:

id = 1  info.name=lily  info.sex=F
id = 2  info.name=yuri  info.sex=M
请按任意键继续. . .
9.1.6 结构体赋值

代码示例:

#include
#include

//结构体类型的定义
struct stu
{
	char name[50];
	int age;
};

int main()
{
	struct stu s1;

	//如果是普通变量,通过点运算符操作结构体成员
	strcpy(s1.name, "abc");
	s1.age = 18;
	printf("s1.name = %s, s1.age = %d\n", s1.name, s1.age);

	//相同类型的两个结构体变量,可以相互赋值
	//把s1成员变量的值拷贝给s2成员变量的内存
	//s1和s2只是成员变量的值一样而已,它们还是没有关系的两个变量
	struct stu s2 = s1;
//memcpy(&s2, &s1, sizeof(s1));
	printf("s2.name = %s, s2.age = %d\n", s2.name, s2.age);

	return 0;
}

输出结果:

s1.name = abc, s1.age = 18
s2.name = abc, s2.age = 18
请按任意键继续. . .
9.1.7 结构体和指针
1、指向普通结构体变量的指针

代码示例:

#include

//结构体类型的定义
struct stu
{
	char name[50];
	int age;
};

int main()
{
	struct stu s1 = { "lily", 18 };

	//如果是指针变量,通过->操作结构体成员
	struct stu *p = &s1;
	printf("p->name = %s, p->age=%d\n", p->name, p->age);
	printf("(*p).name = %s, (*p).age=%d\n",  (*p).name,  (*p).age);

	return 0;
}

输出结果:

p->name = lily, p->age=18
(*p).name = lily, (*p).age=18
请按任意键继续. . .
2、堆区结构体变量

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include 
#include 

//结构体类型的定义
struct stu
{
	char name[50];
	int age;
};

int main()
{
	struct stu *p = NULL;

	p = (struct stu *)malloc(sizeof(struct  stu));

	//如果是指针变量,通过->操作结构体成员
	strcpy(p->name, "test");
	p->age = 22;

	printf("p->name = %s, p->age=%d\n", p->name, p->age);
	printf("(*p).name = %s, (*p).age=%d\n", (*p).name, (*p).age);

	free(p);
	p = NULL;

	return 0;
}

输出结果:

p->name = test, p->age=22
(*p).name = test, (*p).age=22
请按任意键继续. . .
3、结构体嵌套一级指针

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include

//结构体类型定义
struct stu
{
	char *name;  //一级指针
	int age;
};

int main()
{
	struct stu *p = NULL;
	 
	p = (struct stu *)malloc(sizeof(struct stu));

	p->name = (char *)malloc(strlen("test") + 1);
	strcpy(p->name, "test");
	p->age = 12;

	printf("p->name = %s, p->age = %d\n", p->name, p->age);
	printf("(*p).name = %s, (*p).age = %d\n", (*p).name, (*p).age);

	if (p->name != NULL)
	{
		free(p->name);
		p->name = NULL;
	}

	if (p != NULL)
	{
		free(p);
		p = NULL;
	}

	return 0;
}

输出结果:

p->name = test, p->age = 12
(*p).name = test, (*p).age = 12
请按任意键继续. . .
9.1.8 结构体做函数参数
1、结构体普通变量做函数参数

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

//结构体类型定义
struct stu
{
	char name[50];
	int age;
};


//函数参数为结构体普通变量
void set_stu(struct stu tmp)
{
	strcpy(tmp.name, "mike");
	tmp.age = 18;
	printf("tmp.name = %s, tmp.age = %d\n", tmp.name, tmp.age);
}

int main()
{
	struct stu s = { 0 };
	set_stu(s);  //值传递

	printf("s.name = %s, s.age = %d\n", s.name, s.age);

	return 0;
}

输出结果:

tmp.name = mike, tmp.age = 18
s.name = , s.age = 0
请按任意键继续. . .
2、结构体指针变量做函数参数

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

//结构体类型定义
struct stu
{
	char name[50];
	int age;
};

//函数参数为结构体指针变量
void set_stu_pro(struct stu *tmp)
{
	strcpy(tmp->name, "mike");
	tmp->age = 18;
}

int main()
{
	struct stu s = { 0 };
	set_stu_pro(&s);  //地址传递
	printf("s.name = %s, s.age  = %d\n", s.name, s.age);

	return 0;
}

输出结果:

s.name = mike, s.age  = 18
请按任意键继续. . .
3、结构体数组名做函数参数

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

//结构体类型定义
struct stu
{
	char name[50];
	int age;
};

void set_stu_pro(struct stu *tmp, int n)
{
	int i = 0;
	for (i = 0; i < n; i++)
	{
		sprintf(tmp->name, "name%d%d%d", i, i, i);
		tmp->age = 20 + i;
		tmp++;
	}
}

int main()
{
	struct stu s[3] = { 0 };
	int i = 0;
	int n = sizeof(s) / sizeof(s[0]);
	set_stu_pro(s, n);  //数组名传递

	for (i = 0; i < n; i++)
	{
		printf("%s , %d\n", s[i].name, s[i].age);
	}

	return 0;
}

输出结果:

name000 , 20
name111 , 21
name222 , 22
请按任意键继续. . .
4、const修饰结构体指针形参变量

代码示例:

//结构体类型定义
struct stu
{
    char name[50];
    int age;
};

void fun1(struct stu* const p)
{
    //p=NULL;  //错误,指针常量,指针的指向不可以修改,但指针指向的内容可以修改
    p->age = 10;  //ok
}

void fun2(const struct stu *p)
{
    p = NULL; //ok
    //p->age = 10;   //错误,常量指针,指针的指向可以修改,但指针指向的内容不可以修改
}

void fun3(const struct stu * const p)
{
    //p = NULL; //错误
    //p->age = 10;  //错误
}
9.2 共用体(联合体)

1、联合union是一个能在同一个存储空间存储不同类型数据的类型

2、联合体所占的内存长度等于其最长成员的长度倍数,也有叫做共用体

3、同一内存段可以用来存放几种不同类型的成员,但每一瞬时只有一种起作用

4、共用体变量的地址和它的各成员的地址都是同一地址

代码示例:

#include

//共用体也叫联合体
union Test
{
	unsigned char a;
	unsigned int b;
	unsigned short c;
};

int main()
{
	//定义共用体变量
	union Test tmp;

	//1、所有成员的首地址是一样的
	printf("%p , %p , %p\n", &(tmp.a), &(tmp.b), &(tmp.c));

	//2、共用体大小为最大成员类型的大小
	printf("%lu", sizeof(union Test));

	//3、一个成员赋值,会影响另外的成员
	tmp.b = 0x44332211;
	//左边是高位,右边是低位
	//低位放地地址,高位放高地址

	printf("%x\n", tmp.a);  //11
	printf("%x\n", tmp.c);  //2211

	tmp.a = 0x00;
	printf("short:%x\n", tmp.c);  //2200
	printf("int:%x\n", tmp.b);  //44332200

	return 0;
}

输出结果:

0038FE80 , 0038FE80 , 0038FE80
411
2211
short:2200
int:44332200
请按任意键继续. . .
9.3 枚举

枚举:将变量的值一一列举,变量的值仅限于列举出来的值的范围内

枚举类型的定义:

enum 枚举名
{
    枚举值表;
}
说明:

1、在枚举值表中应列出所有可用值,也称为枚举元素

2、枚举值是常量,不能在程序中用赋值语句再对它赋值

3、枚举元素本身由系统定义了一个表示序号的数值从0开始顺序定义为0,1,2 …

代码示例:

#include

enum weekday
{
	sun = 2, mon, tue, wed, thu, fri, sat
};


int main()
{
	enum weekday a, b, c;
	a = sun;
	b = mon;
	c = tue;

	printf("%d , %d , %d\n", a, b, c);

	return 0;
}

输出结果:

2 , 3 , 4
请按任意键继续. . .
9.4 typedef

typedef为C语言关键字,作用是为一种数据类型(基本类型或者自定义数据类型)定义一个新的名字,不能创建新的类型

说明:

1、与#define不同,typedef仅限于数据类型,而不能是表达式或者是具体的值

2、#define发生在预处理阶段,typedef发生在编译阶段

代码示例:

#include

typedef int INT;
typedef char BYTE;
typedef BYTE T_BYTE;
typedef unsigned char UBYTE;

typedef struct type
{
	UBYTE a;
	INT b;
	T_BYTE c;
}TYPE,*PTYPE;

int main()
{
	TYPE t;
	t.a = 254;
	t.b = 10;
	t.c = 'c';

	PTYPE p = &t;
	printf("%u , %d , %c\n", p->a, p->b, p->c);

	return 0;
}

输出结果:

254 , 10 , c
请按任意键继续. . .


十、C语言学习第十天之文件操作

10.1 概述
10.1.1 磁盘文件和设备文件

1、磁盘文件:指一组相关数据的有序集合,通常存储在外部介质(如磁盘)上,使用时才调入内存

2、设备文件:在操作系统中把每一个与主机相连的输入、输出设备看作是一个文件,把它们的输入、输出等同于对于磁盘文件的读和写

10.1.2 磁盘文件的分类

计算机的存储在物理上是二进制的,所以物理上所有的磁盘文件本质上都是一样的:以字节为单位进行顺序存储

从用户或者操作系统使用的角度(逻辑上)可以把文件分为

1、文本文件:基于字符编码的文件

2、二进制文件:基于值编码的文件

10.1.3 文本文件和二进制文件
1、文本文件

1、基于字符编码,常见编码有ASCLL、UNICODE等

2、一般可以使用文本编辑器直接打开

3、数字5678的以ASCLL存储形式(ASCLL码)为:00110101 00110110 00110111 00111000

2、二进制文件

1、基于值编码,自己根据具体应用,指定某个值是什么意思

2、把内存中的数据按其在内存中的存储形式原样输出到磁盘上

3、数5678的存储形式(二进制码)为:l 00010110 00101110

10.2 文件的打开和关闭
10.2.1 文件指针

在C语言中用一个指针变量指向一个文件,这个指着称之为文件指针

代码示例:

typedef struct
{
	short           level;	//缓冲区"满"或者"空"的程度 
	unsigned        flags;	//文件状态标志 
	char            fd;		//文件描述符
	unsigned char   hold;	//如无缓冲区不读取字符
	short           bsize;	//缓冲区的大小
	unsigned char   *buffer;//数据缓冲区的位置 
	unsigned        ar;	 //指针,当前的指向 
	unsigned        istemp;	//临时文件,指示器
	short           token;	//用于有效性的检查 
}FILE;

FILE是系统使用typedef定义出来的有关文件信息的一种结构体类型,结构体中含有文件名、文件状态和文件当前位置等信息

声明FILE结构体类型的信息包含在头文件“stdio.h”中,一般设置一个指向FILE类型变量的指针变量,然后通过它来引用这些FILE类型变量。通过文件指针就可对它所指的文件进行各种操作

C语言中有三个特殊的文件指针由系统默认打开,用户无需定义即可直接使用:

1、stdin: 标准输入,默认为当前终端(键盘),我们使用的scanf、getchar函数默认从此终端获得数据

2、stdout:标准输出,默认为当前终端(屏幕),我们使用的printf、puts函数默认输出信息到此终端

3、stderr:标准出错,默认为当前终端(屏幕),我们使用的perror函数默认输出信息到此终端

10.2.2 文件的打开

任何文件在使用之前都必须打开:

函数介绍:

#include 
FILE * fopen(const char * filename, const char * mode);
功能:打开文件
参数:
	filename:需要打开的文件名,根据需要加上路径
	mode:打开文件的模式设置
返回值:
	成功:文件指针
	失败:NULL

第一个参数的几种情况:

FILE *fp_passwd = NULL;

	//相对路径:
	//打开当前目录passdw文件:源文件(源程序)所在目录
	FILE *fp_passwd = fopen("passwd.txt", "r");
	
	//打开当前目录(test)下passwd.txt文件
	fp_passwd = fopen(". / test / passwd.txt", "r");
	
	//打开当前目录上一级目录(相对当前目录)passwd.txt文件
	fp_passwd = fopen(".. / passwd.txt", "r");
		
	//绝对路径:
	//打开C盘test目录下一个叫passwd.txt文件
	fp_passwd = fopen("c:/test/passwd.txt","r");

第二个参数的几种形式(打开文件的方式):

打开模式 含义
r或rb 以只读方式打开一个文本文件(不创建文件,若文件不存在则报错)
w或wb 以写方式打开文件(如果文件存在则清空文件,文件不存在则创建一个文件)
a或ab 以追加方式打开文件,在末尾添加内容,若文件不存在则创建文件
r+或rb+ 以可读、可写的方式打开文件(不创建新文件)
w+或wb+ 以可读、可写的方式打开文件(如果文件存在则清空文件,文件不存在则创建一个文件)
a+或ab+ 以添加方式打开可读、可写的文件。若文件不存在则创建文件;如果文件存在,则写入的数据会被加到文件尾后,即文件原先的内容会被保留。
注意:

1、b是二进制模式的意思,b只是在Windows有效,在Linux用r和rb的结果是一样的

2、Unix和Linux下所有的文本文件行都是\n结尾,而Windows所有的文本文件行都是\r\n结尾

3、在Windows平台下,以“文本”方式打开文件,不加b:

  • 当读取文件的时候,系统会将所有的 “\r\n” 转换成 “\n”
  • 当写入文件的时候,系统会将 “\n” 转换成 “\r\n” 写入
  • 以"二进制"方式打开文件,则读写都不会进行这样的转换

4、在Unix/Linux平台下,“文本”与“二进制”模式没有区别,"\r\n" 作为两个字符原样输入输出

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include

int main(void)
{
	FILE *fp = NULL;

	fp = fopen("../test", "w");

	if (fp == NULL)
	{
		//perroe()是标准出错打印函数,能打印调用库函数出错的原因
		perror("open");
		return -1;
	}

	return 0;
}
10.2.3 文件的关闭

任何文件在使用后都应该关闭:

1、打开的文件会占用内存资源,如果总是打开文件而不进行关闭,就会消耗很多的内存

2、一个进程同时打开的文件数是有限制的,超过最大同时打开文件数,再次调用fopen打开文件会失败

3、如果没有明确的调用fclose关闭打开的文件,那么程序在退出的时候,操作系统会统一进行关闭

函数介绍:

#include 
int fclose(FILE * stream);
功能:关闭先前fopen()打开的文件。此动作让缓冲区的数据写入文件中,并释放系统所提供的文件资源。
参数:
	stream:文件指针
返回值:
	成功:0
	失败:-1

代码示例:

FILE * fp = NULL;
	fp = fopen("abc.txt", "r");
	fclose(fp);
10.3 文件的顺序读写
10.3.1 按照字符读写文件fgetc、fputc
1、写文件

函数介绍:

#include 
int fputc(int ch, FILE * stream);
功能:将ch转换为unsigned char后写入stream指定的文件中
参数:
	ch:需要写入文件的字符
	stream:文件指针
返回值:
	成功:成功写入文件的字符
	失败:返回-1

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

int main(void)
{

	FILE *fp = NULL;

	fp = fopen("../test", "w");

	if (fp == NULL) 
	{
		perror("open");
		return -1;
	}


	char buf[] = "this is a test for fputc";
	int i = 0;
	int n = strlen(buf);

	for (i = 0; i < n; i++)
	{
		int ch = fputc(buf[i], fp);
		printf("ch = %c\n", ch);
	}

	return 0;
}

输出结果:

ch = t
ch = h
ch = i
ch = s
ch =
ch = i
ch = s
ch =
ch = a
ch =
ch = t
ch = e
ch = s
ch = t
ch =
ch = f
ch = o
ch = r
ch =
ch = f
ch = p
ch = u
ch = t
ch = c
请按任意键继续. . .
2、文件结尾

在C语言中,EOF表示文件结束符(end of file)。在while循环中以EOF作为文件结束标志,这种以EOF作为文件结束标志的文件,必须是文本文件。在文本文件中,数据都是以字符的ASCII代码值的形式存放。我们知道,ASCII代码值的范围是0~127,不可能出现-1,因此可以用EOF作为文件结束标志。

#define EOF    (-1)

当把数据以二进制形式存放到文件中时,就会有-1值的出现,因此不能采用EOF作为二进制文件的结束标志。为了解决这个问题,ANSI C提供了一个feof函数,用来判断文件是否结束。feof函数即可以判断二进制文件又可以判断文本文件

函数介绍:

#include 
int feof(FILE * stream);
功能:检测是否读取到了文件结尾。判断的是最后一次“读操作的内容”,不是当前位置内容(上一个内容)。
参数:
	stream:文件指针
返回值:
	非0值:已经到文件结尾
	0:没有到文件结尾
3、读文件

函数介绍:

#include 
int fgetc(FILE * stream);
功能:从stream指定的文件中读取一个字符
参数:
	stream:文件指针
返回值:
	成功:返回读取到的字符
	失败:-1
10.3.2 按照行读写文件fgets、fputs
1、写文件

函数介绍:

#include 
int fputs(const char * str, FILE * stream);
功能:将str所指定的字符串写入到stream指定的文件中,字符串结束符 '\0'  不写入文件。 
参数:
	str:字符串
	stream:文件指针
返回值:
	成功:0
	失败:-1

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

int main(void)
{

	FILE *fp = NULL;

	fp = fopen("../test.txt", "w");

	if (fp == NULL) 
	{
		perror("open");
		return -1;
	}

	char *buf[] = { "123456\n", "bbbbbbbbbb\n", "ccccccccccc\n" };
	int i = 0;
	int n = 3;
	for (i = 0; i < n; i++)
	{
		int len = fputs(buf[i], fp);
		printf("len = %d\n", len);
}

	return 0;
}

输出结果:

len = 0
len = 0
len = 0
请按任意键继续. . .
2、读文件

函数介绍:

#include 
char * fgets(char * str, int size, FILE * stream);
功能:从stream指定的文件内读入字符,保存到str所指定的内存空间,直到出现换行字符、读到文件结尾或是已读了size - 1个字符为止,最后会自动加上字符 '\0' 作为字符串结束。
参数:
	str:字符串
	size:指定最大读取字符串的长度(size - 1)
	stream:文件指针
返回值:
	成功:成功读取的字符串
	读到文件尾或出错: NULL

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

int main(void)
{

	FILE *fp = NULL;

	fp = fopen("../test.txt", "r");

	if (fp == NULL)
	{
		perror("open");
		return -1;
	}

	char buf[100] = {0};

	while (!feof(fp)) //文件没有结束
	{
		memset(buf, 0, sizeof(buf));
		char *p = fgets(buf, sizeof(buf), fp);
		if (p != NULL)
		{
			printf("buf = %s", buf);

		}
	}

	return 0;
}

输出结果:

buf = 123456
buf = bbbbbbbbbb
buf = ccccccccccc
请按任意键继续. . .
10.3.3 按照格式化文件fprintf、fscanf
1、写文件

函数介绍:

#include 
int fprintf(FILE * stream, const char * format, ...);
功能:根据参数format字符串来转换并格式化数据,然后将结果输出到stream指定的文件中,指定出现字符串结束符 '\0'  为止。
参数:
	stream:已经打开的文件
	format:字符串格式,用法和printf()一样
返回值:
	成功:实际写入文件的字符个数
	失败:-1

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

int main(void)
{

	FILE *fp = NULL;

	fp = fopen("../test.txt", "w");

	if (fp == NULL)
	{
		perror("open");
		return -1;
	}

	int n = 0;

	n = fprintf(fp, "%d%d%d\n", 1, 2, 3);

	printf("n = %d\n", n);

	return 0;
}

输出结果:

n = 4
请按任意键继续. . .
2、读文件

函数介绍:

#include 
int fscanf(FILE * stream, const char * format, ...);
功能:从stream指定的文件读取字符串,并根据参数format字符串来转换并格式化数据。
参数:
	stream:已经打开的文件
	format:字符串格式,用法和scanf()一样
返回值:
	成功:参数数目,成功转换的值的个数
	失败: - 1

代码介绍:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

int main(void)
{

	FILE *fp = NULL;

	fp = fopen("../test.txt", "r");

	if (fp == NULL)
	{
		perror("open");
		return -1;
	}

	int a = 0;
	int b = 0;
	int c = 0;
	fscanf(fp, "%d %d %d\n", &a, &b, &c);
	printf("a = %d, b = %d, c = %d\n", a, b, c);


	return 0;
}

输出结果:

a = 1, b = 2, c = 3
请按任意键继续. . .
10.3.4 按照块读写文件fread、fwrite
1、写文件

函数介绍:

#include 
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:以数据块的方式给文件写入内容
参数:
	ptr:准备写入文件数据的地址
	size: size_t 为 unsigned int类型,此参数指定写入文件内容的块数据大小
	nmemb:写入文件的块数,写入文件数据总大小为:size * nmemb
	stream:已经打开的文件指针
返回值:
	成功:实际成功写入文件数据的块数目,此值和 nmemb 相等
	失败:0

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

int main(void)
{

	FILE *fp = NULL;

	fp = fopen("../test.txt", "w");

	if (fp == NULL)
	{
		perror("open");
		return -1;
	}

	typedef struct Stu
	{
		char name[50];
		int id;
	}Stu;

	Stu s[3];

	int i = 0;

	for (int i = 0; i < 3; i++)
	{
		sprintf(s[i].name, "stu%d%d%d", i, i, i);
		s[i].id = i + 1;
	}

	int ret = fwrite(s, sizeof(Stu), 3, fp);

	printf("ret = %d\n", ret);

	return 0;
}

输出结果:

ret = 3
请按任意键继续. . .
2、读文件

函数介绍:

#include 
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:以数据块的方式从文件中读取内容
参数:
	ptr:存放读取出来数据的内存空间
	size: size_t 为 unsigned int类型,此参数指定读取文件内容的块数据大小
	nmemb:读取文件的块数,读取文件数据总大小为:size * nmemb
	stream:已经打开的文件指针
返回值:
	成功:实际成功读取到内容的块数,如果此值比nmemb小,但大于0,说明读到文件的结尾。
	失败:0
	0: 表示读到文件结尾。(feof())

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

int main(void)
{

	FILE *fp = NULL;

	fp = fopen("../test.txt", "r");

	if (fp == NULL)
	{
		perror("open");
		return -1;
	}

	typedef struct Stu
	{
		char name[50];
		int id;
	}Stu;

	Stu s[3];
	int ret = fread(s, sizeof(Stu), 3, fp);
	printf("ret = %d\n", ret);

	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("s = %s, %d\n", s[i].name, s[i].id);
	}

	return 0;
}

输出结果:

ret = 3
s = stu000, 1
s = stu111, 2
s = stu222, 3
请按任意键继续. . .
10.4 文件的随机读写
函数1介绍:
#include 
int fseek(FILE *stream, long offset, int whence);
功能:移动文件流(文件光标)的读写位置。
参数:
	stream:已经打开的文件指针
	offset:根据whence来移动的位移数(偏移量),可以是正数,也可以负数,如果正数,则相对于whence往右移动,如果是负数,则相对于whence往左移动。如果向前移动的字节数超过了文件开头则出错返回,如果向后移动的字节数超过了文件末尾,再次写入时将增大文件尺寸。
	whence:其取值如下:
		SEEK_SET:从文件开头移动offset个字节
		SEEK_CUR:从当前位置移动offset个字节
		SEEK_END:从文件末尾移动offset个字节
返回值:
	成功:0
	失败:-1
函数2介绍:
#include 
long ftell(FILE *stream);
功能:获取文件流(文件光标)的读写位置。
参数:
	stream:已经打开的文件指针
返回值:
	成功:当前文件流(文件光标)的读写位置
	失败:-1
函数3介绍:
#include 
void rewind(FILE *stream);
功能:把文件流(文件光标)的读写位置移动到文件开头。
参数:
	stream:已经打开的文件指针
返回值:
	无返回值

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

int main(void)
{

	FILE *fp = NULL;

	fp = fopen("../test.txt", "r");

	if (fp == NULL)
	{
		perror("open");
		return -1;
	}

	typedef struct Stu
	{
		char name[50];
		int id;
	}Stu;

	//假如已经往文件写入3个结构体
	//fwrite(s, sizeof(Stu), 3, fp);

	Stu s[3];
	Stu tmp;
	int ret = 0;

	//文件光标读写位置从开头往右移动2个结构体的位置
	fseek(fp, 2 * sizeof(Stu), SEEK_SET);

	//读第3个结构体
	ret = fread(&tmp, sizeof(Stu), 1, fp);
	if (ret == 1)
	{
		printf("[tmp]%s, %d\n", tmp.name, tmp.id);
	}

	//把文件光标移动到文件开头
	//fseek(fp, 0, SEEK_SET);
	rewind(fp);

	ret = fread(s, sizeof(Stu), 3, fp);
	printf("ret = %d\n", ret);

	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("s === %s, %d\n", s[i].name, s[i].id);
	}


	return 0;
}

输出结果:

[tmp]stu222, 3
ret = 3
s === stu000, 1
s === stu111, 2
s === stu222, 3
请按任意键继续. . .
10.5 Windows和Linux文本文件区别

1、b是二进制模式的意思,b只是在Windows有效,在Linux用r和rb的结果是一样的

2、Unix和Linux下所有的文本文件行都是\n结尾,而Windows所有的文本文件行都是\r\n结尾

3、在Windows平台下,以“文本”方式打开文件,不加b:

  • 当读取文件的时候,系统会将所有的 “\r\n” 转换成 “\n”
  • 当写入文件的时候,系统会将 “\n” 转换成 “\r\n” 写入
  • 以"二进制"方式打开文件,则读\写都不会进行这样的转换

4、在Unix/Linux平台下,“文本”与“二进制”模式没有区别,"\r\n" 作为两个字符原样输入输出

判断文本文件是Linux格式还是Windows格式:

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include

int main(int argc, char **args)
{
	if (argc < 2)
		return 0;

	FILE *p = fopen(args[1], "rb");
	if (!p)
		return 0;

	char a[1024] = { 0 };
	fgets(a, sizeof(a), p);

	int len = 0;
	while (a[len])
	{
		if (a[len] == '\n')
		{
			if (a[len - 1] == '\r')
			{
				printf("windows file\n");
			}
			else
			{
				printf("linux file\n");
			}
		}
		len++;
	}

	fclose(p);

	return 0;
}
10.6 获取文件状态

函数介绍:

#include 
#include 
int stat(const char *path, struct stat *buf);
功能:获取文件状态信息
参数:
	path:文件名
	buf:保存文件信息的结构体
返回值:
	成功:0
	失败-1

保存文件状态信息的结构体:

struct stat {
	dev_t         st_dev;         //文件的设备编号
	ino_t         st_ino;          //节点
	mode_t        st_mode;   //文件的类型和存取的权限
	nlink_t       st_nlink;     //连到该文件的硬连接数目,刚建立的文件值为1
	uid_t         st_uid;         //用户ID
	gid_t         st_gid;         //组ID
	dev_t         st_rdev;      //(设备类型)若此文件为设备文件,则为其设备编号
	off_t         st_size;        //文件字节数(文件大小)
	unsigned long st_blksize;   //块大小(文件系统的I/O 缓冲区大小)
	unsigned long st_blocks;    //块数
	time_t        st_atime;     //最后一次访问时间
	time_t        st_mtime;    //最后一次修改时间
	time_t        st_ctime;     //最后一次改变时间(指属性)
};

代码示例:

#include 
#include 
#include 

int main(int argc, char **args)
{
	if (argc < 2)
		return 0;

	struct stat st = { 0 };

	stat(args[1], &st);
	int size = st.st_size;//得到结构体中的成员变量
	printf("%d\n", size);
	return 0;
}
10.7 删除文件、重命名文件
函数1介绍:
#include 
int remove(const char *pathname);
功能:删除文件
参数:
	pathname:文件名
返回值:
	成功:0
	失败:-1
函数2介绍
#include 
int rename(const char *oldpath, const char *newpath);
功能:把oldpath的文件名改为newpath
参数:
	oldpath:旧文件名
	newpath:新文件名
返回值:
	成功:0
	失败: - 1
10.8 文件缓冲区
10.8.1 文件缓冲区

ANSI C标准采用“缓冲文件系统”处理数据文件。所谓缓冲文件系统是指系统自动地在内存区为程序中每一个正在使用的文件开辟一个文件缓冲区从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去。如果从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(给程序变量) 。

10.8.2 磁盘文件的存取

【五】 C语言基础知识学习回顾 | 一篇搞定C语言基础 | 内附详细代码以及注释_第21张图片

1、磁盘文件,一般保存在硬盘、U盘等掉电不丢失的磁盘设备中,在需要时调入内存

2、在内存中对文件进行编辑处理后,保存到磁盘中

3、程序与磁盘之间交互,不是立即完成,系统或程序可根据需要设置缓冲区,以提高存取效率

10.8.3 更新缓冲区

函数介绍:

#include 
int fflush(FILE *stream);
功能:更新缓冲区,让缓冲区的数据立马写到文件中。
参数:
	stream:文件指针
	返回值:
	成功:0
	失败:-1


更多有关于Linux C++后台开发的学习分享,感兴趣的朋友们可以关注我的个人公众号
在这里插入图片描述

你可能感兴趣的:(C/C++)