【C初阶】第七篇——指针

简介: 【C初阶】第七篇——指针

指针是什么?


指针的定义


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

那么我们可以这样理解:

在内存中,内存被细分为一个个大小为一个字节的内存单元,每一个内存单元都有自己对应的地址.

image.png

可以将这些内存单元看作是一个个房间,将内存单元(房间)对应的地址形象地看成房间的门牌号.而我们通过门牌号(地址)就可以唯一的找到对应的房间(内存单元),即地址指向对应内存单元.所以说,可以将地址形象化的称为"'指针"

指针是个变量,存放内存单元的地址(编号)

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

总结:

  • 指针变量是用于存放地址的变量(存放在指针中的值都被当作地址处理)

那问题来了:

  • 一个小的单元到底是多大?(一个字节)
  • 如何编址?

指针的大小


对于32位的机器,即有32跟地址线,因为每根地址线能产生正电(1)或负电(0),所以在32位的机器上能够产生的地址信号就是32个0/1组成的二进制序列:

image.png

一共有2^32个地址.


同样,在64位的机器上一共也能产生2^64个不同的地址.


2^32可以用32个bit位进行存储,而8个bit位等价于1个字节,所以在32位的平台下指针的大小位4个字节.


2^64可以用64个bit位进行存储,所以在64位的平台下指针的大小位8个字节.

总结:

  • 在32位平台下指针的大小为4个字节,在64位平台下指针的大小为8个字节.

指针和指针类型


指针有哪些类型?


指针可以根据指针指向的变量的数据类型来进行分类,有整型指针,字符指针,数组指针,函数指针等等.(后面进阶会详细介绍)

整型指针和字符指针

这两个是比较常见和容易理解的指针,依次用int*和char*表示,他们的区别在于指向变量类型不同,内存也不一样,在进行解引用操作时访问的字节大小也因为变量类型的区别会有所差异。整型指针可以访问4个字节,而字符指针只能访问1个字节。也就是说对整型指针变量解引用,一次可以操作一个整型,而对字符变量解引用一次只能操作一个字符。


较为特殊的char*p="hello"这并不是将整个字符串的地址传个了p,而是传了字符串首元素‘h'的地址,可以通过’h‘的地址来找到整个字符串。此时出现char*p2=“hello”,p2和p代表的是同一处地址,因为hello是常量字符串,没有必要开辟两块不同的空间的来存储它。这是字符指针的一个特性。

void型指针

void型的指针可以接受任何类型的地址,但是不能对void型指针进行解引用操作。解引用操作要有特定的访问字节的数量,比如对整型指针解引用就是访问4个字节,字符型指针解引用就是访问1个字节,而void型指针无法确定访问字节个数,所以不能进行解引用操作。同时void*这种类型的指针也不能进行加减整数的操作,因为无法确定跳过的字节个数。

数组指针

这是一种指向数组的指针,例如int(*p)[10]这就是一个指向数组的指针,它指向的数组有10个元素,每个元素都是整型。给*p加上括号是因为p和[10]优先结合,这样的话就变成了一个数组而不是指针了。这个数组叫 指针数组 ,int*p[10]这样的写法意思是一个有10个元素的数组,每一个元素都是整型指针,这和数组指针是两个不同的东西。


指向数组的指针里面存放的便是数组的地址,而非数组某个元素的地址,所以在定义数组指针时要用 &+数组名,而不是简单使用 数组名。

image.png

上图显示出&arr和arr的不同,虽然起始地址相同,但arr+1只让指针向后移动了一个元素的空间,而&arr+1让指针移动了一个数组的空间。

函数指针

函数指针顾名思义就是指向函数的指针,每个函数都有一个入口,这个入口的地址便是函数指针所指向的地址。函数地址的表示方法为 函数名或 &+函数名。例如一个函数叫Add,&Add和Add都是表示这个函数的地址没有什么差别。函数指针的写法是 函数的返回类型(*)(函数的参数),例如函数Add,其函数指针的写法就是int(*p)(int,int)=Add 。*p要加上括号来保证*和p的优先结合来形成一个指针变量,如果不加括号来优先结合,则会出现int* p(int,int)这样的写法,这就变成了函数的声明,这个函数的返回类型是int*,函数的名字叫p,函数的参数是2个整型和原先的函数指针不是同一个意思。


用函数指针调用函数时可以不加*这个解引用符号,因为这个符号将不会在程序运行的时候起到作用。

image.png

上图显示了*这个解引用符号在函数指针调用函数时候不起作用,以上的写法都可以用。

指针大小与类型有关吗?


指针的大小与硬件有关。

内存中有各种各样的数据,整型、浮点型、字符型等等。这些数据在内存中占据不同大小的储存空间,用sizeof运算符(注:sizeof是种运算符而不是函数,它在编译时发挥作用)进行运算时结果是不同的。然而不同类型的指针在相同系统环境下进行这种运算时结果却是相同的。


众所周知,C语言中的指针描述的是内存中的地址。而内存地址这种东西则是由CPU进行编址的。对于一个4位的CPU来讲,它能同时输出的数据为4位,即0000-1111共2^4 种情况,故这些二进制数字只能对应到16个位置的内存地址,即CPU仅能识别出16个内存地址。即便你的内存再大,它也显示只有16个位置的内存可用。这种原理同样应用于32位和64位的CPU。


32位的CPU能同时呈现32个位的数据,故有2^32 种情况,对应到2^32 个内存位置也就是最大3.85GB大小,因此32位的系统只能支持最大4GB的内存。相比之下,64位的CPU能同时吞吐2^64 位的数据,这显然能够对应到2^64 个内存的地址,而理论上这个大小换算成10进制则是相当大的数,如果对应到内存,此时一个很大的内存。所以我们说64位系统理论支持无穷大内存(这里的无穷大只是一种概念,因为我们不可能用到如此巨大容量的内存)。


 综上,因为指针存放的是地址,所以32位内存,共4个字节;64位系统的64位地址共8个字节——你应该明白什么了吧!没错,32位指针4字节,64位指针8字节.


当然,CPU只是影响指针大小的首要因素,除了它之外还要看操作系统和编译器的位数。这里指针的大小由这三个东西中位数最小的那项决定。比如,如果CPU、系统都是64位的,但编译器是32位的,那么很显然指针只能是32位4字节大小。

指针类型有什么意义?


1.指针+-整数

若指针类型为int * 的指针+1,那么它将跳过4个字节的大小指向4个字节以后的内容:

image.png

若指针类型为char*的指针+1,那么它只会跳过1个字节的大小指向下一个字节的内容.

2.指针解引用

指针的类型决定了指针解引用的时候能够访问几个字节的内容。

若指针类型为int *,那么将它进行解引用操作,它将可以访问从指向位置开始向后4个字节的内容

若指针类型为char *,那么将它进行解引用操作,它将可以访问从指向位置开始向后1个字节的内容,以此类推。

总结:

  • 指针的类型决定了指针向前或向后走一步有多大距离。
  • 指针的类型决定了指针在进行解引用操作时,能向后访问的空间大小.

野指针


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

野指针成因


1.指针未初始化

#include<stdio.h>
int main()
{
  int* p;
  *p=10;
  return 0;
}

运行结果:局部指针变量p未初始化,默认为随机值,所以这个时候p就是野指针

image.png

#include<stdio.h>
int main()
{
  int arr[10] = { 0 };
  int* p = &arr[0];
  int i = 0;
  for (i = 0; i < 11; i++)
  {
    *p++ = i;
  }
  return 0;
}

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

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

#include<stdio.h>
int* test()
{
  int a = 10;
  return &a;
}
int main()
{
  int* p = test();
  return 0;
}

指针变量p得到地址后,地址指向的空间已经释放了,所以这个时候的p就是野指针。(局部变量出了自己的作用域就被释放了)

如何规避野指针


1.指针初始化

当指针明确知道要存放某一变量地址时,在创建指针变量时就存放该变量地址。

当不知道指针将要用于存放哪一变量地址时,在创建指针变量时应置为空指针(NULL)

#include<stdio.h>
int main()
{
  int a = 10;
  int* p1 = &a;//明确知道存放某一地址
  int* p2 = NULL;//不知道存放哪一地址时置为空指针
  return 0;
}

2.小心指针越界

3.指针指向的空间被释放后及时置为NULL

4.使用指针之前检查有效性

在使用指针之前需确保其不是空指针,因为空指针指向的空间是无法访问的。

指针运算


指针+-整数


#include<stdio.h>
int main()
{
  int arr[5] = { 0 };
  int* p = arr;
  int i = 0;
  for (i = 0; i < 5; i++)
  {
    *(p + i) = i;
  }
  return 0;
}

指针-指针


指针-指针的绝对值是是两个指针之间的元素个数。

int my_strlen(char* p)
{
  char* pc = p;
  while (*p != '\0')
    p++;
  return p - pc;
}

strlen函数的模拟实现就可以运用指针-指针的代码实现。

指针的关系运算


指针的关系运算,即指针之间的大小比较。

我们如果要将一个数组中的元素全部置0,可以有两种方法。

第一种:从前向后置0

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

最终指向数组最后一个元素后面的那个内存位置的指针将与&arr[4]比较,不满足条件,于是结束循环。

第二种:从后向前置0

#include<stdio.h>
int main()
{
  int arr[5] = { 1, 2, 3, 4, 5 };
  int* p = &arr[4];
  for (p = &arr[4]; p >= &arr[0]; p--)
  {
    *p = 0;
  }
  return 0;
}

最终指向第一个元素之前的那个内存位置的指针将与&arr[0]比较,不满足条件,于是结束循环。

这两种方法在绝大部分编译器下均能将arr数组中的元素置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;
}

运行的结果如下:

image.png

可以看到数组名和数组首元素的地址是一样的.

总结:数组名表示的是数组首元素的地址.

那么就是说明这样写代码是可以的:

1. int arr[10] = {1,2,3,4,5,6,7,8,9,0};
2. int *p = arr;//p存放的是数组首元素的地址

既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个就成为可能。

例如:

image.png

所以 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;
}

运行结果如下:

image.png

二级指针


指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?这就是二级指针.

image.png

a的地址存放在pa中,pa的地址存放在ppa中,即这里pa是一级指针,而ppa是二级指针.

对于二级指针的运算有:

  • *ppa通过对ppa中的地址进行解引用,这样找到的是pa,*ppa其实访问的就是pa
1. int b = 20;
2. *ppa = &b;//等价于 pa = &b;
  • **ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a .
1. **ppa = 30;
2. //等价于*pa = 30;
3. //等价于a = 30;

指针数组


我们已经知道了整型数组、字符数组等。整型数组是用于存放整型的数组,字符数组是用于存放字符的数组。

1. int arr1[4];
2. char arr2[5];

数组arr1包含4个元素,每个元素的类型是整型;数组arr2包含5个元素,每个元素的类型是字符型。

image.png

指针数组也是数组,是用于存放指针的数组。

int* arr3[5];//是什么?

数组arr3包含5个元素,每个元素是一个一级整型指针。

image.png

以此类推:

1. char* arr4[10];//数组arr4包含10个元素,每个元素是一个一级字符型指针。
2. char** arr5[5];//数组arr5包含5个元素,每个元素是一个二级字符型指针。

数组arr4包含10个元素,每个元素是一个一级字符型指针;数组arr5包含5个元素,每个元素是一个二级字符型指针。

相关文章
|
7月前
|
存储 C语言
C语言内功修炼---指针详讲(初阶)
C语言内功修炼---指针详讲(初阶)
|
7月前
|
C语言
C语言(指针详解)重点笔记:指针易错点,都是精华
C语言(指针详解)重点笔记:指针易错点,都是精华
82 0
|
7月前
|
C语言
【C语言进阶篇】什么还没学会指针? 一篇文章让你彻底搞懂指针的奥秘
【C语言进阶篇】什么还没学会指针? 一篇文章让你彻底搞懂指针的奥秘
53 0
|
7月前
|
存储 编译器 C语言
【C语言必知必会| 第十篇】指针入门,这一篇就够了
【C语言必知必会| 第十篇】指针入门,这一篇就够了
77 0
|
C语言
指针基础必备知识【C语言/初阶】
指针基础必备知识【C语言/初阶】
47 0
指针基础必备知识【C语言/初阶】
|
存储 C语言 C++
【C语言初阶】初学必看,深入了解指针的概念!看完让你不在害怕指针(图文并茂)
【C语言初阶】初学必看,深入了解指针的概念!看完让你不在害怕指针(图文并茂)
137 0
|
存储 C语言 C++
《C和指针》读书笔记(第六章 指针)
《C和指针》读书笔记(第六章 指针)
|
存储 C语言
进阶C语言 第二章-------《进阶指针》 (指针数组、数组指针、函数指针、回调指针)知识点+基本练习题+深入细节+通俗易懂+完整思维导图+建议收藏(二)
进阶C语言 第二章-------《进阶指针》 (指针数组、数组指针、函数指针、回调指针)知识点+基本练习题+深入细节+通俗易懂+完整思维导图+建议收藏(二)