nullptr
NULL一般来自C语言,是宏定义,在C++语言中,NULL被定义为整数0,但是这样会无法与整数0区分,所以C++11引入新特性nullptr,可以区分整数与指针特性,但是仍然存在问题-不能区分指针类型,这种情况下,必须显示的指明参数类型。
forward_list
只能单向遍历。
- 内存占用小
- 插入删除效率高
- 无法随机访问
- 插入删除会改变链表结构,所以迭代器可能会失效
- 不支持逆向遍历
bind
可以通过引用来直接操作变量的值,而bind函数可以用来将函数对象和参数列表绑定成一个可调用对象。如果想要在bind函数中传递引用,可以使用std::ref()函数来包装引用对象,例如:
#include <functional>
#include <iostream>
void add(int& a, int& b)
{
a += b;
}
int main()
{
int a = 10;
int b = 5;
auto func = std::bind(add, std::ref(a), std::ref(b));
func();
std::cout << "a = " << a << std::endl; //a=15
std::cout << "b = " << b << std::endl; //b=5
return 0;
}
auto与decltype
auto可以通过初始值来进行类型推演,所以auto类型必须要有初始值。
decltype的作用是选择并返回操作数的数据类型,在这个过程中,编译器只是得到表达式的类型,而不用实际计算出表达式的值。
lambda
可以在代码中直接定义匿名函数,lambda函数可以作为参数传递给函数或者算法也可以作为返回值。编译器会生成一个匿名类,被称为闭包类型。
capture list用于捕获外部变量,可以是空或包含一个或多个变量。parameter list和return type(如果函数体是一个表达式则可以自动推导返回类型;如果是一个语句块,则需要显式指定返回类型)与普通函数的参数列表和返回类型相同。function body是函数体,可以包含一系列语句。
[capture list] (parameter list) -> return type { function body }
例子
auto add = [](int a, int b) -> int {
return a + b;
};
int sum = add(1, 2); // sum = 3
lambda函数还可以捕获外部变量
int x = 10;
auto print = [x](int n) {
std::cout << "x = " << x << ", n = " << n << std::endl;
};
print(5); // x = 10, n = 5
还支持函数对象,可以用于实现比较、排序等算法
std::vector<int> v{5, 1, 3, 2, 4};
std::sort(v.begin(), v.end(), [](int a, int b) {
return a > b;
});
lambda函数可以忽略参数列表和返回值,但是必须包括捕获列表和函数体。
智能指针
防止堆内存泄露。智能指针是一个模板类,用来存储指向动态分配的对象的指针,负责自动释放动态分配的对象。符合RAII机制
shared_ptr
用于管理动态分配的对象,可以实现多个智能指针共享一个对象,并在所有智能指针都不再引用该对象时自动释放该对象。使用引用计数,每使用一次,计数加一,析构一次,计数减一,对象读取需要加锁,内部引用计数是线程安全的。
shared_ptr会有循环引用的问题,也就是两个对象互相使用一个shared_ptr成员变量指向对方。
weak_ptr
所以引入weak_ptr,弱引用一般是辅助shared_ptr使用的,并不对对象的内存进行管理,类似于普通指针,但是弱引用可以检测到所管理的对象是否已经被释放。弱引用只引用,不计数,弱引用并不保证所指向的内存是一定有效的。
std::weak_ptr可以通过std::shared_ptr进行初始化,当std::shared_ptr对象被销毁时,std::weak_ptr并不会影响该对象的引用计数,也不会自动释放该对象。相反,当需要使用该对象时,可以通过std::weak_ptr的lock()方法获取一个可用的std::shared_ptr对象,如果该对象已经被释放,则返回一个空的std::shared_ptr对象。
为什么可以解决循环引用的问题?
- 可以将循环引用中的某个shared_ptr指针换成weak_ptr指针,这样不会增加引用计数。
#include <iostream>
#include <memory>
struct Node;
struct Edge {
std::weak_ptr<Node> end;
};
struct Node {
std::vector<std::shared_ptr<Edge>> edges;
};
int main() {
std::shared_ptr<Node> node1(new Node);
std::shared_ptr<Node> node2(new Node);
node1->edges.push_back(std::make_shared<Edge>());
node1->edges.back()->end = node2;
// 在使用node2之前,先通过weak_ptr获取node2
std::shared_ptr<Node> node2_copy = node1->edges.back()->end.lock();
if (node2_copy) {
std::cout << "node2 is alive" << std::endl;
} else {
std::cout << "node2 is dead" << std::endl;
}
// 手动销毁node2
node2.reset();
// 再次使用node2
node2_copy = node1->edges.back()->end.lock();
if (node2_copy) {
std::cout << "node2 is alive" << std::endl;
} else {
std::cout << "node2 is dead" << std::endl;
}
return 0;
}
make_shared
make_shared是C++11引入的一个模板函数,它可以用于创建指向动态分配对象的std::shared_ptr智能指针,并将对象的内存分配和构造函数调用合并在一起,从而可以提高性能和内存使用效率。make_shared可以避免多次分配内存,减少内存碎片,并且可以减少动态分配对象的构造和析构次数,从而提高程序的性能。
实现一个shared_ptr类
1、shared_ptr需要计数指向同一个对象的指针的数量,保证在所有的指针全部释放后,对象才会被删除。
2、shared_ptr需要正确处理空指针。
3、shared_ptr需要能够正确地拷贝和赋值。
代码来源阿秀的学习笔记
template<typename T>
class SharedPtr
{
public:
SharedPtr(T* ptr = NULL):_ptr(ptr), _pcount(new int(1))
{}
SharedPtr(const SharedPtr& s):_ptr(s._ptr), _pcount(s._pcount){
(*_pcount)++;
}
SharedPtr<T>& operator=(const SharedPtr& s){
if (this != &s)
{
if (--(*(this->_pcount)) == 0)
{
delete this->_ptr;
delete this->_pcount;
}
_ptr = s._ptr;
_pcount = s._pcount;
*(_pcount)++;
}
return *this;
}
T& operator*()
{
return *(this->_ptr);
}
T* operator->()
{
return this->_ptr;
}
~SharedPtr()
{
--(*(this->_pcount));
if (*(this->_pcount) == 0)
{
delete _ptr;
_ptr = NULL;
delete _pcount;
_pcount = NULL;
}
}
private:
T* _ptr;
int* _pcount;//指向引用计数的指针
};
make_shared是C++11之后的函数模板,头文件是,用于在动态内存中分配一个对象并初始化它,返回指向这个对象的shared_ptr。使用方法如下:
std::shared_ptr sp = std::make_shared(args…);
使用make_shared可以提高效率,避免使用shared_ptr(new T(args…))的方式。
unique_ptr
独享所有权,转移一个unique_ptr将会把所有权全部从源指针转移给目标指针,原指针置空.。unique_ptr不支持普通的拷贝和赋值操作,拷贝一个unique_ptr,那么在拷贝结束之后,都会指向相同的资源,最后多次对同一块内存指针多次释放,造成程序崩溃。
实现一个unique_ptr
unique_ptr不能被复制,所以拷贝构造函数和拷贝赋值运算符都被禁用了。
移动构造函数和移动运算符则是将指针所有权转移给目标unique_ptr,原始指针成为nullptr。
template class unique_ptr {
public:
unique_ptr() : ptr(nullptr) {}
explicit unique_ptr(T* p) : ptr(p) {}
unique_ptr(const unique_ptr& other) = delete;
unique_ptr& operator=(const unique_ptr& other) = delete;
unique_ptr(unique_ptr&& other) : ptr(other.ptr) { other.ptr = nullptr; }
unique_ptr& operator=(unique_ptr&& other) {
if (&other == this) return *this;
delete ptr;
ptr = other.ptr;
other.ptr = nullptr;
return *this;
}
~unique_ptr() { if (ptr) delete ptr; }
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
//返回对象原始指针
T* get() const { return ptr; }
void reset() {
delete ptr;
ptr = nullptr;
}
void reset(T* p) {
delete ptr;
ptr = p;
}
private:
T* ptr;
};
int main() {
unique_ptr up(new int(42));
std::cout << up << std::endl; // 输出42
up.reset(new int(17));
std::cout << up << std::endl; // 输出17
}
auto_ptr
为了解决“有异常抛出时发生内存泄漏”的问题,因为常规指针在抛出异常的时候有可能导致空间得不到释放而内存泄露。auto_ptr构造的时候获得某个对象的控制权,析构的时候释放这个对象。
- auto_ptr对象析构的时候会删除它所拥有的指针,使用的时候避免多个auto_ptr对象管理同一个指针。
- auto_ptr内部是使用“delete”所以不能管理数组。
左值和右值
-
在C++11中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值、将亡值。在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。
-
左值是指既能出现在等号左边也能出现在等号右边的变量(或表达式),右值则只能出现在等号右边
-
左值持久,右值短暂,左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象(将要被销毁的对象)。
-
纯右值指的是临时变量和不跟对象关联的字面量值;将亡值就是在确保其他变量不再使用的时候,通过盗取的方式避免内存空间释放,延长声明周期。
-
右值表示无法获取地址的对象,有常量值、函数返回值、lambda表达式等。无法获取地址,但不表示其不可改变,当定义了右值的右值引用时就可以更改右值。
左值引用和右值引用
- 由于右值通常不具有名字,所以也只能通过引用的方式找到他。C++11中增加了右值引用,右值引用关联到右值时,右值被存储到特定位置,右值引用指向该特定位置,也就是说,右值虽然无法获取地址,但是右值引用是可以获取地址的,该地址表示临时对象的存储位置
- 左值引用是具体变量值的别名,右值引用是匿名的变量的别名。常量左值引用可以接受非常量左值、常量左值、右值对其初始化;非常量左值只能接受非常量左值对其进行初始化。
- 右值值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值。
- 右值引用独立于左值和右值,也就是说右值引用类型的变量有可能是左值或者右值。
代码来源阿秀的学习笔记
#include <bits/stdc++.h>
using namespace std;
template<typename T>
void fun(T&& t)
{
cout << t << endl;
}
int getInt()
{
return 5;
}
int main() {
int a = 10;
int& b = a; //b是左值引用
int& c = 10; //错误,c是左值不能使用右值初始化
int&& d = 10; //正确,右值引用用右值初始化
int&& e = a; //错误,e是右值引用不能使用左值初始化
const int& f = a; //正确,左值常引用相当于是万能型,可以用左值或者右值初始化
const int& g = 10;//正确,左值常引用相当于是万能型,可以用左值或者右值初始化
const int&& h = 10; //正确,右值常引用
const int& aa = h;//正确
int& i = getInt(); //错误,i是左值引用不能使用临时变量(右值)初始化
int&& j = getInt(); //正确,函数返回值是右值
fun(10); //此时fun函数的参数t是右值
fun(a); //此时fun函数的参数t是左值
return 0;
}
move
- 移动语义:有些类的资源是独一无二的,比如IO,unique_ptr等,他们不可以复制,但是可以把资源交出所有权给新的对象,成为可移动的。
- move语义在一些对象的构造时可以获取到已有的资源而不需要通过拷贝,申请新的内存。这样数据的拷贝就不是深拷贝了,而是浅拷贝。这样移动而非拷贝将会大幅度提升性能。例如一些即将消亡的右值,可以用移动语义接管
move的实现
实际上move就是一个类型转换器,将左值转换成右值
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_case<typename remove_reference<T>::type&&>(t);
}
remove_reference
通过模板去除引用,拿int来说,不管传入的是左值引用,还是右值引用,都只会返回这个值去掉引用之后的类型,那么经过remove_reference之后,返回的都是int。
template <typename T>
struct remove_reference{
typedef T type; //成员类型
};
template <typename T>
struct remove_reference<T&> //左值引用
{
typedef T type;//返回T本身的类型
}
template <typename T>
struct remove_reference<T&&> //右值引用
{
typedef T type;//返回T本身的类型
}
static_cast的作用是强制类型转换,可以强行将左值转换为右值,那么在move中,就强转为右值引用。所以move底层代码可以翻译成如下形式:
template <typename T>
int&& move(T&& t)
{
return (int&&)(t);
}
简单来说,move函数就是通过remove_reference获取引用对象本身的类型,强行转成右值引用的方式实现的。
通用引用
move的输入参数类型称为通用引用类型,通用引用就是既可以接收左值也可以接收右值。
代码来源C++高阶知识:深入分析移动构造函数及其原理
#include<iostream>
template<typename T>
void f(T&& param){
std::cout << "the value is "<< param << std::endl;
}
int main(int argc, char *argv[]){
int a = 123;
auto && b = 5; //通用引用,可以接收右值
int && c = a; //错误,右值引用,不能接收左值
auto && d = a; //通用引用,可以接收左值
const auto && e = a; //错误,加了const就不再是通用引用了
func(a); //通用引用,可以接收左值
func(10); //通用引用,可以接收右值
}
在上述代码中有两种类型的通用引用,一种是auto,另一种是通过模板定义的T&&,实际上auto和T是等价的。
完美转发
forward
作用是保持原来值的属性不变也就是说,如果原来是左值,那么处理之后还是左值;如果原来是右值,那么处理之后还是右值。
代码来源聊聊C++中的完美转发
#include<iostream>
template<typename T>
void print(T & t){
std::cout << "lvalue" << std::endl;
}
template<typename T>
void print(T && t){
std::cout << "rvalue" << std::endl;
}
template<typename T>
void testForward(T && v){
print(v);
print(std::forward<T>(v));
print(std::move(v));
}
int main(int argc, char * argv[])
{
testForward(1);
std::cout << "======================" << std::endl;
int x = 1;
testFoward(x);
}
运行结果如下
lvalue
rvalue
rvalue
=========================
lvalue
lvalue
rvalue
传入的1是右值,但是经过函数传参之后变成了左值,第二行使用forward函数,所以不会改变它的右值属性,第三行move将传入的参数强制转换成左值。
传入的x是左值,forward之后也是左值,move之后成为右值。
forward实现原理
template <typename T>
T&& forward(typename std::remove_reference<T>::type& param)
{
return static_cast<T&&>(param);
}
template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param)
{
return static_cast<T&&>(param);
}
可以翻译成下面这种形式:
template <typename T>
T&& forward(int& param)//左值引用
{
return (T&&)(param);//万能引用
}
template <typename T>
T&& forward(int&& param)//右值引用
{
return (T&&)(param);//万能引用
}
完美转发失败
完美转发是指将一个函数的参数列表中的参数原封不动地转发到另一个函数中,但是完美转发有可能失败。
- 函数模板和非函数模板参数类型不匹配
- 模板参数推导失败
会导致完美转发失败的实参种类有大括号初始化、以值0或者NULL表达空指针、仅有声明的整型static const成员变量、模板或重载的函数名字、以及位域。
条款30 熟悉完美转发的失败情形