文章目录
1. 地址和变量
-
指针的作用
- 让程序简介、紧凑、高效、
- 有效地表示复杂的数据结构
- 动态分配内存
- 得到多于一个的函数返回值
-
地址:在计算机内存中,以字节为单元,每个字节都有一个编号,称为地址
-
如果我们定义
int i
,然后用&i
查看地址,得到的是i
的存储地址 -
变量:对程序中数据存储空间的抽象
2. 指针
2.1 基本声明
- 指针是内存单位的地址,专门用来存放地址的变量。
- 用法:
存储类型 数据类型 *指针变量名=地址
- 举例:
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int a = 10;
int *p=&a;
printf("&p: %p, sizeof: %ld\n", &p, sizeof(p));
printf("%p: %p\n", p, &a);
return 0;
}
Return,返回值告诉我,我的电脑是8个字节的,但是我发现我返回只有12位16进制,也就是只有48位而不是64位,产生了困惑。经查询得到解答。地址数值只有48位是表像,实际上它是64位的地址,这是当前的x86_64处理器硬件限制所致。因为目前面世的x86_64处理器的地址线只有48条,硬件要求传入的地址的48到63位必须与47位相同。因此有两段合法的地址空间,最直观的是0 - 0x00007fff ffffffff,另一段是0xffff8000 00000000 - 0xffffffff ffffffff。两段加在一起一共2^48 = 256TB,这就是当前处理器的寻址能力。但一般我们是见不到第二段地址的,因为操作系统一般使用低段地址,高段这部分需要你的机器至少有128TB以上的内存。
&p: 0x7ffc78ef9b90, sizeof: 8 \\8*8=64->64位的计算机
0x7ffc78ef9b8c: 0x7ffc78ef9b8c
2.2 指针的目标
-
指针指向的变量称为目标变量或指针的目标
-
假设
px=0x0012fe80
是x=-126
的指针,那么有以下三种方法可以拿到x
的值:*px=*(&x)=x
,在这里*
不在是之前声明是指针的作用,而是做取值的作用 -
例1:假设
px
是一个指针,则:px
:指针变量,内容是地址*px
:指针所指向的对象,内容是数据&px
:存放这个指针的地址,是个长亮
-
例2
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int a = 10;
int *p=&a;
printf("%d %d\n", a, *p); //10 10
return 0;
}
3. 指针的运算
3.1 指针的赋值运算
- 通过赋值运算符向指针变量送一个地址
- 赋的值的要求:必须是地址常量
double x=15, *px, *py;
px = &x;
py = px;
3.2 指针的算术运算
- 指针运算是对地址进行运算
- 指针运算的种类有限,它只能进行赋值运算、算术运算和关系运算
运算符 | 计算形式 | 意义 |
---|---|---|
+ | px+n | 向地址大的方向移动n个数据 |
- | px-n | 向地址小的方向移动n个数据 |
++ | px++或++px | 向地址大的方向移动1个数据 |
– | px–或–px | 向地址小的方向移动1个数据 |
- | px-py | 两个指针之间相隔数据元素的个数 |
3.2.1 指针与常量的加法与减法
- 举例
int a = 10, *p;
double b = 3, *q;
p = &a;
q = &b;
printf("p: %p p+2: %p\n", p, p+2);
printf("q: %p q+2: %p\n", q, q+2);
Return
p: 0x7fff26799fec p+2: 0x7fff26799ff4 \\在这里是查了八个字节,也就是两个int,所以+2是移动2个数据
q: 0x7fff26799ff0 q+2: 0x7fff2679a000 \\移动了16个字节,两个double的大小
- 注意
- 不同数据类型的两个指针实行佳见证书运算是没有意义的,因为加两个int是加四个字节,而加1个double就是八个字节。
- px+n表示的实际位置地址量是:
(px)+sizeof(px的类型)*n
- px-n表示的实际位置地址量是:
(px)-sizeof(px的类型)*n
3.2.2 两指针相减运算
- 两指针相减的结果值不是地址量,而是一个整数值,表示两指针之间相隔数据的个数,比如数组中
&a[3]-&a[0]=3
- 举例
int a[5] = {4, 8, 1, 2, 3};
int *p, *q;
p = a; //&a[0]
q = &a[3];
printf("q-p: %ld\n", q-p);
Return:3
3.3 指针的关系运算
- 两指针之间的关系运算表示它们指向的地址位置之间的关系。指向地址大的指针大于指向地址小的指针
- 指针与一般整数变量之间的关系运算没有意义,但可以和零进行等于或不等于的关系运算,判断指针是否为空。下面举个例子
int *p=NULL;
printf("%d %p\n", p, p); //return 0 (nil)
运算符 | 说明 |
---|---|
> | 大于 |
< | 小于 |
>= | 大于等于 |
<= | 小于等于 |
!= | 不等于 |
== | 等于 |
- 例子:通过指针给数组赋值
int a[5];
int *p = a, i;
for (i=0; i>N; i++)
scanf("%d", p++);
p=a
for (i=0; i<N; i++)
printf("%d ", *p++);
puts("");
4. 指针与数组
4.1 一维数组
-
数组的指针是指数组在内存中的起始地址,数组元素的指针是指数组元素在内存中的起始位置
-
访问第i+1个数组元素的方法:
x[i]
*(px+i)
*(x+i)
px[i]
- 可以通过
px++
或px--
来找第i+1个数组元素,但是不可以用a++
来找,会报错,因为a
是常量并不是地址变量
-
举例
int a[6] = {1,2,3,4,5,6};
int *p=a, *q=a+5; // *q=&a[5];
4.2 二维数组
- 首先通过2x2的二维数组了解二维数组在内存空间中的存储方式:
我们直观的中的二维数组是这样的
第1列 | 第2列 | |
---|---|---|
第一行 | a[0][0] | a[1][1] |
第二行 | a[1][0[] | a[1][1] |
在内存中的二维数组是下面这样的。a[0]
代表第一行,a[1]
代表第二行,因此二维数组常被称为行地址
行 | 内存 |
---|---|
a[0] ⇔ \Leftrightarrow ⇔*a | a[0][0] |
a[0][1] | |
a[1] ⇔ \Leftrightarrow ⇔*(a+1) | a[1][0] |
a[1][1] |
- 练习1:用一个指针遍历二维数组
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int a[2][2] = {{1,2},{3,4}};
int *p=a[0], i, n;
n = sizeof(a)/sizeof(int);
for (i=0; i<n; i++)
printf("%d ", *p++);
puts("");
return 0;
}
- 练习2:二维数组a, a+1与a[0], a[0]+1 的区别
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int a[2][2] = {{1,2},{3,4}};
printf("%p %p\n", a, a+1); // 0x7ffcad435160 0x7ffcad435168 移动了八个字节
printf("%p %p\n", *a, *(a+1)); // 0x7ffcad435160 0x7ffcad435168 移动了八个字节
printf("%p %p\n", a[0], a[0]+1); // 0x7ffcad435160 0x7ffcad435164 移动了四个字节
printf("%p %p\n", *a, *a+1); // 0x7ffcad435160 0x7ffcad435164 移动了四个字节
return 0;
}
4.3 行指针
- 用法:
存储类型 数据类型 (*指针变量名)[n];
- 原来指针
+1
都是前进1个数据,在这里因为有n(列数),又是行指针,所以+1
是前进n个数据,二维数组就是很好的一个例子 - 练习1:
+1
为前进三个数据
int a[2][3];
int(*p)[3];
- 练习2:访问取值的方法
p[row][col]
和*(*(p+row)+col)
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int a[2][3] = {{1,2,3}, {4,5, 6}};
int (*p)[3];
p = a;
printf("%d %p %p\n", a[1][1], a, a+1); //5 0x7ffefe3406a0 0x7ffefe3406ac
printf("%d %p %p\n", p[1][1], p, p+1); //5 0x7ffefe3406a0 0x7ffefe3406ac, 和a一样的用法
printf("%d %d %d\n", a[1][2], p[1][2], *(*(a+1)+2)); //6 6 6
return 0;
}
5. 指针数组
- 指针数组指若干个具有相同存储类型和数据类型的指针变量构成的集合
- 用法:
存储类型 数据类型 *指针数组名[大小]
- 思考:
- 指针数组占用的内存空间大小:
sizeof(p)
- 指针数组
- 指针数组占用的内存空间大小:
- 指针数组名表示的是数组的起始地址
- 举例
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int *p[3];
int a[] = {1,2,3,4,5,6};
p[0] = a;
p[1] = a + 1;
p[2] = a + 3;
printf("%d %d %d\n", a[0], a[1], a[3]);
printf("%d %d %d\n", *p[0], *p[1], *(p[2]));
return 0;
}
返回
1 2 4
1 2 4
- 例2:指针数组经常和二维数组结合在一起
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int a[2][3] = {{1, 4, 6}, {12, 2, 3}};
int *p[2];
p[0] = a[0]; //&a[0][0]
p[1] = a[1]; //&a[1][0]
printf("%d\n", a[0][1]);
printf("%d\n", *(a[0]+1));
printf("%d\n", *(p[0]+1));
return 0;
}
- 例3:用六种方式遍历二维数组(
*(p[row]+col)
代表二维数组的第row行第col列)
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int a[2][3] = {{1, 4, 6}, {12, 2, 3}};
int *p[2] = {a[0], a[1]};
int i, j;
printf("第一种写法: a[i][j]\n");
for (i=0; i<2; i++){
for (j=0; j<3; j++)
printf("%d ", a[i][j]);
puts("");
}
printf("第二种写法: *(a[i]+j)\n");
for (i=0; i<2; i++){
for (j=0; j<3; j++)
printf("%d ", *(a[i]+j));
puts("");
}
printf("第三种写法: *(*(a+i)+j)\n");
for (i=0; i<2; i++){
for (j=0; j<3; j++)
printf("%d ", *(*(a+i)+j));
puts("");
}
printf("第四种写法: p[i][j]\n");
for (i=0; i<2; i++){
for (j=0; j<3; j++)
printf("%d ", p[i][j]);
puts("");
}
printf("第五种写法: *(p[i]+j)\n");
for (i=0; i<2; i++){
for (j=0; j<3; j++)
printf("%d ", *(p[i]+j));
puts("");
}
printf("第六种写法: *(*(p+i)+j)\n");
for (i=0; i<2; i++){
for (j=0; j<3; j++)
printf("%d ", *(*(p+i)+j));
puts("");
}
return 0;
}
return,省略了遍历内容
第一种写法: a[i][j]
第二种写法: *(a[i]+j)
第三种写法: *(*(a+i)+j)
第四种写法: p[i][j]
第五种写法: *(p[i]+j)
第六种写法: *(*(p+i)+j)
1 4 6
12 2 3
6. 多级指针
6.1 基本用法
- 指向指针变量的指针,称为多级指针
- 用法:
存储类型 数据类型 ** 指针名
- 举例
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int m = 10;
int *p;
int **q; //二级指针
int ***qq; //三级指针
p = &m;
q = &p;
qq = &q;
printf("p: %p, &p: %p\n", p, &p); //p: 0x7ffcda64d38c, &p: 0x7ffcda64d390
printf("q: %p, &q: %p\n", q, &q); //q: 0x7ffcda64d390, &q: 0x7ffcda64d398
printf("%d %d %d %d\n", m, *p, **q, ***qq); //10 10 10 10
return -1;
}
6.2 多级指针运算
int **p; p+1;
移动一个int *变量
所占用的内存空间。再比如int ***p, p+1
就是移动一个int **
所占用的内存空间
6.3 多级指针和指针数组
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int a[] = {1, 2, 3};
int *p[2] = {&a[0], &a[1]};
int **q;
q = p; //&p[0]
printf("%d %d\n", a[0], a[1]);
printf("%d %d\n", *p[0], *p[1]);
printf("%d %d\n", **q, **(p+1)); //*q是p **q=*p=a[0]
return -1;
}
7. void指针和const修饰符
7.1 void指针
- 一般形式:
void *变量名称
- 一种不确定数据类型的指针变量,他可以通过强制类型转换让该变量指向任何数据类型的变量
void
在没有强制转换前不能进行数值运算- 常用在设计的时候告诉用户可以转换成用户任意想要用的变量
- 举例
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int m = 10;
double n = 3.14;
void *p, *q;
p = &m; //隐式转换不易理解
p = (void *)&m; //最好强制转换
q = &n;
printf("%d %d\n", m, *(int *)p);
printf("%d %d\n", m, *p); // 这句会报错 error: invalid use of void expression
q = (void *)&n;
printf("%.2lf %.2lf\n", n, *(double *)q);
return -1;
}
- 用指针遍历一维数组
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int a[] = {1, 2, 3, 4};
int i, n;
void *p;
p = a;
i = 0;
n = sizeof(a)/sizeof(int);
for (i=0; i<n; i++)
printf("%d ", *((int *)p+i));
puts("");
return -1;
}
7.2 const修饰符
- 用法:
- 变量:
const 数据类型 变量名=[表达式];
- 指针:
const 数据类型 *变量名=[表达式]
,限制通过指针改变目标的数值,但指针变量存储的地址值可以修改 - 指针:
数据类型 * const 变量名=[表达式]
,限制存储的地址值不能修改,但可以通过指针变量修改对应数值
- 变量:
- 常量化变量使变量的值不能修改
- 变量有
const
修饰时,若想用指针间接访问变量,指针也要有const
修饰 - 常量化的作用是限制通过指针改变目标的数值,但指针变量存储的地址值可以修改,下面这个例子就是不可取的
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int m = 10;
const int *p;
int * const q;
//int * const q=&m; 正确用法
const int * const r=&m; //这个值之后地址不能更改,地址对应的值也不能更改,只能给个初始值
p = &m; //正确用法
(*p)++; //会报错error: increment of read-only location ‘*p’
q = &m; //会报错error: assignment of read-only variable ‘q’
return -1;
}