C++部分知识点记录

文章目录

  • 反问
  • 为什么想要加入国微芯?
  • 自己的优势/岗位匹配度在哪?
  • EDA(电子设计自动化)
  • 前言
  • 《C++ Primer》知识点记录
    • 1.声明一个返回数组指针的函数(P205)
    • 2.头文件应该放什么?
    • 3.assert和NDEBUG(P216)
    • 4.成员函数作友元(P252)
    • 5.explicit关键字(P265)
    • 6.动态绑定(P527、550)
  • 一、封装继承多态
  • 一、C和C++区别
  • 二、static
  • 三、const
  • 四、extern
  • 五、虚函数
  • 六、三种访问权限
  • 七、编译语言和脚本语言的区别
  • 十六、跨平台开发注意原则
  • 八、new和malloc区别
  • 九、运算符重载
  • 十、智能指针
  • 十一、三五原则
  • 十二、OpenMP
  • 5.5 性能优化
  • 5.6 底层剖析
  • 十三、设计模式六大原则
  • 十四、项目的软件设计体现在哪里?
  • 十五、排序
  • 其他零碎知识点
  • 总结

面试官您好!很荣幸获得这次面试机会。我叫毛子彧,现在是东南大学机械工程学院的2023应届硕士毕业生,我的专业是工业工程与管理。我应聘的岗位是C++软件工程师。我本科就读于湖南农业大学的信息工程。在本科的时候我就对C++、Linux、还有数据结构、网络、系统等有了系统性的学习,有一定的基础。在东南大学,我所在的是江苏省微纳重点实验室,我们组主要研究内容是数值模拟和仿真这一块,在该过程中主要在Windows平台开发C++语言编写的数值计算软件。
  硕士期间,我主要做了三个项目,首先是我自己独立在Linux平台上开发的一个轻量级高并发服务器,主要设计网络编程、多线程、socket、IO多路复用等;我的研究生工作是微尺度细胞数值模拟,这个工作从无到有都是我一人负责。主要开发了一套可靠模拟细胞流动现象,并且可以定量分析其参数变化的程序,程序具有低耦合、可扩展、可复用性特点,并将结果转化为一篇SCI论文和一项发明专利。我看到岗位描述里提到并行计算几何计算这两点,这在我硕士工作的两个项目里都有涉及,都利用了OpenMP并行编程去提高计算效率;然后几何计算的话是设计了大量的梯度、速度、涡量等向量之间的计算,虽然我不确定这和EDA的几何计算是不是一回事。
  现在的话我在深圳拿了一些offer,但我经过一些了解和比较的话,其实我秋招最想加入的是国微芯,希望我可以顺利走完面试流程,取得一个好的结果。


反问

  • 主要业务有一些了解(工具、算法的开发,测试等等),这个岗位进去后有进一步的方向划分吗?
  • 新人培养方案?
  • 对于胜任这份岗位的话,对我有什么建议吗?
  • 团队氛围和风格是什么样的?

为什么想要加入国微芯?

了解:设计和制造国产的EDA工具系统和相关服务。像后端、制造端EDA工具和后端设计服务。

首先我开学那年东南大学和国微集团就成立了一个EDA联合实验室。所以我早先对国微集团有了了解(比方说它是深圳第一家半导体设计企业),首先是一个很大的平台,它的规模和国家的投入都是很大的;其次就是我对EDA软件开发非常有兴趣,我了解了一下芯片设计的主要流程,发现在前端和后端的每个步骤里都要用到大量的EDA工具,所以说即使现在有一些干扰因素,但也是非常有前景的,放在13年以前,中国人自己也不相信国产新能源汽车可以弯道超车。另外细分的步骤和工具非常多,所以我觉得可以有机会钻研具体某一类型的去提高自己的技术深度和竞争力;最后是我朋友的姐姐在国微芯,了解的一些研发氛围很好,沟通简单有效,是我很向往的那种氛围。刚刚提到的是我考虑的几个点,平台、行业、氛围,薪资,还有城市吧,综合来说我最想加入的就是国微芯。

自己的优势/岗位匹配度在哪?

  1. 并行计算或者说高性能计算。作为对并行计算的支撑,我具有OpenMP并行编程使用经验,了解一些它的底层编译原理;并对常见的并行编程方法有过对比和适用范围的了解。
  2. 几何计算。虽然不太确定EDA中的几何计算主要是指什么,但是我的项目中涉及了大量的梯度、速度、涡量、还有拉普拉斯算子等向量之间的计算,所以对于几何计算来说也算是有一定的经验。
  3. 具有比较多的C++开发经验,不论是在Win平台还是Linux平台。

EDA(电子设计自动化)

楷登和新思
布局布线、时序分析、功耗分析、仿真

芯片的设计的主要流程可以分为前端和后端,前端负责芯片的逻辑电路设计,包括系统架构的定义,RTL编码,逻辑综合,这期间会进行多次的仿真和验证,最终得到门级的网表。后端主要负责芯片的物理设计,包括布局规划,时钟树综合,布线,参数提取等等步骤,最终会得到一个芯片电路的物理版图,然后提供给晶圆厂去制造。

比如要做一个简单的加法电路,比如a+b=c,我们需要先用Verilog,或者VHDL这些硬件专用语言,把这个加法电路实现出来,为了验证加法的功能是不是正确,就要用EDA的仿真软件,比如新思的VCS和VC Formal,让a=1,b=1,看c是不是等于2。如果输入1+1,结果等于3,那么就需要调试软件,比如Verdi来确定问题出现在什么地方,还需要用到静态和动态的分析软件,比如SpyGlass来诊断分析电路是否有一些潜在的问题。如果代码没有问题就可以去编译了,这在芯片设计里叫做综合Synthesis。综合的结果就是生成一堆互相连接的门电路,也叫做网表,这就需要使用专门的综合工具 Design Complier综合生成网表,再用IC Compiler做布局布线,用Prime Time做时序分析,用PrimePower做功耗优化,用IC Validator做物理验证,用StarRC做寄生参数提取等等,最终生成一个符合设计要求,也符合晶圆代工厂要求的GDSII文件,这个文件就被拿去做流片生产

前言

仅对C++部分知识点做一些记录,大部分内容为搬运,会标明出处。该文章不定期更新。


《C++ Primer》知识点记录

1.声明一个返回数组指针的函数(P205)

btw:

给数组定义一个类型别名:

typedef int[10] arr;	//错误
typedef int arr[10];	//正确

基础前提:

int arr[10];   			//含有10个整数的数组
int *p1[10];  			//含有10个指针的数组
int (*p2)[10]=&arr;  	//p2是一个指针,它指向含有10个整数的数组

声明一个返回数组指针的函数:

类型 (*fun (参数))[length]
int (*func(int i))[10];

eg1:(反例)

int(*f())[10]
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
	return &arr;
}

int main() {
	int (*p)[10] = f();
	cout << *(*p+2) << endl;	//输出3
	cout << *(*p+3) << endl;	//此时输出错误,原因应该是返回了局部对象的指针
	return 0;
}

eg2:

我们要返回a数组的值给别的类使用

int a[40];
int(*GetData())[40]
{
    return &a;
}

int main() {
    int recvData[40] = { 0 };
    int(*p)[40] = GetData();
    for (int i = 0; i < 40; i++)
    {
        recvData[i] = (*(*p + i));	//本质上p解引用一次后才是指向a[0]的指针
        cout << recvData[i] << endl;
    }
    return 0;
}

来源:《C++ 返回数组指针的函数》

2.头文件应该放什么?

头文件中通常包含那些只能被定义一次的实体。

  1. 定义类(类所在头文件名应与类名一样)
  2. 函数声明
  3. constconstexpr变量
  4. inline函数(包括inline成员函数)和constexpr函数的定义(可以多次定义)
  5. 如果非成员函数是类接口的组成部分(概念上属于类但不定义在类中),则应该与类定义在同一个头文件

3.assert和NDEBUG(P216)

使用assert需要满足两个条件:

  1. assert头文件:
#include 
#include 
  1. 以Windows平台下VS举例,程序运行模式必须是Debug(调试)

此外,assert可以输出错误信息:

assert(("b不是0", b));	//提示信息放前,加双括号

开发调试阶段 ->  需要assert
发行版    -> 不需要
所以需要定义#define NDEBUG该定义一定要在assert头文件之前
《NDEBUG预处理变量问题》

4.成员函数作友元(P252)

如果是另一个类作友元,直接声明即可:

friend class Y;

如果需要另一个类的成员函数作友元,则需要遵守一些顺序:

class Y {
    void f();   //1.先定义友元函数的类并声明这个函数
};

class X {
    friend void Y::f();     //2.定义要访问私有成员的类,并声明友元
    int x = 9l;
};

void Y:: f() {      //3.最后在类外定义这个友元函数
    X obj;
    cout << obj.x << endl;
}

5.explicit关键字(P265)

《C++ explicit关键字详解》
作用:修饰一个构造函数,表示该构造函数是显示的,而非隐式的(默认情况下构造函数是隐式的,且 ,没有implicit关键字)。主要防止类构造函数的隐式自动转换

使用条件:

  1. 只对一个实参的构造函数有效(或者其他参数都有默认值 int y = 0)
  2. explicit只允许出现在类内的构造函数处

作用:

  1. 一方面,阻止了类型的隐式自动转换
第一种:有一个构造函数作为类型转换的“桥梁”,将它设为explicit后阻止隐式转换
class Sale {
public:
    int a = 2;
    int b = 2;

    Sale combine(Sale obj) {
        a += obj.a;
        b += obj.b;
        cout << a << endl << b << endl;
        return *this;
    }
    explicit Sale(int num):a(2),b(num){}	//桥梁
    Sale(){}
};

int main() {
    Sale obj1;
    //Sale obj2;
    int temp = 5;
    obj1.combine(temp);//没加explicit时,输出4,7;加了后报错。本来这里是可以隐式转换的
    return 0;
}
解决方法:
obj1.combine(Sale(temp));
  1. 另一方面,只能用于直接初始化而不能拷贝形式的初始化(=
class Sale {
public:
    int a = 2;

    explicit Sale(int num){
		a = num;
}
};

int main() {
    Sale obj1(66);  // 直接初始化
    Sale obj1 = 66; // 拷贝形式初始化。原本是可以的,用explicit关键字后,只能使用直接初始化了。(
    				/? ps:构造函数用初始化列表貌似不受这一影响)
    cout << obj1.a << endl;
    return 0;
}

6.动态绑定(P527、550)

《C++动态绑定》
条件:

  1. 调用基类的指针/引用
  2. 调用虚函数

作用:
当我们用指针/引用调用虚函数时,该调用将被动态绑定;该调用可能执行基类或某个派生类的版本
同一段代码,既能调用基类虚函数,也能调用派生类虚函数

条件详解:(B是A的派生类)

  1. 首先指针或引用的静态类型必须是A(B静态类型无法向A转换),并指向派生类的动态类型;
  2. 调用的函数必须是在基类A中定义为虚函数、且在派生类B中重写的(形参稍有不同都不算)

示例情况:

A指针指向B,调用B的函数,但A类没有这个成员函数、或者调用的是非虚函数(没有满足重写条件),调用失败 ×
这个时候实际调用的函数版本仅仅只由指针的静态类型决定了

class A {
public:
	virtual void f1();
	void f2();
};

class B :public A {
public:
	void f1();
	void f(int x);
	void f2();
};

int main() {
	A a; B b;
	A* p1 = &a;
	A* p2 = &b;
	p1->f1();	//动态绑定,调用A::f1()
	p2->f1();	//动态绑定,调用B::f1()
	p2->f1(5);	//错误,没有重写,A内没有f1(int)
	p2->f2();	//因为A::f2()没有声明为虚函数,所以调用A::f2()(仅由静态类型决定)
}



一、封装继承多态

首先,我们先将一类对象的共同特征抽象出来构造类,抽象只关注对象的静态特征(属性)和动态特征(函数)。

封装

对外隐藏复杂细节,提供简单易用的接口

利用抽象数据类型将静态特征和动态特征封装在一起,使其构成一个独立实体

  • 可以减少数据被获取、修改的可能
  • 提高代码的复用性(方法和类可以反复使用)
  • 降低了耦合性、提高了可维护性,不同对象和模块间能更好的协同

继承

复用代码最直接重要的手段

在原有类基础上进行扩展,以满足新的需求和功能

多态

派生类重写基类的虚函数,基类指针指向派生类

多态指方法而非属性;不同继承关系对象去调用同一函数,产生不同行为

  • 派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容,可以提高可扩充性和可维护性

一、C和C++区别

  1. C++新增重载
  2. C++有虚函数重写概念,可以实现多态
  3. C++的struct新增成员函数权限访问
    C++中还有class表示类。
  4. C++增加了模板,提供了更强大的STL标准库。
  5. C++有新增的语法和关键字。
    语法有头文件命名空间的不同,而且可以自己定义自己的空间,C中不可以;
    关键字方面,增加了newdelete,增加了引用
  6. 总结:C是结构化语言,重点在于算法和数据结构。C程序的设计首先考虑如何通过一个代码,一个过程对输入进行运算处理输出。而C++首先考虑如何构造一个对象模型,让这个模型能够契合与之对应的问题领域,这样就能通过获取对象的状态信息得到输出。
    (搬运自公众号:herongwei)

二、static

程序中

  1. static修饰局部变量;编译器会把局部变量分配到全局区/静态区,生命周期为整个周期
  2. 修饰全局变量;该变量的外部链接属性变成内部链接属性,其他源文件无法访问
  3. 修饰函数;同全局变量

类中
静态数据成员:

  1. 类内定义,类外初始化(无需再加static)
  2. 不属于任何对象,可通过对象.作用域::访问

静态成员函数:

  1. 没有this指针,无法访问非静态成员,但可以调用静态成员;而非静态成员函数可以访问静态成员

三、const

const作用
一、修饰变量

  1. 定义变量为常量
  2. 如果const全局变量,和static类似,在其他文件不可见

二、引用
引用及其对象都得是常量

三、指针

顶层const 底层const
对象 基本类型和复合类型 复合类型:指针引用
特点 指针指向不能变(自己的值不能变) 指针指向的对象不能变
举例 指针常量
int* const p
常量指针
const int* p或者int const* p

四、函数

  1. 修饰参数,表示参数在函数体中不可变,结合引用增加效率
  2. 修饰返回值,避免返回值被修改
  3. 修饰成员函数(非全局函数):(1) 该函数不能修改成员变量 (2) 不能调用非const成员函数

五、类成员

  1. 成员变量不能修改,且只能在初始化列表赋值
  2. 修饰类对象,对象的任何成员都不能被修改,且只能调用const成员函数

哪些函数不能声明为const?

  1. 构造函数:因为构造函数需要修改成员变量
  2. static函数:const函数本质想修饰this指针,表示this指向内容不可变,static静态成员没有this指针

const常量和宏的区别:

  1. const常量进行类型检查,define只类型替换
  2. const常量在运行过程中只有一份拷贝,更省空间,而define是若干份

四、extern

《C/C++中的 extern 和extern“C“关键字的理解和使用》

  1. 跨文件访问变量
       1) 直接通过在文件中extern int i声明来访问
       2) 在头文件中extern声明变量/函数,主函数包含头文件即可
  2. extern "C"按C语言去编译函数、编译文件、头文件(仅限C++)
  3. 令 extern"C" 不管在C语言文件中还是在C++文件中都可以使用该库:#ifdef __cplusplus即可

五、虚函数

虚函数

基类希望派生类进行覆盖的函数:定义为虚函数。
当我们用指针/引用调用虚函数时,该调用将被动态绑定;该调用可能执行基类或某个派生类的版本

  1. 构造函数静态成员函数内联函数不能是虚函数。《构造函数可以是虚函数吗》《构造析构、静态、内联可以是虚函数吗》
  2. static一样,只能出现在类内声明,不能出现在类外函数定义。
  3. 如果基类定义了虚函数,则该函数在所有派生类中隐式地也是虚函数。
  4. 派生类的虚函数必须和基类函数拥有相同的形参列表和返回值,唯一例外:返回指向本类对象的指针。
  5. 如果派生类要调用虚函数的基类版本,利用作用域运算符。
  6. 虚析构函数:待补充 -------------------

纯虚函数

将一个函数定义为纯虚函数,告诉用户这个函数没有实际意义。

  1. = 0即可讲一个虚函数说明为纯虚函数,且只能再类内部的虚函数声明语句处操作;此后它就不能在类内定义了,要提供定义也只能在类外
  2. 含有纯虚函数的类是抽象基类,不能创建对象

重构: 在继承体系中增加一个抽象类是重构的典型示例。重构负责重新设计类的体系以便将操作或数据从一个类移动到另一个类中。《什么是重构?》

六、三种访问权限

《C++中三种访问权限》

访问权限 public protected private
对本类 可见 可见 可见
对派生类 可见 可见 不可见
对外部调用 可见 不可见 不可见
  1. 外部只能通过调用外部可见的函数,才能利用函数体内本身的功能,对不可见的成员进行操作(也就是所谓的接口
  2. 函数成员函数都可以成为友元,其中成员函数做友元稍微麻烦点,见上第4点。成为友元后私有和保护成员对这些函数或类是可见的。
  3. 派生类的成员友元只能通过派生类对象访问基类的protected成员,不能访问基类对象protected成员
  4. 派生访问说明符(即派生类指定的继承权限)对派生类的成员(及友元)访问基类成员无影响。其目的是控制派生类的用户(包括派生类的派生类)对于基类的访问权限。
派生类的继承权限 public protected private
对派生类 保持不变 保持不变 保持不变
用户&派生类的派生类 保持不变 public->protected 全部private
  1. 类型转换: 假设B是A的派生类
    (1) 对于 用户:只有B 公有地 继承A时,才可以使用B对A的类型转换
    (2) 对于 B的成员函数和友元: 均可使用B对A的类型转换
    (3) 对于 B的派生类的成员和友元:在B 私有地 继承A时,B的派生类成员和友元不可使用B对A的类型转换,其余情况可以

可以概括为:将类型转换视为基类中的一个public对象,再看继承权限结果对它的影响。

  1. 改变权限:
    派生类只能为那些它可以访问的名字提供using声明,通过在类中某一个权限后声明,来改变基类中的成员的权限
public:
	using Base::size;	//记得加上类作用域

七、编译语言和脚本语言的区别

类型 特点 软件 举例 优点 使用场景
编译型语言 必须在程序运行之前将所有代码都翻译成二进制形式,也就是生成可执行文件,用户拿到的是最终生成的可执行文件,看不到源码 完成编译过程的叫做编译器(Compiler)
IDE集成开发环境包括了编译器,还有编辑器、调试器、用户界面等
C++、Go、Pascal 执行速度快、硬件要求低、保密性好 开发操作系统、大型应用程序、数据库
脚本型语言(解释型) 程序运行后会即时翻译,一边执行一边翻译,不会生成任何可执行文件,用户必须拿到源码才能运行程序 解释器 Shell、JavaScript、Python、PHP 使用灵活、部署容易、跨平台性好 Web开发和小工具制作

十六、跨平台开发注意原则

《跨平台应用程序开发方法大盘点》
《跨平台C、C++代码注意的事项及如何编写跨平台的C/C++代码》

脚本型语言

脚本型编程语言有良好的平台兼容性,它一边解释一边运行,在任何环境中都可以运行–前提是安装了解释器。
php、python、Ruby等,这些语言在设计之初就考虑了在PC操作系统上的运行问题,分别为不同操作系统设计了对应的解释器,因此它们能够实现“写一份代码,在多个操作系统上运行“–移动端系统除外(IOS和Android等)

C/C++

  1. 在不同的操作系统上,均有相应的编译器,开发人员在开发相应软件时,根据编译器中定义的预编译宏对于代码中的部分系统API调用代码单独编写,然后通让特定平台的编译器去编译对应的代码。
    windows32/64平台_WIN32都会被定义,而_WIN64只在64位windows上定义,因此要先判断_WIN64,再判断Linux
  2. 对于编译型语言开发的软件来说,如果涉及图形界面(GUI)界面,问题就会比较复杂。不能使用与操作系统绑定的GUI接口或框架,例如windows下的MFC库;应该使用一个跨平台的GUI库,例如QT,GTK。
#ifdef _WIN32
   //define something for Windows (32-bit and 64-bit, this part is common)
   #ifdef _WIN64
      //define something for Windows (64-bit only)
   #else
      //define something for Windows (32-bit only)
   #endif
#elif __APPLE__
    #include "TargetConditionals.h"
    #if TARGET_IPHONE_SIMULATOR
         // iOS Simulator
    #elif TARGET_OS_IPHONE
        // iOS device
    #elif TARGET_OS_MAC
        // Other kinds of Mac OS
    #else
    #   error "Unknown Apple platform"
    #endif
#elif __ANDROID__
    // android
#elif __linux__
    // linux
#elif __unix__ // all unices not caught above
    // Unix
#elif defined(_POSIX_VERSION)
    // POSIX
#else
#   error "Unknown compiler"
#endif

八、new和malloc区别

malloc new
性质 标准库函数,可覆盖 运算符,可重载
分配时 需显示指出尺寸
不调用构造函数
无需指定大小,编译器自动计算
调用构造函数进行初始化
返回类型 返回void指针 返回具体指针
失败后 失败时返回NULL 失败时抛出异常
包含关系 new封装了malloc,直接free不会报错,
但是这只是释放内存,而不会析构对象

deletefree的区别:
delete会调用析构函数进行清理
delete释放对象数组时候不要忘了 []

九、运算符重载

《C++运算符重载》

  1. 运算符重载为成员函数时,参数比运算符目数 / 类外重载函数的参数 少1
  2. =只能重载为成员函数,注意最好深拷贝
  3. (a =b) = c; a = b其实是a的引用,为了保持=的这个特性,=的返回值为&才是风格最好的写法
  4. 一般重载为成员函数,但有时成员函数不满足使用要求(5 + a),全局函数又不能访问私有成员,此时重载为友元
  5. 重载<<时,os只能是引用,因为ostream的拷贝构造函数时私有的,无法生成对象。
  6. 重载++--时,写一个无用int类型形参的版本operator int() { return n; }用于后置表达式传参;前置返回引用,后置拷贝this,操作后再返回(本身,前置就是返回引用;后置不能返回引用,不能作为左值)
  7. 以下不能被重载 ..*::? :sizeof
成员函数 非成员函数
=->[]()必须是成员函数
改变状态的:++--&必须是
因为没办法修改ostream和istream类,<<>>必须非成员且声明友元
具有对称性、两边可互换的应该是非成员

十、智能指针

总结:

特点 优点 缺点
shared_ptr 具有引用计数器 解决 auto_ptr 在对象所有权上的局限性 循环引用:当两个对象相互使用一个shared_ptr成员变量指向对方,使引用计数失效,导致内存泄漏
weak_ptr 协助 shared_ptr 工作,可获得资源的观测权
没有引用计数,不控制对象生存期
解决循环引用问题
unique_ptr 持有对对象的独有权,不能拷贝只能移动 禁止了拷贝语义,会在编译期报错
auto_ptr 类似于unique,new获得对象,auto销毁时对象也被delete 可能会对一块堆空间多次delete,导致报错(解决方法 1.深拷贝 2.unique 3.计数shared)《auto被废弃的原因》

如何选择?

  1. 如果程序要使用多个指向同一个对象的指针,应选择 shared_ptr
  2. 如果程序不需要多个指向同一个对象的指针,则可使用 unique_ptr
  3. 任何情况下都不应该使用 auto_ptr(即使可以替代unique)
  4. 为了解决 shared_ptr 的循环引用问题,我们可以祭出 weak_ptr

RAII(Resource Acquisition Is Initialization):资源获取就是初始化。保证使用对象时先构造对象最后析构对象,是常用的管理资源、避免内存泄漏的办法。
引入了智能指针类型来管理动态对象,用来安全使用动态内存

《终于有一篇小白能看懂的智能指针详解了》
《C++ STL 四种智能指针》
《shared_ptr循环引用问题以及解决方法》

问题:

  1. 忘记delete / 重复delete
  2. 释放内存后还使用指针

解决:

  • 对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会自动释放它

智能指针注意事项:

  1. 不使用相同的内置指针值初始化 / reset多个智能指针
  2. 不delet get()返回的指针
  3. 不使用get()初始化或reset另一个智能指针
  4. 如果使用get()返回的指针,切记最后一个智能指针销毁后你的指针就无效了

shared_ptr 允许多个指针指向同一对象

对象具有引用计数器,故会出现循环依赖问题。

#include 

make_shared<int>(arg) 返回一个arg初始化的对象
shared_ptr<int>p(obj) 可以拷贝	(递增obj计数器)
p = obj 						(递增obj,递减p)
p.use_count()		返回与p共享对象的智能指针数量
p.unique()			若p.use_count()1返回true,否则false
p.reset(new T(obj))	指向一个新对象,更新引用计数

唯一内存泄漏的情况:循环引用。 当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏
对ap来说:只有调用了A的析构函数,才会去释放它的成员变量bptr。何时会调用A的析构函数呢?就是ap的引用计数为0;对于ap和bp来说,它们都拿着对方的share_ptr(有点类似于死锁的现象),没法使得ab和bp的引用计数为0。那么A和B的对象均无法析构
解决方法是弱引用 weak_ptr

unique_ptr 独占对象

auto_ptr是C++98中的智能指针,有拷贝语义,原对象拷贝给新对象的时候,原对象就会被设置为nullptr,如果再去使用原来的对象的话,那么整个程序就会崩溃掉;unique_ptr会在编译期报错

unique_ptr<int>p(new int(10))	定义时需要绑定到new返回的指针上
不支持拷贝和赋值
p.release()	放弃指针控制权,返回指针并将p置为空
p.reset()	释放对象
p.reset(u)	如果提供了内置指针u,令p指向u,否则将p置为空

将所有权从p1转移给p2
unique_ptr<int>p2(p1.release());

将所有权从p3转移给p2
unique_ptr<int>p3(new int(6));
p2.reset(p3.release());

weak_ptr 弱引用,指向shared_ptr管理的对象

没有引用计数,不控制对象生存期;
指向由shared_ptr管理的对象,不影响引用计数;
用于打破循环引用

weak_ptr<int>p	
p = obj;	obj可以是shared_ptr或weak_ptr,赋值后共享对象
p.reset()	置为空
p.use_count()	与p共享的shared_ptr的数量
p.expired()		若use_count为0,返回true,否则false
p.lock()		如果expired为true,返回空shared_ptr;否则返回指向w对象的shared_ptr

十一、三五原则

三五法则:
C++11前,对拷贝行为的管理通过 “一析两拷” 来控制;
后扩充两个 “移动” 后成为五法则,用于对对象的拷贝、移动、赋值和销毁的控制。

如果一个类
需要一个析构 ==> 需要拷贝构造和拷贝赋值
需要拷贝构造 <==> 需要拷贝赋值 (不一定需要析构)

构造函数

析构函数

释放对象在生存期分配的所有资源

销毁时机:

  1. 变量离开作用域时
  2. 对象被销毁时,成员被销毁
  3. 容器被销毁时,元素被销毁
  4. 动态分配对象被delete时

为什么一个类定义了析构函数就几乎肯定要定义拷贝构造函数和拷贝赋值运算符
如果需要析构函数,类中必然出现了指针类型的成员;有指针类型的成员,我们必须防止浅拷贝问题,拷贝构造函数和赋值操作符是防止浅拷贝问题所必须的

拷贝构造函数

初始化

三种调用情况:(1)赋值初始化;
(2) 对象作为函数参数,值传递; (3)对象作为函数返回值,值传递返回

  1. 唯一形参必须是引用,不一定是const。《清晰透彻:拷贝构造函数的引用和无限递归》
  2. 如果类中成员变量有指针 / 动态内存分配,新对象会指向旧对象指针指向(浅拷贝),此时必须自己定义拷贝构造函数(深拷贝)

拷贝赋值运算符

赋值时
同样地,当成员变量存在指针时需要自己定义拷贝赋值运算符

三部曲(同重载运算符 = ):

  1. 将传入的形参对象拷贝到临时对象
  2. 释放p1指针原来的内存,并开辟新的内存(接收)
  3. 返回自身的引用

移动构造函数

C++11移动构造函数详解
适用情况: 以移动而非深拷贝的方式初始化含有指针成员的类对象(用a初始化b后就不需要了)

  1. 参数是右值引用 Str(Str &&s),只有用一个右值 / 将亡值初始化另一个对象的时候,才会调用
  2. 析构函数记得判断是否为nullptr
  3. move()可以将一个左值变成右值,a = move(b) 调用移动构造
  4. 临时对象初始化的时候优先调用移动构造函数,只有当此类中没有合适的移动构造函数时,才会调用拷贝构造

优点:

拷贝构造 移动构造
先将传入的参数对象进行一次深拷贝,再传给新对象 省略了拷贝开销

移动赋值运算符

同样地,是内存移动而不是拷贝,省略了拷贝开销

十二、OpenMP

《OpenMP和MPI并行模式的区别》
OpenMP是多线程程序设计的编译处理方案。

并行编程框架对比:

  • MPI(message passing interface):多主机联网协作并行计算的接口。 进程级。优点是并行规模的伸缩性强,可处理大规模问题;缺点是如果在单主机上并行计算效率低,因为使用进程间通信来协调,内存开销大、编程复杂、调试麻烦。通信延迟和负载平衡问题;一个进程出问题会造成程序错误;另外对源代码的改动较大
  • OpenCL(Open Computing Language):主要面向异构系统(不同语言实现的系统)的GPU并行编程。
  • OpenMP(Open Multi-Processing):单主机上多核并行计算的工具,主要针对循环并行化。 线程级。使用线程间共享内存的方式协调并行,在多核CPU上效率很高,内存开销小,编程语句简洁,编译器普遍支持;缺点是只能在单台主机上工作,不适用于计算机集群、且不适用于复杂的线程间同步和互斥。

为什么选OpenMP?

  1. 支持C++(C、Fortran),支持不同平台(Linux、Windows)
  2. 不用显示构造回调函数,降低了并行编程的复杂度,改动较小
  3. 解决了线程粒度和负载平衡问题(线程调度)

如何使用?
利用OpenMP预编译指令,并行化处理区域、调度方式等。
也可以在需要的时候加入线程同步及通信机制

代码须满足的条件:

  1. 不能有数据依赖性:即计算依赖于先前的迭代结果
  2. 不涉及线程之间的底层交互
  3. 循环次数需要提前确定,且不能包含break,return,goto语句

《OpenMP并行构造的schedule子句详解》

#pragma omp 指令[子句,[子句]]
指令:
并行域产生 - parallel;
任务分担 - for;(需保证无数据依赖)

子句:
#pragma omp parallel for schedule(static, 16) 指定任务调度
不同参数采用不同调度方式,chunk表示每一块分块的大小(chunk个for子句一起算)
shedule(static,[chunk]):直接获取整块内容;低开销,但分配不均衡(for循环长度不变时用)
shedule(dynamic,[chunk]):执行完毕后自动获取下一个块;高开销,但解决分配不均衡(for循环长度变化的时候使用)
shedule(guide,[chunk]):

#pragma omp parallel for private(i,x,y)
private(<variable list>):指定变量在线程中有自己的私有副本
具体:所有线程不能访问其他的i;所有线程不能给共享的i赋值

5.5 性能优化

1. 线程池

  1. 空间换时间,响应速度更快
  2. 可以反复利用,减少创建销毁次数

2. 触发模式
工作模式:(回调次数的差异)

LT模式(水平触发) ET模式(边沿触发)
阻塞 block、non-block non-block,否则会在最后一次阻塞
特点 就算对就绪的fd不作IO操作,内核还是会继续通知 对fd的就绪只通知一次
优点 减少了epoll事件重复触发的次数,效率比LT高。(必须非阻塞、必须一次读完)
缺点 系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用epoll_wait都会返回
底层原理 检测到buf里面有数据就调用回调 从协议栈检测到接收数据,就调用一次回调

如何设置? 1. 添加fd时设置为非阻塞 2. fd的epoll_event为EPOLLET 3. while读出
特别注意: read返回-1时要判断EAGAIN,此时不退出,下次还可继续读取

本来1w左右并发量,试一试水平和边缘触发的区别

3. 数据复制

  1. 如果内核可以直接处理则没必要复制到应用中
  2. 用户代码内部避免复制,比如在有限状态机中用指针指向buffer的起始位置,而不是复制到另外一个缓冲区;在进程通信时也是,共享内存会避免数据复制,相比管道和消息队列。

4. 上下文切换 锁

  1. 上下文切换:没必要为每一个客户都创建一个工作线程,否则线程切换将占用大量CPU事件;一个线程同时处理多客户链接,当线程数量不大于CPU的数量时,上下文切换很少
  2. 锁:用读写锁代替互斥锁,这样只有在写操作时才会锁,减少内核访问

5.6 底层剖析

socket
《深入理解socket中的recv函数和send函数》

send()仅仅是把应用层buffer的数据拷贝到socket的内核发送缓冲区中,发送是TCP的事情
recv()所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,并返回拷贝的字节数。(注意:是拷贝,不是像read那样取之后,清空接受缓冲区内的数据。)

epoll

epoll_create会创建一个eventpoll的对象,并返回一个与之对应文件描述符;

struct eventpoll {
  spin_lock_t       lock;
  struct mutex      mtx;
  wait_queue_head_t     wq;
  wait_queue_head_t   poll_wait;
  struct list_head    rdllist;   //就绪链表
  struct rb_root      rbr;      //红黑树根节点
  struct epitem      *ovflist;
}

epoll_ctl控制红黑树结点的增删,其中每个结点上保存的是epitem类型的结构体

struct epitem {
  struct rb_node  rbn;
  struct list_head  rdllink;
  struct epitem  *next;
  struct epoll_filefd  ffd;
  int  nwait;
  struct list_head  pwqlist;
  struct eventpoll  *ep;
  struct list_head  fllink;
  struct epoll_event  event;
}

epoll_ctl原理:

  1. 创建并初始化一个strut epitem类型的对象,完成该对象和被监控事件的关联(epoll_event时间参数);
  2. 将epitem加入eventpoll的红黑树;
  3. 将epitem类型的对象加入到被监控事件对应的目标文件的等待列表中,并注册回调函数 ep_poll_callback()(事件就绪时会调用)
  4. ovflist主要是暂态处理,调用ep_poll_callback()回调函数的时候发现eventpoll的ovflist成员不等于EP_UNACTIVE_PTR,说明正在扫描rdllist链表,这时将就绪事件对应的epitem加入到ovflist链表暂存起来,等rdllist链表扫描完再将ovflist链表中的元素移动到rdllist链表;

epoll_wait原理:(回调)
协议栈将数据解析出来触发回调通知epoll,epoll根据四元组+协议这么一个五元组来确定fd,进而在红黑树中找到结点,回调函数执行以下操作:

  1. 通过通过fd找到对应的结点
  2. 把结点加入到就绪队列

一共有5个通知的地方:

  1. 三次握手完成,往全连接队列添加TCB结点后(读事件)
  2. 接收数据回复ACK之后(读事件)
  3. 发送数据收到ACK之后,buf有剩余空间(写事件)
  4. 四次挥手接收FIN回复ACK之后 (读事件)
  5. 接收RST回复ACK之后(EPOLLERR错误事件)

再就是LT和ET的知识点了

十三、设计模式六大原则

《设计模式六大原则》

各种性质

名字 定义 具体使用 优点
1. 单一职责原则 一个类 / 接口只负责单一职责。如果有两个职责且发生变化影响到这两个职责,那就不需要拆分。 可以降低类的复杂度;提升可读性;
降低修改风险
2. 开闭原则(基础设计原则) 软件应该对扩展开放,对修改关闭;即应尽量在不修改原有代码的情况下进行扩展;抽象层是关键,将实现行为移至实现层。 可以提高复用性和维护性
是面向对象开发的要求
3. 里氏代换原则 所有引用基类的地方能使用派生类对象,尽量不要改写,而是实现 父类尽量设计为抽象类或接口,子类所有方法必须在父类中声明 或 必须实现父类中声明的所有方法。
在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象
在一定程度上克服了继承的缺点:子类必须拥有父类的属性和方法,且修改父类时还得考虑子类的修改
4. 依赖倒置原则 要面向接口编程,不要面向实现编程 降低了耦合性、提高了稳定性、可读性和可维护性
5. 接口隔离原则 接口要精简,即客户端不应该依赖那些它不需要的接口 减小接口的粒度,提高灵活性和可维护性
高内聚低耦合,减少代码冗余
6. 迪米特法则 降低耦合,不和不相关的对象联系。 缺点:容易引入太多中介类。因此应该保证结构清晰 降低耦合性
提高可复用性和可扩展性(耦合性低的结果)

十四、项目的软件设计体现在哪里?

  1. 首先,整体采用一个菱形继承的模式来设计(用虚继承来避免二义性和重复存储),比较符合逻辑。
  2. 应用迪米特法则(不和不相关的对象联系),降低耦合度。因为无量纲和有量纲的数据之间交互较少。《如何降低耦合度》、
  3. 可扩展性。在我的理解看来,这一块儿类会自动把读到的物理参数转换为无量纲参数,这样在未来接入流场,需要无量纲速度的时候可以直接处理;因为统一采用无量纲参数对外交互(好比网络字节序统一用大端),所以便于扩展。
  4. 可复用性。输入端采用XML文件解析(调库),输出端按照可视化软件Paraview的读取格式手写了一个输出的函数,可以打包成库,直接在其他数值模拟软件里复用。
  5. 对于最后执行的类采用单例模式的饿汉模式,保证只生成一个实例且提供一个全局访问点。
单例模式三要素:
1.构造函数私有化
2.实例具有静态属性
3.get方法返回静态实例

class Singleton   //实现单例模式的类  
  {  
  private:  
      Singleton() {}  //私有的构造函数       
  public:  
      static Singleton& GetInstance()  // 实例化创建。获得本类实例的唯一全局访问点
      {   
     	  static Singleton instance;  // 实例
          return instance;  
      }  
  }; 
模式 懒汉 饿汉
内容 第一次调用时创建 一开始就创建
优点 节省空间 节省运行时间
缺点 双重锁定式才保证线程安全;另外耗费一定的判断时间 提前占用系统资源,加载耗时
适用情况 第一次调用时创建 一开始就创建
  1. 接口与实现分离原则,函数之间传递信息由各自的场变量指针传递,尽可能避免全局变量和全局函数;
    接口使用的时候,我们可以使用Student指针指向具体的子类对象
    《接口与实现分离》

十五、排序

《912.排序数组》

分类 简单算法 改进算法
交换 冒泡 快速
插入 直接插入 希尔
选择 简单选择
归并 归并
排序方法 平均 最好 最坏 空间 稳定 备注
冒泡排序 O ( n 2 ) O(n^2) O(n2) O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) 1 稳定
简单选择排序 O ( n 2 ) O(n^2) O(n2) 1 稳定 略优于冒泡i
直接插入排序 O ( n 2 ) O(n^2) O(n2) O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) 1 稳定 略优于前二者
希尔排序 O ( n l o g n ) O(nlogn) O(nlogn)~ O ( n 2 ) O(n^2) O(n2) O ( n 1 . 3 ) O(n^1.3) O(n1.3) O ( n 2 ) O(n^2) O(n2) 1
堆排序 O ( n l o g n ) O(nlogn) O(nlogn) 1 不适用个数少
归并排序 O ( n l o g n ) O(nlogn) O(nlogn) O ( n ) O(n) O(n) 稳定
快速排序 O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) O ( n 2 ) O(n^2) O(n2) O ( l o g n ) O(logn) O(logn)~ O ( n ) O(n) O(n)

最好情况: 基本有序时,用简单算法即可
最坏情况: 堆 / 归并
个数少: 用简单算法即可;个数多: 用改进算法



其他零碎知识点

《sizeof和strlen的区别及使用详解》:strlen除了空字符\0不统计外,其余的转义字符都统计

《typedef和define区别》

总结

本文章不定期更新,欢迎讨论交流。

你可能感兴趣的:(C++,c++)