img yechun


发表于2001/5/12 20:54:00  452人阅读

FAQs Tips

The TeamB guide to avoiding common mistakes in C++Builder.

This article contains a list of suggestions put together by the members of TeamB for C++Builder. The suggestions will help you avoid subtle coding errors that often have disastrous effects at runtime. Most of the suggestions in the list contain a link to a paragraph of text that explains why you should follow the suggestion. Some of the suggestions are self explanatory, and these suggestions don't have a corresponding link.

Note: Updated Feb 21, 2000. New items have a NEW icon.

  1. AnsiString
    1.1 Don't store the result of AnsiString::c_str()
    1.2 Don't use the unsigned long constructor for AnsiString
    1.3 Don't use the += AnsiString operator on properties

  2. TList
    2.1 Don't forget to delete pointers in a TList
    2.2 Don't delete the void pointers in a TList
    2.3 Don't overuse the TList class

  3. General VCL suggestions
    3.1 Don't Change the Name property of a control at runtime
    3.2 Don't forget to set the Parent property of a control created at runtime
    3.3 Don't set the Parent property of MDI child forms
    3.4 Don't forget to call Synchronize when modifying visual controls in a background thread
    3.5 Don't delete child controls after their parent is gone
    3.6 Don't pass parameters by reference to property methods
    3.7 Don't use the Selected property of TListBox in single selection ListBox controls
    3.8 Don't try to put more than 32k of text into a TMemo on Win95/Win98
    3.9 Don't use OnCreate and OnDestroy, use C++ constructors and destructors instead
    3.10 Use new instead of Application->CreateForm

  4. C++ Language Suggestions
    4.1 Don't mix operator new[] with delete, and don't mix delete[] with new
    4.2 Always set a pointer variable to NULL or 0 after deleting it
    4.3 Don't use memset or ZeroMemory as constructor replacements
    4.4 Don't make assumptions about the size of an enum variable

  5. Project Suggestions
    5.1 Don't alter the makefile switches for alignment or enum variables
    5.2 Don't link with LIB files or OBJ files created by other compilers
    5.3 Don't link with OBJ files created by a previous version of the Borland compiler
    5.4 Don't import COM type libraries using the IDE menu option
    5.5 When you import BCB3 forms into BCB4, set OldCreateOrder to false

  6. Database Suggestions
    6.1 Don't use AsDateTime or AsInteger to assign one TField to another
    6.2 Don't use TCurrencyField unless you have to
    6.3 Don't turn on CachedUpdates when the Filtered property of dataset is on
    6.4 Don't call ApplyUpdates when the Filtered property of dataset is on
    6.5 Don't use the VCL's data aware controls
    6.6 Don't use TDBLookupComboBox or TDBLookupListBox
    6.7 Don't set the Active property of a dataset to true at design time
    6.8 Don't change the Filter property of a dataset while the dataset is in edit mode
    6.9 Don't look at the value of UpdateStatus in the OnUpdateRecord handler of a dataset
    6.10 Don't call Post after calling DisableControls on a dataset

Don't store the result of AnsiString::c_str()

Examine the following code segment.

    AnsiString strText = "Howdy Mr. Ditka.";
    char *ptr = strText.c_str();
    strText = "Goodbye Mr. Ditka";
    Label1->Caption = ptr;

This code contains a serious defect. If you execute this code, you will see that the label displays the first string that was assigned to strText. This may surprise you. Why doesn't the label contain the string that says "Goodbye Mr. Ditka"? After all, doesn't ptr point to the string that is contained in the strText variable?

Whenever you assign a new string to an AnsiString variable, the AnsiString deallocates whatever memory it previously owned and allocates new memory for the new string. When the new memory is allocated, it is unlikely to be the same memory that was originally returned by the c_str function.

The second line of code in the example above stores the pointer returned by c_str for later use. After the pointer is stored, a new string is assigned to the AnsiString variable. At this point, the strText variable deallocates the first string and allocates memory for the new string. Now ptr points to memory that has been deleted. When you copy that memory into the label, you see the remnants of the original string. For larger strings, the label may contain garbage characters, or the label may appear to be truncated. De-referencing the data in the ptr variable may also cause an access violation because you are using memory that the application no longer owns.

Storing the result of c_str can lead to problems that are more difficult to track down. For example:

    char *ptr1 = Edit1->Text.c_str();
    char *ptr2 = Edit2->Text.c_str();

    Label1->Caption = "Edit1 contains " + AnsiString(ptr1);
    Label2->Caption = "Edit2 contains " + AnsiString(ptr2);

On the surface, the code looks like it should work. Sure, we store the pointer returned from c_str, but nothing is done that would cause the string to be re-allocated as in the first example. However, if you test this code, you will see that it does not work. Sometimes the labels will both display the string from the second edit box, and sometimes they contain garbage.

The problem is that the Text property of TEdit returnes a new AnsiString by value. This new AnsiString object is a temporary object. Temporary objects don't last forever. They only hang around as long as they are needed. They are destroyed before the next line of code executes.

In this code example, the temporary object is only needed for the call to c_str. Once the c_str method has been called, the temporary object is destroyed because it is no longer needed. Here is the catch. Deleting the temporary object deletes the memory that was pointed to by c_str, which means that ptr1 points to deleted memory. You can see the destruction of the temporary object by viewing the assembly code generated by these statements.

    // assembly output for the statement
    char *ptr1 = Edit1->Text.c_str();

    mov   ...
    lea   ...
    call  System::AnsiString::AnsiString(); // create temp object
    mov   ...
    inc   ...
    mov   ...
    call  Controls::TControl::GetText();    // load Caption into temp AnsiString
    lea   ...
    call  System::AnsiString::c_str()       // get c_str of temp object
    mov   ...
    dec   ...
    lea   ...
    mov   ...
    call  System::AnsiString::~AnsiString   // delete temp object

All of this code is generated by the one statement where ptr1 is assigned the value of Edit1->Text.c_str(). Before the next line of code executes, the temporary AnsiString object is destroyed. Deleting the temporary AnsiString object renders the previous c_str result worthless, and that is exactly what happens (and it's exactly what should happen, the compiler is obeying the standard by cleaning up temporary objects in this manner).

The moral of this story is to avoid saving the result from AnsiString::c_str because you run the risk of keeping a pointer to memory that has been de-allocated.

Don't use the unsigned long constructor for AnsiString

If you are using C++Builder 3, the unsigned long constructor for AnsiString has a bug in it. The bug looks like this:

    __fastcall AnsiString::AnsiString(unsigned long src) : Data(0)
        char buff[20];
        wsprintf(buff, "%lu", src);
        *this = src;

The last line should be *this = buff; The bug is fixed in BCB4 and newer.

Don't use the += AnsiString operator on properties

Examine the following code that attempts to add some exclamation points to the string that is already in a label control:

    Label1->Caption += "!!!!!!!";

When you use the += operator on the Caption property, the compiler creates code that constructs a new temporary AnsiString object. The compiler then calls the GetText function to copy the contents of the label into the temporary variable. Next, the compiler constructs another AnsiString object and initializes it with the string "!!!!!". Finally, the compiler calls the += operator of the first temporary object to combine the two strings. The problem is that the compiler does not generator code to write the resulting temporary AnsiString value back into the Caption property. Instead, the temporary object is deleted because it is no longer needed.

The assembly code looks something like this:

    // assembly output for the statement
    Label1->Caption += "!!!!!!!";

    mov   ...
    lea   ...
    call  System::AnsiString::AnsiString() // create temp object
    mov   ...
    inc   ...
    mov   ...
    mov   ...
    call  Controls::TControl::GetText()    // read Caption into temp object
    lea   ...
    push  ...
    mov   ...
    lea   ...
    call  System::AnsiString::AnsiString(char *) // create AnsiString for "!!!!"
    inc   ...
    lea   ...
    pop   ...
    call  System::AnsiString::operator +=        // combine the two strings
    dec   ...
    lea   ...
    mov   ...
    call  System::AnsiString::~AnsiString() // destroy one temp
    dec   ...
    lea   ...
    mov   ...
    call  System::AnsiString::~AnsiString() // destroy the other temp

In the assembly code above, locate the += operator call. Notice that nothing is done with the resulting string after the += operator returns. After the += operator returns, the strings are destroyed. In order for the property assignment to take affect, the resulting string should be passed to the SetText function. Because the write method for the property is not called, the TLabel object is not modified.

Through experimentation, I have found that the += operator does work on integer properties, such as the Width property of the form. However, in order to avoid confusion, it may be wise to avoid the += operator on all properties. The same can be said for the other combination operators (-= , *= , /=, ^= , and so on).

Don't forget to delete pointers in a TList

If you use TList to store pointers, make sure that you delete the pointers. TList makes no assumptions about the data it holds, and it does not delete the pointers in the list when it is deleted. That is your responsibility.

Don't delete the void pointers in a TList

Yes, it is your responsibility to delete pointers that are contained in a TList, but you have to be careful about how you do it. Consider the following code.

    // construct a list of bitmaps
    TList *list = new TList;
    const int nButtonCount = 100;
    for(int j=0; j<nButtonCount; j++)
        list->Add(new Graphics::TBitmap);

    // do something with the list

    // now delete the list
    for(int j=0; j< list->Count; j++)
        delete list->Items[j];
    delete list;

Everything looks good on the surface. You construct a list and add some items. When you are done with the list, you loop through and delete the pointers in the list before deleting the list itself. It all looks good, but it doesn't work correctly. In fact, the code usually crashes. The problem is that the Items property of TList returns a void pointer. So how do you destroy a void pointer? I don't know, but I do know that deleting a void pointer is not the same as deleting a TBitmap pointer.

Since TList doesn't know what kind of stuff you are storing in it, it returns a void pointer when you fetch an item from the Items array. When you try to delete a void pointer, the operator delete doesn't really know what you are asking it to do. It doesn't know that the pointer is really a TBitmap pointer. As a result, the destructor for TBitmap will not be called when you delete the items in the for loop. In order to correctly destroy the items in the list, you must tell the compiler what type of objects you are deleting. The correct code looks like this:

    // construct a list of bitmaps
    TList *list = new TList;
    const int nButtonCount = 100;
    for(int j=0; j<nButtonCount; j++)
        list->Add(new Graphics::TBitmap);

    // do something with the list

    // now delete the list
    for(int j=0; j< list->Count; j++)
        delete reinterpret_cast<Graphics::TBitmap *>(list->Items[j]);
    delete list;

There are a couple of additional things to think about. First, remember that reinterpret_cast is not a type safe cast. It does not check that the items are the correct type. Secondly, don't bother trying to use dynamic_cast because you cannot use dynamic_cast on a void pointer. Thirdly, if your list contains different types of objects, cast the objects to the most immediate, common base class. For example, if your list contains TEdit pointers and TButton pointers, then cast the items to a TWinControl pointer before deleting. In order for this to work, the destructors in your classes must be declared as virtual. Also, this won't work if the pointers do not have a common base class. Finally, many of these headaches can be avoided by using a type-safe derivative of TList or one of the typesafe template containers in the STL.

Don't overuse the TList class

C++ programmers should generally use the containers from the STL instead of using TList. There are several reasons. First of all, STL code is portable between compilers and operating systems. Secondly, TList is not typesafe. When you use TList as a container for TButton objects, nothing prevents you from adding a TListBox to your container. STL containers prevent you from adding the wrong types of objects to the container. Lastly, TList is very poor at storing large numbers of objects (> 5000 or so). The STL containers are much more efficient at storing large numbers of objects.

Don't forget to set the Parent property of a control created at runtime

When you create a control at runtime, the control will not appear if you forget to set the control's Parent property. You will usually set the Parent to be a form, a panel, or a group box. See the FAQ on how to create a control at runtime for more info.

Don't set the Parent property of MDI child forms

Doing so will cause runtime problems in your program. To create an MDI child form, simply construct the form object. Don't worry about the Parent property.

Don't use OnCreate and OnDestroy, use C++ constructors and destructors instead

If you need to run some code during the construction of a form, you should place the code inside the constructor of the form. If you need to do something while a form is being destroyed, add a destructor to your class and place the finalization code there. Avoid using the OnCreate and OnDestroy events that are provided by the form.

OnCreate and OnDestroy are handy to use, because you can create them from the Object Inspector. Despite this ease of use, you should avoid them. There are several reasons. The most important reason is that you don't know when the OnCreate and OnDestroy events will fire.

Let's look at the OnCreate event. It is triggered from inside the VCL function TCustomForm::DoCreate. OK, so who calls DoCreate? It is called from one of two places depending on the value of the OldCreateOrder property. If OldCreateOrder is false (ie the good setting), then DoCreate is called from TCustomForm.AfterConstruction. AfterConstruction executes immediately after all of the constructors for your form have finished running (how this happens can be attributed to compiler magic). The second function that calls DoCreate is TCustomForm::Create, the pascal constructor for the form.

This is where things get interesting. What are the consequences of triggering an event, such as OnCreate, from inside the constructor of a base class? Well, the consequences are serious. Recall that the base class constructors execute before the body of your derived constructor and more importantly, before any derived member objects have been initialized. Take this code for example:

// header file
class TForm1 : public TForm
    void __fastcall FormCreate(TObject *Sender);
    AnsiString m_foo;
    __fastcall TForm1(TComponent* Owner);

// cpp file
__fastcall TForm1::TForm1(TComponent* Owner)
    : TForm(Owner)

void __fastcall TForm1::FormCreate(TObject *Sender)
    m_foo = "hello world";

If OldCreateOrder is true, FormCreate will execute before the derived TForm1 constructor runs, and before the m_foo member variable has been constructred. In this code, m_foo is default constructed. This default construction happens just after the base class constructor is called (ie when :TForm(Owner) returns). But FormCreate is triggered from inside of the base class constructor. When the "hello world" assignment executes, m_foo hasn't been constructed yet. It essentially does not exist.

Assigning values to variables that haven't been constructed is not a good thing. So what happens to this code if OldCreateOrder is true? At best, the assignment of "hello world" will be lost. That's what happened when I ran the code. In a worst case scenario, the app would crash. What's really scary is that this code switches from being malformed to being perfectly legal with a switch of the OldCreateOrder property.

Ok, so let's summarize the most important reason why OnCreate is dangerous: because it could execute before your constructor executes and before any member objects have been initialized. In C++, it is generally mandated that a base class constructor should not be able to call the member functions of a derived class. OnCreate violates this. OnDestroy does too, but during destruction.

Now, you might be thinking to yourself: "hey they danger isn't OnCreate, its that evil OldCreateOrder property. As long as OldCreateOrder is false, OnCreate and OnDestroy are safe." This statement is for the most part correct. While it is true that you can control the behavior of OnCreate and OnDestroy through OldCreateOrder, it is in fact difficult to keep control of OldCreateOrder itself. OldCreateOrder is true by default when upgrading projects from BCB3 (true == the bad setting). And it gets stuck on true when using form inheritance. In BCB5, OldCreateOrder, the deadliest property of all, is not even displayed by the object inpsector unless you specifically tell the OI to show it. In the end, it just isn't worth it. Avoid the use of OnCreate and OnDestroy, and you won't have to worry about OldCreateOrder rearing its ugly head.

There is another reason to avoid OnCreate, even if you have OldCreateOrder set properly. It is inefficient to initialize objects inside of OnCreate. In the code above, m_foo is default constructed. When the AfterConstruction event fires the OnCreate handler, the string "hello world" is copied into m_foo. This occurs via the construction of a temporary AnsiString object, followed by a called to AnsiString's assignment operator. This is somewhate ineffiectient. If all we wanted to do was initialize m_foo with "hello world", the most efficient method is to use direct initialization in the constructor. Like this:

__fastcall TForm1::TForm1(TComponent* Owner)
    : TForm(Owner),
      m_foo("hello world")

This code initializes m_foo using the char * conversion constructor of AnsiString. As a result, we have replaced a default constructor call, creation of a temporary AnsiString object, and a call to the assignment operator with a single call to a conversion constructor. Plus, this method of construction is the C++ way of doing things, as opposed to the Delphi way.

It is our advice that BCB users pretend that OnCreate and OnDestroy don't exist. You are using a C++ product, so we feel it is wise just code things the C++ way.

Use new instead of Application->CreateForm

When you need to create a form in code, use the new operator instead of calling Application->CreateForm. If the IDE puts CreateForm in your code, then just leave it alone. This applies primarily to the WinMain function in your project cpp file. But for code that you write, use new instead.

Here are some differences between CreateForm and the new operator (5 is the best):

1- If your form is the first form being constructed with CreateForm, then it is automatically promoted to the job of being the mainform of the app. This may not be what you want. What if you need to display a splash screen or a login dialog when your app starts? If you create this dialog first with CreateForm, then it gets promoted to be the main form. With new, this does not happen.

2- CreateForm always sets the global application object to be the owner of the form. You can't override this. With new, you get to explicitly pass the owner.

3- CreateForm has to invoke the constructor for your form class. Your constructor is off in your c++ code somewhere. Have you ever wondered how code written in a pascal library can find and execute your constructor? How does it even know anything about your constructor? It doesn't even know anything about your form's type (ie TForm1).

The answer is that CreateForm invokes your constructor virtually. This is that virtual constructor stuff that everyone is always talking about. If the idea of a virtual constructor (which should not exist in c++) makes you queasy, then just use new instead.

4- Because of the virtual constructor stuff, CreateForm can only call one type of form constructor: the constructor that takes a single TComponent * for an owner. If you write alternative constructors, then you can't use them with CreateForm.

5- What happens if you call CreateForm and your form does not have a constructor that takes a single TComponent * as an owner? The answer is 'bad stuff happens'. Try it out. Create a new application. In the main form, add an AnsiString parameter to the existing constructor. Then add a ShowMessage or something to the body of the constructor. Like this:

__fastcall TForm1::TForm1(TComponent* Owner, const AnsiString &foo)
    : TForm(Owner)
    ShowMessage("Foo = " +  foo);

Make sure you change the header file too. Compile and link the app (no warnings or errors occur). Put a breakpoint on the ShowMessage call (if you can, hint hint). Run the app. Notice anything missing? Does the ShowMessage line ever execute?

No it doesn't. How come? How can a class be constructed without calling its constructor? The answer lies in that virtual constructor stuff. When you change the argument pattern for the constructor, you are no longer overriding the virtual constructor of the base class. Since CreateForm invokes the virtual constructor, and because your class does not override that virtual function, code execution jumps straight to the constructor of the base class, completely bypassing the derived class (ie *your* class). The initialization of your form and all its member variables is circumvented.

For these reasons, it is wise to use operator new for code that you write. CreateForm is one of those pascal things that C++ programmers are wise to avoid.

Always set a pointer variable to NULL or 0 after deleting it

Dereferencing a NULL pointer will always cause an access violation. Dereferencing a non-null pointer that has been deleted will sometimes raise an access violation, but many times, your program will continue to run. Sometimes, the program will appear to behave normally, and then mysteriously crash on you without warning. For debugging purposes, it's best to try and force your program to always crash when you dereference a pointer that has already been deleted. It is much easier to fix a program that always fails than it is to fix a program that sometimes fails.

Note: A couple of readers have pointed out some problems with this suggestion. Hendrik Schober had this to say

[Always setting] a pointer variable to NULL or 0 after deleting it has one disadvantage: It makes it harder to find multiple deletes.

Hendrik Schober

He has a good point. Deleting a null pointer is guaranteed to be safe. However, deleting the same pointer multiple times may be the result of poor coding. Setting your pointers to NULL will mask this problem. Chris Uzdavinis, a fellow TeamB member for C++ also had some comments:

There are two things that I think should be mentioned.

1) class member pointers that point to objects deallocated in a destructor need not be zeroed, because the class containing the pointer is going away and the dangling pointer problem doesn't exist. Zeroing the pointer is pure overhead without any benefit.

2) If a pointer has its object deleted, something non-zero but recognizable is preferable, IMHO, because when a particular address is referenced that is known to be out-of bounds, then you know what the error is immediately. Null pointers may or may not be problems.

Also, nullifying pointers encourages bad coding. People stop paying as much attention to writing correct code. Instead, there are lots of "if (x) x->do_someting()" kinds of statements, because the programmers seem to stop caring about if the pointer should be valid.


If a pointer must be set to anything, I think a non-zero value that is guarenteed to be invalid is a better solution.

Chris Uzdavinis

Both items are true. Point #1 highlights the fact that assigning NULL to all of your pointers is going to cost you some CPU cycles. For the most part, those CPU cycles are wasted cycles. Point #2 suggests that you use some other, non-zero constant to make the assignment. What's the benefit of this? Well, if you try to access a null pointer, the error box will say "Read of address 0". This error message does not provide much information about where the access violation occurred. Using a non-zero constant can help you isolate the problem.

You should way the pros and cons of this suggestion before deciding whether or not you actually want to use this technique.

Don't alter the makefile switches for alignment or enum variables

Note: This suggestion applies to BCB3. Borland fixed the VCL headers in BCB4 and BCB5. You can now change the project options for alignment and enums in BCB4 and BCB5 without worrying about messing up the VCL. The VCL headers contain #pragma guards to ensure correct alignment for VCL objects.

The VCL relies on dword alignment (-a4) and enum types that are not forced to be the same size as an int (-b-). If you switch the alignment mode to byte size alignment (-a1) or word alignment (-a2), you will start to see some strange access violations when you run your program. The same applies if you turn on the treat enums as integers compiler option (-b).

If you have a section of code that requires a different setting for alignment or enums, you can use the #pragma push and #pragma pop compiler directives.

#pragma option push -a1
struct Foo

  int  x;
  int  y;
#pragma pop

Don't link with LIB files or OBJ files created by other compilers

Visual C++ uses a derivative of the COFF file format format for LIB and OBJ files. Borland compilers use the OMF format. The two are not compatible. You cannot link with a LIB or OBJ file that was created with Visual C++ (or any other compiler for that matter).

Don't link with OBJ files created by a previous version of the Borland compiler

The OBJ file format changed in C++Builder 3. OBJ files created with older versions of C++Builder or Borland C++ are not compatible with the new format.

Don't import COM type libraries using the IDE menu option

Note: This suggestion applies only to BCB3. Ignore this suggestion for BCB4, BCB5, and newer.

C++Builder 3 contained a bug that made it difficult, if not impossible, to import type libraries. The patch to C++Builder 3 fixes many of these problems. However, the patch only fixes the command line TLIBIMP.EXE utility. The patch does not fix the code in the IDE that imports a type library when you choose the Project | Import Type Library menu item.

If you experience problems importing a particular type library, try to import the library using the command line utility instead of using the IDE. After running TLIBIMP, add the resulting files to your project.

When you import BCB3 forms into BCB4, set OldCreateOrder to false

Be careful when you migrate your projects from BCB3 to BCB4. You need to watch out for the OldCreateOrder property on your forms and datamodules. For some reason, when you import your BCB3 forms into BCB4, the OldCreateOrder property gets set to true. Having OldCreateOrder set to true can cause access violations when your forms are created or destroyed.

In C++Builder 4, TForm contains a new property called OldCreateOrder. If this property is true, the OnCreate event of your forms will execute before the constructor runs. Actually, the OnCreate handler will fire from within the Create method of TCustomForm. Recall that in C++, base class constructors execute before constructors in derived classes.

Having the OnCreate handler fire before your constructor runs creates a scenario that is compatible with Delphi 1, 2, and 3, and C++Builder 1. However, this backward compatability can also cause memory problems. If OldCreateOrder is true, your OnCreate handler will run before your constructor has a chance to initialize variables and construct member objects. Access violations can occur if your OnCreate handler attempts to dereference or use member variables. For this reason, it is best to always leave OldCreateOrder false. When OldCreateOrder is false, the OnCreate event is fired after your constructor runs. This mode is compatible with C++Builder 3.

If you upgrade a project to C++Builder 4, you should open each form in your project and manually set OldCreateOrder to false. You can leave OldCreateOrder set on true if you don't write OnCreate or OnDestroy handlers, but this could cause code maintenance headaches down the road.

The OldCreateOrder property also applies to the OnDestroy event. The following table explains how OldCreateOrder affects the sequence of events during a form's lifetime.

Table 1: Understanding the OldCreateOrder property in C++Builder 4.0
OldCreateOrder = false                               OldCreateOrder = true
 TCustomForm.Create                                   TCustomForm.Create
 TMyForm::TMyForm()                                   TMyForm::OnCreate
 TMyForm::OnCreate                                    TMyForm::TMyForm()
 ...                                                  ...
 TMyForm::OnDestroy                                   TMyForm::~TMyForm()T
 TMyForm::~TMyForm()                                  TMyForm::OnDestroy
 TCustomForm.Destroy                                  TCustomForm.Destroy

Don't use AsDateTime or AsInteger to assign one TField to another

Assigning the value of one field to another is a common task in database applications. In C++Builder or Delphi programs, you may find yourself writing code like this:

    Table1->FieldByName(seq_no)->AsInteger  = Table2seq_no->AsInteger;
    Table1->FieldByName(date)  ->AsDateTime = Table2date  ->AsDateTime;

As long as the fields in Table2 are not null, these two code statements work great. What happens when the fields in Table2 are null (ie, the fields contain no value)? You might think that the fields in Table1 will also be null after the AsInteger and AsDateTime assignments.

This isn't the way it works. When you assign a null field to another field through the AsInteger property, the VCL copies over a value of 0. The field will not be null. Likewise, the datetime field wont be null if you use AsDateTime to assign its value. It will also contain the value 0, which corresponds to a date of Jan 1 1900. In the sample above, the integer field in Table1 will contain the value 0 if Table2seq_no is empty, and the date field of Table1 will contain the date Jan 1 1900 if Table2date is null.

Assigning a null string field to another field through the AsString property works ok. However, AsCurrency, AsBoolean, and AsFloat suffer from the same problems that plague AsDateTime and AsInteger. As a workaround, you can use the AsVariant property. Variant types support the concept of a null value. If the source field is null, the destination field will also be null if you use the AsVariant property to assign its value. As an added bonus, you can use AsVariant on almost all field types. To use the AsVariant property, change the previous code to:

    Table1->FieldByName(seq_no)->AsVariant = Table2seq_no->AsVariant;
    Table1->FieldByName(date)  ->AsVariant = Table2date  ->AsVariant;

Don't use TCurrencyField unless you have to

TCurrencyField is a poorly named class. This field type is used to represent decimal numbers in a database. Since the class name is TCurrencyField, you might expect that the class uses the Currency datatype internally. This is not the case. TCurrencyField uses floating point numbers internally to store data.

Unfortunately, floating point numbers can lead to problems in database applications, particularly when dealing with fields that represent dollar amounts. Your dollar amounts may, at times, appear to be off by a penny, and to some bankers, this is a big deal! To see what I mean, run a program that displays a TCurrencyField in a DBEdit. Edit the field and set the amount to $12.1351. When the value is written, the half penny causes the .1351 to round up to .14. This is correct. Now try to enter 12.1350. Once again, the amount should be rounded up. When I test this on the orders database that comes with C++Builder, I see that the amount rounds down to $12.13. This may not seem like a big deal, but the problem gets worse when you start connecting to SQL servers, such as MS SQL Server and Oracle, where the database's internal format of a number may be different than the BDE's representation of the value.

The solution to this problem is to switch to the TBCDField class whenever possible. TBCDField represents its data internally using the Currency datatype. The Currency datatype doesn't suffer from the same formatting and rounding problems that plague floating point numbers. You can tell the BDE to use TBCDField's by default. To do so, run the BDE administrator, select the configuration tab, find the driver that you are using, and set the value of Enable BCD to true. The Enable BCD parameter can also be added to the Params property of TDatabase.

Don't turn on CachedUpdates when the Filtered property of dataset is on

When you turn on CachedUpdates, only the records that are active in the filter are cached. If this is the behavior that you expect, then it's no big deal. This presents a problem if you were expecting that all records would get cached, regardless of whether they meet the current filter requirements. For example:

    // assume that filters and cachedupdates are already off
    // filter out all records where the stat is not iowa
    Table1->Filter = "state = 'IA'";
    Table1->Filtered = true;
    Table1->CachedUpdates = true;

In this code example, only records where the state is Iowa will be cached.

Don't call ApplyUpdates when the Filtered property of dataset is on

If you call ApplyUpdates on a filtered dataset, only the active records that meed the filter requirements will be updated. Records that are hidden because of the filter will not be updated. This isn't so bad if you want this behavior. However, you will notice subtle data loss problems if you expect all records to get updated. Inserted records that are hidden because of the filter won't be inserted, and modified records that are hidden won't be updated.

To ensure that all records get applied, turn off filters while applying the updates.

    Query1->Filtered = false;
    Query1->Filtered = true;

Don't use the VCL's data aware controls

The data aware controls that come with C++Builder are extremely inflexible to change. Deriving a new control from TDBEdit or TDBCheckBox doesn't buy you much because just about everything in the base class is private. If you want to modify a data aware control, you essentially have to create a new control from scratch and duplicate the existing functionality that exists in the VCL class. Duplicating functionality is bad practice.

Take the TDBCheckBox class for example. I recently wanted to derive a new control that modified the behavior of the default DBCheckBox. All I wanted to do was to update the record as soon as the box was checked, instead of waiting until the user tabbed to a new control. In order to modify the control, I needed access to the TDataLink member of the base class. Unfortunately it is declared private, so I could't access it my derived class. Because the TDataLink and its associated methods were declared private, I could not modify the behavior of the control without resorting to hacks (and yes, the CM_GETDATALINK message is a hack).

Before you begin a large database project with C++Builder, I suggest that you copy all of the code from DBCTRLS.PAS into a new unit. This way, if you need to make a change or derive a new class, you can do so without hacking your way into the default VCL controls. If you find that you don't need to modify the default functionality, then you haven't lost anything.

Note: In order to avoid confusion with the existing VCL controls, I rename my altered controls. For example, TDBEdit becomes TXDBEdit, and TDBCheckBox becomes TXDBCheckBox. I then add these controls to my user package.

Don't use TDBLookupComboBox or TDBLookupListBox

Update: This suggestion applies to BCB3 and BCB4. The bug has been fixed in BCB5.

TDBLookupComboBox and TDBLookupListBox both derive from TDBLookupControl. Unfortunately, as of version 4.0 of the VCL, TDBLookupControl has a nasty bug that can cause access violations whenever a lookup object is destroyed.

The problem resides in the Destroy method of TDBLookupControl.

destructor TDBLookupControl.Destroy;
  FListLink.FDBLookupControl := nil;
  FDataLink.FDBLookupControl := nil;
  inherited Destroy;

Destroy is responsible for deleting two datalink objects called FListLink and FDataLink. Notice that neither object is set to nil after being deleting. This causes problems in the Notification method, which gets called when control passes to the base class Destroy methods via the inherited call. The Notification method looks like this.

procedure TDBLookupControl.Notification(AComponent: TComponent;
  Operation: TOperation);
  inherited Notification(AComponent, Operation);
  if Operation = opRemove then
    if (FDataLink <> nil) and (AComponent = DataSource) then DataSource := nil;
    if (FListLink <> nil) and (AComponent = ListSource) then ListSource := nil;

Observe how the Notification method first checks to see if the datalink objects are nil before it attempts to dereference them. The problem is that the destructor does not set the datalink objects to nil after it destroys them. This causes the if statement to pass even after the datalink objects have been deleted. As a result, the Notification method makes an assigment to deleted memory, which can cause access violations.

To work around this bug, you must either not use the lookup controls, or you will have to find a way to recode TLookupControl::Destroy so that it sets the datalink objects to nil after deleting them. If you heeded the advice about not using the built in data aware controls, then you can fix the Destroy function by changing it as shown below.

destructor TDBLookupControl.Destroy;
  FListLink.FDBLookupControl := nil;
  FListLink := nil;

  FDataLink.FDBLookupControl := nil;
  FDataLink := nil;

  inherited Destroy;

Note: Borland is aware of this bug.

Don't set the Active property of a dataset to true at design time

In my database projects, I have a large number of TQuery controls that function as lookup datasets for combo boxes and lookup fields. These controls usually perform a select * out of some lookup table. For these lookup datasets, it is often convenient to set Active to true at design time. However, you should avoid this temptation.

Here is the problem that I have witnessed. Whenever I open a data module in the IDE that contains a dataset where Active is true, the dataset attempts to perform the query against that database. What happens if the server is down, or a connection cannot be made to the database? If the dataset cannot be opened, C++Builder silently sets the Active property of the control back to false. This is bad. No warning is given that the control is being modified, and you won't notice the problem until you run your program. Lookup fields will appear to be blank, and lookup combo boxes will be empty.

To avoid this problem, especially in a large project, make the assignment to Active in code. The constructor of a data module is a good place to do this.

Don't change the Filter property of a dataset while the dataset is in edit mode

When you change the Filter or Filtered property of a dataset, the VCL calls the CheckBrowseMode function. This function posts any changes if the dataset was in insert or edit mode. This may or may not cause problems in your projects, but it is something to be aware of. Messing with the filter of a dataset takes the dataset out of edit mode.

Don't look at the value of UpdateStatus in the OnUpdateRecord handler of a dataset

UpdateStatus returns the cached update record status of the current record. When you apply cached updates, the VCL loops through and calls your OnUpdateRecord handler for each record that was modified, inserted, or deleted. However, the VCL does not navigate from record to record as it calls your handler. The record pointer, or cursor, does not move. As such, UpdateStatus does not change to reflect the status of the record being updated. It reflects the status of the current record, and not the record that is being updated.

Don't call Post after calling DisableControls on a dataset

The dataset classes in BCB provide two methods called DisableControls and EnableControls that come in handy when you have to perform some processing on the dataset. Calling DisableControls effectively disconnects the dataset from all of its data aware controls. The benefit of doing this is that you can work with a dataset without having the data-aware controls refreshing themselves all of the time.

There is something you should be aware of though. You never want to call the Post method after calling DisableControls. Why is this? Well, let's say that a user has entered something into a DBEdit control. A DBEdit control updates its field whenever you tab to a new control, or if you call the Post method. However, if you call DisableControls before calling Post, the changes that were made in the DBEdit control do not get posted to the dataset.

Note: This bug will only show up if the DBEdit retains the input focus during the entire time that DisableControls and Post are called. Well, how likely is that? More likely than you might think. If you call Post from a menu click event, or from the OnClick event of a toolbar button, then the DBEdit will keep the focus. If you call DisableControls before calling post, then changes that were made to the DBEdit control will be lost.

Copyright © 1997-2000 by Harold Howe.
All rights reserved.
0 0



取 消