目录
一.什么是指针?
1.地址、指针
如图,我们对内存空间进行简单的理解,将计算机的内存空间分成若干个区域:
每个区域大小为1个字节,并且为每个区域命名一个编号,每个编号的区域就是一个地址。而指针就是地址,我们说的指针通常指的是指针变量。
2.指针变量
我们可以通过取地址操作符(&)取出变量的内存其实地址,把地址可以存放到一个变量中,这个变量就是指针变量。
int main()
{
int n = 1;//初始化变量n,
//也就是在内存中开辟了一块4字节的空间,用于储存变量n。
int* p = &n;//定义一个指针变量p,
//将储存变量n的那块空间的起始地址存在指针变量p中。
return 0;
}
所以,指针变量是用来存放地址的变量。
指针的大小在32位平台是4个字节,在64位平台是8个字节。
二.指针的类型、基本作用
1.指针类型
我们知道变量有许多不同的类型,如:char、int 等。不出意外,指针也有相应的不同类型。
指针变量定义的方式为:type + * + 变量名
如:char* 类型的指针是为了存放 char 类型变量的地址、int* 类型的指针是为了存放 int 类型变量的地址。
除此外还有其他类型,如float*、double* 等,那么指针有这么多类型有什么用处呢?
2.指针 +- 一个整数
用一段代码更能直观的了解
int main()
{
int n = 10;
char* p_char = (char*)&n;
int* p_int = &n;
printf("%p\n\n", &n);
printf("%p\n", p_char);
printf("%p\n\n", p_char + 1);
printf("%p\n", p_int);
printf("%p\n", p_int + 1);
return 0;
}
结果如下:
由结果得知,将同一个变量地址存在不同类型指针变量中,由于存放的是起始地址,所以变量指向的地址相同。
但是不同类型的指针+1后,它们就变得不同,如char*类型+1后,地址也+1,但int*类型+1后,地址就+4,联想一下char和int类型所占用的字节大小,不难理解:
指针的类型决定了指针向前或者向后走一步有多大(地址的距离)。
3.指针的解引用
int main()
{
int n = 10;
int* p = &n;
printf("%d\n", *p);
return 0;
}
//最终结果为10
所以我们可以理解为*p就是n, *是解引用操作符。
接下来进行一串代码的调试,更直观一些,如图:
观察变量n内存区域的变化
n初始化后的内存
通过char*类型地址为n赋值0后,可见只修改了一个字节的地址;
通过int*类型地址为n赋值0后,可见n内存中剩余3个地址也发生了变化。
(若是在*p=0前面再将n赋值为原始值更为直观,可以直接观察到4个字节的变化)
所以:指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。 比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
三.野指针
概念: 野指针就是指针指向的位置是不可知的、随机的、不正确的、不明确的。
1.成因
a. 指针未初始化
int main()
{
int* p;
*p = 1;
return 0;
}
b. 指针越界访问
int main()
{
int arr[5] = { 0 };
int* p = arr;
for (int i = 0; i <= 10; i++)
{
*(p++) = i;
}//指针指向的范围超出数组arr的范围时,
//就属于越界访问,p是野指针.
return 0;
}
c. 指针指向的空间释放(NULL)
2.如何避免野指针
a. 指针初始化
b. 小心指针越界
c. 指针指向空间释放即使置NULL (p != NULL)
d. 避免返回局部变量的地址
e. 指针使用之前检查有效性: 利用assert
四.指针的运算
1.指针与整数
前面有提到过。
指针的类型决定了指针向前或者向后走一步有多大(地址的距离)。
2.指针与指针相减
在两个指针指向同一个空间为前提下,指针减指针得到两个指针间元素个数。
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int* p = arr;
printf("%d\n",&arr[9]-&arr[0]);
return 0;//结果为9
}
注:如一个数组,允许指向数组第一个元素的指针与指向数组末尾元素最后面的那个内存位置的指针比较,但是不允许与第一个元素地址前面的内存位置的指针进行比较。 因为数组每个元素的地址是该元素首个字节的地址。
五.二级指针
变量有地址,指针变量也有地址,也叫二级指针
int a=1;
int *p=&a; 一级指针,解引用*p
int **pp=&p; 二级指针,解引用**p
**pp 先通过 *pp 找到 p ,然后对 p 进行解引用操作, *p找到a
六.字符指针
int main()
{
char ch = 'w';
char* pc = &ch;
*pc = 's';
printf("%c\n", ch);
//结果为s
return 0;
}
利用指针修改字符型变量
int main()
{//char* str = "happy",会报错,要加const修饰
const char* str = "happy";//这里是把一个字符串放到str指针变量里了吗?
printf("%s\n", str);
printf("%c\n",*str);
return 0;
}
可以看出本质上str中存放的依然是首字符h的地址
int main()
{
char str1[] = "hello!";
printf("%c\n",*str1);
return 0;
}//结果为h
可以看出数组名即首元素地址
接着从这串代码中可以看出:
int main()
{
char str1[] = "hello!";
char str2[] = "hello!";
const char* str3 = "hello!";
const char* str4 = "hello!";
if (str1 == str2)
printf("str1 = str2 \n");
else
printf("str1 != str2 \n");
if (str3 == str4)
printf("str3 = str4 \n");
else
printf("str3 != str4 \n");
return 0;
}
str1和str2是分别初始化的,所以地址不同。但是str3和str4为什么地址相同?
答案是str3和str4指向的是一个同一个常量字符串。C/C++中会把常量字符串存储到单独的内存区域,当 几个指针。指向同一个字符串的时候,他们实际会指向同一块内存,所以地址相同。
七.指针数组
由指针组成的数组,只不过数组的元素是指针。
int*arr1[10]; //整形指针的数组,可存放10个整形指针
char*arr2[10]; //一级字符指针的数组,可存放10个字符型指针
char**arr3[10]; //二级字符指针的数组,可以存放10个二级字符型指针
八.数组指针
1.数组指针概念及定义
我们知道int *str[10]是指针数组
那么int (*str)[10]是数组指针,:str先和*结合,说明str是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以str是一个 指针,指向一个数组,叫数组指针。
注意:[ ]的优先级要高于*号的
2.数组名与&数组名
int main()
{
int arr[10] = { 0 };
printf("arr = %p\n", arr);
printf("&arr= %p\n", &arr);
printf("arr+1 = %p\n", arr+1);
printf("&arr+1= %p\n", &arr+1);
return 0;
}
&arr 的类型是: int(*)[10] ,是一种数组指针类型 数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40 。&arr 表示的是数组的地址,而不是数组首元素的地址。
注意:sizeof(数组名)中,虽然没有&符号,但也是表示整个数组!
除了sizeof(数组名)和&数组名表示的是整个数组的地址,其余数组名都代表首元素地址
那么,数组指针有什么用呢?
void print(int (*arr)[5], int row, int col)
{
}
int main()
{
int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
//可以数组指针来接收
print(arr, 3, 5);
return 0;
}
即接收二维数组传参(二维数组首地址)
九.数组和指针传参
1. 一维数组传参
#include <stdio.h>
void test(int arr[])
{}
void test(int arr[10])
{}
void test(int *arr)
{}
void test2(int *arr[20])
{}
void test2(int **arr)
{}
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}
上面函数接收方式都没问题
2 .二维数组传参
void test(int arr[3][5])//正确
{}
void test(int arr[][])//错误
{}
void test(int arr[][5])//正确
{}
void test(int *arr)//错误
{}
void test(int* arr[5])//错误
{}
void test(int (*arr)[5])//正确
{}
void test(int **arr)//错误
{}
int main()
{
int arr[3][5] = {0};
test(arr);
}//注意二维数组传参,函数形参的设计只能省略第一个[]的数字。
二维数组传参中传值用和自己本身相同的二维数组第一个[ ]可以省略,第二个不可以。
传址调用中,二维数组的地址是首元素地址,也就是第一行的地址,而第一行是一个一维数组,所以二维数组传参用一维数组指针来接收。
3.一级指针传参
一级指针传参,一级指针接收,如下:
void test(int* p2)//接收一级指针要用一级指针接收
{
}
int main()
{
int n = 10;
int*p = &n;
test(p);//两种传
test(&n);//参数方式
return 0;
}
4. 二级指针传参
二级指针传参,二级指针接收,如下:
void test(int**p2)//接收二级指针要用二级指针接收
{
}
int main()
{
int n = 10;
int*p = &n;
int**p1=&p;
test(p1);//两种传
test(&p);//参数方式
return 0;
}
十.函数指针
由下图:
可见Add()函数也有一个地址,那么函数指针应该怎么定义呢?
由此可见函数指针的定义,函数类型+(*+指针变量名)+(函数参数类型)=&函数名,参数名可以省略
其解引用时为(*+指针变量名)+(需要传递的参数)
数组可以直接使用数组名作为首元素地址,那么函数可以不使用取地址符号吗?
由此可见,在取函数的地址时,可以直接使用函数名。并且对于函数的解引用也可以直接使用指针变量名+(参数)形式
十一. 函数指针数组
存放函数指针的数组该如何定义呢?
p先和 [] 结合,说明 p是数组,是 int (*)(int,int) 类型的函数指针。
所以int(*p[2])(int, int)是一个可以存放2个int (*)(int,int)类型函数指针的数组。
通过访问函数指针数组不同下标,就可以实现访问改下标所对应的函数(函数地址)。
而&p就是一个函数指针数组的指针。
十二. 指向函数指针数组的指针
我们知道函数指针、函数指针数组,那么有没有指向函数指针数组的指针呢?肯定是有的。
指向函数指针数组的指针:是一个 指针,该指针指向一个数组,数组的元素都是函数指针。
如下图: