C语言之漫谈指针(上)

 

在C语言学习的途中,我们永远有一个绕不了的坑,那就是——指针。

在这篇文章中我们就谈一谈指针的一些基础知识。

 

纲要:

  • 零.谈指针之前的小知识
  • 一.指针与指针变量
  • 二.指针变量的类型
  • 三.指针的解引用
  • 四.野指针
  • 五.指针运算
  • 六.指针与数组
  • 七.二级指针
  • 八.指针数组
  • 九.相关例题 

零.谈指针之前的小知识


在谈指针之前我们先来说一说  计算机的储存器.

 

我们在码代码时, 每当声明一个变量,计算机都会在存储器中某个地方为它创建空间。

如果在函数(例如main()函数)中声明变量,计算机会把它保存在一个叫栈(Stack)的存储器区段中;

如果你在函数以外的地方声明变量,计算机则会把它保存在存储器的全局量段(Globals)。

 

程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些

   存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有

   限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似

      于链表。
  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。

  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

 

 

如下图:

 

 

 当然了,这张图对于一些初学者并不是很友好。但是接下来一张图就友好了很多。

 

 那么,我们如何知道我们的变量储存在哪?

这时我们就需要用到 & 操作符了。

 

这时可能有人问了,我这两次 x 的地址为什么不相同,这是因为:

我们每一次程序开始运行的时候,系统都会给我们的变量重新分配地址,

程序结束的时候销毁地址,再次从头开始运行时再次重新分配,结束时再次销毁。

 

有了一定的知识之后,我们便开始正文。

 

一.指针以及指针变量。

1.指针

  在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向
(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以
说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址
的内存单元。

  对于上面的概念我们再简练一些便可以概括为:

  指针 :内存单元的地址(编号)。如上例中 x 的地址 (指针) 为 0x00D3FD34,

2.指针变量

   指针变量:存储地址 (指针) 的变量。

3.指针和指针变量的关系

  1、指针就是地址,地址就是指针。

  2、地址就是内存单元的编号。

  3、指针变量就是存放内存地址的变量。

    指针变量的值就是变量的地址。指针与指针变量的区别,就是变量值与变量的区别。

  4、为表示指针变量和它指向的变量之间的关系,用指针运算符"*"表示。如:

//分别定义了 int、float、char 类型的指针变量int *x = 1;float *f = 1.3;char *ch = 'z';

     要注意的一点就是 此时 x、y、z 就是指针变量的名字,而不是 *x、*y、*z。

如下:

#include <stdio.h>int main(){int a = 10;  //在内存中开辟一块空间来放制aint* p = &a;//这里我们对变量a,取出它的地址,可以使用 & 操作符。            //将a的地址存放在p变量中,p就是一个之指针变量。return 0;}

4.指针变量的大小

指针变量所占空间的大小和该指针变量指向的数据类型没有任何直接关系,而是跟其所在地址的所占空间的大小有关。

同一编译器下,同一编译选项下所有类型的指针变量大小都是一样的,

指针变量的大小是编译器所对应的系统环境所决定的。

如:

  指针变量的大小在16位平台是2个字节,在32位平台是4个字节,在64位平台是8个字节。

 

二.指针变量的类型

提前声明一下:为了方便讲述后面的知识,下文将以指针代替指针变量!

我们先来看以下代码:

    int a = 1;    p = &a;

现在我们都知道了 p 是一个存储着a的地址的指针变量。

可是它的类型是什么呢?

 这时,我们就需要看它所存储的地址中的变量是什么类型了。

    --->  *  --->  *  --->  *   --->  * --->  *   --->  *

我们可以看到:

指针的定义方式是: type + * 。

其实: char* 类型的指针是为了存放 char 类型变量的地址。

    short* 类型的指针是为了存放 short 类型变量的地址。

       int* 类型的指针是为了存放int 类型变量的地址。

 


那么指针类型又意味着什么呢?

我们来看看下述代码:

#include <stdio.h>int main(){int n = 10;char* pc = (char*)&n;int* pi = &n;    printf("%p\n", &n);    printf("%p\n", pc);    printf("%p\n", pc + 1);    printf("%p\n", pi);    printf("%p\n", pi + 1);return 0;}

 

 

 我们可以发现:

  int* + 1 向后跳过了4个字节---恰好为一个int 型的大小

  char* + 1 向后跳过了1个字节---恰好为一个char 型的大小


而这,是不是巧合呢——答案当然是否定的。

所以,我们可以得到一个结论:

  针的类型决定了指针向前或者向后走一步有多大(距离)。

 

三.指针的解引用

1.指针的解引用

现在我们有了变量存储的地址,可是我们要怎么样用到它呢?

这时就需要指针的解引用操作符了—" * "

int main(){int a = 1;int* b = &a;    printf("%d", a);    printf("%d", *b); //*p 就相当于把p所指向的空间的内容拿出来return 0;}

我们会发现其结果都相同。

我们也可以利用解引用的方式来改变一个变量的值:

int main(){int a = 1;int* b = &a;    printf("改变之前: %d\n", a);*b = 2;    printf("改变之后: %d\n", a);return 0;}

 

 

 如果上面图中的语言有点抽象,那我们可以举一个形象的例子:

  有一个人叫张三,有一天他在XX宾馆中定了一间房,且房子的门号为100,到这天晚上的时候,他觉得有点寂寞,

于是打电话喊了他好朋友小刘来找他玩,张三在描述地址时是这样说的:我在XX宾馆100号房间……但小刘那时有事,所以等到小刘来宾馆的时候

已经是第二天中午了,可时张三在第二天早上就退了房,且李四又住了进来,所以当小刘打开宾馆的100室见到的还会是张三吗?肯定不会了

 

住在100室的人--------a

100室------&a、b

张三-------1

李四--------2

不知大家这回理解了没有

 

2.指针的类型与解引用的关系

我们来看看这一个例子:

#include <stdio.h>//在此程序运行时,我们要重点在调试的过程中观察内存的变化int main(){int n = 0x11223344;char* pc = (char*)&n;int* pi = &n;*pc = 0; *pi = 0;return 0;}

 

 

 所以,我们又可以推出:

  指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。

  比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。

 

四.野指针

野指针:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

1.野指针的危害 

  1、指向不可访问的地址
  2、指向一个可用的,但是没有明确意义的空间
  3、指向一个可用的,而且正在被使用的空间,造成瘫痪

2.野指针成因:

  1. 指针未初始化:

#include <stdio.h>int main(){int* p;//局部变量指针未初始化,默认为随机值*p = 20;return 0;}

  2. 指针越界访问:

#include <stdio.h>int main(){int arr[10] = { 0 };int* p = arr;int i = 0;for (i = 0; i <= 11; i++)    {//当指针指向的范围超出数组arr的范围时,p就是野指针*(p++) = i;    }return 0;}

  3.指针指向的空间释放:

int func(){int *p = malloc(sizeof(int));free(p);//没有将p值为NULL的操作 }

3.如何避免野指针 

  1. 指针初始化
  2. 小心指针越界
  3. 指针指向空间释放及时置NULL
  4. 指针使用之前检查有效性

 

五.指针运算

1.指针+-整数

 在我们指针变量大小那块我们便展示了一个例子,接下来我们继续看一个:

#define N_VALUES 5int main(){float values[N_VALUES];float* vp;//指针+-整数;指针的关系运算for (vp = &values[0]; vp < &values[N_VALUES];)    {*vp++ = 0;    }for (int i = 0; i < N_VALUES; i++)    {        printf("%f ", values[i]);    }return 0;}

此例是运用指针来放置数组变量

到这,我们便又掌握了一种操作数组的方法。(详见六.指针与数组

 

2.指针-指针

 在之前我们模拟strcpy()的实现中,便用到此方法,下面我们继续再来写一下

//1. 计数器的方法int my_strlen(char* str){int count = 0;while (*str != '\0')    {        count++;        str++;    }return count;}
//2.递归实现int my_strlen(const char* str){if (*str == '\0')return 0;elsereturn 1 + my_strlen(str + 1);}
//3.指针-指针int my_strlen(char* s){char* p = s;while (*p != '\0')        p++;return p - s;}

详情参见:C语言之库函数的模拟与使用

在指针-指针中我们需要注意的一点就是:两个指针一定要指向的是同一块连续的空间

 

下面以一个实例来说明:

int main(){int arr[10] = { 0 };    printf("%d\n", &arr[0] - &arr[9]);//-9char ch[5] = {0};//指针-指针   绝对值的是指针和指针之间的元素个数printf("%d\n", &arr[9] - &ch[3]);//err//指针-指针 计算的前提条件是:两个指针指向的是同一块连续的空间的return 0;}

我们会看到编译器报一个警告

 

3.指针的关系运算

#define N_VALUES 5int main(){float values[N_VALUES];float* vp;//指针的关系运算for (vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--)    {*vp = 0;    }for (int i = 0; i < N_VALUES; i++)    {        printf("%f ", values[i]);    }return 0;}

我们来看看上面这个例子有没有什么问题

 

 

 

 

六.指针与数组

我们先在这看一个实例:

#include <stdio.h>int main(){int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };    printf("%p\n", arr);    printf("%p\n", &arr[0]);return 0;}

在运行结果中,我们居然发现,这两个地址居然一样!

由此我们可以得到:数组名表示的是数组首元素的地址。

那么我们用指针来接收数组时便可写成这个样子

 arr[] = {,,,,,,,,,}; *p = arr;
int main(){int arr[5] = { 1, 2, 3, 4, 5 };int* p = arr;int i = 0;for (i = 0; i < 5; i++)    {        printf("%d ", *(p + i));//通过指针来访问数组元素    }    printf("\n");for (i = 0; i < 5; i++)    {        printf("&arr[%d] = %p < === > %p\n", i, &arr[i], p+i);//打印两地址看是否相同    }return 0;}

所以 p+i 其实计算的是数组 arr 下标为i的地址。

那我们就可以直接通过指针来访问数组。

如下:

int main(){int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };int* p = arr; //指针存放数组首元素的地址int sz = sizeof(arr) / sizeof(arr[0]);int i = 0;for (i = 0; i < sz; i++)    {        printf("%d ",*(p + i));    }return 0;}

 

七.二级指针

我们知道,存放地址变量的叫做指针变量,那存放指针变量的呢,自然就叫做二级指针变量了啊!

 

由此我们可以继续推下去:

 a =  * p = &a; * * pp = &p;**pp = , a);  //……………………

 

 

 

八.指针数组

在谈这个主题之前,我们先来想一想 指针数组到底是指针还是数组?

数组

整形数组 - 存放整形的数组

字符数组 - 存放字符的数组

所以:

指针数组 - 存放的指针

如:

int main(){//int arr[10] = {0};//整形数组//char ch[5] = { 'a', 'b' };//字符数组//指针数组int a = 10;int b = 20;int c = 30;//arr就是指针数组//存放整形指针的数组int* arr[3] = { &a, &b, &c };//int* char* ch[5];//存放字符指针的数组return 0;}

 

九.相关例题

 在我们解决问题时,第一个遇到用指针的问题应该是这个:

写一个函数可以交换两个整形变量的内容。

#include <stdio.h>void Swap1(int x, int y){int tmp = 0;    tmp = x;    x = y;    y = tmp;}int main(){int num1 = 1;int num2 = 2;    Swap1(num1, num2);    printf("Swap1::num1 = %d num2 = %d\n", num1, num2);return 0;}

当时,我们说,函数里的 x,y 只是对于数据的一份临时拷贝,而与真实数据并没有本质的联系。

所以,我们把函数改写成了:

void Swap2(int* px, int* py){int tmp = 0;    tmp = *px;*px = *py;*py = tmp;}

这时,我们或许知道了这样是怎么把值给交换过来的

我们将地址先传过去,然后再进行解引用赋值操作,整个函数过程都与我们传过去的变量息息相关

 

例题1:

#include <stdio.h>int main(){int a = 0x11223344;char* pc = (char*)&a;*pc = 0;    printf("%x\n", a);return 0;}

这一题要用到我们之前谈到过的大小端储存模式-----详见: C语言之数据在内存中的存储

首先 a在内存中按小端存储的是 44 33 22 11

char* 只能解引用一个字节,所以获取的是 44 这个字节

而现在 *pc=0 就是把这个字节的值由 44 变为了 0;

所以该程序在Vs的编译器结果是 0x 11 22 33 00

 

例题2:

#include <stdio.h>int main(){int arr[] = { 1,2,3,4,5 };short* p = (short*)arr;int i = 0;for (i = 0; i < 4; i++)    {*(p + i) = 0;    }for (i = 0; i < 5; i++)    {        printf("%d ", arr[i]);    }return 0;}

首先我们要知道 short* 解引用只能解引用 2 个字节

而 int 类型为 4 个字节

所以第一个for循环只改变了数组 arr 的前两个变量(改为了0)

所以最后的结果为: 0 0 3 4 5 

 

 

 

到此,我们便掌握了指针的一些基础知识

下节,我们将谈到一些指针的高级应用

详见:C语言之漫谈指针(下)

 

 

|-----------------------------------------------

|因笔者水平有限,若有错误,还请指正!

 

发表于 2021-03-16 08:48 guguguhuha 阅读(278) 评论(0) 编辑 收藏

©著作权归作者所有:来自51CTO博客作者??咕咕咕呼的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. Ansible 日常使用技巧 - 运维总结
  2. 【DB笔试面试846】在Oracle中,TWO_TASK环境变量的作用是什么?
  3. 10天入门go语言教程- 常量变量
  4. Oracle如何使用spool导出utf8字符集的文本文件
  5. 面向对象、类和对象、封装---------私有private、this关键字
  6. JDK1.8简单配置环境变量---两步曲
  7. 提高前端代码的质量
  8. C 存储类
  9. Python 函数(二)

随机推荐

  1. Android对SlidingDraw组件修改
  2. Android常用URI以及URI简介
  3. Android Studio 基础控件使用
  4. Android SeekBar的使用
  5. 基于Android P 背光流程
  6. Android 获取天气预报
  7. Android onTouch事件
  8. Android从asset中获取drawable
  9. Android常用功能实例 如IMEI号
  10. Android静默安装相关