C语言基础语法
1.程序语言的基本构成要素:
自然语言 | 程序设计语言 |
---|---|
字 | 数字,字母,运算符,分隔符 |
词/词组 | 关键字,标识符,常量 |
句子/段落 | 语句 |
篇章 | 程序 |
1.关键字:也称保留字(Reserved Word),是C语言预先定义的、具有特殊意义的单词
2.标识符:是大小写字母,数字和下划线构成的一个字符序列
(1)系统预定义标识符
(2) 用户自定义标识符:用来标识变量名、符号常量名、数组名、函数名等
命名规则:
a.首字符必须是字母或下划线
b.见名知意,不要使用汉语拼音
c.不能与关键字及系统预定义的标识符相同
2.C语言处理的数据形式:
2.1 常量:
(1)分类:
常量:在程序中不能改变其值的量
1.整型常量(默认为int):
类型:
int:78,23(默认)
long int:78L
unsigned int:23u
2.实型常量(默认为double):
(1)小数形式
double:3.14(默认)
float:3.14F
long double:3.14L
(2)指数形式
double:1.2e-3
float:1.2e-3F
long double:1.2e-3L
3.字符型常量:使用单引号,只能有一个数字或者字母或者其他字符
①普通的字符:'z','A','2'
②转义字符(无法从键盘输入的字符,有特殊的含义)
4.字符串:使用双引号,里面是任意个字符/字母
"UXS","33","2a"
5.枚举型
(2)为什么不能在程序中直接使用常数?
1.在程序中直接使用的常数,被称为幻数
2.直接使用幻数存在的问题:
①程序的可读性变差
②容易发生书写错误
③当常数需要被修改时,要修改使用它的代码,繁琐还可能有遗漏
#include <stdio.h>
main(){
printf("area=%f\n",3.14*4.4*4.4);
}
(3)怎么避免以上错误?
可以把幻数定义为宏常量或者const常量
优点:
①减少重复书写常数的工作量
②提高程序的可独享和可维护性
1.宏常量
1.定义宏常量:#define 标识符 字符串
说明:
①"#"指的是预编译处理命令,即在源程序编译之前,对程序中的编译预处理命令进行处理,然后该结果和源程序一起编译,以便得到目标代码.
怎样处理?
在预编译的时候,将程序中的宏名全部替换为字符串--宏替换.但是格式控制字符串中的宏名不会被替换
②标识符也叫做宏名,一般全大写
③它是预编译处理命令,而不是一条语句
④不区分后面的数据类型,无论输入什么,都把它看作一个字符串
==============================================
2.宏常量存在的问题:
①没有数据类型,编译器在宏替换的时候不会检查数据类型
②只进行简单的字符串替换,易产生意想不到的错误
例1:
#include <stdio.h>
#define PI 3.14
#define R 4.4
main(){
printf("area=%f\n",PI*R*R);
}
----------------------------------------------
执行过程:
#include <stdio.h>
main(){
printf("area=%f\n",3.14*4.4*4.4);
}
例2:
#include <stdio.h>
#define PI 3.14;
#define R 4.4;
main(){
printf("area=%f\n",PI*R*R);
}
----------------------------------------------
#include <stdio.h>
main(){
printf("area=%f\n",3.14;*4.4;*4.4;);
}
注意:宏替换是预先处理指令,不会进行语法检查,那么再编译的时候才会检查语法错误
2.const常量
1.语法: const 数据类型 标识符=值;
说明:const表示常量,它是只读,在程序运行的时候不能被修改,因此它只能在定义的时候赋初值.这里的不能被修改指的是,在程序运行过程中,不能通过赋值的方式修改const常量的值
例:
const int a=0;
a=1;(×,不能再次赋值)
==============================================
2.const常量与宏常量相比的优点:
①const常量有数据类型,编译器可以对他进行检查
②某些集成化测试工具可以对它进行调试
例1:
#include <stdio.h>
main(){
const float pi=3.14234;//报错
const float r=4.4;
printf("area=%f\n",pi*r*r);
}
常量3.14234,默认是double类型,这样子会产生警告:截断
2.2 变量:
(1)使用规则:
1.变量定义:在程序执行过程中可改变其值的量
2.使用规则:变量必须先定义,后使用
3.语法:
①先定义:
类型关键字 变量名 ;
变量名=值
同时定义多个同类型变量:
类型关键字 变量名1,变量名2,…,变量名n;
int a, b, c;
②同时初始化
类型关键字 变量名=值;
(2)变量属性
1.变量的属性:
①变量名:标识内存中一个具体的存储单元
②变量的地址:首字节的地址被看作变量的地址
③变量的类型:编译器按变量定义的类型分配相应大小的内存空间
④变量的值:把值存进从首字节地址开始,类型决定了内存大小
内存按照字节编地址,用唯一的一个十六进制无符号整数来标识地址,例如32位机的内存地址是32位,从0x00000000到0xFFFFFFFF
2.访问变量的值:
通过变量名,只要程序中出现了变量名(除了定义),就是在访问,包括读操作,写操作
sum=sum+i;//第一个对sum进行写操作,第二个进行读操作
3.数据类型:
3.1 为什么引入数据类型:
在冯诺依曼体系中,程序代码和数据都是以二进制存储的,对于计算机系统和硬件本身,不存在数据类型概念
1.高级语言为什么区分数据类型?
①更有效的组织数据,规范数据的使用
②提高程序的可读性,方便用户使用
2.引入数据类型的好处:
①带来了程序的简明性和数据的可靠性
②提高程序的执行效率,节省内存空间
3.2 变量的类型决定了什么?
变量的类型决定了什么?
①占用内存空间的大小
②数据的存储形式
③合法的表数范围
④可参与的运算种类
(1)占用内存大小:
1.计算变量或者类型所占内存的大小
① sizeof(变量或表达式):返回该变量或者表达式结果所属的类型占的字节数
② sizeof(数据类型):返回数据类型所占的字节数
注意:sizeof是一元运算符,而不是函数
2.使用该运算符计算占用内存大小的好处:
①增加程序的可移植性
②编译时执行该运算符,不会导致额外的运行时间
(2)表述范围:
1.有符号整型:最高位是符号位,剩余的存储数据
2.无符号整型:都是存储数据
以2字节(16位)的短整型为例:
有符号:有16位可以存储数据
无符号:只有15位可以存储数据
(3)数据存储形式:
1.整型
小端次序:便于计算机从低位字节向高位字节运算
大段次序:与人们从左到右的书写习惯顺序相同,便于处理字符串
2.实型
①小数形式:
1.小数形式(定点数):小数点的位置固定
2.分类:
定点整数:只有整数部分,纯整数
定点小数:只有小数部分,纯小数
②指数形式:
指数形式(浮点数):小数点位置不固定
1.浮点数实现小数点位置不固定的原因:
①将实数拆分成阶码和尾数.阶码决定实数范围,尾数决定实数精度.N=r^E*M,如果是十进制,r=10
②对于同样的位数,存储阶码的位数越多,浮点数表示数值的绝对值越大,精度越低
2.结论
①在计算机中,通常用定点数存储整数和纯小数
②用浮点数存储既有整数也有小数部分的实数
3.字符型
字符型数据:单引号括起来的一个字符,包括英文字母,数字,控制字符
以二进制编码存储,一个字节保存一个字符.在内存中保存该数据所对应的字符编码值(二进制形式).字符编码方式由计算机系统使用的字符集决定,一般使用ASCII字符集.
例如:'B'的ASCIIC码值:
十进制:66
八进制:0102
十六进制:0x42
二进制:01000010
但是在内存中是以二进制形式保存
说明:
①所有的ASCII码都可以通过转义方式表示出来
②ASCII码是数值,转义字符是字符
例:'B'的ASCIIC码值转义表示:
八进制的转义字符表示:'\102'
十六进制的转义字符表示:'\x42'
③在内存中字符型常量以二进制保存,也就是一个整数,可以进行算数运算
'B'+32---->66+32=98--->'b'
④转义字符是一个字符,必须使用单引号
'\n' //转义字符
"\n" //普通字符串
(4)参与运算种类:
整型:加,减,乘,除,求余
实型:加,减,乘,除
字符型:加,减(实质上是对ASCII码值进行运算)
指针类型:加,减,比较
3.3 分类:
(1)基本类型
1.整型
a.int(基本整型):C标准未规定,与系统相关,大多数系统占4个字节
b.short int(短整型):2个字节
c.long int(长整型):4个字节,可以简写为 long
unsigned(无符号整型):修饰int,short,long,表示正整数和0
2.实型(浮点型)
a.float(单精度实型):4个字节
b.double(双精度实型):8个字节
c.long double(长双精度实型):与系统相关
3.字符型
char(字符型):1个字节
4.枚举类型
(2)构造类型
1.数组类型
2.结构体类型
3.共用体类型
(3)指针类型
(4)空类型
4.运算符:
4.1 根据运算符性质:
(1)算数运算符(一元)
1.只出现一个运算符:
算术表达式:由算术运算符和操作数组成.
注意:表达式不是语句
1./:除法
规则:两个相同类型的操作数,运算结果和操作数类型相同
例: 11/5=2 11.0/5=2.2
2.%:求余
a.要求:两个操作数必须是整数,返回数学运算a/b的余数
例: 11/5=2...1 11%5=1
11/(-5)=-2...1 11%(-5)=1
-11/5=-2...(-1) -11%5=-1
b.结论:
①求余运算中结果的符号与被除数符号相同,并且求余的结果不会超过除数
②除法运算中被除数,除数只要有一个是负数,结果就是负数
c.应用:
①求余运算可以用来把一个大范围的数映射到只有p个元素的集合=k%p,从0~p
②判断一个数能否被k整除: h%k==0
2.出现多个不同类型的运算符:
①先看优先级
②再看结合性
如果优先级相同,再看结合性
1.左结合:从左向右计算
算数运算符中的二元运算符具有左结合性
2.右结合:从右向左计算
-也可以作为一元运算符,也有右结合性
小括号可以改变优先级,结合性
(2)赋值运算符
1.语法:
1.赋值运算符"="和数学中的"="有什么区别?
①赋值有方向性
②左值和右值的数据类型一致
2.赋值表达式:由操作数和赋值操作符组成
(1)简单赋值:变量=表达式
例:a=a+1;//对第一个a进行写操作,对第二个a进行读操作.也就是把a的值+1之后赋值给a
(2)多重赋值:变量1=变量2=表达式
有两个赋值运算符,优先级相同,看结合性.它是右结合性,从有往左计算
(3)复合赋值:变量1 算数运算符= 表达式
"算数运算符="包括: += -= *= /= %=
a += 1;//相当于a=a+1; 前者的效率更高
2.如何给变量赋值?
C语言中没有专门的赋值语句,通过使用赋值表达式语句赋值
1.先定义,后赋值
数据类型 变量;
变量 = 表达式;//将表达式的值赋值给该变量,表达式的值就是表达式中左侧变量的值
例: int x,y;
x=y=0;//y=0是一个表达式,该表达式的值就是左侧变量y的值
----------------------------------------------
2.定义变量的同时为变量赋值——初始化
数据类型 变量 = 表达式;
注意:
不能在定义多变量的同时,给他们同时赋值.但是可以分别赋值
例: int a=b=3;(×)
int a=3,b=3;(√)
例2:计算
int a=3;
a += a -= a*a;
S1.+=,-=是赋值运算符,*是算数运算符,算数运算符优先级高
即 a += a -=9
S2.然后又因为+=和-=优先级一样,看结合性,是右结合
即 a += (a -=9)
a=a-9 =和+,算数优先级高,先算-,再赋值给a
a=(a-9)
a=-6
S3.a +=(-6)
a=a+(-6) =和+,+是算数运算符,先算+,再赋值给a
a=-12
3.自动类型转换
1.赋值操作中何时发生隐式转换?
赋值操作中,左侧(目标侧)与右侧数据类型不一致时会发生
2.自动类型转换的规则:
变量 = 表达式;
类型2 ← 类型1,把右边表达式的类型转换成左侧变量的类型
例:
int total, number;
float aver;
aver = total / number;
//15/2=7--->7.000000 ,两个操作数是相同类型int/int没有发生自动类型转换,结果是int型,但是因为aver是float型,发生自动类型转换
3.自动类型转换造成的问题:
①取值范围大的类型--->取值范围小的类型---数值溢出
②取值范围小的类型--->取值范围大的类型---精度损失
①自动类型转换之数值溢出:
1.何时发生数值溢出?
任何类型都只能用有限的位数来存储数据,也就是说任何数据类型的表数范围有限.当向变量赋的值超出了其类型的表数范围就会发生数值溢出(取值范围大的类型--->取值范围小的类型)
2.分类:
(1)整数溢出:
发生上溢出:|一个数值运算结果| > |类型能表示的最大数|
原因:进位超过最高位而发生进位丢失/进位到达最高位而改变符号位.
①无符号类型的溢出---进位超过最高位而发生进位丢失
int a=65535;
unsigned short int b;
b=a+1;//unsigned short<---int(65536),数值溢出
printf("%hu",b);//0,不应该是65536吗?
a(65535) 1111 1111 1111 1111
+ 1
-----------------------
10000 0000 0000 0000
b是无符号短整型,只有2字节16位,最高位保存数据,结果为0000 0000 0000 0000,也就是0(十进制)
溢出运算遵循循环规则,如下图:
无符号短整型表数范围:0~65535,那么65535+1--->0
②有符号类型溢出---进位到达最高位而改变符号位
例1:
int a=32767;
short int b;
b=a+1;//short <--- int(32768),数值溢出
printf("%h",b);//-32768
a(32767) 0111 1111 1111 1111
+ 1
-----------------------
10000 0000 0000 000
①如果b是有符号,只有16位,最高位是符号位,那么10000 0000 0000 000中1表示负数,结果是(-32768);
②如果b是无符号,那么10000 0000 0000 000都是数据位,结果为32768(没有发生数据溢出,无符号最大可以是65535)
----------------------------------------------
例2:对于无符号数,不能随意用a-b<0取代a<b
unsigned short a=8;
unsigned short b=10;
printf("%hu\n",a-b);
a(8) 0000 0000 0000 1000
-
b(10) 0000 0000 0000 1010
----------------------------
1111 1111 1111 1110
因为以%hu形式输出结果,无符号整型最高位时数据位,所以结果是:65534
溢出运算遵循循环规则,如下图:
有符号短整型表数范围:-32768~32767,那么32767+1--->-32768
(2)浮点数溢出:
1.为什么会发生上溢出?下溢出?
浮点数在内存中是以阶码和尾数形式存储,阶码表示范围,尾数表示精度.但是阶码是用有限位数存储的,所以不能表示绝对值很大的数---上溢出,也不能表示绝对值很小的数---下溢出
2.什么时候发生溢出?
上溢出:|浮点数运算结果| > |类型能表示的最大数|
下溢出: |浮点数运算结果| < |类型能表示的最小数|,此时,系统将运算结果处理成机器0
注意:比较两个浮点数大小,不能使用a == b
因为a,b在内存中是以对应的二进制形式储存,有的可能没有对应的二进制形式,只能近似相等.因此我们用差值比较法
fac(a-b)<=0.1来比较
3.数值溢出的危害:
①编译器有时对它熟视无睹,只是输出奇怪的结果
②在平台间移植时,例如程序从高位计算机向低位计算机移植
(如从64位系统移植到32位系统)时,可能出现溢出,这种
问题经常被忽视
4.解决办法:
①用取值范围更大的类型,有助于防止数值溢出;但可能会导致存储空间的浪费
②了解处理问题的规模,选取恰当的数据类型
③同种类型在不同的平台其占字节数不完全相同,因此不要对变量所占的内存空间字节数想当然,用sizeof获得变量或者数据类型的长度,提高可移植性
②自动类型转换之数值精度损失:
1.什么时候发生精度损失?
当取值范围小的类型--->取值范围大的类型,发生精度损失.因为低精度的数据位数比高精度的少,容纳不下高精度的所有信息,就会发生舍入,也称截断
2.为什么发生精度损失?
C语言中,浮点数在内存中是以阶码和尾数的形式存储的,但是ANSI C未规定3种浮点类型的长度、精度和表数范围.
因此使用更多的位存储阶码:扩大了变量值域(即表数范围),但精度降低;使用更多的位存储尾数,增加了有效数字位数,提高了数值精度,但表数范围缩小
例:
long a = 123456789;
float b;
printf("%f\n", b);//123456792.000000
3.为什么浮点数的输出结果也不准确呢?
尾数所占的位数决定了实数可以精确表示的位数,这些精确位数也称为有效数字
例:double c = 123456789123.456765;
printf("%f\n", c);//123456789123.456770,只能精确到16位
4.为什么精确度后面的位数与原来的不一样?
因为十进制小数在内存中是以二进制小数存储的.当我们输入一个十进制,CPU先把它--->二进制保存到内存中,当我们读取的时候,把二进制--->十进制输出.
①二进制小数与十进制小数之间并不是一一对应的关系
因为二进制小数连续,但是对应的十进制小数不连续,就导致有些十进制小数不能用二进制表示,只能近似相等.这就导致了多个近似相等的十进制小数需要用同一个二进制小数来表示.
即浮点数不是真正意义上的实数,只是在某种范围内的近似,受精度限制不能直接比较相等
十进制----->二进制(有些十进制没有二进制形式,只能近似相等,造成精度损失)
二进制---->十进制(唯一),就导致最初的十进制小数和最终的十进制小数,只有有效位数上的数字相同
②不是所有的实数都能用有限的位数精确表示
5.注意:
①两个数量级相差很大的浮点数做加减运算时,数值小的数会受
浮点数精度限制而被忽略
②当使用整数可以解决问题,尽量减少使用浮点数类型
#include <stdio.h>
int main()
{
float a, b;
a = 1234567800;
b = a + 20;
printf("a=%.0f, b=%.0f\n",a,b);
//a=1234567808 b=1234567808,因为float有效位数是6~7位,20太小了被忽略
return 0;
}
使用long型,有效数字位数增多,可以解决这个问题.
(3)自增运算符(一元)
1.前缀
++:最终使得变量的值加1个单位
--:最终使得变量的值减1个单位
自增表达式:操作数++或者++操作数
1.注意:
①因为赋值运算符左值不能是表达式,所以操作数只能是变量,不能是表达式
②他们是一元运算符
2.作为前缀运算符时:
S1.先对操作数n增1/减1
S2.然后再使用自增表达式的值(即忽略自增运算符,看它所处的表达式是什么操作)
① ++n -----> 先进行n = n + 1,再使用n的值
例:m =++n;
S1. n = n + 1;
S2.忽略自增运算符 m = n;
② --n -----> 先进行n = n - 1,再使用n的值
2.后缀
作为后缀运算符时:
S1.先使用自增表达式的值(先忽略自增运算符,看所处的表达式操作)
S2.然后再对操作数n增1/减1
① n++ ----> 先使用n的值,然后再n = n + 1
例:m = n++;
S1.先使用n的值 m = n;
S2. n = n + 1;
② n-- ----> 先使用n的值,然后再n = n – 1
例:printf("%d",-n++);//因为-和++都是一元运算符,是右结合,即相当于 printf("%d",-(n++));
S1.printf("%d",-n);
S2.n = n + 1;
为什么将n++括起来,但是先不执行n++?
-(n++),只是表示该自增运算符的操作数是n
前缀与后缀对变量和表达式的影响
1.前缀与后缀对变量和表达式的影响
优点:
①增1和减1运算生成的代码效率更高一些
②无论是前缀还是后缀,操作数的值相同,最终都是+1或者-1;但是包含自增运算符的表达式,作前缀还是后缀结果不同
如下图
缺点:
①过多的增1和减1运算混合会导致可读性差
②不同编译器产生的运行结果不同
2.良好的程序设计风格提倡
在一行语句中,一个变量只出现一次增1或减1运算
(4)类型转换
1.自动类型转换:
什么是自动类型转换?
不同类型数据的运算时,C编译器将所有操作数都转换成取值范围较大的操作数的类型——类型提升
①赋值运算
②算数运算
1.算数表达式的自动类型转换:
①根据参与运算的操作数类型,从低级别向高级别自动换.(横向箭头不是每步都是必须的)
例如:
//int 可以直接提升为 float,不用经过中间的
int+float---->float+float
//long可以表示(2147483647),unsigned int最大可以表示4294967295,本来应该转换为long,但是当unsigned int类型的数值超过2147483647,再这样转换会溢出,所以都转换为unsigned long
例:
int number;
float total, aver;
aver = total / number;
//15 / 2=7.500000,float/int因为最大是float类型,都转换为float类型,结果是float类型
特殊的:
a.long + unsigned int--->unsigned long
b.float型数据在运算时一律转换为双精度(double)型
②C99增强了整数提升规则,char和short都可以直接提升为unsigned int型.(纵向箭头是必须的)
③当表达式中存在有符号类型和无符号类型时,所有的操作数都自动转换为无符号类型
==============================================
2.为什么是这样设置的类型提升规则?
高级别的数据类型所占的内存空间字节数多,这样自动类型转换的时候不会发生数据丢失
③函数调用
①函数调用中参数传递时,系统隐式地将实参转换为形参的类型后,赋给形参
②函数有返回值时,系统将隐式地将返回表达式类型转换为返回值类型,赋值给调用函数
2.强制类型转换:
1.什么是强制类型转换?
将一个表达式的类型强制转换为用户指定的类型,只是临时转换用于计算
2.语法:(数据类型) 表达式
说明:
①"()"是类型强转运算符,是一元运算符
②只是在运算的过程中,进行强制类型转换,但是实际上并不改变该变量的数据类型和值
例1:
int total, number;
float aver;
aver = (float)total / number;
// 15/2-->15.000000/2
// 先把total转换为float,又因为是float/int,所以编译器把他们都转换为float,但是实际上total还是15,还是int类型
(5)关系运算符
运算符 | 对应的数学运算符 | 优先级 | 结合性 |
---|---|---|---|
< | < | 高 | 左 |
> | > | ||
<= | ≤ | ||
>= | ≥ | ||
== | =(这是等于,不是赋值) | 低 | 右 |
!= | ≠ |
1.关系表达式:
①关系表达式:由关系运算符和操作数组成.
②关系表达式的值只有两个:逻辑上的真/假.但是C语言并没有提供布尔值,如果表达式的值为真,用1表示;如果为假,用0表示
③关系运算实质上是比较运算,比较两个操作数的大小关系
2.如何判断表达式的真假?
①因为表达式的结果是一个数字.在判断一个数据的"真"或"假"时,以0和非0为根据:
如果数据的值非0<--->为真<--->1
如果数据的值为0<--->为假<--->0
②因此拓展了判断条件,不局限于关系表达式,可以是任意的数值表达式
例1:
n%2 != 0
n%2的值非0 <---> n%2为真
例2:int a=3,b=2,c=1;
printf(a>b>c);
//a>b>c是两个关系运算符,左结合性.因为a>b为真,故表达式结果为1,1>c结果为假,故表达式结果为0
(6)逻辑运算符
运算符 | 含义 | 使用规则 | 结合性 |
---|---|---|---|
&&(逻辑与) | 交集(AND) | 两个操作对象都为真才为真 | 左结合 |
||(逻辑或) | 并集(OR) | 两个操作对象中只要有一个为真,结果为真 | 左结合 |
!(逻辑非) | 取反(NOT) | 将逻辑真假取反,真变假,假变真 | 右结合 |
1.逻辑运算符的优先级:
逻辑非>算术运算符>关系运算符>逻辑与>逻辑或
2.逻辑表达式:
表达式1 逻辑运算符 表达式2
①逻辑运算符作用:连接多个条件
②结果是逻辑上的真/假,用0/1表示
3.短路特性:若表达式的值可由先计算的左操作数的值单独推导出来,那么将不再计算右操作数的值
①逻辑与:表达式1 && 表达式2,如果表达式1的结果为假,则整个式子结果为假
②逻辑或:表达式1 || 表达式2,如果表达式1的结果为真,则整个式子结果为真
用&&或者||连接的两个子表达式互换位置,对整个逻辑
表达式的值有影响吗?
对整个逻辑表达式的值没有影响,但是对其余变量可能有影响
4.短路的应用:
(i != 0) && (j / i>0)//“短路”使得“除0”运算不会发生
5.良好的程序设计风格
①不建议在程序中使用多用途、复杂而晦涩难懂的复合表达式
②因为在C语言中,圆括号也是一种运算符,而且它的优先级永远是最高的。因此,如果表达式中的运算符较多,则宜用圆括号来确定表达式的计算顺序,这样可以避免使用默认的优先级。
(7)位运算符
4.2 根据操作数个数
操作数:指的是运算对象(常量、变量、函数)
表达式:由操作数和运算符构成,表达式不是一条语句
(1)一元(单目)
(2)二元(双目)
(3)三元(三目)
5.输入与输出:
程序获得输入数据的方式:从键盘输入,从文件中读取
程序输出数据的方式:显示到屏幕,保存到文件中
5.1 格式化屏幕输出:
1.格式化屏幕输出:按指定格式和类型输出变量的值,或者输出一行字符串
C语言没有专门的输出语句,通过调用C的标准库函数.必须导入头文件(#include <stdio.h>).一个程序至少有一个输出,所以都有该行代码
===============================================
2.函数: printf(格式控制字符串, 输出值参数表);
说明:
①输出值参数类型应与格式转换说明符相匹配,数量一致
②可输出多个任意类型的数据
③普通字符原样输出,想要输出"%",在控制格式字符串中写"%%"
④可以识别转义字符
printf("hello\n");
printf("word");
/*
hello
word
*/
格式控制字符串参数:
①指定输出数据格式:
1.指定输出数据格式
a.格式字符:
%d(decimal)---输出十进制有符号int型
%u(unsigned)---输出十进制无符号int型
%f(float)--以小数形式(6位小数)输出float,double型
%e(exponent)---以标准指数形式输出float,double型
%c(character)---以字符形式输出单个字符
(字符型变量可以用%C,也可以用%d输出)
例:
float a=1.3;
printf("a=%f a=%e", a, a);
//a=1.300000 a=1.300000e+00
b.格式修饰符
l----------加d、u前输出long型
h----------加d、u前输出short型
例:
long a=10;
printf("a=%ld",a); //a=10
short b=1;
printf("b=%hd",b);
②指定域宽:
1.输出数据的最小域宽----%m
a.没有指定域宽:原样输出
b.指定域宽m:
数据位宽>m,按实际位宽输出;小于m时,右对齐左补空格
c.指定域宽-m:
-m------左对齐,右补空格
例:
printf("a=%10f", 32.6784728);
//a= 32.678473,f默认是6位,再加上小数点一共9位<10,左补空格
③指定精度:
1.指定显示精度,对浮点数表示输出n位小数----%m.nf
没有指定,按照默认的位数输出
例:
printf("a=%10.4f", 32.6784728);
//a= 32.6785,小数点后面保留4位+小数点和整数位数一共7位<10,左补充3个空格
5.2 格式化键盘输入:
1.函数: scanf(格式控制字符串,输入地址表);
说明:
①&是取地址运算符,输入地址表指定了用于接收输入值的变量的地址.把该值保存到指定的变量中
②数据输入结束的标志:
a.空格/回车/Tab
b.达到指定的输入位宽,多余的保存在缓冲区
c.遇到非法数据,非法是相对于前面的格式字符串中指定的
(1)如何输入多个数据:
①空格/回车/Tab:
②达到指定输入位宽
1234再回车,分别截取相应的长度给两个变量,多余的数据保存在缓冲区
③非法输入:
第一个输入12,按空格表示第一个数据输入结束
第二个数据输入的时候输入3a再回车,但是我们要求是%d,不符和要求,a输入非法,数据输入结束
第一个数据输入123a回车,遇到a,不是%d,输入非法,第一个数据输入结束,第二个数据时乱码
④通过控制格式字符串
格式控制字符串中的普通字符原样输入:通过控制格式字符串中的分隔符,分割多个数据
(2)正确输入的项数:
1.如何判断scanf()是否正确输入?
scanf()返回值=正确读入的数据项数
(3)格式控制字符串:
①指定输出数据格式:
1.指定输出数据格式
a.格式字符:
%d(decimal)---输入十进制有符号int型
%f(float)--以小数形式(6位小数)输入float,double型
%e(exponent)---以标准指数形式输入float,double型
%c(character)---以字符形式输入单个字符
注意:
①不能指定输入数据的精度,scanf("%.2f")
②使用%c读取键盘数据时,按回车/空格/Tab也算一个字符
③ scanf("\n");不能换行
b.格式修饰符:
l----------加d、u前输入long型;加f,e前输入double型
L----------加f,e前输入long double型
h----------加d前输入short型
*----------输入项在读入后跳过该输入项,不赋值给变量
②指定域宽:
二者的区别:
1.为什么用%lf读入 double,用%f输出?
①调用scanf()函数输入数据的时候:
通过地址指向变量.%f告诉编译器scanf(,&a)在a地址存储一个float型(4字节),%lf告诉编译器scanf(,&a)在a地址存储一个double型(8字节).换句话说,scanf()需要区分二者,从而分配不同的内存大小
② 调用printf()函数输入数据的时候:
编译器将float参数自动转换为double类型,所以printf()无法区分float和double
5.3 单个字符的输入输出:
(1)getchar():
1.使用规则:
getchar():把从键盘输入字符的ACSII码值作为getchar()的函数值返回
ch=getchar()
注意:
①只能输入一个字符
例:输入字符的两种方法
ch = getchar(); //速度快
scanf("%c", &ch); //更灵活
2.问题一:
第一种输入方式的缓冲队列:
1.原因:行缓冲(Line-buffer)输入方式
S1.以回车符'\n'结束字符的输入,输入的字符(包括回车符)都放在输入缓冲队列中.
S2.直到键入回车符或文件结束符EOF时,程序才认为一行输入结束
S3.一行输入结束,getchar()才开始从输入缓冲队列读取一个字符,前面函数没读走的数据仍在缓冲队列中,将被下一个函数读取.
并且如果缓冲队列中还有数据,getchar()也不会起作用,不会让我们再输入,直到缓冲队列为空才会让我们输入数据.
===============================================
2.为什么getchar()要读到一个回车符或文件结束符EOF才
进行一次处理操作呢?{为什么 getchar()以行(而非字符)为单位读取字符呢?}
实际是按文件的方式读取字符,文件一般都是以行为单位的
3.问题二:可能返回负值
Windows下:如果我们键盘按Ctrl+Z,返回EOF(一般定义为-1)
例1:
char ch;
ch = getchar();//把-1赋值给字符型变量,报错
解决办法:
int ch;
ch = getchar();//把-1赋值给int变量,不会报错
(2)putchar():
putchar(ch):向屏幕输出一个字符,字符型变量ch的值
putchar(ch)
例:输出字符的两种方法
putchar(ch);//速度快
printf("%c", ch);//更灵活
注意:
①putcahr()只能输出一个字符
②转义字符就是一个字符,可以直接输出
putchar('\n');//打印'\n',使光标移动到下一行开头
putchar("\n");//报错,"\n"是普通字符串,但是putchar只能输出一个字符
(3)scanf()使用%c输入问题
用%c格式读入字符时:
空格和回车等空白字符都会被当作有效字符读入
解决方案1:
解决方案2:忽略所有空白字符
应用:
12赋值给data1,空格赋值给op,+对于data2来说是非法字符,就结束输入,data2没有正确输入,所以是乱码
6. 分支控制:
6.1 条件语句
1.为什么有分支结构?
以前我们写的程序都是自上而下顺序执行的.(输入数据--->处理数据--->输出数据,一般处理数据操作是赋值语句)
但是让计算机求解问题,必须考虑所有的“如果”.
2.分支结构的作用:
采用选择结构---分支结构.根据给定的判定条件,判断结果,并根据判断的结果来控制程序的流程
说明:
①判定条件用任意的数值表达式来表示,包括关系表达式
②连接多个判断条件使用逻辑运算符
(1)条件运算符
1.条件表达式:
表达式1 ? 表达式2 : 表达式3
相当于一个if---else分支
2.执行原理:
如果表达式1的值为真,把表达式2的值作为整个式子的结果;如果表达式1的结果为假,把表达式3的值作为整个式子的结果
(2)分支结构
1.单分支
1.语法:
if ( 表达式P )
{
语句A
}
说明:
①表达式P不局限于关系表达式,也可为数值表达式,表达式的值非0时,为真
2.什么时候用单分支?
判断时面临的选择是:要么执行一个操作,要么跳过它
2.双分支
1.语法:
if ( 表达式P )
{
语句A
}
else
{
语句B
}
2.什么时候用双分支?
判断时面临的选择是:在两个不同的操作中选择其中的一个来执行
3.多分支
1.语法:级联式
if (表达式1)
{
语句A
}
else if(表达式2)
{
语句B
}
else if (表达式3)
{
语句C
}
...
else
{
语句D
}
说明:if--eles中的每个表达式都会被判断
2.什么时候用单分支?
需连续执行多个条件判断时,就选择多分支结构
3.在分支中使用花括号的好处
①{}中所有操作被当作一条语句看待;
②向if和else子句中添加语句时不易出错,能保证程序逻辑的正确性.如果不使用{},那么只有紧随其后的一条语句被当做if的代码块,另外的不是,会导致if--else之间插入了其他代码,不能匹配,如下图1
③使代码结构更清晰.else 总是和同一级中离它最近的if匹配,根据{}判断层级,不是看缩进,如下图2,3
6.2 开关语句
(1)为什么有开关语句?
例1:并列单分支结构
每个if分支的判断条件都会被判断
例2:多分支结构
(2)语法:
1.语法:
switch ( 表达式 )
{
case 常量1:语句序列1;
break;
case 常量2:语句序列2;
break;
...
default :语句序列n;
break;
}
说明:
①常量:起到语句标号的作用,不能出现区间,也不能出现运算符,并且常量值唯一,因此常量值的顺序不会影响程序结果
default 分支:一般用来处理非法数据
②表达式的值一般是int/char类型,并且代码块中常量的值应该和表达式类型一致
③如果case后面没有要执行的语句,表示它与后续第一个有代码的case分支操作一样
2.执行过程:
遇到switch结构,先计算它后面的表达式的值,然后依次判断case后面的常量值,一旦匹配上,就执行后面的代码,遇到break退出分支(没有break,执行完此分支,继续执行下一个分支,直到遇到break);如果都匹配不上,执行default分支
6.3 算法:
1.如何用计算机求解问题?
步骤:
S1.数学建模:将待求问题抽象成数学模型,写出计算的过程
t=x*y
S2.设计算法:根据建立的数学模型,设计流程图,或者用自然语言来描述
①输入x,y
②计算t=x*y
③输出t
S3.使用编程语言设计程序(只有第三步才是计算机可以做的)
#include <stdio.h>
int main()
{
int x,y,t;
scanf("%d,%d",&x,&y);
t=x*y;
printf("t=%d\n");
return 0;
}
S4.检查问题的解,看程序是否可以优化
2.程序:
1.程序=数据结构+算法
①数据结构:定义数据在计算机中是如何存储和组织的,选择恰当的数据结构可以提高程序的执行效率
②算法:对数据上的操作的描述,不同的算法有不同的时间或空间效率
2.常用的描述算法的方法:
①自然语言
②伪码
③传统的流程图
③N-S结构流程图:
取消了流程线,只能自上而下顺序执行
6.4 程序测试
1.测试用例的选取方法
①尽量覆盖所有分支(路径)
②应考虑到合法的输入和不合法的输入以及各种边界条件
1.白盒测试:
1.结构测试:完全了解程序内部的逻辑结构和处理过程,按照程序内部的逻辑测试程序,检验程序中的每条逻辑路径是否都能按预定要求正确工作
2.目的:主要用于测试的早期和重要的路径
3.修改程序
①当程序出现了错误,为了尽快找到错误,我们可以先画出正确范围图,根据图看各个分支之间的关系,然后判断程序中的关系和图中的关系是否一致
②有范围是包含关系的,通常先设计小范围
③每次修改程序,都要重新测试之前的测试用例
7.循环控制:
7.1 计数控制的循环:
1.语法:
1.思想:
①设定一个循环次数,达到这个次数就停止循环.也就是设置一个计数器变量保存这个数值.
②使用for循环的前提:执行循环体之前,循环的次数就已知了
2.语法:
//循环初始条件,循环控制条件,循环退出条件
for (表达式1 ; 表达式2; 表达式3)
{
语句1;
语句2;
}
说明:
①循环初始条件---初始化一个计数器变量
循环控制条件---控制循环执行的条件,通常判断计数器变量的值的上限/下限.为真则执行循环体
循环转化条件---改变计数器变量的值,使得可以退出循环
①复合语句做循环体,被当作一条语句看待
②不要在循环体内重复执行表达式3,即不要在循环体内改变计数器变量的值
3.如何保证循环是可终止的?
①执行循环体时必须改变一个或多个变量的值
②保证经有限次重复后,不再满足循环控制条件
2.实现多个数据的输入:
1.固定的个数:输入5个数据,由开发人员决定
#include <stdio.h>
int main()
{
int i, sum = 0, m;
for (i=0; i<5; i++) //输入5个数据
{
scanf("%d", &m);
sum = sum + m;
}
printf("sum = %d", sum);
return 0;
}
2.不固定个数:输入n个数据,n是用户决定
#include <stdio.h>
int main()
{
int i, sum = 0, m;
printf("Input n:");
scanf("%d", &n);
for (i=0; i<n; i++) //输入n个数据,n是用户决定
{
scanf("%d", &m);
sum = sum + m;
}
printf("sum = %d", sum);
return 0;
}
3.逗号运算符:
例:计算1+2+...+100
写法1:
#include <stdio.h>
int main()
{
int i, sum = 0, n;
for (i=1; i<=100; i++)
{
sum = sum + i;
}
printf("sum = %d", sum);
return 0;
}
写法2:
int main()
{
int i, sum = 0, m, n;
printf("Input n:");
scanf("%d", &n);
for (i=0; i<n; i++)
{
scanf("%d", &m);
sum = sum + m;
}
printf("sum = %d", sum);
return 0;
}
逗号运算符:
表达式1,表达式2,...,表达式n
①最后一个表达式n的值是整个表达式的值.多数情况下,并不使用整个逗号表达式的值.更常见的是分别得到各表达式的值——顺序求值运算符
②主要用在循环语句中,同时对多个变量赋初值等
写法3:首尾相加,循环50次
#include <stdio.h>
int main()
{
int i, j, sum = 0; //两个计数器变量i,j
for (i=1,j=100; i<=j; i++,j--)
{
sum = sum + i + j;
}
printf("sum = %d", sum);
return 0;
}
4.小结:
1.循环初始条件:
累加求和:sum初始化为0或第一项
累乘求积(求阶乘):初始化为1
2.循环控制条件:
使用计数循环设置计算器变量记录循环执行的次数,控制循环的结束.也就是说在执行循环体之前,我们就知道循环次数
3.循环转化条件:
4.循环体:关键是寻找累加项的构成规律 (通项)
①累加项的前后项之间无关:通过序号表示出每一项
例如: 1*2*3 + 3*4*5 +… + 99*100*101
sum = sum + i*(i+1)*(i+2)
i = i + 2 //循环转化条件
(i=1,3,…,99)//循环控制条件
②累加项的前后项之间有关
例如:x0 + x1 + x2 + …+ xn
a.通过无关的方式:通过序号表示出每一项
sum = sum + pow(x, i);
i = i + 1; (i=0,1,2,…,n)
b.前后有关找通项:通过前项表示出后项
term = term * x;
term初值为x0 即1
7.2 条件控制的循环:
思想:设定一个条件,当达到这个条件,就停止循环
1.当型循环:
1.语法:与上面的for循环等价
表达式1; //循环初始条件
while (表达式2)//循环控制条件
{
语句1
语句2
表达式3; //循环转化条件
}
特殊的:
while(表达式2);//说明循环体为空
2.执行原理:
①当表达式2结果为真,执行循环体;为假,退出循环
即表达式2是循环继续执行的条件
②设置循环控制条件:
题目一般给出"当xxx,结束循环",那么xxx是退出循环的条件,那么表达式2应该是:对xxx取反
2.直到型循环:
1.语法:
表达式1; //循环初始条件
do{
语句1
语句2
表达式3; //循环转化条件
}while (表达式2);//循环控制条件
2.执行原理
①表达式2结果为真,继续执行循环体;为假退出循环.
即表达式2是循环继续执行的条件
②设置循环控制条件:
一般题目中会给出"直到xxx为止",退出循环的条件是xxx,
那么表达式2是对xxx取反
3.三者的区别:
for循环等价于while循环.当判断条件为假,while循环直接跳出循环,不会执行循环体;但是 do-while先执行,再判断,也就是说它至少执行一次循环体
①循环次数已知
循环次数已知:指的是在执行循环之前,就已经知道循环次数了
例:计算n个数字之和
1.计数控制的循环:等价于当型while循环
#include <stdio.h>
int main()
{
int i, sum = 0, m, n;
printf("Input n:");
scanf("%d", &n);//设置循环次数
for (i=0; i<n; i++)
{
printf("Input m:");
scanf("%d", &m);
sum = sum + m;
}
printf("sum = %d\n", sum);
return 0;
}
2.while循环
#include <stdio.h>
int main()
{
int i, sum = 0, m, n;
printf("Input n:");
scanf("%d", &n);//设置循环次数
i = 0;
while (i < n)
{
printf("Input m:");
scanf("%d", &m);
sum = sum + m;
i++;
}
printf("sum = %d\n", sum);
return 0;
}
当判断条件为假时,直到型和当型结果不一样
sum=0
3.do-while循环:
#include <stdio.h>
int main()
{
int i, sum = 0, m, n;
printf("Input n:");
scanf("%d", &n);//设置循环次数
i = 0;
do{
printf("Input m:");
scanf("%d", &m);
sum = sum + m;
i++;
}while (i < n);
printf("sum = %d\n", sum);
return 0;
}
结果:
5↙
sum=5
②循环次数未知
循环次数未知:执行循环之前,不知道要循环多少次.可以设置循环结束的标记,到达此标记就停止循环------标记循环
例:输入数据,显示每次累加的结果,直到输入0时为止
1.while循环
#include <stdio.h>
int main()
{
int sum = 0, m;
while (m != 0)
{
printf("Input m:");
scanf("%d", &m);
sum = sum + m;
printf("sum = %d\n", sum);
}
printf("sum = %d\n", sum);
return 0;
}
2.do-while循环:
#include <stdio.h>
int main()
{
int sum = 0, m;
do{
printf("Input m:");
scanf("%d", &m);
sum = sum + m;
printf("sum = %d\n", sum);
}while (m != 0);
return 0;
}
循环次数已知 | for语句 |
---|---|
循环次数未知,循环的次数由一个给定的条件来控制 | while或do-while |
循环体至少要执行一次 | do-while |
4.猜数游戏:
产生随机数:
1.产生随机数:rand()
①magic = rand(); //产生[0,RAND_MAX]间的随机数
RAND_MAX在stdlib.h中定义,不大于双字节整数的最大值32767,开头写 #include <stdlib.h>
②产生[0,99] 之间的随机数
magic = rand()%100;
③产生[1,100] 之间的随机数
magic = rand()%100 + 1;
2.存在的问题:
rand()产生的是伪随机数,就是说每次运行得到同一个序列
int i;
for(i=0;i<10;i++){
printf("%d\n",rand());
}
解决办法:
①为rand()设置随机数种子,设置不同的种子,使得产生随机数
int i;
unsigned int seed;
printf("Please enter seed:");
scanf("%u", &seed);//用户输入种子
srand(seed);
for (i=0; i<10; i++)
{
printf("%d\n", rand());
}
return 0;
②改用系统时间作为随机数种子更好,用函数time()获得系统时间
用NULL作为函数参数,使其仅能从返回值取得系统时间,便于将函数写到表达式中
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
int i;
srand(time(NULL));
for (i=0; i<10; i++)
{
printf("%d\n", rand());
}
return 0;
}
①猜一次
②直到猜对为止
此程序还存在一个问题,每次执行程序产生同一个随机数
还存在一个问题,要求我们输入%d,当我们输入非法数据时(非数字):
当我们按回车,scanf()按指定格式读取缓冲区中的数据,若读取失败,则缓冲区中的非数字字符不会被读走,继续进行条件判断,判断为真,继续执行循环.因不等而一直处于判断、读取、判断、读取、…死循环
③最多猜10次
因为判断条件是用逻辑运算符连接的多个条件,所以循环推出有两种情况:就是对该条件取反
即:guess=magic || counter>=10
1.会出现死循环吗?
不会.如果猜不对,猜到第十次,就会退出循环.
④猜多个数
还存在一个问题,要求我们输入%d,当我们输入非法数据时(非数字):
当我们按回车,scanf()按指定格式读取缓冲区中的数据,若读取失败,则缓冲区中的非数字字符不会被读走,继续进行条件判断,判断为真,继续执行循环.当counter>=0退出循环
然后继续向下执行,scanf()读取了刚刚的非法字符
7.4 递推:
1.递推思想:
1.什么是递推?
利用问题本身所具有的一种递推关系来求解问题的一种方法
2.递推思想:
从已知的初始条件出发,依据某种递推关系,逐次推出所要计算的中间结果和最终结果.
初始条件要么在问题本身中已经给定,要么需要通过对问题的分析和化简后来确定
3.递推的本质:
把一个复杂的计算过程转化为一个简单过程的重复计算
可求解问题的特点:
①问题可以划分为多个状态
②除原始状态外,其他各状态都可用固定的递推关系式来表示
4.递推的应用:
按照一定的规律来计算序列中的指定项,递推关系式通常不会直接给出
2.递推的基本方法
(1)正向顺推:
正向顺推:从已知条件出发,向着所求问题的解前进,一步一步推出答案
例:假设一对小兔成熟期是1个月.每对成兔每个月可以生一对小兔,一对新生的小兔第二个月长成成兔就开始生小兔,问一对成兔开始繁衍,一年后总计有多少对小兔,成兔?
---每个月的小兔对数=上个月的成兔对数
---每个月的成兔对数=上个月的成兔对数+上个月的小兔对数
---->每个月的成兔对数=前两个月的成兔对数
(2)反向逆推:
反向逆推:已知问题结果,一步一步还原出答案
例:猴子第一天摘下若干个桃子,吃了一半,还不过瘾,又多吃了一个。第二天早上又将剩下的桃子吃掉一半,并且又多吃了一个。以后每天早上都吃掉前一天剩下的一半零一个。到第10天早上再想吃时,发现只剩下一个桃子。问第一天共摘了多少桃子?4
xn=1(n=10)
xn-1=2*(xn+1)(1≤n<10)
①记录天数
方式1:while型循环
#include <stdio.h>
int main()
{
int x = 1, day = 10;
while (day > 1)
{
x = (x + 1) * 2;
day--;
}
printf("x = %d\n", x);
return 0;
}
方式2:do-while循环
#include <stdio.h>
int main()
{
int x = 1, day = 10;
do{
x = (x + 1) * 2;
day--;
}while (day > 1);
printf("x = %d\n", x);
return 0;
}
因为第一次开始的时候,判断条件为真,所以二者结果一样
②记录递推次数:
1.while循环
#include <stdio.h>
int main()
{
int x = 1, d = 1;
while (d < 10)
{
x = (x + 1) * 2;
d++;
}
printf("x = %d\n", x);
return 0;
}
2.do-while循环
#include <stdio.h>
int main()
{
int x = 1, d = 1;
do{
x = (x + 1) * 2;
d++;
}while (d < 10);
printf("x = %d\n", x);
return 0;
}
7.5 嵌套循环:
(1)设计嵌套循环:
1.怎样设计嵌套循环程序?
由内到外,先考虑每一行中的数据如何输出,再考虑如何输出这样的多个行.
关键是寻找累加项的规律:
①累加项的前后项之间无关:通过序号表示出每一项
例如: 1*2*3 + 3*4*5 +… + 99*100*101
sum = sum + i*(i+1)*(i+2)
i = i + 2 //循环转化条件
(i=1,3,…,99)//循环控制条件
②累加项的前后项之间有关
例如:x0 + x1 + x2 + …+ xn
a.通过无关的方式:通过序号表示出每一项//嵌套循环
sum = sum + pow(x, i);
i = i + 1; (i=0,1,2,…,n)
b.前后有关找通项:通过前项表示出后项
term = term * x;
term初值为x0 即1
2.嵌套循环执行过程:
S1.判断外层循环的条件,
----为真,进入外层循环的循环体---->S2
----为假,退出外层循环
S2.判断内层循环条件为真,执行内层的循环体,直到内层循环条件为假,退出内层循环.(此时执行完一遍内层循环体)再继续往下执行外层循环体内剩下的语句.此时外层循环的循环体执行完一次
S3.再去判断外层循环条件
----为真,继续刚刚的操作;
----为假,退出外层循环
3.注意:
①内层,外层控制变量不能重名
②总循环次数=内循环次数*外循环次数
①前后项有关:
计算并输出1!+ 2!+ 3!+ … +n!
#include <stdio.h>
int main()
{
int i, n;
long p = 1;
printf("Please enter n:");
scanf("%d", &n);
for (i=1; i<=n; i++)
{
p = p * i;
printf("%ld\n", p);
}
printf("sum = %ld\n", sum);
return 0;
}
②前后项无关(嵌套):
例1:
例1:计算并输出1!+ 2!+ 3!+ … +n!
#include <stdio.h>
int main()
{
int i, j, n;
long p, sum = 0;
printf("Input n:");
scanf("%d", &n);
for (i=1; i<=n; i++)
{
p = 1;
for (j=1; j<=i; j++)
{
p = p * j;
}
sum = sum + p;
}
printf("sum = %ld\n", sum);
return 0;
}
例2:九九乘法表
#include <stdio.h>
int main()
{
int m, n;//m控制高,n控制宽
for (m=1; m<10; m++)
{
for (n=1; n<10; n++)
{
printf("%d\t", m*n);//每列之间有4个空格
}
printf("\n"); //输出完一行换行
}
return 0;
}
(2)解决死循环:
1.穷举法思想:
根据问题的部分已知条件,列举所有可能,逐一测试,如果满足题目的全部条件,就是一个解.直到找到满足已知条件的所有解
2.穷举的两个要素:
①确定穷举对象和穷举范围---循环结构
②确定判定条件----分支结构
3.穷举法的特点:
①优点:算法简单,逻辑清晰,结构简单(分支+循环)
②缺点:如果使用穷举法求解,没有搜索上限且无解时,会造成死循环
4.应用:
穷举法常用于密码破译.就是在有效的时间内找到密码
为了防止密码被破译,可以设置可容许的试误次数
例:韩信点兵
确定穷举对象:士兵数量x 搜索范围:从1开始
判断条件:x%5==1 && x%6==5 && x%7==4 && x%11==10
#include <stdio.h>
int main()
{
int x;
for (x=1; ;x++)//控制条件为空,死循环
{
if (x%5==1 && x%6==5 && x%7==4 && x%11==10)
{
printf("x = %d\n", x);
}
}
return 0;
}
1.跳出循环:
①goto
1.语法:
goto END;
END: ;
说明:标号(标识符)后面必须有语句,哪怕是空语句
2.执行原理:遇到goto,跳转到goto后面的标号处往下执行
例:
#include <stdio.h>
int main()
{
int x;
for (x=1; ;x++) //控制条件为空,死循环
{
if (x%5==1 && x%6==5 && x%7==4 && x%11==10)
{
printf("x = %d\n", x);
goto END;//跳转到END,退出循环
}
}
END: ;
return 0;
}
3.什么时候使用goto?
①跳向共同的出口位置,进行退出前的处理工作
②跳出多重循环
{…
{…
{…
goto Error;
}
}
}
Error:
注意:
①不要使用过多的goto语句标号
②只允许在一个单入口单出口的模块内向前跳转
②continue
1.作用:
跳过此次循环后面未执行的循环体语句,开始下一次循环
2.continue对循环的影响
大多数for循环可以转换为while循环,但并非全部
例如当循环体中有continue时,二者不等价
①for循环
sum = 0;
for (n=0; n < 10; n++)
{
scanf("%d", &m);
if (m == 0)
continue;
sum = sum + m;
}
②while循环
n = 0;
sum = 0;
while (n < 10)
{
scanf("%d", &m);
if (m == 0)
continue;
sum = sum + m;
n++;
}
③break
1.作用:
退出所在最近的一个循环或switch,跳转的位置限定为紧接着循环语句后的第一条语句.本质是受限的goto语句
#include <stdio.h>
int main()
{
int x;
for (x=1; ;x++) //控制条件为空,死循环
{
if (x%5==1 && x%6==5 && x%7==4 && x%11==10)
{
printf("x = %d\n", x);
break;//找到,就退出所处的一层循环
}
}
return 0;
}
2.exit()
1.exit()作用:
它是一种标准库函数,终止整个程序的执行强制返回操作系统
当其参数为0时,表示程序正常退出;
非0时表示程序出现某种错误后退出
必须加#include <stdlib.h>
例:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int x;
for (x=1; ;x++) //造成死循环
{
if (x%5==1 && x%6==5 && x%7==4 && x%11==10)
{
printf("x = %d\n", x);
exit(0);//找到强制退出
}
}
return 0;
}
3.标志变量
#include <stdio.h>
int main()
{
int x;
int find = 0; //置为假,0表示没找到
for (x=1; !find ;x++) //!find为真,即没找到
{
if (x%5==1 && x%6==5 && x%7==4 && x%11==10)
{
printf("x = %d\n", x);
find = 1; //置为真,1表示找到了
}
}
return 0;
}
此题中判断条件:
①!find
②find == 0
③find != 1
最好使用①,因为有些程序可能不使用1表示找到了
4.while循环
当使用条件控制循环,如果不知道搜索上限,也无解,那么会在称死循环
但是此题有解
找到一个就退出
#include <stdio.h>
int main()
{
int x = 0;
do{
x++;
}while (!(x%5==1 && x%6==5 && x%7==4 && x%11==10));
printf("x = %d\n", x);
return 0;
}
8.函数:
8.1 函数
1.模块化编程-----C语言的逻辑结构:
一个C程序由一个或多个源程序文件组成;
一个源程序文件由一个或多个函数组成,
可把每个函数看作一个模块
函数是构成C语言程序的基本模块,模块化编程的最小单位
2.分类:
①标准库函数:
a.ANSI/ISO C定义的标准库函数
使用时,必须在程序开头把定义该函数的头文件包含进来,因为函数原型在此文件中
b.第三方库函数:不在标准范围内,能扩充C语言的功能,由其他厂商自行开发的C语言函数库
③自定义函数:用户自己定义的函数,包装后,也可成为函数库,供别人使用
3.使用函数编程的好处:信息隐藏
对于函数的使用者,无需知道函数内部如何运作,只了解其与外界的接口即可.就是把函数内的具体实现细节对外界隐藏起来,只要对外提供的接口不变,就不影响函数的使用.便于实现函数的复用和模块化编程
8.2 自定义函数:
返回值类型 函数名(形参数据类型 变量名)
{//函数体定界符
//形参也相当于一个局部变量
int x,y; //局部变量
return x;//只能返回一个值
}
1.返回值类型:
①可以是void,表示无返回值
那么对应写return;或者不写该语句
②省略返回值类型,默认为int
那么对应写return a;(a是int类型)
2.形参列表----函数的入口
①如果函数体内不需要外面的数据
那么形参可以为void,表示无参数,也可以省略
3.return 表达式;---函数的出口
①不需要返回值:
写return;或者没有return
②返回表达式的结果:函数只能返回一个值,且类型和前面定义的返回值类型一致
(return 变量;),返回值类型可以是除数组之外的任意类型
③如果放在分支结构中,每个分支都要有return语句,但是函数返回值还是一个
4.函数名也是一个标识符
8.3 函数调用:
(1)调用方式:
1.调用方式:
调用者通过函数名调用函数:函数名()
2.使用:
①函数无返回值时,单独作为一个函数调用语句
DisplayMenu();
②有返回值时,可放到一个赋值表达式语句中
ave = Average(a, b);
③还可放到一个函数调用语句中,作为另一个函数的参数
printf("%d\n", Average(a, b));
注意:
①函数调用时,实参与形参的数目、类型和顺序要一致
②实参和形参的作用域不同,所以二者可以同名
函数定义时的参数,形式参数,简称形参
函数调用时的参数,实际参数,简称实参
(2)调用过程:
每次执行函数调用时:
每次都是从main()函数开始,main()结束
S1.现场保护,并为函数内的局部变量(包括形参)分配内存
S2.把实参值复制一份给形参,单向传值(实参-->形参)
S3.程序控制权交给被调函数,执行函数内的语句.当执行到"return语句"或"}"时,从被调函数退出
S4.从函数退出时
①根据函数调用栈中保存的返回地址,返回到本次函数调用的地方
②把函数值返回给主调函数
③收回分配给函数内所有变量(包括形参)的内存,同时把控制权还给调用者
继续执行调用者以后的语句,直到执行完成
8.4 函数原型
(1)为什么需要函数声明?
1.在一个函数中调用另一个函数,需要具备哪些条件呢?
(1)调用标准库函数:
在main()开头,用#包含头文件
(2)调用自定义函数:
①先定义,后调用:
a.C89允许不明确地给出函数原型,编译器自动创建隐含的函数声明,即我们可以不写函数声明.CodeBlocks允许不写函数声明
b.但C99不支持隐含的函数声明,编译器不会自动创建函数声明,必须写函数声明,
②先调用,后定义:
a.有的编译器,自动创建一个返回值为int类型的函数声明,给出警告.当自定义函数返回值类型不是int时,会报错
b.有的不会创建函数声明
所以最好都在函数调用之前写上函数原型
2.语法:
返回值类型 函数名(形参数据类型 变量名);
在函数被调用之前写上该函数原型
3.把所有函数的定义都放在main函数的前面,是否可以不用
函数原型了呢?
①但是其他函数之间也会相互调用
②函数也可能在不同的文件中
(2)函数声明的作用:
1.函数原型的作用:
返回值类型 函数名(形参数据类型 变量名);
①告诉编译器,被调函数需要接收几个何种类型的参数,并让其进行参数匹配检查
②函数原型中的形参及其类型可省略不写,但写上有助于参数类型匹配检查
2.良好的编程习惯
在程序开头给出所有的函数原型
3.在函数调用时,若实参与形参不匹配,结果会怎样?
①某些编译器会保持沉默,仅当函数原型与函数定义中的形参类型不一致时才给出编译错误
②某些编译器可以捕获实参与形参类型不匹配的错误,并发出警告
#include <stdio.h>
unsigned long Fact(unsigned int n);
int main()
{
int m;
long ret;
printf("Input m:");
scanf("%d",&m);
ret=Fact(m);//long=unsigned long,自动类型转换
if(ret == -1){
printf("Input Error");
}
else{
printf("%d!=%ld\n",m,ret);
}
return 0;
}
unsigned long Fact(unsigned int n){
unsigned int i;
unsigned long result=1;
if(n<0){//永远为假
return -1;
}
else{
for(i=2;i<=n;i++){
result *=i;
}
return result;
}
}
结果:
-3↙
很大的正数
因为m是int,m=-3,-3又被传参给n,n是 unsigned int,-3的补码最高位被解释为数据位,计算的结果是很大的正数
#include <stdio.h>
unsigned long Fact(unsigned int n);
int main()
{
int m;
long ret;
do(){
printf("Input m:");
scanf("%d",&m);
}while(m<0);
ret=Fact(m);
printf("%d!=%ld\n",m,ret);
return 0;
}
//当被调函数不具有健壮性,调用函数需要具有健壮性
unsigned long Fact(unsigned int n){//n>0
unsigned int i;
unsigned long result=1;
for(i=2;i<=n;i++){
result *=i;
}
return result;
}
(3)函数原型与定义的区别:
8.5 函数封装
(1)什么是函数封装?
1.封装:
外界对函数的影响——仅限于入口参数
函数对外界的影响——仅限于一个返回值和数组、指针形参
(2)如何增强健壮性:
①在函数的入口处,检查输入参数的合法性
②在调用处检查函数的返回值
例1:求阶乘:
#include <stdio.h>
long Fact(int n);
int main()
{
int m;
long ret;
printf("Input m:");
scanf("%d",&m);
ret=Fact(m);
printf("%d!=%ld\n",m,ret);
return 0;
}
long Fact(int n){
int i;
long result=1;
for(i=2;i<=n;i++){
result *=i;
}
return result;
}
如果输入-1,会出错,因为负数没有阶乘
#include <stdio.h>
long Fact(int n);
int main()
{
int m;
long ret;
printf("Input m:");
scanf("%d",&m);
ret=Fact(m);//在调用处检查函数的返回值
if(ret == -1){
printf("Input Error");
}
else{
printf("%d!=%ld\n",m,ret);
}
return 0;
}
long Fact(int n){
int i;
long result=1;
if(n<0){//在函数的入口处,检查输入参数的合法性
return -1;
}
else{
for(i=2;i<=n;i++){
result *=i;
}
return result;
}
}
(3)函数设计基本原则:
①函数规模要小
②函数功能要单一
③函数接口定义要清楚
8.6 断言与防御式编程:
(1)断言:
1.断言的使用:
1.如何确定程序中的假设是真还是假?
①某个特定点的某个表达式的值一定为真
②某个特定点的某个表达式的值一定位于某个区间等
2.断言的作用:
测试程序中假设的正确性:如果假设被违反,则中断程序的执行
3.语法:
相当于一个函数:void assert(int 表达式);
表达式为真,无声无息
表达式为假,中断程序
实际上是在<assert.h>中定义的宏,有参数
加上#include <assert.h>
#include <stdio.h>
unsigned long Fact(unsigned int n);
int main()
{
int m;
long ret;
do(){
printf("Input m:");
scanf("%d",&m);
}while(m<0);//退出循环之后,我们认为m>=0
assert(m>=0);//因为m>=0成立,继续执行
//如果assert(m<0);就提示出错行,中断程序
ret=Fact(m);
printf("%d!=%ld\n",m,ret);
return 0;
}
//当被调函数不具有健壮性,调用函数需要具有健壮性
unsigned long Fact(unsigned int n){//n>0
unsigned int i;
unsigned long result=1;
for(i=2;i<=n;i++){
result *=i;
}
return result;
}
2.条件语句和断言:
3.使用条件语句代替断言,可不可以?
if(表达式){
exit(1);
}
使用断言便于在调试程序时发现错误,不会影响程序执行效率
仅用于调试程序,不能作为程序的功能.
3.什么时候使用断言?
4.什么时候适合使用断言?
①检查程序中的各种假设的正确性
②证实或测试某种不可能发生的状况确实不会发生
只能用来检测某种不可能发生的情况一定不会发生,而不能用来检测有可能发生的情况不会发生
int MakeNumber(void)
{
int number;
//number一定是[1,100]
number = rand() % 100 + 1;
assert(number >= 1 && number <= 100);
return number;
}
5.使用断言的基本原则
①使用断言捕获不应该或者不可能发生的情况.但这不是非法情况,因为非法情况是有可能发生的
②每个assert只检验一个条件
(2)防御式编程:
1.作用:
①让你编写的代码具有防弹功能
2.养成良好的编码风格
①避免闪电式编程,用怀疑的眼光审视所有的输入和结果
②简单就是一种美,不要滥用技巧,让你的代码过于复杂
③编译时打开所有警告开关,不要忽略它们
④使用安全的数据结构和函数调用
⑤做内存的“好管家""
8.7 嵌套调用:
1.函数嵌套?
在一个函数的函数体内部又调用另一个函数---嵌套调用
注意:
①函数不能嵌套定义,即在一个函数的函数体内部不能定义另一个函数
②换句话说,除了main方法()函数,其他的函数都是平行的,其他函数不能调用main()函数,但是其他函数之间可以互相调用
#include <stdio.h>
long Fact(int n);
long Comb(int m, int k);
int main()
{
int m, k;
do{
printf("Input m,k (m>=k>0):");
scanf("%d,%d", &m, &k);
}while (m<k || m<0 || k<0);
printf("%ld\n", Comb(m, k));
return 0;
}
long Comb(int m, int k)//计算组合数
{
return Fact(m)/(Fact(k)*Fact(m-k));
}
long Fact(int n)//计算阶乘
{
int i;
long result = 1;
for (i=2; i<=n; i++)
{
result *= i;
}
return result;
}
8.8 递归调用:
(1)什么是递归:
1.什么是递归?
函数直接或间接调用自己,称为递归调用,这样的函数叫做递归函数
2.递归思想:
把规模较大的,较难解决的问题---->规模较小的、易于
解决的同类子问题--->规模更小的子问题(递归的一般条件),且小到一定程度可以直接得出它的解(递归的基本条件),从而得到原始问题的解
两个基本要素:
①基本条件---终止条件,出口(一般是边界值)
②一般条件---递推关系
(2)普通递归:
1.条件递归:
if (基本条件)
return 递归公式的初值;
else
return 递归函数调用的返回值;
执行过程:
①基本条件:控制递归调用结束,是递归的出口.
当满足这个限制条件的时候,递归便不再继续
②一般条件:控制递归调用----->转化成基本条件
即每次递归调用之后越来越接近这个限制条件
2.递归调用可以终止的条件是什么?
如果没有基本条件或者一般条件不能最终转化为基本条件,递归调用不会结束
(3)什么时候用递归:
1.什么时候使用递归?
如果一个问题中具有以下特点:
a. 有一个初始状态
b. 后续的情况可由前面的状态推出
则可以用递归的方法求解
2.常见的递归:
①数学定义是递归的
如计算阶乘,最大公约数和Fibonacci数列
②数据结构是递归的
如队列、链表、树和图
③问题的解法是递归的
如Hanoi塔,骑士游历、八皇后问题(回溯法)
#include <stdio.h>
void Hanoi(int n, char a, char b, char c);
void Move(int n, char a, char b);
int main()
{
int n;
printf("有几个盘子:");
scanf("%d", &n);
printf("%d 盘子从A到B\n", n);
Hanoi(n, 'A', 'B', 'C');
return 0;
}
//功能:将n个从A--->B
void Hanoi(int n, char a, char b, char c)
{
if (n == 1)
{
Move(n, a, b);//将第一个从A--->B
}
else
{
Hanoi(n-1, a, c, b);//将n-1个从A-->C
Move(n, a, b);//将第n个从A--->B
Hanoi(n-1, c, b, a);//将n-1个从C-->B
}
}
void Move(int n, char a, char b)
{
printf("Move %d: from %c to %c\n", n, a, b);
}
(4)调用栈:
1.递归函数的调用过程
①递推阶段(递归前进阶段):把复杂--->简单
②回归阶段(递归返回阶段)
当满足递归终止条件,从递归前进阶段--->回归阶段
2.栈结构:
栈的操作是后进先出,与函数调用与返回吻合,因此适合存储函数调用.用来保存函数调用信息的栈空间---函数调用栈
3.保存什么信息?
输入参数,返回值,调用时的状态信息,输出参数
3.堆栈溢出:堆是从低到高存储,栈是从高到低存储.
往堆栈中存入的数据超出预先给堆栈分配的容量,会发生溢出
(5)尾递归:
1.什么是尾递归?
1.什么是尾递归?
尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。
这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况
2.与普通递归的区别:
与普通递归的区别:
①在传统的递归中:
a.递归阶段:你执行第一个递归调用,然后接着调用下一个递归来计算结果,每次递归的结果都依赖于下一次的递归结果,中途得不到计算结果.所以必须保存每次调用函数,都会有一个函数调用栈,容易溢出.
b.返回阶段:直到满足条件,把结果返回给上一个函数,才能计算调用函数的返回值,再返回给上一个函数,直到返回给main()函数,得到最终结果
②在尾递归中:
a.递归阶段:先执行某部分的计算,然后开始调用递归,所以你可以得到当前的计算结果,而这个结果也将作为参数传入下一次递归.
b.返回阶段:直到满足结束条件,因为每次的结果不需要计算,所以把最终结果直接返回给main()
3.尾递归调用栈:
(6)递归缺点:
1.递归方法的优缺点:
①优点:简洁、直观、精炼,易编、易懂、逻辑清楚,结构清晰、可读性好,更符合人的思维习惯,逼近数学公式的表示
②缺点:
a.函数调用开销大,耗费更多的时间和栈空间,时空效率偏低
b.易产生大量的重复计算
2.递归与迭代的区别,如下图:
①递归:
是一个树结构,从字面可以其理解为重复“递推”和“回归”的过程,当“递推”到达底部时就会开始“回归”,其过程相当于树的深度优先遍历。
②迭代:
是一个环结构,从初始状态开始,每次迭代都遍历这个环,并更新状态,多次迭代直到到达结束状态
注意:
①可以使用循环的地方通常都可以使用递归
②尽量使用迭代而不是递归
8.10 变量的作用域:
1.什么是作用域:
1.什么是作用域?
变量的作用范围,也就是能被读写访问的范围.取决于变量在源程序中被定义的位置.下面的访问变量都是在定义变量之后,访问变量
2.根据作用域范围分类:
全局变量,局部变量
2.作用域范围:
①局部变量:
1.局部变量的特点:
局部变量:在语句块内(函数、复合语句)定义的变量
作用范围:仅能在定义它的语句块(包括其下级语句块)内访问
2.特点:
作用域较小的局部变量隐藏作用域较大的局部变量
②全局变量:
1.全局变量的特点:
全局变量:在所有函数之外定义的变量
作用范围:从定义变量的位置开始,到本程序结束
2.优点:
全局变量在某些场合下很有用
①当多个函数必须共享同一个固定类型的变量时
②当少数几个函数必须共享大量数据时
3.缺点:
①破坏了函数的封装性,不能实现信息隐藏
谁都可改写它,很难确定谁改写了它
②依赖全局变量的函数很难在其他程序中复用
依赖全局变量的函数不是“独立”的,因为如果想要复用该函数,必须还要加上全局变量
③对于使用全局变量的程序,维护比较困难
如果修改了全局变量,那么要检查所有使用全局变量的代码
④建议在可以不用时尽量不用
多数情况下,通过形参和返回值进行数据交流比共享全局变量的方法更好
变量同名:
1.变量同名:
①局部变量与全局变量同名:下图1
局部变量隐藏全局变量,互不干扰
②形参与全局变量同名:下图2
局部变量隐藏全局变量,互不干扰
③并列语句块内的局部变量同名:下图3
互不干扰,形参值改变不影响与其同名的实参值
即只要同名的变量出现在不同的作用域内:
二者互不干扰,编译器有能力区分不同作用域中的同名变量
2.假如同名变量出现在同一个作用域中?
编译器只能区分不同作用域中的同名变量.如果同名变量出现在同一个作用域中,编译器也将束手无策
如何区分同名变量:
硬件没有作用域的概念,只是区分内存地址.如果两个同名变量代表不同的内存地址,就可以区分
原理:编译器通过将同名变量映射到不同的内存地址来实现
作用域的划分
由于局部变量在动态存储区中被分配内存,而全局变量在静态存储区被分配内存,被分配的内存区域不同,因而内存地址也不同
形参和实参的作用域、内存地址不同,所以形参值的改变不会影响实参
3.变量的存储类型:
1.如何区分同名变量?
1.编译器是如何区分不同作用域中的同名变量的?
编译器通过将同名变量映射到不同的内存地址来实现
2.内存映像:
①只读存储区:存放机器代码和常量等只读数据
②静态存储区:在程序编译或链接时分配内存.
也就是在程序编译的时候,如果遇到定义了全局变量,静态变量,就已经为全局变量和静态变量分配内存了
③动态存储区:在程序载入和运行时分配内存,包括堆和栈
栈用于保存函数调用时的返回地址、函数的形参、局部变量等信息
2.什么是变量的存储类型?
1.什么是变量的存储类型?
就是指编译器为变量分配内存的方式,决定了变量的生存期
与作用域范围不同,作用域是指变量被访问的范围
2.根据变量的存储类型分类:
静态,动态
3.变量的生存期:
①在静态存储区中分配内存的变量
生存期是整个程序 全程占据内存.
静态存储区中的变量:与程序 共存亡
②在动态存储区中分配内存的变量
生存期是定义它的语句块
动态存储区中的变量:与语句块“共存亡”
3.如何声明变量的存储类型?
1.什么变量的存储类型语法:
存储类型 数据类型 变量名;
C存储类型关键字:
① auto ---自动变量
② static---静态变量
③ extern---外部变量
④ register---寄存器变量
①外部变量:
1.外部变量(是一种全局变量):
声明变量的存储类型:
extern 数据类型 变量名;
2.全局变量的特点:
①生存期:在静态存储区内分配内存,所以与程序共存亡.
从程序运行起占据内存,程序退出时释放内存
②在静态存储区分配,即编译时分配内存初始化.
a.没有显式初始化的外部变量,编译程序自动初始化为0
int a;//0
int main(){
printf("%d",a);//使用前未初始化,但是是静态变量,编译器在编译阶段自动初始化为0
}
b.自己初始化变量,编译时赋该值.每次运行代码,执行此代码都会跳过.因为只在编译时初始化,且只初始化一次
③作用域:
外部变量的作用域是整个项目.它只需要在一个源文件中定义就可以作用于所有的源文件
3.问题:若在定义变量之前或在其他文件中访问,怎么办?
声明变量的存储类型:
extern 数据类型 变量名;//编译器并不对其分配内存,只是表明“我知道了”
②自动变量:
1.自动变量——动态局部变量(缺省类型)
声明变量的存储类型:
auto 数据类型 变量名;
定义时,没有指定变量类型,默认为 auto.
2.自动变量的特点:
①生存期:在动态存储区分配内存,所以与语句块共存亡
进入语句块时,系统自动申请内存;
退出时,系统自动释放内存
离开函数,值就消失
②在动态存储区分配内存:运行时赋值
如果没有显式赋值,所以每次运行程序都会给该变量重新赋值,随机值
int main(){
int a;
printf("%d",a);//使用前未初始化,是自动变量,编译器不会自动初始化,所以是随机值
}
如果显式赋值,每次运行程序执行该赋值代码,都会重新赋给定值
③作用域:代码块中
③静态变量:
1.静态变量:
声明变量的存储类型:
static 数据类型 变量名;
2.特点:静态全局变量,静态局部变量
①生存期:生存期相同,在静态存储区分配内存,与程序共存亡.从程序运行起占据内存,程序退出时释放内存
所以当程序离开函数,但是未结束程序,值仍保留
②在静态存储区分配,即编译时分配内存初始化.
a.没有显式初始化的外部变量,编译程序自动初始化为0
int main(){
static int a;
printf("%d",a);//使用前未初始化,但是是静态变量,编译器在编译阶段自动初始化为0
}
b.自己初始化变量,编译时赋该值.每次运行代码,执行此代码都会跳过.因为只在编译时初始化,且只初始化一次
③作用域:
静态局部变量----局部作用域只对定义自己的函数可见
静态全局变量----作用于定义它的程序文件,但是不能作用于项目里的其它文件
④寄存器变量:
1.寄存器变量:
声明变量的存储类型:
register 类型名 变量名;
2.特点:
①生存期:与程序“共存亡”,在CPU的内部
②适用于使用频率较高的变量,可使程序更小、执行速度更快
现代编译器有能力自动把普通变量优化为寄存器变量,且可以忽略用户的指定,一般无需特别声明变量为register
9.数组:
9.1 为什么使用数组?
例1:计算100个学生的平均值
方案1:
int score, i, sum = 0;
for (i=0; i<100; i++)
{
scanf("%d", &score);
sum = sum + score;
}
aver = sum / 100;
缺点:只能看出最后一个学生的成绩
方案2:
int score[100], i;
for (i=0; i<100; i++)
{
scanf("%d", &score[i]);
}
优点:保存大量同类型的相关数据
9.2 数组的定义:
(1)一维数组:
1.一维数组的定义:
数据类型 数组名[数组长度];
说明:
①一个"[]"表示一维
②如果是字符数组,还要多一个单元存储结束标识"\0"
①数据类型:
1.数组的数据类型----每一元素占内存空间的字节数,分配多少内存(图1)
一维数组在内存中占用的字节数为:
数组长度× sizeof(基类型)
2.数组的存储类型----在哪里分配内存(内存的动态、静态存储区或CPU的寄存器)图2
②数组名:
1.注意:
①数组名也是标识符,唯一标识一个数组
②在内存中分配连续的存储空间给数组,数组元素的下标从0开始,数组名a代表首地址a[0].
地址:a+i*sizeof(ElemType)(下标从0开始)
a+(i-1)*sizeof(ElemType)(下标从1开始)
2.为什么数组下标从0开始?
使编译器的实现简化一点,且下标的运算速度少量提高
③数组长度:
1.注意:
①数组长度不能是变量,只能用整型常量.
int n=10;
int a[n];(×)
②数组长度可能会被修改,最好使用宏定义
#define N 11
int a[N];
2.如果希望下标从1到10而非从0到9,怎么办?
int a[11];//从a[1]开始使用
④逻辑存储结构:
①一维数组:只需要一个下标就可以表示数组元组
int a[5]; //用一行来表示逻辑存储结构
⑤物理存储结构:
(2)二维数组:
1.逻辑结构:
①二维数组用两个下标来确定
②存放顺序:按行存放,线性存储.
所以已知每行列数才能正确读出数组元素
2.物理结构:
存放顺序:按行存储,线性存储
所以已知每行列数才能正确读出数组元素
3.定义:
1.语法:
类型名 数组名[ 行常量表达式 ][ 列常量表达式];
注意:
①一个二维数组在本质上是有多个一维数组构成.所以每一个一维数的大小必须相同
②我们可以看作是几行一维数组构成,一维数组的长度就是列数
9.3 数组初始化:
(1)一维数组:
1.未初始化的数组元素值是什么?
(1)静态数组和全局数组在静态存储区分配内存,就是在程序编译时,自动初始化为0值;
int arr[1024];//都是0
int main(void)
{
}
(2)动态数组没有初始化,是在动态存储区分配内存,就是在程序运行时,随机数
int main(void)
{
int a[5];//随机数
}
==============================================
2.一维数组的初始化
(1)动态局部数组初始化:
①全部初始化:没有固定长度,根据初始化元素的个数确定数组长度.
int a[5] = {62, 74, 56, 88, 90};
int a[] = {62, 74, 56, 88, 90};
int a[5]={1}(×)//不能整体赋他值,但是可以整体赋值为0
int a[5]={1,1,1,1,1}
②部分初始化:如果一个数组,固定长度.初值列表提供的元素个数不能超过数组长度;但可以小于数组长度,部分被初始化,没有被初始化的元素会被编译器自动设置为相应类型的0
int a[5] = {62, 74};//{62, 74,0,0,0}
注意:只给数组部分元素初始化,不能省略长度
(2)更高效的数组初始化方法:
memset(数组名, 初始值, sizeof(数组名));
用sizeof(a)来获得数组a所占的内存字节数
加上#include <string.h>
(2)二维数组:
①全部初始化:第二维长度不能省略,根据给定的初值个数计算行数,行数=初值个数/列数
int a[][4]={1,2,3,4,5,6,7,8};
//int a[3][4]={1,2,3,4,5,6,7,8};
②按行初始化:每个"{}"代表一行,"{}"里面的每个值就是该行的每一列,所以"{}"初值个数<=第二维长度,没有赋值的编译器自动设为0
int a[][4]={{1,2,3},{2,4},{1}}
9.4 数组的访问:
(1)访问一维数组:
一维数组的引用:
①数组名[下标] //下标可以是常量
②允许快速随机访问
引用时下标允许是int型变量或表达式a[i]
(2)访问二维数组:
注意:
虽然有多种赋值方式,但是因为光标是不可能向前跳动,即从第二行变到第一行,所以想要正确的读出数据,应该按行输出
3.下标越界:
访问数组元素时,下标越界是大忌!
因为语法是没有错的,编译器通常不检查下标越界,导致程序运行时错误.下标越界,将访问数组以外的空间,那里的数据是未知的,不受我们掌控,可能带来严重后果
例1:一维数组越界:
#include <stdio.h>
int main()
{
int i, a = 1, c = 2, b[5] = {0};
printf("%p, %p, %p, %p\n", b, &c, &a, &i);
for(i=0; i<=8;i++)//b长度为5,只有b[4],越界访问
{
b[i] = i;
printf("%d ", b[i]);
}
printf("\nc=%d, a=%d, i=%d\n", c, a, i);
return 0;
}
我们可以观察物理结构就知道计算机内存中保存的内容了
例2:二维数组越界:
9.5 数组的赋值:
(1)一维数组:
如何使两个数组的值相等?
int a[4] = {1,2,3,4};
int b[4];
b = a;(×)//数组名代表首地址a[0],b[0]
1.逐个赋值:
b[0]=a[0];
b[1]=a[1];
b[2]=a[2];
b[3]=a[3];
如果没有对后面一个赋值,那么就是b[3]是随机数
因为此时是运行程序,而编译器只在编译阶段初始化为0
2.通过循环赋值:
int i;
for (i=0; i<4; i++)
{
b[i] = a[i];
}
3.更高效的赋值
memcpy(b, a, sizeof(a));//数组a复制给数组b
加上#include <string.h>
(2)二维数组:
注意:
虽然有多种赋值方式,但是因为光标是不可能向前跳动,即从第二行变到第一行,所以想要正确的读出数据,应该按行输出.
这样赋值相当于把二维数组转换成一维数组,具有随机存取的特点
a[M][N]:逻辑上声明了一个M行N列的二维数组
计算地址就是计算前面有几个元素
1.按行赋值:
先给第一行赋值,再给第二行赋值,即列是先变化的.
外层循环控制行,内层循环控制列.也就是先控制行不变,列变化.
a[i][j]的地址:LOC+(i*N+j)*sizeof(ElemType)
2.按列赋值:
先给第一列赋值,再给第二列赋值,即行是先变化的.
外层循环控制列,内层循环控制行.也就是先控制列不变,行变化.
a[i][j]的地址:LOC+(j*M+i)*sizeof(ElemType)
1.按行赋值
2.按列赋值:
3.按圈赋值:
控制走过的圈数 :(n+1)/2
①迭代法:
②递归法:
③控制格子数:
④控制边界:
9.6 数组做参数:
1.变量与数组做实参的区别?
①按值调用:
变量做实参传值是把变量的值赋值一份给形参,形参对该变量的操作与原变量无关.
②引用调用:
但是数组做实参是把数组变量中保存的地址传给形参,形参对该变量的操作都会修改该数组
2.数组作实参:
①直接把数组名传进去,数组名a就是该数组的首地址&a[0]
②数组既可以作实参,也可以作形参
(1)一维数组
1.技术控制:
2.标记控制:
(2)二维数组:
1.计算二维数组元素a[i][j]对于首地址的偏移量:
必须知道列数才能正确计算a[i][j]在数组中相对于第一个
元素的偏移位置
例:假设定义一个二维数组a[][n]
偏移量=i * n + j
元素地址:首地址+偏移量
注意:在声明函数的二维数组形参时,不能省略数组第二维的长度
2.什么时候用一维数组?
只有一个变量,使用一维数组.
通常不指定数组的长度,用另一个形参来指定数组的大小
例如:保存n个学生一门课程的成绩
int Average(int score[], int n);//score[]就是形参,传进去的数组赋值给score,score就代表整个数组
3.什么时候使用二维数组?
数据中有两个变量,用二维数组.
可省略数组第一维的长度,不能省略第二维的长度
例:void Average(int score[][COURSE_N], float aver[], int n);
//数组aver可保存每个学生的平均分,或每门课程的平均分
9.9 计算最大值:
9.10 查找算法
9.11 排序算法
9.12 应用:
(1)求素数:
10.指针:
10.1 为什么出现指针?
电脑维修师傅眼中的内存是这样的:
内存在物理上是由一组DRAM芯片组成的。
而作为一个程序员:
我们不需要了解内存的物理结构,操作系统将DRAM等硬件和软件结合起来,给程序员提供的一种对物理内存使用的抽象。这种抽象机制使得程序使用的是虚拟存储器,而不是直接操作物理存储器。所有的虚拟地址形成的集合就是虚拟地址空间。
也就是说,虚拟存储器是一个很大的,线性的字节数组(平坦寻址)。每一个字节都是固定的大小,由8个二进制位组成。最关键的是,每一个字节都有一个唯一的编号,编号从0开始,一直到最后一个字节。如上图中,这是一个4GB的虚拟存储器的模型,它一共有4x1024x1024x1024 个字节,那么它的虚拟地址范围就是 0 ~ 4x1024x1024x1024-1 。
由于内存中的每一个字节都有一个唯一的编号,因此,在程序中使用的变量,常量,甚至数函数等数据,当他们被载入到内存中后,都有自己唯一的一个编号,这个编号就是这个数据的地址。指针就是这样形成的。
指针的值实质是内存单元(即字节)的编号,所以指针单独从数值上看,也是整数,他们一般用16进制表示。
指针的值(虚拟地址值)使用一个机器字的大小来存储,也就是说,对于一个机器字为w位的电脑而言,它的虚拟地址空间是0~2^w-1 ,程序最多能访问2^w个字节。这就是为什么xp这种32位系统最大支持4GB内存的原因了。
我们可以大致画出变量ch和num在内存模型中的存储。(假设 char占1个字节,int占4字节)
10.2 如何对变量进行寻址:
当我们在代码中访问变量时:
有两种找该变量的内存地址的方式
(1)直接寻址:
①直接寻址:直接到变量名标识的存储单元中,读取变量的值
#include <stdio.h>
int main()
{
int a = 97;
scanf("%d",&a);
printf("a=%d\n", a);//去a的内存中读取4个字节
return 0;
}
1.输入数据时忘记使用取地址运算符&,这样会如何?
a的值被当作地址。
如a值为100,则输入的整数就会从地址100开始写入内存,但是地址100可能是只读存储区,不能被写入
#include <stdio.h>
int main()
{
int a = 97;
scanf("%d", a);//a的值就被解释为一个地址
printf("a=%d\n", a);
return 0;
}
1、内存的数据:
内存的数据就是变量的值对应的二进制,一切都是二进制。比如:
97的二进制是 : 00000000 00000000 00000000 0110000 , 但使用的小端模式存储时,低位数据存放在低地址,所以图中画的时候是倒过来的。
2、内存数据的类型:
内存的数据类型决定了这个数据占用的字节数,以及计算机将如何解释这些字节。
比如:
num的类型是int,因此将被解释为一个整数。相同的内存数据以不同的数据类型去解析的时候,会得到不同的值,所以数据的类型是非常重要的。
3、内存数据的名称:
内存的名称就是变量名。实质上,内存数据都是以地址来标识的,根本没有内存的名称这个说法,这只是高级语言提供的抽象机制 ,方便我们操作内存数据。而且在C语言中,并不是所有的内存数据都有名称,例如使用malloc申请的堆内存就没有。
比如:我们定义的a
4、内存数据的地址:
如果一个类型占用的字节数大于1,则其变量的地址就是其占用的所有字节的地址值最小的那个字节的地址。
因此num的地址是 0028FF40。 内存的地址用于标识这个内存块。
5、内存数据的生命周期:
num是main函数中的局部变量,因此当main函数被启动时,它被分配于栈内存上,当main执行结束时,消亡。
如果一个数据一直占用着他的内存,那么我们就说他是“活着的”,如果他占用的内存被回收了,则这个数据就“消亡了”。C语言中的程序数据会按照他们定义的位置,数据的种类,修饰的关键字等因素,决定他们的生命周期特性。实质上我们程序使用的内存会被逻辑上划分为:栈区,堆区,静态数据区,代码区。不同的区域的数据有不同的生命周期和读写权限。
(2)间接寻址:
②间接寻址:让其他变量保存该变量的地址,通过其他变量得到变量的值
1.用什么数据类型去理解它所指向的存储单元中的数据呢?
指针类型.指针是程序数据在内存中的地址,而指针变量是用来保存这些地址的变量。
10.3 如何使用:
(1)定义
1.如何定义指针?
数据类型 *变量名;//*作用是定义指针
说明:
①地址就是指针
②*变量名,就代表一个指针,指针名字是变量名.
该变量名存储了一个地址,是指针变量
2.指针四要素:
指针的类型,指针所指向的类型,指针的值,指针本身所占据的内存区
①如何找到指针的数据类型?
根据语法,去掉变量名,就是指针的类型,这是指针本身所具有的类型
例:
int *ptr; //指针的类型是int *
int **ptr; //指针的类型是 int **
int*(*ptr)[4];//指针的类型是int*(*)[4]
②指针指向的数据类型:
当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
同样的地址,因为指针的类型不同,对它指向的内存的解释就不同,得到的就是不同的数据
从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型
例:
int *ptr; //指针所指向的类型是int
int (*ptr)[3]; //指针所指向的的类型是 int()[3]
int *(*ptr)[4]; //指向的类型是 int *()[4]
③指针的值:或者叫指针变量保存的的内存区或地址---指针的指向
指针所指向的内存区和指针所指向的类型是两个完全不同的概念.
指针所指向的内存区:就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。
以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址
④指针本身所占据的内存区
你只要用函数 sizeof(指针的类型)测一下就知道了。在32位平台里,指针本身占据了4个字节的长度
指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用
(2)初始化:
初始化:
当我们初始化一个指针或给一个指针赋值时,赋值号的左边是一个指针,赋值号的右边是一个指针表达式,指针表达式看后面.
(1)定义的同时初始化:
数据类型 *变量名=&变量;
(2)先定义后初始化:
数据类型 *变量名;
变量名=&变量;
说明:"&"是取地址符,表示得到该变量的地址,赋值给指针变量.
①也就是说指针指向了该变量;
也可以说指针指向了变量所在的内存块.
所以也可以说是该变量的指针
②使用%p格式符输出指针保存的地址
③特殊的赋值,不需要"&":
a.数组名的值就是这个数组的第一个元素的地址
b.函数名的值就是这个函数的地址
c.字符串字面值常量作为右值时,就是这个字符串对应的字符数组的名称,也就是这个字符串在内存中的地址
int add(int a , int b){
return a + b;
}
int main(void)
{
int arr[3] = {1,2,3};
int* p_first = arr;//数组名就是地址
int (*fp_add)(int ,int ) = add;
//常量字符串就是地址
const char* msg = "Hello world";
return 0;
}
简单的指针的赋值运算:
1.把一个变量的地址,赋予指向相同数据类型的指针变量
int a,*pa;
pa=&a; //把整型变量a的地址赋给指针变量pa
2.把一个指针变量的值,赋予指向相同数据类型的另一个指针变量
int a,*pa=&a,*pb;
pb=pa; //把a的地址赋予指针变量pb
3.把数组的首地址赋予指向数组的指针变量
int a[4],*pb;
pb=a;//数组名表示数组的首地址,故可赋予指向数组的指针变量pb
pb=&a[0]; //数组第一个元素的地址也是整个数组的首地址,
4.把字符串的首地址赋予指向字符类型的指针变量。例如:
char *pc;
pc="xianyu";
/*或用初始化赋值的方法写为:*/
char *pc="xianyu";
这里并不是把整个字符串装入指针变量, 而是把存放该字符串的字符数组的首地址装入指针变量。
5.把函数的入口地址赋予指向函数的指针变量。
int (*pf)();
pf=fun; //fun为函数名
(3)指针解引用:
1.如何访问指针变量指向的存储单元中的数据?
通过引用指针变量指向的变量的值----指针的解引用
2.语法:
数据类型 *变量名=&变量;
*指针变量;//此时*表示间接寻址作用
①实质是:从指针指向的内存块中取出这个内存数据,即访问指针指向的内容
只要指针指向一个变量,*p的结果是p所指向的东西
②一般指针变量的类型要和它指向的数据的类型匹配
例如:
int b=97;
int *a;
a=&b;//只要a指向b,那么*a就代表b
int main(void)
{
int num = 97;
int *p1 = #
char* p2 = (char*)(&num);
printf("%d\n",*p1); //输出 97
putchar(*p2); //输出 a
return 0;
}
①指针的值:保存了num的地址,其地址的值就是0028FF40,因此 p1的值就是0028FF40。数据的地址用于在内存中定位和标识这个数据,因为任何2个内存不重叠的不同数据的地址都是不同的
②指针的类型:指针的类型决定了这个指针指向的内存的字节数并如何解释这些字节信息。一般指针变量的类型要和它指向的数据的类型匹配。
由于num的地址是0028FF40,因此p1和p2的值都是0028FF40
*p1:将从地址0028FF40开始解析,因为p1是int类型指针,int占4字节,因此向后连续取4个字节,并将这4个字节的二进制数据解析为一个整数 97。
*p2:将从地址0028FF40 开始解析,因为p2是char类型指针,char占1字节,因此向后连续取1个字节,并将这1个字节的二进制数据解析为一个字符,即'a'。
(4)指针表达式:
1.什么是指针表达式?
一个表达式的结果如果是一个指针,那么这个表达式就叫指针表式.即如果表达式的结果是一个地址,那么该表达式就是指针表达式.
在理解指针表达式之前先有一个概念就是“左值”和“右值”.对于左值就是可以出现在赋值符号左边的东西,右值就是那些可以出现在赋值符号右边的东西。
进一步抽象可以这样理解:
①左值应该可以作为一个地址空间用来存放一个值
②右值可以作为一个值来处理,
③当然有些是既可以作为左值也可以作为右值
2.那么怎么判断是否可以作为左值?
当一个指针表达式的结果指针已经明确地具有了指针自身占据的内存的话,这个指针表达式就是一个左值;否则就不是一个左值
3.如果左值和右值的类型不一致,会出现错误?
也就是如果我们把一个int类型的数据存进去了,那么内存中就会存储这个数据的二进制,如果我们使用char*指针,那么从该地址只读取一个字节作为值,不是我们想要的数据.
①绝大多数情况下,指针的类型和指针表达式的类型是一样的
②如果不一样,可以进行强制转换
float f=12.3;
int *p;
p=(int*)&f;//&f是一个指针,因为里面存储的是地址
表达式 | 作为右值 | 作为左值 |
---|---|---|
ch | 指的是字符a | 地址标号为 0xA12 的这块内存 |
&ch | 变量ch所占内存的地址标号 0xA12 | 无效左值 |
ptr_ch | 变量ptr_ch的内容0xA12 | 地址标号为 0E12的这块内存 |
&ptr_ch | 指针变量ptr_ch所在地址标号 0E12 | 无效左值 |
*ptr_ch | 指针ptr_ch所指内存中的内容 | 指针ptr_ch所指的内存 |
*ptr_ch+1 | 变量ptr_ch所指内存中的内容加1后的值 | 无效左值 |
*(ptr_ch+1) | ptr_ch中内容加1后所指的内存中的内容 | ptr_ch所占内存中的内容加1后所指的内存 |
++ptr_ch | ptr_ch所指内存的下一个内存单元的地址 | 无效左值 |
ptr_ch++ | ptr_ch所指内存的地址 | 无效左值 |
*++ptr_ch | ptr_ch所指内存的下一个内存单元的内容 | ptr_ch所指内存的下一个内存单元 |
*ptr_ch++ | ptr_ch所指内存单元的内容 | ptr_ch所指内存单元 |
++*ptr_ch | ptr_ch所指内存中内容加1的值 | 无效左值 |
(*ptr_ch)++ | ptr_ch所指内存中内容 | 无效左值 |
++*++ptr_ch | ptr_ch所指内存下一个内存单元中的内容加1后的值 | 无效左值 |
++*ptr_ch++ | ptr_ch所指内存单元中的内容加1后的值 | 无效左值 |
10.4 指针类型:
1.空指针:
1.什么是空指针?
空指针----值为NULL的指针,即无效指针.
指针变量使用之前必须初始化;若你不知把它指向哪里,那就指向NULL(在stdio.h中定义为0)
2.p = 0 和 p = NULL 有什么区别吗?
p = NULL可以明确地说明p是指针变量,而不是一个数值型变量
3.空指针就是指向地址为0的存储单元的指针吗?
不一定.
①并非所有编译器都使用0地址
②某些编译器为空指针使用不存在的内存地址
2.坏指针:
1.什么是坏指针?
指针变量的值是NULL,或者未知的地址值,或者是当前应用程序不可访问的地址值,这样的指针就是坏指针.
2.注意:
①不能对坏指针做解指针操作,否则程序会出现运行时错误,导致程序意外终止。
②任何一个指针变量在做解地址操作前,都必须保证它指向的是有效的,可用的内存块,否则就会出错。
void opp()
{
int*p = NULL;
*p = 10; //不能对NULL解地址
}
void foo()
{
int*p;
*p = 10; //不能对一个未知的地址解地址
}
void bar()
{
int*p = (int*)1000;
*p =10;//不能对一个可能不属于本程序的内存的地址的指针解地址
}
3.void*:
由于void是空类型,因此void*类型的指针只保存了指针的值,而丢失了类型信息,我们不知道他指向的数据是什么类型的,只知道这个数据在内存中的起始地址.
如果想要完整的提取指向的数据,程序员就必须对这个指针做出正确的类型转换,然后再解指针。
前面已经提到过,数据的类型是正确解析出内存数据的关键,相同的内存数据以不同的数据类型去解析的时候,会得到不同的值。程序需要得到什么数据,不光要知道其地址,还要明确其类型,因此编译器不允许直接对void*类型的指针做解指针操作。
注意:void*和空指针不一样,void*是不知道指针的类型,但是知道指针的值;而空指针是指指针的值=NULL
10.5 函数和指针:
(1)指针作参数:
1.与普通类型数据作参数的区别:
基本类型的变量作函数参数 | 指针类型的变量作函数参数 |
---|---|
按值传递 | 按引用传递 |
实参变量的值 ->形参 | 实参变量的地址 ->形参 |
在被调函数中不能改变实参的值 | 在被调函数中可以改变实参的值 |
1.引用和指针的区别:
他们本质上来说是同样的东西。
①引用是编程语言提供给程序员的抽象机制,用于诸如Java,C#等在语言层面封装了对指针的直接操作的编程语言中
②指针是操作系统提供给软件开发模型的抽象机制,指针常用在C语言中
2.按值传递和按引用传递的区别:
其实传递参数时,都是把实参的值复制一份给形参,形参和实参只是在值上面一样,而不是同一个内存数据对象.
但是指针复制之后传过去的是地址,那么指针形参也就指向了该地址的变量,就可以直接对变量进行操作了
2.为什么用指针作参数:
优点:
①为函数提供了修改实参值的手段
普通数据传参是单向的,即从调用者传递给被调函数,而被调函数无法修改传递的参数达到回传的效果。
当然有时候我们可以使用函数的返回值来回传数据,但是如果返回值有其它用途(例如返回函数的执行状态量),或者要回传的数据不止一个,返回值就解决不了了.传递变量的指针可以轻松解决上述问题。
②有的时候,我们通过指针传递数据给函数不是为了在函数中改变他指向的对象.相反,我们防止这个目标数据被改变可以使用常量指针。传递指针只是为了避免拷贝大型数据。
(1)按值传参:
(2)按引用传参:
例1:
例2:两数交换
(2)什么是函数指针?
1.什么是函数指针?
每一个函数本身也是一种程序数据,一个函数包含了多条执行语句,它被编译后,实质上是多条机器指令的合集。在程序载入到内存后,函数的机器指令存放在一个特定的逻辑区域:代码区。既然是存放在内存中,那么函数也是有自己的指针的。
C语言中,函数名作为右值时,就代表了该函数在内存中的地址--即函数的指针
函数指针:就是指向函数的指针变量
2.定义函数类型指针:
数据类型 (*指针变量名)(形参列表);//指针可以指向一个函数,但是还没有赋值,不知道指向哪个函数
指针变量名=函数名;//该指针指向此函数
例如:
float Fun(float a, float b);//函数原型
float (*f)(float, float);//函数指针
注意:
①定义时的参数类型与指向的函数参数类型要一致,否则会发生错误
② 忘了写前一个() :
int *f(int, int);//声明了一个函数名为f、返回值是整型指针类型的函数
③忘了写后一个()
int (*f); //定义了一个整型指针变量
1.使用函数指针的好处:
2.应用:
1.函数指针的应用:
编写通用性更强的函数
2.主要应用:
①通用的计算任意函数定积分的函数
②通用的排序函数(既能升序,又能降序)
例1:计算定积分:
法1:不使用函数指针
法2:使用函数指针
例2:通用的排序(选择法)
10.6 const和指针:
1.const 修饰谁?谁才是不变的?
①如果const后面是一个类型----指针常量
则跳过最近的原子类型,修饰后面的数据。(原子类型是不可再分割的类型,如int, short , char,以及typedef包装后的类型)
②如果const后面就是一个数据,则直接修饰这个数据---常量指针
修饰谁,谁就不变
2.常量指针和指针常量?
①指针常量即指针类型是常量:
指针的指向不可以改,但指针指向的值可以改
②常量指针即是指向常量的指针:
指针的值可以改变(指向可以变),指针所指的地址中的内容为常量不能改变
int main()
{
int a = 1;
int const *p1 = &a; //const后面是*p1,那么 *p1就不可变。*p1表示a,此时a=1不可变。其他的都可变,p1的指向可变。
const int*p2 = &a;//const后面是int类型,则跳过int ,修饰*p2, 效果同上
int* const p3 = NULL; //const后面是数据p3,p3中保存的内容不可变,即p3永远等于NULL
const int* const p4 = &a; // 通过p4不能改变a 的值,同时p4本身也是 const
int const* const p5 = &a; //效果同上
return 0;
}
11.数组和指针的关系:
11.1 一维数组:
(1)计算:
1.算数运算:
指针可以加上或减去一个整数,但只有在数组中使用才是有意义的.也就是说只有指向数组的指针进行算数运算才有意义.所以当指针变量指向数组元素时,可以使用指针代替数组下标进行操作
假设p为某种类型的指针变量或者表达式,i是一个整数,
则凡是形如:p[i] 都等价于 *(p+i)
①与整数相加减:
1.指针+整数(一个地址+整数):
如果p指向a[i],则p+j指向a[i+j](前提是a[i+j]必须存在)
注意:
p+j是加j个存储单元,不是j个字节,而是取决于p的基类型.
例:
int a[10];
int *p;*q;
p=&a[2];//p指向a[i]
q=p+3;//结果指针p指向a[i+j]
2.指针-整数(一个地址-整数):
如果p指向a[i],那么p-j指向a[i-j](前提是a[i-j]必须存在)
②两个指针相减:
前提:两个指针指向同一个数组时,指针相减才有意义
当两个指针相减时(一个地址-一个地址):
p-q的结果为指针之间的距离i-j,用来计算数组中元素的个数
例如:
char a[10];
char *p,*q;
p=&a[0];
q=&a[9];
int len=q-p;//9
③自增:
指针的算术运算允许通过对指针变量重复自增来访问数组的元素:
①指针++(相当于 指针=指针+1)
②指针--(相当于 指针=指针-1)
例如:
int a[10];
int *p;
for(p=a;p<a+10;p++){//p=p+1
printf("%4d", *p);
}
2.关系运算:
两个指针指向同一个数组时,指针比较才有意义
– 比较的结果依赖于数组中两个元素的相对位置
如果p在q后面,p >= q 为真
例如:
char a[10];
char *p,*q;
p=&a[2];
q=&a[6];
printf(a[2] >a[6]);//false
3.sizeof
当对数组名使用sizeof运算符时,返回的是整个数组占用的内存字节数。当把数组名赋值给一个指针后,再对指针使用sizeof运算符,返回的是指针的大小。
int main(void)
{
int arr[3] = {1,2,3};
int *p = arr;
printf("sizeof(arr)=%d\n",sizeof(arr));
//sizeof(arr)=12
printf("sizeof(p)=%d\n",sizeof(p));
//sizeof(p)=4
return 0;
}
(2)指针和一维数组的关系:
1.一维数组元素的引用:
1.访问一维数组a的元素:
方式1:&a[i] 等价于 (a + i)//第i个元素的地址
方式2:a[i] 等价于 *(a + i)//第i个元素
原理:计算一个地址+一个整数
a+i,实际上根据数组首地址a[0],计算相对于首地址的偏移量i*sizeof(数组基类型) ,得到最终地址a + i*sizeof(基类型).所以(a+i)的结果是一个地址
2.①第i个元素值的表示(p的初值为a):
*(a+i) 数组名法//a就是&a[0]
*(p+i) 指针法//而定义时p=&a[0],二者一样
a[i] 下标法
p[i] 下标法//把p看作一个数组名
②第i个元素地址的表示(p的初值为a,p=&a[0]或p=a):
a+i 数组名法
p+i 指针法
&a[i] 下标法
&p[i] 下标法
注意:
①数组名a代表数组的首地址&a[0]
int *p=&a[0];//int *p=a;相当于p表示一个数组名,那么可以用p的下标表示数组元素
②用下标形式访问数组元素的本质:计算该元素在内存中的地址
③通过指针变量和数组名访问元素的重要区别:
指针变量是地址变量,其指向由所赋值确定;数组名是地址常量(指针常量),恒定指向数组的第1个元素。
2.为什么一个int型指针能指向一个int型的一维数组呢?
int a[5];
int *p = a;//相当于int *p = &a[0];
&a[0]是int型元素的地址,p是int型指针,基类型是int
p的基类型与它指向的元素类型相同,可以表示
3.访问一维数组元素:
①下表法:
#include <stdio.h>
int main()
{
int a[5], i;
for (i=0; i<5; i++)
{
scanf("%d", &a[i]);
}
for (i=0; i<5; i++)
{
printf("%4d", a[i]); //第i个元素的下标
}
printf("\n");
return 0;
}
②数组名法:
#include <stdio.h>
int main()
{
int a[5], i;
for (i=0; i<5; i++)
{
scanf("%d", a+i);
}
for (i=0; i<5; i++)
{
printf("%4d", *(a+i)); //根据数组名计算偏移量
}
printf("\n");
return 0;
}
③下标法:
#include <stdio.h>
int main()
{
int a[5], i, *p = NULL;
p = a;
for (i=0; i<5; i++)
{
scanf("%d", &p[i]);
}
p = a;
for (i=0; i<5; i++)
{
printf("%4d", p[i]);
}
printf("\n");
return 0;
}
④指针法:
#include <stdio.h>
int main()
{
int a[5], *p = NULL;
for (p=a; p<a+5; p++)
{
scanf("%d", p);
}
for (p=a; p<a+5; p++)
{
printf("%4d", *p);
}
printf("\n");
return 0;
}
4.*和++混合使用:
表达式 | 含义 |
---|---|
p++或者(p++) | 自增前表达式的值是 p ,以后再自增p |
(*p)++ | 自增前表达式的值是 p ,以后再自增 p |
++p或者(++p) | 先自增 p,自增后表达式的值是p |
++*p或者++(*p) | 先自增 p,自增后表达式的值是p |
改变*p的值:
printf("%d\n", ++(*p));//输出表达式的结果,先计算(*p)先读取*p的值,再+1,输出结果-->7
printf("%d\n", (*p)++);//输出表达式的结果,表达式结果是*p即读取*p的值,输出之后再+1--->6
改变p的值(改变p的指向):
printf("%d\n", *p++);//都是一元运算符,看结合性.都是右结合性,所以相当于printf("%d\n", *(p++));输出表达式的结果,先计算(p++),再解引用,表达式结果是*p,输出*p,再p+1--->
(3)数组和一维数组做函数参数:
1.数组作参数:
2.指针作参数:
传参注意事项:
①被调函数的形参声明为数组类型,用下标法访问数组元素
②被调函数的形参声明为指针类型,用指针算术运算访问数组元素
③当想要往被调函数中传一个数组时,在主函数中直接把数组名做实参即可.没有必要定义一个指针指向该数组,把指针作为实参.因为数组名就是一个地址,也就相当于一个指针.如下图
④一般情况下,一维数组和指针作形参是等同的.
但是sizeof(数组名)和sizeof(指针变量名)不一样
(4)数组指针和指针数组:
1.指针数组:即用于存储指针的数组,也就是数组元素都是指针
例:
int* a[4]//指针数组
表示:数组a中的元素都为int型指针.(假如没有*,那么就是数组a中全为int 类型的数据)
元素表示:*a[i]--->*(a[i])是一样的,因为[]优先级高于*
2.数组指针:即指向数组的指针
例:
int (*a)[4]//数组指针
表示:指向数组a的指针.(假如是int (*a),那么就是指向整型a的指针)
元素表示:(*a)[i]
11.2 二维数组:
二维数组在内存中按行存储,但可以以两种方式看待它.
二维数组的行指针和列指针.关键是:一个x型的指针指向x型的数据
(1)行指针:
1.理解行指针:
1.利用行指针理解二维数组:
S1.将a看成一维数组,有2个"int[3]型"元素,a包含2个元素a[0],a[1],实际上并不存在该类型.那么a是一维数组数组名,代表首地址,也是第0行的地址---行地址.
因为此时基类型是"int[3]型",所以a+i代表第i行的地址.
获取第i行元素:a[i] 等价于 *(a+i)//a[i]是一个数组名,则a[i]也表示第i行第0列元素的地址
获取第i行地址:&a[i] 等价于 (a+i)
S2.a[0],a[1]又分别是一个一维数组,包含3个元素
因为a[0]是一个数组,那么a[0]是第一个数组的第一个元素的首地址
获取第i行第j个元素:a[i][j] 等价于 *(*(a+i)+j)
获取第i行第j个元素的地址:&a[i][j] 等价于 *(a+i)+j
分析:*(*(a+i)+j)
S1. a+i是第i行地址,
S2. *(a+i),取出第i行元素就是a[i],也就是第i行数组名(*把行地址转换为列地址),即第i行第0列地址
S3. *(a+i)+j就是指向第i行第j列的地址
S4. *(*(a+i)+j)就是得到第i行第j列的元素值,即a[i][j]
2.定义初始化:
1.定义行指针:
int (*p)[3];//行指针,基类型“int[3]型”
2.初始化:给其赋一维数组
int a[2][3]={{1,2,3},{4,5,6}};
p = a;//a看作是一个一维数组里面有两个行元素a[0],a[1],那么数组名a代表第一个行元素首地址,相当于p=&a[0];指向第0行的“int[3]型”元素
3.行指针访问数组元素:
访问m行n列数组:
for (i=0; i<m; i++) //行下标变化
{
for (j=0; j<n; j++)//列下标变化
{
printf("%d", *(*(p+i)+j));
}
}
(2)列指针:
1.理解列指针:
将a看成一个一维数组,有6个"int型"元素,.就不区分行和列.那么a[0]就是数组名,a[0]表示一个数组
分析:*(a+i*n+j)
i*n+j表示偏移量:在a[i][j]前面有i行元素,每行元素有n列,并且本行前面有j个元素.
最终地址:首地址+偏移量=a+i*n+j
*(a+i*n+j):就是对最终地址解引用得到元素值
把a看作一维数组,只能有一个下标,所以是a[i*n+j]
访问元素:a[i*n+j] 等价于 *(a+i*n+j)
访问地址:a+i*n+j
2.定义及初始化:
1.定义:
int *p; //列指针,基类型是int型
2.初始化:
int a[2][3]={{1,2,3},{4,5,6}};
p = &a[0][0];//a看作一个一维数组,也就是第一个元素的首地址.也可以用一个数组名a[0]表示,也等价于*a,a看作一个一维数组,那么a是第0行地址,那么*把行地址转化为列地址就是第0行第0列地址,即&a[0][0]
3.列指针访问数组元素:
已知首地址,根据相对偏移量逐列查找:
for (i=0; i<m; i++)
{
for (j=0; j<n; j++)
{
printf("%d", *(p+i*n+j));
}
}
(3)二者的区别:
1.概念理解:
行指针:指的是一整行,不指向具体元素。
列指针:指的是一行中某个具体元素。
可以将列指针理解为行指针的具体元素,行指针理解为列指针的地址
2.二者之间的具体转换是:
*行指针----列指针
&列指针----行指针
行指针 | 转换成列指针 | 列指针等价表示 | 内容 | 内容等价表示 | 含义 |
---|---|---|---|---|---|
a或a+0 | *a | a[0] | *a[0] | *(*a) | a[0][0] |
a+1 | *(a+1) | a[1] | *a[1] | *(*(a+1)) | a[1][0] |
a+2 | *(a+2) | a[2] | *a[2] | *(*(a+2)) | a[2][0] |
列指针 | 行指针 | 等价表示 | 含义 |
---|---|---|---|
a[0] | &a[0] | &a或&(a+0) | 第0行 |
a[1] | &a[1] | &(a+1) | 第1行 |
a[2] | &a[2] | &(a+2) | 第2行 |
(4)二维数组作参数:
1.普通的指针:
2.行指针:
3.列指针:
(5)嵌套定义:
嵌套定义:
1.怎么判断标识符的类型?
S1.抓住其中的标识符,从标识符开始,①从内向外,②同一层按照优先级③同一优先级,看结合性,一层层往外看,
S2.同时心里想着,外层描述了内层(先判断派生类型,再判断基类型)最终描述的都是标识符
比如:
①确定了标识符是指针,就要判断它的基类型是什么--指针指向什么
②确定了标识符是数组,就要判断它的基类型是什么--每个元素是什么
2.注意:
①结合方向只有三个是从右往左,其余都是从左往右
一个是单目运算符,另一个是三目运算符,还有一个就是双目运算符中的赋值运算符"="
②逗号运算符的优先级最低。
③对于优先级,有一个普遍规律:
算数运算符>关系运算符>逻辑运算符>赋值运算符。逻辑运算符中的“逻辑非!”除外
例:
int a;//标识符是a,向外是int,int描述了a,即a是一个整形变量
int *a;//标识符是a,向外是*,*描述了a,即a是一个指针,这是内层; 外层还有一个int,这个int描述了内层的(指针a). 也是说 指针a是个整型指针
int *a[5];//标识符是a,外层用两个描述符,一个是*,一个是[],显然[]的优先级高,所以先看a[5],显然这是把a描述为一个大小为5的数组,至于数组元素是什么类型,目前还不知道,但是再往外看,发现是*(a[5]), 这个*又把a[5]这个数组描述为指针数组; 但是,是什么指针我们还不知道,于是再向外看,发现是int, 这个int 又把前面的指针描述为整型指针。所以最后我们说这是一个数组元素为整形指针的大小为4的数组,数组名是a. a[1]是一个整型指针,而数组名a 本身也是一个地址。这里定义的是5个指针
int (*a)[5];//a是个指针(括号的优先级最高),什么指针呢?或者说这个指针指向什么呢? [5]将其描述为一个数组,于是知道是一个数组指针,意思就是一个指向有四个元素的数组的指针,注意,a 只是一个指针,不是5个指针,此时 a+1又是什么意思? 同理int (*a)[5][5] 也知道是什么了吧,这可是一个指针,并不是5*5个指针
void (*a[4])(int *x);//从标识符a开始。看a[] ,[4]将a定义为了一个数组。 什么数组呢? 看*a[4], *又把这个数组的元素定为了指针,也就是说是个指针数组,这个数组有4个元素,a[0], a[1], a[2], a[3], 每个元素都是一个指针。 至此,我们依然不知道这是些什么指针,也就问:这些指针指向什么呢? 那就再向外层看吧。 看到了(*a[4])(); 最右边的()又把这个指针定义成了函数指针。于是又有函数所必须考虑的问题 参数和返回值。 显然参数是 int *x. 返回值是:void.
12.字符串:
12.1 字符串输入输出:
C语言规定字符串以空字符结尾,所以如果是一个字符串数组,必须比要保存的内容多一个单元,来存储字符串结束标志
(1)逐个输入输出:
#define STR_LEN 80
char str[STR_LEN+1];
for (i=0; str[i]!='\0'; i++)
{
putchar(str[i]);
}
putchar('\n');
注意:一般不用字符串长度控制,因为可能我们定义了长度为10,但是实际上只存储了6个字符,那么此时通过字符串定义的长度输出,就会访问未知的元素.
(2)整体输入输出:
1.scanf()
①使用
#define STR_LEN 80
char str[STR_LEN+1];
scanf("%s",str); //以%s输入,不能输入带空格的字符串
printf("%s\n",str);
②为什么不能输入带空格的字符串?
用d输入数字或%s输入字符串时,在开始读把输入缓冲区里面的数据之前,会跳过空格、回车或制表符等空白字符,再次遇到这些字符时,系统认为读入结束,那么空格后面的字符虽然输入了,但是不会存储到变量里面.
#include <stdio.h>
#define STR_LEN 80
int main()
{
char str[STR_LEN+1];
printf("Input a string:");
scanf("%s", str);
printf("%s\n", str);
return 0;
}
③处理回车:
scanf()不读走回车符,回车仍留在缓冲区中
④使用scanf输入字符串:
2.gets()
①使用:
#define STR_LEN 80
char str[STR_LEN+1];
gets(str); //可以输入带空格
puts(str);//可以自动换行
②为什么能输入带空格的字符串?
gets()以回车换行作为终止符,当可输入带空格的字符串,因为空格和制表符都是字符串的一部分,不会认为输入结束,会继续把输入缓冲区里面的空格保存到变量里面
#include <stdio.h>
#define STR_LEN 80
int main()
{
char str[STR_LEN+1];
printf("Input a string:");
gets(str);
printf("%s\n", str);
return 0;
}
③处理回车:
gets():
可以将回车从缓冲区读走,但不作为字符串的一部分,用空字符代替
getchar():
getchar()可以读取回车符,并且作为字符串输出
④使用gets输入字符串:
12.2 字符串的表示和存储:
(1)字符串常量:
1.存储形式:
1.一个字符串:
用双引号括起的一串字符是字符串常量,系统自动为其添加空字符----'\0'.(空字符就是ASCII码值为0)
2.表示:
1.含有转义字符:
转义字符被当做一个字符,并且具有一定的功能
printf("How are you.\n");
printf("\"How are you.\"\n");
2.如果字符串太长,怎么表示?
①在行尾+"\" ,但是第二行开头如果有空格,也被当作字符串里面的内容.所以如果有缩进格式,不适合用该方法
②直接把一个长字符串分成两个短字符串,放进不同行.编译器会自动连接,可以有缩进格式
(2)字符串变量:
1.表示:
C语言没有提供专门的字符串数据类型,但是可以用字符数组来表示.即:每个元素都是字符类型的数组
字符数组中的每个元素来表示字符串中的每个字符.
注意:
①是字符数组,但不一定代表字符串.数组的最后一个元素必须是'\0'才表示字符串
2.字符数组:
①一维数组:
1.定义:
#define STR_LEN 80
char str[STR_LEN+1];
注意:①把字符数组可以存储的个数定义为宏常量,保留一个单元存'\0'
②一维数组只能存储一个字符串
2.初始化:
①用字符常量的初始化列表对数组初始化:最后一个赋值为'\0'
char str[6] = {'C','h','i','n','a','\0'};
②用字符串常量直接对数组初始化:会自动添加'\0'
a.全部初始化:
char str[6] = {"China"};
char str[6] = "China";
char str[ ] = "China";
b.部分初始化:字符串中字符个数<=可存储元素个数,部分初始化,多余的单元自动初始化为0;如果字符串中字符个数>可存储元素个数,多余的就丢失了
char str[10] = "China";
②二维数组:
1.定义二维数组:
char country[][10] = {"America", "England", "Australia", "China", "Finland"};
注意:
①定义二维数组不能省略第二维的长度,根据给定个数初始化
②必须把最长的字符长度作为数组的第二维长度,但是因为字符串长度不一样,造成空间浪费
2.为了解决上面的问题:
保存一个参差不齐的字符串集合,使用指针数组
3.字符指针:
1.什么是字符指针?
字符指针就是指向字符串首地址的指针.就是定义一个字符指针,使其指向一个字符串常量
2.定义即初始化:
char *pStr;
pStr="Hello China";
4.二者的区别:
①字符指针
1.可以修改指针的值,不能通过指针修改字符串的值
pStr是一个指向常量存储区中的字符串的指针变量,所以可修改pStr的值(指向).因为字符串是常量,保存在常量存储区里面,只读操作,所以不可以对它所指向的存储单元进行写操作
char *pStr;
pStr="Hello China";
*pStr = 'W';(×)
②字符数组
2.数组中的字符可以被修改,但是数组名的值不能被修改
如果在数组函数外定义,或定义为静态数组,字符串保存在静态存储区;函数内定义,保存在动态存储区.数组名就是字符串常量的首地址,是常量,一旦产生字符串,首地址就不能改变,因此数组名的值不能被改变.
但是字符数组中的元素可以被修改
char str[]="Hello China";
str="Hello China";(×)
str[0]='W';//数组中存储的字符可被修改
5.字符指针指向数组:
1.什么是字符指针数组?
每一个数组元素都是一个指向字符串的指针
①指向一维数组:
1.使用字符指针和数组:
char str[] = "Hello China";//定义字符数组
char *pStr;//定义字符指针
pStr = str;//字符指针指向字符数组,pStr = &str[0];
*pStr = 'W';//修改字符串
str[0] = 'W';
pStr[0]= 'W';
2.正确使用字符指针:
①明确字符串被保存在哪里
②明确字符指针指向谁
②指向二维数组:
1.定义:
char *country[] = {"America", "England","Australia", "China", "Finland"};
//指针的类型:char *[]
//指针指向的类型:char [],是字符数组
2.访问数组元素:
for (i=0; i<5; i++)
{
printf("%s\n", country[i]);//行指针
}
12.3 字符串处理函数:
#include <string.h>
函数声明在此文件中.
strlen(字符串);
strcpy(目的字符数组, 源字符串);
strcat(目的字符数组, 源字符串);
strcmp(字符串1, 字符串2);
1.计算字符串长度
控制字符串的输出:
#define STR_LEN 80
char str[STR_LEN+1];
方案1:通过判断"\0"
for (i=0; str[i]!='\0'; i++)
{
putchar(str[i]);
}
putchar('\n');
方案2:通过计算字符串实际长度
len = strlen(str);//计算的是字符串实际长度
for (i=0; i<len; i++)
{
putchar(str[i]);
}
putchar('\n');
2.字符串拷贝:
1.strcpy(目的字符数组, 源字符串);
例如:strcpy(str2, str1);
①把str1赋值给前面的str2中
②str2数组必须足够大
③str1字符串可以是一个字符串常量,也可以是一个字符数组
④返回值是指向str2的一个指针
strcpy(str2, strcpy(str1, "Hello"));//多重拷贝
2.字符串能否用赋值运算符(=)整体复制?
str2 = str1;//赋值表达式左边必须是一个变量,但是str2是一个数组名,是常量
3.strncpy(str2, str1, n):只拷贝前n个字符
比前面的函数更安全
3.字符串连接:
1.strcat(目的字符数组, 源字符串):
例如:strcat(str2, str1);
strcat(str2, strcat(str1, "Hello"));//多次连接
注意:
①str2数组必须足够大
②返回值时指向str2的字符指针
2.strncat(str2, str1, n);
比前面的函数更安全
4.字符串比较:
1.strcmp(字符串1, 字符串2)
例如:if (strcmp(str2, str1) == 0)
返回值=0,表示str1和str2相等
返回值>0,表示str1>str2
返回值<0,表示str1<str2
2.字符串能否用关系运算符>,<,==直接比较大小?
if (str2 == str1),不能达到我们的要求,这里比较的是str1和str2的值,也就是两个数组名的值,就是比较字符串的地址.所以比较字符串必须使用函数
3.strncmp(str2, str1, n):比较前n个字符
4.执行原理:
依次比较两个字符串的字符,如果相等比较下一个字符;当出现第一对不相等的字符时,就由这两个字符决定所在字符串的大小,
返回其ASCII码比较的结果值(差值)
12.4 字符串作参数:
无论是哪种作实参,都是按引用传递.传字符串的首地址,而非字符串中的全部字符
(1)字符数组做参数:
1.const修饰谁?
保护指针变量指向的内容不被修改.因为const修饰str[],传过来数组名,也就是.
这样,在被调函数中不能通过数组改变数组元素
(2)字符指针做参数:
1.计算字符串长度:
优化:
1.const修饰谁?
保护指针变量指向的内容不被修改.因为const修饰*pStr,*pStr是指针指向的地址保存的内容,也就是内容不可变.
这样,在被调函数中不能通过指针改变该内容
2.字符串复制:
12.5 返回字符串:
返回字符串,字符指针和字符数组都可以做参数,但是数组不能作为函数的返回值
指针,数组,及其他类型:
1.基本数据类型
int、long、char、short、float、double……
2.派生类型:
①数组是一种从其他类型派生的数据类型
每个元素都有一个类型---称为数组的基类型
②指针是一种从其他类型派生的数据类型
指向X型变量的指针---X称为指针的基类型
注意:任何类型都可作为指针或数组的基类型
一个类型派生出新的类型,新的类型又派生出新的类型
例如:
①用数组作为指针的基类型——指向数组的指针
例如:int (*p)[5];
p--->*--->int [5]
②用指针作数组的基类型——指针数组,元素为指针类型的数组
例如:char *p[5];
p--->[5]--->char *
13.结构体:
13.1 为什么引入结构体类型?
1.在程序里表示一个学生(学号、姓名、性别…),怎么表示?
long studentID;
char studentName[10];
char studentSex;
2.如果表示多个学生的信息呢?
long studentID[30];
char studentName[30][10];
char studentSex[30];
使用多个数组存储信息的缺点:
分配内存不集中,结构零散,内存管理困难,寻址效率不高
对数组赋初值时,易发生错位
因此,我们提出了结构体类型--可以自己定义的数据类型
13.2 数组和结构体类型的区别:
数组存放信息与结构体存储信息的区别:
结构体类型:逻辑相关但类型不同的数据放在一起存储
数组:相同类型的数据单独放在一起存储
13.3 使用结构体:
(1)声明结构体类型:
1.声明结构体类型:
struct 标识符{
数据类型 变量名;
}
说明:
struct后面的标识符是结构体类型的名字,也叫结构体标签
里面的每一个变量叫做结构体类型的成员
例:
struct student{
long studentID;
char studentName[10];
char studentSex;
int yearOfBirth;
int scoreMath;
int scoreEnglish;
int scoreComputer;
int scoreProgramming;
};
struct student{
long studentID;
char studentName[10];
char studentSex;
int yearOfBirth;
int score[4];//4门课程的成绩
};
(2)命名结构体:
1.方式1:
方式1:使用结构体标签
struct student
{
long studentID;
char studentName[10];
char studentSex;
int yearOfBirth;
int score[4];
};
2.方式2:
方式2:使用 typedef 给数据类型起别名
注意:typedef为已存在的类型起别名,并未定义新类型
那么此时别名就相当于 struct 标识符
1.不能省略结构体标签
struct student
{
long studentID;
char studentName[10];
char studentSex;
int yearOfBirth;
int score[4];
};
typedef struct student STUDENT;
==============================================
2.可以省略结构体标签
写法1:
typedef struct student
{
long studentID;
char studentName[10];
char studentSex;
int yearOfBirth;
int score[4];
}STUDENT;
写法2:
typedef struct
{
long studentID;
char studentName[10];
char studentSex;
int yearOfBirth;
int score[4];
}STUDENT;
定义结构体变量:
struct student stu1;
STUDENT stu1;
(3)定义结构体变量:
1.方式1:
方式1:先定义结构体类型再定义变量名
struct student{
long studentID;
char studentName[10];
char studentSex;
int yearOfBirth;
int score[4];//4门课程的成绩
};
struct student stu1;
2.方式2:
方式2:在定义结构体类型的同时定义变量
struct student{
long studentID;
char studentName[10];
char studentSex;
int yearOfBirth;
int score[4];//4门课程的成绩
}stu1;
3.方式3:
方式3:直接定义结构体变量(不指定结构体标签)
相当于没有给该类型起名字,如果定义的另一个结构体的成员变量和它一样,那么就会产生错误.并且如果形参是该类型,也没办法声明类型.
struct
{
long studentID;
char studentName[10];
char studentSex;
int yearOfBirth;
int score[4];//4门课程的成绩
}stu1;
struct
{
long studentID;
char studentName[10];
char studentSex;
int yearOfBirth;
int score[4];//4门课程的成绩
}stu2;
(4)初始化结构体变量:
1.声明结构体类型:
typedef struct student
{
long studentID;
char studentName[10];
char studentSex;
int yearOfBirth;
int score[4];
}STUDENT;
2.初始化:
注意:
①在定义结构体变量的同时对其进行初始化,初始化列表中成员的顺序必须和结构体类型定义的顺序一致
②初始值必须是常量
③初始值的长度可以短于结构体类型的长度,没有初始化的会自动为0
STUDENT stu1 = {100310121, "王刚", 'M', 1991, {72,83,90,82}};
struct student stu1 = {100310121, "王刚", 'M', 1991, {72,83,90,82}};
13.4 结构体的嵌套:
(1)定义嵌套结构体:
1.什么是结构体的嵌套:
在一个结构体内包含了另一个结构体作为其成员
==============================================
2.定义嵌套的结构体:
方式1更好,因为方式2传值时需要传3个值
方式1:
typedef struct student
{
long studentID;
char studentName[10];
char studentSex;
DATE birthday; //结构体类型
int score[4];
}STUDENT;
typedef struct date
{
int year;
int month;
int day;
}DATE;
方式2:
typedef struct student
{
long studentID;
char studentName[10];
char studentSex;
int year;
char month[10];
int day;
int score[4];
}STUDENT;
(2)初始化结构体变量:
1.定义嵌套的结构体:
typedef struct student
{
long studentID;
char studentName[10];
char studentSex;
DATE birthday;
int score[4];
}STUDENT;
typedef struct date
{
int year;
int month;
int day;
}DATE;
2.初始化结构体变量:
STUDENT stu1 = {100310121, "王刚", 'M', {1991,5,19}, {72,83,90,82}};
注意:①结构体赋值时最好使用"{}"括起来
②数组赋值看前面的数组
结构体数组的定义及初始化:
STUDENT stu1 = {{100310121, "王刚", 'M', {1991,5,19},{72,83,90,82},
{100310121, "王刚", 'M', {1991,5,19},{72,83,90,82},
{100310121, "王刚", 'M', {1991,5,19},{72,83,90,82}};
说明:①stu1是一个数组,每一个数组元素都是STUDENT类型
②每一个元素的初始值使用"{}"括起来,并且每个元素的初始值使用","分隔
③每一个元素对应数据库中的一条记录
13.5 结构体类型所占内存字节数:
1.使用sizeof()获取
sizeof(变量或表达式)
sizeof(类型)
计算结构体所占内存的字节数时,一定要使用sizeof运算符
2.结构体类型占用内存字节数是所有成员占内存的总和吗?
不是,为了提高内存寻址效率,存在内存对齐.
3.内存对齐:
对于大多数计算机,数据项要求从某个数量字节的倍数开始存放
比如:short型数据从偶数地址开始存放,而int型数据则被对齐在4字节地址边界.因此,为了满足内存地址对齐的要求,需要在较小的成员后加入补位
4.所占字节数与什么有关?
结构体在内存中所占的字节数不仅与所定义的结构体类型有关,还与计算机系统本身有关
例1:
typedef struct sample
{
char m1;
int m2;
char m3;
}SAMPLE;
int main()
{
SAMPLE s = {'a', 2, 'b'};
printf("%d\n", sizeof(s));//12
return 0;
}
如图1:内存对齐:
读写一个4字节int型数据,只需一次内存访问操作.因为m2是对齐在4字节地址边界,所以需要小字节存储的变量需要补位
如图2:内存不对齐
读写一个4字节int型数据,只需2次内存访问操作
例2:
typedef struct sample
{
char m1;
short m2;
char m3;
}SAMPLE;
例3:
typedef struct sample
{
char m1;
char m3;
int m2;
}SAMPLE;
13.6 如何访问结构体成员:
1.访问成员:
1.访问数组元素:
数组名[下标];
因为数组元素都是同一个数据类型,那么通过下标找到数组元素在数组中的位置即可.
2.访问结构体变量的成员:
结构体变量名.成员名;
因为结构体成员的类型可能不一致,只能通过唯一的标识符(成员的名字)来访问.
注意:对嵌套的结构体成员,必须以级联方式访问
例如:
typedef struct student
{
long studentID;
char studentName[10];
char studentSex;
DATE birthday;
int score[4];
}STUDENT;
STUDENT stu;
stu1.studentID = 100310121;
stu1.studentSex = 'M';
stu1.birthday.year = 1991;
stu1.birthday.month = 5;
stu1.birthday.day = 19;
2.成员赋值:
typedef struct student
{
long studentID;
char studentName[10];
char studentSex;
DATE birthday;
int score[4];
}STUDENT;
1.整体赋值:
STUDENT stu1 = {100310121, "王刚", 'M', {1991, 5, 19},{72,83,90,82}};
STUDENT stu2;
stu2 = stu1;
注意:只能在相同类型的结构体变量之间进行赋值
2.逐个赋值:
stu2.studentID = stu1.studentID;
/*数组不能直接整体赋值,只能通过函数,
stu2.studentName=stu1.studentName;错误
*/
strcpy(stu2.studentName, stu1.studentName);
stu2.studentSex = stu1.studentSex;
stu2.birthday.year = stu1.birthday.year;
stu2.birthday.month = stu1.birthday.month;
stu2.birthday.day = stu1.birthday.day;
for (i=0; i<4; i++)
{
stu2.score[i] = stu1.score[i];
}
注意:①字符数组在赋值的时候,不会直接把字符串赋值给它.因为字符数组名表示一个首地址
整体赋值的应用:
因为数组类型不允许直接通过整体赋值;
把数组放到一个"空"的结构体内封装以后,就可以直接复制数组了.但是结构体不能通过"=="来判断相等
例1:
typedef struct
{
int member[5];
}ARRAY;
ARRAY a = {1,2,3,4,5};
ARRAY b;
b = a;
3.取地址:
结构体成员的地址:
与该成员在结构体中所处的位置及其所占内存的字节数相关
结构体变量的地址:&stu2 是该变量stu2所占内存空间的首地址
13.7 结构体指针:
(1)指向结构体变量的指针
1.结构体指针定义及初始化:
1.什么是结构体指针:
就是指向结构体变量的指针.
typedef struct student //定义结构体类型
{
long studentID;
char studentName[10];
char studentSex;
DATE birthday;
int score[4];
}STUDENT;
typedef struct date
{
int year;
int month;
int day;
}DATE;
2.定义及初始化:
S1.定义结构体指针:
STUDENT stu1;//定义结构体变量
STUDENT *pt; //定义结构体指针
S2.初始化
pt = &stu1;//将指针指向变量
2.通过指针访问结构体变量成员:
①结构体成员不嵌套:
1.通过结构体访问:
方式1:stu1.studentID = 1;
方式2:(*pt).studentID = 1;
2.通过指针访问:
pt -> studentID = 1;
②结构体成员嵌套:
1.通过结构体访问:
方式1:stu1. birthday. year = 1999;
方式2:(*pt). birthday. year = 1999;
2.通过指针访问:
//pt是指针,使用'->',但是pt->birthday是一个结构体,应该使用'.'
pt -> birthday. year = 1999;
(2)指向结构体数组的指针:
1.定义及初始化:
1.结构体数组指针:
就是定义一个指针指向结构体数组
typedef struct student //定义结构体类型
{
long studentID;
char studentName[10];
char studentSex;
DATE birthday;
int score[4];
}STUDENT;
typedef struct date
{
int year;
int month;
int day;
}DATE;
2.定义及初始化:
S1.定义
STUDENT stu[30];//定义结构体数组
STUDENT *pt;//定义结构体指针
S2.初始化
方式1:pt = stu;//结构体指针指向数组
方式2:STUDENT *pt = stu;
方式3:STUDENT *pt = &stu[0];
2.访问成员:
1.通过结构体访问:
方式1:stu[0].studentID;
方式2:(*pt).studentID;
2.通过指针访问:
pt -> studentID;
pt++;//pt指向的首地址+sizeod(结构体类型),那么pt指向下一个数组元素
13.8 向函数传递结构体:
(1)按值传递:结构体变量作函数参数
因为C语言支持结构体变量的整体赋值,所以结构体变量可作实参传递:
1.传递结构体的单个成员:复制单个成员的内容
2.传递结构体的完整结构:复制结构体的所有成员
结果:复制结构体的所有成员给函数,函数对结构体内容的修改不影响原结构体
(2)按引用传递:
按引用传递:传递结构体的首地址
1.结构体指针作实参
2.结构体数组作实参
结果:向函数传递结构体变量的地址,函数对结构体的修改影响原结构体
可以用const修饰指针,保护结构体的内容
1.结构体指针:
2.结构体数组:
(3)结构体作参数的应用:
1.使用结构体类型封装函数参数的好处是什么?
①封装函数参数作为一个参数,精简参数个数
②使函数接口更简洁可扩展性好
例1:优化上面的函数:
例2:
计算n个学生m门课程的总分和平均分:
返回sum[i],就要把它作为形参,那么就会改变函数接口,造成引用该函数的代码都要修改,如下图1
我们应该使用结构体封装形参,不需要修改接口,但是应该修改函数内部代码,如下图2
向函数传递结构体的完整结构 | 向函数传递结构体的 |
---|---|
首地址用结构体变量作函数参数 | 用结构体数组/指针作参数 |
复制整个结构体成员的内容,一组数据 | 仅复制结构体的首地址,一个数据 |
结构体指针作函数参数传递直观,但开销大,效率低 | 参数传递效率高 |
函数内对结构内容的修改不影响原结构体 | 可修改结构体指针所指向的结构体的 |
14.枚举:
14.1 声明枚举类型:
1.语法:
enum 标识符{
常量1,常量2,常量3
};
2.起别名:
typedef enum 标识符 {
常量1,常量2,常量3
} 别名;
注意:①里面的常量的值是整型数据值,默认从0开始,一次递增1
②如果自己指定一个整型值,那么后面的常量的整数值从该值开始递增1
例如:
enum weeks {
//TUE是2,WED是3
SUN=7, MON=1, TUE, WED, THU, FRI, SAT
};
14.2 枚举是什么:
1.枚举类型究竟是一种什么数据类型?
在C语言中,基本类型包括:整型、字符型、浮点型、枚举类型
它与共用体,结构体没有任何关系
2.枚举常量究竟是什么?
枚举变量是整型变量,枚举常量是整型常量,不是字符串
例:
enum weeks {SUN, MON, TUE, WED, THU, FRI, SAT};
today = MON;//相当于today = 1;
today++;//today=2,或者today=TUE
printf("%d", today);//2
注意:
①与宏常量类都是常量但不同,枚举常量有作用域,但是宏常量没有类型
②宏常量是全局常量,枚举常量有作用域
14.3 应用:
枚举类型究竟有什么用?
①增强程序的可读性
例如:定义布尔类型,用来定义标志变量
②用枚举类型声明结构体中的标记字段
15.共用体:
15.1 用户自定义数据类型:
1.结构体类型:
把关系紧密且逻辑相关的多种不同类型的的变量,组织到一个统一的名字之下
2.共用体:也称联合
把情形互斥但逻辑相关的多种不同类型的变量,组织到一个统一的名字之下
15.2 与结构体的区别:
(1)结构体类型:
结构体 | 共用体 |
---|---|
关系紧密且逻辑相关的多种不同类型的数据的集合 | 情形互斥但逻辑相关的多种不同类型的数据的集合 |
可以保存所有成员的值,用sizeof计算内存字节数 | 内存重叠存储,每一瞬间只能保存一个成员,即最后赋值的成员 |
可以对成员全部初始化 | 只能对第一个成员初始化 |
1.声明类型:
struct sample
{
short i;
char ch;
float f;
};
注意:所占内存是根据内存对齐方式决定
2.初始化:
struct sample s = {1, 'A', 3.14};
3.访问成员:
s.i = 1;
s.ch = 'A';
s.f = 3.14;
(2)共用体类型:
1.声明类型:
union sample
{
short i;
char ch;
float f;
};
注意:
①所占内存数取决于占空间最多的那个成员变量
②并且每一个成员变量的内存从同一个地址开始分配,所以后面赋值的成员变量覆盖前面的成员变量,也就是说共用体类型的变量只能存储一个成员,就是最后赋值的成员变量
③同一内存在每一瞬时只能保存一个成员,起作用的成员是最后一次赋值的成员
④本质:就是定义了一个空间,既可以存储short,也可以存储char,也可以存储float类型的数据,只能存储其中一个类型
2.初始化:
union sample u = {1};
注意:只能对共用体的第一个成员进行初始化
3.访问成员:
u.i = 1;
u.ch = 'A';
u.f = 3.14;
15.3 共用体的应用:
(1)节省内存空间
struct person
{
char name[20];
char sex;
int age;
union maritalState marital;
int marryFlag; //标记婚姻状况,用来确定共用体使用哪种状态
};
union maritalState //三种情况是互斥的,适合使用共用体
{
int single; /*未婚*/
struct marriedState married; /*已婚*/
struct divorceState divorce; /*离婚*/
};
struct marriedState //已婚
{
struct date marryDay;
char spouseName[20];
int child;
};
struct divorceState //离婚
{
struct date divorceDay;
int child;
};
struct date
{
int year;
int month;
int day;
};
每次对共用体的成员赋值时,程序负责改变标记字段的内容
struct person p1;
if (p1.marryFlag == 1)
{
//未婚
}
else if (p1.marryFlag == 2)
{
//已婚
}
else
{
//离婚
}
(2)构造混合的数据结构:
10.代码风格:
10.1 程序板式
1.对齐与缩进
对齐与缩进——保证代码整洁、层次清晰的主要手段
①同层次的代码在同层次的缩进层上
现在的许多开发环境、编辑软件都支持“自动缩进”,VC中有自动整理格式功能(ALT+F8)
②一般用设置为4个空格的Tab键缩进,不用空格缩进
2.变量的对齐规则
变量的对齐规则:
数据类型 + n个TAB + 变量名 + [n个TAB] = [初始化值];
例:
char name[20];
char sex = 'F';
3.空行——分隔程序段落的作用
空行——分隔程序段落的作用
①在每个函数定义结束之后加空行
②在一个函数体内,相邻两组逻辑上密切相关的语句块之间加空行,语句块内不加空行
4.代码行内的空格——增强单行清晰度
①关键字之后加空格,但函数名之后不加空格
②赋值、算术、关系、逻辑等二元运算符前后各加一空格;但一元运算符以及"[] . ->"前后不加空格
例:sum = sum + term;
③"("向后紧跟,")" "," ";"向前紧跟,紧跟处不留空格,",",";"后留一个空格
Function(x, y, z)
for (initialization; condition; update)
④对表达式较长的for和if语句,为了紧凑可在适当地方去掉一些空格
for (i=0; i<10; i++)
if ((a+b>c) && (b+c>a) && (c+a>b))
5.代码行:
代码行
①一行只写一条语句,便于测试
②一行只写一个变量,便于写注释
int width; //宽度
int height; //高度
int depth; //深度
int width, height, depth; //宽度高度深度(不建议)
③ 尽可能在定义变量的同时,初始化该变量
int sum = 0;
④if、for、while、do等语句各占一行,便于测试和维护
if (width < height)
{
DoSomething();//执行语句无论有几条都用{和}将其包含在内
}
6.长行拆分
①代码行不宜过长,应控制在10个单词或70-~80个字符以内
②实在太长时要在适当位置拆分,拆分出的新行要进行适当缩进
10.2 程序注释:
1.写注释给谁看?
①给自己看,使自己的设计思路得以连贯
②给继任者看,使其能够接替自己的工作
2.写注释的最重要的功效在于传承
①要站在继任者的角度写
②简单明了、准确易懂、防止二义性
③让继任者可以轻松阅读、复用、修改自己的代码
④让继任者轻松辨别出哪些使自己写的,哪些是别人写的
3.注释不是白话文翻译
①注释不是教科书
②注释不是标准库函数参考手册
③注释不是越多越好
④不写做了什么,写想做什么
4.在哪些地方写注释?
①在重要的文件首部
文件名 + 功能说明+[作者]+[版本]+[版权声明] + [日期]
②在用户自定义函数前,对函数接口进行说明
函数功能 + 入口参数 +出口参数 + 返回值(包括出错处理)
③在一些重要的语句块上方
对代码的功能、原理进行解释说明
④在一些重要的语句行右方
定义一些非通用的变量,函数调用,较长的、多重嵌套的语句块结束处
⑤在修改的代码行旁边加注释
在调试程序中对暂不使用的语句通常可先用注释符括起来,使编译器跳过这些语句
10.3 命名规则:
1.标识符密码规则:
按照执行级别分为:
共性规则---必须执行
简化规则---建议采用
可选规则---灵活运用
1.共性规则:
①有意义,直观可拼读,见名知意,不必解码
②最好采用英文单词或其组合,切忌用汉语拼音,尽量避免出现数字编号
③不要出现仅靠大小写区分的相似的标识符
④不要出现名字完全相同的局部变量和全局变量
⑤用正确的反义词组命名具有互斥意义的变量或相反动作的函数
⑥尽量与所采用的操作系统或开发工具的风格保持一致
2.Windows应用程序命名规则
主要思想
1.在变量和函数名前加上前缀,用于标识变量的数据类型
[限定范围的前缀] + [数据类型前缀] +[有意义的英文单词]
①限定范围的前缀
a.静态变量前加前缀s_ ,表示static
b.全局变量前加前缀g_ ,表示global
c.默认情况为局部变量
②数据类型前缀
a. ch 字符变量前缀
b. i 整型变量前缀
c. f 实型变量前缀
d. p 指针变量前缀
③限定范围的前缀与数据类型前缀可要可不要
④无特殊意义的循环变量可以直接定义成i,j,k等单字母变量
2.简化的Windows应用程序命名规则
①变量名形式:小写字母开头,“名词”或者“形容词+名词”
oldValue, newValue
②函数名形式:大写字母开头,“动词”或者“动词+名词”(动宾词组)
GetValue(), SetValue()
③宏和const常量全用大写字母,并用下划线分割单词
#define ARRAY_LEN 10
const int MAX_LEN = 100;
程序测试:
1.错误类型:
①编译错误:一般是语法错误
②链接错误:缺少包含文件,或者包含文件的路径错误...
③运行时错误:编译器无法精确到错误的地方
a.程序的运行结果与语气不一致--逻辑错误
b.程序无法正常运行--非法操作(除0)
2.调试方法:
1.调试工具:
①设置断点
②单步跟踪:每次执行一行
③监视窗:观察数据的变化情况
2.调试方法:
①逆向推理
②分治排除:注释掉一些代码,排除错误
③缩减输入:设法找到导致出错的最小输入
标签:变量,int,函数,C语言,数组,类型,指针
From: https://www.cnblogs.com/zhaoyangnizhenmei/p/17604287.html