提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
2.1 整型(int)
一、整型数说明
加上不同的修饰符, 整型数有以下几种类型;
signed short int 有符号短整型数说明。简写为short或int, 字长为2
字节共16位二进制数, 数的范围是-32768~32767。
signed long int 有符号长整型数说明。简写为long, 字长为4字节共
32位二进制数, 数的范围是-2147483648~2147483647。
unsigned short int 无符号短整型数说明。简写为unsigned int, 字长
为2字节共16位二进制数, 数的范围是0~65535。
unsigned long int 无符号长整型数说明。简写为unsigned long, 字长
为4字节共32位二进制数, 数的范围是0~4294967295。
二、整型变量定义
可以用下列语句定义整型变量
int a, b; /a、b被定义为有符号短整型变量/
unsigned long c; /c被定义为无符号长整型变量/
三、整型常数表示
按不同的进制区分, 整型常数有三种表示方法:
十进制数: 以非0开始的数
如:220, -560, 45900
八进制数: 以0开始的数
如:06; 0106, 05788
十六进制数:以0X或0x开始的数
如:0X0D, 0XFF, 0x4e
另外, 可在整型常数后添加一个"L"或"l"字母表示该数为长整型数, 如22L,0773L,0Xae4l。
2.2 浮点型(float)
一、浮点数说明
Turbo C中有以下两种类型的浮点数:
float 单浮点数。字长为4 个字节共32位二进制数,数的范围是3.4x10-38E~3.4x10+38E。
double 双浮点数。字长为 8个字节共 64位二进制数,数的范围是1.7x10-308E~1.7x10+308E。
说明:浮点数均为有符号浮点数, 没有无符号浮点数。
二、浮点型变量定义
可以用下列语句定义浮点型变量:
float a, f; /a, f被定义为单浮点型变量/
double b; /b被定义为双浮点型变量/
三、浮点常数表示
例如: +29.56, -56.33, -6.8e-18, 6.365
说明:
浮点常数只有一种进制(十进制)。
所有浮点常数都被默认为double。
绝对值小于1的浮点数, 其小数点前面的零可以省略。如:0.22可写为.22, -0.0015E-3可写为-.0015E-3。
Turbo C默认格式输出浮点数时, 最多只保留小数点后六位。
2.3 字符型(char)
加上不同的修饰符, 可以定义有符号和无符号两种类型的字符型变量, 例如:
char a: /a被定义为有符号字符变量/
unsigned char l; /l被定义为无符号字符变量/
字符在计算机中以其ASCII码方式表示, 其长度为1个字节, 有符号字符型数取值范围为-128~127, 无符号字符型数到值范围是0~255。因此在Turbo C语言中,字符型数据在操作时将按整型数处理, 如果某个变量定义成char,则表明该变量是有符号的, 即它将转换成有符号的整型数。
Turbo C中规定对ASCII码值大于0x80的字符将被认为是负数。例如ASCII值为0x8c的字符,定义成char时,被转换成十六进制的整数0xff8c。这是因当ASCII码值大于0x80时,该字节的最高位为1,计算机会认为该数为负数,对于0x8c表示的数实际上是-74(8c的各位取反再加1),而-74转换成两字节整型数并在计算机中表示时就是0xff8c(对0074各位取反再加1)。因此只有定义为
unsigned char 0x8c转换成整型数时才是8c。这一点在处理大于0x80的ASCII码字符时(例如汉字码)要特别注意。一般汉字均定义为unsigned char(在以后的程序中会经常碰到)。
另外,也可以定义一个字符型数组(关于数组后面再作详细介绍),此时该数组表示一个字符串。
例如:
char str[10];
计算机在编译时, 将留出连续10个字符的空间, 即str[0]到str[9]共10个变量,但只有前9个供用户使用。第10个str[9]用来存放字符串终止符NULL即"\0",但终止符是编编译程序自动加上的, 这一点应特别注意。
二、字符常数表示
能用符号表示的字符可直接用单引号括起来表示, 如’a’, ‘9’, ‘Z’, 也可用该字符的ASCII码值表示, 例如十进制数85表示大写字母’U’, 十六进制数0x5d表示 ‘]’, 八进制数0102表示大写字母’B’。
一些不能用符号表示的控制符, 只能用ASCII码值来表示, 如十进制数10 表示换行,下六进制数0x0d表示回车, 八进制数033表示Esc。Turbo C2.0中也有另外一种表示表示方法, 如’\033’表示Esc, 这里’\ 0’ 符号后面的数字表示十六进制的ASCII值当然这种表示方法也适用于可睦接用符号表示的字符。
另外, Turbo C2.0中有些常用的字符用以下特殊规定来表示:
规定符 等价于 含义
‘\f’ ‘\X0C’ 换页
‘\r’ ‘\X0D’ 回车
‘\t’ ‘\X09’ 制表键
‘\n’ ‘\X0A’ 换行
‘\’ ‘\X5C’ \符
‘’’ ‘\X27’ '符
‘"’ ‘\X22’ "符
对于字符串常量, 一般用双引号括起来表示, 如"Hello Turbo C2.0"。
2.4 指针型(*)
指针是一种特殊的数据类型, 在其它语言中一般没有。指针是指向变量的地址,实质上指针就是存贮单元的地址。根据所指的变量类型不同,可以是整型指针(int *)、浮点型指针(float *)、字符型指针(char *)、结构指针(struct *)和联合指针(union *)(结构指针和联合指针将在第4节中介绍)。
2.5 无值型(void)
无值型字节长度为0, 主要有两个用途: 一是明确地表示一个函数不返回任何值; 一是产生一个同一类型指针(可根据需要动态分配给其内存)。
例如:
void *buffer; /buffer被定义为无值型指针/
5.2 基本运算符
基本算术运算的运算符: =、 +、 -、 *和/
C 没有指数运算符。不过, C 的标准数学库提供了一个pow()函数用于指数运算。 例如, pow(3.5,
2.2)返回3.5的2.2次幂) 。
C使用符号/来表示除法。 /左侧的值是被除数, 右侧的值是除数。 例如, 下面four的值是4.0:
four = 12.0/3.0;
整数除法和浮点数除法不同。 浮点数除法的结果是浮点数, 而整数除法的结果是整数。 整数是没有小数部分的数。 这使得5除以3很让人头痛, 因为实际结果有小数部分。 在C语言中, 整数除法结果的小数部分被丢弃, 这一过程被称为截断(truncation) 。
运行程序清单5.6中的程序, 看看截断的情况, 体会整数除法和浮点数除法的区别。
/* divide.c -- 演示除法 */
#include
int main(void)
{
printf("integer division: 5/4 is %d \n", 5 / 4);
printf("integer division: 6/3 is %d \n", 6 / 3);
printf("integer division: 7/4 is %d \n", 7 / 4);
printf("floating division: 7./4. is %1.2f \n", 7. / 4.);
printf("mixed division: 7./4 is %1.2f \n", 7. / 4);
return 0;
}
程序清单5.6中包含一个“混合类型”的示例, 即浮点值除以整型值。 C相对其他一些语言而言, 在类型管理上比较宽容。 尽管如此, 一般情况下还是要避免使用混合类型。 该程序的输出如下:
integer division: 5/4 is 1
integer division: 6/3 is 2
integer division: 7/4 is 1
floating division: 7./4. is 1.75
mixed division: 7./4 is 1.75
注意, 整数除法会截断计算结果的小数部分(丢弃整个小数部分) , 不会四舍五入结果。 混合整数和浮点数计算的结果是浮点数。 实际上, 计算机不能真正用浮点数除以整数, 编译器会把两个运算对象转换成相同的类型。
本例中, 在进行除法运算前, 整数会被转换成浮点数。
在C99以前,不同的实现采用不同的方法。 但是C99规定使用趋零截断。 所以, 应把-3.8转换成-3
5.3.1 sizeof运算符和size_t类型
sizeof运算符以字节为单位返回运算对象的大小(在C中, 1字节定义为char类型占用的空间大小。 过去, 1字节通常是8位, 但是一些字符集可能使用更大的字节) 。 运算对象可以是具体的数据对象(如, 变量名) 或类型。 如果运算对象是类型(如,float) , 则必须用圆括号将其括起来。
5.3.2 求模运算符: %
例如, 13 % 5(读作“13求模5”)得3
求模运算符只能用于整数, 不能用于浮点数。
负数求模如何进行? C99规定“趋零截断”之前, 该问题的处理方法很多。 但自从有了这条规则之后, 如果第1个运算对象是负数, 那么求模的结果为负数; 如果第1个运算对象是正数, 那么求模的结果也是正数:
5.4 表达式和语句
表达式(expression) 由运算符和运算对象组成(运算对象是运算符操作的对象) 。
下面是一些表达式:
4 -6 4+
21
a*(b + c/d)/20
q = 5*2
x = ++q % 3
q > 3
C 表达式的一个最重要的特性是, 每个表达式都有一个值。
表达式q = 5*2作为一个整体的值是10。 那么, 表达式q > 3的值是多少? 这种关系表达式的值不是0就是1
5.4.2 语句
语句(statement) 是C程序的基本构建块。 一条语句相当于一条完整的计算机指令。 在C中, 大部分语句都以分号结尾。 因此,
legs = 4
只是一个表达式(它可能是一个较大表达式的一部分) , 而下面的代码
则是一条语句:
legs = 4;
只要控制表达式为 true,while 循环就会反复地执行语句:
while (表达式)语句
while 表达式是顶部驱动(top-driven)的循环:先计算循环条件(也就是控制表达式)。如果为 true,就执行循环体,然后再次计算控制表达式。如果控制表达式为 false,程序跳过循环体,而去执行循环体后面的语句。
从语法上讲,循环体只有一条语句组成。如果需要执行多条语句时,可以使用语句块把它们组合在一起。例 1 展示了一个简单的 while 循环,从控制台读入多个浮点数,并把它们累加。
例 1 展示了一个简单的 while 循环,从控制台读入多个浮点数,并把它们累加。
【例1】一个 while 循环
/* 从键盘输入数字,然后输出它们的平均值
* -------------------------------------- */
#include
int main()
{
double x = 0.0, sum = 0.0;
int count = 0;
printf( "\t--- Calculate Averages ---\n" );
printf( "\nEnter some numbers:\n"
"(Type a letter to end your input)\n" );
while ( scanf( "%lf", &x ) == 1 )
{
sum += x;
++count;
}
if ( count == 0 )
printf( "No input data!\n" );
else
printf( "The average of your numbers is %.2f\n", sum/count );
return 0;
}
在例 1 中,只要用户输入一个小数,下面的控制表达式即为 true:
scanf( "%lf", &x ) == 1
然而,只要函数 scanf()无法将字符串输入转换成浮点数(例如,当用户键入字母 q 时),则 scanf()返回值 0(如果是遇到输入流的尾端或发生错误时,则返回值 -1,表示 EOF)。这时,循环条件为 false,程序将会跳出循环,继续执行循环体后面的 if 语句。
for 循环
和 while 一样,for 循环也是一个顶部驱动的循环,但是它包含了更多的循环逻辑,如下所示:
for ([表达式1];[表达式2];[表达式3])
语句
在一个典型的 for 循环中,在循环体顶部,下述三个动作需要执行:
(1) 表达式 1:初始化
只计算一次。在计算控制表达式之前,先计算一次表达式 1,以进行必要的初始化,后面不再计算它。
(2) 表达式 2:控制表达式
每轮循环前都要计算控制表达式,以判断是否需要继续本轮循环。当控制表达式的结果为 false,结束循环。
(3) 表达式 3:调节器
调节器(例如计数器自增)在每轮循环结束后且表达式 2 计算前执行。即,在运行了调节器后,执行表达式 2,以进行判断。
例 2 展示了使用一个 for 循环初始化数组内每个元素的过程。
【例2】用 for 循环初始化数组
#define ARR_LENGTH 1000
/* ... */
long arr[ARR_LENGTH];
int i;
for ( i = 0; i < ARR_LENGTH; ++i )
arr[i] = 2*i;
for 循环头部中的三个表达式可以省略一个或多个。这意味着 for 循环头部最短的形式是:
for ( ; ; )
事实上,每个 for 循环都可以被改写成 while 循环,反之亦然。例如,例 2 的 for 循环可完全等效为下面的 while 循环:
i = 0; // 初始化计数器
while ( i < ARR_LENGTH ) // 循环条件
{
arr[i] = 2*i;
++i; // 递增计数器
}
一般来说,当循环内有计数器或索引变量需要被初始化,并且在每次循环时需要调整它们的值时,最好使用 for 循环,而不是 while 循环。
在ANSI C99中,也可以使用声明来替代表达式1。在这种情况下,被声明变量的作用域被限制在 for 循环范围内。例如:
for ( int i = 0; i < ARR_LENGTH; ++i )
arr[i] = 2*i;
变量 i 被声明在该 for 循环中(与例 2 不同)for 循环结束之后,变量 i 将不会再存在。
逗号运算符常常被用在 for 循环头部,以在表达式 1 中实现多个初始化操作,或者在表达式 3 对每个变量做调整操作。例如,函数 strReverse()使用两个索引变量以保存字符串中字符的次序:
void strReverse( char* str)
{
char ch;
for ( size_t i = 0, j = strlen(str)-1; i < j; ++i, --j )
ch = str[i], str[i] = str[j], str[j] = ch;
}
借助于逗号运算符,可以在只允许出现一个表达式的地方,计算多个表达式。
do…while 循环
do…while 循环是一种底部驱动的循环:
do 语句 while (表达式);
在控制表达式被第一次计算之前,循环体语句会首先被执行一次。与 while 和 for 循环不同,do…while 循环会确保循环体语句至少执行一次。如果控制表达式的值为 true,那么另一次循环就会继续;如果是 false,则循环结束。
在例 3 中,读入与执行命令的函数至少会被调用一次。当使用者离开菜单系统,函数 getCommand()将返回常量 END 的值。
【例3】do···while
// 读入和执行所选的菜单命令
// --------------------------------------------
int getCommand( void );
void performCommand( int cmd );
#define END 0
/* ... */
do
{
int command = getCommand(); // 询问菜单系统
performCommand( command ); // 执行所选的菜单命令
} while ( command != END );
选择结构和转移语句
if 语句
if语句是用来实现双分支选择结构的语句。
一般形式:
1
2
if(逻辑量)
语句1
else
语句2
1
2
3
4
if和else是构成if 语句的关键字,语句中的逻辑量是选择结构的条件,else和语句2可以省略。
语句1和语句2都只能是单条语句,如果在逻辑量为真或为假的分支中需执行多个操作,应该使用复合语句将多个操作构成一条语句。
条件表达式
条件运算符
由两个符号“?”和“:”复合而成,是C语言中唯一的三目运算符。
1
第一目写在“?”之前,是逻辑量,第二目写在“?”和“:”中间,是数据,第三目写在“:”之后,是数据。条件运算符的优先级只比赋值运算符和逗号运算符高,结合性为右结合。
条件表达式
用条件运算符将运算对象连接起来的符合C语言规则的表达式。
1
运算过程:先判断第1目逻辑量,如果逻辑量为真,选取第2目的结果作为整个表达式的值,否则选取第3目结果作为整个表达式的值。
条件表达式中存在类型的自动转换,条件表达式的值的类型是第2目和第3目中类型较高的运算对象的类型。
条件表达式运算的短路
if 语句的嵌套
if 语句的嵌套中,会有多个if和多个else。按照if 语句的规则if和else中间只能有一条语句,嵌套的if 语句中else总是和它前面最近的没有和其它else配对的if配对。
switch 语句
C语言提供了专门处理多分支选择结构的switch语句,又称为开关语句。
1
在switch 语句实现的多分支选择结构中,不同的分支通过不同的常量来标识,因此使用switch 语句实现多分支选择结构时,首要的任务是构造表达式,使得该表达式的取值可以唯一明确某一分支。
具体形式:
switch(表达式)
{
case 常量1: 语句组1
case 常量2: 语句组2
···
case 常量n: 语句组n
default : 语句组n+1
}
其中switch、case、default是switch 语句的关键字。“{}”内是switch 语句的语句体,不表示复合语句。switch后面括号里的表达式的值可以是任意类型,系统会自动转换为整型或字符型。case之后只能是常量或常量表达式,不能是变量或其它表达式,其值就是switch后括号内的表达式的各种可能的取值,各个常量应互不相同。语句组可以是多条语句,也可以没有语句,不需要用“{}”括起来构成复合语句。语句组中可以使用“break;”语句,它的作用是终止switch 语句的执行,转向执行switch 语句的后续语句。
switch 语句执行时,先求解switch后面括号里的表达式的值,然后用表达式的值和case后面的常量逐一进行比较,如果表达式的值和某一个case后面的常量相等,则由此进入switch 语句,开始顺次执行后面所有的可执行语句,如果和所有的case后面的常量都不相符,就由default后面的语句进入开始执行,直到遇到switch 语句结束的“}”或者遇到“break;”语句,结束switch 语句的执行。
case和case之后的常量之间必须有空格。
如果多个常量用来标识同一个分支,要用多个case来标识,不能省略。
“case 常量:”之后可以有多条语句,不需要用“{}”括起来。
“case 常量:”之后也可以一条语句都没有。
default 语句可以放在任意位置上。default 语句所处的位置不影响switch 语句的执行方式。
switch 语句中右大括号之前必须有语句,如果没有则应用空语句来完善程序结构。
break 和 continue 语句
break 语句
break 语句在循环体中的作用是结束循环,继续执行循环的后续语句。break
语句在循环体中必须和if 语句配合使用,才不会破坏循环语句的作用。
continue 语句
continue 语句只能使用在 循环体语句 中。
1
continue 语句的功能是结束本次循环,即不再执行循环体中continue 语句之后的语句,转入下一次循环执行。
continue 语句只结束本层本次的循环,并不终止循环。
C语言无参函数的定义
如果函数不接收用户传递的数据,那么定义时可以不带参数。如下所示:
dataType functionName(){
//body
}
dataType 是返回值类型,它可以是C语言中的任意数据类型,例如 int、float、char 等。
functionName 是函数名,它是标识符的一种,命名规则和标识符相同。函数名后面的括号( )不能少。
body 是函数体,它是函数需要执行的代码,是函数的主体部分。即使只有一个语句,函数体也要由{ }包围。
如果有返回值,在函数体中使用 return 语句返回。return 出来的数据的类型要和 dataType 一样。
例如,定义一个函数,计算从 1 加到 100 的结果:
int sum(){
int i, sum=0;
for(i=1; i<=100; i++){
sum+=i;
}
return sum;
}
累加结果保存在变量sum中,最后通过return语句返回。sum 是 int 型,返回值也是 int 类型,它们一一对应。
return是C语言中的一个关键字,只能用在函数中,用来返回处理结果。
将上面的代码补充完整:
#include
int sum(){
int i, sum=0;
for(i=1; i<=100; i++){
sum+=i;
}
return sum;
}
int main(){
int a = sum();
printf(“The sum is %d\n”, a);
return 0;
}
运行结果:
The sum is 5050
函数不能嵌套定义,main 也是一个函数定义,所以要将 sum 放在 main 外面。函数必须先定义后使用,所以 sum 要放在 main 前面。
注意:main 是函数定义,不是函数调用。当可执行文件加载到内存后,系统从 main 函数开始执行,也就是说,系统会调用我们定义的 main 函数。
无返回值函数
有的函数不需要返回值,或者返回值类型不确定(很少见),那么可以用 void 表示,例如:
void hello(){
printf (“Hello,world \n”);
//没有返回值就不需要 return 语句
}
void是C语言中的一个关键字,表示“空类型”或“无类型”,绝大部分情况下也就意味着没有 return 语句。
C语言有参函数的定义
如果函数需要接收用户传递的数据,那么定义时就要带上参数。如下所示:
dataType functionName( dataType1 param1, dataType2 param2 … ){
//body
}
dataType1 param1, dataType2 param2 …是参数列表。函数可以只有一个参数,也可以有多个,多个参数之间由,分隔。参数本质上也是变量,定义时要指明类型和名称。与无参函数的定义相比,有参函数的定义仅仅是多了一个参数列表。
数据通过参数传递到函数内部进行处理,处理完成以后再通过返回值告知函数外部。
更改上面的例子,计算从 m 加到 n 的结果:
int sum(int m, int n){
int i, sum=0;
for(i=m; i<=n; i++){
sum+=i;
}
return sum;
}
参数列表中给出的参数可以在函数体中使用,使用方式和普通变量一样。
调用 sum() 函数时,需要给它传递两份数据,一份传递给 m,一份传递给 n。你可以直接传递整数,例如:
int result = sum(1, 100); //1传递给m,100传递给n
也可以传递变量:
int begin = 4;
int end = 86;
int result = sum(begin, end); //begin传递给m,end传递给n
也可以整数和变量一起传递:
int num = 33;
int result = sum(num, 80); //num传递给m,80传递给n
函数定义时给出的参数称为形式参数,简称形参;函数调用时给出的参数(也就是传递的数据)称为实际参数,简称实参。函数调用时,将实参的值传递给形参,相当于一次赋值操作。
原则上讲,实参的类型和数目要与形参保持一致。如果能够进行自动类型转换,或者进行了强制类型转换,那么实参类型也可以不同于形参类型,例如将 int 类型的实参传递给 float 类型的形参就会发生自动类型转换。
将上面的代码补充完整:
#include
int sum(int m, int n){
int i, sum=0;
for(i=m; i<=n; i++){
sum+=i;
}
return sum;
}
int main(){
int begin = 5, end = 86;
int result = sum(begin, end);
printf(“The sum from %d to %d is %d\n”, begin, end, result);
return 0;
}
运行结果:
The sum from 5 to 86 is 3731
定义 sum() 时,参数 m、n 的值都是未知的;调用 sum() 时,将 begin、end 的值分别传递给 m、n,这和给变量赋值的过程是一样的,它等价于:
m = begin;
n = end;
函数不能嵌套定义
强调一点,C语言不允许函数嵌套定义;也就是说,不能在一个函数中定义另外一个函数,必须在所有函数之外定义另外一个函数。main() 也是一个函数定义,也不能在 main() 函数内部定义新函数。
下面的例子是错误的:
#include
void func1(){
printf(“http://c.biancheng.net”);
void func2(){
printf(“C语言小白变怪兽”);
}
}
int main(){
func1();
return 0;
}
有些初学者认为,在 func1() 内部定义 func2(),那么调用 func1() 时也就调用了 func2(),这是错误的。
正确的写法应该是这样的:
#include
void func2(){
printf(“C语言小白变怪兽”);
}
void func1(){
printf(“http://c.biancheng.net”);
func2();
}
int main(){
func1();
return 0;
}
func1()、func2()、main() 三个函数是平行的,谁也不能位于谁的内部,要想达到「调用 func1() 时也调用 func2()」的目的,必须将 func2() 定义在 func1() 外面,并在 func1() 内部调用 func2()。
有些编程语言是允许函数嵌套定义的,例如 JavaScript,在 JavaScript 中经常会使用函数的嵌套定义。
一维数组
一维数组的定义方式如下:
类型说明符 数组名[常量表达式];
例如:
int a[5];
它表示定义了一个整型数组,数组名为 a,定义的数组称为数组 a。数组名 a 除了表示该数组之外,还表示该数组的首地址(关于地址现在先不讨论,稍后讲指针的时候再说)。
此时数组 a 中有 5 个元素,每个元素都是 int 型变量,而且它们在内存中的地址是连续分配的。也就是说,int 型变量占 4 字节的内存空间,那么 5 个int型变量就占 20 字节的内存空间,而且它们的地址是连续分配的。
这里的元素就是变量的意思,数组中习惯上称为元素。
在定义数组时,需要指定数组中元素的个数。方括号中的常量表达式就是用来指定元素的个数。数组中元素的个数又称数组的长度。
数组中既然有多个元素,那么如何区分这些元素呢?方法是通过给每个元素进行编号。数组元素的编号又叫下标。
数组中的下标是从 0 开始的(而不是 1)。那么,如何通过下标表示每个数组元素的呢?通过“数组名[下标]”的方式。例如“int a[5];”表示定义了有 5 个元素的数组 a,这 5 个元素分别为 a[0]、a[1]、a[2]、a[3]、a[4]。其中 a[0]、a[1]、a[2]、a[3]、a[4] 分别表示这 5 个元素的变量名。
为什么下标是从 0 开始而不是从 1 开始呢?试想,如果从 1 开始,那么数组的第 5 个元素就是 a[5],而定义数组时是 int a[5],两个都是 a[5] 就容易产生混淆。而下标从 0 开始就不存在这个问题了!所以定义一个数组 a[n],那么这个数组中元素最大的下标是 n–1;而元素 a[i] 表示数组 a 中第 i+1 个元素。
另外,方括号中的常量表达式可以是“数字常量表达式”,也可以是“符号常量表达式”。但不管是什么表达式,必须是常量,绝对不能是变量。通常情况下 C 语言不允许对数组的长度进行动态定义,换句话说,数组的大小不依赖程序运行过程中变量的值。非通常的情况为动态内存分配,此种情况下数组的长度就可以动态定义,这个稍后会讲。
一维数组初始化
一维数组的初始化可以使用以下方法实现:
定义数组时给所有元素赋初值,这叫“完全初始化”。例如:
int a[5] = {1, 2, 3, 4, 5};
通过将数组元素的初值依次放在一对花括号中,如此初始化之后,a[0]=1;a[1]=2;a[2]=3;a[3]=4;a[4]=5,即从左到右依次赋给每个元素。需要注意的是,初始化时各元素间是用逗号隔开的,不是用分号。
可以只给一部分元素赋值,这叫“不完全初始化”。例如:
int a[5] = {1, 2};
定义的数组 a 有 5 个元素,但花括号内只提供两个初值,这表示只给前面两个元素 a[0]、a[1] 初始化,而后面三个元素都没有被初始化。不完全初始化时,没有被初始化的元素自动为 0。
需要注意的是,“不完全初始化”和“完全不初始化”不一样。如果“完全不初始化”,即只定义“int a[5];”而不初始化,那么各个元素的值就不是0了,所有元素都是垃圾值。
你也不能写成“int a[5]={};”。如果大括号中什么都不写,那就是极其严重的语法错误。大括号中最少要写一个数。比如“int a[5]={0};”,这时就是给数组“清零”,此时数组中每个元素都是零。此外,如果定义的数组的长度比花括号中所提供的初值的个数少,也是语法错误,如“a[2]={1,2,3,4,5};”。
下面给大家写一个简单的程序:
#include
int main(void)
{
int a[5] = {1, 2, 3, 4, 5};
int i;
for (i=0; i<5; ++i)
{
printf("%d\n", a[i]);
}
return 0;
}
输出结果是:
1
2
3
4
5
a 表示数组的名字,[5] 表示这个数组有 5 个元素,并分别用 a[0]、a[1]、a[2]、a[3]、a[4] 表示。并分别把花括号内的 1、2、3、4、5 赋给变量 a[0]、a[1]、a[2]、a[3]、a[4]。再次强调,下标从 0 开始,即从 a[0] 开始,而不是 a[1]。
也可以用 scanf 手动从键盘对数组进行初始化:
#include
int main(void)
{
int a[5] = {0}; //数组清零初始化
int i;
printf(“请输入5个数:”);
for (i=0; i<5; ++i)
{
scanf("%d", &a[i] );
}
for (i=0; i<5; ++i)
{
printf("%d\x20", a[i]);
}
printf("\n");
return 0;
}
输出结果是:
请输入5个数:1 2 3 4 5
1 2 3 4 5
同使用 scanf 给字符数组输入字符串时有所不同,输入数字时必须用 for 循环进行输入。而输入字符串时无须用循环,直接用 scanf 就可以了。
一维数组元素的引用
数组必须先定义,然后使用。C 语言规定,只能逐个引用数组元素,而不能一次引用整个数组。前面讲过,数组元素的表示形式为:
数组名[下标]
下标可以是整型常量或整型表达式,比如:
a[0] = a[5] + a[7] - a[2 * 3]
千万要注意,定义数组时用到的“数组名[常量表达式]”和引用数组元素时用到的“数组名[下标]”是有区别的,定义数组时的常量表达式表示的是数组的长度,而引用数组元素时的下标表示的是元素的编号。比如:
#include
int main(void)
{
int a[5] = {1, 2, 3, 4, 5}; //定义长度为5的数组a
int t;
t = a[3]; /引用数组a中下标为3的元素a[3], 此时的3不代表数组的长度/
printf(“t = %d\n”, t);
return 0;
}
输出结果是:
t = 4
“int a[5];”是定义了有 5 个元素的数组,这 5 个元素分别为 a[0]、a[1]、a[2]、a[3]、a[4]。而 t=a[3] 中的 a[3] 不是数组,只是其中的元素 a[3]。
因此,下面这个程序是错的:
纯文本复制
#include
int main(void)
{
int a[5];
a[5] = {1, 2, 3, 4, 5};
return 0;
}
错误的原因是下面的 a[5] 不是数组。只有在定义的时候“a[常量]”表示的才是数组,此时方括号中的数字才表示数组长度。除此之外程序中任何地方看到“a[常量]”都不是数组,都只是数组的一个元素、一个变量,此时的“常量”表示的是元素的下标。
此外,当给元素单独赋值时不能加大括号,因为元素就是变量,即 a[5] 只是一个变量名。前面是怎么给变量赋值的现在就怎么给数组元素赋值,比如“a[5]=1;”。但是对于上面这个程序,这么写还是错误的。因为数组元素的下标是从 0 开始的,数组 a 的元素只有 a[0]~a[4],并没有 a[5] 这个元素。
联合体
用途:使几个不同类型的变量共占一段内存(相互覆盖)
结构体是一种构造数据类型
用途:把不同类型的数据组合成一个整体-------自定义数据类型
总结:
声明一个联合体:
union abc{
int i;
char m;
};
========================================================================================================
结构体变量所占内存长度是各成员占的内存长度的总和。
共同体变量所占内存长度是各最长的成员占的内存长度。
共同体每次只能存放哪个的一种!!
共同体变量中起作用的成员是最后一次存放的成员,在存入新的成员后原有的成员失去了作用!
=====================================================================================
Struct与Union主要有以下区别:
struct和union都是由多个不同的数据类型成员组成, 但在任何同一时刻, union中只存放了一个被选中的成员, 而struct的所有成员都存在。在struct中,各成员都占有自己的内存空间,它们是同时存在的。一个struct变量的总长度等于所有成员长度之和。在Union中,所有成员不能同时占用它的内存空间,它们不能同时存在。Union变量的长度等于最长的成员的长度。
对于union的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于struct的不同成员赋值是互不影响的。
在C/C++程序的编写中,当多个基本数据类型或复合数据结构要占用同一片内存时,我们要使用联合体;当多种类型,多个对象,多个事物只取其一时(我们姑且通俗地称其为“n 选1”),我们也可以使用联合体来发挥其长处。
首先看一段代码:
union myun
{
struct { int x; int y; int z; }u;
int k;
}a;
int main()
{
a.u.x =4;
a.u.y =5;
a.u.z =6;
a.k = 0;
printf("%d %d %d\n",a.u.x,a.u.y,a.u.z);
return 0;
}
union类型是共享内存的,以size最大的结构作为自己的大小,这样的话,myun这个结构就包含u这个结构体,而大小也等于u这个结构体的大小,在内存中的排列为声明的顺序x,y,z从低到高,然后赋值的时候,在内存中,就是x的位置放置4,y的位置放置5,z的位置放置6,现在对k赋值,对k的赋值因为是union,要共享内存,所以从union的首地址开始放置,首地址开始的位置其实是x的位置,这样原来内存中x的位置就被k所赋的值代替了,就变为0了,这个时候要进行打印,就直接看内存里就行了,x的位置也就是k的位置是0,而 y,z的位置的值没有改变,所以应该是0,5,6
==========================================================================================================================================================
面对一个大型C/C++程序时,只看其对struct的使用情况我们就可以对其编写者的编程经验进行评估。因为一个大型的C/C++程序,势必要涉及一些(甚至大量)进行数据组合的结构体,这些结构体可以将原本意义属于一个整体的数据组合在一起。从某种程度上来说,会不会用struct,怎样用struct是区别一个开发人员是否具备丰富开发经历的标志。在网络协议、通信控制、嵌入式系统的C/C++编程中,我们经常要传送的不是简单的字节流(char型数组),而是多种数据组合起来的一个整体,其表现形式是一个结构体。经验不足的开发人员往往将所有需要传送的内容依顺序保存在char型数组中,通过指针偏移的方法传送网络报文等信息。这样做编程复杂,易出错,而且一旦控制方式及通信协议有所变化,程序就要进行非常细致的修改。一个有经验的开发者则灵活运用结构体,举一个例子,假设网络或控制协议中需要传送三种报文,其格式分别为packetA、packetB、packetC:
struct structA
{
int a;
char b;
};
struct structB
{
char a;
short b;
};
struct structC
{
int a;
char b;
float c;
}
优秀的程序设计者这样设计传送的报文:
struct CommuPacket
{
int iPacketType; //报文类型标志
union //每次传送的是三种报文中的一种,使用union
{
struct structA packetA;
struct structB packetB;
struct structC packetC;
}
};
在进行报文传送时,直接传送struct CommuPacket一个整体。
假设发送函数的原形如下:
// pSendData:发送字节流的首地址,iLen:要发送的长度
Send(char * pSendData, unsigned int iLen);
发送方可以直接进行如下调用发送struct CommuPacket的一个实例sendCommuPacket:
Send( (char *)&sendCommuPacket , sizeof(CommuPacket) );
假设接收函数的原形如下:
// pRecvData:发送字节流的首地址,iLen:要接收的长度
//返回值:实际接收到的字节数
unsigned int Recv(char * pRecvData, unsigned int iLen);
接收方可以直接进行如下调用将接收到的数据保存在struct CommuPacket的一个实例
recvCommuPacket中:
Recv( (char *)&recvCommuPacket , sizeof(CommuPacket) );
接着判断报文类型进行相应处理:
switch(recvCommuPacket. iPacketType)
{
case PACKET_A:
… //A类报文处理
break;
case PACKET_B:
… //B类报文处理
break;
case PACKET_C:
… //C类报文处理
break;
}
以上程序中最值得注意的是
Send( (char *)&sendCommuPacket , sizeof(CommuPacket) );
Recv( (char *)&recvCommuPacket , sizeof(CommuPacket) );
中的强制类型转换:(char *)&sendCommuPacket、(char *)&recvCommuPacket,先取地址,再转化为char型指针,这样就可以直接利用处理字节流的函数。
利用这种强制类型转化,我们还可以方便程序的编写,例如要对sendCommuPacket所处内存初始化为0,可以这样调用标准库函数memset():
memset((char *)&sendCommuPacket,0, sizeof(CommuPacket));
====================================================================================================================
Intel、微软等公司曾经出过一道类似的面试题:
#include
#pragma pack(8)
struct example1
{
short a;
long b;
};
struct example2
{
char c;
example1 struct1;
short e;
};
#pragma pack()
int main(int argc, char* argv[])
{
example2 struct2;
cout << sizeof(example1) << endl;
cout << sizeof(example2) << endl;
cout << (unsigned int)(&struct2.struct1) - (unsigned int)(&struct2) << endl;
return 0;
}
问程序的输入结果是什么?
答案是:
8
16
4
不明白?还是不明白?下面一一道来:
2.1 自然对界
struct是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如 array、struct、union等)的数据单元。对于结构体,编译器会自动进行成员变量的对齐,以提高运算效率。缺省情况下,编译器为结构体的每个 成员按其自然对界(natural alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
自然对界(natural alignment)即默认对齐方式,是指按结构体的成员中size最大的成员对齐。
例如:
struct naturalalign
{
char a;
short b;
char c;
};
在上述结构体中,size最大的是short,其长度为2字节,因而结构体中的char成员a、c都以2为单位对齐,sizeof(naturalalign)的结果等于6;
如果改为:
struct naturalalign
{
char a;
int b;
char c;
};
其结果显然为12。
2.2 指定对界
一般地,可以通过下面的方法来改变缺省的对界条件:
使用伪指令#pragma pack (n),编译器将按照n个字节对齐;
使用伪指令#pragma pack (),取消自定义字节对齐方式。
注意:如果#pragma pack (n)中指定的n大于结构体中最大成员的size,则其不起作用,结构体仍然按照size最大的成员进行对界。
例如:
#pragma pack (n)
struct naturalalign
{
char a;
int b;
char c;
};
当n为4、8、16时,其对齐方式均一样,sizeof(naturalalign)的结果都等于12。而当n为2时,其发挥了作用,使得sizeof(naturalalign)的结果为8。
2.3 面试题的解答
至此,我们可以对Intel、微软的面试题进行全面的解答。
程序中第2行#pragma pack (8)虽然指定了对界为8,但是由于struct example1中的成员最大size为4(long变量size为4),故struct example1仍然按4字节对界,struct example1的size为8,即第18行的输出结果;
struct example2中包含了struct example1,其本身包含的简单数据成员的最大size为2(short变量e),但是因为其包含了struct example1,而struct example1中的最大成员size为4,struct example2也应以4对界,#pragma pack (8)中指定的对界对struct example2也不起作用,故19行的输出结果为16;
由于struct example2中的成员以4为单位对界,故其char变量c后应补充3个空,其后才是成员struct1的内存空间,20行的输出结果为4。
C和C++之间结构体的深层区别
在C++语言中struct具有了“类” 的功能,其与关键字class的区别在于struct中成员变量和函数的默认访问权限为public,而class的为private。
例如,定义struct类和class类:
struct structA
{
char a;
…
}
class classB
{
char a;
…
}
则:
struct A a;
a.a = ‘a’; //访问public成员,合法
classB b;
b.a = ‘a’; //访问private成员,不合法
许多文献写到这里就认为已经给出了C++中struct和class的全部区别,实则不然,另外一点需要注意的是:
C++中的struct保持了对C中struct的全面兼容(这符合C++的初衷——“a better c”),因而,下面的操作是合法的:
//定义struct
struct structA
{
char a;
char b;
int c;
};
structA a = {‘a’ , ‘a’ ,1}; // 定义时直接赋初值
即struct可以在定义的时候直接以{ }对其成员变量赋初值,而class则不能。
看看下面的程序:
#include
struct structA
{
int iMember;
char cMember;
};
int main(int argc, char argv[])
{
structA instant1,instant2;
char c = ‘a’;
instant1.iMember = 1;
instant1.cMember = &c;
instant2 = instant1;
cout << *(instant1.cMember) << endl;
*(instant2.cMember) = ‘b’;
cout << *(instant1.cMember) << endl;
return 0;
}
14行的输出结果是:a
16行的输出结果是:b
Why?我们在15行对instant2的修改改变了instant1中成员的值!
原因在于13行的instant2 = instant1赋值语句采用的是变量逐个拷贝,这使得instant1和instant2中的cMember指向了同一片内存,因而对instant2的修改也是对instant1的修改。
在C语言中,当结构体中存在指针型成员时,一定要注意在采用赋值语句时是否将2个实例中的指针型成员指向了同一片内存。
在C++语言中,当结构体中存在指针型成员时,我们需要重写struct的拷贝构造函数并进行“=”操作符重载。
===================================================================================================================
C语言中的结构体(struct)和联合体(union)的简介
看到有朋友介绍union,我以前还没有用过这个东西呢,也不懂,就去搜了点资料来看,也转给大家,希望坛子里的给予改正或补充。谢谢!
联 合(union)
union a_bc{
int i;
char mm;
};
再用已说明的联合可定义联合变量。
例如用上面说明的联合定义一个名为lgc的联合变量, 可写成:
union a_bc lgc;
在联合变量lgc中, 整型量i和字符mm公用同一内存位置。
当一个联合被说明时, 编译程序自动地产生一个变量, 其长度为联合中最大的变量长度。
联合访问其成员的方法与结构相同。同样联合变量也可以定义成数组或指针,但定义为指针时, 也要用"->;"符号,此时联合访问成员可表示成:
联合名->;成员名
另外, 联合既可以出现在结构内, 它的成员也可以是结构。
例如:
struct{
int age;
char *addr;
union{
int i;
char *ch;
}x;
}y[10];
若要访问结构变量y[1]中联合x的成员i, 可以写成:
y[1].x.i;
若要访问结构变量y[2]中联合x的字符串指针ch的第一个字符可写成:
*y[2].x.ch;
若写成"y[2].x.*ch;"是错误的。
2. 结构和联合的区别
结构和联合有下列区别:
例4:
main()
{
union{
int i;
struct{
char first;
char second;
}half;
}number;
number.i=0x4241;
printf("%c%cn", number.half.first, number.half.second);
number.half.first=‘a’;
number.half.second=‘b’;
printf("%xn", number.i);
getch();
}
输出结果为:
AB
6261
从上例结果可以看出: 当给i赋值后, 其低八位也就是first和second的值;当给first和second赋字符后, 这两个字符的ASCII码也将作为i的低八
共用体
构造数据类型,也叫联合体
用途:使几个不同类型的变量共占一段内存(相互覆盖)
学习 C 语言的指针既简单又有趣。通过指针,可以简化一些 C 编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。所以,想要成为一名优秀的 C 程序员,学习指针是很有必要的。
正如您所知道的,每一个变量都有一个内存位置,每一个内存位置都定义了可使用 & 运算符访问的地址,它表示了在内存中的一个地址。
请看下面的实例,它将输出定义的变量地址:
实例
#include
int main ()
{
int var_runoob = 10;
int *p; // 定义指针变量
p = &var_runoob;
printf(“var_runoob 变量的地址: %p\n”, p);
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
var_runoob 变量的地址: 0x7ffeeaae08d8
通过上面的实例,我们了解了什么是内存地址以及如何访问它。接下来让我们看看什么是指针。
什么是指针?
指针也就是内存地址,指针变量是用来存放内存地址的变量。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:
type *var_name;
在这里,type 是指针的基类型,它必须是一个有效的 C 数据类型,var_name 是指针变量的名称。用来声明指针的星号 * 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。以下是有效的指针声明:
int ip; / 一个整型的指针 */
double dp; / 一个 double 型的指针 */
float fp; / 一个浮点型的指针 */
char ch; / 一个字符型的指针 */
所有实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,对应指针的值的类型都是一样的,都是一个代表内存地址的长的十六进制数。
不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。
如何使用指针?
使用指针时会频繁进行以下几个操作:定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。这些是通过使用一元运算符 * 来返回位于操作数所指定地址的变量的值。下面的实例涉及到了这些操作:
实例
#include
int main ()
{
int var = 20; /* 实际变量的声明 */
int ip; / 指针变量的声明 */
ip = &var; /* 在指针变量中存储 var 的地址 */
printf(“var 变量的地址: %p\n”, &var );
/* 在指针变量中存储的地址 */
printf(“ip 变量存储的地址: %p\n”, ip );
/* 使用指针访问值 */
printf("*ip 变量的值: %d\n", *ip );
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
var 变量的地址: 0x7ffeeef168d8
ip 变量存储的地址: 0x7ffeeef168d8
*ip 变量的值: 20
C 中的 NULL 指针
在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。
NULL 指针是一个定义在标准库中的值为零的常量。请看下面的程序:
实例
#include
int main ()
{
int *ptr = NULL;
printf(“ptr 的地址是 %p\n”, ptr );
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
ptr 的地址是 0x0
在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。但按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。
如需检查一个空指针,您可以使用 if 语句,如下所示:
if(ptr) /* 如果 p 非空,则完成 /
if(!ptr) / 如果 p 为空,则完成 */
#define命令是C语言中的一个宏定义命令,它用来讲一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本。该命令有两种格式:一种是简单的宏定义(不带参数的宏定义),另一种是带参数的宏定义。
(1) 简单的宏定义
格式:#define <宏名/标识符> <字符串>
eg:#define PI 3.1415926
说明:
①宏名一般用大写
②宏定义末尾不加分好;
③可以用#undef命令终止宏定义的作用域
④宏定义可以嵌套
⑤字符串“”中永远不包含宏
⑥宏替换在编译前进行,不分配内存,变量定义分配内存,函数调用在编译后程序运行时进行,并且分配内存
⑦预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查
⑧使用宏可提高程序的通用性和易读性,减少不一致性,减少输入错误和便于修改。例如:数组大小常用宏定义
(2) 带参数的宏定义(除了一般的字符串替换,还要做参数代换)
格式:#define <宏名>(<参数表>) <字符串>
eg:#define S(a,b) a*b
area=S(3,2);
第一步被换为area=ab;第二步换为area=32;
一个标识符被宏定义后,该标识符便是一个宏名。这时,在程序中出现的是宏名,在该程序被编译前,先将宏名用被定义的字符串替换,这称为宏替换,替换后才进行编译,宏替换是简单的替换。
说明:
①实参如果是表达式容易出问题
#define S® r*r
area=S(a+b);第一步换为area=rr;第二步换成area=a+ba+b;
当定义为#define S®(®®)时area=((a+b)(a+b))
②宏名和参数的括号间不能有空格
③宏替换之作替换不做计算,不做表达式求解
④宏的哑实结合不存在类型,也没有类型转换
⑤宏展开不占用运行时间,只占用编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值)
#define 第一位置 第二位置
(1)“”内的东西不会被宏替换
#define NAMEzhang
程序中有"NAME"则,它会不会被替换呢?
答:否
(2)宏定义前面的那个必须是合法的用户标识符(可以使关键字)
#define 0x abcd
可以吗?也就是说,可不可以用把标识符的字母替换成别的东西?
答:否
(3)第二位置如果有字符串,必须“”配对
#define NAME "zhang
这个可以吗?
答:否
(4)只替换与第一位置完全相同的标识符
#define NAME “zhangyuncong”
程序中有上面的宏定义,并且,程序里有句:NAMELIST这样,会 不会被替换成"zhangyuncong"LIST
答:否
(5)带参数宏的一般用法
例如:
①#define MAX(a,b) ((a)>(b)?(a):(b))
则遇到MAX(1+2,value)则会把它替换成:
((1+2)>(value)?(1+2):(value))
②#define FUN(a) “a”
则,输入FUN(345)会被替换成什么?
其实,如果这么写,无论宏的实参是什么,都不会影响其被替换成 “a”。也就是说,""内的字符不被当成形参,即使它和一模一样。
③#define N 2+2
void main()
{
int a=N*N;
printf(“%d”,a);
}
出现问题:在此程序中存在着宏定义命令,宏N代表的字符串是2+2,在程序中有对宏N的使用,一般同学在读该程序时,容易产生的问题是先求解N为 2+2=4,然后在程序中计算a时使用乘法,即NN=44=16,其实该题的结果为8,为什么结果有这么大的偏差?
问题解析:如1节所述,宏展开是在预处理阶段完成的,这个阶段把替换文本只是看作一个字符串,并不会有任何的计算发生,在展开时是在宏N出现的地方 只是简单地使用串2+2来代替N,并不会增添任何的符号,所以对该程序展开后的结果是a=2+2*2+2,计算后=8,这就是宏替换的实质,如何写程序才 能完成结果为16的运算呢?
解决办法:将宏定义写成如下形式
#define N (2+2)
这样就可替换成(2+2)*(2+2)=16
④#define area(x) x*x
void main()
{
int y=area(2+2);
printf(“%d”,y);
}
按理说给的参数是2+2,所得的结果应该为44=16,但是错了,因为该程序的实际结果为8,仍然是没能遵循纯粹的简单替换的规则,又是先计算再替换 了,在这道程序里,2+2即为area宏中的参数,应该由它来替换宏定义中的x,即替换成2+22+2=8了。那如果遵循(1)中的解决办法,把2+2 括起来,即把宏体中的x括起来,是否可以呢?#define area(x) (x)(x),对于area(2+2),替换为(2+2)(2+2)=16,可以解决,但是对于area(2+2)/area(2+2)又会怎么样呢,有的学生一看到这道题马上给出结果,因为分子分母一样,又错了,还是忘了遵循先替换再计算的规则了,这道题替换后会变为(2+2)(2+2)/(2+2)(2+2)即44/44按照乘除运算规则,结果为16/44=44=16,那应该怎么呢?解决方法是在整个 宏体上再加一个括号,即#definearea(x) ((x)*(x)),不要觉得这没必要,没有它,是不行的。
要想能够真正使用好宏定义,那么在读别人的程序时,一定要记住先将程序中对宏的使用全部替换成它所代表的字符串,不要自作主张地添加任何其他符号,完全展开后再进行相应的计算,就不会写错运行结果。如果是自己编程使用宏替换,则在使用简单宏定义时,当字符串中不只一个符号时,加上括号表现出优先级,如果是 带参数的宏定义,则要给宏体中的每个参数加上括号,并在整个宏体上再加一个括号。
⑤多行宏定义
#define doit (m,n) for(inti=0;i<(n);++i) { m+=i; }
#define Conn(x,y) x##y
#define ToChar(x) #@x
#define ToString(x) #x
x##y表示什么?表示x连接y,举例说:
int n = Conn(123,456); 结果就是n=123456;
char* str = Conn(“asdf”,“adf”)结果就是 str = “asdfadf”;
#@x,其实就是给x加上单引号,结果返回是一个constchar。
举例说:
char a = ToChar(1);结果就是a=‘1’;
做个越界试验char a = ToChar(123);结果是a=‘3’;
但是如果你的参数超过四个字符,编译器就给给你报错了!error C2015:too many characters in constant :P
#x是给x加双引号
char* str = ToString(123132);就成了str=“123132”;
如果有#define FUN(a,b) vo##a##b()那么FUN(idma,in)会被替换成void main()
附录:
① 预处理功能:
(1)文件包含:可以把源程序中的#define扩展为文件正文,即把包含的.h文件找到并展开到#include所在处。
(2)条件编译:预处理器根据#if和#ifdef等编译命令及其后的条件,把源程序中的某些部分包含进来或排除在外,通常把排除在外的语句转换成空行。
(3)宏展开:预处理器将源程序文件中出现的对宏的引用展开成相应的宏定义,经过预处理器处理的源程序与之前的源程序有所不同,在这个阶段所进行的工作只是纯粹的替换和展开,没有任何计算功能。
②使用带参数的宏定义可完成函数调用的功能,又能减少系统开销,提高运行效率。
正如C语言中所讲,函数的使用可以使程序更加模块化,便于组织,而且可重复利用,但在发生函数调用时,需要保留调用函数的现场,以便子函数执行结束后能返回继续执行,同样在子函数执行完后要恢复调用函数的现场,这都需要一定的时间,如果子函数执行的操作比较多,这种转换时间开销可以忽略,但如果子函数完成的功能比较少,甚至只完成一点操作,如一个乘法语句的操作,则这部分转换开销就相对较大了,但使用带参数的宏定义就不会出现这个问题,因为它是在预处理阶段即进行了宏展开,在执行时不需要转换,即在当地执行。宏定义可完成简单的操作,但复杂的操作还是要由函数调用来完成,而且宏定义所占用的目标代码空间相对较大。所以在使用时要依据具体情况来决定是否使用宏定义。