重温C++的设计思想

时间:2022-07-25
本文章向大家介绍重温C++的设计思想,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

一、内存管理(RAII)

内存管理分为堆、栈和RAII(Resource Acquisition Is Initialization)。除了C,还有几个语言D、Ada和RAII少数派语言也采用RAII

  • RAII依托栈和析构函数,对包括堆内存的资源进行管理,所以不需要GC垃圾回收。
  • 对象很大、对象编译器不能确定大小、对象是函数返回值,不能或不应该存储在栈上。
  • 操作1:内存分配;操作2:内存释放;操作3:内存垃圾收集。C++通常会做上面的操作1和2;JAVA会做上面的操作1和3,Python会做操作1、2、3
  • 栈上分配和释放,只需要移动一下栈指针,由于后进先出的执行过程,所以不可能出现内存碎片、

二、智能指针

C++简单类型称为POD(Plain Old Data),有构造和析构函数称为非POD。

栈展开(stack unwinding):在发生异常时对析构函数的调用。也就是说不管是否发生了异常,析构函数都会得到执行。

std的智能指针(std::unique_ptr,std::shared_ptr),使用智能指针目的之一是减少对象的拷贝:对超出作用域的对象进行释放。

2.1 unique和shared_ptr

一个unique_ptr只能独自拥有一个对象,而多个shared_ptr可以同时共享指针。

多个不同的shared_ptr不仅共享同一个对象,也需要共享计数。

2.2 左值和右值

左值:有标识符、可以取地址的。反之为右值。

常见的左值有:变量、函数、成员;返回左值的表达式(++x,x=1,cout<<''),字符串常量

常见的右值有:返回右值得表达式(x++,x+1,make_shared<int>(42)),非字符串的字面量(42,true)

左值引用(T&)和右值引用(T&&)。左值和右值首先是个值,所有对于指针,因为用值传递,不关心它是左值还是右值。

std::move(ptr)是个右值引用。等价于static_cast<smart_ptr<T>&&>(ptr)。

2.3 内存对象的局部性

C++的对象缺省为值语义。

Class A{
    B b_;
    C c_;
}

很多语言包括Java和Python会在A对象放B和C的指针。而C++则会把B和C的对象放在A的内存空间。既有优点也有缺点。

优点是保证了内存访问的局限性。局限性在现代处理器架构上是绝对有优势,缺点是复制对象的开销大大增加,所以C++需要移动语义,而Java里根本没有。

对象支持移动需要下列几步:

  • 对象有拷贝构造和移动构造(除非你只需要像unique_ptr只打算支持移动,不支持拷贝)
  • 对象有swap成员函数
  • 对象命名空间里,有一个全局的swap函数swap(T&lhs, T&rhs),这样方便自己和别人调用。
  • 实现operator=函数。

三、容器

3.1 连续内存的vector容器

vector保证强异常安全性,如果元素类型没有提供一个保证不抛异常的移动构造函数,vector使用拷贝构造函数。如果自定义类型拷贝构造代价较高,则使用移动构造函数,并标其为noexcept,或者只在容器中放置对象的智能指针。

C++11提供的emplace系列函数,是为了提升容器性能设计。emplace_back比push_back 少额外生成临时对象,少一次拷贝构造和一次析构。

现代处理器架构对连续内存访问速度比不连续内存访问速度快很多,所以vector的连续内存是他的优点。而为了保证连续性,vector的一个缺点是大小增长时导致的元素移动。所以如果有可能,尽可能使用reserve函数保留所需内存。

3.2 部分元素连续的双端队列daque

deque是双端队列(double-ended queue),但是和vector比,多了push_front,emplace_front,和pop_front函数,但是不提供data,capacity和reserve函数。因为deque的内存结构有点像hash桶,元素只是部分连续的,所以无法提供data函数。内存中元素大部分仍然连续,每一段存储大小相等,所以还是可以用数组下标的方式访问。index[i/chunk_size][i%chunk_size]

3.3 list、单向链表

list是双向链表,对比vector,优化插入和删除。

单向链表C++11里面叫forward_list。在元素大小较小的时候,他可以有效的节约内存。

3.4 关联容器

c++的关联容器(set,map,multiset,multimap)是有序的,而在别的语言通常是无序的。名字带multi的允许键重复。不带的不允许键重复。set和multiset只存放键,而map和multimap存放键值对。与序列容器相比,关联容器没有前、后的概念。但是提供insert、emplace等函数。关联容器有find、lower_bound、upper_bound等查找函数,返回是一个迭代器。

3.5 容器共性

容器的共性:容器类都有begin()和end()函数,大部分容器拥有sizie(),push_back()。不必集成一个共同的容器积累,便可以拥有通用地遍历一个容器的方法。

四、返回值优化

c++的返回值优化,对于非值类型,当返回值可能是子对象的情况,使用unique_ptr或shared_ptr,对于移动代价很高的对象,考虑分配在堆上,然后返回一个句柄(unique_ptr),或者从外部传递一个非const的对象引用。

在函数调用复用一个自动容量对象,作为引用传递。

五、标准泛型算法

c++的标准泛型算法:

  • sort:排序
  • reverse:反转
  • count:计数
  • find:查找
  • max:最大值
  • min:最小值
  • minmax:最小值和最大值
  • next_permutation:下一个排列
  • gcd:最大公约数
  • lcm:最小公约数

标准算法,一般会做一些约定,只要容器满足这种条件就可以使用这个算法。比如说sort:

参数满足随机访问迭代器,迭代器指向对象可以使用<比较大小,满足严格弱序关系。

迭代器指向的对象可以移动。

5.1 C++算法比C语言的优势

  • 他的性能比c语言的qsort更好,是因为编译器对比较操作做了内联,而c语言里面是通过一个额外的函数调用来实现。
  • c的qsort函数要求数组内容是可以按比特复制的,c++则要求迭代器执行的内容是可移动的。

六、其他

constexpr和const是编译期常量和运行期常量的意思

lambda表达式:以一对中括号开始,不需要说明返回值(类似auto)