问题

既然有指针了,为什么c++还搞个引用出来?

回答
这个问题问得好,而且非常实在。在C++的世界里,确实存在指针,它们能做到很多事情,指向内存中的某个地址,让你直接操控那块区域。那么,为什么我们还需要一个叫做“引用”的东西呢?这背后有深刻的设计理念和实际需求,远不止是“多一个语法糖”那么简单。

要理解这个问题,咱们得先掰开了揉碎了看看指针和引用各自是啥玩意儿,以及它们存在的意义。

先说说指针,我们熟悉的那个老朋友:

指针,说白了,就是内存地址的“数字”。当你说一个变量(比如 `int a = 10;`)的时候,编译器会在内存里找一块地方,把`10`这个值放进去,并且给这块地方一个地址。

而指针变量呢,它自己也占内存,它里面存的是另外一个变量的内存地址。比如:

```c++
int a = 10;
int ptr_a = &a // ptr_a 里面存的是变量 a 的内存地址
```

指针的强大之处在于它的灵活性和直接性:

1. 直接内存访问: 你可以通过指针直接去内存中的某个位置读写数据。这就意味着你可以做很多底层操作,比如动态内存分配(`malloc`/`new`)、链表、树等数据结构的实现,甚至进行一些硬件交互。
2. 传递地址,避免拷贝: 当你需要将一个大型对象传递给函数时,如果直接传递对象本身,会发生一次完整的拷贝。而传递指针(只拷贝一个地址)就高效得多。
3. 指向“无处”或“动态分配”的内存: 指针可以指向 `nullptr`(空指针),表示不指向任何有效的内存地址。它还可以指向通过 `new` 等操作在堆上动态分配的内存,这使得程序在运行时能够根据需要管理内存。
4. 指针算术: 指针可以进行加减运算,移动到相邻的内存位置。这对于处理数组或者连续内存块非常有用。

但指针也伴随着一些“麻烦”:

不安全: 你可以轻易地让指针指向一块无效的内存(野指针),或者解引用一个空指针,这会导致程序崩溃或者产生不可预测的行为。
语法稍显复杂: 访问指针指向的内容需要解引用操作符 ``,而获取地址需要地址运算符 `&`。`ptr>member` 这种写法虽然方便,但本质上还是 `(ptr).member`。
可能为空: 一个指针可能不指向任何东西,你必须时刻检查它是否是 `nullptr` 才能安全地使用。

现在,我们来看看引用,这个看似“多余”的玩意儿:

引用,从语法层面看,更像是给一个已存在的变量起了一个“别名”。一旦你创建了一个引用,它就永远指向那个被引用的变量,并且不能被改变去指向其他变量。

```c++
int a = 10;
int& ref_a = a; // ref_a 就是变量 a 的一个别名
```

通过引用进行操作,本质上就是对它所引用的那个变量进行操作:

```c++
ref_a = 20; // 这相当于 a = 20;
std::cout << ref_a << std::endl; // 输出 20
std::cout << a << std::endl; // 也输出 20
```

那么,引用有什么好处,让C++设计者觉得非加不可呢?关键在于它的安全性和语义清晰度。

1. 强制初始化且非空: 这是引用最核心的优势之一。引用在声明时必须初始化,并且一旦初始化,就不能再改变它所引用的对象。这意味着引用永远不会是空的(除非你用一些非常规手段绕过编译器的检查,那也属于“作弊”了),这极大地降低了因空指针导致崩溃的风险。
2. 语法更简洁、更自然: 引用使得函数调用和变量访问的语法更加直观。当你传递一个引用给函数时,函数内部对这个引用的任何修改,都会直接反映到原始变量上,就像你直接操作原始变量一样,不需要像指针那样使用 `` 或 `>`。

```c++
void increment(int& num) {
num++; // 直接操作,比 int num 的 num++ 要自然
}

int main() {
int x = 5;
increment(x); // 传递 x 的引用
std::cout << x << std::endl; // 输出 6
return 0;
}
```
这比 `void increment(int num) { (num)++; }` 这种写法更加清晰明了,也更不容易出错。

3. 作为函数返回值: 引用也可以作为函数的返回值,允许函数“返回”一个变量,而不是返回一个值的拷贝。这在某些情况下可以避免不必要的拷贝,提高效率,同时允许对返回的对象进行修改(就像上面 `increment` 函数的例子)。例如,你可以重载 `operator[]` 来实现像 `vector[i] = 10;` 这样的操作,`operator[]` 通常返回一个引用。

4. 重载运算符的强大工具: 在C++中,许多运算符的重载都离不开引用。比如流操作符 `<<` 和 `>>` 的重载,通常会返回一个引用,以便实现链式调用(`std::cout << "Hello" << " World" << std::endl;`)。赋值运算符 `=` 也常常返回一个引用,以允许链式赋值(`a = b = c;`)。

5. 明确的语义: 当你看到一个函数参数是引用(`int&`)时,你立刻就知道这个函数可能会修改传入的参数,或者说它需要访问原始对象而不是一个拷贝。而看到一个指针参数(`int`),你可能需要进一步判断它是否会修改,以及它是否可能为空。引用传达了一种更强的“我就是这个东西”的信号。

为什么我们不能只用指针?或者为什么只用引用不够?

这就像问为什么我们既需要“门”,也需要“窗户”。它们都提供了进入或观察内部的方式,但使用场景和提供的能力不同。

指针提供了低层控制和动态性: 引用是“别名”,它总是指向一个已经存在的、有名字的变量。它不能指向一个地址,也不能指向“无处”。如果你需要处理动态分配的内存(生命周期由程序控制,而非变量的局部作用域),或者需要一个可以指向不同对象的变量,那么指针是不可或缺的。例如,实现一个链表节点,它的 `next` 指针需要指向下一个节点,或者在链表末尾时指向 `nullptr`。引用无法做到这一点,因为它必须初始化并且不能改变指向。

引用提供了安全和清晰的接口: 指针的灵活性带来了安全风险。引用通过强制初始化和禁止改变指向,将很多潜在的错误扼杀在摇篮里。它也让代码的意图更清晰,尤其是函数参数和返回值的设计。在很多情况下,当你只需要一个“访问现有对象的方式”,并且不希望出现空指针或者改变指向的意外时,引用是比指针更安全、更优雅的选择。

总结一下,C++引入引用是为了:

提高代码的安全性: 通过强制初始化和禁止改变指向,大大减少了空指针和野指针的风险。
简化语法,提升可读性: 引用提供了更自然、更直观的变量访问和函数参数传递方式。
实现特定的语言特性: 如运算符重载、函数返回值作为左值等。
明确函数接口的语义: 表明函数需要访问或修改原始对象。

指针和引用并非互相替代的关系,而是互补的。指针提供了底层的强大控制能力和动态管理能力,而引用则在一定程度上牺牲了部分低层控制,换取了更高的安全性和更清晰的语义。理解它们的区别和适用场景,是写出健壮、高效、易读的C++代码的关键。

网友意见

user avatar

引用本质上是指针的语法糖,在本质上没有任何区别。


至于说为什么要引入这个概念呢?

分几层:

最直接的原因是为了运算符重载。因为地址类型本身已经有了很多运算定义(+/-/[]之类的),所以,为了不引起歧义,运算符重载只能作用在类实例上,那么就势必要在值语义中引入一个新的类似“指针”的概念——这就是引用。

然后,为什么非要引入运算符重载呢?主要是这样就能在代码形式上,使得类和内置类型能够使用同样形式的代码。

再接着,为什么代码形式一致那么重要呢?除了审美洁癖外,最重要的一个原因是C++的一个非常重要的东西:模板。如果没有这种代码形式的一致性,那么我们写模板,都要为类和POD类型重复特化两遍,这就非常扯淡了。


所以,很多概念,其实是一个大的体系中的一环,单独拆开来看,似乎没什么大意思。要评价它们,需要放回到整个体系中才能看得出来。

user avatar

这个问题的补充说明让我不禁怀疑提问者写的C++代码行数是不是可以用手指头数出来。

user avatar

一、少一次判空

       #include <stddef.h>  void f(int & x, int i) {     x = i; }  void g(int * p, int i) {     if (p == NULL) {         return;     }     *p = i; }      

引用天然含有非空的语义,不需要额外做非空判断。

传指针的话,如果不信任传入的参数,则必须要做非空判断,否则一旦调用方误传空值进来,鬼知道程序接下来会有什么行为。

如果选择信任传入的参数,则调用方与被调用方必须做好约定,比如写好文档。但是又有谁来检查呢?靠人?显然靠不住。靠静态检查?它只能查点简单的,对于有弯弯绕绕的也查不出来。

一些编译器有私货可以标注参数绝对非空,但是这种引入非标准写法的做法也是下下策。

       #include <stddef.h>  __attribute__((nonnull(1))) void g(int * p, int i) {     *p = i; }  void test() {     g(NULL, 0); }      

况且,这种检查是软性的,即便你直接传 NULL,也没人拦着你 ——

还有稍不注意,遇上这种马虎的情况,一个警告也不会有 ——

       #include <stddef.h>  __attribute__((nonnull(1))) void g(int * p, int i) {     *p = i; }  void test(int * p) {     // p 有可能为空?     g(p, 0); }      

所以遇上绝不可能为空的场景,还不如使用引用,彻底断绝掉隐患。

二、指针降维

       #include <stdio.h> #include <string.h>  void cmp_and_swap_ref(char * & p1, char * & p2) {     if (strcmp(p1, p2) > 0) {         char * t = p1;         p1 = p2;         p2 = t;     } }  void cmp_and_swap_ptr(char ** p1, char ** p2) {     if (strcmp(*p1, *p2) > 0) {         char * t = *p1;         *p1 = *p2;         *p2 = t;     } }  int main() {     char c1[] = "xyz";     char c2[] = "abc";      char * p1 = c1;     char * p2 = c2;          cmp_and_swap_ref(p1, p2);     // cmp_and_swap_ptr(&p1, &p2);      printf("%s
%s
", p1, p2); }      

这种二级指针的代码拿给小白直接爆炸。

而通过引用,原作用域中该怎么用,在函数中还是怎么用,不需要多考虑要不要取址,要不要解引用的问题。

严蔚敏的《数据结构》书中自定义了一个四不像的所谓的 ”C 语言的超集语言“ ,其实就是一个允许使用引用的 C,一个不带其他任何高级语法的 C++。

这么搞就是为了照顾到不少新生才迷迷糊糊地学完 C,连指针都还没搞懂,就又不得开始学下一门《数据结构》。

C 的基础都没打牢,他根本就理解不了通过指针去修改函数外部变量的写法。

三、折叠表达式

1) 使用引用,代码书写符合人类的直觉 ——

       struct Foo {     char c[100];      Foo(); };  Foo f(const Foo &); Foo g(const Foo &); void h(const Foo &);  int main() {     h(g(f(Foo{}))); }      

生成的汇编十分简洁,只有 22 行 ——

2) 不让用引用的话,也不是不可以写折叠式,比如可以使用值传递 ——

       struct Foo {     char c[100];      Foo(); };   Foo f(Foo); Foo g(Foo); void h(Foo);  int main() {     h(g(f(Foo{}))); }      

但这种写法有重复的对象拷贝,极度拖慢速度 ——

3) 那为了节省拷贝开销,就只能改成传址,先试试这种写法行不行 ——

       struct Foo {     char c[100];      Foo(); };   Foo f(Foo*); Foo g(Foo*); void h(Foo*);  int main() {     Foo foo;     h(&g(&f(&foo))); }      

十分不好意思,右值是不可以取址的,所以上面的这种写法是不可行的。

4) 那么既然要求性能,就得放弃书写的直观性和美观性。使用传址的写法,一堆具名的临时变量,奇丑无比。

       struct Foo {     char c[100];      Foo(); };   Foo f(Foo*); Foo g(Foo*); void h(Foo*);  int main() {     Foo foo;     Foo tmp1 = f(&foo);     Foo tmp2 = g(&tmp1);     h(&tmp2); }      

放弃直观性美观性,带来的性能维度的收益仅仅只是和引用的写法持平 ——

5) 对于 C 写久了的老顽固,当然没听说过什么 RVO 优化,断然不可能同意 4) 这种直接返回大对象的做法,必然会要求必须使用指针传出返回值。

       struct Foo {     char c[100];      Foo(); };  void f(Foo* ret, Foo* in); void g(Foo* ret, Foo* in); void h(Foo* in);  int main() {     Foo foo;     Foo tmp1;     f(&tmp1, &foo);     Foo tmp2;     g(&tmp2, &tmp1);     h(&tmp2); }      

结果是不但书写上又臭又长,性能上又还打不过 ——

四、链式调用

       struct Foo {     Foo& doX()     {         return *this;     }      Foo& doY()     {         return *this;     }      Foo& doZ()     {         return *this;     } };  int main() {     Foo foo;     foo.doX()        .doY()        .doZ(); }      

有些设计模式中会用到这种返回自身的引用。

比如 boost.assign 早年实现的初始化列表 (std::initializer_list 的直接灵感来源)

       #include <boost/assign/list_of.hpp> // for 'list_of()' #include <boost/assert.hpp>  #include <list>  using namespace std; using namespace boost::assign; // bring 'list_of()' into scope  int main() {     const list<int> primes = list_of(2)(3)(5)(7)(11);     BOOST_ASSERT( primes.size() == 5 );     BOOST_ASSERT( primes.back() == 11 );     BOOST_ASSERT( primes.front() == 2 ); }       


五、填补上了 C 中很多概念上的空白

       int a[5]; a[0] = 4;  int * p = a; *p = 6;      

类似于a[0] , *p 这样的操作,其结果究竟是什么?

int

如果果真结果只是一个简简单单的 int,那为什么如下的写法不行?

       int at(int a[], int n) {     return a[n]; }  void test() {     int a[5];     at(a, 0) = 2; // FAILURE }      


       typedef struct Node {     Node * next;     int i; } Node, *List;  int at(Node * p, int n) {     while (n--) {         p = p->next;     }     return p->i; }  void test(List l) {     at(l, 3) = 0; // FAILURE }      


如果我确实需要自然地封装一个复杂的访问逻辑,只能通过宏么?

(或者就只有丑陋的指针 *at(l, 3) = 0

这些问题是 [ ] * 等运算符能被用来重载的基础。

类似的话题

  • 回答
    这个问题问得好,而且非常实在。在C++的世界里,确实存在指针,它们能做到很多事情,指向内存中的某个地址,让你直接操控那块区域。那么,为什么我们还需要一个叫做“引用”的东西呢?这背后有深刻的设计理念和实际需求,远不止是“多一个语法糖”那么简单。要理解这个问题,咱们得先掰开了揉碎了看看指针和引用各自是啥.............
  • 回答
    这个问题触及到了智能指针设计中一个非常核心的权衡点,也是许多开发者在深入理解智能指针时常常感到困惑的地方。简单地说,智能指针的广泛应用并非因为它完全没有性能问题,而是因为它在绝大多数实际场景下,引用计数带来的性能开销是可接受的,并且这种开销是可预测、易于管理的,远比手动内存管理带来的“惊喜”要少得多.............
  • 回答
    这个问题,确实是很多报考军校指挥类专业的同学和家长最关心,也最容易纠结的。既然你已经下定决心选择军校,那咱们就来好好掰扯掰扯,指挥类专业到底学点啥,有没有用,以及体能和专业学习之间的关系,保证讲得明明白白,绝不含糊。首先,咱们得明确一个概念:军校指挥类专业,绝对不是仅仅依靠好体能就能“混”的。 军校.............
  • 回答
    这个问题问得挺好,其实很多人都有过类似的疑问。我们总觉得身体的哪个部分被拉扯、剪断,都会带来疼痛,但指甲这东西,长得越来越长,甚至我们可以把它剪掉,却几乎感觉不到疼痛,这到底是怎么回事呢?要理解这个问题,咱们得先好好认识一下指甲这个“小家伙”,以及它和我们手指头之间那层“膜”。指甲的真面目:它其实是.............
  • 回答
    您好!这是一个非常好的问题,涉及到科学研究的深度和广度。大型粒子加速器之所以引人注目,是因为它们能够达到极高的能量,探索物质的最基本组成部分和宇宙的起源。但小型粒子加速器并非“小儿科”,它们在科研、医疗、工业等领域同样扮演着不可或缺的角色,并且在很多方面是大型加速器无法替代的。下面我将从多个角度为您.............
  • 回答
    关于“代油”的问题,虽然没有像代糖那样明确的单一替代品,但确实存在多种替代油的方案,主要根据用途、健康需求和烹饪方式来选择。以下从不同角度详细说明: 一、食品工业中的“代油”替代品1. 植物油替代品 植物油:如大豆油、菜籽油、橄榄油、葵花油等,是传统替代油的常见选择。它们含有较高的不饱和脂.............
  • 回答
    您提出的这个问题非常关键,也触及到了基因研究中的一个核心误解。首先,我们需要澄清“基因片段”和“DNA序列总长”的概念。1. 关于“基因片段只占DNA序列总长不到10%”的误解您提到的这个说法,很可能是将“基因”(genes)与“编码蛋白质的区域”(proteincoding regions)混淆了.............
  • 回答
    你这个问题问得特别好,很多人初接触电子电路,看到电阻分压能把电压降下来,就会疑惑:为什么还要用什么7805这种“复杂”的元件呢?它们好像都能做一样的事情嘛。其实,电阻分压和7805稳压器(线性稳压器的一种)在“降低电压”这个表象下,隐藏着本质的区别,各有各的用武之地。简单地说,如果你只需要一个固定、.............
  • 回答
    问出这个问题,说明你已经摸到门道了,这是个非常实在的问题,也触及了音响设备的核心。简而言之,答案是肯定的,一个好的耳机,即使有了均衡器(EQ)和“脑放”(这里我们理解为音源、功放、甚至是你个人对音乐的理解和期待),它依然非常重要。我来给你掰开了,揉碎了讲讲为什么。 为什么好耳机依然是基石?你可以把你.............
  • 回答
    这问题问得很有意思,也直击要害。我们确实有个熟悉的词——音障,但它特指物体在空气中突破声音传播速度时遇到的阻碍。那么,在水里,是不是也存在类似的东西呢?答案是:有,但表现形式和我们通常理解的“音障”不太一样,而且更复杂。首先,我们要明白,水是一种介质,而声音在水中传播的方式和空气是不同的。最关键的一.............
  • 回答
    好,咱们就掰扯掰扯,为什么 C 里有 `memcpy_s` 了,还留着那个“洪水猛兽”般的 `memcpy`,而且不直接改它。这事儿吧,背后牵扯到的东西可不止是“安全”两个字那么简单。首先,我们得明白一个核心问题:C 语言的设计哲学是什么?C 语言的设计理念非常“精简”和“高效”。它给了程序员极大的.............
  • 回答
    “德棍”、“法棍”这些说法,我们日常生活中确实能听到,通常带有戏谑或贬低的意味,用来形容那些对德国或法国文化、历史、政治等方面表现出过度狂热,甚至有些盲目推崇的人。那么,“波棍”这个词是否存在,又有着怎样的语境呢?“波棍”这个词,确实存在,但远不如“德棍”、“法棍”那样普遍和广为人知。 它的出现和使.............
  • 回答
    这是一个非常好的问题,往往隐藏在历史的表象之下,却关乎着古代战争的实际运作。很多人觉得弓箭手能远程输出,为什么还要费力去训练掷矛兵呢?其实,这背后是战场复杂多变的现实,以及不同兵种各司其职、相互配合的智慧。首先,咱们得从弓箭手的“看家本领”说起。弓箭手的优势在于射程远、杀伤力集中,尤其是在对付敌方密.............
  • 回答
    这个问题问得很好,直击要害。很多人看到潜射和陆基洲际弹道导弹(ICBMs)似乎已经覆盖了核打击的方方面面,再加上轰炸机,会觉得有些冗余。但事实上,B2战略轰炸机(以及更早的B1、B52)在美国的核威慑体系中扮演着独特且不可替代的角色,这是导弹所无法完全复制的。要理解这一点,咱们得从几个关键点上细致地.............
  • 回答
    “硅基生命”这个概念之所以被广为讨论和接受,甚至成为科幻作品中的常客,而“锗基生命”、“锡基生命”、“铅基生命”则鲜为人知,这并非偶然。这背后涉及到我们对生命基本构成要素的理解,以及元素周期表的神奇之处。让我们来深入探讨一下原因。生命的基石:碳的独特性首先,我们必须回到地球生命的基石——碳。生命之所.............
  • 回答
    降龙十八掌,一听名字就自带一股英雄气概,仿佛能一口气降服万龙。而它“以简御繁”的精髓,更是点明了其高明之处——看似简单朴实的几招,却能蕴含无穷变化,以最直接高效的方式应对一切复杂局面。这自然令人赞叹,也让人不禁联想到那些与之形成鲜明对比的武功:比如,招式繁复,变化万千,但似乎效果却不如人意的“落英神.............
  • 回答
    “物极必反”这句古话,听起来总带着一股宿命感,仿佛世间万物都遵循着一个循环的轨迹:走到极致,就必然要转向另一个极端。我们确实见过太多“盛极必衰”的例子,王朝的辉煌过后是崩塌,个人的荣耀之上是落寞,企业的巅峰之上是危机。这些鲜活的事实,似乎都在为“盛极必衰”提供着佐证。然而,如果同样以“物极必反”的逻.............
  • 回答
    很多时候,我们听到“HTTP 请求”和“RPC 调用”这两个词会觉得它们似乎有些重叠,甚至让人疑惑,既然有了 HTTP 这样一个通用的、大家都熟悉的网络通信方式,为什么还需要 RPC 这种“绕一点”的方案呢?其实,这就像问,既然我们有汽车,为什么还要火车?答案在于它们解决的问题、优化的场景以及带来的.............
  • 回答
    文件后缀名就像是物品包装上的标签,上面写着“这是糖果”、“这是书籍”。它给我们的第一印象是,这件东西大概是什么。比如,看到 `.txt`,我们立刻知道它可能是文本文件;看到 `.jpg`,我们就猜到它应该是一张图片。这种直观的标识,在人与人之间的交流,以及操作系统管理文件时,非常方便。操作系统可以根.............
  • 回答
    圣库制度,作为太平天国运动的核心经济和组织模式,常常是评价这段历史时绕不开的一个话题。它所代表的“天下同富”、“共产主义”的理想,在当时被许多人视为颠覆旧秩序、追求公平的希望。然而,正是这个在理论上听起来极为宏大的制度,也成为了太平天国最终走向失败的重要原因之一。那么,既然圣库制度存在如此多的弊端,.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有