C
0.语言分类
高级语言分为:
1)面向过程的(如C)和面向对象的(如C++,JA)
2)编译型的(如C、C++等)和解释型的(如Python等脚本语言,以及Linux下的Shell编程)
面向对象语言 相对于面向过程的编程语言,面向对象的编程语言具有3个显著的特点:封装、继承、多态
⒈封装 (encapsulation):隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读取和修改的访问级别。
⒉继承(inheritance):继承是面向对象语言的重要机制。借助继承,可以扩展原有的代码,应用到其他程序中,而不必重新编写这些代码。在java语言中,继承是通过扩展原有的类,声明新类来实现的。扩展声明的新类称为子类,原有的类称为超类(父类)。继承机制规定,子类可以拥有超类的所有属性和方法,也可以扩展定义自己特有的属性,增加新方法和重新定义超类的方法。
⒊多态(Polymorphism):多态性是允许你将父对象设置成为一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。
C语言: 1)C语言属于面向过程的编译型编程语言
2)C语言是美国Dennis Ritchie在1972年设计发明的,C语言主体诞生于1973年,正式发行于1977年
3)C语言经历过3个修订版本:C89、C99、C11。分别代表1989年版本、1999年版本、2011年版本。
4)C语言长期作为编程语言排行榜的前2名。
C语言的优缺点: 优点: 1)简洁紧凑、灵活方便
2)运算符丰富
3)数据类型丰富
4)表达方式灵活实用
5)允许直接访问物理地址,对硬件进行操作
6)生成目标代码质量高,程序执行效率高
7)可移植性好
8)表达力强
缺点: 1)C语言的缺点主要表现在数据的封装性上,这一点使得C在数据的安全性上有很大缺陷,这也是C和C++的一大区别。
2)C语言的语法限制不太严格,对变量的类型约束不严格,影响程序的安全性,对数组下标越界不作检查等。从应用的角度,C语言比其他高级语言较难掌握。也就是说,对用C语言的人,要求对程序设计更熟练一些。
1.编写简单的C程序:
HelloWorld
1.1HelloWorld
|
|
1.2GCC
编译器GCC:把程序代码变成可执行程序
1)GCC是GNU C Compiler的缩写,是开源的C语言编译器
2)把hello.c变成可执行程序步骤:
|
|
练习:编写程序,输出“XXX欢迎来到动物园!”(XXX是自己的名字) 提示: ⒈不要忘记头文件#include<stdio.h>
⒉不要忘记main函数以及main后有()
⒊不要忘记printf()中输出字符串需要用引号引起
⒋不要忘记在这句话之后加\n
⒌不要忘记在main函数内的语句结尾加; 答案:
|
|
3)注释: 注释是解释代码用的文本,是编写程序代码的人为了方便理解代码所写的文本。
常见注释:
行注释:使用//,注释一行
块注释:使用/**/,注释一段
被注释的文本不会被执行
|
|
4)C语言从代码变成可执行程序的步骤:
|
|
练习:编程实现打印用*组成的字母C。要求先使用分步编译并观察每步的产物,再使用完整编译。 生成图案如下:
|
|
答案:
|
|
2.C语言的编程规范:
2.1命名规范:
⒈在所有命名中,都应使用标准的英文单词或缩写。
⒉所有命名都应遵循达意原则,即名称应含义清晰、明确。
⒊所有命名都不易过长,应控制在规定的最大长度以内,一般不超过32个字节。
2.2编码规范:
⒈在一行代码内只写一条语句;
⒉在嵌套的函数块中使用一个TAB缩进;
⒊每行长度不应超过80个字符;(如超过,在行尾加\表示换行) ⒋一个函数建议不要超过200行代码;
⒌程序中结束符号;(分号)前面不要加空格;
⒍为程序添加适当的注释;
2.3代码要求:
⒈代码格式清楚、层次分明;
⒉代码易于理解、可读性强;
⒊代码具有健壮性,且没有错误;
有关于Linux C的编码规范,我们强烈推荐阅读linux kernel coding style文档(读1~8章和12章) 百度linux kernel coding(中文版:百度linux kernel coding cn)
3.标识符,变量与常量
3.1标识符
用来标识变量名、符号常量名、函数名、类型名、文件名等的有效字符序列;
|
|
3.2关键字
关键字:对编译器具有特定含义的标识符,是标识符的一个特殊的集合。
C语言有32个关键字,且所有的关键字都是小写
3.2.1基本数据类型关键字
void:声明函数无返回值或无参数,声明无类型指针,显示丢弃运算结果。(C89标准新增)
char:字符型类型数据,属于整型数据的一种。(K&R时期引入)
int:整型数据,表示范围通常为编译器指定的内存字节长。(K&R时期引入)
float:单精度浮点型数据,属于浮点数据的一种。(K&R时期引入)
double:双精度浮点型数据,属于浮点数据的一种。(K&R时期引入)
_Bool:布尔型(C99标准新增)
_Complex:复数的基本类型(C99标准新增)
_Imaginary:虚数,与复数基本类型相似,没有实部的纯虚数(C99标准新增)
_Generic:提供重载的接口入口(C11标准新增)
3.2.2类型修饰关键字
short:修饰int,短整型数据,可省略被修饰的int。(K&R时期引入)
long:修饰int,长整型数据,可省略被修饰的int。(K&R时期引入)
long long:修饰int,超长整型数据,可省略被修饰的int。(C99标准新增)
signed:修饰整型数据,有符号数据类型。(C89标准新增)
unsigned:修饰整型数据,无符号数据类型。(K&R时期引入)
restrict:用于限定和约束指针,并表明指针是访问一个数据对象的唯一且初始的方式。(C99标准新增)
3.2.3复杂类型关键字
struct:结构体声明。(K&R时期引入)
union:联合体声明。(K&R时期引入)
enum:枚举声明。(C89标准新增)
typedef:声明类型别名。(K&R时期引入)
sizeof:得到特定类型或特定类型变量的大小。(K&R时期引入)
inline:内联函数用于取代宏定义,会在任何调用它的地方展开。(C99标准新增)
3.2.4存储级别关键字
auto:指定为自动变量,由编译器自动分配及释放。通常在栈上分配。与static相反。当变量未指定时默认为auto。(K&R时期引入)
static:指定为静态变量,分配在静态变量区,修饰函数时,指定函数作用域为文件内部。(K&R时期引入)
register:指定为寄存器变量,建议编译器将变量存储到寄存器中使用,也可以修饰函数形参,建议编译器通过寄存器而不是堆栈传递参数。(K&R时期引入)
extern:指定对应变量为外部变量,即标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。(K&R时期引入)
const:指定变量不可被当前线程改变(但有可能被系统或其他线程改变)。(C89标准新增)
volatile:指定变量的值有可能会被系统或其他线程改变,强制编译器每次从内存中取得该变量的值,阻止编译器把该变量优化成寄存器变量。(C89标准新增)
3.2.5流程控制关键字
1)跳转结构 return:用在函数体中,返回特定值(如果是void类型,则不返回函数值)。(K&R时期引入)
continue:结束当前循环,开始下一轮循环。(K&R时期引入)
break:跳出当前循环或switch结构。(K&R时期引入)
goto:无条件跳转语句。(K&R时期引入) 2)分支结构 if:条件语句,后面不需要放分号。(K&R时期引入)
else:条件语句否定分支(必须与if连用)。(K&R时期引入)
switch:开关语句(多重分支语句)。(K&R时期引入)
case:开关语句中的分支标记,与switch连用。(K&R时期引入)
default:开关语句中的“其他”分支,可选。(K&R时期引入)
for:循环(K&R时期引入)
while:循环(K&R时期引入)
4.变量与常量
4.1变量
4.1.1变量的类型:
常见的变量类型有: int:整型,保存整数
char:字符型,保存一个字符(字母、数字、其他符号等)
float:浮点型,保存小数 double:双精度浮点型,保存小数范围更大,小数点后位数更多(更精确)
4.1.2变量的定义:
变量必须先定义,再使用
定义一个int型变量:
int a;//定义一个int型变量 定义一个char型变量:
char c;//定义一个char型变量
注意:定义变量不要重名,而且在选择变量名和其它标识符时,应注意做到“见名知意”,即选有含意的英文单词(或其缩写)作标识符。
4.1.3变量的赋值
a = 10;//给int型变量赋值需要整数
c = ‘A’;//给char型变量赋值需要字符
有时为了书写简便,可以直接在定义变量时赋值。 int a = 10; char c = ‘A’; 这种在定义时就直接赋值的使用方法称为“初始化”。
变量之间也可赋值,如: int a = 10; int b; b = a;//将a中存放的值(10)赋值给b
4.1.4变量的打印
使用printf()来打印变量的值。
要打印a中的值10:printf("%d\n",a);
%d代表十进制输出一个整数。程序运行到这里,printf函数会扫描逗号后面的内容,遇到第一个变量后将这个变量的值放进%d的位置然后输出。
练习:交换杯子里的液体
有两个杯子(用int型代替),其中一个装酒,其中一个装水。如何让两个杯子中的液体交换? 提示:需要第三个杯子 答案:
|
|
4.2.常量
4.2.1常量:在程序执行过程中,其值不被改变的量
直接常量:直接引用的数字等;
符号常量:使用标识符来代替一个数字(常见的:宏定义常量 和 常变量)
4.2.2宏定义:又称为宏代换,是定义一个标识符来代表一个值或一个表达式
使用方法:在程序开头使用,#define #define MAX 10 定义一个宏定义,使用MAX来代替10
练习1: 使用宏定义PI来定义3.1415926,计算圆的面积 答案:
|
|
练习2:租船问题。 写程序计算游客需要付的租船的费用。租船每小时30元,押金100元。游客输入租船时间,计算出租船费用。租船费用=时间*每小时钱数+押金。 要求押金与每小时钱数使用宏定义。 答案:
|
|
宏定义的使用阶段:在预处理阶段,将所有的宏换成宏值 宏定义也可定义表达式:
#define ADD(a,b) (a)+(b)
#define MUL(a,b) (a)*(b)
注意:如果要使用宏定义来代替表达式,需要在每个表达式的变量都加上括号以防止出现计算错误 练习:输入程序,对比以下两个宏定义的不同,思考产生不同结果的原因。
|
|
4.3常变量
|
|
4.4常量与后缀:
|
|
5.数据类型与类型转换
5.0 原码、反码与补码
|
|
5.1数据类型
C语言的数据类型分类:
|
|
5.2 int,unsigned int,short,long
5.2.1整型int:
大小:16位2字节 或 32位4字节(取决于编译器)
存储格式:0~30位是数据位,第31位是符号位,用0代表正数,1代表负数。负数用补码存储。
存储范围:-2^31 ~ 2^31-1
打印格式:%d(十进制)、%o(八进制)、%x或%X(十六进制)
如需要打印八进制和十六进制的特殊格式,加#
%#o(打印八进制数,数前有0表示八进制数),%#x(打印十六进制数,数前有0x表示十六进制数)
说明: int类型用于存放整数,占16或32位(取决于编译器),2或4字节。其中第31位为符号位(符号位0代表正数,1代表负数),后面几位为数据位
负数在内存中是以补码的形式存储的。
补码的计算方法:按位取反,末位加1
常见的有十进制,八进制,十六进制三种数字进制。
八进制输入输出格式化控制符使用%o或%#o。八进制数以0开头,如0666
十六进制输入输出格式化控制符使用%x或%X或%#x或%#X。十六进制数以0x开头,如0x12345678
5.2.2无符号整型unsigned int:
无符号整型unsigned int:
大小:同int型
存储格式:0~31位都是数据位,无符号位
存储范围:0 ~ 2^32-1
打印格式:%u
说明:
与int型基本相同,区别是int型的符号位不再是符号位而也作为数据位,因此无符号整型数据比整型数据存储范围大2倍,不过无法存储负数,适用于只有正数的情况
unsigned关键字不仅仅可以修饰int类型,还可修饰short long char等类型。
注意:unsigned关键字修饰的变量无法接收负数。
无符号数的输入输出格式控制是%u
5.2.3短整型short:
大小:16位2字节
存储格式:0~14位为数据位,第15位为符号位,用0代表正数,1代表负数
存储范围:-32768 ~ 32767
打印格式:%hd
说明:
short类型与int类型类似,只不过是16位,2字节。第15位为符号位,后面几位为数据位
short类型适用于存储不太大的数据,节省内存空间。
short类型的输入输出格式控制是%hd
5.2.4长整型long:
大小:32位4字节
存储格式:同32位int类型
存储范围:同32位int类型
打印格式:%ld
说明: 在过去的16位编译器中,int型是16位,所以long类型是32位。
不过在现代的32位编译器中,int型与long类型已无本质区别。
如果需要考虑程序跨平台移植(如16位编译器<—>32位编译器)需要谨慎选择使用int还是long
5.3char类型:
大小:8位1字节
存储格式:0~6位都为数据位(128) 或 0~7位都为数据位(256,扩展后的ASCII表)
存储范围:0 ~ 255
打印格式:%c
说明:
char类型也属于整型数据类型(int、short、long)的一份子,只不过char类型运算时使用ASCII表来确定值。与其他三个整型数据类型不同的是,char类型默认就是unsigned类型,而其他三个则默认是signed类型。
ASCII表与转义字符
|
|
5.4float与double
1)浮点数存储位划分 浮点数分为float类型(32位)和double类型(64位)
其中最高位是符号位,代表整个数的正负
指数部分实际存储的是移码E,E=e+127(double型中E=e+1023)。若e为正代表小数点向左移动了e位,反之e为负代表小数点向右移动了e位。
尾数部分使用科学计数法,转化成1.XXXXXXX的形式(二进制,浮点数的有效数字),但不存1和.
由此我们可以看出,尾数代表了浮点数的精度,指数代表了浮点数的范围。
2)浮点数存储方法:
示例1:2.5的浮点数存储 ⒈先将该数字转化成二进制形式
2.5(十进制)—–>10.1(二进制)
⒉移位。移动小数点,使之变成1.XXXXX2^e的二进制科学计数法的形式
10.1—–>1.01*2
⒊舍弃1.,将尾数部分(XXXXX)存数在浮点数的尾数位部分。注意是由高位起存储。其余位补0
01000000000000000000000(尾数)
⒋在第二步的移位过程中,小数点向左移动,因此e的值为+1,获得移码EE=e+127=128
⒌将E转换成二进制,存储在指数位部分
128(十进制)—–>1000000
⒍最后确定这个数是正数,最高位为0。
通过以上几步我们就得到了2.5的浮点数存储
2.5(十进制)—–>0 10000000 01000000000000000000000(float类型)
示例2:125.5的浮点数存储
|
|
示例3:0.5的浮点数存储
|
|
练习1:将以下十进制数转换成浮点数存储 ①17.625 ②-0.75 答案: ①0 10000011 00011010000000000000000 ②1 01111110 10000000000000000000000 练习2:已知以下的十六进制数均是以单精度浮点型(float)形式存储的数,求它们的实际值 例如:0x40200000—–>0100000000100000000000000000000—–>2.5(十进制) ①0xC1480000 ②0x42F6E800 答案: ①-12.5 ②123.453125
由上文我们可以看出,如果遇到无法完全转化成浮点数的小数(例如0.3这种没法完美转化成二进制数),则浮点数只能存储这个数的近似值而非实际值。
因此,浮点数存储的小数不是实际值,只能是近似值。因此,若判断一个浮点数是否等于某数,我们通常不使用==(判等)符号,而是使用判断这个数是否在某个极小区间内的方法。
例如:判断某浮点数f是否等于0
if(f==0)//错误 if(f<0.000001 && f>-0.000001)//正确
1)float类型: 单精度浮点型
大小:32位4字节
存储格式:符号位(1位)+尾数(23位)+指数(8位)
精度:小数部分最多有效7位,实际有效6位(2^23=8388608,所以是7位)
存储范围:-1.17E+38 ~ +3.40E+38(即-2^128 ~ 2^128)
打印格式:%f(十进制计数法),%e(指数计数法)
2)double类型: 双精度浮点型
大小:64位8字节
存储格式:符号位(1位)+尾数52(位)+指数(11位)
精度:小数部分最多有效16位,实际有效15位(2^52=4503599627370496,所以是16位)
存储范围:-2.22E+308 ~ +1.79E+308(即-2^1023 ~ 2^1023)
打印格式:同float类型
注意:double类型是8字节而float类型是4字节,因此double型比float型的数据更加精确,但计算速度会更慢。
思考:若有一个浮点数(float类型或double类型)以%d的方式打印,会出现什么结果? 示例:
|
|
答案:由上文我们可以发现,无论float类型还是double类型,打印格式都是%f(或%e),二者的打印格式是一样的。由此我们可以得出结论,无论float类型还是double类型,printf()函数是一视同仁的。
而float类型是4字节,double类型是8字节。因此为了方便,编译器将printf()函数需要处理的数据都转换成double类型(即float类型数据会转换成double类型,double类型不变)。
而在实际输出过程中,printf()函数是根据数据所占内存大小来读取数据的,即打印int类型的时候取4字节。
2.5从float类型转换成double类型后,低32位全部是0。而在打印的时候,printf()函数只取32位(由最低位向前)打印,会取得32个0。因此会打印出整数0。
而2.3因为无法完全转换成二进制数,因此在从float类型转换成double类型后,低32位仍有数据。在打印的时候,printf()取32位后会取得不全是0的一个数,因此会打印出一个很大的数。
3)long double类型: 大小:96位12字节(取决于编译器,有8字节、10字节、12字节、16字节几种情况)
存储格式:未知
精度:不少于double类型
存储范围:不少于double类型
打印格式:%lf,%le
5.5总结
5.5.1变量类型与大小:
int型:4字节
long类型:4字节
short类型:2字节
char类型:1字节
float类型:4字节
double类型:8字节
5.5.2输出变量:
%d:十进制输出
%o:八进制输出
%x或%X:十六进制输出
%c:字符输出
%f:浮点数十进制形式输出
%e:浮点数指数计数形式输出
5.5.3转义字符:
\n:换行
\t:tab制表符
\:输出\
5.5.4宏定义常量与常变量:
#define宏定义常量
const int int型常变量
6.类型转换
6.1隐式类型转换(自动转换)
在有多种数据类型混合计算的时候,若未人为控制类型,系统会自动进行类型转换
转换的规则是:存储长度较短的转换成存储长度较长的,且不丢失数据
char—>short—>int—>unsigned int(—>long—>unsigned long)—>double
↑
float——–
练习:思考以下程序的输出结果
|
|
答案:输出yes
思考:对于a+b来说,计算结果如果使用%d格式进行打印,则会输出-5;若以%u形式打印,则会输出一个很大的正整数。思考这是为什么。
6.2赋值转换
如果赋值运算符两侧数据类型不一致,则在赋值时会发生赋值类型转换
1)int型与float型
将float类型赋值给int型时,会舍去小数部分。如:
int a = 3.5;
//此时a的值是3
将int类型赋值给float型时,数值会以float类型存储。如:
float f = 4;
//此时f的值是4.0
2)float型与double型
将double型赋值给float型时,截取7位有效数字存储在float类型中。但要注意不要超过float类型的存储范围(因为double类型存储范围比float类型大)
3)char型与int型
将int类型赋值给char类型时,直接截取低8位存储在char型中。
将char类型赋值给int类型时,分两种情况:
⑴无符号char类型(即unsigned char型),数据存储在int类型低8位,剩下24位补0
⑵有符号char类型,数据存储在int型低8位。若char首位是0,则int型剩下24位补0;若char首位是1,则int型剩下24位补1。
4)int型与long类型(只考虑int型与long类型长度不一致情况)
将long类型赋值给int类型时,直接截断数据,将低位原封不动存储在int型中。
将int类型赋值给long类型时,分两种情况:
⑴无符号int类型(即unsigned int型),数据存储在long类型低位,剩下位补0
⑵有符号int类型,数据存储在long类型低位。若int首位是0,则剩下位补0;若int首位是1,则剩下位补1。
6.3强制类型转换
隐式类型转换或赋值转换有时不会得到我们预期的结果,这时我们可以使用强制类型转换来实现我们想要的类型转换结果。
强制类型转换:人为地将某类型转换为另一类型
强制类型转换符:()括号
用法: (待转换的类型)变量
例如:
float a = 3.5;
(int)a;//将a强制转换成int类型
float pi = 3.1415926;
int x = (int)pi;
在这里x会变成3,而pi还是原数值(3.1415926)。即经过赋值运算后,pi的类型又变回浮点数,pi的值不会变。
练习:从键盘输入5个学生的成绩(整数),要求输出总成绩(整数)和平均成绩(小数)
答案:
|
|
循环输入
|
|
数组存储输入数据
|
|
7.运算符与表达式
7.1表达式
用运算符和括号将操作数连接起来的符合C语言语法规则的句子称为表达式。
如2*y+5中,*和+是运算符,2,y,5是操作数,整个句子符合C语言语法规则。
7.2运算符
运算符用于执行程序代码运算,对一个以上的操作数进行运算。
在C语言中,运算符需要至少1个操作数(不超过3个)
在C语言中,把除了控制语句(如if,for等)和输入输出语句以外的几乎所有的基本操作都当做运算符处理。
7.3运算符的分类
1)根据结合数的数目分类:
单目运算符:即操作数只有1个的运算符
双目运算符:即操作数有2个的运算符
三目运算符:即操作数有3个的运算符,只有一个( ?: )
2)根据运算符的用途分类:
赋值运算符:如=及其扩展运算符
逻辑运算符:如&&(与)、||(或)、!(非)
算数运算符:如+ - * / %等
关系运算符:如> < >= <= == !=等
位运算符:如« »等
条件运算符:只有一个 ?:
逗号运算符:只有一个 , 用于分隔
7.4运算符的结合顺序
运算符有两种结合顺序:自左至右、自右至左
自左至右(左结合性):表示运算符的操作数是从左向右(先左再右)与运算符结合的,如加法+
例:3 + 5,即3加上5,先取3,再取+,再取5
自右至左(右结合性):表示运算符的操作数是从右向左(先右再左)与运算符结合的,如赋值=
例:a = 3,即先取数3,再给a赋值
7.5运算符的优先级
C语言中运算符有优先级,从高到低分为15级(不同书籍划分方式不同,级数不同,这里采用百度百科划分方式),1级优先级最高,15级最低。
混合运算时,先算优先级高的,再算优先级低的(类比数学中,先算乘除,再算加减),同等级运算符则从左向右依次运算
7.6算数运算符
符号及含义:+(加) -(减)*(乘) /(除) %(取余)
数目:双目运算符
结合性:自左至右
优先级:+和-:4级。*和/和%:3级
使用方法:表达式 运算符 表达式。如:3+5 a-b等
注意: ⒈C语言中,若相除2个数都是整数,则计算/时只取商,余数部分被舍去。
⒉%的操作数必须是整数
练习1:输入以下程序,查看输出结果
|
|
练习2:在练习1基础上,将变量类型改成float类型,查看输出结果
|
|
练习3:读以下程序,猜想输出的结果,再执行程序检验自己的猜想
|
|
7.7正运算符与负运算符
符号及含义:+(正数) -(负数)
数目:单目运算符
结合性:自右至左
优先级:2级
使用方法:+表达式 或 -表达式。如:
int a = 3;
printf("%d\n",-a);//对a取负数,会输出-3
7.8关系运算符
符号及含义:>(大于) <(小于) >=(大于等于) <=(小于等于) ==(判等) !=(不等)
数目:双目运算符
结合性:自左至右
优先级:> < >= <=:6级。==和!=:7级
使用方法:表达式 运算符 表达式。如3<5,a==7等
注意:
⒈关系运算符只会返回0或1两个值。其中返回0代表假,返回1代表真。
练习1:输入以下程序,查看输出结果并思考为什么
|
|
⒉在C语言中,所有非0的数代表真,0代表假。 练习2:输入以下程序,查看输出结果
|
|
练习题2,对于熟练使用C语言的同学建议使用?:表达式
注意判断两个表达式相等的运算符是==,与=(赋值运算符)要分开
7.9自增运算符与自减运算符
符号及含义:++(自增) –(自减)
数目:单目运算符 结合性:自左至右 或 自右至
决于运算符位置)
优先级:2级
使用方法:
自增;表达式++ 或 ++表达式。如a++ 或 ++a
自减:表达式– 或 –表达式。如b– 或 –b
注意:
⒈自增自减运算符表示将表达式+1或-1。如:
int a = 3;
a++;
a会变成4。
⒉注意++/–运算符与+/-(正数/负数运算符)是同等优先级。当这两个运算符一起使用时,是右结合性。即-a++ <—等价于—> -(a++);
⒊++在前与++在后的区别(–同理):
++在后(如a++):先使用数,再加1
++在前(如++a):先加1,再使用数
若单独使用自增运算符时,a++ <—等价于—> ++a <—等价于—> a=a+1 <—等价于—> a += 1(见下)
练习1:思考以下问题,之后编写程序观察输出结果
若有int a = 3,b;
⒈b = a++ 与 b = ++a,b的值相同么?
⒉b = -a++的结果是什么?
答案:
⒈不同。第一个b=3,第二个b=4。
⒉b=-3。
练习2:若有int i=10;思考
⒈i+++i+++i的结果是多少?
⒉运算后i的值是多少?
答案:⒈30或33(视编译器而定) ⒉12
练习3:读以下程序,猜想输出的结果,再执行程序检验自己的猜想
|
|
答案:输出 i=9,j=9,k=11 m=10,n=11,p=111
7.10逻辑运算符
符号及含义:&&(逻辑与) ||(逻辑或) !(逻辑非)
数目:!为单目运算符,&&和||为双目运算符
结合性:!为自右至左,&&和||为自左至右
优先级:!:2级。&&:11级。||:12级
使用方法:
⒈&&和||:表达式 运算符 表达式。如:a&&b a||b
⒉!:!表达式。如!a
注意:
逻辑运算符用于计算表达式的逻辑关系
⒈逻辑与:当且仅当两个表达式都为真时,则计算结果为真。否则为假
⒉逻辑或:当且仅当两个表达式都为假时,则计算结果为假。否则为真
⒊逻辑非:当原表达式为真时,计算结果为假;当原表达式为假时,计算结果为真
⒋逻辑运算符的返回结果同关系运算符,计算结果为真返回1,计算结果为假返回0
练习1:若要使得内容8的练习题1能够正确输出,则需要怎样改变程序?
答案:
|
|
练习2:将内容8的练习题2的所有if(a)改写成if(!a),执行程序查看结果
逻辑运算符的短路现象
当多个逻辑运算符连续使用的时候,会出现逻辑运算符短路的情况
1、与(&&)运算符短路
表达式1 && 表达式2 && 表达式3
当表达式1是真时,才会去判断表达式2的真/假。否则,如果表达式1是假,则之后的都不会进行运算。
当表达式2是真时,才会去判断表达式3的真/假。否则,如果表达式2是假,则之后的都不会进行运算。
示例:
int a=0,b=1,c=2,d;
d = a++ && b– && ++c;
程序的执行结果是
a->1,b->1,c->2,d->0
分析:
⒈首先会计算a++,计算a++会首先取出a的值,此时a的值是0
⒉因为此时a的值是0,表达式1(a++)的位置是假,发生短路,后面的表达式2(b–)和表达式3(++c)都不会进行运算,此时返回假(0),赋值给d
⒊执行a++(a的值加1)
2、或(||)运算符短路
表达式1 || 表达式2 || 表达式3
当表达式1是真时,跳过判断表达式2和表达式3,直接返回真;当表达式1是假时,才去判断表达式2的真/假
当表达式2是真时,跳过判断表达式3,直接返回真;当表达式2是假时,才去判断表达式3的真/假
示例:
int a=0,b=1,c=2,d;
d = a++ || b– || ++c;
程序的执行结果是
a->1 b->0 c->2 d->1
分析:
⒈首先会计算a++,计算a++会首先取出a的值,此时a的值是0
⒉因为此时a的值是0,表达式1(a++)的位置是假,需要判断表达式2(b–)的值
⒊计算b–,首先取出b的值,此时b的值是1
⒋因为此时b的值是1,表达式2(b–)的位置是真,发生短路,无需判断表达式3(++c)的真/假,此时返回真(1),赋值给d
⒌执行a++(a的值加1)和b–(b的值减1)
思考1:
如若将以上2个示例改写成:
d = a++ || b– && ++c;
程序的执行结果是什么?先猜想结果,再上机验证
答案:
a->1 b->0 c->3 d->1
思考2:
如若将以上2个示例改写成:
int a=1,b=2,c=3,d;
d = a++ || b– && ++c;
程序的执行结果是什么?先猜想结果,再上机验证
答案:
a->2 b->2 c->3 d->1
注意:这里要特别注意&&的运算优先级比||高。运算优先级高并不代表会先运算,而是代表先与操作数结合。而整个算式还是按照自左至右顺序计算。
在示例2中,因为&&的优先级比||高,因此算式等价于:
d = a++ || (b– && ++c);
此时进行自左至右的运算,当遇到a为真(1)的时候,整个算式发生了短路,括号内的b–和++c都不会执行
在这里要特别注意运算优先级高不代表会先运算,而是代表先与操作数结合
练习3:设int a=3,b=4,c=5,d;
分别求出下列表达式的d的值(即判断此表达式真假)
1)d = a+b>c && b==c
2)d = a || b+c && b-c
3)d = !(a>b) && !c || 1
4)d = !(x=a)&&(y=b)&&0(x和y均为int型,求出x和y的值)
5)d = !(a+b)+c-1 && b+c/2.
答案:
1)d=0
2)d=1
3)d=1
4)d=0,x=3,y未知
5)d=1
7.11赋值运算符及复合赋值运算符
符号及含义:=(赋值) +=(加后赋值) -=(减后赋值) *=(乘后赋值) /=(除后赋值) %=(取余后赋值) «=(左移后赋值)
>>=(右移后赋值) &=(按位与后赋值) ^=(按位异或后赋值) |=(按位或后赋值)
数目:未知(取决于是否存在多重赋值)
结合性:自右至左
优先级:14级
使用方法:
一次赋值:待赋值 运算符 表达式。如a=3,a=5+4,a+=5等
多重赋值:将同一个值赋给多个变量的操作,如a=b=c=10。此时a,b,c都为10
复合赋值:先将左值取出,之后与右值进行运算(运算取决于运算符),将运算后的结果再赋值给左值。如:a += 3 —–> a = a + 3
练习:思考以下问题,之后编写程序观察输出结果
⒈int a = 2,b = 3,c = 4;
a *= b+c;
问:a的值是多少?
⒉int a = 3;
a += a *= a;
问:最后a的值是多少?
答案:⒈a的值是14。a = b+c —–> a = a(b+c);
⒉a的值是18。a += a = a —> 先算a = aa(此时a是9),再算a = a+a(此时a是18)
7.12条件运算符
符号及含义:?:(判断?前表达式是否成立,若成立取:前表达式,否则取:后表达式)
数目:三目运算符(C语言中唯一的三目运算符)
结合性:自右至左
优先级:13级
使用方法:表达式1?表达式2:表达式3。先判断表达式1是否成立,若成立取表达式2,否则取表达式3。
条件运算符相当于小型的if-else表达式
练习1:读以下程序,先说明程序的功能,再输入程序并执行验证自己的猜想
|
|
练习2:读以下程序,猜想输出的结果,再执行程序检验自己的猜想
|
|
7.13长度运算符
符号及含义:sizeof()(计算括号内数据类型或表达式的大小)
数目:无
结合性:无
优先级:2级
使用方法:sizeof(表达式) 或 sizeof(类型名)
sizeof()用于计算某类型或某表达式所占内存的大小,经常与结构体/数组等连同使用
7.14位运算符
注意:位运算符的操作数只能是整数或字符型数。当操作数进行位运算的时候,操作数转换为2进制数。
1)按位运算符
符号及含义:&(按位与) ^(按位异或) |(按位或) ~(按位取反)
数目:&和^和|:双目运算符。~:单目运算符。
结合性:&和^和|:自左至右。~:自右至左
优先级:
~:2级
&:8级
^:9级
|:10级
使用方法:按位运算会先将操作数转换成2进制数,对每一个位(bit)进行运算
按位与&:1&1=1 1&0=0 0&1=0 0&0=0(当且仅当全是1时值才为1,否则为0)
按位异或^:1^1=0 1^0=1 0^1=1 0^0=0(当且仅当两个数不相同时值才为1,否则为0)
按位或|:1|1=1 1|0=1 0|1=1 0|0=0(当且仅当全是0时值才为0,否则为1)
按位取反~:1–>0 0–>1(若是1则变成0,若是0则变成1)
注意:要将&(按位与)和&&(逻辑与)、|(按位或)和||(逻辑或)区分开
练习1:读以下程序,思考程序逻辑,猜想程序的功能,之后上机检验
|
|
答案:三步异或法可以实现交换2个整数
(交换2个整数的3种方法:申请中间变量法;三步异或法;加和分减法)
练习2:思考如何使用按位操作使一个数清零
答案:将这个数&0即可 或 将这个数^自身
练习3:在不使用%2的情况下,如何判断一个整数是奇数还是偶数?
答案:将这个整数与1做按位与计算,若结果是1则是奇数,若结果是0则是偶数
练习4:思考(6&6)>4 和 6&6>4的结果一样吗?为什么?
答案:(6&6)>4的结果是1,6&6>4的结果是0。因为>的优先级高于&,若不加括号则会先计算6>4(结果是1),然后计算6&1(结果是0)
练习5:如何将一个数的所有位进行翻转?如0x0f—>0xf0(1变0,0变1)
答案:将这个数与所有位全是1的数做异或运算
0x0f ^ 0xff —> 0xf0
在进行位运算时一定要注意运算符优先级。对于不确定优先级的,我们要学会使用()来分隔运算符和操作数
2)移位运算符
符号及含义:«(左移) »(右移)
数目:双目运算符
结合性:自左至右
优先级:5级
使用方法:表达式1 左移/右移 表达式
2。如a«3 左移/右移运算符是先将操作数转换成2进制数,然后将这个数向左/右位移x(表达式2的值)位
左移的高位会溢出,直接舍去。低位会补0
右移的低位会溢出,直接舍去。高位补0还是补1见以下规则:
⒈对于无符号数,高位补0
⒉对于有符号数,若为非负数(即最高位(符号位)是0),则高位也是补0。若为负数,在高位补0还是补1取决于系统。补0的称之为“逻
辑右移”,补1的称之为“算数右移”
对于整数,左移x位相当于乘2的x次方,右移x位相当于除2的x次方
练习1:输入以下程序,查看输出结果
|
|
练习2:使用位运算符计算,输入整数k和num,计算以下内容:
⒈取出num的第k位的数
⒉将num的第k位置0
位运算符的操作是非常快的(几乎可以忽略它的耗时),因此从程序优化角度来考虑,使用位运算符来代替一些耗时操作(如/,%等)是十分必要的。
练习3:在不使用%的情况下,怎样计算num%8的值?(num为整数)
答案:num &= 7即可
15、逗号运算符
符号及含义:,(分隔作用)
数目:未知
结合性:自左至右
优先级:15级(最低)
使用方法:表达式,表达式……
逗号运算符是运算符里优先级最低的运算符,仅仅起到分隔的作用。
8.预处理命令
预处理命令是C语言编译系统的一个组成部分,是在编译前由预处理程序对源文件的预处理文件进行加工
预处理是在C语言编译的4个阶段(预处理、编译、汇编、链接)的第一个阶段。C语言的预处理功能有以下3种:
1、宏定义
2、文件包含
3、条件编译
分别用宏定义命令、文件包含命令、条件编译命令实现。为了与其他的C语句区分,预处理命令以符号#开头
8.1宏定义
8.1.1不带参数宏定义
用法:
#define 宏名 宏值
说明:
在之前我们已经使用过不带参数的宏定义,如:
#define PI 3.141592
它的作用是使用标识符PI来代替数字3.141592。在预处理阶段,将该程序中所有出现的PI(宏名)全部替换成3.141592(宏值)。
将宏名替换成宏值的过程我们称之为“宏展开”
注意:
⒈宏名一般使用大写字母表示以便与变量名区别
⒉使用宏名来代替宏值可以减少一个程序中重复书写某些值的操作的工作量,同时也避免了写错的可能性。同时使用宏名来代替宏值能够将宏值赋予一定意义,增强代码的可读性
⒊宏定义是简单的替换,不做语法正确性检查。语法正确性检查是下个阶段(编译)所做的工作
⒋宏定义不是C语句,因此不要在末尾加分号;
⒌通常#define写在文件开头,函数之前,这样宏的使用范围是全文件有效。若要指定宏的有效范围,则可使用#undef命令来终止宏的作用域。
|
|
⒍在进行宏定义时,可以引用已定义的宏名进行层层置换。如:
#define R 3
#define PI 3.141592
#define CIRCLE 2PIR
#define AREA PIRR
8.2.2带参数宏定义
带参数的宏定义不是简单的用字符串去代替标识符,还要进行参数替换。
用法:
#define 宏名(参数列表) 字符串
说明:
带参数的宏定义是这样展开置换的:如果要替换的字符串中有宏名中的参数,则将相应的参数代替宏名的参数列表中的参数,如果宏定义中的字符串不是参数字符(如ab中的号)则保留
示例:
|
|
注意:
⒈对带参数的宏的展开仅仅是进行简单的字符串替换。如果有多个参数,推荐在每个参数都加上()以防止出现错误结果
示例:
|
|
⒉在宏定义的时候,在宏名与参数列表之间不要加空格,否则空格后的字符都会作为替代字符串的一部分。如:
#define S (r) PIrr//注意S与(r)之间有空格
则在
area = S(a);
会被展开成:
area = (r) PIrr(a);
这显然是不对的。
8.2文件包含
文件包含是指将其他文件的内容全部包括进来,即将另外的文件内容包含到本文件中。
用法:
#include <文件名>
或
#include “文件名”
使用<>和"“的区别:使用尖括号时,系统到存放C库函数的头文件的目录中去找要包含的文件,这称为标准方式。使用引号时,系统优先在用户当前目录下查找要包含的文件,若找不到,再按标准方式查找。
一般来说,包括系统库函数使用<>以节省时间,包括用户自定义的文件使用”"
注意:
⒈一个#include命令只能包含一个文件,如若要包含多个文件需使用多个#include命令
⒉被包含文件与其所在文件在预处理后已变成同一个文件。因此被包含文件中的全局静态变量在所在文件中也同样有效,不必再次使用extern声明
3、条件编译 一般情况下,源程序中所有的文件都要参加编译。但有时只需要程序的一部分代码进行编译,这时就要指定编译条件,这就是条件编译 条件编译有以下几种形式:
用法1:
#ifdef 标识符
程序段1
#else
程序段2
#endif
说明:当定义过标识符,则编译程序段1,否则编译程序段2。#else部分可以省略,即:
#ifdef
程序段1
#endif
用法2:
#ifndef 标识符
程序段1
#else
程序段2
#endif
说明:与上一种类似,只不过将ifdef换成ifndef。当未定义过标识符,则编译程序段1,否则编译程序段2。#else部分可以省略
用法3:
#if 表达式
程序段1
#else
程序段2
#endif
说明:当指定的表达式是真(非零)的时候,编译程序段1,否则编译程序段2。#else部分可以省略
9.标准输入输出
所谓的输入输出是针对计算机为主体而言的。计算机向外部设备(显示器、打印机等)输出数据称为输出,输入设备(如鼠标键盘等)向计算机输入数据称为输入。
C语言本身不提供输入输出语句,输入输出操作是C函数库中的函数来实现的。诸如printf、scanf等不是C语言的关键字,而是函数名
C语言函数库提供一批“标准输入输出函数”,它是以标准输入输出设备作为输入输出对象。其中有:putchar()、getchar()、printf()、
scanf()、puts()、gets()。在这里主要讲解前4个函数,后2个函数在讲解字符串之后再深入讲解。
在使用C库函数的时候,需要先将有关头文件包含进来,标准输入输出的头文件:
#include<stdio.h>
stdio是standard input&output的缩写,.h是表示这是个头文件(head)
格式化输入输出:是指在用户控制下的各种按照预指定格式输入/输出数据。其中的格式控制符指定了输入/输出设备的数量和格式
格式化输入输出有printf()和scanf()两个函数
printf()与scanf()的格式控制
9.1printf()
函数原型:
int printf(“格式控制”,参数列表……);
返回值:正确:输出的字符数量;错误:返回EOF(end of file)
说明:
1)格式控制:是用"“引起来的字符串,包括
①格式说明:由”%“和格式字符组成的(如%d、%f等)。它的作用是把输出的数据按指定的格式输出。格式说明总是由%开头的
②普通字符:需要原样输出的字符
2)参数列表:一个参数需要一个格式说明,参数列表与格式说明需要在数量、类型、顺序上相匹配
3)printf()用于在标准输出设备上显示数据
4)printf()的宽度修饰符:
宽度修饰符用于为输出的数据(整型、浮点型、字符型、字符串等)设置输出的宽度。如果输出数据不足宽度则在左(或右)补齐(空格或0等),如果输出数据超过宽度则原样输出。
用法:整型:printf("%md”,整型变量);
①m是正整数,则输出宽度是m,数据靠右,不足的在左侧补空格
②m是负整数,则输出宽度是m,数据靠左,不足的在右侧补空格
若要空白的部分不补空格而是补0的话可以:printf("%0md",整型变量); 浮点型:printf("%m.nf",浮点型变量);
与整型类似,指定整数部分与小数部分的宽度。区别是n的值不能指定负数 5)其他修饰符:
⒈"l":可借助此修饰符将整型或浮点型输出成长整型或双精度浮点型。用法:printf("%ld",a);printf("%lf",f);
⒉"h":可借助此修饰符将整型输出成短整型。用法:printf("%hd",a);
⒊"":如果用户不希望预先指定输出宽度,而是通过程序指定,则可以使用修饰符。用法:printf("[%d]",5,123);在这里的位置会变成5
练习:输入以下程序,查看输出结果
|
|
9.2scanf()
函数原型:
int scanf(“格式控制”,参数列表……);
返回值:正确:输入成功的参数数量;错误:0
说明:
1)格式控制:与printf()基本相同,由"%“和格式字符组成,作用是指定输入的数据的格式和类型
2)参数列表:scanf()使用地址值作为接收输入数据。因此当接收数据的变量为基本数据类型时,要加&(&:取地址运算符,取出这个变量所在的内存的地址。不是按位与)。如果是数组、指针等,直接写变量名即可不要加&
3)scanf()使用非打印字符来判断输入数据的开始和结束,因此它可以忽略空白区域和行界限来获取数据。如scanf("%d%d%d”,&a,&b,&c);
以下3中输入方式都是合法的:
①3空格4空格5(多个空格也可)
②3tab4tab5
③3回车4回车5
4)如果在格式控制中指定了数据分界,则需要按规定输入分界才算输入有效数据。如:
scanf("%d,%d,%d",&a,&b,&c);
则输入3,4,5才是合法的,输入3空格4空格5是非法的。同样还有:
scanf(“a=%d,b=%d,c=%d”,&a,&b,&c);
则输入a=3,b=4,c=5才是合法的
缓冲输入/输出:
缓冲区:在程序与和输入/输出设备间建立起来的用于缓冲的空间,是一块临时建立起来的存储区域,可以在内存中或在设备控制卡上 缓冲区的作用:暂时存放输入/输出数据,等待CPU(或其他的运算设备)来处理数据。
设立缓冲区的作用是为了解决高速设备和低速设备读写速率不吻合的问题。
输入设备—>输入缓冲区stdin—>程序
程序—>输出缓冲区stdout—>输出设备
字符数据输入输出:
9.3putchar()
函数原型:
int putchar(int c);//c的范围:0~127
返回值:正确:输出的字符的unsigned char值;错误:返回EOF
说明:
putchar()是从标准输出流stdout中输出一个字符。其中参数c可以是0~127之间的整数、字符变量、转义字符等。以下的用法都是合法的:
putchar(‘A’);
putchar(65);
putchar('\n');
9.4getchar()
函数原型:
int getchar(void);
返回值:正确:输入字符的ASCII码;错误:返回-1
说明:
getchar()是从标准输入流stdin中输入一个字符。当程序运行到getchar()时,程序会阻塞等待用户输入。用户的输入的字符会先暂存在stdin中直至输入回车为止(回车也放入缓冲区中)。之后getchar()会读出缓冲区的第一个字符并将该字符回显至屏幕。
用户可以在回车前输入多个字符,但是每个getchar()函数只读取第一个字符,剩余的字符会继续存储在缓冲区中,等待后续的getchar()调用。
练习:
|
|
运行程序,输入x回车y回车,会发现输出的并不是xy,而是
x
思考这是为什么。
答案:第一次输入的回车还存在缓冲区中,第二个putchar()会读取这个回车而不会读取y
从输入流中获取字符串:
fgets()
函数原型:char* fgets(char *s,int n,FILE stream);
参数列表:
char *s:将读取出的数据存
入的字符串的地址
int n:要读取的长度
FILE stream:文件结构体指针,需要读取的文件流
返回值:当读到第n-1个字符或读到换行符的时候,fgets()停止操作,在s末尾添加'\0'。执行成功返回字符串的首地址s,执行失败返回EOF
有关fgets()函数的用法将在接下来的课程(字符串、文件IO操作)中详细讲解,这里只简单理解即可。
10.一维数组
我们之前所学过的数据类型都属于基本数据类型(整型、浮点型、字符型)。
实际上C语言不仅仅可以操作基本数据类型,还可以操作构造数据类型(如结构体、数组等)。
数组是有序数据的结合,数组中的每个元素都属于同一种数据类型。
每个数组都有一个数组名,数组名代表了数组的起始地址。
数组中的元素由下标(或索引)标识,下标是从0开始到n-1(数组长度为n)的整数。
示例:一个拥有10个元素的int型数组a
int a[10];
其元素是a[0]、a[1]、a[2]、a[3]、a[4]、a[5]、a[6]、a[7]、a[8]、a[9]
数组同基本数据类型变量一样,必须要先定义再使用
10.1数组的定义:
数据类型 数组名[长度]
其中长度必须是整数必须大于0。以下的定义都是合法的
int a[10];
float f[10];
int b[2*3]; 获取数组元素:数组名[下标]。下标可以是变量、常量或常量表达式。
数组在定义后,就在内存中划分了一块空间用于存储数组。以int a[n]为例(n大于0):
内存中划分出一块空间用于存储数组,这块空间的大小是sizeof(int)*n,划分成n块来顺序存储a[0]~a[n-1]。数组名代表这块空间的首地址(也就是a[0]的地址)
计算数组所占内存空间的大小:sizeof(a);//计算数组a占内存空间大小
思考:已知一个数组(如int a[n]),在只知道数组名,不知道数组元素个数的情况下,如何算出数组中元素个数(即求出n的值)?
答案:sizeof(a)/sizeof(a[0]);
10.2数组的初始化:
1)数组初始化的一般形式:
类型 数组名[数组长度]={值1,值2……};
示例:int a[10]={0,1,2,3,4,5,6,7,8,9};
将花括号内的值一一对应赋值到数组各个元素中
2)如果只给数组一部分元素赋值,则剩下的会变成0
示例:int a[10]={1,2,3};
则a[3]及以后的值都是0
如果想使一个int型数组中元素全部为0可以这样写:
int a[10]={0};
10.3对数组内全部元素赋值时,由于元素个数已经确定,因此可以不指定数组长度。
示例:int a[5]={1,2,3,4,5};<—等价—>int a[]={1,2,3,4,5};
如果采取第二种写法,系统就会根据后面花括号内的个数来分配数组长度是5。但若数组长度与提供初值个数不同,则数组长度不可省略。
示例:定义一个数组,反序输出
|
|
练习1:使用数组存储斐波那契数列前30项,并输出
答案:
|
|
练习2:从键盘输入10个学生的成绩,如果遇到大于100或者小于0的成绩需要提示输入错误重新输入。之后计算10个学生的总成绩和平均成绩
答案:
|
|
练习3(选做):使用数组法解决约瑟夫环问题
约瑟夫入狱,监狱内共有33个犯人。某日33名犯人围成一圈,从第一个犯人开始报数,报到数字7的犯人出列,被枪毙,下一名犯人重新从1开始报数。依次类推,直至剩下最后1名犯人可被赦免。聪明的约瑟夫在心里稍加计算,算出了最后枪毙的位置,他站在这个位置,最终避免了自己被枪毙,逃出升天。
问:约瑟夫算出的是哪个位置?
提示:对于约瑟夫环问题来说,需要解决4个问题
⒈需要一个长度为33的数组,数组内需要存储什么?
⒉如何解决数组循环的问题?
⒊如何解决“逢7一杀”这个逻辑?
⒋如何处理“已死之人”?
答案:
|
|
练习4(选做):从键盘输入10个各不相同的整数,存储在数组中,使用冒泡排序法将数组排序并输出
冒泡排序:是一种简单的排序算法
1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
3)针对所有的元素重复以上的步骤,除了最后一个。
4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
答案:
|
|
11.二维数组
C语言中不仅可以存储一维数组,还可以存储二维数组。二维数组类似矩阵,有行/列等
11.1二维数组的定义:数据类型 数组名[常量表达式1][常量表达式2];
示例:
int a[2][3];//定义一个2*3的二维int型数组
float f[3][4];//定义一个3*4的二维float型数组
11.2二维数组的存储方式:
C语言对二维数组的存储方式是:将二维数组视为一种特殊的一维数组,它的元素又是一个一维数组。例如,对于二维数组:
int a[3][4];
它的存储方式是:
|
|
在内存中的存储顺序是:
|
|
获取数组元素:类似一维数组:数组名[下标1][下标2]
11.3二维数组的初始化
1)分行给二维数组赋值。例如:
int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
每行的元素使用花括号分隔开,中间用逗号分隔开
2)把所有的元素都写在一个花括号内,这样会按照数组在内存中的存储顺序给二维数组赋值。例如:
int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
类似于一维数组,如果有未写的则默认为0
3)可以对二维数组的部分元素赋值:例如:
int a[3][4]={{1},{5},{9}};
则赋值结果是a[0][0]=1,a[1][0]=5,a[2][0]=9
int a[3][4]={{1},{0,6},{0,0,11}};
则赋值结果是a[0][0]=1,a[1][1]=6,a[2][2]=11
这种方法对非0元素较少时比较方便。
4)可以提供全部元素的初值,这样常量表达式1(即第一个下标)可以缺省不写,系统会根据输入的多少来计算行数。但常量表达式2(即第二个下标)不可缺省。例如:
int a[][4]={1,2,3,4,5,6,7,8,9,10,11,12};
则系统自动计算出这个二维数组是a[3][4]
思考:如果int a[][4]={1,2,3,4,5,6,7,8,9,10};则该二维数组是多少?
答案:同样是a[3][4],只不过10以后的两个元素未初始化,变成默认值0
注意:C语言对于数组下标越界不予检查。因此对于数组的操作要十分注意其边界值。如果指定了数组长度而初始化元素个数多余指定的数组元素个数,如:
int a[2][3]={1,2,3,4,5,6,7,8,9};
则会只取前n个数据进行初始化(n是指定长度,如这里n=6),后面的剩余数据舍弃。编译期间编译器会报warning。
练习1:自定义一个3*4的矩阵,输出矩阵中值最大的元素,并输出其数组下标
答案:
|
|
练习2:打印杨辉三角型前10行
杨辉三角型:杨辉三角型是形如以下矩阵的三角形: 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 1 5 10 10 5 1
答案:
|
|
11.4使用数组的注意事项:
1)二维数组的第一个下标可以省略不写,编译器会根据我们初始化值的多少计算出数组的第一个下标。而第二个下标不可以省略不写。
例如:
int a[3][]={1,2,3,4,5};//非法
如果这样写,编译器在分配内存的过程中不知道需要分配多大的空间(因为第二个下标未定),因此会报错。
2)不能为数组整体赋值。如:
int a[10]=1;
int a[10];a={1,2,3,4,5,6,7,8,9,10};
以上2种都是错误的
12.字符数组
12.1字符数组的定义:
字符数组的定义方法与之前介绍的一维数组的类似。例如:
char c[10];
12.2字符数组的初始化:
字符数组的初始化与之前介绍的一维数组的类似。例如:
char c[]={‘I’,' ‘,‘a’,’m’,' ‘,‘a’,’ ‘,’s’,’t',‘u’,’d',‘e’,‘n’,’t'};
练习:输入以下代码,编译,运行,查看结果。
|
|
12.3字符串
如果我们一个字符一个字符键入的话,不仅操作不便,而且还容易出现错误。因此对于字符型数组,我们可以使用字符串来操作。
字符串:由双引号"“引起来的字符序列。
字符串中每个字符都视为一个字符型数组的元素,存储占1字节。
字符串的结束标志是'\0'
关于'\0' ‘\0’是转义字符,在ASCII表中的编码为0(NULL)。它不是一个可见的字符,而是一个空操作符,即什么都不做。用’\0’表示字符串结束标志不会产生其他的附加操作,只是提供一个标识。
思考:0、‘0’、'\0'、“0"相同么?有什么区别?
答案:
⒈0代表数字0
⒉'0’代表字符零,在ASCII表中的编码是48,是一个可见字符
⒊'\0’代表NULL,在ASCII表中的编码是0,是一个不可见字符、空操作符,作为字符串的结束标志。
⒋"0"代表字符串"零”,这是一个字符串,实际上等价于'0''\0'(字符0后面加字符串结束标志'\0')
使用字符串常量对字符数组进行初始化:
char c[]={“I am student”};
或者省略花括号直接写成:
char c[]=“I am student”;
使用这种方式为字符数组赋值,访问数组内元素的方法同一维数组
c[0]—>I
c[1]—>' ‘(空格)
如果使用字符串常量给字符数组赋值,则字符数组的最后一个元素不是’t’而是’\0'
char c[]=“China”;<—等价—>char c[]={‘C’,‘h’,‘i’,‘n’,‘a’,'\0'};//多出的'\0’是字符串结束标志
因此,字符串与字符数组有稍许差别,不完全等价。字符数组若不人为添加,末尾是不会出现'\0’的,而字符串末尾系统会自动添加'\0’表示字符串结束。
输出字符串不必使用for()循环遍历整个数组,直接使用%s格式控制符即可。
printf("%s”,c);
示例:
|
|
当然,我们也可以使用%s在scanf()函数中直接输入一个字符串:
scanf("%s",c);
不过,使用%s输入整个字符串需要注意以下2点:
⒈因为C语言不检查数组下标越界问题,因此如果直接键入字符串给字符数组,需要将字符数组设置的足够大,这样才不会丢失数据。
⒉输入字符串给字符数组,注意scanf()的第二个参数不要带&(取地址符)。因为数组名就代表了数组的地址。
示例:
|
|
13.字符串处理函数
C语言提供各种处理字符串的函数,可以简便地处理字符串。
字符串处理函数在头文件string.h中,使用时不要忘了包含头文件
13.1字符串输入输出函数:puts()和gets()
1)puts()函数
函数原型:
int puts(char *string); 使用方法:
puts(字符数组名 或 字符串指针);
说明:
puts()函数将一个字符串输出到标准输出流中,再输出到屏幕上。注意puts()函数只能输出字符串,不能进行其他操作
puts()函数等价于:printf("%s\n",string);
示例:
|
|
2)gets()函数
函数原型:
char* gets(char *string);
使用方法: gets(字符数组名);
说明:
gets()函数从标准输入流中获得一个字符串,直至接收换行符\n或EOF为止。将获得的字符串储存到字符数组内,读取出的换行符转换成'\0'
注意:
gets()函数不会检查待存储的字符数组的大小,若没有接收到换行符或EOF则会无限制读取下去。因此待存储的字符数组一定要足够大。
事实上gets()函数缺点很多,因此我们不推荐使用。推荐使用scanf()或fgets()函数。
gets()函数和puts()函数一次只能操作一个字符串,不能写成
gets(str1,str2);
gets()和scanf("%s")的区别:当键入空格时,scanf("%s")认为该字符串已经结束,空格后的是下一个字符串内容。而gets()函数则会当空格做本字符串的一部分。
13.2字符串连接函数:strcat()
函数原型:
char* strcat(char *s1,const char *s2);
使用方法:
strcat(字符数组1,字符数组2);
说明:
strcat是string catenate的缩写,作用是把后一个字符串(字符数组2)拼接到前一个字符串(字符数组1)之后,结果存放在字符数组1中。
示例:
|
|
注意:
1)字符数组1的空间需要足够大以能够存储得下两个字符串,否则数组长度不够,编译阶段会报warning,并且不会输出正确结果。
2)两个字符串拼接时,字符数组1之后的'\0’会取消,只在拼接后的新的字符数组之后添加'\0'
练习:自定义2个字符数组,不使用系统提供的strcat()函数,实现strcat()函数功能。
答案:
|
|
13.3字符串复制函数:strcpy()和strncpy()
1)strcpy()函数
函数原型:
char* strcpy(char *str1,const char *str2);
使用方法:
strcpy(字符数组1,字符数组2或字符串2);
说明:
strcpy()是string copy的缩写,表示“字符串复制函数”,将字符数组2或字符串2的内容复制给字符数组1。
示例:
|
|
注意:
1)字符数组之间不能互相赋值。以下这种方式是非法的:
char s1[];
char s2[]=“China”;
s1=s2;
因此如果给字符数组间赋值,必须使用strcpy()函数。
2)字符数组1的空间必须足够大以能够容纳字符数组2或字符串2的所有字符。这点与strcat()相同。
3)strcpy()的两个参数,第一个参数必须是字符数组名,第二个参数可以是字符数组名或直接是一个字符串常量。例如:
strcpy(s1,“China”);
4)如果字符数组2或字符串2比字符数组1短,则在复制操作后,字符数组1内没被复制所覆盖掉的内容将会保留
练习:自定义2个字符数组,不使用系统提供的strcpy()函数,实现strcpy()函数功能。
答案:
|
|
2)strncpy()函数
函数原型:
char* strncpy(char *s1,const char *s2,int n);//实际上第三个参数是size_t型
使用方法:
strncpy(字符数组1,字符数组2或字符串2,长度);
说明:
类似strcpy()函数,只不过是取出字符数组2或字符串2前n个字符复制到字符数组1中。
注意:n的值不能超过字符数组2或字符串2的总长度。
13.4字符串比较函数:strcmp()
函数原型:
int strcmp(const char *s1,const char *s2);
使用方法:
strcmp(字符数组1或字符串1,字符数组2或字符串2);
说明:
字符串比较的规则是:将字符串1和字符串2的第一个字符拿出比较ASCII码,如果字符串1大则返回一个正数,如果字符串2大则返回一个负数。如果相等则比较下一个字符。直至遇到不同字符或遇到字符串结尾'\0'。如果两个字符串相同,则返回0。
示例:
|
|
注意:
字符数组或字符串之间不能直接使用关系运算符来比较大小,以下的操作都是非法的:
str1<str2;str1==str2;str1>str2;
必须使用strcmp函数来比较字符串
练习:自定义2个字符数组,不使用系统提供的strcmp()函数,实现strcmp()函数功能
答案:
//注意:这里不是main()函数而是另一个自定义函数。有关函数的使用方法我们将在接下来的课程中学习
|
|
13.5测字符串长度函数:strlen()
函数原型:
int strlen(const char *s);//实际不是int类型而是size_t类型
使用方法:
strlen(字符数组 或 字符串);
说明:
strlen()函数是string length的缩写,其功能是测试字符串的长度(不包括'\0')
示例:
|
|
注意:
sizeof()与strlen()的区别:
sizeof()的长度是整个字符数组的总长度,其中包括'\0';而strlen()不包括'\0'。例如:
char s1[]=“Hello”;
则sizeof(s1)=6,而strlen(s1)=5。
练习:自定义1个字符数组,不使用系统提供的strlen()函数,实现strlen()函数功能
答案:
|
|
14.选择结构
14.1C语言程序有3种基本结构:顺序结构、选择结构和循环结构
1)顺序结构:所谓顺序结构,就是从上到下的所有语句都会依次执行
2)选择结构:选择结构中存在一条(或多条)分支语句,当满足分支的条件时语句才会执行,否则不会执行
3)循环结构:循环结构即是在某些条件的控制下重复执行一段代码语句,当满足循环条件时执行循环语句,否则不执行循环语句。
4)流程图:开始/结束使用圆角矩形,一般语句使用矩形,选择语句使用菱形,各形状使用有向箭头连接起来。
14.2选择结构if-else
if(表达式)-else是最常见的2分支判断选择结构,常见的if()-else有3种形式:
⒈
|
|
⒉
|
|
⒊
|
|
1)if(表达式) 用法:
|
|
说明:
当程序执行到if语句时,会判断if()内表达式的真假,若为真,则会执行语句块,否则跳过语句块
示例:
|
|
执行结果:输出一条"a大于0"
2)if(表达式)-else 用法:
|
|
说明:
当程序执行到if语句时,会判断if()内表达式的真假,若为真,则会执行if下语句块1;若为假,则会执行else下语句块2
示例:
|
|
3)多个if(表达式)-else
用法:
|
|
说明:当程序执行到if(表达式1)时,会判断表达式1的真/假。当表达式1为真时,执行语句块1;
否则,执行if(表达式2),会判断表达式2的真/假
依次进行每个if(表达式)的判断
如果以上if(表达式)都为假,则执行最后的else语句后的语句块3
实际上多个if()-else表达式就是使用多个2分分支判断实现了多重分支判断
示例:
程序输入成绩,为0~100之间的整数。之后输出成绩代表的分数段:
90~100为优,80~89为良,70~79为中,60~69为及格,0~59为不及格,其他则输出错误信息
|
|
4、如果if()或者else表达式下未使用{}限制,则只会执行if()或else下的第一条语句。
示例:
|
|
程序输出结果:
a是正数
a不是正数
a is 5
尽管在本例中,else下的两句表达式缩进相同(以代表两句都隶属于else语句块)但是由于else后没有{},因此实际上隶属于else的语句只有第一句(即a=-3,如果能执行的话),而printf()语句则会照常执行。因此我们建议使用{}将if()和else后的语句都括起来,以免出现逻辑错误
上面输入成绩输出分数段的示例等价于:
|
|
练习1:输入3个整数,从小到大输出
答案:
|
|
练习2:输入3个正整数作为3条线段的长度,判断这3条线段能否构成三角形。构成三角形的条件是:任意两边之和大于第三边
答案:
|
|
5、if()的嵌套使用
在if()-else语句中又包含一个或多个if()-else语句称为if()的嵌套使用。一般形式如下:
|
|
程序的执行逻辑:
|
|
示例:输入一个整数,判断这个整数是正整数、0还是负整数
1)使用多个if()-else
|
|
2)使用if()嵌套
|
|
练习:输入一个年份(正整数),判断这年是否是闰年
闰年判断标准:年份能被4整除;如若遇到100的倍数,则需判断年份能否被400整除。(逢4一闰,逢百不闰,逢400又闰)
如1900年不是闰年,1904年是闰年,2000年是闰年
答案:
|
|
6、if()和else的配对
(当没有{}限定的时候)当else与if配对的时候,else总是与它上面最近的未配对的if()结合
|
|
在这段程序中,虽然编程人员将else 语句2放在与第一个if()同一缩进上以表示该else与首个if()结合,但实际上这个else是与它的上一个if(即if()语句1)结合。因此在使用if()-else语句(尤其在嵌套使用时)一定要使用{}来限定
14.2switch()语句
if()-else语句只能判断2个分支,若要判断多个分支则需要if()-else的多次使用或嵌套使用,十分不便
switch()语句是多分支选择语句。通过switch()的多分支判断可以简便地实现多分支选择结构
switch()语句的一般形式如下:
switch(表达式)
|
|
说明: 1)switch(表达式)表达式的值应是一个整数(包括字符数据)
2)switch()下的{}是一段语句块,这段语句包含若干个以case开头的语句块和至多一个以default开头的语句块
3)case后需要一个常量(或常量表达式),case和default都是标号作用。首先判断switch(表达式)的表达式的值,之后与各个case之后的值进行比对,如果某个case后的值与表达式的值相同,则跳转到此case语句;如果所有的case都不匹配,则跳转到default后的语句
4)可以没有default语句。若没有default语句,则如果没有匹配的case,则程序不执行任何语句
5)各个case之间的先后顺序以及default的位置不影响程序执行结果,不过我们强烈推荐按一定顺序排列case语句,default语句放在最后
6)每个case语句后的常量值必须各不相同,否则会发生互相矛盾现象
7)break语句用来终止switch()语句。如果case语句后没有break语句,则当匹配到合适的case后,程序会一直执行接下来的语句直至遇到break或switch()结束
示例:使用switch()实现:输入一个正整数,输出对应的星期。如输入1代表星期一,输入2代表星期二……输入7代表星期日。输入其他数输出错误信息
|
|
在示例中,如果去掉所有的break语句,则:
|
|
程序执行结果:
输入4,输出:今天是星期四 今天是星期五 今天是星期六 今天是星期日 输入错误
练习1:将上文的“输入学生成绩,输出优、良、中、及格、不及格”问题使用switch()语句来实现
答案:
|
|
注意在case 10处,以及case 5~case 0处均没有break,则匹配到这些case后会顺序执行之后的语句直至出现break或switch()语句结束
练习2:运输公司对用户按路程计算费用。路程越远,每吨*千米运费折扣越高。计算运费公式如下:
freight = weight * distance * price * (1-discount);
路程与折扣的关系如下:
s<250:无折扣
250<=s<500:2%折扣
500<=s<1000:5%折扣
1000<=s<2000:8%折扣
2000<=s<3000:10%折扣
3000<=s:15%折扣
要求从键盘输入货运总重(单位吨)、货运路程(单位千米)、每吨*千米货运单价(单位元),输出总花费
答案:
|
|
15.循环语句
在生活中我们常常遇到需要重复处理的问题,我们在编程时解决需要重复处理的问题需要使用循环语句
循环语句主要有3种:while()循环;do-while()循环和for()循环
15.1while()循环
用法:
|
|
说明:
当程序遇到while()循环的时候,首先会判断while()的括号内的表达式,若为真(即满足循环条件)则执行循环,执行完循环体后再次返回到while();若为假则结束循环
示例:
输入10个学生的成绩,输出这10个学生的成绩总和以及平均分
|
|
思考:若在循环体中没有i++这一句将会发生什么结果?
答案:i++这一句的作用是改变循环的状态。若没有i++这句,则i的值会永远是1,则循环永远都不会结束,即程序会进入死循环
初学者在使用循环时一定要注意不要陷入死循环。当你的程序使用了循环结构,在编译阶段无语法错误,在执行时发现程序会无限期进行下去,则很可能你的程序进入了死循环。此时要检查循环条件设置是否正确,以及检查是否设置了改变循环状态的量。
若发生了死循环,按ctrl+c(或ctrl+z)组合键可强制终止当前程序
练习:求1+2+3+……+100=?
答案:
|
|
若while()后没有{}限制,则循环只执行到while()后第一个分号处(即只执行一条语句),这点与if()-else相同
15.2do-while()循环
用法:
|
|
说明:
当程序遇到do,会首先执行do下面的语句(即循环体),之后执行到while(),判断while()的括号内的表达式是真/假,若为真(即满足循环条件)则返回do语句处再次执行循环体;若为假则结束循环。
示例:将上文示例改写成do-while()循环
|
|
练习:将上文练习“1+2+3+……+100=?”改写成do-while()循环 答案:
|
|
while()与do-while()的区别:
while()循环是先判断循环条件,再进入循环体。而do-while()循环是先进入循环体,再判断循环条件。
while()循环有可能不执行循环体,而do-while()循环一定会执行一次循环体
15.3for()循环
用法:
|
|
注意:for()括号内的3个表达式的分隔符是分号;不是逗号,
说明:
表达式1:循环的初始条件,只执行一次。可以为0个、1个或多个变量设置初值
表达式2:判断循环结束的条件。在每次执行循环体前判断此表达式,若表达式为真则进入循环,否则不执行循环
表达式3:作为循环的调整(即改变循环状态),在执行完循环体之后执行
|
|
表达式1可以省略。若如此做,相当于没有给for()循环设定起始初值。因此,若要循环正常进行,需要在for()之前设置好循环起始初值
表达式2可以省略。若如此做,相当于for()循环没有结束条件,即死循环。等价于:
|
|
表达式3可以省略。若如此做,需要在循环体内设置循环调整语句,否则循环无法正常执行。
若将3个表达式都省略(即for(;;)),则相当于设置了一个无限循环
若for()后没有{}限制,则循环只执行到for()后第一个分号处(即只执行一条语句),这点与if()-else相同
示例:将上文示例改写成for()
答案:
|
|
练习:将上文练习“1+2+……+100”改写成for()循环
答案:
|
|
三种循环的比较:
1)如果使用while()或do-while(),需要注意在循环体内设置改变循环状态的变量。而for()循环则是在表达式3中设置
2)一般来说,for()循环的表达式3的位置甚至可以将循环体都放入,所以for()循环更常用
3)在知道循环次数的情况下,推荐使用for()循环;在不知循环次数的情况下推荐使用while()或do-while()循环
4)从C99版本开始,C语言支持以下用法:
|
|
这样就可以不用事先特地定义一个用于循环的变量,随时使用随时定义即可。
注意1:这种使用方式循环变量i的生命周期仅仅是在for()循环中,当for()循环结束i即结束其生命周期。如果这样使用:
|
|
printf("%d",i);//此时i的生命周期已经结束,企图访问不存在的变量i,非法 则编译会显示语法错误。
注意2:这种用法仅支持C99及以后C语言版本的编译器,如果这样使用出现语法错误则证明其编译器版本不支持这种语法,请不要使用这种语法而是在for()循环开始前就定义循环变量。
练习:打印斐波那契数列的前n项,其中n由键盘输入。斐波那契数列的特点如下:
F1=1 (n=1)
F2=1 (n=2)
Fn=Fn-1 + Fn-2 (n>=3)
答案:
|
|
15.4循环的嵌套使用
一个循环体内又包含一个循环体的结构称之为循环的嵌套。内嵌循环还可以再嵌套循环。3种循环都是可以互相嵌套使用的。例如下面的例子都是合法的:
1)
|
|
2)
|
|
3)
|
|
练习1:输出如下图形:
其中需要输出几行由键盘输入。
答案:
|
|
练习2:现有100元钱要买以下3种文具。其中一个笔记本20元,一套尺6元,一个笔4元,每样文具至少买1个。列举出所有的购买方案,
以及列举出所有正好花光100元的购买方案
答案:
|
|
15.5提前结束循环的语句:continue、break、return、exit()
1)continue
continue语句的用法是:
continue;
其作用是结束本次循环,即跳过所有continue下的语句,进入下次循环。
示例:输出100~200之间不能被3整除的整数
|
|
2)break
break语句的用法是:
break;
其作用是结束循环,执行循环体外的下一个语句
回顾:break在switch()语句中的作用
break在switch()语句中的作用是跳出switch()语句,执行switch()语句下一条语句。
示例:从r=1开始,输出所有半径是正整数的圆的面积,直至出现面积大于100为止
|
|
可以看到,尽管在示例程序中while()循环使用了死循环,当出现break语句后,依然会跳出循环。
3)return
return语句的用法是:
return 需要的返回值;
其中需要的返回值由函数类型决定。如main()函数是int类型,则需要返回一个整数。如果函数是void类型则无需写返回值。
return语句的作用是结束当前函数,并将返回值返回给函数调用的位置。
有关return语句的用法我们将在函数的课程中详细讲解。
需要注意的是return不仅仅是结束了循环,更是结束了当前的函数。因此要慎用。
4)exit
exit语句的用法是:
exit(x);//x为0或正整数
当x为0时表示正常结束,不为0时表示异常结束(异常号)
exit()在头文件 stdlib.h 中
exit()不仅仅是结束循环,它的真正意义是退出当前程序。因此非常不推荐使用exit()结束循环
从效果来看,4个语句的效果如下:
exit() > return > break > continue
强<—————————->弱
练习:输出100~200间的所有素数
答案:
|
|
6、goto语句
用法:
goto 语句标号;
说明:
goto语句是无条件跳转语句,当执行到goto语句时,程序会跳转到goto语句所指向的标号处。语句标号的命名规则与C语言标识符的命名规则相同。
示例:
|
|
滥用goto语句会使程序无规律、可读性差。goto语句违背了C语言的模块化编程的基本思想,因此goto语句不推荐使用。