[note] C++程序设计原理与实践 vector与其实现相关的背景知识

可变长数组

vector比数组的优越性:

  1. 沿袭了数组的任意访问的特性,支持任意类型的元素。同时进行范围检查近似最优的高效访问。
  2. 对底层内存访问机制进行了良好的封装,同时加入了一定的成员函数
  3. 元素个数运行时动态可变,且可以在操作系统允许的情况下容纳的相同类型的数据元素。

数据的存储与访问

几个概念

  • 数据类型
  • 多字节数据的Big-Endian和Little-Endian:高位放在高地址,那么数据的地位的“尾巴”在低位,于是称作小尾存储,也叫Little-Endian

指针

用于表示存储内存地址的数据类型,指针的值是该数据的地址最低位。

指针的运算

加减运算:单位‘1’是指向的数据类型的地址长度。
关系运算:比较保存的地址大小。

内存模型

  • 代码区(code):可执行代码
  • 静态存储区(static):全局/静态变量
  • 栈区(Stack):局部变量
  • 堆(Heap):程序中通过new运算符动态申请。也就是我们所说的自由存储区

自由存储区分配

动态内存分配机制——new运算符的使用

一维动态分配

通过new运算符在堆中动态申请内存。

声明格式:

T *p = new T;
int *q = new int[7];
double *pd = new double[n]

说明:

  • 申请失败:如果申请空间(out of memory)失败,运算符new将抛出bad_alloc的异常
  • 指针的机制:由于指针知道指向对象的类型(通过数据类型长度),但不确定内存包含对象的数量,所以符合我们对vector的功能的想法。(重要
  • 任意类型兼容由于指针仅有指向处和内存长度决定。所以可以通过储存任意类型数据的指针来实现任意对象的vector

    这一点给我们的FLTK lab创建Triangle对象的vector创造了条件

多维数组的动态内存申请

二维
int **array
// 假定数组第一维长度为 m, 第二维长度为 n。这个维度的顺序仍然与一般数组的几个中括号的顺序相同。
// 动态分配空间
array = new int *[m];
for( int i=0; i<m; i++ )
{
    array[i] = new int [n];//这个n必须给出,否则连续空间的分配将出现重大困难
}
//释放
for( int i=0; i<m; i++ )
{
    delete [] arrary[i];
}
delete [] array;
三维
int ***array;
// 假定数组第一维为 m, 第二维为 n, 第三维为h
// 动态分配空间
array = new int **[m];
for( int i=0; i<m; i++ )
{
    array[i] = new int *[n];
    for( int j=0; j<n; j++ )
    {
        array[i][j] = new int [h];
    }
}
//释放
for( int i=0; i<m; i++ )
{
    for( int j=0; j<n; j++ )
    {
        delete[] array[i][j];
    }
    delete[] array[i];
}
delete[] array;

动态内存和指针

通过指针的访问

利用解引用运算符*或下标运算符[]访问:

p[i] 等效于*(p+i),在内存中的移动,单次移动长度由类型决定。

这里如果要利用[]访问,那么要将原数组的[]重载成相应类型的引用类型。

函数返回值为对象的引用?那么我们就可以把它作左值进行随机访问改写了。

此外我们还可以说说另一个返回引用的原因。赋值构造函数我们应使用引用类型。这是STL中各个数据类型共同遵守的习惯,目的在于连等赋值

关于返回引用我们还可以思考,ostream重载为什么是对象的引用
返回值类型应该为:ostream&。ostream是为了可以连续输出,引用是对ostream禁止拷贝,对于流对象复制这个复杂的过程且是不被允许的。

关于为什么使用引用类型,以下这个例子可以给出比较合理的解释:

// 重载下标运算符
double vector::operator[]( int n ) { return elem[n]; }	// 返回元素的值
double d = a[0];      // 正确
a[0] = 1.0;                // 错误, v[0] 是一个值

double * vector::operator[]( int n ) { return &elem[n]; }	// 返回指针
double d = *a[0];    // 正确
*a[0] = 1.0;              // 正确

double& vector:: operator[]( int n ) { return elem[n]; }	// 返回引用
double d = a[0];      // 正确
a[0] = 1.0;                // 正确

其他

代码间地址传递——无类型指针void*

无类型是机器内存地址的直接表示。

为了在两段代码之间传递地址,而双方无法确定使用的地址类型时,使用void*。这个设计提高了函数对地址这种长短不一的变量的兼容性

例如FLTK lab中回调函数的地址传递

     void Lines_window::cb_next( Address pb, Address pw)
     {   
          // 等价于( (Lines_window *) pw )->next( );
	     reference_to< Lines_widow >( pw ).next( );    
      }
//其中,
    typedef void* Address;

注意在使用过程当中,要指明指针指向对象的类型。在指明前void*型变量不能进行指针的一般运算

指针、常量和引用
指针和常量

指针常量T* const p 是定向指针不能改变指针的保存的地址,即不能转指。

int a = 1, b = 2;
int * const p = &a;     // 常量必须初始化
p = &b;               // 错误:常量的值不可修改
*p = 10;	    // 正确:可以修改指向的变量的值

常量指针`const T* p是无能指针不能改变指针所指向的变量。

int a =1, b = 2;
const int *p = &a; 	// 也可以写作 int const * p = &a
*p = 10;		// 错误:指针指向的是常量,值不可修改
p = &b;         // 正确:指针不是常量,可以重新赋值
指针与引用

可以将引用看作是自动解引用的指针常量。
即:reference == *const pointer

动态内存的释放——delete运算符的使用

内存泄漏

  • 如果只申请、不释放,那么堆中的内存终将被耗费掉。
  • 指针转指的时候,先前指向的动态区间未被释放。

程序退出时,OS会将动态内存返回,在日常微型程序和oj刷题完全可以不归还。但如果申请动作高频发生,或者程序长时间运行,那么这个内存泄漏的效应将是显著的。

一般格式

delete p;//deleteing both a unit --or-- array.
delete []p;//deleting arrays 

deletedelete[]的区别

对内置类型使用new分配后的数组还是非数组形式内存空间,用两种方式均可正确释放内存。

int *a = new int[10];   
delete a;   
delete [] a; 

但对自定义类型,delete p_obj调用p_obj指向的析构函数

这反映了析构函数的实质:释放类的对象占用的资源。

所以在如下的情形当中:

string *p = new vector[10]; 
delete   p; 	// 仅调用第一个对象的析构函数
delete[] p;	// 调用所有对象的析构函数	

必须使用delete[] p;(链接effective C++ item 16)

派生类与封闭类的析构

考虑到多态中已经讨论过的,如果对基类指针使用delete,那么当基类的析构函数非virtual时只调用基类的析构函数,这将导致派生类自有成员内存的大量泄漏。

成员对象的析构函数调用晚于封闭类。

堆or栈

堆的使用场景:

  • 程序运行时需要动态分配内存
  • 需要分配的内存超过栈空间大小

栈的好处:

  • 自动释放
  • 线性表效率高

vector的初始化

构造

先前是利用传参的方式,
C++11中多种STL容器给出了利用 initializer_list的构造函数,所以我们可以使用形如:

std::vector<int> vec = {1, 2, 3, 4, 5};
std::list<int> arr = {1, 2, 3, 4, 5};
std::map<std::string, int> mapOfMarks = {
        {"Riti",2},
        {"Jack",4}
};

的构造语句。

拷贝构造

浅拷贝和深拷贝

对于指针的拷贝可以分成两种类型:

浅拷贝:拷贝的是指针的值,拷贝后两个指针会指向同一对象。这也是内置类型自身拷贝的原则导致的,不过指针的独特性质导致了这个麻烦

double *p1 = new double {1.0};
double *p2 = p1;

深拷贝:拷贝的是指向指针的数据,拷贝后指针指向不同的对象

深拷贝对于指针来说常用

double *p1 = new double{1.0};
double *p2 = new double {*p1};

当我们希望在对象管理中拥有较高程度的保密性时,我们不允许深拷贝。这也正是 auto_ptr的意义。

拷贝构造

class vector {
	int sz;
	double* elem;
public:
	vector( const vector& ) ;	// 拷贝构造函数
	// …
};


// 为元素分配存储空间,通过元素值得拷贝对元素进行初始化
vector::vector( const vector& a )
:sz{ a.sz },  elem{ new double[a.sz] }
{
	for ( int i = 0; i < sz; ++i ) elem[i] = a.elem[i];
}

对数据逐个进行深拷贝

拷贝赋值

拷贝赋值对应着作为左值的对象已经存在的情况,所以如果直接使用默认的拷贝构造,会出现先前申请的内存区域未被释放的危险。所以我们在为vector设计拷贝构造函数的时候,应当注意对先前资源的释放。

class vector {
	int sz;
	double* elem;
public:
	vector& operator=(const vector& a); // 拷贝赋值
};

vector& vector::operator = ( const vector& a )
{
	double* p = new double[a.sz];			// 分配新的内存空间
      for ( int i = 0; i<a.sz; ++i ) p[i] = a.elem[i];		// 拷贝元素
	delete[ ] elem;					// 释放之前占用的内存空间

	sz = a.sz;					// 设置元素个数
	elem = p;					// 设置指向元素的指针

	return *this; 		                                   	// 返回对象自身的引用
}

类对象指针和移动构造

类对象的指针我们仅在此记录this指针的用法。
谨注意:类对象指针的重要作用发生在virtual函数的使用上

this指针

第一个 C++ 的编译器实际上是将 C++ 程序翻译成C语言程序,然后再用C语言编译器进行编译。

然而C语言没有类的概念,只有结构,函数都是全局函数,没有成员函数。翻译时,将class翻译成struct、对象翻译成结构变量是显而易见的,但是对类的成员函数应该如何翻译?

C语言中只有全局函数,因此成员函数只能被翻译成全局函数;这样的语句也只能被翻译成普通的调用全局函数的语句。

那如何保证翻译后的全局函数还能作用在某个确定的结构变量上呢?答案就是引入“this指针”。

移动构造

  • 防止返回的对象被销毁。
  • 对象比较大的数据结构,为了避免构造和析构的开销

C++11中提供了一个移动构造的机制:即函数参数定为&&型变量。

思考:(引用的引用)如果对引用,则是对传进来的参数进行复制,对引用的引用,就干脆把地址复制进来。这在效应上类似于浅拷贝的机制,只不过我们在同时将原来的位置清空,从而仅此一份不发生多余的构造和析构。

// 移动构造函数
vector::vector( vector&& a )	
:sz{ a.sz }, elem{ a.elem }	// 拷贝对象a的成员 elem 和 sz 的值
{
   a.sz = 0;               	 	// 对象 a 包含 0 个元素
   a.elem = nullptr;               	// 对象 a 为空 vector
}

// 移动赋值函数
vector& vector::operator = ( vector&& a )
{
     delete[] elem;		// 释放已占用空间
     elem = a.elem; 	                // 拷贝对象 a 的成员 elem 和 sz 的值
     sz = a.sz;
     a.elem = nullptr;	 // 对象 a 为空 vector
     a.sz = 0;
     return *this;       	 // 返回对象自身的引用
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值