C++动态数组类的封装实例
作者:shichen2014 发布时间:2022-07-03 14:52:25
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.介绍预览针对需要在IOS手机上接入原生微信支付场景,调用微信进行支付。如图:1.资料准备1.1 账号注册打开https://open.w
- 制作透明窗体办法有好几种,各有优缺点. 我们先来看看C#本身提供的办法 1:通过设置窗体的 TransparencyKey实现 例:窗体中
- 一、统一全站字符编码通过配置参数charset指明使用何种字符编码,以处理Html Form请求参数的中文问题package me.gacl
- 目录一、 全局JDK设置(默认配置)二、主题设置三、字体大小设置四、字符集和配置文件编码格式设置五、自动导入设置六、自动忽视大小写设置七、关
- 题目我们可以用2×1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2×1的小矩形无重叠地覆盖一个2×n的大矩形,总共有多少种方法?程序核心
- 工作中有做过手机App项目,前端和android或ios程序员配合完成整个项目的开发,开发过程中与ios程序配合基本没什么问题,而andro
- ListBox控件的使用: 1)控件属性 Items SelectedItems SelectioModes 2)数据绑定 DataSour
- 本文实例为大家分享了java实现KFC点餐系统的具体代码,供大家参考,具体内容如下package KFC点餐系统;//food 类 publ
- 概述引入多态是继封装、继承之后,面向对象的第三大特性。 生活中,比如跑的动作,小猫、小狗和大象,跑起来是不一样的。再比如飞的动作,昆虫、鸟类
- 本文实例为大家分享了Android实现简单顶部标题栏的具体代码,供大家参考,具体内容如下实现功能:1)自定义View标题栏布局;2)灵活的可
- 若干年前在使用SpringMVC的时候,发现springMVC可以把HttpSession,HttpRequest组件化注入:@Autowi
- 生成1. 生成error 文件的路径:你可以通过参数设置-XX:ErrorFile=/path/hs_error%p.log, 默认是在Ja
- Linux内核实现名称空间的创建ip netns命令可以借助ip netns命令来完成对 Network Namespace 的各种操作。i
- 前提:集成开发环境(IDE):eclipsejdk版本:8.0File类的几个方法:1)isFile()测试此抽象路径名表示的文件是否为普通
- 本文实例讲述了Java实现的简单网页截屏功能。分享给大家供大家参考,具体如下:package awtDemo;import java.awt
- 一、RabbitMQ介绍RabbitMQ是实现AMQP(高级消息队列协议)的消息中间件的一种,最初起源于金融系统,用于在分布式系统中存储转发
- //activity的xml<?xml version="1.0" encoding="utf-8&qu
- 本文实例讲述了java旋转二维数组的操作,分享给大家供大家参考。具体实现方法如下:package test;/* * &
- 1. 前言Spring最重要的一个概念当属Bean了,我们写的Controller、Service、Dao凡是加了对应注解交给Spring管
- private void button2_Click(object sender, EventArgs e) &nbs