指针在C语言是核心,在C++中更是核心。所以本章节将详细讲解指针的使用方法以及指针的一些特殊用法,和引用的区别,以及指针涉及到一些算法基础。通过案例引导,使得能更清楚命明白。在C++中的指针是一种数据类型,其使用方法和C语言并无差别,用于存储变量的内存地址。指针提供了一种间接访问内存的方式,允许我们在程序中操作和处理内存地址,以及访问存储在该地址上的数据。
三.、指针变量
1.内存相关知识介绍
在32位平台,每一个进程 拥有4G的空间。(进程的生命周期通常包括创建、运行和终止三个主要阶段。(去学习一下有关进程的章节))
系统为内存的每一个字节分配一个32位的地址编号 (虚拟地址)。这个编号称之为内存地址。
无论什么数据类型的地址,都是存储单元的编号,在32位平台下都是4个字节,即任何类型的指针变量都是4个字节大小。
在64位平台上,每个进程通常拥有更大的内存空间。具体的大小取决于操作系统和硬件的限制,但通常远远大于32位平台的4GB。
在64位平台上,操作系统为每个进程分配的虚拟地址空间通常达到了 2^64(即16EB,其中EB表示exabyte,1EB等于1亿亿字节)。这样巨大的地址空间允许进程能够使用更多的内存,处理更大的数据集。
注意:对于一个变量来取它的地址,实际上是取的该变量存储位置的首地址。
2. 地址和指针变量的关系
地址 就是内存的地址编号。
指针变量:本质是变量 只是该变量 保存的是内存的地址编号。(不是普通的数值)
3.1 指针变量的定义
1、定义步骤 (定义的时候)
*修饰指针变量p。(引用千锋教育C++基础班笔记)
保存谁的地址 就先定义谁。
从上往下整体替换。
定义一个指针变量
*p
,保存int num
的地址:int *p;
定义一个指针变量
p
,保存数组int arr[5]
的首地址:int (*p)[5];
定义一个指针变量
p
,保存函数int fun(int,int)
的入口地址:int (*p)(int,int);
定义一个指针变量
p
,保存结构体变量struct stu Tucy
的地址:struct stu *p;
*p;
struct stu Tucy;
从上往下替换:struct stu
*p;
定义一个指针变量
p
,保存指针变量int *p
的地址:int **p ;
2.在32位平台任何类型的指针变量都是4字节(我的是64位,8字节)
注:定义指针的大小和类型没有关系,指针指向的的内存地址,是地址的首地址位数的大小。
3.指针变量和普通变量建立关系
4.指针变量操作之前必须初始化
指针变量在操作之前必须指合法的地址空间
1、指针变量如果不初始化 立即操作会出现段错误
2、指针变量如果没有指向合法的空间建议初始化为NULL (代码中使用nullptr)
int *p = nullptr;//nullptr是赋值给p int *p; p = nullptr;
案例:使用qt5.13.2时,可以看到此时引发了错误,并且编译器也在提示警告信息,黄色的灯泡(warning: variable 'p' is uninitialized when used here)
不用操作指向null的指针变量 (如下案例,使用qt5.13.2,此时操作了指向空的指针变量,产生了段错误)
5. 将指针变量初始化为 合法的地址 (变量的地址、动态申请的地址、函数入口地地址)
可以使用一行语句进行定义 ,需要注意英文输入法下的“,”前的数据类型是什么。
此时,data为int类型,p为int *类型。
3.2 指针变量的类型
1、指针变量自身的类型
将指针变量名使用鼠标拖拉遮住蓝色,剩下的类型就是指针变量自身的类型
例:int *p; p的类型就是int *
指针变量自身的类型一般用于赋值语句的判断
int num = 10;
int *p = #
//在使用中
//num 为int &num为int* ---->对变量名 取地址 整体类型加一个*//p 为int * *p 为 int ---->对指针变量 取* 整体类型减一个*
//在使用中&和*相遇 从右往左 依次抵消
*&p == P
案例1:int num=11,*p=&num,**q=&p;以下结果正确的是 ABC
int num = 11;
int *p = #
int **q =&p;
A;*p =11 B:*q = &num C:p=&num D:q=&num
2. 指针变量指向的类型
将指针变量名和离它最近的一个* 一起拖蓝,剩下的类型就是指针变量指向的类型
int *p; p指向的类型为int
3.指针变量的指向类型 决定了取值宽度
int num = 0x01020304;
int *p = #
为啥*p == num == 0x01020304?
(引用千锋教育C++基础班笔记)
4、指针变量的指向类型 决定了+1跨度是多少
案例1: 取出0x0102的值
short *p = (short *)#*(p+1);
案例2: 取出0x02的值
char *p = (char )#*(p+2);
案例3:取出0x0203的值
char *p = (char *)#*(short *)(p+1)
3.3 指针变量的注意事项
1.void 不能定义普通变量
void num;//错误,不能给num变量申请内存空间
2.void*可以定义指针变量
void *p; //允许,p本身的类型为void* 。32位为4B,64位位8B
那么系统可以直接开辟空间,定义是成功的(不说没有初始化。另说)
p就是万能指针的一级指针变量,能保存任意一级指针的地址编号。
int num = 10;
vold *p=# ----->int *
short data = 20;p=&data; ----->short *
万能指针一般用于函数的形参达到算法操作多种数据类型的目的;
特殊:不要对void *p的指针变量取*
int num = 10;
void *p = #
*p;//err p指向的类型为void 无法确定p的取值宽度 所以不能*p
对p取*之前对p先进行指针类型强转
int num=10;
void *p = #
cout<<*(int*)p<<end1;
3.指针变量未初始化之前不要取*
int *p;
*p;//err 段错误
4、指针变量初始化nullptr 不要取*
int *p = nullptr;
*p;//err 段错误
5、指针变量不要越界访问
char ch = 'a';
int *p = &ch;
*p;//error越界访问非法内存
int num = 10;
int *p = #
p++;
*p;//越界访问
面试背题
(拓展,引用了阿秀的学习笔记中的一些知识)
1.指针和引用的区别
-
定义和初始化:
- 指针是一个变量,它存储了一个地址,并允许通过该地址访问相应的内存空间。指针需要使用
*
操作符进行声明和初始化,例如int *ptr = #
,其中ptr
是一个指向num
变量的指针。 - 引用是一个别名,它相当于对象的一个别名,该别名在创建时必须被初始化,并且不能被重新绑定到另一个对象上。引用使用
&
操作符进行声明和初始化,例如int &ref = num;
,其中ref
是num
的引用。
- 指针是一个变量,它存储了一个地址,并允许通过该地址访问相应的内存空间。指针需要使用
-
内存管理和空值:
- 指针可以指向任何有效的内存地址,在使用指针前需要确保它指向一个有效的对象,否则可能会导致未定义的行为。指针也可以被设置为空指针(
nullptr
)来表示不指向任何对象。 - 引用必须在初始化时指向一个有效的对象,并且不能被重新绑定到其他对象上。引用不存在空值的概念,它始终引用一个有效的对象。
- 指针可以指向任何有效的内存地址,在使用指针前需要确保它指向一个有效的对象,否则可能会导致未定义的行为。指针也可以被设置为空指针(
-
操作和语法:
- 指针可以通过解引用操作符
*
来访问所指向的对象,并可以通过->
操作符访问指向对象的成员(如果指向的是一个类类型)。指针还可以进行指针运算(如加法、减法等)。 - 引用在使用时不需要解引用操作符,可以直接像访问对象一样使用,编译器会自动解引用引用,并访问相应的对象。引用不能进行指针运算。
- 指针可以通过解引用操作符
-
重新指向和重新绑定:
- 指针可以在任何时候重新指向不同的对象,即可以通过改变指针指向的地址来改变其所指向的对象。
- 引用在初始化后不能再重新绑定到其他对象上,它始终引用同一个对象。
其实根本是在于,指针指向的是内存地址,引用是引用的指针变量。引用必须初始化一个对象,指针可以为空nullptr,也可以指向内存地址编号。
2.野指针和悬空指针
首先二者都是是指向无效内存区域(这里的无效指的是”不安全不可控)的指针,访问行为将会导致未定义行为。
野指针,指的是没有被初始化过的指针
int main(void) {
int* p; // 未初始化
cout<< *p << endl; // 未初始化就被使用
return 0;
}
因此,为了防止出错,对于指针初始化时都是赋值为 nullptr,这样在使用时编译器就会直接报错,
产生非法内存访问。
悬空指针,指针最初指向的内存已经被释放了的一种指针
int main(void) {
int * p = nullptr;
int* p2 = new int;
p = p2;
delete p2;
}
此时 p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料。需要设置为p=p2=nullptr。此时再使用,编译器会直接保错。
避免野指针比较简单,但悬空指针比较麻烦。c++引入了智能指针,C++智能指针的本质就是避免悬空指针的产生。
3.指针加减计算要注意什么?
指针加减本质是对其所指地址的移动,移动的步长跟指针的类型是有关系的,因此在涉及到指针加减运算需要十分小心,加多或者减多都会导致指针指向一块未知的内存地址,如果再进行操作就会很危险。
#include <iostream>
using namespace std;
int main()
{
int *a, *b, c;
a = (int*)0x500;
b = (int*)0x520;
c = b - a;
printf("%d\n", c); // 8
a += 0x020;
c = b - a;
printf("%d\n", c); // -24
return 0;
}
首先变量a和b都是以16进制的形式初始化,将它们转成10进制分别是1280(5*16\^2=1280)和1312(5*16\^2+2*16=1312), 那么它们的差值为32,也就是说a和b所指向的地址之间间隔32个位,但是考虑到是int类型占4位,所以c的值为32/4=8
a自增16进制0x20之后,其实际地址变为1280 + 2*16*4 = 1408,(因为一个int占4位,所以要乘4),这样它们的差值就变成了1312 - 1280 = -96,所以c的值就变成了-96/4 = -24
遇到指针的计算,需要明确的是指针每移动一位,它实际跨越的内存间隔是指针类型的长度,建议都转成10进制计算,计算结果除以类型长度取得结果;
4.数组名和指针(这里为指向数组首元素的指针)区别
二者均可通过增减偏移量来访问数组中的元素。
数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。
当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了。
5.函数指针
1) 什么是函数指针?
函数指针指向的是特殊的数据类型,函数的类型是由其返回的数据类型和其参数列表共同决定的,而函数的名称则不是其类型的一部分。
一个具体函数的名字,如果后面不跟调用符号(即括号),则该名字就是该函数的指针(注意:大部分情况下,可以这么认为,但这种说法并不很严格)。
2) 函数指针的声明方法
int (*p1)(const int&, const int&);
上面的pf就是一个函数指针,指向所有返回类型为int,并带有两个const int&参数的函数。注意*p1两边的括号是必须的,否则上面的定义就变成了:
int *p2(const int&, const int&);
而这声明了一个函数p2,其返回类型为int *, 带有两个const int&参数。
3) 为什么有函数指针
函数与数据项相似,函数也有地址。我们希望在同一个函数中通过使用相同的形参在不同的时间使用产生不同的效果。
4) 一个函数名就是一个指针,它指向函数的代码。
一个函数地址是该函数的进入点,也就是调用函数的地址。函数的调用可以通过函数名,也可以通过指向函数的指针来调用。函数指针还允许将函数作为变元传递给其他函数;
5) 两种方法赋值:
指针名 = 函数名; 指针名 = &函数名