存档

‘语言’ 分类的存档

侯捷-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++ 标签: ,

cpp.sh: 一个在线c++编译器

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

如果你突发灵感,手边一时没有编译环境,或者自己懒得每次手写Makefile,手动make+gcc编译,在线编译环境是一个不错的选择。

当然你的代码只能支持c++标准,不涉及其他系统调用。

那么cpp.sh就是一个很不错的在线编译器。它具有以下功能:

  • 在线编辑、编译、运行c++代码
  • 可设置编译选项:使用的c++标准(c++98、c++11或c++14),编译器告警级别(-W)优化级别(-O)

尝试一下吧。

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

c++类重载的输入输出运算符为什么不是类的成员函数

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

我们知道自定义的类可以重载输入输出运算符(<<和>>),以实现便捷的输入输出操作。

通常我们是通过友元函数而不是成员函数来实现输入输出运算符的重载的。

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

class CA{
    public:
      CA(int _i):i(_i){}
      
      friend ostream& operator<<(ostream& os, const CA& a){
          os << a.i;
          return os;
      }
      
    private:
        int i;
};

int main()
{
  CA a(12), b(34);
  cout << a << endl << b;
  
  return 0;
}

那么,能否通过成员函数的方式实现输入输出运算符的重载呢?语法上可行。

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

class CA{
    public:
      CA(int _i):i(_i){}
      
      ostream& operator<<(ostream& os){
          os << i;
          return os;
      }
      
    private:
        int i;
};

int main()
{
  CA a(12), b(34);
  //a << b << cout;
  a << (b << cout << endl);
  
  return 0;
}

但是请留意一下这个成员函数版本的输出运算符的用法。

  • 对象需要出现在输出运算符<<的左侧(a<<cout),实际上是执行了a.operator<<(cout)。
  • 连续输出同类型对象的写法非常别扭(a<<(b<<cout)),必须用括号扩住前一次的输出。中间如果插入其他类型的变量,需要思考一下该怎么使用,开发效率很低下。

所以,设计与实现类时,需要站在用户的角度考虑,提高代码的易用性。

分类: C/C++ 标签: