C++指针和引用
引用
什么是引用
引用是一个变量的别名,就是给变量再取一个名字。
如何创建一个引用
int i = 17;
int& r = i; //表示创建一个引用,引向int类型。这里的r是i的引用。
引用的原理
如下图,i和r表示的都是同一段内存。
![](https://i.loli.net/2019/09/15/oWdp3z8tH45PrSI.png)
由于i和r表示的都是同一段内存,所以通过i改变内存空间的值,会影响到r。比如下面的代码:
int i = 17;
int& r = i;
r=18;
cout<<i<<endl; //i也会变成18
引用的注意事项
- 必须初始化。即必须给引用赋值。
- 必须引用到变量,不能引用到常量。(因为常量没有内存空间)
- 不能改变引用的值。(自始至终都只能引用同一个变量,很专一,不像指针那么浪荡)
指针
取地址运算符 &
注意:这里不是“与操作”,在C++的语法中,相同的符号用在不同的地方有不同的作用(运算符重载)。
背景知识:
内存空间的每一个字节都是有自己的地址的。操作系统是通过这些地址找到变量值的。如下图。
![](https://i.loli.net/2019/09/15/HWMjEZuwIPVpL29.png)
语法:
&<变量名> //表示得到变量的地址
比如:
int var1;
char var2[10];
cout << "var1 变量的地址: ";
cout << &var1 << endl;
cout << "var2 变量的地址: ";
cout << &var2 << endl;
输出结果:
![](https://i.loli.net/2019/09/15/o6AeilxhLT21gPH.png)
注意:每一次运行输出的地址都不一样。因为变量的内存空间是向内存申请的,并且程序运行完后,所有的内存空间都会被回收删除。所以,下一次程序运行时申请得到的地址就会不一样。(如果一样,就可以买彩票了)
什么是指针
指针是一个变量,这个变量存储的类型是:所指向类型变量的地址。
因为指针是一个变量,所以它也有自己的存储空间,也有自己的地址。
为什么会有指针?因为保存了变量的地址,就可以直接对内存空间进行操作。
如何创建一个指针
语法:数据类型* 指针名 = 变量地址;
int i=8;
int* p=&i; //变量p中存储着变量i的地址
cout<<p<<endl; //输出p的值,即输出i的地址
上面的代码创建了一个指向int类型变量的指针p,里面保存着变量i的地址。
int*是指针p的类型,所以p只能指向int整型变量。
上面的代码中:p保存的是i的地址,我们读作p指向i。
![](https://i.loli.net/2019/09/22/OQDiyoXHbzaltF6.png)
如何使用一个指针-用地址运算符 *
在这里,星号*又有一个新的作用:用地址运算符。
比如,在上面的代码中,*p的意义和i是一样的。
int i = 8;
int* p = &i; //变量p中存储着变量i的地址
cout << “p的值为:"<< p << endl; //输出p的值,即输出i的地址
cout << "*p = " << *p << endl; //用地址:得到该地址的值
*p = 10; //用地址:修改该地址的值
cout << "修改后 p = " << p << " *p = " << *p << endl;
*p = 10;
相当于i=10;
。即*p
相当于i
。
因为p是指向i的,那么上面的代码
*p = 10
修改值为10后,i的值就变成10了。
指针的内存大小:
指针是一个变量,所以它也有自己的内存空间。通过上面的代码
输出p
,我们可以看到地址是一个8位二进制组成(32位二进制,4字节)。
即不管指针所指向的类型是什么,指针的内存大小都是4字节。
例如:double* p;
指针p的内存大小依然为4字节,而不是double的8字节。
空指针
- 指针是保存地址的,如果随意设置指针的值,就很有可能随意修改其他程序的内存空间。导致其他程序崩溃。
- 在C++中,NULL表示空,它和0相等。
- 操作系统中,0的内存空间不被使用,也无法访问。
- 所以我们可以将指针初始化为空。也就是说,我们定义指针的时候,将指针的值设置为NULL(也就是0)。
#include <iostream>
using namespace std;
int main (){
int *ptr = NULL;
cout << "ptr 的值是 " << ptr ; //这里会输出0
return 0;
}
指针与一维数组
数组名其实是一个地址,保存着第一个元素的地址。
int i[5] = { 0, 1, 2, 3, 4 };
cout << i << endl; //输出第一个元素的地址
程序是通过变量起始地址来找到元素值的。
比如,如果我要得到i[2]
,就相当于*(i+2)
。
可以使用指针来代替数组名:
int i[5] = { 0, 1, 2, 3, 4 };
int *p = i;
cout << p[2] << endl; //输出2
字符类型的指针可以保存字符串:
char* s = "hello"; //s保存的是字符串的起始地址
cout << s[1] << endl; //输出e
char* s = "hello";
相当于char s[]={'h','e','l','l','o','\0'};
指针的算术运算
因为指针是一个地址,地址也是一个数字,所以它可以进行算术运算。
指针可以进行++,–,+,-四种算术运算。
比如,前面讲到的i[2]
相当于*(i+2)
,就使用到了算术运算+ (地址加了2)。
代码举例:
char c[5] = { 'a', 'b', 'c', 'd', 'e' };
char* p1 = c;
p1++;
cout << *p1 << endl; //输出:b
int i[5] = { 0, 1, 2, 3, 4, };
int* p2 = i;
p2++;
cout << *p2 << endl; //输出:1
问题引出:
内存空间的地址是按字节分配的,也就是说,每一个字节都有自己的地址。
问题来了:int类型有4个字节,那么上面的数组中,p2++为什么能指向1。
问题解释:
指针的算术运算是按照所指向数据类型的空间大小为单位的。
比如,上面的p2指向的是int类型的变量,int类型大小为4个字节,那么p2++实际上的地址加了4。
说白了,程序员不必关心所指向数据类型的空间大小。
动态内存分配
为什么会出现动态内存分配?
如果我们需要很多内存空间(如数组),申请太少会不够用,申请太多会浪费。
这是因为我们不知道会需要多少内存空间,只能在写代码的时候猜测需要多少。
即运行前已经确定内存空间大小。如何解决这个问题?
使用动态内存分配,在运行时根据需要申请。
这是一个动态的过程,不运行的话谁也不知道要申请多少,运行前是不确定的。
代码示例:
int* p1 = new int; //申请了1个整型的内存空间
*p1 = 1;
int *p2 = new int[5]; //申请了5个整型的内存空间(可以通过变量动态的修改这个值)
for (int i = 0; i < 5; i++){
p2[i] = i; //分别对5个整型的内存空间赋值
}
动态内存分配的回收:
在C++中,函数或程序运行完会删除创建的所有变量,但是没有变量名的存储空间不会被删除。
即通过动态内存分配申请的内存空间不会被删除。所有我们要手动回收。
注意:指针是一个变量,也会被自动回收。但是指针指向的内存空间如果没有变量名,就不会被自动回收。
也就是说:只要出现new,就要手动回收这些new出来的内存空间。
代码示例:
int* p1 = new int; //申请了1个整型的内存空间
*p1 = 1;
int *p2 = new int[5]; //申请了5个整型的内存空间
for (int i = 0; i < 5; i++){
p2[i] = i; //分别对5个整型的内存空间赋值
}
delete p1; //回收p1
delete[] p2; //回收p2
代码解释:
new和delete需要成对出现。
这里的delete[] p2表示回收连续的内存空间(数组),这里不用写明内存空间的大小,delete会自动判断。
指针和二维数组
在二维数组i[2][2]中:有两个数组,分别是i[0]和i[1] ,i是此数组的地址。
i[0]是一个数组的地址,i[0][0]和i[0][1]。
i[1]是一个数组的地址,i[1][0]和i[1][1]。
即:i指向一个指针数组,其中的元素也指向数组。
代码示例:
int i[2][2] = { { 1, 2 }, { 3, 4 } };
cout << i << endl; //输出二维数组的起始地址,也就是i[0],也就是i[0][0]的地址
cout << i[0] << endl; //输出的是i[0][0]的地址
cout << i[0][0] << endl; //输出1
代码解释:
这样创建数组得到的内存空间是连续的。
i[0]和i的值是相等的,都是此数组的起始地址。
指向指针的指针
什么是指向指针的指针?
A指针指向B指针,B指针再指向C变量,如下图。
因为指针也是一个变量,所以指针也是可以被指向的。
![](https://i.loli.net/2019/10/03/EbTvouJjxcsZmlD.png)
int i = 1;
int* p1 = &i;
int** p2 = &p1;
cout << "p2 = " << p2 << endl; //得到p2的值,即p1的地址
cout << "*p2 = " << *p2 << endl; //得到p2所指向的内容,即p1的内容,即i的地址
cout << "**p2 = " << **p2 << endl; //得到p2两重指向的内容,即i
代码解释:
**p2相当于*(*p2),因为*p2也是一个地址(即p1)。
这样的指针称作为二级指针,所以可以有三级,四级…
我们常用的是一级指针和二级指针。
二级指针如何进行动态内存分配
代码示例:
int** p = NULL;
p = new int*[5]; //申请5个int*类型的数组(每个元素都是指针)
for (int i = 0; i < 5; i++){
p[i] = new int[3]; //每个元素申请3个int类型的数组
}
p[0][0] = 100; //通过p对二维数组进行操作
cout << "p[0][0] = " << p[0][0] << endl;
for (int i = 0; i < 5; i++) //由里到外依次delete
delete[] p[i];
delete p;
代码解释:
首先申请了指针类型的数组。
然后使用这些指针类型的元素再来申请一维数组。
即每一个元素都指向了一个一维数组,这些一维数组合起来就变成了二维数组
最后不要忘记delete和new成对出现。
注意delete的顺序是new的过程相反的。因为一旦删除了指针元素的数组,这些指针指向的一维数组就无法找到了,所以要先回收这些一维数组,而不是指针数组。
指针和引用的比较
引用的特点:
直接引用到存储空间。通过引用直接对变量操作。
不存在空引用
必须用一个变量给引用赋值。
不可引用到其他变量。指针的特点:
指针保存的是内存空间的地址,通过地址间接对变量操作。
初始化时建议赋值为空。
指针保存的是一个变量的地址。
可以指向其他同类型的变量。
函数参数为引用或指针
知识回顾:
形参与实参是不同的变量,调用函数时,只是将实参的值传给形参。
得出结论:
如果形参和实参的内存空间不一样,那么形参值的变化不会影响到实参值。
获得启发:
如果将函数形参设置为引用和指针,那么形参和是实参将共享同一段内存空间,他们的值也能相互影响了
代码示例:使用引用作为参数形参
void func(int &a){
a = a + 1;
return;
}
int main(){
int a = 10;
func(a);
cout << "a = " << a << endl;
}
代码示例:使用指针作为参数形参
void func(int *a){
*a = *a + 1;
return;
}
int main(){
int a = 10;
func(&a);
cout << "a = " << a << endl;
}
代码解释:
由于形参是指针或者引用。那么,调用函数的时候就会发生传值过程。
在传值过程中,会将变量赋值给引用,或者把变量地址赋值给指针。
这样的话,形参就会引用到实参或者指向实参,这样就可以直接操作实参的内存空间。
所以通过引用和指针的方式可以使得实参值发生改变。