C++指针和引用

引用

什么是引用

引用是一个变量的别名,就是给变量再取一个名字。

如何创建一个引用
int i = 17;
int& r = i;    //表示创建一个引用,引向int类型。这里的r是i的引用。
引用的原理

如下图,i和r表示的都是同一段内存。

由于i和r表示的都是同一段内存,所以通过i改变内存空间的值,会影响到r。比如下面的代码:

int i = 17;
int&  r = i;
r=18;
cout<<i<<endl;	//i也会变成18
引用的注意事项
  • 必须初始化。即必须给引用赋值。
  • 必须引用到变量,不能引用到常量。(因为常量没有内存空间)
  • 不能改变引用的值。(自始至终都只能引用同一个变量,很专一,不像指针那么浪荡)

指针

取地址运算符 &

注意:这里不是“与操作”,在C++的语法中,相同的符号用在不同的地方有不同的作用(运算符重载)。

背景知识:
内存空间的每一个字节都是有自己的地址的。操作系统是通过这些地址找到变量值的。如下图。

语法:
&<变量名>   //表示得到变量的地址
比如:
int  var1;
char var2[10];
 
cout << "var1 变量的地址: ";
cout << &var1 << endl;
 
cout << "var2 变量的地址: ";
cout << &var2 << endl;

输出结果:

注意:每一次运行输出的地址都不一样。因为变量的内存空间是向内存申请的,并且程序运行完后,所有的内存空间都会被回收删除。所以,下一次程序运行时申请得到的地址就会不一样。(如果一样,就可以买彩票了)

什么是指针

指针是一个变量,这个变量存储的类型是:所指向类型变量的地址。
因为指针是一个变量,所以它也有自己的存储空间,也有自己的地址。
为什么会有指针?因为保存了变量的地址,就可以直接对内存空间进行操作。

如何创建一个指针

语法:数据类型* 指针名 = 变量地址;
int i=8;
int* p=&i;	//变量p中存储着变量i的地址
cout<<p<<endl;	//输出p的值,即输出i的地址

上面的代码创建了一个指向int类型变量的指针p,里面保存着变量i的地址。
int*是指针p的类型,所以p只能指向int整型变量。
上面的代码中:p保存的是i的地址,我们读作p指向i。

如何使用一个指针-用地址运算符 *

在这里,星号*又有一个新的作用:用地址运算符。
比如,在上面的代码中,*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变量,如下图。
因为指针也是一个变量,所以指针也是可以被指向的。

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;
}

代码解释:

由于形参是指针或者引用。那么,调用函数的时候就会发生传值过程。
在传值过程中,会将变量赋值给引用,或者把变量地址赋值给指针。
这样的话,形参就会引用到实参或者指向实参,这样就可以直接操作实参的内存空间。
所以通过引用和指针的方式可以使得实参值发生改变。


posted @ 2019-10-06 21:19  NetRookieX  阅读(16)  评论(0编辑  收藏  举报