Uploaded by xuemei peng

C语言sherry

advertisement
高质量C语言编程
— C语言概述
3 Sept. 2008
© Neusoft Confidential
第一章 概述
C 程序的开发流程
IDE工具
编辑器
预处理器
动作名称
输出结果
编辑
文本文件 .c .h
预编译
处理后的文本
编译器
编译
汇编
.obj(.asm)文件
链接器
链接
.exe文件
加载器
加载
加载到内存
运行器
运行
这是“动态”“静
态”的分界线
C语言的标识符与关键字
只能是字母、数字、下划线组成,且首字符必须是字母或下划线。
输出
输入与输出
输入与输出
第二章 算法
1、简单算法举例看一下
2、算法的特性:有穷性、确定性、有零个或多个输
入、有一个或多个输出、有效性
3、流程图、伪代码能看懂就行,后期复习不用再看
第三章 数据类型、运
算符与表达式
数据类型的分类
整 型
基本类型
C
的
数
据
类
型
实 型
构造类型
空类型 void
类型重定义 typedef
字符型 char
短整型short
整型int
长整型long
枚举类型(enum)
单精度型float
双精度型double
长双精度型long double
数组
结构体(struct)
共用体(union)
指针类型
…
不是独立类型.
C语言的基本元素-变量
变量:在程序运行过程中,其值可以改变的量叫变量。
变量在内存中占一定的存储单元,在存储单元中存放变量的值。
C语言中变量一定要先定义,再使用。且所有的变量定义要放在
其作用域的第一条可执行语句之前。
变量定义的一般格式:
数据类型 变量1[= 初值],变量2[= 初值],…,变量n [= 初值];
决定了分配的字节数
和取值范围。
int a = 10;//定义的同时初始化
int a;//定义时未初始化
a = 10;//赋值
变量的特点
申请空间并初始化变量:
int a = 5;
Memory
Address
一个变量必然包含以下概念:
名字(变量名)
Name (a)
Value(5)
Type
(int)
类型(※)
值(变量的内容)
地址(变量在内存中所处的位置)
字节
变量的初值
#include <stdio.h>
int i;
void main( )
{
static int j;
int k;
printf("%d,%d,%d.\n",i, j, k);
}
这运行结果说明
了什么?
0, 0, -485173492
结论:
当定义变量没有初始化它时,
全局变量、静态变量则由系统初始化为默认值。
而局部变量则不会被初始化,它会是任意值。
变量定义和变量声明
变量声明:仅告诉编译器变量的类型、存储类型,不分配存
储空间 。
如 extern int i;
变量定义:告诉编译器在此处分配存储空间且创建变量。
如 int i= 1;
程序中,一个变量只能定义一次,但变量声明可以有多次。
P
奇怪的运行结果
判读这段代码的运行结果,分析其原因:
void main( )
{
int i = -1;
发生了隐式类型转换
unsigned int ui ;
ui = i;
printf("%d %u\n",i,ui);
}
竟然是:
-1 4294967295
为什么会是这样?
负数的存储形式
C语言对于负数是以“补码”形式表示的。所谓“补码”即将
一个负数去掉符号后“求反加1”。
如, int i = 1; 在内存中存储为:
0000 0000 0000 0000 0000 0000 0000 0001 1的原码
int x = -1; 在内存中存储为:
1111 1111 1111 1111 1111 1111 1111 1111 -1的补码
当 unsigned int ui = x; 后,ui的内存视图为:
被视作原码
1111 1111 1111 1111 1111 1111 1111 1111
X的值原封不动的给了ui。
结论:
不同类型的变量赋值时会发生隐式类型转换。这是应当避免的。
P
数据类型的意义
已在前页的图上表现出来。
定义了数据占用的内存空间大小;
定义了数据的取值范围;
类型的字节数也决定了取值范围
定义了数据在内存中的存储格式;
决定了数据的运算行为;
为编译器提供了检查依据。
怎么来理解“决定了数据的运算行为”?
怎么来理解“为编译器提供了检查依据”?
void main()
void main()
{
{
int a = -10;
float a = -10.3f;
unsigned b = 20; float b = 20.2f;
if( a>b)
printf(“%d\n”, a%b );
printf(“%d > %d\n”,a,b);
//…
else
}
printf(“%d >=
%d\n”,b,a);
结果是编译出错!
因为C语言要求求模运算
}
的两个操作数都必须是整型。发现 a和b是
你认为结果如何? 浮点型,不符合类型要求,报错。
竟然是-10>20 ! Why?
同一块内存的不同视角
一块内存,4个字节中存有二进制数据
void main(
)
00110000
{
11000001
11101100
float f = 3.14f; //对于一块内存,按浮点型初始化
00000000
int * ip = (int *)(& f);
char * cp = (char *)(& f);
printf("the float : %f\n", f); //以浮点视角看
printf("the int : %d\n", *ip); //以整型视角看
printf(“the char : %s\n”, cp); //以字符型视角看
这个例子说明了类型是解释一
}
串二进制码到底表示何种含义
的关键,只有解释才会使之有
意义。可见“类型决定了数据
在内存中的存储格式” 。
运行结果:
the float : 3.140000
the int : 1078523331
the char : 悯H@?
类型决定了数据的取值范围
int main(void)
{
char a[1002];
int i;
}
P
for(i=0; i<1002; i++){
a[i] = -1-i;
}我们知道在计算机系统中,负数一律用补码存储 。按
补码规则,-1 的补码为0xff,-2 的补码为0xfe……当 i
for(i=0;
i<1002; i++){
的值为127时,a[127]的值为-128,而-128是char类型
printf(“%d、”, a[i] ); // 你认为会打印什么结果?
数据能表示的最小的负数。当
i继续增加,a[128]的值
}不是-129。因为-129 需要 9 位才能存储得下。这时发
生了溢出,最高位被丢弃。低8
结果是: 位是原来 9 位补码剩下
return
0; 0x7f。当 i 继续增加到255时,a[255]的值不
的值,即
-1、-2、-3、-4、…、-127、-128、
是-256,它的低 8位为全
0。即 a[255]的值为0、…
0。如此
127、126、125、…、
又开始一轮新的循环……
数据类型起作用的时机
数据类型起作用的时机在编译时。就是说,数据类型是给
编译器用的,等到编译完成,运行程序时,所有的事情都已
确定下来了。编译完成的结果是机器码,它仅仅是CPU运行
的指令集,此时不再作任何类型判断,数据类型的作用已经
完成了历史使命。
假定我们有一个工程, 它由两个.C源文件组成,一个文件中
包含外部变量n的声明:extern float n;
另一个文件中包含外部变量n的定义:int n; 两个n类型不相同!
大多数编译器不能检查出这个错误。编译器在编译一个源
文件时,并不知道另一个源文件的内容。
类型转换的禁忌
编译器像个警惕的哨兵,它根据类型,时时监视着程序对
各种数据的使用,这种不讲情面的严格检查,可以避免许多人
为的错误。但也给程序员带来种种限制和不便。
于是,C语言也提供一种通融的手段——强制类型转换,用
于放宽类型检查的制约。
原则上讲,各种类型之间都是可以互相转换的(只要理由
充分)。对于 int fun(void)函数和double d可以 d = fun(),但
是printf("%lf",fun());是垃圾值。
另,C语言不允许任何类型转换为数组或函数类型,并且不
允许任何数组之间或函数之间相互转换。
凡默认类型转换都是数据安全的,而强制类型转换不能保证
数据的安全性。
P
数组类型
数组类型是由基本类型衍生出来的构造类型。
char s=0;
//申请空间并初始化
char str[2]=“A”; // 定义了一个数组
typedef char
char[2]
INTARRAY[2];
INTARRAY;
INTARRAY str =“A”;
数组名:str
类型:char [2]
值:“A”
地址:0x12ff7c
数组名 (str)
类型(char [2])
数组元素无名,
元素类型是char
65
\0
地址(0x12ff7c)
地址(0x12ff7d)
指针类型
指针类型是由基本类型衍生出来的构造类型。
char a=0;
//申请空间并初始化
char * p=&a;
//基本类型所衍生出来的指针类型。
typedef char *
PChar;
PChar p =&a;
*p =0x0A;
指针名:p
类型:char *
值:0x12ff7c
地址:0x12ff80
变量名 (a)
类型 (char)
0x0A
地址(0x12ff7c)
指针名 (p)
0x7c
地址(0x12ff80)
类型 (char *)
0xff
0x12
0x00
void类型的指针
void 类型是一种特殊类型,表示“无类型”类型。
只能用于指针和函数。
各种各样的指针,无论多么复杂,写在指针前面的类型就是它
尽管称之为“无类型”类型,可表示它是
所指向的目标类型,但有一种指针除外——void型的指针。
“万能型”——能接受任何类型、但没指
明确定类型的一种类型。
void *p;
说明: 无类型的指针,是说 p指针没保存类型信息。void 不是
目标类型,而是用于表明“指针的行为不确定”。该指针只
是用来保存地址,是个“地址存储器”。
于是编译器不知该怎么用这个地址,因此,对于void类型的指
针,使用前一定要强转,以弥补它缺失的类型信息,即指明
它缺失的行为(比如:malloc()函数的返回)。
P
结构体类型和共用体类型
结构体类型是由基本类型衍生出来的构造类型。它包含了
多个成员,每个成员都可以是基本类型或构造类型。每个成员
按定义次序排列存储空间,各成员共存于结构体变量之内。
用途:用于将五花八门的多个类型整合为一种新类型。
共用体类型是由基本类型衍生出来的构造类型。它包含了
多个成员,每个成员都可以是基本类型或构造类型。每个成员
尽管按次序定义,但它们共用了存储空间,各成员在共用体变
量内是互斥的。
例题
写一个C函数,若处理器是Big_endian,则返回非0值;
若Little_endian,则返回0。
函数原型:bool IsBigendian():
bool IsBigendian()
{
union {
int i;
给四个字节里放上不同的数
char ch[4];
} test;
test.i= 0x12345678;
return (test.ch[0]!=0x78);
}
P
只看低字节里的内容
嵌套结构体变量的初始化:
结构体、共用体、枚举如同数组一样可以使用初始化表——
是用一对花括号括起来的一系列的值组成。
struct S1 {
int a;
这种类型间的关系叫嵌套
struct S2
{
double b ;
char c;
} b;
char c[4];
} x = { 1, { 4.5 } }; //OK. 属于不完全初始化
相当于 x = { 1, { 4.5,’\0’ } ,{ 0,0,0,0 } };
注意:在不完全初始化时,可省略的值只可以排在后面,不可以
放在前面或夹在中间。初始化式还可以省略内层花括号。
结构体的成员是数组还是指针的区别
看下例:
#define LEN 20
struct name {
char first [LEN];
char last [LEN];
};
struct pname {
char *first ;
char *last ;
};
这是两个不同的结构体类型,
struct name veep = {“Talia” , “Summers”};
struct pname treas = { “Brad”, “Fallingjaw” };
printf(“%s and %s\n”,veep.first ,treas.first);
这样看来它们是正确代码。但想想字符串存在哪?
对于veep来说,字符串是存在结构体内部,占用40个字节。
但对于treas 则存于字符串常量区,结构中只是存放着两个
地址,8个字节。这只适用于别处已为字符串分配了数组时,否
则会出问题。例如:
struct name accountant; (会计师)
struct pname attorney; (律师)
puts(“Enter the last name of your accountant:”);
scanf(“%s”,accountant.last);
puts(“Enter the last name of your attorney:”);
scanf(“%s”,attorney.last); //此时将名字送到哪去了?
这段代码可能运行正常,也可能无法运行。运行正常更可怕,因
为它掩盖了错误,使得错误象定时炸弹一样危险。
P
结构体变量占用内存大小
下面两个结构体类型声明中的成员类型、名称、个数完全一致,
只是成员排列顺序不同,两个结构体类型会一样大小吗?
struct A
{
char c1;
int a;
float f;
char c2;
};
struct B
{
char c1;
char c2;
int a;
float f;
};
sizeof(struct A) 是 16 sizeof(struct B) 是 12
为什么会这样?
原因分析
常识:因为32位的CPU字长是32位,所以读内存数据时,要
一次读取4字节的内容,即读取4个字节数据的首地址要能被4
整除!
int型的数据(蓝色部分)若以下面两种方式存放在内存中,
如下图所示:
内存地址
(4的整数倍)
4000
4004
4008
400C
前一个数据要分两次读取,后一个可以
一次读取完毕。哪个访问效率高?
P
边界对齐
为了避免随意分配内存造成的碎片现象,也为了加快CPU的
存取速度,提高运行效率,C语言编译器在处理数据时,采
用了“对齐原则”。即,把结构体变量中成员按照首地址是
4的整数倍予以存储,这种结构体成员的对齐原则就叫边界
对齐。
sizeof(struct A) == 16;就是这个原因。是编译器为了使数据
符合对齐原则,在结构中加上了一些填充字节造成的。
这样做可能会稍浪费一点内存空间,但却换来了效率的大幅
提高。
结构体类型大小
struct
{
int a;
char b;
} s1;
struct
{
int a;
char b;
double c;
}s2;
struct
{
int a;
double c;
char b;
} s3;
以上变量s1、s2、s3所占用的空间大小分别是多少?
8、16、24
结论:在设计结构体时,要合理的排布结构体的各个成员的顺序
,使之所占用的内存尽量节省。否则会因设计不当而无谓浪费内
存空间。
填充字节
边界对齐的细节和编译器实现相关。一般而言,
结构体每个成员相对于结构体首地址的偏移量,是该成员大小
与对齐基数(默认为8)中的较小者的整数倍,必要时,编译器
会在成员间加上填充字节;
结构体变量的大小是结构体中最大数据成员所占字节数与对齐
基数中较小者的整数倍。
s1占用内
存图示:
struct A
{ char c1;
int a;
float f;
char c2;
}s1;
c1
padding
a
f
c2
padding
所以结构体变量s1所占的内存空间大小是16。
关于“对齐基数”
“对齐基数”是1,2,4,8,16等数字,后者是前者的2 倍,编
译器默认的是 8。
但是,这个值是可以人为指定的。例如,使用
# prama pack( 4 )
就可以对编译器指定以 4 为对齐基数。
还可以用 # prama pack( push,4 ) 使规则生效;
用 # prama pack( pop ) 使规则结束。
“对齐” 规则补充
struct A
由于结构体的成员可以是复合类型, {
比如含有另外一个结构体,
int i;
double d;
1. 在寻找最宽基本类型成员时,
应当考虑到复合类型成员的子成员, };
对齐基数是8,
而不是把复合成员当成是一个整体
struct B
看待。
{
一个8
2. 但在计算复合类型成员的偏移
int i;
位置时,则是将复合类型作为整体
struct A a; 两个8
看待。
char ch; 一个8
}s1;
struct B 类型所占内存空间大小是多少?
P
32:8*4
练习
struct
{
double value;
char name[ 10 ];
} sc1; 占几个字节?
填充 6
value 8
name 10
struct
{
char name[ 10 ];
double value;
} sc2; 又占几个字节?
填充 6
name 10
value 8
例题
#include <stdio.h>
typedef struct student {
4
int num;
// 4
40
char name[2][20]; // 40
4
float aver_score;
// 4
4 有填充
char sex;
// 1
4
int age;
// 4
32 有填充
char addr[30];
// 30
}Stu;
void main() {
88
为什么?
Stu b;
printf(“%d\n”, sizeof(b)); // 你认为会打印83吗?
}
共用体有“对齐原则”吗?
也有! 比如:
value 8
填充 8
union U
{
double value ;
char name[10];
填充 6
name 10
};
由于该共用体的两个成员类型不同,且占用的空间长度不同,
要共用同一块内存空间,就必须用“对齐原则”来使用这块
相同大小的内存空间。
另外,对共用体成员的使用要符合最后一次赋值的类型,否则
会出现未可知的后果。
这个要求必须达到,但通常很难。如何做到?往下看:
如何得知共用体使用的是哪个成员?
下面给出一个解决方案:
首先定义一个枚举类型,用以标识共用体中哪个成员是可用的:
enum widget_tag { count_w, value_w, name_w };
然后再用结构体将共用体和枚举封装起来:
struct WIDGET {
enum widget_tag tag;
union {
这个 double,决定了union的对
long count;
齐指数是 8,也影响到WIDGET的
对齐指数。
double value;
char name[ 10 ];
} data;
} x;
思考:x的布局是什么样子?
x的布局:
data.count 4
data.value 8
data.name 10
tag4
填充4
填充 6
然后,对共用体变量的使用都要涉及两个操作:
既要使用数据成员:
x.data.count = 100 ; //或使用value 、name
又定要加上标签: x.tag = count_w ; //或value_w, name_w
之后就可以使用共用体数据了: 这个标签的作用如同运动员后背的
号码——我们叫不上运动员的名字
,但号码帮助我们识别他们。
void print_w ( WIDGET w)
{
switch(w.tag)
{
case count_w:
printf(“count %ld\n”,w.data.count);
break;
case value_w:
printf(“value %lf\n”,w.data.value);
break;
case name_w:
printf(“name \”%s\”\n”,w.data.name);
break;
}
}
P
const 类型修饰符
是“只读”的意思。可以保护被修饰的变量或其他
标识符,防止被意外地修改。增强程序的健壮性。
int i = 10;
变量
int const j = 10;
常量
const int j = 10;
至于const修饰其他类型的标识符,如数组、指针、
形参、函数返回、结构体等情况,将在相应的章
节再讲解。
volatile类型修饰符
volatile关键字是类型修饰符,用来拒绝编译器优化该变量之用。
很多人认为这是个奇怪的关键字,然而如果没有它帮忙,程序
可能出现致命问题。 如:
int x = 0,val1,val2;(假定x为全局变量)
val1 = x;
…/*一些不使用x的代码,但并不能保证未知因素不使用*/
val2 = x;
这时,编译器可能自作主张地将x优化到寄存器当中,以提高
效率。但是,某些未知的因素(比如操作系统、硬件或者其它
问:
线程等)可能更改了x,导致下一个x不再是原先的值。而用
volatile修饰x: const volatile int i=10;这行代码有没有问题?如果
没有,那 i 到底是什么属性?
volatile int x;
是告诉编译器对 x 变量不要进行寄存器优化,每次使用它时必
须从内存中取出“原版”的那份。
static类型修饰符
static可用来修饰变量,也可以修饰函数。它的作用分两个
方面表现:
1、当它用于修饰变量时,该变量成为静态变量:
static int i;
当 i 是局部变量时, static使之变成静态局部变量。
将使变量的生存期变长(和普通局部变量比较更“长
命”),但作用域不变,仍然由函数域限制着(即
“{}”限制)。
当 i 是全局变量时, static使之变成静态全局变量,
生存期不变(依然“长命”),但将变量访问本地化
(作用域在本源文件之内)。
2、当它用于修饰函数时:
static void fun()
{
}
将函数的调用本地化,即,只供本源文
件中的其他函数调用,其它文件的函数
不可以调用它。
extern类型修饰符
extern置于变量或者函数前,以标示某变量或某函数是定
修饰变量:
修饰函数:
义在别的文件中的——“外部”。在本文件使用到这些外
extern
int i;
extern void fun();
来的变量或函数,提示编译器遇到此变量或函数时,到其
他文件中寻找其定义。
仅仅是声明,不是定义。随后可以使用它们。无论该变量
或函数在其他源文件中定义,还是在本文件中定义。
这个结论的前提是变量定义或者函数定义时没有static
修饰!也就是说static比extern强硬!
P
编译器对字符串常量的处理
C语言将连续的多个字符串视为一个字符串。不管
是否用空格、tab或回车隔开。例如:
void main( )
其间的空格不影响它
们同是一个串。
{
char str[ ] = "Neu“ "soft";
printf("%d\n",sizeof(str));
8
printf("%s\n",str);
Neusoft
}
编译器对字符串常量的处理
P
“ ”和’’的使用在本质上是完全不同的。如:
#include <stdio.h>
void main( ) {
int x = (int)“A”; // ?
int z = (int)“B”; // ?
int y ='A' ;
// ?
int *p = &y;
x 记录的是串"A"的首地址:0x00422024
printf("%x\n",x);
printf(“%x\n”,z); 串“B”的首地址:0x00422020
printf("%x\n",p);
'A'的首地址是:0x0012ff74
}
常量的种类
从表现形式上看,常量有下面四种:
1.
2.
3.
4.
字面常量:1,3,4,…… ‘a’,’b’,’c’,”hello”
宏常量:#define PRICE 30
const常量:const double PI = 3.1415;
枚举常量:enum color{ BLACK, RED, BLUE,
GREEN};
可以使用宏来表示常量,它提高了代码的可读性、可
维护性: 最好用const来定义常量。 尽量不使用字
面常量。
用枚举常量定义数组
尽管建议使用 const 定义常量,可是它不能用于指定数组长度:
enum {int
const
cici
==
10};
10;
无论设定为全局还是局部的const
在C语言程序中,无论设定为全局
,都将报错。
还是局部的const
,都将报错。
int main() {
const int ci = 10;
这时只能使用枚举常量。
int array[ci];
for(int i =0; i<10;++i)
array[i] = i+1;
for(i =0; i<10;++i)
printf("%d \n",array[i]);
return 0;
}
使用const来定义常量的好处
1、const定义的常量编译器可以对其进行数据类型安全检查,而
#define宏定义的常量却只是进行简单的字符替换,且有时还会产
生边际效应。
所谓边际效应举例如下:
#define N 100
#define M 200 + N
当程序中使用 N*M 时,原本想得到 100 * (200+ 100 ),编译器
却理解成了 100 * 200 + 100。
2、调试时,可对const进行调试,但不能对#define调试。
3、用const定义局部常量时,作用域仅限于定义局部常量的函数体内
。但用#define定义常量时,就无所谓全局还是局部,从定义点到
整个程序的结束点都可以使用,即#define不体现作用域概念。
const用于结构体
const struct student1
此时, struct student1
类型不是常类型。
{
int a, b ;
是个结构体常量。
} x={23,15} ; 此时 x 是什么?
x.a = 5 ; 这样写可以吗?
非法,因为x是常量,它的数据成
员也个个是常量,不可修改。
const struct student2
{
int a, b ;
是个结构体变量。
};
struct student2 y ={24,10} ; 此时 y 是什么?
可以。
y.a = 50 ; 这样写可以吗?
P
“表达式的值”
一个复杂表达式是经过多步计算完成的。计算机要按优先级和
结合性一步一步地做,这就产生了“中间结果”,它们存
在哪里?只有内存。内存里会有一些无名的、占一定空间
的、短命的变量存在,它们用于存放计算的中间结果,它
不过,最重要的是函数返回的临时变量;
们稍纵即逝,由编译器来管理着它们的诞生与消亡,叫做
最难理解的是++运算的临时变量;
表达式的值。
最隐晦的是强制甚至隐含类型转换的临时变量。
特点:
用于临时存放计算结果,如同变量;
任何表达式都有值;
表达式的值都没有名字;
它们是短命的 —— 语句结束(;)即被自动释放;
它们被临时存放在函数运行栈中;
它们有可能被编译器优化,或消失或改变寿命;
表达式的求值顺序
表达式的求值顺序取决于三个因素:
运算符的优先级,运算符的结合性,运算符的控制性。
首先由优先级分出谁先做谁后做;
优先级相同的运算符再由结合性分出谁先谁后。所谓结合性就是
先做右侧运算符的表达式还是左侧的。
最后是控制性,它表明了必须在哪个表达式做完后才做另一个,
或者一个做完后另一个干脆不做。有控制性的运算符只有4个
? : && || ,
对于以上规则之外的运算次序,各编译器有权自行决定先做哪
个后做哪个。比如:
a*b + c*d - e*f 中的三个乘法,到底是先做 a*b 再做 c*d 再 e*f
,还是先做 e*f 再做 c*d 再 a*b ;还有上式的加减法,是先
加,还是先减 。所有这些都是由编译器厂商自行规定的,并
无一定规矩。
这样就使得用户莫衷一是。
结论:
不要企图摸到编译器的普适规律,既然是“自行规定的”,那就无共
同规律可言,只能是一种编译器一套规矩。因此,不要写依赖于某种
规矩的代码,那将是不可靠的、不可移植的。
P
自增、自减运算符
使用要点:
1) ++,--的操作对象只能是变量,如: ++(a+b)非法!
2) ++, --具有“右结合性”,如:*p++相当于 *(p++)
3) ++i与 i++的运算结果有差别。
例:
变量i每遇到逗号时,会认为本计算单位已经
j=3; k=++j;
结束,这时候 i应该完成自加。
j=3; k=j++;
j=3; printf(“%d”,++j);
j=3; printf(“%d”,j++);
a=3; b=5;c=(++a)*b;
a=3; b=5;c=(a++)*b;
i = 0; j =(i++,i++,i++);
3 2
printf("%d\n%d\n\n",i,j);
后++,后到何时?
#include <stdio.h>
void main( )
{
int i=10,j=2;
i =( i++ ) / 3+ 5* ( j-- );
printf(“%d %d\n”, i, j ); // 此时显示几?
}
14 1
右端表达式得:10 / 3 + 5 *2 = 13
i得13后再加1。后到赋值完成才认为本计算单位结束
结论:
无逗号的表达式,后++是后到整个表达式完成才做。
P
例题
#include <stdio.h>
void main( )
{
输出:0, 9, 3, 3
int i=0, j=0, p, q;
p=( i++ ) + ( i++ ) + ( i++ ); // 遇到分号才认为本计算单位
结束,i 这时候自加。
q=( ++j ) + ( ++j ) + ( ++j );
printf(“%d,%d,%d,%d”, p, q, i, j ); // ?
}
int x; int i = 3;
x = (++i, i++, i+10); 问 x的值为多少?i的值为多少?
逗号表达式中,i 在遇到每个逗号后,认为本计算单位已经结束,
这时候 i自加。所以本例子计算完后,i的值为5,x的值为 15。
指针变量与0值比较
尽管 NULL 的值与 0 相同,但是两者意义不同。假设指针
变量的名字为 p,它与零值比较的 if 语句如下:
if (NULL == p) 或
if (p != NULL) // p与NULL显式比较,强调p是指针变量
不要写成:
if (p == 0)
if (p != 0) // 容易让人误解p是整型变量
条件运算符的结合方向
条件运算符的结合方向:
自右至左 。
复习:
由于结合性是在优先级分辨不开的情况下使用的,而同一优先
三目运算符是将三个表达式联系起来运算的运算符。
形如:
级下只有三目运算符一个。那么,唯一可能将结合性派上
表达式一 ?表达式二 :表达式三
此时的三个表达式可以简单如10、a;也可以写的很复杂。
用场的就是在三目运算中又镶嵌了三目运算。如:
a>b ? a : c>d ? c : d (等效于(a>b)?
a : ((c>d)? c : d) )
鉴于三目运算表达式解析
或:
的复杂性,不提倡编程时
使用,非用不可,则要拆
a>b >c?c:d?a:b (等效于(a>b>c)?c
: ((d) ? a : b) )
分成多条语句使用。
或
c?d? e:f:m?n:t (等效于 (c)?((d) ? e : f )) : ((m)? n : t )
当表达式中含有两个或更多条件运算符时,会将第一个问号右侧的
其它式子都视作二、三表达式,而二、三表达式的区分依靠此时出
现的第一个冒号,或能够构成完整条件表达式之外的冒号。这个规
则再递归的使用,直到将表达式解析完毕。
P
逻辑表达式的短路性
逻辑表达式有“短路性”,这样做可以提高运行效率。但客
观效果会使有些表达式可能没有被执行。
a&&(b= 1)
对于&&,如果a表达式为假,则不再计算b=1;
a||( b= 1)
对于||,如果a表达式为真,则不再计算b=1.
条件表达式的短路性
条件表达式也有“短路性”。看下面例题:
#include <stdio.h>
int main() {
int a =0;
int b = 1;
int c;
c = a > b ? ++a : ++b;
printf(“%d, %d, %d\n”, a, b, c); // 结果如何?
return 0;
是 0,2,2。
}
P
首先要弄清运算次序。尽管自增运算的优先级高于三目运算
,但是按前面“条件运算符的结合方向”的讨论结果,
++a和++b仅是三目运算表达式的组成部分,应先算a > b,
根据其真假来决定是计算 ++a还是 ++b。于是最后结果是
0, 2, 2
表达式运算次序
各种不同的编译器,会根据自身的理由,对表达式的运算顺
序作以安排:或从左向右,或从右向左运算。
( 算式 A )
+ ( 算式 B ) *
( 算式 C )
int a = 3, b = 5;
int c = ++b * (a + b );
我们会理所当然地认为:
c = ++b * 8
= 6*8
= 48
事实上不然,VC++6.0会得54,而有些编译器得48 !
解决的办法是:分割复杂表达式为多个简单表达式,不再使
用复杂表达式。“一条语句只做一件事!”
左值和右值
 “左值”和”右值”的名称是相对于赋值运算符而言的;
 左值是能用于赋值运算符左侧的表达式,它指明了一个存储
位置,该位置应具有可访问性、可读写性。这两条是否同时
成立,是判断能否能作”左值” 依据;
 右值是能用于赋值运算符右侧的表达式,是个值,自身不可
修改;
 a + 4 = b; 之所以错,是因为临时变量不具有可访问性,因
此仅可作右值,不可作左值;
 比如 ++i和i++表达式,都不具有可访问性,且只具有可读
性,因此只可作右值;
于是它们又被引申到:“左值”是
一块内存的名称,这意味着它可以
被赋值;“右值”则是这块内存中
存放的值,它是不可以被更改的。
可作左值的表达式
 下标表达式 A[k] = ...
 间访表达式 *p = ...
 成员表达式 S.mem = ...
或 P->mem = ...
 其它表达式不可作左值。(但有时系统并不警告)
P
编译器对表达式的理解
你认为输出的是:
10
{
4
int i = 10;
11
printf("i : %d\n",i); 其实不是!
int main()
其实结果是:
10
4
10
为什么?
printf("sizeof(i++) is: %d\n",sizeof(i++));
printf("i : %d\n",i);
return 0; 原因是,sizeof不是一个函数,是个操作符,仅仅
}
求i++表达式的类型尺寸,而不计算表达式的值。
将显示什么?
这是一件可以在程序运行前(编译时)完成的事,
所以,sizeof(i++)直接就被4取代了,并不计算i++,
在运行时也就不会有 i++这个表达式。
switch 的设计理念
switch 中的case语句们不是互斥的关系,本质上是“或”关
系;当然也可以用break使它们互斥。
下面的代码片段可以看出这种设计的便利:
switch(ch) {
case ‘a’: case ‘A’:
case ‘e’: case ‘E’:
case ‘i’: case ‘I’:
case ‘o’: case ‘O’:
case ‘u’: case ‘U’:
++vowel_cnt; //对元音字母计数
break;
.....
}
switch和多重 if 的区别
有了if 语句为什么还要switch 语句?
switch 是多分支选择语句,而if 语句最多只有两个分支可供选
择。虽然可以用嵌套的if 语句来实现多分支选择,但那样的程序
冗长难读。这恰是switch 语句存在的理由。
switch 语句用于“一次计算,多种结果”;计算结果必须为整
数。
多重 if 用于“多次计算,多个结果”;计算结果可以是任何类
型数,而且各表达式类型可各不相同。
可见,它们各有约束,各有所长。应用于不同的需求环境。
P
“无限循环”的用途
这些都是“无限循环”
实用框架:
的应用场合。
while ( 1 )
{
//一些语句,如输入、遍历、比较、选择 等
if ( 条件 )
break;
}
其语义是:只有“条件”满足才能跳出循环,否则无休止地执行
“一些语句”。
比如:在用户输入数据时,只有输入的数据合乎要求才能跳出循
环,否则重新输入数据。这就提供了用户纠错的机会。此框架可
确保其后的语句得到的数据是正确的。还可以用于拦截检查等。
“无限循环”的应用
一个专检查是否超
int choice(int nStart, int nEnd)
出范围的函数
{
int nInput;
边界
是否在合理的范围之内。
while(1)
{
printf("Please Input a Number(%d-%d)", nStart,
nEnd);
scanf("%d", &nInput);
if((nStart<=nInput )&&(nInput<=nEnd)) {
break;
}
合理才能跳出循环,
}
否则再次输入。
return nInput;
}
P
打印如下图形:
*
***
*****
*******
*********
***********
#include<stdio.h>
#include<stdlib.h>
void main()
{
int i,j,hang; //三角形行数
for(;;) // 又一种“无限循环”,用于多次接受用户输入
{
printf("请输入显示行数,按q退出:");
scanf("%d",&hang);
if(getchar()=='q')
break; // 如果用户输入q,则退出
for(i=0;i<hang;++i) ) // 行循环
{
这个循环打印每行中*前面的空格
for(j=0;j<10-i;++j)
。10是个任意常数,仅是将三角
形向右推移一些距离。
printf(" ");
for(j=0;j<2*i+1;++j)
这个循环打印每行中的*号。它
printf("*");
和行的关系是2*i+1,比如,第
printf("\n");
3行应该打印7个*。
}
}
}
P
typedef
功能:用自定义的类型名为已有数据类型命名。
一般形式: typedef type NAME;
例如:
能隐藏笨拙的语法构造以及平台相
typedef int INTEGER;
关的数据类型,从而增强可移植性
typedef float REAL;
和未来的可维护性,简化代码。但
INTEGER a,b,c;
降低了代码的可读性。
REAL f1,f2;
说明:
1. typedef 没有创造新数据类型;
2. typedef 是专用于定义类型的,不能定义变量;
3. typedef 与 define 不同。(见下页)
typedef与 define 区别
define 预编译时处理,简单字符置换。
typedef 编译时处理,为已有类型命名 。
#define dpchar char *
dpchar p1, p2;
二者的使用一
样效果吗?
typedef char * tpchar;
tpchar p3, p4;
p1是指针而p2不是指针,仅是个char型的普通变量。
p3、p4则不同,它们都是char型的指针。
P
typedef的用处
1、增强代码的可移植性。
例如:对于16位机器: typedef short
int INT16;
INT16 num1; //使用新类型名
INT16 num2; … INT16 numn;
16位机
int
2个字节
32位机
int
4个字节
在16位机上,如果不使用typedef,而是直接用int定
义变量,则移植到32位机平台时,需要将代码中所
有的int改为short,改动量很大,而且容易遗漏。
typedef的用处
2、简化代码。
例如,用于重定义struct,union等类型:
typedef struct student
{
int num;
int age;
} STU;
这时使用:
STU stu1; // 相当于struct student stu1;
3、还可以掩饰复合类型,如指针和数组 :
typedef char UINT8[32];
其中 UINT8 等价于 char [32]。
如 UINT8 devString; == char devString[32];
typedef的用处
4、还可以掩饰函数指针
如: typedef int (* pFun)(int );
pFun p;
其中 pFun p; == int (*p)(int );
例:
typedef int* (* pt )(int* (*pn)(int * p1,int *p2),int * p3);
pt p;
常能见到一些程序里出现size_t类型,这其实是在<stddef.h>
头文件里定义的unsigned int类型的别名。另外还有 ptrdiff_t
类型,它也是标准库定义的类型名,用于表示两个指针之差。
是 int 或long。
typedef的用处
5、这里的 typedef 掩饰了一个结构体指针
typedef struct student
{
int i;
char name[10];
}*pSTU;
pSTU是什么?如何理解?
把“struct student { /*code*/} *”看成一个整体,typedef就是
给“struct student {/*code*/} * ”取了个别名叫“pSTU” 。
这种技巧在系统头文件中常被使用!
P
typedef的陷阱
typedef char * pstr;
若我们这样使用:
int mystrcmp(const pstr, const pstr);
我们希望表达的是:
int mystrcmp( const char*, const char* );
(即 2个指向常量char的指针)
可它被编译器解释为:
int mystrcmp( char* const, char* const ) ;
(2个指向char的常指针)
为何是这样?
分析:
不要被const pstr的表象所迷惑,尽管const 放在了pstr的前面,
但它修饰的是pstr这个类型,而pstr已被定义为char *这个整
体,不是char。 所以const修饰的是 char *型的指针 ,即语
义是 char* const,而不是指针所指向的目标类型char .
这被称为:类型限定符迁移
应修改为:
typedef const char* cpstr;
然后再这样用:
int myctrcmp( cpstr, cpstr);
P
typedef的其它用法
再看typedef的如下的用法:
typedef int ARRAY[10];
ARRAY a;
ARRAY 所代表的类型是什么?
ARRAY b[5];
int [10]
int a[10];
ARRAY *c;
int b[5][10];
int (*c)[10];
typedef可以定义多个类型名
typedef struct student
{
int i;
char name[10];
}STU,*pSTU;
这里 struct student、 STU、pSTU 都是类型名。
这种技巧在实际开发中经常被使用!
现在我们来判断 s1,s2 各是什么?如何理解?
STU s1;
它是struct student类型的变量。
pSTU s2 ;
它是struct student*类型的变量,
即s2 是个指针。
typedef的错误使用
typedef static int STINT;
该语句编译错误:
error C2159: more than one storage class specified
分析:
typedef 与 static ,auto, extern, register等关键字被看做
同是存储类型。
C语言规定,存储类型每句只能使用一个。编译不通过的原
因是因为声明中存在了多个存储类关键字。
P
判断
typedef int a[10];
A) a[10] a[10];
错!
错!
B) a[10] a;
错!
C) int a[10];
错!
D) int a;
E) a b;
F) a b [10];
G) a* b;
H) a* b[10];
对!定义了一维数组int b [10];
对!定义了二维数组int b [10][10];
对!定义了int (* b) [10];
对!定义了int (* (b [10]))[10];
根据题意可以将解题过程分为四步:
1)屏幕提示,输入日期值,判是否合理;
2)计算从1990年1月1日开始至指定日期共有多少天;
3)由于“打鱼”和“晒网”的周期为5天,所以将计算出的天
数用5求余;
4)根据余数判断他是在“打鱼”还是在“晒网”;
若余数为1,2,3,则他是在“打鱼”,否则是在“晒网”。
在这四步中,关键是第二步。求从1990年1月1日至指定日
期之间共有多少天,还要判断经历年份中是否有闰年,二月为
29天,平年为28天。
闰年的方法可以用如下伪语句描述:
如果 ((年能被4除尽 且 不能被100除尽)或 能被400除尽)
则 该年是闰年;
否则 不是闰年。
P
断言
断言是指在程序开发期间使用的、让程序在运行时进行自检的
代码。断言为真,则表明程序运行正常;断言为假,则意味
着代码中出现了意料之外的错误。
断言对于大型复杂程序或可靠性要求极高的程序,是必须的。
通过使用断言,程序员能更快速地排查出程序里的错误。
断言通常只在开发阶段被编译到目标代码中,而在生成产品代
码时并不编译进去。在开发阶段,断言可以帮助查清相互矛
盾的假定、预料之外的情况以及传递给子程序的错误数据等
隐患。在生成产品代码时,不把断言编译进目标代码里,以
免降低系统性能。
为了便于生成产品代码时去除断言,断言必须做成宏,而非函
数形式。
C标准库中提供assert宏为C程序员提供断言支持。
诊断宏
宏名: assert
宏原 型: void assert(int expression);
功 能: 测试一个表达式 ,测试结果为真时什么事情也不做,为
假时打印诊断信息并终止程序。
注意:
assert只应在程序调试时有效,程序发布时无效。为了使发
布版的assert无效,应该在 #include <assert.h>之前定义
NDEBUG宏:
……
#define NDEBUG
#include <assert.h>
……
实例
// #define NDEBUG
自定义的内存拷贝函数
#include <assert.h>
#include <stdio.h>
void mcpy(void* pvTo, void* pvFrom, size_t size){
char* pcTo = (char*)pvTo;
char* pcFrom = (char*)pvFrom;
assert(pvTo != NULL && pvFrom != NULL);
while(size-->0){ *pbTo++ == *pbFrom++; }
return(pvTo);
}
用于拦截指针为空的情况
void main(){
char *pSrc = “NeuEdu”;
char *pDest1 = NULL;
char Dest2[20];
mcpy(pDest, pSrc, strlen(pSrc) + 1);
按位拷贝
mcpy(Dest2, pSrc, strlen(pSrc) + 1);
}
使用注意
 断言一般用在函数开始处,检查传入参数的合法性;
 用断言来处理绝不应该发生的情况;
文件打开失败、内存申请失败等逻辑,不应该由断言来处理
 不要将assert宏用于程序的执行逻辑中;
程序的 if( 条件 ) 语句不可用 assert( 条件 ) 来替代;
 断言中不可使用改变环境的语句;
assert(i++ < 100); //不可
 每个assert只检验一个条件 ,同时检验多个条件时,如果断
言失败,无法判断是哪个条件导致的失败 ;
 一定要给断言加注释,否则断言就语意不清。
使用VisualStudio开发程序,则不必采用在#include
<assert.h>前定义NDEBUG的方式,使assert失效。
因为在VisualStudio中,将程序分为:Debug版和
Release版。Debug版用于开发、调试,此时assert宏
有效。开发完成程序发布时,则要制作成Release版,
在Release版中, VisualStudio 自动定义了NDEBUG,
使assert宏无效。
P
谢谢!
Download