C (c反应蛋白高说明什么)
本文目录导航:
C/C++最底层是怎么实现的
许多同学可能在学习C++的时候,都会感到一定的困惑,继承到底是怎样分配空间的,多态到底是如何完成的,许许多多的问题,必须挖掘到C++底层处理机制,才能搞明白。
有许多C程序员也并不认同C++,他们认为C++庞大又迟缓,其更重要的原因是,他们认为“C++是在你的背后做事情”。
的确,C++编译器背着程序员做了太多的事情,所以让很多不了解其底层机制的人感到困惑。
想成为一个优秀的程序员,那么这样的困惑就不应该存在,只有了解了底层实现模型,才能写出效率较高的代码,自信心也比较高。
我们先从一个简单但有趣的例子谈起。
有如下的4个类:class X {};class Y : public virtual X {};class Z : public virtual X {};class A : public Y, public Z {};上面的4个类中,没有任何一个类里含明显的数据,之间只是表示了继承关系,那么如果我们用sizeof 来测量它们的大小,将会得到什么结果呢?你可能会认为,既然没有任何数据和方法,大小当然为0,而结果肯定会出乎你的意料,即使是class X 的大小也不为0。
在不同的编译器上,将会得到不同的结果,而在我们现在最常用的VC++编译器上,将得到如下的结果:sizeof X 的结果是 1 。
sizeof Y 的结果是 4 。
sizeof Z 的结果是 4 。
sizeof A 的结果是 8 。
惊讶吗?那么为什么会得到这样的结果?让我们一个一个来分析。
对于一个空的class,事实上并不是空的,它有一个隐晦的1 byte ,那是被编译器安插进去的一个char 。
这使得这个class 的两个对象得以在内存中配置独一无二的地址,这就是为什么 sizeof X 的结果是 1。
那么Y和Z呢,怎么会占用 4 byte ?其实它们的大小受到三个因素的影响:1. 语言本身所造成的额外负担:当语言支持多态时,就会导致一些额外负担。
在派生类中,这个额外负担表现在一个指针上,它是用来指向一个被称作“虚函数列表”的表格。
而在VC++编译器上,指针的大小正好是 4 byte 。
2. 编译器对于特殊情况所提供的优化处理:在class Y 和 class Z 中,也将带上它们因为继承class X 而带来的 1 byte ,传统上它被放在派生类的固定部分的尾端。
而某些编译器(正如我们现在所讨论的VC++编译器)会对空的基类提供特殊的处理,在这个策略下,一个空的基类被视为派生类对象最开头的一部分,也就是说它并没有花费任何额外空间(因为既然有了成员,就不需要原本为了空类而安插一个char了),这样也就节省了这 1 byte 的空间。
事实上,如果某个编译器没有提供这种优化处理,你将发现class Y 和 class Z 的大小将是8 byte ,而不仅仅是5 byte 了,原因正如下面第3点所讲。
3. “对齐”(Alignment)机制:在大多数机器上,群聚的结构体大小都会受到alignment的限制,使它们能够更有效率地在内存中被存取。
Alignment 就是将数值调整到某数的整数倍。
在32位计算机上,通常alignment 为 4 byte(32位),以使总线达到最大的“吞吐量”,在这种情况下,如上面所说,如果 class Y 和class Z 的大小为 5 byte ,那么它们必须填补 3 byte ,最终得到的结果将是 8 byte 。
是不是开始感谢VC++编译器幸好有这样的优化机制,使得我们节约了不少内存空间。
最后,我们再来看看 class A ,它的大小为什么是 8 byte ?显而易见,它继承了class Y 和class Z ,那么它的大小直接就把 class Y 和class Z 的大小加起来就够了。
真有这么简单吗?实际上这只是一个巧合而已,这是因为之前编译器的优化机制掩盖这里的一些事实。
对于没有优化的 class Y 和class Z 来说,他们的大小都是8 byte ,那么继承了它们两个的 class A 将是多大呢?16 byte?如果你有这样的编译器试一下的话,你会发现答案是12 byte 。
怎么会是12 byte 呢?记住,一个虚拟继承的基类只会在派生类中存在一份实体,不管它在 class 继承体系中出现了多少次!class A的大小由下面几部分决定:l 被大家共享的唯一一个 class X的实体,大小为1 byte。
l 基类class Y 的大小,减去因虚拟继承的基类class X而配置的大小,也就是4 byte 。
基类class Z的算法相同,它们加起来就是8 byte 。
l class A自己的大小,0 byte 。
l class A 的alignment的大小(如果有的话)。
前述三项的总和是9 byte ,那么调整到4 byte的整数倍,也就是12 byte 。
我们前面讨论的VC++编译器得出的结果之所以是8 byte ,是因为 class X 实体的那1 byte被拿掉了,于是额外的3 byte也同样不必了,因此就直接把class Y 和class Z的大小加起来,得到8 byte 。
这个例子看懂了吗?是不是对C++的底层机制开始感兴趣了?那么我们再来举一个同样有趣的例子。
有这样一个类:class A {private:int a;char b;char c;char d;};它的大小是多少呢?如果你有记得我之前提到的alignment机制的话,你应该会猜到它的大小是8 byte 。
的确如此,int a占用4 byte ,char b , char c 和char d各占1 byte ,加起来是7 byte ,再加上alignment额外带来的1 byte ,总共是8 byte 。
瞧,就是这么简单,那么现在我们把里面的成员变量换换位置,如下:class A {private:char d;int a;char b;char c;};我们将char d拿到第一个位子,放在int a之前。
那么现在你能告诉我class A的大小是多少呢?你肯定不会再猜8 byte了,因为你会觉得这与上面似乎有些不同,但你不能肯定到底是多大。
不敢确定的时候就去试试吧,原来是12 byte ,这又是怎么回事呢?同样的类,只是改变了成员变量的位子,怎么就会多出4 byte的存储空间?其实这一切又是由变量的存储规则造成的。
对于一个类来说,它里面的成员变量(这里单指非静态的成员变量)是按声明的顺序存储在内存空间中的。
在第一种的情况中,它们紧紧的排列在一起,除了由于alignment所浪费的1 byte空间外,它们几乎用了最小的存储空间;而在第二种情况中,它们则不是排列得那么紧密了,错误就在于char d ,它一个人就占用了4 byte 。
为什么它会占用4 byte呢,其实责任也不全在它,后面的int a也有不可推卸的责任。
Int 型数据在VC++编译器中正好是占用4 byte的,等于一个alignment量,而这4 byte一定是密不可分的。
当char d占用了1 byte后,其后空出了3 byte(对于一个alignment量来说),而一个int型数据不能被拆成3 byte +1byte来存储,那样编译器将无法识别,因此int a只有向后推到下一个alignment的开始,这样char d就独占了4 byte ,中间有3 byte浪费掉了。
而后面的char b和char c依旧紧密排列,最后又由于alignment调整2 byte ,整个类的大小就变为了12 byte 。
看了这个例子,是不是该反省以前随意定义成员变量了?如果你要定义一个含3个int型数据和4个char型数据的类,本来最优化的方法只需要16 byte ,而你却随意的定义成如下的样子:class F{private: char c1; int i1; char c2; int i2; char c3; int i3; char c4;};看看结果是什么,这个类竟然要占据28 byte的空间,比刚才整整大了12 byte!再来看看继承的时候,成员变量是怎样存放的。
我们将第2个例子中的class A 改成三层的继承模式,或许我们在做项目中,真的会遇到这样的情况。
class A1{private:int a;char b;};class A2: public A1{private:char c;};class A3:public A2{private:char d;};现在我们来预测一下class A3 的大小,是8 byte吗?不,结果竟是16 byte ,竟然整整多了1倍。
这是为什么呢?按照成员变量的排列顺序,int a,char b,char c,char d应该紧密的排列在一起,8 byte没错。
但事实并非如此,这些都是因为继承而造成的。
知道“在继承关系中,基类子对象在派生类中会保持原样性”吗?或许这样专业的一句话,你并不能明白是什么意思,那么听我下面的分析。
在为派生类分配内存空间的时候,都是先为基类分配一块内存空间,而所谓的“原样性”是指基类原本在内存空间中是什么样子,那么它在派生类里分配的时候就是什么样子。
拿这个例子来说,class A1占据了8 byte的空间,其中int a占4 byte ,char b占1 byte ,因alignment而填补3 byte 。
对于class A1来说,占据8 byte空间没什么好抱怨的,但是class A2呢?轻率的程序员会认为,class A2只在class A1的基础上增加了唯一一个char c ,那么它应该会和char b绑在一起,占用原本用来填补空间的1 byte ,于是class A2的大小是8 byte,其中2 byte用于填补空间。
然而事实上,char c是被放在填补空间所用的3 byte之后,因为在class A2中分配的class A1应该完全保持原样,于是class A2的大小变成12 byte ,而不是8 byte了,其中有6 byte浪费在填补空间上。
相同的道理使得class A3 的大小是16 byte ,其中9 byte用于填补空间。
那么也许你会问,既然“原样性”会造成这样多的空间浪费,那么编译器为什么还要这样做呢?其实这样做是有它的必要的。
我们考虑下面这种情况:A1* pA1=new A1();A1* pA2=new A2();*pA1=*pA2;我们定义了两个A1型指针,一个指向A1对象,一个指向A2对象。
现在我们执行一个默认的复制操作(复制一个个的成员变量),那么这样一个操作应该是把pA2所指的对象的A1那部分完全复制到pA1所指的对象里。
假设编译器不遵循“原样性”,而是将派生类的成员和基类的成员捆绑在一起存放,去填补空间,那么这样的操作变会产生问题了。
A1和A2都占8 byte ,pA2会将其所指的8 byte空间里的内容全部复制给pA1所指的对象,那么pA1所指的对象本来只有2个数据,3 byte的填补空间,而复制后却变成了3个数据,2 byte的填补空间了,对于char c ,我们并不想把它复制过来的。
这样完全破坏了原语意,而这样引起的bug几乎是无法察觉的。

对C with class底层的研究
本文旨在深入研究 C++ 的底层实现,特别关注如何访问类的私有成员变量。
通常情况下,类的私有成员变量仅对类内部有效,但在底层层面,我们可以通过指针操作实现对这些变量的访问。
首先,我们定义了一个类 `Base`,包含私有成员变量 `m_x` 和 `m_y`。
在类外部,我们无法直接修改这些私有成员。
然而,我们知道在计算机内存中,所有数据都以二进制形式存储,通过指针操作,我们能够实现对内存数据的直接访问。
为了验证这一点,我们利用 Visual Studio 的命令行功能,通过特定命令 `/d1 reportAllClassLayout` 或 `/d1 reportSingleClassLayoutXXX`(其中 XXX 是类名),来查看 `Base` 类的内存布局。
这让我们清晰地了解类成员在内存中的实际位置。
创建一个 `Base` 类的实例后,我们首先通过强制类型转换将指针类型从 `Base*` 转换为 `char*`,然后加上偏移量(即指向私有成员变量的地址),再转换回 `Base*` 类型,从而访问到私有成员变量的值。
进一步,我们展示了如何利用这种方法修改 `Base` 类的 `m_x` 和 `m_y` 的值。
为了更直观地说明这一点,我们在 `Base` 类中添加了一个 `inline` 方法,并使用指针修改 `m_x` 的值,随后通过该 `inline` 方法获取和输出值。
关于调用私有函数,直接通过指针获取非虚函数的指针地址是不可行的,因为编译器会将其替换为指向 `this` 指针的全局函数。
然而,虚函数表的指针可以获取,这为通过指针调用虚函数提供了可能。
在某些场景下,如序列化数据传输,我们能直接将类实例保存为字节数组,然后发送到其他计算机,并强制转换成类。
例如,将 `Base` 类的 `m_x` 和 `m_y` 的值保存为字节数组,然后反序列化回 `Base` 类。
删除 `Base` 类的虚函数表后,我们重新运行程序,检查 `m_x = 88` 和 `m_y = 100` 时内存中的情况,并将 `Base` 类实例保存为字节数组。
随后,我们将此字节数组反序列化为 `Base` 类的实例。
通过字节数组保存类实例的方法,实际上是在实现类的序列化。
进一步地,我们探讨了如何通过即时编译(JIT)保存类的函数。
将 `Base` 类的 `add` 方法修改为非虚方法后,我们能够获取其汇编指令,并将其保存为字节数组。
通过使用 `VirtualProtect`(在 Linux 环境中为 `mprotect`)将数据放置于可执行内存空间,并声明一个函数指针指向该指令数组,实现了函数的保存与调用。
综上所述,本文通过深入探讨 C++ 类的底层实现,展示了如何在特定场景下访问和操作类的私有成员变量、函数,以及实现类的序列化与函数的即时编译。
这些底层技术在特定应用中具有重要意义,如网络通信、数据持久化和性能优化等。
C++中,举个例子描述一下STL中的allocator分配器是个啥玩意儿?
说一说C++里的allocator。
我们知道,C++ STL里定义了很多的容器(containers),每一个容器的第二个模板参数都是allocator类型。
比方说在VC10里,vector类的模板声明为:template<class _Ty, class _Ax = allocator<_Ty> >class vector但是,基本上很少有人会自定义一个allocator。
一来,默认的allocator已经够用了;二来,确实不知道该怎么用。
一般来说,我们没有必要重新定义一个allocator。
自定义的方式主要是为了提高内存分配相关操作的性能。
而STL提供的方式性能已经足够好了。
事实上,在windows平台上,new的底层实现是基于C语言的malloc函数;malloc函数家族又是基于Windows HeapCreate、HeapAlloc、HeapFree等相关API来实现的(具体可以参考%VSInstallFolder%\VC\crt\src目录中的heapinit.c、malloc.c和等相关函数)。
先撇开性能的问题不说,我们看一看如何实现一个自己的allocator。
在C++ 2003标准文档里,关于allocator的说明其实并不多,大概就20.1.5 Allocator requirements和20.4.1 The default allocator两处主要位置。
虽然内容不多,但是足够我们写出一个自己的allocator。
根据Allocator requirements我们需要提供一些typedefs:1: template <typename T> 2: class CHxAllocator 3: { 4: public: 5: // typedefs... 6: typedef T value_type; 7: typedef value_type* pointer; 8: typedef value_type& reference; 9: typedef value_type const* const_pointer;10: typedef value_type const& const_reference;11: typedef size_tsize_type;12: typedef ptrdiff_t difference_type;13:14: // rebind...15: template <typename _other> struct rebind { typedef CHxAllocator<_other> other; };16: };在这里有一个比较不太容易理解的东西:rebind。
C++标准里这么描述rebind的:The member class template rebind in the table above is effectively a typedef template: if the name Allocator is bound to SomeAllocator<T>, thenAllocator::rebind<U>::other is the same type as SomeAllocator<U>.啥意思?可以用一个简单的例子来说明下:学校都学过数据结构,比方说栈、单向列表、树。
我们就拿栈和列表来对比,看看有什么大不一样的地方。
撇开数据结构上的差异,从allocator的角度来看,我们可以发现:堆栈是存贮元素本身的,但是列表实际上不是直接存储元素本身的。
要维护一个列表,我们至少还需要一个所谓的next的指针。
因此,虽然是一个保存int的列表list<int>,但是列表存储的对象并不是int本身,而是一个数据结构,它保存了int并且还包含指向前后元素的指针。
那么,list<int, allocator<int>>如何知道分配这个内部数据结构呢?毕竟allocator<int>只知道分配int类型的空间。
这就是rebind要解决的问题。
通过allocator<int>::rebind<_Node>()你就可以创建出用于分配_Node类型空间的分配器了。
接下来要提供其他的接口。
根据The default allocator的描述,我们要提供如下一些接口:pointer address(reference val) constconst_pointer address(const_reference val) const返回val的地址pointer allocate(size_type cnt, CHxAllocator<void>::const_pointer pHint = 0)分配空间。
类似malloc。
pHint可以无视,主要是给类库使用,用于提高性能。
void deallocate(pointer p, size_type n)释放空间,类似free。
size_type max_size() const throw()可分配的最大数量。
void construct(pointer p, const_reference val)在地址p所指向的空间,使用val进行填充。
需要使用到palcement new,以便保证调用到构造函数。
void destroy(pointer p)析构p指向的内存块中内容。
一般通过显示调研析构函数来执行。
allocator() throw ()allocator(const_reference) throw ()template <typename _other> allocator(CHxAllocator <_other> const&) throw()~CHxAllocator() throw() 各种构造函数和析构函数如何实现上面这些函数,你只要照抄标准库中的实现就可以了。
如果你想要用c的malloc和free来实现,也可以这么写:1: pointer allocate(size_type cnt, CHxAllocator<void>::const_pointer pHint = 0) 2: { 3: UNREFERENCED_PARAMETER(pHint); 4: 5: if (cnt <= 0) 6: { 7: return 0 ; 8: } 9:10: void* pMem = nullptr ;11: if (max_size() < cnt || (pMem = malloc(cnt * sizeof(value_type))) == NULL)12: {13: throw std::bad_alloc(0);14: }15:16: return static_cast <pointer>(pMem);17: }18:19: void deallocate(pointer p, size_type)20: {21: free(p);22: }23:24: void construct(pointer p, const_reference val)25: {26: :: new ((void *)p) T(val);27: }28:29: void destroy(pointer p)30: {31: p->~T();32: }基本上,我们就简单实现了一个自己的allocator。
另外,除了这些最主要的接口函数,你还需要实现比较操作符==和!=,但是这些函根据标准文档,都直接返回true和false。
开头已经说了,重写allocator的主要目的是为了提高性。
那怎样才能提高性能呢?直接使用Windows的HeapXXXX堆内存API?其实,你自己用一下就会发现,性能提升并不明显。
因为通过new,再通过malloc,最后通过HeapAlloc不比直接调用HeapAlloc多几句话。
如何实现一个高性能的allocator,需要借助memory pool的想法。
另外,侯捷的stl源码剖析里分析了SGI STL利用类似想法实现的一个alloc。