New 和 delete
关键字 new(new operator)做了两件事情:
- 调用 operator new() 函数为对象分配内存;
- 调用 placement new() 函数在分配的内存上构造对象(调用对象的构造函数)。
关键字 delete(delete operator)做了两件事情:
- 调用对象的析构函数;
- 调用 operator delete() 函数释放对象占用的内存。
class A {
public:
A() = default;
~A() = default;
};
int main() {
auto area = operator new(sizeof A); // 分配内存
auto p = new(area) A{}; // 构造
p->~A(); // 析构
operator delete(area); // 释放内存
}
智能指针
回顾一下 make_human() 函数:
// 返回一个指向 Human 对象的指针
Human* make_human(Type type, const std::string& name) {
if (type == Type::student) {
return new Student{ name };
}
else { // type == Type::teacher
return new Student{ name };
}
}
我们在函数内部使用 new operator 创建了 Human 对象(分配了内存)。如果对象没有销毁(内存没有释放),就会造成内存泄露。函数调用者得到了指向这块内存的指针,所以释放内存的责任就落在了函数调用者身上。如果函数调用层数很多,或者对象的创建和销毁距离很远,那么用户很可能忘记释放。
解决的方法是使用 RAII(Resource Acquisition Is Initialization,资源获取即初始化),让对象自己管理自己的内存。定义一个资源管理类,在构造的时候获取资源,在析构的时候释放资源。资源管理类定义在栈上,所以它的析构函数一般会自己调用,不需要用户调用。
class HumanPtr {
public:
HumanPtr(Human* p) : p_{ p } {}
~HumanPtr() { delete p_; }
private:
Human* p_;
};
// 返回一个内部管理 Human 对象的 HumanPtr 对象
HumanPtr make_human(Type type, const std::string& name) {
if (type == Type::student) {
return new Student{ name };
}
else { // type == Type::teacher
return new Teacher{ name };
}
}
int main() {
auto p = make_human(Type::student, "mimi");
} // 对象 p 被释放,其管理的 Human 对象也被释放
C++ 提供了智能指针,帮助我们管理资源:
- Unique_ptr:资源只能被一个地方持有,不能复制,只能移动。
- Shared_ptr:资源可以被多个地方持有,可以复制和移动。
- Weak_ptr:不持有资源,用来解决 shared_ptr 循环引用的问题。
一般使用 make_unique() 和 make_shared() 函数创建智能指针,下面两种写法是一样的:
auto p1 = std::unique_ptr<Student>{ new Student{"mimi"} };
auto p2 = std::make_unique<Student>("mimi");
注意,下面两种写法是不一样的:
auto p3 = std::shared_ptr<Student>{ new Student{"mimi"} };
auto p4 = std::make_shared<Student>("mimi");
- p3 分配了 2 次内存,先分配 Student 的内存,再分配 shared_ptr 引用计数的内存;
- p4 分配了 1 次内存,直接一起分配 Student 和 shared_ptr 引用计数的内存。
在一些情况下,我们需要从对象返回一个指向自己的 shared_ptr。可以让类继承 enable_shared_from_this 类,在需要返回 shared_ptr 时使用 shared_from_this() 函数。注意,该类本身只能通过 shared_ptr 访问,外部不能直接访问构造函数,需要通过该类提供的 create() 函数获得 shared_ptr。有两种方法让构造函数对外部不可见:
- 将构造函数定义为 private,这样做的缺点是无法使用 make_shared() 函数;
- 在内部定义 private 的 token 类,并作为参数传入构造函数。比如,一个简单的时间服务器:
#include <iostream>
#include <string>
#include <memory>
#include <boost/asio.hpp>
#include <absl/time/time.h>
#include <absl/time/clock.h>
using boost::asio::ip::tcp;
auto make_daytime_string() {
return absl::FormatTime(absl::Now(), absl::LocalTimeZone());
}
class tcp_connection
: public std::enable_shared_from_this<tcp_connection> {
class token {
token() = default;
friend tcp_connection;
};
public:
// 外部无法访问私有的 token,也就无法调用构造函数
tcp_connection(tcp::socket socket, token)
: socket_{ std::move(socket) } {}
// 外部只能使用 create() 创建 tcp_connection 对象
static auto create(tcp::socket socket) {
return std::make_shared<tcp_connection>(std::move(socket), token{});
}
void start() {
message_ = make_daytime_string();
boost::asio::async_write(
socket_,
boost::asio::buffer(message_),
[self = shared_from_this()](boost::system::error_code, std::size_t) {}
);
}
private:
tcp::socket socket_;
std::string message_;
};
class tcp_serve {
public:
tcp_server(boost::asio::io_context& io_context)
: acceptor_{ io_context, tcp::endpoint{ tcp::v4(), 13 } } {
start();
}
private:
void start() {
acceptor_.async_accept(
[this](boost::system::error_code ec, tcp::socket socket) {
if (!ec) {
tcp_connection::create(std::move(socket))->start();
}
start();
}
);
}
tcp::acceptor acceptor_;
};
int main( {
try {
boost::asio::io_context io_context;
tcp_server server{ io_context };
io_context.run();
}
catch (std::exception& e) {
std::cerr << e.what() << '\n';
}
}
Lamda 表达式
仿函数是可以像函数一样被调用的对象,它重载了 operator() 函数:
class Add {
public:
template <typename T>
T operator()(T a, T b) { return a + b; }
};
int main() {
auto add = Add{}; // 定义 Add 对象
auto sum = add(1, 2); // 调用 add 的 operator() 函数
}
仿函数可以作为参数传递给 STL 的算法:
class Cmp {
public:
template <typename T>
T operator()(T a, T b) { return a > b; }
}
int main() {
std::vector<int> nums{ 2,0,2,2 };
std::sort(nums.begin(), nums.end(), Cmp{});
}
每次使用仿函数都要定义一个类,其实编译器可以帮我们完成,使用 lambda 表达式即可:
int main() {
auto add = [](auto a, auto b) { return a + b; }; // 定义闭包
auto sum = add(1, 2); // 调用闭包的 operator() 函数
}
我们使用 lambda 表达式时,编译器会定义一个闭包类,然后定义相应的闭包对象:
class Add {
public:
template <typename T1, typename T2>
auto operator()(T1 a, T2 b) const { return a + b; }
};
int main() {
auto add = Add{};
auto sum = add(1, 2);
}
Lambda 表达式可以捕获外部的变量,有 3 种形式:
- 值捕获:做一份外部变量的拷贝放在闭包中,生命期是由闭包管理的。
- 引用捕获:在闭包中引用外部变量,生命期是由外部管理的。注意,要确保在闭包被调用时,外部变量还没有被销毁。
- 移动捕获:将外部变量移动到闭包中,生命期是由闭包管理的。
int main() {
std::string s{ "abc" };
auto func1 = [s] {}; // 值
auto func2 = [&s] {}; // 引用
auto func3 = [s = std::move(s)] {}; // 移动
}
编译器会定义一个闭包类,然后定义相应的闭包对象:
class Func1 {
public:
Func1(const std::string& s) : s_(s) {}
void operator()() const {}
private:
std::string s_;
};
class Func2 {
public:
Func2(std::string& s) : s_(s) {}
void operator()() {}
private:
std::string& s_;
};
class Func3 {
public:
Func3(std::string&& s) : s_(s) {}
void operator()() {}
private:
std::string s_;
};
int main() {
std::string s{ "abc" };
auto func1 = Func1{ s }; // 值
auto func2 = Func2{ s }; // 引用
auto func3 = Func3{ std::move(s) }; // 移动
}