c++学习笔记

防止头文件重复引用

  1. 其中 #pragma once 和 _Pragma("once") 可算作一类,其特点是编译效率高,但可移植性差(编译器不支持,会发出警告,但不会中断程序的执行);
  2. 而 #ifndef 的特点是可移植性高,编译效率差。读者可根据实际情况,挑选最符合实际需要的解决方案。

当编译器可以识别 #pragma once 时,则整个文件仅被编译一次;反之,即便编译器不识别 #pragma once 指令,此时仍有 #ifndef 在发挥作用。

Static

  1. static无论是全局变量还是局部变量都存储在全局/静态区域,在编译期就为其分配内存,在程序结束时释放。不同的是局部静态变量的初始化,程序会在局部变量所在内存的第一个bit位来表示是否已经初始化。
  2. 函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件中可见,不能被其他文件所用。
  3. 对于被static修饰的类成员变量和成员函数,它们是属于类的,而不是某个对象,所有对象共享一个静态成员。没有this指针。静态类成员函数只能调用静态类成员变量。

Const

  1. const定义的常量超出作用域后,空间会被释放。
  2. 在类中,const变量也不能在类定义处初始化,需要通过构造函数初始化,必须有构造函数。只有Const static可以。
  3. 目的在于防止修改,只读。Const对象只能访问const成员函数,典型的就是写重载。
  4. 函数前加const表示返回值,后边加表示不可修改类成员。mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。

智能指针

C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。

  • 为什么要使用智能指针:我们知道c++的内存管理是让很多人头疼的事,当我们写一个new语句时,一般就会立即把delete语句直接也写了,但是我们不能避免程序还未执行到delete时就跳转了或者在函数中没有执行到最后的delete语句就返回了,如果我们不在每一个可能跳转或者返回的语句前释放资源,就会造成内存泄露。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。

unique_ptr

  • 替代了auto_ptr,auto_ptr是所有权模式,一个auto_ptr指针拥有指向的对象,让另一个auto_ptr等于这个指针,不会报错,但这个对象的拥有权已经让另一个auto_ptr占有,当前的再去访问指向的对象会报错。拥有它指向的对象,无法使两个unique_ptr指向同一个对象。任何时刻只能有一个对象,或者一个函数拥有它。想要交给别人用move,move之后就不用关心它的生命周期了,并且不能再使用它了。unique_ptr<A> p(new C);

shared_ptr

  • 资源可以被多个指针共享,用计数机制保存资源被几个指针共享,use_count查看被几个指针共享,调用release时计数减一,变为0时自动释放资源。但是会出现循环引用。计数器在堆,当shared_ptr<int> b = a;执行后,b对象浅拷贝a对象的计数区指针,然后将计数区的值+1。这样就相当于拷贝赋值出的一组智能指针都指向同一块堆上的数据空间,同时还共享另外一块堆上计数区(这也是叫做shared_ptr的原因)。

weak_ptr

  • 用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。注意的是不用用weak_ptr直接访问对象,要转为share:share_ptr<B> p = pa-> pb_.lock(); p->print();不计数,使用前需要检查其是否为空。

智能指针的内存泄露与解决:

  • Share的循环引用,用weak指针,其类似一个普通指针,但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。

垃圾回收机制

  • 引用计数:引用为零时会释放,存在循环引用产生内存泄露。
  • 标记清除:标记阶段从根出发标记所有可达节点。清除阶段回收未标记的内存块。解决了循环引用,但是当程序占用内存大,对象多,中断过程长。 有的语言把引用计数算法搭配Mark & Sweep 算法构成GC机制。
  • 节点复制:从根节点开始,被引用的对象都会被复制到一个新的存储区域中,剩下的是不再被引用的,即为垃圾,留在原来的存储区域。释放内存时,直接把原来的存储区域释放掉,继续维护新的存储区域即可。过程如图

 

  • 分代回收:而高级GC技术中最重要的一种为分代回收。它的基本思路是这样的:程序中存在大量的这样的对象,它们被分配出来之后很快就会被释放,但如果一个对象分配后相当长的一段时间内都没有被回收,那么极有可能它的生命周期很长,尝试收集它是无用功。为了让GC变得更高效,我们应该对刚诞生不久的对象进行重点扫描,这样就可以回收大部分的垃圾。为了达到这个目的,我们需要依据对象的”年龄“进行分代,刚刚生成不久的对象划分为新生代,而存在时间长的对象划分为老生代,根据实现方式的不同,可以划分为多个代。

    一种回收的实现策略可以是:首先从根开始进行一次常规扫描,扫描过程中如果遇到老生代对象则不进行递归扫描,这样可大大减少扫描次数。这个过程可使用标记清除算法或者复制收集算法。然后,把扫描后残留下来的对象划分到老生代,若是采用标记清除算法,则应该在对象上设置某个标志位标志其年龄;若是采用复制收集,则只需要把新的存储区域内对象设置为老生代就可以了。而实际的实现上,分代回收算法的方案五花八门,常常会融合几种基本算法。

    而其他的改进算法数量非常庞大,但大都基于上述的三种基本算法。

 

 malloc包的垃圾回收机制:当应用程序使用malloc试图从堆上获得内存块时,通常都是以常规方式来调用malloc,而当malloc找不到合适空闲块的时候,它就会去调用垃圾收集器,以回收垃圾到空闲链表。

 

 

 

引用和指针的区别

  1. 指针有自己的一块空间,而引用只是一个别名;
  2. 使用sizeof看一个指针的大小是4或者8,而引用则是被引用对象的大小;
  3. 指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用;
  4. 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
  5. 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
  6. 指针可以有多级指针(**p),而引用至于一级;   
  7. 指针和引用使用++运算符的意义不一样;
  8. 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

 

New与malloc

  1.  new在自由存储区,分配在那由operator new实现细节决定,可以是堆也可以是静态存储区。Malloc在堆。
  2.  new返回对象类型指针,类型安全,malloc返回void*,需要强转,类型不安全。
  3.  内存分配失败,new会抛出异常,而malloc是返回null。
  4.  malloc需要指定大小。
  5.  new会调用构造/析构函数,malloc不会。
  6.  new和delete允许重载。
  7.  new无法重新分配内存,realloc可以重新分配,实现内存的扩充。

New的过程:调用operator new分配内存,如果产生异常会调用delete回收已经分配的内存,然后调用对象构造函数构造对象内容。

Delete:调用析构函数,再调用operator delete释放内存。

Calloc(num,size) realloc重分配。

析构函数

  • 对象结束生命周期,系统自动调用,只能有一个析构函数,不能重载。不能带任何参数,也没有返回值。如果用户不编写,编译器会自己生成一个。很少会有显示调用析构函数,如果在堆中的数据,自定义的析构函数中存在释放内存比如delete操作,显示调用后,内存已经被释放了,但当生命周期结束时,系统会再次析构,会试图清理已经不存在的数据,会引起重复释放堆内存的异常。
  • 类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数。
  • 析构函数通常用虚析构函数,防止内存泄露,基类指针指向子类对象时,如果基类的析构函数不是虚函数,子类不会被调用,就会内存泄露。

虚基类

为了解决多继承时的命名冲突和冗余数据问题,使得在派生类中只保留一份间接基类的成员。虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类。

虚函数

  • 虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针vptr,这个指针指向一个虚函数表vtable,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。虚函数的调用关系:this -> vptr -> vtable ->virtual function
  •  虚函数表是编译时就创建了,编译器把虚函数表的首地址赋给虚指针。一个类有一个虚函数表,一个实例化对象有一个或者多个虚指针,指向虚函数表。
  • 为何静态函数不能是虚函数?static没有this指针,而虚函数就是调用this指针,找到虚指针,用虚指针找到虚函数表,再去找到虚函数的地址调用虚函数,static都没有this指针,无法实现。以多态的视角看,static没办法不同的类有不同的功能,只能重命名,也是多余的。
  • 与纯虚函数的区别:纯虚函数只能在基类,并且不能被实例化,定义一个虚函数表示这个函数没有被实现,必须被子类中被重写,使用虚函数是在于只提供一个接口,比如动物可以派生出老鼠孔雀大象,但是动物这个类不能实例化,比如动物中写一个叫声的函数,最好用纯虚函数。定义虚函数是为了基类能通过指针调用子类的这个函数。

抽象类

  • 有纯虚函数的类就是抽象类,virtual void test() = 0; 不能被实例化,比如一个抽象类是动物,派生出来猴子,老鼠,猴子老鼠可以被实例化,但是动物要是实例化太不合理了,就引入了这个抽象类,抽象类的纯虚函数必须被子类重写,虚函数不用。并且只能作为基类。

模板与多态区别

泛型编程即以一种独立于任何特定类型的方式编写代码。

比如常见的容器都用了模板,vector<int> vector<string>……

template <typename T>

inline T const& Max (T const& a, T const& b)

{

    return a < b ? b:a;

}

模板是静态多态,虚函数是动态多态。

 

重载 重写 重定义

  • 重载是函数名相同,参数和返回类型可以不同。在同一个作用域中。
  • 重写也称为覆盖,上边的重载可以根据不同的参数调用不同的重载函数,而重写是派生类重新定义了基类的虚函数。需要和基类虚函数的参数返回值相同。重写的作用域不同。
  • 重定义也称为隐藏,派生类和基类有一个相同的非虚函数名字,返回值可以不同,派生类隐蔽了基类的函数,也可以理解为发生在继承中的重载。

四种类型转换

  • reinterpret_cast可以用于任意类型的指针之间的转换,对转换的结果不做任何保证
  • dynamic_cast:这种其实也是不被推荐使用的,更多使用static_cast,dynamic本身只能用于存在虚函数的父子关系的强制类型转换,对于指针,转换失败则返回nullptr,对于引用,转换失败会抛出异常
  • const_cast用于移除对象的const。
  • static_caststatic_cast 用于进行比较“自然”和低风险的转换,如整型和浮点型、字符型之间的互相转换。

C++中拷贝赋值函数的形参能否进行值传递?

  • 不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数。如此循环,无法完成拷贝,栈也会满。

STL的构成

  • STL主要由以下几部分组成:容器 迭代器 仿函数 算法 分配器 配接器
  • 他们之间的关系:分配器给容器分配存储空间,算法通过迭代器获取容器中的内容,仿函数可以协助算法完成各种操作,配接器用来套接适配仿函数。 Vector扩容后,把原来的数据给复制到新的存储地。

为什么分.h和.cpp

  • 可以不分,分为了实现软件的模块化,让别人便于使用你的程序,也成起到加密的作用,不用公布源码,相当于提供个接口。实际上用include时,是把这个.h这一行删掉,把.h文件原封不动地给复制过来,有了这个也不用考虑调用顺序了。

C++11新特性

  • auto关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导
  • nullptr关键字:nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。
  • 智能指针:C++11新增了std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理的问题。
  • 右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。避免深拷贝。

手写单例

class Bank{

    private:

        Bank(){

            cout << "create" << endl;

        }

        ~Bank(){

            cout << "delete" << endl;

        }

    public:

        static Bank *GetInstance(){

            static Bank instance;

            return &instance;

        }

};

单例就是只能产生一个实例。不能通过构造函数构造,否则能实例化多个,构造函数需要私有声明。

 

多态

  • 多态有静态多态(重载,模板)在编译时就确定了调用函数的类型。模板
  • 动态多态(虚函数的重写覆盖),在运行时才知道调用哪个函数,动态绑定。虚函数

 

野指针与悬空指针

  • 野指针:指向内存被释放的内存,或者指向没有访问权限的内存的指针。
  • 野指针产生原因:指针变量没有初始化,指针被创建是不是默认的NULL。Delete后,没有把指针指向NULL。指针越界,超出了变量的作用范围。
  • 悬空指针:野指针一般就是不知道指向那的指针,一个指针指向的对象已经被删除,这个指针就是悬空指针。

大小端

  • 大端模式:是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址端。
  • 小端模式,是指数据的高字节保存在内存的高地址中,低位字节保存在在内存的低地址端。

直接读取存放在内存中的十六进制数值,取低位进行值判断

int a = 0x123456;

char p = *(char *)&a;

if(p == 0x56) cout << "xiao" << endl;

else cout << "da" << endl;

初始化列表

  • 初始化引用成员时,初始化const成员是,调用带参数的类的构造函数。编译器会生成一个list,顺序和声明有关。

那些不能用虚函数

  • 类的成员函数中,构造函数(虚表指针在构造时才初始化,如果构造函数是虚函数,连虚指针都没有,更不可能找到虚函数),内联函数(内联是编译时替换,虚函数是运行时动态进行类型确定),静态函数(无this)。
  • 不是类的成员函数,普通函数(无继承),友元函数(无法被继承)。
  •  Const可以修饰虚函数

Inline与define与普通函数

  • Define是在预编译阶段,只是简单的字符串替换,无检查类型。Inline是内联函数,在编译阶段完成,会做类型检查。就是宏会出现二义性,不安全,用inline替代,内连一些短小的函数能够提高速度。在类里定义的成员函数会被默认的认为是指定为内置函数。

内联函数和普通函数的区别:

  • 普通函数:在f1()函数调用普通函数f0()时,将程序执行地址转变为f0的函数地址,执行完f0后,再将程序的执行地址转换为F1的函数地址。这种函数地址的转移操作需要保护现场和函数地址的压栈和出栈等操作,执行完成后恢复现场。
  • 内联函数:在编译阶段,将f0的代码拷贝到f1的指定位置,在f1()函数调用普通函数f0()时,不需要函数地址的转移以及压栈出栈、保护现场等操作。内联后编译器会自己进行一些简单的优化。

内联函数的优点和缺点:

  • 优点:对于一些较小的函数,如果频繁调用,可以将其设计为内联函数,省去了执行函数地址转移等操作,占用系统资源更少,执行效率更高。
  • 缺点:如果调用内联函数的地方太多,就会造成代码膨胀,因为编译器会把每个调用内联函数的位置都拷贝一份函数实现嵌入其中,重复的嵌入。

inline函数原理: 被知乎大佬嘲讽后的一个月,我重新研究了一下内联函数

静态库与动态库

 浅谈静态库和动态库

  1. 动态库后缀是.so;静态库后缀是.a;
  2. 静态库和动态库最大的区别是,静态情况下,把库直接加载到程序中,而动态库连接时,只是保留接口,将动态库和代码段独立,这样可以提高代码的可复用度,和降低程序的耦合性
  3. 静态库体积比较大,编译后执行的程序不需要外部函数的支持;静态库改变之后,程序必须重新编译
  4. 动态库产生的可执行程序比较小,但是需要打包库;动态函数的改变并不影响程序
  5. 静态库的执行速度比动态库快
  • 静态链接缺点就是当静态库更新时,所有用到库的程序都要重新链接。可以按照写代码的函数理解,静态库是将函数里执行的过程放在main函数里,动态库是直接调用函数,前者如果函数执行过程改变,所有都要改,后者不用管main函数,就改函数就行了。

 struct与union的sizeof

C语言中关于sizeof(struct)和sizeof(union)  看了好一会,其实很简单,下边的例子足够。止于union用处,我觉得

 

extern作用

  • 第一,可以实现跨文件访问。
  • 第二,extren C {}可以使用C语言的函数

std::move

std::move基本等同于一个类型转换,static_cast<T&&>(lvalue);
当我们调用push_back的时候,会产生参数的复制,而std::move是转换对象的所有权,没有内存的拷贝,所以会提高利用效率
std::move本质是将一个左值转换成右值引用
通过右值引用模板实现,利用折叠原理将右指经过T &&传递类型保持不变,而左值经过强制转换变成T &&,从而保证模板可以传递任何实参。然后经过static_cast<>进行强制的类型转换返回T&&右值引用,而static_cast之所以能用类型转换,是通过remove_reference<T>::type模板移除T&&, T&的引用,获取具体的类

posted @ 2021-02-22 16:53  philo_zhou  阅读(92)  评论(0编辑  收藏  举报