C++基础部分
- 1. 基础知识
- 2. C++面向对象
- 3. S T L STL STL
-
- 3.1 v e c t o r vector vector的底层原理
- 3.2 v e c t o r vector vector中的 r e s e r v e reserve reserve和 r e s i z e resize resize的区别
- 3.3 v e c t o r vector vector中的 s i z e size size和 c a p a c i t y capacity capacity的区别
- 3.4 v e c t o r vector vector中 e r a s e erase erase方法与 a l g o r i t h m algorithm algorithm中的 r e m o v e remove remove方法区别
- 3.5 v e c t o r vector vector迭代器失效的情况
- 3.6 正确释放 v e c t o r vector vector的内存 ( c l e a r ( ) , s w a p ( ) , (clear(), swap(), (clear(),swap(), s h r i n k shrink shrink_ t o to to_ f i t ( ) ) fit()) fit())
- 3.7 l i s t list list的底层原理
- 3.8 什么情况下用 v e c t o r vector vector,什么情况下用 l i s t list list,什么情况下用 d e q u e deque deque
- 3.9 p r i o r i t y q u e u e priority_queue priorityqueue的底层原理
- 3.10 m a p 、 s e t 、 m u l t i s e t 、 m u l t i m a p map 、set、multiset、multimap map、set、multiset、multimap的底层原理
- 3.11 为何 m a p map map和 s e t set set的插入删除效率比其他序列容器高
- 3.12 为何 m a p map map和 s e t set set每次 I n s e r t Insert Insert之后,以前保存的 i t e r a t o r iterator iterator不会失效?
- 3.13 当数据元素增多时(从 10000 10000 10000到 20000 20000 20000),map的set的查找速度会怎样变化?
- 3.14 m a p 、 s e t 、 m u l t i s e t 、 m u l t i m a p map 、set、multiset、multimap map、set、multiset、multimap的特点
- 3.15 为何 m a p map map和 s e t set set的插入删除效率比其他序列容器高,而且每次 i n s e r t insert insert之后,以前保存的 i t e r a t o r iterator iterator不会失效?
- 3.16 为何 m a p map map和 s e t set set不能像 v e c t o r vector vector一样有个 r e s e r v e reserve reserve函数来预分配数据?
- 3.17 s e t set set的底层实现实现为什么不用哈希表而使用红黑树?
- 3.18 h a s h hash hash _ m a p map map与 m a p map map的区别?什么时候用 h a s h hash hash _ m a p map map,什么时候用 m a p map map?
- 3.19 迭代器失效的问题
- 3.20 S T L STL STL线程不安全的情况
- 4. 补充
1. 基础知识
1.1 内存
1.1.0 内存四区
意义在于:赋予其不同的生命周期,给编程带来更大的灵活性
运行前
代码区:存放函数体的二进制代码,由操作系统管理
共享的
只读的:防止程序意外修改其指令
全局区:存放全局变量和静态变量以及常量,结束后由系统释放
全局区还包括常量区(字符串常量,const修饰的全局常量)
运行后
栈区:由编译器自动分配和释放,存放函数体的参数值、局部变量等
不能返回局部变量的地址,当离开作用域后,开辟在栈区的局部变量会被编译器自动回收
堆区:由程序员分配和释放,若不释放,程序结束后由操作系统释放
分全局堆和局部堆
全局堆就是所有没有分配的空间,局部堆就是用户分配的空间
堆在操作系统对进程 初始化的时候分配,运行过程中也可以向系统要额外的堆
1.1.1 简述C、C++程序编译的内存分配情况
从静态存储区域分配:
内存在程序 编译 时 就已 经 分配 好,这块内 存在 程序 的整 个运行 期间 都存在。速度快不容易出错,因为有系统会善后。例如全局变量, static 变量, 常量字符串等。
在栈上分配:
在执行函数时, 函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。 栈内存分配运算内置于处理器的指令集中, 效率很高, 但是 分配的内存容量有限 。大小为2M。
从堆上分配:
即动态内存分配。程序在运行的时候用 malloc 或 new 申请任意大小的内存,程序员自己负责在何时用 free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活。如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏 ,另外频繁地分配和释放不同大小的堆空间将会产生堆内碎块 。
1.1.2 分配函数与释放函数
C:malloc、calloc、realloc / free
C++:new / delete
大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间用来记录管理信息——分配块的长度,指向下一个分配块的指针等等
1.1.2.1 malloc / free
malloc函数向内存申请一块连续可用的空间
开辟成功则返回一个指向该空间的void* 型指针,所以需要对其进行强制类型转换,转换成我们想要的类型
开辟失败则返回 NULL,所以一定要对malloc的返回值进行检查
free 用来释放动态开辟的内存,而不是释放指针
int* ptr = NULL;
ptr = (int*)malloc(1000*sizeof(int));//开辟一千个int大小的内存,并强制类型转换
if(NULL == ptr){
exit(1);
}
free(ptr);
ptr = NULL;
释放只能一次,如果释放两次及两次以上会出现错误
释放空指针例外,释放空指针其实也等于什么都没做,所以释放空指针释放多少次都没有问题
1.1.2.2 new / delete
new分配内存步骤
调用operator new 函数
调用相应的构造函数构造对象,并传入初值
对象构造完成后,返回一个指向该对象的指针
delete释放内存步骤
调用对象的析构函数
调用operator delete 函数释放内存空间
//开辟变量
int* a = new int(10);
delete a;
//开辟数组
int* arr = new int[10];
delete[] arr;
1.1.2.3 new/delete 与 malloc/free 区别
开辟位置
严格来说,malloc动态开辟的内存在堆区,new开辟的叫做自用存储区
若不重载new操作符,c++编译器一般默认使用堆来实现自用存储,此时等价于堆区
特别:new可以不为对象分配内存
重载
new、delete是操作符,可以重载,只能在C++ 中使用。 malloc、free 是函数,可以覆盖,C、C++ 中都可以使用。
是否调用构造与析构函数
new 可以调用对象的构造函数,对应的delete 调用相应的析构函数。malloc 仅仅分配内存,free 仅仅回收内存,并不执行构造和析构函数
是否需要指定内存大小
malloc 需要显式指出开辟内存的大小,new 无需指定,编译器会自动计算
返回值类型
new返回的是某种数据类型指针,malloc返回的是void 指针,new比malloc更安全
new内存分配失败时,会抛出bac_alloc异常,不会返回NULL;malloc开辟内存失败会返回NULL指针,所以需要判断
1.1.2.4 calloc 、realloc
calloc(number,size):为number个大小为size的元素开辟一块空间,并把每个字节初始化为0
realloc(内存地址,大小):用于调整申请的空间大小
1.1.2.5 在C++中,使用malloc申请的内存能否通过delete释放?使用new申请的内存能否用free?
不能,malloc/free主要为了兼容C,new和 delete 完全可以取代malloc/free的。malloc/free 的操作对象都是必须明确大小的。而且不能用在动态类上。new 和 delete会自动进行类型检查和大小 ,malloc/free不能执行构造函数与析构函数 ,所 以动态对象它是不行的。当然从理论上说使用malloc 申请的内存是可以通过delete释放的 。不过一般不这样写的。而且也不能保证每个C++的运行时都能正常
1.2 预编译
1.2.1 头文件 < > < > <>和 “ “ “ ” ” ”的问题
#include < >:只搜索系统目录,不会搜索本地目录
#include " “:首先搜索本地目录,若找不到才会搜索系统目录
#include<>相较于#include” " 快一些
1.2.2 c o n s t const const 与 # d e f i n e define define 相比有什么优点
const 常量有数据类型,而宏常量没有数据类型,编译器可以对前者进行安全检查。对后者只进行字符替换,没有安全类型检查,并且在字符替换可能会产生意想不到的错误
有些集成化的调试工具可用对const进行调试,但是不能对宏常量进行调试
1.3 宏,内联函数
1.3.1 内联函数
定义:在函数定义体前加入关键字inline,使函数成为内联函数
增加空间消耗换取效率提高,这点与宏一样
内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用
void fun(int x,int y);
inline void fun(int x,int y)//必须放在定义体前面,不能放在声明前面
{
}
适用情况
一个函数不断被重复调用
函数只有简单几行,且函数内不包括for、while、switch语句
1.3.2 内联函数与宏的差别
内联函数要做类型检查,而宏不需要
宏是在代码处不加任何验证的简单替代,而内联函数是将代码直接插入到调用处,而减少了普通函数调用时的资源消耗
1.3.3 写一个 “标准”宏 m i n min min
#define min(a,b)((a)<=(b)?(a):(b))
特别注意括号
1.3.4 t y p e d e f typedef typedef 和 d e f i n e define define 有什么区别
用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义常量,以及书写复杂使用频繁的宏。
执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。
作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在define 声明后的引用都是正确的。
对指针的操作不同:typedef 和define 定义的指针时有很大的区别。
注意:typedef 定义是语句, 因为句尾要加上分号。 而define不是语句,千万不能在句尾加分号
1.4 指针
1.4.1 指针常量和常量指针
常量:const int p = a;
常量指针:const int* p = &a; 、 int const *p = &a;
指针常量:int * const p = &a;
常量指针常量:const int * const p = &a;
const象征内容,* 象征地址
指针常量定义必须初始化
谁在前就先读,谁就不许变
常量指针:const修饰的是指针,指针指向可以改变,但是指针指向的值不能改变
指针常量:修饰的是常量,指针指向不可改变,但是在指针指向的值可以改变
常量指针常量:指针指向和指针指向的值均不可改变
int a = 10;
int b = 10;
//const修饰的是指针,指针指向可以改,指针指向的值不可以更改
const int * p1 = &a;
p1 = &b; //正确
//*p1 = 100; 报错
//const修饰的是常量,指针指向不可以改,指针指向的值可以更改
int * const p2 = &a;
//p2 = &b; //错误
*p2 = 100; //正确
//const既修饰指针又修饰常量
const int * const p3 = &a;
//p3 = &b; //错误
//*p3 = 100; //错误
1.4.2 指针函数和函数指针
- 指针函数:类型说明符 * 函数名(参数);
是一个函数,返回一个指针,实际上就是返回一个地址给调用函数
在调用指针函数时,需要一个同类型的指针来接收其函数的返回值。
也可以将其返回值设为void * 类型,调用时强制转换返回值为自己想要的类型 - 函数指针:类型说明符 (* 函数名)(参数) ;
int (*FunPointerName)(int a,int b);
是一个指针,指向函数的指针,包含了函数的地址,可以用它来调用函数,本质是一个指针变量,该指针指向这个函数
把函数地址赋值给函数指针
FunPoniterName = &FunctionName;
FunPoniterName = FunctionName;
调用函数指针
x = (*FunPointerName)(参数);
x = FunPointerName(参数);
#include <iostream>
using namespace std;
int add(int a,int b){
return a + b;
}
int main(){
int (*fun)(int a,int b);
fun = add;
cout << fun(10,20) << endl;
}
1.4.3 指针数组数组指针
指针数组:int *a[10];
是一个数组,a[ ]里面存的是地址
数组指针:int (*a)[10];
是一个指针,指向整个数组
1.4.4 函数传参
函数传参的三种方式:值传递,地址传递,引用传递
//值传递:就是函数调用时实参将数值传入给形参
//值传递时,如果形参发生,并不会影响实参
void swap01(int a,int b){
int temp = a;
a = b;
b = temp;
}
//地址传递:利用指针作函数参数,可以修改实参的值
void swap02(int* a,int* b){
int temp = *a;
*a = *b;
*b = temp;
}
//引用传递
//通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单
void swap03(int& a;int& b){
int temp = a;
a = b;
b = temp;
}
1.4.5 一些定义
定义 | 说明 |
---|---|
int a | 一个整型数 |
int *a | 一个指向整型的指针 |
int **a | 一个指向指针的指针,它指向的指针是一个整数类型 |
int a[10] | 一个有10个整型的数组 |
int *a[10] | 指针数组:一个有10个指针的数组,指针指向整型 |
int (*a)[10] | 数组指针:一个指向有10个整型数数组的指针 |
int (*a)(int) | 函数指针:一个指向函数的指针,该函数有一个整型参数,并返回一个整型 |
int (*a[10])(int) | 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型 |
1.4.6 指针与引用的区别
指针有自己的一块空间,而引用只是一个别名,所以不能创建引用的引用,引用必须初始化,而指针可用为空;
使用sizeof看一个指针的大小是4,而引用的大小则是被引用对象的大小;
指针和引用使用++运算符的意义不一样;引用自增自减时,是引用所代表的空间的值发生变化,而指针自增自减时是指针指向的位置发生变化
作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
可以有const指针,但是没有const引用;
指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
指针可以有多级指针(**p),而引用止于一级;
如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
引用本身不是一种数据类型,因此引用本身并不占存储单元,系统也不给引用分配存储单元,不能建立数组的引用
1.4.7 t h i s this this指针
this指针本质上是一个函数参数,只是编译器隐藏起形式的,语法层面上的参数
this 只能在成员函数中使用,全局函数和静态函数(属于类,不属于对象)都不能使用this
this 在成员函数的开始前构造,在成员的结束后清除。调用类成员函数时,编译器将类的指针作为参数传递进去
用途:
当形参与成员变量同名时,可以用this指针来区分
在类的非静态成员函数中返回对象本身,可用return *this;
this 指针并不占用对象空间,所以成员函数的参数,不管是不是隐含的,都不会占用对象空间,只会占用参数传递时的栈空间,或者直接占用一个寄存器
this 会因编译器不同而有不同的存放位置,可能是堆、栈、也可能是寄存器
this 指针只有在成员函数中才有定义,不能通过对象使用this指针,无法知道一个对象的this指针位置(只有在成员函数里才有this指针的位置,可通过&this获取)
1.4.8 指针和句柄
句柄和指针其实是两个截然不同的概念,window系统用句柄标记系统资源,隐藏系统的信息,它一个一个32bit的整数
而指针则标记某个物理内存地址,两者概念不同
1.4.9 如何避免“野指针”
指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向NULL。
指针p被free或者delete之后,没有置为NULL。解决办法:指针指向的内存空间被释放后指针应该指向NULL。
指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向NULL。
1.4.10 空指针与迷途指针区别
当delete一个指针的时候,实际上仅仅是让编译器释放内存,但指针本身依然存在,此时他就是一个迷途指针
可令ptr = 0; 使迷途指针变为空指针
1.5 c o n s t const const
任何不会修改数据成员的函数都应该声明为const 类型
在参数中使用const应该使用引用或指针,而不是一般的对象实例
除了重载操作符外一般不要将返回值类型定为对某个对象的const引用
1.5.1 c o n s t const const 使用
const使用:定义常量、修饰函数参数、修饰函数返回值
const 修饰类的成员变量,表示成员变量,不能被修改
如果const构成函数重载,const对象只能调用const函数,非const对象优先调用非const函数
const 函数只能调用const函数,非const函数可以调用const函数
类体外定义的const成员函数,在定义和声明处都需要const修饰符
1.5.2 c o n s t const const 作用
1.5.3 如何修改 c o n s t const const成员函数
用mutable修饰成员变量名后,就可以修改类成员变量
1.5.4 将 c o n s t const const类型转化为非 c o n s t const const类型
采用const_cast 进行转换。
用法:const_cast <type_id> (expression)
1.6 s i z e o f sizeof sizeof
数据对齐原则:是指数据所在的内存地址必须是该数据长度的整数倍。
1.6.1 s i z e o f sizeof sizeof 和 s t r l e n strlen strlen 的区别
sizeof是一个操作符,strlen是库函数。
sizeof的参数可以是数据的类型,也可以是变量、函数;而strlen只能用char*做参数且且以结尾为‘\0’的字符串。
编译器在编译时就计算出了sizeof的结果,而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度。
数组做sizeof的参数不退化,传递给strlen就退化为指针了
sizeof不能返回被动态分配的数组或外部的数组的尺寸
sizeof不能作用于函数类型,不完全类型或位字段,不完全类型是指具有未知存储大小数据的类型,如未知存储大小的数组类型、未知内容的结构或联合类型、void类型等
1.6.2 s i z e o f sizeof sizeof 的使用场合
其中一个主要用途就是与存储分配和I/O系统那样的例程通信
可以查看某种类型的对象在内存中所占的单元字节
在动态分配一个对象时,可以让系统知道要分配多少内存
便于一些类型的扩充。在window中有很多结构类型就有一个专用的字段来存放该类型的字节大小
如果操作数是函数中的数组形参或函数类型的形参,sizeof给出其指针的大小
1.7 强制类型转换运算符
static_cast
用于非多态类型的转换
不执行运行时类型检查(转换安全性不如 dynamic_cast)
通常用于转换数值数据类型(如 float -> int)
可以在整个类层次结构中移动指针,子类转化为父类安全(向上转换),父类转化为子类不安全(因为子类可能有不在父类的字段或方法)
用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知
dynamic_cast
用于多态类型的转换,只能用于含有虚函数的类
执行行运行时类型检查
只适用于指针或引用
对不明确的指针的转换将失败(返回 nullptr),但不引发异常
可以在整个类层次结构中移动指针,包括向上转换、向下转换
const_cast
用于将const变量转为非const
用于删除 const、volatile 和 __unaligned 特性(如将 const int 类型转换为 int 类型 ) reinterpret_cast
用于位的