指针【初阶】

系列文章目录

对C语言来说数组和指针二者存在千丝万缕的关系,所以贴在下面以方便随时回顾。

C语言☞数组


目录

  • 系列文章目录
  • 前言
  • 指针简介
    • 指针和指针类型
      • 野指针(野狗)
  • 指针操作(指针运算)
    • 指针和数组
      • 指针表示法和数组表示法
  • 二级指针
    • 指针和多维数组
  • 总结


前言

指针为什么如此重要?我的信念是:正是指针使C威力无穷。有些任务用其他语言也可以实现,但C能够更有效地实现;有些任务无法用其他语言实现,如直接访问硬件,但C却可以。要想成为一名优秀的C程序员,对指针有一个深入 而完整的理解是先决条件。


指针简介

  • 指针(pointer)是什么?
    要想理解,我们先要理解什么是内存:
  1. 把内存划分为一个个的内存单元,这个内存单元的大小是1个字节 ,每个字节都包含了存储一个字符所需要的位数。在许多现代的机器上,每个字节包含8个位,所以存储无符号值0至255,或有符号值-128至127。
  2. 每个字节都给一个唯一的编号,这个编号称为地址地址在C语言中也称为指针

指针【初阶】_第1张图片

理解了内存后,理解指针就容易多了。

  1. 指针是内存中的最小单元的编号,也就是地址。
  2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
  • 指针变量:
    我们可以通过&(取地址操作符)取出变量的内存真实地址,把地址可以存放到一个变量中,这个变量就是指针变量
#include
int main()
{
	int a = 10;         //在内存中开辟一块空间
	int* ptr = &a;        //取出a变量的地址,a变量占用4个字节的空间,
	                  // 这里将a的4个字节的第一个的地址存放在p变量中,p就是一个指针变量
	return 0;
}

对于上面的代码,我们说 ptr “指向”a。ptr和&a的区别是ptr是变量,而&a是常量。或者说,ptr是可修改的左值,而&a是右值。

  • 总结:
  1. 编号 = 地址 = 指针
  2. 指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
  3. 指针的大小在32位平台是4个字节,在64位平台是8个字节。也就是说,只要是个指针,其大小要么是4个字节,要么是8个字节,不会出现其他的情况。

指针和指针类型

相信读者已经很熟悉如何声明 int 类型和其他类型的变量,那么如何声明指针变量?
我们都知道,变量有不同的类型,整型,字符型,浮点型等。如此说来,是变量应该就有类型,那么指针变量的类型是怎样的呢?

  • type + *
int* pi;        //pi是指向int类型变量的指针
char* pc;       //pc是指向char类型变量的指针
float* pf      //pf是指向float类型变量的指针

pc指向的值(* pc)是char类型。pc本身是什么类型?
我们描述它的类型是“指向char类型的指针
所以,指针实际上是一个新类型。
这里可以看到:
int* 类型的指针是为了存放 int 类型变量的地址。
char* 类型的指针是为了存放 char 类型变量的地址。
float* 类型的指针是为了存放 float 类型变量的地址。

  • 声明指针
    上面的代码就是一些指针的声明示例。
    声明指针变量时必须指定指针所指向变量的类型,因为不同的变量类型占用不同的存储空间,一些指针操作要求知道操作对象的大小。另外,程序必须知道存储在指定地址上的数据类型。例如,long和float可能占用相同的存储空间,但是它们存储数字却大相径庭。

  • 那指针类型的意义又是什么?
    指针【初阶】_第2张图片
    在上面的代码中,对pc定义的时候强制类型转换为char类型,通过打印它们的地址可以看出pc到pc+1隔了1个字节,而pi到pi+1之间隔了4个字节。
    pc + n --> pc + n* sizeof (char)
    pi + n --> pi + n* sizeof (int)

  • 总结:
    指针类型决定了在解引用指针的时候能访问几个字节
    比如:char* 的指针解引用就只能访问一个字节,而int* 的指针的解引用就能访问四个字节。
    指针的类型决定了指针向前或者向后走一步有多大(距离)


野指针(野狗)

概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有,明确限制的)。
野指针就像是一条野狗,是非常危险的存在,因为假如你被野狗咬了,你也得不到赔偿。

  • 野指针成因
  1. 指针未初始化
    例如,考虑下面的例子:
#include
int main()
{
	int* pt;             //第一行
	*pt = 5;              //第二行
	printf("%d\n", *pt);
	return 0;
}

指针【初阶】_第3张图片
为何不行?第二行的意思是把5存储在pt指向的位置。但是pt未被初始化,其值是一个随机值,所以不知道5将存储在何处。这可能会擦写数据或代码,或者导致程序崩溃。
切记:创建一个指针时,系统只分配了存储指针本身的内存,并未分配存储数据的内存。因此,在使用指针之前,必须先用已分配的地址初始化它

  1. 指针越界访问

指针【初阶】_第4张图片
Run-Time Check Failure #2 - Stack around the variable ‘arr’ was corrupted.

这段话翻译过来的意思是:运行时检查失败#2-变量“arr”周围的堆栈已损坏

通过排查,了解到可能是数组“arr”变量可能存在堆栈溢出或内存访问越界的问题。

  • 问题原因:
    报错提示数组“arr”存在问题,再三检查之后发现问题出在for循环中的数组下标。
    变量“i”进循环时,将“i”赋值为“1”,导致数组:“arr[i]”,第一次循环时下标为“1”,for循环10次,则下标对应为“1-10”。
    而实际情况数组“arr[10]”的下标为“0-9”,导致了内存越界,报错的产生。

问题原因是数组里输入的字符超过了这个数组的范围,故导致了访问越界,报错的产生 。编译器不会显示出此类问题,在代码运行过后才会发出一个警告。
遇到此类问题,如若存在数组变量,则需再三检查数组下标访问的合法性。

当指针指向的范围超出数组arr的范围时,p就是野指针

  1. 指针指向的空间被释放了
  • 如何规避野指针
    1.指针初始化(一开始不知道指针指向哪里,暂时可初始化为NULL)
    2.小心指针越界(数组的下标是从0开始计算的)
    3.指针指向空间释放,及时置NULL
    4.避免返回局部变量的地址
    5.指针使用之前检查有效性
#include
int main()
{
	int* p = NULL;
	int a = 10;
	p = &a;
	if (p != NULL)
	{
		*p = 20;          //检查指针有效性
	}
	printf("%d\n", *p);

	return 0;
}

这步if判断非常关键,是一个程序员严谨思维的体现。


指针操作(指针运算)

可以对指针进行哪些操作?C提供了一些基本的指针操作。

  • 赋值
    可以把地址赋给指针。例如:用数组名、带地址运算符(&)的变量名、另一个指针进行赋值。
    注意:地址应该和指针类型兼容。也就是说,不能把double类型的地址赋给指向int的指针,至少要避免不明智的类型转换。C99/C11已结强制不允许这样做。
  • 解引用
    *运算符给出指向地址上存储的值。
  • 取址
    和所有变量一样,指针变量也有自己的地址和值。对指针而言,&运算符给出指针本身的地址。
  • 指针 + - 整数

指针【初阶】_第5张图片
像上面的例子中,初始化一个数组为全0。递增指向数组元素的指针可以让该指针移动至数组的下一个元素。
可以使用 + - 运算符把指针与整数相加相减,该整数将乘以指针指向类型的大小(以字节为单位),然后把结果与初始地址相加相减。

  • 指针求差
    可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。
#include
//strlen 的模拟实现
int my_strlen(char* s)
{
	char* p = s;
	while (*p != '\0')
	{
		p++;
	}
	return p - s;            //计算出字符串的元素个数
}

int main()
{
	char s[] = "abcdef";
	int len = my_strlen(&s);
	printf("%d\n", len);
	return 0;
}

两个指针指向同一块空间,指针 - 指针得到的是指针和指针之间的元素个数

  • 比较
    使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。
#include
int main()          //初始化数组为全0
{
	int  arr[5];
	int* p;
	for (p = &arr[0]; p < &arr[5];)
	{
		*p = 0;
		p++;
	}
	return 0;
}

其实,细心的小伙伴会发现我在比较时,取出了数组后面的空间(&arr[5])。其实,这是合法的。
标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针进行比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较


指针和数组

  • 我们看一个例子:
#include
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("arr = %p\n", arr);
	printf("&arr[0] = %p\n", &arr[0]);
	return 0;
}

指针【初阶】_第6张图片
从运行结果可以看出,数组名和数组首元素的地址是一样的。
结论:数组名表示的是数组首元素的地址
除了下面两种情况:
1、sizeof(数组名),计算的是整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组
2、&数组名,取出的是数组的地址。&数组名,数组名表示整个数组

  • 既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个数组就成为可能。
    例如:
#include
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;                //指针存放数组首元素的地址
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < sz; i++)
	{
		printf("&arr[%d] = %p  <====> p+%d = %p\n", i, &arr[i], i, p + i);
	}
	return 0;
}

指针【初阶】_第7张图片

从上面的运行结果可以看出,p+i 其实计算的是数组arr 下标为 i 的地址。

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

指针【初阶】_第8张图片

arr[ i ] 和 *(arr + i)二者是等价的
其实,arr[ i ] = * (arr + i) = * (i + arr) = i [ arr ],虽然后两种形式不常见,但语法是正确的

  • 总结来说,数组就是数组,指针就是指针
    1、指针可以指向数组元素。
    2、数组名表示的是数组首元素的地址。
    3、因为指针可以指向数组元素,所以借助于指针可以访问数组。

指针表示法和数组表示法

我们在处理数组的函数实际上用指针作为参数,但是在编写这样的函数时,可以选择是使用数组表示法还是指针表示法

  • 数组表示法 (arr[ i ] )
    使用数组表示法,让函数是处理数组这一意图更加明显。另外,许多其他语言的程序员对数组表示法更熟悉,如FORTRAN、Pascal、Modula-2或BASIC。但是其他程序员可能更习惯使用指针,觉得使用指针更自然。
  • 指针表示法(*(arr + i))
    指针表示法(尤其与递增运算符一起使用时)更接近机器语言,因此一些编译器早编译时能生成效率更高的代码。

二级指针

  • 指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?
  • 这就是二级指针。
    指针【初阶】_第9张图片
    从上面的画图分析中可以看出,a的地址存放在pa中,pa 的地址存放在ppa中
    所以,pa是一级指针ppa是二级指针
    对于二级指针的运算有:
  • ppa 通过对ppa中的地址进行解引用,这样就找到的是pa,* ppa其实访问的就是pa。
int b = 20;
*ppa = &b;
//等价于 pa = &b;
  • **ppa 先通过*ppa找到pa,然后对pa进行解引用操作:*pa,那找到的就是a。
***ppa = 30;
//等价于*pa = 30;
//等价于a = 30;

指针和多维数组

指针和多维数组有什么关系?为什么要了解它们的关系?

  • 假设有下面的声明:
int zippo[4][2];

数组名 zippo 是该数组首元素的地址。在本例中,zippo 的首元素是一个内含两个 int 值的数组,所以 zippo 是这个内含两个 int 值的数组的地址

  • 因为zippo 是数组首元素的地址,所以 zippo 的值和 &zippo [ 0 ] 的值相同。而 zippo[ 0 ] 本身是一个内含两个整数的数组,所以 zippo[ 0 ] 的值和它首元素的地址(即 &zippo[ 0 ] [ 0 ] 的值 )相同
    简而言之,zippo[ 0 ] 是一个占用一个 int 大小对象的地址,而 zippo 是一个占用两个 int 大小对象的地址。由于这个整数和内含两个整数的数组都开始于同一个地址,所以 zippo 、zippo[ 0 ] 和zippo[ 0 ] [ 0 ] 三者的值是相同的。

  • 解引用一个指针(在指针前使用 * 运算符)或在数组名后使用带下标的[ ] 运算符,得到引用对象代表的值。
    因为 zippo[ 0 ] 是该数组首元素( zippo[ 0 ] [ 0 ] )的地址,所以*(zippo[ 0 ] )表示存储在zippo[ 0 ] [ 0 ] 上的值。
    与此类似, * zippo 代表该数组首元素(zippo[ 0 ] )的值,但是 zippo[ 0 ] 本身是一个int类型的值。该值的地址是&zippo[ 0 ] [ 0 ] ,所以 * zippo 就是 &zippo[ 0 ] [ 0 ] 。**zippo 与 *&zippo[ 0 ] [ 0 ] 等价,相当于 zippo[ 0 ] [ 0 ] .

  • 简而言之,zippo 是地址的地址,必须解引用两次才能获得原始值

大家看文字可能不太理解的话,下面将以视图来演示数组地址、数组内容和指针之间的关系。
指针【初阶】_第10张图片
可以用数组表示法或指针表示法来表示一个二维数组元素:

zippo[ m ][ n ] == *( * zippo + m) + n )


总结

C把数组名解释为该数组首元素的地址。换言之,数组名与指向该数组首元素的指针等价。概括的说,数组和指针的关系十分密切。如果 arr 是一个数组,那么表达式 arr [ i ] 和 *(arr)+ i
等价。


后记
指针的用法灵活巧变,指针的使用会贯穿整个C语言,这只是初阶的指针,后期可能还会更新指针进阶的内容。

你可能感兴趣的:(大一小白如何快速入门C语言,c++,开发语言)