C++学习笔记
一 编程设计 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); } |
|