之前在本地记了很多笔记,今天才发现分享到博客上被更多人看到,似乎更有价值!
文章目录
- QT_C++学习笔记
- 0.break与continue
- 1. 文件系统
- 2. C++输入输出流
- 3.Qt字符串类型
- 4.匿名函数的使用
- 5.C++11 遍历容器元素的方法
- 6.auto关键字
- 7.for_each算法
- 8.decltype关键字
- 9.QDesktopWidget包含头文件报错
- 10.Qt中的信号槽
- 11.Qt中按钮上设置图标
- 12、Qt中线程的使用
- 13、Qt网络编程
- 14、TCP粘包问题
- 15、 new一个新对象
- 16、 相关数据类型
- 17、网络编程
- 18、C++ 中 main函数
- 19、UDP通信中的问题
- 20、C++中多态的例子(以Animal类举例)
- 21、C++中多线程代码
- 22.C++和java中指针和引用的用法
- 23、Non-member Functions for Type Conversion
- 24、C++函数模板
- Qt斗地主项目
QT_C++学习笔记
0.break与continue
在 C++ 语言中,break 和 continue 的用法与 Python 语言基本相同。
break 的使用场景
- 在循环中遇到错误或异常时,可以使用 break 语句提前终止循环。
- 在循环中找到所需结果后,可以使用 break 语句提前终止循环。
continue 的使用场景
- 在循环中需要跳过某些特定条件的迭代时,可以使用 continue 语句。
- 在循环中需要根据某些条件执行不同的操作时,可以使用 continue 语句。
// 使用 break 语句提前终止循环
#include <iostream>
int main() {
for (int i = 0; i < 10; i++) {
if (i == 5) {
break;
}
std::cout << i << std::endl;
}
return 0;
}
// 输出:
// 0
// 1
// 2
// 3
// 4
// 使用 continue 语句跳过某些特定条件的迭代
#include <iostream>
int main() {
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
continue;
}
std::cout << i << std::endl;
}
return 0;
}
// 输出:
// 1
// 3
// 5
// 7
// 9
注意事项
- break 和 continue 语句只能在循环语句中使用。
- break 语句会跳出整个循环,而 continue 语句只会跳过本次循环。
1. 文件系统
文件操作是应用程序必不可少的部分。Qt 作为一个通用开发库,提供了跨平台的文件操作能力。Qt 通过QIODevice提供了对 I/O 设备的抽象,这些设备具有读写字节块的能力。下面是 I/O 设备的类图(Qt5):
- l QIODevice:所有 I/O 设备类的父类,提供了字节块读写的通用操作以及基本接口;
- l QFileDevice:Qt5新增加的类,提供了有关文件操作的通用实现。
- l QFlie:访问本地文件或者嵌入资源;
- l QTemporaryFile:创建和访问本地文件系统的临时文件;
- l QBuffer:读写QbyteArray, 内存文件;
- l QProcess:运行外部程序,处理进程间通讯;
- l QAbstractSocket:所有套接字类的父类;
- l QTcpSocket:TCP协议网络数据传输;
- l QUdpSocket:传输 UDP 报文;
- l QSslSocket:使用 SSL/TLS 传输数据;
文件系统分类:
l 顺序访问设备:
是指它们的数据只能访问一遍:从头走到尾,从第一个字节开始访问,直到最后一个字节,中途不能返回去读取上一个字节,这其中,QProcess、QTcpSocket、QUdpSoctet和QSslSocket是顺序访问设备。
l 随机访问设备:
可以访问任意位置任意次数,还可以使用QIODevice::seek()函数来重新定位文件访问位置指针,QFile、QTemporaryFile和QBuffer是随机访问设备,
1.1基本文件操作
文件操作是应用程序必不可少的部分。Qt 作为一个通用开发库,提供了跨平台的文件操作能力。在所有的 I/O 设备中,文件 I/O 是最重要的部分之一。因为我们大多数的程序依旧需要首先访问本地文件(当然,在云计算大行其道的将来,这一观点可能改变)。QFile提供了从文件中读取和写入数据的能力。
我们通常会将文件路径作为参数传给QFile的构造函数。不过也可以在创建好对象最后,使用setFileName()来修改。QFile需要使用 / 作为文件分隔符,不过,它会自动将其转换成操作系统所需要的形式。例如 C:/windows 这样的路径在 Windows 平台下同样是可以的。
QFile主要提供了有关文件的各种操作,比如打开文件、关闭文件、刷新文件等。**我们可以使用QDataStream或QTextStream类来读写文件,也可以使用QIODevice类提供的read()、readLine()、readAll()以及write()这样的函数。**值得注意的是,有关文件本身的信息,比如文件名、文件所在目录的名字等,则是通过QFileInfo获取,而不是自己分析文件路径字符串。
下面我们使用一段代码来看看QFile的有关操作:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QFile file("in.txt");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "Open file failed.";
return -1;
} else {
while (!file.atEnd()) {
qDebug() << file.readLine();
}
}
QFileInfo info(file);
qDebug() << info.isDir();
qDebug() << info.isExecutable();
qDebug() << info.baseName();
qDebug() << info.completeBaseName();
qDebug() << info.suffix();
qDebug() << info.completeSuffix();
return app.exec();
}
l 我们首先使用QFile创建了一个文件对象。
这个文件名字是 in.txt。如果你不知道应该把它放在哪里,可以使用QDir::currentPath()来获得应用程序执行时的当前路径。只要将这个文件放在与当前路径一致的目录下即可。
l 使用open()函数打开这个文件,打开形式是只读方式,文本格式。
这个类似于fopen()的 r 这样的参数。open()函数返回一个 bool 类型,如果打开失败,我们在控制台输出一段提示然后程序退出。否则,我们利用 while 循环,将每一行读到的内容输出。
l 可以使用QFileInfo获取有关该文件的信息。
QFileInfo有很多类型的函数,我们只举出一些例子。比如:
n isDir()检查该文件是否是目录;
n isExecutable() 检查该文件是否是可执行文件等。
n baseName() 可以直接获得文件名;
n completeBaseName() 获取完整的文件名
n suffix() 则直接获取文件后缀名。
n completeSuffix() 获取完整的文件后缀
我们可以由下面的示例看到,baseName()和completeBaseName(),以及suffix()和completeSuffix()的区别:
QFileInfo fi(“/tmp/archive.tar.gz”);
QString base = fi.baseName(); // base = “archive”
QString base = fi.completeBaseName(); // base = “archive.tar”
QString ext = fi.suffix(); // ext = “gz”
QString ext = fi.completeSuffix(); // ext = “tar.gz”
1.2 二进制文件读写
QDataStream提供了基于QIODevice的二进制数据的序列化。数据流是一种二进制流,这种流完全不依赖于底层操作系统、CPU 或者字节顺序(大端或小端)。例如,在安装了 Windows 平台的 PC 上面写入的一个数据流,可以不经过任何处理,直接拿到运行了 Solaris 的 SPARC 机器上读取。由于数据流就是二进制流,因此我们也可以直接读写没有编码的二进制数据,例如图像、视频、音频等。
QDataStream****既能够存取 C++ 基本类型,如 int、char、short 等,也可以存取复杂的数据类型,例如自定义的类。实际上,QDataStream对于类的存储,是将复杂的类分割为很多基本单元实现的。
结合QIODevice,QDataStream可以很方便地对文件、网络套接字等进行读写操作。我们从代码开始看起:
QFile file(“file.dat”);
file.open(QIODevice::WriteOnly);
QDataStream out(&file);
out << QString(“the answer is”);
out << (qint32)42;
l 在这段代码中,我们首先打开一个名为 file.dat 的文件(注意,我们为简单起见,并没有检查文件打开是否成功,这在正式程序中是不允许的)。然后,我们将刚刚创建的file对象的指针传递给一个QDataStream实例out。类似于std::cout标准输出流,QDataStream也重载了输出重定向<<运算符。后面的代码就很简单了:将“the answer is”和数字 42 输出到数据流。由于我们的 out 对象建立在file之上,因此相当于将问题和答案写入file。
l 需要指出一点:最好使用 Qt 整型来进行读写,比如程序中的qint32。这保证了在任意平台和任意编译器都能够有相同的行为。
如果你直接运行这段代码,你会得到一个空白的 file.dat,并没有写入任何数据。这是因为我们的file没有正常关闭。为性能起见,数据只有在文件关闭时才会真正写入。因此,我们必须在最后添加一行代码:
file.close(); // 如果不想关闭文件,可以使用 file.flush();
接下来我们将存储到文件中的答案取出来
QFile file(“file.dat”);
file.open(QIODevice::ReadOnly);
QDataStream in(&file);
QString str;
qint32 a;
in >> str >> a;
唯一需要注意的是,你必须按照写入的顺序,将数据读取出来。顺序颠倒的话,程序行为是不确定的,严重时会直接造成程序崩溃。
那么,既然QIODevice提供了read()、readLine()之类的函数,为什么还要有QDataStream呢?QDataStream同QIODevice有什么区别?区别在于,**QDataStream****提供流的形式,性能上一般比直接调用原始 API 更好一些。**我们通过下面一段代码看看什么是流的形式:
QFile file(“file.dat”);
file.open(QIODevice::ReadWrite);
QDataStream stream(&file);
QString str = “the answer is 42”;
stream << str;
1.3 文本文件读写
上一节我们介绍了有关二进制文件的读写。二进制文件比较小巧,却不是人可读的格式。而文本文件是一种人可读的文件。为了操作这种文件,我们需要使用QTextStream类。QTextStream和QDataStream的使用类似,只不过它是操作纯文本文件的。
QTextStream会自动将 Unicode 编码同操作系统的编码进行转换,这一操作对开发人员是透明的。它也会将换行符进行转换,同样不需要自己处理。QTextStream****使用 16 位的QChar作为基础的数据存储单位,同样,它也支持 C++ 标准类型,如 int 等。实际上,这是将这种标准类型与字符串进行了相互转换。
QTextStream同QDataStream的使用基本一致,例如下面的代码将把“The answer is 42”写入到 file.txt 文件中:
QFile data(“file.txt”);
if (data.open(QFile::WriteOnly | QIODevice::Truncate))
{
QTextStream out(&data);
out << "The answer is " << 42;
}
这里,我们在open()函数中增加了QIODevice::Truncate打开方式。我们可以从下表中看到这些打开方式的区别:
枚举值 描述
l QIODevice::NotOpen 未打开
l QIODevice::ReadOnly 以只读方式打开
l QIODevice::WriteOnly 以只写方式打开
l QIODevice::ReadWrite 以读写方式打开
l QIODevice::Append 以追加的方式打开,
新增加的内容将被追加到文件末尾
l QIODevice::Truncate 以重写的方式打开,在写入新的数据时会将原有
数据全部清除,游标设置在文件开头。
l QIODevice::Text 在读取时,将行结束符转换成 \n;在写入时,
将行结束符转换成本地格式,例如 Win32 平台
上是 \r\n
l QIODevice::Unbuffered 忽略缓存
我们在这里使用了QFile::WriteOnly | QIODevice::Truncate,也就是以只写并且覆盖已有内容的形式操作文件。注意,QIODevice::Truncate会直接将文件内容清空。
虽然QTextStream的写入内容与QDataStream一致,但是读取时却会有些困难:
QFile data(“file.txt”);
if (data.open(QFile::ReadOnly))
{
QTextStream in(&data);
QString str;
int ans = 0;
in >> str >> ans;
}
在使用QDataStream的时候,这样的代码很方便,但是使用了QTextStream时却有所不同:读出的时候,str 里面将是 The answer is 42,ans 是 0。这是因为**当使用QDataStream写入的时候,实际上会在要写入的内容前面,额外添加一个这段内容的长度值。而以文本形式写入数据,是没有数据之间的分隔的。**因此,使用文本文件时,很少会将其分割开来读取,而是使用诸如使用:
l QTextStream::readLine() 读取一行
l QTextStream::readAll()读取所有文本
这种函数之后再对获得的QString对象进行处理。
默认情况下,QTextStream的编码格式是 Unicode,如果我们需要使用另外的编码,可以使用:
stream.setCodec(“UTF-8”);
这样的函数进行设置。
2. C++输入输出流
[3.C++输入输出流.pdf](E:\C++短期班\第二周\课件\C++ Day10\3.C++输入输出流.pdf)
1、统计一篇英文(The_Holy_Bible.txt)文章中出现的单词和词频,
输入:某篇文章的绝对路径
输出:词典(词典中的内容为每一行都是一个“单词 词频”)
词典的存储格式如下
| a 66 |
| abandon 77 |
| public 88 |
| ...... |
|_________________|
struct Record
{
string _word;
int _frequency;
};
class Dictionary
{
public:
//......
void read(const std::string &filename);
void store(const std::string &filename);
private:
vector<Record> _dict;
};
提示:因为我们需要统计圣经文件中单词以及该单词在文件中出现的次数,所以可以看去读圣经文件,然后将单词存到数据结构中,并记录单词的次数,如果单词第二次出现的时候,只需要修改单词的次数(也就是这里说的单词的频率),这样当统计完整个圣经文件后,数据都存在数据结构vector了。接着遍历vector数据结构就可以将单词以及单词次数(也就是频率)存储到另外一个文件。(当然如果不存到另外一个文件,就只能打印到终端了)
注意:在读圣经文件的时候,有可能字符串是不合法的,比如:abc123 abc?这样的字符串,处理方式两种:直接不统计这样的字符串或者将非法字母去掉即可。
最终得到结果类似:
a 10
public 20
welcome 30
…
2.1 第一种方法实现
#include <iostream>
#include <sstream>
#include <string>
#include <fstream>
#include <vector>
#include <algorithm>
using std::cout;
using std::endl;
using std::cerr;
using std::ostringstream;
using std::istringstream;
using std::stringstream;
using std::string;
using std::ifstream;
using std::ofstream;
using std::vector;
struct Record
{
//默认public类型
Record(const string &word,int frequency)
:_word(word)
,_frequency(frequency)
{
}
string _word;//记录单词
int _frequency;//记录词频
};
class Dictionary
{
public:
void read(const string &filename);
//判断单词形式是否合格,如果合格就放到vector中
void store(const string &filename);
string dealWord(const string &word);//处理不合规单词的函数
void insert(const string &word );//将经过处理后的单词插入到vector中的insert操作
private:
vector<Record> _dict;//用vector来存放单词和词频
};
void Dictionary::read(const string &filename)
{
ifstream ifs(filename);
if(!ifs.good())
{
cerr << "Failed to open file: " << filename << endl;
return;
}
//读filename文件,对每一个单词做处理
/*
string word;
while(ifs >> word) // 效率慢
*/
string line;
while(getline(ifs,line))
{
istringstream iss(line);
string word;
//ss >> word ;
while(iss >> word)
{
//对于word还要进行过滤和筛选
// string newWord;//存放经过处理后的单词、
// newWord = dealWord(word);
string newWord = dealWord(word);
insert(newWord);//将newWord存放到vector中
//insert(word);
}
}
ifs.close();
}
//让单词与词频按照字母顺序进行排列
/*about 1
abstract 2
banana 23
call 45
……
zoo 17*/
bool compareRecords(const Record &record1, const Record &record2)
{
return record1._word < record2._word;
}
void Dictionary::store(const string &filename)
{
sort(_dict.begin(), _dict.end(), compareRecords);
ofstream ofs(filename);
if(!ofs.good())
{
cerr << "Failed to open file: " << filename << endl;
return;
}
//通过迭代器访问
// for (const Record &record : _dict)
// {
// ofs << record._word << " " << record._frequency << "\n";
// }
for(size_t idx = 0; idx != _dict.size();++idx)
{
ofs << _dict[idx]._word << " "
<< _dict[idx]._frequency << endl;
}
ofs.close();
}
string Dictionary::dealWord(const string &word)
{
for(size_t idx = 0;idx != word.size();++idx)
{
if(!isalpha(word[idx]))//判断是不是一个字母
{
//直接让不合法的字符变成一个空串
return string();//临时对象
}
}
return word;
}
void Dictionary::insert(const string &word )
{
// if(word == string())
// {
// return;
// }
if(!word.empty())
{
size_t idx= 0;
for(idx;idx != _dict.size();++idx)
{
if(word == _dict[idx]._word)
{
++_dict[idx]._frequency;
break;//不用一直遍历到文件末尾
}
}
//两种情况,整合成一句代码,第一种:_dict.size()有可能为0;第二种:
if(idx == _dict.size())
{
_dict.push_back(Record(word,1));//单词第一次出现的代码
}
}
}
int main()
{
Dictionary dictionary;
cout << "begin read" << endl;
dictionary.read("The_Holy_Bible.txt");
cout << "end reading" << endl;
dictionary.store("daxugua_pro.txt");
return 0;
}
2.2 第二种方法实现
#include <iostream>
#include <sstream>
#include <string>
#include <fstream>
#include <vector>
#include <algorithm>
using std::cout;
using std::endl;
using std::cerr;
using std::ostringstream;
using std::istringstream;
using std::stringstream;
using std::string;
using std::ifstream;
using std::ofstream;
using std::vector;
struct Record
{
//默认public类型
Record(const string &word,int frequency)
:_word(word)
,_frequency(frequency)
{
}
string _word;//记录单词
int _frequency;//记录词频
};
class Dictionary
{
public:
void read(const string &filename);
//判断单词形式是否合格,如果合格就放到vector中
void store(const string &filename);
string dealWord(const string &word);//处理不合规单词的函数
void insert(const string &word );//将经过处理后的单词插入到vector中的insert操作
private:
vector<Record> _dict;//用vector来存放单词和词频
};
//让单词与词频按照字母顺序进行排列
/*about 1
abstract 2
banana 23
call 45
……
zoo 17*/
bool operator<(const Record &record1, const Record &record2)//针对两个Record重载小于符号
{
return record1._word < record2._word;//两个string类型可以直接进行比较,不用重载小于<符号
}
void Dictionary::read(const string &filename)
{
ifstream ifs(filename);
if(!ifs.good())
{
cerr << "Failed to open file: " << filename << endl;
return;
}
//读filename文件,对每一个单词做处理
/*
string word;
while(ifs >> word) // 效率慢
*/
string line;
while(getline(ifs,line))
{
istringstream iss(line);
string word;
//ss >> word ;
while(iss >> word)
{
//对于word还要进行过滤和筛选
// string newWord;//存放经过处理后的单词、
// newWord = dealWord(word);
string newWord = dealWord(word);
insert(newWord);//将newWord存放到vector中
//insert(word);
}
}
sort(_dict.begin(), _dict.end());
ifs.close();
}
void Dictionary::store(const string &filename)
{
ofstream ofs(filename);
if(!ofs.good())
{
cerr << "Failed to open file: " << filename << endl;
return;
}
//通过迭代器访问
// for (const Record &record : _dict)
// {
// ofs << record._word << " " << record._frequency << "\n";
// }
for(size_t idx = 0; idx != _dict.size();++idx)
{
ofs << _dict[idx]._word << " "
<< _dict[idx]._frequency << endl;
}
ofs.close();
}
string Dictionary::dealWord(const string &word)
{
for(size_t idx = 0;idx != word.size();++idx)
{
if(!isalpha(word[idx]))//判断是不是一个字母
{
//直接让不合法的字符变成一个空串
return string();//临时对象
}
}
return word;
}
void Dictionary::insert(const string &word )
{
// if(word == string())
// {
// return;
// }
if(!word.empty())
{
size_t idx= 0;
for(idx;idx != _dict.size();++idx)
{
if(word == _dict[idx]._word)
{
++_dict[idx]._frequency;
break;//不用一直遍历到文件末尾
}
}
//两种情况,整合成一句代码,第一种:_dict.size()有可能为0;第二种:
if(idx == _dict.size())
{
_dict.push_back(Record(word,1));//单词第一次出现的代码
}
}
}
int main()
{
Dictionary dictionary;
cout << "begin read" << endl;
time_t beg = time(NULL);
dictionary.read("The_Holy_Bible.txt");
time_t end = time(NULL);
cout << "end reading" << endl;
cout << "总共用时" << (end - beg) << "s" << endl;
dictionary.store("manba.txt");
return 0;
}
3.Qt字符串类型
在Qt中不仅支持C, C++中的字符串类型, 而且还在框架中定义了专属的字符串类型, 我们必须要掌握在Qt中关于这些类型的使用和相互之间的转换。
语言类型 | 字符串类型 |
---|---|
C | char* |
C++ | std::string, char* |
Qt | QByteArray, QString 等 |
3.1 QByteArray
在Qt中QByteArray可以看做是c语言中 char*的升级版本。我们在使用这种类型的时候可通过这个类的构造函数申请一块动态内存,用于存储我们需要处理的字符串数据。
下面给大家介绍一下这个类中常用的一些API函数,大家要养成遇到问题主动查询帮助文档的好习惯。
构造函数
// 构造空对象, 里边没有数据
QByteArray::QByteArray();
// 将data中的size个字符进行构造, 得到一个字节数组对象
// 如果 size==-1 函数内部自动计算字符串长度, 计算方式为: strlen(data)
QByteArray::QByteArray(const char *data, int size = -1);
// 构造一个长度为size个字节, 并且每个字节值都为ch的字节数组
QByteArray::QByteArray(int size, char ch);
数据操作
// 在尾部追加数据
// 其他重载的同名函数可参考Qt帮助文档, 此处略
QByteArray &QByteArray::append(const QByteArray &ba);
void QByteArray::push_back(const QByteArray &other);
// 头部添加数据
// 其他重载的同名函数可参考Qt帮助文档, 此处略
QByteArray &QByteArray::prepend(const QByteArray &ba);
void QByteArray::push_front(const QByteArray &other);
// 插入数据, 将ba插入到数组第 i 个字节的位置(从0开始)
// 其他重载的同名函数可参考Qt帮助文档, 此处略
QByteArray &QByteArray::insert(int i, const QByteArray &ba);
// 删除数据
// 从大字符串中删除len个字符, 从第pos个字符的位置开始删除
QByteArray &QByteArray::remove(int pos, int len);
// 从字符数组的尾部删除 n 个字节
void QByteArray::chop(int n);
// 从字节数组的 pos 位置将数组截断 (前边部分留下, 后边部分被删除)
void QByteArray::truncate(int pos);
// 将对象中的数据清空, 使其为null
void QByteArray::clear();
// 字符串替换
// 将字节数组中的 子字符串 before 替换为 after
// 其他重载的同名函数可参考Qt帮助文档, 此处略
QByteArray &QByteArray::replace(const QByteArray &before, const QByteArray &after);
子字符串查找和判断
// 判断字节数组中是否包含子字符串 ba, 包含返回true, 否则返回false
bool QByteArray::contains(const QByteArray &ba) const;
bool QByteArray::contains(const char *ba) const;
// 判断字节数组中是否包含子字符 ch, 包含返回true, 否则返回false
bool QByteArray::contains(char ch) const;
// 判断字节数组是否以字符串 ba 开始, 是返回true, 不是返回false
bool QByteArray::startsWith(const QByteArray &ba) const;
bool QByteArray::startsWith(const char *ba) const;
// 判断字节数组是否以字符 ch 开始, 是返回true, 不是返回false
bool QByteArray::startsWith(char ch) const;
// 判断字节数组是否以字符串 ba 结尾, 是返回true, 不是返回false
bool QByteArray::endsWith(const QByteArray &ba) const;
bool QByteArray::endsWith(const char *ba) const;
// 判断字节数组是否以字符 ch 结尾, 是返回true, 不是返回false
bool QByteArray::endsWith(char ch) const;
遍历
// 使用迭代器
iterator QByteArray::begin();
iterator QByteArray::end();
// 使用数组的方式进行遍历
// i的取值范围 0 <= i < size()
char QByteArray::at(int i) const;
char QByteArray::operator[](int i) const;
查看字节数
// 返回字节数组对象中字符的个数
int QByteArray::length() const;
int QByteArray::size() const;
int QByteArray::count() const;
// 返回字节数组对象中 子字符串ba 出现的次数
int QByteArray::count(const QByteArray &ba) const;
int QByteArray::count(const char *ba) const;
// 返回字节数组对象中 字符串ch 出现的次数
int QByteArray::count(char ch) const;
类型转换
// 将QByteArray类型的字符串 转换为 char* 类型
char *QByteArray::data();
const char *QByteArray::data() const;
// int, short, long, float, double -> QByteArray
// 其他重载的同名函数可参考Qt帮助文档, 此处略
QByteArray &QByteArray::setNum(int n, int base = 10);
QByteArray &QByteArray::setNum(short n, int base = 10);
QByteArray &QByteArray::setNum(qlonglong n, int base = 10);
QByteArray &QByteArray::setNum(float n, char f = 'g', int prec = 6);
QByteArray &QByteArray::setNum(double n, char f = 'g', int prec = 6);
[static] QByteArray QByteArray::number(int n, int base = 10);
[static] QByteArray QByteArray::number(qlonglong n, int base = 10);
[static] QByteArray QByteArray::number(double n, char f = 'g', int prec = 6);
// QByteArray -> int, short, long, float, double
int QByteArray::toInt(bool *ok = Q_NULLPTR, int base = 10) const;
short QByteArray::toShort(bool *ok = Q_NULLPTR, int base = 10) const;
long QByteArray::toLong(bool *ok = Q_NULLPTR, int base = 10) const;
float QByteArray::toFloat(bool *ok = Q_NULLPTR) const;
double QByteArray::toDouble(bool *ok = Q_NULLPTR) const;
// std::string -> QByteArray
[static] QByteArray QByteArray::fromStdString(const std::string &str);
// QByteArray -> std::string
std::string QByteArray::toStdString() const;
// 所有字符转换为大写
QByteArray QByteArray::toUpper() const;
// 所有字符转换为小写
QByteArray QByteArray::toLower() const;
3.2 QString
QString也是封装了字符串, 但是内部的编码为utf8, UTF-8属于Unicode字符集, 它固定使用多个字节(window为2字节, linux为3字节)来表示一个字符,这样可以将世界上几乎所有语言的常用字符收录其中。
3.2 QString
QString也是封装了字符串, 但是内部的编码为utf8, UTF-8属于Unicode字符集, 它固定使用多个字节(window为2字节, linux为3字节)来表示一个字符,这样可以将世界上几乎所有语言的常用字符收录其中。
下面给大家介绍一下这个类中常用的一些API函数。
构造函数
// 构造一个空字符串对象
QString::QString();
// 将 char* 字符串 转换为 QString 类型
QString::QString(const char *str);
// 将 QByteArray 转换为 QString 类型
QString::QString(const QByteArray &ba);
// 其他重载的同名构造函数可参考Qt帮助文档, 此处略
数据操作
数据操作
// 尾部追加数据
// 其他重载的同名函数可参考Qt帮助文档, 此处略
QString &QString::append(const QString &str);
QString &QString::append(const char *str);
QString &QString::append(const QByteArray &ba);
void QString::push_back(const QString &other);
// 头部添加数据
// 其他重载的同名函数可参考Qt帮助文档, 此处略
QString &QString::prepend(const QString &str);
QString &QString::prepend(const char *str);
QString &QString::prepend(const QByteArray &ba);
void QString::push_front(const QString &other);
// 插入数据, 将 str 插入到字符串第 position 个字符的位置(从0开始)
// 其他重载的同名函数可参考Qt帮助文档, 此处略
QString &QString::insert(int position, const QString &str);
QString &QString::insert(int position, const char *str);
QString &QString::insert(int position, const QByteArray &str);
// 删除数据
// 从大字符串中删除len个字符, 从第pos个字符的位置开始删除
QString &QString::remove(int position, int n);
// 从字符串的尾部删除 n 个字符
void QString::chop(int n);
// 从字节串的 position 位置将字符串截断 (前边部分留下, 后边部分被删除)
void QString::truncate(int position);
// 将对象中的数据清空, 使其为null
void QString::clear();
// 字符串替换
// 将字节数组中的 子字符串 before 替换为 after
// 参数 cs 为是否区分大小写, 默认区分大小写
// 其他重载的同名函数可参考Qt帮助文档, 此处略
QString &QString::replace(const QString &before, const QString &after, Qt::CaseSensitivity cs = Qt::CaseSensitive);
子字符串查找和判断
子字符串查找和判断
// 参数 cs 为是否区分大小写, 默认区分大小写
// 其他重载的同名函数可参考Qt帮助文档, 此处略
// 判断字符串中是否包含子字符串 str, 包含返回true, 否则返回false
bool QString::contains(const QString &str, Qt::CaseSensitivity cs = Qt::CaseSensitive) const;
// 判断字符串是否以字符串 ba 开始, 是返回true, 不是返回false
bool QString::startsWith(const QString &s, Qt::CaseSensitivity cs = Qt::CaseSensitive) const;
// 判断字符串是否以字符串 ba 结尾, 是返回true, 不是返回false
bool QString::endsWith(const QString &s, Qt::CaseSensitivity cs = Qt::CaseSensitive) const;
遍历
// 使用迭代器
iterator QString::begin();
iterator QString::end();
// 使用数组的方式进行遍历
// i的取值范围 0 <= position < size()
const QChar QString::at(int position) const
const QChar QString::operator[](int position) const;
查看字节数
查看字节数
// 返回字节数组对象中字符的个数 (字符个数和字节个数是不同的概念)
int QString::length() const;
int QString::size() const;
int QString::count() const;
// 返回字节串对象中 子字符串 str 出现的次数
// 参数 cs 为是否区分大小写, 默认区分大小写
int QString::count(const QStringRef &str, Qt::CaseSensitivity cs = Qt::CaseSensitive) const;
类型转换
// 将int, short, long, float, double 转换为 QString 类型
// 其他重载的同名函数可参考Qt帮助文档, 此处略
QString &QString::setNum(int n, int base = 10);
QString &QString::setNum(short n, int base = 10);
QString &QString::setNum(long n, int base = 10);
QString &QString::setNum(float n, char format = 'g', int precision = 6);
QString &QString::setNum(double n, char format = 'g', int precision = 6);
[static] QString QString::number(long n, int base = 10);
[static] QString QString::number(int n, int base = 10);
[static] QString QString::number(double n, char format = 'g', int precision = 6);
// 将 QString 转换为 int, short, long, float, double 类型
int QString::toInt(bool *ok = Q_NULLPTR, int base = 10) const;
short QString::toShort(bool *ok = Q_NULLPTR, int base = 10) const;
long QString::toLong(bool *ok = Q_NULLPTR, int base = 10) const
float QString::toFloat(bool *ok = Q_NULLPTR) const;
double QString::toDouble(bool *ok = Q_NULLPTR) const;
// 将标准C++中的 std::string 类型 转换为 QString 类型
[static] QString QString::fromStdString(const std::string &str);
// 将 QString 转换为 标准C++中的 std::string 类型
std::string QString::toStdString() const;
// QString -> QByteArray
// 转换为本地编码, 跟随操作系统
QByteArray QString::toLocal8Bit() const;
// 转换为 Latin-1 编码的字符串 不支持中文
QByteArray QString::toLatin1() const;
// 转换为 utf8 编码格式的字符串 (常用)
QByteArray QString::toUtf8() const;
// 所有字符转换为大写
QString QString::toUpper() const;
// 所有字符转换为小写
QString QString::toLower() const;
字符串格式
字符串格式
// 其他重载的同名函数可参考Qt帮助文档, 此处略
QString QString::arg(const QString &a,
int fieldWidth = 0,
QChar fillChar = QLatin1Char( ’ ’ )) const;
QString QString::arg(int a, int fieldWidth = 0,
int base = 10,
QChar fillChar = QLatin1Char( ’ ’ )) const;
// 示例程序
int i; // 假设该变量表示当前文件的编号
int total; // 假设该变量表示文件的总个数
QString fileName; // 假设该变量表示当前文件的名字
// 使用以上三个变量拼接一个动态字符串
QString status = QString("Processing file %1 of %2: %3")
.arg(i).arg(total).arg(fileName);
4.匿名函数的使用
C++中的匿名函数通常是指使用 lambda 表达式创建的函数。Lambda 表达式允许你在需要时在代码中内联定义和使用匿名函数。Lambda 表达式通常用于传递函数或谓词给标准库算法、STL容器、信号槽连接等情况,它们也可以用于创建临时的、局部的、无需命名的函数。
Lambda 表达式的一般语法如下:
[capture_clause](parameter_list) -> return_type {
// 函数体
}
capture_clause
用于捕获外部变量,可以是空的,也可以通过值、引用或隐式捕获。parameter_list
包含函数的参数列表。return_type
是函数的返回类型。- 函数体包含函数的实际实现。
以下是一个简单的匿名函数(lambda 表达式)的示例:
#include <iostream>
int main() {
int x = 10;
// 使用 lambda 表达式定义匿名函数
auto addOne = [x](int y) -> int {
return x + y;
};
// 调用匿名函数
int result = addOne(5);
std::cout << "Result: " << result << std::endl;
return 0;
}
在这个示例中,我们使用 lambda 表达式创建一个匿名函数 addOne
,它接受一个整数参数 y
,并将 x
和 y
相加。通过 [x]
捕获外部变量 x
,并使用 -> int
指定返回类型。然后,我们调用这个匿名函数,将参数 5 传递给它,得到结果并输出。Lambda 表达式的优势之一是能够轻松地在需要时定义小型的匿名函数,而无需显式命名它们。
5.C++11 遍历容器元素的方法
范围-based for
循环是 C++11 引入的一种用于遍历容器元素的简洁语法。它适用于各种容器类型,包括数组、std::vector
、std::list
、std::set
、std::map
等。下面是一个详细示例,展示如何使用范围-based for
循环以及适用的范围:
#include <iostream>
#include <vector>
#include <set>
#include <map>
#include <array>
int main() {
// 适用于数组
int arr[] = {1, 2, 3, 4, 5};
for (int num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;
// 适用于 std::vector
std::vector<int> vec = {6, 7, 8, 9, 10};
for (int num : vec) {
std::cout << num << " ";
}
std::cout << std::endl;
// 适用于 std::set
std::set<int> numSet = {11, 12, 13, 14, 15};
for (int num : numSet) {
std::cout << num << " ";
}
std::cout << std::endl;
// 适用于 std::map(遍历键值对)
std::map<std::string, int> studentScores = {{"Alice", 95}, {"Bob", 87}, {"Charlie", 92}};
for (const auto& entry : studentScores) {
std::cout << entry.first << ": " << entry.second << std::endl;
}
// 适用于 std::array
std::array<int, 5> stdArray = {16, 17, 18, 19, 20};
for (int num : stdArray) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
在上面的示例中,我们使用范围-based for
循环遍历了不同类型的容器,包括数组、std::vector
、std::set
、std::map
和 std::array
。这个语法非常简洁,不需要显式的迭代器或索引,使代码更易读和维护。
请注意,在遍历 std::map
时,我们使用了一个键值对的循环,这样可以轻松访问键和值。范围-based for
循环可用于大多数 STL 容器,以及用户自定义的容器,只要容器支持范围迭代。
6.auto关键字
auto
关键字用于自动推断变量的数据类型,使代码更加灵活,减少了繁琐的类型声明。它在 C++11 引入,并在以后的 C++ 标准中得到了进一步的支持。以下是一个详细示例,演示了 auto
关键字的使用:
#include <iostream>
#include <vector>
int main() {
// 使用 auto 推断变量类型
auto x = 5; // 推断 x 为整数类型 int
auto name = "John"; // 推断 name 为字符串类型 const char*
// 使用 auto 遍历容器
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (auto num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
// 使用 auto 和范围-based for 循环遍历数组
int arr[] = {6, 7, 8, 9, 10};
for (auto element : arr) {
std::cout << element << " ";
}
std::cout << std::endl;
// 使用 auto 推断复杂类型
std::vector<std::pair<std::string, int>> studentScores = {
{"Alice", 95},
{"Bob", 87},
{"Charlie", 92}
};
for (const auto& entry : studentScores) {
std::cout << entry.first << ": " << entry.second << std::endl;
}
return 0;
}
在上面的示例中,我们使用 auto
关键字来声明变量 x
和 name
,它们的类型分别被自动推断为整数和字符串。然后,我们使用 auto
结合范围-based for
循环来遍历 std::vector
和数组,以及在复杂类型(std::pair
)的容器中遍历键值对。
auto
关键字的使用可以简化代码并提高可读性,特别是在处理复杂数据结构或迭代器时,它可以减少需要手动声明类型的情况。但要注意,过度使用 auto
可能会使代码可读性下降,因此建议在适当的时候使用它。
7.for_each算法
std::for_each
算法用于遍历容器中的元素并应用指定的操作。以下是一个详细示例,演示如何使用 std::for_each
算法以及适用范围:
#include <iostream>
#include <vector>
#include <algorithm>
// 自定义函数,用于在遍历时打印元素
void printNumber(int num) {
std::cout << num << " ";
}
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 使用 std::for_each 遍历容器并应用 printNumber 函数
std::for_each(numbers.begin(), numbers.end(), printNumber);
std::cout << std::endl;
// 使用 Lambda 表达式作为操作
std::for_each(numbers.begin(), numbers.end(), [](int num) {
std::cout << num * 2 << " ";
});
std::cout << std::endl;
// 使用 Lambda 表达式修改容器中的元素
std::for_each(numbers.begin(), numbers.end(), [](int& num) {
num *= 2;
});
// 打印修改后的容器
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
在这个示例中,我们首先使用 std::for_each
算法遍历了一个整数向量 numbers
,并在每次迭代中应用了自定义的 printNumber
函数,以打印每个元素。接着,我们使用 std::for_each
和 Lambda 表达式来遍历容器,打印每个元素的两倍值,并使用另一个 Lambda 表达式修改了容器中的元素,将每个元素翻倍。最后,我们再次遍历容器以查看修改后的值。
std::for_each
算法的适用范围包括任何支持迭代器的容器,如数组、std::vector
、std::list
、std::set
、std::map
等。你可以使用自定义函数或 Lambda 表达式来执行不同的操作,包括打印元素、修改元素、或执行其他自定义操作。这种算法可以提高代码的可读性和可维护性,同时减少重复的循环代码。
8.decltype关键字
decltype
是 C++11 引入的关键字,用于获取表达式的类型,而不进行实际的计算或求值。它通常用于模板编程、泛型编程以及在需要根据表达式的类型来声明变量或返回类型的情况。
decltype
的语法如下:
decltype(expression) variable;
其中 expression
是一个任意的表达式,variable
是使用 decltype
推断出的变量的名称。decltype
的结果是表达式的类型,而不是表达式的值。以下是一些示例来说明 decltype
的用法:
int x = 42;
double y = 3.14;
// 使用 decltype 推断变量的类型
decltype(x) result1 = x; // result1 的类型是 int
decltype(x + y) result2 = x + y; // result2 的类型是 double
// 使用 decltype 推断返回类型
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u;
}
int a = 5;
double b = 3.5;
auto sum = add(a, b); // sum 的类型是 double,因为 add 函数的返回类型由 decltype 推断
// 使用 decltype 推断迭代器类型
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<int>::iterator it = numbers.begin();
decltype(numbers.begin()) newIt = it; // newIt 的类型与 numbers.begin() 的类型相同
在上面的示例中,我们使用 decltype
来推断变量的类型、函数的返回类型以及迭代器的类型。这在泛型编程、模板编程和一些复杂的类型推断场景中非常有用。decltype
可以帮助编写更加通用和灵活的代码,而无需显式指定所有类型。
9.QDesktopWidget包含头文件报错
QT6不支持QDesktopWidget包含头文件报错Qt 获取设备屏幕大小
Qt 获取设备屏幕大小->QDesktopWidget这个类官方介绍说过时了,强烈建议不要使用,可以用QGuiApplication代替。先看下QDesktopWidget类获取设备信息的代码:
//获取设备屏幕大小
2 QDesktopWidget* desktopWidget = QApplication::desktop();
3 QRect screenRect = desktopWidget->screenGeometry();
4 qDebug()<<"screenRect"<<screenRect;
下边是QGuiApplication的方法:
头文件中:#include
1 //获取设备屏幕大小
2 QRect screenRect = QGuiApplication::primaryScreen()->geometry();
3 //获取设备像素比
4 double devicePixelRatio = QGuiApplication::primaryScreen()->devicePixelRatio();
5 int screenW = screenRect.width();
6 int screenH = screenRect.height();
-----------------------------------
Qt 获取设备屏幕大小
10.Qt中的信号槽
10.1信号和槽概述
信号槽是 Qt 框架引以为豪的机制之一。所谓信号槽,实际就是观察者模式(发布-订阅模式)。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,将想要处理的信号和自己的一个函数(称为槽(slot))绑定来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。
10.2 信号和槽的关系
在Qt中信号和槽函数都是独立的个体,本身没有任何联系,但是由于某种特性需求我们可以将二者连接到一起,好比牛郎和织女想要相会必须要有喜鹊为他们搭桥一样。在Qt中我们需要使用QOjbect类中的connect函数进二者的关联。
连接信号和槽的connect()函数原型如下, 其中PointerToMemberFunction是一个指向函数地址的指针
QMetaObject::Connection QObject::connect(
const QObject *sender, PointerToMemberFunction signal,
const QObject *receiver, PointerToMemberFunction method,
Qt::ConnectionType type = Qt::AutoConnection);
参数:
- sender: 发出信号的对象
- signal: 属于sender对象, 信号是一个函数, 这个参数的类型是函数
指针, 信号函数地址
- receiver: 信号接收者
- method: 属于receiver对象, 当检测到sender发出了signal信号,
receiver对象调用method方法,信号发出之后的处理动作
// 参数 signal 和 method 都是函数地址, 因此简化之后的 connect() 如下:
connect(const QObject *sender, &QObject::signal,
const QObject *receiver, &QObject::method);
使用connect()进行信号槽连接的注意事项:
- connect函数相对于做了信号处理动作的注册
调用conenct函数的sender对象的信号并没有产生, 因此receiver对象的method也不会被调用
method槽函数本质是一个回调函数, 调用的时机是信号产生之后, 调用是Qt框架来执行的
connect中的sender和recever两个指针必须被实例化了, 否则conenct不会成功
10.3自定义信号槽使用
Qt框架提供的信号槽在某些特定场景下是无法满足我们的项目需求的,因此我们还设计自己需要的的信号和槽,同样还是使用connect()对自定义的信号槽进行连接。
如果想要在QT类中自定义信号槽, 需要满足一些条件, 并且有些事项也需要注意:
要编写新的类并且让其继承Qt的某些标准类
这个新的子类必须从QObject类或者是QObject子类进行派生
在定义类的头文件中加入 Q_OBJECT 宏
// 举例: 信号重载
// Qt中的类想要使用信号槽机制必须要从QObject类派生(直接或间接派生都可以)
class Test : public QObject
{
Q_OBJECT
signals:
void testsignal();
// 参数的作用是数据传递, 谁调用信号函数谁就指定实参
// 实参最终会被传递给槽函数
void testsignal(int a);
};
10.3.1自定义信号
在Qt中信号的本质是事件, 但是在框架中也是以函数的形式存在的, 只不过信号对应的函数只有声明, 没有定义。如果Qt中的标准信号不能满足我们的需求,可以在程序中进行信号的自定义,当自定义信号对应的事件产生之后,认为的将这个信号发射出去即可(其实就是调用一下这个信号函数)。
下边给大家阐述一下, 自定义信号的要求和注意事项:
信号是类的成员函数
返回值必须是 void 类型
信号的名字可以根据实际情况进行指定
参数可以随意指定, 信号也支持重载
信号需要使用 signals 关键字进行声明, 使用方法类似于public等关键字
信号函数只需要声明, 不需要定义(没有函数体实现)
在程序中发射自定义信号: 发送信号的本质就是调用信号函数
习惯性在信号函数前加关键字: emit, 但是可以省略不写
emit只是显示的声明一下信号要被发射了, 没有特殊含义
底层 emit == #define emit
// 举例: 信号重载
// Qt中的类想要使用信号槽机制必须要从QObject类派生(直接或间接派生都可以)
class Test : public QObject
{
Q_OBJECT
signals:
void testsignal();
// 参数的作用是数据传递, 谁调用信号函数谁就指定实参
// 实参最终会被传递给槽函数
void testsignal(int a);
};
10.3.2自定义槽
槽函数就是信号的处理动作,在Qt中槽函数可以作为普通的成员函数来使用。如果标准槽函数提供的功能满足不了需求,可以自己定义槽函数进行某些特殊功能的实现。自定义槽函数和自定义的普通函数写法是一样的。
下边给大家阐述一下, 自定义槽的要求和注意事项:
返回值必须是 void 类型
槽也是函数, 因此也支持重载
槽函数需要指定多少个参数, 需要看连接的信号的参数个数
槽函数的参数是用来接收信号传递的数据的, 信号传递的数据就是信号的参数
举例:
信号函数: void testsig(int a, double b);
槽函数: void testslot(int a, double b);
总结:
槽函数的参数应该和对应的信号的参数个数, 从左到右类型依次对应
信号的参数可以大于等于槽函数的参数个数 == 信号传递的数据被忽略了
信号函数: void testsig(int a, double b);
槽函数: void testslot(int a);
Qt中槽函数的类型是多样的
Qt中的槽函数可以是类的成员函数、全局函数、静态函数、Lambda表达式(匿名函数)
槽函数可以使用关键字进行声明: slots (Qt5中slots可以省略不写)
public slots:
private slots: –> 这样的槽函数不能在类外部被调用
protected slots: –> 这样的槽函数不能在类外部被调用
// 槽函数书写格式举例
// 类中的这三个函数都可以作为槽函数来使用
class Test : public QObject
{
public:
void testSlot();
static void testFunc();
public slots:
void testSlot(int id);
};
根据特定场景自定义信号槽:
还是上边的场景:
女朋友说:“我肚子饿了!”,于是我带她去吃饭。
// class GirlFriend
class GirlFriend : public QObject
{
Q_OBJECT
public:
explicit GirlFriend(QObject *parent = nullptr);
signals:
void hungry(); // 不能表达出想要吃什么
void hungry(QString msg); // 可以通过参数表达想要吃什么
};
// class Me
class Me : public QObject
{
Q_OBJECT
public:
explicit Me(QObject *parent = nullptr);
public slots:
// 槽函数
void eatMeal(); // 不能知道信号发出者要吃什么
void eatMeal(QString msg); // 可以知道信号发出者要吃什么
};
11.Qt中按钮上设置图标
方式一、setIcon()
方式二、setDefaultAction();
方式三、setArrowType();
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QBoxLayout>
#include <QDebug>
#include <QScreen>
#include <qrect.h>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
// // 设置窗口位置为屏幕左上角
// this ->move(0, 0);
// // 获取屏幕的宽度和高度
// int screenWidth = QApplication::desktop()->width();
// int screenHeight = QApplication::desktop()->height();
// 获取主屏幕对象
QScreen *screen = QGuiApplication::primaryScreen();
QRect screenGeometry = screen->geometry();
// 计算窗口的左上角坐标,使其位于屏幕正中间
int windowWidth = width();
int windowHeight = height();
int x = (screenGeometry.width() - windowWidth) / 2;
int y = (screenGeometry.height() - windowHeight) / 2;
// 使用 move() 函数将窗口移动到计算出的坐标位置
move(x, y);
// 其他初始化代码...
// 设置窗口的固定大小
setFixedSize(800, 600);
// 创建布局管理器,并将窗口中的控件添加到布局中
QBoxLayout *layout = new QBoxLayout(QBoxLayout::TopToBottom, this);
layout->addWidget(ui->btn_normal);
layout->addWidget(ui->btn_checked);
layout->addWidget(ui->btn_menu);
// 设置布局管理器,使其自适应窗口
setLayout(layout);
//普通按钮
ui->btn_normal->setText("我是一个普通按钮");
ui->btn_normal->setIcon(QIcon(":/OnePiece.png"));
ui->btn_normal->setIconSize(QSize(50,50));
connect(ui->btn_normal,&QToolButton::clicked,this,[=](){
qDebug() << "我是一个普通的小按钮";
});
ui->btn_normal->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
//创建一个QAction对象
QAction *actionButton = new QAction(QIcon(":/LuffyQ.png"),"路飞");
ui->btn_action->setDefaultAction(actionButton);
connect(ui->btn_action,&QToolButton::triggered,this,[=](QAction *act){
act->setText("我是修改之后的骷髅");
act->setIcon(QIcon(":/OnePiece.png"));
});
ui->btn_action->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
//带有箭头的按钮
ui->btn_arrow->setArrowType(Qt::DownArrow);
ui->btn_arrow->setText("向下");
ui->btn_arrow->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
//有checked属性的按钮
ui->btn_checked->setCheckable(true);
connect(ui->btn_checked,&QToolButton::toggled,[=](bool status){
qDebug() << "我是一个有check属性的按钮" << "当前状态为" << status;
});
//关联菜单按钮
//让当前的按钮和菜单进行关联
ui->btn_menu->setText("你所喜欢的水果类型");
QMenu *menu = new QMenu();
//调用addAction方法会得到QAction类型的实例指针对象
QAction *bolo = menu->addAction("菠萝");
menu->addAction("香蕉");
menu->addAction("杨桃");
ui->btn_menu->setMenu(menu);
connect(bolo,&QAction::triggered,[=](){
qDebug() << "菠萝是我i最喜欢吃的水果:";
});
}
MainWindow::~MainWindow()
{
delete ui;
}
12、Qt中线程的使用
程序功能:生成一串随机数,并在控件中显示,再分别通过冒泡排序和快速排序进行显示
第一种方法
让子类继承QThread类,然后重写QThread类的run()方法
具体的实现方式参见:D:\Qt\2th\QThread
一、生成随机数
//generate.h
#ifndef GENERATE_H
#define GENERATE_H
#include <QVector>
#include <QDebug>
#include <QObject>
#include <QThread>
#include <QRandomGenerator>
//生成随机数
class Generate : public QThread
{
Q_OBJECT
public:
explicit Generate(QObject *parent = nullptr);
void recvNum(int num);
protected:
void run() override;
signals:
//发射随机数生成的信号
void sendArray(QVector<int> num);
private:
int m_num;
};
#endif // GENERATE_H
xxxxxxxxxx //generate.cpp#include "generate.h" G enerate::Generate(QObject *parent) : QThread{parent}{}void Generate::recvNum(int num){ m_num = num;}void Generate::run(){ qDebug()<<"生成随机数的线程的线程地址"<<QThread::currentThread(); QVector<int> list; QElapsedTimer time; time.start();//开始计时 for(int i= 0;i<m_num;i++) { list.push_back(QRandomGenerator::global()->bounded(1,10000)); } int msec = time.elapsed();//返回执行这个流程所消耗的时间 qDebug()<<"生成"<<m_num<<"个随机数共用时"<<msec<<"毫秒"; emit sendArray(list);}c++
二、冒泡排序
//头文件 bubblesort.h
#ifndef BUBBLESORT_H
#define BUBBLESORT_H
#include <QDebug>
#include <QObject>
#include <QThread>
class BubbleSort : public QThread
{
Q_OBJECT
public:
explicit BubbleSort(QObject *parent = nullptr);
void readyForSort(QVector<int> list);
signals:
//发送排序完成后的数据
void finishSort(QVector<int> list);
protected:
void run() override;
private:
//接收需要排序的Vector类型的数
QVector<int> m_list;
};
#endif // BUBBLESORT_H
//实现文件
#include "bubblesort.h"
BubbleSort::BubbleSort(QObject *parent)
: QThread{parent}
{
}
void BubbleSort::readyForSort(QVector<int> list)
{
m_list = list;
}
void BubbleSort::run()
{
qDebug()<<"随机数冒泡排序的线程的线程地址"<<QThread::currentThread();
QElapsedTimer time;
time.start();//开始计时
//冒泡排序算法
int temp;
for(int i = 0;i < m_list.size();++i)
{
for(int j = 0;j<m_list.size()-i-1;++j)
{
if(m_list[j] > m_list[j+1])
{
temp = m_list[j];
m_list[j]= m_list[j+1];
m_list[j+1]= temp;
}
}
}
int bubble_msec = time.elapsed();//返回执行这个流程所消耗的时间
qDebug()<<"冒泡排序用时"<<bubble_msec<<"毫秒";
emit finishSort(m_list);
}
三、快速排序
//quicksort.h
#ifndef QUICKSORT_H
#define QUICKSORT_H
#include <QThread>
#include <QObject>
#include <QDebug>
class QuickSort:public QThread
{
Q_OBJECT
public:
QuickSort(QObject *parent = nullptr);
void readyForSort(QVector<int> list);
signals:
//发送排序完成后的数据
void finishSort(QVector<int> list);
protected:
void run() override;
private:
void quickSort(QVector<int> &s, int l, int r);
private:
//接收需要排序的Vector类型的数
QVector<int> m_list;
};
#endif // QUICKSORT_H
//quicksort.cpp
#include "quicksort.h"
QuickSort::QuickSort(QObject *parent)
:QThread{parent}
{
}
void QuickSort::readyForSort(QVector<int> list)
{
m_list = list;
}
void QuickSort::run()
{
qDebug() << "随机数快速排序的线程的线程地址: " << QThread::currentThread();
QElapsedTimer time;
time.start();
quickSort(m_list, 0, m_list.size()-1);
int milsec = time.elapsed();
qDebug() << "快速排序用时" << milsec << "毫秒";
emit finishSort(m_list);
}
void QuickSort::quickSort(QVector<int> &s, int l, int r)
{
if (l < r)
{
int i = l, j = r;
// 拿出第一个元素, 保存到x中,第一个位置成为一个坑
int x = s[l];
while (i < j)
{
// 从右向左找小于x的数
while (i < j && s[j] >= x)
{
//左移, 直到遇到小于等于x的数
j--;
}
if (i < j)
{
//将右侧找到的小于x的元素放入左侧坑中, 右侧出现一个坑
//左侧元素索引后移
s[i++] = s[j];
}
// 从左向右找大于等于x的数
while (i < j && s[i] < x)
{
//右移, 直到遇到大于x的数
i++;
}
if (i < j)
{
//将左侧找到的元素放入右侧坑中, 左侧出现一个坑
//右侧元素索引向前移动
s[j--] = s[i];
}
}
//此时 i=j,将保存在x中的数填入坑中
s[i] = x;
quickSort(s, l, i - 1); // 递归调用
quickSort(s, i + 1, r);
}
}
四、MainWindow中的主要操作
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "generate.h"
#include "bubblesort.h"
#include "quicksort.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
//1、创建子线程对象
Generate *gen = new Generate;
BubbleSort *bubble = new BubbleSort;
QuickSort *quick = new QuickSort;
connect(this,&MainWindow::setRandomNum,gen,&Generate::recvNum);
//2、启动子线程
connect(ui->btn_start,&QPushButton::clicked,this,[=](){
//告诉子线程要生成随机数的个数
emit setRandomNum(10000);
gen->start();
bubble->start();
quick->start();
});
//在生成随机数后,再将这些随机数分别用冒泡和快排进行排序
connect(gen,&Generate::sendArray,bubble,&BubbleSort::readyForSort);
connect(gen,&Generate::sendArray,quick,&QuickSort::readyForSort);
//3、主线程接收子线程发送的数据
connect(gen,&Generate::sendArray,this,[=](QVector<int> list){
// for(auto num:list)
// {
// ui->randList->addItem(QString::number(list.at(num)));
// }
for(int i=0 ;i<list.size();++i)
{
ui->randList->addItem(QString::number(list.at(i)));
}
});
//冒泡排序接受子线程发送的数据
connect(bubble,&BubbleSort::finishSort,this,[=](QVector<int> list){
// for(auto bubble_num:list)
// {
// ui->bubbleList->addItem(QString::number(list.at(bubble_num)));
// }
for(int i=0 ;i<list.size();++ i)
{
ui->bubbleList->addItem(QString::number(list.at(i)));
}
});
//快速排序接受子线程发送的数据
connect(quick,&QuickSort::finishSort,this,[=](QVector<int> list){
// for(auto quick_num:list)
// {
// ui->quickList->addItem(QString::number(list.at(quick_num)));
// }
for(int i=0 ;i<list.size();++ i)
{
ui->quickList->addItem(QString::number(list.at(i)));
}
});
}
MainWindow::~MainWindow()
{
delete ui;
}
第二种方法
让执行任务的类继承QObject类,然后再主程序函数中创建线程类对象和任务类对象
这种方法的好处,比较直观,代码简洁
任务类对象->moveToThread(线程类对象);
具体的实现方式参见:D:\Qt\2th\QThread_2override
第三种方法
使用线程池,让任务类继承QObject类和QRunnable()类,(注意继承的先后顺序),让线程池统一管理线程对象
这种方法的好处,代码简洁。线程池管理线程可以避免浪费线程,节省资源
具体的实现方式参见:D:\Qt\2th\QThread\Qthreadpool
可以通过下面的方法设置是否需要自动析构资源
setAutoDelete(true);
13、Qt网络编程
套接字-Socket | 爱编程的大丙 (subingwen.cn)
13.1服务器端
-
第一步
QTcpSever *m_sever = new QTcpServer(this);
在创建新的连接时,发出newConnection()信号[signal] void QTcpServer::newConnection();
-
第二步
给监听的套接字设置监听
bool QTcpServer::listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0); // 判断当前对象是否在监听, 是返回true,没有监听返回false bool QTcpServer::isListening() const;
-
第三步
得到和客户端建立连接之后用于通信的QTcpSocket套接字对象,它是QTcpServer的一个子对象,当QTcpServer对象析构的时候会自动析构这个子对象,当然也可自己手动析构,建议用完之后自己手动析构这个通信的QTcpSocket对象 QTcpSocket *QTcpServer::nextPendingConnection();
并在随后初始化QTcpSocket的实例化对象 m_tcp = m_sever->nextPendingConnection();
-
第四步
下面是QTcpSocket类的相关信号
在使用QTcpSocket进行套接字通信的过程中,如果该类对象发射出readyRead()信号,说明对端发送的数据达到了,之后就可以调用 read 函数接收数据了。
[signal] void QIODevice::readyRead();
调用connectToHost()函数并成功建立连接之后发出connected()信号。
[signal] void QAbstractSocket::connected();
在套接字断开连接时发出disconnected()信号。
[signal] void QAbstractSocket::disconnected();
//QtTcpSever
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
this->setWindowTitle("服务器端:");
ui->port->setText("8989");
m_sever = new QTcpServer(this);
connect(m_sever,&QTcpServer::newConnection,this,[=](){
m_tcp = m_sever->nextPendingConnection();
m_status->setPixmap(QPixmap(":/connected.png").scaled(30,30));
connect(m_tcp,&QTcpSocket::readyRead,this,[=](){
QByteArray data = m_tcp->readAll();
ui->historyMsg->append("客户端说:" + data);
});
connect(m_tcp,&QTcpSocket::disconnected,this,[=](){
m_tcp->close();
m_tcp->deleteLater();
m_status->setPixmap(QPixmap(":/disconnected.png").scaled(30,30));
ui->historyMsg->append("客户端和服务器断开了连接");
});
});
//更改状态栏
m_status = new QLabel;
ui->statusbar->addWidget(new QLabel("连接状态"));
m_status->setPixmap(QPixmap(":/disconnected.png").scaled(30,30));
ui->statusbar->addWidget(m_status);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_btn_listen_clicked()
{
qDebug() << "这样真的可以";
QString port = ui->port->text();
m_sever->listen(QHostAddress::Any,port.toUShort());
ui->btn_listen->setDisabled(true);//按钮设置为不可用状态
}
void MainWindow::on_btn_start_clicked()
{
QString data = ui->msg->toPlainText();
m_tcp->write(data.toUtf8());
//ui->msg->append(data);
ui->historyMsg->append("服务器说" + data);
}
13.2客户端
-
第一步
初始化套接字对象QTcpSocket *m_tcp = new QTcpSocket(this);
-
第二步
调用connectToHost()函数并成功建立连接之后发出connected()信号。
-
第三步
使用 [signal] void QIODevice::readyRead();检测对方发送的数据是否到达
-
第四步
判断和服务器的连接状态
[signal] void QAbstractSocket::connected();
在套接字断开连接时发出disconnected()信号。
[signal] void QAbstractSocket::disconnected();
//QtTcpClient
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
this->setWindowTitle("客户端");
ui->port->setText("8989");
ui->ip->setText("127.0.0.1");
m_tcp = new QTcpSocket(this);
connect(m_tcp,&QTcpSocket::readyRead,[=](){
QByteArray data = m_tcp->readAll();
ui->historyMsg->append("服务器说:" + data);
});
connect(m_tcp,&QTcpSocket::connected,[=](){
m_status->setPixmap(QPixmap(":/connected.png").scaled(30,30));
ui->historyMsg->append("客户端和服务器成功建立连接");
ui->btn_disconnect->setDisabled(false);
ui->btn_connect->setDisabled(true);
});
connect(m_tcp,&QTcpSocket::disconnected,[=](){
m_tcp->close();
//由于刚开始实例化m_tcp是设置了父对象,资源释放时会自动析构,不需要再delete
m_status->setPixmap(QPixmap(":/disconnected.png").scaled(30,30));
ui->historyMsg->append("服务器和客户端断开了连接");
ui->btn_disconnect->setDisabled(true);
ui->btn_connect->setDisabled(false);
});
//更改状态栏
m_status = new QLabel(this);
ui->statusbar->addWidget(new QLabel("连接状态:"));
m_status->setPixmap(QPixmap(":/disconnected.png").scaled(30,30));
ui->statusbar->addWidget(m_status);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_btn_connect_clicked()
{
unsigned short port = ui->port->text().toShort();
QString ip = ui->ip->text();
m_tcp->connectToHost(QHostAddress(ip),port);
}
void MainWindow::on_btn_disconnect_clicked()
{
m_tcp->close();
ui->btn_disconnect->setDisabled(true);
ui->btn_connect->setDisabled(false);
}
void MainWindow::on_btn_send_clicked()
{
QString msg = ui->msg->toPlainText();
m_tcp->write(msg.toUtf8());
//ui->msg->append(msg);
ui->historyMsg->append("客户端说" + msg);
}
14、TCP粘包问题
1.服务器端
D:\Qt\2th\tcpPacketSticking\TcpSever
#ifndef MYTCPSEVER_H
#define MYTCPSEVER_H
#include <QTcpServer>
#include <QObject>
//在当前的类中重写inComingConnection()
//[virtual protected] void QTcpServer::incomingConnection(qintptr socketDescriptor)
class MyTcpSever : public QTcpServer
{
Q_OBJECT
public:
explicit MyTcpSever(QObject *parent = nullptr);
//重写虚函数
void incomingConnection(qintptr socketDescriptor) override;
signals:
void newConnect(qintptr socket);
};
#endif // MYTCPSEVER_H
#include "mytcpsever.h"
MyTcpSever::MyTcpSever(QObject *parent)
: QTcpServer{parent}
{
}
void MyTcpSever::incomingConnection(qintptr socketDescriptor)
{
emit newConnect(socketDescriptor);
}
2.客户端
D:\Qt\2th\tcpPacketSticking\TcpClient
15、 new一个新对象
C++ 中 new 一个新对象,变量类型可以是指针,也可以是引用。
1. 使用指针类型
这是最常见的方式。使用指针类型,需要使用 *
运算符来访问对象的成员变量和成员函数。
C++
// 使用指针类型
Person* person = new Person();
// 访问成员变量
int age = person->age;
// 调用成员函数
person->sayHello();
2. 使用引用类型
C++11 引入了引用类型。使用引用类型,可以直接访问对象的成员变量和成员函数,无需使用 *
运算符。
C++
// 使用引用类型
Person& person = new Person();
// 访问成员变量
int age = person.age;
// 调用成员函数
person.sayHello();
3. 比较
使用指针类型和引用类型各有优缺点。
- 指针类型的优点是灵活性强,可以指向任何类型的对象。缺点是需要使用
*
运算符来访问对象的成员变量和成员函数,比较繁琐。 - 引用类型的优点是简洁易用,可以直接访问对象的成员变量和成员函数。缺点是只能引用同一类型的对象,灵活性较差。
4. 结论
C++ 中 new 一个新对象,变量类型可以是指针,也可以是引用。具体选择哪种类型,可以根据实际情况进行权衡。
以下是一些使用引用类型的示例:
C++
// 使用引用类型创建数组
int* arr = new int[10];
// 使用引用类型创建结构体
struct Point {
int x;
int y;
};
Point& point = new Point();
// 使用引用类型创建对象
class Person {
public:
Person() {}
~Person() {}
};
Person& person = new Person();
在这些示例中,arr
、point
和 person
都是引用类型。
16、 相关数据类型
// 无符号8位整数类型
typedef unsigned char uint8_t;
// 无符号16位整数类型
typedef unsigned short uint16_t;
// 无符号32位整数类型
typedef unsigned int uint32_t;
// 有符号64位整数类型
typedef long long int64_t;
// 无符号64位整数类型
typedef unsigned long long uint64_t;
在Java中,没有原生的无符号64位整数类型。如果需要使用无符号64位整数,可以使用Java的BigInteger类来存储和操作无符号整数。BigInteger类是一个不可变的任意精度整数,可以存储任意大小的整数,包括超出long范围的整数。
在C++中,可以使用uint64_t
类型来表示无符号64位整型。uint64_t
类型是C++标准中定义的无符号64位整数类型,其值范围为0到2^64-1。
BigInteger bigInteger = new BigInteger("12345678901234567890");
uint64_t value = 12345678901234567890;
17、网络编程
1、基础
在网络环境中,要去识别一台主机,需要知道ip;在网络环境中,要去识别一个进程,需要知道ip + port.
2、字节序
大端:低地址存高位,高地址存低位;
小端:低地址存低位,高地址存高位(x86采用小端存储) mac arm/x86 交叉编译
x86采用小端存储,但是数据在网络传输过程中采用的是大端法。
0x12345678
#include <arpa/inet.h>
//网络字节序与本机字节序的转换
uint32_t htonl(uint32_t hostlong);//h = host n = network l = long s = short
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
3、网络编程原理图
4、常用结构体
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
};
struct sockaddr_in
{
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
struct in_addr
{
uint32_t s_addr; /* address in network byte order */
};
typedef uint32_t in_addr_t;
5、网络编程常用函数
1、创建网络套接字函数socket
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
//domain:协议族,常用的三个AF_INET/AF_INET6/AF_UNIX,
//type:协议类型,SOCK_STREAM(TCP)/SOCK_DGRAM(UDP)
//protocol:默认情况就传0,代表使用的是tcp或者udp的默认协议
//返回值,如果成功的话,会返回一个文件描述符fd,如果失败的话,那么会返回-1.
文件描述符可以看成是一个句柄、门把手、钥匙。
2、bind函数
绑定服务器的ip与端口号。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//sockfd:就是上面socket函数的返回值。
//addr:传递包含ip与端口号的结构体。(服务器的ip与端口号)
//socklen_t:结构体的大小
//返回值:如果正确的话,会返回0;否则会返回-1.
3、listen函数
让服务器处于监听状态。允许同时多少个客户端与服务器建立连接
listen - listen for connections on a socket
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
//sockfd:就是上面socket函数的返回值。
//backlog:允许同时多少个客户端与服务器建立连接,默认128。
//返回值:如果正确的话,会返回0;否则会返回-1.
4、accept函数
接收连接请求的函数,阻塞等待客户端发起连接。
accept, accept4 - accept a connection on a socket
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//sockfd:就是上面socket函数的返回值。
//addr:也是地址,但是在此处是将客户端的ip与端口号存放在这个addr中。
//addrlen:是addr对应的结构体的长度。
//返回值:成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1
5、close函数
关闭文件描述符。
#include <unistd.h>
int close(int fd);
//在服务器端会关闭两个文件描述符,一个是socket的返回值,一个是accept的返回值。
6、connect函数
客户端调用该函数,连接到服务器上。主动发起连接
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//sockfd:就是上面socket函数的返回值。
//addr:该结构中包装的是服务器的ip与服务器的端口号。
//addrlen:结构体的长度。
//返回值:如果正确的话,会返回0;否则会返回-1.
7、read/recv函数
从对应的文件描述符fd中读取数据到buf中,读的长度是count。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
//如果recv中的flags为0的话,那么read与recv是等价的。
8、write/send函数
将buf中的count个字节写到fd对应的文件描述符中
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
//如果send中的flags为0的话,那么send与write是等价的。
18、C++ 中 main函数
C++ 中 int main(int argc, char **argv)
的格式是固定的,它是 C++ 程序的标准入口点,用于接收命令行参数。
参数详解:
argc
: 命令行参数的个数,第一个参数是程序本身的名称,其余参数是用户输入的参数。argv
: 一个指向字符指针数组的指针,每个元素指向一个命令行参数字符串。例如,如果用户运行程序并输入以下命令:
myprogram argument1 argument2
那么 argc
将为 3,argv[0]
将指向 "myprogram"
字符串,argv[1]
将指向 "argument1"
字符串,argv[2]
将指向 "argument2"
字符串。
固定格式的原因:
int main(int argc, char **argv)
格式是 C 语言的标准,C++ 继承了 C 语言的这一格式,以保持兼容性。- 该格式为程序提供了一种标准的方式来接收和处理命令行参数,这对于许多应用程序来说都是非常有用的功能。
- 固定格式使得编译器和链接器能够轻松识别程序的入口点,并正确链接程序。
修改入口点格式:
虽然 int main(int argc, char **argv)
格式是固定的,但并非绝对不可修改。在某些特殊情况下,您可以通过以下方法修改入口点格式:
- 使用预处理器宏:例如,您可以使用
#define main mymain
宏来将main
函数重命名为mymain
。然后,您可以编写mymain
函数,该函数具有与main
相同的签名和功能。 - 使用平台特定的 API:一些平台提供 API 来指定程序的入口点。例如,在 Windows 中,您可以使用
WinMain
函数作为程序的入口点。
但是,修改入口点格式通常是不建议的,因为它会破坏 C++ 程序的标准兼容性,并可能导致一些意想不到的问题。
总结:
int main(int argc, char **argv)
是 C++ 程序的标准入口点格式,用于接收命令行参数。- 该格式是固定的,但并非绝对不可修改。
- 修改入口点格式通常是不建议的,因为它会破坏 C++ 程序的标准兼容性。
希望以上解释对您有所帮助。如果您还有其他问题,请随时提出。
19、UDP通信中的问题
recvfrom()
和sendto()
函数是用于UDP套接字的数据收发函数,它们的用法如下:
recvfrom()
函数
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
-
参数说明:
sockfd
:UDP套接字描述符。buf
:接收数据的缓冲区。len
:缓冲区的大小。flags
:接收标志,通常为0。src_addr
:指向存放数据发送方地址信息的结构体的指针。addrlen
:指向src_addr
结构体的长度。
-
返回值:返回接收到的字节数,如果出错返回-1。
sendto()
函数
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
-
参数说明:
sockfd
:UDP套接字描述符。buf
:待发送的数据。len
:待发送数据的长度。flags
:发送标志,通常为0。dest_addr
:指向目标地址信息的结构体的指针。addrlen
:目标地址信息结构体的长度。
-
返回值:返回发送的字节数,如果出错返回-1。
示例用法
服务端接收消息:
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
ssize_t bytes_received = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&client_addr, &addr_len);
if (bytes_received < 0) {
perror("recvfrom");
// 处理接收错误
} else {
// 处理接收到的数据
}
服务端发送消息:
char message[] = "Hello, client!";
ssize_t bytes_sent = sendto(sockfd, message, sizeof(message), 0, (struct sockaddr*)&client_addr, addr_len);
if (bytes_sent < 0) {
perror("sendto");
// 处理发送错误
} else {
// 发送成功
}
客户端接收消息:
char buffer[1024];
struct sockaddr_in server_addr;
socklen_t addr_len = sizeof(server_addr);
ssize_t bytes_received = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&server_addr, &addr_len);
if (bytes_received < 0) {
perror("recvfrom");
// 处理接收错误
} else {
// 处理接收到的数据
}
客户端发送消息:
char message[] = "Hello, server!";
ssize_t bytes_sent = sendto(sockfd, message, sizeof(message), 0, (struct sockaddr*)&server_addr, addr_len);
if (bytes_sent < 0) {
perror("sendto");
// 处理发送错误
} else {
// 发送成功
}
这些函数允许你在UDP套接字上收发数据,并指定目标地址和发送方地址。
20、C++中多态的例子(以Animal类举例)
在这个例子中,虽然我们使用相同的 pAnimal
指针调用了 eat()
和 sleep()
函数,但实际执行的是由 Cat
类或 Dog
类重写的函数,这体现了多态性。
多态性的优点在于,它使得代码更加灵活和可扩展。我们可以根据不同的情况,创建不同类型的动物对象,并使用相同的基类指针调用它们的行为方法,而无需关心具体的对象类型。这使得代码更加简洁易懂,并且易于维护。
在实际应用中,多态性被广泛应用于各种软件系统中,例如图形界面、网络通信等。它是一种非常强大的面向对象编程特性,可以帮助我们开发更加灵活、可维护的代码。
#include <iostream>
#include <string>
using std::cout;
using std::endl;
class Animal
{
private:
/* data */
public:
// 通用属性
// 通用方法
virtual void eat() = 0; // 虚函数:需要在派生类中实现
virtual void sleep() = 0; // 虚函数:需要在派生类中实现
public:
std::string name;
int age;
};
class Dog :public Animal
{
private:
// 特有方法
void bark(){
std::cout << "汪汪汪" << std::endl;
}
// 特有属性
std::string breed;
private:
void eat() override{
std::cout << "狗在吃骨头" << std::endl;
}
void sleep() override{
std::cout << "狗趴在地上睡觉" << std::endl;
}
};
class Cat :public Animal
{
public:
// 特有属性
bool hasFur;
// 特有方法
void meow() {
std::cout << "喵喵喵" << std::endl;
}
private:
void eat() override{
std::cout << "猫在吃鱼" << std::endl;
}
void sleep() override{
std::cout << "猫在蜷缩着睡觉" << std::endl;
}
};
int main(){
Animal *pAnimal;
pAnimal = new Cat();
pAnimal->eat(); // 输出:猫在吃鱼
pAnimal->sleep(); // 输出:猫在蜷缩着睡觉
pAnimal = new Dog();
pAnimal->eat(); // 输出:狗在吃骨头
pAnimal->sleep(); // 输出:狗趴在地上睡觉
}
21、C++中多线程代码
C++ 多线程类库示例代码分析
代码概述
该代码提供了一个名为 Thread
的线程类,用于简化 C++ 多线程编程。该类封装了 pthread
函数库的底层接口,提供以下功能:
- 创建线程
- 启动线程
- 等待线程结束
- 线程分离
此外,代码还提供了一个名为 MyThread
的派生类,用于演示如何使用 Thread
类创建和运行线程。
代码分析
1. Thread 类
-
Thread
类包含以下成员:
_thid
: 线程 ID_isRunning
: 线程运行状态标志run()
: 线程入口函数,虚函数,需要在派生类中实现
-
Thread
类提供以下成员函数:
start()
: 启动线程join()
: 等待线程结束threadFunc()
: 静态函数,作为线程入口函数的包装
-
threadFunc()
函数负责将run()
函数的调用与线程的执行关联起来。
2. MyThread 类
MyThread
类继承自Thread
类,并重写了run()
函数。run()
函数中包含具体的线程执行逻辑,例如打印信息、睡眠等。
3. main() 函数
- 创建一个
MyThread
对象 - 使用
start()
函数启动线程 - 使用
join()
函数等待线程结束
代码功能
该代码的主要功能是演示如何使用 Thread
类创建和运行线程。具体来说,它实现了以下功能:
- 创建一个名为
MyThread
的线程 - 启动
MyThread
线程 - 等待
MyThread
线程结束 - 在
MyThread
线程中打印信息
改进建议
#ifndef _THREAD_H
#define _THREAD_H
#include <iostream>
#include <pthread.h>
#include <stdio.h>
class Thread {
public:
Thread();
virtual ~Thread();
void start();
void join();
private:
pthread_t _thid;//线程id
bool _isRunning;
/**
* 线程入口函数
* @param void *
*/
static void *threadFunc(void *);//为了消除this指针
//现成的执行任务
virtual void run() = 0;
};
#endif //_THREAD_H
#include "Thread.h"
/**
* Thread implementation
*/
Thread::Thread()
:_thid(0)
,_isRunning(false)
{
}
Thread::~Thread() {
if(_isRunning){
//让子线程与主线程分离,主线程不负责子线程的回收
pthread_detach(_thid);
_isRunning = false;
}
}
/**
* @return void
*/
void Thread::start() {
int ret = pthread_create(&_thid,nullptr,threadFunc,this);//第四个参数把this指针交给threadFunc函数
if(ret){
perror("pthread_create\n");
return;
}
_isRunning = true;
}
/**
* @return void
*/
void Thread::join() {
if(_isRunning){
int ret = pthread_join(_thid,nullptr);
if(ret){
perror("pthread_join\n");
return;
}
_isRunning = true;
}
}
/**
* @param void *
* @return void *
*/
void * Thread::threadFunc(void *arg) {
/**
* 因为threadFunc()已经是静态函数了,没有this指针
* 需要通过参数argc把Thread类的this指针传递进来,这样的话Thread类对象就可以正常调用run()方法
*/
Thread *pthread = static_cast<Thread *>(arg);
if(pthread){
pthread->run();//这里体现出多态(基类的指针指向派生类对象,执行派生类的纯虚函数run())
}
pthread_exit(nullptr);//让子线程主动退出
}
/**
* @return void
*/
void Thread::run() {
return;
}
#include "Thread.h"
#include <unistd.h>
#include <memory>
/**
* MyThread implementation
*/
class MyThread: public Thread {
private:
void run() override;
};
/**
* @return void
*/
void MyThread::run() {
//run()方法中具体实现逻辑可以自行实现
while(1){
std::cout << "MyThread is Running..." << std::endl;
sleep(1);
}
/* for (size_t i = 0; i < 5; i++)
{
std::cout << "MyThread is Running..." << std::endl;
} */
}
int main(int argc,char **argv){
MyThread myThread;
/* myThread.start();
myThread.run();
myThread.join(); */
// Thread *pth = new MyThread();
std::unique_ptr<Thread> pth(new MyThread());//用基类指针指向派生类对象
/**
* 因为派生类构造函数没有实现,所以会自动去执行基类的构造函数,初始化_thid和_isRunning
*
*/
pth->start();
pth->join();
return 0;
}
22.C++和java中指针和引用的用法
C++中的指针、引用与Java中的引用比较
C++ 和 Java 都是支持面向对象编程的语言,但两者在指针和引用的用法上存在一些差异。
C++ 中的指针和引用
指针
C++ 中的指针是变量,它指向一块内存地址。可以使用指针来访问和修改内存中的数据。指针的声明需要使用星号 *
。
C++
int i = 10;
int* p = &i; // p 指向变量 i 的内存地址
指针可以用于以下操作:
- 访问内存中的数据:可以使用指针来读取或写入指针指向的内存地址中的数据。
- 修改内存中的数据:可以使用指针来修改指针指向的内存地址中的数据。
- 指向数组元素:指针可以用来指向数组元素。
- 进行指针运算:可以对指针进行加减、比较等运算。
引用
C++ 中的引用是另一个指向对象的别名。与指针不同,引用必须在声明时进行初始化,并且不能重新赋值。引用的声明需要使用引用符号 &
。
C++
int i = 10;
int& r = i; // r 引用变量 i
引用可以用于以下操作:
- 访问对象:可以使用引用来访问引用的对象。
- 修改对象:可以使用引用来修改引用的对象。
Java 中的引用
Java 中的引用类似于 C++ 中的引用,但它与 C++ 中的引用存在一些关键的区别:
- 引用必须初始化:Java 中的引用必须在声明时或赋值之前进行初始化,否则会导致编译错误。
- 引用不能重新赋值:Java 中的引用一旦初始化,就不能再指向其他对象。
- 引用指向对象:Java 中的引用指向实际的 对象,而不是对象的内存地址。
Java 中的引用可以用于以下操作:
- 访问对象:可以使用引用来访问引用的对象。
- 修改对象:可以使用引用来修改引用的对象。
总结
特性 | C++ 指针 | C++ 引用 | Java 引用 |
---|---|---|---|
声明 | 使用星号 * | 使用引用符号 & | 使用引用符号 & |
初始化 | 不需要 | 必须 | 必须 |
重新赋值 | 可以 | 不可以 | 不可以 |
指向类型 | 内存地址 | 对象 | 对象 |
操作 | 访问/修改内存、指针运算 | 访问/修改对象 | 访问/修改对象 |
区别
- C++ 中的指针可以指向任意内存地址,而 Java 中的引用只能指向对象。
- C++ 中的指针可以进行指针运算,而 Java 中的引用不能。
- C++ 中的指针可以被释放,而 Java 中的引用始终有效。
选择指针还是引用
在 C++ 中,指针和引用都可以用于指向对象。一般情况下,建议使用引用,因为它更加安全和易于使用。但是,在某些情况下,指针可能更加灵活和高效。例如,当需要直接操作内存或进行指针运算时,可以使用指针。
在 Java 中,只能使用引用来指向对象。引用更加安全和易于使用,因此是推荐的选择。
示例
以下是一个 C++ 和 Java 中使用指针和引用的示例:
C++
C++
int main() {
int i = 10;
int* p = &i; // 使用指针指向变量 i
int& r = i; // 使用引用指向变量 i
// 使用指针访问和修改数据
std::cout << *p << std::endl; // 输出 10
*p = 20;
std::cout << *p << std::endl; // 输出 20
// 使用引用访问和修改数据
std::cout << r << std::endl; // 输出 20
r = 30;
std::cout << r << std::endl; // 输出 30
return 0;
}
Java
Java
public class Main {
public static void main(String[] args) {
int i = 10;
int& r = i; // 使用引用指向变量 i
// 使用引用访问和修改数据
System.out.println(r); // 输出 10
r = 20;
System.out.println(r); // 输出 20
}
}
在这个例子中,我们分别演示
23、Non-member Functions for Type Conversion
条款24:若所有参数皆需类型转换,请为此采用non-member函数
条款24出自C++领域的Effective C++书籍,作者是Scott Meyers。该条款的核心思想是:当一个函数需要对其所有参数进行类型转换时,应该将其定义为一个非成员函数(non-member function),而不是类的成员函数。这背后的原因包括以下几点:
-
更好的封装性:非成员函数可以减少类的公开接口,保持类的封装性。
-
提高通用性:非成员函数可以作用于多个类,而不仅仅是某一个类的成员函数。
-
隐式类型转换:非成员函数在所有参数上都允许隐式类型转换,而成员函数的隐式类型转换只适用于其参数(不包括调用该函数的对象)。
举个例子,假设你有一个表示复杂数的类 Complex
:
class Complex {
public:
Complex(double real, double imag) : real_(real), imag_(imag) {}
// 成员函数加法
Complex operator+(const Complex& rhs) const {
return Complex(real_ + rhs.real_, imag_ + rhs.imag_);
}
private:
double real_;
double imag_;
};
在这个例子中,如果你有两个 Complex
对象 a
和 b
,你可以直接使用 a + b
来进行加法运算。然而,如果你想用一个 double
和一个 Complex
对象进行加法运算,比如 a + 2.5
或 2.5 + a
,那么成员函数方式就有局限性,因为 2.5 + a
不会被识别为有效的操作。
为了解决这个问题,可以定义一个非成员函数:
class Complex {
public:
Complex(double real, double imag) : real_(real), imag_(imag) {}
private:
double real_;
double imag_;
// 声明为友元函数,使其能访问私有成员
friend Complex operator+(const Complex& lhs, const Complex& rhs);
};
// 非成员加法函数
Complex operator+(const Complex& lhs, const Complex& rhs) {
return Complex(lhs.real_ + rhs.real_, lhs.imag_ + rhs.imag_);
}
通过这样做,a + 2.5
和 2.5 + a
都可以正常工作,因为编译器会进行适当的类型转换来匹配参数类型。
总之,当需要对所有参数进行类型转换时,采用非成员函数不仅能提升代码的灵活性和可读性,还能保持类的封装性。
在C++中,操作符重载的实现方式影响了类型转换的行为。为了理解为什么 2.5 + a
不会被识别为有效的操作,先来看一下成员函数和非成员函数的不同处理方式。
成员函数操作符重载
当你在一个类中重载操作符(如 +
),通常是以成员函数的形式进行:
class Complex {
public:
Complex(double real, double imag) : real_(real), imag_(imag) {}
// 成员函数加法
Complex operator+(const Complex& rhs) const {
return Complex(real_ + rhs.real_, imag_ + rhs.imag_);
}
private:
double real_;
double imag_;
};
在这种情况下,成员函数操作符的第一个参数是隐含的 this
指针,也就是说它的原型实际上是这样:
Complex operator+(const Complex& this, const Complex& rhs);
因此,当你写 a + 2.5
时,编译器会尝试将 2.5
转换为 Complex
类型,以匹配成员函数的参数 rhs
,这是可以成功的,因为你可以有一个从 double
到 Complex
的转换构造函数。
但是,当你写 2.5 + a
时,2.5
是左操作数,编译器会尝试查找 double
类型的 operator+
成员函数,但 double
并没有成员函数 operator+
,所以这种方式不能工作。
非成员函数操作符重载
为了使 2.5 + a
也能工作,你需要重载一个非成员函数:
class Complex {
public:
Complex(double real, double imag) : real_(real), imag_(imag) {}
// 声明为友元函数,使其能访问私有成员
friend Complex operator+(const Complex& lhs, const Complex& rhs);
private:
double real_;
double imag_;
};
// 非成员加法函数
Complex operator+(const Complex& lhs, const Complex& rhs) {
return Complex(lhs.real_ + rhs.real_, lhs.imag_ + rhs.imag_);
}
在这种情况下,非成员函数的原型是:
Complex operator+(const Complex& lhs, const Complex& rhs);
当你写 2.5 + a
或 a + 2.5
时,编译器会尝试将 2.5
转换为 Complex
,以匹配 lhs
或 rhs
参数。因为非成员函数不依赖于任何对象的成员,因此可以处理任意顺序的参数并进行适当的类型转换。
示例代码
以下是一个完整的示例代码:
#include <iostream>
class Complex {
public:
Complex(double real, double imag) : real_(real), imag_(imag) {}
// 声明为友元函数,使其能访问私有成员
friend Complex operator+(const Complex& lhs, const Complex& rhs);
void print() const {
std::cout << "(" << real_ << ", " << imag_ << "i)" << std::endl;
}
private:
double real_;
double imag_;
};
// 非成员加法函数
Complex operator+(const Complex& lhs, const Complex& rhs) {
return Complex(lhs.real_ + rhs.real_, lhs.imag_ + rhs.imag_);
}
int main() {
Complex a(1.0, 2.0);
Complex b = a + 2.5; // 使用 Complex::Complex(double, double)
Complex c = 2.5 + a; // 使用 Complex::Complex(double, double)
b.print();
c.print();
return 0;
}
输出将是:
(3.5, 2.0i)
(3.5, 2.0i)
通过这种方式,2.5 + a
和 a + 2.5
都可以正常工作,因为编译器会根据需要进行类型转换,以匹配非成员操作符函数的参数。
24、C++函数模板
C++ 中的模板是一种泛型编程工具,可以使函数和类能够处理任意类型的数据,从而实现代码的重用和类型安全。模板有两种主要形式:函数模板和类模板。
函数模板
函数模板允许你编写一个通用的函数,该函数可以处理不同类型的数据。你可以用一个函数模板来替代多个函数重载。
语法:
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
在这个例子中,max
函数模板可以接受任意类型 T
的参数,并返回它们中较大的一个。
使用示例:
#include <iostream>
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
std::cout << max(10, 20) << std::endl; // 输出 20
std::cout << max(10.5, 7.3) << std::endl; // 输出 10.5
std::cout << max('a', 'z') << std::endl; // 输出 z
return 0;
}
类模板
类模板允许你定义一个通用的类,该类可以处理不同类型的数据。你可以用一个类模板来替代多个类定义。
语法:
template <typename T>
class MyClass {
private:
T data;
public:
MyClass(T data) : data(data) {}
T getData() {
return data;
}
};
在这个例子中,MyClass
类模板可以处理任意类型 T
的数据。
使用示例:
#include <iostream>
template <typename T>
class MyClass {
private:
T data;
public:
MyClass(T data) : data(data) {}
T getData() {
return data;
}
};
int main() {
MyClass<int> intObj(10);
MyClass<double> doubleObj(10.5);
MyClass<std::string> stringObj("Hello");
std::cout << intObj.getData() << std::endl; // 输出 10
std::cout << doubleObj.getData() << std::endl; // 输出 10.5
std::cout << stringObj.getData() << std::endl; // 输出 Hello
return 0;
}
模板的高级特性
非类型模板参数
模板不仅可以接受类型参数,还可以接受非类型参数(如整数、指针、引用等)。
template <typename T, int size>
class Array {
private:
T arr[size];
public:
int getSize() const {
return size;
}
};
使用示例:
#include <iostream>
template <typename T, int size>
class Array {
private:
T arr[size];
public:
int getSize() const {
return size;
}
};
int main() {
Array<int, 10> intArray;
std::cout << "Size of intArray: " << intArray.getSize() << std::endl; // 输出 10
Array<double, 20> doubleArray;
std::cout << "Size of doubleArray: " << doubleArray.getSize() << std::endl; // 输出 20
return 0;
}
模板特化
模板特化允许你为特定类型或特定值提供自定义的实现。
#include <iostream>
// 通用模板
template <typename T>
class MyClass {
public:
void display() {
std::cout << "General template" << std::endl;
}
};
// 模板特化
template <>
class MyClass<int> {
public:
void display() {
std::cout << "Specialized template for int" << std::endl;
}
};
int main() {
MyClass<double> obj1;
MyClass<int> obj2;
obj1.display(); // 输出 "General template"
obj2.display(); // 输出 "Specialized template for int"
return 0;
}
变量模板
C++14 引入了变量模板,使得你可以为常量或静态变量创建模板。
template<typename T>
constexpr T pi = T(3.1415926535897932385);
int main() {
std::cout << pi<double> << std::endl; // 输出 3.14159...
std::cout << pi<float> << std::endl; // 输出 3.14159...
return 0;
}
总结
- 函数模板:允许你编写通用函数,适用于不同类型的数据。
- 类模板:允许你编写通用类,适用于不同类型的数据。
- 非类型模板参数:模板不仅可以接受类型参数,还可以接受非类型参数。
- 模板特化:为特定类型或值提供自定义实现。
- 变量模板:为常量或静态变量创建模板。
通过使用模板,C++ 程序可以实现更高的代码重用性和类型安全性,同时保持性能优势。
使用模板的好处
使用模板在 C++ 编程中有很多好处,特别是在编写泛型代码和创建可重用的组件时。以下是一些主要的好处:
1. 代码重用性
模板允许你编写一次代码,然后对多种数据类型进行重用。你不需要为每种数据类型分别编写相同功能的代码。
示例:模板实现的最大值函数
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
std::cout << max(3, 7) << std::endl; // 使用 int 类型
std::cout << max(3.5, 7.2) << std::endl; // 使用 double 类型
std::cout << max('a', 'z') << std::endl; // 使用 char 类型
return 0;
}
2. 类型安全
模板在编译时进行类型检查,确保类型的一致性和安全性。这可以防止许多运行时错误,使得代码更加健壮。
示例:类型安全的容器
#include <vector>
template <typename T>
class MyContainer {
private:
std::vector<T> elements;
public:
void add(const T& element) {
elements.push_back(element);
}
T get(int index) const {
return elements.at(index);
}
};
int main() {
MyContainer<int> intContainer;
intContainer.add(1);
intContainer.add(2);
std::cout << intContainer.get(0) << std::endl;
MyContainer<std::string> stringContainer;
stringContainer.add("Hello");
stringContainer.add("World");
std::cout << stringContainer.get(0) << std::endl;
return 0;
}
3. 性能优化
模板在编译时实例化,生成针对特定类型的代码。这意味着没有运行时的类型检查开销,可以提高程序的运行效率。
4. 简化代码
使用模板可以大大简化代码,使得代码更加简洁和易读。你不需要为每种类型重复编写类似的代码。
示例:模板实现的通用算法
#include <algorithm>
template <typename T>
void sortArray(T* array, int size) {
std::sort(array, array + size);
}
int main() {
int arr1[] = {3, 1, 4, 1, 5};
sortArray(arr1, 5);
double arr2[] = {3.5, 1.2, 4.6, 1.1, 5.3};
sortArray(arr2, 5);
return 0;
}
5. 支持泛型编程
模板是泛型编程的基础,允许你编写能够处理多种数据类型的算法和数据结构。这使得代码更加灵活和通用。
示例:模板实现的通用容器
#include <iostream>
#include <vector>
template <typename T>
class Stack {
private:
std::vector<T> elems;
public:
void push(const T& elem) {
elems.push_back(elem);
}
void pop() {
if (!elems.empty()) {
elems.pop_back();
}
}
T top() const {
if (!elems.empty()) {
return elems.back();
}
throw std::out_of_range("Stack<>::top(): empty stack");
}
bool empty() const {
return elems.empty();
}
};
int main() {
Stack<int> intStack;
intStack.push(1);
intStack.push(2);
std::cout << intStack.top() << std::endl;
Stack<std::string> stringStack;
stringStack.push("Hello");
stringStack.push("World");
std::cout << stringStack.top() << std::endl;
return 0;
}
6. 与标准库兼容
C++ 标准模板库(STL)广泛使用模板,包括容器、迭代器和算法。学习和使用模板可以更好地利用 STL。
7. 减少代码冗余
通过使用模板,可以避免为每种类型编写相同的函数或类定义,从而减少代码冗余和维护成本。
总结
模板使得 C++ 语言在编写泛型和高性能代码方面具有很大的优势。它们不仅提高了代码的重用性、类型安全性和性能,还简化了代码,使其更容易阅读和维护。学习和掌握模板编程对于成为一名高级 C++ 程序员非常重要。
Qt斗地主项目
调试过程中遇到的问题
1.MyButton类
自定义类MyButton中,鼠标事件中重写虚函数,粗心导致的问题
//头文件
#ifndef MYBUTTON_H
#define MYBUTTON_H
#include <QPushButton>
class MyButton : public QPushButton
{
Q_OBJECT
public:
explicit MyButton(QWidget *parent = nullptr);
void setImage(QString normal,QString hover,QString pressed);
protected:
//重写鼠标事件
//鼠标按下
void mousePressEvent(QMouseEvent *ev) override;
//鼠标释放
void mouseReleaseEvent(QMouseEvent *ev) override;
//鼠标进入
void enterEvent(QEnterEvent *ev) override;//
//鼠标离开
void leaveEvent(QEvent *ev) override;
//绘图
void paintEvent(QPaintEvent *ev) override;
private:
QString m_normal;
QString m_hover;
QString m_pressed;
QPixmap m_pixmap;
};
#endif // MYBUTTON_H
//实现文件
#include "mybutton.h"
#include <QMouseEvent>
#include <QPainter>
MyButton::MyButton(QWidget *parent)
: QPushButton{parent}
{
}
void MyButton::setImage(QString normal, QString hover, QString pressed)
{
m_normal = normal;
m_hover = hover;
m_pressed = pressed;
m_pixmap.load(m_normal);
update();
}
void MyButton::mousePressEvent(QMouseEvent *ev)
{
if(ev->button() == Qt::LeftButton)
{
m_pixmap.load(m_pressed);
update();
}
QPushButton::mousePressEvent(ev);
}
void MyButton::mouseReleaseEvent(QMouseEvent *ev)
{
if(ev->button() == Qt::LeftButton)
{
m_pixmap.load(m_normal);
update();
}
QPushButton::mouseReleaseEvent(ev);
}
//轮到玩家出牌时,才显示出牌/不出牌按钮组
void MyButton::enterEvent(QEnterEvent *ev)
{
Q_UNUSED(ev)
m_pixmap.load(m_hover);
update();
}
void MyButton::leaveEvent(QEvent *ev)
{
Q_UNUSED(ev)
m_pixmap.load(m_normal);
update();
}
void MyButton::paintEvent(QPaintEvent *ev)
{
Q_UNUSED(ev)
QPainter p(this);
p.drawPixmap(rect(),m_pixmap);
}
MyButton::mousePressEvent(QMouseEvent *ev)和 MyButton::mouseReleaseEvent(QMouseEvent *ev)函数中一定要注意:
*MyButton::mousePressEvent(QMouseEvent *ev)*鼠标按下事件中是处理动作是QPushButton::mousePressEvent(ev);
*MyButton::mouseReleaseEvent(QMouseEvent *ev)*鼠标离开事件中处理动作是QPushButton::mouseReleaseEvent(ev);
2.Cards类
在Cards::takeRandomCard()函数中因为for循环导致的问题
#ifndef CARDS_H
#define CARDS_H
#include "card.h"
#include <QSet>
class Cards
{
public:
enum SortType{
Asc,
Desc,
NoSort
};
Cards();
//自定义一个带参构造函数,在GamePanel::onDispatchCard()中调用
Cards(const Card &card);
//添加单张扑克牌(抢地主)
/*void add(Card &card);*/
//非const 左值引用不能绑定到const对象
void add(const Card &card);
//添加多张扑克牌
/*void add(Cards &cards);*/
void add(const Cards &cards);
void add(const QVector<Cards>& cards);
//一次性插入多个数据(<< 操作符重载)
Cards &operator <<(const Card &card);
Cards &operator <<(const Cards &cards);
//删除单张扑克牌
void remove(const Card& card);
//删除多张扑克牌
void remove(const Cards& cards);
void remove(const QVector<Cards>& cards);
//扑克牌的数量
int cardCount();
//是否为空
bool isEmpty();
//清空扑克牌
void clear();
//最大点数
Card::CardPoint maxPoint();
//最小点数
Card::CardPoint minPoint();
//指定点数的 卡牌的数量
int pointCount(Card::CardPoint point);
//某张牌是否在集合中
bool contains(const Card &card);
bool contains(const Cards &cards);
//随机取出一张扑克牌
Card takeRandomCard();
//转换函数QSet -->> Qvector
//根据设定,窗口里面的扑克牌是按照从右到左,从大到小的顺序来排列的
CardList toCardList(SortType type = Desc);//QVector<Card>
// 测试函数, 打印所有的卡牌信息
void printAllCardInfo();
private:
//错误QSet不能存储自定义类型的类对象
QSet<Card> m_cards;
};
#endif // CARDS_H
fefe
#include "cards.h"
#include <QRandomGenerator>
#include <QDebug>
//记录扑克牌的点数和卡牌的数量
Cards::Cards()
{
}
Cards::Cards(const Card &card)
{
add(card);
}
void Cards::add(const Card &card)
{
m_cards.insert(card);
}
void Cards::add(const Cards &cards)
{
m_cards.unite(cards.m_cards);//并集
}
void Cards::add(const QVector<Cards> &cards)
{
for(int i=0; i<cards.count(); ++i)
{
add(cards.at(i));
}
}
Cards &Cards::operator <<(const Card &card)
{
add(card);
return *this;
}
Cards &Cards::operator <<(const Cards &cards)
{
add(cards);
return *this;
}
void Cards::remove(const Card &card)
{
m_cards.remove(card);
}
void Cards::remove(const Cards &cards)
{
m_cards.subtract(cards.m_cards);//差集
}
void Cards::remove(const QVector<Cards> &cards)
{
for(int i=0; i<cards.size(); ++i)
{
remove(cards.at(i));
}
}
int Cards::cardCount()
{
return m_cards.size();
}
bool Cards::isEmpty()
{
return m_cards.isEmpty();
}
void Cards::clear()
{
m_cards.clear();
}
Card::CardPoint Cards::maxPoint()
{
Card::CardPoint max = Card::Card_Begin;
//set容器的遍历
// for(auto it : m_cards)
// {
// if(it.point() > max)
// {
// }
// }
if(!m_cards.isEmpty())
{
for(auto it = m_cards.begin(); it!=m_cards.end(); ++it)
{
if(it->point() > max)
{
max = it->point();
}
}
}
return max;
}
Card::CardPoint Cards::minPoint()
{
Card::CardPoint min = Card::Card_End;
if(!m_cards.isEmpty())
{
for(auto it = m_cards.begin(); it!=m_cards.end(); ++it)
{
if(it->point() < min)
{
min = it->point();
}
}
}
return min;
}
int Cards::pointCount(Card::CardPoint point)
{
int count = 0;
for(auto it = m_cards.begin(); it!=m_cards.end(); ++it)
{
if(it->point() == point)
{
count++;
}
}
return count;
}
bool Cards::contains(const Card &card)
{
return m_cards.contains(card);
}
bool Cards::contains(const Cards &cards)
{
return m_cards.contains(cards.m_cards);
}
/*版本3*/
Card Cards::takeRandomCard()
{
// 生成一个随机数
int num = QRandomGenerator::global()->bounded(m_cards.size());
// 创建只读迭代器,遍历num次
QSet<Card>::const_iterator it = m_cards.constBegin();
//基于上面的迭代器进行遍历,进循环即可,不需要在循环体中做任何操作
for (int i = 0; i < num; ++i, ++it) {}
// 检查是否到达集合末尾
if (it != m_cards.constEnd()) {
Card card = *it;
m_cards.erase(it); // 从容器中删除已经取出的扑克牌
return card;
}
// 如果集合为空,可以返回一个默认的扑克牌,或者抛出异常,具体取决于你的设计要求
// 这里简单返回一个默认的扑克牌
return Card();
}
/*版本2*/
//Card Cards::takeRandomCard()
//{
// // 生成一个随机数
// int num = QRandomGenerator::global()->bounded(m_cards.size());
// QSet<Card>::const_iterator it = m_cards.constBegin();
// for(int i=0; i<num; ++i, ++it);
// Card card = *it;
// m_cards.erase(it);
// return card;
//}
/*版本1*/
//Card Cards::takeRandomCard()
//{
// //生成一个随机数
// int num = QRandomGenerator::global()->bounded(m_cards.size());
// //遍历num次
// QSet<Card>::const_iterator it = m_cards.constBegin();
// for(int i =0;i<num;++i,++it)
// {
// Card card = *it;
// m_cards.erase(it);//从容器中删除已经取出的扑克牌
// return card;
// }
//}
CardList Cards::toCardList(SortType type)
{
CardList list;
for(auto it = m_cards.begin(); it != m_cards.end(); ++it)
{
list << *it;
}
if(type == Asc)
{
std::sort(list.begin(), list.end(), lessSort);
}
else if(type == Desc)
{
std::sort(list.begin(), list.end(), greaterSort);
}
return list;
}
void Cards::printAllCardInfo()
{
QString text;
char pts[] = "JQKA2";
for(auto it = m_cards.begin(); it != m_cards.end(); ++it)
{
QString msg;
Card::CardPoint pt = it->point();
Card::CardSuit suit = it->suit();
if(suit == Card::CardSuit::Club)
{
msg = "梅花";
}
else if(suit == Card::CardSuit::Diamond)
{
msg = "方片";
}
else if(suit == Card::CardSuit::Heart)
{
msg = "红桃";
}
else
{
msg = "黑桃";
}
if(pt>=Card::Card_3 && pt <= Card::Card_10)
{
msg = QString("%1%2").arg(msg).arg(pt+2);
}
else if(pt >= Card::Card_J && pt <= Card::Card_2)
{
msg = QString("%1%2").arg(msg).arg(pts[pt-Card::Card_J]);
}
if(pt == Card::Card_BJ)
{
msg = "Big Joker";
}
if(pt == Card::Card_SJ)
{
msg = "Small Joker";
}
msg += " ";
text += msg;
}
qDebug() << text;
}
错误的版本:
Card Cards::takeRandomCard()
{
//生成一个随机数
int num = QRandomGenerator::global()->bounded(m_cards.size());
//遍历num次
QSet<Card>::const_iterator it = m_cards.constBegin();
for(int i =0;i<num;++i,++it)
{
Card card = *it;
m_cards.erase(it);//从容器中删除已经取出的扑克牌
return card;
}
}
上面的代码中for循环只是为了遍历迭代器中的数据,不需要在循环体中做任何操作。该版本的 takeRandomCard
函数中,如果将处理动作写在for循环内,在循环中就直接执行了 return card;
语句,这意味着在第一次循环迭代之后就会立即返回一个扑克牌,并结束函数的执行。这导致了只能取到一个随机扑克牌的问题,而不是取得一个随机的扑克牌并从集合中删除。
第三个版本的 takeRandomCard()
是正确的,而前两个版本(被注释为 “版本1” 和 “版本2”)存在潜在问题。
问题分析如下:
-
版本1:
for(int i =0;i<num;++i,++it) { Card card = *it; m_cards.erase(it); // 此处有问题! return card; }
在这个版本中,
erase
操作在循环内部执行,这是有问题的。在从集合中删除元素之后,迭代器it
变得无效,然后在下一次迭代中对其进行解引用或在循环外部返回会导致未定义的行为。这可能导致崩溃或意外的行为。 -
版本2:
QSet<Card>::const_iterator it = m_cards.constBegin(); for(int i=0; i<num; ++i, ++it); Card card = *it; m_cards.erase(it); // 此处有问题! return card;
与版本1类似,这个版本有相同的问题。
erase
操作不应该在循环内执行,而且在第一次删除后,迭代器it
变得无效。 -
版本3(正确):
for (int i = 0; i < num; ++i, ++it) {} Card card = *it; m_cards.erase(it); // 在循环外执行 erase return card;
在这个版本中,
erase
操作在循环之后执行,这是正确的方法。循环用于将迭代器推进到所需位置,然后在循环外执行擦除操作。
请记住,从容器中擦除元素会使指向该元素的迭代器无效,因此必须小心地进行擦除,以避免未定义的行为。