Skip to content

Latest commit

 

History

History
431 lines (277 loc) · 20.7 KB

File metadata and controls

431 lines (277 loc) · 20.7 KB

C++

C和C++的区别

  • C是一个结构化语言,重点在于算法和数据结构。
  • C考虑的是如何通过一个过程,对输入进行运算处理得到输出,面向过程的;C++,首要考虑的是如何构造一个对象模型,让这个模型能够配合对应的问题
  • C++拓展了面向对象设计的内容,像类、继承、虚函数、模板和容器
  • C++需要考虑数据封装,对象接口的设计和继承、组合与继承的使用

Python和C++的区别

  • python是一种先编译后解释的语言,.pyc文件是由 .py文件经过编译后生成的字节码文件;C++是一种编译语言,完整的源代码将直接编译为机器代码,由CPU直接执行
  • Python既是动态类型语言,运行时进行类型检查;C++是一种静态类型的语言,变量类型在编译时检查
  • Python使用垃圾回收器进行内存管理;C++没有垃圾回收器
  • 运行效率上C++要比Python快

编程范式

编程范式分为四种,分别是:面向过程编程,面向对象编程,泛型编程,函数式编程

  • 面向过程编程(Procedure Programming, PP):
  • 面向对象编程(Object Oriented Programming, OOP):利用类层级及虚函数进行编程,从而可以通过接口操作各种类型的对象,并且程序本身也可以通过派生进行功能增量扩展
  • 泛型编程(Generic Programming,GP):独立于任何特定类型的方式编写代码,模板,本质是屏蔽数据和操作数据的细节,让算法更为通用,让编程者更为关注算法的结构
  • 函数编程:把运算过程尽量写成一系列嵌套的函数调用
    • 不依赖于外部的数据,而且也不改变外部数据的值
    • 把函数当成变量来用,关注于描述问题而不是怎么实现

多态

多态指的是同一名字的事物可以完成不同的功能。多态可以分为编译时的多态和运行时的多态。

  • 静态多态:函数重载(包括运算符的重载)、模板
  • 动态多态:继承、虚函数等概念有关

虚函数

虚函数:为了实现多态性,能够用一种方式去调用同一类族中不同类的同名函数;允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数

虚函数表的原理

  • 虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针
  • 虚表的构造(虚函数指针的赋值)发生在编译器的编译阶段
  • 虚表属于类,存储在静态存储区,对象 this内部包含一个虚表指针,来指向虚表
  • 一个类只需要一个虚表即可,同一个类的所有对象都使用同一个虚表

虚函数的调用关系:this -> vptr -> vtable ->virtual function

参考:

https://zhuanlan.zhihu.com/p/75172640

调用虚函数和非虚函数(普通函数)的区别

  • 虚函数的地址,是通过实例的地址(this)去查找它的虚表所在的地址,然后从虚函数表里取出该函数所对应的元素
  • 非虚函数的地址对编译期来说“静态”的,也就是函数地址在编译期就已经确定了,不为类的对象分配内存也可访问

虚函数可以是静态的吗

不可以:

  • static成员函数没有this指针,也就无法访问vptr(虚表指针,用来指向虚表)
  • virtual成员函数的关键是动态类型绑定的实例调用。静态函数和任何类的实例都不相关,它是class的属性

构造函数和析构函数可以是虚函数吗

(1)构造函数不能是虚函数

  • vptr角度解释

    • 虚函数的调用是通过虚函数表来查找的,而虚函数表由类的实例化对象的 vptr指针指向,该指针存放在对象的内部空间中,需要调用构造函数完成初始化。如果构造函数是虚函数,那么调用构造函数就需要去找 vptr,但此时 vptr还没有初始化
  • 从多态角度解释

    • 虚函数主要是实现多态,在运行时才可以明确调用对象,根据传入的对象类型来调用函数。而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用,那使用虚函数也没有实际意义

(2)析构函数可以且常常是虚函数

  • 若析构函数不是虚函数,delete时只释放基类,不释放子类,内存泄漏
  • 析构函数不是虚函数的话,只会调用基类析构函数

哪些函数不能是虚函数

构造函数:先有构造函数后有虚函数

静态成员函数:静态函数不属于对象属于类,静态成员函数没有this指针

内联函数:编译期间展开,虚函数是在运行期间绑定

友元函数:不能被继承

普通函数:不具有继承特性

虚基类

  • 如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员。虚基类使得在继承间接共同基类时只保留一份成员
  • 虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。

纯虚函数

  • 纯虚函数:基类不关心函数的具体实现,只为派生类保留一个函数名字,以便派生类需要的时候进行定义
  • 纯虚函数在类的虚函数表中对应的表项被赋值为0,也就是指向一个不存在的函数。由于编译器绝对不允许有调用一个不存在的函数,所以该类不能生成对象。在它的派生类中,除非重写此函数,否则也不能生成对象。

抽象类为什么不能被实例化

纯虚函数在类的虚函数表中对应的表项被赋值为0,也就是指向一个不存在的函数。由于编译器绝对不允许有调用一个不存在的函数,所以该类不能生成对象

C++对象内存模型

  • 每个对象有独立的非静态成员变量
  • 内存中成员函数只有一份,存储在代码区
  • 静态数据成员存储在静态区

inline 关键字

  • 如果一些函数被频繁调用,不断地有函数入栈,会造成栈空间或栈内存的大量消耗。为了解决这个问题,特别的引入了 inline修饰符
  • 在编译阶段在调用内联函数的地方直接展开函数代码,避免函数调用的开销。

什么时候使用内联函数

当函数调用产生的开销大于函数执行的开销,或者两者相差不大时,建议使用内联函数;而当函数执行的开销远远大于函数调用的开销时,不建议使用内联函数。

内联函数inline和#define的区别

  • 内联函数的调用是传参,宏定义只是简单的文本替换
  • 内联函数可以在程序运行时调用,宏定义是在程序编译进行
  • 内联函数有类型检测更加的安全,宏定义没有类型检测
  • 内联函数在运行时可调式,宏定义不可以
  • 内联函数可以访问类的成员变量,宏不可以
  • 类中的成员函数是默认的内联函数

#define和const的区别

  • 类型和安全检查不同
    • define宏没有类型,不做任何类型检查,仅仅是展开
    • const常量有具体的类型,在编译阶段会执行类型检查
  • 编译器处理不同
    • define在预处理阶段展开
    • const常量是编译运行阶段使用
  • 存储方式不同
    • define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存
    • const常量会在内存中分配
  • 调试
    • define在预处理阶段进行文本替换,不能调试
    • const常量可以调试

cast显示类型转换

static_cast

  • 通常用于转换数值数据类型(如 float -> int
  • 下行转换时,因为没有动态类型检查,是不安全的(即使转换失败,它也不返回NULL)

const_cast

用于将常量对象转换成非常量对象(去掉 const性质)

reinterpret_cast

改变指针或引用的类型

dynamic_cast

  • 用于多态类型的转换
  • 对于指针类型转换失败返回 nullptr,对于引用类型抛出 std::bad_cast异常
  • dynamic_cast转换类指针时,需要虚函数
    • dynamic_cast转换是在运行时进行转换,运行时转换就需要知道类对象的信息(继承关系等),可以通过虚函数表获取这个信息,通过这个虚函数表,我们就可以知道该类对象的父类,在转换的时候就可以用来判断对象有无继承关系

static 关键字

  • 修饰局部变量

    static修饰局部变量时,使得被修饰的变量成为静态变量,存储在静态区。存储在静态区的数据生命周期与程序相同,在main函数之前初始化,在程序退出时销毁。(改变存储位置)

  • 修饰全局变量

    全局变量本来就存储在静态区,因此static并不能改变其存储位置。但是,static限制了其链接属性。被static修饰的全局变量只能被包含该变量定义的文件访问(改变作用域)

  • 修饰函数

    static修饰函数使得函数只能在包含该函数定义的文件中被调用(改变作用域)

  • 修饰成员变量

    用static修饰类的数据成员使其成为类的全局变量

  • 修饰成员函数

    用static修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含this指针,因而只能访问类的static成员变量

extern 关键字

  • extern可以置于变量或者函数前,表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义
  • extern"C"修饰的变量和函数是按照C语言方式编译和连接的

extern 和static

  • extern表明该变量在别的地方已经定义过了,在这里要使用那个变量
  • static 表示静态的变量,分配内存的时候,存储在静态区,不存储在栈上面
  • extern和static不能同时修饰一个变量
  • static修饰的全局变量声明与定义同时进行
  • static修饰全局变量的作用域只能是本身的编译单元

volatile关键字

  • 使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据
  • 编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问

delete this

在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。

在类的析构函数中调用delete this

delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃

union

使几个不同的变量共占同一段内存的结构,称为“联合体”类型的结构

共享不是指把多个成员同时装入一个联合变量内, 而是指该联合变量可被赋予任一成员值,但每次只能赋一种值, 赋入新值则冲去旧值

struct和union的区别

  • 在结构中各成员有各自的内存空间, 一个结构变量的总长度是各成员长度之和(空结构除外,同时不考虑边界调整)
  • 而在联合体中,各成员共享一段内存空间, 一个联合变量的长度等于各成员中最长的长度

struct和class的区别

  • class 是引用类型,它在堆中分配空间,栈中保存的只是引用;而 struct 是值类型,它在栈中分配空间
  • struct成员变量和成员函数默认访问权限是public的,class是private的
  • struct的继承默认是public,class是private的
  • struct是值类型,直接存储数据,在一个对象的主要成员为数据且数据量不大的情况下,struct会带来更好的性能
  • 将一个struct变量赋值给另一个struct变量,复制的是数据;class复制的是引用,而不是class数据

struct内存对齐

为什么要内存对齐

因为处理器读写数据,并不是以字节为单位,而是以块(2,4,8,16字节)为单位进行的。如果不进行对齐,那么本来只需要一次进行的访问,可能需要好几次才能完成,并且还要进行额外的合并或者数据分离,导致效率低下

对齐规则

  • 前面单元的大小必须是后面单元大小的整数倍,如果不是就补齐
  • 整个结构体的大小必须是最大字节的整数倍

模板

程序实体所要完成的功能完全一样,但是它们所操作的数据类型却不一样。对于这些函数或者类,采用一个函数和一个类型来进行描述的话,将会大大简化程序设计的工作

重载、重写和重定义的区别

  • 重载:函数名相同,函数的参数个数、参数类型或参数顺序三者中必须至少有一种不同。函数返回值的类型可以相同,也可以不相同。发生在一个类内部,不能跨作用域。
  • 重写(覆盖):子类重新定义父类中有相同名称、参数列表的虚函数。(override)
  • 重定义(隐藏):子类重新定义父类中有相同名称 ( 参数列表可以不同 )的非虚函数 ,指派生类的函数屏蔽了与其同名的基类函数

指针和引用的区别

  • 指针:从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,包括其地址的改变和指向地址中数据的改变;引用:是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量)
  • 指针传递参数本质上是值传递的方式,不会影响主调函数的实参变量的值;引用传递过程中,被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量,会改变实参变量

浅拷贝和深拷贝的区别

在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的;但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致野指针现象,所以,此时,必须采用深拷贝

对同一块动态内存执行2次以上释放的结果是未定义的,所以这将导致内存泄露或程序崩溃

memcpy拷贝

void * memcpy ( void * dest, const void * src, size_t num );

memcpy() 会复制 src 所指的内存内容的前 num个字节到 dest 所指的内存地址上

  • memcpy是浅拷贝,对于非内置类型会有指针悬挂的问题
  • memcpy是直接拷贝,比for循环效率更高

左值和右值

左值和右值

  • 左值是可寻址的变量,有持久性
  • 右值一般是不可寻址的常量,或在表达式求值过程中创建的无名临时对象,短暂性的
int a; // a 为左值
a = 3; // 3 为右值

左值引用和右值引用

  • 左值引用:引用一个对象
  • 右值引用:必须绑定到右值的引用,C++11中右值引用可以实现“移动语义”,通过 && 获得右值引用。
int x = 6; // x是左值,6是右值
int &y = x; // 左值引用,y引用x

int &z1 = x * 6; // 错误,x*6是一个右值
const int &z2 =  x * 6; // 正确,可以将一个const引用绑定到一个右值

int &&z3 = x * 6; // 正确,右值引用
int &&z4 = x; // 错误,x是一个左值

emplace_back和push_back

  • push_back:向容器中加入一个右值元素(临时对象)的时候,首先会调用构造函数构造这个临时对象,然后需要调用拷贝构造函数将这个临时对象放入容器中
  • 移动构造函数:这样造成的问题是临时变量申请的资源就浪费。引入了右值引用,移动构造函数后,push_back()右值时就会调用构造函数和移动构造函数。
  • emplace_back:这个元素原地构造,不需要触发拷贝构造和移动构造。而且调用形式更加简洁,直接根据参数初始化临时对象的成员

移动语义

  • 移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。
  • 对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。
  • 当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数

C++移动语义即提出了一个右值引用,使用 std::move 可以强制将左值引用转为右值引用。而对于右值引用,程序可以调用移动构造函数进行对象的构造,减少了原来调用拷贝构造函数的开销。 移动构造函数和移动赋值运算符的实现即是对象所有权的转让,让那些左值对象(临时对象)变成右值对象

完美转发

std:forward()则会保留参数的左右值类型

C++类的构建顺序

  • 如果某个类有基类,执行基类的默认构造函数
  • 非静态数据成员,按照声明的顺序创建
  • 执行该类的构造函数

即构造类时,会先构造其父类,然后创建类成员,最后调用本身的构造函数

空类默认有哪些函数

  • 默认构造函数
  • 默认拷贝构造函数
  • 默认析构函数
  • 默认赋值运算符
  • 取址运算符
  • 取址运算符const

函数指针

可以用一个指针变量指向函数,然后通过该指针变量调用函数

C++11新特性

  • 列表初始化:由花括号括起来大的初始值
  • nullptr
  • auto
  • 范围for语句
  • emplace
  • lambda表达式
  • 智能指针
  • 移动语义
  • 新增容器
    • array
    • forward_list
    • unordered_map、unordered_set

lambda底层原理

仿函数:重载 operator(),让对象有函数的行为

编译器会把一个lambda表达式生成一个匿名类的匿名对象,并在类中重载函数调用运算符

传值是 const,不能被修改

emplace 和 push的区别

  • emplace可以直接传入构造对象需要的元素,然后自己调用其构造函数
  • push只能让其构造函数构造好了对象之后,再使用复制构造函数

智能指针

  • shared_ptr:允许多个指针指向同一个对象

    • shared_ptr<int> p=make_shared<int>(1);
    • 引用计数:拷贝操作,计数增加;shared_ptr销毁,计数减少。计数为0会自动释放所管理的对象(析构函数)
    • 引用计数问题:导致指针所指的对象不能被释放
  • unique_ptr:禁止其他智能指针与其共享同一个对象

    • unique_ptr<int> p(new int(0));
  • weak_ptr

    • auto p = std::make_shared<int>(1);
      weak_pt<int> wp(p); // wp弱共享p;p的引用计数不变
    • 弱引用,指向一个 shared_ptr管理的对象,不会改变 shared_ptr的引用计数
  • auto_ptr:可以转移指针资源

    auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
    auto_ptr<string> p2;
    p2 = p1; //auto_ptr不会报错.

    此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。

    auto_ptr的缺点是:存在潜在的内存崩溃问题

share_ptr直接类构造和用makeshared区别

  • 直接类构造要分配两次内存
    • 被管理的对象
    • 控制块(引用计数等)
  • 分配一次内存:申请一个单独的内存块来同时存放对象和控制块

智能指针的线程安全

  1. 同一个shared_ptr被多个线程读,是线程安全的;
  2. 同一个shared_ptr被多个线程写,不是线程安全的;
  3. 共享引用计数的不同的shared_ptr被多个线程写,是线程安全的。

强引用和弱引用的区别

  • 强引用:

    • 当对象被创建时,计数为1;每创建一个变量引用该对象时,该对象的计数就增加1;当上述变量销毁时,对象的计数减1,当计数为0时,这个对象也就被析构了。
    • 循环引用的问题
  • 弱引用:

    • 弱引用指针就代表它指向这东西,但对象的释放不关它的事
  • 弱引用不更改引用计数,类似普通指针,只要把循环引用的一方使用弱引用,即可解除循环引用

STL

STL是标准模板库,STL库是用模板(template)写出来的,模板是STL库的基础。STL大致由以下几部分组成:

容器、算法、迭代器、仿函数、适配器、配置器

c++ 14 新特性

  • 所有函数返回类型推导auto(c++11只支持lambda函数)