参考链接:7.18、面试回顾
1、c++特性
1) 封装、继承、多态
a) 封装是为了代码模块化和增加安全性
保护数据成员,不让类以外的程序直接访问或者修改类的成员,只能通过其成员对应方法访问(即数据封装)
隐藏方法实现的具体细节,仅仅提供接口,内容修改不影响外部调用(即方法封装)
private:
只能被访问:1.该类中的函数、2.其友元函数。
不能被任何其他访问,该类的对象也不能访问。
protected:可被访问:1.该类中的函数、2.子类的函数、3.其友元函数。
但不能被该类的对象访问。
public:可被访问:1.该类中的函数、2.子类的函数、3.其友元函数、4.该类的对象。
b) 继承的目的: 重用代码一个类B继承另一个类A,则B就继承了A中申明的成员以及函数
派生的目的: 代码扩展,继承自一个类然后添加自己的属性和方法则实现代码的扩展
c) 多态: 接口的复用,一个接口多种实现
用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数
多态包括静态多态(编译时多态)和动态多态(运行时多态)。
静态多态:编译时多态,是早绑定;(编译时根据形参类型或运算符进行选择)。
包括函数重载、运算符重载、重定义。动态多态:运行时多态,是晚绑定;(在程序运行时根据对象的实际类型进行选择)
通过继承和虚函数实现。
2) c++class和C语言struct的区别
a) C++ 中保留了C语言的 struct 关键字,并且加以扩充。
b) 在C语言中,struct 只能包含成员变量,不能包含成员函数。而在C++中,struct 类似于 class,既可以包含成员变量,又可以包含成员函数。
C++中的 struct 和 class 基本是通用的,唯有几个细节不同
a) 使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public属性的。
b) class 继承默认是 private 继承,而 struct 继承默认是 public 继承。
c) class 可以用在模板参数中,而struct 不能。
3) 析构函数、虚析构函数、纯虚析构函数
对象在结束其生命周期之前,都会调用析构函数以完成必要的清理工作;派生类调用的析构函数顺序是“先子类,后基类”;
普通析构函数:
1、若delete运算符删除的是子类指针对象,则会调用子类和基类的析构函数;
2、若析构函数是非虚的,即使基类指针指向的是子类对象,则delete 指针对象时,也仅调用基类的析构函数
虚析构函数:(目的:通过父类指针释放整个子类空间。)
1、虚析构函数用于在析构对象时调用其派生类的析构函数,确保释放所有内存空间。2、虚析构函数在基类中有默认的实现,可以在派生类中重新实现。也就是虚析构使用virtual修饰,有函数体,不会导致父类变成抽象类。
3、当基类的析构函数为虚函数时,基类指针指向的是子类对象时,使用delete运算符删除指针对象,析构函能够按照“先子类,后基类”的原则完成对象清理;这样在多重继承的类中,能够保证每个类都能够得到正确的清理;比如基类和子类的缓冲区都能被释放;
纯虚析构函数:
纯虚析构的本质是析构函数,复杂各个类的回收工作。而且析构函数不能被继承。
注意:
必须为纯虚析构函数提供一个函数体。
纯虚析构函数必须类外实现。
也就是:尽管纯虚析构函数在基类中没有具体的实现(即没有函数体),但它必须在类的外部提供定义(一个空的函数体)
虚析构函数
原理剖析:
构造的顺序:父类
-->
成员-->
子类。析构的顺序:子类
-->
成员-->
父类。class Animal { public: Animal() { cout << "Animal构造函数" << endl; } // 纯虚函数 virtual void speak() = 0; virtual ~Animal() { cout << "Animal析构函数" << endl; } }; class Dog :public Animal { public: Dog() { cout << "Dog构造函数" << endl; } // 子类一定会重写父类的虚函数 void speak() { cout << "狗在汪汪" << endl; } ~Dog() { cout << "Dog析构函数" << endl; } }; int main() { Animal *p = new Dog; p->speak(); delete p; return 0; }
输出:
Animal构造函数 Dog构造函数 狗在汪汪 Dog析构函数 Animal析构函数
4)#include “filename.h”和#include < filename.h>的区别
答:对于#include <filename.h>编译器从标准库开始搜索filename.h
对于#include “filename.h” 编译器从用户工作路径开始搜索filename.h
5)友元函数(能访问类的私有成员和保护成员)
友元函数不是类的成员函数,但可以被声明为某个类的友元,从而能够访问该类的私有成员(private)和保护成员(protected)。
声明通常放在类的内部,但定义(即函数体)可以在类的外部进行。
友元函数不一定是全局函数。其可以是全局函数、其他类的成员函数,甚至是静态函数。
class Box { double width; // 私有成员 public: Box(double w) : width(w) {} // 声明友元函数 friend void printWidth(const Box& box); // 其他成员函数... }; // 定义友元函数 void printWidth(const Box& box) { cout << "Width of box : " << box.width << endl; } int main() { Box box(10.0); // 调用友元函数 printWidth(box); return 0; }
友元关系不是相互的:如果类A声明类B的一个成员函数为友元,那么只有那个特定的成员函数是类A的友元,而不是类B的所有成员函数。
友元函数不是类的成员函数:它不能通过类的实例来调用,也不能访问类的
this
指针(即没有this
指针)。友元可以定义在类外部:如上例所示,友元函数的定义可以放在类的外部。
友元函数可以访问类的所有成员:包括私有成员和保护成员。
友元函数破坏了封装性:虽然友元函数在某些情况下很有用,但它允许外部代码访问类的内部状态,这可能会破坏类的封装性。因此,应该谨慎使用友元函数。
友元函数可以是另一个类的成员函数:在这种情况下,整个类(而不是单个成员函数)成为友元。
友元函数可以重载:就像其他函数一样,友元函数也可以被重载。
6)虚函数与纯虚函数,即动态多态(运行时多态)
C++ 多态深度解析:理解虚函数与纯虚函数 (qq.com)
C++中的虚函数和纯虚函数都是为了实现多态性而存在的:
虚函数:在基类中声明的函数,可以在派生类中被覆盖(重写)。虚函数在基类中有一个默认的实现,但在派生类中可以重新实现,实现与基类虚函数的方法签名相同。派生类中定义虚函数时,必须使用关键字“virtual”来声明。也就是虚函数使用virtual修饰,有函数体,不会导致父类变为抽象类。
纯虚函数:在基类中声明的函数,在基类中没有实现,必须在派生类中实现。纯虚函数使用“=0”来声明,例如“virtual void foo() = 0;”。纯虚函数没有默认实现,所以派生类必须实现它们。也就是纯虚函数有virtual修饰,=0,没有函数体,导致父类为抽象类,子类必须重写父类的所有纯虚函数。
区别:
虚函数可以有实现,纯虚函数没有实现。
虚函数可以在基类中有默认的实现,纯虚函数没有默认实现。
虚函数不一定要在派生类中实现,但纯虚函数必须在派生类中实现。
如果一个类中有纯虚函数,那么它就是抽象类。不能创建抽象类的对象,只能创建它的派生类对象。
虚函数是可选的,它有一个默认的实现,但可以在派生类中重新实现;
纯虚函数是必须实现的,它没有默认的实现,只有声明,必须在派生类中实现。
虚函数用于实现多态性,而纯虚函数用于定义接口
虚函数
- 虚函数是在基类中用
virtual
关键字声明的成员函数。- 当通过基类指针或引用调用虚函数时,会根据对象的实际类型(即对象的动态类型)来调用相应的函数版本,这称为动态绑定或晚期绑定。
- 虚函数允许子类覆盖基类的实现,从而实现多态性。
- 虚函数必须有函数体,即使它只是一个空的函数体(即只包含
{}
)。
class Animal {
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Dog :public Animal {
public:
void speak()
{
cout << "狗在汪汪" << endl;
}
};
class Cat :public Animal {
public:
void speak()
{
cout << "猫在喵喵" << endl;
}
};
//设计一个算法
void showAnimal(Animal *p)
{
p->speak();
delete p;
}
int main()
{
showAnimal(new Dog);
showAnimal(new Cat);
return 0;
}
输出:
狗在汪汪
猫在喵喵
纯虚函数
- 纯虚函数是在基类中用
virtual
和= 0
关键字声明的成员函数。- 纯虚函数没有函数体,只有函数声明。它要求任何继承该基类的子类都必须提供该函数的实现。
- 含有纯虚函数的类被称为抽象类,抽象类不能被实例化。
- 纯虚函数的主要目的是强制派生类实现特定的接口,从而允许在基类中定义接口而在派生类中实现这些接口
class 类名{
public:
// 纯虚函数
virtual 函数返回值类型 函数名(参数列表)=0;
};
//例如
class Animal {
public:
virtual void speak()=0;
};
一旦类中有纯虚函数,那么这个类就是抽象类。
抽象类不能实例化对象。
抽象类必须被继承,同时子类必须重写父类的所有纯虚函数,否则子类也是抽象类。
抽象类主要目的是设计类的接口。
应用实例:
class Animal { public: // 纯虚函数 virtual void speak() = 0; }; class Dog :public Animal { public: // 子类一定会重写父类的虚函数 void speak() { cout << "狗在汪汪" << endl; } }; class Cat :public Animal { public: // 子类一定会重写父类的虚函数 void speak() { cout << "猫在喵喵" << endl; } }; //设计一个算法 void showAnimal(Animal *p) { p->speak(); delete p; } int main() { showAnimal(new Dog); showAnimal(new Cat); return 0; }
2、静态链接库和动态链接库
a) 静态库的扩展名一般为“.a”或“.lib”;动态库的扩展名一般为“.so”或“.dll”。
b) 静态库在编译时会直接整合到目标程序中,编译成功的可执行文件可独立运行;动态库在编译时不会放到连接的目标程序中,即可执行文件无法单独运行。
c) 静态库的缺点:利用静态库编译成的文件比较大;升级难度没有明显优势,如果函数需要更新,需要重新编译;
d) 动态函数库在编译的时候,在程序里只有一个“指向”的位置而已,也就是说当可执行文件需要使用到函数库的机制时,程序才会去读取函数库来使用;从产品功能升级角度方便升级,只要替换对应动态库即可,不必重新编译整个可执行文件。
e) 综上,不能看出:从产品化的角度,发布的算法库或功能库尽量使动态库,这样方便更新和升级,不必重新编译整个可执行文件,只需新版本动态库替换掉旧动态库即可。
1) 动态链接和静态链接
扩展:静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器或汇编器生成)链接到一块生成可执行程序。静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。
动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在Windows的管理下,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。
2)QT中引入SDK开发包的原理讲解
原理简介:头文件(.h)、源文件(.c)、库文件(.lib .dll)
1.头文件(,h):声明函数接口,一些函数方法名
2.源文件(.c):对头文件中函数的实现源代码
3. .lib库文件有两种
(1)静态链接库(静态库):把程序中调用的某函数的相关模块链接在一起,然后放入内存进行执行。
(2)动态链接库(导入库):把调用的函数所在文件模块(DLL)和调用函数在文件中的位置等信息链接进目标程序,程序运行时再从DLL中寻找相应函数代码进行执行。
所以.lib库就是导入.dll文件用的。
4. .dll库文件:
含有函数的可执行代码;与.c文件的源代码的关系是:.c的源代码编译封装之后就形成.dll文件
综上所述,这几个文件调用关系大致为:当我们在自己的程序中引用了一个H文件里的函数,编链器怎么知道该调用哪个.dll文件呢?这就是.lib文件的作用: 告诉链接器 调用的函数在哪个.dll模块中,函数执行代码在.dll中的什么位置,这也就是为什么需要 ”附加依赖项“ .lib文件,它起到连接的桥梁作用。
.h头文件是编译时必须的,lib库是链接时需要的,dll动态链接库是运行时需要的。
参考链接:QT中引入海康威视SDK开发包(原理讲解:头文件(.c)、库文件(.lib .dll))_csdn qt导入头文件与.lib-CSDN博客
3、常用的设计模式
1) 单例模式
a) 私有化它的构造函数,以防止外界创建单例类的对象;
b) 使用类的私有静态指针变量指向类的唯一实例;
c) 使用一个公有的静态方法获取该实例。
class Singleton
{
private:
static Singleton* instance;
private:
Singleton() {};
~Singleton() {};
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
public:
static Singleton* getInstance()
{
if(instance == NULL)
instance = new Singleton();
return instance;
}
};
// init static member
Singleton* Singleton::instance = NULL;
通过加锁避免:两个以上线程调用GetInstance(),同时监测到instance为null的情况;
2)工厂模式
工厂模式:子类对象用相同的父类模块方法,不同的子类分别实现模块方法中的抽象方法,从而实例化不同的子类对象(父类提供抽象方法,继承了的子类自己各自慢慢写怎么叫,怎么飞)
3) 策略模式和工厂模式区别
它们的用途不一样。简单工厂模式是创建型模式,它的作用是创建对象。策略模式是行为型模式,作用是在许多行为中选择一种行为,关注的是行为的多样性。
(简单来说,工厂模式就是英语考试的完形填空题(自己考虑填什么词、句子),策略模式就是信息匹配题(提前写好选择项,当你有多个填空时,直接选就好了,没有适合的就再多写几个选择项))
4、指针与引用
值传递、引用传递、指针传递
指针和引用的区别:
1 指针有自己的一块空间(指针所指向的地址需要存放数据的时候需要申请空间),而引用只是一个别名;(本质)
2 使用sizeof看一个指针的大小是4(32位下),而引用则是被引用对象的大小; (大小)
3 指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;(初始化)
4 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;
5 可以有const指针,但是没有const引用;
6 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
补充:
7、指针可以有多级指针(**p),而引用至多一级;
8、指针和引用使用++运算符的意义不一样;
9、如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
5、内存的分配方式有几种?
1、静态存储区域分配,内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量、静态变量
2、在栈上创建,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
3、在堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
4、文字常量区,常量字符串就是放在这里的,程序结束后由系统释放。
5、程序代码区,存放函数体的二进制代码。
6、STL容器
Vector(后面的类型莫忘)
1、什么是vector?
向量(vector)是一个封装了动态大小数组的顺序容器(Sequence Container)。跟任意其它类型容器一样,它能够存放各种类型的对象。可以简单的认为,向量是一个能够存放任意类型的动态数组。
2、容器特性
1.顺序序列
顺序容器中的元素按照严格的线性顺序排序。可以通过元素在序列中的位置访问对应的元素。
2.动态数组
支持对序列中的任意元素进行快速直接访问,甚至可以通过指针算述进行该操作。操供了在序列末尾相对快速地添加/删除元素的操作。
3.能够感知内存分配器的(Allocator-aware)
容器使用一个内存分配器对象来动态地处理它的存储需求。
3、常用操作
1、 构造
vector v1; //默认构造 无参数
int arr[] = { 20,10,23,90 };
vector v2(arr,arr+sizeof(arr);//使用数组对vector进行初始化 参数:起始地址、长度
vector v3(v2.begin(), v2.end());//使用向量对vector进行初始化 参数:向量起始地址、向量末地址
vector v4(v3); //使用其他向量对vector进行初始化 参数:vector
vector<int> dp(w + 1, 0);//初始化dp数组,往里放进w+1个0
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));//初始化
2、 赋值操作
Vector v2;
V2.assaign(v1.begin(), v1.end()); //使用向量对vector进行赋值 参数:起始地址、末地址
vector v3;
v3 = v2;//=号操作符重载
int arr1[] = { 200,100,300,400 };
vector v4(arr1, arr1 + sizeof(arr1) / sizeof(int));
v4.swap(v1); //使用swap函数
3、 常用函数
resize(int n,element) ;将容器的大小设为n,扩容后的每个元素的值都为element
v1[i]、v1.at(i)、v1.front()、v1.back():取值操作
v1.insert(v1.begin(), 2, 1000); 在指定位置loc前插入num个值为val的元素
map
1、什么是map?
C++ 中 map 提供的是一种键值对容器,里面的数据都是成对出现的,如下图:每一对中的第一个值称之为关键字(key),每个关键字只能在 map 中出现一次;第二个称之为该关键字的对应值。
2、常用操作
1、 插入操作
m.insert(pair<int, int>(1, 10)); //插入pair
m.insert(make_pair<int, int>(2, 20));
m[4] = 40;
2、 查找操作
map<int,int> ::iterator pos = m.find(3);
if (pos != m.end()) // true为元素存在,false元素不存在
3、 删除操作
iterator erase(iterator it) ;//通过一个条目对象删除
iterator erase(iterator first,iterator last); //删除一个范围
size_type erase(const Key&key); //通过关键字删除
clear();//就相当于enumMap.erase(enumMap.begin(),enumMap.end());
7、多线程
7.1、多线程和多进程的区别
1)进程是资源分配的最小单位,线程是CPU调度的最小单位
2)
组成:
核函数 =(1个线程网络 + 1个线程块);
1个线程网络 =(多个线程块);
线程块 =(多个线程)。
7.2、Qt程序主界面运行卡断如何解决
原因:显示数据和处理数据放在同一个线程中处理
做法:将读取处理数据和显示数据两个步骤分离,这属于异步操作(同步:等待数据处理接口的返回值)
异步:线程间相互排斥的使用临界资源的现象,就叫互斥。
同步:线程之间的关系不是相互排斥临界资源的关系,而是相互依赖的关系。
7.3、多线程通线(共享内存和消息传递)
1、volatile关键字来实现线程间通信是使用共享内存的思想。程序中运用的方式大致意思是多个线程同时监听一个变量,当这个变量发生变化的时候,线程能偶感知来执行相应的业务。
2、QT中常用信号与槽,来实现跨线程通信。跨线程的信号与槽使用自定义类型时,使用qRegisterMetaType对自定义类型进行注册。
7.4、Qt信号与槽的链接方式
1、Q::DirectConnection:槽函数会在信号发送的时候直接被调用(本质:槽函数运行于信号发送者所在的线程),多线程环境下比较危险,可能会造成崩溃。
2、Qt::QueuedConnection:槽函数在控制回到接收者所在线程的事件循环时被调用(本质:槽函数运行于信号接收者所在的线程的事件循环时被调用),多线程环境下一般用这个。
7.5、多进程通信(进程间通信)
每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。
7.5.1 管道
(最简单的通信方式)
1)管道传输数据是单向的。
2)管道就是内核里面的一串缓存。(进程写入的数据都是缓存在内核中)
3)通信方式效率低,不适合进程间频繁地交换数据。
4)通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。
ps:匿名管道通信的数据是无格式的流并且大小受限。
匿名管道:通信范围是存在父子关系的进程
命名管道:可以在不相关的进程间也能相互通信
7.5.2 消息队列
(解决了管道通信方式效率低的弊端)
1)消息队列是保存在内核中的消息链表
2)不适合比较大数据的传输
3)通信过程中,存在 用户态与内核态之间 的数据拷贝开销
不足:
一是通信不及时,二是附件也有大小限制(消息体都是固定大小的存储块)。
7.5.3 共享内存
(解决了消息队列通信过程存在用户态与内核态之间的消息拷贝过程的弊端)
共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。
(直接分配一个共享空间,每个进程都可以直接访问)
弊端:
如果多个进程同时修改同一个共享内存,很有可能就冲突了。例如两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。
(多进程竞争共享资源,而造成的数据错乱)==》需要保护机制,使得共享的资源,在任意时刻只能被一个进程访问。(信号量能解决)
7.5.4 信号量
(解决了共享内存中多进程竞争共享资源,而造成的数据错乱的弊端)
信号量作为保护机制,使得共享的资源,在任意时刻只能被一个进程访问。
1)信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。
2)信号量不仅可以实现访问的互斥性,还可以实现进程间的同步,如下:
信号初始化为
1
,就代表着是互斥信号量,它可以保证共享内存在任何时刻只有一个进程在访问,这就很好的保护了共享内存。信号初始化为
0
,就代表着是同步信号量,它可以保证进程 A 应在进程 B 之前执行。
信号量表示资源的数量,控制信号量的方式有两种原子操作:
- 一个是 P 操作,这个操作会把信号量减去 -1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
- 另一个是 V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;
P 操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,这两个操作是必须成对出现的。
7.5.5 信号
上面说的进程间通信,都是常规状态下的工作模式。
对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。
信号是进程间通信机制中唯一的异步通信机制。
用户进程对信号的处理方式:(进程响应信号的三种方式)
1.执行默认操作。
2.捕捉信号。
3.忽略信号。
7.5.6 Socket
管道、消息队列、共享内存、信号量和信号都是在同一台主机上进行进程间通信。
跨网络与不同主机上的进程之间通信 ==》 Socket 通信。
实际上,Socket 通信不仅可以跨网络与不同主机的进程间通信,还可以在同主机上进程间通信。
根据创建 socket 类型的不同,通信的方式也就不同:
- 实现 TCP 字节流通信: socket 类型是 AF_INET 和 SOCK_STREAM;
- 实现 UDP 数据报通信:socket 类型是 AF_INET 和 SOCK_DGRAM;
- 实现本地进程间通信: 「本地字节流 socket 」类型是 AF_LOCAL 和 SOCK_STREAM,「本地数据报 socket 」类型是 AF_LOCAL 和 SOCK_DGRAM。另外,AF_UNIX 和 AF_LOCAL 是等价的,所以 AF_UNIX 也属于本地 socket;
7.6、TCP与UDP的区别
TCP与UDP的区别(超详细)_tcp和udp的区别-CSDN博客
总结:
1)TCP是面向连接的,UDP是无连接的
2)TCP是可靠的,UDP是不可靠的
3)TCP是面向字节流的,UDP是面向数据报文的
4)TCP只支持点对点通信,UDP支持一对一,一对多,多对多
5)TCP报文首部20个字节,UDP首部8个字节
6)TCP有拥塞控制机制,UDP没有
7)TCP协议下双方发送接受缓冲区都有,UDP并无实际意义上的发送缓冲区,但是存在接受缓冲区
7.7、请详细介绍一下TCP 的三次握手机制
位码即tcp标志位,有6种标示:SYN(synchronous建立联机) ACK(acknowledgement 确认) PSH(push传送) FIN(finish结束) RST(reset重置) URG(urgent紧急)Sequence number(顺序号码) Acknowledge number(确认号码)
思路:TCP连接的三次握手机制,最重要的知识点,必须得会,通讯过程以及客户端、服务器的对应的状态都需要记住哈。
TCP提供可靠的连接服务,连接是通过三次握手进行初始化的。三次握手的目的就是同步连接双方的序列号和确认号并交换TCP窗口大小信息。我们一起来看下流程图哈:
TCP三次握手
第一次握手(SYN=1, seq=x),发送完毕后,客户端就进入SYN_SEND状态
第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1), 发送完毕后,服务器端就进入SYN_RCV状态。
第三次握手(ACK=1,ACKnum=y+1),发送完毕后,客户端进入ESTABLISHED状态,当服务器端接收到这个包时,也进入ESTABLISHED状态。
从tcpdump的数据,可以明显的看到三次握手的过程是:
第一次握手:client SYN=1, Sequence number=2322326583 —> server
第二次握手:server SYN=1,Sequence number=3573692787; ACK=1, Acknowledgment number=2322326583 + 1 —> client
第三次握手:client ACK=1, Acknowledgment number=3573692787 + 1 -->serverc
7.8、TCP握手为什么是三次,为什么不能是两次?不能是四次?
TCP三次握手的原因
原因如下:建立tcp连接的前两次握手,一个是客户端向服务器发出建立连接的请求,另一次是服务器向客户端确认收到这个请求,这两次只能证明客户端与服务器之间的网络是通畅的,最后一次握手是为了让客户端确认收到服务器发送的数据,避免服务器等待造成资源浪费 。如果过于频繁会导致服务器停止响应。
思路:TCP握手为什么不能是两次,为什么不能是四次呢?为了方便理解,我们以男孩子和女孩子谈恋爱为例子:两个人能走到一起,最重要的事情就是相爱,就是我爱你,并且我知道,你也爱我,接下来我们以此来模拟三次握手的过程:
为什么握手不能是两次呢?
如果只有两次握手,女孩子可能就不知道,她的那句我也爱你,男孩子是否收到,恋爱关系就不能愉快展开。
为什么握手不能是四次呢?
因为握手不能是四次呢?因为三次已经够了,三次已经能让双方都知道:你爱我,我也爱你。而四次就多余了。
7.9、说说TCP四次挥手过程
思路:TCP的四次挥手,也是最重要的知识点,一般跟三次握手会一起考的,必须得记住。
TCP四次挥手过程
第一次挥手(FIN=1,seq=u),发送完毕后,客户端进入FIN_WAIT_1状态。
第二次挥手(ACK=1,ack=u+1,seq =v),发送完毕后,服务器端进入CLOSE_WAIT状态,客户端接收到这个确认包之后,进入FIN_WAIT_2状态。
第三次挥手(FIN=1,ACK1,seq=w,ack=u+1),发送完毕后,服务器端进入LAST_ACK状态,等待来自客户端的最后一个ACK。
第四次挥手(ACK=1,seq=u+1,ack=w+1),客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入TIME_WAIT状态,等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入CLOSED状态。服务器端接收到这个确认包之后,关闭连接,进入CLOSED状态。
四次挥手的具体过程如下:
1)第一次挥手:客户端向服务器发送一个FIN(结束)请求,表示客户端不再发送数据。此时客户端处于FIN_WAIT_1状态。
2)第二次挥手:服务器收到请求后,回复客户端一个ACK响应确认,但这个响应可能还携带有未传输完的数据。此时服务器处于CLOSE_WAIT状态。注意,在第三次挥手之前,数据还是可以从服务器传送到客户端的。
3)第三次挥手:服务器完成数据传输后,向客户端发送一个FIN请求,表示服务器也没有数据要发送了。此时服务器状态变为LAST_ACK状态。
4)第四次挥手:客户端收到服务器的请求后,回复服务器一个ACK响应确认。此时客户端处于TIME_WAIT状态,需要经过一段时间确保服务器收到自己的应答报文后,才会进入CLOSED状态。
到这里,四次挥手就已经结束了。最后,服务器收到ACK报文后,就关闭连接,也处于CLOSED状态了。
7.10、 TCP挥手为什么需要四次呢?
思路:TCP挥手为什么需要四次呢?为了方便大家理解,再举个生活的例子吧。
★小明和小红打电话聊天,通话差不多要结束时,小红说,“我没啥要说的了”。小明回答,“我知道了”。但是小明可能还有要说的话,小红不能要求小明跟着她自己的节奏结束通话,于是小明可能又叽叽歪歪说了一通,最后小明说,“我说完了”,小红回答,“我知道了”,这样通话才算结束。”
7.11、TCP四次挥手过程中,为什么需要等待2MSL,才进入CLOSED关闭状态
思路:这个问得频率特别高。去面试前,一定要把这道题拿下哈。
2MSL,two Maximum Segment Lifetime,即两个最大段生命周期。假设主动发起挥手的是客户端,那么需要2MSL的原因是:
★1.为了保证客户端发送的最后一个ACK报文段能够到达服务端。这个ACK报文段有可能丢失,因而使处在LAST-ACK状态的服务端就收不到对已发送的FIN + ACK报文段的确认。服务端会超时重传这个FIN+ACK 报文段,而客户端就能在 2MSL 时间内(超时 + 1MSL 传输)收到这个重传的 FIN+ACK 报文段。接着客户端重传一次确认,重新启动2MSL计时器。最后,客户端和服务器都正常进入到CLOSED状态。2. 防止已失效的连接请求报文段出现在本连接中。客户端在发送完最后一个ACK报文段后,再经过时间2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样就可以使下一个连接中不会出现这种旧的连接请求报文段。
2MSL是指TCP连接关闭后等待的最长时间;
MSL是指数据报文在网络中存活的最长时间,通常为2分钟。(最长报文段寿命)
(MSL是经验值,不同的系统中可能不同。)
服务器向客户端3挥,客户端收到后开始向服务器4挥。
但可能因为各种原因导致服务器并没有收到这个4挥。
如何判断服务器收没收到4挥:1MSL为3挥完成的最长时间,1MSL为4挥完成的最长时间.
即:2MSL内若4挥成功了,则服务器一定能收到4挥完成的结果。
2MSL过去了服务器仍没收到ACK报文段,则代表4挥失败。===》服务器再发一次3挥并重新计时(计2MSL)。
此时,证明了2MSL是为了保证客户端发的最后一个ACK能给到服务器!(第一个原因)
1MSL为报文段再网络中可存活的最长时间。
2MSL过去了,则上一次(无效的)丢失的3挥4挥的过程报文一定已经嘎了。即:
表明了2MSL可保证已失效的连接请求报文段一定不会出现在本连接中。(第二个原因)
over
7.12 获取硬件支持并发数 std::thread::hardware_concurrency
用于获取当前机器的CPU逻辑核心数,并可根据此值来设置线程池的工作线程数量。
正常返回支持的并发线程数,若值非良定义或不可计算,则返回 0。
//C++11多线程: hardware_concurrency()函数
#include <stdio.h>
#include <thread>
unsigned int hardware_threads = std::thread::hardware_concurrency(); //unsigned 表示的就是 unsigned int
std::cout << hardware_threads << std::endl;
printf("thread_count=%d\n",hardware_threads);
![](https://i-blog.csdnimg.cn/blog_migrate/8a166b8a6a5a6e521ae6197d1bb6a532.png)
单核cpu
:不能实现真正意义上的线程并行,虽然可以通过中断机制实现在单核cpu上,通过分配cpu时间片的方式使多个线程进行交替执行,但是同一时刻还是只有一个线程在执行,不能达到真正意义上的线程并行。
多核cpu
:能够实现真正意义上的线程并行。线程并行
:指的是同一时刻,有多个线程同时在不同的cpu核心上进行指令的执行。
线程的最大并行数:就是cpu的逻辑核心数。
8、C++11新特性
C++11新特性梳理_c++11引入了以下哪些新特性? 移动语义、垃圾回收机制、lambda表达式、auto关键字-CSDN博客
自动类型推导关键字 auto
关于效率: auto实际上实在编译时对变量进行了类型推导,所以不会对程序的运行效率造成不良影响。另外,auto并不会影响编译速度,因为编译时本来也要右侧推导然后判断与左侧是否匹配。
9、关键字
C++中的一些重要关键字:
序号 关键字 描述 1 auto 用于自动推断变量类型 2 const 用于定义常量,表示变量的值不可更改 3 static 用于静态变量和静态函数的声明 4 extern 用于声明在其他文件中定义的全局变量或函数 5 inline 用于行内函数的定义 6 friend 用于声明友元函数或友元类 7 virtual 用于定义虚函数,实现多态性 8 new/delete 用于动态分配和释放内存 9 try/catch/throw 用于异常处理 10 typedef 用于给已有类型定义一个别名 11 template 用于泛型编程和模板类的定义 12 namespace 用于命名空间的定义,避免命名冲突 13 explicit 用于构造函数声明,禁止隐式转换 14 mutable 用于类中成员变量的定义,表示该变量可以修改 15 operator 用于操作符重载 16 sizeof 用于返回数据类型或变量所占的字节数 17 typeid 用于获取对象的类型信息 18 volatile 用于声明易变变量,防止编译器进行优化 19 register (已被弃用) 用于将变量存储在CPU寄存器中,提高访问速度
9.1 inline 内联函数
内联函数的定义与普通函数基本相同,只是在函数定义前加上关键字 inline。
inline void print(char *s) { printf("%s", s); }
但加上inline不一定就会有效果。因为使用内联inline关键字修饰函数只是一种提示,编译器不一定认。一个好的编译器将会根据函数的定义体,自动地取消不值得的内联。
可以理解为内联函数的关键词是:替换
一般情况,在函数频繁调用且函数内部代码很少的情况下使用内联。(普通函数频繁调用的过程消耗栈空间)
inline是一个弱符号;
隐式内联与显式内联
C++中在类内定义的所有函数都自动称为内联函数(隐式内联)
1.隐式内联:如第三节说的C++中在类内定义的所有函数都自动称为内联函数,类的成员函数的定义直接写在类的声明中时,不需要inline关键字。
#include <stdio.h> class Trace{ public: Trace() { noisy = 0; } void print(char *s) { if (noisy) { printf("%s", s); } } void on(){ noisy = 1; } void off(){ noisy = 0; } private: int noisy; };
2.显示内联:需要使用inline关键字。
#include <stdio.h> class Trace{ public: Trace() { noisy = 0; } void print(char *s); //类内没有显示声明 void on(){ noisy = 1; } void off(){ noisy = 0; } private: int noisy; }; //类外显示定义 inline void Trace::print(char *s) { if (noisy) { printf("%s", s); } }
可用与不可用
可用:
1)对程序执行性能有要求时,可适当使用;
2)想宏定义一个函数时,可用内联函数;
3)功能专一且性能关键的函数,函数体不大,包含了很少的执行语句,可用。(通过inline声明,编译器不需要跳转到内存其他地址去执行函数调用,也不需要保留函数调用时的现场数据。)
4)类内部定义的函数会默认声明为inline函数,这有利于 类实现细节的隐藏。(但也需要斟酌如果不需要隐藏的时候,其实大部分是不推荐默认inline的)
不可用:
1)函数体内的代码比较长,不可用。(会导致内存消耗代价较高)
2)函数体内出现循环或者开关语句,不可用。(执行函数体内代码的时间要比函数调用的开销大)
static inline
static inline 函数,跟 static 函数单独没有差别,所以没有意义,只会混淆视听。
9.2 static 静态成员
QT学习笔记一的25也有。
省时间读这里:
开发中static是为了限制其作用域只在定义该变量或函数的源文件内有效,而在同一工程下的其他源文件中不能使用。
(static 意味着本地化)
在头文件中定义static变量会造成变量多次定义,浪费内存空间,而且也不是真正的全局变量。应该避免使用这种定义方式!!!
(避免在头文件中声明并定义static变量或函数)
静态成员
成员变量和成员函数前加上关键字static,即为静态成员(静态成员只能在当前源文件使用)。
静态成员变量
静态成员变量特点:
1)在编译阶段分配内存
2)类内声明,类外初始化
3)所有对象共享同一份数据
#include <iostream>]
using namespace std;
class Person
{
public:
static int m_A; //静态成员变量
//静态成员变量特点:
//1 在编译阶段分配内存
//2 类内声明,类外初始化
//3 所有对象共享同一份数据
private:
static int m_B; //静态成员变量也是有访问权限的
};
//在类外初始化
int Person::m_A = 10;
int Person::m_B = 10;
void test01()
{
//静态成员变量两种访问方式
//1、通过对象
Person p1;
p1.m_A = 100;
cout << "p1.m_A = " << p1.m_A << endl;
Person p2;
p2.m_A = 200;
cout << "p1.m_A = " << p1.m_A << endl; //共享同一份数据
cout << "p2.m_A = " << p2.m_A << endl;
//2、通过类名
cout << "m_A = " << Person::m_A << endl;
//cout << "m_B = " << Person::m_B << endl; //私有权限访问不到
}
int main() {
test01();
system("pause");
return 0;
}
上面代码输出:
p1.m_A = 100
p1.m_A = 200
p2.m_A = 200
m_A = 200
静态成员函数
静态成员函数特点:
1)程序共享一个函数(所有对象共享同一个函数)
2)静态成员函数只能访问静态成员变量
#include <iostream>
using namespace std;
class Person
{
public:
//静态成员函数特点:
//1 程序共享一个函数
//2 静态成员函数只能访问静态成员变量
static void func()
{
cout << "func调用" << endl;
m_A = 100;
//m_B = 100; //错误,不可以访问非静态成员变量
}
static int m_A; //静态成员变量
int m_B; //
private:
//静态成员函数也是有访问权限的
static void func2()
{
cout << "func2调用" << endl;
}
};
int Person::m_A = 10;
void test01()
{
//静态成员变量两种访问方式
//1、通过对象
Person p1;
p1.func();
//2、通过类名
Person::func();
//Person::func2(); //私有权限访问不到
}
int main() {
test01();
system("pause");
return 0;
}
上面代码输出:
func调用
func调用
静态成员变量和函数的具体分类
(在类外定义)静态全局变量
使用:全局变量前加static,修饰全局变量为静态全局变量。
作用:改变全局变量的可见性。对于一个全局变量,它既可在本源文件中被访问,也可在同个工程的其他源文件中被访问,使用extern即可。静态全局变量的存储位置在静态存储区,未被初始化的静态全局变量会被自动初始化为0。静态全局变量在声明它的文件之外是不可见的,仅在从定义该变量的开始位置到文件结尾可见。
(可见性:从声明该变量的开始位置到文件结尾(整个程序中) ==》从定义该变量的开始位置到文件结尾)
(在类外定义)静态局部变量
使用:局部变量前加static,修饰局部变量为静态局部变量。
作用:改变局部变量的销毁时期。静态局部变量的作用域和局部变量的作用域一样,当定义它的函数或语句块结束的时候,作用域结束。不同的是,一般局部变量是存储在栈区,静态局部变量存储在静态存储区,当静态局部变量离开作用域后,并没有被销毁。当该函数再次被调用的时候,该变量的值为上次函数调用结束时的值。
(销毁时期:所在的语句块执行结束时(离开作用域时) ==》整个进程结束时)
ps:static修饰局部变量后,该变量只在初次运行时进行初始化,且只进行一次。
#include <stdio.h> void fun() { static int a = 1; a++; printf("a = %d\n", a); } int main() { fun(); fun(); fun(); return 0; }
(在类外定义)静态函数
使用:函数返回类型前加static,修饰函数为静态函数。
作用:改变函数的可见性。函数的定义和声明在默认情况下都是extern的,但静态函数只在声明它的文件中可见,不能被其他文件使用。
(可见性:整个源程序 ==》声明其的本源文件)
(在类内定义)类的静态成员变量
使用:类成员前加static,修饰类的成员为类的静态成员。
作用:实现多个对象之间的数据共享,并且使用静态成员不会破坏封装性,也保证了安全性。
(类中的某个变量用static修饰,表示该变量为类以及其所有对象所有且共用。它们在存储空间中只有一个副本,可通过类或对象去调用)
class Person { public: static int m_A; //静态成员变量 }
(在类内定义)类的静态成员函数
使用:类函数前加static,修饰类的函数为静态函数。
作用:减少资源消耗,不需要实例化就可以使用。
(类中的某个函数用static修饰,表示该函数属于一个类,而不属于任何特定对象)
ps:静态成员函数,只能访问静态成员函数和静态成员变量!
class Person { public: static void func() { cout << "func调用" << endl; m_A = 100; //m_B = 100; //错误,不可以访问非静态成员变量 } }
static 的作用
1. 维持数据的持续性
1)静态变量和静态函数的生命周期是整个进程,存储在全局静态存储区(全局变量和静态变量的存储都放在这里)。
2)若希望函数中局部变量的值在函数调用结束之后不会消失,仍然保留函数调用结束的值,则可将该局部变量用关键字static声明为静态局部变量。
3)局部变量被声明为静态局部变量时,其存储位置从原来的栈中存放改为静态存储区存放(全局变量也存放在静态存储区)。
4)静态局部变量与全局变量的主要区别就在于可见性,静态局部变量只在其被声明的代码块中是可见的。
===》维持数据的持续性
2. 保护数据,做到数据隔离
全局变量的作用域是整个源程序,若希望全局变量仅限于在本源文件中使用,在其他源文件中不能引用(限制其作用域只在定义该变量的源文件内有效,而在同一源程序的其他源文件中不能使用),则可通过在全局变量上加static来实现,使全局变量被定义成一个静态全局变量。这样就可以避免其他源文件使用该变量、避免其他源文件因为该变量引起的错误。 ===》保护数据,做到数据隔离
static const
对于C/C++语言来讲,
const 就是只读的意思,只在声明中使用;
static 一般有2个作用,规定作用域和存储方式。对于局部变量,static规定其为静态存储方式,每次调用的初始值为上一次调用的值,调用结束后存储空间不释放;
对于全局变量,如果以文件划分作用域的话,此变量只在当前文件可见;对于static函数也是在当前模块内函数可见。
static const 应该就是上面两者的合集。
下面分别说明:
全局:
const,只读的全局变量,其值不可修改。
static,规定此全局变量只在当前模块(文件)中可见。
static const,既是只读的,又是只在当前模块中可见的。
文件:
文件指针可当作一个变量来看,与上面所说类似.
函数:
const,返回只读变量的函数。
static,规定此函数只在当前模块可见。
类:
const,一般不修饰类,(在VC6.0中试了一下,修饰类没啥作用)static,C++中似乎没有静态类这个说法,一般还是拿类当特殊的变量来看。C#中有静态类的详细说明,且用法与普通类大不相同。
9.3 __declspec(selectany)
参考链接:浅谈__declspec(selectany)该何时用
当在头文件定义全局变量,并且这个头文件被include多次时可以用这个开关剔除由于多次include而产生的重定义。
模板类的静态成员必须在类外部初始化,如果是全写在头文件,当头文件include多于一次的时候就会出现类的静态变量重定义的问题,可以做一个简单的实验:sy.h:sy.h:
class A { public: static int u; }; int A::u=1;
1.cpp:
#include"sy.h" int main() { return 0; }
2.cpp:
#include"sy.h"
就这样三个文件,在一个工程中,组建一下(编译没问题,问题出在链接的时候),就会出现:
2.obj : error LNK2005: "public: static int A::u" (?u@A@@2HA) already defined in sy.obj
Debug/sy.exe : fatal error LNK1169: one or more multiply defined symbols found这时候就只能使用__declspec(selectany)去解决了,将sy.h的第六行改为:
__declspec(selectany) int A::u=1;
即可解决问题。
使用示例:(使用的同时,必须有初始化)
class GlobalData //公共变量 { public: static HWND MaimWinID; static QString ExePath; static bool Test; static bool Test_HML;//用于建模时的测试 static bool Task_Stop;//整个程序任务是否停止状态。 true:触发了任务停止 }; __declspec(selectany) HWND GlobalData::MaimWinID= 0; __declspec(selectany) QString GlobalData::ExePath = ""; __declspec(selectany) bool GlobalData::Test = false; __declspec(selectany) bool GlobalData::Test_HML = false; __declspec(selectany) bool GlobalData::Task_Stop = false;
9.4 extern 全局变量和函数
全局变量和函数
1)一般变量函数默认都为全局的。
2)全局变量及函数:在当前源文件以及其他源文件都可以使用。若其他源文件使用,需要extern对全局变量及函数进行声明。3)使用extern声明变量时不能带有初始值,否则仍然属于变量定义,会出现变量重定义的错误。
全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,全局变量在所有的源文件中都是有效的。
若希望全局变量仅限于在本源文件中使用,在其他源文件中不能引用(限制其作用域只在定义该变量的源文件内有效,而在同一源程序的其他源文件中不能使用),则可通过在全局变量上加static来实现,使全局变量被定义成一个静态全局变量。这样就可以避免其他源文件使用该变量、避免其他源文件因为该变量引起的错误。 ===》保护数据,做到数据隔离。
使用方式
1)在 Head.hpp 中 extern 该 全局变量;
2)在 Head.cpp 中定义 全局变量;
3)所有想使用该全局变量的模块直接 #include “Head.hpp” 即可。
Head.h
#ifndef HEAD_H #define HEAD_H extern int Global; //int Func(); 其实就是等价于extern int Func(); //在头文件中修饰函数的时候,extern关键字可省略 int Func(); #endif
Head.cpp
#include <stdio.h> #include "Head.hpp" //在这里定义Global变量 int Global; int Func() { Global = 10; printf("Head Func, %d, %x\n", Global, &Global); }
Main.cpp
#include <stdio.h> //直接#include "Head.hpp"即可引用到extern全局变量 #include "Head.hpp" int main() { Global = 100; Func(); printf("Main main, %d, %x\n", Global, &Global); }
这时候Global确实就是多个.cpp文件都可以访问到的全局变量了。
9.4.1 extern "C"
声明一个函数或变量是以C语言的方式进行编译和链接的。
通常用于在C++程序中调用C语言库的函数或变量。
(为了在链接时就可以找到与之匹配的C语言库中的函数)原因如下:
- C函数
void myFunc(){}
被编译成函数myFunc
;- C++函数
void myFunc(){}
被编译成函数_Z6myFuncv
;- 这是由于C++需要支持函数重载,所以c和C++中对同一个函数经过编译后生成的函数名是不相同的;
- 所以:如果在C++中调用一个使用c语言编写模块中的某个函数,那么C++是根据C++的名称修饰方式来查找并链接这个函数,这就会发生链接错误。
extern "C"
的主要作用就是为了实现C++代码能够调用其他C语言代码,加上extern "C"
后,这部分代码编译器按照c语言的方式进行编译和链接,而不是按照C++的方式。
使用extern "C"
的格式:
#if __cplusplus
extern "C"{
#endif
//函数声明
//...
#if __cplusplus
}
#endif
使用示例:
func.h
#ifndef _FUNC_H_ #define _FUNC_H_ #if __cplusplus extern "C" { #endif int func(int a, int b); #if __cplusplus } #endif #endif // !_FUNC_H_
func.c
#include <stdio.h> #include "func.h" int func(int a,int b) { printf("c func\n"); return a + b; }
mian.cpp
#include "func.h" int main() { func(100, 200); return 0; }
9.5 explicit 显式
用于修饰构造函数,防止隐式转换。针对单参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造)而言。
(声明为explicit的构造函数不能在隐式转换中使用。)
(表明该构造函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式)。)
注意事项:
1. explicit关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了
2.就是当除了第一个参数以外的其他参数都有默认值的时候, explicit关键字依然有效, 此时, 当调用构造函数时只传入一个参数, 等效于只有一个参数的类构造函数
3.当类的声明和定义分别在两个文件中时,explicit只能写在在声明中,不能写在定义中
4.将拷贝构造函数声明成explicit并不是良好的设计,一般只将有单个参数的构造函数声明为explicit,而拷贝构造函数不要声明为explicit。
9.6 override 虚函数重写
参考链接:C++干货系列——override和final详解
(加了这个编译器会检查重写的函数是否正确,报错就是没写对)
作用:在成员函数声明或定义中, override 确保该函数为虚函数并覆写来自基类的虚函数。
位置:函数调用运算符之后,函数体或纯虚函数标识 “= 0” 之前。
使用以后有以下好处:
1.可以当注释用,方便阅读
2.告诉阅读你代码的人,这是方法的复写
3.编译器可以给你验证 override 对应的方法名是否是你父类中所有的,如果没有则报错.
override是让编译器检查你是否重写虚函数啦,没有的话 编译阶段提示一下。
以下为QT学习笔记一的22的内容。
来自《C++ Primer 中文版》总结
override 使用条件:基类成员函数为虚函数(virtual),派生类中重写该成员函数。
使用 override 的好处:程序员的意图更清晰,让编译器发现一些错误。
final使用:出现在形参列表(包括const或引用修饰符)以及尾置返回类型之后
不允许后续的其它类覆盖该函数final(当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器就会报错。)
参考链接:C++ 中提供的override 关键字
作用与 Q_DECL_OVERRIDE 相同。
区别在于:低版本的qt不支持直接用override,也没有qoverride类
override 关键字需要编译器支持 C++11。
如果使用的是 gcc 编译器,需要加入命令行参数 -std=c++11
如果是用 qt 的话,要在pro文件中增加如下内容:QMAKE_CXXFLAGS += "-std=c++11"
关于c++中继承多态virtual和override的几点总结。
、子类可以直接使用基类中的protected下的变量和函数。
、基类函数没加virtual,子类有相同函数,实现的是覆盖。用基类指针调用时,调用到的是基类的函数;用子类指针调用时,调用到的是子类的函数。
、基类函数加了virtual时,实现的时重写。用基类指针或子类指针调用时,调用到的都是子类的函数。
、函数加上override,强制要求子类相同函数需要是虚函数,而且必须重新实现,否则会编译报错。
、子类的virtual可加可不加,建议加override不加virtual。
、基类中的纯虚函数(virtual void play() = 0;)在基类中无需在cpp中实现,但是必须在子类实现,否则编译报错。
、继承多态最大的好处就是提炼共性,将通用的变量和方法信号等,全部放在基类,子类负责实现自己需要的特殊的部分即可。
9.7 dynamic_cast 多态类型之间的转换
参考链接:【C++】dynamic_cast基本用法(详细讲解)
dynamic_cast
是 C++ 中的一个类型转换操作符,它主要用于多态类型之间的转换。其特点是在运行时进行类型检查,确保所执行的转换是安全的。因此,它主要用于指向类的指针或引用之间的转换,尤其是在类的继承体系中。
1. 使用场景
1.1向下转型
1.2横向转型
2. 前提条件
为了使 dynamic_cast 能够进行运行时类型检查,以下条件必须满足:转换涉及的类型至少有一个虚函数。换句话说,基类必须有虚函数,以支持运行时类型信息 (RTTI)。
class Base { public: virtual void foo() {} };
编译器需要启用 RTTI。大多数现代 C++ 编译器默认启用 RTTI,但有些情况下可能需要显式地开启它。
3. 优点
安全性:dynamic_cast 提供运行时的类型检查,这使得转换更加安全。如果转换无法进行,对于指针转换,它返回 nullptr;对于引用转换,它抛出一个 std::bad_cast 异常。
4. 缺点
性能开销:由于 dynamic_cast 需要在运行时进行类型检查,所以它相对于其他转换(如 static_cast)来说,有一定的性能开销。
总之,dynamic_cast 在处理与多态相关的类型转换时是非常有用的,尤其是当你不确定实际类型时。但由于其性能开销,你应该在必要时才使用它,并确保 RTTI 在你的编译器中是启用的。
10、string 转换为 整型(stoi、stol、stoll 函数用法)
10.1 stoi() 函数(转int)
string str = "-1235";
int a = stoi(str);
cout << "a = " << a << endl; //a = -1235
str = "0x123";
a = stoi(str, NULL, 16); //base = 16,指定十六进制
cout << "a = " << a << endl; //a = 291
str = "0x123";
a = stoi(str, NULL, 8); //base = 8,指定八进制
cout << "a = " << a << endl; //a = 0
10.1.1 atoi()
string str=“123”;
int a=atoi(str.c_str());
int b=stoi(str);
转int可以用atoi或者stoi,atoi的效率高一些。
10.2 stol() 函数(转long)
string str = "0123";
long a = stol(str);
cout << "a = " << a << endl; //a = 123
10.3 stoll() 函数(转long long)
string num="719387492312";
long long res=stoll(num);
11、位(比特)、字节、字
1位=1比特
1字节=8位
以及一般情况下:
1字=2字节,也就是 1字=16位 。(受编码格式,英文或中文影响)
11.1 位(bit)
来自英文bit,音译为“比特”,表示二进制位。
位是计算机内部数据储存的最小单位,11010100是一个8位二进制数。
- 一个二进制位只可以表示0和1两种状态(21);
- 两个二进制位可以表示00、01、10、11四种(22)状态;
- 三位二进制数可表示八种状态(23)……。
即每个0或1就是一个位(bit)。
11.2 字节(byte)
字节来自英文Byte,音译为“拜特”,习惯上用大写的“B”表示。
字节是计算机中数据处理的基本单位。计算机中以字节为单位存储和解释信息,规定一个字节由八个二进制位构成,即1个字节等于8个比特(1Byte=8bit)。
八位二进制数最小为00000000,最大为11111111;
通常1个字节可以存入一个ASCII码,2个字节可以存放一个汉字国标码。
11.3 字(word)
计算机进行数据处理时,一次存取、加工和传送的数据长度称为字(word)。
一个字通常由一个或多个(一般是字节的整数位)字节构成。例如
- 286微机的字由2个字节组成,它的字长为16;
- 486微机的字由4个字节组成,它的字长为32位机。
每个字中二进制位数的长度(每个字所包含的位数),称为字长。
不同的计算机系统的字长是不同的,常见的有8位、16位、32位、64位等/
计算机的字长决定了其CPU一次操作处理实际位数的多少,由此可见计算机的字长越大,其性能越优越(一次处理的信息位就越多)。
注意字与字长的区别,字是单位,而字长是指标,指标需要用单位去衡量。正象生活中重量与公斤的关系,公斤是单位,重量是指标,重量需要用公斤加以衡量。
12、路径中的 '.' 和'..'还有'./'和'../'
表示当前目录
.. 表示当前目录的上一级目录。
./表示当前目录下的某个文件或文件夹,视后面跟着的名字而定
../表示当前目录上一级目录的文件或文件夹,视后面跟着的名字而定。
//当前文件是main.cpp,当前路径为main.cpp所在的路径,
// "../../img" 为当前路径的上一级再上一级的路径中的img文件夹
std::string filePath = "../../img/圆弧中点";
13、大小端存储(当前计算机存储模式)
(利用当前一个高类型的变量给其赋值,然后取到其低地址,查看其存储的数据。)
int a = 1; ===》即,a = 0000 0000 0000 0001 。(4字节,每个字节一bit)
num = (*(char*)&a); //&a 取出a的地址; (char*)&a 代表a变量地址的第一个字节的地址
&a :a的地址
(char*) :char类型的指针(指向a的地址)
(char*)&a : 指向a的地址的 char类型的指针,(取到了a变量所在地址,指向的是其地址开头)
*(char*)&a :取到a变量地址的第一个字节的值,即如下
(取的是第一位,即最左边那个位。4字节,每字节有4bit,取最左的字节。)
大端 ===》 a = 1 ===》a = 0000 0000 0000 0001 中的 0000。
小端 ===》 a = 1 ===》a = 0001 0000 0000 0000 中的 0001。
因此:
返回值为0时,代表取存储模式为大端;(高位在前(左))人类阅读习惯
返回值为1时,代表取存储模式为小端。(低位在前(左))
14、排序算法
冒泡,直接插入,选择
冒泡排序:
直接插入排序:
选择排序:
std::vector<int> a = { 6,78,3,24,32,7,9,97,4,61,1,11,-3,0,-54,25 };
for (int i : a) std::cout << i << ", ";
std::cout << endl << "升序排序后(冒泡):************** ";
for (int i = 0; i < a.size(); i++) {
for (int j = i + 1; j < a.size(); j++) {
if (a[i] > a[j]) {
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
}
}
for (int i : a) std::cout << i << ", ";
std::cout << endl << endl;
a = { 6,78,3,24,32,7,9,97,4,61,1,11,-3,0,-54,25 };
for (int i : a) std::cout << i << ", ";
std::cout << endl << "升序排序后(直接插入(法一)):************** ";
for (int i = 1; i < a.size(); i++) {//直接插入(法一)
if (a[i] > a[i - 1]) continue;
int temp = a[i]; //待插入元素
for (int j = i - 1; j >= 0; j--) {
a[j + 1] = a[j]; //往后挪,为插入者让位
if (a[j] <= temp) { //当发现已排序成员<待插入元素,则认为找到位
a[j + 1] = temp;
break;
}
//因为第0位元素前面为空,会漏掉,所以补上
if (j == 0 && a[j] > temp) a[j] = temp;
}
}
for (int i : a) std::cout << i << ", ";
std::cout << endl << endl;
a = { 6,78,3,24,32,7,9,97,4,61,1,11,-3,0,-54,25 };
for (int i : a) std::cout << i << ", ";
std::cout << endl << "升序排序后(直接插入(法二)):************** ";
for (int i = 1; i < a.size(); i++) {//直接插入(法二)
if (a[i] > a[i - 1]) continue;
int temp = a[i]; //待插入元素
int j;
for (j = i - 1; j >= 0; j--) {
a[j + 1] = a[j]; //往后挪,为插入者让位
if (a[j] <= temp) break; //当发现已排序成员<待插入元素,则认为找到位
}
//当发现已排序成员<待插入元素,则改已排序成员后一位就是待插入位
//当已排序成员全都遍历完,此时j指向首位成员的前一位,则待插入位就是首位
a[j + 1] = temp;
}
for (int i : a) std::cout << i << ", ";
std::cout << endl << endl;
a = { 6,78,3,24,32,7,9,97,4,61,1,11,-3,0,-54,25 };
for (int i : a) std::cout << i << ", ";
std::cout << endl << "升序排序后(选择):************** ";
for (int i = 0; i < a.size(); i++) {
int minIdx = i;
for (int j = i + 1; j < a.size(); j++) {
if (a[minIdx] > a[j]) {
minIdx = j;//找到要选择的元素的下标
}
}
//若当前元素即为要选择元素,则无需交换元素值
if (minIdx != i) {
int temp = a[minIdx];
a[minIdx] = a[i];
a[i] = temp;
}
}
for (int i : a) std::cout << i << ", ";
15、模板
15.1 函数模板
C++ 函数模板全解析:如何写出高效、灵活的通用函数? (qq.com)
#include <iostream>
using namespace std;
template <typename T>
void swapAll(T &a, T &b)
{
T tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 100, b = 200;
// 函数调用时,根据实参的类型,自动推导T的类型
swapAll(a, b);
cout << a << " " << b << endl;
char c = 'c';
char d = 'd';
swapAll(c, d);
cout << c << " " << d << endl;
return 0;
}
输出:
200 100
d c
函数模板会编译两次:
-
第一次,对函数模板本身编译。
-
第二次,函数调用处,将
T
的类型具体化。
函数模板的注意点
(1)当函数模板和普通函数都识别时,会优先使用普通函数。
(2)函数模板和普通函数都识别时,强制使用函数模板需要使用
<>
指定。(3)函数模板自动类型推导时,不能对函数的参数进行自动类型转换。
(4)函数模板也是可以重载的,例如:
template <typename T> void MyPrint(T a) { cout<<a<<endl; } template <typename T> void MyPrint(T a,T b) { cout<<a<<endl; cout<<b<<endl; }
函数模板的局限性
当函数模板推导出T为数组或其他自定义数据类型,可能导致运算符不识别。
解决方案一:运算符重载,推荐这种解决方案。
解决方案二:具体化函数模板。
15.2 类模板
// 类模板
template <class T1,class T2>
class Data {
private:
T1 a;
T2 b;
public:
Data();
Data(T1 a, T2 b);
void showData();
};
template <class T1, class T2>
Data<T1,T2>::Data()
{
cout << "Data的无参构造" << endl;
}
template <class T1, class T2>
Data<T1, T2>::Data(T1 a, T2 b)
{
this->a = a;
this->b = b;
cout << "Data的有参构造" << endl;
}
template<class T1,class T2>
void Data<T1,T2>::showData()
{
cout << a << " " << b << endl;
}
int main()
{
// 类模板实例化对象
// Data ob(100,200);// error,必须指定类型
// 类模板实例化对象,必须指明T的类型
Data<int, int> ob(300, 400);
ob.showData();
Data<int, char> ob2(100, 'A');
ob2.showData();
return 0;
}
16、static_cast ,reinterpret_cast的用法和区别
C++中的static_cast ,reinterpret_cast的用法和区别-CSDN博客
C++中static_cast和reinterpret_cast的区别
C++ primer第五章里写了:
编译器隐式执行任何类型转换都可由static_cast显示完成;
reinterpret_cast通常为操作数的位模式提供较低层的重新解释。
1、C++中的static_cast执行非多态的转换,用于代替C中通常的转换操作。因此,被做为显式类型转换使用。比如:
int i; float f = 166.71; i = static_cast<int>(f);
此时结果,i的值为166。
2、C++中的reinterpret_cast主要是将数据从一种类型的转换为另一种类型。所谓“通常为操作数的位模式提供较低层的重新解释”也就是说将数据以二进制存在形式的重新解释。比如:
int i; char *p = "This is an example."; i = reinterpret_cast<int>(p)
此时结果,i与p的值是完全相同的。reinterpret_cast的作用是说将指针p的值以二进制(位模式)的方式被解释为整型,并赋给i,//i 也是指针,整型指针;一个明显的现象是在转换前后没有数位损失。
17、对txt文件的读写操作
参考链接:C++对txt文件的写入读取操作_c++读取txt-CSDN博客
C++ 中用于实现数据输入和输出的这些流类以及它们之间的关系:
- istream:常用于接收从键盘输入的数据;
- ostream:常用于将数据输出到屏幕上;
- ifstream:用于读取文件中的数据;是输入文件流(就是通过它定义的对象获取文件中的内容)。
- ofstream:用于向文件中写入数据;是输出文件流(将内容写入文件)。
- iostream:继承自 istream 和 ostream 类,因为该类的功能兼两者于一身,既能用于输入,也能用于输出;
- fstream:兼 ifstream 和 ofstream 类功能于一身,既能读取文件中的数据,又能向文件中写入数据
注意:要使用输入输出文件流要包含头文件
#include<fstream>
if (1) {
cv::Mat imgGetRoi;
src.copyTo(imgGetRoi);
cv::namedWindow("imgGetRoi", cv::NORMCONV_FILTER);
cvSetMouseCallback("imgGetRoi", on_mouse_getROI, (void*)(&imgGetRoi));
cv::imshow("imgGetRoi", imgGetRoi);
//ofstream ofs("../../img/1roiParam.txt", ios::out);//打开时会清除原来的内容
//ofstream ofs("../../img/1roiParam.txt", ios::app);//打开时会在原有的尾部添加数据
string roiTXT_name = filePath + name;
roiTXT_name = roiTXT_name.substr(0, roiTXT_name.find("."));//除去文件后缀名之外的路径+文件名
cout << "roiTXT=" << roiTXT_name << endl;//与原图放于同一路径
ofstream ofs(roiTXT_name + ".txt", ios::app);//打开时会在原有的尾部添加数据
if (ofs)
{
//ofs.clear();
ofs << "--++-日志启动" << endl;
}
//cv::waitKey(0);
}