|
Page 1 of 3 Hiding the Implementation A typical C library contains a struct and some associated functions to act on that struct. So far, you've seen how C++ takes functions that are conceptually associated and makes them literally associated byputting the function declarations inside the scope of the struct, changing the way functions are called for the struct, eliminating the passing of the structure address as the first argument, and adding a new type name to the program (so you don’t have to create a typedef for the struct tag). These are all convenient – they help you organize your code and make it easier to write and read. However, there are other important issues when making libraries easier in C++, especially the issues of safety and control. This chapter looks at the subject of boundaries in structures.
Setting limits In any relationship it’s important to have boundaries that are respected by all parties involved. When you create a library, you establish a relationship with the client programmer who uses that library to build an application or another library. In a C struct, as with most things in C, there are no rules. Client programmers can do anything they want with that struct, and there’s no way to force any particular behaviors. For example, even though you saw in the last chapter the importance of the functions named initialize( ) and cleanup( ), the client programmer has the option not to call those functions. (We’ll look at a better approach in the next chapter.) And even though you would really prefer that the client programmer not directly manipulate some of the members of your struct, in C there’s no way to prevent it. Everything’s naked to the world. There are two reasons for controlling access to members. The first is to keep the client programmer’s hands off tools they shouldn’t touch, tools that are necessary for the internal machinations of the data type, but not part of the interface the client programmer needs to solve their particular problems. This is actually a service to client programmers because they can easily see what’s important to them and what they can ignore. The second reason for access control is to allow the library designer to change the internal workings of the structure without worrying about how it will affect the client programmer. In the Stack example in the last chapter, you might want to allocate the storage in big chunks, for speed, rather than creating new storage each time an element is added. If the interface and implementation are clearly separated and protected, you can accomplish this and require only a relink by the client programmer. C++ access control C++ introduces three new keywords to set the boundaries in a structure: public, private, and protected. Their use and meaning are remarkably straightforward. These access specifiers are used only in a structure declaration, and they change the boundary for all the declarations that follow them. Whenever you use an access specifier, it must be followed by a colon. public means all member declarations that follow are available to everyone. public members are like struct members. For example, the following struct declarations are identical: //: C05:Public.cpp // Public is just like C's struct
struct A { int i; char j; float f; void func(); };
void A::func() {}
struct B { public: int i; char j; float f; void func(); };
void B::func() {}
int main() { A a; B b; a.i = b.i = 1; a.j = b.j = 'c'; a.f = b.f = 3.14159; a.func(); b.func(); } ///:~ The private keyword, on the other hand, means that no one can access that member except you, the creator of the type, inside function members of that type. private is a brick wall between you and the client programmer; if someone tries to access a private member, they’ll get a compile-time error. In struct B in the example above, you may want to make portions of the representation (that is, the data members) hidden, accessible only to you: //: C05:Private.cpp // Setting the boundary
struct B { private: char j; float f; public: int i; void func(); };
void B::func() { i = 0; j = '0'; f = 0.0; };
int main() { B b; b.i = 1; // OK, public //! b.j = '1'; // Illegal, private //! b.f = 1.0; // Illegal, private } ///:~ Although func( ) can access any member of B (because func( ) is a member of B, thus automatically granting it permission), an ordinary global function like main( ) cannot. Of course, neither can member functions of other structures. Only the functions that are clearly stated in the structure declaration (the “contract”) can have access to private members. There is no required order for access specifiers, and they may appear more than once. They affect all the members declared after them and before the next access specifier. protected The last access specifier is protected. protected acts just like private, with one exception that we can’t really talk about right now: “Inherited” structures (which cannot access private members) are granted access to protected members. This will become clearer in Chapter 14 when inheritance is introduced. For current purposes, consider protected to be just like private. Friends What if you want to explicitly grant access to a function that isn’t a member of the current structure? This is accomplished by declaring that function a friend inside the structure declaration. It’s important that the friend declaration occurs inside the structure declaration because you (and the compiler) must be able to read the structure declaration and see every rule about the size and behavior of that data type. And a very important rule in any relationship is, “Who can access my private implementation?” The class controls which code has access to its members. There’s no magic way to “break in” from the outside if you aren’t a friend; you can’t declare a new class and say, “Hi, I’m a friend of Bob!” and expect to see the private and protected members of Bob. You can declare a global function as a friend, and you can also declare a member function of another structure, or even an entire structure, as a friend. Here’s an example : //: C05:Friend.cpp // Friend allows special access
// Declaration (incomplete type specification): struct X;
struct Y { void f(X*); };
struct X { // Definition private: int i; public: void initialize(); friend void g(X*, int); // Global friend friend void Y::f(X*); // Struct member friend friend struct Z; // Entire struct is a friend friend void h(); };
void X::initialize() { i = 0; }
void g(X* x, int i) { x->i = i; }
void Y::f(X* x) { x->i = 47; }
struct Z { private: int j; public: void initialize(); void g(X* x); };
void Z::initialize() { j = 99; }
void Z::g(X* x) { x->i += j; }
void h() { X x; x.i = 100; // Direct data manipulation }
int main() { X x; Z z; z.g(&x); } ///:~ struct Y has a member function f( ) that will modify an object of type X. This is a bit of a conundrum because the C++ compiler requires you to declare everything before you can refer to it, so struct Y must be declared before its member Y::f(X*) can be declared as a friend in struct X. But for Y::f(X*) to be declared, struct X must be declared first! Here’s the solution. Notice that Y::f(X*) takes the address of an X object. This is critical because the compiler always knows how to pass an address, which is of a fixed size regardless of the object being passed, even if it doesn’t have full information about the size of the type. If you try to pass the whole object, however, the compiler must see the entire structure definition of X, to know the size and how to pass it, before it allows you to declare a function such as Y::g(X). By passing the address of an X, the compiler allows you to make an incomplete type specification of X prior to declaring Y::f(X*). This is accomplished in the declaration: struct X; This declaration simply tells the compiler there’s a struct by that name, so it’s OK to refer to it as long as you don’t require any more knowledge than the name. Now, in struct X, the function Y::f(X*) can be declared as a friend with no problem. If you tried to declare it before the compiler had seen the full specification for Y, it would have given you an error. This is a safety feature to ensure consistency and eliminate bugs. Notice the two other friend functions. The first declares an ordinary global function g( ) as a friend. But g( ) has not been previously declared at the global scope! It turns out that friend can be used this way to simultaneously declare the function and give it friend status. This extends to entire structures: friend struct Z; is an incomplete type specification for Z, and it gives the entire structure friend status.
|