指针详解
1 什么是指针(*)
计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用 个字节,char 占用1个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。 我们将内存中字节的编号称为地址(&,Address)或指针(*,Pointer)。地址从 0开始依次增加,对于 32 位环境,程序能够使用的内存为 4GB。
(*)运算符在不同场景下的意义
int at = 5 * 6; //乘号
int *pt = &at; //定义一个指针
*pt = 100; //指针解引用,获取指针指向的数据
所谓指针,也就是内存的地址;所谓指针变量,也就是保存了内存地址的变量。
指针的大小在 32 位平台是 4 个字节,在 64 位平台是 8 个字节(和类型没有任何关系)
2 指针(*)变量的定义
定义指针变量与定义普通变量非常类似,不过要在变量名前面加星号格式为: datatype * name;
在指针定义中 * 号是和变量名结合
下面为定义指针的几种方式:
- 将一个已经在内存中存在变量的地址传递给定义的指针,这个指针就指向这个变量的内存地址(相同的数据类型),完成初始化。
int at = 5 ;
int *pt = &at;
-
2.利用new开辟一块地址空间
使用 new开辟的空间,记得使用delete释放,因为new出来的是返回的堆的空间,堆的空间是不会自动释放的,存放变量的栈才会自动释放。
delete释放其实只是释放了申请的这块内存空间,但是指针并没有没撤销,指针还是指向这块地址,但是不可用,是非法的。所以用delete释放掉一块堆内存时,应该自己手动将指针设置为NULL。
struct Node
{
int a;
string str;
};
struct Node *pnode = new Node();
pnode->a = 10;
- 把指针设置为NULL或者0
这样做一般只是为了没有具体初始化的时候做的,这样避免了野指针,后面可以使用if(指针==NULL)来判断,然后再进行操作。
int *p = NULL;
3 指针(*)变量的使用
使用指针变量是首先要明确指针变量自身的值 (存储的是地址),!
再明确指针变量所指的实体 (解引用)
3.1 指针传递(指针和函数)
void Swap(int *ap,int *bp)
{
int temp = *ap;
*ap = *bp;
*bp = temp;
}
int main()
{
int a = 10;
int b = 20;
Swap(&a,&b); //此时a = 20,b = 10
return 0;
}
3.2 指针运算
3.2.1 指针在不同类型下+1的比较
- int指针变量+1
int main()
{
int ar[5] = {1,2,3,4,5};
int *pt = ar;//数组名是数组首元素的地址(&ar[0])
for(int i = 0;i < 5;i++)
{
cout<< pt<< "--->"<< *pt<< endl;
pt++;
}
return 0;
}
2. char指针变量+1
int main()
{
char ch[5] = {'1','2','3','4','5'};
char *pt = &ch[0];//数组名是数组首元素的地址(&ar[0])
for(int i = 0;i < 5;i++)
{
printf("0X%08X ---> %c\n",pt,*pt);
pt = pt+1;
}
return 0;
}
总结: 指针的类型不同,加 1 的能力也不一样,具体可以表示如下:
typename * p;
p=p+ 1;被编译器解释成:p = p + sizeof(typename)*1
3.2.2 指针和指针相减
int main()
{
int ar[5] = {1,2,3,4,5};
int *p0 = &ar[0];
int *p3 = &ar[3];
cout<< p3 - p0<< " " << *p3 - *p0<<endl;
//3 3
return 0;
}
两个同类型指针,指向连续空间可以相减。减后的结果是数据元素的大小
例如:int类型指针 - int类型指针结果是整型元素的个数。
char类型指针char类型指针的结果是char数据元素的个数
注意:
当且仅当两个同类型指针变量指向同一数组中的元素时,可以用关系运算符>,==,!=等进行比较,比较规则是指向后面元素的指针高,指向同一元素的相等。
3.2.3 指针和整数相加
见4.2
3.3 指针和数组的关系
3.3.1 数组名访问
数组名被看作该数组的第一个元素在内存中的首地址(仅在 sizeof 操作中例外,该操作给出数组所占内存大小)
数组名在表达式中被自动转换为一个指向数组第一个元素的指针常量。
数组名是指针,非常方便,但是却丢失了数组另一个要素:数组的大小,即数组元素的数量。编译器按数组定义时的大小分配内存,但运行时(run time) 对数组的边界不加检测。这会带来无法预知的严重错误.
int main()
{
int ar[5] = {1,2,3,4,5};
for(int i = 0;i < 5;i++)
{
cout<< ar+i<< "--->"<<ar[i]<< "---"<< *(ar+i)<< endl;
}
return 0;
}
C 语言的下标运算符[]是以指针作为操作数的,ar[i]被编译系统解释为*art(i),即表示为 ar 所指(固定不可变元素向后第 i个元素。无论以下标方式或指针方式存取数组元素时,系统都是转换为指针方法实现。逻辑上有两种方式物理上只有一种方式。
3.3.2 指针访问
int main()
{
int ar[5] = {1,2,3,4,5};
int *p = ar;
for(int i = 0;i < 5;i++)
{
cout<< p+i<< "--->"<<p[i]<< "---"<< *(p+i)<< endl;
}
return 0;
}
总结:
- 指针变量与整型量的加减表示移动指针,以指向当前目标前面或后面的若干个位置的地址。指针与整型量 i的加减等于指针值(地址)与 ixsizeof(目标类型)积的加减,得出新的地址。
- 运算结果并不表明那儿有一个指针所规定的数据类型的变量,即使是对数组操作。这称作不对数组边界做检查指针的算术运算很容易超出数组的边界,需要小心越界问题。
3.3.2 指针数组
是一个数组,是一个存放元素是指针的数组,其定义如下:
int* a[10]; //存放十个int*的指针数组;
初始化举例:
char *array[2]={“China”,”Beijing”};
//初始化一个有两个指向字符型数据的指针的数组,这两个指针分别指向字符串”China”和”Beijing”。
3.3.3 数组指针
是一个指针,指针的类型是数组类型,即指针指向一个数组的首地址
int (*p)[10];
int a[5][10];
p = a; //p指向int类型 大小为n的数组指针,即该指针指向a的第一行数据
p++; //此时指针将指向a的第二行数组
3.4 指针和const
3.4.1 常量指针(常量数据的非常量指针)
不可以通过修改所指向的变量的值,但是指针可以指向别的变量
int a = 5;
const int *p =&a;
*p = 20; //error 不可以通过修改所指向的变量的值
int b =20;
p = &b; //right 指针可以指向别的变量
总结: 区分 const 是限制的指针变量还是指针变量指向数据的值: 沿着* 号划一条线,如果 onst 位于的左侧,则 const就是用来修饰指针所指向的变量,即指针指向为常量;如果 const 位于的右侧,const 就是修饰指针本身,即指针本身是常量。
3.4.2 指针常量(非常量数据的常量指针)
指针常量的值不可以修改,就是不能指向别的变量,但是可以通过指针修改它所指向的变量的值。
int a = 5;
int *const p = &a;
*p = 20; //right 可以修改所指向变量的值
int b = 10;
p = &b; //error 不可以指向别的变量
3.4.3 常量数据的常量指针
const int* const a = &a;
指针前有指针后也有,两者均不可以改变
3.5无类型指针 void *(泛型指针)
void 的作用:
- 对函数返回的限定,这种情况我们比较常见。
- 对函数参数的限定,这种情况也是比较常见的
一般我们常见的就是这两种情况:
- 当函数不需要返回值值时,必须使用 void 限定,这就是我们所说的第一种情况。例如: void fun(int a)
- 当函数不允许接受参数时,必须使用 void 限定,这就是我们所说的第二种情况。例如: int fun(void)。
void 不能定义变量,但可以定义指针变量。特别之处在于 void 指针可以指向任意类型变量的地址。
我们也把 void 指针变量称为泛型指针,是指针都可以给 void 指针变量赋值。甚至 p = &vp; 也可以。如果要将 void 指针 vp 赋给其他类型的指针,则需要强制类型转换。
在ANSIC 标准中,不允许对 void 指针进行一些算术运算如 p++ 或 p+=1 等,因为既然 void 是无类型,那么每次算术运算我们就不知道该操作几个字节,例如 char 型操作 sizeof(char) 字节,而 int 则要操作 sizeof(int) 字节而在 GNU 中则允许,因为在默认情况下,GNU 认为 void* 和 char* 一样,既然是确定的,当然可以进行一些算术操作,在这里 sizeof(* p)==sizeof(char)
好处是: void 指针可以任意类型变量的地址。函数中的形参为 vid*指针变量时,函数就可以接受任意类型变量的地址
4.二级指针
指针可以指向普通类型的数据,例如 int、double、char 等,也可以指向指针类型的数据,例如 int 、double.*等。如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。
#include <iostream>
#include <string.h>
using namespace std;
int main()
{
int a = 10;
int b = 20;
int *ap = NULL; //一级指针
int **ap2 = NULL; //二级指针
ap = &a; //ap存放a的地址,指向a
ap2 = ≈ //ap2存放ap的地址,指向ap
cout<< "a:"<< a<< " (*ap):"<< *ap<< " (**ap2):"<< **ap2<< endl;
//a:20 (*ap):20 (**ap2):20
*ap2 = &b;
cout<< "a:"<< a<< " (*ap):"<< *ap<< " (**ap2):"<< **ap2<< endl;
//a:20 (*ap):10 (**ap2):10
return 0;
}
下图展示了上述代码运行之后的结果: