重温C++的指针和引用


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++的函数参数传递中还是优先使用引用而非指针。毕竟指针会直接操作内存,使用不当就会踩内容;而引用实现为对象的一个别名,不会直接操作内存,比较安全。


添加新评论

选择表情 captcha

友情提醒:不填或错填验证码会引起页面刷新,导致已填的评论内容丢失。

|