C++ 面向对象高级编程上-侯捷

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成员
	}
	....
}
重点:
  1. 构造函数要使用 initialization list
  2. 函数本体内定义的函数需要加 const 的要加
  3. 参数尽可能 by reference, 另需考虑加不加 const
  4. 返回值尽可能 by reference
  5. 数据放在 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
      1. Complex c1(1,2);
      2. 生命在作用域(Scope)结束之际结束
      3. 编译器自动调用其析构函数,因此又称为 auto object
    • Static Stack Objects
      1. static Complex c2(1,2);
      2. 生命在作用域(Scope)结束之后仍存在,直至整个程序结束
    • Global Object
      1. Complex c3(1,2);

        int main(){

        ...

        }
      2. 生命在作用域(Scope)结束之后仍存在,直至整个程序结束
    • Heap Objects
      1. Complex* p = new Complex;
      2. 生命在它被 delete 之际结束,调用其析构函数
      3. 若未 delete 则会造成 memory leak(指针p的生命已经结束了,但所致的heap object仍存在)
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 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);

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 中需要的部分

  • Composition
// 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)
}

Delegation

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

构造和析构
  • Inheritance
  • 构造由内而外
    • 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
    Template Method
对象组合
Inheritance + Composition
  • Inheritance + Composition
  • 构造由内而外
    • Derived 的构造函数首先调用 Base 的 default 构造函数,然后调用 Component 的 default 构造函数,最后才调用自己。
    • Derived::Derived(...): Base(), Component() { ... };
  • 析构由外而内
    • Derived 的析构函数首先执行自己,然后调用 Component 的析构函数,最后调用 Base 的析构函数。
    • Derived::~Derived(...): { ...~Component(), ~Base(); };
Delegation + Inheritance

1.Observer
Observer

  • Subject 包含一个委托容器 Observer
  • Observer 类可继承
  • Subject 控制流程,让 Observer 的子类根据需求来注册、注销

2. Composite
103.png

  • Composite—add不能写成纯虚函数,因为Primitive没有add的动作,如文件是不能有+的动作的,只有文件夹才有

3. Prototype (Design Patterns Explained Simply)
Prototype

  • 创建未来的对象,需要每个子类自己创建一个自己给父类,让父类可以看到
  • LandSatImage(子类) 创建静态的自己挂接到框架中-addPrototype
  • Image(框架)用于创建未知的子类-findAndClone
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值