Effective Modern C++
第一章 新特性
类型推导
auto
关键字:隐式定义,也是强类型定义。在编译期让编译器自动推断出变量类型以便分配内存,必须在定义时进行初始化decltype
关键字:获取表达式的类型typedef
重定义一个模板需要借助外敷类,但是using
别名语法覆盖了typedef
全部功能。使用using
重定义模板会更简洁,定义函数指针会更加清晰。
template <typename Val>
struct str_map
{
typedef std::map<std::string, Val> type;
};
// 借助外敷类
str_map<int>::type map1;
=========================
template <typename Val>
using str_map_t = std::map<std::string, Val>;
// 直接重定义一个模板
str_map_t<int> map1;
typedef void(*func_ptr)(int a); //可以把func_ptr理解为一个类
using func_ptr = void(*)(int a); //等价上一条
基于范围的循环
for_each()
函数可用于很多容器类,它接受3个参数。前面两个是定义容器中区间的迭代器,最后一个是指向函数的指针(是函数对象)for_each()函数将被指向的函数应用于容器区间的各个元素。被指向的函数不能修改容器元素的值(值传递)。可以用for_each()来代替for循环。
void f(int n) { //函数对象
...
}
vector<int> v; //容器
for_each(v.begin(), v.end(), f); //只读模式
for
循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
for (auto &ch : myvector){ //使用引用可以修改容器内的值
ch++;
}
for (auto ch : myvector){ //只读模式
cout << ch;
}
std::function和bind绑定器
可调用对象有如下几种定义:
- 是一个函数指针。
- 是一个具有
operator()
成员函数的类对象(仿函数)。 - 是一个可被转换为函数指针的类对象。
- 是一个类成员(函数)指针。
std::function
可以取代函数指针的作用
std::bind
用来将可调用对象与其参数一起进行绑定成一个仿函数或者将多元(参数个数为n
,n>1
)可调用对象转成一元或者(n-1
)元可调用对象,只绑定部分参数。
lambda表达式
lambda
表达式定义了一个匿名函数,并且可以捕获一定范围内的变量:[ capture ] ( params ) opt -> ret { body; };
- capture 是捕获列表;
- [] 不捕获任何变量。
- 捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
- 捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。
- [=, &foo] 按值捕获外部作用域中所有变量,并按引用捕获 foo 变量。
- [bar] 按值捕获 bar 变量,同时不捕获其他变量。
- [this] 捕获当前类中的 this 指针,让 lambda 表达式拥有和当前类成员函数同样的访问权限。如果已经使用了 & 或者 =,就默认添加此选项。捕获 this 的目的是可以在lamda 中使用当前类的成员函数和成员变量。
- params 是参数表;
- opt 是函数选项;
- ret 是返回值类型;编译器根据 return 语句可以自动推导出返回值类型,允许省略返回值。
- body 是函数体。
auto f = [](int a) -> int { return a + 1; };
std::cout << f(1) << std::endl;
第二章 改进程序性能
右值引用
- 左值是指表达式结束后依然存在的持久对象,右值(
&&
)是指表达式结束时就不再存在的临时对象,右值由两个概念构成,一个是将亡值(xvalue, expiring value
),另一个则是纯右值(prvalue, PureRvalue
),C++11
中所有的值必属于左值、将亡值、纯右值三者之一。 - 右值引用绑定了右值,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。可以通过右值引用做性能优化,避免临时对象的拷贝构造和析构。
#include <iostream>
using namespace std;
void printValue(int &i)
{
cout << "l-value: " << i << endl;
}
void printValue(int &&i)
{
cout << "r-value: " << i << endl;
}
void forward(int &&k)
{
printValue(k);
}
int main()
{
int i = 520;
printValue(i); // 输出结果l-value: 520
printValue(1314); // 输出结果r-value: 1314
forward(250); // 输出结果l-value: 250
return 0;
};
Move语义
std::move
可以将一个左值转换成右值,移动语义可以直接使用临时对象已经申请的资源,既能节省资源,又能节省资源申请和释放的时间(避免无谓的深拷贝)。std::move()
将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝,move
对拥有内存、文件句柄等资源的对象有效,如果是POD类型,仍然会发生拷贝。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int main()
{
string str = "hello";
cout << "before str: " << str << endl;
vector<string> vstr;
vstr.emplace_back(std::move(str)); // str变为空了,原来资源所有权转移到vstr
cout << "after str: " << str << endl;
return 0;
}
注意: 编译器会将已命名的右值引用视为左值,将未命名的右值引用视为右值
forward 和完美转发
在函数内部转发形参时,该参数已经变成了一个左值,并不是原本的类型。完美转发(Perfect Forwarding
)能够按照参数原本的类型转发到另一个函数,保持参数的左值、右值特征。
emplace_back 减少内存拷贝和移动
emplace_back
能就地通过构造函数的参数构造对象,不需要拷贝或者移动内存,对象有对应的构造函数,如果没有对应的构造函数,编译器会报错。相比 push_back
能更好地避免内存的拷贝与移动,使容器插入元素的性能得到进一步提升。
unordered container 无序容器
有 序 容 器 map/multimap
和 set/multiset
内部是红黑树,在插入元素时会自动排序,而无序容器内部是散列表(Hash Table
),通过哈希(Hash
),而不是排序来快速操作元素,使得效率更高。对于基本类型来说,不需要提供 Hash 函数和比较函数,用法上和 map/set 一样,对于自定义的结构体,需要提供函数和比较函数。
第三章 基础用法
函数加修饰符
- 在一般的函数前面加上
static
- 作用: 加了
static
后表示该函数失去了全局可见性,只在该函数所在的文件作用域内可见 。 - 原因:当函数声明为
static
以后,编译器在该目标编译单元内只含有该函数的入口地址,没有函数名,其它编译单元便不能通过该函数名来调用该函数。
- 作用: 加了
- 括号后面加const
- 作用:用于申明类中的成员函数为只读函数,即函数后面加了const之后该函数不能改变类的数据成员(唯一的例外是对于
mutable
修饰的成员)。 - 只有非静态成员函数后面可以加
const
,加到非成员函数或静态成员后面会产生编译错误。 - 加了
const
的成员函数可以被非const
对象和const
对象调用,加了const
的成员函数可以被非const
对象和const
对象调用
- 作用:用于申明类中的成员函数为只读函数,即函数后面加了const之后该函数不能改变类的数据成员(唯一的例外是对于
#include<iostream>
using namespace std;
class Test
{
public:
Test(int a) {this->a = a;}
static void print_static() //const //1.错误
{
cout<< "静态成员函数" <<endl;
}
void print_const() const
{
//a++; //2.错误
cout << "加const的成员函数" <<endl;
}
void print()
{
cout<< "普通成员函数" <<endl;
}
private:
int a;
};
int main()
{
const Test t1(10);
Test t2(20);
t1.print_const();
//t1.print(); //3.错误 const对象不能调用非const函数
t2.print_const();
t2.print();
return 0;
}
this指针
this
是 C++ 中的一个关键字,也是一个 const
指针,它指向当前对象,通过它可以访问当前对象的所有成员。
class Student{
public:
void setname(char *name);
private:
char *name;
};
// 成员函数的参数和成员变量重名,只能通过 this 区分。成员函数setname(char *name)的形参是name,和成员变量name重名,如果写作name = name;这样的语句,就是给形参name赋值,而不是给成员变量name赋值。
void Student::setname(char *name){
this->name = name;
}
两个类头文件互相引用
两个类不能互相include
对方的头文件,因为两个类相互引用,不管哪个类在前面,都会出现有一个类未定义的情况,所以可以提前声明一个类,而类的声明就是提前告诉编译器,所要引用的是个类,但此时后面的那个类还没有定义,因此无法给对象分配确定的内存空间,因此只能使用类指针。
//A.h
#include "B.h"
class A
{
B b;
};
//B.h
#include "A.h"
class B
{
A a;
};
通过声明一个不完全的类,且只能使用类指针,不能定义对象实体。
//B.h
class A; //声明一个不完全的类
class B;
{
//如果直接定义A类的对象,则编译器会报错的,因为是一个不完整定义的类
//A a; 错误
//必须 智能声明一个之前A的指针,这样只是一个指针,而不需要知道A 是否定义。
A * a;
/*还有如果定义成员函数的话,用于返回类型、参数类型也是可以,但不能直接在 B类里直接定义函数,
需要在定义完A类的定义 之后才能跟在后面 外部定义函数。*/
A func(A); //这样也是可行的。
//所以 直接声明什么的无所谓,只要不是直接定义一个实体对象出来就可以。
}
使用技巧
- 删除指向派生类的基类指针:如果基类里有虚函数,然后定义了基类指针指向派生类,就需要定义基类的虚析构函数,这样,基类指针析构的时候,就会首先析构派生类,再析构基类。
- constexpr目的是将运算尽量放在编译阶段,而不是运行阶段。constexpr可以修饰函数、结构体。
- enum:不限范围的枚举型别
- enum class:限定作用域的枚举类
单字节数据与其他数据类型
- 把数据分成一个一个字节
#define BYTE0(data) (*(char*)(&data))
#define BYTE1(data) (*((char*)(&data) + 1))
#define BYTE2(data) (*((char*)(&data) + 2))
#define BYTE3(data) (*((char*)(&data) + 3))
#define BYTE4(data) (*((char*)(&data) + 4))
#define BYTE5(data) (*((char*)(&data) + 5))
#define BYTE6(data) (*((char*)(&data) + 6))
#define BYTE7(data) (*((char*)(&data) + 7))
- 把多个字节组合成其他数据类型
uint8_t resolutionData[4];
float resolution = *(float*)resolutionData;
第四章 智能指针解决内存泄漏
智能指针
std::shared_ptr
、std::uniqe_ptr
、std::weak_ptr
是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保在离开指针所在作用域时,通过使用引用计数自动正确地销毁动态分配的对象,防止内存泄漏,使用时需要引用头文件<memory>
。
std::shared_ptr
使用引用计数,每一个shared_ptr
的拷贝都指向相同的内存,在最后一个析构时内存才会被释放。
std::shared_ptr<int> p(new int(1)); //智能指针的初始化
std::shared_ptr<int> p2 = p; //智能指针的赋值
std::shared_ptr<int> ptr; //对于未初始化的智能指针,可以通过reset初始化
ptr.reset(new int(1));
std::shared_ptr<int> p = new int(1); //编译报错,不允许直接赋值
malloc()函数动态内存分配
malloc
函数只能返回第一个字节的地址,这个是系统规定的,第一个字节的地址没有实际含义。因为不能根据第一个字节地址来确定这个变量占了几个字节。因为整形变量占四个字节,它也是以第一个字节地址来表示的,如果这个变量是double
类型的话,它也是以第一个字节地址来表示的。无论这个变量占几个字节它都是以第一个字节来表示的,所以malloc
返回的第一个字节地址是没有实际意义的地址(干地址)。所以我们需要在 malloc(sizeof(int)*len)
的前面加一个强制类型转换 (int*)malloc(sizeof(int)*len)
来告诉我们的编译器,我们返回的第一个字节地址到底是整型的地址,还是其他类型的地址。所以强制转换成 int *
的话,那么我们的 pArr+1
的话那么它将会指向后面的一个 int *
的位数,会指向后四位,如果强制类型是 double *
的话, pArr + 1
将会指向后八位的地址。
int * pArr = (int *)malloc(sizeof(int) * len); //这里要使用强制类型转化
//do something
free(pArr); //把pArr所代表的动态分配的20个字节内存释放
第五章 多线程
线程的创建
用std::thread
创建线程需要提供线程函数和函数对象,同时可以指定线程函数的参数,线程不可以复制,但可以移动std::move()
。
#include <thread>
void func( int id, int numIter )
{
//函数执行体
}
void main1() {
std::thread t1( func, 1, 6 );
t1.join(); //**如果不希望线程被阻塞执行,使用detach()可以将线程和线程对象分离**
}
join()
会阻塞线程,直到线程函数执行结束detach()
将线程和线程对象分离,但是线程对象无法再和线程发生联系,无法控制线程结束
线程的基本用法
- 获取当前信息:
thread_name.get_id()
- 线程休眠:
std::this_thread::sleep_for(std:;chrono::seconds(3))
互斥量
互斥量(mutex
)是一种同步原语,是一种线程同步的手段,用来保护多线程同时访问的共享数据。
std::mutex
,独占的互斥量,不能递归使用。一个线程多次获取同一个互斥量会发生死锁std::time_mutex
,带超时的独占互斥量,不能递归使用。std::recursive_mutex
,递归互斥量,不带超时功能。允许同一个线程多次获得该互斥锁std::recursive_timed_mutex
,带超时的递归互斥量
一般用法是通过lock()
方法阻塞线程,直到获得互斥量的所有权,在完成任务后通过unlock()
解除对互斥量的占用。tyr_lock()
尝试锁定互斥量,是非阻塞的。使用lock_guard
可以简化写法,同时更安全(在构造时会自动锁定互斥量,在退出作用域时会自动解锁,保证了互斥量的正确操作)
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
using namespace std;
mutex mt;
void thread_task()
{
for (int i = 0; i < 10; i++)
{
lock_guard<mutex> guard(mt);
cout << "print thread: " << i << endl;
}
}
int main()
{
thread t(thread_task);
for (int i = 0; i > -10; i--)
{
lock_guard<mutex> guard(mt);
cout << "print main: " << i << endl;
}
t.join();
return 0;
}
条件变量
条件变量是利用线程间共享的全局变量进行同步(按照预定的先后次序顺序进行)的一种机制,主要包括两个动作:线程等待条件变量的条件成立而挂起;线程变量的条件成立给出条件成立信号。
- 拥有条件变量的线程获取互斥量。
- 循环检查某个条件,如果条件不满足则阻塞直到条件满足;如果条件满足则向下执行。
- 某个线程满足条件执行完之后调用notify_one或notify_all唤醒一个或者所有等待线程。
工作原理:
- 当前线程调用wait()后将被阻塞并且函数会解锁互斥量(必须使用 unique_lock),直到另外某个线程调用notify_one或者 notify_all唤醒当前线程;一旦当前线程获得通知(notify),wait()函数也是自动调用lock(),同理不 能使用lock_guard对象。
- 如果wait没有第二个参数,第一次调用默认条件不成立,直接解锁互斥量并阻塞到本行,直到某一 个线程调用notify_one或notify_all为止,被唤醒后,wait重新尝试获取互斥量,如果得不到,线程 会卡在这里,直到获取到互斥量,然后无条件地继续进行后面的操作。
- 如果wait包含第二个参数,如果第二个参数不满足,那么wait将解锁互斥量并堵塞到本行,直到某 一个线程调用notify_one或notify_all为止,被唤醒后,wait重新尝试获取互斥量,如果得不到,线 程会卡在这里,直到获取到互斥量,然后继续判断第二个参数,如果表达式为false,wait对互斥 量解锁,然后休眠,如果为true,则进行后面的操作。
第六章 便利工具
处理时间与日期:chrono库
duration
:时间间隔,原型模板中第一个参数Rep
是数值类型(int
、double
…),第二个模板参数std::ratio
表示时钟周期:表示Num/Denom
秒。
template<
class Rep,
class Period = std;:ratio<1,1>
> class duration;
template<
std::intmax_t Num,
std::intmax_t Denom = 1
> class ratio;
线程的休眠可以使用时间间隔(例如std::chrono::duration<3.5, std::ratio<1,30>>
),常用的时间间隔有:
typedef duration <ReP,ratio<3600,1>> hours;
typedef duration <ReP,ratio<60,1>> minutes;
typedef duration <Rep, ratio<1,1>> seconds;
typedef duration <Rep, ratio<1,1000>> milliseconds;
typedef duration <Rep,ratio<1,1000000>> microseconds;
typedef duration <Rep,ratio<1,1000000000>> nanoseconds;
不同时钟周期的时间间隔可以做运算,通过std::chrono::duration_cast<std::chrono::seconds>().count()
可以转换成分钟的时间间隔。
clocks
:系统时钟,system_clock、steady_clock、high_resolution_clock
std::chrono::system_clock
它表示当前的系统时钟,系统中运行的所有进程使用now()
得到的时间是一致的。std::chrono::steady_clock
为了表示稳定的时间间隔,后一次调用now()
得到的时间总是比前一次的值大,即使中途修改了系统时间,也不影响的结果,每次tick
都保证过了稳定的时间间隔。
system_clock::time_point today = system_clock::now();
system_clock::time_point tomorrow = today + hours(24);
steady_clock::time_point start = steady_clock::now();
std::this_thread::sleep_for(std::chrono::seconds(2));
steady_clock::time_point end = steady_clock::now();
auto elapsed = end - start;
cout << endl << elapsed.count() << endl;
cout << duration<double>(elapsed).count() << endl; // converts to seconds
time point
:时间点,获取从开始纪元(可能是1970.1.1)所经历过的duration
和当前时间
// 原型模板
template <class Clock, class Duration = typename Clock::duration> class time_point;
std::put_time(std::local_time())
可以格式化日期的输出,且利用high_resolution_clock
可以实现计时器功能,方便用于测试函数耗时。
数值类型和字符串的相互转换
string
存储单字节编码的字符(ASCII编码格式),wstring
存储双字节编码的字符(UNICODE编码格式,比如中文、日文等)
std::wstring str = L"中国人"; //定义unicode 字符串,用L定义宽字符
- 宽字符串与窄字符串转换需要用
codecvt
库中的wstring_convert
- 数值转字符串:
to_string(value)
和to_wstring(value)
- 字符串转换为数值:
atoi
:将字符串转为int
类型atol
:将字符串转为long
类型atoll
:将字符串转为long long
类型atof
:将字符串转为float
类型
数值极限
标准库中提供数值型别的类型:numeric_limits<>
- 提供了更好的型别安全性
- 可借此写出一些
Templates
以核定(evaluate
)这些极值
#include <limits>
cout << "max(short): " << numeric_limits<short>::max() << endl;
cout << "max(int): " << numeric_limits<int>::max() << endl;
cout << "max(long): " << numeric_limits<long>::max() << endl;
denorm_min | 返回最小的非规范化非零值。 |
digits | 返回类型可以表示而不会降低精度的基数数字的位数。 |
digits10 | 返回类型可以表示而不会降低精度的十进制数字的位数。 |
epsilon | 返回数据类型可以表示的 1 与大于 1 的最小值之间的差值。 |
has_denorm | 测试类型是否允许非规范化值。 |
has_denorm_loss | 测试是否将准确度降低检测为非规范化损失,而不是不准确结果。 |
has_infinity | 测试某一类型是否能够表示正无穷。 |
has_quiet_NaN | 测试某一类型是否能表示非信号性沉寂非数值 (NAN)。 |
has_signaling_NaN | 测试某一类型是否能表示信号性沉寂非数值 (NAN)。 |
infinity | 某一类型用于表示正无穷的值(若适用)。 |
is_bounded | 测试某一类型可表示的值设置是否为有限。 |
is_exact | 测试针对某一类型进行的计算是否不产生舍入错误。 |
is_iec559 | 测试某一类型是否符合 IEC 559 标准。 |
is_integer | 测试某一类型是否具有具有整数表示形式。 |
is_modulo | 测试某一类型是否具有具有取模表示形式。 |
is_signed | 测试某一类型是否具有带符号的表示形式。 |
is_specialized | 测试某一类型是否具有在类模板 numeric_limits 中定义的显式专用化。 |
lowest | 返回最小的负有限值。 |
max | 返回某个类型的最大有限值。 |
max_digits10 | 返回确保类型的两个非重复值具有不同的十进制表示形式所需的十进制数字的位数。 |
max_exponent | 返回最大正整数指数,当计算基数的该指数次幂时,浮点类型可将其表示为有限值。 |
max_exponent10 | 返回最大正整数指数,当计算 10 的该指数次幂时,浮点类型可将其表示为有限值。 |
min | 返回某个类型的最小规范化值。 |
min_exponent | 返回最大负整数指数,当计算基数的该指数次幂时,浮点类型可将其表示为有限值。 |
min_exponent10 | 返回最大负整数指数,当计算 10 的该指数次幂时,浮点类型可将其表示为有限值。 |
quiet_NaN | 返回类型的静默非数值 (NAN) 表示形式。 |
radix | 返回用于表示类型的整数底数(称为基数)。 |
round_error | 返回类型的最大舍入误差值。 |
round_style | 返回一个值,该值描述可供实现选择用于将浮点值舍入为整数值的各种方法。 |
signaling_NaN | 返回类型的信令非数值 (NAN) 表示形式。 |
tinyness_before | 测试某个类型是否可在舍入某个值之前确定该值太小而无法表示为规范化值。 |
traps | 测试是否为某个类型实现了报告算术异常的捕获。 |
第七章 C++14
新特性
std::make_unique
std::make_unique
创建并获取unique_ptr
类型的智能指针,相比unique_ptr
更加安全。可以取代new
而且无需清空指针,尽量使用std::make_unique
和std::make_shared
,而不是new。
template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&&... params){
return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}
make
函数( std::make_unique
、std::make_shared
和std::allocate_shared
(允许传递你自己的分配器作为第一个参数))用来把一个任意参数的集合完美转移给一个构造函数从而生成动态分配内存的对象,并返回一个指向那个对象的smart pointer
。share_ptr
限定的资源可以被多个指针共享,weak_ptr
是一种用于解决shared_ptr
相互引用时产生死锁问题的智能指针,unique_ptr
是一个独享所有权的智能指针(必须是指向动态分配的对象的指针)。
auto upw1(std::make_unique<int>()); // with make func
std::unique_ptr<int> upw2(new int); // without make func
auto spw1(std::make_shared<int>()); // with make func
std::shared_ptr<int> spw2(new int); // without make func
区别:使用new
的版本重复了被创建对象的键入,执行两次内存分配:一次内存分配给X,一次内存分配给控制块,但是make
函数则没有。
二进制字面值
- 二进制数定义:进制字面值可使用0b或者0B开头来表示
int i = 0b0100010001; // 273
- 数位分隔:数位分隔符不仅可以用在2进制数中,也可以用在8进制,10进制和16进制数中
int i = 0b01'0001'0001; // 273
读写锁
读写锁(shared_lock
)可以有三种状态,只有一个线程可以占有写模式的读写锁,但是可以有多个线程占有读模式的读写锁。
- 读模式加锁状态;
- 写模式加锁状态;
- 不加锁状态;
当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。相比互斥锁,读写锁允许更高的并行性,互斥量要么锁住状态要么不加锁,而且一次只有一个线程可以加锁。
- 当读写锁处于写加锁状态时,在其解锁之前,所有尝试对其加锁的线程都会被阻塞;
- 当读写锁处于读加锁状态时,所有试图以读模式对其加锁的线程都可以得到访问权,但是如果想以写模式对其加锁,线程将阻塞。
第八章 通信
SOCKET 通信
服务器端的执行步骤
socket
: 建立一个socket
,原型为int socket(int af, int type, int protocol)
,返回本次socket
连接的文件句柄。af
为地址族(Address Family
),也就是 IP 地址类型,常用的有AF_INET
和AF_INET6
。AF_INET
表示IPv4
地址,例如 127.0.0.1,表示本机地址;AF_INET6 表示 IPv6 地址。type
为数据传输方式/套接字类型,常用的有SOCK_STREAM
(流格式套接字/面向连接的套接字) 和SOCK_DGRAM
(数据报套接字/无连接的套接字)。protocol
表示传输协议,常用的有IPPROTO_TCP
和IPPTOTO_UDP
,分别表示 TCP 传输协议和 UDP 传输协议。如果将protocol
的值设为 0,系统会自动推演出应该使用什么协议.
sockaddr_in
: 处理网络通信的地址的结构体
struct sockaddr_in {
short sin_family; // 2 字节 ,地址族,e.g. AF_INET, AF_INET6
unsigned short sin_port; // 2 字节 ,16位TCP/UDP 端口号 e.g. htons(3490),
struct in_addr sin_addr; // 4 字节 ,32位IP地址
char sin_zero[8]; // 8 字节 ,不使用
};
// 创建 sockaddr_in 实例
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口号
bind
: 将这个socket
绑定在某个端口上(AF_INET
)recvfrom
: 如果没有客户端发起请求,则会阻塞在这个函数里
//接收函数
int recvfrom(int sockfd, void * buf, size_t len, int flags, struct sockaddr * src_addr, socklen_t * addrlen);
//参数含义
sockfd:用于接收UDP数据的套接字;
buf:保存接收数据的缓冲区地址;
len:可接收的最大字节数(不能超过buf缓冲区的大小);
flags:可选项参数,若没有可传递0;
src_addr:存有发送端地址信息的sockaddr结构体变量的地址;
addrlen:保存参数 src_addr的结构体变量长度的变量地址值。
close
: 通信完成后关闭socket
客户端的执行步骤
socket
: 建立一个socket
sockaddr_in
: 处理网络通信的地址的结构体sendto
:向服务器的某个端口发起请求(AF_INET
)
//发送函数
int 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:存有目标地址信息的 sockaddr 结构体变量的地址;
addrlen:传递给参数 dest_addr的地址值结构体变量的长度。
close
: 通信完成后关闭socket
客户端实例
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define DEST_PORT 8000
#define DSET_IP_ADDRESS "127.0.0.1"
int main()
{
/* socket文件描述符 */
int sock_fd;
/* 建立udp socket */
sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if(sock_fd < 0)
{
perror("socket");
exit(1);
}
/* 设置address */
struct sockaddr_in addr_serv;
int len;
memset(&addr_serv, 0, sizeof(addr_serv));
addr_serv.sin_family = AF_INET;
addr_serv.sin_addr.s_addr = inet_addr(DSET_IP_ADDRESS);
addr_serv.sin_port = htons(DEST_PORT);
len = sizeof(addr_serv);
int send_num;
int recv_num;
char send_buf[20] = "hey, who are you?";
char recv_buf[20];
printf("client send: %s\n", send_buf);
send_num = sendto(sock_fd, send_buf, strlen(send_buf), 0, (struct sockaddr *)&addr_serv, len);
if(send_num < 0)
{
perror("sendto error:");
exit(1);
}
recv_num = recvfrom(sock_fd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr *)&addr_serv, (socklen_t *)&len);
if(recv_num < 0)
{
perror("recvfrom error:");
exit(1);
}
recv_buf[recv_num] = '\0';
printf("client receive %d bytes: %s\n", recv_num, recv_buf);
close(sock_fd);
return 0;
}
服务端实例
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#define SERV_PORT 8000
int main()
{
/* sock_fd --- socket文件描述符 创建udp套接字*/
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if(sock_fd < 0)
{
perror("socket");
exit(1);
}
/* 将套接字和IP、端口绑定 */
struct sockaddr_in addr_serv;
int len;
memset(&addr_serv, 0, sizeof(struct sockaddr_in)); //每个字节都用0填充
addr_serv.sin_family = AF_INET; //使用IPV4地址
addr_serv.sin_port = htons(SERV_PORT); //端口
/* INADDR_ANY表示不管是哪个网卡接收到数据,只要目的端口是SERV_PORT,就会被该应用程序接收到 */
addr_serv.sin_addr.s_addr = htonl(INADDR_ANY); //自动获取IP地址
len = sizeof(addr_serv);
/* 绑定socket */
if(bind(sock_fd, (struct sockaddr *)&addr_serv, sizeof(addr_serv)) < 0)
{
perror("bind error:");
exit(1);
}
int recv_num;
int send_num;
char send_buf[20] = "i am server!";
char recv_buf[20];
struct sockaddr_in addr_client;
while(1)
{
printf("server wait:\n");
recv_num = recvfrom(sock_fd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr *)&addr_client, (socklen_t *)&len);
if(recv_num < 0)
{
perror("recvfrom error:");
exit(1);
}
recv_buf[recv_num] = '\0';
printf("server receive %d bytes: %s\n", recv_num, recv_buf);
send_num = sendto(sock_fd, send_buf, recv_num, 0, (struct sockaddr *)&addr_client, len);
if(send_num < 0)
{
perror("sendto error:");
exit(1);
}
}
close(sock_fd);
return 0;
}
ASIO 网络编程
- 通过
asio
的主动器发起异步连接:
// 定义asio的核心对象io_service,所有的io object对象的初始化都要将这个对象传入
asio::io_service io_service;
// 创建tcp::socket类型的io object对象,通过这个io object对象发起异步操作
asio::ip::tcp::socket socket(io_service);
// 发起异步连接操作
boost::asio::async_connect(socket,server_address, connect_handler);
// 调用io_service的run启动事件循环,等待异步事件的完成
io_service.run();
- 异步读写事件处理器
#include <boost/asio.hpp>
using boost;
using boost::asio;
using boost::asio::ip;
class RWHandler {
public:
RWHandler(boost::asio::io_service& io_service) : socket_(io_service) {}
tcp::socket& socket() { return socket_; }
void HandleRead() {
async_read(socket_, buffer(m_buff), [this](const boost::system::error_code& ec, size_t size));
if (!ec) {
HandleError(ec);
return;
}
HandleRead();
}
void Handlewrite(char* data, int len) {
boost::system::error_code ec;
write(socket_, buffer(data, len), ec);
if (!error) {
HandleError(ec);
}
}
private:
tcp::socket socket_; // 具体的读写执行者
enum { max_length = 1024 }; // 缓冲区长度
std::array<char max_length> buffer; // 缓冲数据
};
第九章 半同步半异步线程池
线程池的基本结构
在处理大量并发任务时,传统大量的线程创建和销毁会消耗过多的系统资源,而且增加了线程上下文切换的开销。线程池技术通过预先创建一定数量的线程,从线程池中分配一个预先创建的线程去处理任务,这种方式好处:
- 避免大量的线程创建和销毁动作,节省系统资源
- 对于多核处理器,线程会被分配到多个CPU,提高并行处理的效率
- 每个线程独立阻塞,防止主线程被阻塞而使主流程被阻塞,导致其他请求得不到响应
半同步半异步线程池分为三层:
- 同步服务层:处理上层的任务请求,并发的任务请求可能不会被立刻处理,这些任务会被放进一个同步排队层
- 同步排队层:任务被放进同步队列中,在排队层进行等待
- 异步服务层:多个线程从同步排队层中取出任务并行处理
// 同步队列的实现 保证线程安全
template <typename T>
class SyncQueue {
public:
SyncQueue(int max_size): max_size_{max_size} {}
void Push(T&& task) {
std::unique_lock<std::mutex> lock{mutex_};
not_full_variable_.wait(lock, [this]{ return queue_.size() < max_size_; });
queue_.push(std::forward<T>(task));
not_empty_variable_.notify_one();
}
void Pop(T& task) {
std::unique_lock<std::mutex> lock{mutex_};
not_empty_variable_.wait(lock, [this]{ return !queue_.empty(); });
task = queue_.front();
queue_.pop();
not_full_variable_.notify_one();
}
void Pop(std::queue<T>& tasks) {
std::unique_lock<std::mutex> lock{mutex_};
not_empty_variable_.wait(lock, [this]{ return !queue_.empty(); });
tasks = std::move(queue_);
not_full_variable_.notify_one();
}
bool Empty() {
std::lock_guard<std::mutex> lock{mutex_};
return queue_.empty();
}
bool Full() {
std::lock_guard<std::mutex> lock{mutex_};
return max_size_ == queue_.size();
}
private:
std::mutex mutex_;
std::condition_variable not_full_variable_;
std::condition_variable not_empty_variable_;
std::queue<T> queue_;
int max_size_;
};
class ThreadPool {
// ...
private:
void threadFunction(); // 线程执行函数的声明
vector<thread> threads; // 线程集合
// 任务队列相关
deque<Task> taskQueue; // 任务队列
mutex queueMutex; // 任务队列访问互斥量
condition_variable condition; // 任务队列条件变量,通知线程有新任务可执行, 实现任务队列的同步访问
};
// 创建线程集合
ThreadPool::ThreadPool(size_t threadCount) {
for (size_t i = 0; i < threadCount; ++i) {
threads.emplace_back(&ThreadPool::threadFunction, this);
}
}
// 添加任务,需使用互斥量锁住任务队列以实现同步访问。任务添加成功后,通知等待中的线程有新任务可以执行。
void ThreadPool::addTask(const Task& task) {
{
lock_guard<mutex> lock(queueMutex);
taskQueue.emplace(task);
}
condition.notify_one();
}
// 线程执行体应按照预设策略从任务队列中获取任务并执行。获取任务时,需要在条件变量上等待,直到有新任务或线程池被终止。任务获取成功后,线程从队列中移除任务并执行。执行完成后,线程可以被再次复用。
void ThreadPool::threadFunction() {
while (true) {
Task task;
{
unique_lock<mutex> lock(queueMutex);
condition.wait(lock, [this]() { return !taskQueue.empty() || terminate; });
if (terminate && taskQueue.empty()) {
break;
}
task = taskQueue.front();
taskQueue.pop();
}
task(); // Execute the task.
}
}