C语言是现在大部分流行语言的源起,也是编写系统软件的不二之选,C语言的一些细节值得深入探究。
1 关于赋值运算符(普通变量和指针变量的赋值)
先看C语言代码和对应的汇编代码
7: int a = 5;
00401048 mov dword ptr [ebp-4],5
8: int* b = &a;
0040104F lea eax,[ebp-4]
int a = 5; 汇编指令是move,表示将5的二进制序列传送到a所对应的内存空间。
//代码效果参考:http://www.zidongmutanji.com/bxxx/217190.html
可以理解为int a ← 5,将值5赋给a。
再看指针变量的赋值:
int* b = &a;
汇编指令是lea,是Load effective address的缩写——取有效地址,也就是取偏移地址。
可以理解为 b → a,b指向a。
用结构体做单链表的结点时,这样理解更容易理解链表指针的移动:
struct pNode* p,p2;
p = p->next; //p指向p的下一个节点空间
p2 = p2->next->next; //p2指向p2的下一个的下一个节点空间
2 赋值与赋初值在效率上的一点差异
二者的区别在于:为变量赋值是通过赋值表达式在运行期间动态赋值,而为变量赋初值则是在定义变量的同时在编译时静态赋值。如对a进行赋值,对b进行赋初值,形式如下。
int a,b=3;
a=2;
为变量赋值占用的是运行时间,而为变量赋初值占用的是编译时间。
从上面的分析得知,变量不是一定要初始化的,也可以先进行定义,再进行赋值,这和初始化的效果是一样的。但是如果想提高运行效率,就得对变量进行初始化。
3 const的一点细节
编译器通常不为普通const只读变量分配存储空间,而是将它们保存在符号表中,这使它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也更高。
4 逗号作为运算符和分隔符
逗号在C语言中有时可以作为运算符來连接表达式,有时还可以作为分隔符,起到分隔的作用。那么,该如何区分逗号是运算符还是分隔符呢?
“,”作为分隔符主要用于以下情况:
(1) 变量声明时,使用逗号分隔多个变量名。
(2) 函数有多个参数时,用逗号分隔参数。
逗号运算符是C语言提供的一种特殊的运算符,用它可以将两个表达式连接起来。例如,“1+2,4+5”,称为逗号表达式,也称为“顺序求值运算符”。它的表达式一般形式为“表达式1,表达式2”,执行顺序是先计算表达式1的值,再计算表达式2的值。表达式也可以扩展为“表达式1,表达式2,表达式3,…,表达式n”。
c=(a=3,2a);
是将一个逗号表达式的值赋给变量c,第一个表达式a=3,第二个表达式2a,计算得到6,因此变量c的值是6。
代码d=a=b=3,2a;整个是一个逗号表达式,这里逗号表达式的值同样是2a的值,结果是6。但是变量d的值是在第一个表达式中计算得到的,得到值为3。
在使用逗号运算符的时候,要注意它的优先级和结合顺序。逗号运算符优先级最低,丼且是自左至右结合的。
5 自增运算的不同写法在效率上的一点区别
x = x + 1,x += 1,x++,这三个表达式哪个执行效率最高?
第一个表达式x=x+1的执行过程是先读取等号右边的x的地址,计算x + 1的值,然后读取等号左边的x的地址,最后将等号右边的值传给等号左边的值。
第二个表达式x+=1的执行过程是先读取等号右边的x的地址,然后计算x+1的值,最后将得到的值传给左边的x,因为x的地址已在前面读出,故省去了传值过程;
第三个表达式的执行过程是先读取x的地址,然后x自增1;
因此,x++的效率最高。
编译器优化后汇编代码应该是一样的。
从汇编来看前自增与后自增的区别:
//代码效果参考:http://www.zidongmutanji.com/bxxx/493706.html
7: a=c++;
004016AF mov eax,dword ptr [ebp-0Ch]
004016B2 mov dword ptr [ebp-4],eax
004016B5 mov ecx,dword ptr [ebp-0Ch]
004016B8 add ecx,1
004016BB mov dword ptr [ebp-0Ch],ecx
8: b=++c;
004016BE mov edx,dword ptr [ebp-0Ch]
004016C1 add edx,1
004016C4 mov dword ptr [ebp-0Ch],edx
004016C7 mov eax,dword ptr [ebp-0Ch]
004016CA mov dword ptr [ebp-8],eax
6 switch语句效率入口与出口
switch相对于if…else if,具有较高的运行效率。因为switch会在编译期建立一个跳转列表,运行时可以根据跳转列表进行直接跳转。
在switch语句中,当找到与swith表达式相等的case时,执行case下的语句。case下的所有语句都执行完成后,如果一直没有break,那么程序将会执行到下一个case,而不管它的值是否与switch表达式相等,即多个case之间不具有天然的互斥性。要想使程序执行完一个case后的语句,而不进入下一个case,必须使用break语句,使程序退出switch结构。这样,后面的case也就不执行了。
C语言一个分支的结束是依赖break完成的。case只决定—序到哪里执行,而不决定到哪里结束。结束位置由break来决定,没有break就一直执行到switch完整结构结束。
所以,case是swithc语句的入口(特殊情况下也可以是default),而由break提供出口(特殊情况下是最后一条case语句的结束块符号“}”或return。
7 联合体变量赋初值
由于联合体变量具有所有成员共享一个内存地址的特点,因此为联合体变量赋初值时只能给该变量的第一个成员赋初值,其他成员不能赋初值。例如:
union number
{
int i;
char c;
float f;
}m={2};
8 数组名
数组名是一指针常量,所以不能被再次赋值。只能对其数据元素逐一操作。
数组名做函数参数时退化为指针,在函数体内不能得到其长度。
二维数组名并不能赋给一个二次指针,只能赋给一个数组指针,或通过强制类型转换,赋给一个一次指针:
//代码效果参考:http://www.zidongmutanji.com/bxxx/472102.html
int arr[5][5] = {1,2,3,4,5,6,7,8};
int (*pa)[5] = arr;
int *p = (int*)arr;
cout<<*pa[1]+2<<endl; // 8
cout<<*(p+7)<<endl; // 8
指针变量声明并指向目标地址时,由类型信息决定目标内存空间的字节长度,当其指向一个n维数组时,其长度信息为一个n-1维数组的字节长度,所以n维数组指针声明时需要显式指定除第一维以外的其它长度信息。
指针数组名在语法层面等价于一个二级指针。
9 内在管理函数及库中一些算法的参数和返回值的void类型
malloc、sort等函数的参数和返回值都使用void类型。原因是C是一门强类型语言,又没有类型模板机制来做泛型表示。
void指针只能单纯表示地址信息,其附加的类型信息不存在,所以其为一个不完全类型,只能用于声明和传址,当需要解引用或做指针算术运算时,其类型信息附加的字节长度信息不可或缺,所以需有具体类型的强制转换。