首页 > 代码库 > C语言中一些乱七八糟的用法与细节(不断更新)

C语言中一些乱七八糟的用法与细节(不断更新)

用C语言比较多,这篇是平时攒下的。有些内容在工作后可能会很常见,但是不用容易忘,所以就写篇博客吧。


一.printf的用法

%*可以用来跳过字符,可以用于未知缩进。像下面一样.

for(i = 1; i < 10; i++)
{
 printf("%*c\r%*c\n",  9 - abs(i - 5), ‘*‘, abs(i - 5) + 1, ‘*‘);
}

%[]可以用来读取指定的内容,%[^]可以用来忽略指定内容(正则表达式?)

%m可以不带参数,输出产生的错误信息


二.关于define的用法

#是将后面所跟随内容字符串化,##则是将前后内容连接起来

还有linux下各种多层宏定义。。。宏定义可谓博大精深啊。。


三.关于setjmp和longjmp

目前在linux多线程的头文件pthreadtypes.h中包含了此函数的头文件。

这个主要作用是用来从异常恢复的.也可以做"软重启".

因为setjmp把环境变量的设置和栈的值都保存在参数env数组中,当longjmp时,根据其值来恢复setjmp时的位置.

这个env通常是一个全局变量.毕竟这是要跨越函数的.


四.#include

其实这个东西可以写在任何的地方,你可以试试将main里面代码写在某个文件中,然后在main中包含这个文件。代码照样运行。


五.sizeof

当sizeof一个数组名的时候,如果数组和sizeof在同一个代码块中,那么返回数组长,否则返回指针大小。

(当数组名传递给函数的时候传递的仅仅是个指针)


六.volatile

让每次读取数据都从内存中读取,而不是在其他位置。可以防止部分编译器优化。在嵌入式中和多线程中有所应用。

最近学linux发现还可以用用来描述自己定义的锁!从而避免了编译器直接把锁优化了。


七.main函数的奇妙用法.

其实main函数是可以递归的。


八.关于||和&&

其实||的优先级比&&低。


九,如何让返回值有多重类型?

用union定义一个结构体,那么就可以选择返回多种类型了.


十.为何通常c中的指针大小为4个字节?

一种可能是CPU的地址线只有32根.另一种是由于最大程序限制在4G=2^32B来确定的.


十一.当函数返回double值的时候是80位的.

所以在返回值赋给一个double变量的时候会损失精度.

具体的可以看http://stackoverflow.com/questions/16888621/why-does-returning-a-floating-point-value-change-its-value


十二. __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  0-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++的时候是移动多个变量长度!其他指针只移动一个变量长度。这里的长度说的是存储变量地址的大小。前者指向一个指针变量的地址,而后者多用于指向一个二维数组的地址。所以传递二维数组参数的时候,用后者!


四十六。关于%模运算

其实是可以模负数的!但和数学定义不同,对于负数的模仍然带有符号。