存档

文章标签 ‘侯捷’

侯捷-c++面向对象程序设计-学习笔记-11-类之间的关系:继承、组合和委托

2020年8月17日 评论已被关闭

1,继承

2,组合

假如有两个class:A和B。若A中包含一个类型为B的成员变量,则A、B之间的关系为组合。用UML来表示为:

[A]◆→[B]

A中的B一定是一个实体对象,A has-a B。

由于A中拥有B,所以在创建A时需要先创建B,这意味着先调用B的构造函数,再调用A的构造函数。【由内而外执行构造】

对象销毁的过程恰恰相反,先调用A的析构函数,再调用B的析构函数。

【可以拿剥洋葱的过程做类比析构过程:由外而内,逐层剥皮】

这里,介绍一个常用设计模式:Adapter(适配器)。

template <class T>
class queue{
  //...
  protected:
    deque<T> c;

  public:
    bool empty() const {return c.empty();}
    size_type size() const {return c.size();}
    void pop(){c.pop_front();}
    //...
};

在这个例子中,queue是我们需要的一个队列,deque是标准库提供的一个功能完备的双端队列。queue的功能全部通过成员变量c来实现,queue只是对c的部分接口进行了封装以使其满足需求,或者说改变c的接口调用形式以适配现有代码。

 

3,委托(以引用方式组合)

假如有两个class:A和B。若A中包含指向B对象的指针类型的成员变量,则A、B之间的关系为委托。用UML来表示为:

[A]◇→[B]

A中的B是一个指针,A has-a-reference to B。

分类: C/C++ 标签: ,

侯捷-c++面向对象程序设计-学习笔记-10-类的static成员、namespace和模板简介

2020年8月13日 评论已被关闭

1,static成员变量

c++类中的非static修饰的成员变量在每个对象中都各自拥有一份,而static修饰的成员变量则只有一份,这种成员变量被同一类的所有对象共有。

【一个变量为多个对象共有,这种情况在多线程环境下是不安全的,需要考虑数据同步问题】

#include <iostream>
using namespace std;

class Account{
  public:
    static double m_rate;
};

//static成员变量需要在类外单独定义
double Account::m_rate = 8.0;

int main(){
  //通过类名访问static成员变量
  Account::m_rate = 6.0;
  cout << Account::m_rate << endl;       //6

  //通过对象访问static成员变量
  Account a;
  cout << a.m_rate << endl;              //6
  a.m_rate = 7.0;
  cout << a.m_rate << endl;               //7

  Account b;
  cout << b.m_rate << endl;               //7
  
  return 0;
}

如上边代码所示,static成员变量需要在类的声明之外单独定义。

 

2,static成员函数

c++类中可以定义一种被static修饰的函数,这类函数形参中【不含隐式的this指针】,这意味着static成员函数不能访问属于某个对象的(非static)成员变量,而【只能访问static成员变量】。

#include <iostream>
using namespace std;

class CS{
  public:
    static void print(){
        cout << "static" << endl;
        cout << m_s << endl;
        //cout << m_i << endl;
        //error: invalid use of member 'CS::m_i' in static member function
    }
    
  public:
    static int m_s;
    int m_i;
};
int CS::m_s = 0;

int main()
{
  //通过类名调用static成员函数
  CS::print();
  
  CS a;
  a.m_s = 1;
  //通过对象调用static成员函数
  //但是static成员函数无法访问对象拥有的非static成员变量m_i
  a.print();
  
  return 0;
}

static成员函数和static成员变量有一个典型的应用场景就是单例(Singleton)模式:类只有一个实例化对象。

【如何确保一个类只能有一个实例对象呢?首先,不允许用户随意创建对象,干脆直接禁止用户手动创建对象,也就是禁止用户调用类的构造函数。其次,保证可以创建类的一个唯一的对象,这可以通过static成员变量实现。最后,需要提供用户一个接口,可以访问这个唯一的对象,static成员函数正好可以实现这个需求。】

下边是一个单例的实现例子:

class Singleton{
  public:
    static Singleton& getInstance(){return s;}
  private:
    Singleton(){}
    Singleton(const Singleton&){}
    static Singleton s;
};

int main()
{
  Singleton::getInstance();
  return 0;
}

这个例子中,我们通过Singleton::getInstance()来获取类的唯一对象。

如果这个接口从未被调用,内存中还是会有一份Singleton对象存在。所以可稍作优化:

class Singleton{
  public:
    static Singleton& getInstance();
  private:
    Singleton(){}
    Singleton(const Singleton&){}
};

Singleton& Singleton::getInstance(){
  static Singleton a;
  return a;
}

int main()
{
  Singleton::getInstance();
  return 0;
}

把static成员变量修改为局部static变量,这样只在getInstance被调用后才会创建一个Singleton的对象,并且后续调用不会重复创建。

【需要注意一点:这种通过static局部对象创建单例的方式在c++98中不是线程安全的,只在c++11之后才是线程安全的。】

3,cout为什么可以输出各种类型的数据?

cout派生自ostream,而ostream派生自ios。ios对operator <<进行了诸多重载,支持输出各种基本类型数据。

 

4,类模板

如果一个类的某些成员变量可以是多种类型,我们可以通过类模板实现类的复用。

类模板将成员变量的类型参数化,可以在编译时自动绑定指定的数据类型,这种编译时执行的类型绑定称为静态绑定。

// Example program
#include <iostream>
#include <string>
using namespace std;

template <class T>
class TT{
  public:
    TT(const T& _l, const T& _r): l(_l), r(_r){}
    T sum(){return l+r;}
    
  private:
    T l,r;
};

int main(){
    TT<int> ti(3,4);
    cout << ti.sum() << endl;
    
    TT<string> ts("Hello", "World");
    cout << ts.sum() << endl;
    
    return 0;
}

//7
//HelloWorld

类模板在使用时需要在<>中显示指定参数的类型。

 

5,函数模板

函数模板和类模板相似,也是为了使函数的功能可以应用在不同类型的数据上,节省编码工作量。

#include <iostream>
#include <string>
using namespace std;

template <class T>
T sum(const T& l, const T& r){ return l+r;}

int main(){
    cout << sum(3,4) << endl;
    
    string s1("Hello"), s2("World");
    cout << sum(s1, s2) << endl;
    
    return 0;
}

这个例子和上边类模板实现的功能相同。

函数模板不需要显示指定参数的类型,编译器会自动进行类型推导。

【类模板和函数模板对于T是有一定要求的,T需要满足代码中的相关调用。比如例子中的相加操作,T类型本身要支持+运算符。】

 

6,namespace(命名空间)

namespace是为了避免项目中的命名冲突。

 

————————-笔记分割线——————–

【侯捷老师推荐的书籍】

分类: C/C++ 标签: ,

侯捷-c++面向对象程序设计-学习笔记-09-包含指针类型成员变量的类的设计总结

2020年8月13日 评论已被关闭

如果类中包含指针类型的成员变量,一定要谨慎设计,避免出现指针引起的各种内存异常!

1,正确为指针分配和释放内存

  • 在构造函数中为指针分配内存
  • 在析构函数中释放指针指向的内存
  • 【这就是RAII原则,资源获取即初始化】

2,正确实现拷贝构造函数和拷贝赋值函数

  • 一定要通过【深拷贝】实现指针成员变量的复制
  • 重载赋值运算符时注意避免自身赋值

 

————————-笔记分割线——————–

【侯捷老师推荐的书籍】

分类: C/C++ 标签: ,

侯捷-c++面向对象程序设计-学习笔记-08-堆、栈和内存管理

2020年8月12日 评论已被关闭

1,c++对象的内存分配方式及生命周期

c++中对象有4种内存分配方式:栈、堆、静态数据区、全局数据区。

  • 栈中变量
    •  在一段代码块中临时分配的变量。该变量在程序退出代码块时【自动】销毁。
    • 【需要准确理解“自动”的含义。在下边代码中,vl和p都是代码块中的局部变量,都在程序退出它们所在的代码块时被销毁。虽然p是一个指针,p所指的内存空间不会被自动释放,但是就p自身来说,这个指针占用的sizeof(int*)大小的内存还是会被自动释放的。】
  • 堆中变量
    • 通过new创建的变量,必须手动通过匹配的delete来释放
    • 【这种变量实际上同时占用栈和堆两种空间。new返回的指针位于栈,指针指向的空间位于系统堆。】
  • 静态变量
    • 由static修饰的变量。在程序执行到变量定义处从静态区分配内存,并在整个进程退出后才释放。
    • static变量可以定义在局部代码块中,只初始化一次。
  • 全局变量
    • 不被任何代码块包含的变量,生命周期和static变量类似。

以下代码展示了这4中不同的内存分配方式。

//全局变量
int vg=0;

void fn(){
    //静态变量
    static int vs=1;
}

int main(){
    //栈中分配的变量
    int vl=2;

    fn();

    //堆中分配的变量
    int* p=new int(3);
    delete p;

    return 0;
}

2,动态内存分配的运行机制

  • T* p = new T()
    • void* pv = operator new(sizeof(T))    //operator new会调用malloc(sizeof(T))
    • p = static_cast<T*>(pv)
    • p->T::T()    //T::T(p), p即this
    • new的作用就是:分配内存,转换指针类型,调用构造函数执行初始化
  • delete p
    • p->T::~T()    //T::~T(p)
    • operator delete(p)    //operator delete会调用free(p)
    • delete的作用就是:调用析构函数,释放内存

3,array new和array delete

  • 动态分配数组时,需要使用new T[N]这样的语法
  • 释放指向数组的动态空间时,需要使用delete[] p语法
  • 如果释放动态数组时使用delete p,【p指向的内存会正常释放】,但是会导致【只调用一次数组对象的析构函数】,如果数组中的对象都各自分配了动态内存,这些内存将不会得到完全释放,从而造成内存泄露。可使用以下代码进行验证。
    • #include <iostream>
      using namespace std;
      
      class A{
        public:
          ~A(){
              cout << ++i << endl;
          }
      
          static int i;
      };
      
      int A::i=0;
      
      int main()
      {
        A* p = new A();
        delete p;
        A::i=0;
        
        p = new A[3];
        delete[] p;
        A::i=0;
        
        p = new A[3];
        delete p;
        
        return 0;
      }

4,vc编译器中new的内存分配策略

  • vc中,内存块的对齐单位为16字节,如果内存块大小不够16字节的倍数,编译器会进行填充
  • 单个对象
    • 【4字节cookie | 32字节内存块控制信息 | 类成员变量 | 4字节内存块控制信息 | 4字节cookie】
    • cookie存放的是对齐后的整个内存块的大小,其最后一位为1表示内存块已分配,为0表示内存块已回收
  • 数组对象
    • 【4字节cookie | 32字节内存块控制信息 | 数组元素个数(4字节) | 数组元素 | 4字节内存块控制信息 | 4字节cookie】

 

————————-笔记分割线——————–

【侯捷老师推荐的书籍】

分类: C/C++ 标签: ,

侯捷-C++面向对象程序设计-学习笔记-07-c++三大函数-拷贝构造函数、拷贝赋值函数和析构函数

2020年8月12日 评论已被关闭

首先明确两个概念。

  • 拷贝构造:通过已有对象来【构造】一个新的对象,这个是在对象的构造过程中发生的,会调用拷贝构造函数
  • 拷贝赋值:用另一个对象给已存在的对象赋予新的值,这是为对象整体赋予新值时发生的,会调用拷贝赋值函数
    • 拷贝赋值函数就是类中重载了赋值运算符(=)的那个函数

每个类都拥有拷贝构造函数和拷贝赋值函数,如果我们没有对这两个函数进行重载,编译器会生成默认的函数,其拷贝操作就是简单的【按位拷贝】(也称为【浅拷贝】),把源对象中成员变量的数据原封不动地拷贝到目标对象中。

侯捷老师把c++类分为带指针成员变量的类和不带指针成员变量的类。

对于不含有指针类型成员变量的类,使用编译器提供的默认拷贝函数是没有问题的。

而对于含有指针类型成员变量的类,按位拷贝会造成两个对象拥有相同指针指向的同一块内存空间。如果两者同时操作(读取、写入、释放)这块内存,很可能会造成脏数据、内存泄露等问题。

因此,针对包含指针类型成员变量的类,我们需要自己来实现以下三个函数:拷贝构造函数、拷贝赋值函数和析构函数。这三个函数也称为c++类的Big Three。

字符串类是一个经典的包含指针的类,我们看一下字符串类的构造函数和Big Three函数是如何实现的。

1,String类的声明

class String{
    public:
        String(const char* cstr=0);
        String(const String& str);
        String& operator=(const String& str);
        ~String();

    private:
        char* m_data;
};

String类有一个m_data指针,用于指向存储字符串的内存空间。

2,构造函数

inline String::String(const char* cstr)
{
    //判断是否为空指针
    if(cstr){
        m_data = new char[strlen(cstr)+1];//多分配一个字节
        strcpy(m_data, cstr);
    }
    else{
        m_data = new char[1];
        *m_data = '\0';
    }
}

构造函数用于从一个c形式的字符串构造一个String对象。对于指针类型的形参,首要的工作就是判断是否为空指针。

为String的m_data分配内存时需要额外分配一个字节,用于存储结束字符。

3,拷贝构造函数

String::String(const String& str){
    m_data = new char[strlen(str.m_data) + 1];
    strcpy(m_data, str.m_data);
}

拷贝构造函数接受一个String类型的对象,为m_data分配内存,并复制源字符串str.m_data所指内存中的数据。这种拷贝称为【深拷贝】。

有两种形式创建String对象时会调用拷贝构造函数。

String s1('Hello');
String s2(s1);
String s3 = s1;

s2和s3都是通过s1来【初始化】,因此都会调用拷贝构造函数。

【还有一些场景会隐式调用拷贝构造函数。比如:

以值的方式传递对象参数;以值的方式返回对象;向容器中存放对象】

4,拷贝赋值(运算符)函数

String& operator=(const String& str){
    //检测自身赋值
    if(this == &str){
        return *this;
    }

    //释放原有内存
    delete []m_data;

    //执行深拷贝
    m_data = new char[strlen(str.m_dadta) + 1];
    strcpy(m_data, str.m_data);

    return *this;
}

赋值运算符的重载有几点细节:

  • 入参为const类型的引用,提高效率
  • 需要判断是否自身赋值,即s=s。这种情况没必要进行后续的内存操作,直接返回即可
  • 需要释放目标对象已有的内存,避免内存泄露
  • 需要重新给m_data分配内存空间,并copy源str.m_data所指内存存放的内容,这里也是【深拷贝】。
  • 返回一个目标对象的引用,这样可以使得赋值运算符应用在连续赋值的场景,即s1=s2=s3

拷贝赋值函数是在已存在的对象之间赋值时才调用的,一定要和构造函数的使用场景区分开来。

String s1("hello"), s2('World');
s2 = s1;

5,析构函数

inline String::~String(){
    delete []m_data;
}

我们看到构造函数、拷贝构造函数和拷贝赋值函数都为m_data分配了内存,那么在对象销毁时,要记得释放这块内存。

这项工作一般在析构函数中实现。编译器会在对象销毁时自动调用析构函数,确保内存得到了释放。

 

————————-笔记分割线——————–

【侯捷老师推荐的书籍】

分类: C/C++ 标签: ,

侯捷-C++面向对象程序设计-学习笔记-06-无指针成员变量的类的设计过程

2020年8月12日 评论已被关闭

以Complex类为例,描述如何设计一个不含指针成员变量的类。

1,头文件使用防卫式声明

#ifndef __COMPLEX__
#define __COMPLEX__

#endif

2,首先考虑类中需要包含哪些数据(成员变量),数据是第一位的。

  • 将这些数据声明为private

3,类需要提供哪些功能,如何设计对外的接口,以哪种形式提供这些接口(成员函数或者全局函数)

  • 对于函数参数和返回值的设计,一定要考虑效率问题,优先使用引用类型的参数
  • 成员函数是否会修改成员变量,如果不修改,将其声明为const
  • 构造函数:通过初始化列表初始成员变量
  • 运算符是否需要重载

 

————————-笔记分割线——————–

【侯捷老师推荐的书籍】

分类: C/C++ 标签: ,