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;// 未初始化,野指针
野指针
-
野指针有三种情况
- 未初始化
- 指针失效——所指向对象被释放:
- 用malloc分配内存的元素被free() 释放
- 或者,局部变量在离开局部命名域后自动被释放
- 如 函数或循环内临时创建的变量
- 越界访问数组
-
野指针非常危险,可能会损坏内存,造成程序崩溃、数据损坏等后果。使用指针时必须确保其指向有效内存,避免产生野指针。
常用预防野指针的方法是对指针进行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 中出现的位置
总而言之,指针可以——
- 在命名域外操作对象(如函数传参、程序间协作)
- 提高代码复用率
- 结合字符串自带函数使用
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 无法获取内存段长度等信息,无法成功释放内存