3

Summary

1)指针的本质是变量,特殊之处在于指针存储的值是内存地址(内存中每个存储单元的编号:计算机中的最小存储单元是1byte ,即每个字节的编号都是一个内存地址)

2)程序中的一切元素都存在于内存中,因此可以通过内存地址访问程序元素

3)内存地址的本质是一个无符号整数(4字节或8字节);4字节和8字节分别对应于32位和64位,所以我们常说的32位和64位系统指的就是可访问的最大内存地址是多少。因为64位系统能支持更大的内存寻址,所以64位系统会比32位系统能同时运行的程序要多。

4)只有通过内存地址+长度才能确定一个变量中保存的值。内存地址只是一个起始值,只有根据类型信息,知道占多少内存、解读方式,才可以准确的读出变量里保存的值。

5)函数参数是指针时(传址调用),可以实现在函数内部修改函数外部变量的值;使用指针作为参数,可以作为一个传出参数,使得函数能返回多个值。

6)对于数组int a[5],a和&a的关系

  • a可以看做一个常量指针,表示数组首元素的地址,值是0xA,类型是int*,即内存长度为4
  • &a是数组的地址,值是0xA,类型是int(*)[5],即内存长度为20。指向数组的指针:int(*pArr)[5] = &a;

7)指针与数组的等价用法:

int a = {1, 2, 3, 4, 5};
int* p = a;

a[i] <--> *(a + i)
     <--> *(p + i) <--> p[i]

8)C语言中,字符串常量的类型是char*(c++里则是const char*);
int v = *p++;等价于int v = *p; p++;因为(*)的优先级和(++)相同

9)函数的本质是一段内存中的代码,占用一段连续的内存。函数名就是函数的入口地址(函数体代码的起始地址)。通过函数名调用函数,本质为指定具体地址的跳转执行。因此可以定义指针,保存函数的入口地址:type (*pFunc)(param) = funcName;

10)问:既然通过函数名可以直接调用函数,那么为什么还需要函数指针呢?
答:可以定义函数指针参数使用相同的代码,实现不同的功能。主调函数只知道函数原型是这样,但不知道具体会调用哪个函数。函数指针回调函数的实现机制:函数作为参数使用时,就构成了callback;

11)堆空间的本质是程序备用的“内存仓库”,以字节为单位预留的可用内存

12)在C语言中,void*指针可以和其他类型的指针type*进行相互赋值,因为C语言对指针的检查并不严格。但是在使用时一定要注意操作的数据类型,void*仅仅是一个初始地址,不包含长度信息

13)malloc用于向堆空间中以字节为单位申请内存;free用于归还堆空间的内存,malloc而不free,内存泄露;多次free,崩溃。

14)指针是一个变量,那么自然也就会有指向指针的指针,即多级指针。之前变量可以使用指针进行传址调用,那么也可以通过多级指针实现对指针的传址调用,在函数内部改变外部的指针的值。

15)二维数组的本质是一维数组,所以二维数组名a表示的是数组的首元素,即a[0]表示的是一个一维数组;a的类型是type (*)[size2]

int arr[2] = {0};
int* pa = arr;
int (*pArr)[2] = &arr;


int bArr[2][2] = {0};
int (*pb)[2] = bArr;
typedef int (oneDimension)[2];
oneDimension (*pbArr)[2] = &bArr;

16)禁止返回局部变量的地址,因为局部变量在函数调用结束后,生命期就结束了。此时返回的是个野指针

1、指针

1.1 指针本质

指针本质是C语言中的变量

  • 因为是变量,所以用于保存具体值
  • 特殊之处,指针保存的值是内存中的地址
  • 什么是内存地址?

    • 内存是计算机中的存储部件,每个存储单元有固定唯一的编号(每个存储单元:1byte)
    • 内存中存储单元的编号即内存地址

程序中的一切元素都存在于内存中,因此可以通过内存地址访问程序元素。

获取地址:

  • C语言中通过&操作符获取程序元素的地址
  • &可以获得变量、函数、数组的起始地址
  • 内存地址的本质是一个无符号整数(4或8字节)

    int var = 0;
    
    printf("var address = %p\n", &var);

1.2 指针语法

指针定义语法:type * pointer

  • type - 数据类型,决定访问内存时的长度范围
  • * - 标志,意味着定义一个指针变量
  • pointer - 变量名,遵循C语言命名规则

指针内存访问:* pointer

  • 指针访问操作符(*)作用于指针变量即可访问内存数据
  • 指针的类型决定通过地址访问内存时的长度范围
  • 指针的类型统一占用4字节或8字节

初学指针时的军规:

  • type*类型的指针只能保存type类型变量的地址(如float和int类型的变量,都是4个字节,但是内存中的二进制是不一样的,如果存的是int,以float的方式来读,肯定是不对的)
  • 禁止不同类型的指针相互赋值
  • 禁止将普通数值当做地址赋值给指针

    • 注意:指针保存的地址必须是有效地址

问题:之前在函数里交换两个变量的值,真的没办法实现么?
思考:想要交换两个变量的值,必须在函数内部修改函数数外部的变量?如果参数是指针呢?把变量的地址传进去,在函数里直接通过内存地址去修改地址是否可行?

void swap(int* a, int* b)
{
    int t = *a;
    *a = *b;
    *b = t;
}

通过传指针就实现了在函数内部修改函数外部的变量。另外,函数的return只能返回一个值,如果在参数中使用指针,这样函数就可以返回多个值,传出参数

2、指针与数组

问题:数组的本质是一段连续的内存,那么,数组的地址是什么?如何获取?

2.1 指针与普通数组

  • 使用取地址符&获得数组的地址:&a,&a表示一个地址(数组的地址),值是0xA,类型是int(*)[5],即长度是20。指向数组的指针:int(*pName)[5] = &a;
  • 数组名可看一个常量指针:a,a代表一个地址(数组首元素的地址),值是0xA,类型是int*,即长度是4
  • 当指针指向数组元素时,可以进行指针运算(指针移动)

    int a[] = {1, 2, 3, 4, 5};
    int* p = a;
    p = p + 1;

注意:数组名只是可以看做一个指针,代表了0号元素的地址。

指针与数组的等价用法:

int a = {1, 2, 3, 4, 5};
int* p = a;

a[i] <--> *(a + i)
     <--> *(p + i) <--> p[i]

2.2 指针与字符串

字符串拾遗

  • 问:C语言中的字符串常量是什么类型?
  • 答:char*类型,一种指针类型

    printf("%p\n", "GrandSoft");

指针移动组合拳:int v = *p++;

  • 指针访问操作符(*)和自增运算符(++)优先级相同
  • 所以,先从p指向的内存中取值,然后p进行移动
  • 等价于:int v = *p; p++;

3、指针与函数

问题:函数调用时会跳转到函数体对应的代码处执行,那么,如何知道函数体代码的具体位置?

深入函数之旅:

  • 函数的本质是一段内存中的代码(占用一段连续内存)
  • 函数拥有类型,函数类型由返回类型和参数类型列表组成

    函数声明类型
    int sum(int n)int(int)
    void g(void)void(void)

函数指针:type func(type1 a, type2 b)

  • 函数名即函数入口地址,类型为type (*)(type1, type2)
  • 对于func的函数,func和&func数值相同意义相同
  • 指向函数的指针:type (*pFunc)(type1, type2) = func;

    int (*pFunc)(int a, int b) = nullptr;
    pFunc = add;
    printf("%d\n", pFunc(1, 2));    // 1)
    printf("%d\n", (*pFunc)(1, 2)); // 2)

    对于两种调用方法的理解:
    1)函数指针保存函数的入口地址,函数名也保存了函数的入口地址,函数名可以直接调用函数,那么用pFunc直接调用函数,也很合理
    2)pFunc是一个地址,里面存放的函数的代码,使用*pFunc就取到了函数体,直接调用函数,也很合理

问题:既然可以通过函数名直接调用函数,那么为什么还需要使用函数指针呢?
答:这样就可以使用函数指针作为函数参数

  • 函数指针的本质还是指针(变量,保存地址)
  • 可以定义函数指针参数,使用相同代码实现不同功能

    int calculate(int a[], int len, int(*cal)(int, int))
    {
      int ret = a[0];
      int i = 0;
      for(i=1; i<len; i++)
      {
          ret = cal(ret, a[i]);
      }
    
      return ret;
    }

    根据传入不同的cal函数,可以实现比如数组相乘、相加、相除等等。在calculate里,只知道要调用的函数原型是这样,但是具体调用的是什么函数,执行了什么功能,只有执行到cal才知道

4、指针与堆空间

再论内存空间:内存区域不同,用途不同

  • 全局数据区:存放全局变量,静态变量
  • 栈空间:存放函数参数,局部变量
  • 堆空间:用于动态创建变量(数组)

堆空间的本质:

  • 备用的“内存仓库”,以字节为单位预留的可用内存
  • 程序可在需要时从“仓库”中申请使用内存(动态借
  • 当不需要再使用申请的内存时,需要及时归还(动态还

预备知识-void*

  • void类型是基础类型,对应的指针类型为void*
  • void*是指针类型,其指针变量能够保存地址
  • 通过void*指针无法获取内存中的数据(无长度信息)

堆空间的使用:

  • 工具包:stdlib.h
  • 申请:void* malloc(unsigned bytes)
  • 归还:void free(void* p),p必须是堆空间中的地址

堆空间的使用原则:

  • 有借有还,再接不难
  • malloc申请内存后,应该判断是否申请成功
  • free只能申请释放到的内存,且不可多次释放

5、指针问题剖析

5.1 多级指针

可以定义指针的指针来保存其他指针变量的地址

type v;
type* pv = &v;
type** ppv = &pv;
type*** ppv = &ppv;
...

使用指针可以取到指针的地址,那么就可以在函数内部,改变指针里保存的值。
指针的传址调用,修改函数外的指针的值:

int getDouble(double** pp, unsigned int n)
{
    int ret = 0;
    double* pd = (double*)malloc(sizeof(double) * n);

    if(pd != NULL)
    {
        *pp = pd;

        ret = 1;
    }

    return ret;
}

5.2 二维数组

问题:一维数组名的类型为type,那么二维数组名的类型是不是type*?

再论二维数组:

  • 二维数组的本质是一维数组,即:数组中的元素是一维数组
  • 因此:二维数组a[][],a表示a[0],首元素是一个一维数组

    int b[][2] = {1, 2, 3, 4};
    int(*pnb)[2] = b;

本文总结自“狄泰软件学院”唐佐林老师《C语言入门课程》。
如有错漏之处,恳请指正。


bryson
169 声望12 粉丝