天天看点

最详细的【指针】详解---C语言从入门到精通

最详细的【指针】详解---C语言从入门到精通

个人名片:

🐼作者简介:一名大一在校生

🐻‍❄️个人主页:​​小新爱学习.​​

🕊️系列专栏:零基础学java ----- 重识c语言

🐓每日一句:身懒,必会毁了你家的身材;心懒,一定会毁了你的梦想!

文章目录

  • ​​指针🎊​​
  • ​​1.指针是什么?👻​​
  • ​​2.指针和指针类型🐸​​
  • ​​带*指针类型的定义🐯​​
  • ​​指针类型的意义🐧​​
  • ​​3.野指针​​
  • ​​4.指针运算🙈​​
  • ​​指针算数运算:🐇​​
  • ​​指针关系运算:🦖​​
  • ​​5.指针和数组🐻‍❄️​​
  • ​​用指针引用数组元素🦕🐟​​
  • ​​指针与数组的区别🐷​​
  • ​​6.二级指针(指向指针的指针)🐒🦍​​
  • ​​7.指针数组(数组每个元素都是指针)🐺​​

指针🎊

1.指针是什么?👻

指针,是C语言中的一个重要概念及其特点,也是掌握C语言比较困难的部分。指针也就是**内存地址**,指针变量是用来存放内存地址的变量,在同一CPU构架下,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。有了指针以后,不仅可以对数据本身,也可以对存储数据的变量地址进行操作。
指针描述了数据在内存中的位置,标示了一个占据存储空间的实体,在这一段空间起始位置的相对距离值。在 C/C++语言中,指针一般被认为是指针变量,指针变量的内容存储的是其指向的对象的首地址,指向的对象可以是变量(指针变量也是变量),数组,函数等占据存储空间的实体。
最详细的【指针】详解---C语言从入门到精通

总的来说所:指针也就是内存地址,指针变量是用来存放内存地址的变量。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。

指针变量声明的一般形式为:

type *var_name;

type 是指针的基类型,

var_name 是指针变量的名称。

*星号是用来指定一个变量是指针。

举例:

`#include <stdio.h>
 
int main ()
{
    int var_runoob = 10;
    int *p;              // 定义指针变量
    p = &var_runoob;
 
   printf("var_runoob 变量的地址: %p\n", p);
   return 0;
}`      
最详细的【指针】详解---C语言从入门到精通

也可以这么说:指针就是一个变量,变量里面存放的是地址,指针就是地址,地址就是指针

2.指针和指针类型🐸

在C语言中,指针类型就是数据类型,是给编译器看的,也就是说,指针类型与数组、int、char这种类型是平级的,是同一类的

最详细的【指针】详解---C语言从入门到精通

带*指针类型的定义🐯

double* pa;
int* pb;//定义了一个整型指针变量 pa,该指针变量只能指向基类型为 int 的整型变量,即只能保存整型变量的地址。
short* pc;
char* pd;
float* pe;
struct* p people;      

总结:

  1. 任何变量都可以带 * ,加上 * 以后变成新的类型,统称“指针类型”。
  2. *可以是任意多个。

指针类型的意义🐧

指针类型的意义:

1.指针的类型决定了指针走一步走多远(指针的步长)

2.指针类型决定了对指针解引用的时候能操作几个字节

1. 指针类型决定了对指针解引用的时候能操作几个字节

2. char*p; p的指针解引用就只能访问一个字节

3. intp; p的指针解引用就能访问四个字节

4. doublep; *p能够访问8个字节

2.指针类型决定了:指针走一步走多远(指针多长)

1. intp;p+1–>4个字节

2. char p; p+1–>1个字节

3. double*p; p+1–>8个字节

最详细的【指针】详解---C语言从入门到精通

3.野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的。

一、野指针

1、指针变量中的值是非法内存地址,进而形成野指针

   2、野指针不是NULL指针,是指向不可用内存地址的指针

   3、NULL指针并无危害,很好判断,也很好调试

   4、C语言中无法判断一个指针所保存的地址是否合法      

二、产生野指针的原因:

  1. 指针变量未初始化

    任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。如果没有初始化,编译器会报错“ ‘point’ may be uninitializedin the function ”。

  2. 指针释放后之后未置空

    有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。

  3. 指针操作超越变量作用域

    不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。

总结:

1、局部指针变量没有初始化

  2、指针所指向的变量在指针之前被销毁

  3、使用已经释放过的指针

  4、进行了错误指针运算

  5、进行了错误的强制类型转换      

三、设计指针基本原则(避免野指针)

1、绝不返回局部变量和局部数组的地址

  2、任何变量在定义后必须0初始化

  3、字符数组必须确认0结束符后才能成为字符串

  4、任何使用与内存操作相关的函数必须指定长度信息      

四、常见内存错误

1、结构体成员指针未初始化

  2、结构体成员指针未分配足够的内存

  3、内存分配成功,但并未初始化

  4、内存操作越界      

五、总结

内存错误是实际产品开发中最常见的问题,然而绝对大多数的bug都可以通过遵循基本的编程原则和规范来避免。

因此,在学习的时候要牢记和理解内存操作的基本原则,目的和意义

 1、内存错误的本质源于指针保存的地址为非法值

      --指针变量未初始化,保存随机值

      --指针运算导致内存越界

 2、内存泄露源于malloc和free不匹配

       --当malloc次数多于free时,产生内存泄露

       --当malloc次数少于free时,程序可能崩溃      

4.指针运算🙈

指针算数运算:🐇

指针是一个用数值表示的地址。因此,您可以对指针执行算术运算。可以对指针进行四种算术运算:++、–、+、-。

自增运算

例:

int *ptr;   //假设ptr是一个指向地址为1000的整型指针
ptr++;    //运算完后ptr指向位置1004      

指针每一次递增(递减),表示指针指向下一个(上一个)元素的存储单元

注意:

  • 这个运算不会影响内存中元素的实际值
  • 指针在递增或递减时跳跃时的字节数取决于指针所指向变量数据类型的长度(int 4个字节 char 1个字节)

增减运算

指针变量加上或减去一个整形数。加几就是向后移动几个单元,减几就是向前移动几个单元。

//定义三个变量,假设它们地址为连续的,分别为 4000、4004、4008
int x, y, z;

//定义一个指针,指向 x
int *px = &x;

//利用指针变量 px 加减整数,分别输出 x、y、z
printf("x = %d", *px);* 

//px + 1,表示,向前移动一个单元(从 4000 到 4004)
//这里要**先(px + 1),再*(px + 1)获取内容**,因为单目运算符“*”优先级高于双目运算符“+”
printf("y = %d", *(px + 1));    
printf("z = %d", *(px + 2));      

第二种类型的指针运算具有如下的形式: 指针—指针

只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针,如下所示:

最详细的【指针】详解---C语言从入门到精通

减法运算的值是两个指针在内存中的距离(以数组元素的长度为单位,而不是以字节为单位),因为减法运算的结果将除以数组元素类型的长度。

例: 利用递归实现求字符串长度

#include<stdio.h>
int my_strlen(char* str)
{
  char* start = str;
  char* end = str;
  while (*end != '\0')
  {
    end++;
  }
  return (end - start);
}
int main()
{
  //strlen - 求字符串长度
  //递归--模拟实现strlen
  char arr[] = "hmm";
  int len = my_strlen(arr);
  printf("%d\n", len);
  return 0;

}      

指针关系运算:🦖

假定有指针变量 px、py;

只有当两个指针指向同一个数组中的元素时,才能进行关系运算。

当指针px和指针py指向同一数组中的元素时,

则有:

1. px > py 表示 px 指向的存储地址是否大于 py 指向的地址

2. px < py 表示 px 指向的存储地址是否小于 py 指向的地址

3. px == py 表示 px 和 py 是否指向同一个存储单元

4. px == 0 和 px != 0 表示 px 是否为空指针

例1:

#define N_VALUES 5
float values[N_VALUES];
float *vp;
for(vp = &values; vp < &values[N_VALUES];) {
    *vp++ = 0;
}      

vp不断的向右移动,每次移动前都会先使用间接访问符把当前值初始化为0,最后vp指向了values数组最后一个元素的后面一个位置,这是合法的,指针可以指向它,它有合法的地址,但是不能使用间接访问符访问,因为那里存储的什么东西,我们并不知道。

最详细的【指针】详解---C语言从入门到精通

例2:

#define N_VALUES 5
float values[N_VALUES];
float *vp;
for(vp = &values[N_VALUES]; vp > &values[0] ;) {
    *--vp = 0;
}      

vp首先指向了values的最后一个元素的后面一个位置,然后不断左移,每次左移过后再使用间接访问符初始化为0,。

例3:

#define N_VALUES 5
float values[N_VALUES];
float *vp;
for(vp = &values[N_VALUES-1]; vp >= &values[0] ; vp--) {
    *vp = 0;
}      

这个版本的思路和版本2一样,只不过把*–vp操作拆分开了,但是这个版本中vp指向第一个元素values[0]后,下一个循环就指向了values数组前面的位置去了,这是前面讲的规则不允许的,是绝不允许和数组第一个元素前面的位置的指针进行比较的有可能在有些机器上运行正确,但也绝对不允许。

标准规定:

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但不允许与指向第一个元素之前那个内现存位置的指针进行比较。

5.指针和数组🐻‍❄️

int main()
{
  int arr[10] = { 0 };
  printf("%p\n", arr);//地址-首元素地址
  printf("%p\n", arr+1);
  
  printf("%p\n", &arr[0]);
  printf("%p\n",&arr[0]+1);

  printf("%p\n", &arr);
  printf("%p\n", &arr+1);
  //1. &arr- &数组名--数组不是首元素地址-数组名表示整个数组--&数组名  取出的是整个数组的地址
  // 2. sizeof(arr) --sizeof(数组名)--数组名表示的整个数组--sizeof(数组名)计算的是整个数组的大小
  return 0;
}      
最详细的【指针】详解---C语言从入门到精通

用指针引用数组元素🦕🐟

引用数组元素可以用“下标法”,这个在前面已经讲过,除了这种方法之外还可以用指针,即通过指向某个数组元素的指针变量来引用数组元素。

数组包含若干个元素,元素就是变量,变量都有地址。所以每一个数组元素在内存中都占有存储单元,都有相应的地址。指针变量既然可以指向变量,当然也就可以指向数组元素。同样,数组的类型和指针变量的基类型一定要相同。

# include <stdio.h>
int main(void)
{
    int a[] = {1, 2, 3, 4, 5};
    int *p = &a[0];
    int *q = a;
    printf("*p = %d, *q = %d\n", *p, *q);
    return 0;
}      

输出结果是:

*p = 1, *q = 1

程序中定义了一个一维数组 a,它有 5 个元素,即 5 个变量,分别为 a[0]、a[1]、a[2]、a[3]、a[4]。所以 p=&a[0] 就表示将 a[0] 的地址放到指针变量 p 中,即指针变量 p 指向数组 a 的第一个元素 a[0]。而 C 语言中规定,“数组名”是一个指针“常量”,表示数组第一个元素的起始地址。所以 p=&a[0] 和 p=a 是等价的,所以程序输出的结果 *p 和 *q 是相等的,因为它们都指向 a[0],或者说它们都存放 a[0] 的地址。

指针与数组的区别🐷

指针 数组
保存数据的地址,任何存入指针变量 p 的数据都会被当作地址来处理 保存数据,数组名 a 代表的是数组首元素的首地址,&a 是整个数组的首地址
间接访问数据,首先取得指针变量 p 的内容,把它当做地址,然后从这个地址提取数据或向这个地址写入数据。 指针可以以指针的形式访问 "(p+i)" 也可以以下标的形式访问 “p[i]”。但其本质都是先取 p 的内容后加上“isizeof(类型)”字节作为数据的真正地址. 直接访问数据,数组名 a 是整个数组的名字,数组内每个元素并没有名字。只能通过"具名+匿名"的方式来访问其某个元素,不能把数组当一个整体进行读写操作。数组可以以指针的形式访问"(a+i)“,也可以以下标的形式访问"a[i]”。但其本质都是 a 所代表的数组首元素的首地址加上"isizeof(类型)"字节来作为数据的真正地址
通常用于动态数据结构 通常用于存储固定数目且数据类型相同的元素
需要 malloc 和 free 等相关的函数进行内存分配 隐式分配和删除
通常指向匿名数据 自身即为数组名

6.二级指针(指向指针的指针)🐒🦍

指针可以指向一份普通类型的数据,例如 int、double、char 等,也可以指向一份指针类型的数据,例如 int *、double *、char * 等。

如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。

假设有一个 int 类型的变量 a,p1是指向 a 的指针变量,p2 又是指向 p1 的指针变量,

最详细的【指针】详解---C语言从入门到精通
int a =100;
int *p1 = &a;
int **p2 = &p1;      

指针变量也是一种变量,也会占用存储空间,也可以使用&获取它的地址。C语言不限制指针的级数,每增加一级指针,在定义指针变量时就得增加一个星号*。p1 是一级指针,指向普通类型的数据,定义时有一个*;p2 是二级指针,指向一级指针 p1,定义时有两个*。

想要获取指针指向的数据时,一级指针加一个*,二级指针加两个*,三级指针加三个*,以此类推,

#include <stdio.h>
int main(){
    int a =100;
    int *p1 = &a;
    int **p2 = &p1;
    int ***p3 = &p2;
    printf("%d, %d, %d, %d\n", a, *p1, **p2, ***p3);
    printf("&p2 = %#X, p3 = %#X\n", &p2, p3);
    printf("&p1 = %#X, p2 = %#X, *p3 = %#X\n", &p1, p2, *p3);
    printf(" &a = %#X, p1 = %#X, *p2 = %#X, **p3 = %#X\n", &a, p1, *p2, **p3);
    return 0;
}      
最详细的【指针】详解---C语言从入门到精通

以三级指针 p3 为例来分析上面的代码。*p3等价于((p3))。p3 得到的是 p2 的值,也即 p1 的地址;(p3) 得到的是 p1 的值,也即 a 的地址;经过三次“取值”操作后,((*p3)) 得到的才是 a 的值。假设 a、p1、p2、p3 的地址分别是 0X00A0、0X1000、0X2000、0X3000,它们之间的关系可以用下图来描述:

最详细的【指针】详解---C语言从入门到精通

结论:

编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是 p,编译器使 p = q(但是&p != &q,也就是他们并不在同一块内存地址,只是他们的内容一样,都是a的地址)。如果函数体内的程序修改了p的内容(比如在这里它指向b)。在本例中,p申请了新的内存,只是把 p所指的内存地址改变了(变成了b的地址,但是q指向的内存地址没有影响),所以在这里并不影响函数外的指针q。因为传了指针q的地址(二级指针**p)到函数,所以二级指针拷贝(拷贝的是p,一级指针中拷贝的是q所以才有问题),(拷贝了指针但是指针内容也就是指针所指向的地址是不变的)所以它还是指向一级指针q(p = q)。在这里无论拷贝多少次,它依然指向q,那么p = &b;自然的就是 q = &b;了。

最详细的【指针】详解---C语言从入门到精通

7.指针数组(数组每个元素都是指针)🐺

如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。指针数组的定义形式一般为:

dataType *arrayName[length];

括号里面说明arrayName是一个数组,包含了length个元素,括号外面说明每个元素的类型为dataType *。

#include <stdio.h>
int main(){
    int a = 16, b = 932, c = 100;
    //定义一个指针数组
    int *arr[3] = {&a, &b, &c};//也可以不指定长度,直接写作 int *arr[]
    //定义一个指向指针数组的指针
    int **parr = arr;
    printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);
    printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));
    return 0;
}      

运行结果:

16, 932, 100

16, 932, 100

arr 是一个指针数组,它包含了 3 个元素,每个元素都是一个指针,在定义 arr 的同时,我们使用变量 a、b、c 的地址对它进行了初始化,这和普通数组是多么地类似。

parr 是指向数组 arr 的指针,确切地说是指向 arr 第 0 个元素的指针,它的定义形式应该理解为int *(parr),括号中的表示 parr 是一个指针,括号外面的int *表示 parr 指向的数据的类型。arr 第 0 个元素的类型为 int *,所以在定义 parr 时要加两个 *。

第一个 printf() 语句中,arr[i] 表示获取第 i 个元素的值,该元素是一个指针,还需要在前面增加一个 * 才能取得它指向的数据,也即 *arr[i] 的形式。

第二个 printf() 语句中,parr+i 表示第 i 个元素的地址,*(parr+i) 表示获取第 i 个元素的值(该元素是一个指针),**(parr+i) 表示获取第 i 个元素指向的数据。

继续阅读