指针
指针
什么是指针(pointer)?
存储其他变量地址的变量类型,称为指针。
例如:我们 & 称为取地址符,它取的地址就是一个指针类型,也就是说所有的地址都可以看作指针类型。但是在 C 语言中,所有的变量,都必须严格声明其数据类型,包括指针,也必须区分它所指向的数据是什么类型。因此,指针依据其存储的地址是何种变量的地址,可以分为 char指针、int型指针,double型指针等等。
由于 C++ 是强类型的语言,指针其实包含两个内容:
- 变量的起始地址
- 该变量占用的字节数
计算机中每个字节对应一个地址,字节是计算机最小的寻址单位。
不同的变量,占用的字节数可能不同,因此,指针必须也要有对应的类型。例如:
int *为int型指针,每个变量占用 4 个字节char *为char类型指针,每个变量占用 1 个字节double *为double类型指针,每个变量占用 8 个字节long long *为long long类型指针,每个变量占用 8 个字节
指针的声明
声明一个指针,需要引入一个特殊的符号 *,把它放在变量名前,说明它不是一个存储该类型数据的普通变量,而是存储该类型数据的地址的指针。
例如:
#include <stdio.h>
int main() {
int *a;//声明一个int型指针a,专门用于存储某个int类型变量的地址
int b = 5;
a = &b;//将b的地址赋值给a,我们称为a指向b
// %p 是将变量值用 16 进制表示,通常是用来表示地址的
printf("%p %p", a, &b);//输出两个值相等
return 0;
}
指针的解引用
请大家注意,刚才我们声明指针时,使用了 *,它在声明语句中只是表示该变量是一个指针类型。
其实 * 符号还可以用在引用指针所指向的数据值的时候,我们直接将 * 放在变量名前,可以得到该指针指向的那个变量的数值。
例如:
#include <stdio.h>
int main() {
int *a;
int b = 5;
a = &b;
*a += 5;//将a所指向的变量值加5
printf("%d %d", *a, b);//输出两个值相等
return 0;
}
我们都知道,数组类型就是一组相同的数据排列在一起。它们的地址是紧挨在一起的。例如:
#include <cstdio>
char a[4];
int main() {
printf("%p %p %p %p", a, a + 1, a + 2, a + 3);
return 0;
}
//运行结果如下:
//000000000040c030 000000000040c031 000000000040c032 000000000040c033
由上面的例子可知,字符数组 a 的 4 个元素的地址是相邻的,其中 a[0] 的地址最小,往后依次增大,每次增加 1,这个 1,代表一个字节 (Byte)。这也证明了,一个字符变量占用一个字节的空间。
我们再来看一下 int 型数组元素的地址,如果猜测不错的话,它们之间两个相邻元素的地址应该差 4,表示一个 int 类型占用 4 个字节的空间。
#include <cstdio>
int a[4];
int main() {
// %p 是将变量值用 16 进制表示,通常是用来表示地址的
printf("%p %p %p %p", a, a + 1, a + 2, a + 3);
return 0;
}
//运行结果如下:
//000000000040c030 000000000040c034 000000000040c038 000000000040c03c
注意:不同的电脑运行的地址可能不同,但地址之间的差值一定都是 4。
果然,相邻元素的地址相差为 4。注意,最后的 3c,为 16 进制,c 代表 12。
其实,整个计算机内存就可以看成是一个巨大的数组。只不过这个巨大的数组不需要我们去声明,而它存储的元素类型也各不相同,我们声明的所有的变量、数组、字符串,甚至是代码,都是存在这个巨大的数组之中。
那么,我们有没有方法像访问数组元素一样去访问内存中的各个地址对应的元素呢?
当然是有的,就是指针。
通过 * 关键字可以声明一个指针变量。由于C\C++ 为强类型的语言,因此强制要求每个指针变量必须说明其指向何种数据类型,以便在取数据的时候能够知道要连续取多少个字节。例如:
int *p1; //声明了一个专用于指向 int 变量地址的指针 p1
char *p2; //声明了一个专用于指向 char 变量地址的指针 p2
double *p3; //声明了一个专用于指向 double 变量地址的指针 p3
...
以上指针的名字不带'*',只是声明时在前面加上'*',以表示它是一个指针类型,存的是地址而不是普通变量数据。
在使用指针前,我们需要先给指针赋值。指针里存储的应该是它所指向的变量的地址。
应该通过对某个变量加取地址符,将其地址赋值给指针;或者直接将某个数组的数组名赋值给指针;绝不允许直接将某个地址数值赋值给指针!!!
#include <cstdio>
int a[4] = {1, 2, 3, 4};
int main() {
//将数组名 a 赋值给指针 p1,将 b 的地址赋值给指针 p2
int *p1 = a, b, *p2 = &b;
printf("%p = %p\n%p = %p", a, p1, &b, p2);
return 0;
}
//运行结果如下:表明指针中存的的确是对应变量的地址
//0000000000408010 = 0000000000408010
//000000000068fe0c = 000000000068fe0c
#include <iostream>
using namespace std;
int main() {
int *p = 1000;//错误,不允许直接用数字常量给指针变量赋值
return 0;
}
/*
error: invalid conversion from 'int' to 'int*' [-fpermissive]
int *p = 1000;
*/
当我们需要取出指针所指向的那个元素的数值时,又一次需要 * 出场,在指针前加上 * 即可获得其所指向的变量数值,该操作称为解引用。
#include <cstdio>
int a[4] = {1, 2, 3, 4};
int main() {
int *p1 = a, b, *p2 = &b;
b = 9;
//除了通过'*'来访问数组元素外,p1还可以像数组名那样通过方括号的方式访问数组元素
printf("%d = %d, %d\n%d = %d", a[2], *(p1+2), p1[2], b, *p2);
return 0;
}
//运行结果如下:表明指针中存的的确是对应变量的地址
//3 = 3, 3
//9 = 9
数组中的指针
-
数组名即为数组的首地址
-
不论一维数组还是二维数组,在内存中均是按照一维的方式存储。(例如 x86,x64 架构,均是行优先,即逐行存储元素)
-
指针加 1,地址即加上该元素对应的字节数
-
二维数组可以看成数组的数组(每一行是一个数组,有多行),因此,二维数组的数组名是一个指向指针的指针,称为二级指针
#include <iostream>
int a[5][3];
int main() {
//a是一个二维数组名,数组的数组,也就是指针的指针,二级指针
//如下,p是一个指向由一维数组作为元素的一个数组
int (*p)[3] = a;
//输出该二维数组中每一行的首地址,p+1,即为第一行的首地址
//p是整个二维数组,也是第0行的首地址
//由于每行有3个int变量,因此两行之间的地址相差为 12
for (int i = 0; i <= 4; ++i) printf("%p\n", p+i);
return 0;
}
/*
000000000040c040
000000000040c04c
000000000040c058
000000000040c064
000000000040c070
*/
二维数组的存储
在内存中,地址是一维的。二维数组通常是按照行优先的方式存储,即从左到右、从上到下将元素逐个排入地址,例如int s[2][4] = {{1,2,3,4}, {5,6,7,8}}; 这 8 个数在内存中排列的情况如下:
| 地址 | 元素值 |
|---|---|
| \(10000\) | 1 |
| \(10004\) | 2 |
| \(10008\) | 3 |
| \(10012\) | 4 |
| \(10016\) | 5 |
| \(10020\) | 6 |
| \(10024\) | 7 |
| \(10028\) | 8 |
#include <iostream>
using namespace std;
int main() {
int s[2][4] = {{1,2,3,4}, {5,6,7,8}}, *p = (int *)s;
for (int i = 1; i <= 8; ++i) {
cout << *p << " ";
++p;
}
}
// 输出:1 2 3 4 5 6 7 8
其中,6、7 两行可以合并为 1 行:
#include <iostream>
using namespace std;
int main() {
int s[2][4] = {{1,2,3,4}, {5,6,7,8}}, *p = (int *)s;
for (int i = 1; i <= 8; ++i) {
cout << *(p++) << " ";
}
}
总结一下:
int s[4], *p = s;这里p存储的是数组s的起始地址,即s[0]的地址;p+1即为s[1]的地址,p的地址和p+1的地址,差了 4 个字节;- 指针和数组名的区别在于,数组名的值是不能更改的,例如
++s是不允许的;- 当我们用指针指向一个二维数组的首地址时,通常需要用二级指针,因为二维数组是“数组的数组”,它的第一维(行)每个元素可以看成一个一维数组(对应的一行),
int s[5][5], **p = s;,p[1]就指向数组s的第一行;- 如果还想像遍历一维数组那样,逐个元素地遍历二维数组,就需要将二维数组的地址强转为一级指针类型,
int s[5][5], *p = (int *) s;,相当于将二维数组s转换为了一个有5*5个元素的一维数组p,元素按内存中地址的升序排列。
例题:压缩技术
#include <iostream>
using namespace std;
const int N = 207;
int main() {
int n, x, a = 0;
cin >> n;
int s[n][n], *p = (int*)s;
while (cin >> x) {
for (int i = 1; i <= x; ++i)
*(p++) = a;
a ^= 1;
}
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j)
cout << s[i][j];
cout << "\n";
}
}
当然本题也可以不用指针,如下方式完成:
#include <iostream>
int main() {
int bc, g, hgs = 0, h = 0;
bool flag = false;
scanf("%d", &bc);
do {
scanf("%d", &g);
for (int i = 1; i <= g; ++i) {
printf("%d", flag);
if (++hgs == bc) {
++h;
puts("");
hgs = 0;
}
}
flag ^= 1;
} while (h < bc);
}
结构体中的指针
结构体中当然也会用到指针,例如:
#include <cstdio>
struct Stu {
//以下称为结构体成员变量
char name[37];
int ch, ma, en;
};
int main() {
Stu a = {"XiaoMing", 99, 100, 99}, *pa = &a;
printf("%s %d %d %d\n", a.name, a.ch, a.ma, a.en);
//指针访问结构体的成员变量时,在指针后加”->“即可引用成员变量名。
printf("%s %d %d %d", pa->name, pa->ch, pa->ma, pa->en);
return 0;
}
//运行结果如下:
//XiaoMing 99 100 99
//XiaoMing 99 100 99
当然,在结构体内部,也可以声明一个结构体本身的指针,例如:
#include <cstdio>
struct Stu {
char name[37];
int ch, ma, en;
//一个指向Stu本身类型的指针
Stu *next;
};
int main() {
Stu a = {"XiaoMing", 99, 100, 99}, *pa = &a;
Stu b = {"XiaoFang", 100, 100, 95}, *pb = &b;
a.next = pb, b.next = pa;
printf("%s 的同桌是 %s.\n%s 的同桌是 %s.", pa->name, pa->next->name, pb->name, pb->next->name);
return 0;
}
//运行结果如下:
//XiaoMing 的同桌是 XiaoFang.
//XiaoFang 的同桌是 XiaoMing.
注意:你绝不能在结构体内部声明Stu类型的变量,只能声明指针。
因为一个指针永远只占用一个字长的空间(什么是字长?32 位系统的字长就是 32 位,即 4 个字节。我们现在几乎都是 64 位系统,字长均为 8 个字节)。而结构体本身的空间大小由其内部的成员变量所占用的空间决定,因此,如果在结构体内部又声明结构体本身的变量,那么编译器将无法计算这个结构体占用的空间,会因此而报错!例如:
#include <cstdio>
struct Stu {
char name[37];
int ch, ma, en;
Stu next;
};
int main() {
return 0;
}
//代码报错:
//5 6 E:\codes\tt.cpp [Error] field 'next' has incomplete type 'Stu'
//2 8 E:\codes\tt.cpp [Note] definition of 'struct Stu' is not complete until the closing brace
当编译器准备计算结构体 Stu 所占的空间数时,会发现假设 Stu 应占的字节数为 \(x\),那么 \(x\) 是由 37 个字节的 name 数组,和 3 个 4 字节的 int 型变量,以及一个 Stu 变量构成,其计算式为 \(x = 37 + 3\times 4 + x \gt x\),导致循环计算,出现矛盾,无法算出 \(x\) 的值。这就是为什么不允许在结构体内声明一个该结构体本身变量的原因。
结构体指针的应用
1. 链表
#include <cstdio>
struct Stu {
char name[37];
int ch, ma, en;
Stu *next;
};
int main() {
//构造一个 a->b->c->null 的链
Stu a = {"Ada"}, b = {"Bob"}, c = {"Cindy"};
a.next = &b, b.next = &c, c.next = 0;
for (Stu *i = &a; i; i = i->next)
printf("%s ", i->name);
return 0;
}
//运行结果:Ada Bob Cindy
有时,我们事先可能也无法知道需要多少个元素,因此,有了指针以后,我们可以动态的根据需要不断地加入新元素。要知道,即使是数组,也不允许超出其设定的元素个数,但指针可以一个个地源源不断地加入新元素,这种操作称为“动态存储”,正是指针的魅力之一。
每次申请一个新结构体空间的代码如下:new Stu;,该语句会在内存的动态存储区(而不是函数空间)开辟一个新的变量,并返回其地址。
需要注意的是:动态申请的空间并不是连续的,前后两次 new Stu 返回的地址可能相距很远,这一点与直接在函数栈上开辟数组空间的方式是不同的。
#include <cstdio>
#include <cstring>
struct Stu {
char name[37];
int ch, ma, en;
Stu *next;
};
int main() {
char name[37];
//head称为头结点指针,通常不存放实际元素,last指向最后一个元素的结点
//初始时,last与head值相同
Stu *head = new Stu, *last = head;
head->next = 0;
while (~scanf("%s", name)) {
//只要计算机内存还有空间,就随时可以再新生成一个结构体
Stu *p = new Stu;
strcpy(p->name, name);
p->next = 0;
if (!head->next) head->next = last = p;
else last->next = p, last = p;
}
for (Stu *i = head->next; i; i = i->next)
printf("%s ", i->name);
return 0;
}
/*
输入内容:
X1
X2
X3
X4
X5
^Z
输出内容:X1 X2 X3 X4 X5
*/
2. 循环链表
既然可以连成链,那么再往前一步,我们让它首尾相接,就可以围成一个圈。
#include <cstdio>
#include <cstring>
struct Stu {
char name[37];
int ch, ma, en;
Stu *next;
};
int main() {
char name[37];
Stu *head = new Stu, *last = head;
head->next = 0;
while (~scanf("%s", name)) {
Stu *p = new Stu;
strcpy(p->name, name);
p->next = 0;
if (!head->next) head->next = last = p;
else last->next = p, last = p;
}
//首尾相接,将last指向第一个实际元素
last->next = head->next;
for (Stu *i = head->next; i; i = i->next)
printf("%s ", i->name);
return 0;
}
//输入同上,输出时为死循环
3. 链表的删除
- 通过使用
delete p;语句实现对指针 \(p\) 指向的空间的删除; - 通过使用
delete []a;语句实现对数组 \(a\) 的删除,其中数组 \(a\) 是通过 new 生成的。
链表删除时,要注意两件事:
-
要记得回收空间
-
不要因为回收空间,影响了循环遍历
#include <cstdio>
#include <cstring>
struct Stu {
char name[37];
int ch, ma, en;
Stu *next, *pre;
};
int main() {
char name[37];
Stu *head = new Stu, *last = head;
head->next = 0;
while (~scanf("%s", name)) {
Stu *p = new Stu;
strcpy(p->name, name);
p->next = 0;
p->pre = last;
if (!head->next) head->next = last = p, p->pre = p;
else last->next = p, last = p;
}
last->next = head->next, head->next-pre = last;
//输入要删除的人的姓名
scanf("%s", name);
for (Stu *i = head->next; i; i = i->next) {
if (strcmp(name, i->name)) continue;
i->pre->next = i->next;
i->next->pre = i->pre;
//先将i赋值给t
Stu *t = i;
if (i == head->next) head->next = i->next;
//将i变为其pre(前趋),保证下次循环i->next可以正常进行
i = i->pre;
//delete 用来删除指针所指向的空间
delete t;
break;
}
for (Stu *i = head->next; i; i = i->next)
printf("%s ", i->name);
return 0;
}
练习
传送门:约瑟夫问题
#include <iostream>
struct Node {
int id;
Node *nx, *pr;
//构造函数,没有返回值,名字与结构体名字相同
//默认为无参构造
Node() {}
//根据具体情况,可改为含参构造
Node(int x): id(x) {}
} head, *tail;
int main() {
int n, k;
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i) {
Node *t = new Node(i);
if (i == 1)
//首元素自己首尾相接
head.nx = tail = t, t->pr = t->nx = t;
else {
//处理尾部指针
tail->nx = t, t->pr = tail, tail = t;
//处理head指针
t->nx = head.nx, head.nx->pr = tail;
}
}
for (Node *i = head.nx; i != i->nx; i = i->nx) {
for (int j = 1; j < k; ++j) i = i->nx;
i->pr->nx = i->nx;
i->nx->pr = i->pr;
Node *t = i;
i = i->pr;
printf("%d ", t->id);
//注意删除的结点如果是头指针指向的结点,要更新head.nx
if (head.nx == t) head.nx = i->nx;
delete t;
}
printf("\n%d", head.nx->id);
return 0;
}

浙公网安备 33010602011771号