C++进阶-彻底理解指针

指针的运算

指针所表示的是内存地址数据,指针也可以参与部分运算,包括算术运算、关系运算和赋值运算,而我们最常用的就是指针的算术加减运算。指针的加减运算,实际上是让指针的指向发生偏转,指向另外的内存位置。

算术运算

数组名实际上就是数组数据所在内存区域的首地址,表示数组在内存中的起始位置。可以通过把首地址赋值给指针,然后对该指针进行加减运算,使指针发生偏转指向数组中的其他元素,从而遍历整个数组。

int nArray[3] = { 1, 2, 3 }; // 定义一个数组
int* pIndex = nArray; // 将数组的起始地址赋值给指针 pIndex
cout<<"指针指向的地址是: "<<pIndex<<endl; // 输出指针指向的地址
cout<<"指针所指向的数据的值是: "<<*pIndex<<endl; // 输出这个位置上的数据
pIndex++; // 对指针进行加运算,使其指向数组中的下一个值
cout<<"指针指向的地址是: "<<pIndex<<endl; // 输出指针指向的地址
cout<<"指针所指向的数据的值是: "<<*pIndex<<endl; // 输出数据

output:
指针指向的地址是: 0016FA38
指针所指向的数据的值是: 1
指针指向的地址是: 0016FA3C
指针所指向的数据的值是: 2

指针的加减运算跟它所指向的数据的真正数据类型相关,指针加 1 或者减 1,会使指针指向的地址增加或者减少一个对应的数据类型的字节数。

关系运算

指针的关系运算通常用“==”或“!=”来判断两个相同类型的指针是否相等,也就是判断它们是否指向同一地址上的同一数据,以此作为条件或循环结构中的条件判断语句。

int nArray[3] = { 1, 2, 3 }; // 定义一个数组
int* pIndex = nArray; // 将数组的起始地址赋值给指针 pIndex
int* pEnd = nArray + 3; // 计算数组的结束地址并赋值给 pEnd
while( pIndex != pEnd ) // 在 while 的条件语句中判断两个指针是否相等,
// 也就是判断当前指针是否已经偏转到结束地址
{
	cout<<*pIndex<<endl; // 输出当前指针指向的数据
	// 对指针进行加 1 运算,
	// 使其偏移到下一个内存位置,指向数组中的下一个数据
	++pIndex;
}

指针变量也常和 nullptr 关键字进行相等比较,来判断指针是否已经被初识化而指向正确的内存位置,也就是判断这个指针是否有效。提倡在定义指针的同时就完成对它的初始化,可有时在定义指针的时候,并没有合适的初始值可以赋给它,但如果让它保持最开始的随机值,又会产生不可预见的结果。在这种情况下,我们会在定义这个指针的同时将这个指针赋值为 nullptr,表示这个指针还没有被初始化,处于不可用的状态。

int* pInt; // 定义一个指针,这时的指针是一个随机值,指向随机的一个内存地址
// 将指针赋值为 nullptr,表示指针还没有合适的值,处于不可用的状态
pInt = nullptr;
//…
int nArray[10] = {0};
pInt = nArray; // 将数组首地址赋值给指针
if( nullptr != pInt ) // 判断指针是否已经完成初始化处于可用状态
{
	// 指针可用,开始使用指针访问它指向的数据
}

void 类型和 void 类型指针

void类型

void 类型并不是一个真正的数据类型,我们并不能定义一个 void 类型的变量。 void 更多的是体现一种抽象,在程序中, void 类型更多的是用于“修饰”和“限制”一个函数。

如果一个函数没有返回值,则可用 void 作为这个函数的返回值类型,代替具体的返回值数据类型;如果一个函数没有形式参数列表,也可用 void 作为其形式参数,表示这个函数不需要任何参数。

void 类型指针作为指向抽象数据的指针,它可以成为两个具有特定类型的指针之间相互转换的桥梁。
在用一个指针对另一个指针进行赋值时,如果两个指针的类型相同,那么可以直接在这两个指针之间进行赋值; 如果两个指针的类型不同,则必须使用强制类型转换,把赋值操作符右边的指针类型转换为左边的指针类型,然后才能进行赋值。

int* pInt; // 指向整型数的指针
float* pFloat; // 指向浮点数的指针
pInt = pFloat; // 直接赋值会产生编译错误
pInt = (int*)pFloat; // 强制类型转换后进行赋值

使用 void 类型指针时,就没有类型转换的麻烦。 void 类型指针显得八面玲珑,任何其他类型的指针都可以直接赋值给 void 类型指针。

void* pVoid; // void 类型指针
pVoid = pInt; // 任何其他类型的指针都可以直接赋值给 void 类型指针
pVoid = pFloat;

void 类型指针不可以直接赋值给其他类型的指针。要完成这个赋值,必须经过强制类型转换,让“无类型”变成“有类型”。

pInt = (int*)pVoid; // 通过强制类型转换,将 void 类型指针转换成 int 类型指针
pFloat = (float*)pVoid; // 通过强制类型转换,将 void 类型指针转换成 float 类型指针

void 类型指针所转换成的其他类型,必须与它所指向的数据的真实类型相符。比如把 int 类型指针赋值给 void 类型指针,那么这个 void 类型指针指向的就是 int 类型数据,这时如果再把这个 void 类型指针强制转换成 double 类型指针并通过它访问它所指向的数据,那么很可能得到错误的结果。

指向指针的指针

指针也是一个变量,其中记录着某个数据的地址,而它本身存放在内存中的某个地址,当有另外指针指向这个地址时,这个指针就是我们所说的指向指针的指针了。

int N = 2;
int* pN = &N; // 定义一个指针变量 pN,记录变量 N 所在的地址
int** ppN = &pN; // 定义一个指针变量 ppN,记录指针变量 pN 所在的地址

普通指针指向的是一个具体的数据,而指针的指针所指向的是一个指针数据,我们可以把指针数据也当做某个具体的数据,它也有自己的数据类型(比如,int*),也占用一定的内存(比如, 0016FA5C)记录一定的数值(比如, 0016FA37),跟普通数据来做一个类比,那么指向指针的指针其数据类型,就是在它所指向的数据的类型之后加一个*。比如,我们要定义一个指针指向另一个 int*类型的指针,那么它自身的类型就是 int*加一个*就成了 int**了。而这个指针的值,同样是用“&”取得被指向的指针变量的地址赋值给它,那它就成了指向这个指针的指针。

在 C++中,可以用下面的语法格式来定义一个指向指针的指针:

数据类型标识符** 指针变量名

其中,数据类型表示它所指向的指针的数据类型。例如:

int** ppN = &pN;

这样就定义了一个指向整型指针的指针 ppN,它指向另一个指针变量 pN,而 pN 指针变量又指向一个整型变量。

指向指针的指针通常用来访问指针数组。数组可以保存基本数据类型的数据,同样,它也可以保存指针,保存指针的数组被称为指针数组。如果要访问一个指针数组,使用指向指针的指针最为方便。

// 这是一个指针数组,其中保存的是各个字符串常量的首地址指针,其类型是 const char*
const char* arrMonth[]={"Jan","Feb","Mar","Apr","May","Jun",
"Jul","Aug","Sep","Oct","Nov","Dec"};
// 定义一个指向指针的指针,
// 因为要指向的指针是 const char*类型,所以加上*后它自身的类型就是 const char**
// 这里数组的首地址 arrMonth 就是数组的第一个元素,
// 也就是指向第一个字符串“Jan”的指针
const char** pMonth = arrMonth;
// 获取用户输入
int nIndex = 0;
cout<<"请输入月份对应的数字: "<<endl;
cin>>nIndex;
// 对指针进行运算,使其指向相应的数组元素,
// 也就是相应的字符串指针
const char* pCurMonth = *(pMonth + ( nIndex - 1 ));
cout<<"对应的月份是: "<<pCurMonth<<endl;

指针在函数中的应用

指针作为函数参数

在函数内部,通过作为参数传递进来的指针,我们可以间接地访问到函数外的原先需要传递的大体积参数数据。这样,我们就以传递一个 4 个字节的指针代替了传递成千上万字节的大体积数据,但却起到了同样的向函数内传递数据的效果,避免了大体积数据的拷贝过程,自然也就提高了函数性能。

使用指针作为函数参数,不仅可以向函数内传入数据,还可以向函数的调用者传出数据。在函数内部,利用指向外部参数数据的指针,我们可以读取其中的数据,实现数据向函数内的传递。同时还可以通过在函数内部修改这些数据,把函数的结果传出给调用者。

// 计算数组中所有数据元数之和
// 其中,参数 pArray 和 nCount 分别表示数组的首地址和数组元素的个数,
// 用于向函数传入一个数组,
// pSum 指向用于保存结果数据的变量,用于从函数中传出计算结果
void SumArray(int* pArray, int nCount, int* pSum )
{
	// 参数有效性检查…
	int nRes = 0; // 结果数据
	// 循环遍历整个数组,计算所有数据元数的和
	for( int i = 0; i < nCount; ++i )
	{
		// 通过 pArray 指针访问它所指向的函数外的 nArray 数组,
		// 读取其中的数据并累加到 nRes 中,实现向函数内传入数据
		nRes += *pArray;
		pArray++; // 指针加运算,访问数组中的下一个元素
	}
	// 通过 pSum 指针访问它所指向的函数外的 nArraySum 变量
	// 将结果数据写入这个变量,实现向函数外传出数据
	*pSum = nRes;
}
int main()
{
	// 保存结果数据的变量
	int nArraySum = 0;
	// 需要统计的数组
	int nArray[5] = { 1, 2, 3, 4, 5 };
	// 使用数组的首地址 nArray 传入数组,
	// 使用指向变量 nArraySum 的指针来接收计算结果
	SumArray(nArray, 5, &nArraySum);
	// 运算结果已经保存在 nArraySum 中,直接输出运算结果
	cout<<"数组中所有数据之和是 :"<<nArraySum<<endl;
	return 0;
}

在主函数中,我们将数组 nArray 的首地址和指向保存结果数据的变量 nArraySum 的地址作为实
际参数传递给数组求和函数 SumArray()。 这样在 SumArray()函数中,我们就可以通过传入的数组地址访问整个数组,完成传入数据的功能。在完成统计后,又可以利用 pSum 指针,将结果数据直接保存到它所指向的函数外用于保存结果数据的变量 nArraySum 中,完成传出数据的功能。

利用指针作为函数参数传递数据的本质,就是在主调函数和被调函数中,通过指向同一内存地址的不同指针访问相同的内存区域,从而实现数据的传递和交换。

指针作为函数返回值

当我们需要从函数内返回某个比较大体积的数据,或者是返回数据不能被拷贝时,就可以采用返回指向这个数据的指针来代替返回数据本身。

例如,在单件模式的 getInstace()函数中,因为需要返回的对象不能在返回过程中被复制,所以就用指针作为它的返回值,从函数内返回指向这个对象的指针来代替返回这个对象本身。

// 以 SalarySys*指针作为返回值
static SalarySys* getInstance()
{
	if ( nullptr == m_pInstance )
	m_pInstance = new SalarySys();
	return m_pInstance;// 返回指向 SalarySys 对象的指针
}

不能把一个指向函数内局部变量的指针作为返回值。这是因为函数内部定义的局部变量在函数结束后,其生命周期已经结束,内存会被自动释放,这时它的内存地址是无意义的。

// 错误的 getInstance()函数
static SalarySys* getInstance()
{
	if ( nullptr == m_pInstance )
	{
		// 定义一个局部变量 sys
		SalarySys sys;
		m_pInstance = &sys; // 获得局部变量的指针
	}
	return m_pInstance;// 返回指向局部的 sys 对象的指针
}
int main()
{
	// …
	// 获得的指针指向 getInstance()函数内的局部变量 sys
	SalarySys* p = SalarySys::getInstance();
	// 局部变量 sys 已经被销毁,对它的访问是无意义的
	p->Input();
	//…
}

以指针为返回值的函数可以返回用 new 全新申请的内存地址; 可以返回全局变量的地址;可以返回静态变量的地址,但就是不可以返回局部变量的地址。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值