Prepare

前提知识:

  • c#/java/js/python/ruby …etc 的基础语法
  • GC相关基础知识
  • cpp/c++的pointer的概念、使用方法。
  • stack和heap的区别的基础概念

Intro

目前市面上比较流行,而且无GC损耗的语言只有三个:C,C++,RUST。

RUST相对而言概念比较新,语法乍的看起来并没有很简洁。 当然c++发展至今,语法看起来也跟乱码比较相像。但是c++归根结底还是源自C的一门语言, 有了C的基础,读c++的(心理)负担会小很多。

笔者对于RUST了解不深,不过但就写出bug的几率而言,c最大,其次是c++,最后是rust。 但是譬如rust为了防止human error,rust中的文字列和c#/java等中的string类型类似,不可以修改。 从这一点而言,c++的自由度更高。

但是自由度高也意味着许多其他的事情。 比如令人困扰的bug率。 比如无数条不得不去遵守的规则 – 这些规则并不是语言规则所限制死的,而是由人来人为规定的。

c++的基本原则就是自由,c++虽然带来了std库的革命, 但是c++从来不在意programmer写的是c with class,还是std c++,正因为它的核心就是自由。

那么这么疯狂的一门语言为什么还会如此的受欢迎? 原因就是足够快。 没有了GC的烦恼,C++可以更精准的掌控时间。

RAII

由于在C++中一切内存的操作都需要手动来完成, 为了避免human error,c++中有许多人为制定的编程手法。 其中一个就是RAII(Resource Acquisition Is Initialization)。

字面并不是很好理解。通过下面的一段简单的代码来讲一下。

PS:有人提议不使用RAII,字面太难理解了,而是Scope-Bound Resource Management(SBRM)

STACKOVERFLOW: What is meant by Resource Acquisition is Initialization (RAII)?

Scope and Stack

1
2
3
4
5
6
7
8
void foo(int num){
  if(num > 0){
    int a = do_sthA(num);
    int b = do_sthB(num);
    int c = do_sthC(num);
    num = a && b && c;
  }
}

和在其他语言一样,在if文结束之后,int a, b, c全部都会在内存中消失。
我们知道,在function里面的是stack领域,这里属于stack领域独特的处理。
c++和其他语言不同,c++可以在stack领域里面宣言object的instance,所以会有这样的代码。

1
2
3
4
5
6
7
8
9
10
struct objA {
  int x, y;
};
void foo(int num){
  if(num > 0){
    objA a();
    a.x = num;
    a.y = num;
  }
}

那么在if文结束之后,ojbA a也同样会在内存中被消失。
好,这个是对于stack中的数据的处理。我们接下来看对于heap领域中的数据的处理。

Scope and Heap

1
2
3
4
5
6
7
8
9
// this is a memory leak
struct objA{
  int x, y;
};
void foo(int num){
  if(num > 0){
    objA* a = new objA();
  }
}

我们用valgrind来跑一下

1
2
3
4
5
6
7
8
9
10
==648== HEAP SUMMARY:
==648==     in use at exit: 8 bytes in 1 blocks
==648==   total heap usage: 2 allocs, 1 frees, 72,712 bytes allocated
==648==
==648== LEAK SUMMARY:
==648==    definitely lost: 8 bytes in 1 blocks
==648==    indirectly lost: 0 bytes in 0 blocks
==648==      possibly lost: 0 bytes in 0 blocks
==648==    still reachable: 0 bytes in 0 blocks
==648==         suppressed: 0 bytes in 0 blocks

可以看到有一块区域直到运行结束也没有被释放。 虽然我们的pointer是在stack区域宣言的, 但是在scope结束之后,虽然pointer消失了,但是留存在heap空间的instance仍然健在, 造成了典型的memory leak。

RAII and std libray

RAII事实上就是利用的我在scope and stack中所说的特性,在scope结束之后立刻开始回收资源。 这样可以不用使用delete语句,也不会造成内存溢出。

c++的标准库中的物件基本都是使用RAII原则来进行设计的。
比如vector<T>
我们在下面的代码中进行观察。

1
2
3
4
5
6
int main(){
  vector<int> vc(10, 2);
  vc = vector<int>(20, 1);
  vc = vector<int>(7, 3);
  return 0;
}

我们将这段代码在valgrind中跑一下。

1
2
3
==789== HEAP SUMMARY:
==789==     in use at exit: 0 bytes in 0 blocks
==789==   total heap usage: 4 allocs, 4 frees, 72,852 bytes allocated

我们发现并没有任何的memory leak。所有的内存全部都被释放了。

接下来我们要讲解的就是RAII在pointer中的应用。 在c++的目前不推荐的auto_ptr以及c++11中推荐使用的几款新的smart pointer都是使用的RAII手法, 来避免human error所导致的memory leak.

Smart Pointer

smart pointer是一种class,不过被实装成了pointer的样子,可以提供一种更安全的pointer使用方式和接口。

早期的c++中使用的auto_ptr,在C++ Primer第四版中甚至还介绍了一种计数器的实装方法。 不过随着9年前的c++11的到来,auto_ptr被正式废弃,并在c++17废弃。 所以这里就不着重介绍。

auto_ptr主要的弊端在于,它的复制操作和一般意义上理解的赋值操作有冲突。 由于在逻辑上并没有保持一致性和自洽性,所以auto_ptr在c++11版本中被unique_ptr所完全取代。

1
2
3
auto_ptr<int> a(new int);
auto_ptr<int> b = a; //<-这里a会被release掉
*a = 2;// <- error~!

unique_ptr

为了避免上述的麻烦。unique_ptr采用了完全不使用等于号的策略。 unique_ptr只允许这个pointer的所有权只、且仅由一个pointer所有。

这么在乎这个数字的原因在于为了遵循RAII的策略,在计数器变为0的时候,直接释放掉pointer和pointer所指向的资源。

赋值。

1
2
3
4
5
unique_ptr<int> a(new int);
unique_ptr<int> b(std::move(a)); //<-这里a会被释放掉,可以避免human error
*a = 2;// <- error~!
D d;
unique_ptr<int[]> c(new int[3]);

它也可以传送一个专门的deleter方法,具体参照这里。

shared_ptr

它的计数器不像unique_ptr一样,它除了0和1以外,还可以1以上。

weak_ptr

这个和其他语言的weak reference类似,就不加赘述了。

注意:它只能引用shared_ptr的pointer。

Cons

smart pointer并不是万能药. 不过可以简化脑力劳动的地方,还是要该简化就简化的。

Smart pointers are not perfect substitutes for plain pointers.
Google C++ Style Guide

Refs


creativ common license