【6】C++进阶系列(动态内存分配)

问题:之前在写程序的时候计划好我们需要哪些数据,都定义好,但是有些时候我们并不知道我要处理的程序规模有多大,也不知道数组开多大合适,是尽量大?分配了太大空间可能会造成内存的浪费。只有在程序真正运行起来才会知道这次运行要处理的数据规模有多大——那就有人想,能不能用变量来确定数组的个数?不能。但是我们有办法在运行时确定数组要多大,这个数组要以动态内存分配的方法来确定。

必须使用指针——当必须用动态内存分配的方式去申请内存单元的时候,动态分配的单元没有机会来取数组名。——返回就是一个首地址,那么我们迫不得已使用指针去访问。

1、动态申请内存new:

new 类型名T(初始化列表)

功能:在程序执行期间,申请用于存放T类型对象的内存空间,并依照初始列表赋以初值。

结果:成功:T类型的指针,指向新分配的内存单元。失败(比如申请的空间很大,而此时内存不够):抛出异常

成功的时候,会返回一个指向内存单元首地址的指针。

释放内存操作符delete:

delete 指针p,功能:释放指针p所指向的内存,p必须是new操作的返回值。

#include<iostream>
using namespace std;

class Point
{
public:
	Point() :x(0), y(0) {
		cout << "Default constructor called." << endl;
	}
	Point(int x,int y):x(x),y(y){
		cout << "Constructor called" << endl;
	}
	int getx()const { return x; }
	int gety()const { return y; }

	~Point();
	void move(int newX, int newY) {
		x = newX;
		y = newY;
	}


private:
	int x, y;
};

Point::~Point()
{
	cout << "Deconstructor called." << endl;
}


int main() {
	cout << "Step one." << endl;
	Point *ptr1 = new Point;
	delete ptr1;
	cout << "Step two." << endl;
	ptr1 = new Point(1, 2);
	delete ptr1;
	return 0;
 }

delete是说删除指针所指的对象,而不是说删除指针自己,指针变量还在,他是主函数的一个局部变量。当然,主函数结束的时候,指针变量自己的空间还是会释放的。

2、申请和释放动态数组

如果我们在编写程序时,不知道要申请的内存有多大,就可能会申请动态数组用完之后释放。

分配:new 类型名T【数组长度】

数组长度可以是任何整数类型的表达式,在运行时计算。

释放:delete 【】数组名p

释放指针p所指向的数组,p必须是用new分配得到的数组首地址。如果没有加【】,只会释放数组首元素的地址。

例子:

#include<iostream>
using namespace std;

class Point
{
public:
	Point() :x(0), y(0) {
		cout << "Default constructor called." << endl;
	}
	Point(int x,int y):x(x),y(y){
		cout << "Constructor called" << endl;
	}
	int getx()const { return x; }
	int gety()const { return y; }

	~Point();
	void move(int newX, int newY) {
		x = newX;
		y = newY;
	}


private:
	int x, y;
};

Point::~Point()
{
	cout << "Deconstructor called." << endl;
}


int main() {
	Point *ptr = new Point[2];
	ptr[0].move(5, 10);
	ptr[1].move(10, 15);
	cout << "Deleting……" << endl;
	delete[]ptr;
	return 0;
 }

在写简单的程序时要养成随用随delete的习惯,不然以后写大型程序会出现很多错误。

动态创建多维数组new 类型名T【第一位长度】【第二位长度】...;

如果内存申请成功,new运算会返回一个指向新分配内存首地址的指针。例如char (*fp)[3];  fp=new char[2][3];

                                                                    

例子:

#include<iostream>

using namespace std;


int main() {
	int(*cp)[9][8] = new int[7][9][8];
	for (int i=0;i<7;i++)
		for (int j = 0; j < 9; j++)
		{
			for (int k = 0; k < 8; k++) {
				*(*(*(cp + i) + j) + k) = (i * 100 + j * 10 + k);
			}

		}
	for (int i = 0; i < 7; i++)
	{
		for (int j = 0; j < 9; j++) {
			for (int k = 0; k < 8; k++) {
				cout << cp[i][j][k] << " ";
			}
			cout << endl;
		}
		cout << endl;
	}
	delete[] cp;
	return 0;
}

上述过程实际上是很繁琐的。那么我们想,把动态数组封装成类,使这个过程更加简洁,便于管理。可以在访问数组前检查下标是否越界。

//dynamic.h
#include<iostream>
#include<cassert>

using namespace std;

class Point
{
public:
	Point() :x(0), y(0) {
		cout << "Default constructor called." << endl;
	}
	Point(int x, int y) :x(x), y(y) {
		cout << "Constructor called" << endl;
	}
	int getx()const { return x; }
	int gety()const { return y; }

	~Point();
	void move(int newX, int newY) {
		x = newX;
		y = newY;
	}
private:
	int x, y;
};

Point::~Point()
{
	cout << "Deconstructor called." << endl;
} 


class ArrayOfPoint//动态数组类
{
public:
	ArrayOfPoint(int size) :size(size) {
		points = new Point[size];
	}
	~ArrayOfPoint() {
		cout << "Deleting..." << endl;
		delete[] points;//析构函数的作用
	}
	Point &element(int index) {//取数组元素,返回的是引用,左值
		assert(index >= 0 && index < size);
		return points[index];
	}

private:
	Point *points;//指向动态数组首地址
	int size;//数组大小
};

//源.cpp
#include<iostream>
#include<cassert>
#include"dynamic.h"

using namespace std;

int main() {
	int count;
	cout << " Please enter the count of points" << endl;
	cin >> count;
	ArrayOfPoint points(count);
	points.element(0).move(5, 10);
	points.element(1).move(15, 20);
	return 0;
}

3、智能指针

使用new和delete显式的内存管理有一定的安全隐患:比如忘记释放的话内存泄漏。——引出智能指针

                          

4、vector对象

vector是标准类库中的类模板。比如前面我们定义的动态数组类只能存放Point类对象,它不够通用,二vector它是相对通用的模板,我们希望存放什么类型的数组,就在<>内说明模板的类型就行了。例如:vector<double>array(n);就定义了一个array对象,他是一个数组,它有n个元素。他可以用数组名小彪的形式访问数组元素。

比自己定义的数组更加安全和方便。封装任何类型的动态数组,自动创建和删除。能进行数组下标越界检查。

定义:vector<元素类型>数组对象名(数组长度);

例如:vector<int> arr(5);//建立大小为5的int数组

a、对数组元素的引用它与普通数组一样:vector对象名【下标表达式】,vector数组对象名不表示数组首地址。

b、获得数组长度,用size函数:vector对象名.size(),当将vector对象传给一个函数的时候就不需要另外将数组长度传过去。

#include<iostream>
#include<vector>
using namespace std;

//计算数组arr中元素的平均值
double average(const vector<double>&arr) {
	double sum = 0;
	for (unsigned i = 0; i < arr.size(); i++) {
		sum += arr[i];
	}
	return sum / arr.size();
}

int main() {
	unsigned n;
	cout << "n:" << endl;
	cin >> n;

	vector<double>arr(n);//创建数组对象
	cout << "Please input" << n << "  real numbers:" << endl;
	for (unsigned i = 0; i < n; i++) {
		cin >> arr[i];
	}
	cout << "average = " << average(arr) << endl;
	return 0;

}

基于范围的for循环配合auto举例:

int main() {
	vector<int>v = { 1,2,3 };
	for (auto i = v.begin(); i != v.end(); i++) {//自动类型
		cout << *i << endl;
	}
	for (auto &e : v) {//遍历,auto里面是什么类型,这个e就是什么类型
		cout << e << endl;
	}
	return 0;
}

5、浅层复制和深层赋制

一个场景:假如我们在类的数据成员中有指针,而指针指向的空间是在构造对象的时候通过动态内存分配方式获得的空间。这个时候想要对构造对象进行复制,就不能用浅层复制了。

所以浅层复制和深层复制有什么不一样呢?

       

将第一个数组两个元素移动一下:

                    

应该是析构4次,但是出错了!

浅层拷贝:

                                    

说明了浅层复制不能满足我们的需求。

深层拷贝:

   

6、移动构造

移动并没有发生复制。

问题:程序中复制构造的时候,是不是有时不一定要去复制呢?——有些复制构造是不必要的

c++11引入移动构造的目的:移动构造就是要将源对象的资源控制权全部交给目标对象。

当临时对象被复制之后就不会被利用了,我们完全可以把临时对象的资源直接移动,这样就避免了多余的复制操作。

两者的区别:

                                 

什么时候触发移动构造?——临时对象即将消亡,但是它却又是可以被利用的临时对象。

移动构造函数:class_name(class_name &&)

&&是右值引用,即将消亡的值就是右值,函数返回的临时变量也是右值。二单个&引用是可以绑定到左值的。

我们调用一个函数,这个函数的返回值是一个对象,而且这个对象有指针成员。这种情况下,怎样达到将对象返回呢?

版本一:就使用深层复制构造函数。

返回值构造临时对象,动态分配将临时对象返回到主调函数,然后删除临时对象。

版本二:使用移动构造函数

将要返回的局部对象转移到主调函数,省去了构造和删除临时对象的过程。效率会高一些

差别:注意看中间被注释掉的程序段

(复制构造)

(移动构造)

7、c风格字符串

有字符串常量:形如"slkajfdl";但是c++基本类型中没有字符串变量。

c++有两种方式来处理字符串。一种是从c语言中继承来的:c风格字符串。就是说用字符数组来存放字符串

而在c++,使用字符串类String。

            

注意:末尾的“\0”是一个标志。另外可以将字符串常量的首地址赋值给char常量指针,如果忘记0,程序就会出错。const char *string1=“program”。

在c++中不建议继续使用c风格字符串的。但是应该要看得懂别人写的c风格的字符串,c++程序中存在很多c风格的字符串

                                                                         

string类:封装起来的字符串,字符数组。不用考虑长度越界,下标越界。而且自身有许多功能,比如拼接,比较。                                类里面重载了我们常用的运算符。所以有这么多操作和功能

                                                          

string S1;默认长度是0,但是可以扩展,不用多余的操作,直接赋值的可以了。

cin:提取键盘输入运算符,自动以空格为分割符的。有的时候我们希望输入一整行字符串,里面包括空格,用cin就完成不了这个任务了。这个时候可以用getline函数来整行输入字符串,包括空格。

getline(cin,s1)第一个参数是输入流对象,这里代表从键盘输入。还可以从文件输入等。第二个对象是字符串对象。第三个是可选参数,作为分隔符标志,以什么符号作为字符串结束的标志。。

                                                         

例子:指定和默认情况(默认是以行结束为分隔符)

                                               

猜你喜欢

转载自blog.csdn.net/qq_21210467/article/details/82810454
今日推荐