📄 chapter4.html
字号:
<br><br>If an object's data structure declaration is private and declared and known only to the one source file that implements the object, how in the world are you going to get the compiler to perform type checking on handles to this object in other source files? <br><br><b>4.3.2 The Breakthrough</b> <br><br>The breakthrough in accomplishing this complete data hiding while still maintaining compiler type checking came after I realized how to get the C compiler to perform type checking on pointers without knowing what the pointer points to. In other words, it is possible to create a pointer in C that points to an unknown object and yet is type checkable by the compiler. It is also impossible to perform an indirection on this pointer in all source files except in the one source file that implements the object, where an indirection is possible. <br><br>Does this sound too good to be true? The remarkable part about this feature of the C language is that you probably have used this feature but never fully realized its potential. <br><br>Consider how you would implement a linked list of nodes. <br><br><table bgcolor="#CCCCEE" cellpadding=0 cellspacing=0><tr><td><pre><b>Linked list of nodes</b>typedef struct tagNODE { struct tagNODE *pNext; ... } NODE, *PNODE;</pre></tr></td></table><br>The solution is to use structure tags. Because PNODE does not even exist when you want to declare pNext, it is declared as a pointer to struct tagNODE.Consider how you would implement two structures that contain pointers to each other. <br><br><table bgcolor="#CCCCEE" cellpadding=0 cellspacing=0><tr><td><pre><b>Circular reference problem, first cut</b>typedef struct tagNODEA { struct tagNODEB *pNodeB; ... } NODEA, *PNODEA;typedef struct tagNODEB { PNODEA pNodeA; .... } NODEB, *PNODEB;</pre></tr></td></table> <br>Take a close look at this example. In it you see that pNodeB is being declared to be a pointer to a structure tag, a structure that does not yet exist. This example can be rewritten as follows. <br><br><table bgcolor="#CCCCEE" cellpadding=0 cellspacing=0><tr><td><pre><b>Circular reference problem, second cut</b>typedef struct tagNODEA *PNODEA;typedef struct tagNODEB *PNODEB;typedef struct tagNODEA { PNODEB pNodeB; ... } NODEA;typedef struct tagNODEB { PNODEA pNodeA; .... } NODEB;</pre></tr></td></table> <br>In this example, PNODEA is a type that points to struct tagNODEA and PNODEB is a type that points to struct tagNODEB. PNODEA and PNODEB are then used even though the structure declarations do not exist yet. <br><br>Structure tags allow you to create a pointer to an object before the object even exists and perform type checking on the pointers. In fact, the pointer declarations could have been placed in an include file and used by other source files. As long as the other source files do not try to perform an indirection on the pointer, everything works. Then in the source file with the structure declaration (with the appropriate structure tag) pointer indirections have meaning. Pointer indirections have meaning when they appear in a source file that has a structure declaration with the appropriate structure tag. <blockquote><table bgcolor="#E0E0E0" border=1 cellpadding=2 cellspacing=0><tr><td> Structure tags allow you to create a pointer to an object before the object even exists and perform type checking on the pointers. </tr></td></table></blockquote><b>4.3.3 NEWHANDLE() Macro</b> <br><br>Now that we know this, we can write a macro that introduces new type checkable handles into the programming environment. <br><br><table bgcolor="#CCCCEE" cellpadding=0 cellspacing=0><tr><td><pre><b>The NEWHANDLE() macro</b>#define NEWHANDLE(Handle) typedef struct tag##Handle *Handle</pre></tr></td></table> <br>The NEWHANDLE() declarations are almost always placed in an include file that gets included into all source files. NEWHANDLE() is usually not used in source files. <br><br>Notice in NEWHANDLE() how the token pasting operator (##) is being used in tag##Handle to create a structure tag that is derived from the handle name. By convention, all handle types must be in uppercase and prefixed with the capital letter H (HRAND, for example). <br><br>If your C environment does not support the token pasting operator, but your preprocessor follows the Reiser model, you can still accomplish token pasting. See <a href="chapter2.html#reiserpasting">§2.2.8</a> for details. This technique involves replacing ## with /**/. <br><br>Going back to the random number generator example, creating a handle called HRAND would now be easy. <br><br><table bgcolor="#CCCCEE" cellpadding=0 cellspacing=0><tr><td><pre><b>HRAND handle declaration</b>NEWHANDLE(HRAND);<br><b>NEWHANDLE(HRAND) macro expansion</b>typedef struct tagHRAND *HRAND;</pre></tr></td></table> <br>So, HRAND is really just a pointer to an unknown structure whose structure tag is tagHRAND. <br><br>The HRAND type can be used even though no structure with a structure tag of tagHRAND exists in the modules being compiled. This is just like the linked list of nodes with the PNODEA and PNODEB types. <br><br>A complete random number generator interface specification in an include file could quite possibly be written as follows. <br><br><table bgcolor="#CCCCEE" cellpadding=0 cellspacing=0><tr><td><pre><b>Random number generator interface</b>NEWHANDLE(HRAND);...EXTERNC HRAND APIENTRY RandCreate ( int );EXTERNC HRAND APIENTRY RandDestroy ( HRAND );EXTERNC int APIENTRY RandNext ( HRAND );</pre></tr></td></table> <br>It is important to realize how the HRAND data type works. Immediately after the NEWHANDLE(HRAND) declaration, HRAND can be used just like any other data type except that it cannot be dereferenced because what HRAND really is is not yet known. In other words, HRAND can be used in function prototypes and HRAND variables can be initialized and passed around, but trying to dereference the HRAND variable will not be possible. <br><br>Consider some code that needs its own random number generator. It creates one using RandCreate(), uses it by calling RandNext() and when finished, calls RandDestroy(). The code is able to use HRAND without knowing what HRAND points to. There can also be an unlimited number of random number generators active at any given time. <br><br><table bgcolor="#CCCCEE" cellpadding=0 cellspacing=0><tr><td><pre><b>Function that uses a random number generator object</b>void Testing( void ){ HRAND hRand=RandCreate(0); LOOP(100) { printf( "Number %d is %d\n", loop, RandNext(hRand) ); } ENDLOOP hRand = RandDestroy( hRand );} /* Testing */</pre></tr></td></table> <br>It is important to realize how the HRAND data type is working in Testing(). The Testing() function is using an HRAND variable hRand even though Testing() has no idea what hRand points to or how the HRAND data type is implemented. In fact, the HRAND structure declaration is not even visible to this Testing() function. This is because HRAND at this point is a pointer to an unknown, but named (tagHRAND), object/structure. <br><br>Notice the spelling of hRand. It is an upper- and lowercase variant of its data type, HRAND. You should always try to derive object variable names from object data types this way. <br><br>The only source line that may not be totally clear in Testing() is the hRand=RandDestroy(hRand); line. By convention, all functions that destroy an object return the NULL object, so this ends up setting hRand to NULL. <blockquote><table bgcolor="#E0E0E0" border=1 cellpadding=2 cellspacing=0><tr><td> All object destroy functions return the NULL object. </tr></td></table></blockquote>The reasoning behind this is that you always want a handle variable to contain a valid handle or NULL. You never want a handle variable to be uninitialized or contain an old, previously valid, handle (see <a href="chapter7.html#pointers">§7.12</a> for more information on the usage of NULL). <br><br><b>4.3.4 Implementing the Random Number Generator</b> <br><br>There are still a lot of loose ends to fully implement the random number generator, but here is a rough shell of what the code will look like. <br><br><table bgcolor="#CCCCEE" cellpadding=0 cellspacing=0><tr><td><pre><b>Random number generator implementation, first cut</b>TYPEDEF struct tagHRAND { long lRand; };HRAND RandCreate( int nSeed ){ HRAND hRand; (allocate memory); hRand->lRand = nSeed; return (hRand);} /* RandCreate */HRAND RandDestroy( HRAND hRand ){ (free hRand memory) return (NULL);} /* RandDestroy */int RandNext( HRAND hRand ){ hRand->lRand = NEXTRAND(hRand->lRand); return(FINALRAND(hRand->lRand));} /* RandNext */</pre></tr></td></table> <br>The struct tagHRAND structure declaration is declared in the source file that implements HRAND and not in an include file. <br><br>The implementation is straightforward. The details of random number generation in the NEXTRAND() and FINALRAND() macros and how memory is allocated and freed for the objects has been left out. This is discussed later. <br><br>The only catch is TYPEDEF. For standard C, TYPEDEF is defined to be typedef. For C++, TYPEDEF is defined to be nothing. This was done to avoid the Microsoft C8 warning message C4091 no symbols were declared under C++. <br><br>In the struct tagHRAND declaration, a declarator is not required after the ending brace and before the semicolon because a structure tag is being used. When a structure tag is used, the declarator is optional. Without a structure tag, the declarator is generally required. This has to do with how C works and it is spelled out in §A8 of <a href="references.html">The C Programming Language</a>. <br><br>This random number generator source is contained in its own source file. It is important to realize this. This code need not and should not be declared along with other code, like the Testing() function, that uses the random number generator. The implementation of an object should be contained in a separate source file and a source file should implement, at most, a single class object. <blockquote><table bgcolor="#E0E0E0" border=1 cellpadding=2 cellspacing=0><tr><td> The implementation of an object is contained in its own source file and is separate from code that uses the object. </tr></td></table></blockquote>This implementation is in its own source file and #includes the same include file that all other source files include. This include file contains the NEWHANDLE(HRAND) declaration and function prototypes. <br><br>Even in the source file that implements the HRAND object, the compiler has no idea what HRAND is until a structure is declared with a structure tag of tagHRAND. At this point, the compiler binds what HRAND is to the tagHRAND structure and indirections are now possible. In other words, as soon as this binding of HRAND to the structure with tag tagHRAND takes place, we are free to implement the HRAND object because indirections on the object are now possible. <blockquote><table bgcolor="#E0E0E0" border=1 cellpadding=2 cellspacing=0><tr><td> Indirections on a handle pointer are valid only in the module that implements the class object. </tr></td></table></blockquote><b>4.3.5 Summary</b> <br><br>The problem in an object model with private objects is getting the compiler to type check pointers to the objects when the objects are not even known to the compiler. The solution is to use an incomplete type, a feature of C. A handle to an object is a pointer to a structure that has a structure tag, but a structure that does not have a body. <br><br>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -