C++11 新特性
std::call_once
std::call_once
是 C++11 标准库中引入的一种机制,用于确保某个代码块只被执行一次,无论这个代码块被多少线程并发地调用。这对于需要初始化或配置共享资源的情况非常有用,可以避免多个线程同时初始化造成的混乱。
std::call_once
的工作原理如下:
- 当你调用
std::call_once
时,它会检查一个名为flag
的内部变量是否已经被设置。这个flag
是std::call_once
内部维护的。 - 如果
flag
还没有被设置,那么std::call_once
会执行传递给它的可调用对象(通常是一个函数或者 lambda 表达式),并将flag
设置为已完成状态。 - 如果
flag
已经被设置,那么std::call_once
不会执行传递给它的可调用对象,而是立即返回。
这确保了无论有多少线程试图并发地调用 std::call_once
,传递给它的代码块都只会被执行一次。
下面是一个简单的例子:
#include <iostream>
#include <thread>
#include <mutex>
#include <once>
std::once_flag flag;
void do_something() {
std::call_once(flag, [] {
std::cout << "Doing something...\n";
});
}
int main() {
std::thread t1(do_something);
std::thread t2(do_something);
t1.join();
t2.join();
return 0;
}
在这个例子中,无论 do_something
函数被多少线程并发地调用,它内部的代码块都只会被执行一次,输出 "Doing something..." 一次。
std::bind
std::bind
是 C++标准库中的一个函数模板,用于将可调用对象(函数、函数指针、成员函数指针、函数对象等)与一组参数结合起来,生成一个新的可调用对象。
std::bind
的语法如下:
template<class F, class... Args>
std::bind(F f, Args&&... args);
其中,F
是可调用对象的类型,Args
是可变参数模板,用于传递给可调用对象的参数。
下面是一个使用std::bind
的示例:
#include <iostream>
#include <functional>
void print(int x, const std::string& y) {
std::cout << x << " " << y << std::endl;
}
int main() {
// 使用 std::bind 将 print 函数与 10 和 "Hello" 结合起来
auto bound_print = std::bind(print, 10, "Hello");
// 调用 bound_print,输出:10 Hello
bound_print();
return 0;
}
在上面的示例中,print
函数接受两个参数,分别是整数和字符串。通过std::bind
,我们将print
函数与值 10 和字符串"Hello"结合起来,生成一个新的可调用对象bound_print
。当调用bound_print
时,它会将 10 和"Hello"作为参数传递给print
函数。
std::bind
还可以用于将成员函数指针与对象和参数结合起来,或者将函数对象与参数结合起来。
使用bind
绑定到成员函数时,即使成员函数不需参数,也要将this
绑定在第一个参数
在C++中,当使用bind
函数将成员函数绑定到可调用对象时,即使成员函数不需要参数,也需要将this指针绑定在第一个参数上。这是因为成员函数是在类的对象上下文中执行的,而this
指针指向该对象。
以下是一个示例,展示了如何在使用bind
时正确绑定this
指针:
#include <iostream>
#include <functional>
class MyClass {
public:
void myMethod() {
std::cout << "Hello from myMethod()" << std::endl;
}
};
int main() {
MyClass obj;
// 正确的绑定方式,将 this 指针作为第一个参数
std::function<void()> boundMethod = std::bind(&MyClass::myMethod, &obj);
// 调用绑定后的函数
boundMethod();
return 0;
}
在上述示例中,即使myMethod
成员函数不需要参数,我们仍然使用bind
将&obj
(对象的地址)作为第一个参数传递。这样,当调用boundMethod
时,this
指针将正确地指向obj
对象,从而确保成员函数在正确的对象上下文中执行。
如果不将this
指针绑定到第一个参数,可能会导致成员函数在错误的对象上执行,或者引发编译错误。
需要注意的是,具体的绑定方式可能会根据你使用的函数库和bind的实现有所不同,但一般来说,将this
指针作为第一个参数进行绑定是常见的做法。这样可以确保成员函数的正确执行和对象的正确上下文。
基于范围的 for 循环
C++11 引入的基于范围的for
循环是一种新的循环语法,它可以用于遍历各种容器(例如数组、向量、列表等)中的元素。基于范围的for
循环的语法如下:
for (auto& element : container)
{
// 循环体
}
在上面的代码中,auto& element
表示循环变量,它的类型是由容器中的元素类型推导出来的。container
是要遍历的容器。
基于范围的for
循环的优点是它更加简洁和易读,并且可以避免一些常见的错误,例如迭代器有效性问题和越界访问问题。此外,它还可以自动推断循环变量的类型,减少了代码的冗余。
下面是一个使用基于范围的for
循环遍历数组的示例:
#include <iostream>
int main()
{
int array[] = {1, 2, 3, 4, 5};
for (int& element : array)
{
std::cout << element << " ";
}
std::cout << std::endl;
return 0;
}
在上面的代码中,我们使用基于范围的for
循环遍历了一个整数数组,并输出了每个元素的值。
需要注意的是,基于范围的for
循环只能遍历可迭代的容器,例如数组、向量、列表等。对于其他类型的容器,需要使用其他方式进行遍历。
std::function
在 C++11 中,std::function
是一个通用的函数封装类,可以用于包装可调用对象(函数、函数指针、函数对象、Lambda 表达式等)。它提供了一种统一的函数封装和调用的方式,使得你可以更加灵活地处理不同类型的可调用对象。
std::function
的主要优点如下:
-
多态性:
std::function
可以包装多种不同类型的可调用对象,包括普通函数、成员函数、函数指针、函数对象、Lambda 表达式等。这使得你可以使用统一的接口来处理不同类型的函数。 -
类型安全:
std::function
会自动推导被包装的可调用对象的类型,并在调用时进行类型检查,确保传递给std::function
的参数和返回值类型与可调用对象的签名匹配。 -
模板参数:
std::function
是使用模板实现的,可以作为通用的函数类型参数传递给其他模板函数。 -
可调用性:
std::function
可以像普通函数一样被调用,并且支持调用运算符operator()
。
下面是一个简单的示例,展示了如何使用std::function
:
#include <iostream>
#include <functional>
int add(int a, int b) {
return a + b;
}
int main() {
// 包装函数对象
std::function<int(int, int)> func = add;
std::cout << func(3, 4) << std::endl;
// 包装函数指针
int (*ptrToAdd)(int, int) = add;
std::function<int(int, int)> funcPtr = ptrToAdd;
std::cout << funcPtr(3, 4) << std::endl;
// 包装 Lambda 表达式
std::function<int(int, int)> lambda = [](int a, int b) { return a + b; };
std::cout << lambda(3, 4) << std::endl;
return 0;
}
在上面的示例中,我们创建了一个std::function<int(int, int)>
类型的对象func
,它可以包装任意接受两个int
类型参数并返回int
类型的可调用对象。然后,我们分别使用函数对象、函数指针和 Lambda 表达式来初始化std::function
,并调用它们进行测试。
需要注意的是,std::function
并不保证被包装的可调用对象的执行效率与原始对象相同,因为它涉及到类型擦除和动态分配。如果需要高效的函数调用,最好直接使用原始的可调用对象。
工作原理
std::function
是 C++11 中的一个通用函数封装类,它的工作原理基于函数指针和函数对象的多态性。
std::function
可以包装可调用对象(函数、函数指针、函数对象、Lambda 表达式等),并提供了一种统一的函数封装和调用的方式。它的主要工作原理如下:
-
函数类型擦除:
std::function
通过类型擦除的技术,将不同类型的可调用对象统一封装为一个通用的函数类型。这意味着你可以将不同类型的函数(包括普通函数、成员函数、函数指针、函数对象、Lambda 表达式等)包装到std::function
中,而不需要关心它们的具体类型。 -
模板参数推导:
std::function
使用模板参数推导来确定被包装的可调用对象的类型和参数类型。在使用std::function
时,你可以通过模板参数指定可调用对象的返回类型和参数类型,std::function
会根据这些信息进行类型推导。 -
调用运算符重载:
std::function
重载了调用运算符operator()
,使得你可以像调用普通函数一样调用被包装的可调用对象。在调用std::function
时,它会根据被包装的可调用对象的类型和参数类型进行动态派发,将参数传递给相应的可调用对象进行调用。 -
函数指针存储:
std::function
内部通常使用函数指针来存储被包装的可调用对象。当你将一个函数指针或函数对象传递给std::function
时,它会将函数指针存储起来,并在调用时使用该指针。 -
类型转换:
std::function
还提供了类型转换函数,例如static_cast
、const_cast
等,以便在需要时进行类型转换。
通过使用std::function
,你可以编写更加通用和灵活的代码,能够处理不同类型的可调用对象,而不需要关心它们的具体实现细节。这有助于提高代码的可重用性和扩展性。
使用std::function来实现回调函数?
std::function
可以用于实现回调函数(Callback Function)。回调函数是一种由调用者传递给被调用者的函数,被调用者在特定的时间或事件发生时调用该函数。
下面是一个使用std::function
实现回调函数的示例代码:
#include <iostream>
#include <functional>
// 定义一个回调函数类型
typedef std::function<int(int)> Callback;
// 定义一个处理回调函数的类
class CallbackHandler {
public:
// 设置回调函数
void setCallback(Callback callback) {
m_callback = callback;
}
// 调用回调函数
int invokeCallback(int value) {
if (m_callback) {
return m_callback(value);
}
return -1;
}
private:
Callback m_callback;
};
int main() {
// 创建一个回调函数对象,接受一个整数参数并返回其平方
Callback callback = [](int value) { return value * value; };
// 创建一个回调处理对象
CallbackHandler handler;
handler.setCallback(callback);
// 调用回调函数
int result = handler.invokeCallback(5);
// 输出结果
std::cout << "回调函数的结果:" << result << std::endl;
return 0;
}
在上面的示例中,我们定义了一个Callback
类型,它是一个接受一个int
类型参数并返回int
类型的可调用对象。然后,我们创建了一个CallbackHandler
类,用于设置和调用回调函数。
在main
函数中,我们创建了一个回调函数对象callback
,它是一个接受一个整数参数并返回其平方的 Lambda 表达式。然后,我们创建了一个CallbackHandler
对象handler
,并使用setCallback
方法将回调函数对象设置给它。
最后,我们调用invokeCallback
方法来触发回调函数的调用,并将 5 作为参数传递给它。回调函数的结果将被存储在result
变量中,并通过std::cout
输出。
POD
在C++11中,POD代表“Plain Old Data”(普通旧数据)的概念。这意味着该类型与C编程语言中使用的类型兼容,可以直接在其二进制形式下与C库进行交换。POD类型通常用于聚合一些对象,没有访问保护,也没有复杂的方法。
在C++11中,POD类型需要满足以下要求:
- 要么是标量类型,要么是聚合类型
- 如果是聚合类型,不能有非POD类型的非静态成员
- 不能有引用类型的成员
- 不能有用户定义的复制构造函数
- 不能有用户定义的析构函数(直到C++11之前)
- 必须是TrivialType
- 必须是StandardLayoutType
- 或者是这些类型的数组
在C++11中,可以使用is_pod
模板类来检查一个类型是否是POD类型。以下是一个示例代码:
#include <iostream>
#include <type_traits>
struct PixelByName
{
int x;
int y;
Color color;
};
int main() {
std::cout << std::boolalpha;
std::cout << "Is PixelByName a POD type? " << std::is_pod<PixelByName>::value << std::endl;
return 0;
}
以上代码将输出PixelByName是否是POD类型的布尔值。
TrivialType
在C++中,TrivialType(平凡类型)是一种类型特征,用于描述一种特殊的类型。一个TrivialType类型是指那些具有平凡(trivial)默认构造函数、拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符和析构函数的类型。这些函数可以是编译器生成的默认实现,也可以是用户显式声明为default
的实现。
换句话说,TrivialType类型是指那些在内存中可以直接进行位拷贝(bitwise copy)的类型。这意味着这些类型的对象可以安全地使用memcpy
函数进行拷贝,而不会导致未定义的行为。
在C++中,TrivialType类型通常用于需要高性能和低级别内存操作的场景,例如在网络通信、文件IO、序列化和反序列化等方面。这些类型可以被视为非常基本和简单的类型,因为它们的构造和拷贝操作可以被简化为位级别的操作。
要检查一个类型是否为TrivialType类型,可以使用std::is_trivial
类型特征模板。如果一个类型被判定为TrivialType类型,那么它就满足了TrivialType的所有要求,可以安全地进行位拷贝和其他低级别的操作。
StandardLayoutType
在C++中,StandardLayoutType(标准布局类型)是一种类型特征,用于描述一种特殊的类型。一个StandardLayoutType类型是指那些在内存中布局规则符合标准布局的类型。标准布局类型具有以下特性:
- 所有非静态数据成员都位于同一类中。
- 基类和派生类的布局遵循一定的规则,没有非静态数据成员的派生类可以与其最直接基类共享地址。
- 没有虚函数或虚基类。
- 所有非静态数据成员都具有相同的访问控制。
- 没有包含非标准布局类型的非静态数据成员。
标准布局类型对于与其他语言或者底层系统进行交互非常重要,因为它们的内存布局规则是明确定义的,可以确保与其他系统的兼容性和可靠性。
在C++中,可以使用std::is_standard_layout
类型特征模板来检查一个类型是否为标准布局类型。如果一个类型被判定为标准布局类型,那么它就满足了标准布局类型的所有要求,可以安全地进行内存布局操作和与其他系统进行交互。
is_pod
、is_trivial
、is_standard_layout
在C++中,is_pod
、is_trivial
和is_standard_layout
是用于检查类型特征的模板。它们分别用于检查给定类型是否为POD(Plain Old Data)类型、trivial类型和标准布局类型。
-
is_pod
用于检查一个类型是否为POD类型,即Plain Old Data。POD类型是指那些与C语言兼容的类型,可以直接在其二进制形式下与C库进行交换。一个POD类型必须是trivial类型和standard layout类型。 -
is_trivial
用于检查一个类型是否为trivial类型。Trivial类型是指那些具有平凡(trivial)默认构造函数、拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符和析构函数的类型。这些函数可以是编译器生成的默认实现,也可以是用户显式声明为default
的实现。 -
is_standard_layout
用于检查一个类型是否为标准布局类型。标准布局类型具有特定的内存布局规则,包括所有非静态数据成员都位于同一类中,没有虚函数或虚基类等特性。
这些类型特征对于在C++中进行类型检查和元编程非常有用,可以帮助程序员在编译期间对类型进行详细的属性检查。
初始化列表 initializer list
C++11引入了初始化列表的新语法,它可以用于初始化对象、数组和容器。初始化列表使用花括号 {}
包裹初始化的值。让我们来看一些示例代码来说明如何使用初始化列表。
#include <iostream>
#include <vector>
int main() {
// 初始化基本类型
int n{5};
double d{3.14};
// 初始化数组
int arr[]{1, 2, 3, 4, 5};
// 初始化STL容器
std::vector<int> vec{1, 2, 3, 4, 5};
// 输出初始化后的值
std::cout << "n: " << n << std::endl;
std::cout << "d: " << d << std::endl;
std::cout << "arr[0]: " << arr[0] << std::endl;
std::cout << "vec[2]: " << vec[2] << std::endl;
return 0;
}
在这个示例中,我们展示了如何使用初始化列表来初始化基本类型、数组和STL容器。这种语法使得初始化更加简洁和直观。
显示类型转换
在C++11中,显示类型转换是指通过使用新的static_cast
、dynamic_cast
、const_cast
和reinterpret_cast
等关键字,手动进行数据类型之间的转换。这些关键字提供了更加明确和安全的方式来进行类型转换,以便程序员可以更好地控制转换的行为。
四种类型转换cast
关键字 | 说明 |
---|---|
static_cast | 用于良性转换,一般不会导致意外发生,风险很低。 |
const_cast | 用于 const 与非 const、volatile 与非 volatile 之间的转换。 |
dynamic_cast | 借助 RTTI,用于类型安全的向下转型(Downcasting)。 |
reinterpret_cast | 高度危险的转换,这种转换仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整, 但是可以实现最灵活的 C++ 类型转换。 |
static_cast
在C++11中,static_cast
是一种类型转换操作符,用于执行显式的类型转换。它可以将一种数据类型转换为另一种数据类型,但是只能进行一些特定的转换。以下是static_cast
的一些用法:
- 将指针或引用从一个类的类型转换为另一个类的类型。
- 将表达式转换为与其引用类型兼容的右值引用类型。
- 执行隐式转换或直接初始化对象或引用的目标类型。
- 将表达式的值丢弃,如果目标类型是void。
- 执行标准转换序列的逆转换。
- 执行显式的lvalue-to-rvalue、array-to-pointer或function-to-pointer转换。
- 将枚举类型转换为整数或浮点数类型。
下面是一个简单的示例,演示了static_cast的用法:
#include <iostream>
using namespace std;
int main() {
double d = 3.14;
int i = static_cast<int>(d); // 将double类型转换为int类型
cout << i << endl; // 输出3
return 0;
}
在这个示例中,static_cast
将double
类型的变量d
转换为int
类型的变量i
,并输出结果为3。
总的来说,static_cast
提供了一种更加严格和明确的类型转换方式,可以帮助开发人员避免一些潜在的问题。
const_cast
在C++11中,const_cast
是一种类型转换操作符,用于移除指针或引用的const
或volatile
属性。这种转换通常用于在需要修改数据的情况下,从指向常量对象的指针或引用中移除const
属性。
以下是const_cast
的一些用法:
- 将指向常量对象的指针或引用转换为指向非常量对象的指针或引用。
- 将指向常量对象的指针或引用转换为指向volatile对象的指针或引用。
- 用于在函数中修改参数的值,而参数被声明为const。
下面是一个简单的示例,演示了const_cast的用法:
#include <iostream>
using namespace std;
int main() {
const int x = 5;
const int* ptr = &x; // 指向常量对象的指针
int* ptrNonConst = const_cast<int*>(ptr); // 移除const属性
*ptrNonConst = 10; // 修改值
cout << *ptrNonConst << endl; // 输出10
return 0;
}
在这个示例中,const_cast
被用于将指向常量对象的指针ptr
转换为指向非常量对象的指针ptrNonConst
,然后修改了指针所指向的值。
需要注意的是,使用const_cast
需要谨慎,因为它可以导致未定义的行为。在实际使用中,应该尽量避免使用const_cast
,并考虑其他更安全的设计方式。
dynamic_cast
在C++11中,dynamic_cast
是一种类型转换操作符,用于在运行时进行安全的指针或引用类型转换。它通常用于处理多态类的指针或引用,以便在继承层次结构中进行向上转换、向下转换和侧向转换。
以下是dynamic_cast
的一些用法:
- 将指向派生类对象的指针或引用转换为指向基类对象的指针或引用(向上转换)。
- 将指向基类对象的指针或引用转换为指向派生类对象的指针或引用(向下转换)。
- 在多态类层次结构中,进行安全的类型转换,以便在运行时检查类型信息。
下面是一个简单的示例,演示了dynamic_cast
的用法:
#include <iostream>
using namespace std;
class Base {
public:
virtual void print() {
cout << "Base" << endl;
}
};
class Derived : public Base {
public:
void print() {
cout << "Derived" << endl;
}
};
int main() {
Base* basePtr = new Derived;
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 向下转换
if (derivedPtr) {
derivedPtr->print(); // 输出Derived
} else {
cout << "Failed to cast" << endl;
}
return 0;
}
在这个示例中,dynamic_cast
被用于将指向派生类对象的指针basePtr
转换为指向派生类对象的指针derivedPtr
,然后调用print
方法输出"Derived"
。
总的来说,dynamic_cast
提供了一种安全的方式来在多态类层次结构中进行类型转换,避免了一些潜在的问题,如空指针引用和未定义的行为。
reinterpret_cast
在C++11中,reinterpret_cast
是一种类型转换操作符,用于执行底层的、不安全的类型转换。它主要用于将一个指针或引用转换为另一种指针或引用,或者将一个整数类型转换为指针类型,或者进行一些其他非常规的类型转换。
以下是reinterpret_cast
的一些用法:
- 将指针类型转换为整数类型,或将整数类型转换为指针类型。
- 将一个指针类型转换为另一种指针类型,即使它们之间没有直接的关联。
- 在一些特殊情况下,进行一些非常规的类型转换,如将一个整数类型转换为浮点数类型。
需要注意的是,reinterpret_cast
执行的转换是非常底层的,可能会导致未定义的行为。因此,它应该谨慎使用,只在确实需要进行底层的类型转换时才使用。
以下是一个简单的示例,演示了reinterpret_cast
的用法:
#include <iostream>
using namespace std;
int main() {
int number = 10;
int* ptr = &number;
double* doublePtr = reinterpret_cast<double*>(ptr); // 将int指针转换为double指针
cout << *doublePtr << endl; // 输出一个不确定的浮点数值
return 0;
}
在这个示例中,reinterpret_cast
被用于将指向整数的指针ptr
转换为指向浮点数的指针doublePtr
,然后输出了一个不确定的浮点数值。
总的来说,reinterpret_cast
提供了一种非常底层的类型转换方式,但由于其潜在的风险,应该尽量避免使用,除非确实需要进行底层的类型转换。
显示转换操作符
在C++11中,显示转换操作符是一种特殊的成员函数,它允许您明确地将一个类类型转换为另一个类型。这种转换操作符使用explicit
关键字进行声明,以防止隐式类型转换。
下面是一个示例,演示了如何在C++11中定义显示转换操作符:
#include <iostream>
using namespace std;
class MyInt {
private:
int value;
public:
MyInt(int val) : value(val) {}
// 显示转换操作符,将 MyInt 类型转换为 int 类型
explicit operator int() const {
return value;
}
};
int main() {
MyInt myInt(42);
int num = static_cast<int>(myInt); // 显示调用转换操作符
cout << "The value of num is: " << num << endl;
// 下面这行代码会导致编译错误,因为转换操作符是显式的,不允许隐式转换
// int num2 = myInt;
return 0;
}
在上面的示例中,explicit operator int()
定义了一个将MyInt
类型转换为int
类型的显示转换操作符。在main
函数中,我们使用static_cast
显式调用了这个转换操作符。
示例2
#include <iostream>
using namespace std;
class MyInt {
private:
int value;
public:
MyInt(int val) : value(val) {}
// 显示转换操作符,将 MyInt 类型转换为 double 类型
explicit operator double() const {
return static_cast<double>(value);
}
};
class MyDouble {
private:
double value;
public:
MyDouble(double val) : value(val) {}
// 显示转换操作符,将 MyDouble 类型转换为 int 类型
explicit operator int() const {
return static_cast<int>(value);
}
};
int main() {
MyInt myInt(42);
double num = static_cast<double>(myInt); // 使用转换操作符将 MyInt 转换为 double
cout << "The value of num is: " << num << endl;
MyDouble myDouble(3.14);
int num2 = static_cast<int>(myDouble); // 使用转换操作符将 MyDouble 转换为 int
cout << "The value of num2 is: " << num2 << endl;
return 0;
}
在上面的示例中,我们定义了MyInt
类和MyDouble
类,并分别定义了将它们转换为double
和int
类型的显示转换操作符。在main
函数中,我们使用这些转换操作符将类类型转换为其他类型。
RAII
RAII是C++中的一种编程习惯,全称为Resource Acquisition is Initialization
。它是一种资源管理技术,用于确保在对象的生命周期内自动获取和释放资源。RAII的核心思想是将资源的获取和释放与对象的生命周期绑定在一起,从而确保资源在对象生命周期内始终有效,并在对象生命周期结束时自动释放。
在C++中,RAII通常通过类的构造函数来获取资源,而通过类的析构函数来释放资源。这样做的好处是,无论是正常返回、对象销毁,还是抛出异常,资源都能够得到正确释放,从而确保程序的安全性和可靠性。
下面是一个使用RAII管理文件访问和互斥锁的C++11示例代码:
#include <fstream>
#include <iostream>
#include <mutex>
#include <stdexcept>
#include <string>
void WriteToFile(const std::string& message) {
static std::mutex mutex; // 用于保护文件访问的互斥锁
std::lock_guard<std::mutex> lock(mutex); // 锁定互斥锁
std::ofstream file("example.txt"); // 尝试打开文件
if (!file.is_open()) {
throw std::runtime_error("无法打开文件");
}
file << message << std::endl; // 将消息写入文件
// 离开作用域时,file会首先关闭,然后互斥锁会解锁
}
上述代码展示了如何使用RAII管理文件访问和互斥锁,确保资源在对象生命周期内得到正确释放。
RAII的优点包括封装性、异常安全性和资源获取与释放逻辑的局部性。它简化了资源管理,减少了代码量,并有助于确保程序的正确性。因此,RAII是C++标准库中广泛使用的一种编程习惯。
除了文件访问和互斥锁,RAII还常用于控制动态分配的内存、网络资源的发送等场景。在C++11标准库中,定义了用于管理资源的智能指针类std::unique_ptr
和std::shared_ptr
,它们也是RAII的典型应用之一。
总之,RAII是一种重要的C++编程习惯,它通过将资源的获取和释放与对象的生命周期绑定在一起,确保了资源的正确管理和程序的安全性。
右值引用和移动语义
C++11引入了右值引用和移动语义,这是一项重要的新特性,旨在提高C++程序的性能和效率。右值引用和移动语义的引入,使得在处理临时对象和资源管理方面变得更加高效。
右值引用是一种新的引用类型,使用双引号(&&)表示。它主要用于绑定临时对象(右值),允许对其进行高效的移动操作。移动语义则是利用右值引用来实现对临时对象的资源移动,而不是传统的复制操作。
传统的复制操作会对临时对象进行深拷贝,这可能会导致性能开销较大。而右值引用和移动语义允许在不进行深拷贝的情况下,将临时对象的资源(如堆上分配的内存)转移给另一个对象,从而避免了不必要的内存分配和数据复制。
通过移动语义,可以将临时对象的资源“移动”到新对象,然后将临时对象置于一种“可析构但不可访问”的状态,这样可以避免资源的多次分配和释放,提高了程序的性能和效率。
std::move
std::move是一个实用工具函数,用于将左值转换为右值引用,从而允许对其进行移动操作。然而,这并不会真正移动任何数据,它只是改变了对数据的引用方式。
MyClass a;
MyClass b = std::move(a); // 使用a的资源来初始化b,a处于有效但未定义的状态
在上面的例子中,a是左值,但我们通过std::move将其转换为右值引用,以便可以将其资源移动到b中。
以下是一个简单的示例,演示了移动语义的用法:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> source = {1, 2, 3, 4, 5};
vector<int> destination = move(source); // 使用move进行资源的移动
cout << "Source size: " << source.size() << endl; // 输出0,因为资源已经被移动
cout << "Destination size: " << destination.size() << endl; // 输出5,资源已经被移动到destination
return 0;
}
在这个示例中,使用move
函数将source
中的资源移动到destination
,避免了不必要的数据复制和内存分配,提高了程序的性能。
总的来说,右值引用和移动语义的引入,使得C++程序在处理临时对象和资源管理方面变得更加高效,为程序员提供了更多的工具来优化代码的性能。
右值引用可以用来延长临时对象的生命周期
当将一个临时对象绑定到右值引用时,它的生命周期会延长以匹配引用的生命周期。这意味着临时对象不会在表达式结束时销毁,而是会持续到引用的生命周期结束。
以下是一个示例代码,演示了如何使用右值引用来延长临时对象的生命周期:
#include <iostream>
void processValue(int&& value) {
// 对临时对象进行处理
std::cout << "Value: " << value << std::endl;
}
int main() {
// 创建临时对象,并将其绑定到右值引用
processValue(5);
// 此时临时对象的生命周期会延长,直到 processValue 函数执行完毕
// 在这里访问 value 是安全的
return 0;
}
在上面的示例中,processValue
函数接受一个 int 类型的右值引用参数。当我们调用processValue(5)
时,临时对象 5 的生命周期会被延长,直到processValue
函数执行完毕。
完美转发
所谓完美转发(perfect forwarding),是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
- 简单来说,左值是指可以取地址的、具有持久性的对象,而右值是指不能取地址的、临时生成的对象。
- 传统上,当一个左值传递给一个函数时,参数会以左值引用的方式进行传递;当一个右值传递给一个函数时,参数会以右值引用的方式进行传递。
- 完美转发是为了解决传递参数时的临时对象(右值)被强制转换为左值的问题。
- 在C++03中,可以使用泛型引用来实现完美转发,但是需要写很多重载函数,非常繁琐。而在C++11中,引入了std::forward,可以更简洁地实现完美转发。
因此,概括来说,std::forward实现完美转发主要用于以下场景:提高模板函数参数传递过程的转发效率
完美转发主要通过“引用折叠”和“std::forward”函数实现
引用折叠
- C++引用折叠是一种特性,允许在模板元编程中使用引用类型的参数来创建新的引用类型。
- 由于存在 T&& 这种万能引用类型,当它作为参数时,有可能被一个左值引用或右值引用的参数初始化,这是经过类型推导的 T&& 类型,推导后得到的参数类型会发生类型变化,这种变化就称为引用折叠。
引用折叠的具体规则如下:
- 若一个右值引用(即带有 && )参数被一个左值或左值引用初始化,那么引用将折叠为左值引用。(即:T&& & –> T&)
- 若一个右值引用参数被一个右值初始化,那么引用将折叠为右值引用。(即:T&& && 变成 T&&)。
- 若一个左值引用参数被一个左值或右值初始化,那么引用不能折叠,仍为左值引用(即:T& & –>T&,T& && –>T&)。
总结一下:
- 所有右值引用折叠到右值引用上仍然是一个右值引用。(A&& && 变成 A&&) 。
- 所有的其他引用类型之间的折叠都将变成左值引用。 (A& & 变成 A&; A& && 变成 A&; A&& & 变成 A&)。
简单来说:
- 右值经过T&&参数传递,类型保持不变还是右值(引用);
- 而左值经过T&&变为普通的左值引用。
为了更好地理解引用折叠,以下为几个示例:
template<typename T>
void func(T&& arg);
int main() {
int a = 5;
func(a); // arg为int&,引用折叠为左值引用
func(10); // arg为int&&,引用折叠为右值引用
int& ref = a;
func(ref); // arg为int&,引用不能折叠
}
在上述示例中,函数模板func接受一个转发引用类型(&&)的参数,并根据传递给func的实参类型决定引用类型:
- 当func(a)时,参数类型折叠后实际为int&,因为a是一个左值,引用类型折叠为左值引用。
- 当func(10)时,参数类型折叠后实际为int&&,因为10是一个右值,引用类型折叠为右值引用。
- 当func(ref)时,参数类型折叠后实际为int&,由于左值引用类型不能折叠,参数类型保持为左值引用。
引用折叠是C++中模板编程中非常有用的特性,可以根据传递实参的左值还是右值来确定引用类型,进而使得编写通用的模板函数或类更简单。
std::forward函数
实现完美转发的关键是使用std::forward函数。std::forward是一个条件转发函数模板,根据参数的左值或右值属性进行转发。
这个模板函数接受一个参数并返回一个右值引用,同时利用引用折叠保留参数的左值或右值属性。
调用std::forward时,根据参数的左值或右值属性,编译器会选择适当的模板实例进行转发。如果参数是一个左值引用,std::forward将返回一个左值引用。如果参数是一个右值引用,std::forward将返回一个右值引用。
例如:
- 如果T为std::string&,那么std::forward(t) 返回值为std::string&& &,折叠为std::string&,左值引用特性不变。
- 如果T为std::string&&,那么std::forward(t) 返回值为std::string&& &&,折叠为std::string&&,右值引用特性不变。
利用std::forward实现完美转发
C++完美转发是指一种能够传递函数参数或对象的同样类型(例如左值或右值属性)和cv限定符(const或volatile)的方式,同时保留原参数的准确数值类别和cv限定符的转发机制。完美转发通过使用引用折叠机制和std::forward函数来实现。
作用
- 在C++11之前,当我们将一个参数转发给另一个函数时,会丢失参数的左值或右值的信息。例如,如果我们有一个函数f,它接受一个左值引用,然后我们通过f来调用一个函数g并传递一个右值,那么在g函数内部,参数将被视为左值,从而可能引入额外的参数转移开销。
- C++11引入了右值引用、移动构造函数、引用折叠、std::forward等概念,使我们能够更准确地传递参数的左值或右值属性。因此,完美转发的目标是在转发参数时保持原始参数的左值或右值属性,从而提高函数参数传递的效率。
完美转发应用实例
首先定义一个对象CData,具体说明看注释:
#include <stdio.h>
#include <unistd.h>
#include <iostream>
class CData
{
public:
CData() = delete;
CData(const char* ch) : data(ch) // 构造函数,涉及资源的复制
{
std::cout << "CData(const char* ch)" << std::endl;
}
CData(const std::string& str) : data(str) // 拷贝构造函数,涉及资源的复制
{
std::cout << "CData(const std::string& str)" << std::endl;
}
CData(std::string&& str) : data(str) // 移动构造函数,不涉及资源的复制!!!
{
std::cout << "CData(std::string&& str)" << std::endl;
}
~CData() // 析构函数
{
std::cout << "~CData()" << std::endl;
}
private:
std::string data; // 表示类内部管理的资源
};
假如我们封装了一个操作,主要是用来创建对象使用(类似设计模式中的工厂模式),这个操作要求如下:
可以接受不同类型的参数,然后构造一个对象的指针。
性能尽可能高。(这里需要高效率,故对于右值的调用应该使用CData(std::string&& str)移动函数操作)
1)不使用std::forward实现
假设我们不使用std::forward,那么要提高函数参数转发效率,我们使用右值引用(万能引用)作为模板函数参数:
template<typename T>
CData* Creator(T&& t) { // 利用&&万能引用,引用折叠: T&& && -> T&&; T&& & -> T&
return new CData(t);
}
int main(void) {
std::string str1 = "hello";
std::string str2 = " world";
CData* p1 = Creator(str1); // 参数折叠为左值引用,调用CData构造函数
CData* p2 = Creator(str1 + str2);// 参数折叠为右值引用,但在Creator函数中t仍为左值,调用CData构造函数!!!
delete p2;
delete p1;
return 0;
}
g++编译上述程序,可得如下结果,印证了注释中的说明:
CData(const std::string& str)
CData(const std::string& str)
~CData()
~CData()
可以看出,在不使用std::forward的情况下,即使传入了右值引用,也无法在Creator函数中触发CData的移动构造函数,从而造成了额外的资源复制损耗。
2)使用std::forward实现
使用std::forward即可完美解决上述问题:
template<typename T>
CData* Creator(T&& t) {
return new CData(std::forward<T>(t));
}
int main(void) {
std::string str1 = "hello";
std::string str2 = " world";
CData* p1 = Creator(str1); // 参数折叠为左值引用,调用CData构造函数
CData* p2 = Creator(str1 + str2); // 参数折叠为右值引用,通过std::forward转发给CData,调用移动构造函数
delete p2;
delete p1;
return 0;
}
g++编译上述程序,可得如下结果,印证了注释中的说明:
CData(const std::string& str)
CData(std::string&& str)
~CData()
~CData()
可以看出,使用了std::forward之后,可以将传入的函数参数按照其原类型进一步传入参数中,从而使右值引用的参数类型可以触发类的移动构造函数,从而避免不必要的资源复制操作,提高参数转移效率。
结论
所谓的完美转发,是指std::forward会将输入的参数原封不动地传递到下一个函数中,这个“原封不动”指的是,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值。
完美转发主要使用两步来完成任务:
- 在模板中使用&&(万能引用)接收参数。
- 使用std::forward()转发给被调函数.
智能指针
智能指针均定义在头文件
#include<memory>
为什么要使用智能指针?
- C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。
- 程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。
- 使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常 时内存泄露等问题等,使用智能指针能更好的管理堆内存。
C++里面的四个智能指针: auto_ptr,unique_ptr,shared_ptr, weak_ptr 其中后三个是C++11支持, 并且第一个已经被C++11弃用。
shared_ptr
- 共享的智能指针
std::shared_ptr
使用引用计数,每一个shared_ptr
的拷贝都指向相同的内存。 - 在最后一个
shared_ptr
析构的时候,内存才会被释放。 shared_ptr
共享被管理对象,同一时刻可以有多个shared_ptr
拥有对象的所有权,当最后一个shared_ptr
对象销毁时,被管理对象自动销毁。
简单来说,shared_ptr实现包含了两部分,
- 一个指向堆上创建的对象的裸指针:
raw_ptr
- 一个指向内部隐藏的、共享的管理对象:
share_count_object
第一部分没什么好说的,第二部分是需要关注的重点. use_count
:当前这个堆上对象被多少对象引用了,简单来说就是引用计数。
shared_ptr的基本用法
初始化
通过构造函数、std::shared_ptr
辅助函数和reset
方法来初始化shared_ptr
,代码如下:
// 智能指针初始化
std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3;
p3.reset(new int(1));
if(p3) {
cout << "p3 is not null";
}
我们应该优先使用make_shared
来构造智能指针,因为他更高效
auto sp1 = make_shared<int>(100);
//相当于
shared_ptr<int> sp1(new int(100));
不能将一个原始指针直接赋值给一个智能指针,例如,下面这种方法是错误的:
std::shared_ptr<int> p = new int(1);
shared_ptr
不能通过“直接将原始这种赋值”来初始化,需要通过构造函数和辅助方法来初始化。
对于一个未初始化的智能指针,可以通过reset
方法来初始化,当智能指针有值的时候调用reset会引起引用计数减1。另外智能指针通过重载的bool类型操作符来判断是否为空。
#include <iostream>
#include <memory>
using namespace std;
void test(shared_ptr<int> sp) {
// sp在test里面的作用域
cout << "test sp.use_count() = " << sp.use_count() << endl;
}
int main() {
auto sp1 = make_shared<int>(100); // 优先使用make_shared来构造智能指针
// 相当于
shared_ptr<int> sp2(new int(100));
// shared_ptr<int> p = new int(1); // 不能将一个原始指针直接赋值给一个智能指针
std::shared_ptr<int> p1;
p1.reset(new int(1)); // 有参数就是分配资源
std::shared_ptr<int> p2 = p1;
// 引用计数此时是2
cout << "p2.use_count() = " << p2.use_count() << endl; // 2
cout << "p1.use_count() = " << p1.use_count() << endl; // 2
p1.reset(); // 没有参数就是释放资源
cout << "p1.reset()" << endl;
cout << "p2.use_count() = " << p2.use_count() << endl; // 1
cout << "p1.use_count() = " << p1.use_count() << endl; // 0
if (nullptr == p1) {
cout << "p1 is empty" << endl;
}
if (nullptr != p2) {
cout << "p2 is not empty" << endl;
}
p2.reset();
cout << "p2.reset()" << endl;
cout << "p2.use_count() = " << p2.use_count() << endl; // 0
if (nullptr == p2) {
cout << "p2 is empty" << endl;
}
shared_ptr<int> sp5(new int(100));
test(sp5);
cout << "sp5.use_count() = " << sp5.use_count() << endl;
return 0;
}
/*
p2.use_count() = 2
p1.use_count() = 2
p1.reset()
p2.use_count() = 1
p1.use_count() = 0
p1 is empty
p2 is not empty
p2.reset()
p2.use_count() = 0
p2 is empty
test sp.use_count() = 2
sp5.use_count() = 1
*/
获取原始指针
当需要获取原始指针时,可以通过get方法来返回原始指针,代码如下所示:
std::shared_ptr<int> ptr(new int(1));
int *p = ptr.get(); //万一不小心 delete p;
谨慎使用p.get()
的返回值,如果你不知道其危险性则永远不要调用get()函数。
p.get()
的返回值就相当于一个裸指针的值,不合适的使用这个值,上述陷阱的所有错误都有可能发生, 遵守以下几个约定:
- 不要保存
p.get()
的返回值 ,无论是保存为裸指针还是shared_ptr
都是错误的,保存为裸指针不知什么时候就会变成空悬指针,保存为shared_ptr
则产生了独立指针 - 不要
delete
p.get()
的返回值 ,会导致对一块内存delete两次的错误
指定删除器
如果用shared_ptr
管理非new对象或是没有析构函数的类时,应当为其传递合适的删除器。
#include <iostream>
#include <memory>
using namespace std;
void DeleteIntPtr(int *p) {
cout << "call DeleteIntPtr" << endl;
delete p;
}
int main() {
shared_ptr<int> p(new int(1), DeleteIntPtr);
shared_ptr<int> p2(new int(1), [](int *p) {
cout << "call lamada1 delete p" << endl;
delete p; });
shared_ptr<int> p3(new int[10], [](int *p) {
cout << "call lamada2 delete[] p" << endl;
delete[] p; // 删除数组
});
return 0;
}
/*
call lamada2 delete[] p
call lamada1 delete p
call DeleteIntPtr
*/
当p的引用计数为0时,自动调用删除器DeleteIntPtr
来释放对象的内存。删除器可以是一个lambda
表达式,上面的写法可以改为:
std::shared_ptr<int> p(new int(1), [](int *p) {
cout << "call lambda delete p" << endl;
delete p;});
当我们用shared_ptr
管理动态数组时,需要指定删除器,因为shared_ptr
的默认删除器不支持数组对象,代码如下所示:
std::shared_ptr<int> p3(new int[10], [](int *p) { delete [] p;});
智能指针什么时候需要指定删除器
在需要delete
以外的析构行为的时候用. 因为shared_ptr
在引用计数为 0 后默认调用delete ptr
; 如果不满足需求就要提供定制的删除器.
一些场景:
- 资源不是
new
出来的(一般也意味着不能delete
), 比如可能是malloc
出来的 - 资源是被第三方库管理的 (第三方提供
资源获取
和资源释放
接口, 那么要么写一个wrapper
类要么就提供定制的deleter
) - 资源不是
RAII
的, 意味着析构函数不会把资源完全释放掉...也就是单纯delete
还不够, 还得做额外的操作,比如你的end_connection
的例子.
使用shared_ptr要注意的问题
- 不要用一个原始指针初始化多个
shared_ptr
,例如下面错误范例:
int *ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr); // 逻辑错误
- 不要在函数实参中创建
shared_ptr
,对于下面的写法:
function(shared_ptr<int>(new int), g()); //有缺陷
因为C++的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的,一般是从右到左,但也可能从左到右,所以,可能的过程是先new int
,然后调用g()
,如果恰好g()发生异常,而shared_ptr
还没有创建, 则int内存泄漏了,正确的写法应该是先创建智能指针,代码如下:
shared_ptr<int> p(new int);
function(p, g());
- 形参和实参的区别和联系
- 形参变量只有在函数被调用时才会分配内存,调用结束后,立刻释放内存,所以形参变量只有在函数内部有效,不能在函数外部使用。
- 实参可以是常量、变量、表达式、函数等,无论实参是何种类型的数据,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参,所以应该提前用赋值、输入等办法使实参获得确定值。
- 实参和形参在数量上、类型上、顺序上必须严格一致,否则会发生“类型不匹配”的错误。当然,如果能够进行自动类型转换,或者进行了强制类型转换,那么实参类型也可以不同于形参类型。
- 函数调用中发生的数据传递是单向的,只能把实参的值传递给形参,而不能把形参的值反向地传递给实参;换句话说,一旦完成数据的传递,实参和形参就再也没有瓜葛了,所以,在函数调用过程中,形参的值发生改变并不会影响实参。
- 通过
shared_from_this()
返回this指针。不要将this指针作为shared_ptr
返回出来,因为this指针本质上是一个裸指针,因此,这样可能会导致重复析构. - 避免循环引用。循环引用会导致内存泄漏
#include <iostream>
#include <memory>
using namespace std;
class A;
class B;
class A {
public:
shared_ptr<B> bptr;
~A() {
cout << "A is deleted" << endl;
}
};
class B {
public:
shared_ptr<A> aptr;
~B() {
cout << "B is deleted" << endl;
}
};
int main() {
shared_ptr<A> pa;
{
shared_ptr<A> ap(new A);
shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
pa = ap;
// 手动释放成员变量才行
ap->bptr.reset();
}
cout << "main leave. pa.use_count() = " << pa.use_count() << endl; // 循环引用导致ap bp退出了作用域都没有析构
return 0;
}
/*
B is deleted
main leave. pa.use_count() = 1
A is deleted
*/
循环引用导致ap和bp的引用计数为2,在离开作用域之后,ap和bp的引用计数减为1,并不回减为0,导致两个指针都不会被析构,产生内存泄漏。
详细解释:
在作用域内ap和bp的引用计数都为2,但是当它们退出循环的时候,ap的引用计数减1,bp的引用计数也减1,但它们依旧不为0,引用计数均为1。
- 对ap来说:只有调用了A的析构函数,才会去释放它的成员变量bptr。何时会调用A的析构函数呢?就是ap的引用计数为0
- 对bp来说,只有调用了B的析构函数,才会去释放它的成员变量aptr。同样是bp的引用计数都为0的时候才能析构。
现在,对于ap和bp来说,它们都拿着对方的share_ptr(有点类似于死锁的现象),没法使得ab和bp的引用计数为0。那么A和B的对象均无法析构。于是造成了内存泄漏。
ap和bp退出作用域了,为什么不会调用析构函数呢?
ap和bp是创建在栈上的,而不是A或者B对象的本身,ap、bp退出作用域,只是ap和bp本身释放了,只会使得A、B对象的引用计数-1,调用析构函数是要A或B的对象的引用计数为0才能执行析构函数。
解决的办法是把A和B任何一个成员变量改为weak_ptr,具体方法见weak_ptr章节。
什么是循环引用?
循环引用(circular reference)是指在编程中,两个或多个对象之间形成一个循环的引用关系,导致这些对象之间的内存无法被正确释放,从而引发内存泄漏。这种情况也被称为循环依赖或循环关联。
unique_ptr独占的智能指针
unique_ptr
是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr
赋值给另一个unique_ptr
。下面的错误示例。
unique_ptr<T> my_ptr(new T);
unique_ptr<T> my_other_ptr = my_ptr; // 报错,不能复制
unique_ptr
不允许复制,但可以通过函数返回给其他的unique_ptr
,还可以通过std::move
来转移到其他的unique_ptr
,这样它本身就不再拥有原来指针的所有权了。例如
unique_ptr<T> my_ptr(new T); // 正确
unique_ptr<T> my_other_ptr = std::move(my_ptr); // 正确
unique_ptr<T> ptr = my_ptr; // 报错,不能复制
std::make_shared
是c++11的一部分,但std::make_unique
不是。它是在c++14里加入标准库的。
auto upw1(std::make_unique<Widget>()); // with make func
std::unique_ptr<Widget> upw2(new Widget); // without make func
使用new的版本重复了被创建对象的键入,但是make_unique
函数则没有。重复类型违背了软件工程的一个重要原则:应该避免代码重复,代码中的重复会引起编译次数增加,导致目标代码膨胀。
除了unique_ptr
的独占性, unique_ptr
和shared_ptr
还有一些区别,比如
unique_ptr
可以指向一个数组,代码如下所示
std::unique_ptr<int []> ptr(new int[10]);
ptr[9] = 9;
std::shared_ptr<int []> ptr2(new int[10]); // 这个是不合法的
unique_ptr
指定删除器和shared_ptr
有区别
std::shared_ptr<int> ptr3(new int(1), [](int *p){delete p;}); // 正确
std::unique_ptr<int> ptr4(new int(1), [](int *p){delete p;}); // 错误
unique_ptr
需要确定删除器的类型,所以不能像shared_ptr
那样直接指定删除器,可以这样写:
std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete p;}); //正确
关于shared_ptr
和unique_ptr
的使用场景是要根据实际应用需求来选择。如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr
,如果希望多个智能指针管理同一个资源就用shared_ptr
。
#include <iostream>
#include <memory>
using namespace std;
struct MyDelete {
void operator()(int *p) {
cout << "delete" << endl;
delete p;
}
};
int main() {
auto upw1(make_unique<int[]>(2)); // with make func make_unique需要设置C++14
unique_ptr<int> upw2(new int); // without make func
upw1[21] = 1;
unique_ptr<int, MyDelete> ptr2(new int(1));
// auto ptr(make_unique<int, MyDelete>(1)); //报错
cout << "main finish! " << upw1[1] << endl;
return 0;
}
/*
main finish! 0
delete
*/
weak_ptr弱引用的智能指针
share_ptr
虽然已经很好用了,但是有一点share_ptr
智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr
成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。
weak_ptr
是一种不控制对象生命周期的智能指针, 它指向一个shared_ptr
管理的对象. 进行该对象的内存管理的是那个强引用的shared_ptr
, weak_ptr
只是提供了对管理对象的一个访问手段。weak_ptr
设计的目的是为配合shared_ptr
而引入的一种智能指针来协助shared_ptr
工作, 它只可以从一个shared_ptr
或另一个weak_ptr
对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr
是用来解决shared_ptr
相互引用时的死锁问题,如果说两个shared_ptr
相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr
之间可以相互转化,shared_ptr
可以直接赋值给它,它可以通过调用lock函数
来获得shared_ptr
。
weak_ptr
没有重载操作符*和->,因为它不共享指针,不能操作资源,主要是为了通过shared_ptr
获得资源的监测权,它的构造不会增加引用计数,它的析构也不会减少引用计数,纯粹只是作为一个旁观者来监视shared_ptr
中管理的资源是否存在。weak_ptr
还可以返回this指针和解决循环引用的问题。
weak_ptr的基本用法
- 通过
use_count()
方法获取当前观察资源的引用计数,如下所示:
shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
cout << wp.use_count() << endl; //结果讲输出1
- 通过
expired()
方法判断所观察资源是否已经释放,如下所示:
expired
常见释义
英[ɪkˈspaɪəd] 美[ɪkˈspaɪərd]
v.到期;届满;(因到期而)失效,终止;去世;逝世;故去;
adj.过期的;失效的;
shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
if(wp.expired())
cout << "weak_ptr无效,资源已释放";
else
cout << "weak_ptr有效";
- 通过
lock
方法获取监视的shared_ptr
,如下所示:
std::weak_ptr<int> gw;
void f()
{
if(gw.expired()) {
cout << "gw无效,资源已释放";
}
else {
auto spt = gw.lock();
cout << "gw有效, *spt = " << *spt << endl;
}
}
int main()
{
{
auto sp = std::make_shared<int>(42);
gw = sp;
f();
}
f();
return 0;
}
weak_ptr返回this指针
shared_ptr
章节中提到不能直接将this指针返回shared_ptr
,需要通过派生std::enable_shared_from_this类
,并通过其方法shared_from_this
来返回指针,原因是 std::enable_shared_from_this类
中有一个weak_ptr
,这个weak_ptr
用来观察this智能指针,调用shared_from_this()
方法是,会调用内部这个weak_ptr
的lock()
方法,将所观察的shared_ptr
返回,再看前面的范例
//1-1-shared_from_this2
#include <iostream>
#include <memory>
using namespace std;
class A: public std::enable_shared_from_this<A>
{
public:
shared_ptr<A>GetSelf()
{
return shared_from_this();
}
~A()
{
cout << "Deconstruction A" << endl;
}
};
int main()
{
// auto spp = make_shared<A>();
shared_ptr<A> sp1(new A);
shared_ptr<A> sp2 = sp1->GetSelf(); // ok
// shared_ptr<A> sp2;
// {
// shared_ptr<A> sp1(new A);
// sp2 = sp1->GetSelf(); // ok
// }
cout << "sp1.use_count() = " << sp1.use_count()<< endl;
cout << "sp2.use_count() = " << sp2.use_count()<< endl;
return 0;
}
/*
sp1.use_count() = 2
sp2.use_count() = 2
Deconstruction A
*/
在外面创建A对象的智能指针和通过对象返回this的智能指针都是安全的,因为shared_from_this()
是内部的weak_ptr
调用lock()方法之后返回的智能指针,在离开作用域之后,spy的引用计数减为0,A对象会被析构,不会出现A对象被析构两次的问题。
需要注意的是,获取自身智能指针的函数尽量在shared_ptr
的构造函数被调用之后才能使用,因为enable_shared_from_this
内部的weak_ptr
只有通过shared_ptr
才能构造。
weak_ptr解决循环引用问题
在shared_ptr
章节提到智能指针循环引用的问题,因为智能指针的循环引用会导致内存泄漏,可以通过weak_ptr
解决该问题,只要将A或B的任意一个成员变量改为weak_ptr
#include <iostream>
#include <memory>
using namespace std;
class A;
class B;
class A {
public:
std::weak_ptr<B> bptr; // 修改为weak_ptr
int *val;
A() {
val = new int(1);
}
~A() {
cout << "A is deleted" << endl;
delete val;
}
};
class B {
public:
std::shared_ptr<A> aptr;
~B() {
cout << "B is deleted" << endl;
}
};
//weak_ptr 是一种不控制对象生命周期的智能指针,
void test()
{
std::shared_ptr<A> ap(new A);
std::weak_ptr<A> wp1 = ap;
std::weak_ptr<A> wp2 = ap;
cout<< "ap.use_count()" << ap.use_count()<< endl;
}
void test2()
{
std::weak_ptr<A> wp;
{
std::shared_ptr<A> ap(new A);
wp = ap;
}
cout<< "wp.use_count()" << wp.use_count() << ", wp.expired():" << wp.expired() << endl;
if(!wp.expired()) {
// wp不能直接操作对象的成员、方法
std::shared_ptr<A> ptr = wp.lock(); // 需要先lock获取std::shared_ptr<A>
*(ptr->val) = 20;
}
}
int main()
{
test2();
// {
// std::shared_ptr<A> ap(new A);
// std::shared_ptr<B> bp(new B);
// ap->bptr = bp;
// bp->aptr = ap;
// }
cout<< "main leave" << endl;
return 0;
}
/*
A is deleted
wp.use_count()0, wp.expired():1
main leave
*/
这样在对B的成员赋值时,即执行bp->aptr=ap;
时,由于aptr是weak_ptr
,它并不会增加引用计数,所 以ap的引用计数仍然会是1,在离开作用域之后,ap的引用计数为减为0,A指针会被析构,析构后其内部的bptr的引用计数会被减为1,然后在离开作用域后bp引用计数又从1减为0,B对象也被析构,不会发生内存泄漏。
weak_ptr使用注意事项
weak_ptr在使用前需要检查合法性。
weak_ptr<int> wp;
{
shared_ptr<int> sp(new int(1)); //sp.use_count()==1
wp = sp; //wp不会改变引用计数,所以sp.use_count()==1
shared_ptr<int> sp_ok = wp.lock(); //wp没有重载->操作符。只能这样取所指向的对象
}
shared_ptr<int> sp_null = wp.lock(); //sp_null.use_count()==0;
因为上述代码中sp和sp_ok离开了作用域,其容纳的K对象已经被释放了。 得到了一个容纳NULL指针的sp_null对象。在使用wp前需要调用wp.expired()函数判断一下。 因为wp还仍旧存在,虽然引用计数等于0,仍有某处“全局”性的存储块保存着这个计数信息。直到最后一个weak_ptr对象被析构,这块“堆”存储块才能被回收。
如果shared_ptr sp_ok
和weak_ptr wp
;属于同一个作用域呢?如下所示:
weak_ptr<int> wp;
shared_ptr<int> sp_ok;
{
shared_ptr<int> sp(new int(1)); //sp.use_count()==1
wp = sp; //wp不会改变引用计数,所以sp.use_count()==1
sp_ok = wp.lock(); //wp没有重载->操作符。只能这样取所指向的对象
}
if(wp.expired()) {
cout << "shared_ptr is destroy" << endl;
} else {
cout << "shared_ptr no destroy" << endl;
}
/*
shared_ptr no destroy
*/
智能指针中的删除器
在C++中,智能指针是一种用于管理动态内存分配的工具,它们可以自动释放内存,避免内存泄漏。删除器(deleter)是智能指针的一部分,用于指定在释放指针所指向的内存时应该执行的操作。删除器通常用于执行一些额外的清理工作,例如释放资源或调用特定的析构函数。
在C++中,有两种常见的智能指针:std::shared_ptr
和std::unique_ptr
。下面将分别介绍如何使用删除器来自定义内存释放操作。
使用std::shared_ptr
的删除器
对于std::shared_ptr
,您可以在构造shared_ptr
对象时传入一个删除器。删除器可以是lambda表达式、函数对象或函数指针。以下是一个示例,演示了如何使用自定义删除器来释放SDL_Surface
对象:
struct SDL_Surface_Deleter {
void operator()(SDL_Surface* surface) {
SDL_FreeSurface(surface);
}
};
using SDL_Surface_ptr = std::shared_ptr<SDL_Surface>;
// 创建shared_ptr对象并指定自定义删除器
SDL_Surface_ptr surfacePtr(new SDL_Surface(), SDL_Surface_Deleter{});
使用std::unique_ptr
的删除器
对于std::unique_ptr
,删除器类型是指针类型的一部分。您可以使用函数对象、函数指针或lambda表达式作为删除器。以下是一个示例,演示了如何使用自定义删除器来释放MyType
对象:
class MyType {
// ...
};
void deleter(MyType* ptr) {
// 自定义的删除操作
}
// 使用std::unique_ptr和自定义删除器
std::unique_ptr<MyType, decltype(&deleter)> uniquePtr(new MyType(), deleter);
总之,删除器是智能指针的一个重要概念,它允许您指定在释放内存时应该执行的自定义操作。这使得智能指针更加灵活,并且可以用于管理各种类型的资源。
区别
在C++中,std::unique_ptr
和std::shared_ptr
是两种不同的智能指针,它们在处理删除器(deleter)时有一些区别。
std::unique_ptr
的删除器
对于std::unique_ptr
,删除器是作为模板参数传递的。这意味着在创建std::unique_ptr
对象时,您可以指定要使用的删除器类型。这种设计使得删除器的类型在编译时就确定了,因此删除器是std::unique_ptr
类型的一部分。
template <class T, class Deleter = std::default_delete<T>>
class unique_ptr;
std::shared_ptr
的删除器
相比之下,std::shared_ptr
的删除器是作为构造函数参数传入的。这意味着您可以在创建std::shared_ptr
对象时,通过构造函数参数来指定要使用的删除器。这种设计使得删除器的类型可以在运行时动态确定,因此删除器不是std::shared_ptr
类型的一部分。
template <class T>
class shared_ptr;
template<class Deleter, class T>
Deleter* get_deleter(const std::shared_ptr<T>& p);
总的来说,std::unique_ptr
和std::shared_ptr
在处理删除器时有所不同,这种差异主要体现在删除器类型的保存和传递方式上。
常见的删除器类型
对于std::shared_ptr
和std::unique_ptr
,常见的删除器类型包括以下几种:
- 函数对象(Function Object):您可以定义一个类,重载
operator()
,并在其中执行所需的删除操作。这种方式适用于需要执行一些复杂的清理操作的情况。
struct CustomDeleter {
void operator()(T* ptr) {
// 执行自定义的删除操作
}
};
- 函数指针(Function Pointer):您可以直接传入一个函数指针作为删除器,该函数指针指向执行删除操作的函数。
void customDeleter(T* ptr) {
// 执行自定义的删除操作
}
// 使用函数指针作为删除器
std::shared_ptr<T> ptr(new T, customDeleter);
- Lambda表达式:在C++11及更高版本中,您可以使用Lambda表达式作为删除器,这使得定义和使用删除器更加简洁。
// 使用Lambda表达式作为删除器
std::shared_ptr<T> ptr(new T, [](T* ptr) {
// 执行自定义的删除操作
delete ptr;
});
总的来说,这些是在std::shared_ptr
和std::unique_ptr
中常见的删除器类型,它们允许您以不同的方式指定在释放指针所指向的内存时应该执行的自定义操作。
lambda表达式
概述
Lambda表达式是现代C++在C++11和更高版本中的一个新的语法糖 ,在C++11、C++14、C++17和C++20中Lambda表达的内容还在不断更新。
lambda表达式(也称为lambda函数)是在调用或作为函数参数传递的位置处定义匿名函数对象的便捷方法。通常,lambda用于封装传递给算法或异步方法的几行代码。
定义
示例
Lambda有很多叫法,有Lambda表达式、Lambda函数、匿名函数,本文中为了方便表述统一用Lambda表达式进行叙述。 ISO C++标准官网展示了一个简单的lambda表示式实例:
#include <algorithm>
#include <cmath>
void abssort(float* x, unsigned n) {
std::sort(x, x + n,
// Lambda expression begins
[](float a, float b) {
return (std::abs(a) < std::abs(b));
} // end of lambda expression
);
}
在上面的实例中std::sort
函数第三个参数应该是传递一个排序规则的函数,但是这个实例中直接将排序函数的实现写在应该传递函数的位置,省去了定义排序函数的过程,对于这种不需要复用,且短小的函数,直接传递函数体可以增加代码的可读性。
语法定义
- 捕获列表。在C++规范中也称为Lambda导入器,捕获列表总是出现在Lambda函数的开始处。实际上,
[]
是Lambda引出符。编译器根据该引出符判断接下来的代码是否是Lambda函数,捕获列表能够捕捉上下文中的变量以供Lambda函数使用。 - 参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号“()”一起省略。
- 可变规格。
mutable
修饰符, 默认情况下Lambda函数总是一个const函数
,mutable
可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)。 - 异常说明。用于Lamdba表达式内部函数抛出异常。
- 返回类型。追踪返回类型形式声明函数的返回类型。我们可以在不需要返回值的时候也可以连同符号“->”一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。
- lambda函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。
参数详解
- Lambda捕获列表
Lambda表达式与普通函数最大的区别是,除了可以使用参数以外,Lambda函数还可以通过捕获列表访问一些上下文中的数据。具体地,捕捉列表描述了上下文中哪些数据可以被Lambda使用,以及使用方式(以值传递的方式或引用传递的方式)。语法上,在“[]
”包括起来的是捕获列表,捕获列表由多个捕获项组成,并以逗号分隔。捕获列表有以下几种形式:[]
表示不捕获任何变量
auto function = ([]{ std::cout << "Hello World!" << std::endl; } ); function();
[var]
表示值传递方式捕获变量var
int num = 100; auto function = ([num]{ std::cout << num << std::endl; } ); function();
[=]
表示值传递方式捕获所有父作用域的变量(包括this
)
int index = 1; int num = 100; auto function = ([=]{ std::cout << "index: "<< index << ", " << "num: "<< num << std::endl; } ); function();
[&var]
表示引用传递捕捉变量var
int num = 100; auto function = ([&num]{ num = 1000; std::cout << "num: " << num << std::endl; } ); function();
[&]
表示引用传递方式捕捉所有父作用域的变量(包括this
)
int index = 1; int num = 100; auto function = ([&]{ num = 1000; index = 2; std::cout << "index: "<< index << ", " << "num: "<< num << std::endl; } ); function();
[this]
表示值传递方式捕捉当前的this
指针
#include <iostream> using namespace std; class Lambda { public: void sayHello() { std::cout << "Hello" << std::endl; }; void lambda() { auto function = [this]{ this->sayHello(); }; function(); } }; int main() { Lambda demo; demo.lambda(); }
[=, &]
拷贝与引用混合[=, &a, &b]
表示以引用传递的方式捕捉变量a
和b
,以值传递方式捕捉其它所有变量。int index = 1; int num = 100; auto function = ([=, &index, &num]{ num = 1000; index = 2; std::cout << "index: "<< index << ", " << "num: "<< num << std::endl; } ); function();
[&, a, this]
表示以值传递的方式捕捉变量a
和this
,引用传递方式捕捉其它所有变量。
不过值得注意的是,捕捉列表不允许变量重复传递。下面一些例子就是典型的重复,会导致编译时期的错误。例如:[=,a]
这里已经以值传递方式捕捉了所有变量,但是重复捕捉a
了,会报错的;[&,&this]
这里&
已经以引用传递方式捕捉了所有变量,再捕捉this
也是一种重复。
如果Lambda主体total
通过引用访问外部变量,并factor
通过值访问外部变量,则以下捕获子句是等效的:[&total, factor] [factor, &total] [&, factor] [factor, &] [=, &total] [&total, =]
- Lambda参数列表
除了捕获列表之外,Lambda还可以接受输入参数。参数列表是可选的,并且在大多数方面类似于函数的参数列表。auto function = [] (int first, int second){ return first + second; }; function(100, 200);
- 可变规格mutable
mutable修饰符, 默认情况下Lambda函数总是一个const函数,mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)。#include <iostream> using namespace std; int main() { int m = 0; int n = 0; [&, n] (int a) mutable { m = ++n + a; }(4); cout << m << endl << n << endl; }
- 异常说明
你可以使用throw()
异常规范来指示Lambda表达式不会引发任何异常。与普通函数一样,如果Lambda表达式声明C4297异常规范且Lambda体引发异常,Visual C++ 编译器将生成警告throw()。
在MSDN的异常规范中,明确指出异常规范是在 C++11 中弃用的 C++ 语言功能。因此这里不建议不建议大家使用。int main() // C4297 expected { []() throw() { throw 5; }(); }
- 返回类型
Lambda表达式的返回类型会自动推导。除非你指定了返回类型,否则不必使用关键字。返回型类似于通常的方法或函数的返回型部分。但是,返回类型必须在参数列表之后,并且必须在返回类型->之前包含类型关键字。如果Lambda主体仅包含一个return语句或该表达式未返回值,则可以省略Lambda表达式的return-type部分。如果Lambda主体包含一个return语句,则编译器将从return表达式的类型中推断出return类型。否则,编译器将返回类型推导为void。auto x1 = [](int i){ return i; };
- Lambda函数体
Lambda表达式的Lambda主体(标准语法中的复合语句)可以包含普通方法或函数的主体可以包含的任何内容。普通函数和Lambda表达式的主体都可以访问以下类型的变量:- 捕获变量
- 形参变量
- 局部声明的变量
- 类数据成员,当在类内声明this并被捕获时
- 具有静态存储持续时间的任何变量,例如全局变量
#include <iostream> using namespace std; int main() { int m = 0; int n = 0; [&, n] (int a) mutable { m = ++n + a; }(4); cout << m << endl << n << endl; }
优缺点
Lambda表达式的优点
- 可以直接在需要调用函数的位置定义短小精悍的函数,而不需要预先定义好函数
std::find_if(v.begin(), v.end(), [](int& item){return item > 2});
- 使用Lamdba表达式变得更加紧凑,结构层次更加明显、代码可读性更好
Lambda表达式的缺点
- Lamdba表达式语法比较灵活,增加了阅读代码的难度
- 对于函数复用无能为力
工作原理
编译器会把一个Lambda表达式生成一个匿名类的匿名对象,并在类中重载函数调用运算符,实现了一个operator()方法。
auto print = []{cout << "Hello World!" << endl; };
编译器会把上面这一句翻译为下面的代码:
class print_class
{
public:
void operator()(void) const
{
cout << "Hello World!" << endl;
}
};
// 用构造的类创建对象,print此时就是一个函数对象
auto print = print_class();
C++仿函数
仿函数(functor)又称为函数对象(function object)是一个能行使函数功能的类。仿函数的语法几乎和我们普通的函数调用一样,不过作为仿函数的类,都必须重载operator()运算符,仿函数与Lamdba表达式的作用是一致的。举个例子:
#include <iostream>
#include <string>
using namespace std;
class Functor
{
public:
void operator() (const string& str) const
{
cout << str << endl;
}
};
int main()
{
Functor myFunctor;
myFunctor("Hello world!");
return 0;
}
适用场景
- Lamdba表达式应用于STL算法库
for_each应用实例
find_if应用实例int a[4] = {11, 2, 33, 4}; sort(a, a+4, [=](int x, int y) -> bool { return x%10 < y%10; } ); for_each(a, a+4, [=](int x) { cout << x << " ";} );
remove_if应用实例int x = 5; int y = 10; deque<int> coll = { 1, 3, 19, 5, 13, 7, 11, 2, 17 }; auto pos = find_if(coll.cbegin(), coll.cend(), [=](int i) { return i > x && i < y; });
std::vector<int> vec_data = {1, 2, 3, 4, 5, 6, 7, 8, 9}; int x = 5; vec_data.erase(std::remove_if(vec.date.begin(), vec_data.end(), [](int i) { return n < x;}), vec_data.end()); std::for_each(vec.date.begin(), vec_data.end(), [](int i) { std::cout << i << std::endl;});
- 短小不需要复用函数场景
sort函数#include <iostream> #include <vector> #include <algorithm> using namespace std; int main(void) { int data[6] = { 3, 4, 12, 2, 1, 6 }; vector<int> testdata; testdata.insert(testdata.begin(), data, data + 6); // 对于比较大小的逻辑,使用lamdba不需要在重新定义一个函数 sort(testdata.begin(), testdata.end(), [](int a, int b){ return a > b; }); return 0; }
- Lamdba表达式应用于多线程场景
#include <iostream> #include <thread> #include <vector> #include <algorithm> int main() { // vector 容器存储线程 std::vector<std::thread> workers; for (int i = 0; i < 5; i++) { workers.push_back(std::thread([]() { std::cout << "thread function\n"; })); } std::cout << "main thread\n"; // 通过 for_each 循环每一个线程 // 第三个参数赋值一个task任务 // 符号'[]'会告诉编译器我们正在用一个匿名函数 // lambda函数将它的参数作为线程的引用t // 然后一个一个的join std::for_each(workers.begin(), workers.end(), [](std::thread &t;) { t.join(); }); return 0; }
std::mutex mutex; std::condition_variable condition; std::queue<std::string> queue_data; std::thread threadBody([&]{ std::unique_lock<std::mutex> lock_log(mutex); condition.wait(lock_log, [&]{ return !queue_data.front(); }); std::cout << "queue data: " << queue_data.front(); lock_log.unlock(); }); queue_data.push("this is my data"); condition.notity_one(); if(threadBody.joinable()) { threadBody.join(); }
- Lamdba表达式应用于函数指针与function
#include <iostream> #include <functional> using namespace std; int main(void) { int x = 8, y = 9; auto add = [](int a, int b) { return a + b; }; std::function<int(int, int)> Add = [=](int a, int b) { return a + b; }; cout << "add: " << add(x, y) << endl; cout << "Add: " << Add(x, y) << endl; return 0; }
- Lamdba表达式作为函数的入参
using FuncCallback = std::function<void(void)>; void DataCallback(FuncCallback callback) { std::cout << "Start FuncCallback!" << std::endl; callback(); std::cout << "End FuncCallback!" << std::endl; } auto callback_handler = [&](){ std::cout << "This is callback_handler"; }; DataCallback(callback_handler);
- Lamdba表达式在QT中的应用
QTimer *timer=new QTimer; timer->start(1000); QObject::connect(timer, &QTimer::timeout, [&](){ qDebug() << "Lambda表达式"; }); int a = 10; QString str1 = "汉字博大精深"; connect(pBtn4, &QPushButton::clicked, [=](bool checked){ qDebug() << a <<str1; qDebug() << checked; qDebug() << "Hua Windows Lambda Button"; });