C语言学习的奇技淫巧.docx
《C语言学习的奇技淫巧.docx》由会员分享,可在线阅读,更多相关《C语言学习的奇技淫巧.docx(27页珍藏版)》请在冰豆网上搜索。
C语言学习的奇技淫巧
C语言学习tips--1sizeof与strlen的区别
sizeof是操作符,不是函数。
对于指针(char*a),sizeof操作符返回这个指针占的空间(8字节),而对于一个数组(chara[10]),sizeof返回这个数组的长度。
对于其它类型的变量,返回的是该变量占用的内存大小,如hs_xxx_tnum,sizeof(num)就是结构体类型hs_xxx_t的长度。
sizeof不能返回动态分配(malloc)的内存空间的大小。
strlen是函数,返回字符串长度,不包含\0
Char*b=“123456”;
Chara[10]=“123456”;
Sizeof(a)=10;strlen(a)=6;
Sizeof(b)=8;strlen(b)=6;
但是在实际运用中,却出现下面的情况:
Char*c=malloc(200);
Chard[200]={0};
……
strlcpy(d,c,sizeof(c));
C语言学习tips--2数组名与对数组名取地址区别
今天说一下数组名与对数组名取地址的区别:
例子:
Char hs[]=“123456”;
hs hs+1
↓ ↓
1
2
3
4
5
6
\0
Hs的值是数组起始地址,类型为char*,*hs的值是hs[0],是一个char,hs+1,即从起始地址开始,偏移一个char的长度,就是sizeof(char)=1。
&hs &hs+1
↓ ↓
1
2
3
4
5
6
\0
&hs的值是也数组的地址地址,类型为(char[])*,(这个写法是不准确,但是可能这么理解吧,) *(&hs),是整个字符串”123456”。
&hs+1,即从起始地址开始,偏移一个hs[]的长度,也就是sizeof(hs)=7.
下面请看详细的情况:
1,以%s的方式打印,看结果
printf(“hs=%s,&hs=%s\n”,hs,&hs);
hs=123456,&hs=123456
两个值是一样的。
2,看两个指针的size
Printf(“sizeof(hs)=%u,sizeof(&hs)%u\n”,sizeof(hs),sizeof(&hs)); //
Sizof(hs)=7,sizeof(&hs)=8
类型不一样,hs的size表示数组hs的大小7。
&hs的size表示一个指针的长度,size为8
3,两个指针的地址,发现如下:
Printf(“hs=%p,&hs=%p\n”,hs,&hs);
hs=0x7ffff8ed17d0, &hs=0x7ffff8ed17d0,
地址是一样的。
4,看+1后的偏移
Printf(“hs+1=%p,&hs+1=%p\n”,hs+1,&hs+1);
Hs=0x7ffff8ed17d1,&hs=0x7ffff8ed17d7,
+1的区别,一个偏移是1,一个偏移是7,即偏移字符串的size。
5,以%s的方式打印指针+1
最后,printf(“hs+1 =%s,&hs+1=%s\n”,hs+1,&hs+1);
hs+1=23456,&hs+1= (空,没有打印任何值)
这个区别曾经引起过bug,原因如下:
某骚年以为hs是char*,那&hs就自然是char**了。
于是写程序时,出现
Char hs[]=“123456”;
Char**b=&hs;这里,因为编译可能会出错,于是某少年就写成char**b=(char**)&hs,
Printf(“%s\n”,*b);
运动时,果断crash。
分析:
根据上面第3步,可以看到hs,与&hs的地址是一样的,于是
Char**b=(char**)&hs=(char**)0x7ffff8ed17d0=(char**)hs。
*b=123456;因为*b还是指针,所以它的%p的形式是0x363534333231(就是123456的二进制形式),表示一个地址。
使用%s打印,需要读取这个地址的内容(*(char*)0x363534333231),而这个地址0x363534333231显然是一个坑爹的值,于是crash就出来了。
解决方法:
如果你想达到你的目的,可以使用如下方法:
Char*x=hs;
Char**b=&x;
然后printf(“*b=%s\n”,*b);
因为&x,是取变量x的地址,而不是取hs的地址&hs。
*b就是x。
C语言学习tips--3根据结构体中的成员地址,求结构体的地址
Typedefstruct{
inta;
intb;
intmember;
}type;
现在有:
type test;
type*ptest;
并且已经test.member的地址为ptr,即,ptr=&test.member,但是并不知道test的地址,求test的地址ptest。
显然ptest=ptr–member在成员中的偏移(上述例子中为8)。
但是偏移要怎么求?
具体要怎么写?
下面请看经典代码中的写法:
ptest=container_of(ptr,type,member);
根据结构体中的成员地址,求结构体的地址:
#definecontainer_of(ptr,type,member)({
consttypeof(((type*)0)->member)*__mptr=(ptr);
(type*)((char*)__mptr-offsetof(type,member));})
#defineoffsetof(TYPE,MEMBER)((size_t)&((TYPE*)0)->MEMBER)
对上面的定义,分析如下:
1,先看offsetof
#defineoffsetof(TYPE,MEMBER)((size_t)&((TYPE*)0)->MEMBER)的意义
((type*)0)为设计一个type类型的结构体,起始地址为0.
于是它的member就是((type*)0)->member;
member的地址=结构体((type*)0)的起始的地址+member的偏移。
&((type*)0)->member=((type*)0)+offset
于是,member在结构体中的偏移offset的计算方法如下:
offset=&((type*)0)->member-((type*)0)
由于结构体起始地址为0,所以此结构体成员变量的偏移地址就等于其成员变量在结构体内的距离结构体开始部分的偏移量。
offset=&((type*)0)->member-((type*)0)=&((type*)0)->member;
即:
&((type*)0)->member就是取出其成员变量的偏移offset,#defineoffsetof将其转换为size_t类型。
2,再看container_of
consttypeof(((type*)0)->member)*__mptr=(ptr);
typeof(((type*)0)->member)为取出member成员的变量类型。
再用其定义__mptr指针。
ptr为指向该成员变量的指针。
__mptr为member数据类型的常量指针,其指向ptr所指向的变量处。
3,(type*)((char*)__mptr-offsetof(type,member))
(char*)__mptr转换为字节型指针。
根据上面的分析ptest=ptr–member在成员中的偏移
要求的地址,假设为X,就有X=(char*)__mptr-offsetof(type,member)),但此是X还是char*,需要强制类型转换。
(type*)X=(type*)(char*)__mptr-offsetof(type,member));
于是type*ptest=(type*)X=(type*)(char*)__mptr-offsetof(type,member));
C语言学习tips--4关于宏的一些奇技淫巧
(1)
这就是从结构体某成员变量指针来求出该结构体的首指针。
指针类型从结构体某成员变量类型转换为该结构体类型
今天就说说关于宏定义中的#号
1,单#
将#后的内容字符串化
#defineHILL(a)(#a)
Printf(“%s\n”,HILL(123));
打印结果是123
这个时候,你是否会突发奇想:
这样就可以把数字转化为字符串了,不用使用传说中的系统函数了。
然后你是否想尝试一下。
Inta=123;
printf(“a=%s\n”,HILL(a));
然后就是见证奇迹的时刻了:
a=a;
是不是有一种坑爹的感觉?
这个东西这么神奇,如果你不想被我坑的话,自己动手试试吧,或许你会有一些碉堡的想法……
2,双#
将##后面的字符拼接,可以理解为用一个宏帮你敲代码,敲出来的结果,就是宏展开的结果,可以是变量名,函数名,关键字什么的。
但是,宏的参数却不能是变量,或者字符串。
HILL(a,b) (a##b)
例子1:
当程序中使用HILL(1,2)
使用printf(“%d\n”,HILL(1,2));
打印的结果是12
例子2:
Voidfun()
{
printf(“fun:
hello\n”);
}
Intmain()
{
HILL(f,un)();
Return0;
}
可以看到程序的结果是fun:
hello
HILL(a,b)展开的结果是ab.
于是HILL(f,un)的结果就是fun,后面再加上(),就得到fun()了。
是不是很神奇……
例子3:
Intc=12,d=3;
Intf=HILL(c,d);这里会是f=123吗?
HILL(“a”,“b”);这里会是“ab”吗?
试试,结果也是很……
上面两句是编译不通过的。
3,函数可变参数中的”##”
预编译处理,当可变参数的个数为0时,自动将前面的”,”删除。
请看下面的两个宏
#defineHILL_PRINT(a,b…) printf(a,b)
#defineSTONE_PRINT(a,b…) printf(a,##b);
如果是打印一个数字
Intb=10;
HILL_PRINT(“b=%d\n”,b)
STONE_PRINT(“b=%d\n”,b);
它们的结果是一样的b=10;
但是,假如可变参数的个数为0,即有如下形式:
HILL_PRINT(“hello”)
STONE_PRINT(“hello”)
这两种写法就不一样了
HILL_PRINT(“hello”)替换展开以后,就是printf(“hello”,);因此,这里是编译不通过的。
STONE_PRINT(“hello”)替换展开为printf(“hello”),逗号被##干掉了,所以没有”,”,可以编译通过。
因此,大家如果定义的宏当中,有printf这样带可变参数的时候,给可变参数加上##。
C语言学习tips--5关于宏的一些奇技淫巧
(2)
今天继续说宏,#undef的技巧。
假设你现在写代码,有以下一组数据
name from to
fliu hubei hebei
sssui Shandong shanghai
……
数据是静态,有限的,但是以后还可能继续添加,要求可扩展。
那么你会怎么样来设计数据结构呢?
又怎么样来保证可扩展性呢?
怎么样来保证易用易懂呢?
很快就可以想到以下结构
Typedefstruct{
char*name;
char*from;
char*to;
}member_s;
然后使用
Member_s ssuigroup[]={
{“fliu”, “ubei”, “hebei”},
{“sssui”, Shandong, “shanghai”},
{……},
……
}
后续添加的时候,也可以很好的添加进去。
但是有一个问题不好解决,比如说,我想知道当前这个数组的大小?
如果每次都去算一遍,效率会很低。
为了解决这个问题,#undef可以神奇的拿来用用了。
具体用法:
1,先定义一个ARRAY的宏
#defineMEMBER_ARRAY\
NAME_CONVERT(0, “fliu”, “hubei”, “hebei”),\
NAME_CONVERT(1, “sssui”, Shandong, “shanghai”),\
……
2,巧妙的定义NAME_CONVERT,来得到数组的长度,以及每个数据在ARRAY的索引。
#defineNAME_CONVERT(id,name,from,to) (id) //这里只有id
Enum{
MEMBER_ARRAY,
MAX_ID
}
在这上枚举的类型中,将宏MEMBER_ARRAY展开替换,则会得到
Enum{
NAME_CONVERT(0, “fliu”, “hubei”, “hebei”),\
NAME_CONVERT(1, “sssui”, “Shandong”, “shanghai”),\
……
MAX_ID
}
再将NAME_CONVERT展开替换,得到
Enum{
0,
1,
……
MAX_ID
}
说明:
正式写代码时,会将1,2,等数字用宏表示,比如FLIU,SSSUI
Enum{
FLIU,
SSSUI,
……
MAX_ID
}
这样,如果你要再添加一条信息,则在MEMBER_ARRAY数组中增加一行
NAME_CONVERT(x, “xxx”, “xxx”, “xxx”)
那么MAX_ID就自动加1了。
3,再次定义NAME_CONVERT,获取和使用宏ARRAY中的具体信息。
是就是要得到ID对应的数据。
#defineNAME_CONVERT(id,name,from,to) {name,from,to} 注意,这里没有id了,而是除id以外的数据。
然后使用上面的结构定义一个数组member_s ssuigroup[MAX_ID]={
MEMBER_ARRAY
}
这个MAX_ID是不是很眼熟?
不错,不要怀疑,它就是上面那个枚举中的MAX_ID。
然后将数组中的MEMBER_ARRAY展开,就可以得到
member_sssuigroup[MAX_ID]={
NAME_CONVERT(0, “fliu”, “hubei”, “hebei”),
NAME_CONVERT(1, “sssui”, “Shandong”, “shanghai”),
……
}
将NAME_CONVERT展开替换得:
member_sssuigroup[MAX_ID]={
{“fliu”, “hubei”, “hebei”},
{“sssui”, “Shandong”, “shanghai”},
……
}
这跟一开始想到的结构一样吗?
很一样吧,很神奇吧,震精了,有木有
哎,其实不一样,这里有明确数组大小的MAX_ID
结合第2上步,你可以使用sssuigroup[FLIU]去得到fliu的信息如下:
sssuigroup[FLIU]={“fliu”, “hubei”, “hebei”};
使用sssuigroup[FLIU]就表示flu的信息,是不是很直观呢?
而且,同样的,你在上面的MEMBER_ARRAY中加一行后,这个数组也自动的就多加一行了。
是不是很方便呢?
好了,到了这里,你有没有发现,讲了这么多,关于#undef的,一句都没有,#undef连酱油都没有打。
那么请注意,上面的NAME_CONVERT定义了两次,是有问题的。
要解决这个问题,只要在第2步和第3步之间,加上
#undefNAME_CONVERT
这句话的意思是解除宏定义……,
但是在#undef后,你仍然可以使用#define和#undef之间的,已经定义过的一些东东,比如那个MAX_ID,比如enum这个枚举类型。
只是不能再使用NAME_CONVERT定义数据,实现你在#define和#undef之间定义的功能,要实现新的功能,要再次定义NAME_CONVERT。
具体过程,把这句放到第3步之前,第2步之后。
再回头看下例子吧,或者自己动手试试
C语言学习tips--6关于宏的一些奇技淫巧(3)
今天讲讲为什么要用do……while(0)把多行语句的宏括起来
#defineIS_DNS_ENABLE(x)\
printf("hellohillstone\n");\
check_dns(x);
如果有以下语句
If(x) IS_DNS_ENABL(x);
else
……
展开后:
If(x)
printf(“hellohillstone\n”);
check_dns(x);;
else
……
If分支有两条语句,最后还多一个;号,但是没有用{}括起来,所以编译是会提示else出错。
就算是你把宏里的最后一个;去掉,还是有两条语句,编译出错。
当然,你会说,用编码规范,if分支自带两个{},就不会有这个问题了。
那么,是否所有人都会认真执行编码规范?
如果那天编码规范里把”强制使用{}”这一条去掉,大家都不用{}了,问题一样会有。
于是你可能会想,我自己在宏里用{},也行啊。
#defineIS_DNS_ENABLE(x){\
printf("hellohillstone/n");\
check_dns(x);\
}
仍然是这个宏,需要先说明,每写一条语句,最后有个;号,是我们的习惯。
If(x)
IS_DNS_ENABL(x);
else
……
展开后如下:
If(x)
{printf(“hellohillstone\n”);check_dns(x);};
else
……
最后有一个;号,又是;号问题,真是太淡腾了。
写成dowhile(0)又会是什么样子呢?
#defineIS_DNS_ENABLE(x)do{\
printf("hellohillstone/n");\
check_dns(x);\
}while(0)
注意,宏里,while(0)最后是没有;号的,
这也和我们定义复杂宏,最后没有;的习惯相同。
展开:
If(x)
do{printf(“hellohillstone\n”);check_dns(x);}while(0);
else
……
刚好,while(0)后面那个;是dowhile语句的一部分,不会对else造成影响。
C语言学习tips--7关于宏的一些奇技淫巧(4)
如果你想在debug信息中,将执行的文件名,行数,函数名称print出来,怎么搞呢?
C语言提供一些特殊的宏,
__LINE__ 当前语句所在的行号,10进制整数
__FILE__ 当前源文件的文件名,字符串常量
__DATE__ 程序被编译的日期,"Mmmddyyyy"格式的字符串.
__TIME__ 程序被编译的时间,"hh:
mm:
ss"格式的字符串,.
__FUNCTION__ 当前语句所在的函数,字符串。
__func__和__FUNCTION__一样。
可以在代码中用以下方式打印
intmain()
{
printf("Thefileis%s.\n",