一 編程設計
1.將程序劃分為多個子系統,包括子系統間的接口和依賴關系、子系統間的數據流、在各子系統間的來回輸入輸出、以及總的線程模型。
2.各個子系統的具體細節,包括進一步細分的類、類層次體系、數據結構、算法、特定的線程模型和錯誤處理。
二 設計流程
1.需求:功能需求和性能需求。
2.設計步驟
(1)把程序劃分為通用功能子系統,并明確子系統的接口和交互。
(2)把這些子系統列在一個表中,并表示出子系統的高層行為或功能、子系統向其它子系統提供的接口,及此子系統使用了其他子系統的哪些接口。
(3)選擇線程模型:確定使用多少個線程,明確線程的交互并為共享數據指定加鎖機制。
(4)為每個子系統指定層次體系
(5)為每個子系統指定類、數據結構、算法和模式
(6)為每個子系統指定錯誤處理(系統錯誤 + 用戶錯誤),指定是否使用異常
三 C++ 的設計原則
1.抽象:將接口與實現相分離
2.重用:代碼重用和思想重用
四 對象關系
1.has-a 關系(聚集)
2.is-a 關系(繼承)
3.組織對象層次體系:
(1)將類按有意義的功能加以組織。
(2)將共同的功能放到超類中,從而支持代碼重用。
(3)避免子類過多的覆蓋父類的功能。
五 重用設計
1.建立可重用的代碼結構
(1)避免將無關或邏輯上分離的概念混在一起
(2)把程序劃分為子系統
(3)使用類層次體系來分離邏輯概念
(4)使用聚集來分離邏輯概念
(5)對通用數據結構和算法使用模板
(6)提供適當的檢查和防護
六 設計易于使用的接口
1.開發直觀的接口
2.不要遺漏必要的功能
3.提供簡潔的接口
(1)消除重復的接口
(2)只提供所需的功能
(3)適當的限制庫的使用
4.提供文檔和注釋
(1)公共的文檔應指定行為,而不是底層實現
七 設計通用的接口
1.提供多種方法來完成同一功能
2.提供定制能力
八 協調一般性和通用性
1.提供多個接口
2.優化常用功能
九 代碼注釋
(1)前綴注釋
* 文件/類名
* 最后一次修改時間
* 原作者
* 文件所實現特性的編號 (特性 ID)
* 版權信息
* 文件/類的簡要描述
* 未完成的特性
* 已知的 bug
(2)注釋示例
/*
* Watermelon.cpp
*
* $Id: Watermelon.cpp,v 1.6 2004/03/10 12:52:33 klep Exp $
*
* Implements the basic functionality of a watermelon. All
* unit are expressed in terms of seeds per cubic centimeter
* Watermelon theory is based on the white paper "Alogorthms
* for Watermelon Processing."
*
* The following code is (c)copyright 2004. FruitSoft, Inc.
* All right reserved
*/
十 編寫代碼
1.類定義在 C++ 中是一條語句,因此必須以分號結束。
2.:: 指作用域解析操作符。
3.在棧和堆上使用對象的區別
(1)在棧上創建對象
SpreadsheetCell myCell, anotherCell;
(2)在堆上使用對象
SpreadsheetCell *myCellp = new SpreadsheetCell();
(3)如果用 new 來分配一個對象,用完該對象時要用 delete 來釋放
4.C++ 程序員通常把構造函數稱為 "ctor"
5.使用構造函數
(1)在棧上使用構造函數
SpreadsheetCell myCell(5);
(2)在堆上使用構造函數
SpreadsheetCell *myCell = new SpreadsheetCell(5);
delete myCell;
(3)不要嘗試從類的一個構造函數調用另一個構造函數
6.使用默認構造函數
(1)在棧上使用構造函數
SpreadsheetCell myCell; //right
SpreadsheetCell myCell();//wrong
(2)在棧上創建對象時,要去掉默認構造函數的小括號
(3)在堆上使用默認構造函數
SpreadsheetCell *myCellp = new SpreadsheetCell();
(4)什么時候需要使用構造函數
SpreadsheetCell cells[3];//fails comilation without default ctor
SpreadsheetCell *myCellp = new SpreadsheetCell[10];//alse fails
(5)使用初始化列表
1)初始化列表允許在創建數據成員的同時完成數據成員的初始化
2)使用初始化列表的情況
數據成員 解釋
------------------------------------------------------------------
const 數據成員 必須在創建時提供值
引用數據成員 引用無法獨立存在
沒默認構造函數的對象成員 對象成員無法初始化
沒有默認構造函數的超類 見后面
------------------------------------------------------------------
7.對象的撤銷
(1)析構函數僅用于釋放內存或釋放其他資源是一個不錯的想法
8.淺復制與深復制
(1)淺復制:只是從源對象直接將數據成員復制或賦值到目標對象
(2)深復制:非淺復制
(3)只要在類中動態分配了內存,就應該編寫自己的復制構造函數來提供內存的深復制
(4)在對一個對象進行賦值前,必須先釋放此對象的所有動態分配的內存
(5)只要類會動態分配內存,就需要編寫析構函數、復制構造函數、賦值操作符
(6)禁止對象賦值,可將復制構造函數與賦值操作符聲明為私有成員
(7)不必為私有復制構造函數和賦值操作符提供實現,編譯器不要求
十一 精通類和對象
1.不能在靜態方法中訪問非靜態數據成員
2.保證一個對象不會修改數據成員,可用 const 來標記
3.保證一個方法不會修改數據成員,可用 const 來標記
(1)在類定義中的聲明 double getValue() const;
(2)在源文件中的實現
double Spreadsheet::getValue() const
{
return this.mValue;
}
4.非 const 對象可以調用 const 和非 const 方法,const 對象只能調用const 方法
5.應將所有不會修改對象的方法都聲明為 const,并在程序中使用 const對象引用
6.將變量置為 mutable,這樣編譯器允許在 const 方法中修改這個變量
7.C++ 不允許僅基于方法的返回類型而重載一個方法名
8.默認參數:從最右參數開始的連續參數表
9.只能在方法聲明中指定默認參數,在定義中并不指定
10一個構造函數的所有參數都有默認值,此函數會作為默認構造函數
11能利用默認參數做到的事情,利用方法重載也可以做,用你最熟悉的
12內聯:將方法體或函數體直接插入到代碼調用處(相當于 #define 宏的安
全版本),內聯示例如下:
(1)在類的源文件(SpreadsheetCell.cpp)
inline double SpreadsheetCell::getValue() const
{
mNumAccess++;
return mValue;
}
(2)或在類的聲明文件中直接實現此方法而不用 inline 關鍵字
//SpreadsheetCell.h
double getValue() const (mNumAccesses++; return mValue;}
13友元可以訪問指定類中的 protected 和 private 數據成員和方法
(1)聲明友元類
class SpreadsheetCell
{
public:
friend class Spreadsheet;
//code omitted here
};
(2)聲明友元方法
class SpreadsheetCell
{
public:
friend bool checkSpreadsheetCell();
//code omitted here
};
十二 C++ 中的繼承機制
1.超類指針(引用)在引用子類時,了類仍然會保留它們覆蓋的方法。而在
強制類型轉換成超類對象時,子類會失去它們的獨有特性。覆蓋方法和子
類數據的丟失稱為切割。
2.作為一條經驗,要把所有的方法都用 virtual 聲明 (包括析構函數,但
是不包括構造函數) 來避免因遺漏關鍵字 virtual 而產生的相關問題。
3.virtual 用法示例:
class Sub : public Super
{
public:
Sub();
virtual void someMethod();
virtual void someOtherMethod();
}
4.要把所有的析構函數都用 virtual 聲明
5.強制類型轉換
(1)靜態轉換 static_cast<type>
示例:
Sub* mySub = static_cast<Sub*>(inSuper);
(2)動態轉換 dynamic_cast<type>
示例:
Sub* mySub = dynamic_cast<Sub*>(inSuper);
if (mySub == NULL)
{
//proceed to access sub methods on mySub
}
注意:如果對指針不能進行動態類型轉換,指針則為 NULL, 而不是指
向無意義的數據。
6.進行向上強制類型轉換時,要使用指向超類的指針或引用來避免切割問題。
7.純虛方法與抽象基類
(1)純虛方法: 在類定義中顯示未定義的方法。
(2)抽象類: 含有純虛方法的類(不能實例化)。
(3)純虛方法語法定義: 在類定義中簡單的設置方法等于 0,在 cpp 文件
中不要編寫其實現代碼。
示例:
class SpreadsheetCell
{
public:
SpreadsheetCell();
virtual ~SpreadsheetCell();
virtual void set(const std::string instring) = 0;
virtual std::string getString() const = 0;
};
8.定制類型轉換函數
(1)double 類型轉換成 string 類型
#include <iostream>
#include <sstream>
double inValue;
string myString;
ostringstream ostr;
ostr << inValue;
myString = ostr.str();
(2)string 類型轉換成 double 類型
#include <iostream>
#include <sstream>
double myDouble;
string inString;
istringStream istr(inString);
istr >> myDouble;
if (istr.fail())
{
myDouble = 0;
}
9.使用預編譯指令避免重復包含頭文件
#ifndef _TEST_H_
#define _TEST_H_
// include header files here
// other code omitted here
#endif
十三 覆蓋方法的特殊情況
1.在 C++ 中不能覆蓋靜態方法。
(1)不能同時用 virtual 和 static 聲明一個方法。
(2)在對象上可以調用 static 方法,但 static 方法只存在于類中。
十四 利用模板編寫通用代碼
1.模板相關概念
(1)類模板: 存儲對象的容器或數據結構。
(2)模板的語法:
template <typename T>
class Grid
{
public:
Grid(int inWidth, int inHeight);
Grid(const Grid<T>& src);
Grid<T>& operator=(const Grid<T>& rhs);
T& getElementAt(int x, int y);
const T& getElementAt(int x, int y);
void setElementAt(int x, int y, const T& inElem);
protected:
void copyFrom(const Grid<T>& src);
T** mCells;
};
(3)語法解釋
template <typename T> : 指在類型 T 上定義的模板。
Grid<T> : Grid 實際上是模板名。
Grid<T> : 將作為類模板中的類名。
(4)模板定義(實現)
template <typename T>
Grid<T>::Grid(int inWidth, int inHeight)
{
mCells = new T* [mWidth];
for (int i = 0; i < mWidth; i++)
{
mCells[i] = new T[mHeight];
}
}
(5)模板實例化
Grid<int> myIntGrid;
十五 C++ 中的一些疑難問題
1.引用
(1)定義: 另一個變量的別名, 對引用的修改會改變其所指向的變量。
(2)引用變量必須在創建時就初始化。
int x = 3; // right
int& xRef = x;// right
int& emptyRef;//wrong
注: 類的引用數據成員可在構造函數的初始化列表中初始化。
(3)不能創建指向未命名值的引用(const 常量值除外)
int& unnameRef = 5; //does not compile
const int& unnameRef = 5;//works as expect
(4)修改引用: 引用總是指向初始化時指定的那個變量。
int x = 3, y = 4;
int& xRef = x;
xRef = y; // change x value to 4, doesn't make refer to y;
xRef = &y; // doesn't compile, type not match
注: 引用指向的變量在初始化之后不能再改變, 只能改變此變量的值
(5)指針引用和引用指針
//指針引用示例(指向指針的引用)
int* intP;
int*& ptrRef = intP;
ptrRef = new int;
*ptrRef = 5;
注:不能聲明指向引用的引用, 也不能聲明引用指針(指向引用的指針)
int x = 3;
int& xRef = x;
int&& xDoubleRef = xRef; // not compile
int&* refPtr = &xRef; // not compile
(6)傳引用vs傳值
1)效率。復制大對象和結構要花費很長時間。
2)正確性。不是所有的對象都允許傳值或正確的支持深復制。
3)不想修改原對象,又利用以上兩優點,可在參數前加 const。
4)對簡單內置類型(如int或double)要傳值,其它所有情況可傳引用。
(7)引用vs指針
1)引用讓程序清晰,易于理解。
2)引用比指針安全,不存在無效的引用,不需要明確解除引用。
3)除非需要動態分配內存或在其它地方要改變或釋放指針指向的值,否則都應使用引用而非指針。
2.疑難字 const
(1)const 變量
使用 const 來聲明變量,對不能對其修改,以保護變量。
(2)const 指針
//不能改變指針指向的值
const int* ip;
ip = new int[10];
ip[4] = 5; // not compile
或
int const* ip;
ip = new int[10];
ip[4] = 5; // not compile
//不能改變指針自身
int* const ip;
ip = new int[10]; // not compile
ip[4] = 5;
//既不能改變指針也不能改變指針指向的值
const int* const ip = NULL;(無用的指針)
注: const 可以放在類型前也可以放在類型后
(3)const 應用規則
const 應用于其左則的第一項。
(4)把對象參數傳遞時,默認的做法是把傳遞 const 引用。
(5)const 方法
用 const 標識類方法,可以防止方法修改類中不可變的數據成員。
3.關鍵字 static
(1)關于連接: C++ 中的每個源文件是獨立編譯的,得到的對象連接在一
起。
(2)外部連接: 一個源文件中每個名字(如函數或全局變量)對其它源文件
是可用的。
(3)內部連接: 一個源文件中每個名字(如函數或全局變量)對其它源文件
是不可用的。內部連接也叫靜態連接。
(4)函數和全局變量默認是外部連接。
(5)聲明前加 static 可指定為內部連接。
4.關鍵字 extern
(1)作用: 與 static 相對,用來為位于它前面的名字聲明外部連接。
(2)extern 用法
// AnotherFile.cpp
extern int x; // 只是聲明 x 為外部連接而不分配內存
int x = 3; //顯示定義以分配內存
或
extern int x = 3;//聲明和定義一起完成
//FirstFile.cpp
extern int x;
cout << x << endl;
5.強制類型轉換
(1)const_cast<type> 去除變量的常量性。
示例:
void g(char* str)
{
// body omitted here
}
void f(const char* str)
{
g(const_cast<char*>(str));
// other code omitted here
}
(2)static_cast<type> 顯示的完成 C++ 語言支持的轉換。
示例:
int x = 3;
double result = static_cast<double>(i) /10;
注: static_cast 進行類型轉換時并不完成運行時類型檢查。
(3)dynamic_cast<type>
1)對類型強制轉換完成運行時類型檢查。
2)對指針轉換失敗時會返回 NULL。
3)對引用轉換失敗時會拋出 bad_cast 異常。
6.函數指針
(1)定義: 把函數地址作為參數,可以像變量一樣使用。
(2)定義函數指針: typedef bool(*YesNoFcn) (int, int);
(3)用法示例
//定義函數指針類型
typedef string(*YesNoFcn)(int, int);
void test(int value1, int values2, YesNoFcn isFunction)
{
cout << isFunction(value1, value2);
}
string intEqual(int intItem1, int intItem2)
{
return (intItem1 == intItem2) ? "match" : "not match";
}
//使用函數指針
test(1, 1, &intEqual);
注: & 是可選的
十六 C++ 中的 I/O 操作
1.使用流
(1)每個輸入流都有一個相關聯的源,每個輸出流都有一個相關聯的目的。
(2)cout 和 cin 都是在 C++ 的 std 命名空間中預定義的流實例。
(3)流的三種類型:
1)控制臺輸入輸出流。
2)文件流。
3)字符串流。
(4)輸出流
1)輸出流在頭文件 <ostream> 中定義,輸入流在 <istream> 中定義<iosream> 中定義了輸入輸出流。
2)cout 和 cin 指控制臺輸入輸出流。
3)<< 操作符是使用輸出流的最簡單的方法。
4)流其它的輸出方法
1)put() 和 wirte()
2)flush() 刷新輸出
5)處理輸出錯誤
1)cout.good() 流是否處于正常的可用狀態。
2)cout.bad() 流輸出是否發生了錯誤。
3)cout.fail() 如果最近的操作失敗則返回 true
4)cout.clear() 重置流的錯誤狀態
6)輸出控制符
1)endl 輸出回車并刷新其緩沖區
2)hex oct dec 以十六/八/十進制輸出
3)setw 設置輸出數值數據時的字段占位符
4)setfill 設置填充空位的占位符
(5)輸入流
1)>> 輸入流操作符
2)輸入方法
1)get() 僅僅返回流中的下一個字符
2)unget() 引起流回退一個位置
3)peek() 預覽下一個值
4)getline() 從輸入流中取一行數據
3)處理輸入錯誤
1)good()
2)eof()
(6)字符串流
1)<ssteam> 定義了字符串流的頭文件
2)ostringstream 字符串輸出流
3)istringstream 字符串輸入流
(7)文件流
1)<fstream> 定義了文件流的頭文件
2)ifstream 文件輸入流
3)ofstream 文件輸出流
4)seek() 定位流的位置
5)tell() 查詢流當前的位置
(8)鏈接流
1)定義: 在任何輸入流與輸出流之間建立連接,一旦訪問就刷新輸出。
2)實現: 用輸入流的 tie() 方法
3)示例
#include <iostream>
#inlcude <fstream>
#include <string>
main()
{
ifstream inFile("input.txt");
ofstream outFile("output.txt");
//set up a link between inFile and outFile.
inFile.tie(&outFile);
string nextToken;
inFile >> nextToken;
}
十七 C++ 中的異常
1.拋出和捕獲異常
(1)<exception> 定義異常類的頭文件。
(2)拋出異常對象 throw exception()
(3)向量的使用(整型)
vector<int> myInts;
for (int i = 0; i < 10; i++)
{
myInts.push_back(i);// 增加元素
cout << myInts[i];
}
(4)string 風格的字符串轉換成 C 風格的字符串
string myString = "string style string";
char* cStyleStrng = myString.c_str();
(5)捕獲運行時異常
try
{
...
}
catch (const runtime_error& e)
{
...
}
(6)拋出和捕獲無效參數異常
throw invalid_argument("");
try
{
...
}
catch (const invalid_argument& e)
{
...
}
注: runtime_error 和 invalid_argument 定義在頭文件<stdexcept> 中
(7)匹配任何異常(...)
try
{
// code omitted here
}
catch (...)
{
// code omitted
}
注: 不建議使用這種方式
(8)使用拋出列表
1)拋出列表: 一個函數或方法能拋出的異常列表
2)必須為函數聲明和實現都提供拋出列表
3)沒有拋出列表就可以拋出任何異常
4)空的拋出列表不允許拋出任何異常
(9)在覆蓋方法中修改參數列表
1)從列表中刪除異常
2)增加超類拋出列表中異常的子類
3)不能完全刪除拋出列表
(10)顯示異常消息
可調用 exception 或子類的 what() 方法顯示捕獲到的異常消息
示例:
try
{
...
}
catch (const exception& e)
{
cerr << e.what() << endl;
exit(1);
}
(11)多態地捕獲異常
1)在多態地捕獲異常時,要確保按引用捕獲異常。如果按值捕獲異常,就會遇到切割問題,丟失對象的信息。
2)按引用捕獲異常可以避免不必要的復制開銷
(12)如果異常處理不夠仔細,就會導致內存和資源泄漏
示例
func ()
{
string str1;
string* str2 = new string();
throw exception();
delete str2; // 內存泄漏
}
(13)使用智能指針防止內存泄漏
#include <memory> // 定義智能指針的頭文件
using namespace std;
func ()
{
string str1;
// 智能指針,不用自己手動釋放
auto_ptr<string> str2(new string("hello"));
throw exception();
}
(14)處理內存分配錯誤
1)new 和 new [] 分配內存失敗默認拋出 bad_alloc 異常
2)可用 try/catch 捕獲異常處理內存分配失敗
3)示例:
try
{
ptr = new int[numInts];
}
catch (bad_alloc& e)
{
cerr << "Unable to alloc memory!" << endl;
return ;
}
或
1)用 new(nothrow) 在內存分配失敗時返回 NULL 來處理
2)示例:
ptr = new(nothrow) int[numInts];
if (ptr == NULL)
{
cerr << "Unable to alloc memory!" << endl;
return;
}
或
1)用 set_new_handler() 回調函數定制分配失敗時的行為
2)示例
void myNewHandler()
{
cerr << "Unable to allocate memory!" << endl;
abort(); // 終止程序 <cstdlib>
}
#include <new>
#include <cstdlib>
#include <iostream>
using namespace std;
int main(int argc, char** argv)
{
new_handler oldHandler = set_new_handler(myNewHandler);
// code omitted here
set_new_handler(oldHandler);
return (0);
}