笔记②:牛客校招冲刺集训营---C++工程师(面向对象(友元、运算符重载、继承、多态) -- 内存管理 -- 名称空间、模板(类模板/函数模板) -- STL)
0618
C++工程师
- 第5章 高频考点与真题精讲
- 5.1 指针 & 5.2 函数
- 5.3 面向对象(和5.4、5.5共三次直播课)
- 5.3.1 - 5.3.11
- 5.3.12-14 友元
- 友元全局函数
- 友元类
- 友元成员函数
- 友元的注意事项
- 5.3.15-23 运算符重载 -->静态多态
- 运算符 + = == != ++ () << 重载
- ☆☆☆指针运算符重载(* 和 ->)(写个智能指针类)
- 运算符重载的应用:封装字符串类
- 5.3.24 类的自动类型转换和强制类型转换(关键字`explicit`)
- 5.3.25-34 继承
- 继承的概念、好处、弊端
- 继承的基本语法
- protected访问权限
- 继承中的对象模型(成员变量和成员函数分开存储)
- 继承中的构造和析构顺序
- 访问继承中的 非静态 同名 成员
- 访问继承中的 静态 同名 成员
- 总结:如何访问继承中的同名成员(静态成员和非静态成员)
- 多继承/多重继承
- 菱形继承-->虚继承、虚基类☆☆☆
- 虚继承的实现原理
- 5.3.35-38 多态(一般是指动态多态)
- 静态联编和动态联编(静态多态和动态多态)
- 虚函数的原理(多态的底层原理)
- 多态案例(开闭原则:对扩展进行开放,对修改进行关闭)
- 纯虚函数 --> 抽象类
- 虚析构和纯虚析构(解决 调用不到子类析构函数 的问题)
- C++面试宝典-->第一章-->1.3 面向对象
- 5.6 内存管理①(结合计算机操作系统笔记)
- 1.重定位(地址转换:逻辑地址-->物理地址)
- 2.交换技术
- 3.分段(将各段分别放入内存)
- 4.分页(从连续到离散)
- 5.7 内存管理②(结合计算机操作系统笔记)
- 5.段页式管理 和 虚拟内存(虚拟地址空间/虚拟内存地址)
- 6.虚拟内存和段页式存储管理知识补充
- 参考链接1
- 参考链接2
- 参考链接3
- 1.如何将段和页结合在一起
- 2.段页结合时进程对内存的使用
- 看个例子(逻辑地址、虚拟地址、物理地址)
- 虚拟内存和虚拟存储器的区别?
- 参考链接4、5
- 7.(5和6的)总结☆☆☆☆☆
- 7.1 虚拟内存的提出是为了解决什么问题?
- 7.2 虚拟内存的原理解释
- 7.3 分页和页表
- 7.4 虚拟内存地址(逻辑地址)-->物理地址 (快表、缺页中断)
- 7.5 虚拟内存的功能
- 7.6 段页式内存管理与虚拟内存☆☆☆
- C++面试宝典--> 1.2 C++内存 和 第2章 操作系统
- 5.7 名称空间、模板
- 补充:内存对齐/字节对齐
- 名称空间
- 作用域解析运算符(两个冒号`::`)
- 名称空间
- using声明和using编译指令
- 模板
- 函数模板
- ①函数模板语法
- ②函数模板和普通函数的区别
- ③函数模板调用规则
- ④模板实现机制及局限性
- 类模板
- ①类模板基础语法
- ②类模板中成员函数的创建时机
- ③类模板做函数参数
- ④类模板派生类
- ⑤类模板成员函数类内实现
- ⑥类模板成员函数类外实现
- ⑦类模板分文件编写(类模板文件 .hpp)
- ⑧模板类碰到友元函数
- ⑨模板案例---数组类封装
- 5.8 STL(标准模板库)
- 总结 常用容器的**排序**的区别:
- 0.自定义排序规则的实现方式
- 1.vector容器,deque容器;&& 2.list容器
- 3.set容器,map容器:
- 4.汇总
- 函数对象 & 谓词
- 内建函数对象
- STL—常用算法
- 5.9 C++新特性
第5章 高频考点与真题精讲
5.1 指针 & 5.2 函数
5.3 面向对象(和5.4、5.5共三次直播课)
5.3.1 - 5.3.11
见笔记①:牛客校招冲刺集训营—C++工程师
5.3.12-14 友元
友元分为三种:
- 友元全局函数
- 友元成员函数
- 友元类
友元的目的就是让一个 函数或者类 能够访问另一个类中私有成员,关键字是 friend
。重载<<
运算符时,会用到友元。
友元全局函数
一个全局函数想访问一个类中的私有成员变量,那么就把这个全局函数设置为这个类的友元(friend),就可以访问了。
友元类
一个好朋友类想访问Building
类中的私有成员变量,那么就把好朋友类设置为Building
类的友元(friend),就可以访问了。
友元成员函数
好朋友类的成员函数visit()
访问另一类的对象的私有成员变量:
成员函数visit()
在类外实现:
Building类中把GoodFriend类的成员函数visit()
设置为友元,就可以访问自己的m_BedRoom
属性;
但GoodFriend类的成员函数visit1()
没被设置为友元,所以还是无法访问到自己的m_BedRoom
属性。
友元的注意事项
友元关系:
1.不能被继承; 2.是单向的; 3.不具有传递性。
5.3.15-23 运算符重载 -->静态多态
运算符 + = == != ++ () << 重载
C++笔记10:运算符重载
概念:
对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
例如:加号运算符重载的作用就是实现两个自定义数据类型(类)相加的运算。
实现的方式有两种:
- 利用成员函数重载
- 利用全局函数重载
C++笔记10:运算符重载中实现了:
- 加号运算符(
+
)重载 - 左移运算符(
<<
)重载
改进 - 递增运算符(
++
)重载(前置++ 和 后置++) - 赋值运算符(
=
)重载
如果类的成员变量在堆区,做赋值操作时就会出现深拷贝与浅拷贝问题。 - 关系运算符重载(
==
和!=
) - 函数调用运算符
()
重载—仿函数(在STL
中用的比较多)
运算符 | 成员函数实现 | 全局函数实现 | 备注 |
---|---|---|---|
加号运算符(+ )重载
|
Students operator+(const Students& stu){}
|
friend Students operator+(const Students& stu1, const Students& stu2){}
|
|
左移运算符(<< )重载
|
friend ostream& operator<<(ostream& cout, const Students& stu){} //可以实现连续cout输出
|
||
递增运算符(++ )重载
|
前置++:Students& operator++(){} 后置++: Students operator++(int){} //这个int是为了和前置++形成重载,以通过编译,int本身没啥用
|
前置++:friend Students& operator++(Students& stu){} 后置++: friend Students operator++(Students& stu,int){} //这个int是为了和前置++形成重载,以通过编译,int本身没啥用
|
|
关系运算符(== 和 != )重载
|
bool operator==(Students& stu) {}
|
friend bool operator==(const Students& stu1, const Students& stu2){}
|
|
赋值运算符(= )重载
|
void operator=(const Person& p){} 升级版: Person& operator=(const Person& p){} //可以实现连续赋值
|
||
函数调用运算符() 重载
|
也叫仿函数 |
(以上是笔记中复制来的内容)
☆☆☆指针运算符重载(* 和 ->)(写个智能指针类)
(视频课从01:32:10开始)
指针的操作就是解引用*
和箭头->
,所以就是重载这两个运算符。
示例:
#include<iostream>
using namespace std;class Person{public:Person(int age){cout << "Person的构造函数" << endl;m_Age = age;}//输出成员信息:void showAge(){cout << "m_Age = " << this->m_Age << endl;}~Person(){cout << "Person的析构函数" << endl;}
private:int m_Age;
};//智能指针类:
class SmartPointer{public://重载箭头运算符->Person* operator->(){return this->m_Person;}//重载解引用运算符*Person& operator*(){return *m_Person;}SmartPointer(Person* p){cout << "SmartPointer的构造函数" << endl;m_Person = p;}~SmartPointer(){cout << "SmartPointer析构函数" << endl;if(this->m_Person != nullptr){delete this->m_Person;this->m_Person == nullptr;}}
//private:Person* m_Person;//内部维护的Person的指针
};int main(){Person* p = new Person(30);p->showAge();(*p).showAge();//正常应该执行下面这行,如果忘了就会造成内存泄漏,所以就用下面的智能指针类//delete p;cout << "=================================" << endl;//智能指针类就是把p替换成了sp,并且在其析构函数中将new的内容delete掉,这样就解决了可能造成的内存泄漏问题SmartPointer sp(p);sp->showAge();//相当于sp.operator->()->showAge(); 其中sp.operator->()就相当于psp.operator->()->showAge();//编译器会把sp.operator->()->showAge()优化成sp->showAge();(*sp).showAge();//相当于sp.operator*().showAge();其中sp.operator*()相当于*psp.operator*().showAge();//编译器会把sp.operator*().showAge()优化成(*sp).showAge();return 0;}
编译运行:
Person的构造函数
m_Age = 30
m_Age = 30
=================================
SmartPointer的构造函数
m_Age = 30
m_Age = 30
m_Age = 30
m_Age = 30
SmartPointer析构函数
Person的析构函数
运算符重载的应用:封装字符串类
视频课:点这里
5.3.24 类的自动类型转换和强制类型转换(关键字explicit
)
被explicit
修饰的构造函数不能用于自动类型转换(隐式类型转换)。
示例:
#include <iostream>
using namespace std;class MyString {public:// 被explicit修饰的构造函数不能用于自动类型转换(隐式类型转换)explicit MyString(int len) {cout << "MyString有参构造MyString(int len)..." << endl;}//explicit MyString(const char* str) {cout << "MyString有参构造MyString(const char* str)..." << endl;}// 转换函数:转换成doubleoperator double() {// 业务逻辑return 20.1;}};// 类的自动类型转换和强制类型转换
/*如果我们想让类对象转换成基本类型的数据,我们需要在类中添加转换函数1.转换函数必须是类方法2.转换函数不能指定返回类型3.转换函数不能有参数
*/
int main() {// 基本内置数据类型的自动类型转换和强制类型转换long count = 8;double time = 10;int size = 3.14;cout << count << endl; //8cout << time << endl; //10cout << size << endl; //3double num = 20.3;cout << num << endl; //20.3// 强制转换成int类型的数据int num1 = (int)num; int num2 = int(num);cout << num1 << endl; //20cout << num2 << endl; //20MyString str = "hello"; //// MyString str = MyString("hello");// MyString str1 = 10; //不能通过隐式类型转换创建对象了MyString str1 = MyString(10);// 类的强制类型转换double d = str1;cout << d << endl; //20.1double d1 = double(str);double d2 = (double)str;cout << d1 << endl; //20.1cout << d2 << endl; //20.1return 0;
}
结果:
8
10
3
20.3
20
20
MyString有参构造MyString(const char* str)...
MyString有参构造MyString(int len)...
20.1
20.1
20.1
5.3.25-34 继承
继承的概念、好处、弊端
继承是面向对象的一大特征。
继承好处:
- 提高代码的复用性;
- 提高代码的维护性(方便维护);
- 让类与类之间产生了关系,是(动态)多态的前提。
继承的弊端:
- 类的耦合性增加了
开发的原则是:高内聚,低耦合
内聚:自己完成一件事的能力;
耦合:类和类之间的关系。
继承的基本语法
继承的语法:
class 子类:继承方式 父类{
...
};例如:
class dogs: public Animals {
...
};
继承方式分为public,protected,private
,即公共继承,保护继承,私有继承。
注意:
如果不写继承方式,默认是私有继承。
protected访问权限
三种权限:
public:
公共的访问权限,被修饰的成员在类内和类外都能够被访问;protected:
受保护的访问权限,如果没有继承关系,就和private的特点一样;privated:
私有的访问权限,被修饰的成员只能在类内被访问,在类外不能够被访问;
在继承关系中,父类中的protected修饰的成员,子类中可以直接访问,但在类外的其他地方不能访问。
成员变量一般使用privated
私有访问控制,不要使用protected
受保护的访问控制;
成员方法如果想要让子类访问,但是不想让外界访问,就可以使用protected
受保护的访问控制。
(下面的内容来自C++笔记3:C++核心编程中的4.6.2 继承方式)
总结:
1.父类中的私有内容(private
)任何一种继承方式都访问不到,即无法被访问/被继承;
2.公共继承:父类中的各访问权限不变
3.保护继承:父类中的各访问权限都变成protected
保护权限
4.私有继承:父类中的各访问权限都变成private
私有权限
无继承关系:
父类中的三个成员变量 | 父类类内访问 | 类外通过父类对象访问 |
---|---|---|
public 修饰的num1
|
可以访问 | 可以访问 |
protected 修饰的num2
|
可以访问 | 不能访问 |
private 修饰的num3
|
可以访问 | 不能访问 |
子类公共继承父类:class Zi : public Fu{ ... };
父类中的三个成员变量 | 子类中的三个成员变量 | 父类类内访问 | 子类类内访问 | 类外通过父类对象访问 | 类外通过子类对象访问 |
---|---|---|---|---|---|
public 修饰的num1
|
public 修饰的num1
|
可以访问 | 可以访问 | 可以访问 | 可以访问 |
protected 修饰的num2
|
protected 修饰的num2
|
可以访问 |
(因为有继承关系,所以) 可以访问 |
不能访问 | 不能访问 |
private 修饰的num3
|
private 修饰的num3
|
可以访问 | 不能访问 | 不能访问 | 不能访问 |
子类保护继承父类:class Zi : protected Fu{ ... };
父类中的三个成员变量 | 子类中的三个成员变量 | 父类类内访问 | 子类类内访问 | 类外通过父类对象访问 | 类外通过子类对象访问 |
---|---|---|---|---|---|
public 修饰的num1
|
protected 修饰的num1
|
可以访问 | 可以访问 | 可以访问 | 不能访问 |
protected 修饰的num2
|
protected 修饰的num2
|
可以访问 |
(因为有继承关系,所以) 可以访问 |
不能访问 | 不能访问 |
private 修饰的num3
|
private 修饰的num3
|
可以访问 | 不能访问 | 不能访问 | 不能访问 |
子类私有继承父类:class Zi : private Fu{ ... };
父类中的三个成员变量 | 子类中的三个成员变量 | 父类类内访问 | 子类类内访问 | 类外通过父类对象访问 | 类外通过子类对象访问 |
---|---|---|---|---|---|
public 修饰的num1
|
private 修饰的num1
|
可以访问 | 可以访问 | 可以访问 | 不能访问 |
protected 修饰的num2
|
private 修饰的num2
|
可以访问 |
(因为有继承关系,所以) 可以访问 |
不能访问 | 不能访问 |
private 修饰的num3
|
private 修饰的num3
|
可以访问 | 不能访问 | 不能访问 | 不能访问 |
示例:
#include<iostream>
using namespace std;class Fu{public:int num1;
protected:int num2;
private:int num3;public:void func(){num1 = 10;num2 = 20;num3 = 30;}
};
class Zi : protected Fu{ //protected保护继承 public公共继承 private私有继承
public:void func1(){num1 = 10;num2 = 20;num3 = 30;}
};int main(){Fu fu;cout << fu.num1 << endl;cout << fu.num2 << endl;cout << fu.num3 << endl;Zi zi;cout << zi.num1 << endl;cout << zi.num2 << endl;cout << zi.num3 << endl;
}
继承中的对象模型(成员变量和成员函数分开存储)
类中的成员变量和成员函数是分开存储的。
在对象中,只保存了(非静态)成员变量的信息;
子类将父类中的所有成员都继承了过来,包括私有的成员(变量和方法)。
(笔记①:牛客校招冲刺集训营—C++工程师中的5.3.8 静态成员(静态成员变量和静态成员函数)— 补充:成员变量和成员函数分开存储 中也有讲到)
普通成员变量(非静态成员变量)属于类的对象;
普通成员函数(非静态成员函数)、静态成员变量、静态成员函数属于类本身。
空对象占1个字节。
示例:
#include<iostream>
using namespace std;class Fu{public:int num1;//4个字节 非静态成员变量
protected:int* num2;//8个字节 非静态成员变量
private:long num3;//8个字节 非静态成员变量
public:static int num4;//静态成员变量void func(){}//非静态成员函数static void func1(){}//静态成员函数
};
class Zi : public Fu{public:int num5;//4个字节 非静态成员变量
protected: int* num6;//8个字节 非静态成员变量
private: long num7;//8个字节 非静态成员变量
public:static int num8; //静态成员变量void func2(){}//非静态成员函数static void func3(){} //静态成员函数
};int main(){Fu fu;cout << "一个具体的对象的大小:" << sizeof(fu) << endl;//24cout << "一个类的大小:" << sizeof(Fu) << endl;//48Zi zi;cout << "一个具体的对象的大小:" << sizeof(zi) << endl;cout << "一个类的大小:" << sizeof(Zi) << endl;return 0;
}
结果:
(为什么是24不是20,因为字节对齐(内存对齐))
一个具体的对象的大小:24
一个类的大小:24
一个具体的对象的大小:48
一个类的大小:48
如果把父类中的内容都屏蔽掉,结果如下:(空对象占1个字节)
class Fu{public://int num1;//4个字节 非静态成员变量//int* num2;//8个字节 非静态成员变量//long num3;//8个字节 非静态成员变量//static int num4; //静态成员变量//void func(){}//非静态成员函数//static void func1(){} //静态成员函数
};结果:
一个具体的对象的大小:1
一个类的大小:1
一个具体的对象的大小:24
一个类的大小:24
如果只屏蔽父类中的非静态成员变量,结果如下:(空对象占1个字节)
class Fu{public://int num1;//4个字节 非静态成员变量//int* num2;//8个字节 非静态成员变量//long num3;//8个字节 非静态成员变量static int num4; //静态成员变量void func(){}//非静态成员函数static void func1(){} //静态成员函数
};结果:
一个具体的对象的大小:1
一个类的大小:1
一个具体的对象的大小:24
一个类的大小:24
所以说:
普通成员变量(非静态成员变量)属于类的对象;
普通成员函数(非静态成员函数)、静态成员变量、静态成员函数属于类本身。
(这里第一次用到了对象模型来辅助理解)
具体是通过Visual Studio的工具查看一个类的内存分布:
1.打开这个工具;
2.切换到当前原文件的目录;
3.cl /d1 reportSingleClassLayout类名 文件名
继承中的构造和析构顺序
(见笔记①:牛客校招冲刺集训营—C++工程师中的5.3.7 类对象作为类成员(构造和析构的顺序))
(见C++笔记3:C++核心编程中的4.6.4 继承中的构造和析构顺序)
继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反。
视频课中的笔记:
访问继承中的 非静态 同名 成员
出现同名成员,就会出现二义性的现象,一般是通过加作用域的方式来避免出现二义性。
(见C++笔记3:C++核心编程中的4.6.5 继承同名成员处理方式)
问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
答:
子类对象可以直接访问到子类中同名成员;
子类对象加作用域可以访问到父类同名成员;
当子类与父类拥有同名的成员函数,子类会隐藏父类中所有
同名成员函数,加作用域可以访问到父类中同名函数。
//子类与父类出现相同的成员(变量/函数)时,子类对象如何访问到同名的数据?Son1 son;//同名成员变量cout << "子类中的a = " << son.a << endl;//直接访问cout << "父类中的a = " << son.Base1::a << endl;//加父类的作用域//同名成员函数son.func();//直接访问son.Base1::func();//加父类的作用域son.Base1::func(10);//加父类的作用域
视频课里的例子:(视频课从19:28开始)
访问继承中的 静态 同名 成员
当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
对于同名的静态成员,也是加上作用域:
但是,由于静态成员(变量和函数)被所有对象共享,所以一般不通过子类对象来访问这些同名成员,而是直接加上父类类名的作用域来访问。
总结:如何访问继承中的同名成员(静态成员和非静态成员)
访问继承中的同名成员有一下几种方式:
- 通过父类对象访问父类中的非静态成员和静态成员;
- 通过子类对象访问子类中的非静态成员和静态成员;
- 通过子类对象访问父类中的非静态成员和静态成员;
- 不通过对象来访问,直接加个作用域行不行?
答:静态成员可以直接加个作用域来访问,非静态成员必须通过特定的对象来访问。
方式4的理由:
由于静态成员(变量和函数)被所有对象共享,所以一般不通过子类对象来访问这些同名成员,而是直接加上父类类名的作用域来访问;
但非静态成员就必须要通过具体的对象来访问了。
代码:
#include<iostream>
using namespace std;class Fu{public:int num1;//非静态成员变量static int num4;//静态成员变量void func(){}//非静态成员函数static void func1(){}//静态成员函数
};
class Zi : public Fu{public:int num1;//非静态成员变量static int num4;//静态成员变量void func(){}//非静态成员函数static void func1(){}//静态成员函数
};int main(){//通过父类对象访问父类中的非静态成员和静态成员:Fu fu;cout << fu.num1 << endl;cout << fu.num4 << endl;fu.func();fu.func1();//通过子类对象访问子类中的非静态成员和静态成员:Zi zi;cout << zi.num1 << zi.num4 << endl;zi.func();zi.func1();//通过子类对象访问父类中的非静态成员和静态成员:cout << zi.Fu::num1 << endl;cout << zi.Fu::num4 << endl;zi.Fu::func();zi.Fu::func1();//不通过对象来访问,直接加个作用域行不行? 静态成员可以直接加个作用域来访问,非静态成员必须通过特定的对象来访问cout << Fu::num1 << endl;//非静态成员引用必须与特定对象相对cout << Fu::num4 << endl;Fu::func();//非静态成员引用必须与特定对象相对Fu::func1();cout << Zi::num1 << endl;//非静态成员引用必须与特定对象相对cout << Zi::num4 << endl;Zi::func();//非静态成员引用必须与特定对象相对Zi::func1();return 0;
}
多继承/多重继承
多继承
概念:一个类继承多个类
语法:class 子类: 继承方式 父类1, 继承方式 父类2...
注意:继承方式不要省略,否则就默认是私有继承。
C++实际开发中不建议用多继承,从多个类继承可能导致成员方法或成员变量同名产生较多的歧义。
看个示例:
基类Base1:
基类Base2:
子类多继承Base1和Base2:
main函数:
输出m_A
的时候两个父类都有,就没办法直接访问了,要加上作用域;
输出m_B
的时候就可以直接输出,或者也可以加上作用域。
菱形继承–>虚继承、虚基类☆☆☆
什么是菱形继承?
菱形继承会带来什么问题?
怎么解决?
示例:
Person类:
Singer类和Waiter类都继承自Person类,然后SingerWaiter类多继承Singer类和Waiter类:
main函数:
解决方法:给Singer
类和Waiter
类的继承方式后面加上virtual
关键字,就成了虚继承,此时Person
类被称为虚基类
结果:
虚继承的实现原理
vbptr:virtual base pointer
(虚基类指针)
原理:
只有一个唯一的成员,通过保存虚基类指针,这个指针指向一张表(虚基类表),这个表中保存了当前获取到唯一的数据的偏移量offset
。
(第二次用了对象模型来辅助理解)
5.3.35-38 多态(一般是指动态多态)
静态联编和动态联编(静态多态和动态多态)
以下的内容来自c++笔记3的4.7.1 多态的基本概念
①多态分为两类:
- 静态多态: 函数重载 和 运算符重载 属于静态多态,复用函数名;
- 动态多态: 派生类和虚函数实现运行时多态,一般我们说多态指的都是动态多态。
②静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
③动态多态满足条件:
- 有继承关系
- 子类重写父类中的虚函数(
virtual
+ 函数名)
④动态多态使用条件
- 父类指针或引用指向子类对象
⑤重写和重载的区别
重写:
- 函数返回值类型、函数名、参数列表完全一致称为重写;
- 重写也叫覆盖、覆写。
重载:
①同一个作用域下;
②函数名称相同;
③函数参数的 类型不同、个数不同 、顺序不同;
④和返回值类型、函数形参名无关;
示例:
关键在于父类中成员函数前的virtual
关键字
#include<iostream>
using namespace std;//动物类
class Animals {public://函数前面加上virtual关键字,speak函数就是虚函数virtual void speak() {cout << "动物在说话" << endl;}
};
//猫类
class Cats: public Animals{public:void speak() {cout << "小猫在说话" << endl;}
};
class Dogs:public Animals {public:void speak() {cout << "小狗在说话" << endl;}
};
//全局函数:父类引用指向子类对象
// void doSpeak(Animals& ani) {// ani.speak();
// }
//全局函数:父类指针指向子类对象
void doSpeak(Animals* ani) {//ani->speak();(*ani).speak();
}int main(){cout << "sizeof Animals类 = " << sizeof(Animals) << endl;//有virtual关键字的Animals类占4字节//没有virtual关键字的Animals类占1字节,空类,并且非静态成员函数不属于类的内存(见4.3.1 成员变量和成员函数分开存储)//加了virtual关键字后Animals类占4字节,不再是空类,而是多了一个指针,叫vfptr(虚函数表指针),表内记录虚函数的地址Cats cat;//当子类 重写 父类的 虚函数 ,子类中的虚函数表内部会替换成子类的虚函数地址//子类重写父类的虚函数,即子类也有个vfptr(虚函数表指针),表内记录虚函数的地址cout << "sizeof Cats类 = " << sizeof(Cats) << endl;//4 //当父类的 指针或者引用 指向子类对象的时候,就发生了多态//doSpeak(cat);//小猫在说话 父类引用指向子类对象doSpeak(&cat);//小猫在说话 父类指针指向子类对象Dogs dog;//doSpeak(dog);//小狗在说话 父类引用指向子类对象doSpeak(&dog);//小狗在说话 父类指针指向子类对象system("pause");return 0;
}
父类的speak
函数前不加关键字virtual
,结果如下:(静态多态)
sizeof Animals类 = 1 //空类(非静态成员函数不属于对象的内存)
sizeof Cats类 = 1
动物在说话
动物在说话
结果是调用了父类的speak
函数,这属于地址早绑定,也叫静态联编,因为在编译期间就知道父类speak
函数的地址了。
而我们想要的结果是如果传进来是猫类的对象,就执行猫的speak
函数;传进来是狗类的对象,就执行狗的speak
函数;这属于地址晚绑定,也叫动态联编。具体做法就是
在父类的speak
函数前加关键字virtual
,结果如下:(动态多态)
sizeof Animals类 = 8 //虚函数表指针vfptr的大小
sizeof Cats类 = 8
小猫在说话
小狗在说话
虚函数的原理(多态的底层原理)
main函数中的部分代码:
cout << "sizeof Animals类 = " << sizeof(Animals) << endl;//有virtual关键字的Animals类占4字节//没有virtual关键字的Animals类占1字节,空类,并且非静态成员函数不属于类的内存(见4.3.1 成员变量和成员函数分开存储)//加了virtual关键字后Animals类占4字节,不再是空类,而是多了一个指针,叫vfptr(虚函数表指针),表内记录虚函数的地址Cats cat;//当子类 重写 父类的 虚函数 ,子类中的虚函数表内部会替换成子类的虚函数地址//子类重写父类的虚函数,即子类也有个vfptr(虚函数表指针),表内记录虚函数的地址cout << "sizeof Cats类 = " << sizeof(Cats) << endl;//4
解释:
①没有virtual
关键字的Animals
类占1
字节,空类,因为非静态成员函数不属于对象的内存(见继承中的对象模型(成员变量和成员函数分开存储));
②加了virtual
关键字后Animals
类占4
字节(32位操作系统)/8
个字节(64位操作系统),不再是空类,而是多了一个指针,叫vfptr
(虚函数表指针),表内记录虚函数的地址;虚函数表指针指向虚函数表vftable
;
③由于子类重写父类的虚函数,因此子类也有个vfptr
(虚函数表指针),表内记录虚函数的地址;
④在子类重写父类的虚函数时,子类中的虚函数表内部会替换成子类的虚函数地址;
多态的底层原理:
- 首先在父类中的虚函数(
virtual void Speak(){}
),使得父类占4
个字节(32位操作系统)/8
个字节(64位操作系统),这4
个字节是个vfptr
(虚函数表指针),它指向虚函数表(vftable
),表内记录虚函数的地址(&Animal::speak
); - 然后是子类重写父类中的虚函数,因此子类也占4个字节,这4个字节也是个vfptr(虚函数表指针),它也指向虚函数表(vftable),表内也记录虚函数的地址;在子类重写父类中的虚函数后,子类中的虚函数表内部会替换成子类的虚函数地址(
&Cats::speak
)。 - 最后是当父类的指针或者引用指向子类对象时,就发生了多态。
派生类虚表:
1.先将基类的虚表中的内容拷贝一份;
2.如果派生类对基类中的虚函数进行重写,使用派生类的虚函数替换相同偏移量位置的基类虚函数;
3.如果派生类中新增加自己的虚函数,按照其在派生类中的声明次序,放在上述虚函数之后。
原文链接:https://blog.csdn.net/qq_39412582/article/details/81628254
(视频课中从01:22:00开始)
静态多态:
动态多态:
(第三次用了对象模型来辅助理解)
①父类中的speak
函数前没加virtual
,查看Animal
类的内存分布:
②父类中的speak
函数前加了virtual
:
查看Animal
类的内存分布:
然后再查看Cats
类的内存分布:
再查看Dog
类的内存分布:
如果Cats
类不重写父类的speak
函数,它的内存分布就变成了:
多态案例(开闭原则:对扩展进行开放,对修改进行关闭)
视频课中的案例和c++笔记3的4.7.2 多态案例1—计算器类中的案例一样。
如果想扩展新的功能,需要修改源码。
在真实开发中提倡开闭原则。
开闭原则:对扩展进行开放,对修改进行关闭。
多态技术:
①继承②父类有虚函数③子类重写父类的虚函数④父类的指针或引用指向子类对象
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
纯虚函数 --> 抽象类
在多态中,通常父类中虚函数的现实无意义的,主要都是调用子类重写的内容,所以可以将虚函数改为“纯虚函数”。
纯虚函数语法:virtual 返回值类型 函数名 (形参列表) = 0;
父类中有了纯虚函数,这个类就被称为抽象类。
抽象类特点:
- ①无法实例化对象;
- ②子类必须重写抽象类中的纯虚函数,
如果子类没有重写父类的纯虚函数,那么子类也是一个抽象类。
示例:
//父类:抽象计算器
class abstractCalculator {public:int a = 0, b = 0;//virtual int getResult() {//②虚函数// return 0;//}virtual int getResult() = 0;//纯虚函数
};
虚析构和纯虚析构(解决 调用不到子类析构函数 的问题)
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。
解决方式:将父类中的析构函数改为虚析构或者纯虚析构(☆☆☆推荐用虚析构)。
总结:
- 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象;
- 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构;
- 拥有纯虚析构函数的类也属于抽象类,
纯虚析构的目的只有一个:让类成为抽象类。
纯虚析构需要有声明,也要有实现;
(在类内声明)virtual ~Animal() = 0;
(在类外实现)Animal::~Animal(){}
注意:
纯虚函数不用实现,但子类必须重写纯虚函数,否则子类也是抽象类;
而纯虚析构必须要实现,而且只能在类外实现;
class Animal{public:Animal(){cout << "Animal类的构造" << endl;}virtual void speak() = 0;//纯虚函数 子类必须重写纯虚函数//纯虚析构:要声明,也要实现virtual ~Animal() = 0;//在类内声明
};//纯虚析构:在类外实现
Animal::~Animal(){cout << "Animal类的析构" << endl;
}
示例:
#include<iostream>
#include<string.h>
using namespace std;class Animal{public:Animal(){cout << "Animal类的构造" << endl;}virtual void speak() = 0;//纯虚函数//常规的析构函数: 会带来 无法调用子类析构函数 的问题~Animal(){cout << "Animal类的析构" << endl; }//虚析构:
// virtual ~Animal(){// cout << "Animal类的析构" << endl;
// }//纯虚析构:要声明,也要实现
// virtual ~Animal() = 0;//在类内声明
};//纯虚析构:在类外实现
// Animal::~Animal(){// cout << "Animal类的析构" << endl;
// }class Cats : public Animal{public: Cats(const char* name){cout << "Cats类的有参构造" << endl;this->m_Name = new char(strlen(name) + 1);strcpy(this->m_Name, name);}void speak(){cout << this->m_Name << "小猫在说话..." << endl;}~Cats(){cout << "Cats类的析构" << endl; if(this->m_Name != nullptr){delete[] this->m_Name;this->m_Name = nullptr;}}
private:char* m_Name;
};int main(){Animal* ani = new Cats("Tom");ani->speak();delete ani;return 0;
}
结果:
Animal类的构造
Cats类的有参构造
Tom小猫在说话...
Animal类的析构
并没有调用子类的析构函数,解决办法是在父类的析构函数前面加个virtual
,将其变为虚析构。
class Animal{public:Animal(){cout << "Animal类的构造" << endl;}virtual void speak() = 0;//纯虚函数//虚析构:virtual ~Animal(){cout << "Animal类的析构" << endl; }
};
然后再编译运行:
Animal类的构造
Cats类的有参构造
Tom小猫在说话...
Cats类的析构
Animal类的析构
或者写成纯虚析构的形式:
class Animal{public:Animal(){cout << "Animal类的构造" << endl;}virtual void speak() = 0;//纯虚函数//纯虚析构:要声明,也要实现virtual ~Animal() = 0;//在类内声明
};//纯虚析构:在类外实现
Animal::~Animal(){cout << "Animal类的析构" << endl;
}
那么当子类中有属性开辟到堆区,为什么会出现无法调用到子类的析构代码的问题呢?
可以联想到上面的静态联编和动态联编中说的:
父类的speak
函数前如果不加关键字virtual
,最终的结果就是调用了父类的speak
函数(即输出"动物在说话"),这里也一样,父类的析构函数前没有关键字virtual
,最终的结果就是调用了父类的析构函数,所以就没有调用子类的析构函数。
那么怎么解决呢?
方法和上图中的内容类似,就是在父类的析构函数前加上关键字virtual
,这样的话就可以调用到子类的析构函数了。具体解释见下图:
(视频课中从01:56:40开始)
(第四次用了对象模型)
Animal
类的析构函数前加了关键字virtual
,查看Cat
类的内存分布:
Cat
类的析构函数就会加入到虚函数表中,这样就可以调用到了:
析构函数:destructor
构造函数:constructor
C++面试宝典–>第一章–>1.3 面向对象
E:\找工作\C++八股文\C面试宝典完整版最最最新.pdf
视频课中从02:00:39开始
5.6 内存管理①(结合计算机操作系统笔记)
(这部分内容可以看计算机操作系统笔记 的0620开始一直到第三章结束)
程序就是文件;
运行起来的程序被称为进程;
01:00:35开始回顾第一节课的内容:
1.重定位(地址转换:逻辑地址–>物理地址)
重定位:修改该程序中的地址(相对地址)
什么时候完成重定位?编译时?还是载入时?
都不对:
编译时重定位的程序只能放在内存固定位置;
载入时重定位的程序一旦载入内存就不能动了;
答案是在运行时(执行每条指令时才)完成重定位,找到真正的物理地址。
(分别对应操作系统笔记中的绝对装入(载入)、静态重定位、动态重定位)
也可以叫地址翻译:基地址(起始地址) + 逻辑地址(偏移量)–> 物理地址
2.交换技术
(对应操作系统笔记中的内存空间的扩充:交换技术,是指内存和外存之间交换进程,以缓解内存吃紧的问题)
交换技术:
(要运行进程2,但内存不够了)
(进程1处于睡眠状态,就把进程1换出到磁盘中,把进程2换入到内存中)
(然后进程3页进入睡眠状态,把进程3换出磁盘,把刚刚换出的进程1再换入到内存中)
3.分段(将各段分别放入内存)
程序是分段管理的;
程序运行时是分段加载的,而不是将整个程序一次性全部载入内存。
(对应操作系统笔记中的分段存储管理的逻辑地址结构:<段号/段名,段内地址/段内偏移量>)
分段:将各段程序分别放入内存。
(对应操作系统比较中的段表中记录的内容:段号、段的起始地址、段的长度)
具体怎么分段?
有固定分区和可变分区(动态分区):
(对应操作系统笔记中的连续分配中的固定分区分配和可变分区分配(动态分区分配))
可变分区算法:首先适配、最佳适配、最差适配。
(对应操作系统笔记中的动态分区分配算法:首次适应、最佳时应、最坏适应、临近时应)
内存紧缩 & 内存碎片
(对应操作系统笔记中的内存紧缩技术,用来解决外部碎片的问题。)
4.分页(从连续到离散)
分页:页表的内容(页号、内存块号/页框号) 逻辑地址结构:<页号,页内地址偏移量>
为了提高内存空间利用率,页的大小应该足够小,但页表就大了,所以就有了二级页表,即页表的页表,称为页目录表。
二级页表的逻辑地址结构:页目录号、页号、页内偏移量
二级页表的出现是因为没必要把所有的页表项都放在内存中,很占内存,所以就弄成二级页表,把用的页表放到内存中,没用到的先放外存中,这样就提高了内存的利用率。
通过二级页表访问一个逻辑地址需要三次访存:
第一次访问内存中的页目录表,第二次访问内存中的页表,第三次访问目标内存单元。
因为需要三次访存,所以引入快表,可以让访存次数降低一次。快表的查询速度非常快。
5.7 内存管理②(结合计算机操作系统笔记)
5.段页式管理 和 虚拟内存(虚拟地址空间/虚拟内存地址)
(视频课中从32:52开始)
这里面的段就是虚拟内存,那虚拟内存中具体是怎么分段的,都分为哪些分区?就是下面的虚拟地址空间,或者叫虚拟内存地址。
C++笔记3:C++核心编程 --> 1、内存分区模型
C++ Primer Plus(嵌入式公开课)—第4章 复合类型–>4.8.5 自动存储、静态存储和动态存储
C++的内存分区/内存模型:(下图的虚拟内存地址中的用户区)
全局区(静态存储)、栈区(自动存储)、堆区/自由存储区(动态存储)
每个进程都有一个虚拟地址空间;
同一个进程下的不同线程共用一个虚拟地址空间。
共享库:静态库、共享内存;
栈:局部变量、返回值(自动释放)后进先出
堆区:malloc或者new的数据,要手动释放
全局区:.bss(未初始化或初始化为0的全局变量)、.data(已初始化全局变量)、.text(代码段、二进制机器指令)
内存分区 | 权限 | ||
---|---|---|---|
共享库 | 静态库、共享内存 | ||
栈区 | 局部变量、返回值(自动释放) | 后进先出 | 可读可写 |
堆区 | malloc或者new的数据 | 要手动释放free或者delete | 可读可写 |
全局区: | |||
.bss未初始化或初始化为0的全局变量 | |||
.data已初始化的全局变量 | |||
.text代码段 | 只读 | ||
常量(全局常量+字符串常量) | 只读 | ||
静态变量(static) | |||
缺页中断
6.虚拟内存和段页式存储管理知识补充
参考链接1
(以下内容来自电子发烧友的文章)
什么是虚拟内存?
在现代操作系统中,多任务已是标配。多任务并行,大大提升了 CPU 利用率,但却引出了多个进程对内存操作的冲突问题,虚拟内存概念的提出就是为了解决这个问题。
操作系统有一块物理内存(中间的部分),有两个进程(实际会更多)P1 和 P2,操作系统偷偷地分别告诉 P1 和 P2,我的整个内存都是你的,随便用,管够。可事实上呢,操作系统只是给它们画了个大饼,这些内存说是都给了 P1 和 P2,实际上只给了它们一个序号而已。只有当 P1 和 P2 真正开始使用这些内存时,系统才开始使用辗转挪移,拼凑出各个块给进程用,P2 以为自己在用 A 内存,实际上已经被系统悄悄重定向到真正的 B 去了,甚至,当 P1 和 P2 共用了 C 内存,他们也不知道。(确保了进程之间互不影响,也可以让两个进程共享同一个内存的内容)
操作系统的这种欺骗进程的手段,就是虚拟内存。
对 P1 和 P2 等进程来说,它们都以为自己占用了整个内存,而自己使用的物理内存的哪段地址,它们并不知道也无需关心。
分页和页表?
虚拟内存是操作系统里的概念,对操作系统来说,虚拟内存就是一张张的对照表,P1 获取 A 内存里的数据时应该去物理内存的 A 地址找,而找 B 内存里的数据应该去物理内存的 C 地址。
我们知道系统里的基本单位都是 Byte 字节,如果将每一个虚拟内存的 Byte 都对应到物理内存的地址,每个条目最少需要 8字节(32位虚拟地址->32位物理地址),在 4G 内存的情况下,就需要 32GB 的空间来存放对照表,那么这张表就大得真正的物理地址也放不下了,于是操作系统引入了页(Page)的概念。
在系统启动时,操作系统将整个物理内存以 4K 为单位,划分为各个页。之后进行内存分配时,都以页为单位,那么虚拟内存页对应物理内存页的映射表就大大减小了。4G 内存,只需要 8M 的映射表即可,一些进程没有使用到的虚拟内存,也并不需要保存映射关系,而且Linux 还为大内存设计了多级页表,可以进一页减少了内存消耗。
操作系统虚拟内存到物理内存的映射表,就被称为页表。
内存寻址和内存分配?
我们知道通过虚拟内存机制,每个进程都以为自己占用了全部内存,进程访问内存时,操作系统都会把进程提供的虚拟内存地址转换为物理地址,再去对应的物理地址上获取数据。CPU 中有一种硬件,内存管理单元 MMU(Memory Management Unit)专门用来将翻译虚拟内存地址。CPU 还为页表寻址设置了缓存策略(快表),由于程序的局部性原理,其缓存命中率能达到 98%。(快表其实就是一种特殊的高速缓冲寄存器Cache,高速缓存)
以上情况是页表内存在虚拟地址到物理地址的映射,而如果进程访问的物理地址还没有被分配,系统则会产生一个缺页中断,在中断处理时,系统切到内核态为进程虚拟地址分配物理地址。
虚拟内存的功能:
- 虚拟内存不仅通过内存地址转换解决了多个进程访问内存冲突的问题,还带来更多的益处。
- 它有助于进程内存管理,主要体现在:
内存完整性:由于虚拟内存对进程的”欺骗”,每个进程都认为自己获取的内存是一块连续的地址。我们在编写应用程序时,就不用考虑大块地址的分配,总是认为系统有足够的大块内存即可。
安全:由于进程访问内存时,都要通过页表来寻址,操作系统在页表的各个项目上添加各种访问权限标识位,就可以实现内存的权限控制。 - 通过虚拟内存更容易实现内存和数据的共享。
在进程加载系统库时,总是先分配一块内存,将磁盘中的库文件加载到这块内存中,在直接使用物理内存时,由于物理内存地址唯一,即使系统发现同一个库在系统内加载了两次,但每个进程指定的加载内存不一样,系统也无能为力。
而在使用虚拟内存时,系统只需要将进程的虚拟内存地址指向库文件所在的物理内存地址即可。如上文图中所示,进程 P1 和 P2 的 B 地址都指向了物理地址 C。
而通过使用虚拟内存使用共享内存也很简单,系统只需要将各个进程的虚拟内存地址指向系统分配的共享内存地址即可。 - 虚拟内存可以帮进程”扩充”内存。
我们前文提到了虚拟内存通过缺页中断为进程分配物理内存,内存总是有限的,如果所有的物理内存都被占用了怎么办呢?
Linux 提出 SWAP 的概念,Linux 中可以使用 SWAP 分区,在分配物理内存,但可用内存不足时,将暂时不用的内存数据先放到磁盘上,让有需要的进程先使用,等进程再需要使用这些数据时,再将这些数据加载到内存中,通过这种”交换”技术,Linux 可以让进程使用更多的内存。
参考链接2
(下面内容来自电子发烧友的文章)
各个进程的虚拟内存地址相互独立。因此,两个进程空间可以有相同的虚拟内存地址,如0x10001000。虚拟内存地址和物理内存地址又有一定的对应关系,如图1所示。对进程某个虚拟内存地址的操作,会被CPU翻译成对某个具体内存地址的操作。
应用程序对物理内存地址一无所知。它只可能通过虚拟内存地址来进行数据读写。程序中表达的内存地址,也都是虚拟内存地址。进程对虚拟内存地址的操作,会被操作系统翻译成对某个物理内存地址的操作。由于翻译的过程由操作系统全权负责,所以应用程序可以在全过程中对物理内存地址一无所知。
本质上说,虚拟内存地址剥夺了应用程序自由访问物理内存地址的权利。进程对物理内存的访问,必须经过操作系统的审查。因此,掌握着内存对应关系的操作系统,也掌握了应用程序访问内存的闸门。借助虚拟内存地址,操作系统可以保障进程空间的独立性。只要操作系统把两个进程的进程空间对应到不同的内存区域,就让两个进程空间成为“老死不相往来”的两个小王国。两个进程就不可能相互篡改对方的数据,进程出错的可能性就大为减少。
另一方面,有了虚拟内存地址,内存共享也变得简单。操作系统可以把同一物理内存区域对应到多个进程空间。这样,不需要任何的数据复制,多个进程就可以看到相同的数据。内核和共享库的映射,就是通过这种方式进行的。每个进程空间中,最初一部分的虚拟内存地址,都对应到物理内存中预留给内核的空间。这样,所有的进程就可以共享同一套内核数据。共享库的情况也是类似。对于任何一个共享库,计算机只需要往物理内存中加载一次,就可以通过操纵对应关系,来让多个进程共同使用。IPO中的共享内存,也有赖于虚拟内存地址。
虚拟内存地址和物理内存地址的分离,给进程带来便利性和安全性。但虚拟内存地址和物理内存地址的翻译,又会额外耗费计算机资源。在多任务的现代计算机中,虚拟内存地址已经成为必备的设计。那么,操作系统必须要考虑清楚,如何能高效地翻译虚拟内存地址?
记录对应关系最简单的办法,就是把对应关系记录在一张表中。为了让翻译速度足够地快,这个表必须加载在内存中。不过,这种记录方式惊人地浪费。如果树莓派1GB物理内存的每个字节都有一个对应记录的话,那么光是对应关系就要远远超过内存的空间。由于对应关系的条目众多,搜索到一个对应关系所需的时间也很长。这样的话,会让树莓派陷入瘫痪。
因此,Linux采用了分页(paging)的方式来记录对应关系。所谓的分页,就是以更大尺寸的单位页(page)来管理内存。在Linux中,通常每页大小为4KB。如果想要获取当前树莓派的内存页大小,可以使用命令:
getconf PAGE_SIZE
得到结果,即内存分页的字节数:
4096
返回的4096代表每个内存页可以存放4096个字节,即4KB。
Linux把物理内存和进程空间都分割成页。
内存分页,可以极大地减少所要记录的内存对应关系。我们已经看到,以字节为单位的对应记录实在太多。如果把物理内存和进程空间的地址都分成页,内核只需要记录页的对应关系,相关的工作量就会大为减少。由于每页的大小是每个字节的4096倍。因此,内存中的总页数只是总字节数的四千分之一。对应关系也缩减为原始策略的四千分之一。分页让虚拟内存地址的设计有了实现的可能。
也就是说,分页其实分的就是虚拟内存地址和物理内存地址的对应关系。
无论是虚拟页,还是物理页,一页之内的地址都是连续的。这样的话,一个虚拟页和一个物理页对应起来,页内的数据就可以按顺序一一对应。这意味着,虚拟内存地址和物理内存地址的末尾部分应该完全相同。大多数情况下,每一页有4096个字节。由于4096是2的12次方,所以地址最后12位的对应关系天然成立。我们把地址的这一部分称为偏移量(offset
)。偏移量实际上表达了该字节在页内的位置。地址的前一部分则是页编号。操作系统只需要记录页编号的对应关系(用的页表,页号对应虚拟页,页框号/内存块号对应物理页)。
内存分页制度的关键,在于管理进程空间页(虚拟页)和物理页的对应关系。操作系统把对应关系记录在分页表(page table)(即页表)中。这种对应关系让上层的抽象内存和下层的物理内存分离,从而让Linux能灵活地进行内存管理。由于每个进程会有一套虚拟内存地址,那么每个进程都会有一个分页表。
参考链接3
操作系统——段页式内存管理与虚拟内存
在虚拟内存中分段、建立段表、将虚拟页映射到空闲物理页框,建立页表。
1.如何将段和页结合在一起
在对内存进行使用的过程中,用户更希望程序以段的形式被放入内存,这样符合用户的使用习惯,譬如找内存中“代码段的第40条指令”。而内存更希望将自己等分成若干页,可以避免因内存碎片导致的内存利用效率的降低。
为了同时满足用户和内存的要求,操作系统需要一种中间结构来完成段与页机制的统一,这就是虚拟内存。
虚拟内存是一个抽象的概念,本身并不存在,它是一连串的虚拟地址构成的。
当程序分段后,从虚拟内存上分割出相应的分区与各段建立映射关系,完成分段机制;(段表:程序段号、段在虚拟内存中的起始地址、段的长度)
再将虚拟内存分割成页,将这些页放在页框中,并建立页和页框的映射,完成分页机制(页表:页号、页框号/内存块号)。
2.段页结合时进程对内存的使用
提出了虚拟内存的概念后,我们已经能够将分段机制和分页机制有机地结合在一起了。现在又要引出两个问题?程序又是如何放置到内存中去的了?又是如何得以正确执行的了?
当一个程序想要放入内存,会经历以下的步骤:
(1)在虚拟内存中划分区域,将程序分段载入到虚拟内存中,其实就是建立了程序段与虚拟内存各个分区间的映射关系,将这种映射关系放到段表中,记录各段与虚拟内存的映射关系。
(2)将虚拟内存中的各段打散分成页,然后建立页表,记录虚拟页号和物理页框号/内存块号之间的映射关系。
看个例子(逻辑地址、虚拟地址、物理地址)
以指令“call 40
”为例,逻辑地址40。设代码段为第一个代码段,页面大小为100(页面大小和页框大小相同)。
段号为0,找到该段在虚拟内存中的起始地址为1000,偏移量(逻辑地址)是40,1000+40=1040,这是在虚拟内存中的地址。
再用1040除以页面大小100,得到虚拟页号为10,查找页表发现对应的物理页框号为5,那么实际内存地址为5*100+40=540,就是“mov 1, [300]”指令。
只要将特定的寄存器(LDTR、CR3)的值设置为正确段表初始地址和页表初始地址,执行每条指令时MMU都会自动完成上述地址转换过程。
虚拟内存和虚拟存储器的区别?
参考链接4、5
参考链接
电子发烧友的文章
一、虚拟内存(操作系统笔记中的内容)
windows下的虚拟内存其实是借用磁盘空间假装它是内存,当应用访问虚拟内存地址的时候,如果内存管理器发现对应的物理地址在磁盘中,那内存管理器就会将这部分信息从磁盘中加载回内存中。
(以下的内容来自操作系统笔记中的3.4.3 虚拟存储技术)
虚拟内存:
在程序装入(载入)时,可以将程序中很快会用到的部分装入内存,暂时用不到的部分留在外存,就可以让程序开始执行了;
在程序执行过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序;(外存–>内存)
若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存;(内存–>外存)
在操作系统的管理下,在用户看来似乎有一个比实际内存大得多的存储器,这就是虚拟存储器。
如何实现虚拟内存技术?
要用到操作系统提供的两个功能:请求调页功能和页面置换功能。
二、虚拟存储器(也叫虚拟内存???)(牛客的视频课中的内容)
更像是一种机制,这种机制在有的书称为虚拟内存,有的书称为虚拟存储器,但是这不重要,重要的是其中的原理、核心。
虚拟内存是硬件异常、硬件地址翻译、主存、磁盘文件和内核文件的完美交互,它为每个进程提供了一个大的、一致的、私有的地址空间。
通过一个很清晰的机制,虚拟内存提供了三个重要的能力:
1)它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存;
2)它为每个进程提供了一致的地址空间,从而简化了内存管理;
3)它为每个进程提供了私有的地址空间,从而保护了每个进程的地址空间不被其他进程破坏。
虚拟内存是计算机系统内存管理的一种技术。 它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上物理内存通常被分隔成多个内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
三、总结
虚拟存储技术:
借用磁盘空间假装它是内存,当应用访问虚拟内存地址的时候,如果内存管理器发现对应的物理地址在磁盘中,那内存管理器就会将这部分信息从磁盘(外存)中加载回内存中。
虚拟内存:
操作系统为每个进程提供了一个大的、一致的、私有的地址空间,叫虚拟内存地址,或者虚拟地址空间。
1)在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存;
2)它为每个进程提供了一致的地址空间,从而简化了内存管理;
3)它为每个进程提供了私有的地址空间,从而保护了每个进程的地址空间不被其他进程破坏。
7.(5和6的)总结☆☆☆☆☆
7.1 虚拟内存的提出是为了解决什么问题?
操作系统中有个概念叫并行,就是多核处理器中每个核都处理一个进程,这些进程是同时进行的,那么就会有多个进程对内存操作的冲突问题,而虚拟内存概念的提出就是为了解决这个问题。
7.2 虚拟内存的原理解释
首先,每个进程都有一个虚拟地址空间(但同一个进程下的不同线程共用一个虚拟地址空间),这样就确保了进程之间互不影响;
程序中表达的内存地址,也都是虚拟内存地址。进程对虚拟内存地址的操作,会被操作系统翻译成对某个物理内存地址的操作。由于翻译的过程由操作系统全权负责,所以应用程序可以在全过程中对物理内存地址一无所知。
本质上说,虚拟内存地址剥夺了应用程序自由访问物理内存地址的权利。进程对物理内存的访问,必须经过操作系统的审查。因此,掌握着内存对应关系的操作系统,也掌握了应用程序访问内存的闸门。借助虚拟内存地址,操作系统可以保障进程空间的独立性。只要操作系统把两个进程的进程空间对应到不同的内存区域,就让两个进程空间成为“老死不相往来”的两个小王国。两个进程就不可能相互篡改对方的数据,进程出错的可能性就大为减少。
操作系统有一块物理内存(中间的部分),有两个进程(实际会更多)P1 和 P2,操作系统偷偷地分别告诉 P1 和 P2,我的整个内存都是你的,随便用,管够。可事实上呢,操作系统只是给它们画了个大饼,这些内存说是都给了 P1 和 P2,实际上只给了它们一个序号而已。只有当 P1 和 P2 真正开始使用这些内存时,系统才开始使用辗转挪移,拼凑出各个块给进程用,P2 以为自己在用 A 内存,实际上已经被系统悄悄重定向到真正的 B 去了;甚至,当 P1 和 P2 共用了 C 内存,他们也不知道。
操作系统的这种欺骗进程的手段,就是虚拟内存。对 P1 和 P2 进程来说,它们都以为自己占用了整个内存,而自己使用的物理内存的哪段地址,它们并不知道,也无需关心。
虚拟内存是操作系统里的概念,对操作系统来说,虚拟内存就是一张张的对照表:
P1 获取 A 内存里的数据时应该去物理内存的 A 地址找,而找 B 内存里的数据应该去物理内存的 C 地址。(这就是逻辑地址和物理地址的一个对应关系)
此外,由于每个进程都一个虚拟地址空间,所以两个进程可以有相同的虚拟内存地址,但经过地址转换后不一定指向同一块物理内存:
7.3 分页和页表
我们知道系统里的基本单位都是 Byte 字节,如果将每一个虚拟内存的 Byte 都对应到物理内存的地址,每个条目最少需要 8字节(32位虚拟地址->32位物理地址),在 4G 内存的情况下,就需要 32GB 的空间来存放对照表,那么这张表就大得真正的物理地址也放不下了,于是操作系统引入了页(Page)的概念。
在系统启动时,操作系统将整个物理内存以 4K 为单位,划分为各个页。之后进行内存分配时,都以页为单位,那么虚拟内存页 和 物理内存页 的映射表就大大减小了。
4G 内存,只需要 8M 的映射表即可:
32位系统的内存是4G = 2^32
每页是4K = 2^12
所以一共有2^20个页,每个页最少占8个字节B
所以就是8M = 2^3 * 2^20
并且一些进程没有使用到的虚拟内存,也并不需要保存映射关系,此外Linux 还为大内存设计了多级页表,可以进一页减少了内存消耗。
从虚拟内存到物理内存的映射表,就被称为页表,即页表记录的是虚拟页号和页框号/内存块号的对应关系。
无论是虚拟页,还是物理页,一页之内的地址都是连续的。这样的话,一个虚拟页和一个物理页对应起来,页内的数据就可以按顺序一一对应。
由于每个进程会有一套虚拟内存地址,那么每个进程都会有一个分页表。
7.4 虚拟内存地址(逻辑地址)–>物理地址 (快表、缺页中断)
我们知道通过虚拟内存机制,每个进程都以为自己占用了全部内存,进程访问内存时,操作系统都会把进程提供的虚拟内存地址转换为物理地址,再去对应的物理地址上获取数据。CPU 中有一种硬件,内存管理单元 MMU(Memory Management Unit)专门用来翻译虚拟内存地址。
CPU 还为页表寻址设置了缓存策略(快表),由于程序的局部性原理,其缓存命中率能达到 98%。(快表其实就是一种特殊的高速缓冲寄存器Cache,高速缓存)
以上情况是页表内存在虚拟地址到物理地址的映射,而如果进程访问的物理地址还没有被分配,系统则会产生一个缺页中断,在中断处理时,系统切到内核态为进程提供的虚拟地址分配物理地址。
7.5 虚拟内存的功能
- 解决了多个进程访问内存冲突的问题;
- 内存完整性:由于虚拟内存对进程的”欺骗”,让每个进程都认为自己获取的内存是一块连续的地址,并且内存足够大;
- 安全:由于进程访问内存时,都要通过页表来寻址,操作系统在页表的各个项目上添加各种访问权限标识位,就可以实现内存的访问权限控制;
虚拟内存地址和物理内存地址的分离,给进程带来便利性和安全性。 - 更容易实现内存和数据的共享,如上文图中所示,进程 P1 和 P2 的 B 地址都指向了物理地址 C。(分段的优点:实现信息的共享和保护)
操作系统可以把同一物理内存区域对应到多个进程空间。这样,不需要任何的数据复制,多个进程就可以看到相同的数据。 - 可以帮进程”扩充”内存,利用交换技术,在内存吃紧的时候把暂时用不到的数据换出到外存中;(装Linux系统的时候有一步是设置交换分区的大小)
7.6 段页式内存管理与虚拟内存☆☆☆
直接看上面参考链接3的全部内容,主要配合例子来理解一下。
C++面试宝典–> 1.2 C++内存 和 第2章 操作系统
E:\找工作\C++八股文\C面试宝典完整版最最最新.pdf
视频课中从01:10:39开始
5.7 名称空间、模板
补充:内存对齐/字节对齐
(视频课中从01:28:45开始)
为什么要有内存对齐,因为加入CPU每次固定的读4个字节,这样可以用空间来换取时间。
对齐模数必须是2的整数次幂。
内存对齐的规则:
- 第一个成员必须是从0位置开始偏移;
- 下面的成员从 成员的大小 和 对齐模数相比 较小的那个数的整数倍 的地方开始;
如果成员是结构体变量,就把它里面最大的成员拿出来和对齐模数作比较,取小的那个的整数倍; - 最后要对结构体整体进行对齐,整个结构体的大小应该是:成员中最大的那个(成员中如果有结构体变量,依然是把它里面最大的成员拿出来)和对齐模数相比,取小的那个的整数倍。
示例:
#include<iostream>
using namespace std;
//#pragma pack(show) //默认的对齐模数是8
//#pragma pack(1) //如果把对齐模数改为1,就相当于不存在内存对齐了,结果就是所有的字节数加在一起的总和struct Student{//对齐模数是8int a; //0 ~ 3float b; //4 ~ 7 float大小是4,4和8相比,4小,从4的整数倍开始char c1; //8 ~ 8 15 char大小是1,1和8相比,1小,从1的整数倍开始double d; //9 16 ~ 23 double大小是8,8和8相比,8小,从8的整数倍开始(所以把9改成16,上面的8改成15//最后,最大的8和对齐模数8相比,8小,所以整个结构体的大小是8的整数倍,结果是24
};
struct Student2{//对齐模数是8int a; //0 ~ 3 7Student stu;//4 8 ~ 31 结构体中最大的是8,8和8相比,8小,所以从8的整数倍开始(把4改成8,上面的3改成7double d; //32 ~ 39 double大小是8,8和8相比,8小,所以从8的整数倍开始char e; //40 ~ 40 47 char的大小是1,1和8相比,1小,所以从1的整数倍开始//最后,最大的8和对齐模数8相比,8小,所以整个结构体的大小是8的整数倍,结果是48(所以上面的40改成47
};
int main(){cout << "float的大小:" << sizeof(float) << endl;//4cout << "double的大小:" << sizeof(double) << endl;//8cout << "int的大小:" << sizeof(int) << endl;//4cout << "char的大小:" << sizeof(char) << endl;//1cout << "结构体Student的大小:" << sizeof(Student) << endl;//24 如果把对齐模数改为1,结果是17cout << "结构体Student2的大小:" << sizeof(Student2) << endl;//48 如果把对齐模数改为1,结果是30return 0;
}
名称空间
(视频课中从02:03:00开始)
C++ Primer Plus(嵌入式公开课)—第九章 内存模型和名称空间
作用域解析运算符(两个冒号::
)
它的优先级是运算符中等级最高的,例如cat.Animals::name
它有三个作用:1.全局作用域符;2.类作用域符;3名称空间作用域符
::
前面没有任何内容,代表全局作用域符。
名称空间
名称空间中可以写什么?(变量、函数、结构体、类…)
using声明和using编译指令
using声明和using编译指令,是用来简化对名称空间中名称的使用;
using声明:使特定的标识符可用;using std::cout; using std::endl;
using编译指令:让整个名称空间中的名称可用;using namespace std;
也可以不用using声明,也不用using编译指令:std::cout << ""<< std::endl;
有了using声明或者using编译指令,下面再使用时就不需要再加作用域了
注意:
using声明:如果局部变量和using声明同时使用会出现问题;
using编译指令:如果出现同名的变量,不会报错,使用就近原则。(局部名称隐藏名称空间名)
示例:
#include<iostream>
using namespace std;//让std这个名称空间下的所有名称都可用namespace fun{int num = 1;double d = 1.1;
}namespace fun1{int num = 1;double d = 1.1;
}int main(){cout << fun::num << endl;//1int num = 2; using namespace fun; //using编译:让fun名称空间下的所有名称都可用 如果出现同名的变量,不会报错,使用就近原则。//using fun::num; //using声明:只让fun名称空间下的变量num可用 如果和局部变量一起用会出错//有了上面的using声明或者using编译指令,下面再使用时就不需要再加作用域了cout << num << endl; //2cout << num << endl; //有了上面的using声明,下面再使用时就不需要再加作用域了cout << d << endl;return 0;
}
模板
(视频课中从02:16:45开始)
C++笔记7:C++提高编程1:模板—[函数模板和类模板]
C++除了面向对象编程思想,还有泛型编程思想。
泛型编程主要是利用模板技术来实现的。
函数模板
①函数模板语法
语法:
template<typename T>
函数声明或定义
其中:
template
— 声明创建模板
typename
— 表面其后面的符号是一种数据类型,可以用class
代替
T
— 通用的数据类型,名称可以替换,通常为大写字母
②函数模板和普通函数的区别
函数模板不允许自动类型转换;
普通函数能够自动类型转换;(char --> int)
③函数模板调用规则
C++编译器优先考虑普通函数;
可以通过空模板实例参数列表的语法限定编译器只能通过模板匹配;
函数模板可以重载;
如果函数模板可以产生一个更好的匹配,那么优先选择模板。
④模板实现机制及局限性
函数模板通过具体类型产生不同的函数。
通过函数模板产生的函数称为模板函数。
类模板
①类模板基础语法
template<typename T>//typename可以用class代替
类
②类模板中成员函数的创建时机
类模板中成员函数并不是一开始创建,而是在使用的时候才生成,在替换T后生成。
#include<iostream>
using namespace std;template<class T>
class testClass{public:void func1(){obj.show1();}void fun2(){obj.show2();}T obj;
};
class Person1{public:void show1(){cout << "调用show1()函数" << endl;}
};
class Person2{public:void show2(){cout << "调用show2()函数" << endl;}
};
int main(){testClass<Person1> tc;//testClass<Person2> tc;//编译错误tc.func1();return 0;
}
③类模板做函数参数
④类模板派生类
⑤类模板成员函数类内实现
⑥类模板成员函数类外实现
⑦类模板分文件编写(类模板文件 .hpp)
把类的声明(.h
)和实现(.cpp
)写在一起放到.hpp
文件中,一看.hpp
文件就知道是类模板文件。
⑧模板类碰到友元函数
⑨模板案例—数组类封装
5.8 STL(标准模板库)
C++笔记8:C++提高编程2:STL—标准模板库
SRL六大组件:容器、算法、迭代器、仿函数、适配器、空间配置器
容器:各种数据结构,vector、list、deque、set、map,用来存放数据,是一种类模板;
算法:各种常用的算法,sort、find、copy、for_each,是一种函数模板;
迭代器:相当于指针,是一种将operator*,operator->,operator++,operator–等指针相关操作予以重载的类模板(运算符重载?);
仿函数:重载函数调用运算符()
的类或者类模板;
适配器:用来修饰容器、仿函数、迭代器的接口
空间配置器:负责空间的配置与管理;比如vector容器的扩容就是这个空间配置器来完成的。
容器:序列式容器(vector、deque、list)、关联式容器(map、set);
算法:质变算法(拷贝、替换、删除)、非质变算法(查找、计数、遍历);
迭代器:输入迭代器(只读)、输出迭代器(只写)、前向迭代器、双向迭代器、随机访问迭代器。
vector扩容:
size()是容器中的元素个数、capacity()是容器的容量;
reserve(int len);//预留len个元素长度;
vector与普通数组区别:数组是静态空间,而vector可以动态扩展。
(补充:动态扩展并不是在原空间之后续接新空间,而是找更大的内存空间,然后将原数据拷贝至新空间,释放原空间;并且原有的迭代器会失效。)
另外,vector容器的迭代器是支持随机访问的迭代器!!!
总结 常用容器的排序的区别:
主要看下面两个地方的总结:
我的C++八股文中的笔记9最大的收获;
C++笔记8:C++提高编程2:STL—标准模板库中的☆☆☆总结 常用容器的排序
的区别
总结:
0.自定义排序规则的实现方式
自定义排序规则:(全局函数)
- mySort0()//内置数据类型
- mySort0()//自定义数据类型
自定义排序规则:(仿函数)
- mySort2()//内置数据类型和自定义数据类型
具体代码:
//自定义排序规则:(全局函数)
bool mySort0(int a, int b){//降序if(a > b)return 1;else return 0;
}
bool C(const Person& p1, const Person& p2){//降序if(p1.age > p2.age)//按年龄降序return 1;else if(p1.age == p2.age){//如果年龄相等,就按身高升序return p1.height < p2.height;}else return 0;
}
//自定义排序规则:(仿函数)
class mySort2{public://重载函数调用运算符()bool operator()(int a, int b){return a > b;}bool operator()(const Person& p1, const Person& p2) {//constif(p1.age > p2.age)//按年龄降序return 1;else if(p1.age == p2.age){//如果年龄相等,就按身高升序return p1.height < p2.height;}else return 0;}
};
1.vector容器,deque容器;&& 2.list容器
对于内置数据类型:
如果想自定义排序规则,可以使用全局函数和仿函数和内建仿函数来实现:
- 如果用全局函数,那么参数就写全局函数名;
- 如果用仿函数,参数就是类名后面加个括号,相当于是个利用无参构造创建的匿名类对象;
- 也可以直接使用内建仿函数(大于仿函数
greater<>()
)直接实现降序排列,需要包含头文件#include<functional>
。
1.vector容器,deque容器:
vector<int> v1;
sort(v1.begin(), v1.end());//默认升序排序
sort(v1.begin(), v1.end(), mySort0);//降序排列 mySort0是一个全局函数
sort(v1.begin(), v1.end(), mySort2());//降序排列 mySort2()是一个类名,仿函数
sort(v1.begin(), v1.end(), greater<>());//降序排列,使用内建函数对象(仿函数),就不用自己写排序规则了2.list容器:
list<int> L;
L.sort();//默认升序排序
L.sort(greater<>()); //降序排列,使用内建函数对象(仿函数),就不用自己写排序规则了
L.sort(mySort0);//降序排列
L.sort(mySort2());//降序排列
对于自定义数据类型:
如果容器中存放的是自定义数据类型,就必须要指明自定义的排序规则,不能使用默认的排序规则。
1.vector容器,deque容器:
vector<Person> v2;
//sort(v2.begin(), v2.end());//报错!!!没有指定排序规则
//sort(v2.begin(), v2.end(), greater<>());//会报错!!!不能使用默认的排序规则
sort(v2.begin(), v2.end(), mySort1);//默认排序不能用,必须指明排序规则,mySort1是一个全局函数
sort(v2.begin(), v2.end(), mySort2());//默认排序不能用,必须指明排序规则,mySort2()是一个类名,仿函数2.list容器:
list<Person> L2;
//L2.sort();//报错!!!没有指定排序规则
//L2.sort(greater<>());//会报错!!!不能使用默认的排序规则
L2.sort(mySort1);//默认排序不能用,必须指明排序规则,mySort1是一个全局函数
L2.sort(mySort2());//默认排序不能用,必须指明排序规则,mySort2()是一个类名,仿函数
3.set容器,map容器:
没有sort(),因为此容器会自动排序,默认是升序,因此可以利用仿函数实现自定义排序规则,不能通过全局函数来实现。
对于内置数据类型:
如果想自定义排序规则,可以使用仿函数和内建仿函数来实现:
- 可以使用仿函数,参数就是类名,后面不加括号;
- 也可以直接使用内建仿函数(大于仿函数
greater<>
,后面也没有括号)直接实现降序排列,需要包含头文件#include<functional>
。
对于自定义数据类型:
如果容器中存放的是自定义数据类型,在创建容器的时候就要指明自定义的排序规则,不能使用默认的排序规则。
注意:这里的排序规则写的是类名,后面不用加括号,跟上面的vector、deque、list不一样,注意区分!!!
set容器:
内置数据类型:
set<int> s1;//默认升序排序
//set<int,mySort0> s2;//错误,这里不能用全局函数,只能用仿函数实现
//set<int,mySort2()> s2;//错误,这里写的是类名,后面不用加括号
//set<int, greater<>()> s2;//错误,后面不用加括号
注意:仿函数和内建仿函数后面都不用加括号,跟上面的vector、deque、list不一样,注意区分:
set<int,mySort2> s2;//降序排列 mySort2是一个类名,后面不用加括号,仿函数
set<int, greater<>> s2;//降序排列,使用内建函数对象(仿函数),这里也没有括号,跟上面不一样自定义数据类型:
//set<Person> s5;//报错!!!没有指定排序规则
//set<Person, greater<>> s3;//会报错!!!不能使用默认的排序规则
//set<Person,mySort1> s3;//错误,这里不能用全局函数,只能用仿函数实现
//set<Person,mySort2()> s2;//错误,这里写的是类名,后面不用加括号
set<Person,mySort2> s3;//如果容器中存储的是自定义数据类型,在创建容器的时候就要指明自定义的排序规则
//注意:这里mySort2后面也没有括号!!!
map容器:
map<double, double> m1;//默认排序规则---按照Key值从小到大排
map<double, double,MySort2> m2;//自定义排序规则---在创建容器的时候就指明排序规则(按Key值降序)
map<double, double, greater<>> m5;//使用内建仿函数(大于仿函数greater<>()):按Key值降序map<double, Person> m3;//默认排序规则:按Key值升序
map<double, Person,MySort2> m6;//利用仿函数自定义排序规则:按Key值降序
map<double, Person, greater<>> m7;//内建仿函数(大于仿函数greater<>()):按Key值降序
4.汇总
默认(升序)排序规则 |
内建仿函数 默认的降序排序规则 |
全局函数实现降序排序规则 | 仿函数实现降序排序规则 | 写参数时加不加括号 | |
---|---|---|---|---|---|
vector容器、deque容器、list容器 (内置数据类型) |
√ | √ | √ | √ | 全局函数后不加括号;类名后面加括号 |
vector容器、deque容器、list容器 (自定义数据类型) |
× | × | √ | √ | 全局函数后不加括号;类名后面加括号 |
set容器 (内置数据类型) |
√ | √ | × | √ |
不能用全局函数实现,只能用仿函数实现 并且是用仿函数时类名后面不加括号 |
set容器 (自定义数据类型) |
× | × | × | √ |
只能用仿函数实现 并且是用仿函数时类名后面不加括号 |
map容器 (内置数据类型) |
√ 按照key值从小到大排序 |
√ 按照key值从小到大排序 |
× | √ |
不能用全局函数实现,只能用仿函数实现 并且是用仿函数时类名后面不加括号 |
map容器 (自定义数据类型) |
√ 按照key值从小到大排序 |
√ 按照key值从小到大排序 |
× | √ |
不能用全局函数实现,只能用仿函数实现 并且是用仿函数时类名后面不加括号 |
函数对象 & 谓词
函数对象基本概念:
重载函数调用操作符()
的类,其对象常称为函数对象;
函数对象在使用重载的函数调用操作符()时,行为类似函数调用,所以也叫仿函数。
注意:函数对象(仿函数)是一个类,不是一个函数。(和普通函数类似,又超出普通函数的概念)
谓词基本概念:
返回bool
类型的仿函数称为谓词;
如果operator()
接受一个参数,那么叫做一元谓词;
如果operator()
接受两个参数,那么叫做二元谓词。
内建函数对象
需要引入头文件:
#include<functional>
关系仿函数中最常用的就是greater<>
大于,
因为默认排序规则都时升序,想要实现降序排序规则,就要是用大于仿函数greater<>()
。
STL—常用算法
可以看C++笔记9:C++提高编程3:STL—函数对象&标准算法中的5、STL—常用算法
5.9 C++新特性
见笔记③:牛客校招冲刺集训营—C++工程师
笔记②:牛客校招冲刺集训营---C++工程师(面向对象(友元、运算符重载、继承、多态) -- 内存管理 -- 名称空间、模板(类模板/函数模板) -- STL)相关推荐
- 笔记③:牛客校招冲刺集训营---C++工程师(5.9 C++新特性)
0625 C++工程师 第5章 高频考点与真题精讲 5.1 指针 & 5.2 函数 5.3 面向对象(和5.4.5.5共三次直播课) 5.3.1 - 5.3.11 5.3.12-38 5.6 ...
- 2021牛客OI赛前集训营-提高组(第四场) T2空间跳跃
2021牛客OI赛前集训营-提高组(第四场) 题目大意 给你三个整数 n , d , l n,d,l n,d,l, n n n为正整数.负整数或0, d , l d,l d,l为正整数,你现在有一个数 ...
- 小a与204(牛客寒假算法集训营1题目B)
链接:https://ac.nowcoder.com/acm/contest/317/B 来源:牛客网 时间限制:C/C++ 1秒, 其他语言2秒 空间限制: C/C++ 262144K,其他语言52 ...
- 【2020牛客NOIP赛前集训营-提高组(第一场)题解】( 牛牛的方程式,牛牛的猜球游戏,牛牛的凑数游戏,牛牛的RPG游戏)
未完待续... T1:牛牛的方程式 title solution code T2:牛牛的猜数游戏 title solution code T3:牛牛的凑数游戏 title solution code ...
- 2020牛客NOIP赛前集训营-普及组第三场C牛半仙的妹子树
链接:https://ac.nowcoder.com/acm/contest/7608/C 来源:牛客网 牛半仙有 n 个妹子,她们所在的位置组成一棵树,相邻两个妹子的距离为 1. 有 m 个妹 ...
- 2020牛客NOIP赛前集训营-提高组(第一场) T2 牛牛的猜球游戏
题目链接: 牛客原站 通过记录: 题目链接2:T277380 牛牛的猜球游戏(被我们搬到洛谷力): 洛谷搬运 题目描述 有十个数 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ...
- 【2020牛客NOIP赛前集训营-提高组(第二场)】题解(GCD,包含,前缀,移动)
文章目录 T1:GCD title solution code T2:包含 title solution code(正解code补充在上面了) T3:前缀 title solution code T4 ...
- 2021牛客OI赛前集训营-提高组(第五场)D-牛牛的border【SAM】
正题 题目链接:https://ac.nowcoder.com/acm/contest/20110/D 题目大意 求一个长度为nnn的字符串的所有子串的borderborderborder长度和. 1 ...
- 2021牛客OI赛前集训营-提高组(第五场)C-第K排列【dp】
正题 题目链接:https://ac.nowcoder.com/acm/contest/20110/C 题目大意 一个长度为nnn的字符串SSS,SSS中存在一些???,有N/O/I/PN/O/I/P ...
最新文章
- mysql 查询结果转置_转置MySQL查询 – 需要将行放入列中
- 11月27号例会记录
- Android—TableLayout自定义表格
- next() 与 nextLine() 区别
- 《大话操作系统——做坚实的project实践派》(6)
- 每日总结-2016年3月9日
- 移动端开发注意之一二
- C++开发者都应该使用的10个C++11特性
- linux转mysql_[转] linux下安装mysql服务器
- 电子书包“翻转”课堂
- 信息学奥赛一本通 1023:Hello,World!的大小 | OpenJudge NOI 1.2 10
- 如何在Postgresql中使用模糊字符串匹配
- 超实用的微信图片转换工具
- C#图片处理之: 获取数码相片的EXIF信息(二)
- python中threading中的lock类
- 「代码随想录」123.买卖股票的最佳时机III【动态规划】力扣详解!
- jQuery.bind事件 详解
- java nextline_使用新一代Java
- 我们是如何优化英雄联盟的代码的
- aho-corasick automaton (AC自动机)的理解