1. 程式人生 > >每個學習C++BUILDER的人必須看的東西。

每個學習C++BUILDER的人必須看的東西。

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.



Examine the following code segment.

    AnsiString strText = ;
    char *ptr = strText.c_str();
    strText = ;
    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 =  + AnsiString(ptr1);
    Label2->Caption =  + 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.


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()
    {
        char buff[];
        wsprintf(buff, , src);
        *this = src;
    }

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


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).


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.


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.

    
    TList *list = new TList;
    const int nButtonCount = ;
    for(int j=; j<nButtonCount; j++)
    {
        list->Add(new Graphics::TBitmap);
    }

    

    
    for(int j=; 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:

    
    TList *list = new TList;
    const int nButtonCount = ;
    for(int j=; j<nButtonCount; j++)
    {
        list->Add(new Graphics::TBitmap);
    }

    

    
    for(int j=; 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.


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.


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.


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.


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:

class TForm1 : public TForm
{
__published:
    void __fastcall FormCreate(TObject *Sender);
public:
    AnsiString m_foo;
    __fastcall TForm1(TComponent* Owner);
}


__fastcall TForm1::TForm1(TComponent* Owner)
    : TForm(Owner)
{
}

void __fastcall TForm1::FormCreate(TObject *Sender)
{
    m_foo = ;
}

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()
{
}

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.


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);
}

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.


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.


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.

struct Foo

{
  int  x;
  int  y;
};


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).


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.


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.


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
--------------------------------------------------------------------------

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;

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.


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:

    
    
    Table1->Filter = ;
    Table1->Filtered = true;
    Table1->CachedUpdates = true;

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


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->ApplyUpdates();
    Query1->CommitUpdates();
    Query1->Filtered = true;

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.


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;
begin
  FListFields.Free;
  FListLink.FDBLookupControl := nil;
  FListLink.Free;
  FDataLink.FDBLookupControl := nil;
  FDataLink.Free;
  inherited Destroy;
end;

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);
begin
  inherited Notification(AComponent, Operation);
  if Operation = opRemove then
  begin
    if (FDataLink <> nil) and (AComponent = DataSource) then DataSource := nil;
    if (FListLink <> nil) and (AComponent = ListSource) then ListSource := nil;
  end;
end;

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;
begin
  FListFields.Free;
  FListLink.FDBLookupControl := nil;
  FListLink.Free;
  FListLink := nil;

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

  inherited Destroy;
end;

Note: Borland is aware of this bug.


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.


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.


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.


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.

 

相關推薦

每個學習C++BUILDER必須東西

The TeamB guide to avoiding common mistakes in C++Builder. This article contains a list of suggestions put together by the members of Tea

C++程式設計師必須的書

一、C++: C++ Primer (學習C++的都懂的) 高質量C/C++ (國內C/C++經典書籍) Effective C++ (提高程式碼質量的兩部書) More effective C++ 深入探索C++物件模型 STL原始碼剖析 設計模式: 大話設

Linux c學習--從標準輸入輸出流和緩沖區

pty 出錯 流的概念 code check 抽象 輸出信息 指針 架構 學習標準輸入輸出,我們都會遇到一個概念,流和緩沖區,但到底什麽是流,什麽是緩沖區呢? 書《C Primer Plus》上說,C程序處理一個流而不是直接處理文件。後面的解釋十分抽象:『流(s

C++builder中遠離惱的W8123 warning警告

今天使用了C++builder10.2Tokyo,在使用BCC32和BCC32C編譯器都遇到了W8123的警告,這對於一個合格的專案來說,是不能出現的答案,查閱了幫助文件,也沒找到解決答案,最終 在CSDN下面的這位老兄給出了答案。 引用文件:http://blog.csdn.net/Jo

如何學習C++之資料哪些,Visual C++入門及深入程式設計

(小編推薦一個學C語言/C++的學習群【 639912742】,入群即送C/C++全套學習資料) 學習C++有一小段時間了,剛開始的時候總感覺資料不知道看哪些比較好,於是總是比較耗費時間來找資料。相信大家在剛開始的時候也是這樣的。如果你是C++新手並想認真學習C++,以及想深入學習C++,那

新手入門學習c++你必須知道的發展前景和市場行情!

c++如今的市場不如從前,競爭很大,工資非常高,標準非常高,想要勝任一份高薪的工作不是那麼容易,只有掌握最新的行情才能更好的瞭解c++,百度內推文章,讓新手小白瞭解c++市場行情如下: 行情一:當下和五年前的區別,在五年我們這個行業不僅僅缺人才,連新手都缺,那個時候公司願意招聘實習生,培

少被坑!每個學習Python的需要經歷的問題

超過十年以上,沒有比直譯器全域性鎖(GIL)讓Python新手和專家更有挫折感或者更有好奇心。 未解決的問題 隨處都是問題。難度大、耗時多肯定是其中一個問題。僅僅是嘗試解決這個問題就會讓人驚訝。之前是整個社群的嘗試,但現在只是外圍的開發人員在努力。對於新手,去嘗試

學習C語言的教材、如何成為一名優秀的C程式設計師、激發程式設計師創意的6本書、國外程式設計師推薦:每個程式設計師都應讀的書

學習C語言的教材 我的C語言是自學的,這些年看過不少教材。 下面,我對其中一些教材做個點評。 1. How to Think Like a Computer Scientist: C version 這是我讀過最易懂的C語言教材。 雖然它只講

Spark學習入門(讓了想吐的話題)

這是個老生常談的話題,大家是不是看到這個文章標題就快吐了,本來想著手寫一些有技術深度的東西,但是

大學生學程式設計(七):學習C++必須先學C語言嗎?

很多初學程式設計的人都比較困惑和迷茫,C語言和C++到底有什麼區別和聯絡?學習C++是不是可以直接跳過C語言? 其實這個問題不難,就是直接瞭解兩者的聯絡和區別就可以給出答案。 歡迎加入學習群【892643663】,獲取全套免費C/C++企業實戰級課程資源(素材+原始碼+視訊)和編譯大

學習c++,大學畢業時必須掌握哪些課程和技能?

作為一個c++的開發人員,大學畢業時必須掌握哪些課程,和實際技能呢? 對於大學生來說,畢業就得找工作,找工作就得符合公司的需求。當然面試可能會遇到你和麵試官技術不匹配的情況,不過大多數情況,面試官絕對不會輕易的刷掉一個基礎特別好的同學。 1、什麼是基礎? 基礎就是大學上的那幾門

為什麼這麼多學習C語言/C++程式設計,最終放棄的三個理由!!

為什麼這麼多人學習C語言/C++程式設計,最終放棄的三個理由!! 趣味程式設計小夥伴 2018-12-28 16:15 C語言是面向過程的,而C++是面向物件的 C和C++的區別: C是一個結構化語言,它的重點在於演算法和資料結構。C程式的設計首要考慮的是如何通過一個過程,對輸入

學習C++應該的書

前段時間偶然看到的這個東西.雖然已經學過了C++,但是確實不能說是精通..所以以後這些書也是我需要認真研究的..希望貼在這裡會對大家有用.其中有幾本是相當經典的,網路上一般都可以搜得到,我這裡也大部分都有,如果找不到可以向我索取.下面是書名... C++/OPP/OOD系列:

畢淑敏:人生本沒有意義,每個必須為自己的人生確定意義

□我覺得中國人太講究生存的數量,而不在乎生存的質量 □人生本沒有意義,每個人必須為自己的人生確定意義 □死亡是一個自然的現象,現在卻被工業化了 畢淑敏說,她從來沒和這麼年輕的朋友談過生死,她沒有把握大學生對這個題目是否有興趣,而我們這個民族是不喜歡談死亡的,只談人生的幾大樂趣,就是福、壽、祿。但是,當她站在大

為什麽學習C語言這麽久,的懂代碼,做不出題沒項目

量變 技術 題目 而且 學c++ 運行時 暫時 功夫 自己的 我看得懂別人的程序,可是我自己卻寫不出來,我應該怎麽辦啊?你了解這些嘛? 你只是能從別人書寫的代碼知道每一步都做些什麽吧? 你明白別人的解題思路嗎? 你知道別人為什麽要用那樣的算法嗎? 如果你看著題目,你能寫出實

每個學習多線程的Java程序員都必須掌握的volatile關鍵字解析

gem 都是 線程安全問題 條件 程序 改變 工作 成員變量 sync volatile基本介紹 volatile可以看成是synchronized的一種輕量級的實現,但volatile並不能完全代替synchronized,volatile有synchronized可見

通過例子進階學習C++(四)計算2的64次方,不服寫寫

本文是通過例子學習C++的第四篇,通過這個例子可以快速入門c++相關的語法。 1.乍一看題目非常簡單,簡單思考一下,可以通過for迴圈實現: #include <iostream> using namespace std; int main() { int num = 1; for

c++Builder debug DataSet Visualizer

table hand r報錯 可能 exce exc dsd dataset mta c++Builder debug DataSet Visualizer delphi 正常,c++builder報錯。 fdMemTable->SaveToFile("d:\\DSd

Victor 串口控件 1.5.0.6 VCL/FMX for C++ Builder 10.2 Tokyo, 10.1 Berlin, 10.0 Seattle, XE8, XE7, XE6 已經發布

blank sms mac 使用 模板 www 文本 clas stat Victor 串口控件 1.5.0.6 更新內容: ? 增加支持 FMX (Firemonkey) Win32/Win64,控件包含 VCL 和 FMX 兩個框架版本的,可以同時安裝 ? 增加

網站增加《C++ Builder 操作指南》欄目,歡迎拍磚

dal cell res cpp position tsp cli welcome page 網站增加《C++ Builder 操作指南》欄目 http://www.cppfans.com/cbknowledge/opguide/ 歡迎拍磚 文檔索引 IDE外