C++动态数组类的封装实例

C++中的动态数组(Dynamic Array)是指动态分配的、可以根据需求动态增长占用内存的数组。为了实现一个动态数组类的封装,我们需要考虑几个问题:new/delete的使用、内存分配策略、类的四大函数(构造函数、拷贝构造函数、拷贝赋值运算符、析构函数)、运算符的重载。涉及到的知识点很多,对此本文只做简单的介绍。

一、内存分配策略

当用new为一个动态数组申请一块内存时,数组中的元素是连续存储的,例如 vector和string。当向一个动态数组添加元素时,如果没有空间容纳新元素,不可能简单地将新元素添加到内存中的其他位置——因为元素必须连续存储。所以必须重新分配一块更大的内存空间,将原来的元素从旧位置移动到新空间中,然后添加新元素,释放旧的内存空间。如果我们每添加一个新元素,就执行一次这样的内存分配和释放操作,效率将会慢到不行。

为了避免上述的代价,必须减少内存重新分配的次数。所以我们采取的策略是:在不得不分配新的内存空间时,分配比新的空间需求更大的内存空间(通常为2倍)。这样,在相当一段时间内,添加元素时就不用重新申请内存空间。注意,只有当迫不得已时才可以分配新的内存空间。

二、类的四大函数

一个C++类一般至少有四大函数,即构造函数、拷贝构造函数、拷贝赋值运算符、析构函数。如果类未自己定义上述函数,C++编译器将为其合成4个默认的版本。但是往往编译器合成的并不是我们所期望的,为此我们有必要自己定义它们。

1.构造函数

类的构造函数(constructor)用来初始化类对象的非static数据成员,无论何时只要类的对象被创建,就会执行构造函数。

class Foo {
public:
  Foo(); // 构造函数
  Foo(string &s);
  // ...
}; 

构造函数的名字和类名相同,没有返回类型。类可以包含多个构造函数(重载),它们之间在参数数量或类型上需要有所区别。构造函数有一个初始化部分和一个函数体,成员的初始化是在函数体执行之前完成的。

2.拷贝构造函数

如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数(copy constructor)。

class Foo {
public:
  Foo();
  Foo(const Foo&); // 拷贝构造函数
  // ...
};

拷贝构造函数定义了如何用一个对象初始化另一个同类型的对象。拷贝初始化通常使用拷贝构造函数来完成。拷贝初始化发生在下列情况中:

使用等号(=)初始化一个变量
将一个对象作为实参传递给一个非引用类型的形参
从一个返回类型为非引用类型的函数返回一个对象
用花括号列表初始化一个数组中的元素

3.拷贝赋值运算符

类的拷贝赋值运算符(copy-assignment operator)是一个名为operator=的函数。类似于其他任何函数,它也有一个返回类型和一个参数列表。

class Foo {
public:
  Foo();
  Foo& operator=(const Foo&); // 赋值运算符
  // ...
};

拷贝赋值运算符定义了如何将一个对象赋值给另一个同类型的对象。赋值运算符是一个成员函数也是一个二元运算符,其左侧运算对象就绑定到隐式的this指针,右侧运算对象作为显式参数传递。注意:为了与内置类型的赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用。

4.析构函数

类的析构函数(destructor)用来释放类对象使用的资源并销毁类对象的非static数据成员,无论何时只要一个对象被销毁,就会自动执行析构函数。

class Foo {
public:
  ~Foo(); // 析构函数
  // ...
};

析构函数的名字由波浪号(~)加类名构成,也没有返回类型。由于析构函数不接受参数,因此它不能被重载。析构函数有一个函数体和一个析构部分,销毁一个对象时,首先执行析构函数体,然后按初始化顺序的逆序销毁成员。

三、运算符的重载

重载的运算符是具有特殊名字的函数:它们的名字由关键字operator和其后要定义的运算符号共同组成。和其他函数一样,重载的运算符也包含返回类型、参数列表、函数体,比如拷贝赋值运算符。

当我们定义重载的运算符时,必须首先决定是将其声明为类的成员函数还是声明为一个普通的非成员函数。有些运算符必须作为成员,而另一些运算符作为普通函数比作为成员更好:

赋值(=)、下标([ ])、调用(( ))和成员访问箭头(->)运算符必须是成员。
复合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同。
改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减、解引用运算符,通常应该是成员。
具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数。
当然,除了赋值运算符之外,我们还需要为动态数组定义下标运算符operator []。下标运算符必须是成员函数。为了让下标可以出现在赋值运算符的任意一端,下标运算符函数通常返回所访问元素的引用。

四、动态数组类的封装

下面给出了动态数组DArray类的接口:

class DArray
{
private:
  double *m_Data; // 存放数组的动态内存指针
  int m_Size;   // 数组的元素个数
  int m_Max;    // 预留给动态数组的内存大小
private:
  void Init();   // 初始化
  void Free();   // 释放动态内存
  inline bool InvalidateIndex(int nIndex); // 判断下标的合法性
public:
  DArray();    // 默认构造函数
  DArray(int nSize, double dValue = 0); // 构造函数,设置数组大小,默认值为dValue
  DArray(const DArray& arr); // 拷贝构造函数
  DArray& operator=(const DArray& arr); // 拷贝赋值运算符
  ~DArray();    // 析构函数 

  void Print();  // 输出显式所有数组元素的值
  int GetSize();  // 获取数组的大小(元素个数)
  void SetSize(int nSize); // 重新设置数组的大小,若nSize小于原大小,截断;否则,新元素置0
  double GetAt(int nIndex); // 获取指定位置元素
  void SetAt(int nIndex,double dValue); // 重置指定元素的值
  void PushBack(double dValue); // 追加一个新元素到数组末尾
  void DeleteAt(int nIndex);   // 删除指定位置地元素
  void InsertAt(int nIndex, double dValue); // 插入一个新的元素到数组中
  double operator[](int nIndex) const;   // 重载下标运算符[]
};

下面是实现方法:

void DArray::Init()
{
  m_Size = 0;  // 默认情况下数组不包含元素
  m_Max = 1;
  m_Data = new double[m_Max];
} 

void DArray::Free()
{
  delete [] m_Data;
} 

bool DArray::InvalidateIndex(int nIndex)
{
  if(nIndex>=0 && nIndex<m_Size)
    return false;
  else
    return true;
} 

// 默认构造函数
DArray::DArray()
{
  Init();
} 

// 构造函数
DArray::DArray(int nSize, double dValue)
{
  if(nSize == 0)
    Init();
  else
  {
    m_Size = nSize;
    m_Max = nSize;
    m_Data = new double[m_Max];
    for(int i=0; i<nSize; ++i)
      m_Data[i]=dValue;
  }
} 

// 拷贝构造函数
DArray::DArray(const DArray& arr)
{
  m_Size = arr.m_Size; /*复制常规成员*/
  m_Max = arr.m_Max;
  m_Data = new double[m_Max];  /*复制指针指向的内容*/
  memcpy(m_Data, arr.m_Data, m_Size*sizeof(double));
} 

// 拷贝赋值运算符
DArray& DArray::operator=(const DArray& arr)
{
  if(this == &arr) /*自赋值*/
    return *this;
  m_Size = arr.m_Size;
  m_Max = arr.m_Max;
  /* 先将右侧对象拷贝到临时对象中,然后再销毁左侧对象*/
  double *m_Temp = new double[m_Max];
  memcpy(m_Temp, arr.m_Data, m_Size*sizeof(double));
  delete [] m_Data;
  m_Data = m_Temp; 

  return *this;
} 

// 析构函数
DArray::~DArray()
{
  Free();
} 

// 打印数组
void DArray::Print()
{
  if(m_Size == 0)
  {
    cout << "Error: The empty array can't be Printed." << endl;
    exit(0);
  }
  else
  {
    for(int i=0; i<m_Size; ++i)
      cout << m_Data[i] << " ";
    cout << endl;
  }
} 

// 获取数组大小
int DArray::GetSize()
{
  return m_Size;
} 

// 重置数组大小
void DArray::SetSize(int nSize)
{
  if(nSize < m_Size)  /*截断*/
  {
    for(int i=nSize; i<m_Size; ++i)
      m_Data[i] = 0;
  }
  if(m_Size<=nSize && nSize<=m_Max) /*新增元素置0*/
  {
    for(int i=m_Size; i<nSize; ++i)
      m_Data[i] = 0;
  }
  if(nSize > m_Max)  /*需要重新分配空间*/
  {
    m_Max = nSize;
    double *temp = new double[m_Max];
    memcpy(temp, m_Data, m_Size*sizeof(double));
    for(int i=m_Size; i<nSize; ++i)
      temp[i] = 0;
    delete [] m_Data;
    m_Data = temp;
  }
  m_Size = nSize; /*设置数组大小*/
} 

// 获取指定位置元素
double DArray::GetAt(int nIndex)
{
  if(InvalidateIndex(nIndex))
  {
    cout << "Error: the index of GetAt is invalid!" << endl;
    exit(0);
  }
  return m_Data[nIndex];
} 

// 设置指定位置元素的值
void DArray::SetAt(int nIndex, double dValue)
{
  if(InvalidateIndex(nIndex))
  {
    cout << "Error: the index of SetAt is invalid!" << endl;
    exit(0);
  }
  else
  {
    m_Data[nIndex] = dValue;
  }
} 

// 追加一个新元素到数组末尾
void DArray::PushBack(double dValue)
{
  if(m_Size < m_Max)
  {
    m_Data[m_Size] = dValue;
  }
  else
  {
    m_Max = m_Max*2;
    double* temp = new double[m_Max];
    memcpy(temp, m_Data, m_Size*sizeof(double));
    delete [] m_Data;
    m_Data = temp;
    m_Data[m_Size] = dValue;
  }
  ++m_Size; /*数组大小加1*/
} 

// 从数组中删除一个元素
void DArray::DeleteAt(int nIndex)
{
  if(InvalidateIndex(nIndex))
  {
    cout << "Error: the index of DeleteAt is invalid." << endl;
    exit(0);
  }
  else
  {
    for(int i=nIndex; i<m_Size; ++i)
      m_Data[i] = m_Data[i+1];
    m_Data[m_Size-1] = 0;
    --m_Size;
  }
} 

// 插入一个新元素到指定位置
void DArray::InsertAt(int nIndex, double dValue)
{
  if(nIndex<0 || nIndex>m_Size)
  {
    cout << "Error: the index of InsertAt is invalid!" << endl;
    exit(0);
  } 

  if(m_Size < m_Max) /* 未满,插入 */
  {
    for(int i=m_Size-1; i>=nIndex; --i)
      m_Data[i+1] = m_Data[i];
    m_Data[nIndex] = dValue;
  }
  else        /* 重新分配空间 */
  {
    m_Max = m_Max*2;
    double* temp = new double[m_Max];
    memcpy(temp, m_Data, m_Size*sizeof(double));
    delete [] m_Data;
    m_Data = temp;
    for(int i=m_Size-1; i>=nIndex; --i)
      m_Data[i+1] = m_Data[i];
    m_Data[nIndex] = dValue;
  }
  ++m_Size; /* 数组大小加1 */
} 

// 重载下标运算符[]
double DArray::operator[](int nIndex) const
{
  if(nIndex<0 || nIndex>=m_Size)
  {
    cout << "Error: the index in [] is invalid!" << endl;
    exit(0);
  }
  return m_Data[nIndex];
}

经过简单的测试,暂时还没有发现Bug。可能测试并不全面,感兴趣的读者可以进一步测试并完善该程序。

附:String类的实现

C++ 的一个常见面试题是让你实现一个 String 类,限于时间,不可能要求具备 std::string 的功能,但至少要求能正确管理资源。

如果你弄懂了上面DArray类的写法,那么实现String类应该就不难了。因为面试官一般只是想考查你能不能正确地写出构造函数、析构函数、拷贝构造函数、拷贝赋值运算符以及+、[ ]、<<、>>运算符重载等等。下面给出一个String类的接口,你可以自己试试手实现一下:

class String{
  friend ostream& operator<< (ostream&,String&); //重载<<运算符
  friend istream& operator>> (istream&,String&); //重载>>运算符
public:
  String();  // 默认构造函数
  String(const char* str);    // 带参构造函数
  String(const String& rhs);    // 拷贝构造函数
  String& operator=(const String& rhs);  // 拷贝赋值运算符
  String operator+(const String& rhs) const; //operator+
  bool operator==(const String&);       //operator==
  bool operator!=(const String&);       //operator!=
  char& operator[](unsigned int);       //operator[]
  size_t size() const;
  const char* c_str() const;
  ~String();  // 析构函数
private:
  char *m_data; // 用于保存字符串
};

本文所述DArray类和String类的源码及测试代码可点击此处本站下载。

(0)

相关推荐

  • c++将数组名作为函数参数对数组元素进行相应的运算

    用数组名做函数参数与用数组元素作实参有几点不同: (1)用数组元素作实参时,只要数组类型和函数的形参变量的类型一致,那么作为下标变量的数组元素的类型也和函数形参变量的类型是一致的.因此,并不要求函数的形参也是下标变量.换句话说,对数组元素的处理是按普通变量对待的.用数组名作函数参数时,则要求形参和相应的实参都必须是类型相同的数组,都必须有明确的数组说明.当形参和实参两者类型不一致时,将会发生错误. (2)用普通变量或下标变量作函数参数时,形参变量和实参变量都是由编译系统分配的两个不同的内存单元.

  • C++结构体数组详细解析

    1.定义结构体数组 和定义结构体变量类似,定义结构体数组时只需声明其为数组即可.如: 复制代码 代码如下: struct Student{     int num;     char name[20];     char sex[5];     int age;     float score;     char addr[30];};Student stu[3]; //定义Student类型的数组stu 2.结构体数组的应用举例 题目:对候选人的票的统计程序. 设有3个候选人,最终只能有一个当

  • 详解C++中的一维数组和二维数组

    C++一维数组 定义一维数组 定义一维数组的一般格式为:     类型标识符  数组名[常量表达式]; 例如: int a[10]; 它表示数组名为a,此数组为整型,有10个元素. 关于一维数组的几点说明: 1) 数组名定名规则和变量名相同,遵循标识符定名规则. 2) 用方括号括起来的常量表达式表示下标值,如下面的写法是合法的: int a[10]; int a[2*5]; int a[n*2]; //假设前面已定义了n为常变量 3) 常量表达式的值表示元素的个数,即数组长度.例如,在"int

  • C++实现从数组中同时取出最大最小元素算法示例

    本文实例讲述了C++实现从数组中同时取出最大最小元素的方法.分享给大家供大家参考,具体如下: 算法思想:先相邻两个两个比较,较大的放入数组max[],较小的放入数组min[],然后从max[]数组求出最大,min[]数组求出最小即可. 比较n+[(n+1)/2] =1.5n次 #include <iostream> #define n 11 #define m ((n+1)/2) using namespace std; void main(void) { int num[] = {11,2,

  • C++中的对象数组详细解析

    类是对象的抽象,我们可以使用一个类来定义很多的对象,然后每个对象都有自己的属性. 当我们使用类来定义很多相同结构的对象的时候,我们可以采取对象数组的方法. 例如,一个班有50个学生,我们定义了一个学生类,该类的学生具有相同的数据成员和成员函数,我们就可以定义一个这样的数组. 复制代码 代码如下: Student stdu[50];//假设已经声明了Student类,定义stud数组,有50个元素 ======================对象数组的初始化====================

  • C++对数组的引用实例分析

    C++中所谓数组引用,即指向数组的引用: 如: int a[10] ; int (&b)[10] = a ; 如果写成: int a[10] ; int* &b = a ; 系统将会报错: cannot convert from 'int [10]' to 'int *&'. 或许你会说在数组名不就是指向这个数组的一个指针吗?题中a是int*类型的,b是指向int*的引用,按理应该是正确的啊,为什么会报错呢?这是因为编译器对指向数组的引用检查更加严格,需要检查数组的维数,在这里a被

  • C++指针数组、数组指针、数组名及二维数组技巧汇总

    本文较为详细的分析了关于理解C++指针数组,数组指针,数组名,二维数组的一些技巧.是比较重要的概念,相信对于大家的C++程序设计有一定的帮助作用. 一.关于数组名 假设有数组: int a[3] = {1, 2, 3} 1.数组名代表数组第一个元素的地址,注意,不是数组地址(虽然值相等),是数组第一个元素地址,a 等同于 &a[0]; a+1是第二个元素的地址.比第一个元素地址a(或者&a[0])超出了一个整型指针的大小,在这里是4个字节(byte) cout << a <

  • C/C++中获取数组长度的方法示例

    学过C/C++的人都知道,在C/C++中并没有提供直接获取数组长度的函数,对于存放字符串的字符数组提供了一个strlen函数获取其长度,那么对于其他类型的数组如何获取他们的长度呢? 其中一种方法是使用sizeof(array) / sizeof(array[0]), 在C语言中习惯上在使用时都把它定义成一个宏,比如: #define GET_ARRAY_LEN(array,len) {len = (sizeof(array) / sizeof(array[0]));} 而在C++中则可以使用模板

  • C++中求旋转数组中的最小数字(经典面试题)

    面试题:旋转数组的最小数字 题目:把一个数组的最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转.输入一个递增数组的旋转,输出旋转数组的最小元素.例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1. 算法: (1)当输入的旋转数组非法时:处理! (2)当输入的旋转数组正常时,index1 = 0:index2=length-1: a:如果arry[index1] <arry[index2]时:说明数组为原数组,并没有进行旋转:    b:如果arry[ind

  • C++按照正态分布来排列整型数组元素

    题目要求如下: 给定一个数组input[], 如果数组长度n为奇数,则将数组中最大的元素放到output[]数组最中间的位置, 如果数组长度n为偶数,则将数组中最大的元素放到 output[] 数组中间两个位置偏右的那个位置上, 然后再按从大到小的顺序,依次在第一个位置的两边,按照一左一右的顺序,依次存放剩下的数. 这种处理后结果,如果按照元素的值表示一种分布的图形的话,那绘制后的图形应该是正态分布. 关于正态分布: 正态分布(Normal distribution)又名高斯分布(Gaussia

  • 深入解析C++中的字符数组和处理字符串的方法

    C++字符数组 用来存放字符数据的数组是字符数组,字符数组中的一个元素存放一个字符.字符数组具有数组的共同属性.由于字符串应用广泛,C和C++专门为它提供了许多方便的用法和函数. 字符数组的定义和初始化 定义字符数组的方法与前面介绍的类似.例如: char c[10]; c[0]=′I′;c[1]=′ ′;c[2]=′a′;c[3]=′m′;c[4]=′ ′;c[5]=′h′;c[6]=′a′;c[7]=′p′;c[8]=′p′;c[9]=′y′; 上面定义了c为字符数组,包含10个元素.在赋值

随机推荐