C++指针与动态内存分配

1 指针的前置知识–计算机内存

1.1 什么是内存

内存是CPU与外存交流的桥梁,计算机中所有程序的运行都在内存中进行。

二进制数系统中,计算机内存以(b,比特)为数据存储的最小单位,其中每个位由二进制表示,即0或1。(补充:计算机中的CPU位数指的是CPU一次能处理的最大位数)

8位组成一字节(1Type=8bit)可以存储0~255无符号类型变量,以此类推组成了不同数量级间数据存储

字节(Byte)=8位(bit)
1KB( Kilobyte,千字节)=1024B
1MB( Megabyte,兆字节)=1024KB
1GB( Gigabyte,吉字节,千兆)=1024MB
1TB( Trillionbyte,万亿字节,太字节)=1024GB
1PB( Petabyte,千万亿字节,拍字节)=1024TB
1EB( Exabyte,百亿亿字节,艾字节)=1024PB
1 ZB(Zettabyte,十万亿亿字节,泽字节)=1024EB
1YB( Yottabyte,一亿亿亿字节,尧字节)=1024ZB
BB( Brontobyte,千亿亿亿字节)=1024YB
——摘自百度百科-字节

1.2 什么是指针

我们常说的指针是一个变量,为复合类型(包括数组、字符串、结构等),指针变量实质是指存储了一个内存地址(参见1.3)。而变量会有自己的内存空间,所以虽然指针变量代表着另外一个内存地址,但其自身也会有对应的内存空间存储值,从而导致双重指针或多重指针的存在(多级间接寻址)。

1.3 内存与指针

在计算机中每个变量都有自己的内存位置,并定义了使用地址运算符(&)可以取用的内存地址。即如果var是一个变量,则&var代表它的地址。

内存位置:即物理地址
内存地址:在C/C++中,指针指向逻辑地址
(辨析参见:虚拟地址、物理地址、逻辑地址、线性地址)
//cpp-1.2-1
#include <iostream>
#include <cstdio>
using namespace std;

int main(){
	int var1 = 0;
	char var2[3] = {'a' , 'b' , 'c'};
	cout << "var1变量地址为:" << &var1 << endl;
	cout << "var2变量地址为:" << &var2 << endl;
	return 0;
}

cpp-1.2-1输出结果:

var1变量地址为:0x28feac
var2变量地址为:0x28fea9

注:在C++中,当使用cout输出地址时,输出为16进制


1.4 指针策略

在定义整型等常规变量时,我们关注的是变量的值(指定的量),而变量的地址则为派生量。而在定义指针时,我们关注的是指针变量所指向(存储)的地址,而指向地址处存储的值作为派生量。
——摘改自《C++ Primer Plus 第六版》

根据此策略,指针名表示的是地址,而在指针名前的*的用处则是得到该地址所存储的值。

*运算符 被称为 间接值运算符解除引用运算符

注:指针相关语法参见第二节

//cpp-1.4-1
#include <iostream>
#include <cstdio>
using namespace std;

int main(){
	int var = 1;	//定义整型变量var
	int *p_var = NULL;	//定义指针变量并初始化为空
	p_var = &var;	//将指针指向var变量
	cout<< "变量值为:" << *p_var << endl;
	cout<< "变量地址为:" << p_var << endl;
	//变量的更改
	*p_var = *p_var + 1;
	cout<< "变量值更改后为:" << *p_var << endl;
	return 0;
}

cpp-1.4-1输出结果:

变量值为:1
变量地址为:0x28fea8
变量值更改后为:2

2 指针相关语法

2.1 指针的声明

指针标准声明定义为:

type *var-name;

下面是两个不同的声明:

int* var; //第一种
int *var; //第二种

这两个等价声明了一个整形指针var,称var的类型为指向int的指针(整型指针),称*var的类型为int而不是指针
虽然二者等价,但第一种声明方式尤其要注意以下情况:

int* var1,var2;

此语句将声明var1为int* , var2则为int(而非int*)

2.2 NULL指针、野指针、垂悬指针

在规范化声明指针时,一般将其直接初始化地址或置为空指针,而非不做操作形成野指针。例如下面的定义:

int* var;  //野指针,危险
int* var1 = NULL;  //空指针,安全
int* var2 = &var3; //已初始化,安全

在第一种定义方式时,由于未对其初始化,导致其存储的地址位置未知,存在潜在风险。
NULL的定义实际为:

#define NULL 0

当指针正常指向一个对象,在运行域中对象被销毁,而指针未置空,此时形成了垂悬指针。因此在free或delete时,应将释放内存对应的指针置空避免潜在风险。

2.3 指针的运算(指针与数组)

c++指针允许 + 、- 、 ++ 、 – 四种运算符以及== 、 > 、 <三种比较运算符。我们通过将其与数组结合,借助实例理解:

(1)指针递归遍历数组

//cpp-2.3-1
#include <iostream>
#include <cstdio>
using namespace std;
int main(){
	int a[10]={0,1,2,3,4,5,6,7,8,9};
	int *p = NULL;
	p = a;	//指向数组第一个位置
	
	//Error: int *p = &a;
	
	for(int i=1;i<=10;i++){
		cout<<"地址为:"<<p<<" 值为:"<<*p<<endl;
		p++;	//指针地址移动到下一个位置
	}
	return 0;
}

cpp-2.3-1输出结果:

地址为:0x28fe80 值为:0
地址为:0x28fe84 值为:1
地址为:0x28fe88 值为:2
地址为:0x28fe8c 值为:3
地址为:0x28fe90 值为:4
地址为:0x28fe94 值为:5
地址为:0x28fe98 值为:6
地址为:0x28fe9c 值为:7
地址为:0x28fea0 值为:8
地址为:0x28fea4 值为:9

注:在此处需注意理解实例中Error提出的错误,错误信息为:

cannot convert 'int (*)[10]' to 'int*' in initialization

即当直接&a时,返回的是一个数组类型的指针(参见指针数组),而并非所定义的int*类型。例如:

int *p = &a[1];

是可以编译通过的,左右均为int*型。而在代码中,我们使用p=a获取数组首地址,因为数组名所代表的便是数组中第一个元素的地址,是常量。
指针递减类似,不再赘述。


(2)指针的比较

//cpp-2.3-2
#include <iostream>
#include <cstdio>
using namespace std;
int main(){
	int a[3]={0,1,2};
	int *p = NULL , *endd = &a[2];	//endd指向数组末尾
	p=a;	//指向数组首地址
	while(p <= endd){
		cout<<"当前指针值:"<<*p<<endl;
		if(p==endd)
			cout<<"到达数组末尾"<<endl;
		p++; 
	}
	return 0;
}

cpp-2.3-1输出结果:

当前指针值:0
当前指针值:1
当前指针值:2
到达数组末尾

2.4 指针数组

类似于普通类型的数组,指针也可以通过数组进行操作。例如:

int *p[maxn];

声明了一个大小为maxn的指针数组,p中的每个元素都是一个int*类型的指针。下面分别给出了整型与字符的指针数组示例:

(1)int

//cpp-2.4-1
#include <iostream>
#include <cstdio>
#define maxn 3 
using namespace std;
int main(){
	int a[maxn] = {0,1,2};
	int *p[maxn];	//数组必须通过封闭括号进行初始化,故不可=NULL或者=&a	
	for(int i = 0; i < maxn; i++)
		p[i] = &a[i];	//循环地址赋值 
	for(int i = 0; i < maxn; i++){
		cout << "下标" << i << "指针 指向的地址为:" << p[i];
		cout << " 值为:" << *p[i] << endl;
	}
	return 0;
}

cpp-2.4-1输出结果:

下标0指针 指向的地址为:0x28fe9c 值为:0
下标1指针 指向的地址为:0x28fea0 值为:1
下标2指针 指向的地址为:0x28fea4 值为:2

(2)char

//cpp-2.4-2
#include <iostream>
#include <cstdio>
#define maxn 3
using namespace std;
int main(){
	const char *p[maxn] = {"ZhangSan","LiSi","WangWu"};	
	for(int i = 0; i < maxn; i++){
		cout << "下标" << i << "指针 指向的地址为:" << p[i];
		cout << " 值为:" << *p[i] << endl;
	}
	return 0;
}

cpp-2.4-2输出结果:

下标0指针 指向的地址为:ZhangSan 值为:Z
下标1指针 指向的地址为:LiSi 值为:L
下标2指针 指向的地址为:WangWu 值为:W

注意:在此处,地址存储的即为全部字符串,而值仅为第一个字符(char)。

2.5 this 指针

this指针允许每一个对象获取自己的地址,在类的成员函数内部,this指针用来访问(指向)当前对象。下面给出示例:

//cpp-2.5-1
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
class STU{
	public:
		int num;
		int age;
		STU();
		void print_info();
};
STU::STU(){
	this->num = rand()%40;  //this指向对象
	this->age = rand()%40;
}
void STU::print_info(){
	cout<<endl; 
	cout<<"当前对象地址为:"<<this<<endl;	  //this直接表示对象地址
	cout<<"当前学生(对象)学号为:"<<this->num<<endl;  //this指向对象
	cout<<"当前学生(对象)年龄为:"<<this->age<<endl;
}
int main(){
	STU s1,s2;
	s1.print_info();
	s2.print_info();
	return 0;
}

cpp-2.5-1输出结果:

当前对象地址为:0x28feb8
当前学生(对象)学号为:1
当前学生(对象)年龄为:27

当前对象地址为:0x28feb0
当前学生(对象)学号为:14
当前学生(对象)年龄为:20

3 动态内存分配

3.1 new / malloc、delete / free

在进行编程时,我们常常需要使用不定长或暂时不确定内存大小的对象。通常我们定义变量时,是申请了一段有名称的内存片段。而在动态内存分配中,通过指针允许申请无名称的内存进行存储。

首先我们先区分一下自由存储区

在函数内部定义的的(未加static的)变量,其作用域是此函数,这样的变量将会存储在栈中。(即动态存储空间,地址不固定)

而全局变量将会存储在全局存储区(静态存储区),地址固定。

而在malloc和free的区分上,malloc分配的是自由存储区的内存块,与堆十分相似,使用free回收内存。new分配的是的内存块,使用delete进行回时候,若未回收则在程序结束后由操作系统回收。
(摘改自:https://blog.csdn.net/jankin6/article/details/77970704)

下面重点介绍new的用法。

使用new分配内存通用语法为:

new data-type;

示例:

int *p = NULL;
p = new int;

注:当自由存储区被分配完毕后,可能造成申请内存失败,问题分析与解决参见3.2.

针对于malloc,new不仅分配了内存而且介绍了对象。

在内存使用完毕后,应及时释放相应的内存以便再次调用。通过使用delete关键字完成相应操作。

delete p;
p = NULL; //!!!

警告:delete释放了p所指向的内存块,但并未释放p指针本身。在delete后,p成为了野指针,存在安全隐患,因此我们应及时将p置空(NULL)。

注:new在动态数组的应用参见3.3.

不同的new申请内存方式如下:

int *p = new int;
delete p;

int *p = new int(1); //申请内存并赋值为1
delete p;

int *p = new int [20]; //申请长度为20的数组
cout << p[1];
delete [] p;	//数组的delete方式

//不要忘记指针置空

范例:

//cpp-3.1-1
#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
int main(){
	int *p = new int;	//申请内存 
	*p = 5; //内存复制为5
	cout<<"p的地址为:"<<p<<" 值为:"<<*p<<endl<<endl;
	delete p;  p = NULL; 
	
	p = new int [10];
	for(int i=0;i<=9;i++)
		p[i]=i;
	for(int i=0;i<=9;i++)
		cout<<"下标"<<i<<" p指针地址为:"<< &p[i] <<" 值为:" << p[i] << endl;
	return 0;
	
}

cpp-3.1-1输出结果:

p的地址为:0x656be0 值为:5

下标0 p指针地址为:0x6515c0 值为:0
下标1 p指针地址为:0x6515c4 值为:1
下标2 p指针地址为:0x6515c8 值为:2
下标3 p指针地址为:0x6515cc 值为:3
下标4 p指针地址为:0x6515d0 值为:4
下标5 p指针地址为:0x6515d4 值为:5
下标6 p指针地址为:0x6515d8 值为:6
下标7 p指针地址为:0x6515dc 值为:7
下标8 p指针地址为:0x6515e0 值为:8
下标9 p指针地址为:0x6515e4 值为:9

3.2 常见问题分析与解决

  1. 内存泄漏
    含义: 内存泄漏指程序在申请使用堆中内存后,未及时释放,造成内存的浪费,最终可导致程序运行缓慢和崩溃等严重后果。此问题具有隐蔽性积累性的特征。

    解决方案: 在动态内存使用完毕后及时释放内存,同时对于暂时保留的内存保留其指针。同时对程序建立相应的垃圾回收机制,避免出现此类问题。(注:垃圾回收机制详解参看账号相关文章)

内存泄漏的类型:

常发性内存泄漏:
发生内存泄漏的代码会被多次执行到,每次被执行时都会导致一块内存泄漏。

偶发性内存泄漏:
发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
一次性内存泄漏:
发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块且仅有一块内存发生泄漏。
隐式内存泄漏:
程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。
__摘自百度百科-内存泄漏

  1. 内存溢出
    含义:(狭义)指所申请的内存大小超出了系统剩余可分配内存的大小。当内存泄漏次数不断增多时,就有可能发生内存溢出(Out Of Memory–OOM)
    解决方案:建立异常抛出的处理机制,预处理相关请求,实现多级缓冲层,防止系统自身抛出OOM导致程序崩溃。同时应注意避免缓冲区溢出带来的安全问题。

异常处理实例:

#include <iostream>
#include <cstdio>
#include <new>
using namespace std;
int main(){
//Solution 1:
try{
	int *p1 = new int;
}
catch(bad_alloc &memExp)
{
	cout<<memExp.what()<<endl;	//分配失败
}

//Solution 2:
int *p2 = new (std::nothrow) int;	//分配失败返回空指针
if(p2==NULL)
	cout<<"Error!"<<endl;	
}

注:在使用malloc时,若发生内存分配失败默认返回空指针,可直接使用if判断,new失败后默认返回异常而非空指针,抛出异常后程序会终止,故不可直接if判断(可进行如Solution2的转化后使用if)。

3.3 动态内存分配在工程中的应用

3.3.1 数组的动态联编

数组的动态联编又称动态数组,常用于大型工程编译中。究其原理是避免常见的静态联编中数组的长度为一常量,在编译时便分配固定的内存空间。而动态数组允许用户延迟确定数组长度为变量,通过指针的运用向内存申请空间进行数组建立与使用。下面给出基本范例。

//cpp 3.3.1-1
#include <iostream>
#include <cstdio>
#include <new>
using namespace std;
int main(){
	int len;
	cin>>len;
	int *p = new int [len];
	for(int i=0;i<=len;i++)
		cin>>p[i];
	delete [] p;
	return 0;
}

3.3.2 智能指针(C++11)

在新的标准库中,c++提供了新的指针方案:智能指针,其与普通指针的区别是,智能指针是一个,会在一个内存块被引用完毕后自动释放内存,减少内存泄露问题的出现。

智能指针采用引用计数策略,在内存块上计数,若添加了一个指针指向了此内存块,则计数+1,当计数为0时则自动释放内存。

https://www.cnblogs.com/WindSun/p/11444429.html
——————————————
最后更新时间:2020.11.6 11:08

4 指针及动态内存分配常见模型

4.1 指针链表

4.2 图论中图的链表存储

4.3 多重指针(多级间接寻址)

5 指针的拓展用法

6 注意事项

7 例题与练习

  • 4
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值