声明:本文为个人c语言学习笔记,所以有比较多的内容为转载,部分内容为自己编写。整理目的仅为了方便个人学习。对应链接在对应章节,没有链接的就是本菜狗结合书和老师的课件弄出来的。
一.数据类型
二.运算符,表达式和语句
三.循环
四.分支和跳转
五.函数
六.数组
七.结构体与联合体(重要)
八.指针 (重要)
九.宏定义
一、数据类型
二.运算符,表达式和语句
1、基本的算术运算符
1、+(加号) 加法运算 (3+3)
2、–(减号) 减法运算 (3–1) 负 (–1)
3、*(星号) 乘法运算 (3*3)
4、/(正斜线) 除法运算 (3/3)
5、%(百分号) 求余运算10%3=1 (10/3=3·······1)
6、^(乘方) 乘幂运算 (3^2)
7、! (阶乘) 连续乘法 (3!=3*2*1=6)
8、|X| x为任何数 (绝对值) 求正 (|1|)
说明:
①两个整数的相除的结果是整数。②%运算符要求参加运算的运算对象为整数,结果也是整数。③除%以外的运算符的操作数都可以是任何算术类型。
2、自增、自减运算符:作用是使变量的值加1或减1。
++i,--i
在使用i之前,先使i的值加(减)1
i++,i--
在使用i之后,再使i的值加(减)1
注意:自增,自减运算符只能用于变量,不能用于常量和表达式。
如:int x = 11;
表达式(x++*1/3)的值为 3.
3、算术表达式和运算符的优先级与结合性
用算术运算符和括号将运算对象连接起来的、符合C语法规则的式子,称为C算术表达式。运算对象包括常量、变量、函数等。
C语言规定求表达式时,先按运算符的优先级别顺序进行,若一个运算对象两侧的运算符的优先级别相同时,则按规定的“结合方向”处理。
“左结合性”:结合方向自左向右,即运算对象先与左边的运算符结合。例:算术运算符。
“右结合性”:结合方向自右向左,即运算对象先与右边的运算符结合。例:赋值运算符。
4、 不同类型之间的混合运算
如果一个运算符的两侧的数据类型不同,则先自动进行类型转换,使二者具有同一种类型,然后进行运算。因此整型、实型、字符型数据间可以进行混合运算。
规律为:
①+、一、*、/运算的两个数中有一个数为float或double型,结果为double
型,因为系统将所有float型数据都先转换为double型,然后进行运算。
②如果int型与 float或double型数据进行运算,先把int型和float 型数据转换为double型,然后进行运算,结果是double型。
③字符型数据与整型数据进行运算,就是把字符的ASCII代码与整型数据进行运算。
5 、强制类型转换运算符:将一个表达式转换成所需类型。其一般形式为(类型名)(表达式)
注意:在进行强制类型转换时,得到一个所需类型的中间变量,而原来变量的类型并未发生变化。
如:设int n;float f=13.8;则执行n=(int) f%3后,n的值为 1。
6、C运算符
①算术运算符
+-*%++--
②关系运算符
><==>=<=!=
③逻辑运算符
!&&ll
④赋值运算符
二
⑤条件运算符
? :
例: a>b?c:d
⑥逗号运算符
例: r=(a, b, c)
⑦指针运算符
*&
⑧求字节数运算符
sizeof
⑨强制类型转换运算符
(类型)
7.1、 C语句的作用和分类
(1)控制语句。控制语句用于完成一定的控制功能。①if ()……else……
条件语句
for ()….
循环语句
③while ()……
循环语句
④do……while()
循环语句
continue
结束本次循环语句
⑥break
中止执行switch或循环语句
switch
多分支选择语句
⑧return
从函数返回语句
以上语句中()表示括号中是一个“判别条件”,“……”表示内嵌的语句。(2)函数调用语句。函数调用语句由一个函数调用加一个分号构成。(3)表达式语句。表达式语句由一个表达式加一个分号构成。
(4)空语句。可以用来作为流程的转向点,也可用来作为循环语句中的循环体。
(5)复合语句。可以用{}把一些语句和声明括起来称为复合语句。注意:复合语句中最后一个语句中最后的分号不能忽略不写。
7.2、赋值语句
(1)赋值运算符
赋值符号=就是赋值运算符,它的作用是将一个数据赋给一个变量,也可以将一个表达式的值赋给一个变量。变量必须先定义,后使用。
int x;
①定义:数据类型变量名;
f1oat y;
x=1;
②赋值:变量名=常量;
int a=2;
③初始化:数据类型变量名=常量;
int b=3,c=4;
试问:double m=n=1.0;这条语句的初始化是否正确?
(2)复合的赋值表达式
在赋值符=之前加上其他运算符,可以构成复合的运算符。
a+=b
等价于
a=a+b
注意:如果b是包含若干项的表达式,则相当于它有括号。
x%=y+3
等价于
X=x%(y+3)
(3)赋值表达式
赋值语句是在赋值表达式的末尾加上一个分号构成的。
由赋值运算符将一个变量和一个表达式连接起来的式子称为“赋值表达式”。它的一般形式为
变量赋值运算符表达式
注意:赋值运算符的左侧只能是变量,而不能是常量或者表达式。
(4)赋值过程中的类型转换
①如果赋值运算符两侧的类型一致,则直接进行赋值。
②如果赋值运算符两侧的类型不一致,但都是算术类型时,在赋值时要进行类型转换。转换的规则是:
将浮点型数据赋给整型变量时,先对浮点数取整,即舍弃小数部分,然后赋给整型变量。
将整型数据赋给实型变量时,数值不变,但以浮点型形式存储到变量中。
字符型数据赋给整型变量时,将字符的ASCII代码赋给整型变量。
将一个占字节多的整型数据赋给一个占字节少的整型变量或字符变量时,只将其低字节原封不动地送到被赋值的变量。
7.4 输入输出语句
#include<stdio.h>int main ()
{
int a;
scanf(“%d”,&a);
printf ( "“a=%d”, a);
return 0;
}
7.5
用putchar函数输出一个字符
putchar函数的一般形式是
putchar(c)
注意: putchar (c)中的c可以是字符常量、整型常量、字符变量或整型变量其值要在字符的ASCII代码范围内)。
用get char函数输入一个字符
getchar函数的一般形式是
getchar(c)
while 循环
只要控制表达式为 true,while 循环就会反复地执行语句:
while (表达式)语句
while 表达式是顶部驱动(top-driven)的循环:先计算循环条件(也就是控制表达式)。如果为 true,就执行循环体,然后再次计算控制表达式。如果控制表达式为 false,程序跳过循环体,而去执行循环体后面的语句。
从语法上讲,循环体只有一条语句组成。如果需要执行多条语句时,可以使用语句块把它们组合在一起。例 1 展示了一个简单的 while 循环,从控制台读入多个浮点数,并把它们累加。
例 1 展示了一个简单的 while 循环,从控制台读入多个浮点数,并把它们累加。
【例1】一个 while 循环
- /* 从键盘输入数字,然后输出它们的平均值
- * -------------------------------------- */
- #include <stdio.h>
- 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 ( ; ; )
如果没有控制表达式,则表示循环条件始终是 true,也就是说,这定义了一个死循环。
下面所示的 for 循环,既没有初始化表达式,也没有调节器表达式,它与 while(表达式)语句含义是等效的:
- 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 );
例 4 展示了标准库函数 strcpy()的一个版本,循环体仅为一条简单的语句,而不是一个语句块。因为在循环体执行之后才计算循环条件,所以字符串终止符'\0'也会被复制。
【例4】函数 strcpy()使用 do...while
- // 将字符串2复制到字符串1
- // ----------------------------
- char *strcpy( char* restrict s1, const char* restrict s2 )
- {
- int i = 0;
- do
- s1[i] = s2[i]; // 循环体:复制每一个字符
- while ( s2[i++] != '\0' ); // 如果刚刚复制的是'\0',则结束循环
- return s1;
- }
四、分支和跳转(
版权声明:本文为CSDN博主「量变决定质变」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/nangeali/article/details/78825123
https://blog.csdn.net/yikaozhudapao/article/details/80172880
)
C语言中常用分支语句有if语句,switch语句
首先来说一下if语句。if语句的基本表达式:
if(表达式) if(表达式)
语句; 语句1;
else
语句2;
如果if语句中的表达式为真,则执行语句1,否则执行语句2;如果条件成立,执行多条语句,应该使用代码块。
在这里,一个if和一个else配对,if可以单独使用,有else出现一定有if,这里着重说下if表达式中浮点数与0的比较,因为在内存中整数与浮点数的存储结构是不同的,有些浮点数在内存中无法准确的存储,所以必然会有精度的丢失,所以无法直接使用 == 来判断。
我们可以自己设置一个在我们接受范围之内的精度,保证误差在这个范围之内就可以了。
#define EPSINON 0.000000000001
float f = 0.000001;
if((f > -EPSINON) && (f < EPSINON))
{undefined
}
switch语句:
switch(常量表达式)
{undefined
case 常量 1 :语句1
case 常量 2 :语句2
... ... ...
case 常量 n :语句n
default : 语句n+1
}
在使用switch语句时,先计算switch后面的表达式,再与case后面的常亮表达式作比较,如果相等则执行后面的语句,没有与case后面常量表达式相同的值,则转去执行default后面的语句。case语句的顺序对switch并没有影响。
我们可以用switch语句设计一个程序,输入1显示星期一,输入二显示星期二,以此类推
#include<stdio.h>
int main()
{undefined
int day = 0;
scanf("%d", &day);
switch (day)
{undefined
case 1:
printf("星期一\n");
break;
case 2:
printf("星期二\n");
break;
case 3:
printf("星期三\n");
break;
case 4:
printf("星期四\n");
break;
case 5:
printf("星期五\n");
break;
case 6:
printf("星期六\n");
break;
case 7:
printf("星期日\n");
break;
}
return 0;
}
值得注意的是,给每条case语句之后都应该加上break语句是一个好习惯,若有其他需要则需要在适当的位置加上break,防止switch语句执行过度,出现与预期相左的结果。
跳转语句,用于实现循环执行过程中,程序流程的跳转
C语言中,跳转语句有break语句、goto语句、continue语句3种
break语句
switch条件语句和循环语句中,都可以使用break语句
switch条件语句中,作用终止某个case,并且跳出switch结构
循环语句中,作用是跳出当前循环语句,执行后面的代码
示例代码
#include <stdio.h>
void main()
{
int x=1;
while(x<=4)
{
printf("x=%d\n",x);
if (x==3)
{
break;
}
x++;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
运行结果
goto语句
当break出现在嵌套循环的内层循环时,只能跳出内层循环
如果,想要直接跳出外层循环,则需要对外层循环添加标记,使用goto语句
示例代码
#include <stdio.h>
int main()
{
int i,j;
for(i=1;i<=9;i++)
{
for(j=1;j<=i;j++)
{
if(i>4)
{
goto end;
}
printf("*");
}
printf("\n");
}
end:return 0;
}
运行结果
continue语句
在循环语句中,如果想要立即终止本次循环,并且执行下一次循环,需要使用continue语句
示例代码
#include <stdio.h>
void main()
{
int sum=0;
for(int i=1;i<=100;i++)
{
if(i%2==0)
{
continue;
}
sum+=i;
}
printf("sum=%d\n",sum);
}
运行结果
五、函数
1.定义函数
所有函数都是平行的,即在定义函数时是分别进行的,是相互独立的。一个函数并不从属于另一个函数,即函数不能嵌套定义。函数之间可以相互调用,但不能调用main函数。main函数是被操作系统调用的。
C语言要求,在程序中用到的所有函数,必须“先定义,后使用”。
(1)定义无参函数
定义无参函数的一般形式为类型名函数名()
类型名函数名(void)
{
{
函数体
或
函数体
}
}
函数后面括号内的void表示“空”,即函数没有参数。函数体包括声明部分和语句部分。
在定义函数时要用“类型名”指定函数值的类型,即指定函数带回来的值的类型。
(2)定义有参函数
定义有参函数的一般形式为
类型名函数名(形式参数表列){
函数体
}
(3)定义空函数
定义空函数的一般形式为
类型名函数名){ }
2.调用函数
2.1函数调用的形式
函数调用的一般形式为
函数名(实参表列)
如果是调用无参函数,则“实参表列”可以没有,但括号不能省略。如果实参表列包含多个实参,则参数间用逗号隔开。
以下3种函数调用方式。
(1)函数调用语句
把函数调用单独作为一个语句,这时不要求函数带回值,只需要函数完成一定的操作。
(2)函数表达式
函数调用出现在另一表达式中,这时要求函数带回一个确定的值以参加
表达式的运算。
(3)函数参数
函数调用作为另一个函数调用时的实参。
说明:调用函数并不一定要求包括分号,只有作为函数调用语句才需要有分号。如果作为函数表达式或函数参数,函数调用本身是不必有分号的。
2.2函数调用时的数据传递
(1)形式参数和实际参数
在调用有参函数时,主调函数和被调用函数之间有数据传递关系。在定义函数时函数名后面括号中的变量名称为“形式参数”(简称“形参”)。在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”(简称“实参”)。
(2)实参和形参间的数据传递
在调用函数过程中,系统会把实参的值传递给被调用函数的形参。或者说,形参从实参得到一个值。该值在函数调用期间有效,可以参加该函数中的运算。
说明:
①实参可以是常量、变量、表达式。②实参与形参的类型应相同或赋值兼容。
2.3函数调用的过程
在定义函数中指定的形参,在未出现函数调用时,它们并不占内存中的存储单元。在发生函数调用时,函数的形参被临时分配内存单元。将实参对应的值传递给形参。如果函数不需要返回值,则不需要return语句。这时,函数的类型应定义为void类型。调用结束,形参单元被释放。
注意:
实参单元仍保留并维持原值,没有改变。实参向形参的数据传递是“值传递”,单项传递,只能由实参传给形参,而不能由形参传给实参。实参和形参在内存中占有不同的存储单元,实参无法得到形参的值。
2.4函数的返回值
(1)函数的返回值是通过函数中的return语句获得的。(2)函数值的类型
(3)在定义函数时指定的函数类型一般应该和return语句中的表达式一致。函数类型决定返回值的类型。
(4)对于不带回值的函数,应当用定义函数为“void类型”(或称“空类型”)。即禁止在调用函数中使用被调用函数的返回值。此时在函数体中不得出现return语句。
3.对被调用函数的声明和函数原型
在一个函数中调用另一个函数(即被调用函数)需要具备如下条件:(1)首先被调用的函数必须是已经定义的函数。
(2)如果使用库函数,应该在本文件开头用#include 指令将调用有关库函数时所需用到的信息“包含”到本文件中来。
(3)如果使用用户自己定义的函数,而该函数的位置在调用它的函数(即主调函数〉的后面,应该在主调函数中对被调用的函数作声明。声明的作用是把函数名、函数参数的个数和参数类型等信息通知编译系统。
函数的声明和函数定义中的第一行(函数首部)基本上是相同的,函数声明比函数定义首行多一个分号。函数的首行(即函数首部)称为函数原型。
函数原型的一般形式有两种,分别为:
(1)函数类型函数名(参数类型1参数名1,参数类型⒉参数名2,……,参数类型n参数名n);
(2)函数类型函数名(参数类型1,参数类型2,……,参数类型n);注意:
对函数的“定义”和“声明”不是一回事。函数的定义是指对函数功能的确定,包括指定函数名、函数值类型、形参及其类型以及函数体等,它是一个完整的、独立的函数单位。而函数的声明的作用则是把函数的名字、函数类型以及形参的类型、个数和顺序通知编译系统,以便在调用该函数时系统按此进行对照检查(例如,函数名是否正确,实参与形参的类型和个数是否一致),它不包含函数体。
4.函数的嵌套调用
C语言的函数定义是相互平行的、独立的。在定义函数时,一个函数内不能再定义另一个函数,也就是不能嵌套定义,但可以嵌套调用,也就是说,在调用一个函数的过程中,又调用另一个函数。
实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值。
通常将函数所处理的数据,影响函数功能的因素或者函数处理的结果作为形参。
实参是用来填充形参的。当函数被调用时,实参列在函数名后面的括号里。执行函数调用时,实参被传递给形参。
funcation a(b){
var x=b;
}
b是形参,x为实参。
————————————————
版权声明:本文为CSDN博主「李狂之」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_35484998/article/details/78836877
六、数组
1.一维数组定义、引用和初始化
一批具有同名的同属性的数据就组成一个数组。
①数组是一组有序数据的集合。数组中各数据的排列是有一定规律的,下标代表数据在数组中的序号。
②用一个数组名和下标来唯一地确定数组中的元素。③数组中的每一个元素都属于同一个数据类型。
1.1、定义一维数组
定义一维数组的一般形式为
类型符数组名[常量表达式];
说明:
①数组名的命名规则和变量名相同,遵循标识符命名规则。
②在定义数组时,需要指定数组中元素的个数,方括号中的常量表达式用来表示元素的个数,即数组长度。
③常量表达式中可以包括常量和符号常量,不能包含变量。C语言不允许对数组的大小作动态定义,即数组的大小不依赖于程序运行过程中变量的值。
1.2、引用一维数组
引用数组元素的表示形式为
数组名[下标]
注意:
定义数组是用到的“数组名[常量表达式]”和引用数组元素时用的“数组名下标]”形式相同,但含义不同。
1.3 一维数组的初始化
在定义数组的同时,给各数组元素赋值,这称为数组的初始化。
(1)在定义数组时对数组元素赋予初值。
将数组中各元素的初值顺序放在一对花括号里,数据间用逗号分开。
(2)可以只给数组中的一部分元素赋值。
(3)若想使一个数组中全部元素值为0,可以写成int a[10]={0,0,0,0,0,0,0,0,0,0} ;
或
int a[10]={0} ;
//未赋值的部分元素自动设为0
(4)在对全部数组元素赋初值时,由于数据的个数已经确定,因此可以不指定数组长度。例如:
int a[5]={1,2,3,4,5} ;可以写成
int a[]={1,2,3,4,5};
但是,如果数组长度与提供初值的个数不相同,则方括号的数组长度不能省略。例如,想定义数组长度为10,就不能省略数组长度的定义,而必须写成
int a[10]={1,2,3,4,5} ;
只初始化前5个元素,后5个元素为0。说明:
如果在定义数值型数组时,指定了数组的长度并对之初始化,凡未被“初始化列表”指定初始化的数组元素,系统会自动把它们初始化为0(如果是字符形数组,则初始化为“\0”。)
2.二维数组定义、引用和初始化
2.1 定义二维数组
二维数组定义的一般形式
类型说明符数组名[常量表达式][常量表达式];
C语言中,二维数组中元素排列的顺序是按行存放的,即在内存中先顺序存放第1行的元素,接着再存放第2行的元素。
注意:
用矩阵形式表示二维数据,是逻辑上的概念,能形象地表示出行列关系。而在内存中,各元素是连续存放的,不是二维的,是线性的。
2.2 引用二维数组的元素二维数组元素的表示形式为
数组名[下标][下标]
数组元素可以出现在表达式中,也可以被赋值。
注:在引用数组元素时,下标值应在已定义的数组大小的范围内。
2.3、二维数组的初始化
(1)分行给二维数组赋初值。例如:
int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
这种赋初值的方法比较直观,把第一个花括号内的数据给第1行的元素,第2个花括号内的数据赋给第2行的元素……即按行赋初值。
(2)可以将所有数据写在一个花括号内,按数组元素在内存中的排列顺序对各元素赋初值。例如:
int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
(3)可以对部分元素赋初值。例如:
int a[3][4]={{1},{5},{9}} ;
它的作用是只对各行第1列(即序号为0的列)的元素赋初值,其余元素值自动为0。
也可以对各行中的某一元素赋初值,例如:int a[3][4]={{1},{0,6},{0,0,11}} ;也可以只对某几行元素赋初值,例如:int a[3][4]={{1},{5,6}} ;
也可以对中间某一行不赋初值,例如:int a[3][4]={[1},{ },{9}} ;
七、联合体与结构体(
联合体
用途:使几个不同类型的变量共占一段内存(相互覆盖)
结构体是一种构造数据类型
用途:把不同类型的数据组合成一个整体-------自定义数据类型
总结:
声明一个联合体:
-
union abc{
-
int i;
-
char m;
-
};
1. 在联合体abc中,整型量i和字符m公用同一内存位置。
2. 当一个联合被说明时,编译程序自动地产生一个变量,其长度为联合中最大的变量长度。
========================================================================================================
结构体变量所占内存长度是各成员占的内存长度的总和。
共同体变量所占内存长度是各最长的成员占的内存长度。
共同体每次只能存放哪个的一种!!
共同体变量中起作用的成员是最后一次存放的成员,在存入新的成员后原有的成员失去了作用!
=====================================================================================
Struct与Union主要有以下区别:
1. struct和union都是由多个不同的数据类型成员组成, 但在任何同一时刻, union中只存放了一个被选中的成员, 而struct的所有成员都存在。在struct中,各成员都占有自己的内存空间,它们是同时存在的。一个struct变量的总长度等于所有成员长度之和。在Union中,所有成员不能同时占用它的内存空间,它们不能同时存在。Union变量的长度等于最长的成员的长度。
2. 对于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
==========================================================================================================================================================
1. struct的巨大作用
面对一个大型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));
====================================================================================================================
2. struct成员对齐
Intel、微软等公司曾经出过一道类似的面试题:
-
#include <iostream.h>
-
#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。
3. 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则不能。
4. struct编程注意事项
看看下面的程序:
-
#include <iostream.h>
-
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)
1. 联合说明和联合变量定义
联合也是一种新的数据类型, 它是一种特殊形式的变量。
联合说明和联合变量定义与结构十分相似。其形式为:
union 联合名{
数据类型 成员名;
数据类型 成员名;
...
} 联合变量名;
联合表示几个变量公用一个内存位置, 在不同的时间保存不同的数据类型 和不同长度的变量。
下例表示说明一个联合a_bc:
-
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. 结构和联合的区别
结构和联合有下列区别:
1. 结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合转只存放了一个被选中的成员, 而结构的所有成员都存在。
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的低八
共用体
构造数据类型,也叫联合体
用途:使几个不同类型的变量共占一段内存(相互覆盖)
结构体是一种构造数据类型
用途:把不同类型的数据组合成一个整体-------自定义数据类型
八、指针
1.指针概念
内存区的每一个字节有一个编号,这就是“地址”。
由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。
因此,将地址形象化地称为“指针”,通过它能找到以它为地址的内存单元。一个变量的地址称为该变量的“指针”。如果有一个变量专门用来存放另一变量的地址(即指针),则称它为“指针变量”。指针变量的值是地址。
2.指针变量
2.1定义指针变量
定义指针变量的一般形式为
类型名*指针变量名;
左边的类型名是在定义指针变量时必须指定的“基类型”。指针变量的基类型用来指定此指针变量可以指向的变量的类型。
注意:
①指针变量前面的“*”表示该变量的类型为指针型变量。②在定义指针变量时必须指定基类型。
一个变量的指针的含义包括两个方面,一是以存储单元编号表示的地址,一是它指向的存储单元的数据类型。
③指向整型数据的指针类型表示“int *”。
④指针变量中只能存放地址(指针),不要将一个整数赋给一个指针变量。
2.2、引用指针变量
在引用指针变量时,可能有3种情况:
(1)给指针变量赋值。如:p=&a;
(2)引用指针变量指向的变量。
如果已执行“p=&a; ”,即指针变量p指向了整型变量a,则
printf(“%d”,*p);
*p=1 ;
表示将整数1赋给p当前所指向的变量,如果p指向变量a,则相当于把赋给a,即“a=1 ; ”。
(3)引用指针变量的值。如:
printf(“%o”, p);
作用是以八进制数形式输出指针变量p的值,如果p指向了a,就是输出了指针a的地址,即&a。
注意:要熟练掌握两个有关的运算符:
&取地址运算符。&a是变量a的地址;
*为指针运算符。
*p代表指针变量p指向的对象。
2.3、指针变量作为函数参数
注意:
1、不能企图通过改变指针形参的值而使指针实参的值改变。
2、函数的调用可以(而且只可以)得到一个返回值(即函数值),而使用指针变量作参数,可以得到多个变化了的值。
3.通过指针引用数组
3.1、数组元素的指针
数组元素的指针就是数组元素的地址。
3.2、在引用数组元素时指针的运算
(1)如果指针变量p已指向数组的一个元素,则 p+1指向同一数组中的下一个元素,p-1指向同一数组中的上一个元素。注意:执行p+1时并不是将p的值(地址)简单地加1,而是加上一个数组元素所占用的字节数。
(2)如果p的数值是&a[0],则p+i和a+i就是数组元素a[i]的地址,或者说,它们指向a数组序号为i的元素。
(3)*(p+i)或*(a+i)是p+i或a+i所指向的数组元素,即a[i]。
(4)如果指针变量p1和p2都指向同一数组,如执行p2-p1,结果是p2-p1的值(两个地址之差)除以数组元素的长度。
注意:两个地址不能相加。
3.3通过指针引用数组元素
引用一个数组元素,可以用下面两种方法:
(1)下标法,如 a[i]形式;
(2)指针法,如*(a+i)或*(p+i)。其中a是数组名,p是指向数组元素的指针变量,其初值 p=a。
如果不用p变化的方法而用数组名a变化的方法(例如,用a++)是不行的。因为数组名a代表数组首元素的地址,它是一个指针型常量,它的值在程序运行过程期间是固定不变的。既然a是常量,所以a++是无法实现的。
3.4、用数组名做函数参数
3.5、通过指针应用多维数组
4.通过指针引用字符串4.1字符串的引用方式
在C程序中,字符串是存放在字符数组中的。想引用一个字符串,可以用以
下两种方法。
(1)用字符数组存放一个字符串,可以通过数组名和下标引用字符串中一个字符,也可以通过数组名和格式声明“%s”输出该字符串。
(2)用字符指针变量指向一个字符串常量,通过字符指针变量引用字符串常
量。
分析定义string的行:
char *string =“l love China ! ”;等价于
char *string;
string =“l love China !”;可以对指针变量进行再赋值。
4.2字符指针作函数参数
如果想把一个字符串从一个函数“传递”到另一个函数,可以用地址传递的办法,即用字符数组名作参数,也可以用字符指针变量作参数。在被调用的函数中可以改变字符串的内容,在主调函数中可以引用改变后的字符串。
4.3使用字符指针变量和字符串的比较
(1)字符数组由若干个元素组成,每个元素中放一个字符,而字符指针变量中存放的是地址(字符串第一个字符的地址),绝不是将字符串放到字符指针变量中。
(2)赋值方式。可以对字符指针变量赋值,但不能对数组名赋值。(3)初始化的含义。对字符指针变量赋初值:
char *a=“l love China ! ”;等价于
char *a ;
a=“I love China ! ”;而对数组的初始化:
char str[14]=“l love China! ”;不等价于
char str[14];
str[]=“l love China!”;
数组可以在定义时对各元素赋初值,但不能用赋值语句对字符数组中全部元素整体赋值。
(4)存储单元的内容。编译时为字符数组分配若干存储单元,以存放各元素的值,而对指针变量,只分配一个存储单元。
(5)指针变量的值是可以改变的,而数组名代表一个固定的值(数组首元素的地址),不能改变。
(6)字符数组中各元素的值是可以改变的(可以对它们再赋值),但字符指针变量指向的字符串常量中的内容是不可以被取代的(不能对它们再赋值)。
(7)引用数组元素。对字符数组可以用下标法(用数组名和下标)引用一个数组元素(如a[5]),也可以用地址法(如*(a+5))引用数组元素a[5]。如果定义了字符指针变量p,并使它指向数组a 的首地址,则可以用指针变量带下标的形式引用数组元素(如p[5]),同样,可以用地址法(如*(p+5))引用数组元素a[5]。
八、宏定义(参考文章
C语言中宏定义的使用_冷冷的那一风的博客-CSDN博客_宏定义)
1. 引言
==预处理==命令可以改变程序设计环境,提高编程效率,它们并不是 C 语言本身的组成部分,不能直接对 它们进行编译,必须在对程序进行编译之前,先对程序中这些特殊的命令进行“预处理” 。经过预处理后,程序就不再包括预处理命令了,最后再由编译程序对==预处理==之后的源程序进行==编译==处理,得到可供执行的目标代码。C 语言提供的预处理功能有三种,分别为==宏定义==、文件包含和条件编译。
1.1 宏定义的基本语法
宏定义在 C 语言源程序中允许用一个标识符来表示一个==字符串==,称为“==宏/宏体==” ,被定义为“宏”的==标识符==称为“==宏名==”。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为“==宏替换==”或“==宏展开==”。 宏定义是由源程序中的宏定义命令完成的,宏代换是由预处理程序自动完成的。
在 C 语言中,宏分为 有参数和无参数两种。无参宏的宏名后不带参数,其定义的一般形式为:
#define 标识符 字符串
#表示这是一条预处理命令(在C语言中凡是以#开头的均为预处理命令)
==define #117411==为宏定义命令
==标识符 #800023==为所定义的宏名,
==字符串 #800019==可以是常数、表达式、格式串等。符号常量
// 不带参数的宏定义
#define MAX 10
/*带参宏定义*/
#define M(y) y*y+3*y
/*宏调用*/
1.2 宏定义的优点
方便程序的修改
使用简单宏定义可用宏代替一个在程序中经常使用的常量,这样在将该常量改变时,不用对整个程序进行修改,只修改宏定义的字符串即可,而且当常量比较长时, 我们可以用较短的有意义的标识符来写程序,这样更方便一些。
相对于==全局变量==两者的区别如下:
1. 宏定义在编译期间即会使用并替换,而全局变量要到运行时才可以。
2. 宏定义的只是一段字符,在编译的时候被替换到引用的位置。在运行中是没有宏定义的概念的。而变量在运行时要为其分配内存。
3. 宏定义不可以被赋值,即其值一旦定义不可修改,而变量在运行过程中可以被修改。
4. 宏定义只有在定义所在文件,或引用所在文件的其它文件中使用。 而全局变量可以在工程所有文件中使用,只要再使用前加一个声明就可以了。换句话说,宏定义不需要extern。
提高程序的运行效率
使用带参数的宏定义可完成函数调用的功能,又能减少系统开销,提高运行效率。正如C语言中所讲,函数的使用可以使程序更加模块化,便于组织,而且可重复利用,但在发生==函数调用 #800023==时,需要保留调用函数的现场,以便子 函数执行结束后能返回继续执行,同样在子函数执行完后要恢复调用函数的现场,这都需要一定的时间,如果子函数执行的操作比较多,这种转换时间开销可以忽 略,但如果子函数完成的功能比较少,甚至于只完成一点操作,如一个乘法语句的操作,则这部分转换开销就相对较大了,但使用带参数的宏定义就不会出现这个问 题,因为它是在预处理阶段即进行了宏展开,在执行时不需要转换,即在当地执行。宏定义可完成简单的操作,但复杂的操作还是要由函数调用来完成,而且宏定义所占用的目标代码空间相对较大。所以在使用时要依据具体情况来决定是否使用宏定义。
1.3 宏定义的缺点
由于是直接嵌入的,所以代码可能相对多一点;
嵌套定义过多可能会影响程序的可读性,而且很容易出错,不容易调试。
对带参的宏而言,由于是直接替换,并不会检查参数是否合法,存在安全隐患。
1.4 宏还是函数
宏函数,函数比较
==从时间上来看 #0c2ac0==
宏只占编译时间,函数调用则占用运行时间(分配单元,保存现场,值传递,返回),每次执行都要载入,所以执行相对宏会较慢。
使用宏次数多时,宏展开后源程序很长,因为每展开一次都使程序增长,但是执行起来比较快一点(这也不是绝对的,当有很多宏展开,目标文件很大,执行的时候运行时系统换页频繁,效率就会低下)。而函数调用不使源程序变长。
==从安全上来看 #0c2ac0==
函数调用时,先求出实参表达式的值,然后带入形参。而使用带参的宏只是进行简单的字符替换。
函数调用是在程序运行时处理的,分配临时的内存单元;而宏展开则是在编译时进行的,在展开时并不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。
对函数中的实参和形参都要定义类型,二者的类型要求一致,如不一致,应进行类型转换;而宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表,展开时带入指定的字符即可。宏定义时,字符串可以是任何类型的数据。
宏的定义很容易产生二义性,如:定义==#define S(a) (a)*(a)==,代码==S(a++)==,宏展开变成==(a++)*(a++)==这个大家都知道,在不同编译环境下会有不同结果。
调用函数只可得到一个返回值,且有返回类型,而宏没有返回值和返回类型,但是用宏可以设法得到几个结果。
函数体内有Bug,可以在函数体内打断点调试。如果宏体内有Bug,那么在执行的时候是不能对宏调试的,即不能深入到宏内部。
C++中宏不能访问对象的私有成员,但是成员函数就可以。
内联函数
在C99中引入了内联函数(==inline==),联函数和宏的区别在于,==宏是由预处理器对宏进行替代 #80001e==,而==内联函数是通过编译器控制来实现的 #80000f==。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的==展开==,所以取消了函数的参数压栈,减少了调用的开销。可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。
内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样了。
宏函数的适用范围
一般来说,用宏来代表简短的表达式比较合适。
在考虑效率的时候,可以考虑使用宏,或者内联函数。
还有一些任务根本无法用函数实现,但是用宏定义却很好实现。比如参数类型没法作为参数传递给函数,但是可以把参数类型传递给带参的宏。
2 、使用宏时的注意点
2.1 算符优先级问题
不仅宏体是纯文本替换,宏参数也是纯文本替换。有以下一段简单的宏,实现乘法:
#define MULTIPLY(x, y) x * y
==MULTIPLY(1, 2) #80000a==没问题,会正常展开成==1 * 2 #80000a==。有问题的是这种表达式==MULTIPLY(1+2, 3) #800019==,展开后成了==1+2 * 3 #80000f==,显然优先级错了。
对宏体和给引用的每个参数加括号,就能避免这问题。
#define MULTIPLY(x, y) ((x) * (y))
2.2 分号吞噬问题
有如下宏定义
#define foo(x) bar(x); baz(x)
假设你这样调用:
if (!feral)
foo(wolf);
这将被宏扩展为:
if (!feral)
bar(wolf);
baz(wolf);
==baz(wolf);==,不在判断条件中,显而易见,这是错误。
如果用大括号将其包起来依然会有问题,例如
#define foo(x) { bar(x); baz(x); }
if (!feral)
foo(wolf);
else
bin(wolf);
判断语言被扩展成:
if (!feral) {
bar(wolf);
baz(wolf);
}>>++;++<<
else
bin(wolf);
==else==将不会被执行
通过==do{…}while(0) #80001e==能够解决上述问题
#define foo(x) do{ bar(x); baz(x); }while(0)
if (!feral)
foo(wolf);
else
bin(wolf);
被扩展成:
#define foo(x) do{ bar(x); baz(x); }while(0)
if (!feral)
do{ bar(x); baz(x); }while(0);
else
bin(wolf);
使用do{…}while(0)构造后的宏定义不会受到大括号、分号等的影响,总是会按你期望的方式调用运行。
2.3 宏参数重复调用
有如下宏定义:
#define min(X, Y) ((X) < (Y) ? (X) : (Y))
当有如下调用时==next = min (x + y, foo (z));==,宏体被展开成==next = ((x + y) < (foo (z)) ? (x + y) : (foo (z)));==,可以看到,foo(z)有可能会被重复调用了两次,做了重复计算。更严重的是,如果foo是不可重入的(foo内修改了全局或静态变量),程序会产生逻辑错误。
2.4 对自身的递归引用
有如下宏定义:
#define foo (4 + foo)
按前的理解,==(4 + foo)==会展开成==(4 + (4 + foo))==,然后一直展开下去,直至内存耗尽。但是,预处理器采取的策略是只展开一次。也就是说,foo只会展开成==4 + foo==,而展开之后foo的含义就要根据上下文来确定了。
对于以下的交叉引用,宏体也只会展开一次。
#define x (4 + y)
#define y (2 * x)
x展开成(4 + y) -> (4 + (2 * x)),y展开成(2 * x) -> (2 * (4 + y))。
注意,这是极不推荐的写法,程序可读性极差。
3. 宏函数的集中特定语法
3.1 利用宏参数创建字符串:”#运算符”
在宏体中,如果宏参数前加个#,那么在宏体扩展的时候,宏参数会被扩展成字符串的形式。如:
#include <stdio.h>
#define PSQR(x) printf("the square of "#x" is %d.\n",((x)*(x)))
#define PSQR2(x) printf("the square of %s is %d.\n",#x,((x)*(x)))
int main() {
int R=5;
PSQR(R); //the square of R is 25.
PSQR2(R); // the square of R is 25.
return 0;
}
这种用法可以用在一些出错处理中
#include <stdio.h>
#define WARN_IF(EXPR)\
do {\
if (EXPR)\
fprintf(stderr, "Warning: EXPR \n");\
} while(0)
int main() {
int R=5;
WARN_IF(R>0);
return 0;
}
3.2 预处理器的粘合剂:”##运算符”
和#运算符一样,##运算符可以用于类函数宏的替换部分。另外,##还可以用于类对象宏的替换部分。这个运算符把两个语言符号组合成单个语言符号。例如
#define XNAME(n) x ## n
int x1=10;
XNAME(1)+=1; //x1 11
这个地方还需要再添加一个常用的用法
3.3 可变宏:… 和_VA_ARGS
有些函数(如==prinft() #06906d==)可以接受可变数量的参数。
int __cdecl printf(const char * __restrict__ _Format,...);
实现思想就是在宏定义中参数列表的最后一个参数作为省略号(三个句号)。这样,预定义宏_VA_ARGS就可以被用在替换部分中,以表明省略号代表什么,
例如
输出
#define PR(...) printf(__VA_ARGS_)
PR("Howdy");
PR("weight=%d,shipping=$%.2f.\n",wt,sp)
参数初始化
通过可以参数可以完成对多个参数的初始化,就像int数组的初始化那样
例如动态数组的添加
darray(int) arr=darray_new();
int *i;
darray_appends(arr, 0,1,2,3,4);
darray_foreach(i, arr)
{
printf("%d ", *i);
}