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 常见问题分析与解决
-
内存泄漏
含义: 内存泄漏指程序在申请使用堆中内存后,未及时释放,造成内存的浪费,最终可导致程序运行缓慢和崩溃等严重后果。此问题具有隐蔽性、积累性的特征。解决方案: 在动态内存使用完毕后及时释放内存,同时对于暂时保留的内存保留其指针。同时对程序建立相应的垃圾回收机制,避免出现此类问题。(注:垃圾回收机制详解参看账号相关文章)
内存泄漏的类型:
常发性内存泄漏:
发生内存泄漏的代码会被多次执行到,每次被执行时都会导致一块内存泄漏。偶发性内存泄漏:
发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
一次性内存泄漏:
发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块且仅有一块内存发生泄漏。
隐式内存泄漏:
程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。
__摘自百度百科-内存泄漏
- 内存溢出
含义:(狭义)指所申请的内存大小超出了系统剩余可分配内存的大小。当内存泄漏次数不断增多时,就有可能发生内存溢出(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