在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
1.异常处理: a)在try块中,一旦发生错误,立即抛出异常,然后转入catch块中(try块中剩余的代码不会被执行)。 b)如果throw异常不去捕获,会造成程序core dump(异常处理没有对应的代码块,同样会引起core dump)。 c)对于不同类型的异常,可以采用不同的catch块分别对其进行处理,也可以采取统一处理(越是通用的异常处理handle,越应该放到最后)。 d)异常在某函数中没有相应的处理,就向外继续throw(函数剩余的代码不会执行,程序会立刻退出该程序)。 2.传引用: a)C语言中的传值,实际上是在函数调用时,将实参的value拷贝给形参(如果传递的类型较复杂,可能造成较大的开销)。 b)在C++中,我们采用传引用来避免对象复制的开销(既可以修改原对象,又可以避免复制的开销)。 c)如果一个函数的形参为A &a,这意味着该函数不仅可以改变a的值,而且期望改变a的值。 d)const引用表示常亮,是一种保护语义,而非const引用,是一种修改语义。 e)绝对不要返回局部对象的引用或指针。 3.宏函数与内联函数: a)宏函数: 在预处理期间被文本展开。 没有函数调用的开销。 缺点是如果不被调用,编译器就无法为其检查错误(编译器根本就找不到这段代码,因为该段代码预处理期间就消失了)。 b)内联函数: 可以看做高级的宏函数。 在编译期间被内联展开。 没有函数调用的开销。 内联函数需要编译器为其检查语法错误。 4.函数的唯一标示与重载: a)C++中函数的唯一标示——函数签名,不仅包括函数的名字,还包括参数列表(不包括返回值)。 b)构成函数重载的几点要素: 函数名称(包含类名)。 形参表(形参的类型与个数)。 const属性(类的成员属性)。 5.IO流: a)C++中IO有三种状态: bad 、fail 、 eof。以cin为例: 如果输入非法数据,cin的 fail 置为1. 如果输入结束,cin的fail与eof都置为1. b)IO流的修复: 如果是文件结束,那么只需调用clear函数即可。 如果是非法数据,除了clear,还需要清空非法输入(如果不清空,那么数据会一直停留在缓冲区中)。 调用以下函数: cin.ignore(std::numeric_limits < std::streamsize > ::max(), '\n'); 6.类: b)C++类的成员函数,均还有一个隐式的参数,指向调用该函数的对象的指针,即this指针。 c)构造函数也可以重载。 d)初始化式中的语句为变量的初始化,而构造函数块中的语句为变量的赋值。 e)初始化列表中的初始化顺序,与class中变量的定义顺序有关,而与初始化列表中的声明顺序无关。 f)尽可能使用初始化列表代替函数体内的赋值 当class中的成员只能被初始化,而不能被赋值时(例如const成员或引用成员),我们必须使用初始化列表。 当class中的成员的初始化必须由我们手工控制,而不能交给系统默认初始化时(通常原因是该成员没有默认构造函数),我们必须使用初始化列表。 g)class内不写访问标号,默认为private,而struct默认为public。这是二者唯一的区别。 h)C++中的friend声明,是一种单向关系。例如A 声明friend class B表示B是A的friend,可以访问A的private数据,但是反过来则不成立。 7.顺序容器: a)用一个容器去初始化另一个容器,要求类型完全一致(容器类型相同,元素类型相同)。 b)用迭代器范围去初始化容器,只要求迭代器元素与容器元素类型匹配即可(不要求容器类型相同)。 c)凡是传入迭代器作为指定范围的参数,可以使用指针代替。 d)凡是放入vector中的元素,必须要求具备复制与赋值能力。 e)vector迭代器持续有效,除非: 使用者在较小的索引位置插入或删除元素。 由于容量的变化引起的内存重新分配。 f)用erase删除元素记得接收返回值,同时最好使用while循环。 g)vector的几个与容量有关的函数: size(), 表示元素数目。 resize(), 调整元素的数目。 capacity(),表示可容纳数目。 reserve(), 调整容量。 h)vector的内存增长是按照成倍增长。 i)vector与list的区别: vector采用数组实现,list采用链表实现。 vector支持随机访问,list不提供下标 大量增加删除的操作适合使用list。 8.map和set: a)pair不是容器,而是代表一个key-value键值对。 b)map是存储pair对象的容器,只是存储方式与vector不同,map采用的是二叉排序树存储pair,一般是红黑树。 c)map使用下标访问时,如果key不存在,那么会在map中添加一个新的pair,value为默认值。 d)map的key必须具有小于操作符operator<。 e)使用insert插入map元素时,如果失败,则不会更新原来的值。 f)map与set的比较: 二者均使用红黑树实现。 key需要支持<操作。 map侧重于key-value的快速查找。 set侧重于查看元素是否存在。 g)map和set中的元素都无法排序。 9.reverse迭代器: a)在逻辑上,rbegin指向最后一个元素,rend指向第一个元素的前一个位置。 b)在实际实现上,rbegin指向最后一个元素的下一个位置,rend指向第一个元素。 c)reverse迭代器的物理位置比逻辑位置增加了1. d)采用这种实现的好处是:将iterator转化为reverse_iterator之后的区间,与之前的区间恰好相反,但内容相同。 e)reverse迭代器不能用于erase函数。删除的正确方式是: it = string::reverse_iterator(s.erase((++it).base()));
a)含有指针成员变量的类在复制时,有两种选择: 复制指针的值,这样复制完毕后,两个对象指向同一块资源,这叫做浅拷贝。 复制指针指向的资源,复制完毕后,两个对象各自拥有自己的资源,这叫做深拷贝。 b)赋值操作符,需要先释放以前持有的资源,同时必须处理自身赋值的问题。 c)复制构造函数、赋值运算符以及析构函数,称为三法则,一旦提供了其中一个,务必提供另外两个。以string为例: 涉及到深拷贝、浅拷贝问题,所以需要提供拷贝构造函数。 为了保持一致,赋值运算符也应该实现深拷贝。 既然实现了深拷贝,那么必定申请了资源(内存),所以需要析构函数手工释放资源。 d)一个空类,编译器默认提供无参构造函数、拷贝构造函数、赋值运算符以及析构函数,一共四个函数。 e)禁止一个类复制与赋值能力的方法: 将copy函数与赋值运算符设为private。 只声明,不实现。 f)复制和赋值必须保证在程序的语义上具有一致性。 g)如果一个类,不需要复制与赋值,那就禁用这种能力,可以避免大量潜在的bug。 h)如果一个类,实现了像value一样的复制和赋值能力(意味着复制和赋值后,两个对象没有任何关联,或者逻辑上看起来无任何关联),那么就称这个类的对象为值语义;如果类不能复制,或者复制后对象之间的资源归属纠缠不清,那么称为对象语义,或引用语义。 11.操作符的重载: a)A(int a)这样的构造函数实现了一种类型转化能力,加上explicit可以禁用这种转化。 b)+ 或者>、< 、==之类的操作符重载最好采用friend的形式。 c)下标操作符的重载最好提供const和非const的版本。 d)>>操作符的重载,应该注意处理输入失败的情况。 12.智能指针: a)构造函数接收堆内存 b)析构函数释放内存 c)必要时要禁用值语义 d)重载*和->两个操作符 e)智能指针是个对象,但其行为表现的像一种指针,它有三种操作符: . 调用的是智能指针这个对象本身。 * 调用的是解引用出持有的对象。 -> 调用的是持有对象内部的成员 f)常用的智能指针: Boost库提供了scoped_ptr和shared_ptr。 C++11内置了unique_ptr和shared_ptr。 C++98提供了auto_ptr(已经被废弃)。 13.函数模板: a)函数模板可以看做是一种代码产生器,往里面放入具体的类型,得到具体化的函数。 b)模板的编译分为: 实例化之前,先检查模板本身的语法是否正确。 根据函数调用,去实例化代码,产生具体的函数。 c)没有函数调用,就不会去实例化模板代码,在目标文件obj中找不到模板的痕迹。 d)一个非模板函数可以和一个同名的函数模板同时存在,构成重载,同样的两个模板函数可以因为参数不同构成重载。 e)模板函数重载时,选择函数版本的特点: 当条件相同时,优先选择非模板函数 在强制类型转化,与实例化模板之间,优先选择实例化模板。 实例化版本不可行,则去尝试普通函数的转化, 参数是指针时,优先选择指针版本。 总之,尽可能采用最匹配的版本。 f)在模板函数重载中,不要混合使用传值和传引用,尽可能使用传引用。 g)传值和传引用,对于参数来说,本质区别在于是否产生了局部变量; 对于返回值来说,本质区别在于返回时是否产生了临时变量。 14.类模板: a)模板类也类似于代码产生器,根据用户输入的类型不同,产生不同的class。 b)模板类的编译: 检查模板class的自身语法。 根据用户输入的指定类型,去实例化一个模板(注意,不是实例化所有代码,而是仅仅实例化用户调用的部分)。 c)模板的缺点是代码膨胀,编译速度慢,带来的好处是运行速度快。 d)将类模板拆分为.h和.cpp文件,构建时产生了链接错误。原因在于: 模板的调用时机和代码的实例化必须放在同一时期, 编译.cpp时,编译器找不到任何用户调用的代码,所以得到的.o文件为空。 编译main.cpp时,编译器获取用户的调用,了解应该去实例化哪些代码,但是这些代码存在于另一模块,所以推迟到链接期间。 链接期间,由于以上原因,需要链接的代码并没有产生。 e)模板参数不仅可以使类型,还可以为数值,需要注意的是:数值也是类名的一部分,例如Stack<int, 5>和Stack<int, 10>不是同一个类,二者的对象无法相互赋值。 f)在模板代码中编写: T::value_type * p; 编译器将其可能解释为乘法,为了显示的告诉编译器这是定义一个变量,需要加上typename, typename T::value_type * p; g)对于非引用类型的参数,在实参演绎的过程中,会出现从数组到指针的类型转换,也称为衰退。 15.原生数据(POD): a)在C++中,非POD变量经过两个步骤生成: 申请原始内存(字节数组)。 在内存上执行构造函数。 b)POD指的是原生数据,包括int、double等基本数据,以及包含基本数据的结构体(struct、class),但是class或者struct不能包含自定义的构造函数,不能含有虚函数,更不能包含非POD数据。 c)对于POD数据,可以通过mencpy系列函数,直接操控内存达到目的。C语言中的数据都是原生数据。 d)POD数据仅仅申请内存就可以使用,不需要执行特殊的构造工作(可以直接使用malloc)。 e)非POD数据只能使用new,不能使用malloc。 16.继承: a)protected仅限于本类和派生类可以访问。 b)经过public继承,父类中的private、protected、public在子类中的访问权限为:不可访问、protected、public。 c)通过子类对象去调用函数: 父类中的非private函数,可以由子类调用。 子类额外编写的函数,可以正常使用。 子类中含有与父类同名的函数,无论参数列表是否相同,调用的始终都是子类的版本(如果想执行父类的版本,必须显示指定父类的名称)。 d)父类与子类含有同名的函数,那么通过子类对象调用函数,总是调用子类的版本,这叫做子类的函数隐藏了父类的函数。 e)子类对象中含有一个父类的无名参数。 f)构造子类对象时,首先需要调用父类的构造函数,其次是子类的构造函数,析构的顺序与之相反。 g)子类的对象可以赋值给父类的对象,其中子类多余的部分被切除,这叫做对象的切除问题。但是,父类的对象赋值给子类对象是非法的。 h)派生类的构造顺序: 构建基类对象。 构造成员对象。 调用自己的构造函数。 析构顺序与之相反。 i)子类在构造对象时,通过初始化列表,指定如何初始化父类的无名对象。而拷贝构造函数用子类去初始化父类对象,赋值运算符中则是显示调用父类的赋值运算符。 j)public继承,塑造的是一种"is-a"的关系。在继承体系中,从上到下是一种具体化的过程,而从下到上则是抽象、泛化的过程。 k)一个类包含另一个类,叫做"has-a"的关系,也称为类的组合。 l)OOP的第二个性质称为继承,第三个性质动态绑定。 m)基类的指针或者引用可以指向派生类的对象。 n)通过基类指针调用函数: 基类中存在的函数,可以调用。 子类额外添加的函数,不可以。 父子类同名的函数,调用的是父类的版本。 以上的原因是:通过基类指针调用的函数,编译器把基类指针指向的对象视为基类对象。 o)派生类指针可以指向基类指针,这叫做"向上塑形",这是绝对安全的,因为继承体系保证了"is-a"的关系。 然而,基类指针转化为派生类指针则需要强制转化,而且需要人为的保证安全性,"向下塑形"本质上是不安全的。 p)静态绑定:编译器在编译期间根据函数的名字和参数,决定调用哪一段代码,这叫做静态绑定,或早绑定。 动态绑定:编译器在编译期间不确定具体的函数调用,而是把这一时机推迟到运行期间,这叫做动态绑定,或晚绑定。 q)C++中触发动态绑定的条件: virtual(虚函数),基类的指针或者引用指向了派生类的对象。 r)触发多态绑定后,virtual函数的调用不再是编译期间确定,而是到运行期间,根据基类指针指向的对象的实际类型,来确定调用哪一函数。 s)动态绑定的执行流程:运行期间,因为触发了动态绑定,所以先去寻找对象的vptr(虚指针),根据vptr找到虚函数表(vtable),里面存储着虚函数的代码地址,根据vtable找到要执行的函数。 t)子类在继承父类的虚函数的时候,如果对函数体进行了改写,那么子类的虚函数版本会在vtable中覆盖掉父类的版本,这叫做函数的覆盖。 u)虚函数具有继承性,如果子类的同名函数,名字与参数与父类的虚函数相同,且返回值相互兼容,那么子类中的该函数也是虚函数。 v)函数的重载、隐藏和覆盖: 隐藏:凡是不符合函数覆盖的情形,都属于函数的隐藏。 父类中的非虚函数,子类中的函数名字和参数与其一致。 父类中的非虚函数,子类对其参数或返回值做了改动。 父类中的虚函数,但是子类中对其参数做了改动,或者返回值不兼容。 覆盖:触发多态的情形。 父类中的虚函数,子类中的函数名字和参数与其一致,且返回值相互兼容。 w)不要改动从父类继承而来的非virtual函数(不要触发函数的隐藏) 。 x)如果父类中的某函数为虚函数,那么两个选择: 不做任何改动,采用默认实现。 覆盖父类的实现,提供自己的行为。 y)virtual void run() = 0;声明了一个纯虚函数,此函数只有声明,没有实现,包含了纯虚函数的类,称为抽象类。 z)子类在继承抽象类后,必须将其中所有的纯虚函数全部实现,否则仍是一个抽象类。 在继承体系中,应该把基类的析构函数设为virtual。 17.生产者消费者问题: a)互斥是一种竞争关系,同步是一种协作关系。 b)生产者消费者问题需要: 一个互斥锁:保证对缓冲区的互斥访问。 两个Condition:一个是生产者通知消费者取走物品,另一个则是消费者通知生产者可以放入物品。 c)pthread_cond_wait: 首先释放锁,等待,重新抢锁(必须在加锁的条件下才可调用该函数)。 d)pthread_cond_signal通常用来通知资源可用。 e)pthread_cond_broadcast一次通知多个线程,通常用来通知状态的改变。滥用broadcast会导致"惊群"问题。 f)使用pthread_cond_wait必须采用while判断,原因在于: 如果采用if,最多判断一次。 线程A等待数据,阻塞在full上,那么当另一个线程放入产品时,通知A去拿数据,此时另一个线程B抢到锁,直接进入临界区,取走资源.A重新抢到锁,(因为采用的是if,所以不会判断第二次)进去临界区时,已经没有资源。 防止broadcast的干扰,如果获得一个资源,使用broadcast会唤醒所有等待的线程,那么多个线程被唤醒,但最终只有一个能拿到资源,这就是所谓的"惊群效应"。 18.线程类的封装: a)MutexLock和Condition利用了RAII,利用构造函数和析构函数自动完成资源的申请和释放。 RAII:智能指针将对资源的获取放在构造函数中,资源的释放置于析构函数中,这样,当智能指针管理资源时,一旦智能指针对象销毁,资源就可以自动释放,实现了资源的自动化管理,这种技术叫做"资源获取即初始化",即RAII。 b)MutexLock、Conditio和Thread涉及到系统资源,这些类全为不可复制的。 c)线程在默认情况下是joinable(可结合状态),需要手工调用join函数,也可以设置为detachable(分离状态),线程运行完毕自动消亡。 d)Thread类采用static函数作为pthread_create的回调函数。原因在于: 普通成员函数含有一个隐式参数this,所以函数指针类型与(void*)(*)(void *)不匹配。 e)Linux中的线程,本质上是一个轻量级进程,拥有自己唯一的tid,可以编写gettid获取,可以通过syscall(SYS_gettid)获取。 19.类型转化: a)static_cast发生在编译期间,如果转化不通过,那么编译错误,如果编译无问题,那么转化一定成功。static_cast仍具有一定风险,尤其是向下塑形,将基类指针转化为子类指针时,指针可以转化,但是指针未必指向子类对象。 b)dynamic_cast发生在运行期间,用于将基类指针或引用转化为派生类的指针或引用,如果成功,返回正常的指针或引用,如果失败,返回(NULL),或者抛出异常. c)typeid运算符能够识别类型,如果要识别的类型不是class或者不含virtual函数,那么typeid指出静态类型。如果class含有virtual函数,那么typeid在运行期间识别类型。 d)typeid和dynamic_cast称为运行时类型识别(RTTI)。 |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论