手拉手教你如何理解c/c++中的指针

目录
  • 前言
  • 一,内存和地址
  • 二,指针的本质就是地址
  • 三,常量指针与指针常量
  • 四,指针与数组
  • 五,数组指针与指针数组
  • 六,指针函数与函数指针
  • 总结

前言

指针是c语言为什么如此流行的一个重要原因,正是有了指针的存在,才使得c/c++能够可以比使用其他语言编写出更为紧凑和有效的程序,可以说,没有掌握指针,就没有权利说自己会用c/c++.然而。然而对于大多数初学者,面对指针这个概念简直是望而生畏,如果前期指针运用的不熟练,后期编写的程序随时都有可能成为一颗定时炸弹,因此今天我就花点时间给大家解释一下我自己对c/c++中指针的理解。

一,内存和地址

我们知道,计算机内存的每个字节都有一个唯一的地址,CPU每次寻址就是通过固定的步长(这就解释了为什么需要内存对齐)来跳跃进行寻址的。举个例子,我们可以把内存看做是一条长街上的一排房屋,每个房屋都有自己固定的门牌号,每座房屋里面都可以容纳数据,为了读取到某个房屋里面的数据,我们必须知道这个房屋的门牌号,根据这个门牌号来打开这个房间,取走数据。同样,计算机也必须为每个内存字节都编上号码,就像门牌号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。

二,指针的本质就是地址

当我们在程序中声明一个变量并给这个变量赋值的时候,编译器做了什么呢?实际上,变量名代表内存中的一个存储单元,在编译器对程序编译连接的时候由系统给变量分配一个地址:

int a = 10;

上面这行代码我们定义并初始化了这个变量a,系统会为a分配一块内存单元,a只是这块内存单元的别名,在程序中从变量中取值,实际上是通过变量名找到相应的内存单元,从其中读取数据。

假如系统为变量 a 分配的内存地址为0xFF00, 那么我们可以说这个地址就是变量 a 的门牌号。一个变量的地址称为该变量的指针。所以说,指针的本质就是地址,指针变量是一种特殊的变量,它专门保存指针(也即地址),当我们说这个地址对应的内存单元的时候,我们可以说这个指针指向这块内存单元。

例如:

int a = 10;
int* p = &a;  //定义指针变量 p
*p = 20;      //将指针p指向的值修改为 20

上面两行代码中,我们首先定义了一个整型变量 a ,然后又定义了一个指针变量 p 指向 a .第二行代码中,符号&代表取地址,相当于把变量a的地址赋值给了指针变量p(p指向a),*加在指针变量前面代表解引用,意思找到指针p指向的值,因此,第三行代码的意思就是讲p指向的值也就是a修改为20.总之一定要记住,符号&代表取值,符号*代表解引用:

符号 意义
& 取地址
* 解引用

这三行代码的内存模型如下:

我们假设系统给变量 a 分配的内存首地址为2000,我们又声明了一个指针变量p,这个p也是要占用内存空间的(32位系统占用4个字节,64位系统占用8个字节,请思考为什么),只不过这个变量p保存的内容是变量a的地址,也就是2000,当我们想通过p来操纵a的话,首先要根据p保存的地址找到它指向的内容,也就是解引用*p,当*p的内容放生改变的时候,首地址为2000的内存单元存储的值也会做出改变,因此变量当*p被重新赋值为20的时候,变量a的值也会做出改变,变为20.

由此扩展到二级指针,如果我们再定义一个指针变量q来指向p,那么q就是一个二级指针,因为它指向的对象还是一个指针,只不过比他自己低一级,是一级指针,那么二级指针如何定义呢,请看下面的代码:

int a = 10;
int* p = &a;
int** q = &p;

上面第三行代码就是定义了一个二级指针q,它指向的是一级指针p,而一级指针p又指向了变量a,它的内存模型如下图所示:

二级指针q保存的内容为一级指针p的地址而非内容,注意p地址是2008,p的内容为2000. 因此对q进行解引用也即*q得出的是p,也就是2008,再对(*q)进行解引用也即*(*q)得出的才是变量a的值,由于运算符的结合性自右向左,因此括号可以省略,也即**q才是a的值。我们可以编写代码试一下:

cout <<"a的值为:"<< **q << endl;

我们观察一下输出结果:

没错,输出的结果完全正确。

由此再扩充到多级指针,二级指针是指向一级指针的指针,那么n级指针便是指向n-1级指针的指针,以此类推。

三,常量指针与指针常量

请看下面两行代码:

int a = 10;
const int * p1 = &a;    //常量指针
int * const p2 = &a;    //指针常量

上面第二行代码中的p1是一个常量指针,就是指向常量的指针变量。意味着它指向的值不可以修改,但是指针的指向可以修改:

int a = 10;
int b = 20;
const int * p1 = &a;    //常量指针
*p1 = 100;  //错误,常量指针指向的值不可以修改
p1 = &b;   //正确

而对于指针常量,它本质是一个常量,但是由指针修饰。意味着它指向的值可以修改,但是指针的指向不可修改,与常量指针刚刚相反:

int a = 10;
int b = 20;
int * const p1 = &a;    //指针常量
*p1 = 100;  //正确
p1 = &b;   //错误,指针的指向不可以修改

因此,我们总结下:

名称 意义 特点
const int * p 常量指针 指向可修改,指向的值不可修改
int * const p 指针常量 指向不可修改,指向的值可修改

四,指针与数组

我们知道,一维数组名本身就是一个指针,但是在使用的过程中要小心,因为这个指针分为指向数组首元素的指针与指向整个数组的指针,那么如何区分它们呢?我们来看下面几行代码:

int arr[] = {1, 2, 3, 4, 5};
int* p1 = arr;
int* p2 = &arr[0];
int* p3 = &arr;    //报错

上面三行代码中,其中p1与p2是等价的,因为数组名arr本身就是一个指针,但是这个指针不是指向整个数组,而是指向数组的首元素的地址。第四行直接报错,因为&arr指的是整个数组的指针,不能把数组指针赋值给整形指针。虽然arr与&arr在数值上是相同的,但是两者意义不同。意味着&arr它的步长为整个数组,而对于arr,步长为单个元素。

所以,我们得出结论,对于一维数组arr:

名称 意义 步长
arr 指向数组首元素 单个元素
&arr[0] 指向数组首元素 单个元素
&arr 指向整个数组 整个数组

在定义了指向数组首元素的指针变量后,我们可以通过这个指针变量来访问数组元素:

  int arr[] = { 1,2,3,4,5 };
  int* p1 = arr;
  int length = sizeof(arr) / sizeof(int);
  for (int i = 0; i < length; i++)
  {
    cout << p1[i] << endl;
    cout << *(p1 + i) << endl;
  }

上面几行代码中,p1[i]与*(p1+i)两者是等价的,所以输出的结果一样。但是要注意,当用sizeof操作符操作arr的时候,这个时候不能把arr当做一个指针来对待,因为sizeof操作数组的时候它返回的是数组的字节长度,而单个指针变量只占用四个字节。上面循环体中,我们也可以通过下面方式访问:

cout << *p1++ << endl;
cout << *(p1++) << endl;

*p1++与*(p1++)是等价的,这是因为++的运算符优先级比*要高,因此不管你加不加括号,都会优先执行p++,然而p++是先返回p的值,再与*结合,最后p再向后移动一位。

不过在这里要特别注意,有一种情况下我们是不能通过sizeof操作符来计算数组的长度的,就是当数组名作为函数参数传递的时候:

void test(int arr[])
{
  int lenth = sizeof(arr) / sizeof(int);
}

上面这行代码语法上没有问题,但是得出的结果却不是我们想要的结果,为什么呢,这是因为数组名作为函数传递的时候,会退化成一个指针,如果是二维数组的话,会退化成指向一维数组的指针,所以sizeof(arr)计算出来的结果就不是数组的字节长度了。所以说,在c/c++中传递数组的时候,一般我们也会把数组的长度作为形参传递过去。

但是我们不能通过下面方式去访问数组元素:

cout << *arr++ << endl;    //报错

这是因为arr本身是一个指针常量,指针的指向不可更改,因此编译器直接报错。

五,数组指针与指针数组

数组指针顾名思义,本质就是一个指针,这个指针指向整个数组;指针数组本质上是一个数组,但是数组的每个元素都是指针。请看下面两行代码:

int *p1[10];    //指针数组
int (*p2)[10];  //数组指针

上面两行代码,p1是一个数组,而p2却是一个指针,它指向一个匿名数组。为什么是这样呢?这是因为[]的优先级比*要高。p1 先与[]结合,构成一个数组的定义,数组名为p1,int *修饰的是数组的内容,即数组的每个元素。那现在我们清楚,这是一个数组,其包含10 个指向int 类型数据的指针,即指针数组。至于p2 就更好理解了,在这里括号的优先级比[]高,*号和p2 构成一个指针的定义,指针变量名为p2,int 修饰的是数组的内容,即数组的每个元素。数组在这里并没有名字,是个匿名数组。那现在我们清楚p2 是一个指针,它指向一个包含10 个int 类型数据的数组,即数组指针。

p1为数组名,每个元素都是int型指针

p2为指针变量,指向一个匿名数组

如果我们定义:

int(*p)[10] = &arr;

那么如何访问数组的元素呢?且看,由于上行代码中,p=&arr,那么对其解引用,*p就是arr,因此我们可以通过(*p)[]来进行访问数组的元素:

for(int i = 0; i < 10; i++)
{
  cout<< (*p)[i] << endl;
}

六,指针函数与函数指针

指针函数顾名思义,他是一个函数,但返回值是一个指针,例如下面这几行代码:

int* test()
{
  int a = 10;
  int* p = &a;
  return p;
}

这个test就是一个指针函数,它返回的是一个int型的指针。

函数指针本质是一个指针,这个指针指向一个函数,那么我们如何定义函数指针呢?请看下面代码:

int myAdd(int a, int b)
{
  return a + b;
}
void test()
{
  int(*pFun)(int, int) = myAdd;    //定义一个函数指针
  cout << (*pFun)(2, 5) << endl;    //用函数指针调用函数
  cout << pFun(2, 5) << endl;      //用函数指针调用函数
}

上面test函数代码中,我们定义了一个函数指针,在最后进行调用函数的时候,有两种方法,一种是用*pFun来调用,一种是直接用pFun来调用,可见两种方法结果都一样。

最后,我们来看个比较混合指针复杂的案例:

char *(* c[10])(int **p);

乍一看,让人眼花缭乱,不知道是什么东西,在这里请大家记住一个规则:C语言标准规定,对于一个符号的定义,编译器总是从它的名字开始读取,然后按照优先级顺序依次解析。注意是从名字开始,不是从开头也不是从末尾,这是理解复杂指针的关键。

有了上面的规则,我们来逐步剖析上面哪行代码的意义:

首先从*c[10]开始,由于[]的优先级比*高,因此,*c[10]代表一个指针数组,每个元素都是指针,但类型还不知道。再看右边的(int** p),它是一个函数,参数为一个二级指针。最左边char* 代表这个函数的返回类型。因此,整行代码的含义就是:c 是一个拥有 10 个元素的指针数组,数组每个元素指向一个原型为char *(int **p)的函数。

总结

到此这篇关于c/c++中指针的文章就介绍到这了,更多相关c/c++中的指针内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++指针数组、数组指针、数组名及二维数组技巧汇总

    本文较为详细的分析了关于理解C++指针数组,数组指针,数组名,二维数组的一些技巧.是比较重要的概念,相信对于大家的C++程序设计有一定的帮助作用. 一.关于数组名 假设有数组: int a[3] = {1, 2, 3} 1.数组名代表数组第一个元素的地址,注意,不是数组地址(虽然值相等),是数组第一个元素地址,a 等同于 &a[0]; a+1是第二个元素的地址.比第一个元素地址a(或者&a[0])超出了一个整型指针的大小,在这里是4个字节(byte) cout << a <

  • 深入理解c++指针的指针和指针的引用

    展示一下使用指针的指针和指针的引用修改传递给方法的指针,以便更好的使用它.(这里说的指针的指针不是一个二维数组) 为什么需要使用它们 当我们把一个指针做为参数传一个方法时,其实是把指针的复本传递给了方法,也可以说传递指针是指针的值传递. 如果我们在方法内部修改指针会出现问题,在方法里做修改只是修改的指针的copy而不是指针本身,原来的指针还保留着原来 的值.我们用下边的代码说明一下问题: int m_value = 1; void func(int *p) { p = &m_value; } i

  • C++中指向结构体变量的指针

    定义: 结构体变量的指针就是该变来那个所占据的内存段的起始地址.可以设一个指针变量,来指向一个结构体变量,此时该指针变量的值是结构体变量的起始地址. 设p是指向结构体变量的数组,则可以通过以下的方式,调用指向的那个结构体中的成员: (1)结构体变量.成员名.如,stu.num. (2)(*p).成员名.如,(*p).num. (3)p->成员名.如,p->num. 复制代码 代码如下: #include<iostream>#include<string>using na

  • C++中的对象指针总结

    指向对象的指针在建立对象的时候,变异系统会给每一个对象分配一定的存储空间,以存放其成员.对象空间的起始地址就是对象的指针.可以定义一个指针变量,用来存放对象的指针. 一个简单的示例1.1: 复制代码 代码如下: #include<iostream>using namespace std;class Student{ public:  int num;  int score;  Student(int ,int );//声明构造函数  void Print();//声明输出信息函数};Stude

  • C++11新特性之智能指针(shared_ptr/unique_ptr/weak_ptr)

    shared_ptr基本用法 shared_ptr采用引用计数的方式管理所指向的对象.当有一个新的shared_ptr指向同一个对象时(复制shared_ptr等),引用计数加1.当shared_ptr离开作用域时,引用计数减1.当引用计数为0时,释放所管理的内存. 这样做的好处在于解放了程序员手动释放内存的压力.之前,为了处理程序中的异常情况,往往需要将指针手动封装到类中,通过析构函数来释放动态分配的内存:现在这一过程就可以交给shared_ptr去做了. 一般我们使用make_shared来

  • C/C++指针和取地址的方法

    先看下面的程序: 复制代码 代码如下: void main() {     int a = 100;     int *ap = &a;     printf("%p\n",&a);//输出:002AF744     printf("%p\n",ap);//输出:002AF744     printf("%d\n",*ap);//输出:100     printf("%p\n",&ap);//输出:00

  • C++智能指针实例详解

    本文通过实例详细阐述了C++关于智能指针的概念及用法,有助于读者加深对智能指针的理解.详情如下: 一.简介 由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete.程序员忘记 delete,流程太复杂,最终导致没有 delete,异常导致程序过早退出,没有执行 delete 的情况并不罕见. 用智能指针便可以有效缓解这类问题,本文主要讲解参见的智能指针的用法.包括:std::auto_ptr.boost::scoped_ptr.boost::shared_p

  • C++中this指针的用法及介绍

    this指针只能在一个类的成员函数中调用,它表示当前对象的地址.下面是一个例子:   复制代码 代码如下: void Date::setMonth( int mn )     {      month = mn; // 这三句是等价的      this->month = mn;      (*this).month = mn;     } 1. this只能在成员函数中使用.全局函数,静态函数都不能使用this.实际上,成员函数默认第一个参数为T* const register this.如:

  • 手拉手教你如何理解c/c++中的指针

    目录 前言 一,内存和地址 二,指针的本质就是地址 三,常量指针与指针常量 四,指针与数组 五,数组指针与指针数组 六,指针函数与函数指针 总结 前言 指针是c语言为什么如此流行的一个重要原因,正是有了指针的存在,才使得c/c++能够可以比使用其他语言编写出更为紧凑和有效的程序,可以说,没有掌握指针,就没有权利说自己会用c/c++.然而.然而对于大多数初学者,面对指针这个概念简直是望而生畏,如果前期指针运用的不熟练,后期编写的程序随时都有可能成为一颗定时炸弹,因此今天我就花点时间给大家解释一下我

  • 一文理解Android系统中强指针的实现

    强指针和弱指针基础 android中的智能指针包括:轻量级指针.强指针.弱指针. 强指针:它主要是通过强引用计数来进行维护对象的生命周期. 弱指针:它主要是通过弱引用计数来进行维护所指向对象的生命周期. 如果在一个类中使用了强指针或者弱指针的技术,那么这个类就必须从RefBase这个类进行做继承,因为强指针和弱指针是通过RefBase这个类来提供实现的引用计数器. 强指针和弱指针关系相对于轻量级指针来说更加亲密,因此他们一般是相互配合使用的. 强指针原理分析 以下针对源码的分析都是来源于andr

  • 手拉手教你如何处理vue项目中的错误

    目录 一.错误类型 二.如何处理 后端接口错误 代码逻辑问题 全局设置错误处理 生命周期钩子 附:Vue统一错误处理 总结一下 一.错误类型 任何一个框架,对于错误的处理都是一种必备的能力 在Vue 中,则是定义了一套对应的错误处理规则给到使用者,且在源代码级别,对部分必要的过程做了一定的错误处理. 主要的错误来源包括: 后端接口错误 代码中本身逻辑错误 二.如何处理 后端接口错误 通过axios的interceptor实现网络请求的response先进行一层拦截 apiClient.inter

  • 深入理解关于javascript中apply()和call()方法的区别

    如果没接触过动态语言,以编译型语言的思维方式去理解javaScript将会有种神奇而怪异的感觉,因为意识上往往不可能的事偏偏就发生了,甚至觉得不可理喻.如果在学JavaScript这自由而变幻无穷的语言过程中遇到这种感觉,那么就从现在形始,请放下的您的"偏见",因为这对您来说绝对是一片新大陆,让JavaScrip慢慢融化以前一套凝固的编程意识,注入新的生机! 好,言归正传,先理解JavaScrtipt动态变换运行时上下文特性,这种特性主要就体现在apply, call两个方法的运用上.

  • 快速理解Java设计模式中的组合模式

    组合模式是一种常见的设计模式(但我感觉有点复杂)也叫合成模式,有时又叫做部分-整体模式,主要是用来描述部分与整体的关系. 个人理解:组合模式就是将部分组装成整体. 定义如下: 将对象组合成树形结构以表示"部分-整体"的层次结构,使得用户对单个对象和组合对象的使用具有一致性. 通用类图如下: 组合模式的包含角色: ● Component 抽象构件角色 定义参加组合对象的共有方法和属性,可以定义一些默认的行为或属性. ● Leaf 叶子构件 叶子对象,其下再也没有其他的分支,也就是遍历的最

  • 深入理解 Go 语言中的 Context

    Hi,大家好,我是明哥. 在自己学习 Golang 的这段时间里,我写了详细的学习笔记放在我的个人微信公众号 <Go编程时光>,对于 Go 语言,我也算是个初学者,因此写的东西应该会比较适合刚接触的同学,如果你也是刚学习 Go 语言,不防关注一下,一起学习,一起成长. 我的在线博客:http://golang.iswbm.com 我的 Github:github.com/iswbm/GolangCodingTime 1. 什么是 Context? 在 Go 1.7 版本之前,context 还

  • 浅谈vue中$event理解和框架中在包含默认值外传参

    在vue中普通方法中默认带有event DOM事件如greet方法,如果是内联函数的话如warn方法,只需要在定义方法的地方同时传入$event即可,这里需要强调的是在iview中,这里用的是select组件,在其on-change事件中如果想要传入自定义的参数,使用直接传参的方式,获取的是传入的参数,那么如何获取到该方法默认的返回值(即不传参数时返回的默认选中值),这里使用 $event传入代表选中的值,如test方法,这里似乎也只要$event可以传入代表选中的值,其他的可能就是普通的参数,

  • 简单的理解java集合中的HashSet和HashTree几个重写方法

    Java中的set是无序的,但是是不可重复的 HashSet底层是哈希表,通过调用hashcode和equals方法实现去重 当我们HashSet里面存的是字符串时,就能默认去重了,因为String已经重写了hashcode和euqals方法 public static void main(String[] args) { HashSet<String> set = new HashSet(); set.add("java"); set.add("c")

  • 我所理解的JavaScript中的this指向

    前言 JS 中的 this 指向是一个经常被问到的问题,网上也有很多文章是关于 this 的.本文整理一下我理解下的 this 以及一些我比较疑惑的关于 this 问题. this 指向 有几个 this 的指向问题是几乎每篇文章都会说的,比如作为函数直接调用,作为对象的方法调用, new 运算符执行中的 this 行为.比较通用的说法是, this 指向的是直接调用该函数的对象.其实也很好理解,就是为什么需要 this 这个关键字,就是我们有需要在函数内部对调用函数的对象进行操作的需求.但是有

  • 深入理解Node.js中的Worker线程

    概述 多年以来,Node.js都不是实现高 CPU 密集型应用的最佳选择,这主要就是因为JavaScript的单线程.作为对此问题的解决方案,Node.jsv10.5.0 通过worker_threads模块引入了实验性的 "worker 线程" 概念,并从 Node.js v12 LTS 起成为一个稳定功能.本文将解释其如何工作,以及如何使用 Worker 线程获得最佳性能. Node.js 中 CPU 密集型应用的历史 在 worker 线程之前,Node.js 中有多种方式执行

随机推荐