C++知识点小结(持续补充)

0、字符串操作:参考1参考2

  • size_t类型
    • x86下,无符号整形
    • x64下,无符号长整形
  • string求子串:
    • 函数签名:string substr(size_t pos =0,size_t len = npos) const;,求从从pos位置开始长度为len的子串,pos默认是0,len默认到string 的结尾
  • 字符串拼接:
    • string中+和+=的效率差距:
      • str = str+a; 是先将右边的两个内容相加得到一个新对象再赋值
      • str+=a;直接将右边的追加到左边的string对象后面,效率可以和append持平
    • snprint
      • 函数签名:int snprint(char* str, size_t, size,const * format, ...);,目标字符串str,拷贝字节数sizeformat格式化字符串,... 可变参
    • strncpy
      • 也可以用于字符串拼接:strncpy(s1+3,s2,strlen(s2)+1),从s1+3的位置开始拷贝字符串
    • strcat
      • 函数签名:char *strncat(char *dest, const char *src, size_t n);,将src追加到dest后面,追加长度为n,一般需要考虑长度为strlen(src)+1,追加到终止符
  • 字符串和数字的转换:
    • to_stringstring to_string(int),将数字常量转换为字符串
    • stoiint stoi(const string& str,size_t* pos=nullptr,int base=10),将base进制的字符串转化为十进制的数字,pos是起始位置,stoi会进行int的范围检查,如果超过范围报错runtime error
    • atoiint atoi(const char* str);,将字符串转化为数字,不会进行范围检查,如果超过范围,就返回的是对应的上下界
    • isdigitint isdigit(int)用于判断字符是否为0~9的数字,是数字返回非0否则返回0,需要引入头文件#include<ctype.h>
  • 字符串拷贝:头文件#include<string.h>
    • strcpy
      • 用于将一个字符串(以'\0'为结束符),复制到另一个字符串中
      • 函数签名char *strcpy(char *destination, const char *source);
      • 需要注意的点:
        • strcpy会给destination,添加结束符;
        • destination传进来的必须是字符数组,如果数组长度不够会发生缓冲溢出
        • 如果src字符串中间出现终止符会导致拷贝不完全
    • strncpy
      • 将源字符的前len字节拷贝到dest所指的内存上,不会主动添加结束符
      • 函数签名:char *strncpy(char *dest, const char *src, size_t len);
      • 需要注意的点:
        • 如果要拷贝终止符,需要将长度设置为strlen(src)+1
        • dest内存长度至少要大于等len
        • 如果src长度小于len,则用NULL填充dest,直到填充完len长度
        • 如果src长度大于len,则只复制srclen个字符,不会添加结束符
    • memcpy
      • 将一段内存的内容按字节拷贝到另一块中,指定拷贝长度,类型不定,不限制于字符串
      • 函数签名void *memcpy(void *destination, const void *source, size_t num);
      • 需要注意的点:
        • 不能用于内存重叠的情况,可能会发生未定义情况
        • 拷贝指定长度,不会遇到'\0'停止
        • destination的长度必须大于等于num;
    • memmove
      • memcpy类似,将一段内存的东西拷贝到另一个内存,但是可以运行目标内存和源内存重叠,底层拷贝需要判断地址大小进行拷贝;
      • 函数签名void *memmove(void *destination, const void *source, size_t num);

1、进程虚拟地址划分:

  • 32位操作系统上,3G用户空间+1G内核空间,从低地址到高地址:
    • .text段->.rodata段->.data段->.bss段->.heap段->.stack段->内核空间
    • .text段:存放指令,只读不写
    • .rodata段:存放字符串常量,只读不写
    • .data段:初始化的全局变量和静态变量(全局或局部),初始化值不为0
    • .bss段:未初始化数据的全局变量和静态变量(全局或局部),或者是初始化为0的全局变量和静态变量(全局或局部)
    • .heap段:new来的数据,程序员分配和回收
    • .stack段:函数的局部变量等,系统分配和回收
  • 不同进程用户空间独立,内核空间共享
  • .stack段高地址向低地址增长,.heap段低地址向高地址增长,这两个分区段之间加载动态连接库(.so/.dll)
  • const修饰的全局变量是存储在.rodata段的,因为是只读属性

2、函数调用队栈的过程

  • 寄存器介绍:
    • eip指令指针:指向下一条即将执行指令的地址
    • ebp基地址指针:用来指向栈底
    • esp栈指针:通常用来指向栈顶
  • 函数调用过程:main栈中调用sum函数,后面补充

3、编译连接

  • 可执行文件产生的四步:预编译、编译、汇编、连接
    • 预编译删除注释,把宏定义和#include全部替换,除了#pragma编译器指令
    • 编译:词法分析,语法分析,代码优化等,生成汇编代码文件
    • 汇编:将汇编代码翻译成机器指令,并生成二进制可重定位文件(.obj文件)
    • 连接:所有的目标文件和静态库文件进行连接:
      • 1、不同文件之间各个段进行合并(.text、.data等),完成符号解析,找到所有未定义符号的定义的位置,这是因为,当前文件可能引用了其他文件的定义的符号
      • 2、符号重定位,将编译阶段没有分配的地址进行分配,就是跑到代码段的指令上给符号地址填成正确的地址,就是如果是.text段的指令,就填写.text段的地址,如果是.data段的数据,就填写.data段的地址
    • 连接分为两种:
      • 静态连接:连接的是静态库(.a/.lib),将静态库(声明和实现)和所有源文件的obj文件进行段合并可执行文件;
      • 动态连接:连接的是动态库(.so/.dll),在运行的时候进行连接,通过声明在找对应动态连接库的实现,heap、stack之间,比静态连接占用空间小更灵活,方便后面的库升级;
  • 可执行文件:windows(PE头的文件)、Linux(ELF文件)

4、形参带默认参数

  • 只要给一个形参默认参数,则后面的所有参数都要给默认参数
  • 默认参数只能给一次,一般建议在函数声明处给,不是在定义的位置

5、内联函数

  • inline标识的函数编译阶段会在调用处全部展开,减少函数调用开销(栈帧的开始和释放是有开销的),有类型检查是安全的
  • 内联成功的函数,不会在符号表产生符号
  • inline只是建议内联,不一定内联成功,比如递归,递归多少次只有运行的时候知道
  • 内联知道release版本起作用,debug没有用
  • 构造函数、析构函数、虚函数声明为内联,在语法上没有问题,
    • 构造和析构声明没有意义,因为编译器并不会对构造和析构进行内联操作,因为构造和析构可能会涉及到内存的开辟和释放比较复杂,而且类中的函数,默认都是内联,再去声明也没什么意义
    • 虚函数声明为内联,通过派生类指针调用的内联虚函数不会产生内联(因为多态在运行时期),而通过对象调用的虚函数会产生内联(不会发生多态),前提是函数比较简单
  • 内联缺点:内联次数太多,可能会导致代码臃肿

6、函数重载:成员函数和非成员函数重载

  • 重载条件:作用域相同(优先调用作用域近的)、函数名相同参数列表不同(参数类型不同、参数个数不同、参数列表顺序不同),编译的时候会根据传入的实参选择重载版本
  • C++支持重载:是因为C++函数产生符号是由函数名+参数列表构成的,C中只有函数名决定
  • 在CPP文件中,可以通过extern "C"来保证编译规则是C的规则
  • 普通函数重载:
    • 需要注意默认参数导致的函数调用二义性
  • 成员函数重载:
    • 普通成员方法虚函数方法之间是重定义,virtual关键字不是重载条件
    • 静态成员方法普通方法之间是可以重载的,因为静态方法不需要接收对象指针,但是必须明面上的参数列表要不一样
    • 带const属性的只读方法普通方法是可以重载的,因为接收对象指针的形参类型不一样
    • const修饰的常对象,调用的方法只能是常方法,因为普通的this指针,不能接收常指针

7、const:const、指针、引用

  • C++中const修饰的量是常量,不能作为左值,初始化后不能被修改,不能再次出现在等号左边,不可以将常量的地址泄露给普通指针
  • C和C++的const
    • C中的const:
      • const修饰的量叫常变量,不是常量,可以不用初始化
      • 虽然也也不能作为左值,但是可以通过内存进行修改值
      • 因为是常变量,所以不能用来初始化数组
    • C++中的const
      • const修饰的变量必须初始化,const修饰的量叫常量(只读属性):

        const int b =200; //b是常量,当凡出现b的地方都会被替换为200
        // 依旧可以通过强转指针,修改内存的值,但是不会影响b被200替换
        
      • 因为是常量所以可以用来初始化数组

        int  arr[b]={};// b被200替换,直接初始化数组
        
      • 如果用变量初始化const修饰的变量,则const修饰的量叫常变量,与C的规则等同

8、const和指针

  • const修饰的量是常量,不能直接修改的,不能把常量的地址泄露给引用或者是指针
  • const和一级指针
    • const int *p = &a; 等价于int const* p = &a;
      • 可以修改p指向的内存的值,但是不能改变*p 的值
      • 不管a是常量还是变量
    • int *const p = &a;
      • p指向的地址不可以改变,*p的值可以改变
    • const int *const p = &a;
      • 既不能修改指针的指向p,也不可以修改值 *p
    • TIP:下面表示的是类型
      • int * <= const int *,错误的,不可以将整型常量地址,泄露给指针
      • const int * <= int *, 可以将普通常量地址赋值给const int *
      • int * aint * const b 是同类型,都是int *类型
  • const 和二级指针
    • 二级指针:
      • int a = 10; int *p = &a; int **q = &p
      • q存的是p的地址,*q是p的值,也就是a的地址,**q就是a的值
    • TIP:
      • int** <= const int ** 错误
      • const int ** <= int ** 错误
      • int ** <= int * const * 错误
      • int * const* <= int ** 正确

9、指针和引用

  • 引用必须初始化,指针可以不初始化,引用比指针安全,指针存在野指针的情况
  • 作为函数参数传入,引用比指针方便,指针需要解引用
  • 引用只有一级引用,指针可以有多级
  • 通过引用修改内存的值,和通过指针解引用修改内存的值,指令操作是一样的
  • 左值引用和右值引用:
    • 左值引用:int c = 10; int &b = c; 只能引用左值,不能引用右值
    • 右值引用: int && b = 10; 右值引用只能引用右值(没内存没名字),先创建一个临时量存储10,将临时量的地址给右值引用b,不能引用左值;右值引用变量b本身是个左值,对b的引用需要用左值引用
    • 常量左值引用:const int & b = 10; 可以引用右值,但是这个b就只能是只读状态;先创建临时量保存10,再用b引用临时量;

10、new和malloc

  • newmalloc的区别:

    • malloc是按字节开辟内存;new是开辟内存需要指定类型(底层依旧是malloc)
    • malloc开辟内存是返回void*new返回的类型对应的指针
    • malloc只负责内存开辟,new不仅开辟内存,还可以进行数据的初始化new int(20); new int[20]();
    • malloc开辟内存失败返回nullptrnew抛出bad_alloc类型的异常
  • deletefree的区别:

    • delete是先析构free
    • 对于整型这样堆区上的变量而言,delete和free没有区别
  • 关于new、new[]和delete、delete[]能不能混用的问题混用问题

    • 对于普通的编译器内置类型,比如int、double之类的,new[]/delete,new/delete[]可以混用,因为只涉及内存开辟和释放

    • 对于自定义的数据类型,不能混用,new[]开辟内存构造对象,delete,虽然会删除内存,但是只会调用一次析构,导致其他对象的没有被析构,如果对象有指向外部的资源,可能会导致内存泄露

    • 如果new开辟内存构造对象,delete[],会从地址起始的前面四个字节寻找对象个数(未知的),可能会进行很多次析构,具体看编译器;

    • 通过new[]开辟对象数组的时候(普通内置类型不会多出四个字节,被编译器优化掉了),会多出四个字节用来记录对象的个数,多出的四个字节在指针地址的前面,delete[]会先访问前面的四个字节,知道有多少个对象存在,从而在删除内存的时候调用调用每个对象的析构函数:

      class Test
      {
      public:
          Test(int data = 10) { cout << "Test()" << endl; }
          ~Test() { cout << "~Test()" << endl; }
      private:
          //int* ptr;
          int ma;
      };
      int main()
      {
      	Test *p = new Test[5];
      	delete p; //只会返回第一个对象的内存,导致还有四个对象的内存没有被释放
      	/*
      		delete p;会让编译器觉得p指向的只是一个对象
      	*/
      	Test *p = new Test();
      	delete[] p; //会让编译器觉得p指向的是数组,从p-4的内存中寻找数组的个数
      	return 0;
      }
      
  • STL空间配置器问题:

    • 将new和delete的构造进行的分割,只有单独的内存开辟和内存释放,对象构造和对象析构
    • 是为了避免不必要容器空间底层不必要的对象构造和析构,如果不分离,你在vector容器里面开辟10的空间,空间元素是对象,就会进行10次无意义的构造,但是这些空间位置你并没有使用,所以只能是单纯的分配空间,只有在push_back对象的时候,才进行对象的构造

11、类和对象、this指针

  • OOP语言四大特征:封装、继承、多态、抽象;

  • 一个类可以产生很多对象,每个对象都有自己的成员变量,但是不同对象共用一套成员方法,这套成员方法通过this指针来区分不同对象的成员

  • 类中实现的方法自动处理为内联函数,内外定义的成员方法,就是普通函数,可以通过加inline变成内联

  • 类的大小只和成员变量有关,和成员函数无关

  • this指针:指向当前对象实例

    • 除了静态成员方法,所有的成员方法通过编译都会增加一个this指针,来接收对象的地址:

      class A{void init(int _m);} => class A{void init(A *this,int _m);}
      
    • this指针并不是对象的一部分,不影响对象大小,作用域在类内部,this指针一般在函数开始前构造,函数结束后销毁

    • thiscall:仅用于成员函数,被调用者维护栈(自己),从右向左压栈,this指针通过ecx寄存器传给被调用者,不是通过堆栈传递

    • 在析构函数里面调用delete this,会无限递归调用析构,栈溢出,程序崩溃

  • 访问限定符:

    • public:不管是成员函数还是成员方法,可以通过对象在类外调用
    • private:不管是成员函数还是成员方法,只能通过类内方法调用,一般是通过类的public方法访问private成员变量;可以在类中将类外方法设置为友元(friend),就可以在类外通过对象访问类的私有成员
    • protected:主要用于派生继承中,同样是类外不能访问,但是派生类可以访问
  • 普通成员变量,每个对象都私有一份数据,静态成员变量,所有对象共用一份,存放在数据段

  • 普通成员函数,编译器会自动添加this指针, 常方法的this指针会多处const属性,只能读不能写,但是可以修改mutable修饰的成员变量

  • 静态成员方法只能访问静态成员变量和静态成员方法,因为这些是不依赖对象的

  • 静态成员方法必须在类内声明,在类外定义,因为不占用类的内存

  • 指向成员对象或者成员函数的指针,需要添加作用域,并解引用需要依赖于对象,静态成员变量和静态成员方法就不需要

12、析构函数和构造函数

  • 构造函数:定义对象的时候,自动调用,可以重载(参数列表不同)
    • 如果没有显示的定义构造函数,编译器会提供:
      • 默认构造函数(无参构造):空函数,如果自己定义了构造函数,就不会产生默认构造
      • 拷贝构造:浅拷贝,拷贝构造必须是引用传递,因为用已有对象进行拷贝构造会导致拷贝构造函数的无穷递归
      • 移动构造(C++11后):对象资源转移
    • explicit关键字:会禁止隐式的对象生成,作用于普通带参构造,如比,=右边的整形无法通过调用带参构造进行隐式转换;
    • C++编译器对于对象构造的优化:用临时对象产生新对象的时候,不会产生临时对象,而是直接构造新对象
    • 构造函数不能是虚函数,编译器会报错,不允许:
      • 一般定义子类对象,vfptr先指向父类的虚函数表,在父类构造完成后,子类的vfptr指向自己的虚函数表,如果构造函数是虚函数,调用构造函数的时候就需要通过vfptr去找对应的虚函数地址,也就是说调用构造函数之前,虚函数指针和虚函数表都没有完成初始化,所以是不被允许的,并且析构函数是创建对象编译器主动调用的,不是我们通过父类指针调用的
  • 析构函数:不带参数,只能有一个析构,析构完了,对象就不存在了
    • 析构函数一般可以在类外自己调用,但不建议,这不安全,可能导致内存释放后再重复使用
    • 析构的顺序和构造的顺序是相反的,临时对象出语句自动析构
  • 构造和析构顺序:
    • 构造:基类构造(继承顺序)->成员类构造(定义顺序)->派生类构造
    • 析构 :与构造相反,派生类析构->成员类析构->基类析构

13、深浅拷贝问题

  • 编译器提供的默认拷贝构造是浅拷贝,做的是内存拷贝,就是将内容完成复制一遍
  • 如果对象内部有指向堆区的资源,则浅拷贝会导致多个指针指向同一个内存资源,当析构的时候会导致内存的重复释放
  • 不只是拷贝构造,编译器默认的赋值重载也是浅拷贝

14、临时对象问题

  • 临时对象产生的三种情况
    • 以值传递的方式给函数传递参数:用引用传递
    • 类型转换生成的临时对象/隐式类型状态以保证函数调用成功:可以通过关键字explicit避免隐式转换,或者是直接将临时对象用于对象初始化
    • 函数返回对象的时候(被编译器优化掉了,不会有额外的临时对象产生) :用初始化的方式接收临时对象,临时对象会被优化掉,如果是用赋值接收,临时对象不会被优化

15、迭代器

  • 类里面实现的嵌套类,底层是对指针的封装,都容器元素元素的访问能像使用指针一样,不需要知道容器底层的数据结构,直接提供一套统一方法容器的方法

  • 前置++和后置++:

    后置++int tmep = a; //有中间量的产生,如果是对象重载后置++,就可能产生临时对象
    a+=1;
    return temp;
    前置++
    a+=1;
    return a;
    // 不管是普通的++,还是迭代器的++,前置的效率都要比后置的高点,
    // 因为后置的有中间变量的产生
    
  • 迭代器失效问题:

    • 容器调用erase方法后,从当前位置到容器末尾元素的迭代器全部失效
    • 容器调用insert方法之后,从当前位置到容器末尾位置的迭代器全部失效
    • insert,引起的容器扩容,内存空间的重新开辟,原来的迭代器就全部失效了,所以需要再迭代器赋值之后避免插入元素;
    • 迭代器失效后,对插入/删除的迭代器进行更新操作,用insert和erase的返回值

多态和继承:继承和多态多重继承和多继承

  • 继承的本质:代码复用,子类继承父类,保留父类的结构

  • 多态的本质:接口复用,通过父类虚函数实现一个接口,在不同的子类中进行覆盖,有不同的用法

    • 静态(编译时期)多态:函数重载、模板实例化
    • 动态(运行时期)多态:继承结构中,基类指针指向子类对象,通过指针调用子类中的同名覆盖方法(虚函数),基类指针指向哪个派生类对象,就调用哪个派生类对象的覆盖方法,称之为多态
    • 多态底层是通过动态绑定来实现的,基类指针指向谁,就会访问谁的vfptr,进而访问对应的vftable,从虚函数表中调用的当然是对应派生类对象的方法
  • 继承关系: 子类会将父类的东西全部继承过来,只是访问限定不同,内存布局是基类的成员在前面,再是子类成员

    • 子类public继承父类:父类public->子类public父类protected->子类protected父类private->子类只能继承但不可以见(无法访问)
    • 子类protected继承父类:父类public->子类protected父类protected->子类protected父类private->子类只能继承但不可以见
    • private继承:父类public->子类private父类protected->子类private父类private->子类只能继承但不可以见
  • C++中struct的继承方式默认是public继承,class的继承方式默认是private继承

  • 派生类的构造:

    • 派生类调用基类的构造函数初始化,初始从基类继承过来的成员
    • 派生类再调用自己的构造函数、初始化自己特有的成员
    • …派生类声明周期到了
    • 调用派生类的析构,释放派生类成员占用的外部资源
    • 调用基类的析构,释放派生类内存中,从基类继承来的成员占用的外部资源
    • TIP:派生不能直接初始化继承过来的成员变量,只能通过调用基类的构造函数来初始化
  • 重载、覆盖、隐藏:

    • 重载关系:必须在相同的作用域中,函数名相同,参数列表不同
    • 隐藏关系:继承结构中,派生类会将基类的同名成员(成员变量和成员方法)隐藏调用了,但是还是实际存在的,只是作用域不同,可以通过添加作用域来访问父类的成员
    • 覆盖关系:基类和派生类的方法,返回值、函数名、参数列表都相同,如果基类的方法是虚函数,派生的方法就自动变成虚函数,他们之间就是覆盖关系,如果父类不是虚函数就是隐藏关系
  • 派生类和基类的类型转换:e.g., 基类Base,派生类Derive

    • 继承关系中,默认支持从派生类到基类的转换,因为子类包含父类所有的东西,可以当成父类进行给父类赋值,但是父类只能调用父类的成员,而反之不行,父类不能当子类使用
    • 将子类对象地址赋值给父类指针,类型从下到上,可以进行,如果是将父类对象地址赋值子类指针,由于子类内存比父类大,解引用可能会出现内存非法访问的问题;
    Base B;
    Derive D;
    B = D; //默认支持,从派生类对象转到基类对象
    //D = B; //不支持,不能从基类转到派生类
    
    Base *pB = &D; //支持,从派生类Derive* D到 Base *pB的转换
    // 虽然父类指针指向子类,但是通过pB只能调用父类的方法
    /*
    基类的指针,默认只能访问基类的成员,若想要访问子类成员需要将pB强转为子类指针
    (Derive*)pB
    */
    
    //Derive* pD = &b; // 不支持从 基类 到 派生类 的转型
    // 因为子类的指针指向的子类内存比基类的内存要大,可能会出现内存非法访问,不安全
    
    
  • 虚函数、静态绑定和动态绑定:

    • 提前注意点:
      Derive d(10);
      Base* pb = &d;
      pb->show();	
      delete pb;
      **关于show()有三点解释**:
      - 如果show定义在子类Derive中,则父类指针pb访问不到
      - 如果show定义在父类Base中,且非虚函数,直接通过地址调用(父类),是静态绑定
       						 且如果子类有同名隐藏,依旧调用父类的方法
      - 如果show定义在父类Base中,是虚函数,则运行时,通过子类对象的vfptr来间接
      						 访问子类的虚函数表,进行调用,是动态绑定
      **关于delete pb;的解释**:
      - 如果父类的析构函数不写成虚函数,delete父类指针,只会调用父类的析构,
      						会导致子类的析构无法调用,因为是静态绑定
      - 如果父类的析构函数是虚函数,delete父类指针的时候,析构函数的调用是动态绑定,
      						通过vfptr调用的就是子类的析构函数
      - 关于父类指针指向的起始地址问题:
      	父类指针指向子类,指针永远指向子类中,**父类数据的起始地址** 
      
    • 虚函数:
      • 一个类添加了虚函数、编译阶段会给这个类生成一个vftable虚函数表,vftable中主要存储得是RTTI指针虚函数地址(函数入口地址):
        • 编译阶段生成虚函数表,这个表会被加载到内存的.rodata段,只读数据段
        • RTTI指针指向的是运行时类型字符串,父类的话,就是"Base";
        • 虚函数地址的是有顺序的,是根据类里面定义的顺序存放的
      • 一个类添加了虚函数、类的对象内存最开始(前四个字节)的地方就会多存储一个vfptr指针,指向类的虚函数表vftable;
      • 虚函数不影响对象的内存大小,只影响虚函数表的大小
      • 一个类定义多个对象,多个对象共用一个虚函数表,一个类一个虚函数表,编译时期,派生类会生成自己的虚函数表,如果派生类重写了父类的虚函数,虚函数表中的虚函数地址也会被覆盖
      • 虚析构:
        • 基类的指针,指向new出来的派生类对象的时候, 对指针进行delete,如果父类析构不是虚函数,就是静态绑定,就会导致子类的析构函数得不到调用,所以一般在继承结构中,将父类的析构设置为虚函数;
      • TIP:
        • 虚函数存在.text段,虚函数地址存放在虚函数表中,虚函数表存在.rodata段,虚函数表的地址存放在虚函数指针中,虚函数指针存放在对象内存中,所以虚函数调用必须依赖对象
        • 所以虚函数的实现必须依赖对象,且是可以调用的,static方法不能实现,构造函数不能实现
        • 虚函数表在编译时期初始化生成,相同的类共用同一个虚函数表,虚函数指针,在是对象实例化,调用构造函数的时候进行初始化,指向当前类的虚函数表
        • 构造函数中调用任何的虚函数都是静态绑定,因为要先生成父类,再生成子类,子类存在需要等构造函数完成
        • 父类析构函数调用虚函数,子类已经析构了,这个时候调用的也是父类的虚函数
    • 静态绑定和动态绑定:Base *pB = &D; 父类指针指向子类
      • 静态绑定针对的是普通函数
        • 在编译阶段,就确定了调用函数的地址,父类的指针只能调用父类的普通方法
      • 动态绑定针对的是虚函数:需要运行的时候根据vfptrvftable中寻址
        • 因为是虚函数,所以子类可能会重写,需要在运行的时候去根据虚函数指针来调用
        • 如果子类重写了方法,则子类的虚函数的地址,会在虚函数表中对父类的虚函数地址进行覆盖,运行的时候调用的就是子类的虚函数
        • 如果父类指针,指向的是子类对象,则vfptr指向的虚函数表是子类的,RTTI指针标识的是子类类型,反之则是则是父类类型
    • 问题:虚函数的调用一定是动态绑定?
      • 在构造函数中调用虚函数,是静态绑定
      • 不通过引用或者指针调用虚函数,也是静态绑定,因为不会访问到虚函数指针,动态绑定必须用指针或者引用,指针指向哪个对象,就访问哪个对象的虚函数指针,进而访问对应的虚函数表
    • 抽象类:
      • 拥有纯虚函数的类叫抽象类,抽象类不可以再进行实例化,但是定义指针和引用,可以说就是完全是为了多态来实现的,子类必须改写这个纯虚函数;
      • 纯虚函数:virtual void bark()=0
      • 析构函数是虚函数同理
    • 多态注意事项:
      • 不同子类对象中的虚函数指针可以通过地址交换,从而可以导致动态绑定虚函数调用混乱;
      • 父类指针指向子类对象,编译期间只能看到父类虚函数的情况:比如参数入栈的值、这个虚函数的访问限定();这些都和子类中改写的情况没有关系;
      • 虚函数表写入虚函数指针的时机是构造函数栈帧准备好后,代码执行前
    • 虚继承:
      • virtual:修饰成员方法就是虚函数,修饰继承方式,就是虚继承,被继承的类就是虚基类class B : virtual public A{};其中A就是虚基类

      • 如果类B虚继承类A,则类B中会多出一个虚基类指针vbptr,指向虚基类表vbtable,虚基类A中的数据会被放到类B内存的后面:

        正常继承         虚继承
        A::vfptr	   vbptr
           ma	 	   mb
        mb	           A::vfptr
        	              ma
        
      • 虚基类表vbtable编译时期生成,运行的时候放到数据段(.rodata段),存放的是虚基类数据在派生类中距离vbptr的偏移量

      • 需要考虑的点:

        • 子类虚继承父类,父类指针指向堆上的子类对象,指向的地址是从父类的vfptr开始的,而windows释放父类指针指向的内存也是从vfptr开始的,程序会中断,Linux g++释放内存是从vbptr开始的
        • 如果虚继承不涉及堆区的对象,则没有问题,因为没有手动delete内存的操作,如果涉及到了堆内存,可能会导致开辟的地址和释放的地址不一致,导致崩溃
        • 构造函数,虚基类的构造优先级大于普通的函数
    • 多重继承-菱形继承问题:
      • 派生类存在成员变量重复、基类构造和析构多次调用
      • 通过虚继承可以解决,对内存重复的间接基类做虚继承处理

C++11汇总:

  • nullptrnullptr和NULL的区别

    • C++中NULL宏定义出来的是0,为了区分0和空指针就有了nullptr
    • C中NULL宏定义出来的是(void*)0
  • final/override/=default/=delete

    • fianl:修饰一个类,表示这个类不能被继承,修饰一个虚函数,表示这个虚函数不可以被子类重写
    • override:显示的标记子类中需要重写的虚函数,比较直观的知道这个方法是要重写的,由于编码错误导致不是重写(函数签名不同,父类没有这个方法),编译器会报错
    • =default:修饰构造、析构、拷贝构造、移动构造、赋值重载,让编码进行默认的实现
    • =delete:修饰构造、析构、拷贝构造、移动构造、赋值重载,让编译器标记delete,不可以被调用
  • bind绑定器(bind、function、lambda):

    • 弥补了bind1st 和 bind2nd的局限(只能作用于二元函数对象),可以将对多元函数对象进行绑定参数,并且还可以作用于普通函数、函数指针、lambda表达式等可以用来调用的实体,用来绑定参数并返回一个新的函数对象,底层的函数类型会随着参数的绑定进行改变;也可以使用占位符(命名空间placeholders),不进行参数绑定,最多可以有20个,在实际用到的时候传入参数;
  • function函数对象需要修改和补充没有彻底搞清楚function源码刨析

    • 是个模板类,可以接收bind返回的函数对象、lambda匿名函数对象、普通函数等一些可以调用的目标实体,相当于是进行函数类型保存并封装,可以类似于函数指针的使用,但是底层调用的也是operator()重载;lambda表达式和bind返回的函数对象都只能作用于当前语句,function就解决了他们的问题,可以保存它们的类型并多次使用;一般是function和配合bind完成回调函数的设置;
  • lambda匿名函数对象

    • 语法是[中括号捕获外部变量](小括号参数列表)->右箭头返回值类型{花括号函数体},底层是编译器实现的匿名函数对象,通过小括号运行符重载实现的,参数列表不带mutale关键字的话,底层的operator重载默认是只读属性(const修饰的函数);比函数对象轻量,不用定义类;对外部变量的捕获可以是引用、也可以是值传递;
  • 智能指针不带引用计数的智能指针带引用计数的智能指针

    • 裸指针的使用,容易忘记delete,所以利用栈对象出作用域的特性管理裸指针,防止内存泄露,智能指针底层提供了->运算符重载和*运算符重载,能像正常指针来使用
    • auto_ptr:
      • 独占式的管理内存资源,永远只有最后一个auto_ptr管理资源
      • 底层只有裸指针和普通的拷贝构造和赋值重载,资源的转移,在代码复杂的时候不容易看出来,容易引起编码上的安全问题,使用已经转移资源的空指针;
    • unique_ptr:
      • 也是独占式的管理内存资源,相比于auto_ptr,删除了普通的拷贝构造和赋值重载,提供右值版本的,可以将资源转移摆在明面上,就是正常的左值必须通过std::move()才能进行资源转移,直接传左值编译会报错
    • shared_ptr
      • 共享式的管理的内存资源,底层有个内存资源计数器,每次拷贝或者赋值的时候,资源加1,每次有shared_ptr被释放的时候,资源减1,当资源数为0 的时候,释放资源;
      • 使用shared_ptr时候需要注意不能用裸指针重复赋值,会导致内存资源重复释放,因为资源计数器都不一样了,应该优先使用make_shared比较安全,通过make_shared构造一个shared_ptr对象,从而可以调用拷贝构造进行构造;
      • 使用get方法获取底层裸指针,要注意资源的安全
    • weak_ptr
      • 和share_ptr搭配使用,不改变引用计数,用来观测shared_ptr管理的资源生命周期,没有提供->运算符重载和*运算符重载,如果想使用资源可以通过lock()方法来将自己提升为shared_ptr,提升成功的前提是,shared_ptr管理的资源还存在(use_count>0);
    • 循环引用问题:不同的类里面定义了指向对方shared_ptr, 彼此相互引用,引用计数增加,导致堆上对象和类里面的智能指针共存,最后都不能被释放导致资源泄露,可以通过weak_ptr来解决,weak_ptr作为资源观测不会改变引用计数;
    • 自定义删除器问题:用智能指针管理不能正常delete的类型,比如文件句柄socket等,需要自己定义删除器
  • 移动语义std::move 和完美转发std::forwad

    • 左值右值

      • 左值:有名字、有内存、值可以修改
      • 右值: 没名字、没内存
    • 引用折叠:& + && = &&& + && = &&

    • int &&a = std::move(b):a的属性本身就是左值

    • std::move:可以将左值转换为右值类型,用于匹配右值拷贝和右值赋值,进行资源的转移,底层是通过引用折叠接收左右值,并将他们通过static_cast强转为对应右值

    • std::forwad:由于模板推演可以引起引用折叠,所以可以将左值和右值的引用的匹配代码统一写成右值引用,并且可以同时接收左值和右值,然后forward可以根据模板的类型推演,将右值引用变量转化为传入的参数的类型,如果是传入的是右值,就转成右值,传入的是左值就转成左值,这样不仅保持了右值属性的变量在函数转移中途属性不会变,并且用统一的风格简化代码

      template<typename Ty>
      void construct(T* p, Ty&& val)  //  Ty 是用来匹配左右值的
      {
      /*int a = 0;
      	比如传入的是左值a,则Ty是int &, 且Ty&& val =>int& &&val=>int &val
      	如果传入的是右值std::move(a),则Ty是int&&, 且Ty&& val =>int&& &&val
      	int&& &&val => int &&val
      */
          new (p) T(std::forward<Ty>(val));// 定位new,可以匹配左右值构造
          //forward可以根据Ty的类型将val转成传入进来的左右值类型
      }
      // 如果没有forward,可能上面的左右值匹配需要写两份代码
      void construct(T* p, const T& val) //  接受左值
      {
          cout << "左值构造" << endl;
          new (p) T(val);// 定位new
      }
      void construct(T* p, T&& val) // 接受右值
      {
          cout << "右值构造" << endl;
          // val虽然用来接收右值,但是本质是左值
          new (p) T(std::move(val));// 定位new 
      }
      
  • 四种强制转型类型强转知乎五种类型强转

    • C中的强转:C中的强转int a = (int)b;, 将变量b的类型强制转换为int型,与C的相比C++提出了4种强转,C++风格的可以更清晰的知道这样的强转是在干嘛;

    • const_cast

      • 正常来说,不能将const修饰的变量地址泄露给普通指针或者引用,但是可以通过const_cast移除const属性来强转为指针或者引用、此外也可以移除volatile属性

      • 用于去掉变量的const属性,并强转为指针对应的指针或者引用,其实就相当于去掉指针或者引用的const属性

        const int a = 10;
        double* p1 = (double*) & a;
        int* p2 = const_cast<int*>(&a); //需要保证&a的类型和<int*>里面的类型要一致
        const int g = 20; 
        int &h = const_cast<int &>(g);//去掉const引用const属性
        
      • int* p1 = (int*) & a;int* p2 = const_cast<int*>(&a); 在汇编层面是一样的,但是前者转换的类型不限,而后者只能需要保持int*的属性,也就是去掉const之后的指针类型要保持一致

    • static_cast:

      • 提供编译器认为安全的类型转换,也就是基本上所有的都能转换,但是转换之间是需要有关联,并且要编译器觉得是安全的(不同类型的指针之间不能转换,因为内存不一致容易出现内存非法访问)
      • 可以用于:
        • 类层次结构的强转,子类转向父类是安全的,父类指针可以转子类指针,但是没有动态类型检查,需要自己保证安全
        • 基本数据类型的强转(long、char、int等)
        • 指针类型的转换,不能之间转,需要通过void*作为在中间量
      • 不可以用于:
        • 不能转换expresssion 的const或者volatile属性
        • 不能将实列父类转成子类,因为编译器默认无关联
    • reinterpret_cast:

      • 类似于C风格的强制类型转换,但是安全性不高,底层是比特位的拷贝

      • 语法:reinterpret_cast<tyep_id>(expression); , type_idexpression需要保证至少有一个是指针或者引用

      • 主要用途:

        • 改变指针或者引用的类型,最主要的是不同类型的指针或者引用之间直接的转换(弥补static_cast),但是不安全
        • 将指针或者引用转换为一个足够长度的整型,就是说整型必须和系统指针内存大小相匹配
        • 将整型转为指针或者引用类型
         int* p4 = nullptr;
         double* b2 = static_cast<double*>(p4); //不成立,b2解引用指向的是8字节内存,不安全
         double* b2 = reinterpret_cast<double*>(p4);//成立,底层是C的强转
        
    • dynamic_cast:

      • 其他的强转是用于编译时期的转换,而这个主要用于继承结构中,可以支持RTTI类型识别的上下转换(运行时类型检查)

      • 父类指针指向子类,可以将父类指针转换为对应的子类类型,如果类型对上就是转换成功,如果失败,转换返回的就是nullptr

        class Base // 抽象类
        {
        public:
            virtual void func() = 0;
        };
        class Derive1 :public Base
        {
        public:
            void func() { cout << "call Derive-1::func" << endl; }
        };
        class Derive2 :public Base
        {
        public:
            void func() { cout << "call Derive-2::func" << endl; }
            // Derive2 实现新功能的API接口函数
            void derive02func() { cout << "call derive02func::func" << endl; }
        };
        void show(Base *p)
        {
           //如果传进来的是Derive2就转换成功,如果不是pd2就是nullptr
             Derive2* pd2 = dynamic_cast<Derive2*>(p);
            // Derive2* pd2 = static_cast<Derive2*>(p);  //static_cast就是直接转了,没有动态类型的识别
            if (pd2 != nullptr) //是Derive2,转换成功
            {
                pd2->derive02func();
            }
            else // p指向的不是 derive2对象,是Derive1
            {
                p->func();
            }
        }
        
      • 基类必须要有虚函数, 否则编译不能通过

      • 子类转父类,和static_cast效果一样,但是对父类转子类的时候,dynamic_cast可以进行类型检测会比static_cast安全,dynamic_cast必须将父类指针转成对应的指向的类型的,否则返回就是nullptr

        • 一般用于父类指针指向子类,但是不能访问子类本身的成员,所以通过动态转换,将父类转成对应的子类指针,如果类型不匹配则会转型失败,比如直接将父类取地址转换(会导致内存非法访问);

STL

  • 六大组件:容器、算法、迭代器、仿函数、容器适配器、空间配置器
  • 容器:
    • 顺序容器:

      • vector
        • 定义:底层是动态开辟的数组,每次以2倍扩容(GCC)/1.5倍扩容(VS) ,在容器中,通过空间配置器进行对象构造和空间开辟,allocate(开辟空间), deallocate(释放空间), construct(对象构造), destroy(对象析构)
        • 增删查:
          • 因为是数组,底层内存是连续的,所以可以随机存取,时间复杂度是O(1),
          • 插入和删除,会导致元素的移动,时间复杂度是O(n),只适用于尾部插入和删除;
          • 尾部插入元素的时候,空间内存不够的时候会重新开辟新的空间,并进行内存拷贝,进而会导致性能消耗;
        • 基本接口
          • push_back()-尾插元素, insert()-通过迭代器插入, erase()-通过迭代器删除, pop_back()-尾删, size()-元素个数capacity()-容量大小empty(), resize(), reserve()swap()
        • 关于resize()reserve()
          • reserve(): 预留空间,底层开辟空间,没有添加元素(size不会变大),且只能用于扩容,不能减少容量
          • resize():可以开辟空间,并且底层会构造对象(size会变大),如果指定第二个参数,使用第二个参数,如果没有就用默认值,此外可以用于减少容量(size)
        • 迭代器失效:
          • 插入和删除之后迭代器会失效,需要进行更新操作
        • 关于clear():只改变清空size(),不改变capacity(),所以vector,底层所占内存是不变的
      • list
        • 定义: 底层是双向的循环链表,每个节点不仅有数据域,还有指向前一个节点的指针和指向后一个节点的指针
        • 增删查:
          • 底层链表,内存不连续,不支持随机存取,只能遍历访问,时间复杂度是O(n);
          • 可以在任何位置高效的对节点进行插入和删除,时间复杂度O(1);
        • 迭代器失效:
          • 由于内存空间不是连续的,删除只会影响当前迭代器,或者erase返回下一个有效的迭代器
          • 插入是不会影响迭代器失效
        • 基本接口:
          • push_back(), psuh_front(), insert(), pop_back(), pop_front(), erase(),
      • deque
        • 定义: 是个动态开辟二维数组,第一维度存放的是第二维度的指针(new出来的),第二维度是固定的长度的数组空间(4096字节),以第一维度的2陪空间进行扩容,可以支持随机访问(随机访问迭代器),但是由于内部是分段的,所以比vector要复杂一点
        • 增删查:
          • 由于是双端队列,在头部的插入和删除是O(1),在尾部的插入和删除是O(1),与vector相比,可以在头部进行方便的插入和删除
          • 在队列中间指定位置进行删除和删除是O(n),但是效率比vector可能低一点,因为deque第二维度不是连续的
      • 基本接口:
        • push_back()push_front(), insert(), pop_back(), pop_front(), erase()
    • 无序关联容器:底层是链式哈希表,是无序的,增删查O(1)

      • unordered_set:无序集合,key不可以重复
      • unordered_multiset:无序集合,key可以重复
      • unordered_map:无序映射表,key不可以重复
        • unordered_mapmap的区别:
          • unordered_map 底层是哈希表,是无序的,增删查的时间复杂度是O(1);
          • map底层是红黑树,红黑树是中序遍历是有序的,增删查需要遍历树,时间复杂度是O(log n);
      • unordered_multimap:无序映射表,key可以重复
      • 无序映射表底层结构:底层结构
        • 底层是hashtale,则有个桶(bucket)的概念,则hash操作是通过key值计算hash code,再根据对应hash code找到对应桶,然后在桶里面找到对应的节点
        • 通过开链法解决哈希冲突,将hash code相同的节点插到链表中,采用的是头插法,即使hash冲突,时间也是O(1),用一个vector指针数组来存储节点指针,vector中的每个元素都指向一个链表,链表中的每个节点都有一个指向下一个节点的next指针,然后还有一个元素是pair结构
        • 需要考虑的问题:链表过长可能导致查找一个节点的时间复杂度是O(n),也就是hash退化问题,为了解决这个问题,引入了负载因子-hashtable元素和bucket个数之间的比值最大负载因子,当负载因子>=最大负载因子的时候会发送rehash操作(限制了每个桶的数量无限增加),也就是扩容+重新插入(重新计算hashcode,再插入),从而确保搜索删除的时间复杂度还是O(1);
        • vs中初始容量是8,以8的倍数扩容,gcc中初始容量是13,以2倍扩容,扩容都是vector的扩容;
        • 一般解决hash冲突的方式:
          • 线性探测:hash函数计算到重复的位置,则依次向后找,找到空位
          • 开链:unorderd_map底层原理
          • 再散列:发生hash冲突,换一种hash函数,直到不冲突
          • 二次探测:如果使用hash函数计算出来的位置已经有元素了,就通过步长1^2、2^2、3^3....进行查找
    • 有序关联容器:底层是红黑树,是有序的,增删查时间O(log n)

      • set:集合,key不能重复
      • multiset:集合,key可以重复
      • map:映射表[key,value],key不可以重复
        • 底层元素是pair对象,可以通过make_pair(key,value)将键值对打包成对象
        • 插入:
          • 方法一:mp.insert(pair<int,int>(1,2));
          • 方法二:mp.insert(map<int,int>::value_type(1,2));
          • 方法三:mp.insert(make_pair(1,2));
          • 方法四:mp[1] = 2;
        • find[]
          • []:通过keyvalue,如果找到了就返回,如果没有这个key,就对这个key插入默认值
          • find: 通过key查找,找到了返回迭代器,没找到返回end()迭代器
          • 迭代器失效:
            • 由于内存空间不是连续的,删除只会影响到当前的节点,需要通过erase返回下一个节点的有效迭代器
      • multimap:映射表,key可以重复
      • 红黑树
        • 定义:进阶版AVL,维护的是树黑节点的平衡,相比AVL树,维护平衡成本低,旋转次数要少;
        • 性质
          • 节点不是黑就是红
          • 根节点必须是黑
          • 叶子节点(空节点)必须是黑
          • 红节点的子节点不能为红-防止树的高度差大于2倍-长度路径可能是最短路径2倍
          • 每条路径上的黑节点树必须相同-维护树的平衡
          • 本身就是个平衡二叉树,中序有序,
        • 旋转

        • 扩展(AVL树旋转-左右子树高度差不能超过1):
  • 容器适配器:对其他容器进行封装,没有自己的数据结构,内部方法也全是依赖其他容器
    • stack
      • 定义:栈,先进后出,底层依赖deque,因为vector动态扩容效率低,deque第二维度大小稳定,而且vector需要大量连续的空间可能分配失败,而deque的第二维度是分断的
      • 基本操作:push 入栈pop() 出栈top() 查看栈顶元素size() 元素个数
    • queue
      • 定义:队列,先进先出, 底层依赖deque,不仅因为vector动态扩容效率低,而且vector头部删除效率低,队列需要在头部出队
      • 基本操作:push() 入队pop() 出队front() 查看对头元素back 查看队尾元素empty()size()
    • priority_queue
      • 定义:优先队列,底层抵赖vector,底层数据结构是通过数组构造的大根堆(默认底层less<int>是大根堆,如果将函数对象换成greater<int>就是小根堆),通过下标获取左右节点
      • 基本操作:push 入队pop 出队top 查看队顶元素empty()size()
      • 大根堆大顶堆
        • 定义:完全二叉树,根节点大于左右子树最大值,左右子树又是个大根堆
        • 相关操作:
          • 建堆 O(n)
            • 依次从左向右的插入节点,每次插入节点的都进行调整满足大顶堆的性质
          • 调整堆 O(log n)
            • 每次插入节点,导致性质丢失,就需要进行调整
  • 迭代器:
    • 定义:对指针的封装,可以向指针一样操作,向上隐藏细节,提供遍历容器的接口,可以在不了解容器底层的情况下对容器进行遍历
    • 常见迭代器: iterator 普通正向迭代器const_iterator 常量正向迭代器,只读不写reverse_iterator 反向迭代器const_reverse_iterator 常量反向迭代器
  • 算法:接收的都是迭代器参数
    • 头文件:#include<algorithm>
    • 常见算法:
      • sort:快速排序
      • find:遍历查找,成功返回迭代器,失败返回end()迭代器
      • find_if:
      • binary_search: 二分查找,成功返回true,失败返回flase
      • for_each: 遍历容器,可以添加函数对象

其他小知识点

  • 大小端编码:
    • 小端编码:本地内存存储的字节序,数字有效位低的存放在低地址
    • 大端编码: 网络字节序,数字有效位低的存放在高位地址
    • 例子:
      • 16进制整型0x12345678,有效位高到低一次为0x120x340x560x78,地址是从左向右是低地址到高地址,则在本地内存中的字节序存储是0x78 0x56 0x23 0x12,转换成网络字节序为0x12 0x23 0x 56 0x78
  • 内存对齐:参考
    • 为什么要内存对齐:

      • 可以提高内存访问速度,如果没有内存对应,对一个变量的读取可能需要多次,以至于效率低
      • 对齐边界:char-1short-2int-4float-4double-8
      • 对齐原则:
        • 根据最大对齐边界进行对齐,
    • 防止不同平台内存对齐不一致:#pragma pack(push,1)#pragma pack(pop)

  • C++友元:参考
    • 友元函数:定义在类外,不属于当前类的函数,可以在类中声明,并加上friend关键字访问,当前类的私有成员, 友元函数可以是其他类的成员函数
    • 友元类: 可以将一个类定义为另外一个类的友元,通过添加关键字,友元类的成员函数可以访问当前类的私有成员
    • TIP:
      • 友元是单向的
      • 友元关系不可以传递
      • 友元不可以被继承
  • 文件操作:

设计模式

  • 单例模式:
  • 观察者模式:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值