C++ 面向对象高级编程上-侯捷
Object Based vs. Object Oriented
- Object Based(基于对象):面对的是单一class的设计, 没有指针.
- Object Oriented(面向对象):面对的是多重classes的设计,classes和classes之间的关系,有指针.
Class without pointer member(complex)
1. 头文件的防御式声明
// 避免头文件重复include
#ifndef __COMPLEX__
#define __COMPLEX__
....
#endif
Header的布局
// forward declaration(前置声明)
#include <cmath>
class ostream;
....
// calss declarations(类-声明)
class complex{
....
};
// class definition(类-定义)
complex::function ....
2. Complex类设计
template<typename T>
class complex {
public:
complex (T r = 0, T i = 0)
: re (r), im (i)
{ }
complex& operator += (const complex&);
T real () const { return re; }
T imag () const { return im; }
private:
T re, im;
friend complex& __doapl (complex*, const complex&);
};
//使用
complex<double> c1(2.5,1.5);
complex<int> c2(2,6);
inline(内联)函数
- 函数若在class内定义完成,便自动成为inline候选人
- 函数体外增加 inline 关键字定义
inline double imag(const complex& x){ return x.imag(); }
- 最终是否成为 inline function 由编译器决定,一般来说复杂的函数无法成为inline function
- 内联是以**代码膨胀(复制)**为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。 如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
访问级别
- public:函数部分(外界使用的)
- private:数据部分&函数部分(仅用于内部使用,不对外的)
- protected:基类可以访问,而其他用户不可以访问
构造函数
-
语法
complex (double r = 0, double i = 0) : re(r), im(i) {}
- 名字与类名相同
- 有参数
- 可以有默认实参
- 不需要返回类型
- 初值列-速度会更快,初始化时即赋值
-
(overloading)重载
- 编译器会将函数名称、参数个数、参数类型进行编码,用于区分
-
构造函数放在 private
- 一般情况不放在 private 里面,除非你不想让外界创建
- Singleton(单例)模式会放在 private 里面, 外面只能有一个对象
//单例模式 class A { public: static A& getInstance(); setup() { ... } private: A(); A(const A& rhs); ... }; A& A::getInstance() { static A a; return a; } //外界调用 A::getInstance().setup();
Const Member Functions(常量成员函数)
- 凡是不会改变数据内容的,尽量都加上const
// 不会改变数据内容的需要加上const
double real() const { return re; }
double imag() const { return im; }
- 如使用者创建了一个const型的complex对象,此时去获取它的实部和虚部,若前面的函数没有加上const,就会出现“使用者不允许改变实部和虚部,但调用的函数有可能改变实部和虚部的情况,导致调用失败,因此能加const的地方,一定要加上const
const complex c1(2,1);
cout << c1.real();
cout << c1.imag();
Pass By Value vs. Pass By Reference(to const)
- Reference 等同于指针,无论对象多大,传的都只有4个byte
- 引用前可加 const ,避免接受者更改我的内存 :complex& operator += (const complex&);
- 参数传递尽量都 By Reference
Return By Value vs. Return By Reference(to const)
- 返回值的传递也尽量都 By Reference
- 函数操作的结果不是由自己创建的内存空间,一般可用 return by reference
- 传递者无需知道接受者是以 reference 形式接收
- 返回的都是object,至于接收端是 value 还是 refrence,传递者无需在乎
Friend(友元)
-
友元函数可以自由取得 private 成员
friend complex& __doapl(complex*, const complex& r)
-
friend 是直接拿,若不设计为友元,也可提供其余接口函数让外部获取数据,不过会慢一些
-
相同 class 的各个 objects 互为 friends
class complex{
....
int func(const complex& param){
return param.re + param.im; // 直接取得了param的private成员
}
....
}
重点:
- 构造函数要使用 initialization list
- 函数本体内定义的函数需要加 const 的要加
- 参数尽可能 by reference, 另需考虑加不加 const
- 返回值尽可能 by reference
- 数据放在 private中
扩展
- Initialization List 为什么好?参考地址
- 对于内置类型的成员初始化和赋值没有大的区别
- 在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的
- 对于非内置类型的成员变量,初始化列表能够避免两次构造
- 类类型的数据成员对象在进入函数体前已经构造完成,也就是说在成员初始化列表处进行构造对象的工作,调用构造函数,在进入函数体之后,进行的是对已经构造好的类对象的赋值,又调用个拷贝赋值操作符才能完成
- 部分情况必须上进行显示的初始化
- 成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败
- const成员或引用类型的成员。因为const对象或引用类型只能初始化,不能对他们赋值。
- 对于内置类型的成员初始化和赋值没有大的区别
- Protected 访问权限
- public 和 private 代表类的封装,protected 代表类的继承
- 成员能被派生类对象访问,不能被类外访问
3. 操作符重载与临时变量
成员函数
- 所有的成员都隐含一个参数 this,谁调用谁就是 this
inline complex& complex::operator +=(const complex& r){
return __doapl(this, r);
}
非成员函数
- 没有 this 指针
- 为了应付 client 的多种可能用法,需对应开发多个函数
inline complex operator + (const complex& x, const complex& y){
....
}
inline complex operator + (const complex& x, double y){
....
}
inline complex operator + (double x, const complex& y){
....
}
- 上述函数不可 return by reference. 因为它们返回的必定是个 local object.
Temp Object(临时对象)
- typename();
complex();
- 临时生成的,无需命名,生命到下一行就结束了
Return Void vs. Return Objects&
- 当使用者是的用法是需要连续使用时需要Return Object&
ostream& operator << (ostream& os, const complex& x){
....
}
cout << c1 << conj(c1);
// cout << c1 执行结果,要能够接受 conj(c1),因此返回的需要是ostream&
Class with pointer member(stirng)
1. Big three
- 拷贝构造
String(const String& str);
- 拷贝赋值
String& operator=(const String& str);
- 析构函数
~String();
构造函数和析构函数
-
构造函数
String(const char* cstr = 0);
- 字符串构造要注意检查是否为 nullptr
-
析构函数
- 带有指针的 Class 多半会动态分配内存,因此在析构函数中要主动将动态分配的内存 delete
inline String::~String(){ delete[] m_data; }
-
Class with pointer members 必须要有 copy ctor 和 copy op=
-
默认的copy ctor 和 copy op= 只会进行浅拷贝,会导致两个指针指向同一内存块,形成 alias 和 memory leak
拷贝构造
inline String::String(const String& str){
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
- String s2(s1) <===> String s2 = s1 两者调用的都是拷贝构造函数
拷贝赋值
inline String& String::operator=(const String& str){
if (this == &str)
return *this; // 检测自我赋值
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
- 步骤:
- 先释放自己
delete[] m_data
- 分配足够的内存
m_data = new char[ strlen(str.m_data) + 1 ];
- 拷贝数据
strcpy(m_data, str.m_data);
- 先释放自己
- 要点:
- 返回类型要是String&,用于s1 = s2 = s3的使用情景
- 一定要检测自我赋值,否则会造成undefined behavior
Stack(栈) 与 Heap(堆)
- stack
Complex c1(1,2);
- 内存由编译器在需要时自动分配和释放。通常用来存储局部变量和函数参数。(为运行函数而分配的局部变量、函数参数、返回地址等存放在栈区)。
- 栈运算分配内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
- Stack 对象的生命在作用域 Scope 结束之际结束,自动调用其析构函数
- heap
- Complex* p = new Comples(3);`
- System Heap, 由操作系统提供的一块 global 内存空间,程序可动态分配从中获得若干区块
- 要配合使用 delete 或 delete[] 进行释放,否则会造成内存泄漏
- 对象
- Stack Odbjects
Complex c1(1,2);
- 生命在作用域(Scope)结束之际结束
- 编译器自动调用其析构函数,因此又称为 auto object
- Static Stack Objects
static Complex c2(1,2);
- 生命在作用域(Scope)结束之后仍存在,直至整个程序结束
- Global Object
Complex c3(1,2);
int main(){
...
}
- 生命在作用域(Scope)结束之后仍存在,直至整个程序结束
- Heap Objects
Complex* p = new Complex;
- 生命在它被 delete 之际结束,调用其析构函数
- 若未 delete 则会造成 memory leak(指针p的生命已经结束了,但所致的heap object仍存在)
- Stack Odbjects
new & delete
new
- 先分配 memory, 再调用 ctor
Complex* pc = new Complex(1,2);
void* mem = operator new(sizeof(Complex)); // 分配内存,内部调用 malloc(n)
pc = static_cast<Complex*>(mem); // 转型
pc->Complex::Complex(1,2); // 调用构造函数
delete
- 先调用 dtor, 再释放 memory
delete pc;
Complex::~Complex(pc); // 析构函数
operator delete(pc); // 释放内存,内部调用 free(pc)
补充: 内存详情
Single Object
- 灰色部分为Debug模式额外添加的信息
- 绿色部分为对象数据所占空间,此处需要满足内存4字节对齐(青绿色标出)
- 内存块收尾为标记为,其值代表整个内存块大小。最后1位用于指示内存块的用途,1:送出 0:回收
Array Object
Array new 一定要搭配 Array Delete
- Array new 分配的内存的使用 delete 释放时,编译器仅会施放申请的内存,且只调用1次析构函数,会导致部分对象未正确析构
Static
Static Data Member
- 将数据与对象分离,与类绑定
- 一定要在类外初始化(真正的分配内存),赋不赋值均可
class Account {
public:
static double m_rate;
static void set_rate(const double& x) { m_rate =x; };
}
double Accont::m_rate = 8.0;
Static Function Member
- 没有 This Pointer,只能用于处理Static Data
- 调用方式:
- 通过 object 调用
Accout a; a.set_rate(7.0);
- 通过 class name 调用
Account::set_rate(5.0);
- 通过 object 调用
Singleton(单例) [参考] — 把 ctor 放在 private 区
class Singleton {
public:
static Singleton& Instance() {
static Singleton theSingleton;
return theSingleton;
}
/* more (non-static) functions here */
private:
Singleton(); // ctor hidden
Singleton(Singleton const&); // copy ctor hidden
Singleton& operator=(Singleton const&); // assign op. hidden
~Singleton(); // dtor hidden
};
- 在 Instance() 调用前,不会存在 theSingleton对象,没有内存的浪费
- Static 全局变量 vs. 普通全局变量
- 全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。 这两者在存储方式上并无不同
- 非静态的全局变量在各个源文件中都是有效的
- 而静态全局变量则限制了其作用域, 只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它
- 全局变量改为静态后改变了它的作用域
- Static 局部变量 vs. 普通局部变量
- Static 局部变量存储在静态区,生命直至整个程序结束
- 普通局部变量存储在栈区,生命仅在 Scope 内有效
- 局部变量改为静态后改变了它的生命期
三. Object Oriented Programming(OOP)
类与类的关系
- Inheritance(继承)
- Composition(复合)
- Delegation(委托)
1. Composition(复合),表示 has-a
Adapter
容器 A 包含实现多种功能的类 B, A 可选择性包含部分 B 中需要的部分
// queue 'has-a' deque
template <class T>
class queue{
....
protected:
deque<T> c; //底层容器
public:
// 以下完全利用 c 的操作函数完成
bool empty() const { retrun c.empty;}
size_type size() const { return c.size(); }
....
}
void push(const value_type& x) { c.push_back(x); }
void pop() { c.pop_front(); }
大小计算
复合类的大小由复合的类的大小决定,计算式逐个相加
// Sizeof: 40
template<clss T>
class queue {
protected:
deque<T> c;
...
};
// Sizeof: 16 * 2 + 4 + 4
template <class T>
class deque {
protected:
Itr<T> start;
Itr<T> finish;
T** map;
unsigned int map_size;
};
// Sizeof: 4 * 4
template <class T>
struct Itr{
T* cur;
T* first;
T* last;
T* node;
....
};
构造和析构 的过程
- 构造由内而外
由内而外基础才稳定,要做一个东西,要先弄地基
// Container 的构造函数首先调用 Component 的 default 构造函数,然后才执行自己
// "Component()"由编译器
Container::Container(...): Component() {};
// 如果 default 的构造函数不满足要求,需要手动写明需要调用的构造函数
Container::Container(...): Component(...) {};
- 析构由外而内
想象在拆掉一个东西的时候需要由外而内一层层的拆
// Container 的析构函数首先执行自己,然后才调用 Component 的析构//
// "~Component()"由编译器添加
Container::~Container(...): {.... ~Component() };
2. Delegation(委托) = Composition By Reference
Handle / Body
容器 A 中只包含指向功能 B 的指针,需要使用时才实例化 B 对象(可在任何时候将任务“委托”)。“编译防火墙”,需要修改时修改 B 即可。
class StringRep;
class String {
public:
String();
...
private:
StringRep* rep; //pimpl (point to implementation)
}
- Reference Counting(共享技术)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vN4hvHwY-1572091759065)(http://upload-images.jianshu.io/upload_images/9987091-0c3a75954b5c03bf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
3.Inheritance(继承),表示 is-a
struct _List_node_base{
_List_node_base* _M_next;
_List_node_base* _M_prev;
};
template<typename _Tp>
struct _List_node: public _List_node_base {
_Tp _M_data;
}
构造和析构
- 构造由内而外
- Derived 的构造函数首先调用 Base 的 default 构造函数,然后才执行自己。
Derived::Derived(...): Base() { ... };
- 析构由外而内
- Derived 的析构函数首先执行自己,然后才调用 Base 的析构函数。
Derived::~Derived(...): { ... ~Base(); };
- Base 的析构函数一定要是 virtual 的,否则会出现 undefined behavior
- (扩展)用基类指针去操作子类对象时,若基类的析构函数不是虚函数,则在施放内存时只会调用基类的析构函数,造成内存泄漏。
inheritance With Virtual Function
class Shape {
public:
virtual void draw() const = 0; // pure virtual
virtual void error(const std::string& msg); // impure virtual
int objectID() const; // non-virtual
...
}
- pure virtual 函数: 你希望 derived class 一定要重新定义它,你对它没有默认定义。
- virtual 函数:你希望 derived class 重新定义它,且你对它已有默认定义。
- non-virtual 函数:你不希望 derived class 重新定义它。
应用
- Template Method
对象组合
Inheritance + Composition
- 构造由内而外
- Derived 的构造函数首先调用 Base 的 default 构造函数,然后调用 Component 的 default 构造函数,最后才调用自己。
Derived::Derived(...): Base(), Component() { ... };
- 析构由外而内
- Derived 的析构函数首先执行自己,然后调用 Component 的析构函数,最后调用 Base 的析构函数。
Derived::~Derived(...): { ...~Component(), ~Base(); };
Delegation + Inheritance
1.Observer
- Subject 包含一个委托容器 Observer
- Observer 类可继承
- Subject 控制流程,让 Observer 的子类根据需求来注册、注销
2. Composite
- Composite—add不能写成纯虚函数,因为Primitive没有add的动作,如文件是不能有+的动作的,只有文件夹才有
3. Prototype (Design Patterns Explained Simply)
- 创建未来的对象,需要每个子类自己创建一个自己给父类,让父类可以看到
- LandSatImage(子类) 创建静态的自己挂接到框架中-addPrototype
- Image(框架)用于创建未知的子类-findAndClone