public、protect和private三者的区别
public是类内类外都可以访问
protect是类内可以访问,类外只有子类、父类可以访问
private只有类内可以访问
namespace的一些作用
除了简化代码上的代码数量,避免加入一些std::这样的作用域之外
还可以避免两个项目组之间的项目出现函数或者类的重叠的问题,每个项目都用自己的namespace进行包装,可以有效避免这个问题
#include <iostream>
namespace zzz;
int main(){
zzz::do();
return 0;
}
异常捕获
C++ 通过 throw 语句和 try…catch 语句实现对异常的处理。throw 语句的语法如下:
throw 表达式;
该语句拋出一个异常。异常是一个表达式,其值的类型可以是基本类型,也可以是类。
try...catch 语句的语法如下:
try {
语句组
如果这里没有抛出异常,throw,就不执行所有catch
如果抛出了异常,那么就立刻跳转到对应的异常类型上,比如抛出int错误,就到int上
}
catch(异常类型) {
异常处理代码
}
...
catch(异常类型) {
异常处理代码
}
catch 可以有多个,但至少要有一个
遇到一些写结构体的场景题目
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
构造函数和析构函数是否可以为虚函数
虚函数表是在构造函数中才初始化的,而不是在其之前。因此构造函数不能为虚函数。不然的话无法找到对应的虚函数表
析构函数必须要是虚函数,可能会在析构的时候出现内存泄漏的错误,如果没有加上virtual的话就只会析构基类,而不会析构子类,会泄漏
在继承的时候,构造函数:1)基类的构造函数,2)再调用对象成员构造函数,3)最后调用派生类的构造函数
纯虚函数和虚函数在用法上的一个区别
今天碰到了一个特例,对于两个类有一个共同的父类
父类:base(成员变量int a,成员函数ADD)
子类:A(成员变量int a)
子类:B(成员函数ADD)
这时候,如果将base中的函数定义为一个纯虚函数,让A类和B类继承的话,对于A类来说就会发生错误,因为纯虚函数是一个函数的声明,但是用某个类的时候一定要定义,但是A类无法做ADD函数的动作,所以不能实现,这里只能使用虚函数定义ADD
C++读取文件中的数据
#include <string.h>
#include <iostream>
#include <fstream>
#include <vector>
using namespace std;
int main()
{
char data[100];
// ofstream outfile;
// outfile.open("测试文本文件.txt", ios::out | ios::trunc);
// // 将用户的输入写入文件中,并对文件的内容进行覆盖
// while (cin >> data && cin.get() != '\n')
// {
// }
// outfile << data << endl;
ifstream infile;
infile.open("测试文本文件.txt");
// 读取文件中的数据
cout << "Reading from the file" << endl;
infile >> data;
cout << data << endl;
return (0);
}
指针数组和数组指针
没有括号的是每个数组中存放了int *的类型的数据
含有括号的是存放了int类型的数据
int *p1[5];
int (*p2)[5];
strcpy的底层实现
主要是通过指针将指针指向的地址中的内容逐个进行赋值到想要的空间中
char* strcpy( char* dst,const char *src ) //[1]
{
assert(dst != NULL && src != NULL); //[2]
char *ret = dst; //[3]
while ((*dst++=*src++)!='\0'); //[4]
return ret;
}
strcpy函数存在的缺陷:strcpy 函数不检查目的缓冲区的大小边界,而是将源字符串逐一的全部赋值给目的字符串地址起始的一块连续的内存空间,同时加上字符串终止符,会导致其他变量被覆盖。
memcpy是增加需要复制的字节数的,所以对于边界来说是安全的
这里特指char类型的数组 要+1
#include <stdio.h>
#include <string.h>
int main ()
{
const char src[50] = "http://www.runoob.com\0";
char dest[50];
//这里加1的原因是strlen是不包含结束字符\0的,所以加上1开辟的空间多一个位置给结束字符
memcpy(dest, src, strlen(src)+1);
printf("dest = %s\n", dest);
return(0);
}
如果这里不加上1,会得到的结果是sxf��3
我们会发现后面添加上了特殊字符,因为没有结束标志位,编译器不知道字符串在什么时候结束
C++和C语言的混编
#ifdef __cplusplus
extern "C"
{
#endif
/* 函数声明代码块 */
#ifdef __cplusplus
}
#endif
typedef关键字
用于给类型起别名或者自定义类型,用于分装函数返回值较多
typedef unsigned int COUNT;
//封装之后让代码更加简洁
//也可以为项目进行封装隔离
typedef int INT_ARRAY_100[100];
INT_ARRAY_100 arr;
const和constexpr之间的区别
const一般是在运行阶段初始化,constexpr一般是在编译的适合进行初始化
const 用于为修饰的变量添加“只读”属性,也就是不可改变值;而 constexpr 关键字则用于指明其后是一个常量(或者常量表达式),编译器在编译程序时可以顺带将其结果计算出来,而无需等到程序运行阶段,这样的优化极大地提高了程序的执行效率。
同时在面向指针的时候,也会存在区别
#include <iostream>
#include <vector>
using namespace std;
int i = 1;
int j = 2;
int main(){
const int *p1 = &i;
constexpr int *p2 = &j;
// p1的内容是固定的
// *p1 = 2;
// p2修饰的内容可以改变,因为constexpr并不是修饰*p2的,他是修饰后面的表达式的,是让后面的表达式变成变量表达式
*p2 = 3;
// p1因为const只固定内容,所以可以可以将p1的地址进行改变
p1 = nullptr;
// p2因为修饰的是整体表达式,所以p2的指针不能变化,整体的常量是修饰指针的
// p2 = nullptr;
return 0;
}
inline内联函数和宏定义的函数
#define ExpressionName(Var1,Var2) (Var1+Var2)*(Var1-Var2)
宏形式与作用跟函数类似,但它使用预编译器,没有堆栈,使用上比函数高效,但是宏只是单纯的替换,如果出现了优先级运算问题需要检查括号的添加与否。但它只是预编译器上符号表的简单替换,也就是不能进行参数有效性检测及使用C++类的成员访问控制。
inline 推出的目的,也正是为了取代这种表达式形式的宏定义,它消除了它的缺点,同时又很好地继承了它的优点。inline代码放入预编译器符号表中,高效;它是个真正的函数,调用时有严格的参数检测;它也可作为类的成员函数。
编译器在编译阶段完成对 inline 函数的处理,即对 inline 函数的调用替换为函数的本体。但 inline 关键字对编译器只是一种建议,编译器可以这样去做,也可以不去做。如果这样做之后,可以节约函数的调用开销;但是不是所有的函数都可以这样,比如在递归的时候不确定递归的次数的时候,编译器就不会将函数转化为内联函数
正确书写一个MAX宏
C++宏的替换是纯纯的替换,不会特意加上()去声明其运算符号的优先级,如果需要确认优先级,则需要对宏上自行添加()以保证其准确性
多多加上括号,保证不会宏展开错误
#define MAX(A, B) ((A)>(B) ? (A):(B))
INT_MAX/INT_MIN头文件
#include <limits.h>
substr找子串
substr(起始位置,需要比较的长度)
s.substr(0, 3);
union联合体
联合体,装入联合体的任何数据都会公用一段内存
与struct之间的区别也就是在公用内存上
struct test
{
char mark;
long num;
float score;
};
//大小为12
union test
{
char mark;
long num;
float score;
};
//大小为4,公用一段内存,取最大的
联合体可以检测PC的大小端存储
#include <iostream>
using namespace std;
void checkCPU()
{
union MyUnion{
int a;
char c;
}test;
test.a = 1;
//这里因为如果是小端地址是低对低,高对高,char应该存放在联合体的最低位上,所以给a赋值也会给char类型的c赋值
if (test.c == 1)
cout << "little endian" <<endl;
else cout << "big endian" <<endl;
}
int main()
{
checkCPU();
return 0;
}
union中如果存储类的话,不能有构造函数和析构函数,不然会错误,因为union里面的东西共享内存,所以不能定义静态、引用类型的变量。也就是概括起来,union不要存放一些对象
//这里的代码没问题,但是如果加上类的构造函数和析构函数就会出现问题
#include <iostream>
using namespace std;
class CA
{
int m_a;
};
union Test
{
CA a;
double d;
};
int main()
{
return 0;
}
如果想要赋值给char类型的数组
想要正常的给char类型数组赋值,可以通过strcpy或者memcpy的方法
//在这里的strcpy上赋值,一定要加入\0字符,这样在strlen判断长度的时候就能分辨出到哪里结束了
strcpy(data, "ABC\0");
for(int i = 0; i < strlen(data); ++i){
cout << data[i] << " ";
}
cout << data << endl;
如果想输入赋值给char类型的数组
char data[100];
//直接对数组名进行cin就可以了,因为基本上都是以数组名所代表的指针进行内存赋值,strlen(里面也是char指针)
while (cin >> data && cin.get() != '\n')
{}
strlen和sizeof之间的区别
strlen判断字符串长度的时候,是直接返回字符串的长度;与s.length()得到的结果相等
如果使用sizeof(字符串)则是计算string在系统中的大小,也就是编译器在底层分配了多少空间给string和sizeof(string)相等
#include <iostream>
#include <vector>
#include <string.h>
using namespace std;
int i = 1;
int j = 2;
int main(){
char str[50];
string s = "asdaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
int len;
strcpy(str, "asd\0");
cout << s.length() << endl;
cout << sizeof(s) << endl;
cout << sizeof(string) << endl;
cout << sizeof(str) << endl;
cout << strlen(str) << endl;
return 0;
}
//得到的结果是
//66
//32
//32
//50
//3
如果对于string组成的数组来说得到的sizeof就是n*sizeof(s)
#include <iostream>
#include <string.h>
using namespace std;
int main(){
string a[] = {"aaaaa","bbbb","ccc"};
int x = sizeof(a);
int y = sizeof(string);
cout << x << endl;
cout << y << endl;
}
//得到的结果是
//96
//32
深拷贝和浅拷贝的区别和应用场景
深拷贝是开辟一个新的空间对对象进行拷贝,全量备份。应用场景在传参上面如果不像改变原始的对象,这时候需要用深拷贝,复制到一个新的内存空间中对其进行操作
浅拷贝是以指针的形式指向原始的对象,有点像起别名,都指向同一块内存空间,所以会修改原对象上的内容。
C++STL相关问题
1.vector
vector和arr之间的区别在于arr是静态分配的内存,vector是动态分配内存,但凡动态分配的东西,大多数都是在堆上开辟的,因为堆是由链表组织起来的,而栈不能满足开辟空间的要求,是顺序存储的,不够灵活
它的内部机制会自行扩充空间以容纳新元素(有保留内存,如果减少大小后内存也不会释放。如果新值>当前大小时才会再分配内存,但是这样也会影响到vector的性能。vector在重新动态增加分配空间的时候,是通过重新开辟一块原内存两倍大小的区域,然后将数据拷贝过去,所以vector上面的迭代器就会需要重新构建,指向新的内存地址空间
元素操作上,头尾插入是较快的,但是中间元素如果存放的是结构体类的元素那么查找的复杂度就很高,因为移动的时候涉及到构造和析构。访问数据上,都是复杂度O(1)级别。vector 常用来保存需要经常进行随机访问的内容,并且不需要经常对中间元素进行添加删除操作。
2.list
list中的元素存放在堆中,所以也是动态开辟的内存
vector是连续的空间内存,而list是链表组织起来的。对于任何位置的元素插入或元素移除,永远是常数时间,list的底层数据结构是双向链表,但是对于随机存取的数据没有效率
3.deque
动态分配内存,在堆中
双向开口数据结构
支持[]操作符,也就是随即存取,可以在前面快速地添加删除元素,或是在后面快速地添加删除元素,然后还可以有比较高的随机访问速度和vector 的效率相差无几。deque 支持在两端的操作:push_back,push_front,pop_back,pop_front等,并且在两端操作上与 list 的效率也差不多。
在标准库中 vector 和 deque 提供几乎相同的接口,在结构上区别主要在于在组织内存上不一样,deque 是按页或块来分配存储器的,每页包含固定数目的元素;相反 vector 分配一段连续的内存,vector 只是在序列的尾段插入元素时才有效率,而 deque 的分页组织方式即使在容器的前端也可以提供常数时间的 insert 和 erase 操作,而且在体积增长方面也比 vector 更具有效率。
STL 中的 unordered_map 的迭代器是向前迭代器,但不是双向迭代器,因此无法自减;而 list 的迭代器是双向迭代器,vector 的迭代器是随机访问迭代器(自然也是双向迭代器,可以随意减去一个常数),都是允许自减的。有顺序的模板类是可以支持双向迭代的,但是例如unordered_map这样不注重顺序的,离散的存储方式是没有减的迭代器的,普通的map/set,它们有rbegin/rend,这两个函数返回的是反向迭代器,非常适合反向遍历。底层红黑树的支持就是对顺序有要求。
三个容器的总结:
vector 是可以快速地在最后添加删除元素,并可以快速地访问任意元素;push_back:O(1) insert:O(n)(也就是中间插入的复杂度) pop_back:O(1) erase:O(n) 访问:O(1)
list 一般来说底层实现的方法是双向链表,是可以快速地在所有地方添加删除元素,但是只能快速地访问最开始与最后的元素;push_back:O(1) insert:O(1) pop_back:O(1) erase:O(1) 访问:O(n)(因为没有[]支持直接索引)
deque 在开始和最后添加元素都一样快,并提供了随机访问方法,像vector一样使用 [] 访问任意元素,但是随机访问速度比不上vector快,因为它要内部处理堆跳转。deque 也有保留空间。另外,由于 deque 不要求连续空间,所以可以保存的元素比 vector 更大,这点也要注意一下。还有就是在前面和后面添加元素时都不需要移动其它块的元素,所以性能也很高。push_back:O(1) insert:O(n) pop_back:O(1) erase:O(n) 访问:O(1)
因此在实际使用时,如何选择这三个容器中哪一个,应根据你的需要而定,一般应遵循下面的原则:
1、如果你需要高效的随即存取,而不在乎插入和删除的效率,使用 vector;
2、如果你需要大量的插入和删除,而不关心随即存取,则应使用 list;
3、如果你需要随即存取,而且关心两端数据的插入和删除,则应使用deque。
原文链接:https://blog.csdn.net/weixin_38337616/article/details/88587388
对于关联容器(如map,set,multimap,multiset),删除当前的iterator,仅仅会使当前的iterator失效,只要在erase时,递增当前的iterator即可。因为map之类的容器,使用了红黑树来实现,插入,删除一个结点不会对其他结点造成影响。
对于序列式容器(如vector,deque等),删除当前的iterator会使后面所有元素的iterator都失效。这是因为vector,deque使用了连续分配的内存,删除一个元素导致后面所有的元素会向前移动一个位置。不过erase方法可以返回下一个有效的iterator。
内存对齐和内存不对齐设置
C++自带内存对齐,也就是对齐到n的倍数上
关闭内存对齐需要加入
#paragma pack(1)
在预处理阶段,可以防止内存对齐,扣内存用
如果想要对对齐的模块进行设置,可以使用#paragma pack(n)
比方说以4的倍数进行对齐
#paragma pack(4)
class Data
{
char c;
int a;
char d;
};
cout << sizeof(Data) << endl;
得到的是12
一般来说linux下的对齐是4,windows下是8
如果不想类被继承
1、加final关键字
class base final
{
//......
};
2、将类的构造函数放入private中
class test{
private:
test(){
}
};
C++拷贝构造函数调用的时机
1、利用拷贝构造函数初始化类对象的时候
2、将类作为参数进行传递的时候
3、将类对象作为返回值返回的时候
类的动态内存分配
类的动态内存分配主要的目的是为了防止拷贝构造或者赋值构造operator=的时候出现两个指针指向同一块内存的区间里面的情况
其中拷贝构造函数和赋值构造函数的区别在于
A a;
A a1 = a;//会调用拷贝构造函数
//会调用赋值构造函数
a1 = a;
//下面会调用2次拷贝构造函数(一次是实参转形参,一次是返回值传临时变量) 和 赋值构造函数(返回值赋值给a1)
a1 = show(a);
//下面会调用1次拷贝构造函数(实参转形参) 和 赋值构造函数(返回值赋值给a1)
a1 = show1(a);
动态分配内存首先想到就是malloc或者new,并且一般在指针的创建的时候使用较多,类成员如果是int、char这种正常变量的时候,不需要对其进行分配内存,只有在成员变量是指针的时候才需要,所以在拷贝构造函数或者在赋值构造的时候需要对现有的指针进行new
错误案例: 如果直接这样拷贝,或者什么都不写,那么就相当于将指针的地址传递给这个成员变量的指针(this->name = name1),所以地址就会指向同一块,这不是我们想要的
class A{
public:
A(){
}
A(char *name1){
//this->name = name1;
}
private:
char *name;
};
如果我们对代码稍做调整,就不会出现指向同一块内存的现象,这是我们需要注意的,也是动态内存分配在构造函数上的应用的重要意义。
class A{
public:
A(){
}
A(char *name1){
this->name = new char(4);
this->name = *name1;
}
private:
char* name;
};
其他特别的地方,例如如果调用了默认构造函数实例化了当前的类之后,还能不能在拷贝的时候有空间进来用于赋值的呢?答案是之后当实例化了之后(也就是调用了默认构造之后,才行)但是实际应用过程中,可能出现不调用默认构造函数的情况。
比如调用有参构造,而不会调用本身的构造。
解决的方法:最好在所有的可能发生赋值的构造中进行new就好了
C++类中嵌套类,然后继承该类的构造顺序
#include <iostream>
#include <vector>
using namespace std;
class A{
public:
int a;
A(){
cout << "A 构造" << endl;
}
class B{
public:
int a;
B(){
cout << "B 构造" << endl;
}
};
};
class C :public A{
public:
C(){
cout << "C 构造" << endl;
}
};
int main(){
// A a;
//调用类中的类通过作用域的方法调用
// A::B b;
C c;
A *a1 = &c;
return 0;
}
什么时候使用虚析构
总的来说虚析构函数是为了避免内存泄露,而且是当子类中会有指针成员变量时才会使用得到的。也就说虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的。一般的继承都需要加上
总的来说,虚析构还是在父类指针指向子类对象,在delete指针的时候,如果不加virtual那么就会出现泄漏的情况
#include <iostream>
#include <vector>
using namespace std;
class A{
public:
int a;
A(){
cout << "A 构造" << endl;
}
virtual ~A(){
cout << "A 析构" << endl;
}
class B{
public:
int a;
B(){
cout << "B 构造" << endl;
}
~B(){
cout << "B 析构" << endl;
}
};
};
class C :public A{
public:
C(){
cout << "C 构造" << endl;
}
~C(){
cout << "C 析构" << endl;
}
};
int main(){
// A a;
// A::B b;
A *a1 = new C;
delete a1;
return 0;
}
什么时候使用虚继承
虚继承一般都是为了防止菱形继承的情况,也就是多个继承出现,在虚函数的表中可能会存在一些重复存储的类,所以在继承的前面加上virtual就可以防止这样的现象发生
C++的类内访问private的变量的方法
1.友元函数,通过定义friend关键字,对函数进行修饰,这样可以访问类内的private变量
2.友元类,和友元函数基本一致
3.通过public成员函数作为桥梁访问,成员函数return一个private变量,然后类对象调用成员函数获取私有变量
vector在push_back之后,对迭代器的影响
首先,vector在push_back的时候,需要进行扩容,他不是在原有空间上,接一个空间,而是需要另外开辟一段空间来存储,这样原本定义的迭代器就失效的了
auto iter = vec.begin()+1;
cout << (*iter)[0];
vec.push_back(1);
cout << (*iter)[0]; //这里就会报错
关于C++内存泄漏的问题
应对内存泄漏的问题,除了检查对应的delete和析构还有free之外,还可以在指针定义的时候使用智能指针的方式初始化指针
epoll的水平触发和边沿触发区别
边沿触发,顾名思义在有上下沿的时候触发读取等一系列操作,所以,他的特性是,在读取的时候如果没有读完(比方说数据块的大小是128,但是每次只能读100)这时候,没有读完的数据在新数据到达之前不会再去读他了,也就是等新的上升沿或者下降沿来;当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果没有全部读写完,那么下次调用epoll_wait()时,它不会事件异步通知,所以读取的数据就分段了
优点:开发者所关心的文件描述符可以很快被找到,不会有其他的无关文件描述符
缺点:如果没有一次性读完,就会有剩余的部分,影响下次和这次的相关操作
水平触发则是会一直读取直到读完,也就是会一直发送事件的异步通知,直到读完;当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果没有全部读写完,那么下次调用 epoll_wait()时,通过异步事件通知没读写完的文件描述符上继续读写;
优点:可以持续完整的读写,不会出现分块的现象
缺点:如果有大量的文件描述符没有读写完成,就会一窝蜂涌向epoll_wait上面,这样epoll_wait所得到的n(就绪文件描述符数量)就会很多,这样开发者所需要关注的文件描述符就会比较难找
红黑树
红黑树存在的目的是为了更好地对数据进行存取,一般来说红黑树是hash表(列表加槽形式的hash)的一种优化,因为红黑树是一种动态平衡二叉搜索树,所以存入的数据是带有顺序的,而且查询的复杂度也低,所以适用于有序的且快速的查询的场景
至于为什么是动态平衡而不是严格意义上的平衡,主要的点还是在于插入一个点或者删除一个点的代价(左旋右旋)比较高,这样不仅复杂而且不便于操作,所以做一个非严格的平衡树,就会简单很多
面试中问到了红黑树的最大高度差是多少:红黑树的节点个数是n的话,相应的最大的高度是2logn
C++开发release和debug的区别
类似与C++开发的volatile优化关键字,可以从编译器级别入手,优化掉一些关键字。debug模式下可能会输出很多信息,例如qDebug()函数,输出的信息就只有在debug模式下才能看见。release是发布版本,也就是不会看见很多的信息,被编译器优化掉,例如qDebug()这里就不会继续编译
大小:release下的版本获得的exe相对较小,而debug的exe较大
国际化字符集是utf-8是一般使用的形式,也就是可以保证中文等一系列奇怪的字符不会乱码
#include <stdio.h>
void main()
{
int i = 10;
int a = i;
printf("i = %d", a);
// 下面汇编语句的作用就是改变内存中 i 的值
// 但是又不让编译器知道
__asm {
mov dword ptr [ebp-4], 20h
}
int b = i;
printf("i = %d", b);
}
例如这里
debug模式下运行结果:
i = 10
i = 32
release模式下运行结果:
i = 10
i = 10
这样就可以保证让编译器不优化某些代码
字节序
大端字节序
高位字节存储在内存的低地址上,低位字节存储在内存的高位地址上
小端字节序
高位字节存储在内存的高地址上,低位字节存储在内存的低地址上
因为大端地址是高字节单元存储在低地址区域,也就是例如
高->低
[1 2 3 4] 存储的是
[1] 低->高
[2]
[3]
[4]
比较符合人类的认知顺序
小端地址是高字节单元存储在高地址区域,也就是例如
高->低
[1 2 3 4] 存储的是
[4] 低->高
[2]
[3]
[4]
小端主要是在计算机内部使用,大端则在外部使用。大端字节序在网络上使用,在获取信号的时候可以让开发者更清晰的阅读,小端地址让计算机阅读的更快
QT相关开发机制
QT的pro文件是一个启动工程文件,第一个qt += core gui等是加载core和gui的模块,也可以在后面添加别的,比如要利用QT开发一个server端,那么server端不需要界面的话就可以qt -= gui卸载对应模块,需要socket网络编程可以qt += network。config是配置一些配置项,也是用+和-控制的,例如+= c++11。除此之外,还有一些自动的项目目录文件
其他文件是source源文件目录,header头文件目录,以及ui可视化界面目录
一般来说QT的类名就是他本身的函数名或者参数名字,例如QApplication类的头文件就是QApplication,这是一个很重要的类。因为QT是窗口驱动的类,所以是基于一些事件触发的,例如拖动窗口,或者点击,都会有一些事件触发。后台会存放一个事件循环,进入事件循环就相当于a.exec
Mainwindow窗口对象,可以直接通过实例化对象,实例化之后,调用show方法,展现出来
Mainwindow中有ui,cpp,h头文件,这三个类是利用指针捆绑的方法,将mainwindow的类和ui中mainwindow的类两个实例化之后的对象进行了捆绑,这样改动cpp和h,就可以相当于改动ui界面了
三大类:Qwidget、Qdialog(对话框类)、Qmainwindow(窗口)
1、Qwidget是所有窗口类的基类,一般创建窗口都是Qwidget的子类
在Qwidget中的窗口如果没有边框就相当于被内嵌到了某个父窗口上,如果有边框就是一个独立的窗口;没有指定父对象的窗口就是个独立窗口,独立窗口想要显示一定要show,如果指定了父对象(实例化对象的时候,让指针new一个this就可以),那么就算不show,也能显示出来。但是这样会导致没有边框了,内嵌在父窗口之中了
2、Qdialog是Qwidget的子类,Qdialog有两种显示方法
(1)非模态显示:指定父对象和不指定都必须要调用show方法才能show出来,鼠标的焦点也是可以选中该窗口的
(2)模态显示:利用exec方法来显示,但是这种方法在显示出来之后,除了dialog之外的窗口鼠标是点不了的,原因是会阻塞在那里,不点击确认和取消等按钮结束事件才行,一般用于强制选项上
3、Qmainwindow也是不能作为别的窗口的内嵌窗口的,具有菜单栏(只能有一个),状态栏(只能有一个),工具栏(可以多创建)这个是别的窗口没有的
QT的窗口的坐标原点在左上方,子窗口的坐标体系是其父窗口的,坐标原点是从父窗口的左上角的原点开始的。最大的窗口是屏幕,一般做为最大的窗口
将窗口移动使用的是move关键字,如果需要移动主窗口this->move(1,1);
QT的析构有一棵父子类的继承树用来描述父子类之间的关系,QT可以通过setParent和构造函数进行父类的指定
QT内存的自动回收必须满足以下两个条件
1.创建的对象必须是QObject的子类(间接子类也可以)
2.创建出的类对象必须要规定父类是什么
析构函数一般跟随类cpp,也就是当父类析构了之后,会析构继承他的子类的对象
QT中的高宽获取,如果前面加上r,例如rwidth和rheight的话相当于用引用的方法传递高和宽,QRect是矩形类,用于设置窗口
QT的信号槽机制,但凡是基于窗口的框架,都有一套自己的事件处理机制。信号就是一个事件,槽就是处理这样一个事件的处理动作,类似涉及模式中的观察者模式,一直观察是否有事件产生,其实就是回调
在事件产生了之后,被事件框架捕捉到了,返回一个信号,这个信号就是对事件的描述,本质是一个函数;再通过槽函数处理;信号是被实例化的对象发出的,只有事件被实例化之后,才能发出相关的信号。
这里的信号可能是广播的,但是收发双发需要建立某种关系才能实现信号处理,也就是特定的槽函数在收到特定的信号才能进行处理。所以这里引入connect函数
connect函数包括的参数有
信号发出者的地址(信号发出者),发出信号的函数地址(发出了什么样的信号),信号接收方的地址(信号接收者),槽函数的地址(如何处理信号,处理信号函数的地址)
信号槽是可以被继承的,所以当前类没有直接的槽函数不代表没有,可以继承父类的槽函数。根据不同的状态会产生很多不同的处理函数。
点击按钮弹出窗口,相当于信号发出与处理接收,这样就需要在按钮中找信号,窗口中找槽函数
先在ui文件中拖拽一个按钮,修改名字为closeBtn,在按钮里面可以有一些方法,其中包括clicked方法等
connect(ui->closeBtn, &QpushButton::clicked, this, &MainWindow::close);这里的语句是只要按下button按钮,主窗口就会关闭。
如果在出现一个控件没有开发者想要的信号,比如悬停啊什么的,或者接收端的槽函数没有适合的操作,那么需要自己写槽函数和信号。
解决的方法并不是修改哪个官方的类,可以通过继承官方的类,自己写一个子类,可以自己扩展。
信号槽机制是继承于QObject,只有继承了这个父类才能使用信号槽机制
自定义信号
信号的要求
1.信号要是成员函数(信号也是函数)
2.返回值是void类型
3.信号的名称可以根据实际情况进行指定
4.参数可以随意指定,信号支持重载
5.信号函数需要在signal下进行声明,信号没有函数体实现,所以只需要定义不需要声明
6.一般习惯在信号函数的前面加上一个emit作为标识
槽函数的要求
1.信号的参数数量可以大于等于槽函数的参数数量(相当于传递的数据被忽略了)
2.返回值是void
3.槽函数一般用public slot来声明
信号与槽函数还是需要通过connect来连接的
但是由于是自定义的信号槽,所以connect是无法识别的,所以这里可以在ui界面里面设置一个按钮,然后这个按钮在clicked的情况下会触发信号,ui->函数,这时候当按钮clicked的时候,connect(按钮,和信号函数)再connect(信号函数,槽函数)就可以连接了
一个信号可以连接多个槽函数,槽函数和信号的connect顺序不代表信号发送的顺序,信号的发送实际上是随机的,谁先到先发送
如果一个信号是存在带参数和不带参数的两种方法的时候,就不能直接通过connect对信号和槽进行连接,需要在函数的前面进行声明(原因是有参构造和无参构造所指向的地址是同一块,只是对函数的重载),在一开始以函数指针的形式对函数进行定义
例如:
void (GirlFriend::*girl1)() = &GirlFriend::hungry;
void (GirlFriend::*girl2)(QString) = &GirlFriend::hungry;
void (Me::*mypoint)(QString) = &Me::eat;
connect(m_girl, girl2, m_me, mypoint)
QT下自适应分辨率应该有两种方法:通过栅格布局;通过缩放比。目的是为了适配不同的操作系统的不同分辨率可能会出现的窗口移动等现象
处理的方法从理论上说
1.最理想的方法
尽可能的使用布局管理器layout,不要把控件设置固定大小,不要使用现成的贴图,使用qml界面编程;
2.获取屏幕DPI
dpi=1的时候最适合,否则所有控件的大小,都需要乘以dpi
3.加入高分屏的支持
epoll
一个文件描述符fd有一个读缓冲区和一个写缓冲区
epoll是IO多路复用,是同步IO的一种,但是也属于NIO非阻塞IO。如果用多线程加上阻塞IO,就是一个线程一个read或者write的话,会造成资源开辟过多,且不能释放的后果。如果用一个线程去遍历是否有准备好的fd的话,就比较适合于连接数量比较多的场景。epoll在epoll_wait的部分依旧会引起线程的阻塞,所以不能算同步IO,真正的异步是直接跳过read/write去做别的事情。
信号驱动也是同步IO的一种。是通过注册信号函数于socket绑定的关系,通过函数指针绑定触发函数,当有相应的socket准备好了之后,就可以触发绑定的函数指针
在Reactor模式中,会先对每个client注册感兴趣的事件,然后有一个线程专门去轮询每个client是否有事件发生,当有事件发生时,便顺序处理每个事件,当所有事件处理完之后,便再转去继续轮询。多路复用IO就是采用Reactor模式。
在Proactor模式中,当检测到有事件发生时,会新起一个异步操作,然后交由内核线程去处理,当内核线程完成IO操作之后,发送一个通知告知操作已完成,可以得知,异步IO模型采用的就是Proactor模式。
Tip
写 if 的时候需要将定量写在 == 号前面
if(0 == somefunc()){
do something;
}
IO模型、线程模型
IO模型决定如何收发数据,线程模型决定如何处理数据
IO模型:管理连接,并获取数据
1.阻塞型的IO
一般直接多线从处理,每个read/write分配一个线程,加入没有数据的时候,线程会因为read阻塞,那么会导致资源的浪费
如果用线程池改进,那么当并发量上来的时候,就很容易阻塞
2.IO多路复用方法
一个fd管理多个fd,是NIO模型。也是会阻塞的,但是于纯粹的阻塞IO不同,是通过集中管理的方法,将所有的socket都在内核态遍历,有数据的时候进行IO操作,这样可以更好地处理并发问题
线程模型:具体处理数据的环节
1.传统阻塞IO服务型
一个线程处理一个事件,每个连接都需要独立的线程进行操作
2.Reactor模型
IO复用结合线程池的做法,将请求传递给一个servicehandler集中处理,下发任务dispatch
3.Proactor模型
Reactor模型和Proactor模型
Reactor模型基于事件驱动,特别适合处理海量的I/O事件。Reactor有三个事件类型;Reactor:负责监听和分配事件,将I/O事件分派给对应的Handler;新的事件包含连接建立就绪、读就绪、写就绪等。Acceptor:处理客户端新连接,并分派请求到处理器链中。Handler:将自身与事件绑定,执行非阻塞读/写任务,完成channel的读入,完成处理业务逻辑后,负责将结果写出channel。可用资源池来管理。
Reactor就有了以下的特点:
1.响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;
2.编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;
3.可扩展性,可以方便地通过增加Reactor实例个数来充分利用CPU资源;
4.可复用性,Reactor模型本身与具体事件处理逻辑无关,具有很高的复用性。
Proactor模型异步触发,异步IO都是操作系统负责将数据读写到应用传递进来的缓冲区供应用程序操作。
由于异步的模型
Proactor的缺点是
1.编程复杂性,由于异步操作流程的事件的初始化和事件完成在时间和空间上都是相互分离的,因此开发异步应用程序更加复杂。应用程序还可能因为反向的流控而变得更加难以Debug;
2.内存使用,缓冲区在读或写操作的时间段内必须保持住,可能造成持续的不确定性,并且每个并发操作都要求有独立的缓存,相比Reactor模型,在Socket已经准备好读或写前,是不要求开辟缓存的;
3.操作系统支持,Windows下通过IOCP实现了真正的异步 I/O,而在Linux系统下,Linux2.6才引入,并且异步I/O使用epoll实现的,所以还不完善。
C++野指针和空悬指针
野指针是没有初始化的指针,相当于没有动态分配内存,此时指针的数值是个垃圾数
空悬指针是原本初始化了一个指针,并且之后也指向了某个对象,在对象销毁了之后,就需要重新将此指针变为空才可以,不然这个指针就悬空在那里
虚拟内存
内存的产生主要是CPU太快,但容量小且功能单一,其他 I/O 硬件支持各种花式功能,可是相对于 CPU,它们又太慢。于是它们之间就需要一种润滑剂来作为缓冲,这就是内存大显身手的地方,而在现代操作系统中,多任务已是标配。多任务并行,大大提升了 CPU 利用率,但却引出了多个进程对内存操作的冲突问题,虚拟内存概念的提出就是为了解决这个问题。
虚拟内存的大小
①虚存的实际容量小于等于内存容量和外存容量之和
②虚存的最大容量小于等于计算机的地址位数能容纳的最大容量
tip:虚拟内存的实际容量:当CPU地址长度能表示的大小远远大于外存的容量的时候,那么由外存的大小决定
虚拟内存的最大容量:由CPU地址长度决定
虚拟内存中的虚拟页可能处于以下三中状况——未分配、未缓存和已缓存,其中未分配的内存页是没有被进程申请使用的,也就是空闲的虚拟内存,不占用虚拟内存磁盘的任何空间,未缓存和已缓存的内存页分别表示已经加载到主存中的内存页和仅加载到磁盘中的内存页。当为用户访问未被缓存的页的时候,会发生缺页中断,被访问的页面被加载到物理内存中,这时虚拟内存表中的相关地址部分就会指向已缓存的内存的部分,这里我认为,如果
A,B进程向操作系统提出内存申请,但是操作系统是通过虚拟映射表的形式向每个进行分配内存的。进程只需要使用分配的内存。他们不需要指导真正的地址空间,通过虚拟内存机制,每个进程都有自己的一块连续内存(物理上并不一定连续)
虚拟内存的作用:
1.虚拟内存可以利用磁盘起到缓存的作用,提高进程访问指定内存的速度。
2.虚拟内存可以为进程提供独立的内存空间,简化程序的链接、加载过程并通过动态库共享内存。
3.虚拟内存可以控制进程对物理内存的访问,隔离不同进程的访问权限,提高系统的安全性。
有错误的地方,还望评论区指正批评,一起学习进步!!