c/c++相关

示例

什么是设计模式

“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动”。 ——Christopher Alexander

如何解决复杂性?

  • 分解

    人们面对复杂性有一个常见的做法:即分而治之,将大问题分解为多个小问题,将复杂问题分解为多个简单问题。

  • 抽象

    更高层次来讲,人们处理复杂性有一个通用的技术,即抽象。由于不能掌握全部的复杂对象,我们选择忽视它的非本质细节,而去处理泛化和理想化了的对象模型。

重构关键技法

  • 静态 → 动态
  • 早绑定 → 晚绑定
  • 继承 → 组合
  • 编译时依赖 → 运行时依赖
  • 紧耦合 → 松耦合

面向对象设计原则

  1. 依赖倒置原则(DIP)

    • 高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定) 。

    • 抽象(稳定)不应该依赖于实现细节(变化) ,实现细节应该依赖于抽象(稳定)。

  2. 开放封闭原则(OCP)

    • 对扩展开放,对更改封闭。

    • 类模块应该是可扩展的,但是不可修改。

  3. 单一职责原则(SRP)

    • 一个类应该仅有一个引起它变化的原因。

    • 变化的方向隐含着类的责任。

  4. 里氏Liskov 替换原则(LSP)

    • 子类必须能够替换它们的基类(IS-A)。

    • 继承表达类型抽象。

  5. 接口隔离原则(ISP)

    • 不应该强迫客户程序依赖它们不用的方法。

    • 接口应该小而完备。

  6. 优先使用对象组合,而不是类继承

    • 类继承通常为“白箱复用”,对象组合通常为“黑箱复用” 。

    • 继承在某种程度上破坏了封装性,子类父类耦合度高。

    • 而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。

  7. 封装变化点

    • 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。
  8. 针对接口编程,而不是针对实现编程

    • 不将变量类型声明为某个特定的具体类,而是声明为某个接口。

    • 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。

    • 减少系统中各部分的依赖关系,从而实现“高内聚、松耦合”的类型设计方案。

一些小的知识点

  1. 默认情况下编译器是以较低的标准来进行编译
g++ -std=c++17 your_file.cpp -o your_program
  1. STL

    1. 容器:存放数据,是一种class template
    2. 算法:STL算法是一种function template
    3. 迭代器:容器与算法之间的胶合剂,共有五种类型,所有STL容器都附带有自己专属的迭代器,原生指针也是一种迭代器。
    4. 仿函数:行为类似函数,可作为算法的某种策略,是一种重载了operator()的class或class template。
    5. 适配器:用来修饰容器或仿函数或迭代器接口。
    6. 空间配置器:负责空间的配置与管理,是一个实现了动态空间配置、空间管理、空间释放的class template。
  2. static:修饰局部变量时,使得被修饰的变量成为静态变量,存储在静态区。其生命周期与程序相同,在main函数之前初始化,程序退出时销毁(无论是局部静态还是全局静态)。此外,static限制了链接属性,被修饰的全局变量只能被包含在该定义的文件访问(多文件编译时,隐藏),在C++中还可以实现不同对象之间数据共享。

  3. volatile:“易变的”,因为访问寄存器要比访问内存单元快,所以编译器一般会作减少存取内存的优化,因此可能会读到脏数据。当要求使用volatile声明变量值时,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问;如果不使用valatile,则编译器将对所声明的语句进行优化。

  4. const:防止变量被修改,必须定义时初始化。对于指针又分为顶层和底层,分别表示指针本身或指向内容的修改。可通过const_cast进行类型转换。(函数的值传递会创建临时变量,不会改变实参,加不加const无影响,即无法重载进行区分)

  5. C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast

    1. const_cast:用于将const变量转为非const
    2. static_cast:用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
    3. dynamic_cast:用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用则抛异常(向上转换:指的是子类向基类的转换;向下转换:指的是基类向子类的转换)。通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
    4. reinterpret_cast:几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用。
  6. 引用的本质就是所引用对象的地址。

  7. C++的四个智能指针(防止内存泄漏): auto_ptr, shared_ptr, weak_ptr, unique_ptr ,其中后三个是c++11支持,并且第一个已经被11弃用。unique_ptr 是一种独享被管理对象指针所有权的智能指针。unique_ptr对象包装一个原始指针,并负责其生命周期。当该对象被销毁时,会在其析构函数中删除关联的原始指针。

  8. ++i / i++ 实现

    int& int::operator++ () { *this +=1return *this;}
    const int int::operator (int) { int oldValue = *this;++(*this);return oldValue;}
  9. __attribute((constructor))是gcc扩展,标记这个函数应当在main函数之前执行。同样,__attribute((destructor))标记函数应当在程序结束之前(main结束之后或调用了exit后)执行。

  10. 虚函数表是怎样实现运行时多态

    编译器为每一个类维护一个虚函数表,虚函数表是类对象之间共享的,子类若重写父类虚函数,虚函数表中的该函数的地址就会被替换。对于存在虚函数的类的对象,在VS中,对象的头部存放指向虚函数表的指针,对虚函数指针的地址解引用得到虚函数表的地址。

  11. 不要在构造函数中调用虚函数

    因为父类对象会在子类之前进行构造,此时子类部分的成员还未初始化, 因此调用子类的虚函数是不安全的。

  12. 不要在析构函数中调用虚函数

    析构函数在销毁一个对象时,先调用子类析构,再调用基类析构,此时派生类对象的数据成员已经“销毁”,再调用子类虚函数没有意义。智能指针能够帮助我们处理资源泄露、空悬指针、比较隐晦的由异常造成的资源泄露问题。

  13. 类之间的关系

    1. 继承:is a 子类继承父类的方法(鹅和鸟的关系)
    2. 组合:has-a 整体和部分的关系,整体和部分之间是不可分离的,它们具有相同的生命周期(鸟和翅膀的关系)
    3. 聚合:contains-a 整体和部分的关系,整体和部分之间是可分离的,它们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享。如员工和公司之间的关系。
    4. 关联:弱关系,双方一般是平等的。如明星和粉丝之间的关系。
    5. 实现:实现是类和接口之间的关系。接口通过纯虚函数来实现。
    6. 依赖: 简单的理解,依赖就是一个类A使用到了另一个类B,而这种使用关系是具有偶然性的、临时性的、非常弱的,但是类B的变化会影响到类A。比如某人要过河,需要借用一条船,此时人与船之间的关系就是依赖。表现在代码层面为,类B作为参数被类A在某个method方法中使用。
  14. unordered_map是如何解决哈希冲突的

    链地址法,一个桶中可以放多个元素,元素的关键字相同。unordered_map会维护桶的平均元素数量,并在需要时添加新的桶,以使得load_factor<=max_load_factor

  15. template <class T> struct atomic;

    C++11标准从不同的视角看待问题(多线程下访问共享资源/数据):需要同步的总是资源/数据,而不是代码。

    • C++11对数据进行了更为良好的抽象,引入**”原子数据类型/atomic”类型**,以达到对开发者掩盖互斥锁、临界区的目的。

    • C++11对常见的原子操作进行了抽象,定义出统一的接口,并根据编译选项/环境产生平台相关的实现。新标准将原子操作定义为atomic模板类的成员函数,囊括了绝大多数典型的操作——读、写、比较、交换等。通常情况下,内存模型是一个硬件上的概念,表示机器指令(或者将其视为汇编指令)是以什么样的顺序被处理器执行的。现代的处理器并不是逐条处理机器指令的(**强顺序的(strong ordered)/弱顺序的(weak ordered)**)。

    • 弱顺序的内存模型: 可以进一步挖掘指令中的并行性,提高指令执行的性能。你一会想顺序执行,一会又想“乱序”执行,更有甚者,还想对“乱”的程度分等级……如何提供这种灵活性呢?在C++11标准中,设计者给出的解决方式是让程序员为原子操作指定所谓的内存顺序:memory_order。实际上,atomic类型的其他原子操作接口都有memory_order这个参数,而且默认值都是std::memory_order_seq_cst

    • 顺序一致内存顺序/memory_order_seq_cst:全部存取都按照顺序执行(不要重排序指令,不要整什么指令乱序执行,就按照代码的先后顺序执行机器指令)。

    • 松散内存顺序/memory_order_relaxed:不对执行顺序做任何保证。

  16. 原子操作彻底宣告C++11来到了多线程和并行编程的时代。相对于偏于底层的pthread库,C++通过定义原子类型的方式,轻松化解了互斥访问共享数据的难题。同样也延续了其易学难精的特性,虽然atomic/原子类型使用上较为简单,但其函数接口(原子操作)却可以有不用的内存顺序。C++11从各种不同的平台上抽象出了一个软件的内存模型,并以内存顺序进行描述,以使得想进一步挖掘并行系统性能的程序员有足够简单的手段来完成以往只能通过内联汇编来完成的工作。《深入理解C++11 - C++11新特性解析与应用》

  17. C++11中对内存顺序相关的设计,主要是为了从各种繁杂不同的平台上抽象出独立于硬件平台的并行操作。对于我们日常的开发工作,默认的顺序一致内存顺序memory_order_seq_cst足可以应付了,但是开发者想让多线程程序获得更好的性能的话,尤其是在一些弱内存顺序的平台上,比如PowerPC,建立原子操作间的内存顺序还是很有必要的,因为着能带来极大的性能提升,这也是一些弱一致性内存模型平台的优势。但对于并行编程来说,可能最根本的,还是思考如何将大量计算的问题,按需分解成多个独立的、能够同时运行的部分,并找出真正需要在线程间共享的数据,实现为C++11的原子类型。虽然有了原子类型的良好设计,实现这些都可以非常的便捷,但并不是所有的问题或者计算都适合用并行计算来解决,对于不适用的问题,强行用并行计算来解决会收效甚微,甚至起到相反效果。因此在决定使用并行计算解决问题之前,程序员必须要有清晰的设计规划。而在实现了代码并行后,进一步使用一些性能调试工具来提高并行程序的性能也是非常必要的。