C语言——基础语法

C语言速通

第一部分

写在前面的话

大家好,我是light。最近闲来无事,想写一些关于c语言的代码,希望对想要学习c语言的读者有帮助。

c语言背景

程序语言发展是由二进制代码语言(机器语言)开始发展,该语言不用翻译,可以用机器直接运行。再到后来的汇编语言,也就是简单的将一些常用的二进制代码片段用一些简单的英文字母所代替,再按照一定规则书写,

因此,汇编语言要比机器语言更便于阅读和理解。由于汇编语言依赖于硬件体系,并且助记符号数量比较多,所以其运用起来仍然不够方便。高级语言便应运而生。高级语言,其语法形式类似于英文,并且因为不需要对硬件进行直接操作,因此易于被普通人所理解与使用。

近年比较流行的高级语言包括C、C++、C#、VC、VB、Python、Java等。而在这些语言中,c语言更偏向于底层语言,对于初学者而言,学习c语言更有助于理解其他语言。很多芯片开发,软件编写都离不开c语言,对于电、通、气、自等专业的学生而言,c语言是必不可少的工具之一。

c语言的优缺点

C语言是一种面向过程的语言,同时具有高级语言和汇编语言的优点。C语言可以广泛应用于不同的操作系统,如UNIX、MS-DOS、Microsoft Windows及Linux等。

c语言具有较高的运行效率。它继承了低级语言的优点,代码运行效率高,并具有良好的可读性和编写性。通常的,使用c语言开发出来的程序运行效率可达到汇编程序的80%~90%。

C语言的丰富的语句与关键字使得其代码不拘一格,灵活多变。

同时c语言支持自定义结构,这极大方便了程序表达一些复杂多变的数据类型。更有甚者可利用该特点,在自定义数据结构类型中加入函数指针,就可以实现c语言面向对象编程。

不仅如此,c语言还在拥有高运行效率的基础上,对语言语法进行优化,更贴近于人类思维。可以这么说,c语言是一 将高效与可读性进行有机结合、介于高级语言与底层语言之间、适用于底层软件开发与芯片开发的 语言。

C语言也有一些缺点。C语言使用指针,而涉及指针的编程错误往往难以察觉。导致代码bug难以察觉,必须有长时间的练习才能写出高质量代码。

C语言部分运算符组合之后晦涩难懂。C语言缺点有很多,但不是我们讨论的重点,在此就不多费口舌了。

c语言的编译环境

对于已经学过一段时间的读者来说,选择一款自己最熟悉的IDE开发环境是最好的,对此我推荐visual studio与visual studio code等类似的具有强大功能的开发环境,这些开发环境内自带的代码补全、自动纠错、代码优化等功能能令我们软件开发变得事半功倍。

而对于初学者而言,我更推荐一些基础的开发IDE环境,例如DEV、VC6.0、或者一些简单的具有代码自动换行的c语言在线编译器即可。由或者可以用比较高级的代码编译器,将其代码自动补全功能关闭即可。因为对于初学者而言,自己手动敲全代码是很重要的。这样可以适应绝大多数开发环境,防止开发环境一换,对于c语言的关键字例如include都无法敲全。

小编之前学c语言时使用的是vc++6.0,这个软件网络上有很多资源,这里给大家提供一个。
链接:https://pan.baidu.com/s/1PApsWKEDMzvqM9NmSuw6hA
提取码:zukk

或者也可以用菜鸟工具中的在线编译器
https://c.runoob.com/compile/11/

VS下载地址官网
https://visualstudio.microsoft.com/zh-hans/

本文章前半部分采用线上编译器,后半部分涉及到程序调试采用visual studio 2019。

第二部分

这部分内容出于全面性考虑,各个部分都有一些介绍,但由于不够系统全面,可能有些难以理解。但是这对于我们学习是没有影响的,这部分内容更多偏向于建立一个系统概念与整体框架,内容了解即可。

第一个c语言程序

当我们有一个舒服的开发环境就可以尝试编写自己的第一个程序了。

#include <stdio.h>
int main()
{
   printf("Hello, World! \n");
   return 0;
}

将其复制或者手动键入菜鸟工具在线编译器后,点击运行即可
运行效果
下面简单介绍一下c语言的书写格式。
在文章开头有#include <stdio.h>,这叫做头文件包含,stdio.h是标准(std)输入(in)输出(out)头文件,里面包含了一些常用函数封装,只有包含对应的头文件,我们的程序才能正常编译。

int main()是程序运行的入口,不管我们程序写的多么复杂,c语言的程序都是从main函数开始运行。

{}内是函数体,也就是函数的具体内容。在这个简单程序中主函数main内一共有两条语句,一个是printf(“Hello world!\n”); 这句话的意思是让屏幕输出一行字Hello world!,而\n表示换行。另一句话return 0;表示这个函数运行完成后给系统一个0的反馈。一般情况下,我们可以通过返回值判断该函数运行成功与否。

通过这个简单程序我们可以大概了解一个程序如何编写,以及程序基本框架是什么样子。初学者可能编译失败,其中几个比较容易出问题的点是:

  • 1、所有标点符号是英文标点,大家可以看一下中文标点和英文标点区别:分号;;;;;;双引号"“”" “”“” 逗号,,,,。
  • 2、要注意每一句话都需要以分号结尾;
  • 3、特别的,当我们在代码中嵌入中文空格时候,代码也会报错,并且这种错误很难发现,一定一定要注意。

我们可以随意修改双引号之间的内容,则屏幕上输出的内容也会随之而变。例如

#include <stdio.h>
int main()
{
	//这是之前语句
	printf("Hello, World! \n");
	
	/*下面两条是新添加的语句*/
	printf("我想换个东西输出,");
	printf("看一下效果\n");
	
	return 0;
}

在这里插入图片描述
一般我们会在难懂的语句后面添加代码注释,方便理解,注释格式有两种
1、以//开始,后面整行都会被注释。但是该方法只能注释一行或半行
2、以/* 开始,以 */结尾,中间文字会被注释。该方法灵活多变,但是不能嵌套。例如下面代码是错误的,嵌套注释1的开始会自动与嵌套注释2的结尾匹配。
在这里插入图片描述

#include <stdio.h>
int main()
{
	//
	return 0;
	/*嵌套注释1
			/*嵌套注释2*/
	*/
	

编写规范的代码格式和添加详细的注释,是一个优秀程序员应该具备的好习惯。

到此为止,我们就学会了c语言的基本编写格式与书写规范了,有兴趣的同学可以尝试用刚学的知识在屏幕上输出一句格言:

不作什么决定的意志不是现实的意志;无性格的人从来不做出决定。
——黑格尔

参考例程

//第一个c语言程序参考例程1
#include <stdio.h>
int main()
{
	printf("不作什么决定的意志不是现实的意志;无性格的人从来不做出决定。\n");
	printf("——黑格尔\n");
	return 0;
}

在这里插入图片描述

了解c语言格式规范

  • 1、C语言程序的格式,main函数不论放在什么位置都没有关系,程序都是从main函数开始执行
  • 2、C语言程序一般是一步一步的按过程执行,通常执行单元是函数,所用可以说C语言程序是由函数组成的。
  • 3、C语言函数内容放在{}中间,函数内容由多个语句组成,每个语句用";"结尾。
  • 4、所有变量必须先定义后使用,并且变量定义必须在函数最开始。
  • 5、一条语句单独占用一行,{和}单独占用一行,每行结尾添加’'可以续行,即下一行内容也算本行内容。
  • 6、要注意合理缩进,空格和空行的作用是增加程序的可读性,使得程序代码位置合理、美观。
  • 7、目前主流C语言有两种标准,旧标准是C99,新标准是C11,本文将全部以C11标准为准。
  • 8、名称命名要能体现该名称作用,要用_或者大写字母区分两个单词,例如表示高度的变量我们用hight,表示屏幕高度的变量用hight_screen。
  • 9、各种命名都不能与关键字相同,关键字指的是C语言中规定的一些具有特殊功能的词语,例如include、int、main、return单词都是关键字。

进制转换与反码补码

本小结仅需了解即可。

进制转换

我们数学所学习的都是十进制数,在c语言无需添加任何符号的数字即表示十进制数,例如0、5、10;除了这个我们所熟悉的数以外,c语言常用进制还有2进制(寄存器)、8进制(较少用)、16进制(地址)。

他们之间可以互相转换,转换表如下

二进制八进制十进制十六进制二进制八进制十进制十六进制
0B00000000x000B100001080x08
0B00010110x010B100101190x09
0B00100220x020B1010012100x0a
0B00110330x030B1011013110x0b
0B01000440x040B1100014120x0c
0B01010550x050B1101015130x0d
0B01100660x060B1110016140x0e
0B01110770x070B1111017150x0f

看上面转换表可能有小伙伴有疑问,为什么有0B和0x前缀,0b或0B是二进制数字前缀,如果加上0b则表示该数据是一个二进制数,当我们看到0B10时要明白这是二而不是十。同理0x或0X是十六进制数前缀,可能有人不理解为什么要写0x08而不是0x8,其实他们是一样的,只是我习惯于写一个字节数据。而单独以0为前缀的默认是八进制。

在c语言中,我们可以直接写
int a=0b10;(a的值为二)
int a=010;(a的值为八)
int a=10;(a的值为十)
int a=0x10;(a的值为十六)

#include <stdio.h>
int main()
{
	int a=0b10;
	int b=010;
	int c=10;
	int d=0x10;
	printf("%d %d %d %d\n",a,b,c,d);		//以十进制输出4个变量,后续介绍
	return 0;
}

在这里插入图片描述

原码、反码、补码

我们首先要建立一个概念就是,计算机内部存储只有0或1,也就是所谓的二进制,更底层一点,在计算机中,只有高低电压变化,我们定义高电压为1,低电压为0,这样我们只要设计出可以存储高低电平的电路就可以存储数据了。

基于这个概念,我们可以简单的将计算机内存看做一堆0或1的数据,一个可以存储01数据的单位我们称为bit(比特),8个bit作为一个Byte(字节),1024(210)个Byte作为1KB,1024KB作为1MB,1024MB作为1GB,1024GB作为1TB……32位电脑就是指电脑最小存储单元是32bit,64位电脑最小存储单元则是64bit。以32位电脑为例,int类型变量占据存储空间有32bit,在计算机中表示数字0和数字6
0000 0000 0000 0000 0000 0000 0000 0000(0)
0000 0000 0000 0000 0000 0000 0000 0110(6)
所以理论上来说,不考虑负数,32bit的无符号unsigned int可以表示的数字范围为0到232-1。
而实际存储时,一般将最高位作为符号位,0表示正,1表示负。32bit带符号数int类型可表示数字范围是-231到2^31-1

0000 0000 0000 0000 0000 0000 0000 0001(1)
1000 0000 0000 0000 0000 0000 0000 0001(-1)
这就是最原始的原码,但是有一个问题,计算机不能直接对数字加减,(-1+1=0)
+1000 0000 0000 0000 0000 0000 0000 0001(-1)
+0000 0000 0000 0000 0000 0000 0000 0001(1)
=1000 0000 0000 0000 0000 0000 0000 0010(-2)
很显然不是我们希望的结果,所以不能用原码直接计算,对此我们计算机采用补码方式存储。

正数的原码反码补码都是同样的。对于负数,我们会将除了最高位符号位外的其他所有数字取反,这就是负数的反码。负数反码加1就是负数的补码。
1000 0000 0000 0000 0000 0000 0000 0001(-1原码)
1111 1111 1111 1111 1111 1111 1111 1110(-1反码)
1111 1111 1111 1111 1111 1111 1111 1111(-1补码)
0000 0000 0000 0000 0000 0000 0000 0001(1原码)
0000 0000 0000 0000 0000 0000 0000 0001(1反码)
0000 0000 0000 0000 0000 0000 0000 0001(1补码)

在计算机中,数据都是以补码形式保存,我们重新计算(-1+1)
+1111 1111 1111 1111 1111 1111 1111 1111(-1补码)
+0000 0000 0000 0000 0000 0000 0000 0001(1补码)
=0000 0000 0000 0000 0000 0000 0000 0000(0补码)
这样算出来的数据就是我们所期望的。

数据类型

本章节介绍数据类型包括int、short、long、unsigned、char、float、double、_Bool
常用运算符=,sizeof()
常用输入输出函数printf(),scanf()
常用修饰符const,extern,static,volatile

常量使用

在程序中,所有确切的数字例如5、1都是常量。除了数字常量外,还有字符常量和字符串常量。

字符常量是用单引号’选中的字符,单引号内只能有一个字符。例如’a’、‘b’、‘A’、‘1’、‘.‘等都是字符常量

字符串常量是用双引号选中的字符,双引号内可以有0到多个字符,例如"asdf\n",“hello world!”,双引号内字符数量总是比我们所看到的字符数量多一个,因为它会在字符串常量结尾自动添加一个’\0’休止符。例如:""内有一个字符’\0’,"asdf"有5个字符’a’,‘s’,‘d’,‘f’,‘\0’。

NULL是空指针常量类型,后续介绍。

变量定义与使用

在C语言中,变量与通常数学意义的自变量不同,C语言变量指的是具有一定内存的,可以修改值的一个存储单元。每当我们要使用变量保存数据时,我们都需要定义该变量,也就是在电脑中为该数据分配一个空间用于存储。

#include <stdio.h>
int main()
{
	int a;			//定义一个变量a,a的值处于非法状态
	int b=1;		//定义一个变量b,b的值为1
	return 0;		//退出主函数
}

在该程序中,我们定义了两个整形变量。int表示变量类型–整形,a和b为变量名称。在变量定义时候我们可以选择是否对变量进行初始化。当时需要注意的是,如果我们没有对变量初始化时,变量就处于非法状态,如果这时候我们使用了该变量,可能会导致不可预知的错误,具体错误由编译器决定。并且这种错误部分编译器不会报错,可能导致程序出现莫名其妙的bug,所用我们使用变量时最好先定义并初始化。

变量可以一次定义一个,也可以一次定义多个同类型变量,并且可以分别对变量进行初始化。例如:

#include <stdio.h>
int main()
{
	int a,b=1,c;					//变量定义
	printf("a=%d,b=%d,c=%d\n",a,b,c);		//输出变量
	return 0;
}

在这里插入图片描述
我们之前使用过printf("Hello World!\n");
其中’\n’是转义字符,他的意思是换行,而这里出现的printf("a=%d,b=%d,c=%d\n",a,b,c);
中’%d’是以十进制输出一个变量,具体变量内容在双引号后按顺序排列,用’,'隔开。

变量命名规则

  • 1、变量名以英文字母或下划线_开头;
  • 2、变量名中的字母是区分大小写的;
  • 3、变量名不能是关键字;
  • 4、变量名中不能包含空格、标点符号和类型说明符。
  • 5、a变量名的开头不能是数字,可以由字母或下划线开头。 但,实际上编程中最常用的是以字母开头,而以下划线开头的变量名是系统专用的。
  • 6、变量名称不能和C语言关键字相同,例如不可以使用int作为变量名称。C语言关键字一共有32个,auto double int struct break else long switch case enum register typedef char extern return union const float short unsigned continue for signed void default goto sizeof volatile do if while static。

变量类型介绍

我们之前使用的一直都是int类型变量,实际上C语言主要使用的变量类型有整型变量、浮点型变量、字符型变量、布尔类型及自定义变量(结构体变量)这几种类型,衍生类型右指针类型、数组类型、字符串类型。

整型变量

整型变量又称实型变量,不同电脑或机器对变量内存大小定义不同。对于32位机器,int整型变量所占内存为4字节(32bit);对于16位机器,int类型所占内存空间为2字节(16bit);对于64位机器,int类型所占空间为8字节(64bit);本文章默认以32位机器为准。

带符号整型有int、short、long和c11新添加的long long类型,不带符号整型有unsigned int、unsigned short、unsigned long、unsigned long long类型。具体见表格

类型名称内存大小理论范围
short2字节-215到215-1
unsigned short2字节0到216-1
int4字节-231到231-1
unsigned int或unsigned4字节0到232-1
long8字节-263到263-1
unsigned long8字节0到264-1
long long16字节-263到263-1
unsigned long long8字节0到264-1

unsigned long long类型是目前C语言中精度最高的数据类型,可以用来表示20以内的阶乘数据,20以外的自测。并且unsigned long long的占用8字节64位,double或者long double 虽然也占有8个字节,但是他们的实际精度只有53位。

通过上述表格不难发现,对于整型变量,只要在类型前添加unsigned前缀就可以将该变量类型定义为无符号整型变量。我们可以用sizeof()运算符获取他们所占用地址的大小。例如:

#include <stdio.h>
int main()
{
	short a;
	unsigned short b;
	int c;
	unsigned d;
	long e;
	unsigned long f;
	long long g;
	unsigned long long h;
	printf("short内存大小%d\n\
		   unsigned short内存大小%d\n\
		   int内存大小%d\n\
		   unsigned int内存大小%d\n\
		   long内存大小%d\n\
		   unsigned long内存大小%d\n\
		   long long内存大小%d\n\
		   unsigned long long内存大小%d\n",\
		   sizeof(a),sizeof(b),sizeof(c),\
		   sizeof(d),sizeof(e),sizeof(f),\
		   sizeof(g),sizeof(h));

	return 0;
}

在这里插入图片描述
这里有两个新知识点,一个是sizeof运算符,它用于计算变量所占据的内存空间;另一个则是当一行语句过长时,在当前行后面添加’‘续行符即可将下一行也当做本行。对比程序可以看到不同类型所占内存大小不同。

后续我们会了解更多关于printf函数的用法,现在我们只需记住’\n’是换行,‘%d’是以十进制输出变量,’%u’是以无符号十进制输出变量即可。

#include <stdio.h>

int main()					//原码					:1000 0000 0000 0000 0000 0000 0000 0001
{							//反码					:1111 1111 1111 1111 1111 1111 1111 1110
	int a=-1;				//计算机中存储的补码		:1111 1111 1111 1111 1111 1111 1111 1111
							//分别用带符号数和无符号数输出
	printf("带符号数:%d\n",a);
	printf("无符号数:%u\n",a);

	return 0;
}

在这里插入图片描述
之所以会将-1输出成4294967295是因为系统将-1的补码翻译成无符号正数,刚好是232-1。

整型变量无法做带小数点的除法运算,他的结果总是除法运算的向下取整,除非我们就是需要这个效果,负责涉及除法的运算应该使用下面即将介绍的浮点型。例如:

#include <stdio.h>
int main(void)
{
	int a=5,b=2;
	int c=a/b;
	printf("%d/%d=%d\n",a,b,c);
	return 0;
}

在这里插入图片描述

浮点型变量

浮点型变量可以简单理解成我们通常所说的小数。整型变量只能保存整型值,如果强行赋值一个小数,系统会自动向下取整。这是因为系统存储小数时候,会自动将实数拆成整数与小数两部分,当遇到类似于需要将小数转为整数的情况,系统会只看整数部分。

#include <stdio.h>
int main()
{
	int a;
	a=0.618;
	printf("%d\n",a);
}

在这里插入图片描述
如果我们想要保存一个小数,那么我们就需要使用浮点型变量。浮点型变量常用的有float、double两种,两者区别在于内存大小与精度不一样

类型名称占用内存精度
float4字节6-7位小数点
double8字节15-16位小数点

他们使用方法与整型变量类似。printf支持输出小数,其符号略有区别,float类型用%f,double类型用%lf(可以理解为long float)。

#include <stdio.h>
int main()
{
	float a=3.1415926535897932384626;
	double b=3.1415926535897932384626;
	printf("单精度a=%f,双精度b=%lf\n",a,b);
	return 0;
}

在这里插入图片描述
默认%f和%lf输出6位小数,我们可以在%后面添加’.'和一个数字用以控制输出小数位数。

#include <stdio.h>
int main()
{
	float a=3.1415926535897932384626;
	double b=3.1415926535897932384626;
	printf("单精度a=%.16f,双精度b=%.16lf\n",a,b);		//保留16位小数
	return 0;
}

在这里插入图片描述
可以看到float类型数据从第8位小数开始,数据已经无效了。

字符型变量

字符型变量其实与整型变量及其相似。字符型变量只有一种类型char,衍生类型有unsigned char

类型名称内存大小数据范围
char1字节(8bit)-128到127
unsigned char1字节(8bit)0到255

虽然字符变量本质是一个整型数,但是大多数时候我们都把这些数赋予一个特殊含义,在国际上通用标准是ASCII码表。

ASCII值控制字符ASCII值控制字符ASCII值控制字符ASCII值控制字符
0NUT1SOH2STX3ETX
4EOT5ENQ6ACK7BEL
8BS9HT10LF11VT
12FF13CR14SO15SI
16DLE17DC118DC219DC3
20DC421NAK22SYN23TB
24CAN25EM26SUB27ESC
28FS29GS30RS31US
32(space)33!34"35#
36$37%38&39
40(41)42*43+
44,45-46.47/
480491502513
524535546557
56857958:59;
60<61=62>63?
64@65A66B67C
68D69E70F71G
72H73I74J75K
76L77M78N79O
80P81Q82R83S
84T85U86V87W
88X89Y90Z91[
92\93]94^95_
96`97a98b99c
100d101e102f103g
104h105i106j107k
108l109m110n111o
112p113q114r115s
116t117u118v119w
120x121y122z123{
124125}126~127DEL

而printf也可以输出字符,符号为%c

#include <stdio.h>
int main()
{
	char a=97;
	char b='b';
	printf("%d,%d\n",a,b);//十进制输出
	printf("%f,%f\n",a,b);//浮点数输出
	printf("%c,%c\n",a,b);//字符型输出
	return 0;
}

在这里插入图片描述
可以看到,当我们用十进制’%d’输出时候,可以将char类型当做普通数字变量使用,当我们使用’%c’输出时候,可以正常输出ascii码所对应的字符;但是当我们试图用浮点型格式符’ %lf’ 输出字符或整型变量时候,却发生错误。这是因为浮点数与整型数存储规则不同导致的。我们写代码时候应该尽量避免变量类型与输出格式符号不匹配。

布尔类型

在c99和c11的c语言标准中都添加了_Bool类型,布尔类型实际就是一个只有0或1的变量,也就是1bit。0表示假,1表示真。当我们将_Bool类型与其他类型混用时,采用的规则是0表示假,非0数表示真。

#include <stdio.h>
int main()
{
	_Bool a=97;
	printf("%d\n",a);
	return 0;
}

在这里插入图片描述
可以看到当我们试图将97赋值给_Bool类型时候,系统判定97是真,于是赋值真(1)给_Bool类型变量a,当以十进制数输出a的时候,a的值为1。

自定义变量类型

自定义变量类型有两大类,一种是结构体struct,另一种是共用体union。这里我们简单介绍一下结构体,后续会有更详细的介绍。

结构体其中一种使用格式为

typedef struct
{
	数据类型 数据1;
	数据类型 数据2;
	...
}结构体名称;

并且结构体类型定义不同于结构体变量定义,一般会将结构体类型定义放在程序最开始,而将结构体变量定义放在main函数内部。定义了结构体类型之后,我们可以定义结构体变量,使用方法类似于int类型。

我们无法直接对结构体变量进行赋值操作,想要访问结构体内的数据时,我们采用’.'运算符。结构体类型定义有几种不同方式,后续我们会详细讨论。

使用示例

#include <stdio.h>
//结构体类型定义
typedef struct
{
	int a;
	double b;
}INT_DOUBLE;
int main()
{
	INT_DOUBLE x;							//结构体变量定义
	x.a=10;									//结构体变量的使用
	x.b=3.1415;
	printf("结构体使用示例\n");
	printf("x内a=%d,x内b=%lf\n",x.a,x.b);
	return 0;
}

在这里插入图片描述
因为结构体与共用体对于现阶段而言较为复杂,考虑文章结构问题,将其放在后续章节统一介绍。

指针类型

指针概念简单,但是由于指针赋予编程者过大的权限,导致指针程序如果编写不当会有严重后果,所以本小节只介绍指针类型,在后续有专门指针章节讨论该问题。

在C语言中我们可以简单且抽象的将数据理解成线性存储空间,每个地址对于1字节内存,如下表:

名称地址内容
0x 0000 0000未知
0x 0000 0001未知
0x 0000 000f未知
0x 0000 0010未知
0x 0000 0011未知
0x f f f f  f f f f未知

当我们使用int a;定义一个变量后,系统会自动分配一个内存地址给他,在之后任何需要a的地方,cpu都会从该地址读取或写入数据。例如:

#include <stdio.h>
int main()
{
	int a=0x12345678;
	printf("a的地址为%p",&a);
	return 0;
}


这里有两个新知识点,%p是以十六进制显示一个地址。'&'取址符是获取变量地址。上述例程作用是定义一个变量,输出该变量的地址。

0x7ffdb0729dbc则是该变量地址,由于这是菜鸟工具在线编译器,其地址位数依赖于在线服务器的系统。

同样的程序在visual studio 2019(vs2019)运行结果为:
在这里插入图片描述

这就是64位机电脑运行结果,0x 0000 0035 525F FCA4转为二进制为:
0B 0000 0000 0000 0000 0000 0000 0011 0101 0101 0010 0101 1111 1111 1100 1010 0100
可以看到变量地址是64位的。

名称地址内容
0x 0000 0000 0000 0000未知
0x 0000 0035 525F FCA3未知
a0x 0000 0035 525F FCA40x78
0x 0000 0035 525F FCA50x56
0x 0000 0035 525F FCA60x34
0x 0000 0035 525F FCA70x12
0x 0000 0035 525F FCA8未知
0x FFFF FFFF FFFF FFFF未知

可以看到,系统为int a;变量分配从0x0019ff2c开始的4个字节。并且低八位在前,高八位在后。

而指针就是专门针对变量地址进行操作。指针变量定义:

int *pint=NULL;			//定义int类型指针,指针内容为空
char *pchar=NULL;		//定义char类型指针,指针内容为空
float a=0,*pfloat1;		//定义float类型变量a=0,float类型变量指针内容无意义
float *pfloat2=&a;		//定义float类型指针,内容为a的地址
pfloat1=&a;				//指针赋值操作,同类型取址符
pfloat2=pfloat1;		//指针赋值操作,同类型直接赋值

不同类型指针只需在类型后添加*即可,可以一条语句定义多个相同类型指针变量与相同类型普通变量,并且可以分别初始化。指针可以选择不初始化(野指针)、初始化为NULL(空指针)、或者初始为某个相同类型变量的地址,用取址符&对变量取地址。例如

int a=0;
int p1;
int p2=NULL;
int p3=&a;


指针使用有一下几种操作,假设有int a=5;int *p=&a;

名称运算符描述示例
赋值=将p指向某一地址p=NULL将地址清空
地址自增++p所指向的地址向后移h字节,h为该地址变量的类型的大小p++ 地址向后移4字节
地址自减p所指向的地址向前移h字节,h为该地址变量的类型的大小p-- 地址向前移4字节
地址加+p所指向的地址向后移x*h字节,x为操作数,h为该地址变量类型的大小p=p+5 地址向后移5*4个字节
地址减-p所指向的地址向前移x*h字节,x为操作数,h为该地址变量类型的大小p=p-5 地址向前移5*4个字节
解引用*获取p所指向的地址内容。*p=6; a的值被修改为6

指针本质就是一个int数,数据内容则是某个变量得地址,下面例程与表格将描述指针的原理。

#include <stdio.h>
int main()
{
	int a=5;
	int *p=NULL;
	printf("a的地址0x%p\n",&a);
	printf("p的地址0x%p\n",&p);
	printf("a的内容%d\n",a);
	printf("p的内容0x%p\n",p);
	//printf("p指向的地址的内容%d\n",*p);		//此时p=NULL,解引用后会导致程序崩溃
	printf("\n");

	p=&a;
	printf("a的地址0x%p\n",&a);
	printf("p的地址0x%p\n",&p);
	printf("a的内容%d\n",a);
	printf("p的内容0x%p\n",p);
	printf("p指向的地址的内容%d\n",*p);
	printf("\n");

	*p=10;
	printf("a的地址0x%p\n",&a);
	printf("p的地址0x%p\n",&p);
	printf("a的内容%d\n",a);
	printf("p的内容0x%p\n",p);
	printf("p指向的地址的内容%d\n",*p);
	
	return 0;
}

VS2019运行结果:
在这里插入图片描述

执行

int a=5;
int *p=NULL;

后,数据如下表

名称地址内容
a的第1个字节0x 0000 007C D583 FAC40x05
a的第2个字节0x 0000 007C D583 FAC50x00
a的第3个字节0x 0000 007C D583 FAC60x00
a的第4个字节0x 0000 007C D583 FAC70x00
上述字节连起来为0x0000 0005
p的第1个字节0x 0000 007C D583 FAE80x00
p的第2个字节0x 0000 007C D583 FAE80x00
p的第3个字节0x 0000 007C D583 FAE80x00
p的第4个字节0x 0000 007C D583 FAE80x00
p的第5个字节0x 0000 007C D583 FAE80x00
p的第6个字节0x 0000 007C D583 FAE80x00
p的第7个字节0x 0000 007C D583 FAE80x00
p的第8个字节0x 0000 007C D583 FAE80x00
上述字节连起来为0x0000 0000 0000 0000

执行p=&a;后,数据如下表

名称地址内容
a的第1个字节0x 0000 007C D583 FAC40x05
a的第2个字节0x 0000 007C D583 FAC50x00
a的第3个字节0x 0000 007C D583 FAC60x00
a的第4个字节0x 0000 007C D583 FAC70x00
上述字节连起来为0x0000 0005
p的第1个字节0x 0000 007C D583 FAE80xC4
p的第2个字节0x 0000 007C D583 FAE80xFA
p的第3个字节0x 0000 007C D583 FAE80x83
p的第4个字节0x 0000 007C D583 FAE80xD5
p的第5个字节0x 0000 007C D583 FAE80x7C
p的第6个字节0x 0000 007C D583 FAE80x00
p的第7个字节0x 0000 007C D583 FAE80x00
p的第8个字节0x 0000 007C D583 FAE80x00
上述字节连起来为0x0000 007C D583 FAC4

执行*p=10;后,数据如下表

名称地址内容
a的第1个字节0x 0000 007C D583 FAC40x0A
a的第2个字节0x 0000 007C D583 FAC50x00
a的第3个字节0x 0000 007C D583 FAC60x00
a的第4个字节0x 0000 007C D583 FAC70x00
上述字节连起来为0x0000 0005
p的第1个字节0x 0000 007C D583 FAE80xC4
p的第2个字节0x 0000 007C D583 FAE80xFA
p的第3个字节0x 0000 007C D583 FAE80x83
p的第4个字节0x 0000 007C D583 FAE80xD5
p的第5个字节0x 0000 007C D583 FAE80x7C
p的第6个字节0x 0000 007C D583 FAE80x00
p的第7个字节0x 0000 007C D583 FAE80x00
p的第8个字节0x 0000 007C D583 FAE80x00
上述字节连起来为0x0000 007C D583 FAC4

所以说,地址变量p就是一个int数保存了另一个变量的地址。&p是地址变量自己的地址,例程中是0x0000 007C D583 FAE8;p是保存的同类型变量的地址,本例程中是0x 0000 007C D583 FAC4;*p是解引用,即访问该地址的所保存的数据,本例程中是0x 0000 000A;

同理,其他类型变量也是类似,仅仅是变量类型改变。例如:

#include <stdio.h>
int main()
{
	char a = 'X';
	char* p = NULL;
	printf("a的地址0x%p\n", &a);
	printf("p的地址0x%p\n", &p);
	printf("a的内容%c\n", a);
	printf("p的内容0x%p\n", p);
	//printf("p指向的地址的内容%c\n",*p);		//此时p=NULL,解引用后会导致程序崩溃
	printf("\n");

	p = &a;
	printf("a的地址0x%p\n", &a);
	printf("p的地址0x%p\n", &p);
	printf("a的内容%c\n", a);
	printf("p的内容0x%p\n", p);
	printf("p指向的地址的内容%c\n", *p);
	printf("\n");

	*p = 'B';
	printf("a的地址0x%p\n", &a);
	printf("p的地址0x%p\n", &p);
	printf("a的内容%c\n", a);
	printf("p的内容0x%p\n", p);
	printf("p指向的地址的内容%c\n", *p);

	return 0;
}

在这里插入图片描述
既然指针也是变量,那么便可以有另一个指针变量指向该变量,称为二维指针。

int a=5;		//a的内容是5
int *p=&a;		//p的内容是a的地址
int **pp=&p;	//pp的内容是p的地址
a=6;			//修改a

p=&a;			//修改p
*p=7;			//修改a

pp=pp+1;		//修改pp
*pp=&a;			//修改p
**pp=8;			//修改a
数组与字符串

当读者看到这里是,基本已经了解C语言类型与变量了,那我们就不得不考虑一下,如果我们想要定义10个变量怎么办,100个呢,1000个呢。于是便有了数组的概念。

可以通过定义数组来解决这个问题。严格来说数组并不是一种数据类型,而是带有一定存储空间的地址。当我们想要一次定义100个int类型的数时,可以

int a[100];
int a[100]={0}
int a[100]={1,2,3,4,5,6,7,8,9,10};

如果不对数组进行初始化,则数组内的数据处于无效状态。数组定义时可以选择不初始化或列表初始化。={0}会将该数组中所有元素全部赋值为0;
={1}会将第一个元素赋值为1,其他元素全部赋值为0;={1,2,3,4,5,6,7,8,9,10};则是将数组中前10个元素对应赋值,后续数据默认赋值为0;

单独一个名称a表示数组首地址,不需要再对a使用取址符&。
当我们想要单独使用数组中某一元素时,可以采用下标索引a[0];例如

#include <stdio.h>
int main()
{
	int a[5]={0,1,2,3,4};
	printf("%d",a[3]);
	return 0;
}

在这里插入图片描述
下标索引需要注意几点

  • (1)下标从0开始。例如int a[5];一共有5个元素,分别是a[0],a[1],a[2],a[3],a[4],要注意没有a[5]。
  • (2)数组只有在初始化时可以采用列表给所有数据赋值,之后只能单个数据访问。例如a={1,2,3}是错误的,应该写:a[0]=1;a[1]=2;a[2]=3;
  • (3)下标越界大部分编译器检查不出错误,但是会导致严重的、难以察觉的bug,一定要注意数组下标不能越界。例如
  • (4)数组长度只能由常量表达式组成,可以定义int a[4+1];但是不能用变量定义数组,例如 int a[i]是错误的。
#include <stdio.h>
int main()
{
	int a[5]={0,1,2,3,4};
	printf("a[0]=%d,不存在的a[100]=%d\n",a[0],a[100]);
	return 0;
}

在这里插入图片描述

有一种特殊数组叫字符串。当我们想要char类型数组时,可以

char a[10];
char a[10]={'a','b'};
char a[10]={101,102,103,104,105,106,107,108,109,110};
char a[10]={0};
char a[10]="hello wor";
char a[]="hello world";

这种char类型数组我们又称字符串。
char a[10]表示定义一个大小为10的char类型字符串,字符串(或称数组)内变量都未初始化。
char a[10]={‘a’,‘b’};则是将ab对应的ascii码值赋值个字符串前两个变量,后面8个变量初始化为0;
char a[10]={101…110};是将字符串a中每个元素对应赋值。
char a[10]={0};是将字符串a中所有元素初始化为0
char a[10]=“hello wor”;定义了一个字符串并对其赋值,等价于
char a[10]={‘h’,‘e’,‘l’,‘l’,‘o’,’ ‘,‘w’,‘o’,‘r’,’\0’};注意字符串常量自定补’\0’结束符
最后一种数组定义较为特殊,系统会根据后续初始化变量来自动判断该数组内存大小,本程序中char a[]="hello world";等价于char a[12]="hello world";


那如果我们给数组初始化一个大于数组容量的列表会怎样,一起看结果

#include <stdio.h>
int main()
{
	char a[11]="hello world";												//第一种初始化
	//char a[11]={'h','e','l','l','o',' ','w','o','r','l','d','\0'};		//第二种初始化
	printf("%s\n",a);
	return 0;
}

在这里插入图片描述
上述程序有一个新知识点,%s输出字符串。

当我们给一个仅有11个字符的字符串赋值"hello world"时,由于字符串常量中含有12个字符(结尾自动补充’\0’字符),赋值后程序可以运行(部分编译器报错),但是当屏幕上显示字符串时,却由于最后一个休止符丢失而导致该字符串不能正常休止,在显示完前11个字符后,后续因为没有休止符而显示乱码。

当使用列表赋值时,该编译器直接报错,错误原因是因为列表元素大于字符串容量(数组容量)

单独使用一个字符同数组。例如a[4]=‘o’。
在这里插入图片描述
在这里插入图片描述
了解到这里相信大家就能理解,字符串其实就是比较特殊的数组。当然,正因为他的特殊性,字符串拥有更多的操作方法。例如针对字符串处理的标准库函数、存储库函数、独特的字符串初始化等。

二维数组与字符串数组

很多时候我们需要存储矩阵形式的数据,例如灰度图、RGB彩色图片数据、数学矩阵等,我们就可以采用二维数组。

二维数组定义与一维数组类似,可以理解成数组的数组。例如

int a[2][3];
//	未知	未知	未知
//	未知	未知	未知

int a[2][3]={{1}};
//	1	0	0
//	0	0	0

int a[2][3]={{1,2},{3}};
//	1	2	0
//	3	0	0

int a{2][3]={{1,2},{3,4,5}};
//	1	2	0
//	3	4	5


也可以对其全部初始化

int a[2][3]={{1,2,3},\
			{4,5,6}};

就定义了一个2行3列的二维数组,矩阵排列为

123
456

同样的,可以采用下标索引的方式获取数组中的元素,下标也是从0开始计数,a[0][1],表示第0行第1个元素,即2。

类比字符串,字符串数组就是char类型的二维数组。例如

char s[3][10];
//未初始化

char s[3][10]={{'a'},{'a','s','\0'}};
//初始化s为
//	"a"
//	"as"
//	""

char s[3][10]={"qwer","asdf","zxcv"};
//初始化s为
//	"qwer"
//	"asdf"
//	"zxcv"

想要单独使用其中一个字符,方法同数组类似,数组名[行][列]。例如s[2][0]=‘z’;
而是s[1]则是字符串"asdf"的首地址,涉及到地址问题统一放在后续指针与数组章节。

默认变量类型

C语言中对于程序中出现的常量都有默认类型。1、2、100、-125等整数类型默认为int型;1.2、-2.5等小数类型默认为double型;‘a’、’b‘、’H’等字符类型默认为char型;“asd”、"keng"等字符串(字符型数组)默认为char类型指针,即char*类型。

标准输入输出

#include <stdio.h>这句话的含义,就是导入了一个名称为stdio.h的文件,这个文件定义了标准(std)输入输出(io),这里的输入输出是相对于电脑来说,例如鼠标、键盘都是输入设备,而屏幕、文件、音响则是输出设备。常用的标准输入输出有scanf、printf、getc、putc、getchar、putchar、gets、puts、fgets、fputs、fget。

printf

printf()函数是标准输出函数, 一般用于向标准输出设备按规定格式输出信息。在编写程序时经常会用到此函数。printf()函数的调用格式为:

  • printf(格式化字符串,参量表);

其中格式化字符串包括三部分内容:一部分是显示字符,,这些字符将按原样输出,例如printf(“你好”);一部分是转义字符或叫做控制字符,例如’\n’是换行、‘\0’是字符串休止符、’\t’是制表符(对齐符号)等,例如printf(“\n”);另一部分是格式化规定字符,以”%”开始,后跟一个或几个规定字符,用来确定输出内容格式,同时后面需加参量表,例如printf(“%d”,a);。参量表是需要输出的一系列参数,其个数必须与格式化字符串所说明的输出参数个数一样多,各参数之间用“,”分开,且顺序需要与格式化字符所对应。
\

  • 可显示字符
ASCII值控制字符ASCII值控制字符ASCII值控制字符ASCII值控制字符
0NUT1SOH2STX3ETX
4EOT5ENQ6ACK7BEL
8BS9HT10LF11VT
12FF13CR14SO15SI
16DLE17DC118DC219DC3
20DC421NAK22SYN23TB
24CAN25EM26SUB27ESC
28FS29GS30RS31US
32(space)33!34"35#
36$37%38&39
40(41)42*43+
44,45-46.47/
480491502513
524535546557
56857958:59;
60<61=62>63?
64@65A66B67C
68D69E70F71G
72H73I74J75K
76L77M78N79O
80P81Q82R83S
84T85U86V87W
88X89Y90Z91[
92\93]94^95_
96`97a98b99c
100d101e102f103g
104h105i106j107k
108l109m110n111o
112p113q114r115s
116t117u118v119w
120x121y122z123{
124125}126~127DEL

使用例程

#include <stdio.h>
int main()
{
	printf("%c,%c,%c,%c\n",'a','A','c','F');
	return 0;
}

在这里插入图片描述

  • 转义字符
转义字符含义ASCII码
\o空字符(NULL)0
\n换行符(LF)10
\r回车符(CR)13
\t水平制表符(HT)9
\v垂直制表符(VT)11
\a响铃(BEL)7
\b退格符(BS)8
\f换页符(FF)12
\’单引号39
\"双引号34
\\反斜杠92
\?问号63
%%百分号37
\ddd任意ASCII码三位八进制数
\xhh任意ASCII码二位十六进制数

使用例程

#include <stdio.h>
int main()
{
	printf("a\0b\0c\n");		//空字符与换行符
	printf("e\rf\tg\v\n");		//回车符、水平制表符与垂直制表符
	printf("h\ai\bj\f\n");		//响铃符、退格符、换页符
	printf("\'\"\\\n");			//单引号、双引号、反斜杠
	printf("\?\101\x41\n");		//问号、八进制ascii字符'A'、十六进制ascii字符'A'
}

由于转义字符大多数是特殊的控制字符,部分无法直接在屏幕上显示出来,而是具有特殊效果,所以部分转义字符可能无法显示或显示乱码。
在这里插入图片描述

  • 格式化规定字符
符号作用
%d十进制有符号整数
%u十进制无符号整数
%f浮点数
%s字符串
%c单个字符
%p地址格式
%e指数形式的浮点数
%x,%X无符号十六进制表示整数
%0无符号八进制表示整数
%g自动选择合适格式显示

注意点:

  • (1)可以在"%" 和字母之间插进"m",m代表数字,表示输出格式占用m格。若m为正数则右对齐,若m为负数则左对齐。例如printf("%5d",a);表示输出场宽为5的十进制数,若a的数据位数不够5位则右对齐,多余5位以实际场宽为准。
  • (2)可以在"%" 和字母之间插入".n",n代表数字n,表示输出数据强制保留n个小数点。例如printf("%.2f",a);表示保留2位小数。
  • (3)可以在"%" 和字母之间插进"m.n",m和n代表数字,表示输出格式占用m格,保留n位小数。若m为正数则右对齐,若m为负数则左对齐。例如: printf("%9.2f",a);表示输出场宽为9的浮点数,其中小数位为2,整数位为6,小数点占一位,若a的数据位数不够9位则右对齐,多余9位以实际占用场宽为准。
  • (4)可以在字母前加小写字母l,表示输出的是长型数。例如: printf("%ld,a);表示输出long整数。printf("%f",a);表示输出float类型单精度浮点数(4字节)。printf("%lf",a);表示输出double类型双精度浮点数(8字节)。
  • (5)可以在"%" 和字母之间插进"0m",m代表数字,表示输出格式占用m格,a的数据位数不够m格,则空余部分用’0’字符填充。若m为正数则右对齐,若m为负数则左对齐。例如printf("%5d",a);表示输出场宽为5的十进制,若a的数据位数不够5位则右对齐,多余5位以实际场宽为准。

使用示例

#include <stdio.h>
int main()
{
	char c;
	int a=1234;
	float f=3.141592653589;
	double x=0.12345678987654321;
	c='\x41'; 				//十进制65		十六进制0x41		ascii 'A'
	printf("a=%d\n", a); 	//结果输出十进制整数a=1234
	printf("a=%6d\n", a); 	//结果输出6位十进制数a= 1234
	printf("a=%06d\n", a);	//结果输出6位十进制数a=001234
	printf("a=%2d\n", a); 	//a超过2位, 按实际值输出a=1234
	printf("x=%lf\n", x);	//输出长浮点数x=0.123457
	printf("x=%18.16lf\n", x);//输出18位其中小数点后16位的长浮点数x=0.1234567898765432
	printf("c=%c\n", c);	//输出字符c=A
	printf("c=%x\n", c);	//输出字符的ASCII码值c=41
	return 0;
}

在这里插入图片描述

scanf

scanf()函数是标准输入函数。它从标准输入设备(简谱)读取输入的信息。其调用格式为:scanf(格式化字符串, 地址表);格式化字符串包括以下三类不同的字符:

  • 格式化说明符:格式化说明符与printf()函数中的格式说明符%d%f等基本相同。例如scanf("%d",&a);
  • 空白字符:空白字符会使scanf()函数在读操作中略去输入中的一个或多个空白字符。例如scanf("%d %d",&a,&b);
  • 非空白字符: 一个非空白字符会使scanf()函数在读入时剔除掉与这个非空白字符相同的字符。例如scanf("%d,%d",&a,&b);
  • 地址表是需要读入的所有变量的地址,而不是变量本身,变量地址可以在变量名称前添加取址符’&‘取地址。这与printf()函数完全不同,要特别注意。各个变量的地址之间同’,'分开。
    例如scanf("%d,%d",&a,&b);

上例中的scanf()函数先读一个整型数, 然后把接着输入的逗号剔除掉, 最后读入另一个整型数。如果“,”这一特定字符没有找到, scanf()函数就终止。若参数之间的分隔符为空格, 则参数之间必须输入一个或多个空格。对于初学者来说,经常会将代码中%d,%d中的逗号写成英文逗号,然后键盘输入时候输入中文逗号,导致程序无法找到特定的英文逗号而出错。

注意:

  • (1)对于字符串数组或字符串指针变量, 由于数组名和指针变量名本身就是地址, 因此使用scanf()函数时, 不需要在它们前面加上”&”操作符。例如
char c;
scanf("%c",&c)		//正确
//scanf("%c",c)		//错误,对于变量输入需要取地址
char s[10];
scanf("%s",s);		//正确
//scanf("%s",&s);	//错误,数组名称本身就是地址,不需要再取地址
  • (2)可以在格式化字符串中的“%”各格式化规定符之间加入一个整数, 表示任何读操作中的最大位数,可以类比printf中的场宽限制。例如
#include "stdio.h"
int main()
{
	int a,b;
	scanf("%3d %d",&a,&b);		//变量a只接受前3位
	printf("a=%d b=%d",a,b);
	return 0;
}

输入

123456789

在这里插入图片描述

请编程:简单加法计算器
参考例程

#include "stdio.h"
int main()
{
	double a,b;
	printf("请输入加法算式,例如 \"1+2=\"\n");
	scanf("%lf+%lf=",&a,&b);
	printf("%lf+%lf=%lf\n",a,b,a+b);
	return 0;
}

输入

3.4+2.5=

在这里插入图片描述
注意输入两个数之间的分隔符必须与scanf中的格式保持一直,本程序中采用’+'分隔。

putchar()与getchar()

putchar()使用格式为

char c='A';
putchar(c);

他的作用就是向标准输出设备输出一个字符,以回车键结束,其等价于printf("%c",c);

getchar()则是与putchar()相对应,putchar()为输出一个字符,getchar()为获得一个字符。getchar

char c='A';
getchar(c);

使用例程

#include <stdio.h>
int main()
{
	char c='A';		//定义char类型变量c初始化为字符A
	
	putchar(c);		//屏幕显示(输出)字符
	putchar('\n');	//输出换行
	
	c=getchar();	//从键盘输入一个字符保存到c
	putchar(c);		//输出字符
	putchar('\n');	//输出换行
	
	return 0;
}

键盘输入

B

在这里插入图片描述
运行结果可以看到,屏幕显示内容为 ‘A’、换行、‘B’、换行。

puts()与gets()

puts(s)与gets(s)可以类比标准输入输出:

char s[10]="hello";
puts(s);				//等价于printf("%s\n",s);
gets(s);				//等价于scanf("%s",s);	//字符数组不用加&

注意`:

  • puts(s);输出完字符串后会自动换行。
  • gets(s);从缓冲区读取字符串直到遇到回车停止。但是scanf("%s",s);是从缓冲区读取字符串直到遇到回车或空格停止。

getch()

getch()与getchar()用法一样。区别在于getchar()读取字符时候,我们输入字符时,屏幕上会后相应的显示;而getch()读取字符则没有回显。当我们不需要屏幕回显时,比如贪吃蛇用awsd控制上下左右,每当我们按下一个键屏幕就显示相应字符显然不是我们想要的效果,这种时候就可以使用不带回显的输入。

getche()

同getchar(),只是该函数不论按下什么按键都直接获取相应字符,不以回车键结束。

C语言基本概念

从编译到运行

C语言程序从我们敲代码到运行有四个步骤:

  • 1、预处理(Preprocessing)
  • 2、编译(Compilation)
  • 3、汇编(Assemble)
  • 4、链接(Linking)
预处理

用于将所有的#include头文件以及宏定义替换成其真正的内容,预处理之后得到的仍然是文本文件,但文件体积会大很多。

编译

这里的编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程。

汇编

汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式。

链接

链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)。

变量的本质

  • 1、c中所有类型的数据存储在内存中,都是按照二进制格式存储的。所以内存中只知道有0和1,不知道是int的、还是float的还是其他类型。
  • 2、int、char、short等属于整形,他们的存储方式(数转换成二进制往内存中放的方式)是相同的,只是内存格子大小不同(所以这几种整形就彼此叫二进制兼容格式);而float和double的存储方式彼此不同,和整形更不同。
  • 3、C语言中的数据类型的本质,就是决定了这个数在内存中怎么存储的问题,也就是决定了这个数如何转成二进制的问题。一定要记住的一点是内存只是存储1010的序列,而不管这些1010怎么解析。所以要求我们平时数据类型存取一致。

作用域与生命周期

作用域(scope):限定一个变量的可用范围,以{}为界限,变量必须定义在作用域开始。

生命周期:变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段。

局部变量

定义在{}内部的变量称局部变量。局部变量的生命周期:进入作用域生命周期的开始,出作用域生命周期结束。例如以下程序,a的作用域在整个main内有效。

#include <stdio.h>
int main()
{
	int a=0;			//局部变量a,作用域为main函数
	printf("a=%d\n",a);
	return 0;
}

在这里插入图片描述

而下例程中,a的作用域仅仅在{}内有效。

#include <stdio.h>
int main()
{
	{
		int a=0;			//局部变量a,作用域为{}内
	}
	printf("a=%d\n",a);		//由于此时a不在作用域内,a的生命周期已经结束
							//所以报错表示a不存在。
	return 0;
}

在这里插入图片描述

全局变量

定义在所有函数之外称为全局变量。全局变量的生命周期:整个程序的生命周期。例如

#include <stdio.h>
int a=1;			//全局变量a=1
int b;				//全局变量b=0
int main()
{
	printf("a=%d,b=%d\n",a,b);
}

在这里插入图片描述
全局变量与局域变量区别:

  • 1、全局变量如果不对其初始化,则系统会自动执行默认初始化。int、short等整型变量默认0,char默认为0,指针默认为NULL。局部变量如果不对其初始化,那么它的值将处于未定义状态,或者默认初始化(编译器不同则不一样)。
#include <stdio.h>
int a;			//全局变量a=0
int main()
{
	int b;		//局部变量b=未定义
	printf("a=%d,b=%d\n",a,b);
}

在这里插入图片描述

  • 2、全局变量自从定义之后,其有效作用域为整个程序,并且其生命周期为整个程序运行期间。而局部变量定义之后,其作用域只在某一区域起作用,并且在程序运行出该作用域后销毁。
  • 3、全局变量和局部变量名称可以相同,但实际上同名变量仅仅是名称相同的两个不同变量,他们有不同存储空间。当一个作用域中出现同名变量时,子作用域中的局部变量会屏蔽掉外层作用域中定义的变量。例如
#include <stdio.h>
int a;			//全局变量a=0

void fun()
{
	printf("非主函数作用域a=%d\n",a);
}

int main()
{
	int a=1;	//局部变量a=1;
	{
		int a=2;//子作用域a=2;
		printf("子作用域a=%d\n",a);
	}
	printf("主函数内a=%d\n",a);
	fun();
	return 0;
}

在这里插入图片描述
这里出现一个新的概念:自定义函数fun。函数具体内容后续章节介绍,在本程序中作用是输出全局变量a=0。

修饰符

修饰符指的是我们定义变量或函数时,在定义的语句添加的用于修饰变量或函数的关键字。常用修饰符有const、static、register、volatile、extern、auto。

const

表示该数据为常量。const修饰的是在它前面的类型,如果它前面没有类型,那它修饰的是紧跟着它的那个类型。使用const定义变量时,由于变量被const限制成不可改变的常量,所以该数据必须初始化,并且定义之后,任何试图修改该数据的操作都是非法的。例如

const int a=0;			//修饰int,a是一个不可改变的数,值为0;
int const b=1;			//修饰int,b时应该不可改变的数,值为1;
const int c=1,d=2;		//修饰int,c和d都是常量
const int f;			//错误,常量必须初始化
a=10;					//错误,试图修改常量
b=1;					//错误,试图修改常量

有点编译器会自动对未初始化变量默认初始化,可能const int f;这种未定义的常量也可以通过编译,但是并没有什么意义,因为常量后期不能修改,而该常量目前的值是毫无意义的,那我们用它干什么呢。

并且通常对于不可改变的常量,我们用大写字母去命名数据,并不是说不可以使用其它名称,但如果拥有一个好的命名习惯,我们就可以通过变量名称看出他的类型及作用。
例程

#include <stdio.h>
int main()
{
	const int W_SCREEN=128;			//屏幕宽度为128像素
	const int H_SCREEN=64;			//屏幕高度为64像素
	printf("屏幕宽度为%d\n",W_SCREEN);
	printf("屏幕高度为%d\n",H_SCREEN);
	printf("总像素点数%d\n",W_SCREEN*H_SCREEN);
	return 0;
}

在这里插入图片描述
对于指针而言,const略有不同,区别在于顶层const与底层const

  • 顶层const——修饰指针变量本身,指指针变量本身不可修改
    例如:int *const p1=&a;const与变量名称p1紧贴,修饰变量本身,这里表示p1指针从定义之后就不能修改了,从此p1就只能指向a。但是可以通过p1修改a的值,例如*p=5;
  • 底层const——修饰目标地址,指目标地址的内容不可修改
    例如:int const *p1; //写法1 const int *p2; //写法2,这两种写法完全等价,其效果是无法通过该指针*p1去改变目标地址的内容(即a的内容)。但是可以修改p1的地址,例如p1=&b;
  • 双层const——无法修改指针变量本身,且无法通过指针修改目标地址内容。
    例如int const *const p1; //写法1 const int *const p2; //写法2
  • 之前定义所有非指针变量都相当于顶层const。

const变量与指针的赋值问题——权限只能缩小,不能放大
假如有

int i=5;
const int ci=10;
  • 普通指针赋值问题
int *p1=&i;				//正确,权限相同,可以修改i的值
int *p2=&ci;			//错误,权限扩大,通过p2修改ci的值,但是ci本身不可修改
p1=&i;					//正确,可以修改指针变量本身,并且权限相同
p2=&ci;					//错误,可以修改指针变量本身,但是权限扩大
*p1=20;					//正确,可以通过普通指针变量修改目标地址的内容
*p2=20;					//正确,可以通过普通指针变量修改目标地址的内容
  • 顶层const赋值问题:
int *const pc1=&i;		//正确,权限相同,不去修改i的值
int *const pc2=&ci;		//正确,权限相同,不能通过pc2修改ci的值,并且ci本身也不可修改
pc1=&i;					//正确,顶层const可以修改指针变量本身,并且权限缩小
pc2=&ci;				//正确,顶层const可以修改指针变量本身,并且权限相同
*pc1=20;				//错误,顶层const不可以通过指针修改目标地址的内容
*pc2=20;				//错误,顶层const不可以通过指针修改目标地址的内容
  • 顶层const赋值问题
const int cp1=&i;		//正确,权限相同,都可以修改i的值,但是cp1指针本身不可以修改
const int cp2=&ci;		//错误,权限扩大,ci本身不可修改,但是cp2指针可以通过地址修改ci内容,所有扩大权限的操作都是非法的
cp1=&i;					//错误,顶层const不能修改指针变量本身
cp2=&ci;				//错误,顶层const不能修改指针变量本身
*pc1=20;				//正确,顶层const可以修改目标地址的内容
*pc2=20;				//正确,顶层const可以修改目标地址的内容 
  • 双重const赋值问题
const int *const cpc1=&i;//正确,权限缩小,并且修改i的值
const int *const cpc2=&ci;//正确,权限相同,不能通过指针修改ci值,并且ci本身不可修改
cpc1=&i;				//错误,指针变量本身不可修改
cpc2=&i;				//错误,指针变量本身不可修改
*cpc1=20;				//错误,不能通过指针变量修改目标地址的内容
*cpc2=20;				//错误,不能通过指针变量修改目标地址的内容

static

static修饰符有3点作用:

  • 1、修饰函数:被修饰的函数只在本文件中调用。
    关于函数概念与文件概念后续介绍。
  • 2、修饰全局变量:被修饰的变量只在本文件中有效。
    文件概念后续介绍。
  • 3、修饰局部变量:保持局部变量内容的持久。存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和 static 变量,只不过和全局变量比起来,static 可以控制变量的可见范围。例如:
#include <stdio.h>

//简单测试函数,目前看不懂没有关系,只需了解这个函数与主函数一样可以运行即可
void fun()
{
	int a=0;
	a=a+1;
	printf("a=%d\n",a);
}
int main()
{
	fun();		//第一次运行
	fun();		//第二次运行
	fun();		//第三次运行
}

未使用静态变量时候,每次执行fun()都会重新创建一个变量a=0;然后a=a+1;然后输出a=1;离开fun()时候a会被销毁。
在这里插入图片描述
而使用static修饰符后。

#include <stdio.h>

//简单测试函数,目前看不懂没有关系,只需了解这个函数与主函数一样可以运行即可
void fun()
{
	static int a=0;
	a=a+1;
	printf("a=%d\n",a);
}
int main()
{
	fun();		//第一次运行
	fun();		//第二次运行
	fun();		//第三次运行
}

添加static修饰符后,当初次执行fun()会创建一个变量a=0;并执行a=a+1;然后输出a=1;离开fun()后变量a不会被销毁,并且下次执行fun()时不会重新创建变量a,而是继续使用之前的变量a;直接执行a=a+1;然后输出a=2;变量a一直持续到程序结束才会被销毁。
在这里插入图片描述

register

register表示寄存器变量,极少使用。简单讲解一下,可以略过不看。

  • 电脑运行时,数据一般存储在存储区。存储区数据量极大,但是效率略低。当我们计算c=a+b时,cpu处理器先将a读到寄存器A中(register),再将读取b与寄存器相加,结果保存到之前的寄存器A中,然后再将a+b的结果放入存储器c中。这样虽然速度不慢,但是如果一开始a和b保存在寄存器中,就会更快。C语言中register修饰符就是将变量存储空间放在寄存器中,但是需要注意的是,cpu中寄存器十分稀有,所以除非是使用频率非常高的数据才会放在寄存器中。

volatile

volatile表示易变变量,较少使用。简单讲解一下,可以略过不看。
register介绍中描述了数据需要从存储器读取到寄存器。我们的电脑十分智能,当我们第一次使用a变量时,cpu会将它读取到寄存器中,假设是寄存器A。当我们第二次使用变量a是,cpu判断之前程序并没有对a的操作,所以cpu认为寄存器A中的值就是a,于是不会再次从存储器中读取a,而是直接使用寄存器中的A。但是电脑中可能有其他程序篡改了存储器中的a,如果我们想要cpu每次使用变量a时都重新从存储器中读取,那就可以使用volatile修饰变量。

extern

全局变量声明符号。在分文件编译中有详细介绍,通过该修饰符可以在不同文件中使用同一变量。

auto

1.c语言中,关键字auto用于声明一个变量为自动变量
  自动变量也称局部变量。将不在任何类、结构、枚举、联合和函数中定义的变量视为全局变量,而在函数中定义的变量视为局部变量。所有局部变量默认都是auto,一般省略不写。
auto声明全局变量时,编译出错:

#include <stdio.h>
auto int i;
int main(void)
{
}

在这里插入图片描述

auto声明局部变量时,编译正常:

int main(void)
{
	auto int i = 1;
	return 0;
}

2.c语言中,只使用auto修饰变量,变量的类型默认为整型。

int main(void)
{
	double a = 1.2, b = 2.7;
	auto c = a + b;//等价于int c=a+b;先计算a+b值为3.9
	return 0;		//然后将3.9赋值给int c,类型介绍中说过,c会向下取整=3
}

类型转换

数据类型转换就是将数据(变量、数值、表达式的结果等)从一种类型转换为另一种类型。

C语言是强类型语言,如果一个运算符两边的运算数据类型不同,先要将其转换为相同的类型,强制类型转换可以消除程序中的警告,即确保写代码的程序员自己清楚类型转换,允许丢失一定精度做类型匹配。

强制类型转换

强制类型转换是显示的、按照我们意愿把变量从一种类型转换为另一种数据类型,通常用于 。表达式为:(新变量类型)原变量名称 例如:

#include <stdio.h>
int main(void)
{
	double a=9.2,b=9.2;	
	printf("a=%lf,b=%d\n",a,(int)b);	//a是double型,用%lf输出
										//b是double型,用%lf输出
	return 0;							//(int)b是int类型,用%d输出
}

在这里插入图片描述
再看一例:

#include <stdio.h>
int main(void)
{
	int a=5,b=2;		//int类型无法保存小数
	double c,d;			//双精度浮点型变量
	c=a/b;
	d=(double)a/b;
	printf("5/2结果为%lf\n",c);
	printf("5/2结果为%lf\n",d);
	return 0;
}

本例中,c=a/b;由于a和b都是int类型,a/b结果也是int类型,即2,然后c=2;
d=(double)a/b;由于(double)a是double类型,b是int类型,两个不同类型数进行计算,系统会先将b也转为double,再进行运算,所以最后结果为2.5;
在这里插入图片描述
较为特殊的是,我们可以将空指针(void*)转为其他任意指针类型,例如:

void *pv=NULL;
int *pi=(int*)pv;
char *pc=(char*)pc;

另外有一种特殊且危险的使用方法是将底层const指针强制修改为可变指针并赋值给另一个指针。例如:

int a=5;			//变量a
int const *p1=&a;	//底层const指针,无法通过p1修改a的值
int *p2=(int*)p1	//强制将int const*类型转为int *类型,可以通过p2修改a的值

还有一种更为危险且特殊的用法:我们可以通过强制类型转换将一个int数转为指针类型,但是极有可能导致错误,所以不推荐使用。例如:

int a=0xffffffff;
int *p=(int*)a;

隐式类型转换

隐式类型转换又称自动类型转换,这种操作不需要程序员处理,系统会自动将运算符两边不同的数据类型进行匹配。
\

  • 1、 将一种类型的数据赋值给另外一种类型的变量时就会发生自动类型转换,例如:float f = 320;这里320 是 int 类型的数据,需要先转换为 float 类型才能赋值给变量 f。再如:int n = f; 这里f 是 float 类型的数据,需要先转换为 int 类型才能赋值给变量 n。

    在赋值运算中,赋值号两边的数据类型不同时,需要把右边表达式的类型转换为左边变量的类型,这可能会导致数据失真,或者精度降低,例如int a=3.4; 这里的3.4会从double类型自动转换为int类型,丢失小数。所以说,自动类型转换并不一定是安全的。对于不安全的类型转换,编译器一般会给出警告。

  • 2、在不同类型的混合运算中,编译器也会自动地转换数据类型,将参与运算的所有数据先转换为同一种类型,然后再进行计算。转换的规则如下:
    (1)转换按数据长度增加的方向进行,以保证数值不失真,或者精度不降低。例如,int 和 long 参与运算时,先把 int 类型的数据转成 long 类型后再进行运算。

    (2)所有的浮点运算都是以双精度进行的,即使运算中只有 float 类型,也要先转换为 double 类型,才能进行运算。

    (3)char 和 short 参与运算时,必须先转换成 int 类型。

通过下面例子更生动的表述一下:

#include<stdio.h>
int main(){
    float PI = 3.14159;			//float类型变量
    int s1, r = 1;				//int类型变量
    double s2;					//double类型变量
    s1 = r * r * PI;			//int*int*float先计算int*int结果为int
								//之后计算int*float,先将int转为float,再计算float*float,结果为float
								//最后int=float,先将float转为int再赋值
	
    s2 = r * r * PI;			//int*int*float计算过程如上,结果为float。
								//double=float,先将float转为double再赋值
	
    printf("s1=%d, s2=%f\n", s1, s2);
    return 0;
}


上例中s1 = r * r * PI; int=intintfloat计算步骤:

  • 先计算后赋值
  • 计算int×int×float时,先计算int×int结果为int;之后先将int和float转为double,再计算double*double,得到结果为double。
  • 最后int=double,先将double转为int再赋值。

上例中s2 = r * r * PI; double=intintfloat计算步骤:

  • 先计算后赋值
  • 计算int×int×float过程同上,结果为double。
  • 最后double=double,直接赋值。

在计算表达式r×r×PI时,r 和 PI 都被转换成 double 类型,表达式的结果也是 double 类型。但由于 s1 为整型,所以赋值运算的结果仍为整型,舍去了小数部分,导致数据失真。

强制类型转换VS隐式类型转换

无论是隐式类型转换还是强制类型转换,都只是为了本次运算而进行的临时性转换,转换的结果也会保存到临时的内存空间,不会改变数据本来的类型或者值。

在C语言中,有些类型既可以隐式转换,也可以强制转换,例如 int 到 double,float 到 int 等;而有些类型只能强制转换,不能隐式转换,例如以后将要学到的 void * 到 int *,int 到 char * 等。

可以隐式转换的类型一定能够强制转换,但是,需要强制转换的类型不一定能够隐式转换。现在我们学到的数据类型,既可以隐式转换,又可以强制转换,以后我们还会学到一些只能强制转换而不能隐式转换的类型。

可以隐式进行的类型转换一般风险较低,不会对程序带来严重的后果,例如,int 到 double 没有什么缺点,float 到 int 顶多是数值精度丢失。只能强制进行的类型转换一般风险较高,或者行为匪夷所思,例如,char * 到 int * 就是很奇怪的一种转换,这会导致取得的值也很奇怪,再如,int 到 char * 就是风险极高的一种转换,一般会导致程序崩溃。

使用强制类型转换时,程序员自己要意识到潜在的风险。

第三部分

本章节为C语言核心部分,可以说掌握本章节对C语言学习至关重要。从本节开始,程序会渐渐变得复杂,这时在线编译器已经不能满足我们的需求了,所以我们后续程序都将使用Visual studio编译运行,Visual studio版本不同区别不大。

Visual Studio 2019软件介绍

下载地址

随着我们学习的深入,在线编译器已经无法满足需求了,我们需要自己再电脑上配置一个C语言程序开发环境。在C语言编译环境中曾给过大家VS、VC、在线编译器的下载地址,这里在展示一下。

vc++6.0,这个软件网络上有很多资源,这里给大家提供一个。
链接:https://pan.baidu.com/s/1PApsWKEDMzvqM9NmSuw6hA
提取码:zukk

或者也可以用菜鸟工具中的在线编译器
https://c.runoob.com/compile/11/

VS下载地址官网
https://visualstudio.

配置与应用安装

第一步: **
首先先找自己电脑上的浏览器,在搜索框中输入“Visual studio”,找到应用程序官方下载地址。
在这里插入图片描述
第二步:
根据自己的电脑选择版本,我自己电脑是Windows 所以以下内容将是Windows的安装教程。
在这里插入图片描述
在这里插入图片描述

第三步:
安装与配置
在这里插入图片描述
在这里插入图片描述
勾选C++桌面开发。选择合适的安装位置。
在这里插入图片描述

第四步:
确认安装,并等安装进度条走完。

此时桌面应该有一个VS图标,打开即可使用。
在这里插入图片描述
在这里插入图片描述

一个完整工程

1、创建工程项目:
在这里插入图片描述
在这里插入图片描述
输入该工程项目名称,支持中文;选择安装地址,创建工程。
在这里插入图片描述在这里插入图片描述

2、添加工程文件
点击源文件-添加-新建项或按快捷键shift+ctrl+N
在这里插入图片描述
选择C++文件,输入文件任意文件名称(可以是中文、不能过长),文件后缀为 “.c” 表示c语言程序,如果为 “.cpp” 表示c++语言程序。点击添加
在这里插入图片描述
在这里插入图片描述
此时工程目录中也已经有程序文件main.c了
在这里插入图片描述
3、编写
其中快捷键有

  • 全选——ctrl+A
  • 剪切——ctrl+X
  • 复制——ctrl+C
  • 粘贴——ctrl+V
  • 撤销——ctrl+Z
  • 取消撤销——ctrl+Y
#include <stdio.h>
int main()
{
	int a = 5;
	printf("原a=%d\n", a);
	printf("请输入新a的值:");
	
	scanf("%d", &a);
	printf("新a的值为%d\n", a);
	
	return 0;
}

在这里插入图片描述

4、编译、链接与运行
开发环境可以直接集成编译、链接、运行。点击调试-开始运行(不调试)或按ctrl+F5
在这里插入图片描述
注意下面提示错误。
在这里插入图片描述
这句话翻译过来的大概意思是该函数在新标准里面已经不再使用。我们不用管它,有两种方法解决

  • 文件开头添加#pragma warning(disable:4996),4996警告全是因为函数版本低,可以直接使用这句话屏蔽。
  • 也可以将scanf写成scanf_s,两者几乎没有区别,使用方法相同,但是scanf_s是新c语言标准,使用更加安全,降低了内存泄漏的风险。

添加忽略警告之后,重新编译,程序成功运行。
在这里插入图片描述
在我们编写的程序中输入10后效果图
在这里插入图片描述
运行成功,并且程序与预期相同。

调试介绍

当程序语句过多,且程序出现非语法错误时候,我们会采用调试功能。
调试指的就是编译全部代码,但是并不运行全部代码,而是按照我们的意愿执行到指定行,常用调试方式有:

  • 断点调试
    断点调试指的是程序运行,直到下一个断点语句出现时,程序暂停运行,这时候我们可以将光标移动到变量上即可看到变量的值,并可以据此判断程序有没有问题。

    点击红框所指的区域,可以设置或取消断点
    在这里插入图片描述
    点击调试-切换断点可以设置或取消断点,同时快捷键F9也是同样的作用
    在这里插入图片描述
    当我们设置好断点时,可以点击调试-开始调试或按快捷键F5进行调试,调试时,程序会自动运行到第一个断点处,按F5可以继续运行直到下一个断点或暂停运行。
    在这里插入图片描述
    提示框1是程序即将运行的语句,提示框2是终止或重启调试。

  • 单步调试
    当程序需要一句话一句话的单步执行时,可以采用单步调试,常用于循环错误检测或错误内容锁定。

    点击调试-逐过程或按快捷键F10即可单步调试。
    在这里插入图片描述
    此时不论是否有断点,程序都将暂停在main函数开头。每按一次F10或点击图标或点击调试-逐过程都将执行一句语句。
    在这里插入图片描述

当我们运行到自定义函数时,可以使用F11或点击快捷菜单中图标,该快捷键作用是进入函数内部。
在这里插入图片描述
当我们跳转到自定义函数内部是,可以使用shift+F11或点击快捷菜单栏中图标,快速运行完该自定义函数并跳出该自定义函数。
在这里插入图片描述

单步调试和断点调试可以配合使用,能够极大提升开发者开发效率。

运算符介绍

运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C 语言内置了丰富的运算符,其种类有:算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、其他运算符。

本章将逐一介绍算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符和其他运算符。
\

算术运算符

下表显示了 C 语言支持的所有算术运算符。假设int变量 A 的值为 10,变量 B 的值为 20,则:

名称运算符描述示例
+把两个操作数相加A + B 将得到 30
-从第一个操作数中减去第二个操作数A - B 将得到 -10
*把两个操作数相乘A * B 将得到 200
/分子除以分母B / A 将得到 2
取模%取模运算符,整除后的余数B % A 将得到 0
自加++自增运算符,整数值增加 1A++ 将得到 11
自减自减运算符,整数值减少 1A-- 将得到 9

请看下面的实例,了解 C 语言中所有可用的算术运算符:

#include <stdio.h>
int main()
{
	int a = 5;
	int b = 2;
	int c ;

	c = a + b;
	printf("a+b的值是 %d\n", c );
	c = a - b;
	printf("a-b的值是 %d\n", c );
	c = a * b;
	printf("a*b的值是 %d\n", c );
	c = a / b;
	printf("a/b的值是 %d\n", c );
	c = a % b;
	printf("a%b的值是 %d\n", c );
	c = a++;  // 赋值后再加 1 ,c 为 21,a 为 22
	printf("a++的值是 %d\n", c );
	c = a--;  // 赋值后再减 1 ,c 为 22 ,a 为 21
	printf("a--的值是 %d\n", c );
	
	return 0;
}

运行完成后结果为:

a+b的值是 7
a-b的值是 3
a*b的值是 10
a/b的值是 2
ab的值是 1
a++的值是 5
a--的值是 6

值得注意的是,++与–运算有左结合和右结合两种类型。区别如下:

#include <stdio.h>
int main()
{
	int a=10;
	int b=10;
	
	printf("a++运算时值为:%d\n",a++);
	printf("a++运算后值为:%d\n\n",a);
	
	printf("++b运算时值为:%d\n",++b);
	printf("++b运算后值为:%d\n",b);
	
	return 0;
}

运行结果为:

a++运算时值为:10
a++运算后值为:11

++b运算时值为:11
++b运算后值为:11

左结合a++和右结合++a运算后结果一样,唯一区别时在a++和++a所处的语句中,表达式的值a++是原来的值,而++a是加1后的值。a–与–a同理。

其原理也很简单。假如a=10:

  • a++运算时,先计算a++的值11并保存到另一个地方,此时a仍然是10,等运行完成a++所在的语句后再用11覆盖a的内存。
  • 而++a是直接计算值11,然后覆盖a的内存,再计算后续语句。
  • 这样看来++a的占用空间更少,运算更快。

下面我们看一个有意思的案例:

#include <stdio.h>
int main()
{
	int a, b, c, d;

	a = 5;
	b = a++ * a++ * a++;
	printf("a=%d,b=%d\n", a, b);

	a = 5;
	c = ++a * ++a * ++a;
	printf("a=%d,c=%d\n", a, c);

	a = 5;
	d = ++a * a++ * ++a;
	printf("a=%d,d=%d\n", a, d);

	return 0;
}

运行结果为:

a=8,b=125
a=8,c=512
a=8,d=343

对于第一个结果,a++*a++*a++时,所有更新的a的数据都在后台保存,但是本条语句中,a的值一直为5。

对于第二个结果,++a*++a*++a时,所有数据都在本条语句生效,即先统计完成所有++a后,a的值为8,然后再8*8*8=512。

对于第三个结果,++a*a++*++a时,两条语句即时生效,a=7,执行完成7*7*7后,程序会再处理a++的值,a的值为8,最后结果7*7*7=343。

关系运算符

下表显示了 C 语言支持的所有关系运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:

名称运算符描述实例
双等于==检查两个操作数的值是否相等,如果相等则条件为真。(A == B) 为假。
不相等!=检查两个操作数的值是否相等,如果不相等则条件为真。(A != B) 为真。
大于>检查左操作数的值是否大于右操作数的值,如果是则条件为真。(A > B) 为假。
小于<检查左操作数的值是否小于右操作数的值,如果是则条件为真。(A < B) 为真。
大于等于>=检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。(A >= B) 为假。
小于等于<=检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。(A <= B) 为真。

请看下面的实例,了解 C 语言中所有可用的关系运算符,其中输出0表示假、1表示真:

#include <stdio.h>
int main()
{
	int a=5,b=2;
	printf("a==b的判断结果为%d\n",a==b);
	printf("a!=b的判断结果为%d\n",a!=b);
	printf("a>b的判断结果为%d\n",a>b);
	printf("a<b的判断结果为%d\n",a<b);
	printf("a>=b的判断结果为%d\n",a>=b);
	printf("a<=b的判断结果为%d\n",a<=b);
	return 0;
}

运行结果为:

a==b的判断结果为0
a!=b的判断结果为1
a>b的判断结果为1
a<b的判断结果为0
a>=b的判断结果为1
a<=b的判断结果为0

逻辑运算符

下表显示了 C 语言支持的所有关系逻辑运算符。假设变量 A 的值为 1,变量 B 的值为 0,则:

名称运算符描述实例
逻辑与&&称为逻辑与运算符。如果两个操作数都非零,则条件为真。(A && B) 为假。
逻辑或▏▏称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。(A ▏▏ B) 为真。
逻辑非!称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。!(A && B) 为真。

请看下面的实例,了解 C 语言中所有可用的逻辑运算符,其中输出0表示假,输出1表示真:

#include <stdio.h>
int main()
{
	int a=5,b=2;
	printf("a&&b的判断结果为%d\n",a&&b);
	printf("a||b的判断结果为%d\n",a||b);
	printf("!(a&&b)的判断结果为%d\n",!(a&&b));
	return 0;
}

运行结果为:

a&&b的判断结果为1
a||b的判断结果为1
!(a&&b)的判断结果为0

并且带有逻辑运算符的语句中,程序并不一定被执行,请看下面例子:

#include <stdio.h>
int main()
{
	int a = 0;
	int b = 0;
	printf("a++&&b++结果为%d\n", a++ && b++);
	printf("此时a为%d,b为%d\n", a, b);
	return 0;
}

运行结果为:

a++&&b++结果为0
此时a为1,b为0

可能有同学发现,程序只执行了a++,并没有执行b++。这是因为系统会先判断&&运算符左边是否有效(是否为非0数),如果左侧有效,系统才会再去判断右边是否有效。在本程序中,系统先判断左侧a++的值在本条语句中为0,所以就不用执行并判断后续语句,直接就可以认定(a++&&b++)结果为假。

同理,对于||运算符而言,如果 (左表达式)||(右表达式) 语句中左表达式值为1,则程序不会执行右表达式,而是直接认定整个或逻辑运算结果为真。例如:

#include <stdio.h>
int main()
{
	int a = 1;
	int b = 1;
	printf("a++||b++结果为%d\n", a++ || b++);
	printf("此时a为%d,b为%d\n", a, b);
	return 0;
}

运行结果为:

a++||b++结果为1
此时a为2,b为1

逻辑运算符与其他运算另一个不同点是,逻辑运算符(&&、||)左右两侧算两条语句。什么意思呢,就比如说++运算符,若a=5;则a++在本条语句中,a的值仍为5,但是在a++&&a++中,a在运算符左边时,其值为5,在运算符右边时,a的值为6,运行完本条语句后,a的值为7。例如:

#include <stdio.h>
int main()
{
	int a = 1;
	printf("a--||a的值为%d\n", a-- && a);
	printf("之后a的值为%d\n", a);
	return 0;
}

若是其他运算符,则由于a–&&a是同一条语句,语句中两个a的值都是1,最终1&&1=1。
但是不然,在逻辑运算中,a–先被执行完成,在左侧时,a–语句值为1,并且判断完成左侧后,a的值就被修改为0了,再到右侧时,a的值已经为0,所以最终a–&&a结果为0。

运行结果为:

a--||a的值为0
之后a的值为0

位运算符

下表显示了 C 语言支持的所有关系运算符。为运算符直接将两个二进制数对应位相计算,假设变量 A 的值为 0B0101(5),变量 B 的值为 0B0011(3),则:

名称运算符描述实例
按为与&对应位上的两个数都为1,则结果的对应为为1,否则为0。( A & B)的结果为 0B0001
按位或对应位上的两个数都为0,则结果的对应位为0,否则为1。( A ▏ B)的结果为0B0111
按位异或^对应位上的两个数不相同,则结果为1,否则为0.(A ^ B)的结果为0B0110
按位取反~对原操作数所有位取反(~ A)的结果为0B1010)
左移<<对原操作数左移x位,x为<<右边的数,右边进0(A << 1)的结果为0B1010
右移>>对原操作数右移x为,x为>>右边的数,左边进0(A >> 1)的结果为0B0010

假设如果 A = 60,且 B = 13,现在以二进制格式表示,它们如下所示:
A = 0011 1100
B = 0000 1101

相关位运算计算:

  • 按位与A&B =
     0B 0011 1100
    & 0B 0000 1101
    = 0B 0000 1100

  • 按位或A|B =
     0B 0011 1100
    | 0B 0000 1101
    = 0B 0011 1101

  • 按位异或A^B =
     0B 0011 1100
    ^ 0B 0000 1101
    = 0B 0011 0001

  • 按位取反~A =
    ~0B 0011 1100
    =0B 1100 0011

  • 左移A<<1 =
     0B 0011 1100
    《1
    = 0B 0111 1000

  • 右移A>>1 =
     0B 0011 1100
    》1
    = 0B 0001 1110

请看下面的实例,了解 C 语言中所有可用的位运算符:

#include <stdio.h>
int main()
{
 
   unsigned int a = 60;    /* 60 = 0011 1100 */  
   unsigned int b = 13;    /* 13 = 0000 1101 */
   int c = 0;           
 
   c = a & b;       /* 12 = 0000 1100 */ 
   printf("a&b 的值是 %d\n", c );
 
   c = a | b;       /* 61 = 0011 1101 */
   printf("a|b 的值是 %d\n", c );
 
   c = a ^ b;       /* 49 = 0011 0001 */
   printf("a^b 的值是 %d\n", c );
 
   c = ~a;          /*-61 = 1100 0011 */
   printf("~a  的值是 %d\n", c );
 
   c = a << 1;     /* 120 = 0000 0111 1000 */
   printf("a<<1 的值是 %d\n", c );
 
   c = a >> 1;     /* 30 = 0001 1110 */
   printf("a>>1 的值是 %d\n", c );
   
   return 0;
}

运行结果为:

a&b 的值是 12
a|b 的值是 61
a^b 的值是 49
~a  的值是 -61
a<<1 的值是 120
a>>1 的值是 30

赋值运算符

下表列出了 C 语言支持的赋值运算符:

名称运算符描述实例
等于=简单的赋值运算符,把右边操作数的值赋给左边操作数(左边为可修改的左值)C = A + B 将把 A + B 的值赋给 C
加等于+=加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数C += A 相当于 C = C + A
减等于-=减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数C -= A 相当于 C = C - A
乘等于*=乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数C *= A 相当于 C = C * A
除等于/=除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数C /= A 相当于 C = C / A
取模等于%=求模且赋值运算符,求两个操作数的模赋值给左边操作数C %= A 相当于 C = C % A
左移等于<<=左移且赋值运算符C <<= 2 等同于 C = C << 2
右移等于>>=右移且赋值运算符C >>= 2 等同于 C = C >> 2
按位与等于&=按位与且赋值运算符C &= 2 等同于 C = C & 2
按位异或等于^=按位异或且赋值运算符C ^= 2 等同于 C = C ^ 2
按位或等于▏=按位或且赋值运算符C ▏= 2 等同于 C = C ▏ 2

请看下面的实例,了解 C 语言中所有可用的赋值运算符:

#include <stdio.h>
int main()
{
   int a = 21;
   int c ;
   c =  a;
   printf("=  运算符实例,c 的值 = %d\n", c );
   
   c +=  a;
   printf("+= 运算符实例,c 的值 = %d\n", c );
   
   c -=  a;
   printf("-= 运算符实例,c 的值 = %d\n", c );
   
   c *=  a;
   printf("*= 运算符实例,c 的值 = %d\n", c );
   
   c /=  a;
   printf("/= 运算符实例,c 的值 = %d\n", c );
   
   c  = 200;
   c %=  a;
   printf("%%= 运算符实例,c 的值 = %d\n", c );
   
   c <<=  2;
   printf("<<= 运算符实例,c 的值 = %d\n", c );
   
   c >>=  2;
   printf(">>= 运算符实例,c 的值 = %d\n", c );
   
   c &=  2;
   printf("&= 运算符实例,c 的值 = %d\n", c );
   
   c ^=  2;
   printf("^= 运算符实例,c 的值 = %d\n", c );
   
   c |=  2;
   printf("|= 运算符实例,c 的值 = %d\n", c );
	
	return 0;
}

运行结果为:

=  运算符实例,c 的值 = 21
+= 运算符实例,c 的值 = 42
-= 运算符实例,c 的值 = 21
*= 运算符实例,c 的值 = 441
/= 运算符实例,c 的值 = 21
%= 运算符实例,c 的值 = 11
<<= 运算符实例,c 的值 = 44
>>= 运算符实例,c 的值 = 11
&= 运算符实例,c 的值 = 2
^= 运算符实例,c 的值 = 0
|= 运算符实例,c 的值 = 2

其他运算符

下表列出了 C 语言支持的其他一些重要的运算符,假设int a=4:

名称运算符描述实例
逗号逗号运算符,用于将语句分割,总是返回最后变量的值a=(5,6,7);值为7
内存计算sizeof()返回变量的大小。sizeof(a) 将返回 4
取址符&返回变量的地址。&a; 将给出变量的实际地址。
解引用符*通过指针调用变量。*p; 将指向一个变量。
条件运算符? :(条件表达式)? (如果为真则执行) : (如果为假则执行)(a?a=2:a=3) 结果为a=2
点运算符.访问结构体成员a.c 访问结构体变量a中的成员变量c
指向运算符->访问结构体指针变量内成员p->c 访问结构体指针所指向的变量的成员

请看下面的实例,了解 C 语言中所有可用的其他运算符:

#include <stdio.h>
struct TYPE
{
	int a;
}mydata;
int main()
{
	char c='D';
	char *pchar=&c;
	int i=10;
	int *pint=&i;
	int b;
	struct TYPE*p=&mydata;
	
	mydata.a=0;		//访问结构体变量内成员
	p->a=2;			//访问结构体指针所指向的结构体变量内成员
	
 	/* , 运算符实例 */
 	printf("(0,1,2)输出值为%d\n",(0,1,2));

   /* sizeof 运算符实例 */
   printf("char变量c大小为%d\n",sizeof(c));			//char类型变量大小为1
   printf("char指针pchar大小为%d\n",sizeof(pchar));	//所有指针类型变量都是用于保存地址,其大小=int类型大小4
   printf("int变量i大小为%d\n",sizeof(i));			//int类型变量大小为4
   printf("int指针pint大小为%d\n",sizeof(pint));	//指针变量同int类型大小
 
   /* & 和 * 运算符实例 */
   pchar = &c;    /* 'pchar' 现在包含 'c' 的地址 */
   printf("pchar地址为%p\n",&pchar);
   printf("pchar内容为%p\n",pchar);					//pchar保存的就是c的地址
   printf("c地址为%p\n",&c);
   printf("通过*pchar访问c的值为%c\n",*pchar);
   printf("c的值为%c\n",c);
 
   /* 三元运算符实例 */
   i = 10;
   b = (i == 1) ? 20: 30;							//i不等于1,所以执行:与;之间内容,等价于b=30;
   printf( "b 的值是 %d\n", b );	
 
   b = (i == 10) ? 20: 30;							//i不等于10,所以执行?与:之间内容,等价于b=20;
   printf( "b 的值是 %d\n", b );
}

运行结果为:

char变量c大小为1
char指针pchar大小为4
int变量i大小为4
int指针pint大小为4
pchar地址为00EFF82C
pchar内容为00EFF83B
c地址为00EFF83B
通过*pchar访问c的值为D
c的值为D
b 的值是 30
b 的值是 20

C 中的运算符优先级

运算符的优先级确定表达式中项的组合。这会影响到一个表达式如何计算。某些运算符比其他运算符有更高的优先级,例如,乘除运算符具有比加减运算符更高的优先级。

例如 x = 7 + 3 * 2,在这里,x 被赋值为 13,而不是 20,因为运算符 * 具有比 + 更高的优先级,所以首先计算乘法 3*2,然后再加上 7。

下表将按运算符优先级从高到低列出各个运算符,具有较高优先级的运算符出现在表格的上面,具有较低优先级的运算符出现在表格的下面。在表达式中,较高优先级的运算符会优先被计算。

结合方向表示运算符计算时,先从那边开始计算,例如左到右表示先运算符左边操作数与运算符计算,然后再用运算符右边操作数参与计算。

单目运算符、双目运算符、三目运算符指的是该运算符有几个操作数,例如++是单目运算符,+ - * / 是双目运算符。

优先级运算符名称或含义使用形式结合方向说明
1[]数组下标数组名[常量表达式]左到右
()圆括号(表达式)函数名(形参表)
.成员选择(对象)对象.成员名
->成员选择(指针)对象指针->成员名
2-负号运算符-表达式右到左单目运算符
(类型)强制类型转换(数据类型)表达式
++自增运算符++变量 或 变量名++单目运算符
- -自减运算符- -变量名 或 变量名- -单目运算符
*取值(即解引用)运算符*指针变量单目运算符
&取地址运算符&变量名单目运算符
!逻辑非运算符!表达式单目运算符
~按位取反运算符~表达式单目运算符
sizeof长度运算符sizeof(表达式)
3/表达式 / 表达式左到右双目运算符
*表达式*表达式双目运算符
%余数(取模)整型表达式%整型表达式双目运算符
4+表达式+表达式左到右双目运算符
-表达式-表达式双目运算符
5<<左移变量<<表达式左到右双目运算符
>>右移变量>>表达式双目运算符
6>大于表达式>表达式左到右双目运算符
>=大于等于表达式>=表达式双目运算符
<小于表达式<表达式双目运算符
<=小于等于表达式<=表达式双目运算符
7==等于表达式==表达式左到右双目运算符
!=不等于表达式!= 表达式双目运算符
8&按位与表达式&表达式左到右双目运算符
9^按位异或表达式^表达式左到右双目运算符
10按位或表达式▏表达式左到右双目运算符
11&&逻辑与表达式&&表达式左到右双目运算符
12▏▏逻辑或表达式▏▏表达式左到右双目运算符
13?:条件运算符表达式1? 表达式2: 表达式3右到左三目运算符
14=赋值运算符变量=表达式右到左
/=除后赋值变量/=表达式
*=乘后赋值变量*=表达式
%=取模后赋值变量%=表达式
+=加后赋值变量+=表达式
-=减后赋值变量-=表达式
<<=左移后赋值变量<<=表达式
>>=右移后赋值变量>>=表达式
&=按位与后赋值变量&=表达式
^=按位异或后赋值变量^=表达式
▏=按位或后赋值变量▏=表达式
15,逗号运算符表达式,表达式,…左到右

请看下面的实例,了解 C 语言中运算符的优先级:

实例

#include <stdio.h>
main()
{
   int a = 20;
   int b = 10;
   int c = 15;
   int d = 5;
   int e;
 
   e = (a + b) * c / d;      // ( ( 20 + 10 ) * 15 ) / 5
   printf("(a + b) * c / d 的值是 %d\n",  e );
 
   e = ((a + b) * c) / d;    // ( ( 20 + 10 ) * 15 ) / 5
   printf("((a + b) * c) / d 的值是 %d\n" ,  e );
 
   e = (a + b) * (c / d);   // ( 20 + 10 ) * ( 15 / 5 )
   printf("(a + b) * (c / d) 的值是 %d\n",  e );
 
   e = a + (b * c) / d;     //  20 + ( (10 *15 ) / 5 )
   printf("a + (b * c) / d 的值是 %d\n" ,  e );
  
   return 0;
}

运行结果为:

(a + b) * c / d 的值是 90
((a + b) * c) / d 的值是 90
(a + b) * (c / d) 的值是 90
a + (b * c) / d 的值是 50

再例如:

#include <stdio.h>
int main()
{
	int a[5]={0,1,2,3,4};
	int *p=a;
	*p++=10;
	printf("a[0]=%d,a[1]=%d,a[2]=%d,a[3]=%d,a[4]=%d\n",a[0],a[1],a[2],a[3],a[4]);
	return 0;
}

运行结果为:

a[0]=10,a[1]=1,a[2]=2,a[3]=3,a[4]=4

解释:

  • int a[5]={0,1,2,3,4};
    创建了一个拥有5个数的数组,数组名称与数组首地址为a。a[0]=0;…a[4]=4;

  • int *p=a;
    定义一个指针指向数组a的首地址

  • *p++=10;
    ++运算符优先级高于*高于=
    1、先执行++运算符。由于++运算符是左结合,则指针p在本条语句中不变,执行完本条语句后,指针向后移动一次。
    2、再执行*运算符。程序通过指针p访问到数组的第0个元素即a[0]。
    3、后执行=运算符。程序将10赋值给a[0]。

  • printf输出a0、a1、a2、a3、a4的值。

顺序语句

我们知道计算机执行程序指令是按顺序的方式执行的,也就是说,按照指定的顺序,一条指令一条指令的执行,执行完一条指定之后,再执行下一条指令。

当然现在很多CPU都是多核心、多线程的,并发执行多条指令,但对于同一个程序而言,CPU还是通过顺序的方式来执行指令的。 在C语言中程序执行时是按语句来顺序执行的,其中每一条语句都以分号结尾。
\

#include <stdio.h>
int main()
{
	int a;
	a=10;
	printf("hello world%d\n",a);
	return 0;
}

例如: 上面的每一条语句都是以分号结尾,语句可以是定义变量、初始化变量、任何表达式、调用的函数等。

可以这样理解:一条语句,就是程序执行的一个动作。 CPU是按顺序的方式执行语句,执行完当前语句之后,再执行下一条语句。 多条语句可以写在一行代码里,也可以将每一条语句书写为单独一行代码。 但是为了编程者能够方便的读写程序代码,通常将一条语句书写为单独的一行代码。

这种顺序往下执行的,最容易理解的语句就是C语言的顺序语句。

条件语句

所谓条件语句就是只有满足一定条件才执行的语句,C语言所支持的条件语句有if语句和switch语句。

if语句

完整形式:

if(条件1)
{//满足条件1将执行
}
else if(条件2)
{//不满足前面条件,但满足条件2则执行
}
else if(条件3)
{//不满足前面条件,但满足条件3则执行
}
else
{//所有条件都不满足则执行
}

例如当我们想根据学生成绩进行评ABCD时,假设>90为A,>80为B,>60为C,否则为D。请看例程:

#include <stdio.h>
int main()
{
	int score;

	printf("请输入学生成绩:");
	scanf("%d", &score);

	if (score > 90)
	{
		printf("成绩为A\n");
	}
	else if (score > 80)
	{
		printf("成绩为B\n");
	}
	else if (score > 60)
	{
		printf("成绩为C\n");
	}
	else
	{
		printf("成绩为D\n");
	}
	return 0;
}

运行并输入100后结果为:

请输入学生成绩:100
成绩为A

相信以上程序并不难理解。但是我们实际使用时,有时候并不需要那么多分支语句,可能就简单判断一下。这时就需要用到衍生的if语句

  • if(条件)
    {//满足则执行
    }

  • if(条件)
    {//满足则执行
    }
    else
    {//不满足则执行
    }

  • if(条件1)
    {//满足条件1则执行
    }
    else if(条件2)
    {//不满足前面条件,但满足条件2则执行
    }

同时,当{}中只有一条语句时,我们可以省略{}。例如之前程序可以重新写为:

#include <stdio.h>
int main()
{
	int score;

	printf("请输入学生成绩:");
	scanf("%d", &score);

	if (score > 90)
		printf("成绩为A\n");
	else if (score > 80)
		printf("成绩为B\n");
	else if (score > 60)
		printf("成绩为C\n");
	else
		printf("成绩为D\n");
		
	return 0;
}

但是不建议使用,因为这样写会导致阅读不便,同时当我们嵌套使用if语句时,会有else匹配错误的情况。例如:

#include <stdio.h>
int main()
{
	int a = 3;

	if (a > 5)
	{
		if (a < 10)
		{
			printf("a在5到10之间\n");
		}
	}
	else
	{
		printf("a小于5\n");
	}
	printf("ok\n");
	return 0;
}

程序运行结果为:

a小于5
ok

因为在该程序中,{}内都是只有一句语句,所以可以省略{}。改写之后程序为:

#include <stdio.h>
int main()
{
	int a = 3;

	if (a > 5)
		if (a < 10)
			printf("a在5到10之间\n");
	else
		printf("a小于5\n");

	printf("ok\n");
	return 0;
}

运行结果为:

ok

之所以程序运行结果不对,是因为else自动与最近的if语句匹配上,实际程序结构变成了:

#include <stdio.h>
int main()
{
	int a = 3;

	if (a > 5)
	{
		if (a < 10)
			printf("a在5到10之间\n");
		else
			printf("a小于5\n");
	}
	printf("ok\n");
	return 0;
}

就已经偏离了我们想要表达的意思,所以建议大家尽量不用省略{}符号。

switch语句

除了if语句以外,C语言还有一种条件语句就是switch语句。相对于if灵活多变的形式而言,switch语句显得更刻板与标准化,这限制了它的使用(并不是所有条件都可以使用switch语句),但同时也给他带来了更高的运行效率。其基本形式为:

switch(总表达式)
{
	case 常量1:语句1;
	case 常量2:语句2;
	...
	case 常量3:语句n;
	default:总语句;
}

执行逻辑是:

  • 当switch语句中,总表达式==常量1,执行后续语句1、语句2、…、语句n、总语句。
  • 当switch语句中,总表达式==常量2,执行后续语句2、…、语句n、总语句。
  • 当switch语句中,总表达式与所有常量都不同时,只执行总语句。

需要注意的是:

  • 不同case标签中,常量不能相同。
  • 标签后语句可以是一条或多条。
  • 可以使用break跳出switch语句。
  • switch语句中default标签可以省略。

下面我们使用switch语句重写之前的判断成绩的程序:

#include <stdio.h>
int main()
{
	int score;

	printf("请输入学生成绩:");
	scanf("%d", &score);

	switch (score / 10)			//int数除以int数,结果仍然为int数,并且向下取整
	{
	case 0:
	case 1:
	case 2:
	case 3:
	case 4:
	case 5:printf("学生成绩为D\n"); break;;
	case 6:
	case 7:printf("学生成绩为C\n"); break;
	case 8:printf("学生成绩为B\n"); break;
	case 9:
	case 10:printf("学生成绩为A\n"); break;
	}
	return 0;
}

运行并输入70,结果为:

请输入学生成绩:70
学生成绩为C

嵌套条件语句

指的是在一个条件语句中包含另一个条件语句,常用于较为复杂的判断场景。
示例:编写一个程序判断是否是闰年。
注释:闰年条件是:

  • 普通年能被四整除且不能被100整除的为闰年。
  • 世纪年能被400整除的是闰年,如2000年是闰年,1900年不是闰年

参考例程:

#include <stdio.h>
int main()
{
	int year;

	printf("请输入年份:");
	scanf("%d", &year);

	if ((year % 4 == 0) && (year % 100 != 0))
	{
		printf("%d年是闰年\n",year);
	}
	else
	{
		if (year % 400 == 0)
		{
			printf("%d年是闰年\n", year);
		}
		else
		{
			printf("%d年不是闰年\n",year);
		}
	}

	return 0;
}

运行并输入2000,结果为:

请输入年份:2000
2000年是闰年

运行并输入1921,结果为:

请输入年份:1921
1921年不是闰年

循环语句

循环语句是指程序满足一定条件从而重复执行的控制语句。C语言中的循环语句有for、while、do while、goto。

for语句

循环语句中for具有较强的语句格式限制,其形式是:

for(初始化语句;条件语句;条件修改语句)
{//如果条件语句为真,则执行以下循环体
}

初始化语句、条件语句、条件修改语句可以由0条或多条语句构成,如果有多条语句,需要以逗号隔开。for语句逻辑是

  • 1、执行初始化语句
  • 2、判断条件语句的真假,若假则退出循环,若真则执行循环体{}
  • 3、执行完循环体后,自动执行条件修改语句,然后重复2、3两步。

举个例子:

#include <stdio.h>
int main()
{
	int a;
	int sum;
	for(a=0,sum=0;a<10;a++)		\\程序从a=0开始,运行到a=9时仍然执行循环体,直到a=10时,不执行循环,直接跳出
	{
		sum+=a;
	}
	printf("从0加到9结果为:%d\n", sum);
	return 0;
}

运行结果为:

0加到9结果为:45

练习:
1、在屏幕上显示由*组成的5行等腰直角三角形
参考程序

#include <stdio.h>
int main()
{
	int i, j;
	for (i = 0; i < 5; i++)
	{
		for (j = 0; j <= i; j++)
		{
			printf("*");
		}
		printf("\n");
	}
	return 0;
}

运行结果为:

*
**
***
****
*****

2、在屏幕上输出0到100的和。
参考例程:

#include <stdio.h>
int main()
{
	int i, sum;
	for (i = 0, sum = 0; i <= 100; i++)
	{
		sum += i;
	}
	printf("0-100的和为:%d\n", sum);
	return 0;
}

运行结果为:

0-100的和为:5050

while语句

相对于for语句,while语句较为灵活多变。格式为:

while(条件)
{//如果条件为真则执行循环体
}

例如:在屏幕上显示01234

#include <stdio.h>
int main()
{
	int i=0;
	while(i<5)
	{
		printf("%d\n",i);
		i++;
	}
	return 0;
}

运行结果为:

0
1
2
3
4

练习:
1、使用while重做:屏幕上输出由*组成的5行等腰直角三角形。
参考例程:

#include <stdio.h>
int main()
{
	int i, j;
	i = 0;
	while (i < 5)
	{
		j = 0;
		while (j <= i)
		{
			printf("*");
			j++;
		}
		printf("\n");
		i++;
	}
	return 0;
}

运行结果为:

*
**
***
****
*****

2、使用while重做:计算0-100内的数的和。
参考例程:

#include <stdio.h>
int main()
{
	int sum = 0;
	int i = 0;

	while (i <= 100)
	{
		sum += i;
		++i;
	}
	printf("0-100的和为%d\n", sum);

	return 0;
}

运行结果为:

0-100的和为5050

do while语句

do while语句与while语句及其相似,其区别在于:while语句先判断条件,在考虑是否执行循环体;而do while语句先执行循环体,在判断是否进行下一次信循环,简单理解,dowhile循环体至少执行一次。实际上,do while语句在工程中用的不多。

do while语句形式:

do
{
}while(条件);

例如:计算5-10的和。

#include <stdio.h>
int main()
{
	int i;
	int sum;
	i = 5;
	sum = 0;
	do
	{
		sum += i;
		i++;
	} while (i < 10);
	printf("5-10的和为:%d\n", sum);
	return 0;
}

运行结果为:

5-10的和为:35

练习:
1、使用do while语句重做:在屏幕上显示一个由*组成的5行等腰直角三角形。
参考例程:

#include <stdio.h>
int main()
{
	int i = 0, j = 0;
	do
	{
		j = 0;
		do
		{
			printf("*");
			j++;
		} while (j <= i);
		printf("\n");
		i++;
	} while (i < 5);
	return 0;
}

运行结果为:

*
**
***
****
*****

2、使用do while语句重做:输出0-100的和。
参考例程:

#include <stdio.h>
int main()
{
	int i = 0, sum = 0;
	do
	{
		sum += i;
		i++;
	} while (i <= 100);
	printf("0-100的和为%d\n", sum);
	return 0;
}

运行结果为:

0-100的和为5050

break语句

break语句相信大家应该还没忘记,在条件语句中我们曾介绍过,break可以用于跳出switch语句。实际上,break语句的作用也正是用于跳出某一循环体。可以使用break语句跳出的有switch、for、while、do while。

例如:使用break+while语句计算0-100的和

#include <stdio.h>
int main()
{
	int i = 0, sum = 0;
	while (1)		//永远循环,又称死循环
	{
		sum += i;
		i++;
		if (i > 100)	//如果i>100时,将强制跳出循环
		{
			break;
		}
	}
	printf("0-100的和为%d", sum);
	return 0;
}

运行结果为:

0-100的和为5050

continue语句

continue作用与break极为相似,break用于跳出循环,continue则用于跳过本次循环进入下次循环,并且continue不能用于switch语句。
例如:计算0-100所有奇数和。

#include <stdio.h>
int main()
{
	int i, sum;
	for (i = 0, sum = 0; i <= 100; i++)
	{
		if (i % 2 == 0)	//如果i为偶数,则跳过本次循环
		{
			continue;
		}

		sum += i;
	}
	printf("0-100的奇数和为%d", sum);
	return 0;
}

运行结果为:

0-100的奇数和为2500

goto语句

goto语句严格来说并不是循环语句,应该称为无条件跳转语句。关于goto语句仅处于内容完整性考虑才介绍,实际使用时该语句极其危险。可以这么说,所有使用goto语句的程序都可以使用其他循环语句代替,并且goto语句极其危险不受控制,所以同学们可以不用看本小节。注意:goto语句本质不是只是程序跳转,所以不支持break语句。

goto语句使用格式:

语句1
语句2
标签1:
语句3
语句4
goto 标签1;

当程序运行到goto 标签1;时,直接无条件跳转到标签1处。
例如:使用goto语句计算0-100数的和。

#include <stdio.h>
int main()
{
	int i = 0, sum = 0;
loop:	
	if (i <= 100)
	{
		sum += i;
		i++;
		goto loop;
	}
	printf("0-100的奇数和为%d", sum);
	return 0;
}

运行结果为:

0-100的奇数和为5050

循环语句对比

一般而言,C语言程序中for语句、while语句、break语句用的较多一点,相对而言do while、continue使用相对较少,goto语句几乎不使用。

那么我们程序到底选取哪种形式的循环语句呢,实际上,循环语句都可以互相转换,例如for(;条件;){}就等价于while(条件){}。但是由于for循环语句中,对于语句格式有着较为死板的要求,可以极大减少我们写程序时由于粗心而出现的错误,所以优先选择使用for语句,选取标准如下:

  • 对于循环次数明确,循环条件不复杂的循环程序应采用for语句。例如在屏幕上输出100句hello、计算0-100的和等程序优先使用for。
  • 对于循环次数不明确,循环条件较为复杂的循环程序应采用while语句。例如找到一个数,满足该数所有位数和相乘等于它本身(a*b*c=abc)。这种不明确循环次数,条件较为复杂的程序优先采用while语句。
  • 不论条件是否成立,都至少执行一次循环体的循环优先采用do while。
  • break与continue可以时循环更灵活,但是容易增加程序复杂度,可以适当使用。
  • 不要使用goto

练习:
1、判断0-100有多少可以被7整除的数,并输出到屏幕。
分析:该程序从0-100遍历所有数,判断是否是7的整数。应采用for循环。
参考例程:

#include <stdio.h>
int main()
{
	int num, i;
	for (i = 0, num = 0; i <= 100; i++)
	{
		if (i % 7 == 0)
		{
			printf("%d是7的整数倍\n", i);
			num++;
		}
	}
	printf("0-100共有%d个7的整数倍数\n", num);
	return 0;
}

运行结果为:

07的整数倍
77的整数倍
147的整数倍
217的整数倍
287的整数倍
357的整数倍
427的整数倍
497的整数倍
567的整数倍
637的整数倍
707的整数倍
777的整数倍
847的整数倍
917的整数倍
987的整数倍
0-100共有157的整数倍数

2、找到一个三位数abc,满足a3+b3+c3=abc。输出到屏幕上。
分析:由于我们并不确定要循环多少次,且循环条件并不是简单地比大小,所有采用while语句。
参考例程:

#include <stdio.h>
int main()
{
	int i = 100;
	int g, s, b;
	while (i < 1000)		//最多判断到999
	{
		b = i / 100;		//百位
		s = (i % 100) / 10;	//十位
		g = i % 10;			//个位

		if (b * b * b + s * s * s + g * g * g == i)
		{
			printf("找到该数字%d\n", i);
			break;			//找到后就退出循环
		}
		else
		{
			i++;
		}
	}
	return 0;
}

运行结果为:

找到该数字153

C语言动作单元–函数

函数概念

函数是一组一起执行一个任务的语句。每个 C 程序都至少有一个函数—主函数 main() 。

我们可以把重复使用的代码、完成某项功能的代码划分为一个函数,当我们需要使用时,只需调用这个函数就可以了。

在主函数前定义的函数可以没有函数声明直接使用;在主函数后面定义的函数必需要函数声明才能在函数中调用,函数声明告诉编译器函数的名称、返回类型和参数。函数定义提供了函数的具体执行的语句。

函数还有很多叫法,比如方法、子例程或程序,等等。

自定义函数

C 语言中的函数定义的一般形式如下:

函数返回类型 函数名称( 形参类型1 形参名称1,形参类型2 形参名称2 )
{
	语句;
	return 变量(同函数返回类型);
}

在 C 语言中,函数由一个函数头和一个函数主体组成。下面列出一个函数的所有组成部分:

  • 函数返回类型:一个函数可以返回一个值。函数返回类型 是函数返回的值的数据类型。有些函数执行所需的操作而不返回值,在这种情况下,函数返回类型 是关键字 void。例如我们一直写的主函数返回值一直是int类型。

  • 函数名称:这是函数的实际名称。函数名和后面的参数列表一起构成了函数签名。

  • 形参参数:形参参数就像是占位符。当函数被调用时,程序会向形参传递一个值,这个值被称为实际参数,这时函数形参就有了具体值了。参数列表包括函数参数的类型、顺序、数量。参数是可选的,也就是说,函数可能不包含参数。

  • 函数主体:函数主体包含一组定义函数执行任务的语句。

示例:

#include <stdio.h>

void fun();

int main()
{
	fun();
	fun();
	return 0;
}

void fun()
{
	printf("hello world\n");
}

运行结果为:

hello world
hello world

上述程序因为fun函数在主函数之后定义,所以在使用fun()函数之前必须有fun函数的声明,否则编译器会因为找不到fun函数而报错。

下面介绍形参与实参的区别,会有一点关于指针的知识。

  • 形参指的是定义或声明函数时,仅用于占位、并没有实际内存的变量,可以理解为形式参数。例如int fun1(int a,double b);中形式参数有int类型的a和double类型的b。形参只有函数被调用时,系统才会分配内存,并且在退出函数时,销毁该内存。

  • 相对应的,实参指的是在调用函数时,按照形式参数的个数与顺序,传递给被调用函数的一些具体的值或变量。传递参数时,仅仅是将实参的数据复制一份放到形参内存中,所以子函数中形参的改变并不影响实参的值。

请看一下程序:

#include <stdio.h>

void fun(int a, int b)
{
	a = 10;
	b = 11;
	printf("子函数中a=%d,b=%d\n", a, b);
}

int main()
{
	int a = 3, b = 5;
	fun(a, b);
	printf("主函数中a=%d,b=%d\n", a, b);
	return 0;
}

运行结果为:

子函数中a=10,b=11
主函数中a=3,b=5

那么我们能否通过子函数从而修改主函数中变量的值呢,答案是肯定的,通过指针修改!指针在C语言中妙用无穷,是灵魂所在。虽然我们不能直接修改主函数中变量的值,但是我们可以将主函数中变量的地址传递给子函数,然后子函数通过该地址访问并修改主函数中的变量,也就达到我们想要的效果了。例如一下程序:

#include <stdio.h>

void fun(int *pa, int *pb)
{
	*pa = 10;
	*pb = 11;
	printf("子函数中a=%d,b=%d\n", *pa, *pb);
}

int main()
{
	int a = 3, b = 5;
	fun(&a, &b);
	printf("主函数中a=%d,b=%d\n", a, b);
	return 0;
}

运行结果为:

子函数中a=10,b=11
主函数中a=10,b=11

上述程序中,主函数中变量被子函数修改了。一个函数可以有0或1个返回值,但是通过传递指针修改主函数变量的方式扩展函数返回值的数量,例如上述程序相当于有两个返回值a和b。

之前我们写过关于计算0到100累加和的程序,我们可以使用函数将其封装起来。

#include <stdio.h>

//输入:
//	参数1:begin累加初值
//	参数2:end累加终值
//输出:累加值
//作用:返回从begin到end的数的累加值
int fun(int begin, int end)
{
	int i, sum;
	for (i = begin, sum = 0; i <= end; i++)
	{
		sum += i;
	}
	return sum;
}

int main()
{
	int a;

	a = fun(1, 5);
	printf("1-5的和为%d\n", a);

	printf("0-100的和为%d\n", fun(0, 100));
	
	return 0;
}

运行结果为:

1-5的和为15
0-100的和为5050

在封装完成之后,我们想要计算从x到y的累加和就可以直接使用fun(x,y)来计算了,这样大大提高了代码可重复利用率。一般的我们会将高度重复的代码、或者实现了某一功能的代码封装成一个函数,然后再其他函数中调用。

可变参函数

本小节可做课外知识了解一下。与我们常见的函数不同,可变参函数的意思就是该函数的参数类型可变,参数个数可变。实际工程中我们自己写可变参函数较少,但是我们却经常使用,常见的可变参函数例如printf(),scanf()。看完本节,我们也可以写出这样的可变参函数。

首先可变参函数相关内容并不是在标准输入输出库(stdio.h)中定义,而是保存在标准参数库(stdarg.h)中,所以只要想使用可变参就需要在文件开头包含#include <stdarg.h>

可变参函数定义形式如下:

函数返回值类型 函数名称(强制形参类型 强制形参名称,...)
{
}


//范例

//输入:
// 参数1:需要计算的数据个数
// 参数2:double类型形参
// 参数3:double类型形参
// ...
//输出:double类型的所有形参累加和
//作用:计算除第一个数以外的其他参数累加和
double add(int n, ...)
{
    double sum = 0.0;
    va_list argptr;
    va_start(argptr, n);             // 初始化argptr
    for (; n > 0; --n)       // 对每个可选参数,读取类型为double的参数,
        sum += va_arg(argptr, double); // 然后累加到sum中
    va_end(argptr);
    return sum;
}

如此看来,可变参函数与普通函数定义相似:

  • 可变参函数定义时需要至少一个强制参数,意思就是该函数至少需要一个参数。
  • 可变参函数强制形参后接…表示接收后续所有参数,并保存至形参列表中。
  • stdarg文件中定义新类型va_list形参列表类型,想要在子函数中访问形参,需要先定义va_list类型变量,并对其进行初始化。
  • void va_start(va_list argptr, int n);va_start用于对参数初始化,从系统中读取n个数据保存至参数列表argptr中。上述例程先定义参数列表argptr,之后使用va_start读取n数据保存在参数列表argptr中。
  • type va_arg(va_list argptr, type);va_arg用于从从参数列表读取一个type类型变量。
  • void va_end(va_list argptr);va_end用于释放argptr参数列表的内存空间,因为argptr本质是指针,需要手动内存释放,后续指针专辑会专门讲解,切记使用完参数列表后需要va_end(argptr);释放内存。
  • 参数列表不会对参数隐式类型转换,需要注意输入类型匹配。

上述例程完善一下为:

#include <stdio.h>
#include <stdarg.h>
//输入:
// 参数1:需要计算的数据个数
// 参数2:double类型形参
// 参数3:double类型形参
// ...
//输出:double类型的所有形参累加和
//作用:计算除第一个数以外的其他参数累加和
double add(int n, ...)
{
    double sum = 0.0;
    va_list argptr;
    va_start(argptr, n);             // 初始化argptr
    for (; n > 0; --n)       // 对每个可选参数,读取类型为double的参数,
        sum += va_arg(argptr, double); // 然后累加到sum中
    va_end(argptr);
    return sum;
}

int main()
{
    printf("1+3+3=%lf\n", add(3, 1.0, 3.2, 3.2));
    printf("2+2=%lf\n", add(2, 2, 2));
    return 0;
}

运行结果为:

1+3+3=7.400000
2+2=0.000000

细心地读者看到了2+2=0.000000是不对的,为什么会导致这种问题呢,原因很简单,参数列表不会对形参进行类型转换,它把int类型的参数2错误的用va_arg(argptr,double);按照double数据类型的格式读取出来了,导致出现问题,如何解决?

  • 调用时可以声明数据类型add((int)2,(double)2,(double)2);
  • 以浮点数形式书写数据add(2,2.0,2.0);

修改之后运行结果为:

1+3+3=7.400000
2+2=4.000000

递归函数

类似于递归证明,函数也可以递归调用,当函数直接或间接调用自己本身时,就构成了递归调用。例如:

void fun()
{
	fun();
}

以上函数即为最简单的递归调用,如果他被调用将会陷入死循环。下面用实例演示递归函数的用处与作用。
例如:计算从0-n的累加和

#include <stdio.h>

//输入:n表示累加终值
//输出:累加值
//作用:返回从0到n的数的累加值
int fun(int n)
{
	if (n > 1)
		return n + fun(n - 1);
	else
		return 1;
}

int main()
{	
	printf("0-100的累加和为%d\n", fun(100));
	return 0;
}

运行结果为:

0-100的累加和为5050

由于递归函数理解较为复杂,这里稍微多费口舌介绍一下具体运行过程:

  • 程序进入main函数。
  • 开始执行printf("0-100的累加和为%d\n",fun(100));先执行fun(100)
  • 第一次进入fun函数,形参为100,满足if语句条件。
  • 执行return 100+fun(100-1);先要计算该语句中fun(100-1)的值。
  • 第二次进入fun函数,形参为99,满足if语句条件。
  • 执行return 99+fun(99-1);先要计算该语句中fun(99-1)的值。
  • 第99次进入fun函数,形参为2,满足if语句条件。
  • 执行return 2+fun(2-1);先要计算该语句中fun(2-1)的值。
  • 第100次进入fun函数,形参为1,不满足if语句条件。
  • 执行return 1;
  • 退出第100次进入的fun函数,第100次fun函数返回return 1;
  • 退出第99次进入的fun函数,第99次fun函数返回return 2+1;
  • 退出第二次进入的fun函数,第二次fun函数返回return 99+(98+...+1);
  • 退出第一次进入的fun函数,第一次fun函数返回return 100+(99+...+1);
  • 最终返回值为0-100的和。

递归函数适用于函数变量较少,函数体简单,递归次数有限且不算太多的情况。递归函数可以帮助我们理清思路,使得复杂的问题得以简化。下面请看经典例题汉罗塔。

练习:汉罗塔
在这里插入图片描述
一次只能移动一个圆盘,并且大圆盘不能放在小圆盘上,要求将A柱汉罗塔移动到C柱,且顺序保持不变。
对于该问题我们可以抽象成表格

当只有一个罗盘时:

ABC方法
第1步1
第2步1A->C

当有两个罗盘时:

ABC方法
第1步12
第2步21A->B
第3步12A->C
第4步12B->C

当有三个罗盘时

ABC方法
第1步123
第2步231A->C
第3步321A->B
第4步312C->B
第5步123A->C
第6步123B->A
第7步123B->C
第8步123A->C

当罗盘个数逐渐上涨时,游戏步骤呈指数递增,那么我们如何使用编写程序完成该任务呢。

递归分析:

  • 我们需要写一个函数,该函数任务是可以将一座汉罗塔从我们指定柱子x柱搬运到目标柱子z柱。
  • 当只有一个罗盘时,步骤为x->z。
  • 当大于一个罗盘时,步骤为先调用函数本身,将前1到n-1个罗盘搬运到y柱子,再将第n个罗盘搬运到z柱子,最后再调用函数本身将y柱子上的1到n-1个罗盘搬运到z柱子。
    参考例程:
#include <stdio.h>

//输入:
// 参数1:x表示此时罗盘所在柱子
// 参数2:y表示此时空的柱子
// 参数3:z表示目标柱子
// 参数4:搬运罗盘的个数
//输出:无
//作用:将n个罗盘从x柱子搬运到z柱子的步骤
void fun(char x, char y, char z, int n)
{
	if (n == 1)						//如果只有一个罗盘,那就直接移过去
	{
		printf("%c->%c\n", x, z);
	}
	else							//如果有多个罗盘
	{
		fun(x, z, y, n - 1);		//先将前面1到n-1个罗盘搬到y上
		printf("%c->%c\n", x, z);	//再将第n个罗盘搬到z上
		fun(y, x, z, n - 1);		//再将1到n-1个罗盘搬到z上
	}
}

int main()
{
	int n;
	printf("请输入罗盘个数:");
	scanf("%d", &n);
	fun('A', 'B', 'C', n);
	return 0;
}

运行并输入1,结果为:

请输入罗盘个数:1
A->C

运行并输入2,结果为:

请输入罗盘个数:2
A->B
A->C
B->C

运行并输入3,结果为:

请输入罗盘个数:3
A->C
A->B
C->B
A->C
B->A
B->C
A->C

当有n个罗盘时,想要经历2n-1个步骤。这就是经典的汉罗塔递归程序,如果有点难以理解可以多看几遍该例程。

除了汉罗塔以外,图像处理中种子生长与数据结构中二叉树对于递归调用的应用也较多,后续涉及到数据结构的章节再详细介绍。

再探scanf

当我们学会了函数并了解函数的作用之后,回头再开之前的一直在用的printf与scanf函数。

printf函数原型是int printf(const char *format, ...);,const char *表示不可改变char类型指针,指的是无法通过指针来改变目标地址内容,后面三个点表示可变参数。其返回值是打印字符个数,例如printf("asdf");语句中,该函数返回值为3。

scanf函数原型是Int scanf (const char *format,...);,使用示例:scanf("%d %d",&a,&b);函数返回值为int型。

  • 如果a和b都被成功读入,那么scanf的返回值就是2;
  • 如果只有a被成功读入,返回值为1;
  • 如果a读取失败,返回值为0;
  • 如果遇到错误或遇到end of file,返回值为EOF。end of file为Ctrl+z 或者Ctrl+d。

这样看来我们可以通过读取scanf函数返回值从而进行入参检查。
例如:

#include <stdio.h>
int main()
{
	int s1, s2, s3;
	printf("欢迎使用成绩录入系统\n");
	printf("请输入学生成绩:");
	while (scanf("%d %d %d", &s1, &s2, &s3) != 3)
	{
		getchar();
		printf("\n输入有误,请重新输入:");
	}
	return 0;
}

运行并输入10 20 30,结果为:

欢迎使用成绩录入系统
请输入学生成绩:10 20 30
ok

运行并输入80 c 90;,结果为:

欢迎使用成绩录入系统
请输入学生成绩:80 c 90

输入有误,请重新输入:

通过scanf函数的返回值,我们就实现了不同数据之间的入参检测。

C语言库

C 标准库提供了大量的程序可以调用的内置函数。我们并不需要了解所有库函数,更不需要学习所有库函数具体是怎么实现的,我们只需要有个简单的印象,方便在我们想要时直接调用即可。例如常见的

输入输出操作

通常存在于stdio.h中,一般用于从键盘鼠标输入,输出到文件或屏幕。
常用函数有
printf()函数
scanf()函数
putchar()函数
getchar()函数
getch()函数
puts()函数
gets()函数
文章第二部分–标准输入输出介绍过,这里就不多赘述了。

还有几个常用的输入输出函数与字符串有关

sprintf()函数

  • 原型:sprintf(char * str,const char *format,…)
  • 作用:将字符串format格式化输出到字符串str
    //使用示例
    char str[128];
    sprintf(str,“hello world%d”,32);
    //此时str中字符串为"hello world32"

sscanf()函数

  • 原型:sscanf(char*str,const char *format,…)
  • 作用:从字符串str中按format格式化输入到后面参数中
    //使用示例
    int i;
    double d;
    char str[128]=“1234 12.6”;
    sscanf(str,“%d %lf”,&i,&d);
    //此时i=1234,d=12.6
文件操作

通常存在于stdio.h文件中,用于读取或写入文件,可以看作特殊的输入输出,其对象是文件。

fopen()函数

  • 原型:FILE * fopen(const char * path,const char * mode);
  • 作用:按指定格式mode打开指定路径path文件。

fprintf()函数

  • 原型:int fprintf(FILE *stream, char *format[, argument,…]);
  • 作用:传送格式化输出到一个文件中

fscanf()函数

  • 原型:int fscanf(FILE *stream, char *format[,argument…]);
  • 作用:从一个文件流中执行格式化输入

clearerr()函数

  • 原型:void clearerr(FILE * stream);
  • 作用:清除文件流的错误旗标

fclose()函数

  • 原型:int fclose(FILE * stream);
  • 作用:关闭指定文件

fget()函数

  • 原型:int fgetc(FILE * stream);
  • 作用:由文件中读取一个字符

fgets()函数

  • 原型:char * fgets(char * s,int size,FILE * stream);
  • 作用:由文件中读取一个字符

fputc()函数

  • 原型:int fputc(int c,FILE * stream);
  • 作用:将一指定字符写入文件流中

fputs()函数

  • 原型:int fputs(const char * s,FILE * stream);
  • 作用:将一指定字符串写入文件流中

fseek()函数

  • 原型:int fseek(FILE * stream,long offset,int whence);
  • 作用:移动文件流的读写位置

fwrite()函数

  • 原型:size_t fwrite(const void * ptr,size_t size,size_t nmemb,FILE * stream);
  • 作用:将数据写至文件流

fread()函数

  • 原型:size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
  • 作用:从文件流中读取数据
动态内存操作

通常存在于stdlib.h文件中,用于对电脑内存控制,直接通过指针访问与控制内存。

malloc()函数

  • 原型:void* malloc (size_t size);
  • 作用:向内存申请一块连续可用的空间,并返回指向这块从堆区上开辟的空间的指针。

calloc()函数

  • 原型:void* calloc(size_t num,size_t size);
  • 作用:函数的功能是为num个大小的size的元素开辟一块空间,并且把空间的每个字节初始化为0。

realloc()函数

  • 原型:void realloc(voidptr,size_t size);
  • 作用:修改动态开辟的内存的大小

free()函数

  • 原型:void free(void*ptr);
  • 作用:释放动态开辟的空间。

文章特殊部分–指针专辑–指针与动态内存将详细介绍介绍

随机数操作

通常存在于stdlib.h文件中,用于生成一个随机数。

srand()函数

  • 作用:设置随机数种子
  • 使用:srand(n);n为int数
    可以配合系统时间使用,时间不停变化,通过不停调用srand(time);则该随机数种子也不停变化。

rand()函数

  • 作用:根据随机数种子产生随机数,该数字在0-RAND_MAX 之间
  • 使用:int a=rand();
    要取得 [a,b) 的随机整数,使用 (rand() % (b-a))+ a;
    要取得 [a,b] 的随机整数,使用 (rand() % (b-a+1))+ a;
    要取得 (a,b] 的随机整数,使用 (rand() % (b-a))+ a + 1;
    通用公式: a + rand() % n;其中的 a 是起始值,n 是整数的范围。
    要取得 a 到 b 之间的随机整数,另一种表示:a + (int)b * rand() / (RAND_MAX + 1)。
    要取得 0~1 之间的浮点数,可以使用 rand() / double(RAND_MAX)。
字符串操作

通常存在于string.h中,用于对字符串实现各种操作。

strcat()函数

  • 原型:char *strcat(char *dest, const char src)
  • 作用:把src所指向的字符串追加到dest 所指向的字符串的结尾。

strcmp()函数

  • 原型:int strcmp(const char *str1, const char str2)
  • 作用:把 str1 所指向的字符串和 str2 所指向的字符串进行比较。

strcpy()函数

  • 原型:char *strcpy(char *dest, const char src)
  • 作用:把 src 所指向的字符串复制到 dest。

strlen()函数

  • 原型:size_t strlen(const char str)
  • 作用:计算字符串 str 的长度,直到空结束字符,但不包括空结束字符。

memchr()函数

  • 原型:void *memchr(const void str, int c, size_t n)
  • 作用:在参数 str 所指向的字符串的前 n 个字节中搜索第一次出现字符 c(一个无符号字
    符)的位置。

memcmp()函数

  • 原型:int memcmp(const void *str1, const void str2, size_t n)
  • 作用:把 str1 和 str2 的前 n 个字节进行比较。

memcpy()函数

  • 原型:void *memcpy(void *dest, const void src, size_t n)
  • 作用:从 src 复制 n 个字符到 dest。

memmove()函数

  • 原型:void *memmove(void *dest, const void src, size_t n)
  • 作用:另一个用于从 src 复制 n 个字符到 dest 的函数。

memset()函数

  • 原型:void *memset(void str, int c, size_t n)
  • 作用:复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。

strncat()函数

  • 原型:char *strncat(char *dest, const char src, size_t n)
  • 作用:把 src 所指向的字符串追加到 dest 所指向的字符串的结尾,直到 n 字符长度为
    止。

strchr()函数

  • 原型:char *strchr(const char str, int c)
  • 作用:在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置。

strncmp()函数

  • 原型:int strncmp(const char *str1, const char str2, size_t n)
  • 作用:把 str1 和 str2 进行比较,最多比较前 n 个字节。

strcoll()函数

  • 原型:int strcoll(const char *str1, const char str2)
  • 作用:把 str1 和 str2 进行比较,结果取决于 LC_COLLATE 的位置设置。

strncpy()函数

  • 原型:char *strncpy(char *dest, const char src, size_t n)
  • 作用:把 src 所指向的字符串复制到 dest,最多复制 n 个字符。

strcspn()函数

  • 原型:size_t strcspn(const char str1, const char str2)
  • 作用:检索字符串 str1 开头连续有几个字符都不含字符串 str2 中的字符。

strerror()函数

  • 原型:char strerror(int errnum)
  • 作用:从内部数组中搜索错误号errnum,并返回一个指向错误消息字符串的指针。

strpbrk()函数

  • 原型:char *strpbrk(const char *str1, const char str2)
  • 作用:检索字符串 str1 中第一个匹配字符串 str2 中字符的字符,不包含空结束字符。也
    就是说,依次检验字符串 str1 中的字符,当被检验字符在字符串 str2 中也包含时,则停止检验,并返回该字符位置。

strrchr()函数

  • 原型:char *strrchr(const char str, int c)
  • 作用:在参数 str 所指向的字符串中搜索最后一次出现字符 c(一个无符号字符)的位置。

strspn()函数

  • 原型:size_t strspn(const char *str1, const char str2)
  • 作用:检索字符串 str1 中第一个不在字符串 str2 中出现的字符下标。

strstr()函数

  • 原型: char *strstr(const char *haystack, const char needle)
  • 作用:在字符串 haystack 中查找第一次出现字符串 needle(不包含空结束字符)的位置。

strtok()函数

  • 原型: char *strtok(char *str, const char delim)
  • 作用:分解字符串 str 为一组字符串,delim 为分隔符。

strxfrm()函数

  • 原型:size_t strxfrm(char *dest, const char src, size_t n)
  • 作用:根据程序当前的区域选项中的 LC_COLLATE 来转换字符串 src 的前 n 个字符,并把它们放置在字符串 dest 中。
计算操作

通常存在于math.h文件中,主要是对一些常用数学运算符号和数学公式的封装。

三角函数

sin()函数

  • 原型:double sin(double);
  • 作用:正弦

cos()函数

  • 原型:double cos(double);
  • 作用:余弦

tan()函数

  • 原型:double tan(double);
  • 作用:正切
反三角函数

asin()函数

  • 原型:double asin (double);
  • 作用:反正弦函数,结果介于[-PI/2,PI/2]

acos()函数

  • 原型:double acos (double);
  • 作用:反余弦函数,结果介于[0,PI]

atan()函数

  • 原型:double atan (double);
  • 反正切(主值),结果介于[-PI/2,PI/2]

atan2()函数

  • 原型:double atan2 (double,double);
  • 反正切(整圆值),结果介于[-PI,PI]
双曲三角函数

sinh()函数

  • 原型:double sinh (double);

cosh()函数

  • 原型:double cosh (double);

tanh()函数
*原型:double tanh (double);

指数与对数

frexp()函数

  • 原型:double frexp(double value,int *exp);
  • 作用:这是一个将value值拆分成小数部分f和(以2为底的)指数部分exp,并返回小数部分f,即f·2^exp。其中f取值在0.5~1.0范围或者0。

ldexp()函数

  • 原型:double ldexp(double x,int exp);
  • 作用:这个函数刚好跟上面那个frexp函数功能相反,它的返回值是x*2^exp

modf()函数

  • 原型:double modf(double value,double *iptr);
  • 作用:拆分value值,返回它的小数部分,iptr指向整数部分。

log()函数

  • 原型:double log (double);
  • 作用:以e为底的对数

log10()函数

  • 原型:double log10 (double);
  • 作用:以10为底的对数

pow()函数

  • 原型:double pow(double x,double y);
  • 作用:计算x的y次幂

powf()函数

  • 原型:float powf(float x,float y);
  • 作用:功能与pow一致,只是输入与输出皆为单精度浮点数

exp()函数

  • 原型:double exp (double);
  • 作用:求取自然数e的幂

sqrt()函数

  • 原型:double sqrt (double);
  • 作用:开平方根
取整

round()函数

  • 原型:double round(double);
  • 作用:四舍五入取整,返回x的四舍五入取整数

ceil()函数

  • 原型:double ceil (double);
  • 作用:取上整,返回不比x小的最小整数

floor()函数

  • 原型:double floor (double);
  • 作用:取下整,返回不比x大的最大整数,即 高斯函数[x]
绝对值

abs()函数

  • 原型:int abs(int i);
  • 作用:求整型的绝对值

fabs()函数

  • 原型:double fabs (double);
  • 作用:求实型的绝对值

cabs()函数

  • 原型:double cabs(struct complex znum);
  • 作用:求复数的绝对值
标准化浮点数

frexp()函数

  • 原型:double frexp (double f,int *p);
  • 作用:标准化浮点数,f = x * 2^p,已知f求x,p (x介于[0.5,1])

ldexp()函数

  • 原型:double ldexp (double x,int p);
  • 作用:与frexp相反,已知x,p求f
取整与取余

modf()函数

  • 原型:double modf (double,double*);
  • 作用:将参数的整数部分通过指针回传,返回小数部分

fmod()函数

  • 原型:double fmod (double,double);
  • 作用:返回两参数相除的余数
字符分类操作

isalnum()函数

  • 原型:int isalnum(int c)
  • 作用:该函数检查所传的字符是否是字母和数字。

isalpha()函数

  • 原型:int isalpha(int c)
  • 作用:该函数检查所传的字符是否是字母。

iscntrl()函数

  • 原型:int iscntrl(int c)
  • 作用:该函数检查所传的字符是否是控制字符。

isdigit()函数

  • 原型:int isdigit(int c)
  • 作用:该函数检查所传的字符是否是十进制数字。

isgraph()函数

  • 原型:int isgraph(int c)
  • 作用:该函数检查所传的字符是否有图形表示法。

islower()函数

  • 原型:int islower(int c)
  • 作用:该函数检查所传的字符是否是小写字母。

isprint()函数

  • 原型:int isprint(int c)
  • 作用:该函数检查所传的字符是否是可打印的。

ispunct()函数

  • 原型:int ispunct(int c)
  • 作用:该函数检查所传的字符是否是标点符号字符。

isspace()函数

  • 原型:int isspace(int c)
  • 作用:该函数检查所传的字符是否是空白字符。

isupper()函数

  • 原型:int isupper(int c)
  • 作用:该函数检查所传的字符是否是大写字母。

isxdigit()函数

  • 原型:int isxdigit(int c)
  • 作用:该函数检查所传的字符是否是十六进制数字。

tolower()函数

  • 原型:int tolower(int c)
  • 作用:该函数把大写字母转换为小写字母。

toupper()函数

  • 原型:int toupper(int c)
  • 作用:该函数把小写字母转换为大写字母。
  • 34
    点赞
  • 101
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值