用指针管理内存是数据结构中指针最重要的功能。
5.1 C++指针
指针是变量,因此,赋值语句能够把一个指针的值复制到另一个指针。左边的指针变量得到一个新值(地址),并与右边的指针指向同一数据项。
5.1.4 数组和指针 int arr[];
编译器还能依据声明把指针与指针指向的数据项类型和大小联系起来。此时,编译器识别出数据项的字节数。因此,指针arr可以是形式为arr+i 的算术表达式的一部分。表达式的值为arr中第i个元素地址。
arr+1 是arr[1]的地址;arr+2 是arr[2]的地址
*arr = arr[0]; *(arr +1) = arr[1];
int* p; p = arr;
指针p指向arr中的第一个元素:*p = arr[0];C++允许指针变量使用指针运算符和下标运算符[].
例如:p[1] = *(p+1) = arr[1];
加1运算符(++)和减1运算符(--)可用于指针,所以程序员可以用他们执行数组数据的操作。语句p++使p向前移动一个数组位置,语句p--使p向后移动一个数组位置。类似的表达式用于arr是无效的,因为arr是指针常量,不能被更新。
5.1.5 指针和类类型
ptr->f() = (*ptr).f() ;ptr 是指向对象的指针,f()是成员函数。
->运算符叫做反引用和选择运算符,她仅与指向对象的指针一起使用。
例如:time24 t(12,0), *tptr = &t ;
cout<<tptr->getHour() ;
对于涉及重载的time24运算符的表达式,为运算数使用反引用运算符* :
*tptr += 75 ; // *tptr 为对象t
cout<<*tptr ; //输出:13:15
5.2 动态内存
堆可以使程序得到附加内存资源。可以把堆想象成内存的银行,就像存储设备储备资金的金融银行一样,程序可以在需要附加存储空间时向堆借(分配)内存,在不需要时还掉(释放掉)。我们把来自堆中内存称为动态内存,因为程序使用运行时指令分配和释放资源。
5.2.1 程序通过使用运算符new和一个指针变量告诉系统她需要堆内存。假设T是基本类型,ptr是指向类型T的指针。new运算符为类型T的变量从堆中请求内存空间。如果内存可以得到,则系统预留适当的字节数,返回内存的起始地址。如果内存不可用,系统返回0,我们称其为NULL指针。文件<iostream>定义了常量NULL。用赋值语句把返回地址赋给ptr:
// 为类型T的变量分配未初始化的内存
ptr = new T ;
如果类型T的对象需要在堆中有一个初始值,则将其值放在类型名后面的圆括号中:
//为类型T分配内存并初始化其值为initValue;
ptr = new T(initValue);
注意:在从堆中分配内存之前,程序必须声明一个保存内存地址的指针。如果操作new包括一个初始值,操作将在堆中分配内存,并给其赋值和返回地址。如果堆没有足够的内存分配给请求的变量或对象,则new操作返回NULL 。
new运算符能够为对象动态分配内存。然而程序员必须考虑构造函数,当程序为类型ClassName的对象分配内存时,会调用构造函数。当为单个对象动态分配内存时,运算符new允许构造函数参数紧跟类型名。如果没有参数,操作调用默认的构造函数。
5.2.2 动态数组分配
对于大小知道运行时才知道的数组来说,动态内存分配的功能是很明显的。C++扩展了new运算符,使用下标运算符[],把数组大小作为参数。new运算符为n个类型T的对象请求内存,这就是大小为n的动态数组。T * ptr;
ptr = new T[n]; //ptr 是n个元素数组的地址
当T是基本类型时,动态数组中的元素没有初始化。然而,当T是类时,系统调用默认的构造函数来建立每个数组元素。
当动态分配数组时,保留返回地址的指针可以像普通的数组名那样使用。
5.23 内存释放运算符delete
当释放分配给单一对象的动态内存时,delete运算符需要一个以前由运算符new赋值的指针运算数,并释放相应的内存。
ptr = new T; //分配动态内存
delete ptr ; //释放分配的动态内存
当释放动态数组时,使用略有不同的delete形式:在delete和指针变量名之间放一个方括号。系统释放所有最初分配给动态数组的内存。
arr = new T[ARRSIZE]; //为ARRSIZE个对象分配空间
delete [] arr ; //释放动态数组内存
注意区分有初始值的单个对象分配内存以及为指定大小的数组分配内存的操作:
//为初始值为100的单个整形变量分配内存
int *p = new int(100);
//为有100个未初始化整数的动态数组分配内存
int *q = new int[100];
相应的释放单个整形变量以及100个整数元素数组的delete操作分别为:
delete p; // 为单个整数释放内存
delete []q ; //为100个整形元素释放内存
5.3 使用动态内存的类
类经常使用动态内存存储数据。我们把这些类称为动态类。在类的设计和实现时,程序员负责动态内存管理。为了管理内存,动态类通常应该包括为类实例分配动态内存的构造函数和对象被销毁时释放内存的析构函数。如果类允许对象之间的赋值,其实现必须包括一个赋值运算符 = 的重载版本,此运算符把相应的动态数据值从右边的对象复制到左边的对象。
5.3.2 析构函数 :当某个类有动态分配内存的对象时,运行时系统只是销毁对象,并不销毁任何相关的动态内存。为了有效管理内存,需要在对象中释放动态数据,将其作为销毁对象过程中的一部分。否则,内存驻留在堆中,而指向内存的指针不再有效。次内存不能被程序的其余部分访问,这就产生了内存泄露。
C++语言提供了析构函数,当销毁对象时运行时系统调用析构函数。对于任意类,析构函数的原型为类名和字符~ 。字符‘~’表示取反,因此,~ClassName 就是构造函数取反。析构函数没有参数和返回类型。
当程序终止时,运行时系统为主程序中声明的所有对象调用析构函数。对于程序块中的局部对象,当程序退出程序块时调用析构函数。
下例为dynamicClass,演示构造与析构函数的动态内存分配与释放
#include <iostream>
using namespace std;
template <typename T>
class dynamicClass
{
public:
dynamicClass(const T& m1 , const T& m2);
//初始化member1 和member2相关的动态对象
~dynamicClass();
private:
T member1;
T* member2;
};
template <typename T>
dynamicClass<T> ::dynamicClass(const T& m1 , const T& m2):member1(m1)
{
member2 = new T(m2);
cout<<"Constructor :"<<member1<<'/'<<*member2<<endl;
}
template <typename T>
dynamicClass<T> ::~dynamicClass()
{
cout<<"Destructor Called!"<<member1<<'/'<<*member2<<endl;
delete member2;
}
void distroyDemo(int m1,int m2)
{
dynamicClass<int> obj(m1,m2);
}
int main()
{
dynamicClass<int> obj(1,100);
dynamicClass<int>* pObj2;
pObj2 = new dynamicClass<int>(2,200);
distroyDemo(3,300);
delete pObj2;
cout<<"Ready to Exit The Program ."<<endl;
return 0 ;
}
5.4 赋值和初始化
赋值和初始化都产生现有数据项的副本。对于分配动态数据的对象,类必须有专门的成员函数,复制构造函数和重载的赋值运算符,使运行时系统能够执行这些操作。
5.4.1 赋值问题
假设声明语句创建了两个dynamicClass对象,对象objA和objB,构造函数分别分配objA.member2和objB.member2指向的独立动态数据项。如果dynamicClass使用默认的赋值操作,语句objB = objA将从objA到objB对数据逐字节进行复制。指针objA.member2 到指针objB.member2的默认复制带来了问题。两个指针变量在内存中指向了同一个位置。这使得最初属于objB动态内存驻留在堆中,导致内存泄露。
如果objA被销毁且程序继续访问objB,将发生运行时错误。销毁objA调用了对象的析构函数,她释放了objA.member2指向的数据。objB.member2也指向这些数据。objB中试图访问被释放数据的指令可能导致致命的应用程序错误。
默认的赋值操作把objA中的指针member2复制给objB中的指针member2。我们实际上想使指针仍然保持原来的值,而将objA的member2指向的内容复制给objB的member2指向的内容。
5.4.2 重载的赋值运算符
C++允许程序员以成员函数形式重载赋值运算符 = 。dynamicClass中运算符函数的原型为:dynamicClass<T>& operator = (const dynamicClass<T>& rhs); 对于赋值运算符,返回值是引用参数。表达式return *this ;返回当前对象的引用。
参数rhs表示右边的运算数。例如:
objB = objA ; // 实现为 objB.operator = (objA);
5.4.3 指针this
*this是对象本身。程序员只能在类成员函数内部使用标识符this。
5.4.4 初始化问题
对象的初始化是创建新对象的操作,这个新对象是另一个对象的副本。除了在声明中使用外,初始化还用于以下两种情况:在函数中以值参数形式传递对象,以及将对象作为函数值返回。
尽管初始化和赋值都涉及从一个对象到另一个对象复制数据,但这两个操作是不
同的。初始化是创建新对象,这个新对象是现有对象的副本。而对于赋值,由于
先前有声明,所以两个对象都已经存在,其焦点在复制上。运行时系统通过调用
一个称作复制构造函数的成员函数,来执行初始化。此成员函数具有描述性。该
函数是一个构造函数,她通过从现存对象复制值来初始化当前对象。
5.4.5 创建复制构造函数
为了正确的处理分配动态内存的类,C++提供了赋值构造函数来为新的对象分配动态内存并初始化其数据成员。复制构造函数是一个构造函数,所以成员函数使用类名作为函数名。同一类型的一个对象是唯一的参数,而且函数没有返回值。dynamicClass中复制构造函数的原型为:dynamicClass(const dynamicClass<T>& obj); //复制构造函数
如果类不能提供一个显式的复制构造函数,则运行时系统调用默认版本,默认版本只是简单的将数据成员从现有对象复制到新对象。对于动态类,默认版本可能产生错误。
复制构造函数的参数必须是引用参数。(常量引
用)。如果不这样做,并且编译器没有认出这种错
误,后果将是灾难性的。
#include <iostream>
using namespace std;
template <typename T>
class dynamicClass
{
public:
dynamicClass(const T& m1 , const T& m2);
//初始化member1 和member2相关的动态对象
dynamicClass(const dynamicClass<T>& obj);
dynamicClass<T>& operator = (const dynamicClass<T>& rhs);
~dynamicClass();
private:
T member1;
T* member2;
};
template <typename T>
dynamicClass<T> ::dynamicClass(const T& m1 , const T& m2):member1(m1)
{
member2 = new T(m2);
cout<<"Constructor :"<<member1<<'/'<<*member2<<endl;
}
template<typename T>
dynamicClass<T>::dynamicClass(const dynamicClass<T>& obj):member1(obj.member1)
{
member2 = new T(*obj.member2);
cout<<"Copy constructor :"<<member1<<'/'<<*member2<<endl;
}
template<typename T>
dynamicClass<T>& dynamicClass<T>::operator = (const dynamicClass<T>& rhs)
{
member1 = rhs.member1;
*member2 = *rhs.member2;
cout<<"Assignment Operator :"<<member1<<'/'<<*member2<<endl;
return *this;
}
template <typename T>
dynamicClass<T> ::~dynamicClass()
{
cout<<"Destructor Called!"<<member1<<'/'<<*member2<<endl;
delete member2;
}
5.5 miniVector类
5.6 矩阵类(基于模板的矩阵容器)
5.6.1 描述矩阵容器
在基于模板的矩阵类的设计中,我们在一个由向量组成的向量中存储元素。私有数据成员mat 保留矩阵项目:
vector<vector<T> > mat ;
外部向量对应矩阵的行。内部向量对应矩阵的列。
注意:当声明在一个模板类中嵌套另一个模板类时,程序员必须确保两个< 符号不能连续出现。编译器会把>>混淆为输入运算符。