Lesson3

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) };	// 移动
}
上一篇:centos 报错:error while loading shared libraries: libpcap.so.0.8: cannot open shared object file: No s


下一篇:C++学习 四、智能指针总结