C++ 之指针
文章目录
- 参考
- 描述
- 指针
- 运算符
- 地址运算符
- 奇偶分体
- 指针的创建
- 间接寻址运算符
- 句点运算符
- 运算符优先级问题
- 箭头运算符
- 运算符优先级
- 指针
- 野指针
- 空指针
- 通用指针
- 解引用
- 分析
- 指针的算术运算
- 加减运算
- 自增运算与自减运算
- 比较运算
- 指针与常量
- 指针常量
- 常量指针
- 指向常量的指针常量
- 指针与数组
- 数组标识符与指针
- 数组元素与指针
- 边界检查
参考
项目 | 描述 |
---|---|
精通C++ (第九版) | 托尼·加迪斯、朱迪·沃尔特斯、戈德弗雷·穆甘达 (著) / 黄刚 等 (译) |
搜索引擎 | Bing |
描述
项目 | 描述 |
---|---|
操作系统 | Windows 10 专业版(64 位) |
C++ 编译器 | gcc version 8.1.0 (x86_64-win32-seh-rev0, Built by MinGW-W64 project) |
指针
指针中存放着指向内存中的某一空间(空间大小为一个字节,内存以字节为基础进行地址的编排及内存的划分)的内存地址,CPU 可以通过这个地址以及一些其它信息获取到该内存地址对应的内存空间中存放的数据。
内存的每一个字节都有唯一的地址。变量的地址是分配给该变量的第一个字节的地址。
运算符
地址运算符
使用地址运算符 & 你将获得 C++ 分配给一个变量或常量(使用 const 等关键字定义的常量)的内存空间的第一个字节所对应的内存地址。你仅需将地址运算符放置在变量或常量的标识符前可取得该变量或常量所对应的地址。对此,请参考如下示例:
// 导入 iostream 以实现基本的输入输出
#include <iostream>
// 导入 string 以使用 String 类创建字符串
#include <string>
// 使用 C++ 标准命名空间
using namespace std;int main(){// 声明一些常量及变量int a;const int b = 1;char c = 'c';string d = "Hello World";// 输出被声明的变量或常量所处的内存空间// 所对应的地址。cout << &a << endl;cout << &b << endl;cout << &c << endl;cout << &d << endl;// 暂停控制台的继续运行,便于观察输出到控制台中的内容。system("pause");
}
执行效果
0x61fe08
0x61fe04
c
0x61fde0
请按任意键继续. . .
注:
- 内存地址前的 0x 表明该内存地址是以十六进制的格式进行显示的。
- 在默认情况下,通过 & 运算符向控制台输出的内容为十六进制格式的内存地址。相比于十六进制的格式的内存地址,你可能更愿意观察十进制格式的内存地址。为此,你可以通过在 &标识符 前添加 (long long) (使用 (long long) 是由于我的操作系统是 64 位 的,使用 8 个字节来实现地址。如果你的操作系统是 32 位 的操作系统,那么你还能够使用 (int) 来转换数值所使用的进制格式。)来将十六进制格式的内存地址转换为十进制格式的内存地址。对此,请参考如下示例:
#include <iostream>
#include <string>
using namespace std;int main(){int a;const int b = 1;char c = 'c';string d = "Hello World";cout << (long long)&a << endl;cout << (long long)&b << endl;cout << (long long)&c << endl;cout << (long long)&d << endl;system("pause");
}
执行效果
6422024
6422020
6422019
6421984
请按任意键继续. . .
- 你可以通过如下方式来得知计算机的地址使用多少个字节进行表示:
#include <iostream>
#include <string>
using namespace std;int main(){// 此处可以声明任意类型的指针,因为在一台计算机中,// 内存地址均使用相同个数的字节进行表示。int* a;// 通过 sizeof 运算符来获取指针所占用的内存空间cout << sizeof a << endl;system("pause");
}
执行结果
由于我的计算机支持 64 位 操作系统,而我使用的也是 64 位 的操作系统。因此,内存地址使用八个字节(一个字节包含八位,也即八个比特)进行表示。
8
请按任意键继续. . .
奇偶分体
在使用 C++ 指针时,你可能会发现这么一个现象:若变量或常量存储的数据的数据类型所占用的内存空间不止一个字节,那么该变量或常量的地址多为偶地址。
奇地址与偶地址
-
奇地址
若一个内存地址属于奇数,那么该地址将被认为是一个奇地址。 -
偶地址
若一个内存地址属于偶数,那么该地址将被认为是一个偶地址。
奇偶分体
为了实现存储器的字节寻址及整字(占用内存空间的大小为两个字节)读写功能,人们将存储器分成容量相等(占据相同的内存空间大小,该空间大小为一个字节)的两类存储体,即奇存储体与偶存储体。
奇存储体所占据的内存空间所对应的地址为奇地址,而偶存储体所占据的内存空间所对应的地址为偶地址。
寻址与整字读取功能
- 寻址
存储器能够通过传输过来的内存地址对内存单元进行查找。
- 整字读取功能
为了充分利用数据总线的传输能力,CPU 从内存中读取数据将以一个字为单位进行读取。且字的起始内存地址需为偶地址(原因暂不明😝)。因此,将包含多个字节数据的变量的起始地址设置为偶地址将有利于提高对数据进行读取操作的效率。
因此机制,当大小为一个字的数据的起始地址为奇地址时,CPU 需要从内存中读取两个字的数据才能获得我们所需要的那一个字的数据
指针的创建
指针是具有类型之分的,整型类型的指针仅能存储指向整型数据的内存地址。若你没有依据指针的类型为其赋值,C++ 将抛出错误。
如果你需要创建一个整型指针,那么你有如下创建方式:
#include <iostream>
#include <string>
using namespace std;int main(){// 声明并初始化一个整型变量int a = 99;// 三种不同的创建整型指针的方式int *b = &a;int * c = &a;int* d = &a;cout << (long long)b << endl;cout << (long long)c << endl;cout << (long long)d << endl;system("pause");
}
执行结果
可以看到,三个整型指针都成功的存储了指向整型变量的内存地址。
6422020
6422020
6422020
请按任意键继续. . .
注:
- 使用哪种风格创建指针取决于你更喜欢哪一种。
- 创建一个存储其它数据类型的变量或常量的地址的指针与创建整型指针的方式类似,你仅需要在合适的位置添加符号 * 即可。
- 在创建指针时指明指针存储的内存地址所指向的数据的数据类型是为了使 C++ 在通过目标地址取出数据的过程中,能够以正确的方式从内存中取出数据。
间接寻址运算符
使用间接寻址运算符你将能够获取到一个指针所指向的内存空间中的数据。对此,请参考如下示例:
#include <iostream>
#include <string>
using namespace std;int main(){// 声明并初始化一个字符型变量char firstCharacter = 'a';// 将字符型变量的地址赋值给字符型指针 char* firstCharacterPtr = &firstCharacter;// 直接输出 firstCharacter 变量中存储的值cout << firstCharacter << endl;// 输出 firstCharacterPtr 所处的内存空间所对应的内存地址cout << (long long)firstCharacterPtr << endl;// 通过 fistCharacter 的内存地址获取到该变量中存储的值cout << *firstCharacterPtr << endl;system("pause");
}
执行结果
a
6422039
a
请按任意键继续. . .
句点运算符
句点运算符 . 用于引用类、结构体及共用体等复合类型的成员。大致用法如下:
#include <iostream>
#include <string>
using namespace std;// 声明 Notebook 类
class Notebook
{public: string title = "TwoMoons";string content = "Hello World";
};int main(){// 实例化 Notebook 类Notebook notebook;// 输出 notebook 中的成员属性所包含的内容cout << notebook.title << endl;cout << notebook.content << endl;system("pause");
}
执行效果
TwoMoons
Hello World
请按任意键继续. . .
运算符优先级问题
使用 * 及 . 符号,你能够通过指针选择对象、结构体等复合类型中的成员。但由于句点运算符 . 的优先级要高于间接运算符 * 所以你还需要用到括号,以提升间接运算符的优先级。对此,请参考如下示例:
#include <iostream>
#include <string>
using namespace std;// 声明一个类
class Notebook
{// 通过关键字 public 将成员声明为公有成员public:string title = "TwoMoons";string content = "Hello World";
};int main(){// 通过类 Notebook 实例化一个对象Notebook notebook;// 创建一个 Notebook 类的指针Notebook* notebookPtr = ¬ebook;// 通过指针选择复合类型中的成员cout << (*notebookPtr).title << endl;cout << (*notebookPtr).content << endl;system("pause");
}
执行结果
TwoMoons
Hello World
请按任意键继续. . .
箭头运算符
通过指针选择复合类型中的成员较为不便(参考前一个示例),因此 C++ 提供了箭头运算符 -> 。箭头运算符能够使你通过指针访问成员的操作更为简便,并且使用箭头运算符还能够提高程序的可读性。对此,请参考如下示例:
#include <iostream>
#include <string>
using namespace std;class Notebook
{public: string title = "TwoMoons";string content = "Hello World";
};int main(){Notebook notebook;Notebook* notebookPtr = ¬ebook;// 通过箭头运算符选择复合类型中的成员cout << notebookPtr -> title << endl;cout << notebookPtr -> content << endl;system("pause");
}
执行效果
#include <iostream>
#include <string>
using namespace std;class Notebook
{public: string title = "TwoMoons";string content = "Hello World";
};int main(){Notebook notebook;Notebook* notebookPtr = ¬ebook;// 通过箭头运算符选择复合类型中的成员cout << notebookPtr -> title << endl;cout << notebookPtr -> content << endl;system("pause");
}
运算符优先级
对于 ()、->、. 及 * 共四种运算符的运算符优先级如下:
项目 | 描述 |
---|---|
最高优先级 | 小括号 () 及箭头运算符 -> |
次级优先级 | 句点运算符 . |
最低优先级 | 间接寻址运算符 * |
指针
野指针
指向非法内存区域的指针被称为野指针,野指针也被称为悬空指针。产生野指针的一种情况是:声明一个指针但未对该指针执行初始化操作。一个未初始化的指针中保存的地址由 C++ 随机分配。对此,请参考如下示例:
#include <iostream>
#include <string>
using namespace std;int main(){int* aPtr;// 向控制台中输出 C++ 分配的随机地址cout << (long long)aPtr << endl;// 向控制台中输出 C++ 分配的随机地址// 中存储的值cout << *aPtr << endl;system("pause");
}
执行效果
16
空指针
在声明指针时,如果你暂时没有可用于初始化该指针的地址,那么你可以将其赋值为 0、NULL 或 nullptr ,存储了这类值的指针被称为空指针。
野指针无法通过 if 语句 检测出来,但空指针可以。使用空指针可以避免无意中对野指针的使用(使用指针前,判断其是否为空指针)。
#include <iostream>
#include <string>
using namespace std;int main(){// 创建三个空指针int* ptr_nullptr = nullptr;int* ptr_0 = 0;int* ptr_NULL = NULL;// 输出指针中存放的内存地址cout << ptr_nullptr << endl;cout << ptr_0 << endl;cout << ptr_NULL << endl;// 对空指针进行判断if(!ptr_nullptr){cout << "指针 ptr_nullptr 为空指针" << endl;};if(!ptr_0){cout << "指针 ptr_0 为空指针" << endl;};if(ptr_NULL){cout << "指针 ptr_NULL 为非空指针" << endl;}else{cout << "指针 ptr_NULL 为空指针" << endl;};system("pause");
}
执行效果
0
0
0
指针 ptr_nullptr 为空指针
指针 ptr_0 为空指针
指针 ptr_NULL 为空指针
请按任意键继续. . .
注:
- 空指针中保存的地址为 0,该地址所对应的内存空间是操作系统所需要的。在大多数操作系统中,C++ 不允许你访问地址 0 中所保存的数据。
- 关键字 nullptr 在 C++ 11 中被提出。如 nullptr 无法正常使用,请检测你的 C++ 编译器是否支持 C++ 11。幸运的是,现在的编译器基本都支持了 C++ 11。
通用指针
通用指针可以接受指向任意数据类型的地址。对此,请参考如下示例:
#include <iostream>
#include <string>
using namespace std;int main(){// 声明一个通用指针void* ptr;// 声明变量int num;double db_num;string str;// 赋值ptr = #ptr = &db_num;ptr = &str;system("pause");
}
执行效果
在程序运行的过程中,C++ 并没有抛出错误。
解引用
通用指针并不能通过使用间接寻址运算符 * 实现对指针进行解引用的功能,这将导致 C++ 抛出错误。对此,请参考如下示例:
#include <iostream>
#include <string>
using namespace std;int main(){int num = 9;void* ptr = #// 下一行语句将导致 C++ 抛出错误cout << *num << endl;system("pause");
}
分析
C++ 无法对通用指针进行解引用操作是由于 C++ 不知道地址所指向的内存空间中存放着何种类型的数据,这导致了 C++ 不知道如何处理内存空间中的数据。
我们可以在对通用指针进行解引用操作前将指针转换为合适的类型的指针,以告知 C++ 如何处理与目标地址关联的内存空间中存储的数据。对此,请参考如下示例:
#include <iostream>
#include <string>
using namespace std;int main(){// 声明通用指针void* ptr;// 声明并初始化变量int num = 9;string first = "Hello World";char a = 'a';// 强制类型转换以实现通用指针的解引用操作ptr = #cout << *(int*)ptr << endl;ptr = &first;cout << *(string*)ptr << endl;ptr = &a;cout << *(char*)ptr << endl;system("pause");
}
执行效果
9
Hello World
a
请按任意键继续. . .
指针的算术运算
加减运算
指针是支持加减运算的,指针加一 并不是指针中保存的地址增加一,增加多少取决于指针所属的类型。
若为 int 类型的指针,则 指针加一 表示指针中保存的地址增加四,这是由于 int 类型的数据占用的内存空间为四个字节。
若为 dobule 类型的指针,则 指针加一 表示指针中保存的地址增加八,这是由于 dobule 类型的数据占用的内存空间为八个字节。
举个栗子
#include <iostream>
#include <string>
using namespace std;int main(){int* aPtr;double* bPtr;cout << "【原】" << endl;cout << (long long)aPtr << endl;cout << (long long)bPtr << endl;cout << "【后】" << endl;// 输出对指针进行加一运算后,指针中存储的值cout << (long long)(aPtr + 1) << endl;cout << (long long)(bPtr + 1) << endl;system("pause");
}
执行结果
【原】
16
1850768
【后】
20
1850776
请按任意键继续. . .
注:
- 指针不止可以进行加一运算,指针可以与任何一个整数相加,规律相同。
- 指针进行减法运算得到的结果可以通过加法运算中的规律来进行判断。换个说法就是,指针进行加减运算时,指针中存储的地址的增减变化遵循相同的规律。
自增运算与自减运算
指针还支持自增运算符(包括前置自增运算符与后置自增运算符)及自减运算符(包括前置自减运算符与后置自减运算符)。在将自增运算符与自减运算符作用到指针时,指针中存储的地址的改变与指针进行加减运算所导致的指针中存储的地址的改变所遵循的规律相同。而地址何时自增或自减则由运算符的位置(前置或后置)所决定。
举个栗子
#include <iostream>
#include <string>
using namespace std;int main(){int* aPtr;cout << (long long)(aPtr) << endl;cout << (long long)(aPtr--) << endl;cout << (long long)aPtr << endl;cout << (long long)(--aPtr) << endl;cout << (long long)(aPtr) << endl;system("pause");
}
执行结果
16
16
12
8
8
请按任意键继续. . .
比较运算
对 C++ 中的指针使用比较运算符,比较的是指针中存储的地址的大小。存储了更大的地址的指针要大于存储了更小的地址的指针。对此,请参考如下示例:
#include <iostream>
#include <string>
using namespace std;int main(){// 声明两个整型指针int* aPtr;int* bPtr;// 向控制台输出指针中存储的地址cout << "【值】" << endl;cout << (long long)aPtr << endl;cout << (long long)bPtr << endl;cout << "【比较】" << endl;// 对指针进行比较操作// 由于运算符优先级的关系,此处使用了括号cout << (aPtr > bPtr) << endl;cout << (aPtr < bPtr) << endl;cout << (aPtr == bPtr) << endl;system("pause");
}
执行效果
【值】
16
12139920
【比较】
0
1
0
请按任意键继续. . .
注:
指针所支持的比较运算符还包括 >=(大于或等于) 及 <=(小于或等于) 。
指针与常量
在某些特殊的情形下,我们可能需要保证指针中保存的内存地址所指向的数据或指针中保存的内存地址不可被改变。要达成这类目标,我们可以将指针声明为指针常量、常量指针或指向常量的指针常量。
指针常量
指针常量可以理解为 指针是常量,通过将指针声明为指针常量可以保证指针中保存的内存地址不被更改。若试图对指针常量中保存的地址进行更改,C++ 将抛出错误。
举个栗子
#include <iostream>
#include <string>
using namespace std;int main(){int a = 9;int b = 3;// 将指针声明为指针常量并对该指针执行初始化操作int* const aPtr = &a;cout << (long long)aPtr << endl;// 由于你已将指针声明为指针常量,故不可执行如下语句// (指针中存放的地址不可被改变)。// 否则,C++ 将抛出错误。// aPtr = &b;system("pause");
}
执行结果
6422028
请按任意键继续. . .
注:
- 将指针声明为指针常量的同时,必须对指针进行初始化操作。
- 对于语句(曾出现于上述示例中)
int* const aPtr = &a;
我们可以采用另一种写法:
int const *aPtr = &a;
使用第二种写法后,C++ 虽不会抛出错误,但采用第二种写法将使得指针不再具有指针常量的功能了。对此,请参考如下示例:
#include <iostream>
#include <string>
using namespace std;int main(){int a = 9;int b = 3;int const *aPtr = &a;cout << (long long)aPtr << endl;// 若常量指针的功能生效,则下一行 C++ 语句// 将导致 C++ 抛出错误。aPtr = &b;cout << *aPtr << endl;system("pause");
}
执行效果
6422036
3
请按任意键继续. . .
常量指针
常量指针可以理解为 常量的指针,通过将指针声明为常量指针可以保证指针中保存的内存地址所指向的数据不被改变。若试图对常量指针中保存的地址所指向的数据进行更改,C++ 将抛出错误。
举个栗子
#include <iostream>
#include <string>
using namespace std;int main(){int a = 9;const int* aPtr = &a;cout << *aPtr << endl;cout << (long long)aPtr << endl;// 由于已将指针声明为常量指针,故不可通对// 常量指针执行解引用操作来修改// 该指针指向的数据。因此不能使用如下语句// &aPtr = 10;// 若使用该语句,C++ 将抛出错误// 虽不能通过对常量指针执行解引用操作来修改// 该指针指向的数据。但可以直接通过相关变量// 修改目标数据。a = 10;cout << a << endl;system("pause");
}
执行效果
9
6422036
10
请按任意键继续. . .
注:
将指针声明为指针常量的同时,必须对指针进行初始化操作。
指向常量的指针常量
通过将指针声明为指向常量的指针常量可以保证指针中保存的内存地址及指针中保存的内存地址所指向的数据不被改变。若试图对指针(指向常量的指针常量)中保存的地址或指针(指向常量的指针常量)中保存的地址所指向的数据进行更改,C++ 将抛出错误。
举个栗子
#include <iostream>
#include <string>
using namespace std;int main(){int a = 3;int b = 9;// 将指针声明为指向常量的指针常量const int* const aPtr = &a;cout << (long long)aPtr << endl;// 在该示例中,对于指向常量的指针常量// 你不可使用如下语句。否则,C++ 将抛// 出错误。// *aPtr = 9;// aPtr = &b;system("pause");
}
执行效果
#include <iostream>
#include <string>
using namespace std;int main(){int a = 3;int b = 9;// 将指针声明为指向常量的指针常量const int* const aPtr = &a;cout << (long long)aPtr << endl;// 在该示例中,对于指向常量的指针常量// 你不可使用如下语句。否则,C++ 将抛// 出错误。// *aPtr = 9;// aPtr = &b;system("pause");
}
注:
- 将指针声明为指向常量的指针常量的同时,必须对指针进行初始化操作。
- 对于语句(曾出现于上述示例中)
const int* const aPtr = &a;
不可使用如下语句代替上述语句。C++ 将由此抛出错误。
const int const *aPtr = &a;
指针与数组
数组标识符与指针
表示数组的变量本身就是一个指针,存储着数组中首个元素所在的内存地址。对此,请参考如下示例:
#include <iostream>
#include <string>
using namespace std;int main(){// 声明并初始化一个数组int arr[] = {1, 2, 3, 4};// 将数组中的首元素所处的内存地址存储到指针中int* arrPtr = arr;cout << (long long)arr << endl;cout << (long long)arrPtr << endl;system("pause");
}
执行结果
6422016
6422016
请按任意键继续. . .
注:
- 用于存储数组等复合类型(对象、结构体等)的变量或常量所保存的值为一个地址。
- 你不可将地址运算符应用于表示数组等复合类型的变量或常量中,C++ 将为此抛出错误。
- 用于存放数组地址的标识符为一个 指针常量 ,你不可改变该标识符中存储的内存地址。对此,请参考如下示例:
#include <iostream>
#include <string>
using namespace std;int main(){int arr[] = {1, 2, 3, 4}; int num = 1;// 下方语句将导致 C++ 编译器抛出错误// arr = #// 上方语句用于将变量 num 的内存地址存储到数组标识符 arr 中system("pause");
}
数组元素与指针
举个栗子
#include <iostream>
#include <string>
using namespace std;int main(){int arr[] = {1, 2, 3, 4}; int num = 1;cout << "arr 首元素的内存地址\t" << (long long)arr << endl;cout << "arr 数组中第三个元素的内存地址\t" << (long long)&arr[2] << endl;// 尝试通过指针运算使用 arr 标识符获取 arr 数组// 中第三个元素的内存地址。cout << (long long)(arr + 2) << endl;system("pause");
}
执行效果
arr 首元素的内存地址 6422016
arr 数组中第三个元素的内存地址 6422024
6422024
请按任意键继续. . .
边界检查
Python 等语言将对数组执行边界检查,当使用了非法索引后,程序将抛出错误。对此,请参考如下示例:
# 定义一个 Python 列表(与 C++ 中的数组类似)
arr = [1, 2, 3, 4]# 输出数组 arr 中的第一个元素
print(arr[0])
# 输出数组 arr 中的第五(索引越界)个元素
print(arr[4])
执行结果
由于数组 arr 所能支持的最大索引为 3 ,在上述代码段中却使用了索引 4 ,由此发生了索引越界。Python 在执行代码 print(arr[4]) 检测到了该问题,立即抛出了错误。
1
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
Cell In[1], line 75 print(arr[0])6 # 输出数组 arr 中的第五(索引越界)个元素
----> 7 print(arr[4])IndexError: list index out of range
C++ 对数组不会执行边界检查,这意味着我们可以使用更大范围内的任意整数值作为数组的索引,而 C++ 将不会抛出错误。对此,请参考如下示例:
举个栗子
#include <iostream>
#include <string>
using namespace std;int main(){// 声明并初始化一个数组int arr[] = {1, 2, 3, 4};// 将数组中的元素输出cout << arr[0] << endl;cout << arr[4] << endl;system("pause");
}
执行效果
1
15678800
请按任意键继续. . .