C语言基础知识
目录
1.数据类型
2.运算符,表达式和语句
3.循环
4.分支和跳转
5.函数
6.数组
7.结构体,联合体(重要)
8.指针(重要)
9.宏定义
一.数据类型
1.1数据类型
算法处理的对象是数据,而数据是以某种特定的形式存在的,如整数、实数、字符等形式。
定义:简单的说,数据类型就是程序给其使用的数据,指定某种数据组织形式,从字面上理解,就是对数据按类型进行分类。
数据类型是按被说明数据的性质、表示形式、占据存储空间的多少、构造特点来划分的。在C语言中,数据类型可分为基本类型、构造类型、指针类型和空类型四大类。
1.2整形数据
整形数据包括:整形变量、整型常量
1.2.1整形变量
【分类】整形变量的基本类型说明符为int。由于不同的编译系统对整形变量所占用的字节数有不同的规定,因此根据在VC++6.0中各整形变量占用内存字节数的不同,可以将整形变量分为以下3类:
基本整形:用int表示,在内存中占4个字节。
短整型:用short int 或short 表示,在内存中占2个字节。
长整形:用long int 或long 表示,在内存中占4个字节。
【注意】为了增加变量的取值范围,还可以将变量定义为“无符号”型
以上3类都可以加上修饰符:unsigned,用以指定是“无符号数”。如果加上signed,则被指定为“有符号数”。如果既不指定unsigned也不指定signed,则系统默认为有符号数(signed)。各种无符号类型量所占的内存空间字节数与相应的有符号类型量相同。但由于省去了符号位,故不能表示负数。
【注意】方括号内的部分是可以不输入的。
1.2.2整型常量
定义:整型常数即整常数。按不同的进制,整型常量有3种表示方式,分别是十进制数表示法、八进制数表示法和十六进制表示法。
十进制数表示法
十进制整型常量没有前缀,其数码为0~9。
八进制数表示法
八进制整型常量以0作为前缀,其数码为0~7。
十六进制数表示法
十六进制整型常量以0X或0x作为前缀,其数码为0~9 和A~F。
【注意】
程序中是根据前缀来区分各种进制数的,因此在书写常量时不要把前缀弄错,否则会出现不正确的结果。
在C程序中,只有十进制数可以是负数,而八进制和十六进制数只能是无符号数。
1.3实型数据
当进行数据运算需要用到小数或指数时,用C语言来实现的话,就需要用到实型数据。
1.3.1实型变量
C语言中的实型变量分为:单精度(float)、**双精度(double)和长双精度(long double)**3种类型。
【定义】实型变量定义的一般形式为:类型说明符 变量名1
例如:
float x,y; //x,y为单精度实型变量
double a,b,c; //a,b,c为双精度实型变量
1
2
【实型变量的舍入处理】
由于实型变量也是用有限的存储单元存储的,所以能够接受的有效数字的位数也是有限的。有效位数以外的位数将被舍去。
1.3.2实型常量
【定义】实型常量即不包括整数的实数,在C语言中又称浮点数。浮点数均为有符号浮点数,没有无符号浮点数。其值有两种表达方式:分别为十进制小数形式和指数形式。
十进制小数形式:由数字和小数组成,必须有小数点,且小数点的位置不受限制。
指数形式:由十进制数加阶码标志“e”,“E”以及阶码(只能为整数,可以带符号)组成。其一般形式为:尾数E(e)整型指数。
【实型常量的类型】实型常量又分:单精度(float)、双精度(double)。一个实型常量可以赋给一个实型变量。
1.4字符型数据
字符型数据就是用来表示英文字母、符号和汉字的数据。
1.4.1字符变量
字符变量的类型说明符是char,其定义的一般形式如下:char 变量名1
例如:
char c1,c2;c1=a;c2=b; //这样就定义了两个字符型的变量c1和c2,并分别赋值为字符型常量a和b。
1
2
【注意】在VC++6.0中,字符型数据占1字节,因此字符型变量的值实质上是一个8位的整数值,取值范围一般是-128~127。char型变量也可以加修饰符unsigned,unsigned char型变量的取值范围是0~255。
1.4.2字符常量
【定义】从表现形式来说,就是用一对单引号括起来的单个字符。
【注意】C语言中还允许使用一种以特殊形式出现的字符常量,以表示某些非图形字符,这就是以“/”开头的转义字符序列。
1.4.3字符串常量
C语言除了允许使用字符常量外,还允许使用字符串常量。字符串常量是用一对双引号(“ ”)括起来的零个或多个字符的序列。
例如:“CHINA”、“0123456789"都是字符串常量。
【注意】在存储字符串常量时,由系统在字符串的末尾自动加一个“/0”作为字符串的结束标志。
1.4.4字符常量与字符串常量的区别
字符常量使用单引号,而字符串常量使用双引号。例如,‘a’表示字符常量;,而“a”则表示的是只有一个字符长度的字符串常量。
二者在内存中的存储也不同,字符常量存储的是字符的ASCII码值,而字符串常量除了要存储有效的字符外,还要存储一个“字符串结束标志(/0)”,以便系统判断字符串是否结束。
(一)运算符
Ⅰ、算术运算符
1.加法运算符:+
用于加法运算,相加的值可以是变量也可以是常量。如printf("%d",4+20); income = salary + bribes;均是正确表达。
2.减法运算符:-
用于减法运算,使其左侧的值减去右侧的值
3.乘法运算符:*
用于乘法运算,不同于数学上的'x'。
4.除法运算符:/
(1)用于除法运算,不同于数学上的‘÷’。/左侧是被除数,/右侧是除数。
(2)整数除法和浮点数除法不同。浮点数除法的结果是浮点数,整数除法的结果是整数。
#include
int main()
{
printf("%d\n",5 / 4);
printf("%d\n",6 / 3);
printf("%.2f\n",7. / 4.);
printf("%.2f\n",7. / 4);
return 0;
}
编译并运行代码,输出如下:
1 //整型除法会截断计算结果的小数部分(丢弃整个小数部分),不会四舍五入
2
1.75
1.75 //整数和浮点数的计算结果是浮点数,实际上,计算机不会真正用浮点数除以整数,编译器会把两个运算对象转换成相同类型
(3)负数如何截断?如-3.8是变成-3还是-4,“趋零截断”这一概念告诉我们-3.8会被处理成-3
5.求模运算符:%
(1)用于整数运算,不能用于浮点数,求模运算符给出其左侧整数除以右侧整数的余数。13%5(读作:13求模5)。
(2)求模运算符常用于控制程序流
(3)负数求模:如果第一个运算对象为负数,那么求模结果为负数;如果第一个运算对象为正数,那么求模结果为正数
6.符号运算符:+和-
用于标明或改变一个值的代数符号。它们是一元运算符(单目运算符),即只需要一个运算对象。
7.递增运算符:++
(1)功能:将其运算对象递增1
(2)两种形式:①前缀模式:++出现在其作用的变量前面
②后缀模式:++出现在其作用的变量后面
(3)这种缩写形式的好处:让程序更加简洁、美观,可读性更高。
(4)前缀模式:先++,再使用
后缀模式:先使用,再++
#include
int main()
{
int a = 1,b = 1;
int a_post,pre_b;
a_post = a++;
pre_b = ++b;
printf("a a_post b pre_b \n");
printf("%d %7d %2d %6d\n", a, a_post, b, pre_b);
return 0;
}
编译并运行该代码,输出如下:
a a_post b pre_b
2 1 2 2
可以看到后缀模式:使用a的值之后,递增a
前缀模式:使用b的值之前,递增b
8.递减运算符:--
与++同理。
Ⅱ、赋值运算符
1.赋值运算符:=
1°、一些术语:
(1)数据对象:用于存储值的数据存储区
(2)左值:用于标识特定数据对象的名称或表达式。
Tips:提到左值,这意味着它①指定一个对象,可以引用内存中的地址②可以用在赋值运算符的左侧。但是const创建的变量不可修改,不满足②,所以后来又提出了可修改的左值这一概念,用于标识可修改的对象
(3)右值:能赋值给可修改左值的量,且本身不为左值
2°、在C语言中,=不意味着“相等”,而是一个赋值运算符,num = 2021,读作“把值2021赋给变量num”,赋值行为从右往左进行
3°、2021 = num;?在C语言中类似这样的语句是没有意义的,因为此时,2021被称为右值,只能是字符常量,不能给常量赋值,常量本身就是它的值。因此,我们要记住赋值运算符左侧必须引用一个存储位置,最简单的方法就是使用变量名。C使用可修改的左值标记那些可赋值的实体。
4°、C语言中不回避三重赋值,赋值顺序从右向左,如a = b = c = 100;首先把100赋给从c,然后再赋给b,最后赋给a。
关于左值,右值,我们来看以下代码例子:
#include
int main()
{
int ex, why, zee;//ex,why,zee都是可修改的左值
const int TWO = 2;//TWO是不可修改的左值,只能放在=的右侧,此处的=是初始化,不是赋值,因此并没有违反规则
why = 42;
zee = why;
ex = TWO * (why + zee);//(why + zee)是右值,该表达式不能表示特定内存位置,而且也不能给它赋值
return 0;
}
2.其他赋值运算符:+=、-=、*=、/=、%=
(1)以下每一行的左右两种表达等价
scores += 20 score = score + 20
dimes -= 2 dimes = dimes - 2
bunnies *= 2 bunnies = bunnies * 2
time /= 2.73 time = time /2.73
reduce %= 3 reduce = reduce % 3
(2)优点:让代码更紧凑,与一般形式相比,组合形式的赋值运算符生成的机器代码更高效
3.以上两大类运算符优先级和求值顺序问题
(1)
运算符优先级(由高至低)
运算符 结合律
() 从左往右
+ - ++ --(单目) 从右往左
* / 从左往右
+ -(双目) 从左往右
= += -= *= /= %= 从右往左
Tip:如果两个运算符优先级相同,则根据它们在语句中出现的顺序来执行
(2)虽然运算符的优先级为表达式中的求值顺序提供了重要依据,但是并没有规定所有的顺序。如:用 y= 6 * 12 + 5 * 20;由(1)可知,先进行6 * 12和5 * 20,再进行加法运算,但是优先级并没有规定先进行哪个乘法。C语言把主动权交给语言的实现者,根据不同的硬件来决定先计算前者还是后者,但是无论采取何种方案,都不会影响最终结果。结合律适用于共享同一运算对象的运算符,如:12 / 3 * 2,/和*的优先级相同,共享运算符3,所以从左往右的结合律可以起作用。
(3)如果一个变量出现在一个函数的多个参数中,不要对该变量使用递增或递减运算符;如果一个变量多次出现在一个表达式中,不要对该变量使用递增或递减运算符。举个例子:ans = num/2 + 5*(1 + num++);我们可能会认为,先计算第一项(num/2),接着计算(5*(1 + num++));但是编译器可能先计算第二项,递增num,然后在num/2中使用num递增后的新值
Ⅲ、关系运算符
1.
关系运算符
< 小于
> 大于
<= 小于等于
>= 大于等于
== 等于
!= 不等于
结合律:从左向右
2.用关系运算符将两个表达式连接起来的式子,称为关系表达式。关系表达式的结果是一个逻辑量,取值“真”或“假”,即“1”或“0”。
3.优先级:赋值<关系<算术。所以x > y + 2和x > (y + 2)相同;x = y >2和x = (y >2)相同,若y>2,给x赋值为1,否则赋值为0
关系运算符之间的优先级
高优先级组 < > <= >=
低优先级组 == !=
4.如果待比较的值是一个常量,可以把该常量放在左侧有助于编译器捕获错误,如:5 = canoes是一种语法错误,而5 == canoes可以检查 canoes的值是否为5。这是因为C语言不允许给常量赋值,编译器会把赋值运算符的这种用法作为语法错误标记出来。我们再构建比较是否相等的表达式时可以把常量放在左侧。
Ⅳ、逻辑运算符
1.与关系运算一样,用整数1代表“真”,用整数0代表“假”
2.
逻辑运算符
目数 单目 双目
运算符 ! && ||
名称 逻辑非 逻辑与 逻辑或
假设exp1和exp2是两个简单的关系表达式,那么:
(1)当且仅当exp1和exp2都为真时,exp1 && exp2才为真
(2)如果exp1或exp2为真,则exp1 || exp2为真
(3)如果exp1为真,则!exp1为假;如果exp1为假,则!exp1为真
3.&& 和 ||都是序列点,所以程序在从一个运算对象执行到下一个运算对象之前,所有的副作用都会生效。
4.&&可用于测试范围,如测试score是否在90~100的范围,可以这样写 if(score >= 90 && score <= 100)
printf ("Good!\n");
但是不可以写成if(90 <= score <= 100)
printf("Good!\n");
这是代码的语义错误,不是语法错误,因此编译器并不会捕捉这样的问题。<=的求值顺序为从左到右,子表达式90 <= score的值要么为1,要么为0,这两个值都小于100,所以不管score的值是多少,整个表达式都恒为真。
5.与其他表达式的运算过程不同,在求解&&和||连接的逻辑表达式时,按从左到右的顺序计算该运算符两侧的操作数,一旦能得到表达式的结果,就停止运算。(1)exp1 && exp2,先计算exp1,若其值为0,则exp1 && exp2的值一定为0(2)exp1 || exp2,先计算exp1,若其值为非0,则exp1 && exp2的值一定为1
如:
#include
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
printf("a = %d,b = %d,c = %d,d = %d\n", a, b, c, d);
return 0;
}
编译并运行该代码,输出如下:
a = 1,b = 2,c = 3,d = 4
//后置++,先使用再++,使用时,a = 0,为假,后面的已经没有必要算了
//i = 0
4.优先级:
逻辑运算符优先级(由高至低)
!
&& ||
Ⅴ、条件运算符(?:)
1.作为if else语句的一种便携方式。是C语言中唯一的三目运算符(带三个运算对象)
2.通用形式:expression1?expression2:expression3 (结合律:从左向右)
如果expression1为真,那么整个条件表达式的值与expression2相同;如果expression1为假,那么整个条件表达式的值与expression3相同
举个例子:(5 > 3)? 1 : 2 值为1
Ⅵ、逗号运算符(,)
1.一些概念:
(1)副作用:对数据对象或文件的修改
(2)序列点:程序执行的点,在该点上,所有的副作用都在进入下一步之前发生。在C语言中,语句中的分号标记了一个序列点
2.在C语言中,逗号既可以作分隔符,又可以作运算符。逗号作为分隔符使用时,用于间断说明语句中的变量或函数中的参数;作为运算符使用时,将若干个独立的表达式连接在一起,组成逗号表达式。其一般形式为:表达式1,表达式2,...,表达式n。其运算过程为:先计算表达式1,然后计算表达式2,......,最后计算表达式n,并将表达式n的值作为逗号表达式的值。
3.一些情况:
(1)假设有该表达式:a++, b = a * 10,在该表达式中,先递增a,然后在第二个子表达式中使用a的新值。作为序列点的逗号保证了左侧表达式的副作用对右侧表达式求值之前发生
(2)如果有人在写数字时不小心输入了逗号,如:price = 12,30; 这不是语法错误,编译器会把它解释为一个逗号表达式
Ⅶ、位运算符
1.位运算符概览
位运算符
& 按位“与”
| 按位“或”
^ 按位“异或”
~ 取反
<< 左移
>> 右移
(1)位运算符中除^是单目运算符外,其它均为双目运算符
(2)位运算符所操作的操作数只能是整型或字符型的数据以及它们的变体
2.按位“与”:
(1)二元运算符&通过逐位比较两个运算对象,生成一个新值。对于每个位,只有两个运算对象中相应的位都为1时,结果才为1。如:
(10010011)& (00111101) 的结果为 00010001
(2)C语言中有一个按位和赋值结合的运算符:&=
3.按位“或”:
(1)二元运算符 | 通过逐位比较两个运算对象,生成一个新值。对于每个位,如果两个运算对象中相应的位为1,结果为1。如:
(10010011)| (00111101) 的结果为 10111111
(2)C语言中有一个按位和赋值结合的运算符:|=
4.按位“异或”:
(1)二元运算符^通过逐位比较两个运算对象,生成一个新值。对于每个位,如果两个运算对象中相应的位一个为1,结果为1。如:
(10010011)^ (00111101) 的结果为 10101110
(2)C语言中有一个按位和赋值结合的运算符:^=
(3)异或是支持交换律的:看一条题:在不创建临时变量(第三个变量),实现两个数的交换。我们用两种方法来实现这条题。
#include
int main()
{
int a = 10;
int b = 20;
a = a + b;
b = a - b;
a = a - b;
printf("%d %d\n",a, b);
return 0;
}
#include
int main()
{
int a = 10;
int b = 20;
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("%d %d\n",a, b);
return 0;
}
两种方法比较:第一种方法在数字太大时会溢出;第二种方法的可读性不好,只适用于整型,效率也没有第一种方法好。
5.取反:
一元运算符~把1变为0,把0变为1。如:~(10011010)的结果为01100101
6.左移:
(1)<<将其左侧运算对象每一位的值向左移动其右侧运算对象指定的位数。左侧运算对象移出左末端位的值丢失,用0填充空出的位置。如图:
(2)该操作产生一个新的位值,但是不改变运算对象 。如:假设a = 1,a<<2为4,但是a本身的值不被改变。可以使用<<=左移赋值运算符来更改变量的值,如:假设a = 1,a<<=2,此时a的值改为4。
7.右移:
(1)>>将其左侧运算对象每一位的值向右移动其右侧运算对象指定的位数。左侧运算对象移出右末端位的值丢失。对于无符号型,用0填充空出的位置;对于有符号类型,其结果取决于机器,空出的位置可以用0填充,也可以用符号位的副本填充
如:有符号的例子:(10001010)>> 2的结果可能是(00100010),也有可能是(11100010),看系统。
无符号的例子:(10001010)>> 2的结果均为(00100010)
(2)该操作产生一个新的位值,但是不改变运算对象 。可以使用<<=左移赋值运算符来更改变量的值
(3)移位运算符对2的幂提供了快速有效的乘法和除法:
number << n number乘以2的n次幂
number >> n 若number>0,则用number除以2的n次幂
注意:移位运算具体实现有3种方式
①循环移位:移入的位等于移出的位
②逻辑移位:移出的位丢失,移入的位取0
③算术移位(带符号):移出的位丢失,左移入的位取0,右移入的位取符号位,即最高位代表数据符号,保持不变
C语言中的移位运算与具体的C语言编译器有关。
8.一道例题:求一个整数存储在内存中的二进制中1的个数。(我们用3种方法实现它)
#include
int main()
{
int num;
int count = 0;
scanf("%d", &num);
while(num)
{
if(num % 2 == 1)
{
count++;
}
num /= 2;
}
printf("%d\n",count);
return 0;
}
#include
int main()
{
int num, i;
int count = 0;
scanf("%d", &num);
for(i = 0; i < 32; i++)
{
if(((num >> i) & 1) == 1)//挪动i位,使得每一位都和1按位“与”
{
count++;
}
}
printf("%d\n", count);
return 0;
}
#include
int main()
{
int num;
int count = 0;
scanf("%d", &num);
while(num)
{
count++;
num = num & (num - 1);
}
printf("%d\n", count);
return 0;
}
以上3种方法,过程逐渐优化,需要我们好好体会。
Ⅷ、其他运算符
1.长度运算符sizeof():单目运算符,以字节为单位返回运算对象的大小。C语言规定,sizeof()返回size_t类型的值,这是一个无符号整数类型。
2.函数调用运算符()
3.下标引用运算符[]:用来表示数组元素
4.强制类型转换运算符(类型):强制类型转换
4.查找地址 &运算符:一元&运算符给出变量的存储地址
5.间接(解引用)运算符*:找出存储在某变量中的值
6.访问结构成员(.)(->)
二、优先级大总结
1.
运算符优先级表(由高到低)
运算符 结合方向
[] () . -> 左到右
-(负号运算符) (类型)++ -- *(取值运算符) &(取地址运算符) ! ~ sizeof() 右到左
/ * % 左到右
+ - 左到右
<< >> 左到右
> < >= <= 左到右
== != 左到右
&(按位“与”) 左到右
^ 左到右
| 左到右
&& 左到右
|| 左到右
?: 左到右
= /= *= %= += -= <<= >>= &= ^= |= 右到左
, 左到右
注意:同一优先级的运算符,运算次序由结合方向决定
2.一些容易出错的优先级问题:
(1).的优先级高于*(->操作符用于消除这个问题):*p.f等价于*(p.f)
(2)[]高于*:int* ap[]等价于int* (ap[])
(3)函数()高于*:int* fp()等价于int* (pf())
(4)==和!=高于位操作:(val & mask != 0)等价于val & (mask != 0)
(5)==和!=高于赋值符:c = getchar() != EOF等价于c = (getchar() != EOF)
(6)算术运算符高于位移运算符:msb << 4 + lsb等价于msb << (4 + lsb)
(7)逗号运算符的优先级最低:i=1,2等价于(i = 1),2
三、类型转换
1.隐式类型转换
(1)C语言的整型算术运算符总是至少以缺省(默认值)整型类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升
(2)整型提升的意义:表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度 一般就是int的字节长度,同时也是CPU的通用寄存器的长度。 因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
如char a, b, c; c = a + b; a和b的值被提升为普通整型,然后再执行加法运算。加法运算完成之后,结果将被截断,然后再存储于c中。
(3)如何整型提升:整形提升是按照变量的数据类型的符号位来提升的
①有符号整型提升:高位补充符号位
负数:char c1 = -1;//11111111变量c1的二进制位(补码)中只有8bit;整型提升:高位补充符号位,即为1,所以结果为11111111111111111111111111111111
正数:char c2 = 1;//00000001变量c2的二进制位(补码)中只有8bit;整型提升:高位补充符号位,即为0,所以结果为 00000000000000000000000000000001
②无符号整型提升:高位补充0
(4)一条例题:
#include
int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
return 0;
}
编译运行代码,输出如下:
1
4
4
//c只要参与表达式运算,就会发生整形提升,表达式 +c ,就会发生提升,所以 sizeof(+c) 是4个字节;同理,sizeof(-c) 也是4个字节
2.算术转换:
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。以下层次称为 寻常算术转换 。 long double, double, float, unsigned long int, long int, unsigned int,int。 如果某个操作数的类型在上面这个顺序中中排名较低,那么首先要转换为另外一个操作数的类型后执行运 算。涉及两种类型的运算,两个值会被分别转换成两种类型的更高级别。
3.强制类型转换 :
即在某个量的前面用圆括号括起来类型名,该类型名即是希望转换成的目标类型。
对比下面两行代码:假设sum是int 型变量:
sum = 1.6 + 1.7;//结果为3。首先,1.6+1.7=3.3,为了匹配int型变量,3.3被类型转换截断成3(自动类型转换)
sum = (int)1.6 + (int)1.7;//结果为2。1.6和1.7在相加之前都被强制转换成1,然后把1+1的结果赋给sum。(强制类型转换)
四、表达式:
表达式是由运算符和运算对象组成的。最简单的表达式是一个单独的运算对象。C表达式的一个最重要特性就是,每个表达式都有一个值。
五、语句:
1.语句时C程序的基本构建块。一条语句相当于一条完整的计算机指令。在C中,大部分语句以分号结尾。最简单的语句是空语句,即只有一个分号。C把末尾上加上一个分号的表达式都看作是一条语句,所以8;4+3;类似这样写也是可以的。虽然一条有用的语句相当于一条完整的指令,但并不是所有的指令都是语句,如:x = 6 + (y = 5);此处的y = 5便是一条完整的指令,但是它只是语句的一部分。
2.复合语句:用花括号括起来的一条或多条语句,复合语句也称为块。
请看下面两个程序段:
while (index++ < 10)
{
sam = 10 * index + 2;
printf("%d\n",sam);
}
while (index++ < 10){
sam = 10 * index + 2;
printf("%d\n",sam);
}
两种风格的区别:第一种:强调语句形成一个块;第二种:突出块附属于while循环。但是对于编译器来说,这两种风格完全相同。
三.循环
①.while语句的格式和功能如下:
格式:while(表达式)循环体
功能:当表达式的值为真时,重复执行其后循环体。
说明:循环体是循环语句的内嵌语句,可以是空或复合语句(下同)。
②.do-while语句的格式和功能如下:
格式:do 循环体while(表达式)
功能:重复执行循环体,直到表达式的值为假。
说明:do-while循环至少执行一次循环体。
③.for语句的格式和功能如下:
格式:for(表达式1; 表达式2; 表达式3) 循环体
功能:
(1)计算表达式1;
(2)当表达式2的值为真时,重复执行循环体和计算表达式3。
说明:表达式1、表达式2、表达式3均可缺省,但保留分号。
④continue语句的格式和功能如下:
格式:continue;
功能:跳过循环体中其后程序段(结束本次循环)。
说明:continue语句只能用于循环
continue和break的区别
continue 语句和 break 语句的区别是,continue 语句只结束本次循环,而不是终止整个循环。break 语句则是结束整个循环过程,不再判断执行循环的条件是否成立。而且,continue 只能在循环语句中使用,即只能在 for、while 和 do…while 中使用,除此之外 continue 不能在任何语句中使用。
所以,再次强调:continue 不能在 switch 中使用,除非 switch 在循环体中。此时 continue 表示的也是结束循环体的本次循环,跟 switch 也没有关系。
四.分支和跳转
4.1 if语句
1、while(scanf(“%f”,&temperature) == 1)
{
all_days++;
if(temperature < a) cold_days++;
}利用scanf()的返回值在scanf()遇到非数字输入的时候终止循环。float既可以输入8也可以-2.5。
2、if语句被称为分支语句或选择语句。
if(关系表达式判定条件) 执行语句;
4.2 在if语句中添加else关键字
1、形式:if(expression) statement1;
else statement2; 利用花括号创建代码快。
if 语句使您能够选择是否执行某个动作。if else 语句使您可以在两个动作之间进行选择。
2、介绍getchar()和putchar()
getchar()函数没有参数,它返回来自输入设备的下一个字符。
ch = getchar(); scanf(“%c”,&ch);
putchar()函数打印它的参数。
putchar(ch); printf(“%c”,ch);
ch = getchar(); //读入一个字符
while(ch != '\n') //当一行未结束时
{
if(ch == SPACE) putchar(ch);
else putchar(ch + 1);
ch = getchar(); //获取下一个字符
}运行示例:CALL ME HAL
DBMM NF IBM while((ch = getchar()) != ‘\n’);
//为ch赋值 ch 与\n比较
该动作将一个值赋给ch并将这个值与换行符进行比较。 字符实际上是作为整数被存储的。
3、多重选择else if
a、提供两个以上的选择,情况很多,分情况讨论。嵌套使用。
b、else与if的配对,规则是如果没有花括号指明,else与和它最接近的一个if相匹配。
c、素数例子进行编程(详细编程)很典型的例子。。。P163页码。
4、if语句的形式
a、if(。。。。) 执行;
b、if( ) 执行; else 执行;
c、if ( ) 执行; else if( ) 执行; else 执行;
5、逻辑运算符号
&&运算符用于测试范围。 if(range >= 90 && range <= 100)
不要写成if(90 <= range <=100) //语义错误 <=运算符的求值顺序是由左到右,所以会把表达式理解为:(90 <= range) <=100 关系表达式90 <= range 的值为1或者0。任何一个都小于100,因此不管range是什么,整个表达式总为真。 if(ch >= ‘a’&& ch <= ‘z’)
4.3 条件运算符 ?:
简写方式来表示if else 语句,条件表达式,条件运算符?:。三元运算符
一个数绝对值的例子:x = (y < 0)? -y : y; 在=和分号之间为条件表达式。
if(y < 0) x = -y; else x = y;
形式:表达式1 ? 表达式2 :表达式3
如果表达式1为真(非零),整个条件表达式的值和表达式2的值相同。表达式1假的话,整个条件表达式的值等于表达式3的值。
典型例子: max = (a > b) ? a : b;将两个值中最大值赋给变量。
4.4 循环辅助手段:continue和break
continue和break语句使你可以根据循环体内进行的判断结果来忽略部分循环甚至终止它。
continue:a、当运行到该语句时,他将导致剩余的迭代部分被忽略,开始下一次迭代。如果continue语句处于嵌套结构中,那么它仅仅影响包含他的最里层的结构。
while(scanf(“%f”,&score) = = 1)
{
if() 执行语句; continue;
}
b、continue另一作用是作为占位符。例如:下面的循环读取并丢弃输入,直到一行的末尾。while(getchar() != ‘\n’) continue;
当程序已经从一行中读取了一些输入并需要跳到下一行的开始时,使用上面的语句很方便。
如果不是为了简化代码,而是使代码更加复杂,就不要使用continue。例如:
while(getchar() != ‘\n’)
{
if(ch == ‘\t’) continue;
putchar(ch);
}
while(getchar() != ‘\n’)
{
if(ch != ‘\t’)
putchar(ch); 通过if的判断取逆以消除对continue的需求。
}
c、continue语句导致循环体的剩余部分被跳过。那么continue在什么地方继续循环?
int count = 0; //对于while循环,continue语句之后发生的动作是求
while(count < 10) //循环判断表达式的值。
{
ch = getchar(); if(ch == '\n') continue;
putchar(ch); count++; }
他输入10个字符(换行符除外,因为ch为换行符时跳过++语句)并回显他们,不包括换行符。
d、对于for循环,下一个动作是先求更新表达式的值,然后再求循环判断表达式的值。
for(count = 0; count < 10; count++)
{ //本例中,当continue执行时,首先递增count,然后把count与10比较
ch = getchar(); if(ch == '\n') continue;
putchar(ch); //换行符号也被包含在计数中,因此读取包含换行符在内的10个字符
}
break:循环中的break语句导致程序终止包含他的循环,并进行程序的下一阶段。不是跳到下一循环周期,而是直接循环退出。break语句使程序直接转到紧接着该循环后的第一条语句去执行。嵌套循环中的break语句只是使程序跳出里层的循环,要跳出外层的循环还需要另外一个break语句。
4.5 多重选择:switch和break
1、使用switch语句
switch后面的圆括号里的表达式被求值,就是刚刚输入给ch的值。然后程序扫描标签列表,直到搜索到一个与该值匹配的标签。然后程序跳转到那一行。要是没有匹配的标签,程序跳到default的标签行。
形式:switch(number)
{
case 1: 语句1;break;case 2: 语句2;break;
case 3: 语句3;break;case 4: 语句4;break;
default:语句5;
}break语句导致程序脱离switch语句,跳到switch之后的下一条语句。如果没有break语句,从相匹配的标签到switch末尾的每一条语句都将被处理。
break语句用于循环和switch中,而continue仅用于循环。
不能在C的case中使用一个范围。圆括号中的switch 判断表达式应该具有整数值(包括char类型)。 case标签必须是整形(char)常量或者整数常量表达式。不能用变量作为case标签。
2、只读取一行的首字符
while((ch = getchar()) != ‘\n’) continue;//跳过输入行的剩余部分
if(ch == ‘\n’) continue;
3、switch和if else
什么时候用哪个?没有选择的。 如果选择是基于求一个浮点型变量或表达式的值,就不能使用switch。 如果变量必须落入某个范围,也不能很方便的使用switch。
五.函数
一. 为什么需要函数
避免了重复性操作
有利于程序的模块化
函数是C语言的基本单位,类是Java、C#、C++的基本单位
C语言没有子程序概念,C语言有函数概念,函数就相当于一个子程序,允许函数单独进行编译,可以实现模块化
二. 什么叫函数
逻辑上: 能够完成特定功能的独立的代码块
物理上:
能够接受数据(当然也可以不接受)
对接受的数据进行处理
能够将数据处理的结果返回(当然也可以不返回)
总结:函数是个工具,为了解决大量类似问题而设计的,当做一个黑匣子
三. 函数概述
1.对于一个C程序而言,它所有的命令都包含在函数内。每个函数都会执行特定的任务。
2.一个C程序可由一个主函数和若干个其他函数构成,并且只能有一个主函数。
3.主函数可以调用其他普通函数,普通函数不能调用主函数,其他普通函数之间也可以相互调用。
4.主函数是程序的入口,也是程序的出口。
5.一个函数中不能定义另一个函数,只能调用其他函数
6.C程序的执行总是从main()函数开始,调用其他函数完毕后,程序流程回到main()函数,继续执行主函数中的其他语句,直到main()函数结束,则整个程序运行结束。
四. 如何定义函数
1. C 语言中的函数定义的一般形式如下:
return_type function_name( parameter list ) //函数头
{
body of the function //函数体( {}之间的内容 )
}
或
函数返回值类型 函数名(函数的形参列表)
{
函数的执行体
}
函数由一个函数头和一个函数主体组成。
2. 示例:
int add(int a, int b)
{
int result;
rusult = a + b;
return result;
}
下面列出一个函数的所有组成部分:
返回类型: 一个函数可以返回一个值。return_type 是函数返回的值的数据类型。有些函数执行所需的操作而不要返回值,在这种情况下,return_type 是关键字 void。
函数名称: 这是函数的实际名称。函数名和参数列表一起构成了函数签名。
形参列表: 参数就像是占位符。当函数被调用时,您向参数传递一个值,这个值被称为实际参数。参数列表包括函数参数的类型、顺序、数量。参数是可选的,也就是说,函数可能不包含参数。
函数主体: 函数主体包含一组定义函数执行任务的语句。
3. 定义函数的本质
详细描述函数之所以能够实现某个特定功能的具体方法
4. 形式参数和实际参数
在定义函数时,函数名后面括号中的变量称为:形参
在主调函数中,函数名后面括号中的参数称为:实参
参数的传递
传递值:
函数名(实参列表)
传递引用
函数名(&参数)
5. return 表达式; 的含义
终止被调函数,向主调函数返回表达式的值
如果表达式为空,则只终止函数,不向主调函数返回任何值
break是用来终止循环和switch语句的,return是用来终止函数的
6. 返回值类型问题
函数的返回值的类型也称为函数的类型,因为如果函数名前面的返回值类型和函数执行体中的return 表达式中表达式的类型不同的话,则最终函数返回值的类型以函数名前面的返回值类型为准,由系统自动进行强制转换。
当函数没有指明返回值,或没有返回语句时,函数返回一个不确定的值。为了使函数不返回任何值,可以使用void定义无返回值类型函数。
五. 函数的分类
六. 函数的声明
注:函数调用和函数定义的顺序
若函数调用写在了函数定义的前面,则必须加函数前置声明
1
函数前置声明:
告诉编译器即将可能出现的若干个字母代表的是一个函数
告诉编译器即将可能出现若干个字母所代表的函数的形参和返回值的具体情况
函数声明是一个语句,末尾必须加分号
对于库函数的声明通过 #include <库函数所在的文件的名字.h> 来实现
七. 函数的调用
函数调用的一般形式:函数名(实参列表);
函数调用分类:
调用无参函数:函数名();
调用有参函数:函数名(实参列表);
若实参列表中有多个实参,各参数间用逗号隔开
C调用函数是,只能传值给函数形参
形参和实参必须个数相同、位置一一对应、数据类型必须相互兼容
举例
#include
/* 函数声明 */
int max(int num1, int num2);
int main()
{
/* 局部变量定义 */
int a = 100;
int b = 200;
int ret;
/* 调用函数来获取最大值 */
ret = max(a, b);
printf("Max value is : %d\n", ret);
return 0;
}
/* 函数:返回两个数中较大的那个数 */
int max(int num1, int num2)
{
/* 局部变量声明 */
int result;
if (num1 > num2)
result = num1;
else
result = num2;
return result;
}
函数的嵌套调用
C语言的函数定义都是独立的、互相平行的,C语句不允许嵌套定义函数,即一个函数内不能定义另一个函数,但可以嵌套调用函数,即在调用一个函数的过程中,又调用另一个函数。
函数的递归调用
定义:在调用一个函数的过程中又出现直接或间接调用该函数自己的。
递归满足的三个条件:
递归必须得有一个明确的中止条件;
该函数所处理的数据规模必须在递减(值可以增加);
这个转化必须是可解的;
举例
//下面的实例使用递归函数生成一个给定的数的斐波那契数列:
#include
int fibonaci(int i)
{
if(i == 0)
{
return 0;
}
if(i == 1)
{
return 1;
}
return fibonaci(i-1) + fibonaci(i-2);
}
int main()
{
int i;
for (i = 0; i < 10; i++)
{
printf("%d\t\n", fibonaci(i));
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
八. 数组作为函数参数
调用有参函数时,需要提供实参
例如:sin(x)、sqrt(2.0)、max(a,b)
实参可以是常量、变量或表达式
数组元素的作用与变量相当,一般来说,凡是变量可以出现的地方,都可以用数组元素代替
1.数组元素也可以用作函数实参,其用法与变量相同,向形参传递数组元素的值
2. 数组名也可以作为实参和形参,传递的是数组第一个元素的地址
1. 数组元素作为函数实参
数组元素可以作为函数的实参,与用变量作为实参一样,按照单向值传递的方式进行传递
数组元素不能作为形参,因为形参是在函数被调用时临时分配存储单元的,不可能为一个数组元素单独分配存储单元
/*
输入10个数,要求输出其中值最大的元素和该数是第几个数
*/
#include
int max(int x, int y)
{
return (x>y? x:y);
}
int main(void)
{
int a[10],m,n,i;
for(i=0; i<10; i++)
{
scanf("%d", &a[i]);
}
printf("\n");
for(i=1,m=a[0],n=0;i<10;i++)
{
if(max(m,a[i])>m) //若max函数返回的值大于m
{
m=max(m,a[i]); //max函数返回的值取代m原值
n=i;
}
}
printf("The largest number is %d\n it is the %dth number.\n", m, n+1);
return 0;
}
2. 数组名可以作为函数参数
可以用数组名作为函数参数,此时实参与形参都应该用数组名,此时的数组名是整个数组的首地址
向形参(数组名或指针变量)传递的是数组首元素的地址
(1). 一维数组名作函数参数
/*
题目:有一个一维数组score,内放10个学生成绩,求平均成绩
结论:
1. 用数组名作函数参数,应该在主调函数和被调函数分别定义数组
2. 实参数组和形参数组类型应一致
3. 形参数组名获得实参数组的首元素的地址,因为数组名代表数组的首元素的地址
所以可以认为,形参数组首元素(array[0])和实参数组首元素(score[0])具有同一地址
他们共同占用同一存储单元,score[n]和array[n]指的是同一单元,它两具有相同的值
形参数组中各元素的值发生变化会使得实参数组元素的值同时发生变化
4. 形参数组可以不指定大小,在定义数组时再数组名后面跟一个空的方括号
*/
#include
float average(float array[10]) //array形参数组名
//如下写法正确:float average(float array[]) 定义的average函数,形参数组不指定大小
/*
以上原因的本质是:系统对源程序进行编译时,编译系统会把形参数组处理为指针变量
即把float array[] 转换为float * array,该指针变量用来接收从实参数组传过来的地址
所以以上两者是等价的,对数组元素的访问,用下标和指针也是完全等价的
使用形参数组是为了便于理解,形参数组与实参数组各元素一一对应
*/
{
int i;
float aver;
float sum=array[0];
for(i=1;i<10;i++)
{
sum=sum+array[i];
}
aver=sum/10;
return aver;
}
int main(void)
{
float score[10];
float aver;
int i;
for(i=0;i<10;i++)
{
scanf("%f", &score[i]); //score是实参数组名
}
printf("\n");
aver=average(score); //调度average函数
printf("average score is %5.2f\n", aver);
return 0;
return 0;
}
(2). 多维数组名作函数参数
多维数组名作为函数的实参和形参,在被调函数中对形参数组定义时可以指定每一维的大小
例如:int array[3][10];
1
也可以省略第一维的大小说明
例如:int array[][10];
1
原因是:二维数组是由若干个一维数组组成的,在内存中,数组是按行存放的,因此在定义二维数组时,必须指定列数(即一行中包含几个元素)由于形参数组与实参数组类型相同,所以他们是由具有相同长度的一维数组所组成的,不能只指定第一维,而省略第二维
在第二维大小相同的前提下,形参数组的第一维可以与实参数组不同
例如:
实参数组定义为 int score[5][10]
形参数组定义为 int array[][10] 或 int array[8][10]
这时候形参数组和实参数组都是由相同类型和大小的一维数组组成,C语言编译不检查第一维大小
题目:有一个3*4的矩阵,求所有元素中的最大值
结论:
在主调函数,把实参二维数组a的第1行的起始地址传递给形参数组array
所以array数组第一行起始地址和a数组第一行起始地址相同
由于两个数组列数相同,所以a[i][j]与array[i][j]同占一个存储单元,
他们本质上是相同的,对形参数组操作就是对实参数组操作
*/
#include
int max_value(int array[][4])
{
int i,j,max;
max=array[0][0];
for(i=0;i<3;i++)
for(j=0;i<3;j++)
{
if(array[i][j]>max)
max=array[i][j] //把最大值放在max中
}
return 0;
}
int main(void)
{
int a[3][4]={{1,3,5,7},{2,4,6,8},{15,17,34,12}}; //对数组元素进行赋值
printf("Max value is %d\n", max_value(a)); //调用函数
return 0;
}
九. 变量的作用域和存储方式
一个变量的生存期(作用域)在一个块内存在/生效
块就是一个{ }
一个块里面只能定义一个名字的变量
1. 按作用域分:
全局变量
定义:在所有函数外部定义的变量
使用范围:从定义位置开始到整个程序结束
局部变量
定义:在一个函数内部定义的变量或者函数的形参
1
/* 例如 */
void f(int i)
{
int j=20;
}
/* i和j都是局部变量 */
使用范围:只能在本函数内部使用
注:全局变量和局部变量命名冲突问题
在一个函数内部如果定义的局部变量的名字和全局变量的名字一样时,局部变量会屏蔽掉全局变量
1
举例
#include
/*
此程序用来判断主函数里面实参变量i和子函数里面的形参变量i是不是同一个变量
说明形参和实参不是一个变量
修改形参的值,不影响实参的值,因为他们作用的范围不同
程序从main函数进入之后,分配了i变量的空间,当进行f()函数调用,又为f函数分配了另外一块存储空间,
当f函数执行完之后,分配给f的函数空间就会被释放,所以普通子函数不能改变主函数的值
*/
void f(int i)
{
i = 99; //i是局部变量
}
int main(void)
{
int i = 6; //i是局部变量
printf("i = %d\n", i);
f(i);
printf("i = %d\n", i);
return 0;
}
/*
结果:
i = 6
i = 6
*/
2. 按变量的存储方式分:
静态变量
当函数体内部用static来说明一个变量时,可以称该变量为静态局部变量。
它与auto变量、register变量的本质区别:
在整个程序运行期间,静态局部变量在内存中的静态存储区中占据着永久性的存储单元,
即使退出函数后下次再进入该函数时,静态局部变量仍然使用原来的存储单元。
由于不释放这些存储单元,这些存储单元中的值得以保存,因而可以继续使用存储单元中原来的值
静态局部变量的初值是在编译时赋予的,在程序执行期间不再赋以初值,
对未赋值的局部变量,C语言编译程序自动给他赋初值为0.
自动变量(auto变量)
当函数内部或复合语句内定义变量是,如果没有指定存储类型,或使用了auto说明符,系统就认为所定义的变量具有自动类别。
寄存器变量(register变量)
寄存器变量也是自动类变量,它与auto变量的区别仅在于:用register说明变量是建议编译程序将变量的值保存在CPU的寄存器中,而不是像一般变量那样占用内存单元。
十. 如何在软件开发中合理设计函数来解决实际问题
方法要求:
一个函数的功能尽量独立、单一
多学习,多模仿牛人的代码
十一. 常用的系统函数
double sqrt(double x); 求x的平方根
int sbs(int x); 求整数x的绝对值
double fabs(double x) 求浮点数x的绝对值
六.数组
1、一维数组
(1)数组是一组有序数据的集合
(2)用一个数组名(如s)和下标(如15)来唯一地确定数组中的元素,如s(15)就代表第15个学生的成绩。
(3)数组中的每一元素都属于同一个数据类型
一维数组的一般形式为:
类型说明符 数组名[常量表达式];
如何引用一维数组元素:
数组名[下标]
一维数组的定义:
int a[10] //前面有int,这是定义数组,指定数组包含10个元素
t=a[6] //这里的a[6]表示引用a数组中序号为6的元素
2、二维数组(矩阵)
二维数组定义的一般形式:
float a[3][4]
\\以上定义了一个float型的二维数组,第一维有3个元素,第二维有4个元素
如何引用二维数组:
数组名[下标][下标]
二维数组的初始化:
int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
3、字符数组
定义字符数组的方法与定义数值型数组的方法类似:
char c[10]={'I','a','m','','h','a','p','p','y'};
c[0]='I';c[1]=' ';c[2]='a';c[3]='m';c[4]=' ';
c[5]='h';c[6]='a';c[7]='p';c[8]='p';c[9]='y';
字符数组的初始化:
char c[6]={'I',' ','L','o','v','e'};
char c[]={'l','o','v','e'};
不设置元素个数时,会自动识别该数组长度为4
字符串和字符串结束标志:
C语言规定了一个“字符串结束标志”,以字符’\n’作为结束标志。如果字符数组中存在有若干字符,前面9个字符都不是空字符(’\0’),而第10个字符是’\0’,则认为数组中有一个字符串,其有效字符为9个。也就是说,在遇到字符’\0’时,表示字符串结束,把它前面的字符组成一个字符串。
字符数组的输入输出:
(1)逐个字符输入输出。用格式符"%c"输入或输出一个字符。
(2)将整个字符串一次输入或输出。用"%s"格式符,意思是对字符串(string)的输入输出。例如:
char c[]={"China"};
printf("%s\n",c);
存储情况为
C h i n a \0
输出为
china
4、使用字符串处理函数
(1)puts函数——输出字符串的函数
其一般形式为:
puts(char str[])
1.用puts函数输出的字符串中可以包含转义字符。
2.再用puts输出时将字符串结束标志’\0’转换成’\n’,即输出完字符串后换行。
3.puts函数在遇到空格也会输出。
(2)gets函数——输入字符串的函数
其一般形式为:
gets(char str[])
1.从终端输入一个字符串到字符数组,并得到一个函数值。
2.该函数值是字符数组的起始地址。
3)strcat函数——字符串连接函数
其一般形式为:
strcat(char str1[],char str2[])
1.其作用是把两个字符数组中的字符串连接起来,把字符串2连接到字符串1的后面,结果放在字符数组1中。
2.字符数组1必须足够大,以便容纳连接后的新字符串。
3.连接前两个字符串的后面都有’\0’,连接时将字符串1后面的’\0’取消,只在新字符串最后保留’\0’。
4)strcpy和strncpy函数——字符串复制函数
其一般形式为:
strcpy(字符数组1,字符串2)
1.作用是将字符串2复制到字符数组1中去,并且字符数组1必须定义得足够大,以便容纳被复制的
2."字符数组1"必须写成数组名形式(如str),"字符串2"可以是字符数组名,也可以是一个字符串常量。例如:strcpy(str1,“C program”);作用与前面相同。
3.不能用赋值语句将一个字符串常量或字符数组直接给一个字符数组。字符数组名是一个地址常量,它不能改变值,正如数值型数组名不能被赋值一样。
1.作用是将字符串2中最前面index个字符复制到字符数组1中,取代字符数组1中原有的最前面index个字符。
2.复制的字符个数n不应多余字符数组1中原有的字符(不包括’\0’)。
(5)strcmp函数——字符串比较函数
其一般形式为:
strcmp(字符串1,字符串2)
1.作用是比较字符串1和字符串2,比较的结果有函数值带回。
(1)如果字符串1与字符串2相同,则函数值为0。
(2)如果字符串1>字符串2,则函数值为一个正整数。
(3)如果字符串1<字符串2,则函数值为一个负整数。
2.字符串比较规则:将两个字符串自左向右逐个字符相比(按ASCII码值大小比较),直到出现不同的字符或遇到’\0’为止。
(1)如全部字符相同,则认为两个字符串相等;
(2)若出现不相同的字符,则以第1对不相同的字符的比较结果为准。
(6)strlen函数——测字符串长度的函数
其一般形式为:
strlen(字符数组)
7)strlwr函数和strupr函数——分别为转换为小写和大写的函数
其一般形式为:
strlwr(字符串);
strupr(字符串);
七.结构体和联合体
结构体
在C语言中,可以使用结构体(Struct)来存放一组不同类型的数据。结构体的定义形式为:
struct 结构体名{
结构体所包含的变量或数组
结构体是一种集合,它里面包含了多个变量或数组,它们的类型可以相同,也可以不同,每个这样的变量或数组都称为结构体的成员(Member)。请看下面的一个栗子:
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
};
stu 为结构体名,它包含了 5 个成员,分别是 name、num、age、group、score。结构体成员的定义方式与变量和数组的定义方式相同,只是不能初始化。
注意大括号后面的分号“;”不能少哦~
结构体也是一种数据类型,它由我们自己来定义,可以包含多个其他类型的数据。
像int、float、char 等是由C语言本身提供的数据类型,不能再进行分拆,我们称之为基本数据类型;而结构体可以包含多个基本类型的数据,也可以包含其他的结构体。
结构体变量
既然结构体是一种数据类型,那么就可以用它来定义变量。例如:
struct stu stu1, stu2;
1
定义了两个变量 stu1 和 stu2,它们都是 stu 类型,都由 5 个成员组成。注意关键字struct不能少。
还可以在定义结构体的同时定义结构体变量:
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
} stu1, stu2;
如果只需要 stu1、stu2 两个变量,后面不需要再使用结构体名定义其他变量,那么在定义时也可以不给出结构体名,如下所示:
struct{ //没有写 stu
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
} stu1, stu2;
这样的写法很简单,但是因为没有结构体名,后面就没法用该结构体定义新的变量了。
理论上讲结构体的各个成员在内存中是连续存储的,和数组非常类似,例如上面的结构体变量 stu1、stu2 的内存分布如下图所示,共占用 4+4+4+1+4 = 17 个字节。但是在编译器的具体实现中,各个成员之间可能会存在空隙,C语言中,结构体大小的内存分配,参考于这片文章:C语言中结构体大小计算即存储分配
这里我在做下总结:
运算符sizeof可以计算出给定类型的大小,对于32位系统来说,sizeof(char) = 1; sizeof(int) = 4。基本数据类型的大小很好计算,我们来看一下如何计算构造数据类型的大小。
C语言中的构造数据类型有三种:数组、结构体和共用体。
数组是相同类型的元素的集合,只要会计算单个元素的大小,整个数组所占空间等于基础元素大小乘上元素的个数。
结构体中的成员可以是不同的数据类型,成员按照定义时的顺序依次存储在连续的内存空间。和数组不一样的是,结构体的大小不是所有成员大小简单的相加,需要考虑到系统在存储结构体变量时的地址对齐问题。看下面这样的一个结构体:
struct stu1
{
int i;
char c;
int j;
};
先介绍一个相关的概念——偏移量。偏移量指的是结构体变量中成员的地址和结构体变量地址 的差。结构体大小等于最后一个成员的偏移量加上最后一个成员的大小。显然,结构体变量中第一个成员的地址就是结构体变量的首地址。因此,第一个成员i的偏 移量为0。第二个成员c的偏移量是第一个成员的偏移量加上第一个成员的大小(0+4),其值为4;第三个成员j的偏移量是第二个成员的偏移量加上第二个成 员的大小(4+1),其值为5。
实际上,由于存储变量时地址对齐的要求,编译器在编译程序时会遵循两条原则:一、结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍) 二、结构体大小必须是所有成员大小的整数倍。
对照第一条,上面的例子中前两个成员的偏移量都满足要求,但第三个成员的偏移量为5,并不是自身(int)大小的整数倍。编译器在处理时会在第二个成员后面补上3个空字节,使得第三个成员的偏移量变成8。
对照第二条,结构体大小等于最后一个成员的偏移量加上其大小,上面的例子中计算出来的大小为12,满足要求。
再看一个满足第一条,不满足第二条的情况:
struct stu2
{
int k;
short t;
};
成员k的偏移量为0;成员t的偏移量为4,都不需要调整。但计算出来的大小为6,显然不 是成员k大小的整数倍。因此,编译器会在成员t后面补上2个字节,使得结构体的大小变成8从而满足第二个要求。由此可见,大家在定义结构体类型时需要考虑 到字节对齐的情况,不同的顺序会影响到结构体的大小。对比下面两种定义顺序
struct stu3
{
char c1;
int i;
char c2;
}
struct stu4
{
char c1;
char c2;
int i;
}
虽然结构体stu3和stu4中成员都一样,但sizeof(struct stu3)的值为12而sizeof(struct stu4)的值为8。
如果结构体中的成员又是另外一种结构体类型时应该怎么计算呢?只需把其展开即可。但有一点需要注意,展开后的结构体的第一个成员的偏移量应当是被展开的结构体中最大的成员的整数倍。看下面的例子:
struct stu5
{
short i;
struct{
char c;
int j;
} ss;
int k;
}
结构体stu5的成员ss.c的偏移量应该是4,而不是2。整个结构体大小应该是16。
如何给结构体变量分配空间由编译器决定,以上情况针对的是Linux下的GCC。其他平台的C编译器可能会有不同的处理,看到这里估计还是有些同学不太明白,多看几遍,领悟领悟,就好啦!
成员的获取和赋值
结构体和数组类似,也是一组数据的集合,整体使用没有太大的意义。数组使用下标[ ]获取单个元素,结构体使用点号.获取单个成员。获取结构体成员的一般格式为:
结构体变量名.成员名;
1
通过上面的格式就可以获取成员的值,和给成员赋值,看下面的栗子:
#include
int main(){
struct{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1;
//给结构体成员赋值
stu1.name = "haozi";
stu1.num = 12;
stu1.age = 18;
stu1.group = 'A';
stu1.score = 136.5;
//读取结构体成员的值
printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", stu1.name, stu1.num, stu1.age, stu1.group, stu1.score);
return 0;
}
运行结果:
haozi的学号是12,年龄是18,在A组,今年的成绩是136.5!
除了这种方式赋值外,还可以在定义的时候赋值:
struct{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1, stu2 = { "haozi", 12, 18, 'A', 136.5 };
不过整体赋值仅限于定义结构体变量的时候,在使用过程中只能对成员逐一赋值,这和数组的赋值非常类似。
结构体与指针
指针也可以指向一个结构体,定义的形式一般为:
struct 结构体名 *变量名;
看例子:
struct Man{
char name[20];
int age;
};
void main(){
struct Man m1 = {"Jack",30};
//结构体指针
struct Man *p = &m1;
printf("%s,%d\n", m1.name, m1.age);
printf("%s,%d\n",(*p).name,(*p).age);
//“->”(箭头)是“(*p).”简写形式
printf("%s,%d\n", p->name, p->age);
//(*env)->
system("pause");
}
编译出的结果是:
Jack,30
Jack,30
Jack,30
请按任意键继续. . .
上面代码:printf(“%s,%d\n”, m1.name, m1.age);还可以换成: printf(“%s,%d\n”, (*p).name, m1.age);或者 printf(“%s,%d\n”, p->name, m1.age);
其运行结果还是一样的。
获取结构体成员通过结构体指针可以获取结构体成员,一般形式为:
(*pointer).memberName
或者:
pointer->memberName
联合体(共用体)
结构体(Struct)是一种构造类型或复杂类型,它可以包含多个类型不同的成员。在C语言中,还有另外一种和结构体非常类似的语法,叫做共用体(Union),它的定义格式为:
union 共用体名{
成员列表
};
共用体有时也被成为联合体;
结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
共用体也是一种自定义类型,可以通过它来创建变量,例如:
union data{
int n;
char ch;
double f;
};
union data a, b, c;
上面是先定义共用体,再创建变量,也可以在定义共用体的同时创建变量:
union data{
int n;
char ch;
double f;
} a, b, c;
共用体 data 中,成员 f 占用的内存最多,为 8 个字节,所以 data 类型的变量(也就是 a、b、c)也占用 8 个字节的内存,请看下面的栗子:
#include
union var{
long j;
int i;
};
main(){
union var v;
v.j = 5;
printf("v.j is %d\n",v.i);
v.i = 6; //最后一次赋值有效
printf("now v.j is %ld! the address is %p\n",v.j,&v.j);
printf("now v.i is %d! the address is %p\n",v.i,&v.i);
system("pause");
}
编译并运行结果:
v.j is 5
now v.j is 6! the address is 0xbfad1e2c
now v.i is 6! the address is 0xbfad1e2c
这段代码不但验证了共用体的长度,还说明共用体成员之间会相互影响,修改一个成员的值会影响其他成员。
一、指针是什么
由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化地称为“指针”。意思就是通过它能找到以它为地址的内存单元。
假如有输入语句:
scanf("%d",&x);
k=x+y;
这种直接按变量名进行的访问,称为**“直接访问”**方式
还可以采用**“间接访问”**的方式,即将变量i的地址存放在另一个变量中,然后通过该变量来找到变量i的地址,从而访问i变量。
i_pointer=&i
1
(1)将3直接送到变量i所标识的单元中,例如“i=3”。
(2)将3送到变量i_pointer所指向的单元(即变量i的存储单元),例如“* i_pointer=3;”,其中* i_pointer表示i_pointer指向的对象。
指向就是通过地址来体现的,由于通过地址能找到所需的变量单元,因此说,地址指向该变量单元(如同说,一个房间号指向一个房间一样),将地址形象化地称为“指针”。
如果有一个变量专门用来存放另一个变量的地址(即指针),则它称为“指针变量”,指针变量就是地址变量,用来存放地址,指针变量的值是指针。
二、指针变量
1、使用指针变量的例子
例题1:通过指针变量访问整型变量
#include
int main(){
int a=100,b=200; //定义整型变量a,b,并初始化
int * pa,* pb; //定义指向整形数据的指针变量pa,pb
pa=&a; //把变量a的地址赋给指针变量pa
pb=&b; //把变量b的地址赋给指针变量pb
printf("a=%d,b=%d\n",a,b); //输出变量a和b的值
printf("* pa=%d,* pb=%d\n",* pa,* pb); //输出变量a和b的值
return 0;
}
执行结果:
a=100,b=200
* pa=100,* pb=200
2、怎样定义指针变量
定义指针变量的一般形式为:
类型名 * 指针变量名
int *pa,*pb;
float * pc;
char * pd;
可以在定义指针变量时,同时对它初始化,如:
int * pa=&a,* pb=&b //定义指针变量pa,pb,并分别指向a,b
一个变量的指针的含义包括两个方面,一是以存储单元编号表示的纯地址,二是它指向的存储单元的数据类型(如 int,char,float等)。
指向整型数据的指针表示为“ int *”,读作“指向int的指针”或简称“int 指针”。
3、怎样引用指针变量
在引用指针变量时,可能有三种情况:
(1)给指针变量赋值,如:
p=&a //把a的地址赋给指针变量p
1
指针变量p的值是变量a的地址,p指向a
(2)引用指针变量指向的变量
如果已经执行(1)的操作,即指针变量p指向了整型变量a,则
printf("%d",* p);
1
其作用是以整数形式输出指针变量p所指向的变量的值,即变量a的值。
如果有以下赋值语句:
* p=1;
1
表示将整数1赋给p当前所指向的变量,如果p指向变量a,则相当于把1赋给a,即”a=1“。
(3)引用指针变量的值,如
printf("%o",p);
1
作用是以八进制数形式输出指针变量p的值,如果p指向了a,就是输出了a的地址,即&a
& 取地址运算符
* 指针运算符(或称”间接访问“运算符),* p代表指针变量p指向的对象
例题2:输入a和b两个整数,按先大后小的顺序输出a和b
#include
int main(){
int * pa,* pb,* pc,a,b;
printf("请输入两个整数:\n");
scanf("%d,%d",&a,&b);
pa=&a;
pb=&b;
if(a
pc=pa;
pa=pb;
pb=pc;
}
printf("a=%d,b=%d\n",a,b);
printf("max=%d,min=%d\n",* pa,* pb);
return 0;
}
执行结果:
请输入两个整数:
4,5
a=4,b=5
max=5,min=4
3、指针变量作为函数参数
例题3:输入a和b两个整数,对输入的两个整数按大小顺序输出,用函数处理,而且用指针类型的数据作函数参数。
#include
int main(){
void swap(int * pa,int * pb);
int a,b;
int * p1,* p2;
printf("请输入两个整数:\n");
scanf("%d,%d",&a,&b);
p1=&a;
p2=&b;
if(a
swap(p1,p2);
}
printf("max=%d,min=%d\n",a,b);
return 0;
}
void swap(int * pa,int * pb){
int temp;
temp=* pa;
* pa=* pb;
* pb=temp;
}
执行结果:
请输入两个整数:
5,6
max=6,min=5
例题4:输入3个整数a,b,c,要求按大到小的顺序将它们输出。用函数实现。
#include
int main(){
void swaps(int * pa,int * pb,int * pc);
int a,b,c,* p1,* p2,* p3;
printf("请输入三个整数:\n");
scanf("%d,%d,%d",&a,&b,&c);
p1=&a;p2=&b;p3=&c;
swaps(p1,p2,p3);
printf("the order is:%d,%d,%d\n",a,b,c);
return 0;
}
void swaps(int * pa,int * pb,int * pc){
void swap(int * q1,int * q2);
if(* pa<* pb) swap(pa,pb);
if(* pa<* pc) swap(pa,pc);
if(* pb<* pc) swap(pb,pc);
}
void swap(int * q1,int * q2){
int temp;
temp=* q1;
* q1=* q2;
* q2=temp;
}
执行结果:
请输入三个整数:
9,5,10
the order is:10,9,5
4、通过指针引用数组
所谓数组元素的指针就是数组元素的地址,可以用一个指针变量指向一个数组元素。例如:
int a[10]={1,3,5,7,9,11,13,15,17,19};
int * p;
p=&a[0];
引用数组元素可以用下标法(如 a[3]),也可以用指针法,即通过指向数组元素的指针找到所需的元素。
在指针已指向一个数组元素时,可以对指针进行以下运算:
加一个整数(用+或+=),如p+1;
减一个整数(用-或-=),如p-1;
自加运算,如p++,++p;
自减运算,如p–,--p;
两个指针相减,如p1-p2(只有当p1和p2都指向同一个数组中的元素才有意义)
p+1指向同一个数组中的下一个元素,p-1指向同一个数组中的上一个元素
引用一个数组元素,可以用下面两种方法:
(1)下标法,如a[i]形式;
(2)指针法,如* (a+1)或* (p+i)。其中a是数组名,p是指向数组元素的指针变量,其初值p=a
例题5:通过指针变量输出整型数组a的十个元素。
#include
int main(){
int i,a[10],* p=a;
printf("请输入10个整数:");
for(i=0;i<=9;i++)
scanf("%d",p++);
p=a;
for(i=0;i<=9;i++,p++){
printf("%d\t",* p);
}
printf("\n");
return 0;
}
执行结果:
请输入10个整数:0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
5、用数组名作函数参数
字符串的引用方式:
例题6:将字符串a复制为字符串b,然后输出字符串b。
#include
int main(){
char a[]="I am a student.",b[20];
int i;
for(i=0;* (a+i)!='\0';i++){
* (b+i)=* (a+i);
}
* (b+i)='\0';
printf("string a is: %s\n",a);
printf("string b is:");
for(i=0;b[i]!='\0';i++){
printf("%c",b[i]);
}
printf("\n");
return 0;
}
执行结果:
string a is: I am a student.
string b is: I am a student.
#include
int main(){
char a[]="I am a boy.",b[20],* p1,* p2;
p1=a;p2=b;
int i;
for(;* p1!='\0';p1++,p2++){
* p2=* p1;
}
* p2='\0';
printf("string a is: %s\n",a);
printf("string b is: %s\n",b);
return 0;
}
执行结果:
string a is: I am a boy.
string b is: I am a boy.
一.基本介绍
1)#define 叫做宏定义命令它也是C语言预处理命令的一种,所谓宏定义,就是用一个标识符来表示一个字符串。如果在后面的代码中出现了该标识符,那么就全部替换成指定的字符串。
2)#define N 100 就是宏定义,N为宏名,100是宏的内容(宏所表示的字符串)。在预处理阶段,对程序中所有出现的“宏名”,预处理器都会用宏定义中的字符串区代换,这称为“宏替换”或“宏展开”。
宏定义是由源程序中的宏定义命令#define完成的,宏替换是由预处理程序完成的。
宏定义的形式
#define 宏名 字符串
1)#表示这是一条预处理命令,所有的预处理命令都以 # 开头。宏名是标识符的一种,命名规则和变量相同。字符串可以是数字、表达式、if语句、函数等。
2)这里所说的字符串是一般意义上的字符序列,不要和C语言中的字符串等同,它不需要双引号。
3)程序中反复使用的表达式就可以使用宏定义
宏定义注意事项和细节
0)宏定义实质:只替换,不计算。
1)宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单的替换。字符串中可以包含任何字符,它可以是常数、表达式、if语句、函数等,预处理程序对它不作任何检查,如有错误,只能在编译已被宏展开后的源程序时发现。
2)宏定义不是说明或语句,在行末不必加分号,如果加上分号则连分号一起替换。
3)宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令
4)代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替
#include
#define OK 100
int main(){
printf("OK%d\n",OK);
//引号内部没有宏替换,第二个OK宏替换了
}
5)宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换
#include
#define PI 3.14159
#define S PI*2*1
void main(){
printf("%f\n",S);
//在宏替换后变为
//printf("%f\n",3.14159*2*1);
}
6)习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母
7)可用宏定义表示数据类型,使书写方便
8)宏定义表示数据类型和用typedef定义数据说明符的区别:宏定义只是简单的字符串替换,由预处理器来处理;而typedef 是在编译阶段由编译器处理的,它并不是简单的字符串替换,而给原有的数据类型起一个新的名字,将它作为一种新的数据类型。
带参数的宏定义
基本介绍
1)C语言允许宏带有参数。在宏定义中的参数称为“形式参数”,在宏调用中的参数为“实际参数”,这点和函数有些类似
2)对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参
3)带参宏定义的一般形式为#define 宏名(形参列表) 字符串,在字符串中可以含有各个形参
4)带参宏调用的一般形式为:宏名(实参列表);
//带参数的宏定义
#define MAX(a,b) (a>b)?a:b
void main(){
int x,y,max;
printf("input two numbers:");
scanf("%d %d",&x,&y);
//说明
//1.MAX(x,y);调用带参数宏定义
//2.在宏替换时(预处理,由预处理器),会进行字符串的替换,同时会使用实参,去替换形参
//3.即MAX(x,y)宏替换后(x>y)?x:y
max = MAX(x,y);
printf("max=%d\n",max);
}
//1.MAX就是带参数的宏
//2.(a,b)就是形参
//3.(a>b)?a:b是带参数的宏对应字符串,该字符串可以使用形参
带参数宏定义的注意事项和细节
1)带参数宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现 例:
#define MAX(a,b) (a>b)?a:b如果写成了#define MAX (a,b) (a>b)?a:b将被认为是无参宏定义,宏名MAX代表字符串(a.b) (a>b)?a:b而不是:MAX(a,b) 代表(a>b)?a:b了
2)在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。而在宏调用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型
3)在宏定义中,字符串内的形参通常要用括号括起来以避免出错。
带参宏定义和函数的区别
1)宏展开仅仅是字符串的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存。
2)函数是一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数,就是执行这块内存中的代码
//要求 使用函数计算平方值,使用宏定义计算平方值,并总结二者的区别
#include
#include
/*int SQ(int y){
return ((y)*(y));
}
int main(){
int i=1;
while(i<=5){//1,4,9,16,25
printf("%d^2 = %d\n",(i-1),SQ(i++));
}
system("pause");
return 0;
}*/
#define SQ(y) ((y)*(y))
int main(){
int i=1;
while(i<=5){//这里相当于计算了1,3,5的平方
//进行循环 3 次,得到的是 1*1 = 1,3*3 = 9,5*5 = 25
//SQ(i++) 宏调用 展开 ((i++)*(i++))(i先运算相乘之后得“1”,再直接加二)
printf("%d^2=%d\n",i,SQ(i++));
}
system("pause");
return 0;
}
常见的预处理指令
指令 说明
# 空指令,无任何效果
#include 包含一个源代码文件
#define 定义宏
#undef 取消已定义的宏
#if 如果给定条件为真,则编译下面代码
#ifdef 如果宏定义已经定义,则编译下面代码
#ifndef 如果宏没有定义,则编译下面代码
#elif 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码
#endif 结束一个 #if…#else 条件编译块
预处理指令使用注意事项
1)预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的,程序员在程序中用预处理命令来调用这些功能。
2)宏定义可以带有参数,宏调用时是以实参代换形参,而不是“值传递”。
3)为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数两边也应加括号。
4)文件包含是预处理的一个重要功能,它可以用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。
5)条件编译允许只编译程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率
6)使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。