运算符及其优先级
优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 | 优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 |
1 | [] | 数组下标 | 数组名[常量表达式] | 左到右 | 7 | == | 等于 | 表达式==表达式 | 左到右 |
() | 圆括号 | (表达式)/函数名(形参表) | != | 不等于 | 表达式!= 表达式 | ||||
. | 成员选择(对象) | 对象.成员名 | 8 | & | 按位与 | 表达式&表达式 | 左到右 | ||
-> | 成员选择(指针) | 对象指针->成员名 | 9 | ^ | 按位异或 | 表达式^表达式 | 左到右 | ||
2 | - | 负号运算符 | -表达式 | 右到左 | 10 | | | 按位或 | 表达式|表达式 | 左到右 |
(类型) | 强制类型转换 | (数据类型)表达式 | 11 | && | 逻辑与 | 表达式&&表达式 | 左到右 | ||
++ | 自增运算符 | ++变量名/变量名++ | 12 | || | 逻辑或 | 表达式||表达式 | 左到右 | ||
-- | 自减运算符 | --变量名/变量名-- | 13 | ?: | 条件运算符 | 表达式1? 表达式2: 表达式3 | 右到左 | ||
* | 取值运算符 | *指针变量 | 14 | = | 赋值运算符 | 变量=表达式 | 右到左 | ||
& | 取地址运算符 | &变量名 | /= | 除后赋值 | 变量/=表达式 | ||||
! | 逻辑非运算符 | !表达式 | *= | 乘后赋值 | 变量*=表达式 | ||||
~ | 按位取反运算符 | ~表达式 | %= | 取模后赋值 | 变量%=表达式 | ||||
sizeof | 长度运算符 | sizeof(表达式) | += | 加后赋值 | 变量+=表达式 | ||||
3 | / | 除 | 表达式/表达式 | 左到右 | -= | 减后赋值 | 变量-=表达式 | ||
* | 乘 | 表达式*表达式 | <<= | 左移后赋值 | 变量<<=表达式 | ||||
% | 余数(取模) | 整型表达式/整型表达式 | >>= | 右移后赋值 | 变量>>=表达式 | ||||
4 | + | 加 | 表达式+表达式 | 左到右 | &= | 按位与后赋值 | 变量&=表达式 | ||
- | 减 | 表达式-表达式 | ^= | 按位异或后赋值 | 变量^=表达式 | ||||
5 | << | 左移 | 变量<<表达式 | 左到右 | |= | 按位或后赋值 | 变量|=表达式 | ||
>> | 右移 | 变量>>表达式 | 15 | , | 逗号运算符 | 表达式,表达式,… | 左到右 | ||
6 | > | 大于 | 表达式>表达式 | 左到右 |
| ||||
>= | 大于等于 | 表达式>=表达式 |
| ||||||
< | 小于 | 表达式<表达式 |
| ||||||
<= | 小于等于 | 表达式<=表达式 |
|
数组和指针
一、指向一维数组元素的指针
int a[10], *p;
p=&a[0]; /* 与语句 p=a; 等价 */
此时p指向数组中的第0号元素,即a[0],*p就是*a,就是a[0]的值,*(a+i)就是a[i]的值。由于数组元素在内存中是连续存放的,根据地址运算规则,p+i和a+i都表示为a[i]的地址(即&a[i])。
二、二维数组元素的地址
为了说明问题,我们定义以下二维数组:int a[3][4]={{0,1,2,3},{4,5,6,7}, {8,9,10,11}};
二维数a也可这样来理解:数组a由三个元素组成:a[0],a[1],a[2],而每个元素又是一个一维数组,且都含有4个元素(相当于4列)。如图所示:
______ _______________
a ---- | a[0] | ---- | 0 | 1 | 2 | 3 | ------> (0x1000)
|______| |___|___|___|___|
| a[1] | ---- | 4 | 5 | 6 | 7 | ------> (0x1010)
|______| |___|___|___|___|
| a[2] | ---- | 8 | 9 | 10| 11| ------> (0x1020)
|______| |___|___|___|___|
但从二维数组的角度来看,a代表二维数组的首地址,当然也可看成是二维数组第0行的首地址,a+1就代表第1行的首地址,依次。如果此二维数组的首地址为0x1000,由于第0行有4个整型元素,所以a+1为0x1010。
既然我们把a[0],a[1],a[2]看成是一维数组名,可以认为它们分别代表它们所对应的数组的首地址,也就是讲a[0]代表第 0 行中第 0 列元素的地址,即&a[0][0],a[1]是第1行中第0列元素的地址,即&a[1][0],根据地址运算规则,a[0]+1即代表第0行第1列元素的地址,即&a[0][1],一般而言,a[i]+j即代表第i行第j列元素的地址,即&a[i][j]。
另外,在二维数组中我们还可用指针的形式来表示各元素的地址,如a[0]与*(a+0)等价,a[i]与*(a+i)等价,它表示数组元素a[i]的地址&a[i][0]。
而二维数组元素a[i][j]可表示成*(a[i]+j)或*(*(a+i)+j),或者写成(*(a+i))[j]。
三、指向一个由n个元素所组成的数组指针
数组指针用的比较少,但在处理二维数组时,还是很方便的。例如:
int (*p)[4]; /* 在数组指针的定义中,圆括号是不能少的,否则它是指针数组 */
int a[3][4];
p=a;
开始时p指向二维数组第0行,当进行p+1运算时,根据地址运算规则,此时放大因子为4x4=16,所以此时正好指向二维数组的第1行。和二维数组元素地址计算的规则一样,*p+1指向a[0][1],*(p+i)+j则指向数组元素a[i][j]。
四、指针数组
因为指针是变量,因此可设想用指向同一数据类型的指针来构成一个数组,这就是指针数组。数组中的每个元素都是指针变量,根据数组的定义,指针数组中每个元素都为指向同一数据类型的指针。
格式: 类型标识 *数组名[整型常量表达式];
例如: int *a[10];
以上指针数组中包含10个指针变量a[0],a[1],a[2],...,a[9],可以指向10个不同的地址。
指针函数和函数指针
一、指针函数
指针函数是指声明其返回值为一个指针的函数,实际上就是返回一个地址给调用函数。
格式: 类型说明符 *函数名(参数)
例如: void *GetDate(int ID);
二、函数指针
指向函数的指针包含了函数的地址,可以通过它来调用函数。
格式: 类型说明符 (*函数名)(参数)
例如: int (*fptr)(int ID);
其实这里不能称为函数名,应该叫做指针的变量名。这个特殊的指针指向一个返回整型值的函数。指针的声明笔削和它指向函数的声明保持一致。指针名和指针运算符外面的括号改变了默认的运算符优先级。如果没有圆括号,就变成了一个返回整型指针的函数的原型声明。
可以采用下面的形式定义函数指针数据类型:
typedef int (*T_MY_FUNC)(intID);/* 此时”T_MY_FUNC fptr;”等价于”int (*fptr)(int ID);”*/
可以采用下面的形式把函数的地址赋值给函数指针:
fptr=&Function; /* 或用“fptr=Function;”*/
可以采用下面的形式通过指针来调用函数:
(*fptr)(ID); /* 或用“fptr(ID); ”的格式,使用这种调用格式看上去与调用普通函数无异,因此使用前一种调用格式可以明确指出是通过指针而非函数名来调用函数的。*/
三、指针的指针
指针的指针用于指向指针的地址,它的声明有两个星号。例如:char **cp;
如果有三个星号,那就是指针的指针的指针,有四个星号那就是指针的指针的指针的指针,依次类推。
四、指向指针数组的指针
指针的指针另一用法是处理指针数组。有些程序员喜欢用指针数组来代替多维数组,一个常见的用法就是处理字符串。
char *Names[]=
{
"Bill",
"Sam",
"Jim",
0
};
main()
{
char **nm=Names; /* 定义一个指向指针数组的指针的指针 */
while(*nm!=0) printf("%s\n",*nm++);
}
可变参数的函数
下面是一个简单的可变参数的函数,该函数至少有一个整数参数,第二个参数也是整数,是可选的。
#include <stdarg.h>
void simple_va_fun(int i, ...)
{
va_list arg_ptr;
int j = 0;
va_start(arg_ptr, i); /*va在这里是可变参数(variable-argument)的意思 */
j=va_arg(arg_ptr, int);
va_end(arg_ptr);
printf("%d %d\n", i, j);
return;
}
从这个函数的实现可以看到,使用可变参数应该有以下步骤:
1) #include <stdarg.h>
2) 在函数里定义一个va_list型的变量,这里是arg_ptr,这个变量是指向参数的指针。
3) 用va_start宏初始化变量arg_ptr,该宏的第二个参数是第一个可变参数的前一参数,是一固定的参数。
4) 用va_arg返回可变的参数,并赋值给j。va_arg的第二个参数是要返回的参数的类型,这里是int型。
5) 最后用va_end宏结束可变参数的获取。
下面的例子可以进一步加深对可变参数的理解,该函数的效果与sprintf函数完全相同:
void my_printf(char *buffer, const char *format, ...)
{
va_list arg_ptr;
va_start(arg_ptr, format);
vsprintf(buffer, format, arg_ptr); /*将arg_ptr按format格式打印到buffer中 */
va_end(arg_ptr);
}
位域的使用
位域的定义和位域变量的说明与结构定义相仿,其形式例如为:
struct bs
{
int a:8;
int b:2;
int :2 /* 无位域名,该2bit不能使用*/
int c:4;
};
对于位域的定义尚有以下几点说明:
1. 一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。
2. 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。
3. 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。
链 表
typedef struct student
{
int number;
int score;
struct student *next;
} STUDENT_LINK;
/**************建立一个有cnt个结点的链表,返回链表头****************/
STUDENT_LINK *link_creat(int cnt)
{
STUDENT_LINK *head =NULL; /* head始终指向链表头,用于返回*/
STUDENT_LINK*new_node; /* new_node始终指向新申请的节点 */
STUDENT_LINK *cur_node; /* cur_node始终指向当前操作的(也是最后的)节点 */
int n = 1;
while (n <= cnt)
{
printf("您正在创建节点%d:\n", n);
new_node =(STUDENT_LINK *)malloc(sizeof(STUDENT_LINK));
link_input(new_node);
if (n == 1) /* 判断是否为第一个结点,若是第1个结点则将head指向新节点 */
{
head = new_node;
}
else /* 若不是第1个结点则将最后的节点的next指向新节点 */
{
cur_node->next= new_node;
}
cur_node =new_node; /* 让cur_node始终指向当前的(也是最后的)节点 */
cur_node->next= NULL; /* 不要忘记让最后的节点的next指向NULL */
n++;
}
return(head);
}
/**********删除链表中number字段为num的结点,并返回链表头***************/
STUDENT_LINK *link_delete(STUDENT_LINK *head, int num)
{
STUDENT_LINK*cur_node; /* cur_node始终指向当前操作的节点*/
STUDENT_LINK*pre_node; /* pre_node始终指向当前操作的节点的上一个节点 */
if (head == NULL)
{
printf("这个链表是空的,请先建立一个链表.\n");
return(head);
}
cur_node = head;
while(cur_node->number!= num && cur_node->next != NULL)/* 当前节点不是要删除的也不是最后的节点 */
{
pre_node =cur_node;
cur_node =cur_node->next; /* 将cur_node向后移一个结点 */
}
if(cur_node->number == num) /* 在链表中找到了要删除的结点*/
{
if (cur_node ==head) /* 要删除的是头结点 */
{
head =cur_node->next;
}
else
{
pre_node->next= cur_node->next;
}
free(cur_node);
printf("学号为 %d 的节点已经从链表中删除.\n", num);
}
else /* 在链表中没有找到要删除的结点 */
{
printf("您想要删除的结点不在此链表中.\n");
}
return(head);
}
/************插入一个新结点到链表的最后,并返回链表头***************/
STUDENT_LINK *link_insert(STUDENT_LINK *head, STUDENT_LINK*node)
{
STUDENT_LINK*cur_node; /* cur_node始终指向当前操作的节点*/
if (head == NULL)
{
head = node;
}
else
{
cur_node = head;
while(cur_node->next!= NULL)
{
cur_node =cur_node->next; /* 将cur_node向后移一个结点 */
}
cur_node->next= node;
}
node->next = NULL; /*不要忘记让最后的节点的next指向NULL */
printf("这个节点已经插入当前链表的最后.\n");
return(head);
}
/**************用户输入一个节点****************/
void link_input(STUDENT_LINK *node)
{
……
}
/**************输出一个链表****************/
void link_output(STUDENT_LINK *head)
{
STUDENT_LINK*cur_node;
int cnt = 0;
cur_node = head;
printf("ID\t学号\t\t分数\n");
while(cur_node !=NULL)
{
printf("%d\t%d\t\t%d\n",++cnt, cur_node->number, cur_node->score);
cur_node =cur_node->next;
}
printf("合计有%d条记录\n", cnt);
}
/**************链表接口调用范例****************/
void link_main(void)
{
STUDENT_LINK *new_list= NULL;
STUDENT_LINK new_stud; /* 要插入的新结点 */
int cnt = 5;
int num = 100;
new_list =link_creat(cnt);
printf("您创建的列表为:\n");
link_output(new_list);
new_list =link_delete(new_list, num);
printf("删除结点后的列表为:\n");
link_output(new_list);
link_input(&new_stud);
new_list =link_insert(new_list, &new_stud);
printf("插入新结点后的列表为:\n");
link_output(new_list);
}
预处理
一、预处理过程和预处理指令
在C语言中,并没有任何内在的机制来完成如下一些功能:在编译时包含其他源文件、定义宏、根据条件决定编译时是否包含某些代码,要完成这些工作,就需要使用预处理程序。尽管在目前绝大多数编译器都包含了预处理程序,但通常认为它们是独立于编译器的。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行响应的转换,预处理过程还会删除程序中的注释和多余的空白字符。下面是部分预处理指令:
指令 用途 指令 用途
# 空指令,无任何效果 #include 包含一个源代码文件
#if 如果给定条件为真,则编译下面代码 #define 定义宏
#ifdef 如果宏已经定义,则编译下面代码 #undef 取消已定义的宏
#ifndef 如果宏没有定义,则编译下面代码
#elif 如果前面的#if给定条件不为真,且当前条件为真,则编译下面代码
#else 如果前面的#if给定条件不为真,则编译#else下面的代码
#endif 结束一个#if……#else条件编译块
二、预定义的宏名
ANSI标准说明了五个预定义的宏名。它们是:LgvLinux联盟
__LINE__:当前语句所在的行号LgvLinux联盟
__FILE__:当前语句所在文件的文件名LgvLinux联盟
__DATE__:该宏指令含有形式为"月 日 年"的串,表示源代码翻译到目标代码的日期LgvLinux联盟
LgvLinux联盟__TIME__:该宏指令含有形式为"时:分:秒"的串,表示源代码翻译到目标代码的时间LgvLinux联盟
__STDC__:如果实现是标准的,则该宏含有十进制常量1,如果它含有任何其它数,则表示实现是非标准的LgvLinux联盟
注:如果编译不是标准的,则可能仅支持以上宏名中的几个,或都不支持,但也许还提供其它预定义的宏名。
三、#运算符
#的功能是将其后面的宏参数进行字符串化操作(Stringfication),简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号,有时把这种用法的#称为字符串化运算符。比如:
#define WARN_IF(EXP) \
if (EXP) \
fprintf(stderr, "Warning: " #EXP ".\n");
那么语句 WARN_IF(divider == 0) 将被替换为:
if (divider == 0)
fprintf(stderr,"Warning: ""divider == 0"".\n"); /* 会打印出“Warning: divider == 0.” */
再比如:
#define PASTE(n) "adhfkj"#n
printf("%s\n",PASTE(15)); /* 宏定义中的#运算符告诉预处理程序,把源代码中任何传递给该宏的参数转换成一个字符串。所以输出应该是adhfkj15。 */
四、##运算符
##被称为连接符(concatenator),##运算符用于把参数连接到一起。预处理程序把出现在##两侧的参数合并成一个符号。先看一个简单的例子:
#define NUM(a,b,c) a##b##c
#define STR(a,b,c) a ## b ## c /* ##前后可以加空格 */
printf("%d\n",NUM(1,2,3));
printf("%s\n",STR("aa","bb","cc"));
最后程序的输出为:
123
aabbcc
再比如要做一个菜单项命令名和函数指针组成的结构体的数组,并且希望在函数名和菜单项命令名之间有直观的、名字上的关系,那么下面的代码就非常实用:
struct command
{
char *name; /*菜单项命令名 */
void (*function)(void); /* 命令名对应的函数指针 */
};
#define COMMAND(NAME) { #NAME, NAME ## _command }
struct command my_commands[] = {
COMMAND(quit), /*相当于 {"quit",quit_command} */
COMMAND(help) /*相当于 {"help", help_command} */
};
五、#error指令
#error指令用于程序的调试,当编译中遇到#error指令就停止编译,并显示相应的出错信息。
#error命令的基本形式为:#error 出错信息
六、#line指令
命令#line主要用于调试及其它特殊应用,#line改变__LINE__与__FILE__的内容,它们是在编译程序中预先定义的标识符。LgvLinux联盟
#line命令的基本形式为:LgvLinux联盟#line number["filename"]LgvLinux联盟
其中的数字为任何正整数,可选的文件名为任意有效文件标识符,行号为源程序中当前行号,文件名为源文件的名字。LgvLinux联盟例如,下面的行计数从100开始,printf()语句输出为102,因为它是语句#line 100后的第3行。LgvLinux联盟
#line 100 /* 初始化行计数器* /LgvLinux联盟
main ( ) /* 行号100 */LgvLinux联盟
{ /* 行号101 */LgvLinux联盟
printf("%d\n",__line__); /* 行号102 */LgvLinux联盟
}
七、#pragma指令
在所有的预处理指令中,#pragma指令可能是最复杂的了,它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作。#pragma指令对每个编译器给出了一个方法,在保持与C和C++语言完全兼容的情况下,给出主机或操作系统专有的特征。依据定义,编译指示是机器或操作系统专有的,且对于每个编译器都是不同的。
其格式一般为: #pragmapara
其中para为参数,下面来看一些常用的参数。
l #pragma message(“_X86macro activated!”)
当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来,这对于源代码信息的控制是非常重要的。
当我们在程序中定义了许多宏来控制源代码版本的时候,我们自己有可能都会忘记有没有正确的设置这些宏,此时我们可以用这条指令在编译的时候就进行检查。
l #pragma comment(lib, "xxx.lib")
导入lib。
l #pragma code_seg(["section-name"[,"section-class"]])
它能够设置程序中函数代码存放的代码段,当我们开发驱动程序的时候就会使用到它。
l #pragma once
只要在头文件的最开始加入这条指令就能够保证头文件被编译一次,这条指令实际上在VC6中就已经有了,但是考虑到兼容性并没有太多的使用它。
l #pragma hdrstop
表示预编译头文件到此为止,后面的头文件不进行预编译。BCB可以预编译头文件以加快链接的速度,但如果所有头文件都进行预编译又可能占太多磁盘空间,所以使用这个选项排除一些头文件。
l #pragma startup
有时单元之间有依赖关系,比如单元A依赖单元B,所以单元B要先于单元A编译。可以用#pragma startup指定编译优先级,如果使用了#pragmapackage(smart_init) ,BCB就会根据优先级的大小先后编译。
l pragma resource"*.dfm"
表示把*.dfm文件中的资源加入工程。*.dfm中包括窗体外观的定义。
l #pragma warning(disable:4507 34; once: 4385; error: 164)
#pragma warning(disable:4507 34; once: 4385; error: 164) 等价于:
#pragma warning(disable:4507 34) // 不显示4507和34号警告信息
#pragma warning(once:4385) // 4385号警告信息仅报告一次
#pragma warning(error:164) // 把164号警告信息作为一个错误。
同时这个pragma warning 也支持如下格式:
#pragma warning(push[, n])
#pragma warning(pop)
这里n代表一个警告等级(1---4)。
#pragma warning(push):保存所有警告信息的现有的警告状态。
#pragma warning(push, n):保存所有警告信息的现有的警告状态,并且把全局警告等级设定为n。
#pragma warning(pop):向栈中弹出最后一个警告信息,取消在入栈和出栈之间所作的一切改动。
l #pragma argsused
如果没有在函数内部使用某个参数,编译时会报告"Parametername is never used in function func-name",使用#pragmaargsused,编译时就不再警告了。VC++不支持这条指令。
l #pragmapack
#pragma pack(n),C编译器将按照n个字节对齐,n必须是1,2,4,8,16,32,64等值。
#pragma pack(),取消自定义字节对齐方式。
#pragma pack规定的对齐长度,实际使用的规则是:
结构,联合,或者类的数据成员,第一个放在偏移为0的地方,以后每个数据成员的对齐,按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。也就是说,当#pragma pack的值等于或超过所有数据成员长度的时候,这个值的大小将不产生任何效果。而结构整体的对齐,则按照结构体中最大的数据成员和#pragma pack指定值之间较小的那个进行。比如:
#pragma pack(2)
struct Test
{
char a; //放在偏移[0]的位置。
int b; //自身长为4,大于2,故按2字节对齐,放在[2,5]偏移的位置,
char c; //自身长为1,小于2,故按1字节对齐,所以放在偏移[6]的位置。
int d; //自身长为4,大于2,故按2字节对齐,放在[8,11]的位置。
char e; //自身长为1,小于2,故按1字节对齐,所以放在偏移[12]的位置。
};
这个结构体实际占据的内存空间是13字节,但结构体之间的对齐是按照结构体内部最大的成员的长度和#pragma pack规定的值之中较小的一个对齐的,所以这个例子中,结构体之间对齐的长度是min(sizeof(int), 2),也就是2,所以sizeof(struct Test)是14。
如果上例中使用#pragma pack(4)代替#pragma pack(2),则
char a; //放在偏移[0]的位置。
int b; //自身长为4,故按4字节对齐,放在[4,7]偏移的位置,
char c; //自身长为1,小于4,故按1字节对齐,所以放在偏移[8]的位置。
int d; //自身长为4,故按4字节对齐,放在[12,15]的位置。
char e; //自身长为1,小于4,故按1字节对齐,所以放在偏移[16]的位置。
使用#pragma pack(4)后这个结构体实际占据的内存空间是17字节,结构体之间对齐的长度是min(sizeof(int), 4),也就是4,所以sizeof(struct Test)是20。