C/C++中指针和引用之相关问题深入研究
作者:
一、基本知识
指针和引用的声明方式:
声明指针: char* pc;
声明引用: char c = 'A'
char& rc = c;
它们的区别:
①从现象上看,指针在运行时可以改变其所指向的值,而引用一旦和某个对象绑定后就不再改变。这句话可以理解为:指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变,但是指定的对象其内容可以改变。
②从内存分配上看,程序为指针变量分配内存区域,而不为引用分配内存区域,因为引用声明时必须初始化,从而指向一个已经存在的对象。引用不能指向空值。
③从编译上看,程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再改,因此指针可以改变指向的对象(指针变量中的值可以改),而引用对象不能改。这是使用指针不安全而使用引用安全的主要原因。从某种意义上来说引用可以被认为是不能改变的指针。
④不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前不需要测试它的合法性。相反,指针则应该总是被测试,防止其为空。
⑤理论上,对于指针的级数没有限制,但是引用只能是一级。
如下:
int** p1; // 合法。指向指针的指针
int*& p2; // 合法。指向指针的引用
int&* p3; // 非法。指向引用的指针是非法的
int&& p4; // 非法。指向引用的引用是非法的
注意上述读法是从左到右。
程序1:
#include "stdio.h"
int main(void)
{
// 声明一个char型指针pc,且让它指向空值
char* pc = 0;
char a = 'a';
// 声明一个引用rc,且让它引用变量a
char& rc = a;
printf("%d, %c\n", pc, rc);
char *pc2;
// 声明一个指针,但可以不初始化
pc2 = pc;
// char& rc2;
// 上面语句编译时,会产生如下错误:
// error C2530: 'rc2' : references must be initialized
// 即,应用必须初始化
// rc = *pc;
// 上面语句编译不会有问题,但运行时,会报如下错误:
// "0x00401057"指令引用的"0x00000000"内存。该内存不能为"read"
// 说明引用在任何情况下,都不能指向空值
return 0;
}
程序2:
#include <iostream>
#include <string>
using namespace std;
int main(void)
{
string s1("Hello");
string s2("World");
// printf("%s\n", s1); 不能用printf输出s1,而应该用cout
cout << "s1的地址 = "<< &s1 << endl;// &s1 = 0012FF64
cout << "s2的地址 = "<< &s2 << endl;// &s2 = 0012FF54
string& rs = s1; // 1. 定义一个引用rs,rs引用s1
cout << "引用rs的地址 = " << &rs << endl; // &rs = 0012FF64
string* ps = &s1; //定义一个指针ps, ps指向s1
cout << "指针ps的地址 = " << ps << endl;// ps = 0012FF64
cout << rs << ", " << *ps << endl; // Hello, Hello
// 如果没有#include <string>,上面的语句在编译的时候,会出现如下错误:
// error C2679: binary '<<' : no operator defined which takes a right-
// hand operand of type 'class std::basic_string<char,struct
// std::char_traits<char>,class std::allocator<char> >'
// (or there is no acceptable conversion)
rs = s2; // 2. rs仍旧引用s1, 但是s1现在的值是"World"
ps = &s2; // ps现在指向s2
cout << "引用rs的地址 = " << &rs << endl; // &rs = 0012FF64 未改变
cout << "引用rs的值 = " << rs << endl; // rs = "World" 已改变
cout << "指针ps的地址 = " << ps << endl;// ps = 0012FF54 已改变
cout << "指针ps所指地址的内容 = " << *ps << endl; // *ps = World已改变
cout << "s1的地址 = "<< &s1 << endl;// 3. &s1 = 0012FF64 未改变
cout << "s1的值 = " << s1 << endl; // 4. s1 = World 已改变
return 0;
}
可以认为:
引用就是变量的别名,在引用初始化的时候就已经确定,以后不能再改变。见程序2的粗体字语句。第1句,声明了rs引用s1,s1的值为”Hello”,从这以后,rs实际上就相当于变量s1了,或者从更本质的意义上来说,rs的地址就是初始化时s1的地址了,以后都不会再改变。这应该比较好理解,比如我们在程序中定义了一个变量a,不管我们如何给a赋值,但它的地址是不会改变的;
第2句,rs仍旧指向初始化时s1的地址,但此处的赋值就相当于重新给s1赋值,因此我们从第3句和第4句可以看到,s1的地址并没有发生变化,但是其值已经发生了变化。
二、作为参数传递
利用引用的这个特性,可以用它作为函数的传出参数。如程序3:
#include <iostream>
#include <string>
using namespace std;
int newEvaluation(string& aStr)
{
string bStr("Hello,");
aStr = bStr + aStr;
return 0;
}
int main(void)
{
string aStr("Patrick!");
newEvaluation(aStr);
std::cout << aStr << endl; // 输出结果:"Hello, Patrick!"
return 0;
}
而一般变量,则不能从函数内部传值出来,比如程序4:
#include <iostream>
#include <string>
using namespace std;
int newEvaluation(string aStr)
{
string bStr("Hello,");
aStr = bStr + aStr;
return 0;
}
int main(void)
{
string aStr("Patrick!");
newEvaluation(aStr);
std::cout << aStr << endl; // 输出结果:"Patrick!",aStr的值没有变化
return 0;
}
当然程序3引用传递的方式也可以写成指针传递的方式,如程序5:
#include <iostream>
#include <string>
using namespace std;
int newEvaluation(string* const aStr)
{
string bStr("Hello,");
*aStr = bStr + *aStr;
return 0;
}
int main(void)
{
string aStr("Patrick!");
newEvaluation(&aStr);
std::cout << aStr << endl; // 输出结果:"Hello, Patrick!"
return 0;
}
注意程序中的陷井,如程序6:
#include <iostream.h>
int *pPointer;
void SomeFunction()
{
int nNumber;
nNumber = 25;
//让指针指向nNumber
pPointer = &nNumber;
}
void main()
{
SomeFunction();//为pPointer赋值
//为什么这里失败了?为什么没有得到25
cout << "Value of *pPointer: " << *pPointer << endl;
}
这段程序先调用了SomeFunction函数,创建了个叫nNumber的变量,接着让指针pPointer指向了它。可是问题出在哪儿呢?当函数结束后,nNumber被删掉了,因为这一个局部变量。局部变量在定义它的函数执行完后都会被系统自动删掉。也就是说当SomeFunction 函数返回主函数main()时,这个变量已经被删掉,但pPointer还指着变量曾经用过的但现在已不属于这个程序的区域。
尽管在SomeFunction中使用所谓的动态分配内存。程序7中也存在陷井:
#include <iostream.h>
int *pPointer;
void SomeFunction()
{
int intNumber = 25;
// 让指针指向一个新的整型
pPointer = new int;
pPointer = &intNumber;
}
void main()
{
SomeFunction(); // 为pPointer赋值
cout<< "Value of *pPointer: " << *pPointer << endl;
delete pPointer;
}
原因也如上面所言,intNumber的作用范围仅限于SomeFunction中,离开了SomeFunction,那么intNumber就不存在了,那么&intNumber即intNumber的地址就变得没有意义了,因此,该地址所指向的值是不确定的。如果改为下面的程序就不会有问题了。
程序8:
#include <iostream.h>
int *pPointer;
void SomeFunction()
{
int intNumber = 25;
// 让指针指向一个新的整型
pPointer = new int(intNumber);
}
void main()
{
SomeFunction(); // 为pPointer赋值
cout<< "Value of *pPointer: " << *pPointer << endl;
delete pPointer;
}
三、指针的指针
前面说到,指针是没有级数限制的。
程序9:
#include<stdio.h>
#include<stdlib.h>
void main(void)
{
int i, j;
int a[10], b[3][4], *p1, *p2, **p3;
for(i = 0; i < 10; i++)
scanf("%d", &a[i]);
for(i = 0; i < 3; i++)
for(j = 0; j < 4; j++)
scanf("%d", &b[i][j]);
p1 = a;
p3 = &p1;
for(i = 0; i < 10; i++)
printf("%4d", *(*p3+i));
printf("\n");
for(p1 = a; p1 - a < 10; p1++)
{
p3 = &p1;
printf("%4d", **p3);
}
printf("\n");
for(i = 0; i < 3; i++)
{
p2 = b[i];
p3 = &p2;
for(j = 0; j < 4; j++)
printf("%4d",*(*p3+j));
printf("\n");
}
for(i = 0; i < 3; i++)
{
p2 = b[i];
for(p2 = b[i]; p2-b[i] < 4; p2++)
{
p3 = &p2;
printf("%4d", **p3);
}
printf("\n");
}
}
输出的结果:
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10
11 12 13 14
15 16 17 18
19 20 21 22
11 12 13 14
15 16 17 18
19 20 21 22
四、函数指针和函数引用
函数指针是C++最大的优点之一。和使用普通指针相比,高级程序员只要有可能都更愿意使用引用,因为引用更容易处理一些。然而,当处理函数时,函数引用对比函数指针就未必有这个优势了。现有的代码很少使用函数引用。下面将向介绍如何函数指针、如何使用函数引用以及分别在什么情况下使用它们。
① 函数指针的例子
#include <iostream>
void print(int i)
{
std::cout << i << std::endl;
}
void multiply(int& nDest, int nBy)
{
nDest *= nBy;
}
void print_something()
{
std::cout << "something" << std::endl;
}
int sayHello()
{
std::cout << "Hello, World!" << std::endl;
return 10;
}
int main()
{
void (*pFunction_1)(int);
pFunction_1 = &print;
pFunction_1(1);
// 输出结果为1
void (*pFunction_2)(int&, int) = &multiply;
int i = 1;
pFunction_2(i, 10);
std::cout << "i = " << i << std::endl;
// 输出结果为10
void (*pFunction_3)();
pFunction_3 = &print_something;
pFunction_3();
// 输出结果为something
int (*pFunction_4)();
pFunction_4 = &sayHello;
int a = pFunction_4();
// 输出结果为Hello, World!
std::cout << a << std::endl;
// 输出结果为10
return 0;
}
② 函数引用的例子
#include <iostream>
void print(int i)
{
std::cout << i << std::endl;
}
void print2(int i)
{
std::cout << i << std::endl;
}
void multiply(int& nDest, int nBy)
{
nDest *= nBy;
}
void print_something()
{
std::cout << "something" << std::endl;
}
int sayHello()
{
std::cout << "Hello, World!" << std::endl;
return 10;
}
int main()
{
// void (&rFunction_1)(int);
// 错误:未初始化引用!引用必须初始化
void (&rFunction_2)(int) = print;
rFunction_2(1);
// 输出1
rFunction_2 = print2;
rFunction_2(2);
// 输出2
void (&rFunction_3)(int&, int) = multiply;
int i = 1;
rFunction_3(i, 10);
std::cout << i << std::endl;
// 输出10
void (&rFunction_4)() = print_something;
rFunction_4();
// 输出something
int (&rFunction_5)();
rFunction_5 = sayHello;
int a = rFunction_5(); // 输出Hello, World!
std::cout << a << std::endl;
// 输出10
return 0;
}
③ 函数指针和函数引用作为函数参数
#include <iostream>
void print(int i)
{
std::cout << i << std::endl;
}
void print2(int i)
{
std::cout << i * 2 << std::endl;
}
void printSomething()
{
std::cout << "Something" << std::endl;
}
void sayHello()
{
std::cout << "Hello, World!" << std::endl;
}
void call_p_func(void (*func)(int))
{
func(1);
func(2);
func(3);
}
void call_r_func(void (&func)(int))
{
func(1);
func(2);
func(3);
}
void call_p_function(void (*func)())
{
func();
}
int main()
{
std::cout << "函数指针作为参数" << std::endl;
call_p_func(&print);
call_p_func(&print2);
call_p_function(&printSomething);
call_p_function(&sayHello);
call_p_function(sayHello);
// 上面两句对于某些编译器来说是一样的,但是推荐使用前者的写法,
// 这样可以是程序的可读性更好一些
std::cout << "函数引用作为参数" << std::endl;
call_r_func(print);
call_r_func(print2);
return 0;
}
总结:
函数指针的声明使用方式:
<想要指向的函数之返回类型>(*函数指针的名称)<想要指向的函数之参数类型…>
如要想声明一个函数指针指向以下函数:
void print(int i)
{
std::cout << i << std::endl;
}
那么就可以如下操作:
void (*pFunction)(int);
然后如下用函数的地址给pFunction赋值:
pFunction = &print;
在然后,pFunction就可以和函数print一样使用了,比如,
pFunction(1);
等等。
函数引用的声明和使用方式:
<欲引用的函数之返回类型>(&函数引用的名称)<欲引用的函数之参数类型…>=<欲引用的函数的名称>,至所以如此,是引用在声明的时候必须初始化,引用不能指向空值。
如要想声明一个函数引用指向以下函数:
void print(int i)
{
std::cout << i << std::endl;
}
那么就可以如下操作:
void (&rFunction)(int)=print;
在然后,rFunction就可以和函数print一样使用了,比如,
rFunction(1);
等等。
五、const修饰指针和引用
大致而言,const修饰指针和引用分三种情况,即const修饰指针、const修饰引用和const修饰指针的引用。下面分别讨论之。
① const修饰指针
const修饰指针又分为三种情况,即const修饰指针本身、const修饰指针所指的变量(或对象)以及const修饰指针本身和指针所指的变量(或对象)。
a. const修饰指针本身
在这种情况下,指针本身是常量,不能改变,任何修改指针本身的行为都是非法的,例如:
double pi = 3.1416;
double* const PI = π
double alpha = 3.14;
PI = α // 错误。因为指针PI是常量,不能再被改变。
*PI = alpha; // OK。虽然指针PI不能被改变,但指针所指的变量或者对象可变。
b. const修饰指针指向的变量(或对象)
在这种情况下,指针本身可以改变,但const所修饰的指针所指向的对象不能被改变,例如:
double pi = 3.1416;
const double* PI = π
double alpha = 3.14;
*PI = alpha;// 错误。因为PI所指向的内容是常量,因此*PI不能被改变。
PI = α// OK。虽然指针所指的内容不能被改变,但指针PI本身可改变。从而通过这种方式改变*PI。
c. const修饰指针本身和指针所指向的变量(或对象)
在这种情况下,指针本身和指针指向的变量(或对象)均不能被改变,例如:
double pi = 3.146;
const double* const PI = π
//double const* const PI = π
cout << "PI = " << PI << endl;
cout << "*PI = " << *PI << endl;
double alpha = 3.14;
//*PI = alpha; // 错误。因为PI所指向的内容是常量,因此*PI不能被改变。
//PI = α // 错误。因为指针PI是常量,不能再被改变。
② const修饰引用
const修饰引用没有指针修饰指针那么复杂,只有一种形式。引用本身不能被改变,但所指向的对象是可以被改变的,见上面“一、基本知识”。
double pi = 3.1416;
//const double& PI = pi;
double const& PI = pi; //和上面一句是等价的
//double& const PI = pi;//有问题。很多编译器会产生warning
cout << PI << endl;
③ const修饰指针引用
我们用例子来说明。
double pi = 3.14;
const double* pPI = π
//const double*& rPI = π //错误。不能将double* 转换成const double *&
const double*& rPI = pPI; //OK。声明指针引用的正确方法
说明:const double*& rPI = π 为什么会出现错误呢?我们知道,引用是被引用对象的别名,正因为如此,由于rPI是pPI的别名,因此rPI和pPI的类型必须完全一致。从上面的代码段我们可以看到,rPI的类型是const double*,而&pi的类型是double*,因此这句程序是错误的。
下面这段代码和 ① 中的b中的情形对应(即内容不可变,指针可变):
double pi = 3.1416;
double api = 3.14;
const double* pPI = π
const double* pAPI = &api;
const double*& rPI = pPI;
const double*& rAPI = pPI;
*rAPI = api; // 错误。指针所指向的值不能被直接改变
rAPI = pAPI; // OK。指针本身可以被改变
指针引用的用法还有其它的情形,由于罕用,故此不谈及。