我对C语言的理解—指针

写在前面

楼主大一开始学的C语言,后来中间也学过,一直没怎么弄懂指针,现在结合网上资源在此记录一下我的理解。希望你看完也有一些收获。

什么是指针

指针是“指向(point to)”另外一种类型的复合类型。复合类型是指基于其它类型定义的类型。

理解指针,先从内存说起:内存是一个很大的,线性的字节数组。每一个字节都是固定的大小,由8个二进制位组成。最关键的是,每一个字节都有一个唯一的编号,编号从0开始,一直到最后一个字节。(存储单元一般以八个二进制单位也就是一个字节为单位,字节是最小的存储单位)。

程序加载到内存中后,在程序中使用的变量、常量、函数等数据,都有自己唯一的一个编号,这个编号就是这个数据的地址。

指针的值实质是内存单元(即字节)的编号,所以指针单独从数值上看,也是整数,他们一般用16进制表示。指针的值(虚拟地址值,关于虚拟地址与物理地址的区别在我看来是针对操作系统而言的,具体下次再说)使用一个机器字的大小来存储,也就是说,对于一个机器字为w位的电脑而言,它的虚拟地址空间是0~[2的w次幂] - 1,程序最多能访问2的w次幂个字节。这就是为什么xp这种32位系统最大支持4GB内存的原因了。

可以理解为:指针是程序数据在内存中的地址,而指针变量是用来保存这些地址的变量。

如果还不理解你可以看一下C Primer Plus(第6版)中文版里面关于指针的简易介绍。

变量在内存中的存储

在了解更多指针知识前,你必须要先理解一下这个问题。而理解这个问题你必须先理解下面这个图(32位系统经典内存布局,现在的32位和64位默认内存布局中,栈区的起始地址是随机的)。这里自行理解
在这里插入图片描述
举一个最简单的例子 int a = 1,假设计算机使用大端方式存储:(大端模式和小端模式即一个超过一字节的数据,它高位和低位存放的位置问题)
在这里插入图片描述
内存数据有几点规律:
1、计算机中的所有数据都是以二进制存储的
2、数据类型决定了占用内存的大小
3、占据内存的地址就是地址值最小的那个字节的地址。

现在就可以理解 a 在内存中为什么占4个字节,而且首地址为0028FF40了。

指针对象(变量)

用来保存指针的对象,就是指针对象。如果指针变量p1保存了变量 a 的地址,则就说:p1指向了变量a,也可以说p1指向了a所在的内存块。

指针对象p1,也有自己的内存空间,32位机器占4个字节,64位机器占8个字节。所以会有指针的指针。

指针的声明(复杂类型说明)

下面让我们先从简单的类型开始慢慢分析吧。

int p;
这是一个普通的整型变量

int * p;
首先从P处开始,先与* 结合,所以说明P是一个指针。然后再与int结合,说明指针所指向的内容的类型为int型,所以P是一个返回整型数据的指针

int p[3];
首先从P处开始,先与[]结合,说明P是一个数组。然后与int结合,说明数组里的元素是整型的,所以P是一个由整型数据组成的数组。

int * p[3];
首先从P处开始,先与[]结合,因为其优先级比高,所以P是一个数组。然后再与* 结合,说明数组里的元素是指针类型。之后再与int结合,说明指针所指向的内容的类型是整型的,所以P是一个由返回整型数据的指针所组成的数组。

理解:先定义一个3个元素的数组P,然后给数组取指针,这样就等于定义了一个数组,数组里有3个指针元素。

int (* p)[3];
首先从P处开始,先与* 结合,说明P是一个指针。然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组。之后再与int结合,说明数组里的元素是整型的。所以P是一个指向由整型数据组成3个整数的指针。

理解:先定义一个指针,这个指针返回的是一个数组,该数组里面有3个整型元素,指针存放的是数组的首地址。不过这种不常用,一般定义一个数组3个元素的数组,p就是指向p[0]的指针。

int * * p;
首先从P开始,先与* 结合,说明P是一个指针。然后再跟它结合,说明指针所指向的元素是指针。之后再与int结合,说明该指针所指向的元素是整型数据。由于二级指针以及更高级的指针极少用在复杂的类型中,所以后面更复杂的类型我们就不考虑多级指针了,最多只考虑一级指针。

理解:指针变量也是一个变量,在内存中也是占内存的,只不过它不存放基本类型数据,而是存放其他基本类型变量的地址。既然指针变量也有自己的物理地址,那么指针变量的地址用什么来存储呢?用比该指针类型高一级的指针变量来存放指针变量的地址,如二级指针变量存放一级指针变量的地址,三级指针变量存放二级变量的地址,依次类推。

举例

Struct char_device_struct **cp
指向指向结构体指针的指针,cp存放指向结构体指针的地址,*cp存放结构体的地址

int p(int);
从P处起,先与()结合,说明P是一个函数。然后进入()里分析,说明该函数有一个整型变量的参数,之后再与外面的int结合,说明函数的返回值是一个整型数据。

Int (*p)(int);
从P处开始,先与指针结合,说明P是一个指针。然后与()结合,说明指针指向的是一个函数。之后再与()里的int结合,说明函数有一个int型的参数,再与最外层的int结合,说明函数的返回类型是整型,所以P是一个指向有一个整型参数且返回类型为整型的函数的指针。

理解:这里代表的是一个函数指针,首先它是一个指针,然后它指向函数p(x),也就是指针指向的是代码区,函数代码所占用的内存块的首地址。

每一个函数本身也是一种程序数据,一个函数包含了多条执行语句,它被编译后,实质上是多条机器指令的合集。在程序载入到内存后,函数的机器指令存放在一个特定的逻辑区域:代码区。既然是存放在内存中,那么函数也是有自己的指针的。其实函数名单独进行使用时就是这个函数的指针。

如果你还没弄清楚,建议阅读:函数指针,指针函数,函数指针数组

int * (* p(int))[3];
可以先跳过,不看这个类型,过于复杂。从P开始,先与()结合,说明P是一个函数。然后进入()里面,与int结合,说明函数有一个整型变量参数。然后再与外面 * 的结合,说明函数返回的是一个指针。之后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组。接着再与 * 结合,说明数组里的元素是指针,最后再与int结合,说明指针指向的内容是整型数据。所以P是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数。

说到这里也就差不多了。理解了这几个类型,其它的类型对我们来说也是小菜了。不过一般不会用太复杂的类型,那样会大大减小程序的可读性,请慎用。这上面的几种类型已经足够我们用了。

获取对象地址

指针用于存放某个对象的地址,要想获取该地址,虚使用取地址符(&),如下:

int add(int a , int b)
{
return a + b;
}

int main(void)
{
int num = 97;
float score = 10.00F;
int arr[3] = {1,2,3};
int* p_num = #
int* p_arr1 = arr;//p_arr1意思是指向数组第一个元素的指针
float* p_score = &score;
int (*p_arr)[3] = &arr;
int (*fp_add)(int ,int ) = add; //p_add是指向函数add的函数指针
const char* p_msg = "Hello world";//p_msg是指向字符数组的指针

return 0;
}

通过上面可以看到&的使用,但是有几个例子没有使用&,因为这是特殊情况:
1、数组名的值就是这个数组的第一个元素的地址。
2、函数名的值就是这个函数的地址。
3、字符串字面值常量作为右值时,就是这个字符串对应的字符数组的名称,也就是这个字符串在内存中的地址。

指针的四方面的内容

指针的类型与指针所指向的类型

指针的类型和指针所指向的类型很明显是不一样的东西,但好多情况下却容易忽视它们的区别。指针的类型是指针自身的类型,而指针所指向的类型是指针指向的数据(内存)的类型。

指针的类型

从语法上来看,我们只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。如:

1 int *ptr;         //指针的类型是 int*
2 char *ptr;        //指针的类型是 char*
3 int **ptr;        //指针的类型是 int**
4 int (*ptr)[3];    //指针的类型是 int(*)[3]
5 int *(*ptr)[4];   //指针的类型是 int*(*)[4]

指针所指向的类型

当通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。

从语法上来看,我们只要把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。 如:

1 int *ptr;         //指针所指向的类型是 int
2 char *ptr;        //指针所指向的的类型是 char
3 int **ptr;        //指针所指向的的类型是 int*
4 int (*ptr)[3];    //指针所指向的的类型是 int()[3]
5 int *(*ptr)[4];   //指针所指向的的类型是 int*()[4]

指针的值

即指针所指向的内存区或地址。指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。

在32位程序里,所有类型的指针的值都是一个32位整数,因为32位程序里内存地址全都是32位长。指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为si zeof(指针所指向的类型)的一片内存区。

以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。

指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在例一中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。

以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?

指针的值(即地址)总会是下列四种状态之一:
1、指向一个对象的地址
2、指向紧邻对象所占空间的下一个位置
3、空指针,意味着指针没有指向任何对象
4、无效指针(野指针),上述情况之外的其他值

第一种状态很好理解就不说明了,第二种状态主要用于迭代器和指针的计算。

空指针

在C语言中,我们让指针变量赋值为NULL表示一个空指针,而C语言中,NULL实质是 ((void*)0) , 在C++中,NULL实质是0。C++中也可以使用C11标准中的nullpte字面值赋值,意思是一样的。任何程序数据都不会存储在地址为0的内存块中,它是被操作系统预留的内存块。

野指针

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的),或者是指向当前应用程序不可访问的地址值。指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的,程序会出现运行时错误,导致程序意外终止。

任何一个指针变量在做解地址操作前,都必须保证它指向的是有效的,可用的内存块,否则就会出错。坏指针是造成C语言Bug的最频繁的原因之一。

未经初始化的指针就是个野指针,所以在定义指针变量的时候一定要进行初始化。如果实在是不知道指针的指向,则使用nullptr或NULL进行赋值。

野指针的成因

野指针主要是因为这些疏忽而出现的删除或申请访问受限内存区域的指针。

指针变量未初始化

任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。如果没有初始化,编译器会报错“ ‘point’ may be uninitializedin the function ”。

指针释放后之后未置空

有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。

指针操作超越变量作用域

不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。示例程序如下:

class A {
public:
  void Func(void){ cout << “Func of class A” << endl; }
};
class B {
public:
  A *p;
  void Test(void) {
    A a;
    p = &a; // 注意a的生命期 ,只在这个函数Test中,而不是整个class B
  }
  void Test1() {
  p->Func(); // p 是“野指针”
  }
};

函数 Test1 在执行语句 p->Func()时,p 的值还是 a 的地址,对象 a 的内容已经被清除,所以 p 就成了“野指针”

野指针的规避

初始化时置 NULL

指针变量一定要初始化为NULL,因为任何指针变量(除了static修饰的指针变量)刚被创建时不会自动成为NULL指针,它的缺省值是随机的。

释放时置 NULL

当指针p指向的内存空间释放时,没有设置指针p的值为NULL。delete和free只是把内存空间释放了,但是并没有将指针p的值赋为NULL。通常判断一个指针是否合法,都是使用if语句测试该指针是否为NULL。例如:

int *p=newint(6);
delete p;
// 应加入 p=NULL; 以防出错
// ...
if(p != NULL)
{
  *p=7;
  cout << p << endl;
}

对于使用 free 的情况,常常定义一个宏或者函数 xfree 来代替 free 置空指针:

#define xfree(x) free(x); x = NULL;
// 在 C++ 中应使用 nullptr 指代空指针
// 一些平台上的 C/C++ 已经预先添加了 xfree 拓展,如 GNU 的libiberty库
xfree(p);
// 用函数实现,例如 GitHub 上的 AOSC-Dev/Anthon-Starter #9:
static inline void *Xfree(void *ptr) {
    free(ptr);
#ifdef __cplusplus
    return nullptr;
#else
    return NULL;
#endif
}
q=Xfree(q);

所以动态分配内存后,如果使用完这个动态分配的内存空间后,必须习惯性地使用delete操作符去释放它。

指针本身所占据的内存区

指针本身占了多大的内存?只要用函数sizeof(指针的类型)测一下就知道了。32位机器指针本身占据4个字节,64位机器占8个字节。所以会有指针的指针。指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用。

指针之间的赋值

指针赋值和int变量赋值一样,就是将地址的值拷贝给另外一个。指针之间的赋值是一种浅拷贝,是在多个编程单元之间共享内存数据的高效的方法。

int* p1 = &a;
int* p2 = * p1;

在这里插入图片描述
p1和p2所在的内存地址不同,但是所指向的内存地址相同,都是0028FF40。

参考C Primer Plus(第6版)中文版使用指针在函数中通信。

更多阅读:什么是指针

本文参考:https://mp.weixin.qq.com/s?__biz=MzIwNzIwMzYxOQ==&mid=2670467988&idx=1&sn=4dc0b2470438a2be9e8318d27932184c&chksm=8dcd47d8babacece194cd27ff5476520150429728917a055e33481713a156c95d3170b7d478b&mpshare=1&scene=23&srcid=0722Exa7xI1pafzIK2TUu65Q&sharer_sharetime=1597641455936&sharer_shareid=5c7f1d2ab137d67891de961c82dbbb87#rd

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一只嵌入式爱好者

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

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

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

打赏作者

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

抵扣说明:

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

余额充值