C++基础知识点

208 阅读18分钟
  1. C++和C语言类似,一个C++程序从源码到执行文件,有四个过程,预编译、编译、汇编、链接。

  2. C++里作用域可分为6种:全局,局部,类,语句,命名空间和文件作用域。

  3. 在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区

  4. 从低地址到高地址,一个程序由代码段、数据段、BSS段组成。

  5. 可执行程序在运行时又会多出两个区域:堆区和栈区。

    1. 数据段:存放程序中已初始化的全局变量和静态变量的一块内存区域。
    2. 代码段:存放程序执行代码的一块内存区域。只读,代码段的头部还会包含一些只读的常数变量。
    3. BSS 段:存放程序中未初始化的全局变量和静态变量的一块内存区域。
    4. 堆区:动态申请内存用。堆从低地址向高地址增长。
    5. 栈区:存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。
    6. 6 文件映射区,位于堆和栈之间。

2.png

  1. 在结构体中,编译器为结构体的每个成员按其自然边界(alignment)分配空间,为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的“对齐”,比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除

  2. 面向对象的三大特征是封装、继承、多态。

    1. 封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行 交互
    2. 继承:可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展
    3. 父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。实现多态,有二种方式,重写,重载。
  3. STL由6部分组成:容器(Container)、算法(Algorithm)、 迭代器(Iterator)、仿函数(Function object)、适配器(Adaptor)、空间配制器(Allocator)。

    1. 容器:是一种数据结构, 如list, vector, 和deques,以模板类的方法提供。为了访问容器中的数据,可以使用由容器类输出的迭代器。
    2. 算法:是用来操作容器中的数据的模板函数
    3. 迭代器:提供了访问容器中对象的方法
    4. 仿函数:仿函数又称之为函数对象, 其实就是重载了操作符()的struct
    5. 适配器: 简单的说就是一种接口类,专门用来修改现有类的接口,提供一中新的接口;或调用现有的函数来实现所需要的功能。主要包括3中适配器Container Adaptor、Iterator Adaptor、Function Adaptor
    6. 空间配制器:为STL提供空间配置的系统,包括对象的创建与销毁,内存的获取与释放。
  4. STL中常用的容器有vector、deque、list、map、set、multimap、multiset、unordered_map、unordered_set

    1. vector 采用一维数组实现,元素在内存连续存放,时间复杂度为: 插入O(N),查看O(1),删除O(N)
    2. deque采用双向队列实现,元素在内存连续存放,时间复杂度为: 插入O(1),查看O(N),删除O(1)
    3. map、set、multimap、multiset 采用红黑树实现,红黑树是平衡二叉树的一种,时间复杂度为: 插入O(logN),查看O(logN),删除O(logN)
    4. unordered_map、unordered_set、unordered_multimap、 unordered_multiset采用哈希表实现,最好时间复杂度为: 插入O(1),查看O(1),删除O(1);最坏时间复杂度为: 插入O(N),查看O(N),删除O(N)
  5. 迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,重载了指针的一些操作符,-->、++、--等。迭代器封装了指针,是一个可遍历STL( Standard Template Library)容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,--等操作。迭代器返回的是对象引用而不是对象的值

  6. STL 中 resize 和 reserve 的区别

    1. 首先必须弄清楚两个概念:
    2. capacity:该值在容器初始化时赋值,指的是容器能够容纳的最大的元素的个数。还不能通过下标等访问,因为此时容器中还没有创建任何对象。
    3. size:指的是此时容器中实际的元素个数。可以通过下标访问0-(size-1)范围内的对象。
    4. resize和reserve区别主要有以下几点:
    5. resize既分配了空间,也创建了对象;reserve表示容器预留空间,但并不是真正的创建对象,需要通过insert()或push_back()等创建对象。
    6. resize既修改capacity大小,也修改size大小;reserve只修改capacity大小,不修改size大小。
    7. 两者的形参个数不一样。 resize带两个参数,一个表示容器大小,一个表示初始值(默认为0);reserve只带一个参数,表示容器预留的大小
  7. map和unordered_map

    1. map实现机理:map内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树有自动排序的功能,因此map内部所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序遍历可将键值按照从小到大遍历出来。
    2. unordered_map实现机理unordered_map内部实现了一个哈希表(也叫散列表),通过把关键码值映射到Hash表中一个位置来访问记录,查找时间复杂度可达O(1),其中在海量数据处理中有着广泛应用。因此,元素的排列顺序是无序的。
  8. hashtable扩容和如何解决冲突

    1. 哈希表的扩容
    2. 为什么要扩容:使用链地址法封装哈希表时, 填装因子(loaderFactor)会大于1,理论上这种封装的哈希表时可以无限插入数据的但是但是随着数据量的增多,哈希表中的每个元素会变得越来越长, 这是效率会大大降低。 因此,需要通过扩容来提高效率。
    3. 如何扩容:Hashtable每次扩容,容量都为原来的2倍加1,而HashMap为原来的2倍。此时,需要将所有数据项都进行修改(需要重新调用哈希函数,来获取新的位置)。 哈希表扩容是一个比较耗时的过程,但是一劳永逸。
    4. 什么情况下扩容:常见的情况是在填装因子(loaderFactor) > 0.75是进行扩容。
    5. 解决哈希冲突
    6. 链地址法: 采用数组和链表相结合的办法,将Hash地址相同的记录存储在一张线性表中,而每张表的表头的序号即为计算得到的Hash地址。
    7. 开放定址法: 即当一个关键字和另一个关键字发生冲突时,使用某种探测技术在Hash表中形成一个探测序列,然后沿着这个探测序列依次查找下去,当碰到一个空的单元时,则插入其中
  9. 哈希表的使用,哈希函数的设计

    1. 哈希表的使用 Hash表采用一个映射函数f :key -> address 将关键字映射到该记录在表中的存储位置,从而在想要查找该记录时,可以直接根据关键字和映射关系计算出该记录在表中的存储位置,通常情况下,这种映射关系称作为Hash函数,而通过Hash函数和关键字计算出来的存储位置(注意这里的存储位置只是表中的存储位置,并不是实际的物理地址)称作为Hash地址。
    2. 哈希函数的设计 Hash函数设计的好坏直接影响到对Hash表的操作效率。通常有以下几种构造Hash函数的方法: 直接定址法: 取关键字或者关键字的某个线性函数作为Hash地址,即address(key) = a*key + b。 平方取中法: 对关键字进行平方计算,然后取结果的中间几位作为Hash地址 折叠法: 将关键字拆分成几个部分,然后将这几个部分组合在一起,以特定的方式进行转化形成Hash地址 除留取余法: 如果知道Hash表的最大长度为m,可以取不大于m的最大质数p,然后对关键字进行取余运算,address(key)=key % p
    3. 哈希表大小的确定 Hash表大小的确定非常关键,如果Hash表的空间远远大于最后实际存储的记录个数,就会造成较大的空间浪费。如果选取小了的话,则容易造成冲突。在实际情况中,一般需要根据最终记录存储个数和关键字的分布特点来确定Hash表的大小。还有一种情况时可能事先不知道最终需要存储的记录个数,则需要动态维护Hash表的容量,此时可能需要重新计算Hash地址。
  10. 友元函数

    1. 在C++中,友元函数(Friend Function)是一种特殊的函数,它可以访问并操作类的私有成员,即使它不是类的成员函数。通过友元函数,我们可以实现对类的私有成员的非成员函数访问权限。
    2. 友元提供了一种突破封装的方式。友元函数提供了一种在需要时访问类的私有成员的机制,但应该慎重使用,因为过多的友元函数可能破坏类的封装性。
    3. 友元函数特性
      1. 友元函数可以访问类的私有和保护成员,但不是类的成员函数。
      2. 友元函数不能被const修饰。由于友元函数不属于任何类的成员函数,它们无法被 const 修饰。
      3. 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
      4. 一个函数可以是多个类的友元函数。
      5. 友元函数的调用与普通函数的调用和原理相同。
  11. 友元类

    1. 友元类(Friend Class)是C++中的另一个重要概念,它允许一个类将另一个类声明为自己的友元,从而使得被声明为友元的类可以访问该类的私有成员。通过友元类,我们可以实现多个类之间的数据和成员函数共享。
    2. 友元类是在一个类的定义中将另一个类声明为友元,从而使得被声明为友元的类可以访问该类的私有成员。友元类提供了一种实现多个类之间数据和成员函数共享的机制,但同样要谨慎使用,以避免过度暴露类的实现细节。
    3. 友元类特性
      1. 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
      2. 友元关系是单向的,不具有交换性。比如在只声明B是A友元的情况下,B可以访问A的私有成员,但是A却不可以访问B的私有成员,即A不是B的友元。
      3. 友元关系不能传递:如果B是A的友元,C是B的友元,则不能说明C是A的友元。(我友元的友元不是我的友元)
  12. static,voliate,const,extern

    1. const
      1. const 基本原理 : 被修饰的对象的值不可以被修改
      2. 表示常量必须进行初始化,有以下两种初始化的方式
        1. 编译时初始化:编译器在编译时会把所有用到j的地方都替换成对应常数,如const int a=42;,即这种情况下,编译器是不为常量a分配内存的
        2. 运行时初始化:初始值不是常量表达式, 如const int i=get_size();
      3. const修饰引用表示对常量的引用,不能通过此引用修改它所指向的对象,只是限制了这个操作,并未限制它指向的对象(可以是非const的
      4. const修饰指针:对于指向常量的非常量指针,也只是限制了通过此指针去修改该它所指的对象这样的操作,并未限制它所指象的对象(可以是非const的
      5. const修饰常对象:定义常对象时,同样要进行初始化,并且该对象不能再被更新,修饰符const可以放在类名后面,也可以放在类名前面。const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数;
      6. const修饰函数形参:传递过来的参数在函数内不可以改变
      7. const修饰函数返回值:在值传递的过程种,将返回值的属性指定为const确实是没有任何意义的。只有在返回值是指针类型的时候,才会用到const。
      8. const修饰成员函数:示该函数不可以修改该类的成员变量的值,并且不可以调用类中非成员函数,但非const成员函数可以调用const成员函数;
    2. static
      1. 修饰全局变量:在全局变量前加上关键字static,全局变量就定义成一个全局静态变量(限定作用域)。全局静态变量作用域被限定,只在定义它的文件之内可见,准确地说是从定义之处开始,到文件结尾。
      2. 静态函数:修饰函数在函数返回类型前加关键字static,函数就定义成静态函数。静态函数只是在定义他的文件当中可见,不能被其他文件所用
      3. 修饰局部变量:
        1. 局部静态变量和普通局部变量的作用域是一模一样的,即都只能在定义它的函数或语句块中使用,在不同的作用域中的同名static变量的内存地址也不一样
        2. 局部静态变量是*定义在静态区,其在程序开始运行时就已经在内存里面。在整个程序结束时才会被销毁
      4. 修饰成员变量-静态成员变量:
        1. 类的成员为static时,即为静态成员,从属于于类,这个类无论有多少个对象被创建,这些对象共享这个static成员变量
        2. 静态数据成员一旦被定义一直存在于程序的整个生命周期中
        3. 静态成员变量初始化:
          1. 不是由类的构造函数初始化,一般不能在类的内部初始化静态成员变量,只是在类的内部声明,在类的外部定义和初始化,格式如下:数据类型类名::静态数据成员名=值
        4. 访问静态成员变量格式:类名::静态成员名, 如果创建了对象,也可以用对象来访问: 对象名.静态成员名, *对象指针->静态成员名
        5. 静态数据成员与普通数据成员的区别:
          1. 静态数据成员的类型可以是它所属的类类型,而非静态数据成员,只能声明成它所属类的指针或引用
          2. 可以使用静态成员变量作为默认实参,而普通成员变量不可以,因为它的值属于对象的一部分,这么做无法真正提供一个对象以便从中获取成员的值
          3. 静态成员变量使用前必须进行初始化,而普通成员变量如果不初始化,会被默认初始化
          4. 对于类中的static成员变量只有在静态区中的一块内存,在编译时确定,不随对象开辟新的空间;而普通成员会随对象开辟新的空间
      5. 修饰成员函数
        1. 静态成员函数没有this指针,不能声明成const,不能在static函数体内使用this指针
        2. 成员函数不用通过作用域运算符就能直接使用静态成员
        3. 可以在类的内部定义静态函数,也可以在类的外部,但static关键字只能出现在类内部的声明语句中,只出现一次
        4. 在类外调用静态成员函数用类名::作限定词,或通过对象调用
        5. 静态成员函数不能调用非静态成员函数或变量,但非静态成员函数可以调用静态成员函数或变量.因为静态成员从属于类,非静态成员从属于对象,静态成员在编译时就确定,而这时对象还没有被创建,所以对非静态成员并没有确定的对象来访问,而在调用对象的非静态成员函数时,静态成员已经确定,所以能正确调用
    3. extern
      1. extern可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义
      2. 修饰变量
        1. 变量前有extern不一定就是声明,如果变量有被初始化则是定义,没有被初始化则为声明
        2. 而变量前无extern就只能是定义。
        3. 变量可以多次声明,但只能定义在一个地方。
      3. 修饰函数
        1. 定义函数要有函数体,声明函数没有函数体并以分号结尾。
        2. 函数同样可以多次声明,但只能在一个地方定义。
        3. 当前模块使用外部模块的函数,建议使用extern进行声明。
        4. 因为声明函数没有函数体(还有以分号结尾),所以在声明函数的时候可以将extern省略掉。但一般是在头文件(.h文件)声明的时候才省略掉extern,如果是在其他c文件声明则建议加上extern,增强代码可读性。
    4. volatile
      1. volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问
      2. 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存
      3. volatile用在如下的几个地方:
        1. 中断服务程序中修改的供其它程序检测的变量需要加 volatile;
        2. 多任务环境下各任务间共享的标志应该加 volatile;**
        3. 存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不同意义;
    1. 虚函数,是指被virtual关键字修饰的成员函数

    2. 在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数,用法格式为:virtual() 函数返回类型 函数名(参数表()) {函数体()};实现多态性通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。

    3. 每个包含虚函数的类(或者继承自的类包含了虚函数)都有一个自己的虚函数表。这个表是一个在编译时确定的静态数组。虚函数表包含了指向每个虚函数的函数指针以供类对象调用

    4. 编译器还在基类中定义了一个隐藏指针,我们称为 __vptr , __vptr 是在类实例创建时自动设置的,以指向类的虚函数表。 __vptr 是一个真正的指针,这和 this 指针不同, this指针实际是一个函数参数,使编译器来达到自引用的目的。

    5. 每个类对象都会多分配一个指针的大小,并且 *__vptr 是被派生类继承的