C++复合类型总结(引用)

引用(reference)是其中C++语言复合类型之一。

C++11中新增了一种引用:所谓的“右值引用(rvalue reference)”,之后再详细介绍。这种引用主要用于内置类。严格来说,我们使用术语“引用(reference)”,指的是“左值引用(lvalue reference)”

引用(reference)实际上就是给对象起了个外号,操作一个变量的引用也就相当于操作变量本身,这一点跟指针很类似,只是相比指针更直观。


一、基本操作

1.引用必须要被初始化

定义引用时,程序把引用和它的初始值绑定(binding)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起(换言之,无法令引用重新绑定到另一个对象)。

int x = 10, y = 9;
int &xx = x;
int &refVal2;      //报错:引用必须被初始化
int &refVal3 = 10; //报错,引用类型的初始值必须是个对象
xx = y; //无法令引用重新绑定到另一个对象
xx++;
std::cout << "x Address:" << &x << std::endl;      //x Address:0x72fe2c
std::cout << "y Address:" << &y << std::endl;      //y Address:0x72fe28
std::cout << "xx Address:" << &xx << std::endl;    //xx Address:0x72fe2c
std::cout << "x:" << x << std::endl;               //x:11
std::cout << "xx:" << xx << std::endl;             //xx:11

2.引用本身并不是对象。

不能建立引用的引用和指向引用的指针,但可以建立指针的引用。

int x = 10;
int *p = &x;
int *&refp = p;

(*refp)++;

cout << *p << endl;      //11
cout << *refp << endl;   //11
cout << x << endl;       //11

3.建立数组的引用。

很多资料都强调,不能建立数组的引用。因为数组是一个由若干个元素所组成的集合,所以无法建立一个数组的别名。其实是无法直接像引用基本类型那样,声明数组的引用,稍加处理就可以建立数组的引用了。

int (&xx)[3]的解读:首先,xx是一个引用;其次,xx是一个存放3个元素的数组引用;最后,xx是一个存放3个整数类型的数组引用。

int x[] = {1,2,3};
int (&xx)[sizeof(x)/sizeof(x[0])] = x;   //A reference to current array x
xx[1]++;
std::cout << "xx[1]:" << xx[1] << std::endl;      //xx[1]:3
std::cout << "x Address:" << &x << std::endl;     //x Address:0x72fe20
std::cout << "xx Address:" << &xx << std::endl;   //x Address:0x72fe20

下面介绍一种标准点的写法(或者更容易理解的写法)。在此之前,先来了解类型别名(type alias),它是某种类型的同义词,便于将复杂的类型名字变得简单明了,易于理解和使用。

传统的方法是使用关键字typedef,例如

typedef int Jack;  // 帮int取名叫Jack
Jack age = 10;     // 等同于int age = 10;

现在,展示容易理解的写法,定义arrayref代表的类型是存放[sizeof(x)/sizeof(x[0])]个整数类型的数组类型。

int x[] = {1,2,3};
typedef int arrayref[sizeof(x)/sizeof(x[0])];
arrayref &xx = x;   //A reference to current array x
xx[1]++;
std::cout << "xx[1]:" << xx[1] << std::endl;      //xx[1]:3
std::cout << "x Address:" << &x << std::endl;     //x Address:0x72fe20
std::cout << "xx Address:" << &xx << std::endl;   //x Address:0x72fe20

二、函数

A.作为函数参数

基本用法:可以直接对实参进行操作,比如对两个数进行交换。

void swap(int &a,int &b){
    int temp;
    temp = a;
    a = b;
    b = temp;
}

传引用参数的好处:
1.安全,引用对象(指向的实参)不会在函数内部改变。指针参数和引用参数都在栈中开辟了内存空间存放的是由主调函数放进来的实参变量的地址。但是,被调函数内部指针存放的内容可以被改变,即可能改变指向的实参,所以并不安全,而引用则不同,它引用的对象的地址一旦赋予,则不能改变。

void compare(int *p, int &ref){
    cout << "In compare function:" << endl;
    cout << "p Address:" << p << endl;
    cout << "ref Address:" << &ref << endl;
    int y = 11;
    p = &y;
    ref = y;
    cout << "p Address:" << p << endl;
    cout << "ref Address:" << &ref << endl;
}
int main() {
    int x = 10;
    int *p = &x;
    int &xx = x;
    cout << "p Address:" << p << endl;
    cout << "xx Address:" << &xx << endl;
    compare(p,xx);
    return 0;
}

输出结果为:

p Address:0x79fe3c
xx Address:0x79fe3c
In compare function:
p Address:0x79fe3c
ref Address:0x79fe3c
p Address:0x79fdfc
ref Address:0x79fe3c
  1. 拷贝大的类型对象或者容器对象比较低效,这时推荐使用引用形参。比如string类。
bool compare(string &s1, string &s2){
    return s1.size() == s2.size();
}

3.有的类型(包括IO类型在内)根本不支持拷贝操作。这时,函数只能通过引用形参访问该类型的对象。

B.作为函数返回值

当函数返回引用类型的时候,没有复制返回值,而是返回对象的引用(即对象本身)。实际上, 就是返回一个变量的内存地址,既然是内存地址的话,那么肯定可以读写该地址所对应的内存区域的值,即就是“左值”,可以出现在赋值语句的左边。

typedef double arr[5];
double& setValues(arr &ref,int i )
{
    return ref[i];   // 返回第 i 个元素的引用
}
int main() {
    using namespace std;
    double vals[5] = {10.1, 12.6, 33.1, 24.1, 50.0};
    arr &refarr = vals;
    cout << "Before:" << endl;
    for ( int i = 0; i < 5; i++ ) cout << vals[i] << " ";
    cout << endl;

    setValues(refarr,1) = 20.23; // 改变第 2 个元素
    cout << "After:" << endl;
    for ( int i = 0; i < 5; i++ ) cout << vals[i] << " ";
    cout << endl;
    
    return 0;
}

一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。举个例子,我们定义一个名为find_char的函数,它返回在string对象中某个指定字符第一次出现的位置。同时,我们也希望函数能返回该字符出现的总次数。

该如何实现呢?一种方法是定义一个新的数据类型,让它包含两个值。另一种简单的方法,我们可以给函数传入一个额外的引用实参,令其保存字符出现的次数:

string::size_type find_char(const string &s, char c, string::size_type &occurs){
    auto ret = s.size();
    occurs = 0;
    for(decltype(ret) i = 0; i != s.size(); ++i){
        if(s[i] == c){
            if(ret == s.size()) ret = i;
            ++occurs;
        }
    }
    return ret;  //出现次数通过occurs隐式地返回
}

三、const的引用

A.初始化和对const的引用

可以把引用绑定到const对象上,我们称之为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。

int i = 42;
const int &r1 = i;       //允许将const int&绑定到一个普通int对象上
const int &r2 = 42;      //正确:r1是一个常量引用
const int &r3 = r1 * 2;  //正确:r3是一个常量引用
int &r4 = r1 * 2;        //错误:r4是一个非常量引用

上面,同样的初始化对于非const引用是不合法的,将导致编译错误。原因有些微妙,需要适当做些解释。

引用在内部存放的是一个对象的地址,它是该对象的别名。对于不可寻址的值,如文字常量,以及不同类型的对象,编译器为了实现引用,必须生成一个临时对象,将该对象的值置入临时对象中,引用实际上指向该对象(对该引用的操作就是对该临时对象的操作),但用户不能访问它。

double dval = 3.14;
const int &ri = dval;

此处ri引用了一个int型的数。对ri的操作应该是整数运算,但dval却是一个双精度浮点数。因此为了确保ri绑定一个整数,编译器把上述代码编程如下形式:

const int temp = dval;
const int &ri = temp;

再放一些例子

double dval = 3.14159;
//下3行仅对const引用才是合法的
const int &ir = 1024;
const int &ir2 = dval;
const double &dr = dval + 1.0;

上面的代码,转化成

double dval = 3.14159;

//不可寻址,文字常量
int tmp1 = 1024;
const int &ir = tmp1;

//不同类型
int tmp2 = dval;//double -> int
const int &ir2 = tmp2;

//另一种情况,不可寻址
double tmp3 = dval + 1.0;
const double &dr = tmp3;

B.函数和const的引用

1.函数形参
如果函数无须改变引用形参的值,最好将其声明为常量引用。

bool isShorter(const string &s1,const string &s2){
    return s1.size() < s2.size();
}
  1. 函数返回值
    由于返回值直接指向了一个生命期尚未结束的变量,因此,对于函数返回值(或者称为函数结果)本身的任何操作,都在实际上,是对那个变量的操作,这就是引入const类型的返回的意义。当使用了const关键字后,即意味着函数的返回值不能立即得到修改!

如下代码,将无法编译通过,这就是因为返回值立即进行了++操作(相当于对变量z进行了++操作),而这对于该函数而言,是不允许的。如果去掉const,再行编译,则可以获得通过,并且打印形成z:7的结果。

const int& abc(int a, int b, int c, int& result){
    result = a + b + c;
    return result;
}
int main() {
    int a = 1; int b = 2; int c=3;
    int z;
    abc(a, b, c, z)++;  //wrong: returning a const reference
    cout << "z: " << z << endl;
    return 0;
}

参考文献

1.《C++ Primer(第5版)》 Stanley B.Lippman, Josee Lajoie, Barbara E.Moo

  1. 【C++专题】C++引用与const引用
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容

  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,519评论 1 51
  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,798评论 0 38
  • 妈妈去杭州的服装厂里打工了,爸爸的那个工厂还了无音讯。但我看八成是没戏了。 家里相当于破产之后,妈妈才告诉我曾经家...
    小乐同学要好好学习阅读 552评论 0 0
  • 有时候纠结,卡在一个地方很久,和自己较劲是因为不想承认自己不够好。早一点承认,早一点move on. 早一点开始学...
    玩儿_温暖阅读 66评论 0 0