天地不仁,以万物为刍狗;圣人不仁,以百姓为刍狗。 ——《道德经》
游客 登录

linux kernel源码中的container_of宏定义

linux kernel源码中有一个神奇的container_of宏,可以根据一个结构体中成员的地址计算出结构体自身的地址:

#define offset_of(type, member) ((size_t) &((type*)0)->member)
/**
* container_of - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
*/
#define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offset_of(type,member) );})

offset_of 的作用是求出结构体成员地址和结构体基地址之间的偏移,拿结构体成员的地址减去成员和结构体基地址之间的offset,就得到结构体的基地址了。

这个宏虽然神奇了一点,但仔细分析也不难理解。真正费解的是,为什么在container_of宏中定义了一个看起来毫无作用的临时变量__mptr,它仅仅把一个指针类型转换成指向常量的指针类型而已,完全可以这样定义container_of宏:

#define container_of(ptr, type, member) (type *)((char *)ptr - offset_of(type,member)) 

结果是完全一样的。

揣测一下这么写的可能原因:

1. linux kernel的开发者是认真严谨的程序员,定义__mptr是用来防止宏的副作用。container_of宏的第一个参数是指针变量,如果我们这样使用宏:container_of(p_member++, struct sample_struct, member_name),那么如果ptr在container_of宏中出现两次,那么ptr就自增了两次!多么危险的副作用!当然实际上container_of宏中ptr只出现了一次,但是认真严谨的程序员为了防止可能出现的可怕的副作用,习惯性的加上了这样一个常量。

2.linux kernel的开发者是认真严谨的程序员,定义__mptr是用来不小心对ptr指向内容的修改。虽然这个container_of宏里也没有对ptr所指的变量做任何修改,但是谁都不能确定宏里究竟会发生什么,认真严谨的程序员这样小心是完全有必要的。

3.linux kernel的开发者是高傲狭隘的程序员,定义__mptr是因为他们“可以定义__ptr”。typeof作为gcc的扩展特性之一,毫无疑问是灵活优越先进的;既然是先进的东西,用得着的时候要用,用不着的时候创造条件也要用。__mptr给了typeof一个绝好的露脸机会,所以无论如何是一定要定义的。

动态分配二维数组

c语言中其实是没有二维数组的,二维数组实际上是“数组的数组”,一个二维数组a[i][j]实际上解释为*(*(a+i)+j)。动态分配二维数组时,为了能够想以a[i][j]这种形式去使用,需要另外申请一个一维数组,用于记录二维数组各列的起始地址;相应的,之后释放空间也需要分别释放二维数组的空间和这个辅助的一维数组所占用的空间。

其实有一个巧妙的方案,可以把申请和释放二维数组操作做的和一维数组一样:

void** malloc_2d(int row, int column, int size){
    int index_size = row * sizeof(void *);
    void **ptr = (void **)malloc(index_size + row*column*size);
    if(!ptr)return NULL;
    char *base_data_ptr = (char *)ptr + row*sizeof(void *);
    for(int i=0; i

例如,申请和释放一个MxN的int数组:

int **a = (int **)malloc_2d(m, n, sizeof(int));
free(a);

over.

c++字节序的检测和转换

计算机从诞生时起就确定一个一个字节(byte)的大小是8bit,从此以后就没用变动过。一个机器的字长通常包含数个byte,在存储数据的方法上出现了大端点(big endian)和小端点(little endian)两种结构,前者如PowerPC和Sun Sparc,后者如Intel x86系列。大端点机使用机器字长的高字节存储数字逻辑编码的低字节,字节的屋里顺序和逻辑顺序相反;小端点机使用机器字长的高字节存储数字逻辑编码的高字节,字节的屋里顺序和逻辑顺序相同。TCP/IP等传输协议使用的都是大端点序。大端点机和小端点机实际上各有优点,《深入理解计算机系统》中写道:
   由于Little Endian提供了逻辑顺序与物理顺序的一致性,让编程者摆脱了不一致性所带来的困扰,C语言开发者可以无所顾忌的按照自己的意愿进行强制类型转换,所以现代体系结构几乎都支持Little Endian。但Big Endian也有其优点,尤其对于汇编程序员:他们对于任意长度的整数,总是可以通过判断Byte 0的bit-7来查看一个整数的正负;对于Little Endian则不得不首先知道当前整数的长度,然后查看最高byte的bit-7来判断其正负。对于这种情况,big endian的开发者可以写出非常高效的代码。
   两派的支持者争论不休,正像他们所支持名词(big endian和little endian)的典故所讲述的那样:Little Endian和Big Endian这两个名词来源于Jonathan Swift的《格利佛游记》其中交战的两个派别无法就应该从哪一端--小端还是大端--打开一个半熟的鸡蛋达成一致。:) 在那个时代,Swift是在讽刺英国和法国之间的持续冲突,Danny Cohen,一位网络协议的早期开创者,第一次使用这两个术语来指代字节顺序,后来这个术语被广泛接纳了。

然而这个差别却给跨平台的程序编写和不同平台主机间的通信带来了相当的困扰。在c/c++中使用强制类型转换,如int到char数组的转换在有些时候可以写出简洁高效的程序,但字节序的不同确实这种写法有些时候变得很困难,跨平台的程序,以及处理网络数据传输和文件数据转换年的时候,必须要考虑字节序不同的问题。其实在C++中检测和转换字节序并不困难,写出的程序可以多种多样,基本的思想却是相同的。
检测平台的Endian基本上都是用联合体union来实现的,在union中的数据占有的是最长的那个类型的长度,这样在union中加入不同字节长度的数据并将较长的那个赋值为1就可以判断了:
typedef union uEndianTest{
struct
{
   bool flittle_endian;
   bool fill[3];
};
long value;
}EndianTest;
static const EndianTest __Endian_Test__ = { (long)1 };
const bool platform_little_endian = __Endian_Test__.flittle_endian;

这样使用这个 platform_little_endian 就可以检测到当前平台是否是little_endian

Endian的转换则只要交换高低位的字节就行了,用与运算的效率较高,以32为数据为例:

inline void ConvertEndian32( LPVOID lpMem )
{
BYTE * p = (BYTE*)lpMem;
p[0] = p[0] ^ p[3];
p[3] = p[0] ^ p[3];
p[0] = p[0] ^ p[3];
p[1] = p[1] ^ p[2];
p[2] = p[1] ^ p[2];
p[1] = p[1] ^ p[2];
}

[IAQ] C/C++ 中的 Trigraph

[Note: IAQ 的意思是 Infrequently Answered Questions. -- end note]-Curvelet @smth

C/C++ 为了照顾老一辈无产阶级革命家,
他们的条件极其艰苦,
键盘上缺了很多键,
无法输入下面九个字符:

     # \ ^ [ ] { } | ~

因此推出了 trigraph,
简单的讲就是把上面的每个字符用其他三个字符来代替,
替换的规则如下:

#: ??=
\: ??/
^: ??'
[: ??(
]: ??)
{: ??<
}: ??>
|: ??!
~: ??-

比如说,下面这个 C++ 程序:

#include

int main() {
   std::cout << "[]" << std::endl;
}

可以改写成这样:

??=include

int main() ??<
   std::cout << "??(??)" << std::endl;
??>

将 trigraph 替换成对应的字符发生在预处理之前,
因此 trigraph 可以在源码中的任何位置都可以用,
包括字符串内,函数体开头,预处理指令等。

有人说,如果那我就是用一个字符串常量,其中包含了 "??=" 怎么办?
比如说就是要打印两个问号接一个等号,怎么办?
很简单,把它拆开写, 为了打印出 "??=",我们把它拆成 "??" "=" 即可,
因为 C/C++ 在处理字符串字面值的时候,会把相邻的多个字符串字面值合并成一个,
而这个合并操作发生在 trigraph 替换之后,
下面的程序就可以正确的打印出两个问号和一个等号:

#include

int main() {
   std::cout << "??" "=" << std::endl;
}

实测中,
VC 8 不给任何提示将 trigraph 替换成对应应字符,
GCC 4.0.3 则要求加上编译参数 -trigraphs 才会做相应转换。

虽然 GCC 的做法是不符合标准的,但是更加安全。
更详细的情况可以参考标准 2.3。

共4篇,第1/1页 首页 1 尾页