百科问答小站 logo
百科问答小站 font logo



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

  

user avatar   haozhi-yang-41 网友的相关建议: 
      

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


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

分几层:

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

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

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


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


user avatar   Ivony 网友的相关建议: 
      

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


user avatar   peter-43-43-80 网友的相关建议: 
      

一、少一次判空

       #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

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




  

相关话题

  NoSql是一种语言,还是一种概念? 
  如何评价 mimalloc? 
  cygwin和mingw选哪个? 
  如何评价《轩辕剑外传穹之扉》制作人杨渊升针称《巫师 3》「200 人三年的制作规模,我们真的也可以」? 
  微博叫博主,贴吧叫楼主,那github叫什么主啊? 
  程序员讲到底就是”增删改查“吗? 
  JetBrains 2022的远程功能和VSCode Remote相比如何? 
  当下软件开发语言腾出不穷,作为老牌c++GUI领域top1的QT未来会怎样? 
  汉语编程语言意义何在? 
  被诺基亚放弃后,Qt的未来在哪里? 

前一个讨论
BOSS 直聘放假全员信曝光:全员留在工作地过年,这一规定合理合法吗?
下一个讨论
大公司为什么无法轻松使用更新的c++版本?





© 2024-06-02 - tinynew.org. All Rights Reserved.
© 2024-06-02 - tinynew.org. 保留所有权利