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

简介: 引用(reference)是其中C++语言复合类型之一。C++11中新增了一种引用:所谓的“右值引用(rvalue reference)”,之后再详细介绍。

引用(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引用
目录
相关文章
|
5月前
|
存储 程序员 C语言
c++primer plus 6 读书笔记 第四章 复合类型
c++primer plus 6 读书笔记 第四章 复合类型
|
1月前
|
存储 编译器 程序员
C++类型参数化
【10月更文挑战第1天】在 C++ 中,模板是实现类型参数化的主要工具,用于编写能处理多种数据类型的代码。模板分为函数模板和类模板。函数模板以 `template` 关键字定义,允许使用任意类型参数 `T`,并在调用时自动推导具体类型。类模板则定义泛型类,如动态数组,可在实例化时指定具体类型。模板还支持特化,为特定类型提供定制实现。模板在编译时实例化,需放置在头文件中以确保编译器可见。
27 11
|
2月前
|
安全 程序员 C语言
C++(四)类型强转
本文详细介绍了C++中的四种类型强制转换:`static_cast`、`reinterpret_cast`、`const_cast`和`dynamic_cast`。每种转换都有其特定用途和适用场景,如`static_cast`用于相关类型间的显式转换,`reinterpret_cast`用于低层内存布局操作,`const_cast`用于添加或移除`const`限定符,而`dynamic_cast`则用于运行时的类型检查和转换。通过具体示例展示了如何正确使用这四种转换操作符,帮助开发者更好地理解和掌握C++中的类型转换机制。
|
3月前
|
C++
使用 QML 类型系统注册 C++ 类型
使用 QML 类型系统注册 C++ 类型
40 0
|
4月前
|
编译器 C++ 运维
开发与运维函数问题之函数的返回类型如何解决
开发与运维函数问题之函数的返回类型如何解决
35 6
|
3月前
|
存储 C++
【C/C++学习笔记】string 类型的输入操作符和 getline 函数分别如何处理空白字符
【C/C++学习笔记】string 类型的输入操作符和 getline 函数分别如何处理空白字符
38 0
|
4月前
|
安全 编译器 C++
C++一分钟之-模板元编程实例:类型 traits
【7月更文挑战第15天】C++的模板元编程利用编译时计算提升性能,类型traits是其中的关键,用于查询和修改类型信息。文章探讨了如何使用和避免过度复杂化、误用模板特化及依赖特定编译器的问题。示例展示了`is_same`类型trait的实现,用于检查类型相等。通过`add_pointer`和`remove_reference`等traits,可以构建更复杂的类型转换逻辑。类型traits增强了代码效率和安全性,是深入C++编程的必备工具。
64 11
|
4月前
|
C++
C++一分钟之-类型别名与using声明
【7月更文挑战第20天】在C++中,类型别名和`using`声明提升代码清晰度与管理。类型别名简化复杂类型,如`using ComplexType = std::vector&lt;std::shared_ptr&lt;int&gt;&gt;;`,需注意命名清晰与适度使用。`using`声明引入命名空间成员,避免`using namespace std;`全局污染,宜局部与具体引入,如`using math::pi;`。恰当应用增强代码质量,规避常见陷阱。
63 5
|
3月前
|
设计模式 安全 IDE
C++从静态类型到单例模式
C++从静态类型到单例模式
33 0
|
4月前
|
C++ 开发者
C++一分钟之-概念(concepts):C++20的类型约束
【7月更文挑战第4天】C++20引入了Concepts,提升模板编程的类型约束和可读性。概念定义了模板参数需遵循的规则。常见问题包括过度约束、约束不完整和重载决议复杂性。避免问题的关键在于适度约束、全面覆盖约束条件和理解重载决议。示例展示了如何用Concepts限制模板函数接受的类型。概念将增强模板的安全性和灵活性,但需谨慎使用以防止错误。随着C++的发展,Concepts将成为必备工具。
87 2