C++和C一个不同的地方就是引入了引用(reference)这个概念,而且引用很容易和指针混淆。
1. 指针
C++中保留了指针,但其使用与C中是完全一样的,没有新增特性。所以这里不再赘述,只说一些使用时的注意点。更详细的内容可以看我的另外一片博客《解读C指针》。
(1)每个指针都有一个与之关联的数据类型,该数据类型决定了指针所指向的对象的类型。
(2)一个有效的指针必然是下面三种状态之一:
- 保存一个特定对象的地址;
- 指向某个对象后面的另一对象;
- 0(即NULL)值。
(3)对指针赋值或初始化只能使用以下四中类型的值:
- 0值常量表达式,例如在编译时可获得0值的整形const对象或字面值常量0;
- 类型匹配的对象的地址;
- 另一对象之后的下一地址;
- 同类型的另一个有效指针。
(4)void*指针是一种特殊的指针类型,它可以保存任何类型对象的地址,但void*只表明该指针与一地址值相关,但并不清楚存储在此地址上的对象的类型。另外,void*指针只支持几种有限的操作:
- 与另一个指针进行比较;
- 向函数传递void*指针或从函数返回void*指针;
- 给另一个void*指针赋值。
- 不允许使用void*指针操作它所指向的对象。
(5)指向const对象的指针(指针指向的对象是const的,但指针本身不是const的)的一些特性:
- 指向const对象的指针也必须具有const特性,即不能将一个const对象的地址赋给一个非const对象的指针;
- 允许把非const对象的地址赋给指向const对象的指针;
- 不能使用void*指针保存const对象的地址,而必须用const void *类型的之后怎保存const对象的地址。
也就是说,指向const对象的指针既可以指向const对象的地址,也可以指向非const对象的地址,但指向非const对象的指针只能指向非const对象的地址。
2. 引用
引用就是对象的另一个名字,通过在变量名前添加“&”符号来定义。和指针一样,引用也只能绑定到和引用定义类型相同的对象上面。但如果对象之间支持隐式转换,也可以关联。比如可以将一个int类型的引用绑定到一个浮点数变量上面,但此时该引用绑定的对象已经被隐式转换了,比如:
float fval = 100.5; int &refval = fval;
此时,refval的值为100,而不是100.5。
const引用(指向const对象的引用):非const引用只能绑定到与该引用类型的对象;但const引用则可以绑定到不同但相关的类型的对象或绑定到右值。这一点和const指针类似。
const int ival4 = 8192; const double fval4 = 8192.1; //int &refval3 = ival4; //错误:refval3是非const引用,不能绑定到const对象ival4 const int &refval4 = ival4; const int &refval5 = fval4; // int类型的const引用可以绑定到相关类型(可隐式转换)的变量,但是值会被转换为引用类型的值,比如这里会将fva4的值由8192.1转换为int型8192,然后再绑定 //int &refval6 = 10; // 错误,非const引用不能绑定到右值 const int &refval7 = 10; //const引用可以绑定到右值
3. 引用和指针的区别
(1)定义与访问对象的方式不一样
指针通过在变量名前加“*”定义,而引用通过在变量名前加"&"来定义;访问关联的对象时,指针需要解引用(加*)才可以使用,而引用直接使用变量名访问。
int ival = 1024; int *pval = &ival; // 定义一个指针,并使它指向ival int &refval = ival; // 定义一个引用,并把它绑定到ival上 cout << "ival = " << ival << endl; // 指针访问对象时需要家*解引用才可以访问 cout << "*pval = " << *pval << endl; // 引用访问它绑定的变量时直接访问,因为它只是对象的另一个名字 cout << "refval = " << refval << endl;
(2)指针定义的时候可以不用初始化(虽然这不是一个好习惯),但引用必须在定义时就初始化。
(3)可以定义指针的指针,但是不能定义引用的引用。
4. 函数参数传递
指针的应用范围很广,但是引用在实际使用中往往大多用于函数参数传递的场合。所以我们这里专门来介绍以下函数参数传递,其中也会体现出指针与引用的联系与区别。
关于函数参数传递,有一条金科玉律:形参的初始化与变量的初始化一样:如果形参具有非引用类型,则复制实参的值;如果形参为引用类型,则它只是实参的别名。
4.1 非引用形参
非引用形参表示对应实参的局部副本。对这类形参的修改仅仅改变了局部副本的值。一旦函数执行结束,这些局部变量的值也就没有了。
4.1.1 指针形参
函数的形参是指针,此时将复制实参指针。与其他非引用类型的形参一样,该类形参的任何改变也仅作用于局部副本。但因为指针传递的是地址,所以我们依旧可以通过这个地址来改变该地址保存的值。这点是指针与普通非引用形参的区别,也是与引用形参的一个相似点(只是效果相同,机制并不同)。但是对于指针形参,我们需要特别注意一个问题:指针形参是指向const类型还是非const类型,因为这将影响函数调用所使用的实参。看下面两个函数:
void func1(int *p) { } void func2(const int *p) { }
我们既可以用int*也可以用const int*类型的实参调用func2函数;但仅能用int*类型的实参调用func1函数。这个差别来源于前面介绍的指针的初始化规则:可以将指向const对象的指针初始化为指向非const对象,但不可以让指向非const对象的指针指向const对象。这样也是为了防止改变const对象的值而引起错误。
4.1.2 const形参
在调用函数时,如果该函数使用非引用的非const形参,则既可以给函数传递const实参,也可以传递非const的实参。这种行为源于之前介绍的const对象的标准初始化规则。因为初始化复制了初始化式的值,所以可以用const对象初始化非const对象;反过来,也可以用非const对象初始化const对象。而且,即使我们将函数形参指定为const,编译器还是会将其声明为普通类型,所以,下面两个定义是相同的:
void func(const int i) {} void func(int i) {}
这样是为了兼容C,因为在C中,具有const形参或非const形参的函数并无区别。
4.1.3 复制实参的局限性
复制实参并不是在所有情况下都适合的,不适宜复制实参的情况包括:
- 当需要在函数中修改实参的值时。
- 当需要以大型对象作为实参传递时。对实际应用而言,复制对象所付出的时间和存储空间代价往往过大。
- 当没有办法实现对象的复制时。
针对上述几种情况,有效的解决方法是将形参定义为引用或指针类型。
4.2 引用形参
与所有引用一样,引用形参直接关联到其所绑定的对象,而并非这些对象的副本。
4.2.1 利用引用形参修改实参的值
这里举一个简单的例子:
void swap_error(int v1, int v2) { int temp = v2; v2 = v1; v1 = temp; } void swap(int &v1, int &v2) { int temp = v2; v2 = v1; v1 = temp; }
第一个swap_error函数显然是实现不了交换的功能的,这里不再啰嗦。而第二个函数swap是可以的,是因为我们的参数都是引用类型的,所以v1、v2并不是实参的拷贝,而只是实参的另一个名字,对v1和v2的任何修改就是对实参的修改。这是引用形参最基本的用法——利用引用形参修改实参的值。除了这个,我们再介绍几个其他用法。
4.2.2 使用引用形参返回额外信息
函数只能返回单个值,但有些时候,函数有不止一个的内容需要返回。例如,定义一个find_val函数,在一个整形vector对象的元素里面搜索某个特定值。如果找到满足要求的元素,则返回指向该元素的迭代器;否则返回一个迭代器,指向该vector对象的end操作返回的元素。此外,如果该值出现了不止一次,我们还希望函数可以返回其出现的次数。这时我们可以实现如下:
vector<int>::const_iterator find_val ( vector<int>::const_iterator beg, vector<int>::const_iterator end, int value, vector<int>::size_type &occurs ) { vector<int>::const_iterator res_iter = end; occurs = 0; for(; beg != end; ++beg) if( *beg == value ) { if (res_iter == end) res_iter = beg; ++occurs; } return res_iter; }
4.2.3 利用const引用避免复制
这个很容易理解,直接看一段代码:
bool is_shorter(const string &s1, const string &s2) { return s1.size() < s2.size(); }
这里我们想比较两个string对象的长度,显然我们只需访问每个string对象的size,而不必修改这些对象。如果此时直接传非引用、非指针形参,则会导致复制操作。
NB:如果使用引用形参的唯一目的是避免复制实参,则应该将形参定义为const引用。
4.2.4 更灵活的指向const的引用
如果函数具有普通的非const引用形参,则不能通过const对象进行调用。因为,此时函数可以修改传递进来的对象,这样就违背了实参const特性。但是定义为const引用的形参,却可以通过非const对象调用。这个之前也已经介绍过。看下面一个例子:
string::size_type find_char(string &s, char c) { string::size_type i = 0; while (i != s.size() && s[i] != c) ++i; return i; } // 第一处调用 ... if (find_char("Hello world", 'o')) ... // 第二处调用 bool is_sentence(const string &s) { return (find_char(s, '.') == s.size() - 1); }
我们定义了一个在string对象中查找特定字符的函数find_char,但由于它的引用形参是非const引用,所以是不能用字面值或const引用或可以产生右值的表达式调用的,所以后面的两处调用都是错误的。
所以,应该将不需要修改的引用形参定义为const引用,普通的非const引用形参在使用时太不灵活——这样的形参既不能用const对象初始化,也不能用字面值或产生右值的表达式实参初始化。
4.2.5 传递指向指针的引用
之前我们写了交换两个整数的swap函数,现在我们编写一个实现两个指针交换的函数。我们知道用*定义指针,用&定义引用。现在问题在于如何将这两个操作符结合起来获取指向指针的引用。这里给出一个例子:
void ptrswap(int *&p1, int *&p2) { int *temp = p2; p2 = p1; p1 = temp; }
形参int *&p1 的定义应从右至左理解:p1是一个引用,与指向int型对象的指针相关联。也就是说,p1只是传递进ptrswap函数的任意指针的别名。比如说,交换前p1指向i,p2指向j,则交换后p1指向j,p2指向i。
至此,函数参数传递就介绍完了。最后再总结以下比较容易混淆的点:我们会发现不论是const指针还是const引用都比非const指针和非const引用有“较强的能力”——可以将指向const对象的指针初始化为指向非const对象,但不可以让指向非const对象的指针指向const对象;如果函数具有普通的非const引用形参,则不能通过const对象进行调用,但是定义为const引用的形参,却可以通过非const对象调用。所以,除我们要使用形参来修改实参的值的场景外,在函数参数传递过程中,应该定义为const指针或const引用。
个人观点:虽然大多数场景,指针和引用可是实现相同的功能,但是在C++的函数参数传递中还是优先使用引用而非指针。毕竟指针会直接操作内存,使用不当就会踩内容;而引用实现为对象的一个别名,不会直接操作内存,比较安全。
评论已关闭