C语言指针学习

C语言指针学习

1 指针是什么

1.1 初识指针

指针——通过记录内存位置(地址)来建立某个对象的副本

  • c程序中的各种对象——普通变量、指针(变量)、函数都在内存区有特定的存储位置,因此,如果要给对象建立一个副本,(在该对象的命名域之外),对其进行引用或修改,只需要得知该对象所占内存区的位置就行了。
  • 于是指针应运而生,其实质就是一个 记录内存地址的变量

1.2 指针的类型

1.2.1 有什么用?

前面说指针就是记录内存地址的,那类型有什么用?

  • 我们都知道,各种变量在内存中所占的字节数是不同的,比如 int占4个bytes,而char 占一个
  • 具体地说,一个 int a; 的值就应该由 &a,&a+1,&a+2,&a+3 这四个连续地址对应的值组合在一起得到。
  • 因此只是知道地址的话,这个指针就无法真正得知所指向对象到底是什么
  • 所以指针类型就指示了这个指针对应的地址要怎么“解读
    • “解读”方式 既包含上述的所占地址长度的信息,
    • 同时也包含 运算规则之类的信息。

extra

  • 由于指针的值就只是地址值,

  • 所以可以通过改变类型来改变“解读”,做到一些有趣的事,

  • 比如下述代码

  • unsigned int a=0x01020304;
    char *p=&a;
    for (int i=0;i<4;++i)
      printf("%d ",*(p+i));
    // output:
    // 4 3 2 1
    
  • 上述代码实现了,将一个unsigned int值每8个二进制位分开一段,再分别打印出来。

    • 注意,其中unsigned int的低位储存在较前的位置
  • 后面还有用在数组指针的用法,更加实用。

1.2.2 各类指针的定义
int *ptr_int,*ptr_int_2;// 普通指针
int **pp,***ppp;// 指向指针的指针,与指向指针指针的指针hhhh
								// 可以称作二级指针,三级指针
void *v;// void指针

char *a[100];// 指针数组
char (*b)[100];// 数组指针
char *(*c)[100];// 指针数组指针

int *vp[m];// 变长指针数组
int (*va)[m];// 变长数组指针

const int *d,*dd;// 不可间接修改所指对象值的指针,指向可改
								// 与写法 int const *d,*dd; 等价
int* const e;// 常指针,不可更改指向,但可间接修改所指对象值
const int* const ee;

int (*f0)();// 无参函数指针
int (*f)(int);// 函数指针
char* (*g)(char,int);// 函数指针

一些说明:

  • a不是指针,是数组,数组的每个元素都是 char指针 类型。
  • b不是数组,是指针,所指向类型为“长度为100的一维char数组”
  • 数组名本身也可看作一个常指针——所指对象为数组首元素,类型与数组元素相同。
    • sizeof(array) 中,array代表整个数组,sizeof 值为整个数组的内存占用,

2 指针怎么用

2.1 指针的初始化

int a,a2;
int *p=&a;// 定义时初始化
					// 普通变量用&来取地址值
int *p2;
(...)
p2=a2;// 定义后初始化

int **pp=p;// 指针值就是地址,不用加“&”
int ***ppp=pp;// 层层套娃

void *ptr=&a;				// 任何类型的指针可隐式转换为void指针类型
int *ptr2=(int*)ptr;// 而void指针需要显式地强制转换

int b[100];
int *p1=b+2;// 指向b的第三个元素
int *p2=&b[2];// 同上

int d1[100];
int (*d2)[10][10]=d1;// 解读转换,重构数组结构
int e2[10][10];
int (*e1)[100]=e2;// 降维

char s[]="POINTER";// 将字符串字面量拷贝给s,s的地址与字面量不同
char *c="POINTER";// 指向字面量首字母地址
									// 字符串字面量存储在只读区,无法通过c修改字符串值
c=s;// c重新指向s首元素,现在可通过c间接修改s数组值

char *d[]={"str1","str2","str3"};
	// 指向字面量的指针数组
	// 可视作二维数组使用

char *tmp=NULL*tmp2=0,*tmp3=((void*)0);// 闲置指针
char *danger;// 未初始化,野指针

野指针

  • 野指针有三种情况

    1. 未初始化
    2. 指针失效——所指向对象被释放:
      • 用malloc分配内存的元素被free() 释放
      • 或者,局部变量在离开局部命名域后自动被释放
        • 如 函数或循环内临时创建的变量
    3. 越界访问数组
  • 野指针非常危险,可能会损坏内存,造成程序崩溃、数据损坏等后果。使用指针时必须确保其指向有效内存,避免产生野指针。

    常用预防野指针的方法是对指针进行NULL检查,使用断言,内存跟踪工具等来检测和避免野指针。

    —— Claude AI

  • 即便暂时未确定指针指向,也最好用 NULL 初始化一下,避免产生野指针。

2.2 指针的运算

2.2.1 指针与整型变量

对于指针a与整型变量b

有运算—— a+b,a-b,a++,++a,a--,--a

a+b

  • 表示一个与a同类型的指针,它指向a所指变量之后第 b*sizeof(*a) 个字节的内存位置

  • 或者说,它指向a所指变量之后第 b 个类型为 type(*a) 的变量的地址

    • 比如,

    • int arr[]={0,1,2},*a=arr;
      // a	 指向 arr[0]
      // a+2 指向 arr[2]
      
  • 但若 a 为一个二维数组名,则规则更为复杂——

    • int arr[][2]={0,1,2,3};
      // arr	 指向 arr[0], 而 arr[0]指向arr[0][0],这里的 arr[0]可看作一个一维数组名
      // arr+1 指向 arr[1]
      	// arr 相当于一个指针指针
      
      int *a=arr;
      // a	 指向 arr[0][0]
      // a+1 指向 arr[0][1]
      // a+2 指向 arr[1][0]
      	//	 跟原来的规则相同
      
      int (*aa)[2]=arr[0];// aa不能定义为二级指针,这样将导致数组结构丢失
      // aa 	  指向 arr[0]
      // aa+1   指向 arr[1]
      

a++

  • 与一般变量的加加类似,
  • 这个表达式返回a的值(地址),然后执行 a=a+1

其余运算同理。

2.2.2 指针与指针

如果有两个同类型指针变量a和b,

则它们可以进行比较和作差,目的都是在一段连续存储结构(如数组)上得出所指向元素的先后顺序以及距离。

默认 a,b 指向同一个数组 arr——

int arr[]={5,4,3,2,1};
int *a=arr,*b=arr+4;
printf("a %s in the front of b",(a<b)?"is":"isn't");
printf("The length of arr is %d.\n",b-a+1);
			// a is in the front of b.
			// The length of arr is 5.

比较运算还有 <=,>,>=,==

2.3 指针的解引用

只知道地址的话,指针就没有意义了,

解引用操作则可以让我们间接操作所指对象——

int a=12;
int *b=a;
printf("the value of a is: %d\n",*b);// 解引用, *b完全可以用a替换
		// the value of a is: 12

int arr[]={0,1,2,3,4};
puts("arr list:");
for (int i=0;i<5;++i)
  	printf("%d ",*(arr+i));
	// 写法等价于 arr[i]
puts("");

int *pa=arr;
puts("arr again:");
for (int i=0;i<5;++i)
  	printf("%d ",pa[i]));
	// 写法等价于 *(pa+i)
puts("");

void *c=a;
printf("a = %d\n",*(int*)c);// void指针要先强制转换类型才能正常使用

2.4 指针的实际应用

上面有一些基本用法,你可能会问“既然 *b 跟 a 完全等价,那为什么不直接写成 a?”

见下面的例子——

1.函数传参

void swap(int *a, int *b)
{
    int tmp=*a;
    *a=*b;
    *b=tmp;
}
// swap函数实现了交换两个整型变量值的功能
// 不用指针的话,则无法在函数内改变外部参数值

// 另外,如果程序中有多处需要用到交换操作
// 这个函数可以让你少写很多次这样的代码

2.函数传参 之 传数组

// 将 a,b 矩阵相加
void add_to_from_1(int *mat1, int *mat2, int n, int m)
{
  int (*A)[n][m]=mat1,(*B)[n][m]=mat2;
  	// 重新获得二维的数组结构,让程序知道如何解读[i][j]的位置
  for (int i=0;i<n;++i)
    for (int j=0;j<m;++j)
    	(*A)[i][j]+=(*B)[i][j];
}
int a[][2]={0,1,2,3};
int b[][2]={1,3,2,4};
add_to_from_1(a,b,2,2);

//非常好写法
void add_to_from_2(int (*A)[COL], int (*B)[COL], int n, int m)
{
    for (int i=0;i<n;++i)
        for (int j=0;j<m;++j)
            A[i][j]+=B[i][j];
}
add_to_from_2(a[0],b[0],2,2);

// 又一种写法
// 在函数中解引用时的写法 符合数组使用习惯
// 但定义数组时略显麻烦
void add_to_from_3(int **A, int **B,int n,int m)
{
    for (int i=0;i<n;++i)
        for (int j=0;j<m;++j)
            A[i][j]+=B[i][j];
}
int **a=malloc(n*sizeof(int*));
for (int i=0;i<n;++i)
    a[i]=malloc(m*sizeof(int));
// 这样定义的a实际上相当于一个指针数组,具有二维结构
(...)	// b 的定义同 a, 然后初始化 a,b
add_to_from_3(a,b,2,2);

3.更改指针指向,提高代码复用率

int min(int a,int b)
{
  return a<b?a:b;
}
int max(int a,int b)
{
  return a>b?a:b;
}
// 计算a数组的极值
// type=0时求min,type=1时求max
int calc(int *a,int type)
{
  int (*f)(int,int)=
    !type ? min : max;
  
  int ret=a[0];
  for (int i=1;i<n;++i)
    ret=(*f)(ret,a[i]);
  
  return ret;
}

4.字符串相关

char a[100], b[100];
// input

char *p = a;
puts("b appears of a at positions:");
while ((p = strstr(p, b))!=NULL)
{
  printf("%d ",p - a);
  ++p;
}
// 上述代码用于检索 b 在 a 中出现的位置

总而言之,指针可以——

  1. 在命名域外操作对象(如函数传参、程序间协作)
  2. 提高代码复用率
  3. 结合字符串自带函数使用

3 其他

3.1 malloc 函数

头文件:#include <stdlib.h>

内部声明

void *malloc(size_t size);
// 声明占用连续 size个bytes 的栈空间,并返回首地址

可见malloc的返回值是 void*,所以使用时要 强制类型转换

int *a=(int*)malloc(sizeof(int) * 10);
// 声明10个int的空间
// a可以当作 int a[10]的a 来用,但其本质仍是指针

int **mat=(int**)malloc(sizeof(int*)*ROW);
for (int i=0;i<ROW;++i,linear+=COL)
	mat[i]=(int*)malloc(sizeof(int)*COL);
// 二维结构
// 可用 mat[i][j] 访问元素
// 注意,mat[0]这一行尾元素 与 mat[1]这行的首元素 这两个内存地址并不连续 —— 这与二维数组有所不同

malloc 实际上会声明比程序请求的空间更大的内存,

多余的内存位于malloc函数返回的首地址之前,这段内存将用于存储后面内存段的长度等信息

free

free(a)
// 释放 a 开始的内存段段空间
  // a 必须指向 malloc, calloc, realloc 声明的内存段的首地址
  // 否则 free 无法获取内存段长度等信息,无法成功释放内存
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值