爬爬爬之路:C语言(八) 指针与指针数组

简单介绍一下C/C++中数据在内存的保存模式
在c/c++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。

栈:就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。
堆:就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
自由存储区:就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
全局存储区(静态存储区):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。



在计算机内存中, 字节是最小的存储单位, 一个字节称为一个存储单元(内存单元), 不同类型数据所占用的存储单元不等
一个字节 = 8个二进制位
这是一个储存单元在计算机里的模样, 一共8个二进制位, 每个空格的取值只有0, 1
        

每个内存单元都有一个编号. 内存单元的编号称为地址

这里要区别一下我们看到的变量和计算机看到的变量的概念

比如说

int a = 10;

在我们可以这么认为

a

10
格子代表变量a的地址空间, 空间里放的是变量a的数据, 这个空间的名字就叫a   我们可以通过 a = 15,  printf("%d", a); 等语句直接获得或操作变量a 空间里的值
这种直接通过变量名来获取或者改变值的方式, 叫做直接访问.


但是我们直到计算机内存是一个只有数字0 和1的世界.  怎样0和1组成的二进制数来表示10这个数据呢

事实上在我们声明一个变量的时候, 系统会自动在内存里开辟一个和变量相对应大小空间

比如上面的代码   在声明一个int型变量的时候系统会开辟 4个字节大小的空间, 空间里的值是10  换算成二进制数就是 1010, 其余高位补零

a

00000000
00000000
00000000
00001010
这一整个区域就是变量a在内存中的样子,  它的物理地址是一串16进制的常数.  而这个地址是随机分配的, 通常是编译器在内存中搜索, 哪一块地址是空的 可以用的的地址, 就会把数据写进去, 分配在哪是编译器自己决定的, 不需要我们去纠结这个问题.   而通常我们用 变量a 就可以直接找到并访问它, 通常情况下可以不必太在意这个地址具体是多少. 但是一定要弄清楚这个物理地址是在计算机内可见的, 并且在某些时候是十分重要的.

这里我们要弄清楚变量名和物理地址的关系, 事实上, 变量名是高级语言中定义的一种东西, 它的作用就是为了方便的寻找地址.在编译了以后,它就变成了它对应的物理地址.所以事实上, 我们调用变量名, 就等于调用了这个变量的物理地址. 

然而这个物理地址我们还是可以获取到的. 通过 取地址符 & 可以直接访问到变量a的物理地址. 这个地址符, 我们大家都不陌生, 在格式输入函数scanf("%d", &a); 中常用到, 至于为什么要用& 呢,  下文中将会提.



指针变量

什么是指针, 指针就是地址.

那么什么是指针变量, 显而易见, 就是保存值是地址的变量就叫做地址变量. 指针变量占8个字节, 它所保存的内容是一个物理地址的值, 也就是一串16进制的常数.

指针的声明方法如下

int* p = NULL;  此时的* 是无意义的, 它仅仅告诉系统变量p是一个指针变量.

为什么要有指针变量这种东西呢?

接下来谈谈我浅薄的理解.

在C语言中, 变量是常量, 是不可改变. 而指针变量的正是为了解决常量不可变的问题, 通过另外一种间接的方式, 将地址这个变成变量的值, 通过改变值 来回避常量不可变的问题.

听着有点绕, 看以下分析.

在C语言中, 变量之间的赋值过程 是拷贝值的过程

比如

int a = 5;
int b = a;
a = 10;
printf("a=%d, b=%d", a, b);
先将5赋值给变量a,

把变量a的值赋值给变量b

再将10赋值给变量a

打印结果发现a=10, b=5.

这里大家思考一下, 变量b的值, 是来源于变量a的, 为什么a的值变了, b的值没有改变呢.

这里其实就是一个拷贝的过程.

int a = 5;

开辟了一个int型所占大小的空间

a:

5

int b = a;

开辟了另一个int型所占大小的空间, 它的值和a的值一样. 所以把a的值再写到b的空间里

b:

5
这时候再改变a里的值, 结果b的空间不会跟着改变. 这里就发现, a和b是两个分别独立的空间, 他们之间的值只有在b = a的时候发生了一次关联, 其他时候是互不影响的.这就是拷贝的过程.只发生了一次.


同样的道理, 实参和形参之间也是拷贝的关系.

注意到函数的写法.

void exChange(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}
大家注意到, 在形参列表里, 有一个变量声明的过程  int a, int b

这两句的作用是在内存中开辟了两个int型所占大小的空间, 这两个空间名分别是a, b.他们的值, 是通过函数调用时, 实参的值通过拷贝传过来的.
这意味着, 实参和形参是分开在两个地方的不同空间, 他们之间的关系只有值相同.注意这一句话.

这会导致什么问题呢?

大家都知道变量的作用域, 形参的作用域就是在函数里, 函数里的代码走完跳出去了以后, 形参的空间就被系统回收了, 也就是说形参里的值, 在函数里的计算结果就消亡不在了.

比如上面这个函数, 它的作用从代码上看, 是为了交换两个参数内的值.

但是当我们在main函数中调用这个函数的时候, 大家都知道结果是交换失败了.

为什么呢?

假设在主函数中的调用语句如下:

int x = 5, y = 6;
exChange(x, y);
printf("x=%d,y=%d",x,y);

为了避免混淆和理解困难,这里我们在主函数中声明了两个int型变量, x和y(和形参的名字不同)

于是系统给这两个变量开辟了两个空间. 分别存放了5 和 6

现在我们来理一下内存中空间开辟的顺序关系:
int x = 5, y = 6;这句语句运行结束后

系统出现了两个空间

x空间 

5
和y空间
6

程序向下运行调用函数exChange()

这时候又为形参a, b开辟了两个空间

a

5
b

6

a 和 b的值为实参x, y拷贝而来

函数执行完毕后

a

6
b

5
形参a, b的值完成了交换.

但是函数调用结束后, a, b的空间被回收了

这时候实参的空间x, y里的值依然没有变化.

很头疼啊. 怎么样才能让实参的元素完成交换呢?



我们要注意到一点, 实参的值未能完成变化的本质原因是, 形参完成了交换功能后, 它们的空间被回收了. 实参的空间除了在拷贝值的时候被访问了一次以外, 再也没有重新赋值.

这时候在主函数里访问实参x, y的空间, 发现里面的值当然也没有发生变化.

怎么样让函数运行完以后, 实参的值也跟着变化呢?

分析本质原因,  其实我们只要在函数中, 改变实参地址里的值, 就可以达到交换数据的目的.

而要改变实参地址里的值, 当然首要的解决目标就是把 实参的地址告诉形参,  让形参在实参的地址里放肆的大概特改.

而通过指针变量就可以达到这一点

将函数改写成

void exChange(int *p, int *q) {
    int temp = *p;
    *p = *q;
    *q = temp;
}
而调用方法是将实参的地址当成变量告诉实参

int x = 5;
int y = 6;
exChange(&x, &y);
这样就可以让形参直接访问到实参的地址而不是再另外一个会被回收的地址里做无意义的改动.

回来谈scanf()函数的问题.

通过以上的分析, 我们可以得到一个结论

要想改变一个值, 并且让这个值再另一个地方尤其是函数外可以看到并且更改, 只能在这个变量所在的地址里更改. 这也是为什么scanf()函数需要访问变量的地址的原因. 因为只有访问这个变量的地址, 并赋值, 才能在scanf函数外得到这个函数的值.


像指针变量这样, 通过访问变量内保存的地址, 再访问该地址对应的值的方法, 叫做间接访问. 而真是由于变量内的地址可以改变(也叫做重指向), 使得指针具有极强的灵活性.

事实上, 在我认为, 指针的存在, 就是为了解决C语言中地址是常量, 且不可更改而采取的绕道解决的策略. 不得不承认它是一个很强大的概念. 但是真的很绕, 初上手常常让人摸不着头脑, 只能多写多练.

以上是比人对指针浅薄里理解.



指针变量的赋值

指针变量的赋值, 相当于指针的重指向

    int num1 = 50;
    int num2 = 40;
    int *p1 = &num1;
    int *p2 = &num2;
    p2 = p1;
    *p2 = 100;
    printf("%d ", num1);
    printf("%d ", num2);
    printf("%d ", *p1);
    printf("%d ", *p2);
结果为100  50  100  100

原因是, p1一开始是指向 num1的地址, p2指向num2的地址

p2 = p1语句将p2重指向p1所指向的地址, 也就是num1的地址

这是  p1, p2 均指向num1的地址

*p2 = 100; 将num1里的值改为100 此时 *p1 num1 均一起改变变成100


接下去 来讨论一下指针变量的类型

首先弄清楚一点 指针变量保存的是地址 无论它的类型是什么, 它的大小都是8个字节

它的类型, 事实上是告诉系统它所指向的地址, 是属于什么类型的区域

这和指针算术运算是息息相关的

    int a = 3;
    int *p = &a;
    p++;
    printf("%d\n", a);
    printf("%p\n", &a);
    printf("%p\n", p);
发现p的值 比a的地址加了4

    char a = 'a';
    char *p = &a;
    p++;
    printf("%c\n", a);
    printf("%p\n", &a);
    printf("%p\n", p);
发现p的值 比a的地址加了1


得出结论, 地址加1  要看指针变量的数据类型.
如果是int型, 地址加1, 相当于地址加了4个字节
如果是char型, 地址加1, 相当于地址加了1个字节

即, 地址加1, 相当于加上一个相应数据类型所占地址长度


注意:

系统在分配内存时, 不一定分配的是连续的
只有在给数组分配空间的时候, 各元素之间肯定连续的


指针与数组

int array[4] = {1, 3, 5, 7};

数组名array 代表了 &array[0]

由于数组各元素之间的地址是连续的

只要获得数组的首元素地址, 和数组长度(或者数组的数据类型) 就可以地址联系的特性, 用指针把数组所有的元素打印出来

    int *p = array;   // 指针变量p保存的是数组首元素的地址
    printf("%d\n", *p);
    printf("%p\n", p + 1);
    printf(“%d\n", *(p + 1));
    printf("%d\n", *(array + 2));


取出数组中的元素方法: 
     1. array[下标]
     2. *(array + 下标)
     3. *(p + 下标)  (事先定义好p = array)
     4. p[下标]
在此p和array看似相同, 但是还是有不同的地方.  比如sizeof(p) == 8,   sizeof(array) == 16
通常用非指针方法取元素的时候, 用方法1,  用指针方法取元素的时候, 用方法3 两者较为常用

计算元素个数的方法
数组中总元素占的字节数 / 数组中单个元素的字节数 = 数组元素个数
sizeof(array) / sizeof(array[0]) = num

在取元素的时候得注意一下 * 的优先级   *(p + 1) 和 *p + 1 结果截然不同

前者是取p下个地址的值, 后者是p地址的值 加上11


当数组元素类型和指针类型不匹配时会怎样
short a[4] = {3, 7, 9, 1};
int *p1 = a;
char *p2 = a;


将数组a在内存中表示出来
由于是char类型的数组 每个元素在内存中所占空间为2个字节
&a[3]    a[3] = 1
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 1
&a[2]    a[2] = 9
0 0 0 0 0 0 0 0
0 0 0 0 1 0 0 1

&a[1]    a[1] = 7
0 0 0 0 0 0 0 0
0 0 0 0 0 1 1 1

&a[0]    a[0] = 3
0 0 0 0 0 0 0 0
0 0 0 0 0 0 1 1


而int型指针指向的区域为int型数据类型保存区域大小的空间(4个字节),  所以实际上*p1所指向的空间是这样的:

p + 1   *(p + 1) = 65545
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 1
0 0 0 0 0 0 0 0
0 0 0 0 1 0 0 1


p    *p = 458755
0 0 0 0 0 0 0 0
0 0 0 0 0 1 1 1
0 0 0 0 0 0 0 0
0 0 0 0 0 0 1 1



char 类型指针指向的区域大小为char类型保存区域大小的空间(1个字节):

p2 + 7        *(p2 + 7) = 0
0 0 0 0 0 0 0 0

p2 + 6        *(p2 + 6) = 1
0 0 0 0 0 0 0 1

p2 + 5        *(p2 + 5) = 0
0 0 0 0 0 0 0 0

p2 + 4        *(p2 + 4) = 9
0 0 0 0 1 0 0 1

p2 + 3         *(p2 + 3) = 0
0 0 0 0 0 0 0 0

p2 + 2         *(p2 + 2) = 7
0 0 0 0 0 1 1 1

p2 + 1         *(p2 + 1) = 0 
0 0 0 0 0 0 0 0

p2               *p2 = 3
0 0 0 0 0 0 1 1

会发现 由于指针的增减所指向的区域是根据指针自身的类型来变化的   若是指针和数组的类型不匹配会导致数据错误




指针与字符数组

    char str1[] = "iphone";
    char *p = str1;
    printf("%s\n", p);
    
    printf("%c\n", *(p + 2));
    
    *(p + 2) = 'W';
    printf("%s", p);


// 计算字符串长度 方法一
    int num = 0;
    while (*(p + num) != '\0') {
        num++;
    }
    printf("长度为:%d", num);


// 方法二:
    int num = 0;
    while (*p != '\0') {
        num++;
        p++;
    }
    printf("长度为:%d", num);

使用方法二的时候  必须注意指针变量p的值已经发生了变化
若是之后的代码还需要用到p, 建议先把p重新赋值为原来的值



指针数组

存放指针的数组称为指针数组
数组当中保存的都是同一类型的数据, 指针数组中保存的都是指针类型 保存的都是地址
    char *string[3] = {"iOS", "Android", "WinPhone"};
    char **p = string;
    for (int i = 0; i < 3; i++) {
        printf("%s\n", *(p + i));
    }

    char *strings[3] = {"iOS", "Android", "WinPhone"};
    printf("%s\n", strings[0]);
    printf("%s\n", *strings);
    printf("%s\n", *(strings + 1));



指针数组的应用:

在主函数中输入6个字符串(二维数组),对他们按从小到大的顺序,然后输出这6个已经排好序的字符串。要求使用指针数组进行处理。

void sortStrings(char *a[], int count) { // 传入的数据是指针数组, count为指针数组的大小
    for (int i = 0; i < count - 1; i++) {
        for (int j = 0; j < count - 1 - i; j++) {
            int num = strcmp(a[j], a[j + 1]);
            if (num > 0) {   // 若是后者比前者大, 两者的指针交换
                char *temp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = temp;
            }
        }
    }
}

    char string[6][50];
    char *strings[6];
    for (int i = 0; i < 6; i++) {
        strings[i] = string[i];
    }
    for (int i = 0; i < 6; i++) {
        printf("请输入第%d个字符串:\n", i + 1);
        scanf("%s", string[i]);
    }
    printf("排序前:\n");
    for (int i = 0; i < 6; i++) {
        printf("%s\n",strings[i]);
    }
    sortStrings(strings, 6); // 将指针数组当成参数传给函数
    printf("排序后:\n");
    for (int i = 0; i < 6; i++) {
        printf("%s\n",strings[i]);
    }


因为时间的和篇幅的关系, 本篇博客对指针数据的介绍并不详尽, 过段时间若有时间将会再详细整理一份. 

指针对于初入门的C语言的朋友而言, 会是一道难关, 但是确实一个很好理解计算机运作的一个关节. 虽然在C语言后来的许多优秀语言中, 指针的概念被弱化了非常多, 也有了更多很好处理内存地址的方法. 但是指针还是一个很好的衡量一个程序员的标准. 不得不承认, 指针是在地址常量不可改的前提下一个极其优秀的处理策略, 望大家能够从本博中获得到那么一点有帮助的信息, 祝大家学习愉快.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值