C++——基础学习中的一些重点笔记

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(), '
');

6.类:
  a)一个类分为两大部分,数据与函数(数据可以看做是对象的属性,函数可以看做是对象的行为)。

  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()));


10.深拷贝与浅拷贝:

  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)。

原文地址:https://www.cnblogs.com/gjn135120/p/4020086.html