用C语言比较多,这篇是平时攒下的。有些内容在工作后可能会很常见,但是不用容易忘,所以就写篇博客吧。
一.printf的用法
%*可以用来跳过字符,可以用于未知缩进。像下面一样.
%[]可以用来读取指定的内容,%[^]可以用来忽略指定内容(正则表达式?)
%m可以不带参数,输出产生的错误信息
二.关于define的用法
#是将后面所跟随内容字符串化,##则是将前后内容连接起来
还有linux下各种多层宏定义。。。宏定义可谓博大精深啊。。
三.关于setjmp和longjmp
目前在linux多线程的头文件pthreadtypes.h中包含了此函数的头文件。
这个主要作用是用来从异常恢复的.也可以做"软重启".
因为setjmp把环境变量的设置和栈的值都保存在参数env数组中,当longjmp时,根据其值来恢复setjmp时的位置.
这个env通常是一个全局变量.毕竟这是要跨越函数的.
四.#include
其实这个东西可以写在任何的地方,你可以试试将main里面代码写在某个文件中,然后在main中包含这个文件。代码照样运行。
五.sizeof
当sizeof一个数组名的时候,如果数组和sizeof在同一个代码块中,那么返回数组长,否则返回指针大小。
(当数组名传递给函数的时候传递的仅仅是个指针)
六.volatile
防止编译器优化,让每次读取数据都从内存中读取,而不是在其他位置。可以防止部分编译器优化。在嵌入式中和多线程中有所应用。
最近学linux发现还可以用用来描述自己定义的锁!从而避免了编译器直接把锁优化了。
七.main函数的奇妙用法.
其实main函数是可以递归的。
并且.C文件中就一句main;也是可以编译过的..刚好连接器可以找到对象..
八.关于||和&&
其实||的优先级比&&低。
九,如何让返回值有多重类型?
用union定义一个结构体,那么就可以选择返回多种类型了.
十.为何通常c中的指针大小为4个字节?
一种可能是CPU的地址线只有32根.另一种是由于最大程序限制在4G=2^32B来确定的.
十一.当函数返回double值的时候是80位的.
所以在返回值赋给一个double变量的时候会损失精度.
十二. __cdecl关键字
这个关键字是用来定义函数参数入栈顺序从右向左的.并且我在VC上测试的时候发现是先计算再入栈.这个估计用的很少吧..
刚学AT&T的32位汇编,发现其实是用pushl来将参数逐一入栈,然后弹出传给函数的.估计加了__cdecl的话就会使用pushl的方式来传值吧.
貌似这样就只能是从右到左传参数给函数啊..嗯..还需学习..
十三.内存对齐
结构体和联合体中,比如struct{int a;char b; double c};那么这个结构体大小为16(32位机),不是13.这一点我经常忘掉..但是发现这个错误的时候又很容易想起来..
十四.位段
这个是1位1位的来分配的..(原来还以为是按字节来的..)
这个东西刚开始觉得似乎蛮有用.但是似乎用途只有在网络协议需要这么斤斤计较..
百度百科上说只能是unsigned和int类型指定,但是我在dev c++上写c程序,也可以用char类型.
并且这个后面:加的值不能超过原来的类型的大小.
考虑到存储器的对齐和效率应该应用不多.
十五.关于union的用法
其实这个可以用来显示一些不能见的内容..比方说将一个指针和一个int定义在一起..如果指针是不能见的,那么可以通过以int的形式来访问..
union a{
point A;
int b;
};
如果A不能直接访问,那么就可以用B来访问。也算是编译器的漏洞吧。
我记得C++好像有一种类型指针是这样的,不允许见到其值.但是可以用int来知道其值.
原先刷题还遇到过一个问题.VC不能用long long类型的于是想用union{char a[8];int a}.这样来变成long long的..结果printf貌似只能识别到32位长度的大小..
所以还是老老实实去linux下编译了...
十六.关于typedef与const联合使用时一个值得注意的地方
typedef char* zifu;
const zifu a;
那么a是个什么样的指针呢?是不能改变所指向地址的值还是不能改变自身?
这个问题我一开是也想错了...想成用define的效果了...其实上面的定义等价于
char* const a;
也就是说不能改变指针自身,但是可以改变所指向的值..typedef与define相比会扩展一些内容.
十七.关于typedef的其他容易忘记的方法.
函数指针 typedef int (*)(char ) a;
那么 a c; c就是一个返回值为int,参数为char类型的函数指针.
避免弄成int (*)(*f)(int ,char)(char)这种一团麻乱的样子..很容易就看错的..
申请数组 typedef int int_shuzu[10];
那么int_shuzu a; a就是一个数组指针了..这种很容易看不懂的..
十八。关于参数传递
void fun(a,b,c)
int a,b,c;
{}
你很可能没见过这种传值方式,不过它确实是可以编译成功的。可以少写几个int~
十九.assert
C的异常处理宏。最近才发现有这个东西。这个比try--catch简单多了。用于判断某个条件是否满足,不满足的话跳转到自己的函数上来显示信息。用来调试程序很方便的。建议学习~
二十.关于如何把递归用函数指针来代替的方式.
这个方法很奇葩..感觉还是能有点用的.
#include <stdio.h>
typedef int (*PFUN)(int);
PFUN ptr[2];
int end(int a){
return 0;
}
int sum(int a){
return a + ptr[!!a](a - 1);
}
int main(){
int T, m;
scanf("%d", &T);
while(T--){
ptr[0] = end;
ptr[1] = sum;
scanf("%d", &m);
printf("%d\n", sum(m));
}
return 0;
}
这是通过函数指针加位运算来模仿递归...目标是求1+2+...+n..
两次逻辑取反刚好把大于1的数转为1,原来为零仍然为零.
这个技巧就当作好玩吧~~
二十一:字符串常量
sizeof( 'a' );你猜猜是几?结果是4!!!!这是C里面估计很少人注意到的差别..当然也没什么用就是了..(C++里面是1,但是C++还有'aa'这种单引号里面两个字符的情况..结果是4)
二十二.强制转换
这是我看一个opencv程序中发现的。你可能永远都只将强制转换用于赋值符号的右边,但其实在左边也是可以的.
就向这样
((float*)(img->imageData + i* img->widthStep))[j]=mapp[i][j];
二十三。重定向调试
freopen("文件名","r",stdin);写在所有从输入流读取信息的函数之前。
将标准输入重定向为文件中的内容。对于那些喜欢刷ACM的人来说,用了这个函数调试真是方便多了啊。
对于stdin这个是操作系统默认分给C程序的3个流指针之一,在linux下貌似文件描述符号0就是指stdin。当然还有1和2,分别是stdout,stderr。
三个指针定义在stdio.h中。
二十四。关于指针与int类型
其实int类型可以赋值给指针类型!!
以前一直记得是必须加强制转换的,但是在一本书上看到居然可以直接这样!!
只是给个warming!!这对于底层程序员是多么方便的事情啊!!!
在dev c++和VC上测试通过。还可以换成short*,double*,long*~
int i;
char*p;
i=1354;
p = i;
二十五。关于宏函数与逗号表达式
最近看操作系统内核源码发现的。。
其实逗号表达式和宏是一对好基友啊~不仅让宏函数的功能更多,还可以让宏函数带上返回值!
并且和内联函数一样高效率!!
好比说交换a和b两个值并且返回它们的和的宏函数,可以这样写:
#define swap(a,b,c) (c=a,a=b,b=c,a+b)
这样只用保证abc三个变量都是同一类型即可,就不用写那么多函数了~~
是不是有点类似C++的模版啊~~~还提高了不少开发效率呢~~~
由于是宏函数,所以就无法用于下面的回调函数中了,不过宏函数本身就这么方便和高效,这点缺点还是可以容忍的.
二十六.回调函数
原来一直用qsort,但是不知道还有回调函数这一名字.
这就有点像C++中的泛型算法了.可以把判断的部分通过函数封装起来.
将判断方法通过参数的方式传递给函数,然后实现泛型~~
void qsort(void *base,int nelem,int width,int (*fcmp)(const void *,const void *));
注意最后一个参数就是排序的条件判断函数,也就是回调函数。
类似的还有多线程中指定的入口函数。
二十七.关于_exit和exit的差别
_exit终止调用进程,但不关闭文件,不清除输出缓存,也不调用出口函数。
exit函数将终止调用进程。在退出程序之前,所有文件关闭,缓冲输出内容.
这实在其他地方直接抄来的.在linux下如果fork子进程来做点事的话可能会用到.
二十八.关于int a[0];的用法。
原来学c的时候弄过这个,但只是当作实验。。没想到还真的有人会用这个东西。
主要用来当作内存地址的标签用。可以通过编译器来记住指针地址,但是不会消耗实际内存。
只有当分配空间之后才能使用。这是得多么节约空间才会这么写啊。。。。
注意,这貌似是C99标准才支持的一个用法,并且gcc编译的时候需要带上-pedantic选项。
二十九.关于unsigned整数做循环变量
unsigned 的循环变量,判断条件 < -1相当于是小于最大的数= =。这一点千万别弄错了。
原来写单片机程序就被坑过..编译器虽然会警告,但是通常都被我忽略了...
记住,只要是for中的循环变量比较,要么全是有符号的,要么全是无符号的!!
三十.关于为什么要使用二进制读文件和写文件
这是学python的时候晓得的。。当你的文件保存为二进制的话,就可以用你的方法来解析文件。
如果不用二进制,那么就得看操作系统了。所以,对于保存独有的数据,多用二进制保存吧。话说数据库的数据文件都是这种方式读写文件的吧。
而且二进制也不容被人了解。。好多游戏的文件都是明文的呢。。。
三十一.sizeof(表达式)的问题
sizeof的表达式并不计算!!!只得出是什么类型的而已!!
int a=1;
printf("%d\n",sizeof(a=2));
printf("%d\n",a);
结果是4,1
测试环境dev c++
三十二.三目运算符与if/else哪个效率高?
个人在gcc下测试,发现汇编没有差别。不知道其他平台是否也是这样。当然我测试的方式可能不对。下文还会再说三目运算符的。
在CSAPP中说gcc会优化三目运算符。所以估计在某些特定情况下效率比if高点。
三十三.一种简单的宏交换
#define swap(a,b) (a ^= b^= a^= b)
多么简洁啊~虽然以前也写过这种异或交换,但是从来想过还能用连等啊~~
三十四.long double
这是C99标准中的新类型.在<深入理解计算机系统>里面说是扩展浮点数.有80个二进制位.
这80位刚好对应intel的CPU中的FPU.我的ATT汇编总结中讲过这个寄存器,原来以为只是浮点返回值和浮点运算中间过程用到.
现在看来还可以当作变量用.需要注意的是在gcc下一般是会按4字节对齐的,所以不是10个字节,而是12个字节.
三十五.关于<符号
这是看CSAPP上的.说<符号对于分支跳转指令来说预测的准确率并不是特别的好.
所以刚好想到c++中循环的条件判断用!=的习惯.虽然C++ primer上说是因为迭代器可能在循环中改变的原因,但是从这个来看还是养成写!=的习惯比较好.
可能得到的效率不一定提高特别多,但是对于高并发高吞吐量的系统来说,我猜这点细微的差别还是有变化的.
还是养成写!=的习惯吧~
三十六.关于三目运算符
一般情况下没什么问题,但是在某些编译器下,对于 xp?*xp:0这种xp是NULL指针的时候就不能用了,否则会造成内存错误!
这时因为编译器对三目运算符进行了优化。先算出后面两个表达式的结果,然后再来判断条件。
不过我用gcc测试了一下,发现gcc不是这样的,所以在gcc下这么用没什么问题。
以后见到这种情况别奇怪就是了~
三十七.关于宏函数中使用 #和##
之前一直不晓得这有什么实际用处,但是在描述路径的时候很有用.
好比说你之前文件在inc/现在改成在incc/下.那么完全可以写一个函数用来拼接
#define f(PATH,FILE) # x##y
这种形式.那么更换文件目录的时候就很方便了.
三十八.关于定义多个同名全局变量
在同一文件中是不能定义多个同名全局变量的,但是在多个文件中就可以。
CSAPP上写的.gcc的连接器在linking的时候额会把全局变量分为弱和强两种状态.并且优先选择强状态,但可能类型会存在问题
好比在A文件如下定义了x和y
void other();
int x=112233;
int y=332211;
int main(){
prntf("x=0x%x y=0x%x \n",x,y);
other();
prntf("x=0x%x y=0x%x \n",x,y);
}
在另一个文件中也定义了x,但类型是double
double x;
void other(){
x=-0.0;
}
然后gcc 两个文件。有一个警告。暂时先不管。
你运行了之后会发现,y的值居然变了!
也就是说,在other中,仍然认为x是double类型的!
double由于是8字节而int是4字节,给x赋值会覆盖原来的y!
表示如果other函数中换成纯粹的0.那么y就是0!
三十九.restrict关键字
这个常常忘记是什么意思...是C99新加的标准.让编译器翻译的时候,对其内存的操作只能通过其指针来完成.方便编译器优化.
不过C++里面好像还没有这个关键字.
四十.const与define定义一个常量的区别
const指定的是一个变量,而define相当于直接替换.
const是可以带指定类型的,define则是默认类型的.
同时const定义的变量还可以用取地址符&来得到其值.
四十一.关于GCC内联汇编
用一般的asm volatile()形式的话,在默认情况下是没什么问题的。但是如果在编译条件中加上了-std=c99的话则不行。
具体我也不清楚怎么搞。。没查到什么有用的东西。自己试过在__asm__volatile_和__asm volatile这些形式都不行。
有高手会的话还请指教!
四十二.关于GNU C的__attribute__
这个是GNU C的一个特色。可以设置函数,变量,以及类型的属性值。通常不用管这个东西,但是有一些软件有要求会需要用到这个。
具体我就不展开了,百度可以搜出很多内容。这里提一个__attribute__((aligned(n))).在结构体后面加上后可以按照n字节对齐。默认按最大变量字节对齐。
四十三.关于可变参数宏的实现(va_arg).
有一部分靠的是编译器支持"..."这种参数形式,然后保证不等长参数都可以完全入栈。实际上,根据C标准的入栈规则,参数是依次从右往左入栈的。只要顺着esp寄存器来找,那么就可以依次读出参数的其值。需要注意的是读出多少个字节,不然栈就乱了。所以必须在读出的时候加上参数类型。printf实现中也是先根据字符串中的类型再读的。我个人实测后确实如此~
测试结果如下,Linux下gcc编译。
如图所示,显然可以看到printf中实现的算法是先根据字符串对应的格式来从内存中读取的。也就是说如果类型选错了,内容显示一定乱了。
四十四。关于返回结构体
最近看编译器实现方面的东西,貌似C++那种传递性的使用(如cout<< xxx<<<xx;这种)应该是不难实现的。然后我就上测试了下,我发现GCC下函数返回结构体是可以直接在后面加上.成员来访问成员的。。结果如下
四十五。关于**指针和(*)[]指针
我发现很多人还是不清楚**指针和(*)[]指针的差别。第一个我就叫它双层指针,第二个叫行指针。有人喜欢把第一个叫二维指针吧,如果这么叫很容易晕。
实际上前者是一个指向指针的指针,而后者是一个带长度的指针。前者可以指向后者。后者与一般指针的差别就是你p++的时候是移动多个变量长度!其他指针只移动一个变量长度。这里的长度说的是存储变量地址的大小。前者指向一个指针变量的地址,而后者多用于指向一个二维数组的地址。所以传递二维数组参数的时候,用后者!
四十六。关于数组名是指针
这里要注意一点。对数组名取地址,那么得到的仍然是数组的地址值!而不是想当然的二级指针。也就是说加不加取地址符,其值都一样。但两者+1的值不同,前者是对指针增加元素长度,而&之后则成了行指针。
int a[50];
<pre name="code" class="cpp">printf("%d\n",a);
printf("%d\n",a+1);printf("%d\n",&a+1);
我在gcc下测试下a的地址后三位为656,a+1为660,&a+1则是856.
四十七。关于%模运算
其实是可以模负数的!但和数学定义不同,对于负数的模仍然带有符号。
四十七。关于作用域(do_div)
这个函数是我在调试我的内核的时候碰见的。发现很神奇。
#define do_div(n,base) ({ \
int __res; \
__asm__("divl %4":"=a" (n),"=d" (__res):"0" (n),"1" (0),"r" (base)); \
__res; })
意思是用n除以base。然后结果存入n,余数存入__res并返回!这个可以当函数用。在gcc下可以编译过。而且很神奇的是,外面有对()括号!替换到相应位置后就相当于是匿名函数了!还有哪些高级语言有的机制C语言也有呢,真是越学C越觉得博大精深啊!
再次测试,在VC6.0下无法通过。。
四十八。关于函数指针取地址
很多人不明白函数名为什么是个指针。。其实取了地址也是函数名的值,没有任何变化。所以别再在函数名前面加&了,看着很别扭。。
四十九。关于do while 0
在Linux内核的宏中很常见。用法感觉也蛮多的,百度也搜得到。我简单罗列一下我觉得比较靠谱的说法。
1.打包指令。相当于把其中的指令包起来,就可以在用的时候当作一条指令来用来了。
2.对于空的宏来说,可以避免编译器给出warming。
3.实际上可以当作函数了。毕竟可以在{}中定义变量。现在绝大部分都用inline吧。感觉也只能在老代码里面看见了。
五十:关于C语言中的typeof
注意是typeof.不是sizeof.主要用于强制转化.下面就用到了.
五十一. 关于0地址的使用方法
估计很多人就只认为NULL地址只能赋值给空指针吧.但其实还有另外的用途.在Linux内核中,数据结构中的指针是封装的.
我用的<Linux内核设计与实现上的例子>
struct list_head{
struct list_head *next;
struct list_head *pre;
};
struct fox{
unsigned long tail_length;
unsigned long weight;
bool is_fantastic;
struct list_head list;
};
#define constainer_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type, member) );})
也就是说,不是单独给数据结构分配个指针,而是封装好指针,塞进数据结构.
并且可以通过0地址+类型判断来得到其相对偏移.这样就可以通过指针地址+便宜来得到指针中的新地址了.
当然这样很麻烦了...注意link的位置很重要!
五十二.关于多个if和switch效率比较
switch会根据代码生成一张表,然后根据输入直接跳转到相应的代码区域..而if..else则挨个的判断..(取自CSAPP)
五十三.关于动态库函数重名
为什么动态库可以有重名函数呢?这是一位网友问我的问题,开始觉得不太可能,因为动态库函数应该有符号表来唯一确定每个函数的入口。
http://bbs.csdn.net/topics/350156218 这个是我搜出来的答案。gcc下,默认是把动态库的符号设置为全局的。加载一个动态库中的函数后,如果后面的动态库有重名的那么就会舍弃后面一个重名的函数。到这里我突然明白namespace 的意义了。gcc下用__attribute__ ((visibility("hidden")))来隐藏其标识,默认不是static 的全部导出到动态库。
五十三.__stdcall, __cdecl, __pascal, __fastcall,__thiscall的差别
__cdecl这个是C的标准调用标识。从右向左入栈,清栈操作由调用者来执行。并不限定参数个数与类型的完全匹配(好像实际用到的时候还是要匹配的。。)。
__stdcall这个是C++的标准调用标识。从右向左入栈,函数栈由被调用者来清除。严格匹配参数的调用。返回指令用的retnX,X表示栈指针移动长度。
__pascal这个简单来说就是和_cdecl的入栈和清栈方式相反。
__fastcall这个是用两个寄存器来保存参数。返回方式和_stdcall相同。(这个估计很)
__thiscall则是用在类成员中,可以要求把this指针放在指定的寄存器中传递。
C只能用cdecl。C++就可以用下面几个了。