堆与指针

本文详细介绍了C++中指针的基本概念及其在内存管理中的应用,包括如何在堆中创建和删除对象、访问堆中数据成员的方法、指针的运算及常量指针的使用等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

用指针创建堆中空间:
堆的好处是可以存储比较大的数据,而且储存的数据只要不是程序员手动将其释放那么就会永远保存在堆中。不像栈,存储的数据值在函数内有效,超出函数就是消失了。也不像全局变量,保存的数据只有程序释放才会释放,而且很容易被修改。
既然了解了它的许多好处,接下来的问题就是如何使用它,我们知道堆是一大堆不连续的内存区域,在系统中由链表将它们串接起来,不像栈,你可以为它其中的某个内存单元命名,为了数据隐秘起见,堆中的每个内存单元都是匿名的,因此你必须先在堆中申请一个内存单元的地址,然后把它保存在一个指针中。这样你只有使用该指针才可以访问到该内存单元的数据。
删除一个空指针是安全的。

注意:
在使用new以后,假如不再使用该内存空间,那么一定要用delete来释放它。

在堆中创建对象:
我们既然可以在堆中保存变量,那么久可以保存对象,我们可以将对象保存在堆中,然后通过指针来访问它。

#include<iostream>
using namespacestd;
class Human
{
public:
Human(){ cout << "构造函数在执行!\n"; i = 999; }
private:
int i;
};
int main()
{
Human*P= new Human;
return 0;
}


//使用new  Human在堆中创建一个类Human的对象,由于创建对象时会自动调用类的构造函数来初始化对象的成员变量,所以调用了第6行的构造函数,调用的结果为初始化了i为999,并输出了一行文字:构造函数在执行!第12行的右半部分在堆中创建对象完毕后,就会返回该对象的地址,这个地址赋给了左边的指针变量P。在堆中创建的对象时匿名的,它没有名字,我们无法直接访问它,只能通过指针来访问它。指针记录了堆中对象的地址,所以只有指针才能找到对象,以其他的方式都不能找到这个对象。

//定义了一个Human类的指针P使用new创建一块内存空间,同时又调用了Human类的默认构造函数来构造一个对象,它所占用的内存大小根据Human类对象的成员变量来决定,假如该类有两个int型成员变量,那么该对象占用为2乘以4等于8个字节。构造函数一般都是在创建对象时被自动调用,它的作用就是初始化该对象的成员数据。本行的右半部分创建一个对象完毕后,跟着将该对象的内存地址赋给左边的指针变量P。


在堆中删除对象:
假如我们要删除再堆中创建的对象,我们可以直接删除指向该对象的指针,这样会自动调用对象的西沟函数来销毁该对象同时释放内存。

#include<iostream>
using namespacestd;
class Human
{
public:
Human(){ cout << "构造函数在执行!\n"; i = 999; }
~Human(){ cout << "析构函数在执行!\n"; }
private:
int i;
};
int main()
{
Human*P= new Human;
delete P;
return 0;
}


访问堆中的数据成员:


#include<iostream>
using namespacestd;
class Human
{
public:
Human(){ cout << "构造函数在执行!\n"; i = 999; }
~Human(){ cout << "析构函数在执行!\n"; }
int get()const{ return i; }
private:
int i;
};
int main()
{
Human jack;
cout<<jack.get();
Human*P= new Human;
//(*P).get();
cout<<P->get();
delete P;
return 0;
}


假如我们要访问对象的数据成员和函数,我们使用成员运算符“.”点。
C++专门为用指针间接访问对象的成员设置了一个运算符--成员指针运算符“->”。该符号可以实现读取对象的内存地址并且访问对象的成员的作用。

在构造函数中开辟内存空间:
我们可以将类的书籍成员定义一个指针,然后再构造函数中开辟新空间,将空间的地址赋给指针。而在析构函数中释放该空间。
由于int是个整型,不是类对象,所以new int(999)不会调用构造函数,而是将999这个数值存储到新建的内存区域中。
在构造函数中开辟了新空间,将空间的地址赋给指针。

#include<iostream>
using namespacestd;
class Human
{
public:
Human(){ cout << "构造函数在执行!\n"; i=new int(999); }
~Human(){ cout << "析构函数在执行!\n"; delete i; }
int get()const{ return *i; }
private:
int *i;
};
int main()
{
Human jack;
cout<<jack.get();
Human*P= new Human;
//(*P).get();  这样也可以
cout<<P->get();
delete P;
return 0;
}
该例仅仅是说明构造函数中也可以开辟堆中空间,在实际程序中,一个在堆中创建的对象通过成员指针再创建新空间用来保存数据并没有什么意义。因此在堆中创建对象时已经为它的所有数据成员提供了保存的空间。


对象在栈与堆中的不同:
一个存储在栈中的对象,会在超出作用领域时,比如说遇到右大括号自动调用析构函数来释放该对象所占用的内存。
#include<iostream>
using namespacestd;
class Human
{
public:
Human(){ cout << "构造函数在执行!\n"; i=new int(999); }
~Human(){ cout << "析构函数在执行!\n"; delete i; }
int get()const{ return *i; }
private:
int *i;
};
int main()
{
Human jack;
return 0;
}

而一个存储在堆中的对象,则需要程序员自行对其所占用的内存进行释放。否则该对象所占用的内存知道程序结束才会被系统回收。

#include<iostream>
using namespacestd;
class Human
{
public:
Human(){ cout << "构造函数在执行!\n"; i=new int(999); }
~Human(){ cout << "析构函数在执行!\n"; delete i; }
int get()const{ return *i; }
private:
int *i;
};
int main()
{
Human *jack = new Human;
delete jack;
return 0;
}



this指针:
对象要在属于自己的每个成员身上写下自己的名字,以证明该成员是自己的成员,而不是别人的对象的成员。this变量帮助对象做到这一点,this变量记录每个对象的内存地址,然后通过间接访问运算符 ->访问该对象的成员。

#include<iostream>
using namespacestd;
class A
{
public:
int get()const{ return i; }
void set(int x){this-> i = x; cout << "this变量保存的内存:\t" << this << endl; }
private:
int i;
};
int main()
{
A a;
a.set(9);
cout << "对象a的内存地址:\t" << &a << endl;
cout << a.get() << endl;
A b;
b.set(999);
cout << "对象a的内存地址:\t" << &b << endl;
cout << b.get() << endl;
return 0;
}
这说明this变量记录每个单独的对象的内存地址,而this指针则指向每个单独的对象。因此不同的对象输出的this变量的内存地址也不同。
默认情况下,this指针可以省略不写。比如this->i=x;假如我们写i=x;,编译器会自动在成员变量i前面加上this指针,用来表示这个i成员是属于某个对象的。
由于this指针保存了对象的地址,因此你可以通过指针直接读取某个对象的数据,它的作用将会在后面的重载运算符中得到演示,现在我们只需要知道this变量保存的是对象的地址,那么this指针就是指向对象的指针。另外this指针的创建与删除由编译器来完成,你不需要操心。


指针的常见错误:
前面我们说过删除一个指针后一定要将该指针设置为空指针,这是因为删除该指针只会释放它所指向的内存空间,不会删除指针,因此这个指针还存在,并且它仍然会指向原来的内存空间,因此这时如果你再次尝试使用该指针,那么就会导致程序出错。


指针的加减运算:

#include<iostream>
using namespacestd;
int main()
{
int *P = new int;
cout << "指正P保存的空间地址为:\t\t\t" << P << endl;//原本的P内存
P++;//将指针变量P中的没存地址自加,由于P指向的是int型变量,因此执行减1操作会将原来的内存地址增加4个字节。
cout << "自加后,指针P保存的空间地址为:\t\t" << P << endl;//P已增加4个字节
P--;//将指针变量P中的没存地址自减,由于P指向的是int型变量,因此执行减1操作会将原来的内存地址减少4个字节。
cout << "自加减,指针P保存的空间地址为:\t\t" << P << endl;//与原来相同
P  -= 2;
cout << "自加减2,指针P保存的空间地址为:\t" << P << endl;//减少8个字节
return 0;
}


指针的赋值运算:

        指针也可以进行赋值运算,比如将一个指针变量地址赋给另一个指针变量地址。
#include<iostream>
using namespacestd;
int main()
{
int *P = new int;
cout << "P:" << P << endl;
int *P1 = new int;
cout << "P1:" << P1 << endl;
P = P1;
cout << "赋值后。。。\n";
cout << "P:" << P << endl;
return 0;
}

指针的相减运算:

这样P指向的内存空间保存的就是P和P1的地址差。
#include<iostream>
using namespace	std;
int main()
{
	int *P = new int;
	cout << "P:" << P << endl;
	int *P1 = new int;
	cout << "P1:" << P1 << endl;
	*P =P- P1;
	cout << "两块内存的地址差:";
	cout  << *P << endl;
	return 0;
}

指针的比较运算:
两个指针之间也可以进行比较运算。
#include<iostream>
using namespace	std;
int main()
{
	int *P = new int;
	cout << "P:" << P << endl;
	int *P1 = new int;
	cout << "P1:" << P1 << endl;
	if (P > P1)
	{
		cout << "P>P1的内存地址。\n";
	}
	else
		cout << "P<P1的内存地址。\n";
	return 0;
}
比较的是内存地址。


常量指针:

我们可以将指针定义为常量指针,这样该指针不可改变。
 
#include<iostream>
using namespace	std;
int main()
{
	int a = 3;
	int *const P= &a;		//这个指针它自身的值是不可以改变的,但是她指向的目标却是可以改变的。
	cout << "a:" << a << endl;
	a = 4;
	cout << "a:" <<a<< endl;
	return 0;
}

稍微复杂一点:
#include<iostream>
using namespace	std;
class A
{
public:
	int get()const{ return i; }
	void set(int x){ i = x; }
private:
	int i;
};
int main()
{
	A*P = new A;				//类A的指针,指向在堆中创建的类A的对象。
	cout << "P:" << P << endl;
	P = P + 1;
	cout << "P:" << P << endl;	//此时P指针内存地址多增加了4个字节。
	A*const P1 = new A;			//P1是常量不能修改它的值。
	//P1 = P1 + 1;
	P1->set(11);
	cout << P1->get()<<endl;		//P1指向的对象的值是可以改变的。
	return 0;
}
这进一步证明了常量指针的特性,常量指针自身不可改变,但是它指向的目标却是可以改变的。无论这个目标是变量还是对象。


指向常量的指针:

#include<iostream>
using namespace	std;
class A
{
public:
	int get()const{ return i; }
	void set(int x){ i = x; }
private:
	int i;
};
int main()
{
	A*P = new A;				
	cout << "P:" << P << endl;
	P = P + 1;
	cout << "P:" << P << endl;	
	const A* P1 = new A;			//这样P1就变成了指向常量的指针,该指针指向的目标是不可修改的,但是该指针可以被修改。
	P1 = P1 + 1;					//指向常量的指针它本身可以被修改,但是它指向的目标不能被修改。
	//P1->set(11);
	cout << P1->get()<<endl;		
	return 0;
}

指向常量的指针只是限制我们修改它指向的目标,它自身是可以被修改的。


指向常量的常指针:

#include<iostream>
using namespace	std;
class A
{
public:
	int get()const{ return i; }
	void set(int x){ i = x; }
private:
	int i;
};
int main()
{
	A*P = new A;				
	cout << "P:" << P << endl;
	P = P + 1;
	cout << "P:" << P << endl;	
	const A*const P1 = new A;		//这样P1就变成了指向常量的常指针,该指针指向的 目标与该指针本身不可修改。
	//P1 = P1 + 1;					
	//P1->set(11);
	cout << P1->get()<<endl;		
	return 0;
}


总结:
    指针可以通过内存地址间接访问数据,每个变量都有自己的地址,我们可以通过取址运算符(&)或者他们的地址。他们的地址可以存储在指针变量中。我们可以通过间接运算符(*)读取指针指向地址的值。
    指针的类型必须与它所指向的目标一致,指针未被初始化一个内存地址或刚删除一个指针都要将该指针赋为0。否则的话就会导致该指针失控。
    假如不想改变指针,那么将该指针定义为const,假如不想改变指针指向的值,将指针指向的值定义为const,假如既不想改变指针也不想改变指针指向的值,那么把指针和指向的值都定义为const。
    用new在堆中可以创建一个对象,然后将内存地址赋给指针,之后我们就可以通过该指针访问堆中的对象,假如要删除该对象,我们只需要对指针使用关键字delete,那么就会删除指针指向的对象并释放存储该对象的内存空间。但是我们要注意一点,指针并没有被删除,并且它仍然指向原来的内存空间,因此我们必须对它的内存地址清0或者重新赋值。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值