可变长数组
vector比数组的优越性:
- 沿袭了数组的任意访问的特性,支持任意类型的元素。同时进行范围检查,近似最优的高效访问。
- 对底层内存访问机制进行了良好的封装,同时加入了一定的成员函数。
- 元素个数运行时动态可变,且可以在操作系统允许的情况下容纳的相同类型的数据元素。
数据的存储与访问
几个概念
- 数据类型
- 多字节数据的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
delete
和delete[]
的区别
对内置类型使用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; // 返回对象自身的引用
}
转载:https://blog.csdn.net/weixin_45502929/article/details/105582300