Archive:Development/Tutorials/Common Programming Mistakes (zh TW): Difference between revisions

From KDE TechBase
(Created page with '{{Template:I18n/Language Navigation Bar|Development/Tutorials/Common Programming Mistakes}} {{TutorialBrowser_(zh_TW)| series=入門| name=常見程式設計錯誤| reading=[...')
 
 
(15 intermediate revisions by 2 users not shown)
Line 1: Line 1:
{{Template:I18n/Language Navigation Bar|Development/Tutorials/Common Programming Mistakes}}
{{Template:I18n/Language Navigation Bar_(zh_TW)|Development/Tutorials/Common Programming Mistakes}}


{{TutorialBrowser_(zh_TW)|
{{TutorialBrowser_(zh_TW)|
Line 13: Line 13:
== 摘要 ==
== 摘要 ==


This tutorial aims to combine the experience of KDE developers regarding Qt and KDE frameworks dos and don'ts. Besides actual mistakes, it also covers things which are not necessarily "bugs" but which make the code either slower or less readable.
本教學的目的是結合 KDE 開發者的經驗,關於 Qt KDE 框架該做什麼和什麼不該做。除了實際的錯誤,還包括不一定是「bug」的東西,但會使程式碼變慢或缺乏可讀性。


== 一般的 C++ ==
== 一般的 C++ ==


This section guides you through some of the more dusty corners of C++ which either tend to be misused or which people often simply get wrong.
本節將指導您一些 C++ 中積灰塵的角落,這些往往被濫用,或是人們常常出錯的地方。


=== Anonymous namespaces vs statics ===
=== Anonymous namespaces vs 靜態(static)===


If you have a method in a class that does not access any members and therefore does not need an object to operate, make it static. If additionally it is a private helper function that is not needed outside of the file, make it a file-static function. That hides the symbol completely.
如果你有一個類別的方法,不存取任何成員,因此並不需要物件來操作;使他成為靜態。如果他還是私有(private)函式,那麼在檔案之外就不需要他;使其成為一個靜態檔案(file-static)函式。這將完全隱藏 symbol。


Symbols defined in a C++ anonymous namespace do not have internal linkage. Anonymous namespaces only give a unique name for that translation unit and that is it; they do not change the linkage of the symbol at all. Linkage is not changed on those because the second phase of two-phase name lookup ignores functions with internal linkages. Also, entities with internal linkage cannot be used as template arguments.
定義在 C++ anonymous namespace 中的 Symbols,不具有內在聯繫。anonymous namespace只提供一個唯一的名稱給翻譯單位和本身,他們不改變 symbol 的聯繫。那些聯繫是沒有改變的,因為第二階段的名稱查找會忽略函式的內在聯繫。此外,實體的內部聯繫不能作為範本(Template)參數。


So for now instead of using anonymous namespaces use static if you do not want a symbol to be exported.
So for now instead of using anonymous namespaces use static if you do not want a symbol to be exported.


=== NULL pointer issues ===
=== 空指標(null pointer)問題 ===


First and foremost: it is fine to delete a null pointer. So constructs like this that check for null before deleting are simply redundant:
首先最重要的一點:刪除空指標是沒有影響的。所以,刪除之前檢查是否為空是多餘的:


<code cppqt>
<syntaxhighlight lang="cpp-qt">
if ( ptr ) {
if ( ptr ) {
   delete ptr;
   delete ptr;
}
}
</code>
</syntaxhighlight>


Note however, that '''a null check ''is'' required when you delete an array''' - that's because a relatively recent compiler on Solaris does not handle it properly otherwise.
Note however, that '''a null check ''is'' required when you delete an array''' - that's because a relatively recent compiler on Solaris does not handle it properly otherwise.


When you delete a pointer, make sure you also set it to 0 so that future attempts to delete that object will not fail in a double delete. So the complete and proper idiom is:
當你刪除指標後,一定要還設定為0,這樣將來嘗試刪除物件不會因為雙重刪除而失敗。因此,完整和正確的寫法是:


<code cppqt>
<syntaxhighlight lang="cpp-qt">
delete ptr;  
delete ptr;  
ptr = 0;
ptr = 0;
</code>
</syntaxhighlight>


You may notice that null pointers are marked variously in one of three ways: 0, 0L and NULL. In C, NULL is defined as a null void pointer. However, in C++, this is not possible due to stricter type checking. Therefore, modern C++ implementations define it to a "magic" null pointer constant which can be assigned to any pointer. Older C++ implementations, OTOH, simply defined it to 0L or 0, which provides no additional type safety - one could assign it to an integer variable, which is obviously wrong.
您可能會注意到空指標有三種標記方法:0、0L 和 NULL。在C中,NULL 定義為一個null void指標。然而,在C++,由於嚴格的類型檢查,這是不可能的。因此,現代C++實作定義它為「magic」空指標常量(null pointer constant),任何指標都可以指定成它。舊的 C++ 實作,另一方面來說,簡單定義它為0L或0,而沒有提供額外的類型安全 - 人們可以指定它為整數變數,這顯然是錯的。


In pointer context, the integer constant zero means "null pointer" - irrespective of the actual binary representation of a null pointer. This means that the choice between 0, 0L and NULL is a question of personal style and getting used to something rather than a technical one - as far as the code in KDE's SVN goes you will see 0 used more commonly than NULL.
在指標背景方面,整數常數零意思是「空指標」 - 空指標的實際二進位表示法。這意味著選擇0、0L和NULL是一個個人風格和習慣的問題,而不是技術性的。就 KDE SVN 中的程式碼而言,你會看到使用0比NULL更普遍。
 
Note, however, that if you want to pass a null pointer constant to a function in a variable argument list, you *must* explicitly cast it to a pointer - the compiler assumes integer context by default, which might or might not match the binary representation of a pointer. Again, it does not matter whether you cast 0, 0L or NULL, but the shorter representation is generally preferred.
但是請注意,如果你想傳遞一個空指標常量給變數參數列表中的一個函數,你必須顯式轉換它為指標 - 編譯器預設情況下假定為整數,可能符合或不符合指標的二進位表示法。同樣的,不論你用0、0L 或 NULL 都沒關係,通常比較喜歡用較短的表示法。


=== 成員變數 ===
=== 成員變數 ===


You will encounter four major styles of marking class member variables in KDE, besides unmarked members:
KDE 中除了未標記的成員,你會遇到四種主要的類別成員變數標記風格:


* '''m_variable''' lowercase m, underscore and the name of the variable starting with a lowercase letter. This is the most common style and one prefered for code in kdelibs.
* '''m_variable''' 小寫m、底線和以小寫字母開始的變數名稱。這是最常見的風格和 kdelibs 中偏好的程式碼風格。
* '''mVariable''' lowercase m and the name of variable starting with a uppercase letter
* '''mVariable''' 小寫 m 和以大寫字母開始的變數名稱。
* '''variable_''' variable name starting with a lowercase letter and then an underscore
* '''variable_''' 以小寫字母開始的變數名稱,然後一個底線。
* '''_variable''' underscore and the name of variable starting with a lowercase letter. This style is actually usually frowned upon as this notation is also used in some code for function parameters instead.
* '''_variable''' 底線和以小寫字母開始的變數名稱。這種風格通常是令人難以接受的,因為這種標記也用在一些代替函數參數的程式碼。


Unmarked members are more common in the case of classes that use [[Policies/Library Code Policy#D-Pointers|d-pointers]].
未標記成員常見於使用 [[Policies/Library Code Policy#D-Pointers|d-pointers]] 的類別。


As it often happens there is not one correct way of doing it, so remember to always follow the syntax used by the application/library to which you are committing. If you're creating a new file, you may want to follow the coding style of the library or module you're adding the file to.
由於沒有一個正確的做法,所以記得要遵循您正在提交的應用程式或函式庫,使用的語法。如果要建立一個新檔案,您可能需要遵循,您要加入檔案的函式庫或模塊的程式碼風格。


Note that symbols starting with undercores are reserved to the C library (underscore followed by capital or double underscore are reserved to the compiler), so if you can, avoid using the last type.
請注意,底線符號開始是保留給 C 函式庫(底線加上大寫或雙底線是保留給編譯器),所以如果可以的話,請避免使用上述的類型。


=== 靜態變數 ===
=== 靜態變數 ===


Try to limit the number of static variables used in your code, especially when committing to a library. Construction and initialization of large number of static variables really hurts the startup times.
嘗試減少靜態變數的使用量,尤其是當提交給函式庫時。建構和大量靜態變數初始化會嚴重增加啟動時間。


Do not use class-static variables, especially not in libraries and loadable modules though it is even discouraged in applications. Static objects lead to lots of problems such as hard to debug crashes due to undefined order of construction/destruction.
不要使用靜態類別(class-static)變數,尤其是函式庫和可裝入模塊。即使在應用程式也是不鼓勵使用。靜態物件會導致很多問題,如由於未定義建構或解構的次序,難以對 crashes 進行除錯。


Instead, use a static pointer, together with <tt>K_GLOBAL_STATIC</tt> which is defined in <tt>kglobal.h</tt> and is used like this:
相反地,請使用靜態指標,連同 <tt>kglobal.h</tt> 中定義的 <tt>K_GLOBAL_STATIC</tt> 一起使用:


<code cppqt>
<syntaxhighlight lang="cpp-qt">
class A { ... };
class A { ... };


Line 99: Line 99:
     qAddPostRoutine(globalA.destroy);
     qAddPostRoutine(globalA.destroy);
}
}
</code>
</syntaxhighlight>


See the [http://www.englishbreakfastnetwork.org/apidocs/apidox-kde-4.0/kdelibs-apidocs/kdecore/html/kglobal_8h.html#75ca0c60b03dc5e4f9427263bf4043c7 API documentation] for <tt>K_GLOBAL_STATIC</tt> for more information.
請參閱 <tt>K_GLOBAL_STATIC</tt> 的[http://api.kde.org/4.x-api/kdelibs-apidocs/kdecore/html/group__KDEMacros.html#ga75ca0c60b03dc5e4f9427263bf4043c7 API 文件]了解更多資訊。


=== Constant 資料 ===
=== 常量(Constant)資料 ===


If you need some constant data of simple data types in several places, you do good by defining it once at a central place, to avoid a mistype in one of the instances. If the data changes there is also only one place you need to edit.
如果你在幾個地方需要一些簡單資料類型的常量資料。你最好在一個集中的地方只定義它們一次,以避免mistype,在其中一個實例(instance)。如果資料改變也只需要修改一個地方。


Even if there is only one instance you do good by defining it elsewhere, to avoid so-called "magic numbers" in the code which are unexplained (cmp. 42). Usually this is done at the top of a file to avoid searching for it.
即使只有一個實例,你最好還是定義在其他地方,以避免程式碼有所謂的「magic numbers」,這是無法解釋(cmp. 42)。通常是放在檔案的頂部,以避免要搜尋它。


Define the constant data using the language constructs of C++, not the preprocessor instructions, like you may be used to from plain C. This way the compiler can help you to find mistakes by doing type checking.
定義常量資料請使用C++的語言結構,而不用前置處理器(preprocessor)指令,就像你使用單純的C。這樣編譯器可以透過類型檢查幫助你找到錯誤。


<code cppqt n>
<syntaxhighlight lang="cpp-qt" line>
// Correct!
// 正確!
static const int AnswerToAllQuestions = 42;
static const int AnswerToAllQuestions = 42;
// Wrong!
// 錯誤!
#define AnswerToAllQuestions 42
#define AnswerToAllQuestions 42
</code>
</syntaxhighlight>




If defining a constant array do not use a pointer as data type. Instead use the data type and append the array symbol with undefined length, <tt>[]</tt>, behind the name. Otherwise you also define a variable to some const data. That variable could mistakenly be assigned a new pointer to, without the compiler complaining about. And accessing the array would have one indirection, because first the value of the variable needs to be read.
如果定義了一個常量陣列(Array)不要使用指標的作為資料型態。而是使用資料型態和未定義長度附加陣列符號,<tt>[]</tt>,在名稱的後面。否則,你還可以定義變數在一些常量資料。這些變數可能錯誤地被指定為新的指標,而編譯器不會出現錯誤。存取陣列應該要有一個間接,因為第一次的變數值需要被讀取。


<code cppqt>
<syntaxhighlight lang="cpp-qt">
// Correct!
// 正確!
static const char SomeString[] = "Example";
static const char SomeString[] = "Example";
// Wrong!
// 錯誤!
static const char* SomeString = "Example";
static const char* SomeString = "Example";
// Wrong!
// 錯誤!
#define SomeString "Example"
#define SomeString "Example"
</code>
</syntaxhighlight>


=== 提前定義 ===
=== 提前宣告 ===


You will reduce compile times by forward declaring classes when possible instead of including their respective headers. For example:
提前宣告類別,而不是含入他們各自的標頭檔,可以減少編譯時間。例如:


<code cppqt>
<syntaxhighlight lang="cpp-qt">
#include <QWidget>    // slow
#include <QWidget>    //
#include <QStringList> // slow
#include <QStringList> //
#include <QString>    // slow
#include <QString>    //
class SomeInterface
class SomeInterface
{
{
Line 145: Line 145:
     virtual void stringListAction( const QStringList& strList ) =0;
     virtual void stringListAction( const QStringList& strList ) =0;
};
};
</code>   
</syntaxhighlight>   
    
    
The above should instead be written like this:
上述內容應當改成這樣寫:


<code cppqt>
<syntaxhighlight lang="cpp-qt">
class QWidget;    // fast
class QWidget;    //
class QStringList; // fast
class QStringList; //
class QString;    // fast
class QString;    //
class SomeInterface
class SomeInterface
{
{
Line 160: Line 160:
     virtual void stringListAction( const QStringList& strList ) =0;
     virtual void stringListAction( const QStringList& strList ) =0;
};
};
</code>
</syntaxhighlight>


=== 迭代器 ===
=== 迭代器 ===
Line 169: Line 169:
Cache the return of the <tt>end()</tt> (or <tt>constEnd()</tt>) method call before doing iteration over large containers. For example:
Cache the return of the <tt>end()</tt> (or <tt>constEnd()</tt>) method call before doing iteration over large containers. For example:


<code cppqt>
<syntaxhighlight lang="cpp-qt">
QList<SomeClass> container;
QList<SomeClass> container;


Line 180: Line 180:
     // use *itr (or itr.value()) here
     // use *itr (or itr.value()) here
}
}
</code>
</syntaxhighlight>


This avoids the unnecessary creation of the temporary <tt>end()</tt> (or <tt>constEnd()</tt>) return object on each loop iteration, largely speeding it up.
This avoids the unnecessary creation of the temporary <tt>end()</tt> (or <tt>constEnd()</tt>) return object on each loop iteration, largely speeding it up.
Line 190: Line 190:
When you want to erase some elements from the list, you maybe would use code similar to this:
When you want to erase some elements from the list, you maybe would use code similar to this:


<code cppqt>
<syntaxhighlight lang="cpp-qt">
QMap<int, Job *>::iterator it = m_activeTimers.begin();
QMap<int, Job *>::iterator it = m_activeTimers.begin();
QMap<int, Job *>::iterator itEnd = m_activeTimers.end();
QMap<int, Job *>::iterator itEnd = m_activeTimers.end();
Line 201: Line 201:
     }
     }
}
}
</code>
</syntaxhighlight>


This code will potentially crash because it is a dangling iterator after the call to erase().
This code will potentially crash because it is a dangling iterator after the call to erase().
You have to rewrite the code this way:
You have to rewrite the code this way:
<code cppqt>
<syntaxhighlight lang="cpp-qt">
QMap<int, Job *>::iterator it = m_activeTimers.begin();
QMap<int, Job *>::iterator it = m_activeTimers.begin();
while (it != m_activeTimers.end()) {
while (it != m_activeTimers.end()) {
Line 216: Line 216:
     }
     }
}
}
</code>
</syntaxhighlight>
This problem is also discussed in the [http://doc.trolltech.com/4.3/qmap-iterator.html#details Qt documentation for QMap::iterator] but applies to '''all''' Qt iterators
This problem is also discussed in the [http://doc.trolltech.com/4.3/qmap-iterator.html#details Qt documentation for QMap::iterator] but applies to '''all''' Qt iterators


=== 記憶體洩漏 ===
=== 記憶體洩漏 ===


A very "popular" programming mistake is to do a <tt>new</tt> without a <tt>delete</tt> like in this program:
一個非常「常犯」的程式設計錯誤是實做 <tt>new</tt> 而沒有 <tt>delete</tt>,就這個程式一樣:


'''mem_gourmet.cpp'''
'''mem_gourmet.cpp'''
<code cppqt>
<syntaxhighlight lang="cpp-qt">
class t
class t
{
{
Line 240: Line 240:
   while (true) pollute();
   while (true) pollute();
}
}
</code>
</syntaxhighlight>


You see, ''pollute()'' instanciates a new object ''polluter'' of the class ''t''. Then, the variable ''polluter'' is lost because it is local, but the content (the object) stays on the heap. I could use this program to render my computer unusable within 10 seconds.
你看,''pollute()''建立一個新的物件,''t''類別的''polluter''。然後,''polluter'' 變數丟失,因為它是區域(local)的,但其內容(物件)仍留在 heap 中。我可以使用這個程式讓電腦在10秒內無法使用。


To solve this, there are the following approaches:
為了解決這個問題,有以下方法:
* keep the variable on the stack instead of the heap:
* 保持變數在 stack 而不是 heap:
<code cppqt>
<syntaxhighlight lang="cpp-qt">
   t* polluter = new t();
   t* polluter = new t();
</code>
</syntaxhighlight>
would become
改為
<code cppqt>  
<syntaxhighlight lang="cpp-qt">  
   t polluter;
   t polluter;
</code>
</syntaxhighlight>
* delete polluter using the complementing function to new:
* 刪除polluter,使用 new 的補充函式:
<code cppqt>  
<syntaxhighlight lang="cpp-qt">  
   delete polluter;
   delete polluter;
</code>
</syntaxhighlight>
* stop the polluter in an [http://www.cppreference.com/wiki/stl/memory/auto_ptr std::auto_ptr] (which will automatically delete the polluter when returning from the method)
* [http://www.cppreference.com/wiki/stl/memory/auto_ptr std::auto_ptr] 停止 polluter(這在從該方法返回時,將自動刪除 polluter)
<code cppqt>
<syntaxhighlight lang="cpp-qt">
  std::auto_ptr<t> polluter = new t();
  std::auto_ptr<t> polluter = new t();
</code>
</syntaxhighlight>


A tool to detect memory leaks like this is [[Development/Tools/Valgrind|Valgrind]].
[[Development/Tools/Valgrind|Valgrind]]之類的工具可以檢測記憶體洩漏。


=== dynamic_cast ===
=== dynamic_cast ===
Line 283: Line 283:
== 程式設計 ==
== 程式設計 ==


In this section we will go over some common problems related to the design of Qt/KDE applications.
在本節中,我們將繼續與 Qt/KDE 應用程式設計相關的常見問題。


=== 延遲初始化 ===
=== 延遲初始化(delayed initialization) ===


Although the design of modern C++ applications can be very complex, one recurring problem, which is generally easy to fix, is not using the technique of [http://www.kdedevelopers.org/node/509 delayed initialization].
儘管設計現代 C++ 應用程式可能會非常複雜。一個反覆出現的問題,通常是很容易解決的。是沒有使用[http://www.kdedevelopers.org/node/509 延遲初始化]的技術。


First, let us look at the standard way of initializing a KDE application:
首先,讓我們看看初始化 KDE 應用程式的標準方式:


<code cppqt>
<syntaxhighlight lang="cpp-qt">
int main( int argc, char **argv )
int main( int argc, char **argv )
{
{
Line 306: Line 306:
     return a.exec();
     return a.exec();
}
}
</code>
</syntaxhighlight>
      
      
Notice that <tt>window</tt> is created before the <tt>a.exec()</tt> call that starts the event loop. This implies that we want to avoid doing anything non-trivial in the top-level constructor, since it runs before we can even show the window.
Notice that <tt>window</tt> is created before the <tt>a.exec()</tt> call that starts the event loop. This implies that we want to avoid doing anything non-trivial in the top-level constructor, since it runs before we can even show the window.
Line 312: Line 312:
The solution is simple: we need to delay the construction of anything besides the GUI until after the event loop has started. Here is how the example class MainWindow's constructor could look to achieve this:
The solution is simple: we need to delay the construction of anything besides the GUI until after the event loop has started. Here is how the example class MainWindow's constructor could look to achieve this:


<code cppqt>
<syntaxhighlight lang="cpp-qt">
MainWindow::MainWindow()
MainWindow::MainWindow()
{
{
Line 339: Line 339:
     */
     */
}
}
</code>
</syntaxhighlight>
    
    
Using this technique may not buy you any overall time, but it makes your app ''seem'' quicker to the user who is starting it. This increased perceived responsiveness is reassuring for the user as they get quick feedback that the action of launching the app has succeeded.
Using this technique may not buy you any overall time, but it makes your app ''seem'' quicker to the user who is starting it. This increased perceived responsiveness is reassuring for the user as they get quick feedback that the action of launching the app has succeeded.
Line 347: Line 347:
== 資料結構 ==
== 資料結構 ==


In this section we will go over some of our most common pet-peeves which affect data structures very commonly seen in Qt/KDE applications.
在本節中,我們會繼續一些最常見的 pet-peeves,在 Qt/KDE 應用程式中影響資料結構非常常見。


=== Passing non-POD types ===
=== Passing non-POD types ===
Line 357: Line 357:
So the canonical signature of a method taking QString arguments is:
So the canonical signature of a method taking QString arguments is:


<code cppqt>
<syntaxhighlight lang="cpp-qt">
void myMethod( const QString & foo, const QString & bar );
void myMethod( const QString & foo, const QString & bar );
</code>
</syntaxhighlight>


=== QObject ===
=== QObject ===
Line 365: Line 365:
If you ever need to delete a QObject derived class from within one of its own methods, do not ever delete it this way:  
If you ever need to delete a QObject derived class from within one of its own methods, do not ever delete it this way:  


<code cppqt>
<syntaxhighlight lang="cpp-qt">
   delete this;
   delete this;
</code>
</syntaxhighlight>


This will sooner or later cause a crash because a method on that object might be invoked from the Qt event loop via slots/signals after you deleted it.
This will sooner or later cause a crash because a method on that object might be invoked from the Qt event loop via slots/signals after you deleted it.
Line 377: Line 377:
It is common to want to see if a {{qt|QString}} is empty. Here are three ways of doing it, the first two of which are correct:
It is common to want to see if a {{qt|QString}} is empty. Here are three ways of doing it, the first two of which are correct:


<code cppqt>
<syntaxhighlight lang="cpp-qt">
// Correct
// Correct
if ( mystring.isEmpty() ) {
if ( mystring.isEmpty() ) {
Line 389: Line 389:
if ( mystring == "" ) {
if ( mystring == "" ) {
}
}
</code>
</syntaxhighlight>


While there is a distinction between "null" {{qt|QString}}s and empty ones, this is a purely historical artifact and new code is discouraged from making use of it.
While there is a distinction between "null" {{qt|QString}}s and empty ones, this is a purely historical artifact and new code is discouraged from making use of it.
Line 410: Line 410:
* Accumulate data chunks in the slots and process them each time a newline arrives or after some timeout passes. [http://websvn.kde.org/trunk/KDE/kdevplatform/util/processlinemaker.cpp?view=markup Example code]
* Accumulate data chunks in the slots and process them each time a newline arrives or after some timeout passes. [http://websvn.kde.org/trunk/KDE/kdevplatform/util/processlinemaker.cpp?view=markup Example code]


=== QString and QByteArray ===
=== QString QByteArray ===


While {{qt|QString}} is the tool of choice for many string handling situations, there is one where it is particularly inefficient. If you are pushing about and working on data in {{qt|QByteArray}}s, take care not to pass it through methods which take {{qt|QString}} parameters; then make QByteArrays from them again.
While {{qt|QString}} is the tool of choice for many string handling situations, there is one where it is particularly inefficient. If you are pushing about and working on data in {{qt|QByteArray}}s, take care not to pass it through methods which take {{qt|QString}} parameters; then make QByteArrays from them again.
Line 416: Line 416:
For example:  
For example:  


<code cppqt>
<syntaxhighlight lang="cpp-qt">
QByteArray myData;
QByteArray myData;
QString myNewData = mangleData( myData );
QString myNewData = mangleData( myData );
Line 425: Line 425:
     return QString(str);
     return QString(str);
}
}
</code>
</syntaxhighlight>
      
      
The expensive thing happening here is the conversion to {{qt|QString}}, which does a conversion to Unicode internally. This is unnecessary because, the first thing the method does is convert it back using <tt>toLatin1()</tt>. So if you are sure that the Unicode conversion is not needed, try to avoid inadvertently using QString along the way.  
The expensive thing happening here is the conversion to {{qt|QString}}, which does a conversion to Unicode internally. This is unnecessary because, the first thing the method does is convert it back using <tt>toLatin1()</tt>. So if you are sure that the Unicode conversion is not needed, try to avoid inadvertently using QString along the way.  
Line 431: Line 431:
The above example should instead be written as:
The above example should instead be written as:


<code cppqt>
<syntaxhighlight lang="cpp-qt">
QByteArray myData;
QByteArray myData;
QByteArray myNewData = mangleData( myData );
QByteArray myNewData = mangleData( myData );


QByteArray mangleData( const QByteArray& data )
QByteArray mangleData( const QByteArray& data )
</code>
</syntaxhighlight>


=== QDomElement ===
=== QDomElement ===
Line 442: Line 442:
When parsing XML documents, one often needs to iterate over all the elements. You may be tempted to use the following code for that:  
When parsing XML documents, one often needs to iterate over all the elements. You may be tempted to use the following code for that:  


<code cppqt>
<syntaxhighlight lang="cpp-qt">
for ( QDomElement e = baseElement.firstChild().toElement();
for ( QDomElement e = baseElement.firstChild().toElement();
       !e.isNull();
       !e.isNull();
Line 448: Line 448:
       ...
       ...
}
}
</code>
</syntaxhighlight>
    
    
That is not correct though: the above loop will stop prematurely when it encounters a {{qt|QDomNode}} that is something other than an element such as a comment.
That is not correct though: the above loop will stop prematurely when it encounters a {{qt|QDomNode}} that is something other than an element such as a comment.
Line 454: Line 454:
The correct loop looks like:  
The correct loop looks like:  


<code cppqt>
<syntaxhighlight lang="cpp-qt">
for ( QDomNode n = baseElement.firstChild(); !n.isNull();
for ( QDomNode n = baseElement.firstChild(); !n.isNull();
       n = n.nextSibling() ) {
       n = n.nextSibling() ) {
Line 463: Line 463:
     ...
     ...
}
}
</code>
</syntaxhighlight>

Latest revision as of 13:16, 23 June 2013

Template:I18n/Language Navigation Bar (zh TW)

Template:TutorialBrowser (zh TW)

摘要

本教學的目的是結合 KDE 開發者的經驗,關於 Qt 和 KDE 框架該做什麼和什麼不該做。除了實際的錯誤,還包括不一定是「bug」的東西,但會使程式碼變慢或缺乏可讀性。

一般的 C++

本節將指導您一些 C++ 中積灰塵的角落,這些往往被濫用,或是人們常常出錯的地方。

Anonymous namespaces vs 靜態(static)

如果你有一個類別的方法,不存取任何成員,因此並不需要物件來操作;使他成為靜態。如果他還是私有(private)函式,那麼在檔案之外就不需要他;使其成為一個靜態檔案(file-static)函式。這將完全隱藏 symbol。

定義在 C++ 的 anonymous namespace 中的 Symbols,不具有內在聯繫。anonymous namespace只提供一個唯一的名稱給翻譯單位和本身,他們不改變 symbol 的聯繫。那些聯繫是沒有改變的,因為第二階段的名稱查找會忽略函式的內在聯繫。此外,實體的內部聯繫不能作為範本(Template)參數。

So for now instead of using anonymous namespaces use static if you do not want a symbol to be exported.

空指標(null pointer)問題

首先最重要的一點:刪除空指標是沒有影響的。所以,刪除之前檢查是否為空是多餘的:

if ( ptr ) {
   delete ptr;
}

Note however, that a null check is required when you delete an array - that's because a relatively recent compiler on Solaris does not handle it properly otherwise.

當你刪除指標後,一定要還設定為0,這樣將來嘗試刪除物件不會因為雙重刪除而失敗。因此,完整和正確的寫法是:

delete ptr; 
ptr = 0;

您可能會注意到空指標有三種標記方法:0、0L 和 NULL。在C中,NULL 定義為一個null void指標。然而,在C++,由於嚴格的類型檢查,這是不可能的。因此,現代C++實作定義它為「magic」空指標常量(null pointer constant),任何指標都可以指定成它。舊的 C++ 實作,另一方面來說,簡單定義它為0L或0,而沒有提供額外的類型安全 - 人們可以指定它為整數變數,這顯然是錯的。

在指標背景方面,整數常數零意思是「空指標」 - 空指標的實際二進位表示法。這意味著選擇0、0L和NULL是一個個人風格和習慣的問題,而不是技術性的。就 KDE SVN 中的程式碼而言,你會看到使用0比NULL更普遍。

但是請注意,如果你想傳遞一個空指標常量給變數參數列表中的一個函數,你必須顯式轉換它為指標 - 編譯器預設情況下假定為整數,可能符合或不符合指標的二進位表示法。同樣的,不論你用0、0L 或 NULL 都沒關係,通常比較喜歡用較短的表示法。

成員變數

在 KDE 中除了未標記的成員,你會遇到四種主要的類別成員變數標記風格:

  • m_variable 小寫m、底線和以小寫字母開始的變數名稱。這是最常見的風格和 kdelibs 中偏好的程式碼風格。
  • mVariable 小寫 m 和以大寫字母開始的變數名稱。
  • variable_ 以小寫字母開始的變數名稱,然後一個底線。
  • _variable 底線和以小寫字母開始的變數名稱。這種風格通常是令人難以接受的,因為這種標記也用在一些代替函數參數的程式碼。

未標記成員常見於使用 d-pointers 的類別。

由於沒有一個正確的做法,所以記得要遵循您正在提交的應用程式或函式庫,使用的語法。如果要建立一個新檔案,您可能需要遵循,您要加入檔案的函式庫或模塊的程式碼風格。

請注意,底線符號開始是保留給 C 函式庫(底線加上大寫或雙底線是保留給編譯器),所以如果可以的話,請避免使用上述的類型。

靜態變數

嘗試減少靜態變數的使用量,尤其是當提交給函式庫時。建構和大量靜態變數初始化會嚴重增加啟動時間。

不要使用靜態類別(class-static)變數,尤其是函式庫和可裝入模塊。即使在應用程式也是不鼓勵使用。靜態物件會導致很多問題,如由於未定義建構或解構的次序,難以對 crashes 進行除錯。

相反地,請使用靜態指標,連同 kglobal.h 中定義的 K_GLOBAL_STATIC 一起使用:

class A { ... };

K_GLOBAL_STATIC(A, globalA)

void doSomething()
{
     A *a = globalA;
     ...
}

void doSomethingElse()
{
    if (globalA.isDestroyed()) {
        return;
    }
    A *a = globalA;
    ...
}

void installPostRoutine()
{
    qAddPostRoutine(globalA.destroy);
}

請參閱 K_GLOBAL_STATICAPI 文件了解更多資訊。

常量(Constant)資料

如果你在幾個地方需要一些簡單資料類型的常量資料。你最好在一個集中的地方只定義它們一次,以避免mistype,在其中一個實例(instance)。如果資料改變也只需要修改一個地方。

即使只有一個實例,你最好還是定義在其他地方,以避免程式碼有所謂的「magic numbers」,這是無法解釋(cmp. 42)。通常是放在檔案的頂部,以避免要搜尋它。

定義常量資料請使用C++的語言結構,而不用前置處理器(preprocessor)指令,就像你使用單純的C。這樣編譯器可以透過類型檢查幫助你找到錯誤。

// 正確!
static const int AnswerToAllQuestions = 42;
// 錯誤!
#define AnswerToAllQuestions 42


如果定義了一個常量陣列(Array)不要使用指標的作為資料型態。而是使用資料型態和未定義長度附加陣列符號,[],在名稱的後面。否則,你還可以定義變數在一些常量資料。這些變數可能錯誤地被指定為新的指標,而編譯器不會出現錯誤。存取陣列應該要有一個間接,因為第一次的變數值需要被讀取。

// 正確!
static const char SomeString[] = "Example";
// 錯誤!
static const char* SomeString = "Example";
// 錯誤!
#define SomeString "Example"

提前宣告

提前宣告類別,而不是含入他們各自的標頭檔,可以減少編譯時間。例如:

#include <QWidget>     // 慢
#include <QStringList> // 慢
#include <QString>     // 慢
class SomeInterface
{
public:
    virtual void widgetAction( QWidget *widget ) =0;
    virtual void stringAction( const QString& str ) =0;
    virtual void stringListAction( const QStringList& strList ) =0;
};

上述內容應當改成這樣寫:

class QWidget;     // 快
class QStringList; // 快
class QString;     // 快
class SomeInterface
{
public:
    virtual void widgetAction( QWidget *widget ) =0;
    virtual void stringAction( const QString& str ) =0;
    virtual void stringListAction( const QStringList& strList ) =0;
};

迭代器

Prefer const iterators and cache end()

Prefer to use const_iterators over normal iterators when possible. Containers, which are being implicitly shared often detach when a call to a non-const begin() or end() methods is made (QList is an example of such a container). When using a const_iterator also watch out that you are really calling the const version of begin() and end(). Unless your container is actually const itself this probably will not be the case, possibly causing an unnecessary detach of your container. So basically whenever you use const_iterator initialize them using constBegin()/constEnd() instead, to be on the safe side.

Cache the return of the end() (or constEnd()) method call before doing iteration over large containers. For example:

QList<SomeClass> container;

//code which inserts a large number of elements to the container

QList<SomeClass>::ConstIterator end = container.constEnd();
QList<SomeClass>::ConstIterator itr = container.constBegin();

for ( ; itr != end; ++itr ) {
    // use *itr (or itr.value()) here
}

This avoids the unnecessary creation of the temporary end() (or constEnd()) return object on each loop iteration, largely speeding it up.

When using iterators, always use pre-increment and pre-decrement operators (i.e., ++itr) unless you have a specific reason not to. The use of post-increment and post-decrement operators (i.e., itr++) cause the creation of a temporary object.

Take care when erasing elements inside a loop

When you want to erase some elements from the list, you maybe would use code similar to this:

QMap<int, Job *>::iterator it = m_activeTimers.begin();
QMap<int, Job *>::iterator itEnd = m_activeTimers.end();

for( ; it != itEnd ; ++it) {
    if(it.value() == job) {
        //A timer for this job has been found. Let's stop it.
        killTimer(it.key());
        m_activeTimers.erase(it);
    }
}

This code will potentially crash because it is a dangling iterator after the call to erase(). You have to rewrite the code this way:

QMap<int, Job *>::iterator it = m_activeTimers.begin();
while (it != m_activeTimers.end()) {
    if(it.value() == job) {
        //A timer for this job has been found. Let's stop it.
        killTimer(it.key());
        it = m_activeTimers.erase(it);
    } else {
        ++it;
    }
}

This problem is also discussed in the Qt documentation for QMap::iterator but applies to all Qt iterators

記憶體洩漏

一個非常「常犯」的程式設計錯誤是實做 new 而沒有 delete,就這個程式一樣:

mem_gourmet.cpp

class t
{
  public:
    t() {}
};

void pollute()
{
  t* polluter = new t();
}

int main()
{
  while (true) pollute();
}

你看,pollute()建立一個新的物件,t類別的polluter。然後,polluter 變數丟失,因為它是區域(local)的,但其內容(物件)仍留在 heap 中。我可以使用這個程式讓電腦在10秒內無法使用。

為了解決這個問題,有以下方法:

  • 保持變數在 stack 而不是 heap:
  t* polluter = new t();

改為

 
  t polluter;
  • 刪除polluter,使用 new 的補充函式:
 
  delete polluter;
  • std::auto_ptr 停止 polluter(這在從該方法返回時,將自動刪除 polluter)
 std::auto_ptr<t> polluter = new t();

Valgrind之類的工具可以檢測記憶體洩漏。

dynamic_cast

You can only dynamic_cast to type T from type T2 provided that:

  • T is defined in a library you link to (you'd get a linker error if this isn't the case, since it won't find the vtable or RTTI info)
  • T is "well-anchored" in that library. By "well-anchored" I mean that the vtable is not a COMMON symbol subject to merging at run-time by the dynamic linker. In other words, the first virtual member in the class definition must exist and not be inlined: it must be in a .cpp file.
  • T and T2 are exported

For instance, we've seen some hard-to-track problems in non-KDE C++ code we're linking with (I think NMM) because of that. It happened that:

  • libphonon loads the NMM plugin
  • NMM plugin links to NMM
  • NMM loads its own plugins
  • NMM's own plugins link to NMM

Some classes in the NMM library did not have well-anchored vtables, so dynamic_casting failed inside the Phonon NMM plugin for objects created in the NMM's own plugins.

程式設計

在本節中,我們將繼續與 Qt/KDE 應用程式設計相關的常見問題。

延遲初始化(delayed initialization)

儘管設計現代 C++ 應用程式可能會非常複雜。一個反覆出現的問題,通常是很容易解決的。是沒有使用延遲初始化的技術。

首先,讓我們看看初始化 KDE 應用程式的標準方式:

int main( int argc, char **argv )
{
    ....
    KApplication a;

    KCmdLineArgs *args = KCmdLineArgs::parsedArgs();

    MainWindow *window = new MainWindow( args );

    a.setMainWidget( window );
    window->show();

    return a.exec();
}

Notice that window is created before the a.exec() call that starts the event loop. This implies that we want to avoid doing anything non-trivial in the top-level constructor, since it runs before we can even show the window.

The solution is simple: we need to delay the construction of anything besides the GUI until after the event loop has started. Here is how the example class MainWindow's constructor could look to achieve this:

MainWindow::MainWindow()
{
    initGUI();
    QTimer::singleShot( 0, this, SLOT(initObject()) );
}

void MainWindow::initGUI()
{
    /* Construct your widgets here.  Note that the widgets you
     * construct here shouldn't require complex initialization
     * either, or you've defeated the purpose.
     * All you want to do is create your GUI objects and
     * QObject::connect
     * the appropriate signals to their slots.
     */
}

void MainWindow::initObject()
{
    /* This slot will be called as soon as the event loop starts.
     * Put everything else that needs to be done, including
     * restoring values, reading files, session restoring, etc here.
     * It will still take time, but at least your window will be
     * on the screen, making your app look active.
     */
}

Using this technique may not buy you any overall time, but it makes your app seem quicker to the user who is starting it. This increased perceived responsiveness is reassuring for the user as they get quick feedback that the action of launching the app has succeeded.

When (and only when) the start up can not be made reasonably fast enough, consider using a KSplashScreen.

資料結構

在本節中,我們會繼續一些最常見的 pet-peeves,在 Qt/KDE 應用程式中影響資料結構非常常見。

Passing non-POD types

Non-POD ("plain old data") types should be passed by const reference if at all possible. This includes anything other than the basic types such as char and int.

Take, for instance, QString. They should always be passed into methods as const QString&. Even though QString is implicitly shared it is still more efficient (and safer) to pass const references as opposed to objects by value.

So the canonical signature of a method taking QString arguments is:

void myMethod( const QString & foo, const QString & bar );

QObject

If you ever need to delete a QObject derived class from within one of its own methods, do not ever delete it this way:

   delete this;

This will sooner or later cause a crash because a method on that object might be invoked from the Qt event loop via slots/signals after you deleted it.

Instead always use QObject::deleteLater() which tries to do the same thing as delete this but in a safer way.

空 QStrings

It is common to want to see if a QString is empty. Here are three ways of doing it, the first two of which are correct:

// Correct
if ( mystring.isEmpty() ) {
}

// Correct
if ( mystring == QString() ) {
}

// Wrong! ""
if ( mystring == "" ) {
}

While there is a distinction between "null" QStrings and empty ones, this is a purely historical artifact and new code is discouraged from making use of it.

QString 和讀取檔案

If you are reading in a file, it is faster to convert it from the local encoding to Unicode (QString) in one go, rather than line by line. This means that methods like QIODevice::readAll() are often a good solution, followed by a single QString instantiation.

For larger files, consider reading a block of lines and then performing the conversion. That way you get the opportunity to update your GUI. This can be accomplished by reentering the event loop normally, along with using a timer to read in the blocks in the background, or by creating a local event loop.

While one can also use qApp->processEvents(), it is discouraged as it easily leads to subtle yet often fatal problems.

Reading QString from a KProcess

KProcess emits the signals readyReadStandard{Output|Error} as data comes in. A common mistake is reading all available data in the connected slot and converting it to QString right away: the data comes in arbitrarily segmented chunks, so multi-byte characters might be cut into pieces and thus invalidated. Several approaches to this problem exist:

  • Do you really need to process the data as it comes in? If not, just use readAllStandard{Output|Error} after the process has exited. Unlike in KDE3, KProcess is now able to accumulate the data for you.
  • Wrap the process into a QTextStream and read line-wise. This should work starting with Qt 4.4.
  • Accumulate data chunks in the slots and process them each time a newline arrives or after some timeout passes. Example code

QString 和 QByteArray

While QString is the tool of choice for many string handling situations, there is one where it is particularly inefficient. If you are pushing about and working on data in QByteArrays, take care not to pass it through methods which take QString parameters; then make QByteArrays from them again.

For example:

QByteArray myData;
QString myNewData = mangleData( myData );

QString mangleData( const QString& data ) {
    QByteArray str = data.toLatin1();
    // mangle 
    return QString(str);
}

The expensive thing happening here is the conversion to QString, which does a conversion to Unicode internally. This is unnecessary because, the first thing the method does is convert it back using toLatin1(). So if you are sure that the Unicode conversion is not needed, try to avoid inadvertently using QString along the way.

The above example should instead be written as:

QByteArray myData;
QByteArray myNewData = mangleData( myData );

QByteArray mangleData( const QByteArray& data )

QDomElement

When parsing XML documents, one often needs to iterate over all the elements. You may be tempted to use the following code for that:

for ( QDomElement e = baseElement.firstChild().toElement();
      !e.isNull();
      e = e.nextSibling().toElement() ) {
       ...
}

That is not correct though: the above loop will stop prematurely when it encounters a QDomNode that is something other than an element such as a comment.

The correct loop looks like:

for ( QDomNode n = baseElement.firstChild(); !n.isNull();
      n = n.nextSibling() ) {
    QDomElement e = n.toElement();
    if ( e.isNull() ) {
        continue;
    }
    ...
}