把指针彻底说清楚


“就像英语一样,经常这么用的就约定俗成了需要背诵的短语,指针本来也是可以随便去用(因为指针本来就是内存上的页码,页码里存的是段落),只不过既成的数据类型、常用的习惯以及对程序崩溃的恐惧让我对指针始终了解不透彻。”


1.指针习惯用法

定义一个指针,一定要指向已有变量、或者malloc/new才可以使用,不然就是野指针,无法使用!

 

C的malloc(free)。malloc就是memory allocate动态分配内存

# include <stdio.h>
# include <malloc.h>
 
void f(int * q1)
{
	*q1 = 200;//在函数中赋值
	//free(q1);  //把q所指向的内存释放掉,不然后面在使用*p的时候会报错,因为p所指向的内容已经被释放了
}
int main(void)
{
	int * p = (int *)malloc(sizeof(int)); //sizeof(int)返回值是int所占的字节数
	*p = 10;
	f(p);  //p是int *类型
	printf("%d\n", *p);
	return 0;
}

数组:

#include <stdlib.h>
int main(void)
{
int *a;//int型指针变量
int n,i;
scanf("%d",&n);
a=malloc(sizeof(int)*n);//sizeof(int)是int型变量所占字节数【由此可见数组就是一类变量的n次重复】
for (i=0;i<n;i++)
a[i]=i;
free(a);/*释放malloc()分配的空间*/
return 0;
}

C++的new(delete)。

# include <iostream>
using namespace std;

void f(int * q1)
{
	*q1 = 200;
	//delete(q1);  //把q所指向的内存释放掉,不然后面在使用*p的时候会【变成乱码~】,因为p所指向的内容已经被释放了
}
int main(void)
{
	int * p = new(int); //从自由存储分配内存,创建一个int数据类型大小的?对象?,并将已适当分类的非零指针返回到?对象?。
	*p = 10;
	f(p);  //p是int *类型
	printf("%d\n", *p);
	return 0;
}

 

动态创建二维数组de函数模板

//(4)二维数组。C + +提供了多种声明二维数组的机制。当形式参数是 - 一个二维数组t,必须指定其第二维的大小。
//例如, a[][10]是 - 一个合法的形式参数,而a[][]则不是。为了打破这种限制,可以使用动态分配的维数组。
//例如,下列代码创建一一个类型为Type的动态数组,该数组有rows行cols列。

template < class Type>
void Make2DArray(Type** &x, int rows, int col)
{
	X=new Type*[rows];  //先行
	for (int i = 0; i < rows; i + +)
		x[i] = new Type[cols];	//再列
}

//当不再需要动态分配的二维数组时,可按以下步骤释放它所占用的空间。
//首先释放在for新环中方科行所分配的空间,然后释放为行指针分配的空间,具体实现可描述如下:

template <class Type>
void Delete2DArray(Type ** &x, int rows)
{
	for (int i = 0; i < rows; i++)
		delete[]x[i];
	delete x;
	x = 0;
}
//注意,在释放空间后将x置为0,
//以防止用户继续访问已被释放的空间。

 


2.指针实质、用法

整个数组地址和数组首元素地址不是同一个东西。
“内存、地址、数据”三者的关系就好像“书本、页号、章节”之间的关系一样
一个章节可能占据多页纸,我们要找到这一章,只需要知道它起始于哪一页;
一个数据可能占据多个字节的内存空间,我们要找到这个数据,只需要知道起始地址。
编译报错只是因为赋值符号两遍数据类型不匹配

 

指针基本概念参照前帖:

https://blog.csdn.net/sinat_27382047/article/details/70950898

指针基本用法:

取址操作符 【&】 ——指针用‘ & ’运算符创建

间接引用操作符 【*】—— ‘ * ’操作符用于间接引用指针

定义性声明

int *p1;
p1 = &a;

引用性声明

int *p2 = &a;

 

指针与数组:*a与a[]

比如main函数里的参数:https://blog.csdn.net/eastmount/article/details/20413773

 

操作系统相关:堆内存、栈内存(创建数组、malloc字符)

实例化类(创建对象)

A a; 
A * a = new a(); 

1.前者在堆栈中分配内存,后者为动态内存分配,在一般应用中是没有什么区别的,但动态内存分配会使对象的可控性增强。

2.不加new在堆栈中分配内存 

3.大程序用new,小程序直接申请 

4.只是把对象分配在堆栈内存中 
5.new必须delete删除,不用new系统会自动回收内存

 


3.指针原理(汇编层面)

节选参照自:《深入理解计算机系统》

每个指针都对应一个类型(void*另说)

——指针类型不是机器代码中的一部分,它们是C语言提供的一种抽象,帮助程序员避免寻址错误。

int型指针:(如果对象类型为int,那么指针类型就为int*)

int *p;

void*型代表通用指针(malloc函数就是返回一个通用指针,然后通过强制类型转换或赋值操作的隐式强制类型转换,将它转换成一个有类型的指针)

 

将指针从一种类型强制转换成另一种类型,只改变它的类型

——强制类型转换的效果是改变指针运算的伸缩。

强制类型转换(c语言形式):

char* p;

则(强制类型转换优先级高于加法):

(int *)p+7 计算为p+28

(int *)(p+7) 计算为p+7

 

数组与指针紧密联系

——数组引用、指针运算都需要用对象大小对偏移量进行伸缩。

int型数组指针:

数组引用:a[3]

指针运算+间接引用:*(a+3)

 

指针可以指向函数

——函数指针的值是该机器代码表示中的第一条指令的地址。

函数指针:

int fun(int x,int *p);//函数原型

int (*fp)(int,int *);//声明指针fp

fp=fun;//将指针fp指向函数

int y=1;

int result=fp(3,&y);//用指针调/用函数

注:如果写成“ int *fp(int,int *) ”则会被解释成“返回值为int*型的函数fp(int,int *) ”

 

 

4.错误用法

(0)内存泄漏

是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

int * p = new int;            //声明一个int型指针,并分配了内存
//delete p;        //没释放内存
p=NULL;            //没释放所指的内存,丢失了原来的内存位置,也就无法再delete了

(1)野指针

当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,此情况下该指针便称悬垂指针(也叫迷途指针)。

int * p = new int;            //声明一个int型指针,并分配了内存
//int * q = malloc(sizeof(int));    //malloc的形式
delete p;//释放了内存
p=NULL;//如果没有这句,p就成了悬垂指针

某些编程语言允许未初始化的指针的存在,而这类指针即为野指针。

int * p;            //声明一个int型指针
p=(int *)123456;    //int型指针现在没有分配内存,无法被赋值

但是这块内存不允许访问

 

(2)符号的错误用法

数组指针int *a[];

指针数组(int *)a[];

函数指针:https://blog.csdn.net/sinat_27382047/article/details/71941458


malloc返回的是void*指针,void*指针和int*指针是不兼容的类型,无法完成隐式的类型转换。
int (*p)  (int *)p在一维中无影响,但是到二维数组、多层结构体之类的就有区别了。
int &p在函数传形参时是引用,方便在于相比传入int *p是指针【声明,调用】f(int*)、f(*a),这个传入的就是变量f(int)、f(a),但是在用的时候会当做指针处理。
 


 

以下摘自:https://blog.csdn.net/lpp1989/article/details/7766574

1)结构体变量作为函数参数[实参与形参]时,形参结构体变量成员值的改变不影响对应的实参构体变量成员值的改变。

2)结构体数组或结构体指针变量作为函数参数[实参与形参]时,形参结构体数组元素[或形参结构体指针变量指向的变量]成员值的改变将影响对应的实参构体数组[或实参结构体指针变量指向的变量]成员值的改变。

3)结构体变量可作为函数的参数,函数可返回一结构体类数据

      4)p=&b; 使结构体指针变量p指向结构体变量b的空间。

            p->num:表示通过指针变量引用结构体变量b的成员num

5)p=a;或p=&a[0];将结构体指针变量指向结构体数组a。则:

             ①p->num:表示通过指针变量引用结构体数组元素的成员num的值。

             ②p->num++:表示通过指针变量先引用结构体数组元素的成员num的值,再使该元素的成员num的值加1,先引用其值然后其加1。

③++p->num:表示使指向的元素的成员num的值加1,再引用其值。

6)p=a;或p=&a[0];表示将结构体指针变量p指向结构体数组a。

           ①(p++)->num:表示通过指针变量先引用结构体数组元素  的成员num的值,再使指针变量本身加1,指针变量加1表示使指针变量指向结构体数组的下一个元素。

           ②(++p)->num:先使指针变量本身加1,先使使指针变量指向结构体数组的下一个元素,然后引用指针变量所指向的结构体数组元素的成员num的值。

 

闲聊:
a:=5比a=5更能体现“赋值动作”

 

5.杂例

PS:受到贴吧帖子的启发 http://tieba.baidu.com/p/5868945331?pid=121842627065

此贴是关于多维数组与指针关系的。

#include <stdio.h>
int main()
{
	int a = 3, b = 2, c = 4;

	int *pp = &a;
	printf("%p \n", *pp);

	int *str[3] = { &a,&b,&c };//这是一一个指针数组。

	int **p; //建立一 个双指针,为什么是双指针呢,因为单指针只能指向“值” , 而数组str里的元素是指针。

	p = str;// str表示首元素的地址,就是说&str[0]  
	printf("%d\n", **p);

	int *(*p2)[3]; // 接下来我建一个“数组指针”的指针!
	p2 = &str;	//为什么不能把str赋给p2呢? str即是&str[0],因为我这个是 "数组指针”的指针,
				//需要指向一整个数组,而不是数组里的单一元素&a的地址 ,所以不能是str。
	printf("%d\n", ***p2);

	int str3[3] = { 3,2,4 };//这是一个数组。
	int *p3; //因为数组里面的是“值” ,所以我可以直接用单指针来表示。
	p3 = str3;//因为我这个指针不是“数组指针”, 所以我可以直接用单一元素的地址: &str3[0]即str3去表示。
	printf("%d\n", *p3);

	int(*p4)[3]; //我建立了一 个“数组指针”
	p4 = &str3; //这个时候就不能用单一元素的地址去表示了,必须要用“整个数组”的地址去表示。
	printf("%d\n",**p4);
	//总结:数组指针就需要用整个数组的地址来表示(:3」 <)_
	return 0;

}

***P,虽然可以*三次,但不要误以为p就是三级指针
几级指针这个概念本身并不严谨,只是为交流方便才产生的这么一个概念。

数组指针,结构体指针
只要数组或结构体里还存在数组或指针,就很难说数组指针和结构体指针是几级指针,这时就不能以几级指针的概念来讨论问题。
比如:
int ** arr[10];一个数组含有10个元素,元素类型为int**
这时我们要关心的是指针本身各级解引用的内容能否合法合理的再解引用
int ** p;p =arr; 
很容易理解p是一个指向int的二级指针。
*p得到int的地址,**p得到int,p是int内容的地址的地址
int **(*p)[10]; p=&arr;
*p得到一个数组,数组类型为int **[10]
数组可以*,**p得到数组首元素int**
int **又可以*两次,****p是合法合理(合逻辑的)。
但一般我们不会以四级指针,来说明p
即使要用,一般也会认为(int **** q;)以这种方式定义的指针是四级指针。

#include<iostream>
//#include<stdio.h>
using namespace std;

int main()
{
	int a = 1, b = 2, c = 3;
	int* str[3] = { &a,&b,&c };//存int*型(int型地址)的3个值
	//str[0]=&a;
	int*(*p)[3];//转化为int*型的 指针*p 数量为3个
	p = &str;

	int* q = str[0];
	printf("q(&a):\t%p\n", q);//str[0]=&a,是int*型变量
	cout << typeid(q).name() << endl;
	printf("\n");

	printf("p:\t%p\n", p);//p=&{&a}(不存在)=&str[0]
	cout << typeid(p).name() << endl;
	printf("*p:\t%p\n", *p);//*p=“&&a”(不存在)=p
	cout << typeid(*p).name() << endl;

	printf("**p:\t%p\n", **p);
	cout << typeid(**p).name() << endl;
	printf("&a:\t%p\n", &a);//**P=&a
	cout << typeid(&a).name() << endl;

	printf("***p的值:\t%d\n", ***p);
	printf("a的值:\t%d\n", a);

	/*
	***p=a;
	**p=&a;
	*p=p(p是用来存a的地址的,也就是“&&a”);
	p=int*(*)[3]型的变量的地址;
	注意,此处只有p是指针,其他的都是存储的 地址(是常量)。
	*/

	//*p = str;	printf("%d\n",**p);//表达式必须是可修改的左值,*p本身是个地址,不是指针变量

	return 0;
}

 

指针两个方面:一个是地址值,还有一个就是地址的寻址方式(步长)
如果指针可以多级解引用,主要关心两方面:
解引用的地址值是否合法合理(不会越界,赋值是否正确等);
地址解引用后是以什么类型来表征的,也就是你*一次以后,是怎么看待得到的内容。
怎么看待得到的内容,决定了还能否*下去。
如果*以后得到的是数组,就还可以*,而且*之前,这个内容+1或-1运算的步长,是以这个数组长度来表征的。
如果*以后得到的是char * ,那么+1,-1的运算,地址值改变的就是一个字节。如果是int*,地址值改变的就是四字节。
如果*以后得到的不再是用地址来表征的对象,比如int,那么就不能再*了,因为,这个int的内容不是以地址来看待的。如果硬要*,一方面在语法层面会给出警告甚至错误。另一方面可能会导致内存读写错误。


如何看待每一级解引用的内容,这个信息映射在了指针声明时的指针类型信息里了。
int *p;只能*一次,而且*到的内容以int来看待。
int (*p)[5];可以*两次,*一次得到的内容以数组来看待,这个数组的类型是:包含5个int元素的数组。*两次,得到的内容以int来看待。

 

只要理解指针的本质,数组的本质,地址的本质。
再复杂的指针,都能马上分析出来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

超自然祈祷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值