2、指针

指针

计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用 4 个字节,char 占用 1 个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。

我们将内存中字节的编号称为地址(Address)或指针(Pointer)。地址从 0 开始依次增加,对于 32 位环境,程序能够使用的内存为 4GB,最小的地址为 0,最大的地址为0XFFFFFFFF

所有指针所占内存空间,在32位操作系统下是4个字节,在64位操作系统下是8个字节

指针变量及取址

#include<stdio.h>

int main(){

   int i = 10;
   int *p = &i;
   
   printf("int类型指针变量p的地址是:%#x\n",&p);
   printf("int类型变量i的地址是:%#x\n",p);
}


int类型指针变量p的地址是:0x62fe10
int类型变量i的地址是:0x62fe1c

img

p是一个指针变量,存储着变量i的地址,使用取址符&可以获取一个变量的地址,%#x表示以十六进制输出,并加前缀0x

指针p的类型必须和变量i的类型一致

指针变量p除了保存变量i的地址,自己也有地址信息

操作指针

解引用

解引用就是引用指针指向的变量值,使用符号*

#include<stdio.h>

int main(){

   int i = 10;
   int *p = &i;

   printf("指针p指向的变量值为:%d",*p);
    
   return 0;
}

指针p指向的变量值为:10

指针运算

利用这一点,我们可以利用指针操作数组,因为数组在内存中是一段连续的空间,而且数组变量的指针可以认为是指向数组的首个元素的一个指针变量,而这个指针变量+1以后,指针就会指向下一个元素,例如是int类型的数组,那么指针就会向后移动一个int的长度(4字节)

#include<stdio.h>

int main(){

   int arr[] = {1,2,3,4,5};
   int *p = arr;
   int len = sizeof(arr) / sizeof(arr[0]);

   for(int i = 0;i < len;i++){
      printf("%d\n",*p);
      p++;
   }

   return 0;
}

函数和指针

指针作为参数

值传递,不会改变入参变量的实际值,只是把值10传进了函数

#include<stdio.h>

void add(int a){
   a += 1;
}

int main(){

   int a = 10;
   add(a);
   printf("%d\n",a);//10
   
   return 0;
}

指针传递,会改变入参变量的实际值,因为传入函数的是这个变量在内存中的地址

#include<stdio.h>

void add(int *a){
   *a += 1;
}

int main(){

   int a = 10;
   add(&a);

   printf("%d\n",a);//11

   return 0;
}

指针作为返回值

C语言允许函数的返回值是一个指针(地址),我们将这样的函数称为指针函数

例如,返回比较两个字符串,返回较长的字符串

#include<stdio.h>
#include<string.h>

char *moreLong(char *str1,char *str2){
   int len1 = strlen(str1);
   int len2 = strlen(str2);
   return len1 > len2? str1 : str2;
}

int main(){

   char s1[] = "hello world";
   char s2[] = "hello c";
   char *s3 = moreLong(s1,s2);
   printf("%s",s3);

   return 0;
}

函数指针

一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。

函数指针的定义:returnType (*pointerName)(param list);

returnType 为函数返回值类型,pointerName 为指针名称,param list 为函数参数列表。参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称,这一点和函数原型非常类似。

由于可以声明一个指向函数的指针,那么这个指针就可以作为参数,使这个函数成为回调函数

#include<stdio.h>

int max(int a,int b){
   return a > b? a : b;
}

int main(){
   //定义函数指针
   int (*m)(int,int) = max;

   int a = 10;
   int b = 20;
   //通过函数指针调用函数
   int maxInt = (*m)(a,b);
   printf("%d\n",maxInt);

   return 0;
}

指针数组

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

指针数组的定义:dataType *arrayName[length];

[ ]的优先级高于*,该定义形式应该理解为,括号里面说明arrayName是一个数组,包含了length个元素,括号外面说明每个元素的类型为dataType *

#include<stdio.h>

int max(int a,int b){
   return a > b? a : b;
}

int main(){
   char s1[] = "hello";
   char s2[] = "world";

   char *strs[2];
   strs[0] = s1;
   strs[1] = s2;

   printf("%s\n",strs[0]);
   printf("%s\n",strs[1]);
   return 0;
}

空指针

在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针,NULL 指针是一个定义在标准库中的值为零的常量。

#include <stdio.h>
 
int main ()
{
   int  *ptr = NULL;
 
   printf("ptr 的地址是 %p\n", ptr  );
 
   return 0;
}

ptr 的地址是 0x0

在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。但按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。

野指针

在使用指针时,要避免野指针的出现: 野指针指向一个已删除的对象或未申请访问受限内存区域的指针。与空指针不同,野指针无法通过简单地判断是否为 NULL避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。

常见指针定义

定 义 含 义
int *p; p 可以指向 int 类型的数据,也可以指向类似 int arr[n] 的数组。
int **p; p 为二级指针,指向 int * 类型的数据。
int *p[n]; p 为指针数组。[ ] 的优先级高于 *,所以应该理解为 int *(p[n]);
int (*p)[n]; p 为二维数组指针。
int *p(); p 是一个函数,它的返回值类型为 int *。
int (*p)(); p 是一个函数指针,指向原型为 int func() 的函数。

内存管理

C 语言为内存的分配和管理提供了几个函数。这些函数可以在 <stdlib.h> 头文件中找到。

序号 函数和描述
1 void *calloc(int num, int size); 在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是0。
2 void free(void *address); 该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。
3 void *malloc(int num); 在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。
4 void *realloc(void *address, int newsize); 该函数重新分配内存,把内存扩展到 newsize

注意:void * 类型表示未确定类型的指针。C、C++ 规定 void * 类型可以通过类型转换强制转换为任何其它类型的指针。