zhyDaDa的个人站点

* 对应课程: \~
目录
指针
指针的定义
重要的小细节
作为参数的指针
取地址
取地址运算符
无法取地址的情况
有趣试验
相邻的变量
数组的地址
二维数组的地址
应用
应用一_交换变量
应用二_多个返回值
应用三_运算状态
常见误区
指针与数组
参数表中的数组
数组变量是特殊的指针
指针与const
指针是const
所指是const
小练习
转换
const数组
保护数组值
指针的运算
加一的实质
可能参与的计算
连招
0地址
指针的类型
未知类型
指针的类型转换
动态内存分配
接收数据
没空间的情况
算算被分配多少空间
free的注意事项


指针

指针就是保存地址的变量

指针的定义

int i;
int* p = &i;

上面这段的含义是:
+ i的地址交给了p
+ p获得的值是i的地址
+ p指向了i

这几种说法都正确

重要的小细节

int* p,q;  
int *p,q;

这里的*是附着于p身上的, 与ta靠近的位置无关
注意: 不存在int*这种变量类型!
正确的说法是: *p是一个int 类型的变量, 于是p是一个 指针

作为参数的指针

void f(int *p)
通过传递指针, 函数得到了参数的地址
这使得函数内部拥有了访问外部的能力

所谓 访问 就是可以对变量进行读写

满分解释: 把你家的门牌号扒下来给物管

取地址

取地址运算符

&取地址运算符, 也是一个运算符
作用是取得变量的地址, 其后必须是一个变量
取得的地址输出时要使用%p
地址的(数据)大小与int是否相同取决于编译器
地址与整数不一定是相同的

无法取地址的情况

int p = (int)(i++); 会报出error
因为, &后面 必须是明确的一个变量

有趣试验

相邻的变量

int main()
{
    int i=0;
    int p = (int)&i;
    printf("%p\n", &i);
    printf("%p\n", &p);
    return 0;
}

输出结果为:

000000000061FE1C
000000000061FE18

在内存中, 先定义的变量在更高的地方
在C语言的内存模型中
本地变量被分配在一个被称为 堆栈(stack) 的地方
堆栈会自上而下分配变量

在这个例子中, 两个 int 是紧挨着定义的
因此相差4

数组的地址

int main()
{
    int a[10]={0};

    printf("%p\n", a);
    printf("%p\n", &a);
    printf("%p\n", &a[0]);
    printf("%p\n", &a[1]);

    return 0;
}

结果是:

000000000061FDF0
000000000061FDF0
000000000061FDF0
000000000061FDF4

数组在堆栈中从下而上分配
数组的名字就是一个地址
其与第0个元素的地址一致

二维数组的地址

    int a[10][3];

    printf("a: %p\n", a);
    printf("&a: %p\n", &a);
    printf("&a[0]: %p\n", &a[0]);
    printf("&a[1]: %p\n", &a[1]);
    printf("&a[0][0]: %p\n", &a[0][0]);
    printf("&a[1][0]: %p\n", &a[1][0]);
    printf("&a[1][2]: %p\n", &a[1][2]);

其结果:

a: 000000000061FDA0
&a: 000000000061FDA0
&a[0]: 000000000061FDA0
&a[1]: 000000000061FDAC
&a[0][0]: 000000000061FDA0
&a[1][0]: 000000000061FDAC
&a[1][2]: 000000000061FDB4

为了让结果更具可读性, 稍微改了一下

    int a[10][3];

    printf("a: %d\n", (int)(a) - (int)a);
    printf("&a: %d\n", (int)(&a) - (int)a);
    printf("&a[0]: %d\n", (int)(&a[0]) - (int)a);
    printf("&a[1]: %d\n", (int)(&a[1]) - (int)a);
    printf("a[0]: %d\n", (int)(a[0]) - (int)a);
    printf("a[1]: %d\n", (int)(a[1]) - (int)a);
    printf("&a[0][0]: %d\n", (int)(&a[0][0]) - (int)a);
    printf("&a[0][1]: %d\n", (int)(&a[0][1]) - (int)a);
    printf("&a[0][2]: %d\n", (int)(&a[0][2]) - (int)a);
    printf("&a[1][0]: %d\n", (int)(&a[1][0]) - (int)a);
    printf("&a[1][1]: %d\n", (int)(&a[1][1]) - (int)a);
    printf("&a[1][2]: %d\n", (int)(&a[1][2]) - (int)a);

结果是:

a: 0
&a: 0
&a[0]: 0
&a[1]: 12
a[0]: 0
a[1]: 12
&a[0][0]: 0
&a[0][1]: 4
&a[0][2]: 8
&a[1][0]: 12
&a[1][1]: 16
&a[1][2]: 20

总结得出:
+ 数组名称对应着a[0][0]的地址
+ 往后的所有元素都越来越大(在堆栈中向上堆叠)
+ 每一行的最后一个元素与下一行的第一个元素紧挨着

应用

应用一_交换变量

void swap(int *pa, int *pb);

int main()
{
    int a = 4, b = 6;
    swap(&a, &b);
    printf("a=%d, b=%d\n", a, b);

    return 0;
}

void swap(int *pa, int *pb)
{
    int t = *pa;
    *pa = *pb;
    *pb = t;
}

遇到swap这种对外部参数的值操作的函数, 只能使用指针

应用二_多个返回值

传入的参数实际上是一些容器
函数实际上在将多个返回值装入这个容器
这一过程必定需要使用指针

void MinMax(int a[], int len, int *min, int *max);

int main()
{
    int a[] = {10, 99, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    int min,max;
    MinMax(a, sizeof(a) / sizeof(a[0]), &min, &max);
    printf("min=%d; max=%d\n", min, max);

    return 0;
}

void MinMax(int a[], int len, int *min, int *max){
    *min = *max = a[0];
    for (int i = 0; i < len; i++)
    {
        if(a[i]<*min){
            *min = a[i];
        }
        if(a[i]>*max){
            *max = a[i];
        }
    }    
}

在这一类场景中, minmax虽然是main函数传入的参数
但他们的实际作用, 是把返回值带出来

应用三_运算状态

函数操作会有成功与否, 一般(如scanf)会返回0表示成功, -1表示失败
如果函数运算的结果也可能是0-1那么就无法判断函数是否成功执行
因此, 需要分开返回 运算状态 和 运算的实际结果
一般来说, 函数的返回值是运算状态, 用另一个指针参数带出实际结果

int divide(int a, int b, int *c);

int main()
{
    int a = 5, b = 2, c;
    if (divide(a, b, &c))
    {
        printf("%d/%d=%d\n", a, b, c);
    }
    return 0;
}

int divide(int a, int b, int *c)
{
    int ret = 1;
    if (b == 0)
    {
        ret = 0;
    }
    else
    {
        *c = a / b;
    }
    return ret;
}

常见误区

int *p=12;
这条语句拆开来看是这样的:

int *p;
*p = 12;

注意: 这类指针俗称 “野生指针”
因为指针没有明确的指向
此时p可能指向内存中的任意位置
此时如果正好指向不可写入的一块区域
那么程序立刻崩溃

指针与数组

参数表中的数组

在函数的参数表中, 传入的数组实质上是指针
以上文应用二_多个返回值中的函数来分析
void MinMax(int a[], int len, int *min, int *max)
这里可以改写成:
void MinMax(int *a, int len, int *min, int *max)
对于这种数组指针, 仍旧可以使用数组运算符[]来参与运算

一定注意, 所谓

*aa[]等价

这只是在函数原型的参数表中的说法

数组变量是特殊的指针

  • 数组变量本身表达地址, 无需对其取地址
    int a[10]; int *p=a;
  • 但是, 对于数组的单个元素, 都表达确切的变量, 需要用&取地址
    a==&a[0]
  • []运算符亦可以对指针使用
    p[0]a[0]完全等价
  • *运算符显然也可以对数组使用
    *(a+2) = 25

p[2]的含义是: 将p视作数组的第一个元素的地址, 取其第3个单元
int a[]int * const a=... 等价
这里的const常量的意思, 即数组变量这种特殊的指针一旦被定义就无法再改变其指向的对象了

指针与const

const 是说值不能被修改
指针本身有可能const , 指针所指的变量也可能const

指针是const

这意味着该指针一旦得到某个变量的地址, 不能再指向其他变量
注意: 指针所指向的值仍旧可以修改, 因为值不是 const

int i = 0;
int * const q = &i;
*q = 26; //OK
q++;     //ERROR

所指是const

const int *p = &i;
这行代码意味着:
+ i可以做修改
+ p可以做修改
+ 唯独不可以通过*p去修改i

小练习

int i;
const int* p1 = &i;
int const* p2 = &i;
int *const p3 = &i;

答案是:
+ p1: 所指是const
+ p2: 所指是const
+ p3: 指针是const

判断的技巧: const 和 * 的位置
const在前就是所指不能修改
const在后就是指针不能修改

转换

总是可以将非 const 的值转化为 const
void f(const int* x);
可以照常将一个普通的地址传入, f函数将向你保证:
f中, 不会对这个地址对应的值做修改

事实上, 这是一个常用的套路
由于传递的值会很大, 自然会用传地址来代替(数组就是这个想法)
为了避免函数修改数值, 就用const来做保证

const数组

const int a[]={1,2,3,4,5,6};
数组变量在声明时就是一个const指针
如果再加上const, 就表明数组中每个单元全都是const
所以, 初始化时 必须赋值

保护数组值

为了避免函数修改数组内的值, 可以在函数声明中设置参数const

指针的运算

加一的实质

int *p1;
char *p2;

打印他们和+1之后的16进制值, 比较其差值:
+ p1+1p1数值上差4
+ p2+1p2数值上差1

对于指针来说, +1的实际含义是 指向下一个单元
因此, 从数值上来看, 实际给指针加上的 是数据类型的size大小

如果+1是字面上的含义, 那么指针移动到代表一个int的四个字节的中间
这样做也是没有任何意义的
同样, 如果指针指向的不是一片连续分配的空间(如数组),那么这种运算也没有意义

可能参与的计算

  • 加减法
  • 加加和减减
  • 两个指针相减
  • 两个指针比较

这里还需注意: 指针相减的意义是两个指针间有几个单元
因此, 对于int指针, 数值上相差24, 但结果是6

数组中, 单元的地址必定是 线性递增的

连招

*p++
用意是:
+ 取出*p, 即指针指向的内容
+ 顺便指针向后移动

*的优先级没有++

有些CPU上, 这个连招会被直接翻译成一条指令 (跑得更快)

0地址

每个程序跑起来的时候, 系统都会赋予ta一块 “虚拟的” 内存空间
因此, 每个程序都有所谓的 0地址

但这个地址不能, 不仅不能更改, 有时还不允许读取
操作ta可能会导致程序崩溃
因此, 0地址被赋予了特殊含义:
+ 指向0的指针可以看做是无效的指针(函数返回这样的指针就可能意味着操作不能执行)
+ 给指针初始化的时候可以先初始化为0, 表示ta还尚未被使用

有个特殊的字面量叫NULL, 就表示0地址
部分编译器仅支持NULL, 所以最好使用ta来表示0地址

指针的类型

指针的类型和其指向的数据类型一致
无论什么类型的指针, 他们都是存放地址的, 因此所有指针大小一致
考虑到上文分析的指针运算的意义, 不同类型的指针之间不可以赋值(主要为了避免用错指针)

未知类型

void *这样的类型表示指向的变量类型未知(或者说不用关心)
其在计算方式上和char *相同
但要注意, 这两个类型的指针仍旧不相通

指针的类型转换

int *p = &i;
void *q = (void *)p;

指针可以转换类型
这种转换并不改变其指向的变量的类型(这个例子里i仍旧是int)
这么做仅仅改变了我们透过p看待i的眼光

动态内存分配

接收数据

数据接收前需要知道数据的个数, 这是为了在内存中开辟相应的空间

在C99之前, 需要手动开辟空间
int *a=(int*)malloc(n*sizeof(int));

这里malloc会返回void *
意思就是开辟了一块空的没有意义的空间, 然后把地址告诉你

而且在程序的最后, 还要加上一句
free(a)
表示把用好的空间还回去

没空间的情况

申请空间(malloc)失败会得到返回值NULL

算算被分配多少空间

#include <stdio.h>
#include <stdlib.h>

int main()
{
    void *p;
    int count = 0;
    while ((p = malloc(100 * 1024 * 1024)))
    {
        count++;
    }
    printf("被分配了%d00MB的空间", count);

    return 0;
}

在笔者的电脑上的结果是:
被分配了43500MB的空间

free的注意事项

void *p;
int count = 0;
(p = malloc(100 * 1024 * 1024));
p = &count;
free(p);

上述代码运行结果就是程序崩溃
一下情况会崩溃:
+ 借了不还
+ 还回去的不是借来的地址
– 例如p+1
– 例如(p=&count)
+ free 了之后再 free

对于小程序, 程序结束之后会直接释放所有内存
但对于服务器/大程序, 长时间 申请不free 会导致内存逐渐下降, 这会造成严重的后果
注意: 唯独free(NULL)不会报错

很可能是为了让我们养成 对指针初始化就用NULL的习惯

Avatar photo
我是 zhyDaDa

前端/UI/交互/独立游戏/JPOP/电吉他/游戏配乐/网球/纸牌魔术

发表回复