导图社区 Cplusplus指针与引用
Cplusplus 即C++,是一种静态数据类型检查的,支持多重编程范式的通用程序设计语言,下图梳理了Cplusplus指针与引用的知识内容,收藏下图了解吧!
编辑于2021-08-10 09:03:45C++指针与引用
指针
定义
内存中的这些数据都有地址,指针就是记录这些地址的变量
指针的类型表示指针指向的地址存储的数据类型
指针与内存的关系
在计算机中所有的数据都存放在内存中,存放的最小单位是位(bit)。一个bit就是一个二进制的最小单元,其值只能为0或者1。8个bit是一个字节(byte),字节是计算机中常用的基本单位,一般的计量都是以字节为单位的,比如char类型数据占1个字节,int类型数据占4个字节, double类型数据占8个字节等。
int 类型数据在内存中所占的字节数,跟使用的硬件系统、编译器有关。在程序中可以使用sizeof运算符检测其所占内存的大小。一般来讲,在32位系统下一个int类型数据占4个字节。
x是一个整型变量,其值为3,存储在起始地址为0x011A的4个存储单元中。p为一个整型指针变量,其值为0x011A,也就是变量x的内存地址。在这种情况下,可以说p是一个指向变量x的指针。而一个变量所占内存的第一个字节的地址,就是该变量的地址。
int四个字节
四个地址
首地址1个
字节为基本内存单位
为啥是内存递减排序的呢
高位到低位读
指针的值是一个内存地址
变量的地址
数组的首地址
函数的地址
指针变量的定义
复合类型的数据
三方面内容
指针所指向的数据类型
指针类型修饰符
指针标识符,即指针名
定义方式
指明该变量是个指针
是一个什么类型的指针
一般形式
类型标识符 *指针名
“*”表示这是一个指针变量
变量名为定义的指针变量名
类型标识符表示本指针变量指向数据的类型
示例
int * pt;
示pt是一个指针变量,其值为某个整型变量的地址,或者说pt指向一个整型变量。至于到底指向哪一个整型变量,可以通过将某个变量的地址值赋给pt来决定。
char * pc;
定义一个指向字符的指针变量
float *pf;
定义一个指向浮点数的指针变量
同定义普通变量一样,指针变量也可以连续定义。如一次定义两个整型指针变量
int *pt, *p2;
但是需要注意的是,每一个指针变量前面的指针修饰符“*”都是必不可少的,否则该变量就不是一个指针
使用指针指向数据
指针变量作为一个变量,跟普通的变量一样,在使用之前不仅要定义说明,还必须赋予具体的值,即让指针指向某个具体的数据。如果使用没有赋值的指针,可能会造成系统混乱,甚至程序崩溃。指针的值必须是一个地址,且必须保证其有效性。
在 C++语言中,使用取地址运算符“&”来取得一个变量的地址
&变量名
如“&x”表示变量x的地址,“&y”表示变量y的地址。取地址的变量必须是已经存在的变量。取地址运算符“&”只能应用于内存中存在的数据,如变量、数组元素等,不能用于表达式、常数或者寄存器(register)变量。
未经初始化的指针将指向一个不确定的内存地址,操作这样的指针是非常危险的。
int x; int * p1 = &x;
int * pt; pt = &x;
不过推荐使用第一种方法,这样可以保证一开始指针就是有效的,
不允许把一个无效的地址,比如数字赋给指针,指针变量和一般变量是一样的,存放的值是可以改变的,也就是改变其指向
在上面指针的定义中,指针变量是pt而不是*pt,因此要给指针赋值,等号左边是pt而不是*pt。
int a; int b; int * p = &a; p = &b;
一个指针变量,只能指向同类型的变量。比如一个整型指针只能指向整型变量,而不能指向其他类型的变量。因此不能用其他类型变量的地址初始化指针变量,或者为其赋值。
获取被指的数据
*指针名
int a; *(&a) = 0;
int a; a = 0;
值得指出的是,间接引用运算符“*”和取地址运算符“&”的作用正好相反,对一个变量取地址,并对结果使用间接运算符“*”,将得到变量。
有时需要知道某个指针所指的数据,此时就应当使用间接引用运算符“*”。与取地址运算符“&”相反,间接引用运算符“*”作用于指针,得到指针所指向的变量
示例
int x = 123; int *p = &x; int a = *p; a = x;
用变量x的地址初始化指针p,即让指针p指向变量x。除了直接访问变量x之外,也可以使用*p来间接地访问x。
p = &x
*p = 0; x = 0;
两者都是等效的,都可以将变量x赋值为0
优劣
通过指针来访问变量,虽然其效果和直接访问变量一样,但指针毕竟是一种间接的方式,其速度略慢于直接访问。不过指针也给程序的开发带来了很大的灵活性,其原因在于指针也是一个变量,可以在运行时修改其指向,从而达到“使用一个指针,访问多个变量”的目的。
雷区
在对指针使用间接运算符“*”之前,必须让指针指向一个合法的地址,否则程序的行为将是难以预料的,甚至会导致系统崩溃。原因是一个非法的地址可能是某个重要的数据地址,或者是某一条程序指令的地址,它们如果被修改,则程序的行为无法预料。
间接引用运算符“*”
int *p = NULL; *p = 0; int *q; *q = 123;
空指针不指向任何空间
会导致程序崩溃
定义一个指针变量但是没有初始化,指针会指向一个无效的地址
使用未初始化的指针变量,程序行为不可预料
使用示例
定义另外的整型指针变量q,并使其和p有同样的值,因此q也指向了变量a,等同于指针p。
指针的运算
指针变量也有加减运算。指针可以加减某个整型数,指针与指针可以相减,但指针与指针相加是没有意义的。
指针的值是一个内存地址,而一个内存地址可以用一个整型数表示,因此指针的运算可以看做整型数间的运算。
不过,指针运算有其特殊性。如果某个数据占据多个字节,那么该数据的指针只能指向其起始地址,即第一个字节的地址,指向中间某个字节是没有意义的。
指针与整数的加减
因此C++标准规定:指针加减某个整数,其效果等同于将指针移动整数个变量大小。
假设一个整型指针p指向整型变量x(占用4个字节的内存),而x的地址是1000,则p+1指向的地址就是1004,p-3指向的地址就是988。
一般来讲,指针加减n,就相当于指针的地址值加减n*sizeof(数据类型)
示例
一个short型(2字节大小)指针加减整型数的运算
取地址运算符“&”只能用于已经定义的变量(已分配内存),而不能用于表达式。但间接引用运算符没有这个限制,当然表达式的结果必须是某个内存地址。
*(p + 1)
求得指针移动后指向的数据。
*(p - 2)
求得指针移动后指向的数据。
指针与指针的减
指针与指针相减表示两个指针间可以存储的变量的个数。
假设两个short型指针p和q所指的地址分别是1000和996,则p-q的值就是2,即两个short型变量。
综合示例
指针的类型是int,因此对指针加1,移动的是4个字节,而double类型的指针加1移动的是8个字节。两个指针的减法,结果是两个地址之间可存放的变量的个数。
指针与数组
好处
数组中的元素,除了用数组名加下标的方法进行访问外,也可以用指针访问,而且用指针访问数组形式更加简单,使用也更加灵活。
指向数组的指针
数组是同类型数据的集合,集合中的元素按照顺序在内存中连续排列。用指针指向数组其实就是让指针指向这段连续内存的首地址,也就是数组中第一个元素(下标为0)的地址。
类型标识符 *指针名
示例
分别定义了两个指针,并指向同一个数组
int arr[10]; int *p; p = &arr[0]; int *q = &arr[0];
int arr[10]; int *p; p = arr; int *q = arr;
除了将第一个元素的地址赋给指针外,用指针指向数组也可以直接使用数组名。在C++程序中,数组名代表的就是数组的首地址(这也是为什么只能给数组元素赋值,而不能直接对数组赋值的原因)。
既然数组名代表的是数组的首地址,那么数组名也可以当做一个指针使用。对数组名使用间接引用运算"*"
可以得到数组中的第一个元素。也就是说对“*数组名”的操作,等同于对“数组名[0]”的操作,对两者进行赋值、计算等效果是一样的。
区别
在这里,虽然p,&arr[0],arr都代表了同一个地址,但是它们之间是有区别的,p是一个变量,而&arr[0]和arr则是两个表示地址的常量,可以修改p的值,但是却不可以修改&arr[0]和arr这两个常量的值。
使用指针访问数组
数组是一段连续的内存,指针可以指向数组,而且可以通过加减整数来移动指针。所以,可以通过指针来访问数组,即数组中的元素。指针指向数组的首地址,进而可以移动指针到指定的元素。也可以一次只增加1,从而达到遍历整个数组的目的。
示例
int arr[9]; int *p = arr;
*(p + i)
*(arr + i)
arr[i]
三个相同
雷区
使用指针访问数组时也要注意不要越界,即保证指针指向数组第一个到最后一个元素,超出这个范围将导致不可预料的后果。
示例
反转数组
在上面的程序中,分别对两个指针施行自加和自减操作,从而实现指针的移动。并且前指针往后移,后指针往前移,两个指针指向的数据正好是需要交换的两个元素。
指向字符串的指针
所以在用指针访问字符串时需要特别注意。字符串的指针就是一个char类型的指针(宽字符串用wchar_t类型的指针)。
char str[] = "Hello world"; char * p = str; char *q; q = str;
char * pstr = "Hello World";
两种写法都可以
这种定义方法从表面上看好像违背了指针的概念:指针就是一个指向地址的变量,在这个地址还没有确定的时候,不能给这个地址里面赋值。但其实这种写法是正确的,原因是在C++中这是一种特殊的情况,这个字符串“Hello World!”被定义在C++的常量存储区中,指针pstr是一个指向常量存储区中一块内存的指针。
常量存储区
常量存储区的内容是不可以修改的,因此对于上面定义的字符串 pstr,不可以修改其内容。
在对字符串进行赋值的时候,初学者很容易犯一个错误——直接给字符串的指针赋值。但正像不能直接给数组赋值一样,也不能直接给字符串的指针赋值。
示例
char buffer[16]; char *p = buffer; p = "Hello World";
定义一个字符数组
定义一个字符指针,并指向字符数组
字符指针指向字符串常量
这并不是为字符数组赋值
在上述程序中,初学者的目的或许是要把字符串“Hello World”存储在数组buffer中,但实际的运行效果却不是这样的。在赋值前,字符指针和字符串的关系如下图,在赋值后,字符指针和字符串的关系如图
其实赋值语句只是让p指向了一个常量字符串,而没有达到修改buffer的目的。正确的做法是使用C++的库函数strcpy,进行字符串复制
strcpy(p, "Hello World")
这个函数改变的是指针所指向内存的值,因此p仍旧指向buffer,而buffer里面的内容已经修改。当使用数组名来访问字符串的时候,因为数组名是一个地址常量,不能被修改,因此只能采用改变下标的方法来依次访问字符串的元素。可是当使用指针的时候,因为指针本身可以通过自增、自减等操作来移动。因此使用指针访问字符串非常灵活方便。
这就是优势
while(*pSrc++ = *pDst++);
这句简单的代码,就是前面提到的字符串复制函数strcpy的关键部分。pSrc和pDst是两个字符指针。因为字符串的结束标志是\0,其值也就是0,因此可以作为结束while循环的条件。++操作保证了每次赋值之后同时移动到字符串的下一位。
当操作符*和操作符++同时作用于一个指针的时候,要注意操作符的优先级,++操作符的优先级高于*操作符,因此*p++等价于*(p++)。
指针与动态内存分配
程序中内存的分配方式
了解变量在内存中的存储方式,对于理解指针的生存周期有着非常重要的作用。
在C++中,内存分为5个不同的区
栈区
存放函数的参数
存放局部变量
由编译器自动分配内存和释放内存
其分配运算内置于处理器的指令集中,效率很高。但是可使用的总量有限,一般不会超过1M字节。
堆区
new
分配内存
delete
释放内存
一个new有且只有且对应一个delete
堆区中内存的分配和释放由开发者负责。
如果开发者没有释放,在程序结束的时候操作系统会自动回收。在堆上可分配的内存比栈上大了很多,且使用非常灵活。
自由存储区
malloc
分配内存
free
释放内存
在C程序中经常使用
虽然在C++程序中仍然可以使用,但不如用堆方便,这里不多做介绍。
自由存储区和堆类似,内存的分配和释放由开发者负责。
全局/静态存储区
存放全局变量
存放静态变量
该存储区分配的内存在整个程序运行期间一直有效,直到程序结束由系统回收。
常量存储区
存放常量
定义的常量
指针字符串常量
示例
总结
栈、全局/静态存储区以及常量存储区中的分配是由编译器来进行的
并且在程序运行之前已经分配,因此称之为静态分配;
complier
堆以及自由存储区上内存的分配,是在程序运行的过程中进行的,称之为动态内存分配。
run
只有在堆上和自由存储区中分配的内存需要开发者管理。其他存储区中的变量只要定义即可,其内存的分配和释放由编译器负责。
在堆上分配内存
在堆上分配内存,要使用new关键字,后面跟一个数据类型。如果需要对分配出的内存进行初始化,则在类型后面加上一个括号,并带有初始值。为了保存分配出内存的地址,应当使用一个指针指向new的结果。
类型标志符 *指针名 = new 类型标志符(初始值)
右半边
new关键字
数据类型
(初始值)
对分配出的内存进行初始化
左边
数据类型
指针
同类型的指针变量
右边的输出结果为分配的内存的地址
举例
int *p = new int(3);
上面的语句在堆上分配一块整型变量的内存,并使指针 p 指向这块内存。括号中的 3 为这块内存提供一个初始值,该内存分配成功之后,会用3去初始化,因此*p的值为3。
int *p = new int;
在堆上分配内存的时候也可以不提供初始值
对于这种不提供显式初始化的情况
如果是内部数据类型,则不会被初始化。
如果new的类型是类,则会调用该类的默认构造函数;
C++ 默认构造函数是对类中的参数提供默认值的构造函数
int *pArr = new int[10];
用new不但可以为一个变量分配内存,还可以为一个数组分配内存,其方法是在new的类型标识符后面跟一个中括号,其中是数组的长度。
int a = 2; int *pArr = new int[a];
同数组的定义不同,用运算符new为数组分配内存空间时,其长度可以是变量,而不必是常量。因为动态内存分配在程序运行的时候才分配空间,所以当用new在堆上定义数组的时候,其长度可以是一个变量。
因此当数组的长度不确定的时候,一般都要使用new的方法定义数组。new数组也有其缺点,那就是不能提供显式初始化值。
释放堆上的内存
通过new分配的内存必须由开发者自己去释放,一块内存如果没有被释放,则会一直存在到该应用程序结束。在C++中使用delete来释放内存。
单个内存的释放
delete 变量名;
数组的释放
delete []指针名;
其中,中括号“[]”表示释放的是一个数组。用delete运算符释放为数组分配的内存时,不需要指定数组的长度。
示例
int *p = new int; delete p; int * pArr = new int[10]; delete []pArr;
示例
需要用户输入数组的长度以及数组中每个元素的值,采用动态创建的方法。
加上一个图片的原因
是因为pArray是指针变量指向首地址
可以写成pArray[i]吗?
new分配的内存,都需要显式地delete。如果缺少了delete,则会造成内存泄漏。含有这种错误的代码每被调用一次就丢失一块内存。也许刚开始时系统的内存充足,其影响还不明显,等到程序运行足够长的时间后,系统内存就会被耗尽,程序也就会崩溃。
内存泄漏
但是对于不是用new分配的内存地址,则在其上使用delete是不合法的。
string类字符串为STL(Standard Template Library, 标准模板库)中的一种自定义的数据类型
在程序中new和delete出现的次数必须相同,而且对于一块使用new分配的内存,只能delete一次,否则也会造成系统错误
const与指针
指向const的指针
const 类型名 *指针名
指针指向的数据是常量
不可以被修改
指针变量本身可以被修改
发挥就近原则,const离谁近,就是谁是常量
示例
const int *p
标识符p前面的“*”表明p是一个指针
int表明p是一个指向整型数据的指针
而const则表明p所指的数据是一个常量
也可以从指针变量往前剥开
示例
int a = 0; const int * pa; pa = &a; *pa = 1; a = 2;
试图对*pa的修改是错误的,因为pa是一个指向const的指针。
但是a本身还是一个变量,因此可以直接对a进行修改。
指向const的指针,这个const限制的是通过指针来修改变量的权限,而不是修改变量的权限,该变量还是可以被修改的。
对于符号常量,即用const修饰的变量,只能用指向const的指针来指向,而不能用普通的指针。
const int a = 10; int * p1 = &a; const int *p2 = &a;
第二个语句是错的
int a; int b; const int *p = &a; p = &b;
应用
在函数定义中,经常用指针来代替数组,对于只读的数组,经常用指向const的指针来限制。
总结
你既然选择了我,就不要改变我,或者你去选择别人
const指针
类型名 * const 指针名
指针变量本身是一个常量
只能指向定义时所给的那个数据
而不能指向别处,而对被指向的数据是没有影响的。
离指针近
int a; int * const p = &a;
因为不能修改const指针的指向,所以在定义的时候必须给一个初始值。
当const指针定义之后,可以修改其指向变量的值
*p = 1;
但不可以修改其指向
int b; p = &b;
错误的
总结
从你出生那天起,我们是命中注定,你可以改变我,但是你必须在我心里
指向const的const指针
上面讲述了const在两个位置的不同效果,const还可以同时出现在这两个位置
const 类型名 * const 指针名
这是前面两种情况的组合,其指向的变量和指向都不可以被修改
示例
int a = 0; const int * const p = &a;
对于这个指针p,下面两种情况的修改都是错误的:
* p =1;
int b; p = &b;
总结
岁月静好,忠心不二
总结
const是一个常量修饰符。使用const修饰一个变量时,将使其值不可改变,从而变成一个常量。const也可以在定义指针时使用,不过此时const不仅可以修饰指针变量,也可以修饰指针所指向的数据。
就近原则
三种const指针的定义很相似,很难以区分,这里有一个简单的技巧:从标识符的开始处读它,并从里向外读,const指定那个“最靠近”的。因此根据这个理解:第一个定义表示int不可以被改变,第二个表示p不可以被改变,第三个表示int和p都不可以被改变。
引用
定义引用
引用也是一种数据类型。
引用不能独立存在,而只能依附于一个变量。
必须要指明哪个变量的应用
类型标志符 &引用名 = 目标变量名;
目标变量的数据类型
引用修饰符“&”
引用的标识符
目标变量的标识符
类型标志符是目标变量的类型
“&”是引用修饰符,表示定义的是一个引用。
而被引用的变量则通过赋值运算符指定。
特点
引用一旦定义,则始终跟其目标变量绑定,而不能改变为其他变量的引用。
假设 b 是变量 a的引用,则在b的生命周期内,b始终都是a的引用,而不能再改变为其他变量的引用。
引用在其生命周期内完全可以替代其目标变量
所有施加于引用上的操作,其效果都等同于直接对引用的目标变量进行操作。
一旦目标变量的值发生了改变,引用的值也会发生同样的改变。
鉴于引用的不可变更性,以及引用与目标变量的等价性,一个变量的引用也可以看做该变量的别名。
定义一个引用只不过是给变量另外起了一个名字。这样两个名字拥有一个实体,对一个名字的操作自然也会影响到另外一个名字。
引用的上述行为和特性类似于const指针。首先,const指针也是定义后不能再指向别的变量的;其次,通过const指针可以间接地修改其目标变量,而且对目标变量的修改也会影响const指针。
示例
int a; int & b = a;
定义一个变量
定义上述变量的引用
“&”在这里不是取地址运算符,而是一个引用修饰符。
常引用
定义引用时也可以用const进行修饰,其结果为一个常引用。
const 类型标志符 &引用名 = 目标变量名;
示例
int a; const int & b = a; a = 1;
不可以通过常引用修改变量
上面语句为a定义了一个const的引用b,这样,就可以限制通过引用b来修改目标变量a,相当于给了这个别名只读不写的权限。
一般的引用在定义时必须有一个已经存在的变量,而常引用则没有这样的限制,可以定义一个字面常量的常引用。
示例
const int &a = 12;
定义一个字面常量的常引用
对于符号常量,即被const修饰的变量,其对应的引用必为常引用。否则对于一个变量实体,其一个名字显示不可修改,而另一个名字显示可以修改,这样显然是矛盾的。
示例
定义符号常量
const int a = 10;
注意,应在声明时对const进行初始化,如果在声明常量时没有提供值,则该常量的值将是不确定的,且无法修改。
使用const关键字来创建符号常量,常量被创建后其值就固定了,编译器将不允许修改该常量的值。
定义引用
const int & b = a;
引用与指针的区别
引用是C++特有的新类型(与C相比)。
引用提供了与指针操作同等的功能。
引用必须与一个变量绑定,不存在没有任何目标的引用。因此如果在使用的过程中有可能出现什么都不指向的情况,则应该使用指针,可以把一个空值给指针。而若一个变量肯定指向某个对象,不允许为空,则可以使用引用。
引用仅仅是一个变量实体的别名。因此在使用引用之前,不需要检测其合法性。但指针在使用之前必须检测其指向的对象是否为空,不能对空指针进行取值操作。
指针可以重新赋值以重新指向新的对象,引用在初始化之后就不可以改变指向对象了。
指针可以指向数组的地址来替代数组使用,而引用不可以替代数组,引用只能指向数组中的某一个元素。
指针可以用于在堆上生成的对象,delete的对象只能是一个指针,不能是引用。
综合示例
冒泡排序