C++中指针与引用详解
在计算机存储数据时必须要知道三个基本要素:信息存储在何处?存储的值为多少?存储的值是什么类型?因此指针是表示信息在内存中存储地址的一类特殊变量,指针和其所指向的变量就像是一个硬币的两面。指针一直都是学习C语言的难点,在C++中又多了一个引用的概念。初学时很容易把这两个概念弄混,下面就来通过一些例子来说明二者之间的差别。
1、指针的声明
上文中提到,指针和其所指向的变量就像硬币的两面,因此通过取址符号"&"
我们可以找到变量的地址,通过解引用符号"*"
可以找到地址内存放的变量值。
int data = 10; //声明了一个变量data,并赋初始值10,存储的值是int类型
int* p_data = &data; //找到 data 在内存中存放的位置,即p_data
cout << "地址为:" << int(p_data) << "\t 存放的值为:" << data << endl;
输出结果为:
地址为:8191436 存放的值为:10
地址默认是16进制,我们在输出时将其转换成了int
类型,因此以十进制输出。输出结果翻译过来就是:在地址编码为8191436的位置存放了值为10的变量data,再进一步地说,data
与*p_data
表示同一个东西。为了更有助于理解,我们绘制了下图:
因此从本质上看,指针与普通的变量并没有什么太大的区别,只是指针变量可以通过解引用的方式找到指针所对应的地址中存放的数值。假如定义如下:
int data = 10;
int* p_data = &data; //定义指向 int 类型的指针 p_data, 存储的是 int 类型的变量 data的地址,其
int** p_p_data = &p_data; //定义指向 int* 类型的指针 p_p_data, 存储的是 int* 类型的变量 p_data的地址
cout << "p_data:" << p_data << "\t 存放的值为:" << *p_data << endl;
cout << "p_p_data:" << p_p_data << "\t 存放的值为:" << *p_p_data << endl;
输出结果为:
p_data:00EFF96C 存放的值为:10
p_p_data:00EFF960 存放的值为:00EFF96C
从输出结果可以看出,p_p_data
中存储的值就是p_data
,而p_data
中存储的值就是data
,很像”我爱她,她爱他“的这种桥段。下面我们就重点分析一下变量与指针之间的关系:我们在上述例子中把指针初始化为变量的地址,而变量是在编译时分配的有名称的内存,指针只是为可以通过名称直接访问的内存提供了一个别名。还拿上面这个例子:对程序员来说,变量10的名字就是data;而对于计算机来说,变量10就是存在 8191436 地址的数据;实现程序员与计算机沟通的方式就是指针,通过对data取址让程序员能够明白计算机的存储结构,同样,通过对地址解引用,也能轻松地找到该地址中存储的数据。在上述情况下,指针的出现显得有些多余,然而指针的真正用武之地在于,在运行阶段分配未命名的内存以存储值,在这种情况下,只能通过指针来访问内存。
最后关于指针声明的一点建议:在声明一个指针变量时,必须要指定一个确定的地址,否则声明的指针变量不知道指向哪里,因此容易造成系统崩溃。
2、使用new
来分配内存
内存四区之代码区,全局区,栈区和堆区 – ZhiboZhao – 博客园 (cnblogs.com) 中提到过,new
会在堆区创建一个内存空间,其返回值就是该内存空间的地址,因此程序员的责任就是将该地址赋给一个指针。下面是一个示例:
int* p_data = new int; //在堆区开辟一个空间,并返回该内存空间的地址
*p_data = 10; //将向该内存中存储数值10
cout << "p_data:\t" << p_data << "\t *p_data: " << *p_data << endl;
通过比较会发现,new
后面指定了数据类型 int
,同样地,p_data
也被声明为指向 int
的指针。这是因为,计算机的内存是以字节为存储单位,不同类型的变量会占用不同的字节,因此使用 new
时必须要告诉编译器分配多少字节的存储空间,并且接收的指针也必须与声明的类型一致。输出结果为:
p_data: 00D0D9A0 *p_data: 10
当处理大型数据,比如数组时,通常会使用的一种方法是定义一个数组类型的数据,在定义的时候分配足够大的空间。但是这种做法太过于死板,但是当使用 new
时,如果在运行阶段需要数组,那么则创建它,如果不需要则不创建,最重要的是可以在程序运行时选择数组的长度。 下面就看一下如何使用 new
来创建动态数组。在C++中,数组名被解释为数组地址,即数组第一个元素的地址。下面是一个实例:
int Arr[10]; // 定义一个包含10个int类型元素的数组
cout << "Arr:" << Arr << "\t&Arr[0]:" << &Arr[0] <<endl;
输出结果为:
Arr:008FFAB4 &Arr[0]:008FFAB4
这种声明方式只能在刚开始就声明固定的数组长度,在C++中创建动态数组时,只需要将数组的元素类型和元素数目告诉给 new
即可,new
的返回值同样是数组的首地址。
int ele_num = 10; //临时指定数组内元素的个数
int* p_arr = new int [ele_num]; //根据临时指定的元素个数创建数组
通过 new
在堆区开辟空间,由程序员管理释放,因此当 new
的内存不用后,需要通过 delete
进行变量,使用 delete []
来释放开辟的数组空间。代码如下:
int* p_data = new int;
*p_data = 10;
cout << "p_data: " << p_data << "\t*p_data:" << *p_data << endl;
int ele_num = 10;
int* p_arr = new int [ele_num];
for(int i = 0; i<9; i++)
*(p_arr+i) = i+2;
cout << "p_arr:" << p_arr << "\t\t*(p_array):";
for(int i = 0; i<9; i++)
cout << *(p_arr + i) << " ";
cout << endl;
delete p_data;
delete [] p_arr;
cout << "\n******使用delete释放内存后......*******" << endl;
cout << "p_data: " << p_data << "\t*p_data:" << *p_data << endl;
cout << "p_arr:" << p_arr << "\t\t*(p_array):";
for(int i = 0; i<9; i++)
cout << *(p_arr + i) << " ";
cout << endl;
输出结果如下:
p_data: 0082B1C8 *p_data:10
p_arr:0082BB58 *(p_array):2 3 4 5 6 7 8 9 10
******使用delete释放内存后......*******
p_data: 0082B1C8 *p_data:-572662307
p_arr:0082BB58 *(p_array):-572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -572662307 -572662307
3、malloc 与 new 的区别
学过C语言的朋友都知道,在C语言中通过malloc
函数开辟一块内存空间,malloc
的函数原型如下:
void* malloc(unsigned int numbytes);
从函数原型的参数可以看出,malloc
函数以字节数为参数,开辟固定字节的内存空间。这与 new
就有了第一点不同:new
不需要自己计算字节数,只需要给定内存中存储的数据类型与元数个数即可。
从函数原型的返回类型可以看出,malloc
函数返回 void*
类型,需要我们在使用时自己指定指针类型。比如:
int* p_malloc = nullptr; // 创建一个指向int的指针
p_malloc = (int*) malloc(10); //将 malloc 的返回值强制转换为 int* 类型
而 new
在使用时则不需要。总结看来,malloc
在使用时需要自己根据内存中的数据类型以及内存长度计算处所需要的字节数,然后返回 void*
类型,需要使用对应类型的指针进行接收。而 new
在使用时只需要给定内存的长度与内存中数据的类型,编译器会自动计算所需要的字节数。
4、引用的声明与本质
C++中新增了引用作为已定义的变量的别名。引用的最主要用途是作为函数形参,这样函数就可以使用原始数据而不是数据副本,这样听起来似乎与指针没什么区别,我们还是从引用的声明说起。
int data = 10;
int& p_data = data; //创建一个引用变量 p_data
cout << "data:" << data << "\tp_data:" << p_data << endl; //p_data 与 data 相当于一个变量的两个名字
输出结果为:
data:10 p_data:10
从输出结果来看,p_data
与 data
就是一个变量的两个不同叫法而已。引用必须在声明时就为其指定初始值,而不能像指针一样可以先声明,再赋值。下面将引用作为函数的参数来进一步说明引用与指针的区别:
template <typename T> //定义一个模板函数
void swap(T a, T b){
int temp;
temp = a;
a = b;
b = temp;
}
int main(void){
int a = 10, b = 20;
swap_value<int>(a,b); //首先进行值传递
cout << "a:" << a << "\t\tb:" << b << endl;
swap_value<int&>(a,b); //然后进行引用传递
cout << "\na:" << a << "\t\tb:" << b << endl;
}
从上述代码中可以看到,值传递和引用传递的形参都是一样的,不同的是引用传递时,实参被声明为引用,引用的用法与使用值一模一样,输出结果如下:
a:10 b:20
a:20 b:10
惊奇的发现,引用传递改变了原始数据的值,这点与指针的用法一致,但是指针在书写 swap
函数时应该这样写:
void swap(int* a, int* b){
int temp;
temp = *a;
*a = *b;
*b = temp;
}
swap(&a, &b); //调用格式
综上发现,引用其实就是变量的另一个名称,它的用法与变量一模一样,但是能在作为形参传递时,改变原始数据的值。除了这些用法上的区别,引用的本质其实就是一个指针常量,意味着指针指向的位置不可变,但是指针指向位置的值可变。即:
// 这两者的语句是等效的,因此引用被当作指针常量来处理
int& p_a = a;
int* const p_a=&a;
再补充一点小知识,关于 const
修饰符的问题,有些新手朋友来说很容易弄不清楚 const
修饰下什么是可变的,什么是不可变的。具体实例如下:
int data = 10, data2 = 20;
const int* p_data = &data; //修饰的是int,即 p_data 所指向的值不可变,而p_data可变
p_data = &data2;
int* const p_data2 = &data; //修饰的是int*,即 p_data 所指向的值可变,而p_data不可变
*p_data2 = data2;
引用即是第二种用法。