(一).了解指针——内存和地址
要理解内存和地址我们以下两者对比:
1:我们平时上课要找到自己上课的教室,如果我们一个教室一个教室去找估计找很久都找不到,可见这样的效率之低,但如果我们知道了准确的楼层和楼号比如301,504,605等等这样准确的房间号,我们就可以迅速准确的找到教室的具体位置。
2:在一个数组中,我们也是通过下标来确定一个元素的。
同样 ,在内存中,我们也可以通过每个元素相应的地址来找到目标元素。所以,我们把内存划分为一个个内存单元,每个单元相当与一个编号(这个编号相当于房间号),在计算机语言中我们把这个编号称之为地址,C语言中把它叫做“指针”。
故我们可以理解为:内存单元编号=地址=指针
(二).指针变量和地址
2.1取地址操作符 &
在程序中,当我们创建了一个整型变量时,这时会开辟4个字节内存空间来存放,每个内存空间都有相应的地址,那我们如何得到它的地址呢?这时我们就会引入取地址操作符(&)。
#include<stdio.h>
int main()
{
int a = 5;
&a;
printf("%d\n", &a);
return 0;
}
这时运行结果就是a的地址。
2.2指针变量
通过取地址操作符 (&)得到的是地址是一个数值,为了方便存储和使用这时我们就会引用一个指针变量,用来存放地址。
注:指针变量是一个变量,专门用来存放地址。
2.3解引用操作符(*)
对于指针变量我们知道它是专门存放地址的变量,但是我们要想在输出结果上打印它的值该怎么办呢?这时我们就回引出一种操作符——解引用操作符(*)。假设我们有一个指针变量p,它里面存放的是地址,这时我们通过解引用*p就表示的是具体的值。
#include<stdio.h>
int main()
{
int a = 4;
int* p = &a;
printf("%d", *p);
return 0;
}
(三).指针变量的类型
3.1指针的解引用
指针的类型会决定指针解引用时有多大空间(即一次操作几个字节),例如:int*一次可以访问四个字节,而char*一次只能访问一个字节。
3.2指针+-整数
指针的类型会决定指针+-整数的结果。例如:int*类型指针+1跳过四个字节,而char*类型指针+1跳过一个字节。
3.3void*指针
void*指针是用来接收不同类型数据的地址的指针,当我们不知道是什么类型指针时我们就可以用void*来接收,但是,void*类型指针不能直接用来进行指针运算。
3.4数组指针变量
数组指针变量:能够指向数组的指针变量.
int *p1[10] -->p1是指针数组,存放指针的数组,简称指针数组。
int (*p2)[10] -->p2是指针变量,指向的是数组,简称数组指针。
3.5函数指针
形式:void(*)()
int(*pf)(int ,int)=&add --> pf是指针变量,指向的是函数地址,简称函数指针。
3.6函数指针数组
int(*pffarr[])(int ,int)-->*pffarr[]是指针数组,指向函数,简称函数指针数组。
(四).const变量
4.1const修饰变量
#include<stdio.h>
int main()
{
int m = 0;
m = 20;//可以改变
int const n = 10
n = 30;//不可改变
return 0;
}
这里我们就可以知道当变量值被const修饰时变量值就不可改变。
4.2const修饰指针变量
#include<stdio.h>
int main()
{
int a = 5;
int* const p = &a;//const修饰的是指针变量本身,p的值不可改变,
//但可以改变指针变量指向的内容
*p = 10;//即改变a的值是可以的
int const* p = &a;//const限制的是指针指向的内容,
*p = 50;//即改变a的值不可以的
return 0;
}
结论:当const修饰指针变量的时候
const放在*左边的时候,修饰的是指针变量指向的内容,指针指向的内容不可改变,但指针变量本身可以改变。
const放在*右边的时候,修饰的是指针变量本身,指针指向的内容可以改变,但指针变量本身不可以改变。
(五).野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
野指针成因:
指针未初始化:局部变量指针未初始化,默认为随机值
在这里我们可以访问修改指针变量本身的内存地址,但是不能解引用来访问变量中存储的垃圾值(也就是把垃圾值当作一个内存地址来解引用了),因为解引用垃圾值所代表的内存单元的控制权限并没有分配给此程序
指针越界访问:当指针指向的范围超出数组的范围时,就是野指针
指针指向的空间释放:当指针中地址已经被销毁\释放的时候,这个指针就成为了野指针
规避野指针的方法
1. 指针初始化
2. 小心指针越界
3. 指针指向空间释放即使置NULL
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性
(六).二级指针
二级指针:指向指针的指针。
一级指针与二级指针的比较:一级指针的值为地址,地址需要空间来存放,是空间就具有地址
,二级指针就是为了获取这一空间的地址。一级指针所关联的是其值(一个地址)名下空间里
的数据,这个数据可以是任意类型并做任意用途,但二级指针所关联的数据只有一个类型一个
用途,就是地址。
指针的用途:提供目标的读取或改写,而二级指针就是对于内存地址的读取和改写。
二级指针分为指向指针变量的指针和指向数组的指针。
#include<stdio.h>
int main()
{
int a = 10;
int* p1 = &a;
int** p2 = &p1;
printf("%d ", *(*p2));
return 0;
}
通过这个例子我们对二级指针就会有更深入的了解。
(七).指针的传址调用
当我们了解了一些关于指针的知识过后那我们怎样将它运用呢?
#include<stdio.h>
void swap(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d,b=%d\n", a, b);
swap(a, b);
printf("交换后:a=%d,b=%d\n", a, b);
return 0;
}
为什么这里调用了a,b的值进行交换但是a,b并没有交换呢?这时我们就要知道函数调用交换值的概念了。
函数调用交换值:当实参传递给形参的时候,形参只是一份临时拷贝,对形参的改变不会影响实参的。
那么我们只能通过指针调用地址来实现两个数的交换。
#include<stdio.h>
void swap(int *p1, int *p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d,b=%d\n", a, b);
swap(&a,&b);
printf("交换后:a=%d,b=%d\n", a, b);
return 0;
}
这样我们通过调用a,b两个数的地址进行交换,这种方法就叫传址调用。
传址调用能建立起函数和主函数之间的联系,能实现通过函数改变主函数值的目的。
(八). 数组名的理解
#include<stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
printf("&arr[0]=%p\n", &arr[0]);
printf("arr = %p\n", arr);
return 0;
}
首先通过这两个的对比说明了什么?很清楚arr和arr[0]的地址相同,说明数组名表示的是首元素的地址。
结论:任何地方数组名都表示的是数组首元素地址
注意:(有两个地方例外)
sizeof(数组名):这里的数组名表示的是整个数组,计算的是整个数组的大小,单位是字节。
&数组名:这里的数组名表示的是整个数组,取的是整个数组的地址。
#include<stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
printf("sizeof=%zd\n", sizeof(arr));
printf("&arr[0]=%p\n",&arr[0]);
printf("&arr[0]+1=%p\n", &arr[0]+1);
printf("&arr=%p\n", &arr);
printf("&arr+1=%p\n ", &arr+1);
return 0;
}
通过这个代码我们可以知道 sizeof中数组名的确时表示的是整个数组。
&arr[0]表示的是首元素地址,&arr[0]+1跳过一个整型4个字节;而&arr表示的是整个数组的地址&arr+1跳过的是整个数组20个字节。通过这个例子相信大家对两个数组名的例外有了更深的理解了吧!
(九).数组与指针的联系
9.1指针指向数组
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
for (int i = 0; i < sz; i++)
{
printf("%d ", p[i]);
}
printf("\n");
for (int i = 0; i < sz; i++)
{
printf("%d ", *(arr+i));
}
printf("\n");
for (int i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
通过这个例子:我们可以得出的结论是p与arr是等价的,arr是表示首元素地址,同样p指向的arr首元素的地址 即arr[i]=p[i]=*(arr+i)=*(p+i)。
9.2一维数组传参
#include<stdio.h>
void print(int arr[],int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
print(arr,sz);
return 0;
}
#include<stdio.h>
void print(int *p, int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", *(p+i));
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
print(arr,sz);
return 0;
}
一维数组传参的本质是传数组首元素地址。
9.3二维数组传参
9.3.1普通二维数组传参
#include<stdio.h>
void print(int arr[3][5], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{4,5,6,7,8},{7,8,9,0,1} };
print(arr, 3, 5);
return 0;
}
9.3.2指针二维数组传参
#include<stdio.h>
void print(int ((*arr)[5]), int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
printf("%d ", *(*(arr+i)+j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{4,5,6,7,8},{7,8,9,0,1} };
print(arr, 3, 5);
return 0;
}
用指针完成二维数组传参,因为数组名是数组首元素地址,用指针接收二维数组时我们可以把二维数组的每行元素当成一维数组的一个元素,而二维数组传递的地址就是首行元素的地址,第一行指针类型就是数组指针类型int(*)[]。