VVbuys Blog

standalone Linux lover

​ 该项目是作者学习数据库技术时整理的项目,非关系型数据库Redis的核心存储引擎使用的数据结构就是跳表,该项目采用C++语言基于跳表结构设计了一个轻量级键值型存储引擎,支持插入查询删除显示导出重加载6种基本操作。为了测试引擎效率,作者开发了一个使用和web bench相似技术的压力测试工具Stress Bench。在随机读写情况下,该引擎每秒在多个并发线程下的累计执行成功次数都可以保持在30000左右。

1、技术栈

  • 基于跳表结构面向对象思想的节点类、跳表类的开发与封装;
  • 基于父子多进程匿名管道技术Stress Bench测试工具;
  • 使用互斥锁,防止并发插入数据时发生冲突;
  • 基于MakeFile的多文件编译方式;

2、对外接口

  • insert_element(int,string) // 插入记录
  • srase_element(int) // 删除记录
  • search_element(int) // 查询记录
  • displayList(int) // 显示结构
  • dumpFile(string) // 导出数据
  • loadFile(string) // 加载数据
  • size() // 获取数据规模

3、跳表原理

跳表是在一个原始链表中添加了多级索引,通过每一级索引来进行二分搜索的数据结构,其架构如下:

![](/img-post/项目背景/【学习项目】基于跳表结构的KV存储引擎设计/跳表的结构.png)

img

在上述跳表中,假如查询key=10的记录,则可以从第二级索引开始快速定位:

  • 遍历第二级索引,从1开始,发现7<10<13,7就是该层要找的索引,通过它跳到下一级索引
  • 遍历第一级索引,从7开始,发现9<10<13,9就是该层要找的索引,通过它跳到下一级索引
  • 遍历原始链表,从9开始,发现10=10,10就是该层要找的最终索引

相比于直接遍历原始链表,多级索引的存在使跳表查询效率更快,总结:

跳表的优点: 可以实现高效的插入、删除和查询 ,时间复杂度为O(logn).

跳表的缺点:需要存储多级索引,增加了额外的存储空间

跳表的用途:经典的以空间换时间的数据结构,常用于非关系数据库的存储引擎中

4、跳表实现

跳表的原理很好理解,在实现上比一般链表要难一点。

(1)跳表的节点

首先对于每个跳表的节点来讲,其肯定要保存两个方向的指针:向右和向下,可以定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 节点类
class Node {
public:
// 构造函数
Node(Node* right,Node* down,const int k, const string v):right(right),down(down),key(k),value(v){}
int get_key() const{return key;};
string get_value() const{return value;}
void set_value(string){this->value=value;}
public:
Node* right;// 右节点指针
Node* down;// 下节点指针
private:
int key; // 索引
string value;// 数据
};

(2)遍历的跳表

首先跳表有一个虚拟头结点_header(下面的“+”)指向链表的最上层索引的第一个节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 
level 4 +-->1+ +---->100
|
|
level 3 1+-------->10+---------------->50+------>70
|
|
level 2 1+------> 10+------> 30+------>50+------->70
|
|
level 1 1+-->4+-->10+------> 30+------>50+------->70
|
|
level 0 1+>4->9+->10+------> 30+->40+->50+-->60+->70
*/

假如要把上述跳表的每一层的key打印出来,则可以从**_header**出发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Node *p_header = this->_header;
Node *p= nullptr;// 每一层的头结点
int level=this->_skip_list_level;
// 从上到下搜索
while(p_header){
std::cout << "Level " << level << ": ";
p=p_header;// 获取当前层的头结点
// 从左向右查找,打印当前层
while(p->right){
p=p->right;
std::cout <<p->get_key() << ":" << p->get_value() ;
}
std::cout << std::endl;
p_header = p_header->down;// 下降一层
level--;
}

上述代码好理解

(3)跳表的插入

跳表的插入负责构建整个跳表,是跳表中最难的需求。跳表的插入原理如下:

![](/img-post/项目背景/【学习项目】基于跳表结构的KV存储引擎设计/跳表的插入原理.png)

img在上图中,要插入一个节点16,则要在多个层都插入这个节点16,这个多个层要可能随机,保证每一层都比上一层的节点少一半。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 插入某条记录
int SkipList::insert_element(const int key, const string value) {
mtx.lock();
//从上至下记录搜索路径
pathList.clear();
Node *p = this->_header;
// 从上到下去搜索,搜索后p指向次小于key的位置,应在p后新插入
while(p){
// 从左向右查找
while(p->right&&p->right->get_key()<key)p=p->right;
pathList.push_back(p);// 此时的p是最后一个小于num的节点
p = p->down;// 下降一层
}
// 如果current存在,放弃插入
Node* current=pathList.back()->right;
if (current!= NULL && current->get_key() == key) {
std::cout << "key: " << key << ", exists" << std::endl;
mtx.unlock();
return 1;
}
// 此时p指向最底层中插入位置前
bool insertUp = true;
Node* downNode = NULL;// 下一个节点为NULL
// 从下至上搜索路径回溯,50%概率
// 这里实现是会保证不会超过当前的层数的,然后靠头结点去额外加层, 即每次新增一层
int level=0;
while (insertUp && pathList.size() > 0){
Node *insert = pathList.back();
pathList.pop_back();
// 添加新结点
insert->right = new Node(insert->right,downNode,key,value);
// 把新结点赋值为downNode
downNode = insert->right;
// 50%概率
insertUp = (rand()&1)==0;
level++;
}
// 说明pathList已经不为空,插入新的头结点,加层
if(insertUp){
this->_header = new Node(new Node(NULL,downNode,key,value), this->_header, -5,to_string(level)+"head");
this->_skip_list_level++;
}

this->_element_count ++;
std::cout << "Successfully inserted key:" << key << ", value:" << value << std::endl;
mtx.unlock();
return 0;
}

(4)跳表的查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool SkipList::search_element(int key) {
mtx.lock();
Node *p = this->_header;// 从虚拟头结点出发
while(p){
// 从左向右寻找目标区间
while(p->right && p->right->get_key()<key)p=p->right;
// 没找到目标值,则继续往下走
if(!p->right||p->right->get_key()>key)p=p->down;
// 如果找到目标值
else{
std::cout << "key: " << key<< ":"<<p->right->get_value()<<endl;
return true;////找到目标值,结束
}
}
std::cout << "无法找到key: "<<key<<endl;
mtx.unlock();
return false;
}

(5)跳表的删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool SkipList::erase_element(int key) {
Node *p = this->_header;
bool seen = false;
while (p){
// 当前层向右找到次小的点
while (p->right && p->right->get_key()<key)p = p->right;
// 无效点,或者 太大, 则到下一层去
if (!p->right||p->right->get_key()>key)p=p->down;
else{
// 搜索到目标结点,进行删除操作,结果记录为true
// 继续往下层搜索,删除一组目标结点
seen = true;
p->right = p->right->right;
p = p->down;
}
}
if(seen) {
this->_element_count--;
std::cout << "成功删除key: "<<key<<endl;
}
else std::cout << "无法删除key: "<<key<<endl;
return seen;
}

5、压力测试

​ 该项目包含一个作者开发的压力测试工具Stress Bench,其中文名叫做压力工作台,是借鉴了Web Bench的技术经验。该项目可以测试在规定时间t秒内n个进程同时执行某项操作的成功次数失败次数

(1)创建N个子进程

​ 该工具首先使用fork()创建n个子进程,子进程负责在规定时间内执行某项操作,父进程负责统计某个子进程的执行结果,父进程子进程间通过匿名管道进行进程间通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
pid_t pid=0;
FILE *f;
// 拷贝clients个进程
for(int i=0;i<_thread_count;i++){
pid=fork();
// 如果是子进程或者出错
if(pid <= (pid_t) 0){
sleep(1); /* make childs faster */
break;// 退出循环
}
}
// 如果出错
if(pid<(pid_t)0) perror("fork failed.");
// 子进程将要执行的代码
if(pid== (pid_t) 0){
int count=0;
int failed=0;
this->test_target_function(count,failed);// 子进程进行测试
// 将该进程的测试结果写入匿名管道
f=fdopen(_pipe[1],"w");// 打开管道的写端
if(f==NULL) perror("open pipe for writing failed.");
fprintf(f,"%d %d\n",count,failed);//每次写入一行格式化的数据
fclose(f);
}
// 父进程执行的代码
else{
f=fdopen(_pipe[0],"r");// 打开匿名管道的读端
if(f==NULL) perror("open pipe for reading failed.");
setvbuf(f,NULL,_IONBF,0);
int count=0;
int failed=0;
int i,j,client=_thread_count;
while(1){
pid=fscanf(f,"%d %d",&i,&j);// 在此处阻塞
if(pid<2){
fprintf(stderr,"Some of our childrens died.\n");
break;
}
count+=i;
failed+=j;
if(--client==0) break;
}
fclose(f);
printf("success= %d failed= %d",count,failed);
}

(A)fork的使用

​ Linux中通过fork()来创建子进程,在执行fpid=fork()之前,只有当前的父进程在执行这段代码;在执行fpid=fork()之后,当前的父进程和新复制出的子进程都开始并发执行之后的代码。所以为了父进程和子进程执行不同的代码,需要根据fpid的值分别处理,否则就会执行相同的代码。子进程的fork返回值是0,父进程的fork返回值>0,这也是区分父进程和子进程的方法,至于其他的内容,在fork之前的东西两个进程的一样的。

(B)fork的底层

​ 在调用fork()时,Linux的内核程序实际上只会复制父进程的页表以及给子进程创建一个进程描述符,并不会立即开辟新的内存空间来复制一份父进程的数据,此时父进程和子进程共享同一份数据资源,只有父进程或者**子进程*发生写的操作时,子进程*才会复制父进程的数据到一个新的内存空间,这种技术就是写时拷贝技术。但是在程序员看来,fork()就是立即克隆出了一个和父进程完全相同的子进程,只不过其底层使用了写时拷贝技术优化**。

(C)父子进程的关闭

假如父进程先于子进程关闭,那么子进程就成为孤儿进程,被操作系统接管

假如子进程先于父进程终止,而父进程又没有调用waitwaitpid函数父进程结束后,子进程成为僵尸进程,此时子进程虽然执行完毕但是始终占有内核资源,同时也减少了系统可以创建的最大进程数。

假如子进程先于父进程终止,而父进程调用了waitwaitpid函数,那么父进程会等待子进程结束后再结束,子进程不会成为僵尸进程

(D)匿名管道技术

​ 这里使用的是Linux的匿名管道(因为其创建管道使用了pipe(),命名管道使用mkfifo()),匿名管道的特点如下:

1、匿名管道是一种半双工通信,数据只能从写端流入读端。

2、匿名管道读写的是内核程序缓冲区的两端fd[0]和fd[1]。

3、匿名管道只能用于父子进程通信。这是因为匿名管道没有标识符,其他无亲缘关系的进程无法访问到该管道,只有创建它的父进程和使用它的子进程可以访问。在进行父子进程间通信时,一定要父进程先创建管道,再创建子进程,这样子进程才能访问匿名管道。

4、匿名管道默认是阻塞的,父子进程抢占式运行,如果父进程或者子进程先读,发现是空管道时就会阻塞,等待写入;某个进程写入一次后,读进程抢占到才能读入。

5、匿名管道读取数据是将数据剪切,所以多个进程读匿名管道,无法拿到相同的数据

6、匿名管道的读写具有原子性,即读操作或写操作要么完成,要么没有开始,不存在边读边写,或者同时写

7、匿名管道的读写是字节流,用户write两次写入字符串,对于管道来说两次输入的数据没有明显边界,read可以读任意字节大小,在读取时无法区分两次输入,前后两次输入粘连在一起,read的返回值是读取到数据的字节数。

(2)定时信号处理

子进程执行某项操作达到规定时间就会退出,其定时操作使用如下操作:

1
2
3
4
5
6
7
8
void StressBench::set_alarm_signal(){
struct sigaction sa;
sa.sa_handler=alarm_handler;// 函数指针,指向一个信号处理函数
sa.sa_flags=0;// 设置信号处理的其他相关操作
if(sigaction(SIGALRM,&sa,NULL))
exit(3);
alarm(_benchtime);// 定时操作
}

​ 该项目是作者在学习Linux服务端编程时整理的项目,Web简易服务器是一款用C++实现的基于Linux的轻量级高性能Web服务器,经过web bench工具的压力测试,可以实现上万的QPS (Query Per Second,每秒查询率)。

技术栈

  • 基于多进程网络通信匿名管道技术Web bench测试工具;
  • 基于互斥锁条件变量等多线程同步技术实现的线程池模块,实现将多个任务同时分配给多个线程;
  • 基于Socket编程IO复用技术Epoll实现的Reactor高并发模型,实现同时监听多个客户端事件;
  • 基于最小堆结构定时器模块,实现非活动连接的超时管理;
  • 基于正则表达式状态机的HTTP/1.1解析和响应模块,实现处理Web静态资源的请求;
  • 支持分散读集中写的缓冲区模块,实现缓存数据的合理管理,提高文件数据拷贝效率;
  • 基于RAII机制实现的MySQL数据库连接池模块,减少数据库连接建立与关闭的开销;
  • 基于单例模式阻塞队列异步日志系统,实现以文件形式按日期记录服务器运行信息;

项目架构

![](/img-post/项目背景/【学习项目】Web简易服务器项目/Web服务器模块关系.png)
img

程序逻辑

创建服务器

![](/img-post/项目背景/【学习项目】Web简易服务器项目/创建对象.png)
img

启动服务器

![](/img-post/项目背景/【学习项目】Web简易服务器项目/启动服务器(一).png)
img

![](/img-post/项目背景/【学习项目】Web简易服务器项目/启动服务器(二).png)
img

HTTP请求和响应格式

![](/img-post/项目背景/【学习项目】Web简易服务器项目/请求报文和响应报文.png)
img

[TOC]

​ 本文总结作者学习的Web服务器项目(WebServer),分析其中的主要模块的作用及相关知识点。一个WebServer是一个运行在linux系统上的C++编码的服务器程序,其需要处理来自浏览器程序(客户端)的各种HTTP请求,并对其请求作出HTTP响应。

​ 从上述图中可以看出其中共有7个模块:日志系统模块、监听套接字模块、IO复用模型epoll模块、线程池模块、数据库连接池模块、HTTP连接模

一、日志模块的作用

1、对于程序来讲

​ 日志模块用来程序执行的过程,帮助开发人员了解程序的运行情况,以便快速维护和调试。

2、对程序员来讲

​ 日志模块提供几个输出日志信息的接口(API),该接口的输入参数可能有:日志等级、日志输出平台。

日志等级:info、debug、warning、error。可以标记日志信息的重要程度,控制不同等级的日志信息的输出

日志输出平台:文件、控制台

二、日志模块的实现

1、设置为单例模式

​ 为什么要将日志模块的输出类设置为单例模式:日志模块需要在程序的任何地方被调用,且最好只有一个对象

2、格式化输出

日志输出本质上是对printf()函数的封装。

日志输出信息一般格式:日志等级+时间戳+格式化输出信息

C++的日志模块如何实现格式化输出?

(1)C++的函数的可变参数列表

C++允许定义形参个数和类型不确定的函数,不确定的形参可以使用可变参数列表。

(2)C++的vprintf()函数

vprintf(format,args);

vprintf()作用和printf()相同, 参数format 格式也相同。

printf 底层就是调用 vprintf 函数来将内容输出到控制台的,常规情况下,输出到控制台,多数情况下使用 printf 函数即可。当你需要自己写一个自定义 printf 函数时候才需要 vprintf 函数

vprintf 函数一般和 va_start / va_end 配套使用;

(3)宏和可变参数宏

为了更加方便地调用日志类的输出函数,我们需要将其经常定义为宏。

在C++中经常使用#define来定义宏:

1
2
#define N 1 // 不带参数的宏
#define AREA(x) x*x // 带参数的宏

宏在调用时进行宏替换,宏替换发生在静态编译期,是简单的文本替换,例如

1
int a=AREA(2+2);//a=8,不等于16,AREA(2+2)等价于2+2*2+2

带参数的宏也经常被叫做宏函数,但是宏函数不是真正的函数,其发生在静态编译期,是简单的文本替换。

由于输出函数可能使用了可变参数列表,这时我们就要使用可变参数宏__VA_ARGS__

可变参数宏的定义如下:

1
#define DEBUG(format,...) printf(format,__VA_ARGS__)

其中,...代表一个可以变化的参数表。使用保留名 VA_ARGS 把参数传递给宏。例如:

1
2
3
4
// 调用宏,因为DEBUG()是可变参数宏,所以传递不同个数的参数
DEBUG("X = %d\n", X);
// 处理器会把宏的调用替换成:
printf("X = %d\n", X);

3、日志等级

(1)C++中的enum的使用

枚举类型(enumeration)是 C++ 中的一种派生数据类型,它是由用户定义的若干枚举常量的集合。

应用举例:

1
2
enum color_set1 {RED, BLUE, WHITE, BLACK}; // 定义枚举类型color_set1
enum week {Sun, Mon, Tue, Wed, Thu, Fri, Sat}; // 定义枚举类型week

枚举常量代表该枚举类型的变量可能取的值,编译系统为每个枚举常量指定一个整数值,默认状态下,这个整数就是所列举元素的序号,序号从0开始。 可以在定义枚举类型时为部分或全部枚举常量指定整数值,在指定值之前的枚举常量仍按默认方式取值,而指定值之后的枚举常量按依次加1的原则取值。 各枚举常量的值可以重复。例如:

1
2
3
4
enum fruit_set {apple, orange, banana=1, peach, grape}
//枚举常量apple=0,orange=1, banana=1,peach=2,grape=3。
enum week {Sun=7, Mon=1, Tue, Wed, Thu, Fri, Sat};
//枚举常量Sun,Mon,Tue,Wed,Thu,Fri,Sat的值分别为7、1、2、3、4、5、6。

枚举常量只能以标识符形式表示,而不能是整型、字符型等文字常量。例如,以下定义非法:

1
2
enum letter_set {'a','d','F','s','T'}; //枚举常量不能是字符常量
enum year_set{2000,2001,2002,2003,2004,2005}; //枚举常量不能是整型常量

可改为以下形式则定义合法:

1
2
enum letter_set {a, d, F, s, T};
enum year_set{y2000, y2001, y2002, y2003, y2004, y2005};

枚举类型的使用:

定义枚举类型之后,就可以定义该枚举类型的变量,如:

1
color_set1 color1, color2;

亦可类型与变量同时定义(甚至类型名可省),格式如下:

1
enum {Sun,Mon,Tue,Wed,Thu,Fri,Sat} weekday1, weekday2;

3、日志输出平台

(1)C++的写入文件

1
2
3
4
#include<fstream>
ofstream outfile;
outfile.open("./log.txt",ios::out|ios::app);
outfile<<"待写入的内容";

[TOC]

​ 本文总结作者学习的Web服务器项目(WebServer),分析其中的网络通信模块的作用及相关知识点

一、socket的作用

​ TCP/IP协议是在操作系统的内核中实现的,因此操作系统需要实现一组系统调用,使得应用程序能够在用户空间访问这些协议提供的服务。实现这组系统调用的API(Application Programming Interface,应用程序编程接口)就是socket和XTI。

​ socket函数库主要提供如下两点功能:

​ 一是实现应用程序数据的send和read。send是将应用程序数据从用户缓冲区中复制到TCP/UDP内核发送缓冲区,以交付内核来发送数据,read是从内核TCP/UDP接收缓冲区中复制数据到用户缓冲区,以读取数据;

​ 二是精细地控制协议栈的通信行为。应用程序可以通过它们来修改内核中各层协议的某些头部信息或其他数据结构,从而精细地控制底层通信的行为。比如可以通过setsockopt函数来设置IP数据报在网络上的存活时间。

二、socket的用法

1、服务端的用法

(1)创建socket:CreateSocket()函数

作用:创建一个监听套接字

1、函数参数:

2、返回值

该函数创建套接字成功则返回一个套接字,失败则返回-1并设置errno。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<sys/tpyes.h> 
#include<sys/socket.h>

#define PF_INET 2
#define SOCK_STREAM SOCK_STREAM
int m_listenfd;
//创建socket
int CreateSocket(){[
//创建一个监听套接字
m_listenfd = socket(PF_INET, SOCK_STREAM, 0);
if(m_listenfd == -1){
printf(" 创建socket出错: %s(errno:%d)\n" ,strerror(errno),errno);
return -1;
}
else {
printf("创建socket成功\n");
return m_listenfd;
}
}

(2)绑定socket:bind()函数

作用:把一个地址族中的特定socket地址(IP地址+端口号)赋给socket,通常用于服务端程序。

在 Linux 下使用 <sys/socket.h> 头文件中 socket() 函数来创建套接字,原型为:

1
int socket(int af, int type, int protocol);
  1. af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET和AF_INET6 。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。

struct sockaddr_in结构体用来处理网络通信的地址。

image-20220514231421272

端口

socket地址:IP地址+端口号 211.211.1.2:3600

socket关闭的2种方式:

有close 和shutdown两种API,

close —– 在多进程的情况下,关闭本进程的socket,但这只是socket的引用计数减1,用这个socket的其它进程还能用这个链接,能读或写这个socket,直到所有的进程都进行了close,才真正关闭这个套接字,但当它真正执行关闭的时候是完全关闭,既不处理发送也不处理接收数据

shutdown – 即便在多进程的情况下面,也是直接进行关闭的,关闭了socket 文件描述符,其他进程的也会被关闭,但他关闭的时候只关闭一半,即发送数据通道关闭,接收数据还是可以的。如果对写操作被关闭的socket执行写操作会触发写就绪(EPOLLOUT)事件,同时触发一个SIGPIPE信号。

1、函数参数

int port:端口号

int m_listenfd:套接字

2、返回值

该函数绑定socket地址成功则返回true,失败则返回false并设置errno。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include<sys/tpyes.h> 
#include<sys/socket.h>
//绑定socket地址
bool bind(int port,int m_listenfd){
//(一)检查端口号是否合法
if(port> 65535 || port< 1024) {
printf(" Port:%d不符合要求!",port);
return false;
}

//(二)创建一个IPv4的socket地址
struct sockaddr_in address//socket地址
bzero(&address, sizeof(address));//将该地址清空为0
address.sin__family= AF_INET://设置IPv4的地址族
//inet_ pton(AF_ INET,ip,&address.sin_ addr);//手动设置IP地址
address.sin_addr.s_addr = htonl(INADDR_ ANY);// 自动获取本机的IP地址并且设置
address.sin_port = htons(port);//端口号

//(三)绑定socket地址
int flag= 1;
setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));//允许重用本地地址和端口
int ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));//绑定socket地址(IP地址+端口号)

//(四)判断是否绑定成功
if(ret==-1){
printf(" 绑定socket出错: ]%s(errno:%d)\n' ,strerror(errno),errno);
return false;
}
else {
printf("绑定socket成功\n");
return true;
}
}

(3)监听socket

1、函数参数

int backlog

int m_listenfd:套接字

2、返回值

该函数监听socket成功则返回true,失败则返回false并设置errno。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//监听socket
bool Listen(int backlog,int m_listenfd){
//开启监听
int ret = listen(m_listenfd, backlog);
if(ret==-1){
printf(" 监听socket出错: %s(errno:%d)\n",strerror(errno),errno);
return false;
}
else {
printf(" 监听socket成功\n");
return true;
}
}

2、客户端的用法

(1)创建socket

(2)发起连接

三、socket的封装

服务器的socket类

1

[TOC]

单例模式作为最常用的设计模式之一,用来保证一个类仅有一个实例化对象,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

1、单例模式的实现思路

​ 单例模式单例模式的实现思路一般有如下两步:

(1)私有化类的构造函数,以防止外界创建单例类的对象;

(2)使用类的私有静态指针变量指向类的唯一实例,并用一个公有静态方法获取该实例。

​ 根据类的唯一实例初始化的时机,可以将单例模式的实现分为:懒汉模式饿汉模式

懒汉模式:非常懒,不用的时候不去初始化,只在第一次被调用时才进行初始化;

饿汉模式:迫不及待,即使还没有程序调用它,其已提前初始化等待被调用

2、单线程的单例模式

我们实现一个经典的懒汉模式的单例模式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<iostream>
using namespace std;

class Singleton{
protected:
//构造函数被保护,不能被外界访问
Singleton(){cout<<"构造函数执行成功"<<endl;};
private:
//设置一个私有的静态类指针,用来保存全局唯一的实例对象
static Singleton* _instance;//静态成员变量
public:
//设置一个公有的静态成员函数,用来作为外界唯一的创建接口
static Singleton* Instance(){
if(_instance==nullptr)_instance=new Singleton();
return _instance;
};
};
// 全局变量类外声明
Singleton* Singleton::_instance=NULL;

我们实现一个经典的饿汉模式的单例模式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
using namespace std;

class Singleton{
protected:
//构造函数被保护,不能被外界访问
Singleton(){cout<<"构造函数执行成功"<<endl;};
private:
//设置一个私有的静态类指针,用来保存全局唯一的实例对象
static Singleton* _instance;//静态成员变量
public:
//设置一个公有的静态成员函数,用来作为外界唯一的创建接口
static Singleton* Instance(){return _instance;};
};
// 全局变量类外声明,直接实例化
Singleton* Singleton::_instance=new Singleton;

可以看到,所谓的懒汉模式饿汉模式的区别非常容易理解。

3、多线程的单例模式

在多线程中,需要考虑共享资源的安全性,使用互斥锁

(1)经典的线程安全懒汉模式

单例模式有两种实现方法,分别是懒汉模式和饿汉模式。顾名思义,。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<iostream>
#include<pthread.h>
using namespace std;

class Singleton{
protected:
//构造函数被保护,不能被外界访问
Singleton(){
pthread_mutex_init(&lock, NULL);
cout<<"构造函数执行成功"<<endl;
};
private:
//设置一个私有的静态类指针,用来保存全局唯一的实例对象
static Singleton* _instance;//静态成员变量
static pthread_mutex_t lock;//静态锁,是由于静态函数只能访问静态成员
public:
//设置一个公有的静态成员函数,用来作为外界唯一的创建接口
static Singleton* Instance(){
if (_instance==NULL){
pthread_mutex_lock(&lock);
if (_instance==NULL){
_instance = new Singleton();
}
pthread_mutex_unlock(&lock);
}
return _instance;
};
};
// 全局变量类外声明,直接实例化
Singleton* Singleton::_instance=NULL;
pthread_mutex_t Singleton::lock;

为什么要用双检测,只检测一次不行吗?

​ 如果只检测一次,在每次调用获取实例的方法时,都需要加锁,这将严重影响程序性能。双层检测可以有效避免这种情况,仅在第一次创建单例的时候加锁,其他时候都不再符合NULL == p的情况,直接返回已创建好的实例。

(2)局部静态变量之线程安全懒汉模式

​ 前面的双检测锁模式,写起来不太优雅,《Effective C++》(Item 04)中的提出另一种更优雅的单例模式实现,使用函数内的局部静态对象,这种方法不用加锁和解锁操作。它不用加锁是因为在C++11之后,编译器会保证局部静态变量的线程安全性,所以不需要程序员额外为局部静态变量设置线程安全性

1
2
3
4
5
6
7
8
9
10
11
class Singleton{
protected:
//构造函数被保护,不能被外界访问
Singleton(){cout<<"构造函数执行成功"<<endl;};
public:
//设置一个公有的静态成员函数,用来作为外界唯一的创建接口
static Singleton* Instance(){
static Singleton _instance;//静态成员变量
return &_instance;
};
};

类的访问权限

​ c++的访问权限(也叫访问级别):指类外和子类对类内成员的访问权限。c++成员的默认访问权限是private。

访问权限 类外(实例化对象) 类内成员 子类成员 友元函数 友元类
public 可访问 可访问 可访问 可访问 可访问
protected 不可访问 可访问 可访问 可访问 可访问
private 不可访问 可访问 不可访问 可访问 可访问

​ 在c++中,鼓励将所有数据声明为private,想要访问数据则通过单独定义public的成员函数来访问,public为公有的成员函数。

static关键字

​ static是c++的一个限定符,用来控制某个变量的存储方式和可见性。static可以修饰变量(全局变量、局部变量、类成员变量等),类成员函数、普通函数。所有的静态变量都存储在c++的全局数据区,在声明处被初始化,如果没有指明初始值就自动初始化为0,一旦初始化就直至程序结束才释放内存。使用场景如下:

静态全局变量:只能在本文件中访问,不能在其他文件中访问,即便是extern外部声明也不可以(外部可见性缩小)。
静态局部变量在首次执行时初始化,直至程序运行结束后才释放(生命周期延长)。
类的静态成员变量必须在类外声明,且类的多个对象共享同一个静态成员变量。
类的静态成员函数只能调用静态成员变量(函数内没有this指针)。

类的静态成员函数

1)静态成员函数只能调用类的静态成员变量或者静态成员函数。

2)静态成员函数在类外定义时,不能加static修饰,否则出错。

3)在类外可以通过对象名或者类名来调用类的静态成员函数。

4)非静态成员函数可以任意地访问静态成员函数和静态数据成员。

内联函数inline

​ (问题:inline关键字的作用是什么) inline是c++的一个限定符,用来显示声明一个函数为内联函数,类中定义的函数都默认为内联函数。

内联函数可以减少函数调用的开销,提高程序执行的效率。编译器处理内联函数时,不会单独进行函数调用,而是在编译期间直接将整个函数体的代码插入调用语句处,就像整个函数体在调用处被重写了一遍一样,这个过程发生在编译期间。显然使用内联函数会使最终可执行程序的体积增加,因为内联函数是以空间换取时间。

​ (什么时候需要使用inline关键字) 内联函数适用于小而简单、执行很快的函数,如果一个函数非常庞大或者需要消耗大量时间,那么将其声明为内联函数虽然节省了函数调用的时间,但是却让程序体积增加了更多,这样程序执行速度很可能反而会下降。现代c++编译器提供了内联函数的保护机制,一般程序员声明的内联函数只是给编译器的建议,具体编译器是否真的按照内联函数的方式处理可能由内部算法决定。

inline函数在class内定义,

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class complex{
public:
//构造函数名称和类的名称相同
//构造函数不需要返回值(因为是用来创建对象的)
complex(double r=0,double i=0)
:re(r),im(i)//初值列,初始列
{ }
//构造函数也可以这样,效率差些,因为上面的是初始化,下面这种是赋值。一般数据要先初始化,再赋值。
complex(double r=0,double i=0)
{
re=r;
im=i;
}
//常量成员函数,加上const,外界不能改变类里面的数据
double real()const{return re;}
private:
double re,im;
};

private里的,只能在class里使用,数据放在private,想被外界调用放在public

创建对象:complex c1(2,1);

函数重载overloading

参数传递:值传递和引用传递、

返回值传递:return by value return by reference.

friend(友元)

private:

friend complex

相同class的各个对象互为friends友元

操作符重载

[TOC]

​ 本文总结作者学习的Web服务器项目(WebServer),分析其中的主要模块的作用及相关知识点。一个WebServer是一个运行在linux系统上的C++编码的服务器程序,其需要处理来自浏览器程序(客户端)的各种HTTP请求,并对其请求作出HTTP响应。

​ 本项目的主要流程如下:

![](/img-post/项目复现/2022-04-08-Web服务器编程之简介/WebServer的流程图.png)

​ 从上述图中可以看出其中共有7个模块:日志系统模块、监听套接字模块、IO复用模型epoll模块、线程池模块、数据库连接池模块、HTTP连接模块、定时器模块。接下来分别介绍这几个模块的相关知识。

一、日志系统模块

1、项目编译工具MakeFile

2、C++11的智能指针

3、C++11中哈希表的使用

二、监听套接字模块

​ 用户在Web浏览器在浏览器中输入“域名”或“IP地址:端口号”,浏览器则先将你的域名解析成相应的IP地址或者直接根据你的IP地址向对应的Web服务器发送一个HTTP请求。这一过程首先要通过TCP协议的三次握手建立与目标Web服务器的连接,然后HTTP协议生成针对目标Web服务器的HTTP请求报文,通过TCP、IP等协议发送到目标Web服务器上。

​ Web服务器想要建立与浏览器的TCP连接,就需要进行Socket编程

1、Socket编程

(1)服务器创建监听套接字的步骤和API分别是什么?

创建socket【socket()】、绑定socket地址【bind()】,监听socket【listen()】

(2)TCP的断开方式有哪些?本项目中如何设置TCP的断开方式?

(3)socket默认是阻塞的,本项目中为什么将其设置为非阻塞模式?

2、TCP/IP协议

(1)TCP/IP协议的特点是什么,TCP/IP协议与socket的关系?

(2)TCP/IP协议的三次握手与四次挥手是什么?

3、IO复用模型Epoll

4、HTTP协议的特点

5、HTTP请求和HTTP相应的实现

三、IO复用模型epoll模块

四、线程池模块

1、互斥锁

2、条件变量

3、线程池

五、数据库连接池模块

1、数据库连接池

2、RAII机制

5、单例模式的C++实现

8、阻塞队列

六、HTTP连接模块

4、正则表达式的使用

6、状态机

七、定时器模块

7、基于小根堆实现的定时器

[TOC]

​ 华为机试题库共103道题目,其中入门级5道,简单级46道,中等级36道,较难级13道,困难级3道。

相关的API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//将一个字符串中的字符全部转换为小写:
for (int i = 0; i < str.length(); i++)
{
if ('A' <= str[i] && str[i] <= 'Z')
str[i] += 32;
}
// 将一个字符转换为小写:
char c=tolower(ch);
// 在字符串末尾添加n个字符
str.append(n, '0');
// 根据索引获取字符串的子串
int len = str.size();
for (int i = 0; i < len; i += 8){
cout << str.substr(i, 8) << endl;
}
// 分割字符串
char s[]= "Golden~Global_View,disk*desk";//待分割的字符数组
const char *d = "~ ,*";// 分割字符集合
char *p;
p = strtok(s,d);//s必须是char[] 类型
while(p)
{
printf("%s\n",p);
p=strtok(NULL,d);
}

// 将n进制的字符串转换为十进制
#include <string>
int stoi(str,x,n)//将 n 进制的str从索引x开始转化为十进制的整数

// 字符串转换为char []
char ch[20];
string s="123456";
strcpy(ch,s.c_str());
//计算x的y次幂
pow(x,y);
//将数字常量转换为字符串
#include<string>
int n;
cin>>n;
string s=to_string(n);
//字符串的长度
s.length();
//将字符串逆序
#include<algorithm>
reverse(s.begin(),s.end());//reverse函数的参数应该是迭代器?
//获取字符串的子串
s.substr(3,5);//表示索引3开始的5个字符
//sort()函数
//sort默认按照字典序排序,即按照ASCII的顺序排序:数字,大写英文字母,小写英文字母
#include <algorithm>//sort位于这个头文件中
#include<vector>
vector<int> arr;
sort(arr.begin(),arr.end());//默认做升序排序

数字字符与数字的转换的两种方法:

第一种:

​ 用数字字符出减去’0’。例如:’1’-‘0’=1  (它俩是用ASCII码相减的。即49-48=1)

1+’0’=’1’

第二种:

​ 用数字字符出减去48。(48是‘0’的ASCII码)例如:’1’-48=1。

ASCII码的大小规则

常见ASCII码的大小规则:09<AZ<a~z

0:48 A:65 a:97

1)数字比字母要小。如 “7”<“F”;

2)数字0比数字9要小,并按0到9顺序递增。如 “3”<“8” ;

3)字母A比字母Z要小,并按A到Z顺序递增。如“A”<“Z” ;

4)同个字母的大写字母比小写字母要小32。如“A”<“a” 。

几个常见字母的ASCII码大小: “A”为65;“a”为97;“0”为 48 [4] 。

在英语中,用128个符号编码便可以表示所有,但是用来表示其他语言,128个符号是不够的。

字符串流

1
2
3
#include <sstream>// 字符串流
string s;
stringstream ss(s);//将字符串转换为字符串流

字符串流是以内存中用户定义的字符串或字符数组为输入输出对象,也称为内存流 .

c++正则表达式

regex_match: match是全文匹配,即要求整个字符串符合匹配规则。

将逆序字符串的函数

1、直接调用函数

1
2
3
#include<algorithm>
#include<string>
reverse(s.begin(),s.end());

2、编写逆序函数

1
2
3
4
5
6
7
8
#include<string>
void reverseStr(string &str,int start,int end){
int left=start;
int right=end;
while(left<right){
swap(str[left++],str[right--]);
}
}

按‘ 空格 ’分割字符串

并将分割出每个子字符串存到一个数组中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void split(string &str,vector<string> &arr){
int left=0;
int len=str.length();
while(left<len){
// left找到了第一个英文字符
while(str[left]==' '&&left<len)left++;
int right=left;
// right找到单词后的第一个空格
while(str[right]!=' '&&right<len)right++;
//[left,right-1]
arr.push_back(str.substr(left,right-left));
//reverseStr(str,left,right-1);
left=right;
}
}

判断字符是否为大小写英文字母

1、直接使用现有的函数

1
2
3
#include<string>
string s;
isalpha(s);//返回值为false或true

2、

1
2
3
4
5
6
7
8
9
bool is_A(char c){
if(c>='A'&&c<='Z')return true;
return false;
}

bool is_a(char c){
if(c>='a'&&c<='z')return true;
return false;
}

动态数组:添加

vector arr;

arr.push_back();

c++的输出格式

1.转换说明符

%f 浮点数(包括float和double)

​ %c 字符
%d 有符号十进制整数

%i 有符号十进制整数(与%d相同)
%u 无符号十进制整数

%s 字符串

2、格式字符: 用以指定输出项的数据类型和输出格式。

f格式:

%f:不指定宽度,整数部分全部输出并输出6位小数。
%m.nf:输出共占m列,其中有n位小数,如数值宽度小于m左端补空格。
%-m.nf:输出共占n列,其中有n位小数,如数值宽度小于m右端补空格。

sort函数

1
2
3
4
5
6
7
8
9
10
11
//sort默认按照字典序排序,即按照ASCII的顺序排序:数字,大写英文字母,小写英文字母
#include <algorithm>//sort位于这个头文件中
#include<vector>
vector<int> arr;
sort(arr.begin(),arr.end());//默认做升序排序

//降序排序
sort(arr.begin(),arr.end(),cmp);
bool cmp(int a,int b){
return a>b;
}

双指针法删除数组中出现次数最少的元素

( 若出现次数最少的字符有多个,则把出现次数最少的字符都删除。 )

1
2
3
4
5
6
7
8
9
10
11
12
13
string s;
int left=0;
int right=0;
int n=s.length();
while(right<n){
if(dict[s[right]]==min_q){//min_q是出现的最小次数
right++;
}
else{
s[left++]=s[right++];
}
}
cout<<s.substr(0,left);

链表结点的定义

1
2
3
4
5
6
7
8
struct ListNode{
int m_nKey;
ListNode* m_pNext;
ListNode(int val){
m_nKey=val;
m_pNext=NULL;
}
};

创建链表

1
2
3
4
5
6
7
8
9
10
11
ListNode *myNode=new ListNode(0);
ListNode *p=myNode;//遍历指针
int temp=0;
//创建链表
while(n--){//n为链表节点个数

cin>>temp;
ListNode *newNode=new ListNode(temp);
p->next=newNode;
p=p->next;
}

找链表中倒数第k个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int k;
//头结点
ListNode *myCode=new ListNode(0);、
//left,right都指向第一个节点(头节点的下一个结点)
ListNode *left=myCode->m_pNext;
ListNode *right=myCode->m_pNext;
//right先走k步
while(k--){
right=right->m_pNext;
}
//left和right一起走,直到right指向NULL
while(right){
left=left->m_pNext;
right=right->m_pNexst;
}
cout<<left->m_nKey<<endl;

判断字符是否大小写字母、数字等的库函数

  1. isupper()判断一个字符是否是大写字母

  2. isalpha()判断一个字符是否是字母

  3. isblank()判断一个字符是否是空白符

  4. isdigit()判断一个字符是否是十进制数字

  5. islower()判断一个字符是否是小写字母

  6. isspace()判断一个字符是否是空白符

  7. tolower()将大写字母转换为小写字母

  8. toupper()将小写字母转换为大写字母

十进制转二进制

输入描述:

输入一个整数

输出描述:

计算整数二进制中1的个数

输入:

1
5

复制

输出:

1
2

复制

说明:

1
5的二进制表示是101,有2个1   
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
using namespace std;

int main(void){
int n;
while(cin>>n){
int m=0;
while(n){
m+=n%2;
n=n/2;
}
cout<<m<<endl;
}


return 0;
}

unordered_map查找某个元素是否存在的函数

1
2
3
4
unordered_map<string,int> mp;
string s;
mp.count(s)//返回值为0:不存在;1:存在。
mp.find(s)==mp.end();//mp中不存在s

按要求对哈希表排序

输入一个只包含小写英文字母和数字的字符串,按照不同字符统计个数由多到少输出统计结果,如果统计的个数相同,则按照ASCII码由小到大排序输出。

方法一:使用自定义sort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<bits/stdc++.h>
using namespace std;
//函数参数加const,可以防止参数被改变
//函数参数加&,效率更高
bool cmp(const pair<char,int>& a,const pair<char,int>& b){
if(a.second!=b.second){
return a.second>b.second;
}else{
return a.first<b.first;
}
}

int main(void){
string s;
getline(cin,s);
unordered_map<char,int> mp;// k-v
for(int i=0;i<s.size();i++){
mp[s[i]]++;
}
vector<pair<char,int>> arr;
for(auto iter=mp.begin();iter!=mp.end();++iter){
arr.push_back(make_pair(iter->first, iter->second));
}
sort(arr.begin(),arr.end(),cmp);// 从大到小
for(auto p:arr){
cout<<p.first;
}
return 0;
}

方法二:优先级队列和仿函数(其实是个类)

仿函数的operator()是函数名。

priority_queue<pair<char,int>,vector<pair<char,int>>,mycomp> que;的<>里的第一个是队列元素类型,第二个是把队列元素放到指定容器中进行排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <bits/stdc++.h>
using namespace std;

class mycomp{
public:
bool operator()(const pair<char,int>& a,const pair<char,int>& b){
if(a.second==b.second){
return a.first>b.first;
}
else{
return a.second<b.second;
}
}
};

int main(){
// 获取输入
string s;
unordered_map<char,int> dict;
priority_queue<pair<char,int>,vector<pair<char,int>>,mycomp> que;
while(getline(cin, s)){
for(char c:s) dict[c]++;//统计长字符串中的所有字符频率
// 遍历
for(auto p:dict){
que.push(p);
}
// 输出
while(!que.empty()){
auto p=que.top();
que.pop();
cout<<p.first;
}
cout<<endl;
}
}

十进制数字转为二进制字符串

1
2
3
4
5
6
7
8
int n;
cin>>n;
string s;
//转为二进制
while(n){
s=to_string(n%2)+s;
n/=2;
}

合并两个有序数组

将两个整型数组按照升序合并,并且过滤掉重复数组元素。

输出时相邻两数之间没有空格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//合并两个有序数组
string res;
int i=0,j=0;
unordered_set<int> set;//哈希集合,用来去重
while(i<s1.size()&&j<s2.size()){
if(s1[i]<=s2[j]){
if(!set.count(s1[i])){//不重复的才添加
set.insert(s1[i]);
res+=to_string(s1[i]);
}
i++;
}
else if(s1[i]>s2[j]){//不重复的才添加
if(!set.count(s2[j])){
set.insert(s2[j]);
res+=to_string(s2[j]);
}
j++;
}
}
//数组s1没有比较完
while(i<s1.size()){
if(!set.count(s1[i])){
set.insert(s1[i]);
res+=to_string(s1[i]);
}
i++;
}
//数组s2没有比较完
while(j<s2.size()){
if(!set.count(s2[j])){
set.insert(s2[j]);
res+=to_string(s2[j]);
}
j++;
}
cout<<res;

判断是否为回文子串

1
2
3
4
5
6
7
8
9
//判断是否是回文子串
bool is_true(string &s){
int left=0;
int right=s.size()-1;
while(left<=right){
if(s[left++]!=s[right--])return false;
}
return true;
}

遍历所有的子串

1
2
3
4
5
6
7
8
//定义动态数组
int len=s.size();
for(int i=0;i<len;i++){
for(int j=1;j<len-i+1;j++){
string str=s.substr(i,j);
//对字串进行处理
}
}

[TOC]

​ 华为机试题库共103道题目,其中入门级5道,简单级46道,中等级36道,较难级13道,困难级3道。

相关的API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//将一个字符串中的字符全部转换为小写:
for (int i = 0; i < str.length(); i++)
{
if ('A' <= str[i] && str[i] <= 'Z')
str[i] += 32;
}
// 将一个字符转换为小写:
char c=tolower(ch);
// 在字符串末尾添加n个字符
str.append(n, '0');
// 根据索引获取字符串的子串
int len = str.size();
for (int i = 0; i < len; i += 8){
cout << str.substr(i, 8) << endl;
}
// 分割字符串
char s[]= "Golden~Global_View,disk*desk";//待分割的字符数组
const char *d = "~ ,*";// 分割字符集合
char *p;
p = strtok(s,d);//s必须是char[] 类型
while(p)
{
printf("%s\n",p);
p=strtok(NULL,d);
}

// 将n进制的字符串转换为十进制
#include <string>
int stoi(str,x,n)//将 n 进制的str从索引x开始转化为十进制的整数

// 字符串转换为char []
char ch[20];
string s="123456";
strcpy(ch,s.c_str());

一、入门级

HJ46 截取字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
#include<string>
using namespace std;

int main(){
// 获取输入
string s;
getline(cin,s);
int k;
cin>> k;
// 返回结果
cout<<s.substr(0,k);
return 0;
}

HJ 58 输入n个整数,输出其中最小的k个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;

int main(){
int n,k;
cin>>n>>k;
int temp;
vector<int> arr;
while(n--){
cin>>temp;
arr.push_back(temp);
}
sort(arr.begin(),arr.end());
for(int i=0;i<k;i++){
cout<<arr[i]<<" ";
}
return 0;
}

HJ101 输入整型数组和排序标识,对其元素按照升序或降序进行排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<iostream>
#include<vector>
#include<bits/stdc++.h>
using namespace std;

bool cmp(int a,int b){
return a>b;
}
int main(){
int n;
cin>>n;
vector<int> arr;
int i;
while(n--){
cin>>i;
arr.push_back(i);
}
int b;
cin>>b;
if(b==0){
sort(arr.begin(),arr.end());
}
else if(b==1){
sort(arr.begin(),arr.end(),cmp);
}

for(int i=0;i<arr.size();i++){
cout<<arr[i]<<' ';
}
return 0;
}

二、简单级

1、HJ1 字符串最后一个单词的长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<iostream>
#include<string>
using namespace std;

int main(){
// 获取输入
string input;
getline(cin,input);
// 计算结果
int res=0;
// 从最后一个字符向前走
int i=input.length()-1;
while(i>=0&&input[i]!=' '){
res++;
i--;
}
cout<<res;
return 0;
}

2、HJ2 计算某字符出现次数

题目要求不区分字母大小写

方法一:将目标字符和字符串中的每个字符都转换为小写后遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <string>

using namespace std;
int main(){
// 获取输入
string s;
getline(cin, s);//不用cin,因为cin默认截断符是空格,tab,enter等空白字符。有个用例的字符串中包含空格
char c;
cin>>c;
// 计算结果
c=tolower(c);
int res = 0;
for (char i : s) {
if (tolower(i) == c) {
++res;
}
}
cout <<res;
}


方法二:转换为小写加入哈希表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>
#include<string>
#include<unordered_map>
using namespace std;

int main()
{
// 获取输入
string s;
getline(cin, s);
char c;
cin>>c;
// 计算结果
c=tolower(c);
//在声明诸如字符数或者数组索引这样的长度变量时用size_t 是好的做法。size_t 类型表示C/C++中任何对象所能达到的最大长度,它是无符号整数。
unordered_map<char, size_t> dict;
for (auto i : s) {
++dict[tolower(i)];
}
cout << dict[tolower(c)] << endl;
}

3、HJ4 字符串分隔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>
#include<string>
using namespace std;

int main(){
string str;
getline(cin,str);

int len=str.size();

if(len%8!=0){
int count=8-len%8;
str.append(count,'0');
}
//此时字符串长度是8的倍数,每8个字符输出即可
int newLen=str.size();
for(int i=0;i<newLen;i+=8){
cout<<str.substr(i,8)<<endl;
}
return 0;
}

4、HJ5 进制转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<iostream>
#include<math.h>
#include<string>
using namespace std;

int main(){
string s; //0xAA
cin>>s;
// 方法一:cout<<stoi(s,0,16)<<endl;
int n=s.size();
int res=0;
// 从后向前遍历,0x不用转换,忽略即可
for(int i=n-1;i>1;i--)
{
int cur=0;
// 如果当前数是0-9,则res+=n^(n-i-1)
if(s[i]>='0'&&s[i]<='9'){
cur=s[i]-'0';
}
// 如果当前数是A-F,则res+=n^(n-i-1)
if(s[i]>='A'&&s[i]<='F'){
cur=s[i]-'A'+10;
}
res+=cur*pow(16,n-i-1);
}
cout<<res<<endl;
return 0;
}

5、HJ10 字符个数统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
#include<string>
#include<unordered_map>
using namespace std;
int main(){
string s;
cin>>s;
unordered_map<int,int> dict;
for(char c:s){
dict[c]++;
}
cout<<dict.size()<<endl;
return 0;
}

6、HJ8 合并表记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
#include<map>
using namespace std;

int main(){
int n;
cin>>n;
map<int,int>dict;
while(n--){
int key,value;
cin>>key>>value;
dict[key]+=value;
}
for(auto kv:dict){
cout<<kv.first<<" "<<kv.second<<endl;
}
return 0;
}

7、HJ12 字符串反转

直接用函数进行字符串反转

1
2
#include<algorithm>
reverse(s.begin(),s.end());

考察自己写字符串反转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
#include<string>
using namespace std;

int main(){
string s;
getline(cin,s);

int i=0,j=s.length()-1;
while(i<j){
swap(s[i],s[j]);
i++;
j--;
}
cout<<s;
return 0;
}

8、 HJ13 句子逆序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include<iostream>
#include<algorithm>
using namespace std;

//逆序字符串
void reverseStr(string &str,int start,int end){
int left=start;
int right=end;
while(left<right)swap(str[left++],str[right--]);
}
//字符串中每个单词拿出来,并将每个单词逆序
void split(string &str){
int left=0;
int len=str.length();
while(left<len){
while(left<len&&str[left]==' ')left++;
int right=left;
while(right<len&&str[right]!=' ')right++;
reverseStr(str,left,right-1);
left=right;
}
}
int main(){
//获取输入
string s;
while(getline(cin,s)){
//字符串中每个单词拿出来,并将每个单词逆序
split(s);
//整体都逆序
reverse(s.begin(),s.end());
cout<<s;
}
return 0;
}

9、 HJ106 字符逆序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
#include<string>
using namespace std;

void reverseStr(string &str,int start,int end){
int left=start;
int right=end;
while(left<right)swap(str[left++],str[right--]);
}
int main(){
string s;
getline(cin,s);
reverseStr(s,0,s.length()-1);
cout<<s;
return 0;
}

10、HJ14 字符串排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;

int main(){
string s;
int k;
cin>>k;
vector<string> arr;
while(k--){
cin>>s;
arr.push_back(s);
}
//按照字典序排列的字符串。sort需要头文件#include<algorithm>
sort(arr.begin(),arr.end());
for(int i=0;i<arr.size();i++){
cout<<arr[i]<<endl;
}
return 0;
}

11、 HJ31 单词倒排

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include<iostream>
#include<algorithm>
#include<vector>
#include<string>
using namespace std;

void reverseStr(string &str,int start,int end){
int left=start;
int right=end;
while(left<right){
swap(str[left++],str[right--]);
}
}

void split(string &str,vector<string> &arr){
int left=0;
int len=str.length();
while(left<len){
// left找到了第一个英文字符
while(str[left]==' '&&left<len)left++;
int right=left;
// right找到单词后的第一个空格
while(str[right]!=' '&&right<len)right++;
//[left,right-1]
arr.push_back(str.substr(left,right-left));
//reverseStr(str,left,right-1);
left=right;
}
}

bool is_A(char c){
if(c>='A'&&c<='Z')return true;
return false;
}

bool is_a(char c){
if(c>='a'&&c<='z')return true;
return false;
}

void update(string &str){
for(int i=0;i<str.size();i++){
if(!isalpha(str[i])) str[i]=' ';
//if(!is_A(str[i])&&!is_a(str[i]))str[i]=' ';
}
}
int main(){
// 获取输入
string s;
getline(cin,s);
// 将所有的间隔符统一替换为空格
update(s);
// 分割
vector<string> arr;
split(s,arr);
reverse(arr.begin(),arr.end());
for(string s:arr){
cout<<s<<" ";
}
return 0;
}

12、HJ34 图片整理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

int main(){
// 获取输入
string s;
getline(cin, s);
// 排序
sort(s.begin(), s.end());
cout <<s;
return 0;
}

13、HJ40 统计字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <string>
using namespace std;

int main(){
// 获取输入
string s;
getline(cin, s);
// 排序
int a=0;
int A=0;
int space=0;
int num=0;
int other=0;
for(char c:s){
if(c>='A'&&c<='Z') A++;
else if(c>='a'&&c<='z') a++;
else if(c==' ') space++;
else if(c>='0'&&c<='9') num++;
else other++;
}
cout <<A+a<<endl;
cout <<space<<endl;
cout <<num<<endl;
cout <<other<<endl;
return 0;
}

14、HJ23 删除字符串中出现次数最少的字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include<iostream>
#include<string>
#include<unordered_map>
using namespace std;

int main(){
// 获取输入
string s;
getline(cin,s);
// 统计频率
unordered_map<char, int> dict;
for(char c:s){
dict[c]++;
}
int min_q=20;
for(auto it:dict){
if(it.second<min_q) min_q=it.second;
}
// 从最后一个字符向前走
int left=0;
int right=0;
int n=s.length();
while(right<n){
if(dict[s[right]]==min_q){
right++;
}
else{
s[left++]=s[right++];
}
}
cout<<s.substr(0,left);
return 0;
}

15、HJ81 字符串字符匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;

int main(){
// 获取输入
string s1;
string s2;
unordered_map<char,int> dict;
while(getline(cin, s1)){
getline(cin, s2);
bool flag=true;
for(char c:s2) dict[c]++;//统计长字符串中的所有字符频率
// 遍历短字符串
for(char c:s1){
// 如果有一个字符在字典中不存在则结果为false
if(dict.count(c)==0){
flag=false;
break;
}
}
if(flag) cout<<"true"<<endl;
else cout<<"false"<<endl;
}
}

16、 HJ56 完全数计算

完全数(Perfect number),又称完美数或完备数,是一些特殊的自然数。

它所有的真因子(即除了自身以外的约数)的和(即因子函数),恰好等于它本身。

例如:28,它有约数1、2、4、7、14、28,除去它本身28外,其余5个数相加,1+2+4+7+14=28

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<iostream>
using namespace std;

int main(void){
int n;
cin>>n;
int count=0;
for(int i=6;i<=n;i++){
int temp=0;
for(int j=1;j<i;j++){//j<n,因为求和时不算这个数的自身
if(i%j==0)
temp+=j;
}
if(temp==i){
count++;
}
}
cout<<count<<endl;
return 0;
}

17、HJ99 自守数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>
using namespace std;

int main(){
int n;
while(cin>>n){
int ans=0;
for(int i=1;i<=n;i++){
if(i%10==1||i%10==5||i%10==6){ //找到可能成为自守数的数
int temp1=i, temp2=i*i;
while(temp1){//验证后几位
if(temp2%10!=temp1%10) break;
temp1/=10;
temp2/=10;
}
if(temp1==0) ans++;//保证是满足条件才跳出的循环
}
}
cout<<ans+1<<endl;//带上0
}
return 0;
}

18、HJ6 质数因子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<bits/stdc++.h>
using namespace std;
int main()
{
long m;
while(cin>>m)
{
for(long i=2;i*i<=m;i++)
{
while(m%i==0)
{
cout<<i<<" ";
m/=i;
}
}
if(m>=2) cout<<m<<" ";//如果m=1,则while循环中刚好被质数分解完,如果大于1,说明没有被分解完,m就是那最后一个质数
//同时,这句也可以应对输入为质数的特殊情况
cout<<endl;
}
}

19 HJ60 查找组成一个偶数最接近的两个素数

质数(也叫素数)是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。

查找组成偶数的最近两个素数

1.从偶数的一半开始查找;
2.判断两个数是否为素数;
3.如果是素数,则即为所求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
using namespace std;

bool isPrime(int n)
{
int i=0;
for(i=2;i*i<=n;i++)
{
if(n%i==0) return false;
}
return true;
}

int main()
{
int n;
while(cin>>n)
{
int left=n/2;
int right=n/2;
while(left>=2)
{
if(isPrime(left)&&isPrime(right))
{
cout<<left<<endl;
cout<<right<<endl;
break;
}
else
{
left--;
right++;
}
}
}
return 0;
}

20、 HJ94 记票统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include<iostream>
#include<vector>
#include<unordered_map>
using namespace std;

int main(void){
int n;
cin>>n;
string s;
vector<string>people;
unordered_map<string,int> mp;
for(int i=0;i<n;i++){
cin>>s;
people.push_back(s);
mp[s]=0;
}
int n2;
cin>>n2;
int Invalid=0;
for(int i=0;i<n2;i++){
cin>>s;
if(mp.count(s)!=0){
mp[s]++;
}
else{
Invalid++;
}
}
for(int i=0;i<n;i++){
cout<<people[i]<<" : "<<mp[people[i]]<<endl;
}
cout<<"Invalid"<<" : "<<Invalid<<endl;
return 0;
}
0%