【C语言】深入浅出理解指针及内存与指针的关系(详细讲解+代码展示)下

简介: 笔记

野指针和空指针


野指针

指针变量也是变量,是变量就可以任意赋值,但是我们不要越界即可(即32位为4字节,64位为8字节);不过,任意数值赋值给指针变量是毫无意义的,因为这样的指针就成了野指针,此指针指向的区域是未知(操作系统不允许操作此指针指向的内存区域)。所以,野指针不会直接引发错误,但是操作野指针指向的内存区域才会出问题。

int a = 100 ;
int *p ;
p = a; //把a的值赋值给指针变量p,p为野指针, ok,不会有问题,但没有意义
p = 0x1111111; //给指针变量p赋值,p为野指针, ok,不会有问题,但没有意义
*p = 1000;  //操作野指针指向未知区域,内存出问题,over

空指针  

但是,野指针和有效的指针变量保存的都是数值,为了标志此指针变量没有指向任何变量(空闲可用);所以在C语言中,可以把NULL赋值给此指针,这样也就标志此指针为空指针,没有任何指针。

  int *p = NULL ; //成功定义一个空指针


万能指针void


void*又被我们成为万能指针,其原因为void *指针可以指向任意变量的内存空间。下面我们来参照代码看看:

void *p = NULL ;  //定义空指针 
  int a = 10 ;
  p = (void *)&a ; //指向变量时,最好转换为void *
  //使用指针变量指向的内存时,转换为int *
  *( (int *)p ) = 11;
  printf("a = %d\n", a);


const修饰的指针变量


有时候我们希望定义这样一种变量,它的值不能被改变,并且在整个作用域中都保持固定。例如,在游戏中,官方在设定物品掉落率的时候,将其设定的一定是一个定值,并且不希望玩家通过其终端去进行修改,那这个时候就需要定义一种变量,它的值不可以被修改了。为了满足这一要求,可以使用const关键字对变量加以限定。


在这里const修饰指针分为两种情况:


第一种 指向常量的指针

const int * p1 = &a这种情况下等价于int const *p1 = &a;也就是我们对*进行修饰,其指针指向内存区域不能修改,指针指向可以变 。


第二种 指针常量

int * const p2 = &a 修饰的是p2,也就是其指针指向不能变,但是指针指向的内存可以修改。


所以我们在平时编辑程序时,指针作为函数参数,如果不想修改指针对应内存空间的值,需要使用const修饰指针数据类型。

  int a = 100;
  int b = 1000;
  // 指向常量的指针
  //修饰* 指针指向内存区域不能修改,指针指向可以变
  const int * p1 = &a ; //等价于int const *p1 = &a;
  //*p1 = 111; //错误 指针指向内存区域不能修改
  p1 = &b; //ok 指针指向可以变
  //指针常量
  //修饰p2,指针指向不能变,但指针指向的内存可以修改
  int * const p2 = &a;
  //p2 = &b;  //错误  针指向不能变
  *p2 = 200;  //ok 指针指向的内存可以修改

在上一串代码中,其指针指向内存区域也就是我们通常认识的值不可以被改变,但是指针的指向可以改变,也就是指针可以从指向a变为指向b;反之亦然。  


指针和数组


数组名

通过之前的博客可知数组名字是数组的首元素地址,但它是一个常量不可以被修改。

  int a[] = { 1, 2, 3, 4 };
  printf("%p\n", a) ;
  printf("%p\n", &a[0]) ;
  //a = 10; //err, 数组名只是常量,不能修改


指针操作数组元素

定义数组时,要给出数组名和数组长度,数组名可以认为是一个指针,它指向数组的第 0 个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址。因为我们数组名就是数组首地址,所以我们就可以用我们的指针来操作数组元素。

#include <stdio.h>
int  main()
{
  int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 } ;
  int i = 0 ;
  int n = sizeof(a) / sizeof(a[0]) ;  //找出数组长度 
  for (i = 0; i < n; i++)
  {
    //printf("%d, ", a[i]); //数组表示 
    printf("%d, ", *(a+i));   //使用指针表示 
  }
  printf("\n");
  int *p = a; //定义一个指针变量保存a的地址
  for (i = 0; i < n; i++)
  {
    p[i] = i ;    //利用指针给数组赋值 
  }
  for (i = 0; i < n; i++)
  {
    printf("%d, ", *(p + i)); //利用指针输出 
  }
  printf("\n");
  return 0;
}

在上面的例子中我们不难看出,我们可以利用指针去操作我们的数组,将数组首地址赋给指针后,就可以用*去查找具体某一位的元素了。


*(a+i)这个表达式,a 是数组名,指向数组的第 0 个元素,表示数组首地址, a+i 指向数组的第 i 个元素,*(a+i) 表示取第 i 个元素的数据,它等价于 a[i]。


a 本身就是一个指针,可以直接赋值给指针变量 p。a 是数组第 0 个元素的地址,所以int *p = a;也可以写作int *p = &a[0];。也就是说,a、p、&a[0] 这三种写法都是等价的,它们都指向数组第 0 个元素,或者说指向数组的开头。


当然如果一个指针指向了数组,那么我们就称它为数组指针(Array Pointer)。数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关。


我们可以反过来想,对于p来说,p 并不知道它指向的是一个数组,p 只知道它指向的是一个整数,究竟如何使用 p 取决于程序员如何去进行操作了。


指针加减运算

在这里指针的加减运算并不是指的我们常规意义上的那种(1+1=2)类似于这种整数相加减的运算,在这里我们的计算是指内存的计算,也就是如果是一个int *,+1的结果是增加一个int的大小同理而言如果是一个char *,+1的结果是增加一个char大小。

#include <stdio.h>
int main()
{
  int a;
  int *p = &a;
  printf("%d\n", p);
  p += 2 ;  //移动了2个int
  printf("%d\n", p);
  char b = 0;
  char *p1 = &b;
  printf("%d\n", p1);
  p1 += 2 ; //移动了2个char
  printf("%d\n", p1);
  return 0;
}


这也是为什么在上面我们使用指针的加减法可以移动数组下标的原因,因为数组每个元素内存大小均相等,所以我们的指针在进行加减时对对应加减其内存大小,这样也就可以对应其元素的移动了。

指针减法也同样如此,下面给大家看个例子,如果这个例子可以看懂这部分知识你也就明白了。

#include <stdio.h>
int main()
{
  int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
  int i = 0;
  int n = sizeof(a) / sizeof(a[0]);
  int *p = a+n-1;
  for (i = 0; i < n; i++)
  {
    printf("%d, ", *p);
    p--;
  }
  printf("\n");
  return 0;
}

指针数组

指针数组,它也是数组,只是这个数组的每个元素都是指针类型。声明一个指针数组的方法如下:

int *p[10];  int *p[10]; // 声明一个指针数组,该数组有10个元素,并且每个元素都是一个指向int类型的指针

在上述声明中,由于 [] 的优先级比 * 高,所以 p 先与 [] 结合,成为一个数组 p[];之后由 int * 指明这是一个 int 类型的指针数组,数组中的元素都是 int 类型的指针。数组的第 i 个元素是 *p[i],而 p[i] 是一个指针。


所以我们来尝试一下指针数组的使用吧:

#include <stdio.h>
int main()
{
  //指针数组
  int *p[3];
  int a = 1;
  int b = 2;
  int c = 3;
  int i = 0;
  p[0] = &a;    //由于数组元素是指针,所以我们要传入地址
  p[1] = &b;
  p[2] = &c;
  for (i = 0; i < sizeof(p) / sizeof(p[0]); i++ )
  {
    printf("%d, ", *(p[i]));
  }
  printf("\n");
  return 0;
}

输出结果:11.png

多级指针

多级指针,听名字大概也可以听得出来,这就是一个套娃指针;也就是就是指针的指针的指针...,实际上也没那么复杂。C语言中允许有多级指针存在,但是我们在实际的程序中一级指针最常用,其次是二级指针,到了三级指针以及三级以上的指针时,我们不会过多的去使用了。


定义一个二级指针

int **q;

我们可以将int**q 分为两部分来看,即为 int* 和 (*q),对于后面 (*q) 中的“*”表示 q 是一个指针变量,而前面的 int* 表示指针变量 q 只能存放 int* 型变量的地址。所有对于二级指针甚至多级指针,我们都可以把它拆成两部分。首先不管是多少级的指针变量,它都是一个指针变量,指针变量就是一个“*”,其余的“*”表示的是这个指针变量只能存放什么类型变量的地址。


就比如我们定义一个三级指针:

int ***p = &q ;

在这里我们按照上面的方法去进行逐步拆分,  p的基类型就是 int** 型。而 q 的基类型是 int* 型,所以 &q 的基类型是 int** 型。所以 r 有三个“*”才能指向 q 的地址。三个“*”表示三级指针,即指针的指针的指针。三级指针需要三个“*”才能指向最终的内存单元。

  int a = 10 ;
  int *p = &a ; //一级指针
  *p = 100 ; //*p就是a
  int **q = &p ;
  //*q就是p
  //**q就是a
  int ***t = &q;
  //*t就是q
  //**t就是p
  //***t就是a

看上面这串代码,指针变量的“基类型”用来指定该指针变量可以指向的变量的类型,即该指针变量只能存放什么类型变量的地址。所以 int*p 表示 p 指向的是 int 型变量,也就是说里面只能放int类型的变量地址。这时的p表示a的地址,而*p等于a ;


好的现在我们向下继续看,到了二级指针这里,在这里为什么我们在存放&p的时候要使用两个**呢?前面我们知道,*p是我们的int类型,p是表示的a的地址,我们在存放p时使用int*类型去存放,那么当我们存放&p的时候,就要使用int**去存放了。


那下面我们同理,由上面可知,存p应该使用int*类型,存&p应该使用int**类型,同时q也等价于&p,那么存q就要使用int**类型,所以题目中存&q就理所当然的要使用int***类型啦。那么我们反过来看,t存的是&q,那么*t就是q了;而**t也就是*q也就p了;***t也是*p也是我们一开始设定的变量a了。


这也就是我们的多级指针了,也是我们在学习C语言时最大的拦路虎之一了,加油,相信你可以的!


指针和函数


为什么C语言中能够说形参的值改变,不影响实参的值,因为在程序运行的过程中,身为形参的静态局部变量m的值随着程序的运行在一直的改变,但是实参的m值却是一直的没有改变一直是,说明了,形参和实参同名的情况下,改变形参的值实参的值不变。


形参,顾名思义,形式上的参数,在函数定义中出现的参数可以看做是一个占位符,它没有数据,只能等到函数被调用时接收传递进来的数据;


实参,平常定义的变量,函数被调用时给出的参数包含了实实在在的数据,会被函数内部的代码使用 。


函数形参改变实参的值

在我们使用函数的时候,我们可能会向函数中传递数去进行操作,这时我们就要判断一下我们所传入的形参在经过函数运算之后,返回时是否可以成功改变其实参的值。

#include <stdio.h>
void swap1(int x, int y)
{
  int tmp;
  tmp = x;
  x = y;
  y = tmp;
  printf("x = %d, y = %d\n", x, y);
}
void swap2(int *x, int *y)
{
  int tmp;
  tmp = *x;
  *x = *y;
  *y = tmp;
}
int main()
{
  int a = 3;
  int b = 5;
  swap1(a, b); //值传递
  printf("a = %d, b = %d\n", a, b);
  a = 3;
  b = 5;
  swap2(&a, &b); //地址传递
  printf("a2 = %d, b2 = %d\n", a, b);
  return 0;
}

在上面的代码中我们提到了值传递和地址传递两个概念;值传递:实参将值传递给形参,形参值发生互换后的值不能回传给主调函数 ;地址传递:传递的是该元素地址或指针的值,而形参接收到的是地址,即指向实参的存储单元,形参和实参占用相同的存储单元,这种传递方式称为“参数的地址传递”。


在题目中我们也是使用&号去获取该元素的地址,这也是地址传递的一种方式,我们只有使用地址传递时,才可以通过形参去改变实参的值,通过值传递是不行的。例子中的值传递,ab值就不会发生改变。


数组名做函数参数

当我们使用数组名作为函数参数时,这时函数的形参就会退化为指针;这并不难理解,因为我们使用数组作形参的时候,我们需要传入数组首地址,而对于传入的地址,我们才上文也提到了,可以通过指针的方式去使其指向相关的元素,所以这时其形参就会退化为指针了。

#include <stdio.h>
void print_Arrary(int *a, int n)       //退化为指针
{
  int i = 0;
  for (i = 0; i < n; i++)
  {
    printf("%d, ", a[i]);
  }
  printf("\n");
}
int main()
{
  int a[] = { 1, 2, 3, 4, 5 };
  int n = sizeof(a) / sizeof(a[0]);
  //数组名做函数参数
  print_Arrary(a, n); 
  return 0;
}

指针做为函数的返回值

当我们使用指针作为函数的返回值时,这时我们应该与我们在使用指针时去联想,例如:

int *p = &a ;

我们在使用指针的时候需要接受的是元素地址,那么同理可得,当我们使用指针做为函数的返回值时,我们应该用函数返回地址可以。

#include <stdio.h>
int a = 10;
int *getA()    //注意创建函数类型
{
  return &a;    //函数返回地址
}
int main()
{
  *( getA() ) = 110 ;    //使用指针接收
  printf("a = %d\n", a) ;
  return 0;
}


运行结果:14.png


指针和字符串


字符指针

其实对于字符指针已经没有什么需要讲的了,因为我们在使用的时候与上文中提到的数组与指针的使用方法相同,只不过这里的数组时char类型的,我们在输入的时候应该注意字符所需要的单引号,其余我们的操作与指针对数组的操作是相同的。

#include <stdio.h>
int main()
{
  char str[] = "hello world";
  char *p = str;
  *p = 'm';
  p++;
  *p = 'i';
  printf("%s\n", str);
  p = "hello c++";
  printf("%s\n", p);
  char *q = "test";
  printf("%s\n", q);
  return 0;
}

相信通过指针与数组的学习你理解上面的代码并不困难,输出结果如下图: 15.png

const修饰的指针变量

我们在上文中提到过const关键字可以固定其变量,也提到了const修饰指针变量的两种情况,那么这里就用一个例子来带大家巩固一下我们刚才讲解的知识吧。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
  const int a = 10 ; //const修饰一个变量为只读
  char buf[] = "wijisjcmsiopjiofjcvs";
  const char *p = buf;
  // 等价于上面 char const *p1 = buf;
  //p[1] = '2'; //错误 
  p = "agdlsjaglkdsajgl"; //正确 
  char * const p2 = buf;
  p2[1] = '3';
  //p2 = "salkjgldsjaglk"; //错误 
  //p3为只读,指向不能变,指向的内存也不能变
  const char * const p3 = buf;
  return 0;
}

在这里我们来总结一下,当我们碰见const修饰指针的时候我们可以首先从左往右看,跳过类型,看修饰哪个字符,如果是*, 说明指针指向的内存不能改变,也就是*修饰什么,什么不能改变。


指针数组做为main函数的形参

main函数是操作系统调用的,它存在两个参数,即第一个参数标明argc数组的成员数量,argv数组的每个成员都是char *类型。其中,argv是命令行参数的字符串数组,而argc代表命令行参数的数量,程序名字本身算一个参数

int main(int argc, char *argv[]);

也就是因为main函数不能被其它函数调用, 不可能在程序内部取得实际值。所以main函数的参数值是从操作系统命令行上获得的。当我们要运行一个可执行文件时,在命令行键入文件名,再输入实际参数即可把这些实参传送到main的形参中去。


总结


好啦,感谢大家支持,如果对我的内容感兴趣的话,还请多多支持一下。



相关文章
|
18小时前
|
Java 程序员 Linux
探索C语言宝库:从基础到进阶的干货知识(类型变量+条件循环+函数模块+指针+内存+文件)
探索C语言宝库:从基础到进阶的干货知识(类型变量+条件循环+函数模块+指针+内存+文件)
5 0
|
21小时前
|
C语言
C语言实现猜数字游戏:代码详解与函数解析
C语言实现猜数字游戏:代码详解与函数解析
4 0
|
1天前
|
存储 缓存 算法
详解JVM内存优化技术:压缩指针
详解JVM内存优化技术:压缩指针
|
1天前
|
存储 编译器 C语言
C语言的联合体:一种节省内存的数据结构
C语言的联合体:一种节省内存的数据结构
6 0
|
1天前
|
C语言
C语言中的函数指针、指针函数与函数回调
C语言中的函数指针、指针函数与函数回调
6 0
|
1天前
|
存储 C语言
C语言中的多级指针、指针数组与数组指针
C语言中的多级指针、指针数组与数组指针
5 0
|
1天前
|
存储 C语言
C语言数组指针详解与应用
C语言数组指针详解与应用
8 0
|
1天前
|
存储 C语言
C语言中的指针
C语言中的指针
5 0
|
12天前
|
消息中间件 存储 Kafka
实时计算 Flink版产品使用问题之 从Kafka读取数据,并与两个仅在任务启动时读取一次的维度表进行内连接(inner join)时,如果没有匹配到的数据会被直接丢弃还是会被存储在内存中
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
4天前
|
存储 Java C++
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据,如局部变量和操作数;本地方法栈支持native方法;堆存放所有线程的对象实例,由垃圾回收管理;方法区(在Java 8后变为元空间)存储类信息和常量;运行时常量池是方法区一部分,保存符号引用和常量;直接内存非JVM规范定义,手动管理,通过Buffer类使用。Java 8后,永久代被元空间取代,G1成为默认GC。
11 2