一、多级指针
多级指针值得是一个指针指向的内容是另外一个指针。
1
一个整型指针
p
指向一个整型变量
a
的地址 , 那么指针
p1
则为一级指针
2
一个整型指针
p2
指向的是一个整型指针的地址
p1
,则
p2
为二级指针
3
一个指针
p3
它指向
p2
的地址,则
p3
为三级指针
4
............

实例:
#include <stdio.h>
int main(int argc, char const *argv[])
{
int a = 250;
int *p1 = &a;
int **p2 = &p1;
int ***p3 = &p2;
printf("&a:%p \t &p1:%p \t &p2:%p \t &p3:%p\n", &a, &p1, &p2, &p3);
printf("a:%d \t p1:%p \t p2:%p \t p3:%p\n", a, p1, p2, p3);
// 如何通过各级指针访问250的数据
printf("a:%d *p1:%d **p2:%d ***p3:%d\n", a, *p1, **p2, ***p3);
return 0;
}
万能指针拆解方法
任何的指针都可以分解成两部分:
第一部分: 明确指针的名字以及他是一个指针
第二部分: 明确该指针所指向的类型
// *p1 第一部分说明该变量名字为 p1 他是一个指针 ,
// char 第二部分确定了该指针应该用来指向一个字符数据的地址
char * p1 ;
// *p2 第一部分说明该变量名字为 p2 他是一个指针 ,
// char * 第二部分 确定了该指针应该用来指向一个字符数据指针的地址[(*p)的地址]
char ** p2 ;
// *p3 第一部分说明该变量名字为 p3 他是一个指针 ,
// int [10] ; 第二部分 确定了该指针应该用来指向一个整型数组的地址
int (*p3) [10] ;
// *p4 第一部分说明该变量名字为 p4 他是一个指针 ,
// int (int , float); 第二部分 确定了该指针应该用来指向一个
// 拥有整型返回以及需要一个整型+浮点型的参数的一个函数
int (*p4) (int , float);
注意:
不管指针的定义有多么复杂都可以分为两个部分,一个用来说明指针的名字以及他是一个指针,另一部分则说明该指针所指向的类型
以上
p1 p2 p3 p4
本质上没有任何区别,他们
都是一个指针
用来存放另外一个数据的地址
唯一的不同在于它所存放的地址所代表的数据的类型
p1
存放的是一个
char
类型的地址
p2
放的是指针的地址
p3
放的是一个数组的地址
p4
方的是一个函数的地址
二、void 型指针
概念: void 表示没有, 因此void 指针表示没有确定类型的指针,
通用类型指针
int * p ; // p 用来存放 整型数据的地址
char * p1 ; // p1 用来存放 字符类型的数据地址
void * p2 ; // p2 用来存放 未知类型的数据地址
注意:
void
型指针因为类型还未确定,因此不可以直接访问。
在访问
void
型指针的时候需要进行类型的转换(强制转换)
void
型指针因为类型不确定因此不可以直接进行加减运算
实例:
#include <stdio.h>
int main(int argc, char **argv)
{
int a=250;
char b=100;
void *p = &a;
/* 取出数据需要强制转换 */
printf("*p = %d\n",*(int *)p);
/* 取出数据需要强制转换 */
p = &b;
printf("*p = %d\n",*(char *)p);
// *p = 350 ; // [错误] 类型不确定无法对其所指向的内存进行修改
//*(int *)p = 350 ; // [正确] 在访问未知类型的指针之前需要进行类型的强转(给他一个明确的数据类型)
return 0;
}
实例:
// 使用 malloc 函数向系统申请空间 为 4 个字节 , malloc函数返回的指针是void *
// 可以通过一个已知类型指针来指向malloc申请的内存空间
int * p1 = malloc(4);
*p1 = 250 ;
printf("%d\n" , *p1 )
如下图,有很多的系统的函数或库函数他们的返回值是 void * 表示该函数在返回的时候不确定用户需要额类型是什么。
比如一下这几个函数,他们的主要功能功能都是帮助用户
申请内存空间
的, 由于函数的设计者无法预测用户拿到该内存后用来
存放 什么类型的数据
,所以返回的类型不做定义(void / 通用类型)。

void 关键字的作用:
修饰指针,表示该指针的类型是未知的
修改函数的返回值,表示该函数没有返回值
修改函数的形参,表示该函数不需要参数
三、const(只读) 指针
const 指针有两种
1. 常指针 (常量)
常指针表示该指针是一个常量,该指针的值不允许修改(该指针的指向不允许被改变)
指针指向不能变,但指向地址可以变(数组内容可以变)

char buf[] = "Hello";
char * const p = buf ; // 定义一个指针 p 并把他修饰为一个常量指针
// 定义的时候必须给他进行初始化
// 离开初始化以后就不允许再对该指针进行修改(不可以让他再指向其他地方)
// p = "Gec";//错误
*(p+1) = 'E' ; // [允许] 常指针所指向的内容是允许被修改的
总结:
常指针,它的指向必须在初始化阶段完成,初始化结束后
不允许修改他的指向。
可以通过该指针来
修改所指向的内存中的值
。
2. 常目标指针 (目标是一个常量)
const 修饰的是
目标
而并
非指针本身
,可以用来表示
不可被修改的变量
(保护变量不会被指针修改)。
指针指向可以变,但初始化所指向的内容不能变

//
常目标指针
char msg [] = "Hello";
char const * p1 = msg ; //定义一个指针名为 p1 并该指针指向的内容被修饰为常量, 因此不允许通过 p1 来修改msg 的内容
const char * p1 = msg ;
p1 = "Gec"; // 【允许】 指针的指向可以被改变
// *(p1+1) = 'z' ; // [错误] 对于p1 而言他的指向是个常量数据,因此不允许被修改。
// 指向的目标本身可以修改值
msg[2] = 'O' ;
总结:
常指针在编程中不多见,但是常目标指针非常常见。
常目标指针通常出现在函数的形参中,用来保护被传入函数中的数据不会被该函数修改。
在日常开发中可以多使用常目标指针来保护传递的各种指针类型的参数。
最常见的例子:
1
char
const
*
argv
[]
--->
常目标 指针 数组
2
因此
argv
中的所有字符串都是只读的,它不允许被修改, 保护了传递到函数中的参数不会被修改。
被const 修饰的指针一般情况下用来保护数据本身。
练习:
尝试使用二级指针来指向一个二维数组。通过该指针访问数组里面的数据。
#include <stdio.h>
int main(int argc, char **argv)
{
char buf[]="Hello";
char * const p = buf;
char const * p1 = buf;
printf("p=%s\n",p);
/* 指针p的值不能修改 */
//p++;
/* 指针p指向的内容可以修改 */
*p='d';
printf("p=%s\n",p);
/* 指针p1指向的内容不可以修改 */
//*p1='f';
/* 指针p1的值能修改 */
p1++;
printf("p1=%c\n",*p1);
return 0;
}
四、指针数组与数组指针(存的都是地址)记住*(p+i)=p[i],[ ]自带解引用
4.1 概念
1. 指针数组:是指一个数组里面装着指针,也即
指针数组是一个数组(int *a[i]的集合体
定义形式:int *a[10];
说明:[]的优先级高于*,该定义形式应该理解为:int * (a[10]);括号里面说明a是一个数组,包含了10个元素,括号外面说明每个元素 的类型为int *。
如图所示:

2. 数组指针:是指一个指向数组的指针,
它其实还是一个指针(*p),只不过是指向数组而已;
既:内部是多个指针的集合体,既*p的集合体。*p存数组内容的地址
[ ]可以抵消一个*号,所以有 *(p+i)=a[i];

定义形式:int (*p)[10]; 其中,由于[]的优先级高于*,所以必须添加(*p).
说明:括号中的*表明 p 是一个指针,该指针变量保存的是一个数组的首地址,它指向一整个数组,数组的类型为int[10],这正是 a 所包 含的每个一维数组的类型。
在32位系统下任何类型的指针所占内存大小都为4个字节,至于它指向的数组占多少字节,具体要看数组的大小。总之,数组指针即为 “指向数组的大小”。
总结:主要看后面的两个字是什么(前面是修饰作用),因此指针数组是数组,而数组指针是指针。
4.2 指针数组和数组指针的使用
1. 指针数组的使用
(1)最简单的使用方式:
#include <stdio.h>
int main()
{
int a = 16, b = 932, c = 100;
//定义一个指针数组
int *arr[3] = {&a, &b, &c}; //也可以不指定长度,直接写作 int *arr[]
// parr 是指向数组 arr 的指针,确切地说是指向 arr 第 0 个元素的指针,
//它的定义形式应该理解为int *(*parr),括号中的*表示 parr 是一个指针,括号外面的int *表示 parr 指向的数据的类型。
// arr 第 0 个元素的类型为 int *,所以在定义 parr 时要加两个 *。
int **parr = arr;
printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]); // 16, 932, 100
printf("%d, %d, %d\n", **(parr + 0), **(parr + 1), **(parr + 2)); // 16, 932, 100
return 0;
}
结果

(2)对多个字符串进行处理:
#include <stdio.h>
int main()
{
char *str[3] = {
"Hello",
"Teacher",
"Won"};
printf("%s\n%s\n%s\n", str[0], str[1], str[2]); //推荐使用这种技巧,提高阅读性
printf("%s\n%s\n%s\n", *str, *(str + 1), *(str + 2)); //难以解读,常见在笔试题
return 0;
}
结果

2.数组指针的使用
(1)对于一维数组:
#include <stdio.h>
int main()
{
char a[5] = {'A', 'K', 'C', 'G', 'L'};
char(*p)[5] = &a; //&a代表的是整个数组的首地址
//char (*p)[5]=a;//a代表的是数组首元素的地址,但是错误的赋值!!!!!!
printf("%c %c %c", **p, *(*p + 1), *(*p + 3)); //输出:A K G
return 0;
}
说明:
a代表的是第一个元素的地址,即:&a[0],而&a代表的是整个数组的地址。a+1代表的是下一个元素的地址,即&a[1],而&a+1 代表的是向后移动5个字节的地址。
p=&a,*p即为第一个元素的地址,再**p即输出了第一个元素。同理,*(*p+1),*(*p+3)输出第二个元素,第四个元素。
此处虽然&a地址值和光写一个a时所代表的值一样,但其意义却不一样(主要体现在他的类型上,代表的是一整个一维数组的地 址,要赋给一个指向数组的指针)
(2)对于二维数组:
二维数组用行指针定义 ,p++操作会使p指向下一行的首地址,这是因为p是行指针,指向的是一行。我们可以用sizeof(*p)测试p 指向的内容的大小:
#include <stdio.h>
int main()
{
int a[3][4] = {{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}};
int(*p)[4]; // p指向数组 a 的开头,也即第 0 行;p+1前进一行,指向第 1 行。
int i, j;
p = a;//二维数组可以这样赋值,区别一维数组的赋值
for (i = 0; i < 3; i++)
{
for (j = 0; j < 4; j++)
printf("%2d ", *(*(p + i) + j)); //*(*(p+i)+j)表示第 i 行第 j 个元素的值。
printf("\n");
}
/*
0 1 2 3
4 5 6 7
8 9 10 11
*/
return 0;
}