本来准备在第二章讲完数据类型的,后来发现实在太多了。虽然c++使用指针的程度不如c,但是我还是想把“c语言的灵魂”详细地讲讲。
目录
回忆之前说的,一个变量包括三个基本属性:存储的信息的类型、内容、存储位置(地址)。信息的类型、内容分别在声明、赋值语句处标明。而信息的存储位置似乎没什么用。这里引出一个问题,:我们为什么需要知道变量的存储位置?知道后有什么用处?读者可以带着这个问题阅读本章。
1 指针是什么?
指针是一种特殊的数据类型,要与已有的数据类型组合才有意义;其值存储该种(与其组合的)数据类型的地址。int型指针便可存储某个int型变量的地址。声明格式如下:
type * name; //在type后加上 * ,以表明这是一个这种类型的指针
1.1 取地址运算符 &
声明任意一种变量后,我们便可在变量左侧使用 & 来获得该变量的地址,如:
int a = 10;
int *p = &a; //p是int型的指针,初始化为变量a的地址
cout << p; //输出a的地址
1.2 解引用运算符 *
声明任意一种指针后,我们便可在指针左侧使用 * 来获得该地址上的值,如:
int a = 10;
int *p = &a; //p是int型的指针,初始化为变量a的地址
cout << *p; //输出a的值,10
2 进一步理解指针
2.1 硬币模型 (形象)
int a = 10;
int *p = &a;
如上述代码,有以下等价关系:
a == *p;
&a == p;
好比一枚硬币,其正面是值,背面是地址。对硬币正面使用 & 运算符,相当于使用硬币背面;对硬币背面使用 * 运算符,相当于使用硬币正面。换而言之,& 和 * 是将硬币翻面的运算。
2.2 内存空间模型(本质)
声明语句构建了部分内存和变量名称之间的所属关系,int a 即表明分配了4字节的内存,可以使用变量名a进行追踪。而指针的值是1000,这表明将直接使用内存地址进行追踪,而不是使用变量名a。
3 使用指针
3.1 声明语法
已知同类型变量可在同一行声明,但指针略有不同,如:
int *a, b; //a是int指针,b是int
上述代码告诉我们声明时,要在每个指针前面写上 * 。也可理解为对该变量解引用后才是该种数据类型(意即该变量是指针)。
3.2 空指针
未初始化的指针指向的位置是未知的。使用未初始化的指针就是使用未知内存,可能会导致系统崩溃,十分危险。如果确实在声明时没有初始化的目标,c++提供了空指针nullptr(c风格是NULL)以供解决风险。
nullptr:本质上就是0,对指向nullptr的指针解引用会直接终止程序,如:
int *p = nullptr; //或 int *p = NULL;
cout << p; //输出0
cout << *p; //程序终止
cout << "here"; //不执行
3.3 手动管理内存:new 和 delete
直接声明的变后,c++将自动管理相应的内存。但是,我们也可通过new和delete运算符来手动管理内存。new 运算符将在堆内存(详见内存空间章节)中分配相应大小的内存,并返回该内存首地址的指针。delete将指针指向的堆内存释放。声明语法如下:
type *point_name = new type;
delete point_name;
int *p = new int;
*p = 10;
cout << *p;
delete p; //一定要记得释放
同时,也能使用new和delete声明数组:
type *point_name = new type [size];
delete [] point_name; //注意这个方括号
int *p = new int[10];
p[1] = 10;
cout << p[1];
delete[] p;
由此可见,可以直接将指针名视为数组名(因为在c++内部数组与指针基本等价,详见后文)。
使用new和delete有以下规则:
- delete只能释放堆内存,即不使用delete释放不是new分配的内存
- delete只能释放一次(多次释放将报错)
- 使用new[],则用delete[];使用new,则用delete
- delete 指向nullptr的指针不会有问题
3.4 只读指针,const指针
注意指针的声明:type * name,name前共有三个位置可以放const,区别如下:
const int *a = new int;
int const *b = new int;
int *const c = new int;
*a = 10; //报错
*b = 10; //报错
*c = 10;
delete a; //记得释放
delete b;
delete c;
const的位置只有在 * 前后的区别,
* 前表示指向的内容不可被修改, * 后表示指向的地址不可被修改。
前者(例中的a,b)可以指向多个地址,但是不能修改它们,是只读指针;
后者(例中的c)只能指向初始化时安排的地址,即这个指针的值不能改变,是const指针。
ps:实际上,我们经常把只读指针说成const指针,因为const指针用处不大。
4 数组和指针
4.1 数组名的意义
你认为下述代码会输出什么?
int a[10] = {1, 2, 3};
cout << a;
回忆数组的定义——在内存空间中连续放置的多个变量的整体,这个整体就是这一段连续内存的首地址。所以前文使用new分配的数组能使用int指针指向,且指针名和普通声明的数组名一样。
int *p = new int[10];
p[0] = 10;
cout << p << '\n';
cout << *p << '\n'; //输出10
int a[10] = {20};
cout << a << '\n';
cout << *a << '\n'; //输出20
数组的首地址,便是第一个元素的地址,对数组名解引用便能得其值。
4.2 指针算术
指针也是变量,也有相应的运算符,p代表指针,n代表常数,具体如下:
*p 获得指针指向的值
&p 获得指针的地址
p + n 获得向后偏移n个单位(根据p指向的类型)的地址
p - n 获得向前偏移n个单位(根据p指向的类型)的地址
p1 - p2 获得两个指针在内存空间中的距离
p++ 等同于p = p + 1 //p--,--p,++p同理
#include <iostream>
#include <iomanip>
using namespace std;
#define show(x) cout << #x ":\r\t" << setw(2) << x << endl;
int main()
{
int *a = new int, *b = new int, x, *c = &x;
show(a);
show(b);
show(c);
show(a + 1); //int是4字节,所以增加4(1个4字节)
show(a - 2); //减少8(2个4字节)
show(b - a); //堆内存之间的变量离得很近
show(a - b);
show(a - c); //栈内存离堆内存很远
delete a;
delete b;
}
我的电脑上的输出:
a: 0xe91600
b: 0xe91620
c: 0x61fdf4
a + 1: 0xe91604
a - 2: 0xe915f8
b - a: 8
a - b: -8
a - c: 2213379
4.3 数组表示法与指针表示法
根据指针算术,这两句等价:
a[i];
*(a + i); // *a 等同于 *(a+0)
因此,很多情况下数组和指针是有共性的, 你甚至可以想象:
int a[10];
int * const a = new int[10];
这两者在使用上是等价的,区别是内存管理手段不同。
而区别之一是:
- 数组名是const指针
- 对数组名使用 sizeof 运算符,得到整个数组大小;指针则是自身长度
- 对数组名使用 &,得到指向数组的指针;指针则是升维
请注意下述代码:
#include <iostream>
#include <iomanip>
using namespace std;
#define show(x) cout << #x ":\r\t\t" << setw(2) << x << endl;
int main()
{
short a[10] = {22}, *p = a;
show(sizeof(&p)); //输出8,short二维指针的大小
show(sizeof(p)); //输出8,short指针的大小
show(sizeof(*p)); //输出2,short的大小
show(sizeof(&a)); //输出8,指针的大小,指向short数组
show(sizeof(a)); //输出20,short数组的大小,指向第一个short
show(sizeof(*a)); //输出2,第一个元素的大小
show(a);
show(&a + 1); //输出+20,单位是数组长
show(a + 1); //输出+2,单位是short
show(*a + 1); //输出23,第一个元素增加
}
参考输出:
sizeof(&p): 8
sizeof(p): 8
sizeof(*p): 2
sizeof(&a): 8
sizeof(a): 20
sizeof(*a): 2
a: 0x61fdf0
&a + 1: 0x61fe04
a + 1: 0x61fdf2
*a + 1: 23
再次可以看到,
- 数组名就是指向第一个元素的指针;
- 取地址是指向本数组的指针;
- 解引用是第一个元素。
- 指针加法的单位就是指针指向的大小。
声明一个5维指针z保证赋值出错,看vscode编辑器的报错可以帮助大家理解:
5 多维数组、多维指针
本节十分复杂,无意深入者可略过。
5.1 多维指针
首先,维数意味着什么?指针可以指向指针,把基本的数据类型定义为0维,每一级指针便增加一级维数。如:
int a0; //0维
int *a1; //1维指针
int **a2; //2维指针
int ***a3; //3维指针
使用时,每用一个 & ,维数上升1;每用一个 * ,维数下降1。维数降至0时,便是原数据。如:
5.2 * [] 组合
分析下列代码:
int a[10][5]; //a是个二维数组,10行5列
int *b[10]; //b是个指针数组,包含10个int*
int **c; //c是个指针,指向int*
int(*d)[10]; //d是个指针,指向一个大小为10的int数组
ps:因为 [] 的 优先级比 * 高,所以产生了b和d的区别
b的声明的重要意义之一是管理多个不等长的数组
5.3 二维数组
对于二维数组a,有以下等价表示:
a[y][x];
*(*(a + y) + x);
*(a[y] + x); //不好的表达
有实用价值的语句是:
int a[10][5]; //a是个二维数组,10行5列
&a; //int (*)[10][5]
a; //int (*)[5]
a[0]; //int * ,指向第一行
a[0][0]; //int,第一行第一列
因为 * 放在操作数左边, [] 放在操作数右边,所以单一地使用其中一种不容易发生混淆。 值得注意的是c++检查指针更为严格,指向多维数组的指针和该数据的同维指针在c++中是不同的,不能相互赋值。