前言
老早之前就想写这个内容了,打了草稿后闲置了两个月,因为其他事就没再动过这个东西了,今天翻草稿箱的时候发现了它,就把它完善出来,顺便我也学习学习。
正文
指针的前世今生
前面先说一下,故事是随便瞎编的。
在一个古老的计算机王国里,国王“硬件”统治着所有资源。他拥有广阔的“内存”领土,这片领土由无数的小村庄(即内存单元)组成,每个村庄都有一个独一无二的名字(地址)。但是由于领土很广阔,村庄又很小,所以分出的小村庄非常多,为了区分这些村庄,使得它们第一无二,只好用很长很复杂的名字来为村庄命名。国王的子民“数据”需要在这片广袤的土地上生活、工作和交流。最早的时候,国王只允许数据们住在固定的村庄里,每个村庄的地址是固定不变的,所有事情都得手动操作,这使得管理变得繁重而不灵活。
为了让国土更加繁荣,国王硬件召集了一位聪明的顾问,他的名字叫“汇编”。顾问汇编聪明绝顶,能记住所有村庄的地址,然后直接操纵村庄的真实地址来完成许多繁琐的任务。然而,国王意识到,不是所有的数据居民都能像汇编一样聪明,他需要一种更简洁的方式来管理这片辽阔的领土。
于是,一位年轻的法师“C语言”应召而来。他提出了一个大胆的想法——在国王的领土上,引入一种神奇的“指针”。指针并不是一种普通的武器或工具,而是一种能够指引方向的魔法。
C语言解释道:“陛下,指针可以指向任何一个村庄(内存单元),它就像是一块地图,能指引我们找到任何一个地方。通过使用指针,我们可以迅速找到我们想要的数据,而不必每次都去记住或寻找那具体的村庄繁杂而又雍长的名字。”
“但这还不是全部!”C语言继续说道,“通过指针,我们还能更灵活地管理村庄和村民(数据)。如果我们需要更多的村庄来容纳更多的村民,只需使用指针来指引他们新的住处;如果我们不再需要某些村庄,我们可以轻松地释放它们的资源,将它们重新利用。”
国王硬件被指针的强大能力所折服。指针不仅帮助数据们更加有效地利用内存,还大大简化了复杂数据结构的管理。通过指针,数据们可以轻松地组织成链表、树状结构,甚至是巨大的图形,这在以前是不可想象的。
最重要的是,指针让整个国度的运行效率提升了数倍。无论是快速找到数据,还是动态分配新的资源,一切都变得如此简单和高效。国王硬件的王国因此变得更加繁荣昌盛。
随着时间的推移,指针的魔法被越来越多的语言学者(程序员)所掌握和使用。虽然它强大而灵活,但它也需要谨慎使用,否则可能会引发混乱(如悬空指针、内存泄漏)。然而,不管怎样,指针依然是计算机王国中最重要的工具之一,它的引入改变了整个世界的运作方式,让数据们在这片辽阔的土地上更加自由地生存和发展。
说实在话我查了资料说指针的引入是多方面的因素,但是我个人认为最有可能当时大家都不想记住那么雍长而又难记的地址吧,就像当初发明汇编语言,以及现在的各种高级语言来代替二进制纸带编码吧。
故事看完了,直接正文走起
前置内容
什么是指针?
指针是一种抽象的思想,是一种数据类型,用于表示内存中的地址,就像int,double,string一样;指针变量是一种变量,用来存储地址数据。
什么又是变量呢?
变量本质上是一个数据的标识符或名字,它为存储在内存中的数据提供了一个方便的引用方式。变量本身不是内存地址,但它对应的内存位置是存储实际数据的地方。
内存地址是计算机内存中的一个具体位置,表示变量在内存中的实际存储位置。每个变量都占用一定的内存空间,这个空间由内存地址来确定。
变量通过其名字与实际的内存地址关联。编译器在编译时将变量名映射到内存地址,以便程序在运行时可以访问和操作存储在该位置的数据。
大家可能都知道,在32位机器上,指针变量的大小是4个字节,而在64位机器上,指针变量的大小是8个字节。这是因为指针的大小与系统的地址空间有关。
在32位系统中,内存地址由32位二进制表示,每个二进制位可以有两个状态(0或1),因此地址空间大小为 (2^{32}) 个地址。为了存储这些内存地址,指针变量需要足够的空间。由于32位系统使用32位的地址,所以指针的大小是32位,即4字节。
通用指针
即 void*
指针:
void*
是一种特殊的指针类型,称为“通用指针”或“未指定类型指针”。它可以指向任何类型的对象,但无法直接访问该对象,因为void
没有具体的类型信息。- 你可以将
void*
指针转换为其他类型的指针,然后访问指向的对象。例如:int a = 42; void *ptr = &a; // ptr 是一个 void* 指针,指向 int 类型的对象 int *intPtr = static_cast<int*>(ptr); // 将 void* 转换为 int* 才能访问数据
空指针
空指针和 void*
并不是同一回事,它们之间有本质上的区别。很多人分不清。
空指针(Null Pointer):
- 空指针是指没有指向任何有效对象或内存地址的指针。C++ 中的空指针通常用
nullptr
(C++11 及以后)或NULL
宏(C 语言和旧的 C++ 标准)表示。 - 空指针的目的是表示“此指针不指向任何有效对象”,例如:
int *ptr = nullptr; // ptr 是一个空指针
- 在 C++ 中,
nullptr
是一种专门的类型std::nullptr_t
,用于表示空指针。
void*
和 nullptr
的区别:
void*
是一个指针类型,可以指向任意类型的对象;但它依然是一个有效的指针,只不过没有类型信息。nullptr
是一个空指针常量,专门用于表示空指针,它并不指向任何对象或内存地址。- 所以,
void*
和nullptr
是不同的,void*
只是类型未知的指针,而nullptr
是没有指向任何对象的空指针。
悬空指针
悬空指针(Dangling Pointer)
悬空指针是指指针原本指向某个有效的内存地址,但该内存已经被释放或被重用。此时,指针仍然保存着原来的地址,但该地址所指的内存区域不再有效(就是我常说的释放后指针要置空)。访问悬空指针会导致未定义行为(一般是程序异常退出)。
悬空指针的形成原因:
- 释放内存后未将指针置空:当动态分配的内存(如使用
delete
或free
)被释放后,指针仍然保存着已经释放的内存地址。如果继续通过这个指针访问原来的数据,就会产生悬空指针问题。
悬空指针示例:
int* ptr = new int(42); // 动态分配内存并赋值
delete ptr; // 释放内存
// 此时 ptr 变为悬空指针,因为它指向已经释放的内存
*ptr = 10; // 未定义行为,可能会导致程序崩溃
如何避免悬空指针:
- 在释放动态分配的内存后,将指针置为空(
nullptr
)。delete ptr; ptr = nullptr; // 避免使用已经释放的内存
野指针
野指针(Wild Pointer)
野指针是指指针没有被初始化,或者指向了一个随机或无效的内存地址。野指针的最大危险在于,它指向的内存可能未被分配给程序,访问该地址会导致程序崩溃。
野指针的形成原因:
- 未初始化的指针:在声明指针时,未将它初始化为有效地址或空指针,导致指针指向随机内存位置。
- 超出作用域后使用局部指针:指针指向的变量超出作用域后,该内存可能已被其他进程或系统使用,继续使用该指针会导致野指针问题。
野指针示例:
int *ptr; // 未初始化的指针,随机指向一个未知内存地址
*ptr = 42; // 未定义行为,可能导致程序崩溃
int* getPointer() {
int a = 10;
return &a; // 返回局部变量的地址,a 在函数结束后会被销毁
}
int* ptr = getPointer(); // ptr 成为野指针
*ptr = 20; // 未定义行为
如何避免野指针:
-
初始化指针:在定义指针时,确保将它初始化为一个有效地址或
nullptr
。int *ptr = nullptr; // 将指针初始化为 nullptr
-
避免返回局部变量的地址:如果函数内定义的变量在函数结束时销毁,返回它的地址会产生野指针。可以使用动态分配内存或通过参数传递。
int* getPointer() { int* a = new int(10); // 动态分配内存 return a; }
悬空指针和野指针的区别
悬空指针(Dangling Pointer) | 野指针(Wild Pointer) | |
---|---|---|
定义 | 指向已释放或无效内存的指针。 | 指向随机或未初始化内存的指针。 |
原因 | 内存被释放后,指针未被置空。 | 指针未初始化或指向无效内存。 |
风险 | 访问已释放的内存,可能导致崩溃。 | 访问随机内存,可能导致崩溃。 |
解决方法 | 释放内存后将指针置为 nullptr 。 | 声明指针时初始化为 nullptr 或有效地址。 |
空指针与悬空指针、野指针的区别
-
空指针(Null Pointer):空指针是指不指向任何内存地址的指针,其值为
nullptr
。空指针是一个安全的指针状态,因为它明确表示指针不指向任何有效的内存。示例:
int* ptr = nullptr; // 空指针,不指向任何有效对象
-
悬空指针 和 野指针 则是错误状态,指向的是无效或未定义的内存区域,可能引发未定义行为。
一级指针
一级指针
#include <iostream>
using namespace std;
//
//变量的本质不是内存地址空间,而是存储在内存中的数据的标识符或名字。内存地址是变量所对应的位置,是变量存储的位置,而变量是对这个位置的命名。
//
int main()
{
//关于编程时的数据可以分为两种常量和变量;变量又可以分为普通变量(int,float,bool等),指针变量;
//普通变量与指针变量,普通变量很简单理解,比如int a =1;相当于在内存中找了一个内存地址空间,将a作为这个内存空间的标签,将常数1赋给这个内存地址空间
//编译器会将变量名 a 映射到实际的内存地址;普通变量是用来存储值的。而指针变量是来存储地址的;计算机在传递参数时,参数只会有两种类型,数值或地址,
//那么如何分辨呢,cpu会帮我们分辨,我们不需要操心;
//指针变量可以用来存储其他变量或者对象的内存地址,通过这些地址可以间接访问存储在其他位置上的数据
//&取值符,取变量本身的地址;*解引用,相当于脱去一层伪装,脱去一层地址,解引用得到结果是值还是地址在于指针类型,指针能存什么值
int a = 1;
int* p = nullptr;
p = &a;
cout <<"普通变量a存储的值为:" << a << endl;
cout <<"普通变量a的地址为:" << &a << endl;
cout << "指针变量p所指向的地址为(指针变量p处内存地址空间所存储的内容):" << p << endl;
cout << "指针变量p本身的地址为:" << &p << endl;
//*p相当于对指针变量p所指向的地址脱去一层地址,显示该地址(普通变量a)所存储的值
cout << "指针变量p所指向的地址的内容为(指针变量p处内存地址空间所存储的内容解引用后的内容):" << *p << endl;
}
运行结果
指针常量
指针常量,指针的指向不可以改,但是指针指向的值可以改
定义:指针常量是指指针的值不可改变,即指针始终指向同一个地址,但它指向的地址中的数据是可以修改的。
记忆:把“指针”当作主语,“常量”修饰“指针”,所以指针本身是常量,也就是指向的地址不可变。
语法:
int a = 10;
int b = 20;
int *const p = &a; // p是指针常量,不能再指向别的变量
*p = 30; // 可以修改p指向的地址里的值(即修改a的值)
p = &b; // 错误!p不能再指向别的地址
解释:在这个例子中,p
是指针常量,它的值(即它指向的内存地址)不能改变,但该地址中的内容是可以修改的。
常量指针
常量指针,指针的指向可以改,但是指针指向的值不可以改
定义:常量指针是指指针指向的值不可改变,即指针指向的地址中的数据是常量,不能修改;但指针本身可以指向其他地址。
记忆:把“常量”修饰“指针指向的值”,所以指向的值是常量,也就是指向的地址中的数据不可变。
语法:
int a = 10;
const int *p = &a; // p是常量指针,不能通过p修改a的值
*p = 20; // 错误!不能修改p指向的值
int b = 30;
p = &b; // 可以修改p的指向
解释:在这个例子中,p
指向的值不能被修改(即*p
是常量),但p
本身可以指向别的地址。
其实可以把指针常量类比为“固定不动的指针”(指向的位置不能变),把常量指针类比为“保护数据的指针”(指向的数据不能改)。
常指针常量
定义:常指针常量是指指针本身不可改变,且它指向的值也不可改变,即指针既不能修改指向的地址,也不能修改指向的值。
语法:
int a = 10;
const int *const p = &a; // p是常指针常量
*p = 20; // 错误!不能修改p指向的值
p = &b; // 错误!不能改变p的指向
解释:在这个例子中,p
既不能修改它指向的地址中的值,也不能指向别的地址。
总结:
- 指针常量(T *const):指针的地址不能修改,指向的数据可以修改。
- 常量指针(const T *):指向的数据不能修改,指针的地址可以修改。
- 常指针常量(const T *const):指针的地址和指向的数据都不能修改。
多级指针
二级指针
二级指针,二级指针存储一级指针的地址,即二级指针的内容是一级指针的地址(说白了还是地址)
#include <iostream>
using namespace std;
//
//变量的本质不是内存地址空间,而是存储在内存中的数据的标识符或名字。内存地址是变量所对应的位置,是变量存储的位置,而变量是对这个位置的命名。
//
int main()
{
//关于编程时的数据可以分为两种常量和变量;变量又可以分为普通变量(int,float,bool等),指针变量;
//普通变量与指针变量,普通变量很简单理解,比如int a =1;相当于在内存中找了一个内存地址空间,将a作为这个内存空间的标签,将常数1赋给这个内存地址空间
//编译器会将变量名 a 映射到实际的内存地址;普通变量是用来存储值的。而指针变量是来存储地址的;计算机在传递参数时,参数只会有两种类型,数值或地址,
//那么如何分辨呢,cpu会帮我们分辨,我们不需要操心;
//指针变量可以用来存储其他变量或者对象的内存地址,通过这些地址可以间接访问存储在其他位置上的数据
//&取地址符,取变量本身的地址;*解引用,相当于脱去一层伪装,脱去一层地址,解引用得到结果是值还是地址在于指针类型,指针能存什么值
int a = 1;
int* p = nullptr;
p = &a;
cout <<"普通变量a存储的值为:" << a << endl;
cout <<"普通变量a的地址为:" << &a << endl;
cout << "指针变量p所指向的地址为(指针变量p处内存地址空间所存储的内容):" << p << endl;
cout << "指针变量p本身的地址为:" << &p << endl;
//*p相当于对指针变量p所指向的地址脱去一层地址,显示该地址(普通变量a)所存储的值
cout << "指针变量p所指向的地址的内容为(指针变量p处内存地址空间所存储的内容解引用后的内容):" << *p << endl;
//二级指针,二级指针存储一级指针的地址,即二级指针的内容是一级指针的地址(说白了还是地址)
int** q = nullptr;
q = &p;
//此时内容为指针变量p的地址
cout << "二级指针变量q所指向的地址为" << q << endl;
cout << "二级指针变量q本身的地址为" << &q << endl;
//只脱去了一层地址,此时内容为普通变量a的地址
cout << "二级指针变量q所指向的内容为(脱一层衣服)" << *q << endl;
//脱了两层地址
cout << "二级指针变量q所指向的内容为(脱两层衣服)" << **q << endl;
}
运行结果
三级指针
三级指针,三级指针存储二级指针的地址,即三级指针的内容是二级指针的地址
#include <iostream>
using namespace std;
//
//变量的本质不是内存地址空间,而是存储在内存中的数据的标识符或名字。内存地址是变量所对应的位置,是变量存储的位置,而变量是对这个位置的命名。
//
int main()
{
//关于编程时的数据可以分为两种常量和变量;变量又可以分为普通变量(int,float,bool等),指针变量;
//普通变量与指针变量,普通变量很简单理解,比如int a =1;相当于在内存中找了一个内存地址空间,将a作为这个内存空间的标签,将常数1赋给这个内存地址空间
//编译器会将变量名 a 映射到实际的内存地址;普通变量是用来存储值的。而指针变量是来存储地址的;计算机在传递参数时,参数只会有两种类型,数值或地址,
//那么如何分辨呢,cpu会帮我们分辨,我们不需要操心;
//指针变量可以用来存储其他变量或者对象的内存地址,通过这些地址可以间接访问存储在其他位置上的数据
//&取值符,取变量本身的地址;*解引用,相当于脱去一层伪装,脱去一层地址,解引用得到结果是值还是地址在于指针类型,指针能存什么值
int a = 1;
int* p = nullptr;
p = &a;
cout <<"普通变量a存储的值为:" << a << endl;
cout <<"普通变量a的地址为:" << &a << endl;
cout << "指针变量p所指向的地址为(指针变量p处内存地址空间所存储的内容):" << p << endl;
cout << "指针变量p本身的地址为:" << &p << endl;
//*p相当于对指针变量p所指向的地址脱去一层地址,显示该地址(普通变量a)所存储的值
cout << "指针变量p所指向的地址的内容为(指针变量p处内存地址空间所存储的内容解引用后的内容):" << *p << endl;
//二级指针,二级指针存储一级指针的地址,即二级指针的内容是一级指针的地址(说白了还是地址)
int** q = nullptr;
q = &p;
//此时内容为指针变量p的地址
cout << "二级指针变量q所指向的地址为" << q << endl;
cout << "二级指针变量q本身的地址为" << &q << endl;
//只脱去了一层地址,此时内容为普通变量a的地址
cout << "二级指针变量q所指向的内容为(脱一层衣服)" << *q << endl;
//脱了两层地址
cout << "二级指针变量q所指向的内容为(脱两层衣服)" << **q << endl;
//三级指针,三级指针存储二级指针的地址,即三级指针的内容是二级指针的地址
int*** r = nullptr;
r = &q;
//此时内容为指针变量q的地址
cout << "三级指针变量r所指向的地址为" << r << endl;
cout << "三级指针变量r本身的地址为" << &r << endl;
//只脱去了一层地址,此时内容为一级指针p的地址
cout << "三级指针变量r所指向的内容为(脱一层衣服)" << *r << endl;
//脱了两层地址,此时内容为普通变量a的地址
cout << "三级指针变量r所指向的内容为(脱两层衣服)" << **r << endl;
//脱了三层地址
cout << "三级指针变量r所指向的内容为(脱三层衣服)" << ***r << endl;
}
运行结果
更多级指针
其实就跟套娃穿衣服一样,反正后面也是要一件一件的脱衣服(指解引用*)
这里说明一下肯定是不允许更高级别的指针指向低它两级的指针这种操作,比如三级指针肯定不能指向一级指针,因为参数类型就不匹配,除非你强转。
数组指针
在说数组指针之前先说一下数组。
一维数组
数组与指针的区别
-
数组:
-
数组是一组相同类型的数据,存储在内存中的连续地址中。
-
数组的大小在编译时是固定的,不能动态改变。
-
数组名代表数组的起始地址(即第一个元素的地址),但它不是普通的指针。数组名是一个指针常量,即它的值不能改变。例如,你不能把数组名赋值给另一个地址。
int arr[5] = {1, 2, 3, 4, 5}; int* ptr = arr; // ptr指向数组的第一个元素
在上面的例子中,
arr
是数组名,表示数组的起始地址,相当于&arr[0]
。虽然你可以使用ptr = arr;
将数组名赋值给指针,但你不能对arr
本身重新赋值。数组名不能作为左值:
arr = &arr[1]; // 错误:数组名是常量,不能修改其指向
-
-
指针:
-
指针是一个变量,存储的是某个数据的地址。
-
指针可以在运行时改变其指向的位置。
-
指针可以指向任意内存地址,而不仅仅是数组的第一个元素。你可以通过指针遍历数组,也可以使用指针指向动态分配的内存。
int* ptr = arr; // ptr可以指向arr中的第一个元素 ptr = &arr[1]; // ptr可以指向数组的第二个元素
-
数组,就是内存中一片连续的内存空间而已,用数组名来标识这片内存空间,为了便于访问这片内存地址空间,令数组名为指针常量
测试代码
#include <iostream>
using namespace std;
//
//变量的本质不是内存地址空间,而是存储在内存中的数据的标识符或名字。内存地址是变量所对应的位置,是变量存储的位置,而变量是对这个位置的命名。
//
int main()
{
// 数组,就是内存中一片连续的内存空间而已,用数组名来标识这片内存空间,为了便于访问这片内存地址空间,令数组名为指针
// 一维数组,一维数组的数组名指向数组首元素的地址,或者说数组名存储着数组首元素的地址;数组名的指向不能改变,但是数组名指向的数据可以改变
// 因此数组名很像指针常量(const int*)
int arr[] = { 1,2 };
int* m = nullptr;
m = arr;
//arr本身的地址,恰好与第一个元素地址相同
cout << "数组名arr所标识的地址(数组首元素的地址):" << &arr << endl;
cout << "数组首元素的地址:" << &arr[0] << endl;
//虽然与&arr得到的结果一样但是二者含义不同,arr指向数组这片空间,&arr就是一个4字节的地址,这个地址与数组首元素地址相同
cout << "数组名所指向的地址:" << arr << endl;
cout << "数组名加1所指向的地址:" << arr + 1<< endl;
cout << "数组名所指向的内容:" << *arr << endl;
cout << "指针变量m所指向的地址:" << m << endl;
cout << "指针变量m本身的地址:" << &m << endl;
cout << "指针变量m所指向的内容" << *m << endl;
}
运行结果
二维数组
二维数组,二维数组也是一片连续的内存空间,只不过该内存空间被分成了一些’段’而已, 对于二维数组,数组名指向的是一维数组,类型为 int (*)[n]
。
测试代码
#include <iostream>
using namespace std;
//
//变量的本质不是内存地址空间,而是存储在内存中的数据的标识符或名字。内存地址是变量所对应的位置,是变量存储的位置,而变量是对这个位置的命名。
//
int main()
{
//二维数组,二维数组也是一片连续的内存空间,只不过该内存空间被分成了一些'段'而已,二维数组名是一个指向一维数组的指针,这与普通的指针不同
//它不是一个二级指针,是一个特殊的一级指针,即数组指针,int (*s)[n],s就是数组指针,数组指针是一个指针,一个指向int [n]的指针
//但是同二级指针一样可以脱两次伪装,指向arr2[0][0]
int arr2[2][3] = { {1,2,3},{4,5,6} };
int(*s)[3] = arr2;
cout << "二维数组arr2所指向的地址:" << arr2 << endl;
cout << "二维数组arr2加1所指向的地址:" << arr2 + 1<< endl;
cout << "二维数组arr2本身的地址:" << &arr2 << endl;
cout << "二维数组arr2本身的地址:" << &arr2[0] << endl;
cout << "二维数组arr2本身的地址:" << &arr2[0][0] << endl;
cout << "二维数组arr2脱一次所指向的内容(arr2[0]):" << *arr2 << endl;
cout << "二维数组arr2脱两次所指向的内容(arr2[0][0]):" << **arr2 << endl;
cout << "数组指针s所指向的地址:" << s << endl;
cout << "数组指针s本身的地址:" << &s << endl;
//一次解引用,得到arr2[0]
cout << "数组指针s所指向的内容:" << *s << endl;
//二次解引用,得到arr2[0][0]
cout << "数组指针s所指向的内容:" << **s << endl;
}
运行结果
三维数组
对于三维数组,数组名指向的是二维数组,类型为 int (*)[m][n]
。
测试代码
#include <iostream>
using namespace std;
//
//变量的本质不是内存地址空间,而是存储在内存中的数据的标识符或名字。内存地址是变量所对应的位置,是变量存储的位置,而变量是对这个位置的命名。
//
int main()
{
//三维数组,三维数组名是一个指向二维数组的数组指针,int (*t)[m][n],t是一个指向int [m][n]的指针,但是同三级指针一样可以脱三次伪装,指向arr3[0][0][0]
int arr3[2][3][2] = { {{{1},{2}},{{3},{4}},{{5},{6}}},{{{7},{8}},{{9},{10}},{{11},{12}} } };
int(*t)[3][2] = arr3;
cout << "三维数组arr3所指向的地址:" << arr3 << endl;
cout << "三维数组arr3本身的地址:" << &arr3 << endl;
cout << "三维数组arr3[0]的内容:" << arr3[0] << endl;
cout << "三维数组arr3[0]的地址:" << &arr3[0] << endl;
cout << "三维数组arr3[0][0]的内容:" << arr3[0][0] << endl;
cout << "三维数组arr3[0][0]的地址:" << &arr3[0][0] << endl;
cout << "三维数组arr3[0][0][0]的内容:" << arr3[0][0][0] << endl;
cout << "三维数组arr3[0][0][0]的地址:" << &arr3[0][0][0] << endl;
cout << "三维数组arr3脱一次所指向的内容:" << *arr3 << endl;
cout << "三维数组arr3脱两次所指向的内容:" << **arr3 << endl;
cout << "三维数组arr3脱三次所指向的内容:" << ***arr3 << endl;
cout << "数组指针t所指向的地址:" << t << endl;
cout << "数组指针t本身的地址:" << &t << endl;
cout << "数组指针t脱一次所指向的内容:" << *t << endl; //一次解引用,得到arr3[0]
cout << "数组指针t脱两次所指向的内容:" << **t << endl; //二次解引用,得到arr3[0][0]
cout << "数组指针t脱三次所指向的内容:" << ***t << endl; //三次解引用,得到arr3[0][0][0]
}
运行结果
更高维度数组
其实更高维的数组无非就是数组名指向不同罢了,即int(*arr)[m][n][p][q][...]
指针数组
指针数组是一个数组,其元素是指针。也就是说,指针数组的每个元素都存储一个指针,该指针指向其他类型的变量或对象。指针数组在处理动态数据、字符串数组、函数指针等场景中非常有用。
说白了还是数组,不过存储的元素为地址(变量地址,数组地址,函数地址等)。比如int* p[n]
,由于在字符优先级表中,[]的优先级大于*,所以int *p[n]
就等价于int *(p[n])
。
定义方式如下:
type* arrayName[size];
其中,type*
表示指针类型,size
表示数组的大小。每个数组元素都是一个指向 type
类型的指针。
例子
int main()
{
int a = 10, b = 20, c = 30;
int* arr4[3]; // 指针数组,包含3个指向int的指针
arr4[0] = &a; // arr[0] 存储 a 的地址
arr4[1] = &b; // arr[1] 存储 b 的地址
arr4[2] = &c; // arr[2] 存储 c 的地址
// 通过指针数组访问变量
for (int i = 0; i < 3; i++) {
cout << "Value of arr[" << i << "]: " << *arr4[i] << endl;
}
// fruits 是一个指向字符串的指针数组,存储了四个字符串的地址
const char* fruits[] = { "Apple", "Banana", "Orange", "Grape" };
for (int i = 0; i < 4; i++) {
cout << "Fruit " << i + 1 << ": " << fruits[i] << endl;
}
return 0;
}
结果
指针函数
指针函数 ,返回值是一个指针。这个非常常见。
例子
// 格式
type* functionName(parameters) {
// 函数体
}
//比如
void* func1(int& a, int& b);
int* func2(const int& a, const int& b);
函数指针
在C++中,函数指针是一种指向函数的指针,用于保存函数的地址,进而通过指针来调用函数。比如针 int (*func)()
,简单的理解,这就可以理解为调用函数,其中(* func)
这个整体可以看作是函数名称,func
代表函数的入口地址。
函数指针允许你通过指针来调用不同的函数,而无需在代码中显式指定具体的函数名称。它非常适合场景是:
- 动态选择要调用的函数(例如在回调机制中)
- 创建函数数组,用于实现类似状态机的结构
- 实现多态性,但不通过继承
语法:
return_type (*function_pointer)(parameter_types);
2. 函数指针的使用示例
#include <iostream>
using namespace std;
// 定义几个函数
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
// 函数指针示例
int main() {
// 定义一个指向返回类型为int、参数类型为(int, int)的函数指针
int (*funcPtr)(int, int);
// 将函数地址赋给函数指针
funcPtr = &add;
cout << "Result of add: " << funcPtr(3, 4) << endl; // 调用add函数
funcPtr = &multiply;
cout << "Result of multiply: " << funcPtr(3, 4) << endl; // 调用multiply函数
return 0;
}
运行结果
智能指针
C++ 中的智能指针是一种用于自动管理动态分配内存的工具,它们通过 RAII
(资源获取即初始化)机制确保在适当的时间自动释放内存,从而避免内存泄漏和其他内存管理问题。
在传统的 C++ 中,动态分配的内存需要手动释放,否则会导致内存泄漏。这种管理方式容易出错,尤其是在异常处理或多个函数返回时。智能指针就是为了解决这些问题而设计的。它们是 C++ 标准库中的类模板,用于管理动态分配的对象。当智能指针超出作用域时,所管理的对象会自动销毁,确保不会发生内存泄漏。
常见的智能指针
std::auto_ptr
在C++11中被抛弃了,建议使用更安全的 std::unique_ptr
来代替。
std::unique_ptr
std::unique_ptr
是一种独占所有权的智能指针,这意味着同一时间内只能有一个智能指针拥有某个对象。它的特点是:
- 所有权独占:不允许另一个
unique_ptr
拥有同一个对象。 - 转移所有权:可以通过
std::move
转移所有权,这时原来的指针会变为空指针。 - 性能好:由于它的独占性,没有引用计数的开销,所以性能最好。
// ptr1 拥有对象
std::unique_ptr<int> ptr1(new int(10));
// ptr1 的所有权 转移给 ptr2,变为空指针
std::unique_ptr<int> ptr2 = std::move(ptr1);
std::shared_ptr
std::shared_ptr
是一种共享所有权的智能指针,多个 shared_ptr
可以同时拥有同一个对象。它的特点是:
- 引用计数:每个
shared_ptr
都有一个内部引用计数,记录有多少个shared_ptr
共享同一个对象。当引用计数变为 0 时,对象会被自动销毁。 - 线程安全:引用计数的增加和减少是线程安全的,但对象本身的操作并不是线程安全的。
- 灵活性强:适用于多个对象共享同一资源的情况。
// 创建一个 shared_ptr,引用计数为 1
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
// ptr1 和 ptr2 共享同一个对象,引用计数变为 2
std::shared_ptr<int> ptr2 = ptr1;
std::weak_ptr
std::weak_ptr
是一种不拥有对象的智能指针,它只对对象进行弱引用,不增加引用计数。它的主要作用是解决 shared_ptr
之间的循环引用问题。
- 弱引用:
weak_ptr
不会影响对象的生命周期,也就是说它不会阻止对象被销毁。 - 安全使用:由于
weak_ptr
不拥有对象,因此在使用前需要通过lock()
方法获取一个shared_ptr
,从而确保对象仍然存在。
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
// wptr 不会增加引用计数
std::weak_ptr<int> wptr = ptr1;
if (std::shared_ptr<int> ptr2 = wptr.lock()) {
// 成功获取 shared_ptr,安全使用对象
} else {
// 对象已被销毁
}
智能指针的优势
- 防止内存泄漏:智能指针会自动管理内存,不再需要手动调用
delete
。 - 异常安全:智能指针与 RAII 模式结合,可以确保在异常发生时,资源仍然能够被正确释放。
- 简化代码:智能指针使得代码更加简洁和易于维护,减少了手动管理内存的负担。
智能指针的注意事项
- 避免循环引用:使用
std::shared_ptr
时要注意循环引用的问题,可以通过std::weak_ptr
解决。 - 性能考虑:
std::unique_ptr
性能最好,因为它没有引用计数的开销,而std::shared_ptr
由于需要维护引用计数,性能相对较低。 - 不要滥用智能指针:智能指针适用于动态分配的内存管理,但对于自动分配的对象(如局部变量),不需要使用智能指针。