01
02
new 和 malloc
malloc 和 free,称作 c 的库函数
new 和 delete,称作运算符
new不仅可以做内存开辟,还可以做内存初始化操作
malloc开辟内存失败,是通过返回值和nullptr做比较:而new开辟内存失败,是通过抛出bad_alloc类型的异常来判断的。
new 可以认为是 malloc + 构造函数, delete 可以认为是 free + 析构函数
引用和指针区别
引用是一种更安全的指针。
- 引用是必须初始化的,而指针可以不初始化
- 引用只有一级引用,没有多级引用;指针可以一级指针,也可以多级指针
- 定义一个引用变量和定义一个指针变量,其汇编指令是一模一样的;通过引用变量修改所引用内存的值,和通过指针解引用修改指针指向的内存的值,其底层指令也是一模一样的
右值引用
- int &&c = 20; 专门用来引用右值类型,指令上,可以自动产生临时量然后直接引用临时量 c = 40;
- 右值引用变量本身是一个左值,只能用左值引用来引用它
- 不能用一个右值引用变量来引用一个左值。
const
const 修饰的变量不能够再作为左值!!初始化完成后,值不能被修改!!
C和C++中const的区别?
C中,const就是当作一个变量来编译生成指令的。C++中,所有出现const常量名字的地方,都被常量的初始化替换了!!!
const和一级、多级指针的结合
const修饰的量常出现的错误是:
-
常量不能再作为左值 = 直接修改常量的值
-
不能把常量的地址泄露给—个普通的指针或者普通的引用变量 = 间接修改常量的
const和一级指针的结合
-
c++的语言规范:const修饰的是离它最近的类型
const int *p = &a; *p = 20 p = &b
可以任意指向不同的int类型的内存,但是不能通过指针间接修改指向的内存的值
总结 const 和指针的类型转换公式:
int* const int* //是错误的!
const int* int* //是可以的!
int** const int** //是错误的!
const int** int** //是错误的!
int** int*const* //是错误的!
int*const* int** //是可以的!
inline 函数和普通函数的区别
inline 内联函数:在编译过程中,就没有函数的调用开销了,在函数的调用点直接把函数的代码进行展开处理了。
inline 只是建议编译器把这个函数处理成内联函数。但是不是所有的 inline 函数都会被编译器处理为内联函数,例如 递归函数。
debug 时候,inline 不起作用。inline 只有在 release 下才起作用。
函数重载
c++为什么支持函数重载,c语言不支持函数重载?
c++代码产生函数符号的时候,函数名+参数列表类型组成的!
c代码产生函数符号的时候,函数名来决定!
什么是函数重载?
- 一组函数,其中函数名相同,参数列表的个数或类型不同,那么这一组函数就称为 函数重载。
- 一组函数要称得上函数重载,一定要处在同一作用域当中。
- const 或者 volatile 的时候。
- 一组函数,函数名相同,参数列表相同,返回值不同,不叫重载。
什么是多态?
静态:编译时期的多态。(多种多样的形态)。如 函数重载
动态:运行时期的多态
C 调用 C++ 无法直接调用,怎么办?
把 C++ 源码括在 extern “C” 里面。还是在 C++ 代码里面写 extern C。
C++ 调用 C 代码,无法直接调用,怎么办?
把 C 函数的声明括在 extern “C” 里面
extern "C"{
int sum(int a, int b){
return a + b;
}
}
只要是 C++ 编译器,都内置了 _cplusplus 这个宏名
#ifdef __cplusplus
extern "C"{
#endif
int sum(int a, int b){
return a + b;
}
#ifdef __cplusplus
}
#endif
03
类和对象、this 指针
面向对象程序设计
Object-oriented programming
OOP 语言的四大特征是什么?
抽象。 封装/隐藏。 继承。 多态。
this 指针
this 是 C++ 中的一个关键字,也是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。
构造函数与析构函数
构造函数
构造函数可以带参数,因此可以提供多个构造函数。定义对象时自动调用;可以重载;构造完成,对象就产生了。
析构函数
析构函数不带参数,所以析构函数只能有一个。不带参数;不能重载,只有一个;析构完成,对象就不存在了。
析构函数调用以后,对象就不存在了。不能再调用对应方法了(堆内存的非法访问)
何时使用
.data 对象(数据段对象)
程序启动时构造,结束时析构
heap
new 时候构造,delete 时候析构。new = malloc(内存开辟)+ 构造函数。 delete = 析构 + free(内存释放)
stack
进函数到定义的地方构造,出函数作用域析构
对象深拷贝与浅拷贝
SeqStack s; //没有提供任何构造函数时,会为你生成默认构造函数和析构函数
SeqStack s1(10);
SeqStack s2 = s1; // #1 会调用拷贝构造函数 //浅拷贝,对象默认的拷贝构造是做内存的数据拷贝,关键是对象如果占用外部资源, //那么浅拷贝就出问题了。因为它们会指向同一个内存指针,这时候s2去释放内存,s1就有问题了。
SeqStack s3(s1); // #2。 #1与#2一样
浅拷贝:自定义拷贝构造函数和赋值重载函数
类和对象代码应用
void push(int val){ // 入队操作
if (full()){
resize();
}
_pQue[_rear] = val;
_rear = (_rear + 1) % _size;
}
void pop(){ // 出队
if(empty()){
return;
}
_front = (_front + 1) % _size;
}
void resize(){ // 扩容
int *ptmp = new int[2 * _size];
int index = 0;
for(int i = _front; i != _rear; i = (i + 1) % _size){
ptmp[index++] = _pQue[i];
}
delete _pQue;
_pQue = _ptmp;
_front = 0;
_rear = index;
_size *= 2;
}
构造函数的初始化列表
可以指定当前对象成员变量的初始化方式
类的成员变量-构造方法/变量
普通的成员方法
编译器会添加一个 this 形参变量
- 属于类的作用域
- 调用该方法时,需要依赖一个对象
- 可以任意访问对象的私有成员变量。 public private
static 静态成员方法
不会生成 this 形参
static void hanshu(){}
没有 this 指针,可以直接用类名调用。当想访问所有方法共享的信息时,可以用静态方法。
而普通方法会产生 this 指针,需要调用一个对象,接受一个对象的地址。
- 属于类的作用域
- 用类名作用域来调用方法
- 可以任意访问对象的私有成员,仅限于不依赖对象的成员(只能调用其他的 static 静态成员)、
static int _count;
不属于对象,而是属于类级别的声明
const 常成员方法
const CGoods *this
- 属于类的作用域
- 调用依赖一个对象,普通对象或常对象都可以
- 可以任意访问
04
函数模版
// 函数模板,是不进行编译的,因为类型还不知道
// 函数实例化,函数调用点进行实例化
// 模板函数,才是要被编译器所编译的
// 模板的实参推演,可以根据用户传入的实参的类型,来推导出模板类型
// 模板的特例化(专用化),特殊(不是编译器提供的,而是用户提供的)实例化
template<typename T> //定义一个模板参数列表
bool compare(T a, T b) // compare 是一个函数模板
{
cout << "template compare" << endl;
return a > b;
}
// 在函数调用点,编译器用用户指定的类型,从原模板实例化一份函数代码出来
/*
bool compare<int>(int a, int b) // compare 是一个函数模板
{
return a > b;
}
*/
// 针对 compare 函数模板,提供 const char* 类型的特例化版本
template<>
bool compare<const char *>(const char *a, const char *b)
{
return strcmp(a, b) > 0;
}
int main()
{
// 函数调用点
compare<int> (10, 20);
compare<double> (10.5, 20.5);
return 0;
}
模板代码是不能在一个文件定义,在另一个文件使用
模板代码调用之前,一定要看到模板定义的地方,这样的话,模板才能够进行正常的实例化,产生能够被编译器编译的代码
所以,模板代码都是放在头文件中,然后在源代码当中直接进行
#include
包含。
类模板
容器的空间配置器 allocator
做四件事情, 内存开辟/内存释放 对象构造/对象析构
向量容器 vector
template<typename T>
class vector
{
public:
vector(int size = 10)
{
_first = new T[size];
_last = _first;
_end = _first + size;
}
~vector()
{
delete[] _first;
_first = _last = _end = nullptr;
}
vector(const vector<T> &rhs)
{
int size = rhs._end - rhs._first;
_first = new T[size];
}
private:
T *_first; // 指向数组起始的位置
T *_last;// 指向数组中有效元素的后继位置
T *_end;// 指向数组空间的后继位置
}
05
复数类 CComplex
编译器做对象运算的时候,会调用对象的运算符重载函数(优先调用成员方法);如果没有成员方法,就在全局作用域找合适的运算符重载函数。
模拟实现 string 类
String 的迭代器 iteratior 实现
vector 的迭代器 iteratior 实现
#include <vector>
#include <iostream>
using namespace std;
struct Point
{
double x;
double y;
Point()
{
x = 0;
y = 0;
}
};
int main()
{
vector<Point> m_testPoint;
m_testPoint.clear();
m_testPoint.shrink_to_fit();
for (int i = 0; i<10; ++i)
{
Point temp;
temp.x = i*i;
temp.y = i*i;
m_testPoint.push_back(temp);
}
//第一种遍历方式,下标
cout << "第一种遍历方式,下标访问" << endl;
for (int i = 0; i<m_testPoint.size(); ++i)
{
cout << m_testPoint[i].x << " " << m_testPoint[i].y << endl;
}
//第二种遍历方式,迭代器
cout << "第二种遍历方式,迭代器访问" << endl;
for (vector<Point>::iterator iter = m_testPoint.begin(); iter != m_testPoint.end(); iter++)
{
cout << (*iter).x << " " << (*iter).y << endl;
}
//第三种遍历方式,auto关键字
cout << "C++11,第三种遍历方式,auto关键字" << endl;
for (auto iter = m_testPoint.begin(); iter != m_testPoint.end(); iter++)
{
cout << (*iter).x << " " << (*iter).y << endl;
}
//第四种遍历方式,auto关键字的另一种方式
cout << "C++11,第四种遍历方式,auto关键字" << endl;
for (auto i : m_testPoint)
{
cout << i.x << " " << i.y << endl;
}
return 0;
}
重看
容器的迭代器失效问题
未看
new 和 delete
1. malloc 和 new 的区别?
-
malloc 按字节开辟内存;而 new 开辟内存时需要指定类型
new int [10]
。所以 malloc 开辟内存返回的都是
void* operator new -> int*
- malloc 只负责开辟空间, new 不仅仅有 malloc 的功能,可以进行数据的初始化。
- malloc 开辟内存失败返回 nullptr 指针; new 抛出的是 bad_alloc 类型的异常。
2. free 和 delete 的区别?
- delete:调用析构函数;
new 和 能混用吗?
06
继承
继承的本质和原理
继承的本质:a. 代码的复用 b.
类和类之间的关系:
组合: a part of … …一部分的关系
继承: a kind of … …一种的关系
private 只有自己或者友元可以访问私有的成员
继承方式 | 基类的访问限定 | 派生类的访问限定 | (main)外部的访问限定 |
---|---|---|---|
public (class B : public A) | public |
public 派生类里面可以访问 |
外部可以访问 |
protected |
protected 派生类里面可以访问 |
外部不可以访问 | |
private |
不可见 派生类里面不可以访问 |
外部不可以访问 | |
protected(class B: protected A) | public |
protected 相当于降级为 protected |
外部不可以访问 |
protected |
protected 派生类里面可以访问 |
外部不可以访问 | |
private |
不可见 派生类里面不可以访问 |
外部不可以访问 | |
protected(class B: private A) | public |
private 相当于降级为 private 派生类里面可以访问 |
外部不可以访问 |
protected |
private 相当于降级为 private 派生类里面可以访问 |
外部不可以访问 | |
private |
不可见 派生类里面不可以访问 |
外部不可以访问 |
总结:
- 外部只能访问对象 public 成员,protected 和 private 的成员无法直接访问
- 在继承结构中,派生类从基类可以继承过来 private 的成员,但是派生类无法直接访问
- protected 和 private 的区别? 在基类中定义的成员,想被派生类访问,但是不想被外部访问,那么在基类中,把相关成员定义成 protected 保护的;如果派生类和外部都不打算访问,那么在基类中,就把相关成员定义成 private 私有的。
默认的继承方式是什么?
要看 派生类是用 class 定义的,还是 struct 定义的。
class 定义派生类,默认继承方式是 private 私有的。struct 定义派生类,默认继承方式是 public 公有的。
class 的成员默认是 private 权限,struct 默认是 public 权限。(与上面继承对应)
派生类的构造过程
派生类从基类可以继承来所有的成员(变量和方法),除构造函数和析构函数
派生类怎么初始化从基类继承来的成员变量呢?
通过调用基类相应的构造函数来初始化。
派生类的构造函数和析构函数,负责初始化和清理派生类部分
派生类从基类继承来的成员由基类的构造函数和析构函数负责。
派生类对象构造和析构的过程是:
-
派生类调用基类的构造函数,初始化从基类继承来的成员
-
调用派生类自己的构造函数,初始化派生类自己特有的成员
… 派生类对象的作用域到期了
-
调用派生类的析构函数,释放派生类成员可能占用的外部资源(堆内存,文件)
-
调用基类的析构函数,释放派生类内存中,从基类继承来的成员可能占用的外部资源(堆内存,文件)
重载、隐藏、覆盖
重载关系
一组函数要重载,必须处在同一作用域中;而且函数名字相同,参数列表不同
隐藏关系
在继承结构中,派生类的同名成员把基类的同名成员给隐藏了,也就是调用的时候调用的是派生类的成员函数。要调用基类那就加作用域,(比如 Base::show)
把继承结构也说成从上(基类)到下(派生类)的结构
Base(10);
Derive(20);
b = d; // 基类对象b <- 派生类对象d 类型从下到上的转换 允许
d = b; // 派生类对象d <- 基类对象b 类型从上到下的转换 不允许
Base *pb = &d; // 基类指针(引用) <- 派生类对象 类型从下到上的转换 允许
Derive *pd = &b; // 派生类指针(引用) <- 基类对象。 类型从上到下的转换 不允许
// 在继承结构中进行上下的类型转换,默认只支持从下到上的类型转换
虚函数、静态绑定和动态绑定
静态绑定:静态–编译时期;绑定–函数的调用
动态绑定:动态–运行时期;绑定–函数的调用
虚函数 virtual
- 一个类里面定义了虚函数,那么编译阶段,编译器会给这个类类型产生一个唯一的 vftable 虚函数表,虚函数表中主要存储的内容就是 RTTI 指针和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的 .rodata 区(read only data)。
- 一个类里面定义了虚函数,那么这个类定义的对象,其运行时,内存中开始部分,多存储一个 vfptr 虚函数指针,指向相应类型的虚函数表 vftable。一个类型定义的 n 个对象,它们的 vfptr 指向的都是同一张虚函数表。
- 一个类里面虚函数的个数,不影响对象内存大小(vfptr),影响的事虚函数表的大小。
- 如果派生类中的方法,和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是 virtual 虚函数,那么派生类的这个方法,自动处理成虚函数。
覆盖:基类和派生类的方法,返回值、函数名以及参数列表都相同,而且基类的方法是虚函数,那么派生类的方法就自动处理成虚函数,它们之间成为覆盖关系(虚函数表中虚函数地址的覆盖。派生类存在和基类 返回值、函数名、参数列表都相同的函数,会进行覆盖,相当于重写。
)
如果发现函数是普通函数,就进行静态绑定
如果发现函数是虚函数,就进行动态绑定了。
cl *.cpp /dlreportSingleClassLayoutDerive
虚析构函数
哪些函数不能实现成虚函数?
虚函数依赖:
- 虚函数能产生地址,存储在 vftable 当中
- 对象必须存在,(vfptr -> vftable -> 虚函数地址)
构造函数:
- 构造函数前面不能加 virtual
- 构造函数中(调用的任何函数都是静态绑定)调用虚函数,也不会进行动态绑定
派生类对象构造过程 先调用基类的构造函数,然后才调用派生类的构造函数
- static 静态成员方法。对象都没有,也就不能 static 前面加 virtual
再谈动态绑定
虚函数和动态绑定的问题:是不是虚函数的调用一定就是动态绑定? 肯定不是的!
在类的构造函数中,调用虚函数,也是静态绑定(构造函数中调用其他函数(虚),不会发生动态绑定)
用对象本身调用虚函数,属于静态绑定
动态绑定,必须由指针调用虚函数(
Base *pb1 = &b;
),或者必须由引用变量调用虚函数(
Base &rb1 = b;
)
虚函数通过指针或者引用变量调用,才发生动态绑定
多态
静态(编译时期)的多态:函数重载、模板(函数模板和类模板)
动态(运行时期)多态:在继承结构中,基类指针(引用)指向派生类对象,通过该指针(引用)调用同名覆盖方法(虚函数)。基类指针指向哪个派生类对象,就会调用哪个派生类对象的同名覆盖方法,称为多态。
多态底层是通过动态绑定来实现的。
继承多态笔试题实战分析_ev.mp4
还没看!!!
07
虚基类和虚继承
多重继承:代码的复用 一个派生类有多个基类
抽象类(有纯虚函数的类) / 虚基类
virtual:
看虚基类这一章
-
修饰成员方法是虚函数
-
可以修饰继承方式,是虚继承。被虚继承的类,称作虚基类);
基类指针指向派生类对象,永远指向的是派生类基类部分数据的起始地址
菱形继承
四种类型转换
C++ 语言级别提供的四种类型转换方式
int a = (int) b;
const_cast : 去掉(指针或者引用)常量属性的一个类型转换
static_cast : 提供编译器认为安全的类型转换(没有任何联系的类型之间的转换就被否定)
reinterpret_cast : 类似于 C 风格的强制类型转换
dynamic_cast : 主要用在继承结构中,可以支持RTTI类型识别的上下转换
08
stl六大组件
stl内容学习简介
一、标准容器
vector
deque
list
stack
queue
priority queue
无序关联容器
链式哈希表 增删查 O(1)
unordered_set
unordered_multiset
unordered_map
unordered_multimap
有序关联容器
红黑树 增删查O(
l
o
g
2
n
log_2{n}
l
o
g
2
n
) 2是底数(树的层数,树的高度)
set
multiset
map
multimap
二、 近容器
数组,string,bitset
三、 迭代器
iterator 和 const_iterator
reverse_iterator 和 const_reverse_iterator
四、 函数对象(类似 C 的函数指针)
五、泛型算法
sort, find, find_if, binary_search, for_each
vector
向量容器。
底层数据结构:动态开辟的数组,每次以原来空间大小的 2 倍进行扩容。
vector<int> vec;
// 增加
vec.push_back(20); //末尾添加元素 O(1) 导致容器扩容
vec.insert(it, 20); // it 迭代器指向的位置添加一个元素 20 O(n) 导致容器扩容
// 删除
vec.pop_back(); // 末尾删除元素 O(1)
vec.erase(it); // 删除 it 迭代器指向的元素 O(1)
// 查询
operator[] // 下标的随机访问 vec[5] O(1)
// iterator迭代器进行遍历。(推荐)
// find, for_each
// foreach 底层就是通过 iterator 实现的。
// 注意!对容器进行连续插入或者删除操作(insert/erase),一定要更新迭代器,否则第一次 insert 或者 erase 完成,迭代器就失效了。
常用方法介绍:
size()
empty()
reserve(20): vector 预留空间的 只给容器底层开辟指定大小的内存空间,并不会添加新的元素
resize(20): 容器扩容用的
swap: 两个容器进行元素交换
deque 和 list
deque
双端队列容器
底层数据结构:动态开辟的二维数组,一维数组从 2 开始,以 2 倍的方式进行扩容,每次扩容后,原来第二维的数组,从新的第一维数组的下标 oldsize/2 开始存放,上下都预留相同的空行,方便支持 deque 的首尾元素添加。
deque<int> deq;
// 增加
deq.push_back(20); // 从末尾添加 O(1)
deq.push_front(20); // 从首部添加元素 O(1)
deq.insert(it, 20); // it 指向的位置添加元素 O(n)
// 删除
deq.pop_back(); // 从末尾删除元素 O(1)
deq.pop_front(); // 从首部删除元素 O(1)
deq.erase(it); // 从 it 指向的位置删除元素 O(n)
//查询搜索
// iterator(连续的 insert 和 erase 一定要考虑迭代器失效的问题)
list
链表容器
底层数据结构:双向的循环链表 pre data next
list<int> mylist;
// 增加
mylist.push_back(20); // 从末尾添加 O(1)
mylist.push_front(20); // 从首部添加元素 O(1)
// 链表中进行 insert 的时候,先要进行一个 query 查询操作,对于链表来说,查询操作效率就比较慢了
mylist.insert(it, 20); // it 指向的位置添加元素 O(1)
// 删除
mylist.pop_back(); // 从末尾删除元素 O(1)
mylist.pop_front(); // 从首部删除元素 O(1)
mylist.erase(it); // 从 it 指向的位置删除元素 O(1)
//查询搜索
// iterator(连续的 insert 和 erase 一定要考虑迭代器失效的问题)
deque 和 list 比 vector 容器多出来的增加删除函数接口:push_front 和 pop_front