引言
- 计算机基础知识和编程基础的查漏补缺以及高频面试题
1 知识点
操作系统中堆和栈的区别
- 堆为按需申请、动态分配,例如
C++中的new操作(当然 C++ 的 new 不仅仅是申请内存这么简单) - 堆可以简单理解为当前使用的空闲内存,其申请和释放需要程序员自己写代码管理
- 栈内存:由编译器自动分配释放,存放函数的参数值,局部变量。操作方式类似于数据结构中的栈,都是先进后出。使用一级缓存,通常都是被调用时处于存储空间中,调用完毕立即释放
- 堆内存:由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定
并发与并行区别
- 并发是指宏观上在一段时间内能同时运行多个程序, 指多个任务在逻辑上交织执行的程序设计(总线程数 <= CPU数量)
- 单核CPU运行多任务, 并发技术:使用时间片轮转进程调度算法, CPU轮流为多个进程服务
- 并行则指在同一时刻能运行多个指令, 指物理上同时执行(总线程数 > CPU数量)
- 多个CPU运行多任务, 并行技术:不同进程分配给不同CPU
2 C++编程题汇总
C++编译相关问题
- 编译过程: 预处理->编译->汇编->链接
- 宏定义的处理
- 宏在编译时候就处理完了,生成的可执行文件里面再没有宏,因此宏是不占内存的
- 在
宏展开后,宏所定义的内容是否需要分配内存,就看宏的替换的结果了, 但这个就不算宏所占用的内存
- Linux中C程序的生命周期
GCC编译器将源程序文件 hello.c 编译为目标文件 hello 的过程分为四个阶段: 预处理器、编译器、汇编器和链接器, 一起构成了编译器系统
C++语言
- C++语言的三个特征: 封装, 继承, 多态
- 多态的理解: 同一函数作用于不同的对象, 会实现不同的功能, 产生不同的执行结果.
- 运行时可以通过指向基类的指针或引用来调用派生类中的方法
- 在程序运行时的多态通过继承和虚函数体现; 在程序编译时的多态在函数和算数符的重载上体现
- 多态的理解: 同一函数作用于不同的对象, 会实现不同的功能, 产生不同的执行结果.
- C++面向对象的特点: 数据抽象, 继承, 动态绑定
- 动态绑定的理解: 函数运行时的版本由实参决定自动选择函数的版本
- 使用基类的引用或指针调用一个虚函数时, 将会发生动态绑定
- 动态绑定的理解: 函数运行时的版本由实参决定自动选择函数的版本
sizeof与strlen
strlen: 为一个函数, 只能以char*作为参数, 用于计算指定字符串的长度, 但不包括结束字符串\0- 参数必须以
\0作为结束符才可以正确统计
- 参数必须以
sizeof: 单目运算符, 计算一个表达式或类型名称所占内存空间的字节数, 满足右结合律- 统计结构体大小时涉及内存对齐问题: 结构变量中的成员按照
4或8的倍数计算(加快CPU存取速度)
- 统计结构体大小时涉及内存对齐问题: 结构变量中的成员按照
常见关键字的作用
extern- 与"C"一起作用, 告诉编译器该函数或变量按照C的规则翻译, 如
extern “C” void fun(int a,int b) - 不与"C"一起作用, 声明函数或变量的作用范围, 作用范围为本模块或其他模块, 常用于声明在外部定义的变量
- 与"C"一起作用, 告诉编译器该函数或变量按照C的规则翻译, 如
const:const类型的对象在程序执行期间不能被修改- 常对象: 声明对象前加
const- 常对象只能调用常函数, 不能调用普通函数
- 常函数: 成员函数后加
const- 常函数不可以修改成员属性, 只能使用数据成员
- 成员属性声明前加
mutable后, 在常函数中依然可以修改
- 顶层
const与底层const- 顶层
const表示指针本身是常量(指针常量)int* const p, 指针指向的地址不可改变 - 底层
const表示指针指向的是一个常量(常量指针)const int* p, 可以指向别的常量
- 顶层
- 优点:
- 便于类型检查, 保护实参
- 类似与宏定义, 方便参数的修改和调整
- 节省空间, 宏定义时会进行宏替换并为变量分配空间
- 为函数重载提供参考, 即可添加const进行重载
- 常对象: 声明对象前加
static:- 修饰内置类型变量为静态变量
- 静态变量只初始化一次, 未初始化的静态变量初始化为0;
- 静态全局变量只在本文件可见, 外部无法访问
- 静态局部变量只在定义的作用域内可见, 但生存周期是整个程序运行时期
- 修饰函数为静态函数
- 只允许在当前文件中使用, 不可以被其他文件引用, 其不会与其他文件中的同名函数冲突
- 修饰成员变量为静态成员变量
- 所有对象(包括派生类)共享同一份数据
- 在编译期间分配内存
- 类内声明, 类外初始化(不可使用构造函数初始化)
- 修饰成员函数为静态成员函数
- 所有对象共享同一函数
- 不含this指针. 不需要通过对象便可直接访问
- 静态成员函数只能访问静态成员变量
- 修饰内置类型变量为静态变量
- 关系
static和extern不能同时修饰一个变量,static即声明也定义,extern只能声明const和static不能同时修饰成员函数
volatile: 防止对变量进行优化, 让编译器每次操作该变量时一定从内存中取出override: 确保该成员函数为虚函数并且一定可以重写来自基类的虚函数final: 虚函数加final, 任何覆盖该函数的行为会报错
new/delete与C中malloc/free的区别
new/delete是C++的运算符,malloc/free是C++/C的标准库函数new/delete会调用构造函数申请或析构函数释放内存new/delete更加灵活:malloc/free为库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加在malloc/free上
delete和delete[]的区别
- 通俗来讲
delete用来释放new申请的动态内存, 释放单个对象指针指向的内存delete[]用来释放new[]申请的动态内存, 释放对象数组指针指向的内存
- 针对数据类型而言
- 对基本数据类型,
delete和delete[]释放内存效果一样 - 对自定数据类型,
delete只会调用第一个对象的析构函数;delete[]会逐个调用析构函数来释放数组的所有对象
- 对基本数据类型,
动态库和静态库的区别
- 库文件是是一个代码仓库或代码组件的集合, 为目标文件提供可直接使用的变量, 函数, 类等
- 静态链接库和动态链接库的根本区别在程序编译过程中如何处理库文件和目标文件的链接关系
- 静态链接库:链接时, 静态库会完全复制到可执行文件中, 构建完成后不再依赖该静态库
- 动态链接库: 链接时仅将重要的信息, 如重定位和符号表信息复制到可执行文件中. 在程序执行时, 会根据信息从系统中寻找对于库文件
| 区别 | 静态库 | 动态库 |
|---|---|---|
| 可执行文件大小 | 较大 动态库的内容会被完全复制 |
较小 |
| 占用磁盘大小 | 较大 多个可执行文件使用同一个静态库, 会被多次复制 |
较小 多个可可执行文件共用同一个动态库 |
| 扩展性与兼容性 | 全量更新 库文件的更新, 可执行文件需重新编译及发布 |
增量更新 只需发布动态库文件 |
| 依赖问题 | 无依赖问题 已构建的可执行文件不依赖静态库文件 |
有依赖问题 可执行文件需要依赖的动态库文件 |
| 复杂程度 | 简单 | 复杂 会引起很多问题,例如如何在运行时确定地址,库文件版本管理等 |
| 加载速度 | 快 | 慢 |
C++指针和引用的区别
- 指针为一个变量, 存储一个指向原变量的地址; 引用为原变量的一个别名, 实质上为一个东西
- 指针可以有多级; 引用只可以有一级
- 指针可以在任何时间初始化; 引用必须在创建时被初始化
- 指针存在空指针,引用不存在空引用
- 指针初始化之后可以再改变; 引用初始化后不可改变
sizeof运算时, 引用为引用类型的大小,指针始终是地址空间所占字节个数- 自增运算意义不同: 引用自增为实体加1, 指针自增为指针向后偏移
- 作为函数参数时, 传递指针会产生临时的指针变量, 传递指针变量的值(指针指向变量的地址值); 传递引用时传递的是变量的地址, 不会产生临时变量
指针, ++的优先级问题
*p++、*(p++)、(*p)++、*++p、++*p的区别*p++与*(p++)相同,后缀++优先级更高,但后缀++先返回值(指针p),指针p与*结合之后,指针p再++,因此对应的结果是,输出指针p在自增前对应的值,指针p自增(*p)++ 括号优先级最高,因此先对指针p取值,然后后缀++先返回值*p,再对*p这个整体自增,因此对应结果是输出*p的值,之后*p的值自增1,指针p指向的位置不变*++p 即*(++p),最左是*,但后面跟的是表达式 ++p 所以要先算++p++*p 即++(*p),最左是++ 但后面跟的是表达式*p 所以要先算*p (感谢@weixin_42004700更正)
++问题- 前缀时, 先自增再返回
- 后缀时, 先返回后自增
- 优先级问题
- 后缀优先级高于前缀和*
- 后缀++结合律从左至右(先返回值后自增)
- 前缀++和*优先级相同, 结合律从右至左
- 谁靠近p就先计算谁
- 如果在p后面就先计算
常量指针和指针常量
- 记忆方法: 指针
\*和常量const谁在前先读谁 - 常量指针: 指向const对象的指针,指向的地址可以改变,但其指向的内容(即对象的值)不可以改变
- 指针常量: 指针本身是常量, 即指向的地址本身不可以改变, 但内容(即对象的值)可以改变
函数指针和指针函数
- 指针函数: 本质上为函数, 其返回值为指针
int* func(int x, int y); - 函数指针: 本质上为指针, 一个指向函数的指针变量
- 声明:
int (*func)(int x, int y); - 赋值:
func = &Function;或func = Function; - 调用:
a=(*func)();或a=func();
- 声明:
- 区分方法: 函数名带括号的为函数指针, 不带括号的为指针函数
数组指针和指针数组
- 数组指针: 本质为指针, 指向一个数组, 数组中每个每个元素都是某种数据类型的值
- 定义:
int (*p)[n] - 也称行指针, 当指针p执行p+1时,指针会指向数组的下一行
- 定义:
- 指针数组: 本质为数组, 数组的元素为指针
- 定义:
int *p[n] - 指针数组是一个包含若干个指针的数组, p是数组名, 当执行p+1时, 则p会指向数组中的下一个元素
- 定义:
- 区分方法:数组名带括号的就是数组指针, 不带括号的就是指针数组
#pragma与#ifndef #endif
- 相同点:都是防止头文件重复包含
- 不同点:
- 原理不同
#pragma once如果发现头文件被包含就不会打开头文件(更快)#ifndef #endif每次都要打开头文件去判断头文件宏
#pragma once在两个头文件名不同, 而内容相同时会把两个头文件都包含进来, 出现重定义的错误; 而后者打开头文件后发现宏名一样, 就不会重复包含, 也就避免率这个问题(更稳定)
- 原理不同
C++11的新特性
- 初始化列表
auto关键字, 编译期间自动推导变量类型decltype关键字, 从表达式中推断类型范围for语句nullptr关键字解决NULL二义性问题:nullptr是一种特殊类型的字面值,可以被转换成任意其他的指针类型,也可以初始化一个空指针lambda表达式- 智能指针: 更安全且更加容易地管理动态内存
- 右值引用
decltype和auto的区别
auto 是根据等号右边的初始值推导出变量的类型,且变量必须初始化,auto的使用更加简洁
decltype 是根据表达式推导出变量的类型,不要求初始化,decltype的使用更加灵活
- 使用场合
auto根据等号右侧表达式的值推导出变量类型,并使用表达式返回值初始化该变量。右侧表达是真实运行了的decltype根据括号内表达式分析出变量的数据类型,不会初始化变量。表达式没有真实的运行,只是用于分析而已
- 顶层const与引用
auto会忽略顶层const和引用,直接将引用指向的数据类型作为推断出的类型(顶层const不会忽略)decltype不会忽略,完全一致
union
union是一种节省空间的特殊的类, 一个union可以有多个数据成员, 但任意时刻只有一个数据成员有值, 当某个成员被赋值后, 其他成员为未定义状态- Union中的默认访问权限是public, 但可以为其成员设置权限
- 能够包含访问权限, 成员变量, 成员函数(可以包含构造函数和析构函数)
- 不能包含虚函数, 静态数据变量和引用类型的变量
- 不能被用作其他类的基类, 本身也不能有从某个基类派生而来
- 不允许存放带有构造函数、析够函数、复制拷贝操作符等的类, 因为他们共享内存,编译器无法保证这些对象不被破坏, 也无法保证离开时调用析够函数
- (尽量不要让union带有对象)
- 当多个基本数据类型或复合数据结构要占用同一片内存时, 要使用联合体
- 当多种类型, 多个对象, 多个事物只取其一时,也可以使用联合体来发挥其长处
1 | // 示例 |
struct、class、union的区别
- 访问权限:
struct,union的默认访问权限都是public,class的默认访问权限是private - 内存大小:
struct,class的内存大小为所有成员的内存之和;union的内存大小为最大的成员的内存大小,成员变量之间共享内存 - 继承:
struct,class都可以进行继承与被继承,不过struct只能添加带参数的构造函数;union不可被继承或派生 - 成员:
union不能包含虚函数, 静态数据变量, 也不能存放带有构造,析构,拷贝构造等函数的类 template模板:class可以使用模板,而struct不可以
深拷贝与浅拷贝的区别
- 浅拷贝: 通过拷贝构造函数实现, 编译器会以浅拷贝方式自动生成缺省的函数, 拷贝时简单复制某个对象的指针
- 引发问题: 假设String类有两个对象a和b,a.data的内容为“hello”,b.data为“world”,当将a的值赋给b时,可能会出现3个问题
- b.data的内存没释放, 造成内存泄漏
- b.data和a.data指向了同一块内存, a或b任何一方的值改变都会修改另一方的值
- 在对象被析构时,data被释放了两次
- 引发问题: 假设String类有两个对象a和b,a.data的内容为“hello”,b.data为“world”,当将a的值赋给b时,可能会出现3个问题
- 深拷贝: 必须显示地提供拷贝构造函数和赋值运算符,而且新旧对象不共享内存
- 使用情景:
- 一个对象以值传递的方式传入函数体
- 一个对象以值传递的方式从函数体返回
- 一个对象需要通过另外一个对象进行初始化
- 使用情景:
map和hashmap(unordered_map)区别
hashmap(unordered_map)- 基础知识
- 使用
hash table组织,数据 通过将键值key映射到hash表中的一个位置进行访问 - 对元素查找的时间复杂度可达到
O(1), 但元素的排序无序 - 在最开始建立时比较耗时, 但查询速度较快
- 使用
- 优点: 查找速度快
- 缺点: 哈希表建立比较耗费时间
- 适用处: 对于查找问题, unordered_map会更加高效一些
- 基础知识
map- 基础知识
- 使用
红黑树组织数据, 默认实现了数据的排序 - 在存储上,
map占用空间较大; 在红黑树中, 每一个节点都要额外保存父节点和子节点的连接, 因此使得每一个节点都占用较大空间来维护红黑树性质
- 使用
- 优点: 有序性, 内部实现的红黑树使得map的很多操作在的时间复杂度下就可以实现, 效率非常的高
- 缺点: 占用空间较大
- 适用处: 有顺序要求的问题, 使用map更高效
- 基础知识
unordered map内存占用率高, 但执行效率高
vector的扩容机制和原理
vector存储的空间在内存中连续. 若vector现有空间已存满元素, 在push_back新增数据时需要更大的内存, 会将原来的数据copy过来, 释放之前的内存, 在新的内存空间中存入新增元素- 不同编译器对
vector的扩容方式实现不一致,vs中以1.5倍扩容(效果更好),gcc中以2倍扩容2倍扩容时, 要申请的空间都比之前释放的空间合并起来大, 无法循环利用之前的空间;1.5倍扩容时, 可以循环利用之前的空间, 内存碎片风险小- 。空间和时间的权衡,空间分配地越多,平摊时间复杂度越低,但浪费空间也多。最好把增长因子设在 (1,2) 之间
vector初始扩容方式, 扩容效率低, 需要频繁增长, 需要频繁申请内存造成过多内存碎片. 需要合理使用resize()和reserve()提高效率减少内存碎片- 对
vector的任何操作, 一旦引起空间重新配置, 指向原vector的所有迭代器就都会失效
resize()和reserve()区别size():返回vector中的元素个数, 已用空间大小capacity():返回vector能存储元素的总数, 总空间大小resize():创建指定数量的的元素并指定vector的存储空间- 既修改
capacity的大小, 也修改size的大小, 即分配了空间的同时也创建了对象
- 既修改
reserve():指定vector的元素总数- 只修改
capacity的大小, 不修改size大小 - 预先分配一块较大的指定大小的内存空间,这样当指定大小的内存空间未使用完时,是不会重新分配内存空间的,这样便提升了效率; 只有当
n>capacity()时,调用reserve(n)才会改变vector容量,不然保持 capacity() 不变
- 只修改
- 用
reserve(size_type)只是扩大 capacity 值,内存空间可能是“野”的,若此时使用[ ]来访问,则可能会越界;resize(size_type new_size)会真正使容器具有new_size个对象, 可以使用[ ]来访问
push_back和emplace_back区别push_back:构造函数+拷贝构造emplace_back:构造函数+移动构造emplace_back省去了拷贝构造的时间, 运行效率更高
构造函数与析构函数
析构函数
- 析构函数
- 一个类只能有一个析构函数, 不能重载
- 析构函数不能有任何参数, 无返回值
- 若没有写析构函数, 编译器会生成默认的析构函数
- 对象的生命周期结束, 系统会自动调用析构函数
- 析构函数为什么一般是虚函数
C++默认析构函数不是虚函数. 对于没有派生类的基类而言, 将析构函数定义为虚函数会浪费内存空间- 若不声明为虚函数, 在析构一个指向派生类的基类指针时, 只会调用基类的析构函数, 不会调用派生类的析构函数, 造成内存泄漏
- 子类在析构时, 需要调用父类的析构函数吗
- 不需要显式调用基类的析构函数, 编译器会自动调用
- 析构时会先析构派生类再析构基类
构造函数
- 构造时: 先调用基类在调用子类
- 构造函数中的初始化列表成员只和类中定义变量的顺序相关, 与初始化列表中变量的排序无关(重要)
- 类中const, reference成员变量或
属于某种未提供默认构造函数的类类型只能用初始化列表初始化或赋值一个默认参数, 不可二次修改
- 构造函数
- 当类中定义了其他构造函数后, 将不存在默认构造函数, 需要自己合成(在无参构造函数后加
=default) - 使用explicit修饰构造函数, 使其无法进行隐式类型转换
explicit只能对含有1个参数或含有n个参数, 但其中n-1个参数有默认值的构造函数有效, 其余构造函数无法约束- 隐式转换就是
=赋值, 禁止隐式转换后, 但其本身还可以进行doubel到int的显式类型转换
- 拷贝构造函数:Foo(const Foo &), 一般发生在使用已有的对象初始化一个正在创建的对象: 包含使用等号, 传参, 返回值<非引用>等
- 处理类的静态变量或需要进行深拷贝时需要自定义拷贝构造函数
- 浅拷贝出错时, 需要定义自己的拷贝构造函数, 或禁止拷贝
- 使用
=delete可以指定函数禁止使用, 如禁止拷贝(将拷贝构造函数, 赋值运算符=delete)
- 移动构造函数:Foo(const Foo &&):右值引用, 可以避免不必要的拷贝赋值, 提升速度
- 当类中定义了其他构造函数后, 将不存在默认构造函数, 需要自己合成(在无参构造函数后加
- 构造函数为什么一般不定义为虚函数
- 虚函数对应一个虚函数表, 类中存储一个虚指针(vptr)指向该虚函数表
- 若构造函数是虚函数, 就需要通过
vptr调用, 但对象没有初始化就没有vptr, 无法找到vtable - 详解:
- 创建对象时必须确定其类型, 类型规定了对象可以进行哪些操作
- 虚函数在运行时才确定对象类型; 构造函数声明为虚函数, 构造对象时对象没有创建, 编译器不知道对象的实际类型
- 虚函数调用需要虚表指针; 构造函数为虚函数, 对象还未创建没有内存空间, 因此没有虚表指针调用虚函数
1 | // 初始化列表成员顺序题 |
- 上面的赋值顺序:
ma,mb,mc- 初始化列表的赋值顺序是由成员变量的定义顺序决定的, 且基类先与子类
C++中的类型转换
static_cast: 可以更改一切非常量性质的, 具有明确定义的类型转换- 一般是一些风险较低的转换, 比如
void*转int*、char*转void*、int转double等常用的转换关系 - 在编译期转换, 转换失败会报编译错误
- 一般是一些风险较低的转换, 比如
const_cast: 改变对象的底层const属性, 去掉const性质- 但是对象本身如果就是一个常量, 执行类型转换后的写操作是未定义的
reinterpret_cast: 较为底层的转换, 可将int * -> char *, 比较危险, 不建议使用- 相当于是对
static_cast的补充,static_cast不能完成的转换,reinterpret_cast都可以完成,比如int转char
- 相当于是对
dynamic_cast: 转换包含虚函数的基类派生类间的相互转换, 将基类的指针或者引用安全地转化成派生类的指针和引用- 为什么是安全的:如果子类含有父类不存在的函数或者变量就会返回一个空值, 说转换不成功
- 使用
static_cast仍然会转换成功, 不过调用子类的成员变量时, 由于不存在这些变量, 不存在的变量就会是随机数
- 使用
- 转换成功的条件: 父类指针指向了子类, 而需要将父类再转回子类时才能成功, 其实本质上还是向上转型
- 为什么是安全的:如果子类含有父类不存在的函数或者变量就会返回一个空值, 说转换不成功
子类与父类的类型转换
- 子类实例指针转型为父类实例指针, 不需要显式转换
- 父类指针转换为子类指针是不建议的, 如果确实需要则建议使用
dynamic_cast- 只有父类指针指向子类, 再将父类转成子类可以成功, 直接将父类转换成子类会失败
左值与右值引用
- 左值与右值
- 左值:可以取地址的,有名字的,非临时的
- 本质上是用户创建的, 通过作用域规则可知道生存周期(包括函数返回的局部变量的应用以及const对象)
- 右值:不能取地址的,没有名字的,临时的
- 本质上, 创建和销毁由编译器控制
- 左值:可以取地址的,有名字的,非临时的
- 左值引用与右值引用
- 左值引用: 要求右边的值必须能够取地址, 如果无法取地址, 可以用常引用
- 使用常引用后, 只能通过引用来读取数据, 无法修改数据, 因为其被
const修饰成常量引用
- 使用常引用后, 只能通过引用来读取数据, 无法修改数据, 因为其被
- 右值引用: 主要用于移动语义
- 生命周期: 绑定到右值以后, 本来会被销毁的右值的生存期会延长到与绑定到它的右值引用的生存期
- 资源调配: 拷贝构造函数是新开辟一个空间, 进行拷贝;
- 移动构造函数, 则直接将对象赋给新对象, 直接使用了已经申请的资源, 既能节省资源, 又能节省资源申请和释放的时间
- 左值引用: 要求右边的值必须能够取地址, 如果无法取地址, 可以用常引用
内联函数(inline)
inline函数作用: 提高函数执行效率, 在程序的每个调用点将函数体展开, 而不是采用通常的函数调用机制, 从而减少额外开销- 定义在类内的成员函数默认是
inline函数, 虚函数除外 - 通常只有函数非常短(
10行以内)时才适合定义为inline函数, 否则会导致程序变慢 - 头文件中不仅要包含
inline函数的声明, 还要包含其定义, 方便编译器查找 inline函数会增加执行文件大小
- 定义在类内的成员函数默认是
虚函数
虚函数的概念
- 虚函数: 指在基类内部声明的成员函数前添加
virtual关键字指明的函数- 存在意义: 实现多态, 让派生类能重写(
override)基类的成员函数 - 虚函数一旦声明就一直是虚函数, 派生类也无法改变其属性
- 虚函数动态绑定, 在运行时才确定, 而非虚函数的调用在编译时确定
- 虚函数必须是非静态成员函数, (因为静态成员函数在编译时确定)
- 构造函数不能是虚函数, 析构函数一般是虚函数
- 存在意义: 实现多态, 让派生类能重写(
- 静态函数与虚函数的区别
- 静态函数在编译时已经确定
- 虚函数在运行时动态绑定, 使用虚函数表机制, 调用时会增加一次内存开销
- 虚函数一般不能声明为
inline函数inline函数在编译期间将函数内容替换到函数调用处, 为静态编译- 虚函数为动态调用, 编译器不知道虚函数绑定的是哪个对象
虚函数工作机制
- 使用虚函数表+虚表指针
- 编译器在含有虚函数的类中创建虚函数表(
vtable, 存放虚函数的地址), 隐式设置虚表指针(vptr, 指向该类对象的虚函数表) - 派生类继承基类时, 也会继承基类的虚函数表
- 派生类重写了基类的虚函数时, 会将重写后的虚函数地址替换掉由基类继承而来的虚函数表中对应的虚函数地址
- 派生类没有重写基类的虚函数, 基类继承而来的虚函数的地址直接保存在派生类的虚函数表中
- 编译器在含有虚函数的类中创建虚函数表(
- 每个类都有一个虚函数表, 一个类的所有对象共享一个虚函数表, 并不是每个实例化对象都分别有一个虚函数表
运行时多态
- 多态: 静态多态, 动态多态
- 静态多态: 通过重载, 在编译时已经确定
- 动态多态: 通过虚函数机制实现, 在运行时动态绑定
- 指基类的指针指向其派生类, 通过基类的指针来调用派生类的成员函数
- 若基类通过引用或指针调用非虚函数, 无论实际对象什么类型, 都调用基类的函数
- C++类的多态性通过虚函数实现
- 基类通过引用或指针调用虚函数时, 只有在运行时才能确定调用的是基类的虚函数还是派生类的虚函数
纯虚函数
- 纯虚函数
- 只在基类中声明, 但没有定义, 没有函数体
- 声明时只需要在虚函数形参列表后添加
=0即可 - 含有纯虚函数的类都是抽象类, 只含有纯虚函数的类是接口类
- 抽象类
- 抽象类不能实例化对象
- 抽象类的派生类可以是抽象类(会继承), 也可通过实现全部的虚函数变成非抽象类, 从而可以实例化对象
- 抽象类的指针可以指向其派生类对象, 并调用派生类对象的成员函数
- 接口类
- 接口类没有任何数据成员, 也没有构造函数和析构函数
- 接口类的指针可以指向其派生类对象
智能指针
- 使用智能指针: 为了更安全且更加容易地管理动态内存
- 智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数来会自动释放资源,不需要手动地释放内存
- 当然,这并不是说使用智能指针就不会发生内存泄漏,只是它在很大程度上可以防止由于程序员的疏忽造成的内存泄漏问题
- 四个智能指针:
auto_ptr,unique_ptr,share_ptr,weak_ptr- 后面
3个为C++11的新特性
- 后面
auto_ptr- 定义在
<memory>中,C++11中因为不够安全被弃用, 被unique_ptr代替
- 定义在
unique_ptr: 同auto_ptr一样也是采用所有权模式, 即同一时间只能有一个智能指针可以指向某个对象unique_ptr相比auto_ptr其禁止了拷贝=操作(引起安全问题), 采用了移动赋值std::move()函数来进行控制权的转移- 不管是
auto_ptr还是unique_ptr, 都可以调用函数release或reset将(非const)unique_ptr的控制权转移给另一个unique_ptr - 如果
unique_ptr是个临时右值, 编译器允许拷贝操作
share_ptr: 可以共享所有权的智能指针- 定义在
<memory>中, 允许多个智能指针指向同一个对象, 并使用引用计数的方式来管理指向对象的指针(use_count()可以获得引用计数), 该对象和其相关资源在最后一个引用被销毁时释放 - 销毁时, 调用析构函数, 析构函数先使引用计数减1, 若减至0则delete对象
- 内存泄露问题: 当两个对象分别使用一个共享指针
share_ptr指向对方- 智能指针使用引用计数机制来管理着它所指对象的生命周期
- 若某个对象A的
share_ptr指向了对象B,那么对象A只能在对象B先销毁之后它才会销毁;同理,若对象B的share_ptr也指向了对象A,则只有在对象A先销毁之后B才会被销毁。因此,当两个对象的share_ptr相互指向对方时,两者的引用计数永远不会减至0,即两个对象都不会被销毁,就会造成内存泄漏的问题
- 定义在
weak_ptr: 弱指针, 不控制对象生命周期的智能指针- 指向一个
share_ptr管理的对象,weak_ptr不会修改引用计数, 只是提供访问其管理对象的方法 share_ptr可以直接赋值给weak_ptr;weak_ptr可调用lock成员函数获得share_ptr
- 指向一个
- 智能指针的一大优点: 在对象离开作用域时自动释放对象(需自定义析构对象)
- 使用可调用类:可调用类指重载了
调用运算符()的类, 其也是一个类, 用于保存一些状态 - 使用
lambda表达式:auto DeleterLambda=[](Connection *connection){close connection;delete connection;}
- 使用可调用类:可调用类指重载了
3 Python编程题汇总
lambda匿名函数
- 精简代码,
lambda省去了定义函数,map省去了写for循环过程res = list(map(lambda x: "填充值" if x=="" else x, str_1))
提高 Python 运行效率的方法
- 使用生成器,因为可以节约大量内存;
- 循环代码优化,避免过多重复代码的执行;
- 核心模块用
Cython PyPy等,提高效率; - 多进程、多线程、协程;
- 多个
if elif条件判断,可以把最有可能先发生的条件放到前面写,可以减少程序判断的次数,提高效率
*args与**kargs, *与**
args与kargs为名称, 可使用其他的代替*args为可变位置参数, 传入的参数会被放进元组里**kargs为可变关键字参数, 传入的参数以键值对的形式存放到字典里
*与**的区别*将元组转换为多个单元素**将字典去除key, 留下的value变成多个单元素
Python中实例方法/静态方法/类方法
- 区别:
- 实例方法只能被实例对象调用
- 静态方法(由
@staticmethod装饰器来声明), 类方法(由@classmethod装饰器来声明),可以被类或类的实例对象调用;
- 传参
实例方法,第一个参数必须要默认传实例对象,一般习惯用self静态方法,参数没有要求类方法,第一个参数必须要默认传类,一般习惯用cls
1 | class Foo(object): |
- 类方法和静态方法
- 大多数情况下, 类方法可以通过静态方法代替, 但在通过类调用时, 对调用者来说不可区分
- 区别:
classmethod增加了一个对实际调用类的引用- 方法可以判断自身是通过基类被调用还是通过某个子类被调用
- 通过子类调用时, 方法可以返回子类的实例而非基类的实例; 且可以调用子类其他
classmethod
staticmethod唯一的好处: 调用时返回的是真正的函数, 没有多态性staticmethod可在子类上被重写为classmethod, 但反之不可以
__new__和 __init __方法的区别
__new__方法是静态方法, 是构造函数(在每次创建类得新对象时执行), 用于创建对象并返回对象,在返回对象时会自动调用__init__方法,执行较__init__方法早__init__是实例方法, 并不是严格意义上的构造函数
Python函数参数传递
- 将可变对象(列表list、字典dict、NumPy数组ndarray和用户定义的类型(类)),作为参数传递给函数,函数内部将其改变后,函数外部这个变量也会改变(对变量进行重新赋值除外)
- 函数传参方式是引用传递, 底层实现的是传递引用变量
- 将不可变对象(字符串string、元组tuple、数值numbers)作为参数传递给函数, 函数内部将其改变后, 函数外部这个变量不会改变
- 可以将数据包装成列表(字典)等可变对象, 通过列表(字典)的方式修改
为什么说Python是动态语言
=是赋值语句,可以把任意数据类型赋值给变量,同样一个变量可以被不同类型的变量反复赋值- Python动态语言:变量本身类型不固定, 可以反复赋值不同类型的变量成为动态语言
迭代器
__getitem__- 类中实现了该方法, 其实例对象可以使用
p[key]进行取值(会调用__getitem__方法) - 一般若想使用索引访问元素, 需要在类中定义
__getitem__方法
- 类中实现了该方法, 其实例对象可以使用
__iter__()- 迭代器, 生成迭代对象时调用(只在生成对象时调用一次)
- 返回值必须是对象本身(
self), 然后使用for循环调用next方法 - 若对同一个对象生成两个迭代器对象, 两个迭代器对象一个被修改, 另一个也会自动执行相应的修改
__next__()- 每一次for循环都会调用该方法(优先级高于
__getitem__())
- 每一次for循环都会调用该方法(优先级高于
- 深度学习的应用
__getitem__:常用于数据加载类(Dataset)中,用于对象的迭代,每次迭代都执行一次__getitem__中的数据加载函数,以获取相应的数据__len__:常用于数据加载类(Dataset)中,以便可以使用len()函数获取数据总数,方便计算训练一个epoch需要多个batch(更多的是显示作用)__iter__():常用于数据加载类(Dataset)中,固定写法返回self__next()__和__getitem__很像,深度学习中一般只使用__getitem__不断读取数据,next()使用较少
- 三者的区别
- 都可以在for中使用, 进行迭代操作
__getitem__用于可迭代对象的索引(如p[key]),也可用于for的迭代(但优先级低于__next__)__iter__和__next__只用于对象的迭代(如for)(通过iter()/next()调用对应的创建迭代器、迭代操作函数)- 可迭代对象必须包含
__getitem__或者同时包含__iter__和__next__,或者同时包含。(同时包含时,在循环中__next__的优先级高于__getitem__,只会执行__next__)
生成器(yield)
- 生成器: 使用了
yield函数- 生成器返回迭代器对象, 只能用于迭代操作; 生成器就是一个迭代器
- 调用生成器的过程中, 每次遇到
yield时函数会自动暂停并保存当前所有运行信息, 返回yield值, 并在下一次执行next()方法是从当前位置继续运行
map 与 reduce函数用法解释
map()函数接受两个参数,一个是函数,一个是Iterable- map将传入的函数依次作用到序列的每个元素, 将结果作为新的
Iterator返回 - 注意
map()返回的是Iterator(惰性序列), 需要通过list()转换为常用列表结构
- map将传入的函数依次作用到序列的每个元素, 将结果作为新的
reduce()函数接受两个参数,一个是函数,一个是序列- 与
map()不同的是reduce()把结果继续和序列的下一个元素做累积计算 reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
- 与
Python赋值、深拷贝、浅拷贝区别
- 复制不可变数据类型
- 不管
copy还是deepcopy, 都是同一个地址, 对象的id值与浅复制原来的值相同
- 不管
- 复制可变数据类型
- 直接赋值: 为对象的引用(别名)
- 浅拷贝(
copy):重新分配一块内存, 创建一个新的对象, 但里面的元素是原对象中各个子对象的引用(只拷贝地址, 但地址指向的仍然是同一个变量)- 使用切片
[:]操作 - 使用工厂函数
- 使用
copy模块和copy()函数, 但是子对象还是指向统一对象(是引用)
- 使用切片
- 深拷贝(
deepcopy): 重新分配一块内存, 创建一个新的对象, 并且将原对象中的元素以递归的方式创建新的子对象拷贝到新对象中- 使用
copy模块和deepcopy()函数,完全拷贝了父对象及其子对象, 两者是完全独立. 深拷贝包含对象里面的子对象拷贝, 所以原始对象的改变不会造成深拷贝里任何子元素的改变
- 使用
Python多态与继承的理解
- 多态是指对不同类型的变量进行相同的操作, 会根据对象(或类)类型的不同而表现处不同的行为
- 继承可以拿到父类的所有数据和方法, 子类可以重写父类的方法, 也可以新增自己特有的方法
- 先有继承后有多态, 不同类的对象对同一消息会做出不同的响应
list与np.array区别
list: 内置数据类型,list内的数据类型不必相同, 保存的是数据存放的地址(指针)np.array(ndarray):numpy中的函数, 元素必须同一类型- 具有矢量运算能力和广播能力
array()方法可以将任何序列类型转换成ndarray数组
numpy堆叠数组函数
ravel(): 让将多维数组展平成一维数组.若不指定任何参数,ravel()将沿着行(第0维)展平/拉平输入数组stack(): 沿给定轴连接数组序列, 两个数组必须有相同的形状,且输出的结果的维度比输入的数组多一维vstack(): 垂直堆叠序列中的数组, 除了需要堆叠的轴外其他轴上具有相同的shapehstack(): 水平堆叠序列中的数组, 除了需要堆叠的轴外其他轴上具有相同的shapeconcatenate(): 可以实现stack(),vstack(),hstack()的功能, 根据指定的维度,对一个元组、列表中的list或者ndarray进行连接- 注意:axis指定的维度(即拼接的维度)可以是不同的,但是axis之外的维度(其他维度)的长度必须是相同的
__init__.py作用
- 标识该目录是一个python的模块包(module package)
- 简化了模块导入操作; 本来需要按文件一级一级导入的程序, 现在可以将这些函数全部写入
__init__.py中, 只需要导入这个模块包就可以导入这个模块包下的所有程序了- 使用
__all__关联一个模块列表, 在执行from xx import *时, 自动导入列表中的模块
- 使用
__init__.py是在程序导入(import)时执行的
装饰器
- 实现的技术前提
- 高阶函数: 函数参数是一个函数名或返回的是函数名
- 函数嵌套: 在一个函数中定义另一个函数
- 闭包: 在函数嵌套中, 内部函数对外部函数作用域的引用, 称内部函数为闭包
- 装饰器: 增强函数或类的功能的一个函数(对函数进行功能更新, 主要是前后处理, 无法对函数进行修改)
- 装饰器本质上是一个
Python函数或类,可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象 - 经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计
- 使用装饰器可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用
- 装饰器本质上是一个
- 使用方法
- 不用
@符号:f=decorator(函数名)或f=(decorator(参数))(函数名) - 使用
@符号:- 本质上
@就是装饰器用来装饰@下面的函数 - 自身不传入参数的装饰器使用两层函数定义装饰器, 自身传入参数的装饰器采用三层函数定义装饰器
- 本质上
- 不用
1 | # 自身不传入参数的装饰器 |
内置装饰器
@propert: 将类内方法当成属性使用, 必须有返回值, 相当于getter- 假如没有定义
@func.setter修饰方法, 就是只读属性, 不可修改
- 假如没有定义
@classmethod: 类方法, 不需要self参数, 但第一个参数需要时表示自身类的cls参数- 内部使用该类方法, 直接使用
cls替代类名(相较@staticmethod方法更加方便, 不需要写类名)
- 内部使用该类方法, 直接使用
@staticmethod: 静态方法, 不需要表示自身对象的self和表示自身类的cls参数- 不需要传入上述参数, 但内部使用类方法时, 需要把类名写上
1 | class Car: |
4 Pytorch基础知识
合并分割
- 合并
.cat()为连接, 不会增加维度.stack()为堆叠, 会增加一个维度
- 分割
.split()将张量拆分为多个块,每个块都是原始张量的视图.chunk(input, chunks, dim=0)将tensor按dim分割成chunks个tensor, 返回的是一个元组