C++中的指针(一) 简单指针 简单总结一下C++中指针的用法,以后再写一篇详细的,关于smart pointer的总结。指针的定义很简单。在变量前打个星。例如一个class的名字叫A,那么指针定义为 A *pa; 有意点点另人混淆的是指针和const的混用。 char chr[] = "abc"; const char *p = chr; //这里p不是常数指针,而是把指针指向的地址定义为了常数。无论chr本身是不是指向常数内存区,但只要用p去操作,那么就不可以通过p去修改其内容。 chr[2] = 'e'; // ok p[2] = 'd'; // error p+=1; // ok, 改的是p指向的地址而不是p的内容。真正的常数指针这么写 char *const cp = s; 这时在常数内存中allocate了一个指针的控件存储cp,cp,也就是这个地址不能改,而其指向的内存的值可以修改。 chr[2] = 'w'; //ok cp[2] = 'y'; // ok cp+=1; // error char* 可以被转换成const char*,因为操作后没有负面影响。反过来const char* 不能转换成char*,如果可以的话会把本部可写的内存的数据改掉。 // good example char chr[] = "abc"; char *p = chr; const char *cp = p; // bad example char chr[] = "abc"; const char *p = chr; char *p = cp; // error. 这种转换常用在函数调用上,例如strcpy(char* source, char*dest)。这个操作只是想修改source,dest只是用于参考。为了避免函数修改dest可以把函数定义成strcpy(char* source, const char* dest)。基本定义就这些了。对于指针的cast,C++作得比C更安全。例如有两个完全不相干的class A和B。 B b; A *p1,*p2; p1 = (A *)(&b); // 这是C式的cast,不管A和B有什么关系,强型转换。后果不堪设想。 C++中引入了static_cast操作,在一定程度上保护了操作的安全性,static_cast检查操作数与要操作的类型是否匹配,匹配是有class继承关系,无论谁继承谁都可以。如果这个关系不存在,出编译错误。 p2 = static_cast(&b); //error。但是这个检查是不完全的, class C : public A {} C* pc = static_cast(p1); // ok. 因为pc,p2欧继承关系。 C++引入了RTTI得概念(Run Time Type Info)。通过dynamic_cast操作,可以检查操作数的内容,以确认这个操作是否成功。检查内容的方法就是把相关类型的继承关系和vtable都查一下。 p2 = dynamic_cast(p1); 在VC下使用dynamic_cast别忘了在当前Project-->Setting下选Enable Run Time Type Info。如果忘了选这个,debug模式下编译会不通过,release模式下会编译通过,运行时Crash。 dynamic_cast比较复杂,另外Visual C++各不同版本的表现不一样,这里详细说一下我学到的和试出来的。一般书上说是三种不同情况,考虑到Visual C++版本的问题,我分五个情况讨论。 1。upcast。从派生类向基类的转换,只要基类的继承关系是唯一的,就会成功,如果不唯一会有warning:"dynamic_cast used to convert to inaccessible or ambiguous base;" 下下面的例子中 class A{public: virtual void a(){}}; class B : public A {}; class C : public B {}; class D : public B {}; class E : public C, public D {}; int main() { E e, *pe = &e; C *pc = dynamic_cast(pe); B *pb = dynamic_cast(pe); return 0; } 转换pb一行会有warning,而且得到NULL指针。其继承关系如下 A / / B B | | C D / / E E到C成功,E到B失败因为不知道怎么转换。同样E到A也会失败。注意这里的检查只是指针类型pe的检查,没有查pe指向的object。把pe改成*pe = (E*)(new D());的话pe到pc的cast还会成功,不过pe到pb的cast会出现crash。这和dynamic_cast的实现有关,这个exception不是bad_cast,所以最好用try{} chatch(...)接着以防不测。 2。对于同类指针的cast应该是直接通过, 不对指针所指的object进行run time check。 int main() { A *p1 = (A*)0x1; A *p2 = dynamic_cast(p1); return 0; } 但是VC6中竟对p1所指的地址进行了检查,这是VC6对ISO C++ standard实现不对的地方,在2003/2005中得到了修正。 3。downcast 从基类向派生类转换,指针指向的object会被检查,还以刚才的结构 E e; A *pa = dynamic_cast((D*)&e); C *pc = dynamic_cast(pa); 这个pc的cast会成功。 4。crosscast class A{public: virtual void a(){}}; class B : public A {}; class C : public A {}; class D{public: virtual void d(){}}; class E : public B, public C, public D {}; int main() { E e; C *pc = dynamic_cast((D*)&e); return 0; } 这个继承关系如下 A / | / B C D / | / E 从D到C的cast叫cross cast,这时查指针指的object的内容。这个具体例子中pc的cast 成功,因为确实有继承关系。一个不理解的问题是下面的测试: C c, *pc = dynamic_cast((D*)&c); 无论什么道理pc都应该成功,结果在VC6,VC2003中都成功了,在VC2005竟然失败,得到NULL。实在不明白,在MSDN的"Breaking Changes in dynamic_cast"也没有明确表述。只有死记住了。总之,dynamic_cast如果成功,p2得到一个合法地址,也就是p1指向的地址。如果失败就不好说了,书上说会得到NULL,这是理想情况,p1,p2有相近的vtable。如果p1,p2的vtable完全不相干,或者一个class B根本没有vtable,dynamic_cast就会出exception,这不是bad_cast的exception,而是C++ first class exception。所以写别人程序传来的指针的时候别指望dynamic_cast管理一切,老老实实catch所有exception。 p2 = NULL; try { p2 = dynamic_cast(p1); } catch (...) {} if (!p2) cout << "Bad cast". 另外两种cast不太常用reinterpret_cast提供很少的保护,几乎和C的cast差不多。const_cast得到最开始的变量的指针,可以用来改变常量的设置。这不是个好习惯,能不用最好不用。 smart pointer C++中一个很有用的概念,它对内存的管理起到了很大的帮助。由于内容比较多,回头我会写一篇详细的总结。 C++中的指针(二) 函数指针 先说一下C式的函数指针。这种函数指针的应用十分广泛。 对于任何函数 void print(string s),它的指针这样定义: void (*pfun)(string) = NULL; pfun= &print; 或者 pfun = print;两种写法没有区别。 pfun是指针变量名。可以指向任何只带一个string参数,返回void的函数。这里让它指向print()函数。 以后调用它的时候直接写 if (pfun) pfun("Hello world"); C++编译器会通过pfun找到print函数,然后call print("Hello world"); 一个简单应用是可以作菜单操作。例如在文本模式下的界面,让用户选择如下操作: "0.print, 1.copy, 2.delete, 3. quit, 4.help" 那么可以写5个函数: void print(); void copy(); void delete(); void quit(); void help(); 然后用一个函数指针数组把他们存在一起: void (*p[])() = {print, copy, delete, quit, help}; 然后根据用户入0,1,2,3,4来直接叫函数 cin >> index; p[index](); 在windows环境下编译这种函数指针被认为是用C/C++呼叫规则(C/C++ calling convention)。就是呼叫函数caller清理函数呼叫时生成的stack。另一种规则叫标准呼叫规则(standard calling convention)。由被叫函数callee清理自己的stack。二者一般情况下区别不大,但standard calling convention更合理,因为这样使函数size变小了一点。 实际上写C/C++函数指针的时候省略了 __cdecl 前缀。 应该写成void (__decel *p[])(); 而标准规范用 __stdcall前缀。 也可以用宏CALLBACK,这也就是著名的回调函数了。 使用CALLBACK的另一个好处就是呼叫函数(caller)不需要具体关心被叫函数(callee)是什么而直接呼叫。例如我们要写一个排序函数。可以用各种不同算法。如冒泡法。 void CALLBACK BubbleSort(int *pStart, int *pEnd); 也可以用quick sort void CALLBACK QuickSort(int *pStart, int *pEnd); 那么呼叫方只需要定义一个指向这种格式的函数指针: void (CALLBACK *p)(int*, int*),然后让p指向想用的函数就可以了。 这里只对int类型排序,实际上这种排序函数可以再叫一个CALLBACK函数来决定排序规则。以使算法可以应用到各种不同类型的变量以及不同的排序规则中。在各算法书上都有介绍。如果大家有兴趣,我可以写一下这个排序函数。 另一个典型的例子是MFC中Timer使用的CALLBACK函数,每当Timer Exprie的时候会去叫这个函数,根据返回值决定下一个动作。 C++中的函数指针与C的不同 class C { public: bool test(); } 这里指向print的指针不是bool *p(),而是bool (C::*p)(); 呼叫这个函数的时候这样写: C c, *pc=&c; bool (C::*p)() = &C::test; c.*p(); 或者 pc->*p(); 赋值那行bool (C::*p)() = &C::test;在VS2003里右边可以省去 C::,到了VS2005语法更严格了,被禁止了。这里的成员函数指针对非静态函数有效。静态函数不依赖于任何object,它的表示方法和C的一样。 对于非静态成员函数的指针的继承关系是这样的:upcast合法,downcast不合法。这样的到的指针永远是安全的。 非静态成员函数指针在实际程序中的应用很多。一个典型的例子是用来写state machine(状态机器?)。例如程序在控制一个机器人的初始化阶段。整个初始化需要三个函数:1。初始化机器人的身子,2。初始化机器的左手,3。初始化机器人的右手。这样我们在state machine中用两个成员函数指针分别指向当前的状态和下一个状态 bool (CStateMachine::*m_pCurrentState), bool (CStateMachine::*m_pNextState)。。 一开始永远叫Start() CStateMachine::CStateMachine { m_pCurrentState = CStateMachine::Start; } 然后在每一个State里面管理状态变化: bool CStateMachine::Start() { ..... m_pNextState = CStateMachine::InitializeLeftHand(); } bool CStateMachine::InitializeLeftHand() { .... m_pNextState = CStateMachine::InitializeRightHand(); } bool CStateMachine::InitializeRightHand() { .... m_pNextState = NULL; } 这样很清晰的标志了整个初始化的过程。当然这个过程也可以用很土的程序实现,设一个flag,然后把flag于函数一一对应。但那样作出来的程序不易懂,同时增加新状态的时候不好维护。 对于CStateMachine的核心部份可以这样控制:对于任何一步操作,如果函数返回true表示成功,执行下一步 (this->*(m_pCurrentState = m_pNextState))()。如果失败则报错,同时让用户选择重试(Retry)还是放弃(Abort)还是忽略(Ignor)。 如果Abort则结束StateMachine, 如果Retry则再次叫当前函数this->*m_pCurrentState(); 如果Ignor则忽略当前错误继续下一步。this->*(m_pCurrentState = m_pNextState)(); 当没有下一个状态的时候StateMachine结束。 (m_pNextState == NULL) 这是标准工业中的用法,大家不妨看一看,写成一个标准的class。这将是个很有用的练习。 C++中的指针(三) 智能指针 Smart Pointer是C++中的一个大题目,要说清楚他的所有好处很需要费点力气。我就一个功能一个功能的说。有我理解不透的地方希望大家指点。 1.copy-to-write 当生成一个C++ object的时候如果这个class很大,这个object会占用很多空间。那么每生成一个就占用一片空间,这样会占用很多系统资源。同时降低效率。一个解决方法就是对用拷贝构造函数生成的object,让他不存储数据,而只存储一个指向原来object数据的指针。这样空间就节省了很多。但问题在于这样两个object完全联结在了一起。如果修改了其中一个,另一个也跟着变了。所以这种方法不可取。这里讲的copy-to-write技术就是解决这类问题的方法。当通过引用一个已有object去拷贝构造新object时,新object只有一个指向已有object的指针。这两个object共享数据。直到其中一个需要修改数据的时候,再把这两块数据分离。这里举一个最简化的例子。假设一个class叫CLargeObject,里面存有很多数据。我们用一个inner class来把所有数据放在一起,叫CData。CData里面存有大量数据,例如一个数据库。这里用最简单的模型来表示,假设只有一个整数int m_nVal; CData里面需要包含另一个变量。叫作索引数目(reference count)。它记录了指向这个CData object的来自CLargetObject类的指针各数。也就是说,总共有多少CLargeObject的object正在引用着当前的CData object。 class CLargeObject { private: struct CData { private: int m_nVal; int m_nReferenceCount; } }; 对于每个CLargeObject的object,我们用一个CData类的指针来指向其数据。 CData *m_pData; CLargeObject至少有两个构造函数。第一个是标准的构造函数,初始化其数据。这时数据是唯一的,所以必须新生成一个CData的object来存储数据。 CLargeObject::CLargeObject(int nVal) { m_pData = new Data(nVal); } 而对于CData类的构造函数而言,初始化他的CLargeObject是第一个指向他的,这一时刻索引数目m_nReferenceCount是1。 CLargeObject::Data::Data(int nVal) : m_nVal(nVal), m_nReferenceCount(1) {} CLargeObject的第二个构造函数是拷贝构造(copy constructor)。这样生成的object不需要有新的数据,和已有的object共享数据就可以了。这是索引数目需要加1。表示又有一个object指向当前的CData了。 CLargeObject::CLargeObject(const CLargeObject &ob) // copy constructor { ob.m_pData->m_nReferenceCount++; m_pData = ob.m_pData; } 这样CLargeObject就构造好了,使用了可能的最少的内存。下面看看他的析够函数(destructor)。当一个object被delete的时候,它的数据不一定无效,如果别的object还在引用着这个数据,数据需要留下来。当然,数据的索引数目无论如何都要减1。 CLargeObject::~CLargeObject() { if (--m_pData->m_nReferenceCount == 0) delete m_pData; } 下面看一看赋值操作。先说用已有的CLargeObject赋值给这个CLargeObject。这时当前CLargeObject里面的数据要指向已有的这个object,就搞定了。 CLargeObject& CLargeObject::operator = (const CLargeObject& ob) // copy assignment { ob.m_pData->m_nReferenceCount++; if (--m_pData->m_nReferenceCount == 0) delete m_pData; m_pData = ob.m_pData; return *this; } 再来看看如何对CLargeObject里面的数据进行真正的修改。这样就一定需要对当前的object独立操作了,否则就影响到了其它指向同一块数据的CLargeObject。这样CData类需要一个新的函数,生成只用于当前CLargetObject的数据。如果当前的引用数目是1,那么当然这个CData就是只用于这个CLargeObject的了。否则就重新new一个CData返回。 Data* CLargeObject::CData::get_own_copy() // clone if necessary { if (m_nReferenceCount==1) return this; m_nReferenceCount--; return new Data(m_nVal); } CLargeObject修改前用这个函数得到唯一的object,然后对它赋值。 void CLargeObject::SetVal(int nNewVal) { m_pData = m_pData->get_own_copy(); m_pData->m_nVal = nNewVal; } 对于所有可能改变CData值的操作,都需要用这种方法。下面是只读函数,简单。直接返回值,什么特殊的都不用作。 int CLargeObject::GetVal() const { return m_pData->m_nVal; } 这样copy-to-write技术就实现了。下面把完整的程序写一下: class CLargeObject { public: CLargeObject(int nVal); CLargeObject(const CLargeObject &ob); ~CLargeObject(); CLargeObject& operator = (const CLargeObject& ob); void SetVal(int nNewVal); int GetVal() const; private: struct Data { public: Data(int nVal) : m_nVal(nVal), m_nReferenceCount(1) {} private: friend class CLargeObject; Data* get_own_copy() // clone if necessary { if (m_nReferenceCount==1) return this; m_nReferenceCount--; return new Data(m_nVal); } // control variables. int m_nReferenceCount; // actual data portion int m_nVal; }; Data *m_pData; }; CLargeObject::CLargeObject(int nVal) { m_pData = new Data(nVal); } CLargeObject::CLargeObject(const CLargeObject &ob) // copy constructor { ob.m_pData->m_nReferenceCount++; m_pData = ob.m_pData; } CLargeObject::~CLargeObject() { if (--m_pData->m_nReferenceCount == 0) delete m_pData; } CLargeObject& CLargeObject::operator = (const CLargeObject& ob) // copy assignment { ob.m_pData->m_nReferenceCount++; if (--m_pData->m_nReferenceCount == 0) delete m_pData; m_pData = ob.m_pData; return *this; } void CLargeObject::SetVal(int nNewVal) { m_pData = m_pData->get_own_copy(); m_pData->m_nVal = nNewVal; } int CLargeObject::GetVal() const { return m_pData->m_nVal; } 很多存储数据的系统class,如string,CString等都有这种设计。所以记住这个应用是很有必要的。
C++中的指针
最新推荐文章于 2023-08-04 18:09:12 发布