Day06 郝斌C语言自学视频之C语言的指针

这篇笔记主要是介绍C语言中的指针,包括指针的重要性、定义、使用等。阅读本文预计需要 10 分钟。

指针的重要性

指针是继流程控制函数之后,又一个重点。可以说指针是 C 语言的灵魂

指针的重要性具体表现

  1. 表示一些复杂的数据结构。
  2. 快递的传递数据,减少内存的耗用。【重点】
  3. 使函数返回一个以上的值。【重点】
  4. 能直接访问硬件。
  5. 能够方便处理字符串。
  6. 是理解面向对象语言中引用的基础。

指针的定义

在介绍指针定义时,我们先看一下地址的概念。

地址:地址是内存单元的编号。它是从 0 开始的非负整数。对于32位系统,它的范围是:4 G [0–(4G-1)]。4 G即2^32。

指针:指针就是地址,地址就是指针。

指针变量:指针变量就是存放内存单元编号的变量,或者说指针变量就是存放地址的变量。

指针和指针变量是两个不同的概念。但是要注意,通常我们叙述时,会把指针变量简称为指针,实际他们含义并不一样。

指针的本质就是一个操作受限的非负整数

指针的使用

基本类型指针

格式:
  指针变量类型  指针变量名

例:

/*
    时间:2020年2月24日13:23:59
    功能:
        测试基本类型指针的使用
*/

# include <stdio.h>

int main(void)
{
    int * p;
    int i = 3;
    int j;

    p = &i;  // OK
    j = *p;
    printf("i = %d, j= %d\n", i, j);

    return 0;
}

/*
    在VSCode中的输出结果是:
--------------------------
i = 3, j= 3
--------------------------
*/

说明
对于语句**int * p;**:

  1. p 是变量的名字, int * 表示 p 变量存放的是 int 类型变量的地址。
  2. int * p; 不表示定义了一个名字为 *p 的变量。
  3. int * p; 应该这样理解: p 是变量名,p 变量的数据类型是 int * 类型。所谓 int * 类型,实际就是存放 int 变量地址的类型。

对于语句**p = &i;**:

  1. p 保存了 i 的地址,因此 p 指向 i
  2. p 不是 i, i 也不是 p, 更准确的说,修改 p 的值不影响 i 的值,修改 i 的值,也不影响 p 的值。
  3. 如果一个指针变量指向了某个普通变量,则 *指针变量 就完全等同于普通变量。解释:如果 p 是个指针变量,并且 p 存放普通变量 i 变量的地址,则 p 指向了普通变量 i。*p 就完全等同于 i。或者说:在所有出现 *p 的地方都可以替换成 i,在所有出现 i 的地方都可以替换成 *p*p 就是以 p 的内容为地址的变量。

对于语句**j = *p;**,就相当于j = i;

* 的含义

  1. 乘法。
  2. 定义指针变量。int * p;定义了一个名字叫 p 的变量,int * 表示 p 只能存放 int 变量地址。
  3. 指针运算符。该运算符放在已经定义好的指针前面,如果 p 是一个已经定义好的指针变量,则 *p 表示以 p 的内容为地址的变量。

如何通过被调函数修改主调函数普通变量的值

  1. 实参必须为该普通变量的地址。
  2. 形参必须为指针变量。
  3. 在被调函数中通过 * 形参名 = XXX的方式就可以修改主调函数相关的变量。

例:

/*
    时间:2020年2月24日14:45:35
    功能:
        通过被调函数修改主调函数普通变量的值
    总结:
        1. 实参必须为该普通变量的地址。
        2. 形参必须为指针变量。
        3. 在被调函数中通过 * 形参名 = XXX 的方式就可以修改主调函数相关的变量。
*/

# include <stdio.h>

void huhuan_1(int, int);
void huhuan_2(int *, int *);
void huhuan_3(int *, int *);

// 不能完成互换功能
void huhuan_1(int a, int b)
{
    int t;

    t = a;
    a = b;
    b = t;

    return; 
}

// 不能完成互换功能
void huhuan_2(int * p, int * q)
{
    int * t; // 如果要互换 p 和 q 的值,则 t 必须是 int *, 不能是 int, 否则会出错

    t = p;
    p = q;
    q = t;

    return; 
}

// 可以完成互换功能
void huhuan_3(int * p, int * q)
{
    int t;  // 如果要互换 *p 和 *q 的值,则 t 必须定义成 int 不能定义成 int *, 否则语法错误

    t = *p;  // p 是 int *, *p 是 int
    *p = *q;
    *q = t;

    return; 
}

int main(void)
{
    int a = 3;
    int b = 5;

    // huhuan_1(a, b);
    // huhuan_2(&a, &b);  // huhuan_2(*p, *q); 是错误的, huhuan_2(a, b); 也是错误的
    huhuan_3(&a, &b);
    printf("a = %d, b = %d\n", a, b);

    return 0;
}

/*
    在VSCode中的输出结果是:
--------------------------
a = 5, b = 3
--------------------------
*/

指针和数组

指针和数组分为指针和一维数组以及指针和二维数组。这里主要介绍指针和一维数组。

一维数组名:一维数组名是个指针常量。它存放的是一维数组第一个元素的地址。

下标和指针的关系:如果 p 是个指针变量,则 p[i] 永远等价于 *(p+i)

确定一个一维数组需要几个参数:需要两个参数。数组第一个元素的地址和数组的长度。即:如果一个函数要处理一个一维数组,则需要接收该数组的第一个元素的地址和数组的长度。

例 指针和一维数组

/*
    时间:2020年2月24日16:35:50
    目的:
        确定一个数组需要的参数
    总结:
        如果一个函数要处理一个一维数组,
        则需要接收该数组的第一个元素的地址和数组的长度。
*/

# include <stdio.h>

void f(int * pArr, int len)
{
    int i;

    for (i=0; i<len; ++i)
        printf("%d ", *(pArr+i)); 
            /*
                *(pArr+i) 等价于 pArr[i]
                也等价于 b[i]
                也等价于 *(b+i)
            */
    printf("\n");
}

int main(void)
{
    int a[5] = {1, 2, 3, 4, 5};
    int b[6] = {-1, -2, -3, 4, 5, -6};
    int c[100] = {1, 99, 22, 33};

    f(b, 6);

    return 0;
}

/*
    在VSCode中的输出结果是:
--------------------------
-1 -2 -3 4 5 -6
--------------------------
*/

指针变量的运算

指针变量不能相加,不能相乘,也不能相除。如果两个指针变量指向的是同一块连续空间中的不同存储单元,则这两个指针变量才可以相减。

例 指针的运算

/*
    时间:2020年2月24日16:57:27
    指针的运算
*/

# include <stdio.h>

int main(void)
{
    int i = 5;
    int j = 10;
    int * p = &i;
    int * q = &j;
    int a[5];
    p = &a[1];
    q = &a[4];
    printf("p和q所指向的单元相隔%d个单元\n", q-p);

    // p-q 没有实际意义

    return 0;
}

/*
    在VSCode中的输出结果是:
--------------------------
p和q所指向的单元相隔3个单元
--------------------------
*/

一个指针变量到底占几个字节【非重点】

预备知识sizeof()函数:
用法一:
  sizeof(数据类型)
功能:返回值就是该数据类型所占的字节数。
例:
sizeof(int) = 4
sizeof(char) = 1
sizeof(double) = 8

用法二:
  sizeof(变量名)
功能:返回值是该变量所占的字节数。

假设 p 指向 char 类型变量(1 个字节)。
假设 q 指向 int 类型变量(4 个字节)。
假设 r 指向 double 类型变量(8 个字节)。
p q r 本身所占的字符数是否一样?
答案: p q r 本身所占的字符数是一样的。

总结: 一个指针变量,无论它指向的变量占几个字节,该指针变量本身只占 4 个字节。一个变量的地址是用该变量的首地址表示的。

多级指针

指针的指针就是多级指针了。对于多级指针需要明白, p 是指针变量,如果 qp 的指针,则 *q = p
例 多级指针

/*
    时间:2020年2月24日21:45:22
    多级指针的示例
*/

# include <stdio.h>

int main(void)
{
    int i = 10;
    int * p = &i;
    int ** q = &p;
    int *** r = &q;

    // r = &p;  // error 因为 r 是 int *** 类型,r只能存放int ** 类型的变量的地址
    printf("%d\n", ***r);
    printf("%d\n", **q);
    printf("%d\n", *p);

    return 0;
}

/*
    在VSCode中的输出结果是:
--------------------------
10
10
10
--------------------------
*/

对于指针和函数以及指针和结构体这里先不介绍。

动态内存分配【重点难点】

传统数组的缺点

传统数组也叫静态数组。

  1. 数组长度必须事先指定,且只能是常整数,不能是变量。如:
      int a[5]; // Ok
      int len = 5; int a[len]; // error

  2. 传统形式定义的数组,该数组的内存程序员无法手动释放。数组一旦定义,系统为该数组分配的存储空间就会一直存在,除非数组所在的函数运行结束。在一个函数运行期间,系统为该函数中所分配的空间会一直存在,直到该函数运行完毕时函数的空间才会被系统释放。

  3. 数组的长度一旦定义,其长度就不能再更改。数组的长度不能在函数运行的过程中动态的扩充或缩小。

  4. A 函数定义的数组,在 A 函数运行期间可以被其他函数使用,但 A 函数运行完毕之后,A 函数中的数组将无法被其他函数使用。即:传统定义的函数不能跨函数使用

为什么需要动态分配内存

动态数组的创造就是为了解决静态数组的 4 个缺陷。

静态内存 VS 动态内存的比较

区别 静态内存 动态内存
内存分配 系统自动分配 程序员手动分配
内存释放 系统自动释放 程序员手动释放
内存分配位置 中分配 中分配

动态分配内存举例——动态数组的构造

假设需要动态构造一个 int 型一维数组。
  int * p = (int *)malloc(int len);

  1. 本语句一共分配了两块内存,一块是动态分配的,总共 len 个字节,一个是静态分配的是 4 个字节,即变量 p 本身所占的内存。

  2. malloc() 只有一个 int 型的形参,表示要求系统分配的字节数。

  3. malloc() 函数的功能是请求系统分配 len 个字节的内存空间,如果请求分配成功,则返回第一个字节的地址,如果分配不成功,则返回 NULL

malloc() 函数只能返回第一个字节的地址,所以我们需要把这个无任何实际意义的第一个字节的地址(俗称干地址)转化成一个有实际意义的地址,因此 malloc 前面必须加 (数据类型 *),表示把这个无实际意义的第一个字节的地址转化为相应类型的地址。

如:
int * p = (int *)malloc(5);
表示将系统分配好的50个字节的第一个字节的地址转化为 int * 型的地址,更准确的说是把第一个字节的地址转化为 4 个字节的地址,这样 p 就指向了第一个的 4 个字节, p+1 就指向了第二个的 4 个字节,p+i 就指向了第 i+1 个的 4 个字节,p[0] 就是第一个元素, p[i] 就是第 i+1 个元素。

double * p = (double *)malloc(80);
表示将系统分配好的80个字节的第一个字节的地址转化为 double * 型的地址,更准确的说是把第一个字节的地址转化为 8 字节的地址,这样 p 就指向了第一个的 8 个字节, p+1 就指向了第二个的 8 个字节,p+i 就指向了第 i+1 个的 8 个字节,p[0] 就是第一个元素, p[i] 就是第 i+1 个元素。

freep(p);
表示将 p 所指向的内存给释放掉,p 本身的内存是静态的。不能由程序员手动释放,p 本身的内存只能在 p 变量所在的函数运行终止时由系统自动释放。

例 动态数组的构造

/*
    时间:2020年2月26日21:15:46
    动态数组的构造
*/

# include <stdio.h>
# include <malloc.h>

int main(void)
{
    int a[5];  // 如果int 占4个字节的话,则本数组总共包含有20个字节,每四个字节被当做了一个int变量来使用
    int len;
    int * pArr;
    int i;

    printf("请输入你要存放元素的个数:");
    scanf("%d", &len);  //
    pArr = (int *)malloc(4 * len);  // 本行动态的构造了一个一维数组,该数组的长度是len,该数组的数组名是pArr,该数组的每个元素是int 整型。类似于 int pArr[len];
   
    // 对一维数组进行操作, 如:对一维数组进行赋值
    for (i=0; i<len; ++i)
        scanf("%d", &pArr[i]);
   
    // 对一维数组进行输出
    printf("一维数组的内容是:\n");
    for (i=0; i<len; ++i)
        printf("%d\n", pArr[i]);

    free(pArr);  // 释放掉动态分配的数组
   
    return 0;
}

跨函数使用内存的问题

静态内存不可以跨函数使用。或者说是:静态内存在函数执行期间可以被其他函数使用,但是在函数执行完毕后就不能再被其他函数使用了。

动态内存可以跨函数使用。动态内存在函数执行完毕之后仍然可以被其他函数使用。

说明

  1. 本学习笔记整理自B站郝斌老师的《郝斌C语言自学教程》片段P121-P150

  2. 笔记中所有代码均在windows10操作系统,在VSCode编辑器中通过测试。具体VSCode C语言开发环境搭建方法请参照我的另一篇CSDN博客——Windows10下利用Visual Studio Code搭建C语言开发环境

后记

如果对你有所帮助,欢迎关注我的公众号。这个公众号主要是慢慢分享和记录自己学习编程的笔记,比如:C,Python,Java等。后续也会分享自己面试以及在职场上的成长心得。

在这里插入图片描述

发布了128 篇原创文章 · 获赞 157 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/qq_27283619/article/details/104534180