C++ 指针详解 (VS 2022 Community)

目录

1. 概述

2. 指针的定义

3.使用指针 (解引用)

4.指针占用的内存空间

5.空指针和野指针

6. const 修饰的指针

6.1 常量指针

拓展 :

6.2 指针常量

6.3 const 修饰的指针和常量

拓展


1. 概述

指针的作用在于, 可以通过指针 来间接地访问内存.

内存编号是从0开始记录的, 一般用十六进制数来表示.

可以利用指针变量, 来保存一段内存的地址.

2. 指针的定义

语法: 

数据类型 * 指针变量名 ;

int * p ;

" * " 就是星号, 在C++中也作乘号使用.

这时, 我们虽然创建了一个指针变量, 但这个指针并没有和任何地址对应起来(没有指向)

所以我们需要为指针变量和指定的内存地址建立联系.

可以用取址符" & " 来获取某个变量的地址. " && " 则表示逻辑的" 与 ", 注意区分.

int a = 0;
int * p = &a;

这样指针变量 p 保存的就是 变量a 的地址.

但是学到此处仍然感觉一头雾水, 于是输出一下这些有关变量的值, 看看它们到底代表什么.

#include <iostream>
using namespace std;
int main()
{
	int a = 0;
	int * p = &a;   //也可以分两行写 int * p ;   p = &a ;
                    
	cout << " a = : " << a << endl;
	cout << " &a = : " << &a << endl;
	cout << " p = : " << p << endl;
	cout << " *p = : " << *p << endl;
	cout << " &p = : " << &p << endl;
	return 0;
}

现在都清楚了.

a 就是变量a 所对应的值, 也就是变量a对应的那一段内存中存放的值. 这个是我赋值的, 为0.

&a 和 p 就是变量a 的地址的值, 也就是变量a对应的那一段内存的地址本身, 它是未知的但可以查.

*p 就是 把变量p 对应的值视为一个地址, 然后去找这个地址的内存中存放的值, 也就是a的值.为0.

【C++】*P、P 、&P的区别_*p和&p的区别_赫于富的博客-优快云博客

那为什么会有 int *p = &a 呢?   不是应该*p = a 才对嘛?

因为int *p 只是表示变量p中存放的是地址, 变量p 是一个指针变量, 并不是说 *p = &a .

也就是说 int * 是一个数据类型, 就像int , float , double.

 int * 类型的数据, 允许用 *变量名 , 但int 不行.


如果上面 *a 就是错的, 就算a = 0 , 尽管编码为0的这段内存确实存在, 也不允许通过这种方式去访问这段内存, 因为a是 int 类型的数据, 不是指针(int *).

如果分两行写, 意思就对了.在学习后面的内容之前, 要先把这个弯转过来.

int *p ;
p = &a ;

3.使用指针 (解引用)

解引用就是一个将值视为内存, 然后去对应编码的内存中寻找值的过程.

比如 *p ,意思是取出变量 p 中的值, 将它作为地址, 然后去找对应地址内存中的值.

这个值才是 *p 的值.

如上面所说, p = &a , *p = a

那么我们就可以通过操作 *p 来间接地操作a.

int a = 0;
int *p = &a;

*p = 1000;

cout << a << endl;
cout << *p << endl;

运行, 发现a 的值变为了1000. 这个过程就叫做 解引用.

注意: 虽然当 p = &a 后, 有*p = a, 

但是 p = &a; 并不等价于 *p = a;

因为赋值号不是等号.

因为赋值号不是等号.

因为赋值号不是等号.

C基础知识(4):指针--p=&a和*p=a的区别详解 - Storm_L - 博客园 (cnblogs.com)

4.指针占用的内存空间

指针数据类型( int *) 在32位操作系统下占据 4 个字节的内存空间, 64位系统占据 8 个字节.

int * 作为一个数据类型同样也可以使用 sizeof 关键字来查询它占据的内存空间大小.

我这里使用的是64位操作系统.

int * p = &a ;

cout << sizeof(int *) << endl;
cout << sizeof(p) << endl;
cout << sizeof(*p) << endl;

结果为8 8 4.

因为 *p 就是a , a 是int 类型的变量, 当然占据4个字节.

5.空指针和野指针

NULL是值吗?为什么=NULL会出错?| 一文讲懂SQL NULL - 知乎 (zhihu.com)

nullptr详解_KFPA的博客-优快云博客

【C++进阶】C++中的空指针和野指针_c++空指针和野指针_CPP攻城师的博客-优快云博客

C++中NULL和nullptr的区别_nullptr和null区别_csu_zhengzy~的博客-优快云博客

空指针的作用浅析_空指针有什么用_Moon_K_H的博客-优快云博客

空指针就是指不指向任何地址的指针.

定义一个指针的时候, 如果还没有想好让它指向哪个地址, 最好将它设为空指针:

int * p = nullptr ; 

如果不进行指向, 那么可能会导致随意分配地址, 导致程序崩溃.

这就是空指针的作用.

野指针就是指向非法内存空间的指针.

我们定义一个指针变量的时候, 程序会根据赋值运算符的右值进行赋值, 此时右值必须是一个地址类型的数据.

int a = 0;
int * p = &a ;

我们可以这么做是因为&a 是地址类型的数据, 而且a的内存是由程序来分配的.

 如果强行给指针变量赋值一个自定义的地址类型数据, 那么这样的指针是非法的,叫做 野指针.

int * p = (int *)0x1100 ;

因为0x1100 这个内存地址不是程序来分配的, 甚至连编译器可能都没有它的访问权限.

那么一旦尝试对这个指针变量进行访问, 甚至是解引用, 就会出现不可预料的后果.

野指针是非常危险的, 更危险的是, 编译器并不一定会对野指针报错, 可能编程者自己都没有发现.

所以写程序的时候务必注意, 不要写出野指针. 

6. const 修饰的指针

const修饰的指针有三种情况:

常量指针, 指针常量, const 既修饰指针又修饰常量.

6.1 常量指针

顾名思义, 常量指针是一个指针, 常量意味着这个指针指向的是一个常量.

也就是 常量的指针.

const int *p = &a ; 

常量指针的特点是:

禁止通过常量指针对变量进行赋值(但不禁止直接用变量再赋值);

允许指针的指向发生改变.

为便于记忆, 常量指针的常量在指针之前, 代码中也是 const 在 int * 之前.

但是此处的 a 并不必须定义成常量, a 仍然可以是变量, 并且对 a 进行重新赋值.

这是因为 a 是变量, 允许对 a 重新赋值, 并且这个过程中确实读写了 a 对应的内存

而*p 才是常量 , 整个过程中又没有对 *p 重新赋值, 因此可以通过编译,而且 a 的值确实改变了

所以 const int * p 禁止的是通过对指针变量 p 进行解引用, 间接地更改a的值. (*p = 1000;)

但并不会禁止单纯的访问. 如 cout << *p ; 就是合法的.

也就是说, 常量指针并不能保护常量的值, 但可以防止它通过解引用被修改.

#include <iostream>
using namespace std;
int main()
{
	int a = 10;
	int b = 20;
	const int* p = &a;    //const int * 就是常量指针, 这意味着指针变量 p 是一个常量指针
	a = 100;

	cout << a << endl;    //输出100, p 是常量指针, 但 a 是变量, 允许重新赋值.

	cout << *p << endl;   //输出100, p 是常量指针, 但允许通过 *p 对 a 进行访问, 前提是没有对 a 重新赋值.

	p = &b;
	cout << *p << endl;   //输出20, p 是常量指针, 常量指针允许指针的指向发生改变, 此时 p 指向 b 的地址.

	//   *p = 1000;  报错, 因为 p 是常量指针, 不允许通过解引用的方式对 a 重新赋值.
	return 0;
}

拓展 :

如果不对指针变量进行const修饰, 而是修饰变量a呢?

	int const  e = 1;
	int *p2 = &e;

答案是会报错.  编译器认为 e 是 const int * 类型的数据.

但可以通过前缀强行修改数据类型, 通过编译:

	int const  e = 1;
	int *p2 = (int *) & e;
	*p2 = 3;
	cout << e << " " << *p2 << endl;

这样输出, e为1 , *p2 为3

这是因为, 编译器对常量是一种粗暴的替换, 在代码中遇到任何 e ,直接替换为1.

在调用 e 的时候, 直接用 1 代替, 并不会真的去 e 对应的内存中取值.

但实际上 e 内存中对应的值已经被指针变量 p2 通过解引用的方式强行修改为3了.

这种方式确实符合常量的概念, 在这个程序中不管再怎么调用 e , e 都没有被修改的风险.

总结起来就是, const 可以很好地保护常量, 但是不会保护常量的内存.

现在, e 是否算真正的被修改了, 并不好说, 但是明白这个原理可以让我们更加灵活地运用.

C/C++可以通过指针修改常量_Echo木的博客-优快云博客

6.2 指针常量

指针常量也是一个指针, 但同时也可以认为是一个常量, 因为指针就是一个地址,

而它指向的地址不变.  也就是 指针是常量.

int c = 0;
int * const p = &c ;

指针常量的特点是:

允许修改指向的变量的值, 也允许通过解引用的方式间接修改变量的值.

禁止通过取址的方式修改指针的指向, 也就是说指针常量的值(p)永远不会变.

int c = 0;
int d = 1;

int * const p = &c ;

// p = &d;   错误,禁止通过取址的方式修改指针的指向(也就是修改指针常量的值.)

看不明白不要紧, 看代码就懂了.

//指针常量
	int c = 0;
	int d = 50;

	int* const p1 = &c;   //int * const p1 ,意味着 p1 是一个指针常量.
	
	c = 25;
	*p1 = 666;
	cout << " 解引用前的*p1 : " << *p1 << endl;   //输出666, p1 是指针常量, 允许通过解引用的方式对 c 重新赋值.

     //p1 = &d;   //报错,因为 p1 是指针常量, p1已经指向 c 的地址, 不允许重新指向 d.

	*p1 = d;
    d++;

	cout << " 解引用后的*p1 : " << *p1 << endl;   //输出50, p1 是指针常量,允许对 *p1 赋值.
//但是!仅在赋值*p1的时候访问, 取得d的值作为 *p1的值, 输出*p1的时候不是去d的内存取, 而是去c的内存.
	
	cout << "c的地址 : " << &c << endl;
	cout << "d的地址 : " << &d << endl;
	cout << "p1 的值 : " << p1 << endl;    //特别注意! 虽然 *p1 的值发生了改变 , 但 p1 的值没有发生改变!

*p1 第二次的值为50.

说明程序在调用 *p1 的时候并没有跑到 d 的内存中去取值(因为此时 d 已经变成51).

而是程序在 *p1 = d ; 的这个过程中,  才会从 d 中取值赋值给 *p1 (也就是c).

再检查一下 c 的地址 和 p1 的值, 发现它俩相等.

这就对了. 

6.3 const 修饰的指针和常量

这可能有点反直觉:

常量指针,cosnt在 * 左侧,是指  const 修饰指向的常量;

指针常量,const在 * 右侧,是指 const 修饰指针变量本身。

那么当 const 既修饰指针 , 又修饰常量呢?

答案是

//双重const
	 int num = 0;
	 const int * const p3 = &num;
	 
	 num = 10;
	 cout << *p3 << endl;     //输出10, const修饰指针,并不禁止通过变量赋值

	// p3 = & num1    报错,const修饰常量,不允许通过取址方式重新指向.

	 int num1 = 0;
	 // *p3 = num1;   报错,const修饰指针,不允许通过解引用方式给变量赋值.

拓展

这一节里面我写了很多的解引用, 但引用又是什么呢, 为了满足好奇心,再贴个链接在这里:

【C++】之引用详解 什么是引用?_c++引用_Brant_zero2022的博客-优快云博客

7.  指针和数组

前面已经说过, 数组在内存空间中是连续的.

那么就可以通过一个自增的指针访问整个数组的元素.

#include <iostream>
using namespace std;
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int * p_arr = arr;   //一定不要写成 &arr , 因为程序认为arr就是一个 int * 类型的数据 , 就是地址.
	                     //int 表示这个指针指向的是整型变量, 也就是 arr[0]是整型变量

	for (int a = 0; a < 10; a++)
	{
		cout << *p_arr << " ";      //解引用, 去数组arr的首地址读内存
		p_arr += 1;                 //指针指向 下 一 段 内存, 这个长度由指针指向的数据类型决定
		                            //因为指针指向整型变量, 所以虽然指针 +1 , 但实际上指针偏移了4个字节.
		cout << "下一段内存的地址:" << p_arr << endl;      //验证一下
	}
	cout << endl;
	
	double arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
	//int* p_arr1 = arr1;   //报错, 因为 arr1的首地址是 double * 类型的数据, 不是 int * 类型.
	double* p_arr1 = arr1;

	for (int a = 0; a < 10; a++)
	{
		cout << *p_arr1 << " ";      //解引用, 去数组arr1的首地址读内存
		p_arr1 += 1;                 //指针指向 下 一 段 内存, 这个长度由指针指向的数据类型决定
		            //因为指针指向双精度浮点型变量, 所以虽然指针 +1 , 但实际上指针偏移了8个字节.
		cout << "下一段内存的地址:" << p_arr1 << endl;      //验证一下
	}
	cout << endl;
	return 0;
}

可见结论是正确的.

所以指针类型变量 +1 的时候要注意, 指针变量的值不是 +1, 而是 +n , n为指针指向的变量类型

而 p_arr在此处是 int * 类型的指针变量, 因此 n = siezeof(int) = 4.

同样地, p_prr1在此处是 double * 类型的指针变量, 因此 n = sizeof(double) = 8.

这是在 int * p_arr = arr; 这一行代码时就已经被决定的.

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值