系列目录
参考资料
指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。
分为移动赋值运算符和移动构造函数,一般而言,这比拷贝效率更高。
左值
可以出现在赋值符号(=)左边(也可以出现在右边)。有名称的变量,可以被寻址。
(纯)右值/将亡值
只能出现在赋值符号右边,没有名称,不能被寻址。
一般而言,右值的作用是给左值赋值,或作为表达式的一部分。当相关运算结束之后,右值的相关资源就会被销毁,即为“将亡”。
譬如字面值1、函数返回值等匿名变量。
右值引用
&&用于右值引用,一般是与移动语义配合使用,其他场合一般不使用,可以简单看作是一种特殊的数据类型。
详见“move()函数”一节。
int num = 10;
int &b = num; //正确,&用于左值引用
int &c = 10; //报错,&不可用于右值引用
int &&c = 10; //正确,&&用于右值引用
int &&c = num; //报错,&&不可用于左值引用
基本上第一行和第四行可以看做是相当的用法,一个变量成为右值的引用之后,其作用相当于左值。
move()函数可以接收左值或者右值作为参数,但会将作为输入参数的左值强制转换为右值引用,于是就能够根据参数是左值或者右值调用不同的构造函数的重载。
move()函数本身并没有任何作用,只有搭配了合适的重载函数才有意义。
std::move()实际上是
static_cast<T&&>
()的简单封装。
class Person{
public:
int p;
Person(int p){
this->p = p;
}
//一般的拷贝构造函数
Person(const Person& p){
this->p = p.p + 1;
}
//传入的参数为右值引用时调用这个构造函数
Person(const Person&& p){
this->p = p.p - 1;
}
};
int main() {
Person* p = new Person(100);
Person* p2 = new Person(move(*p));
Person* p3 = new Person(*p);
cout << p->p << endl;//100
cout << p2->p << endl;//99
cout << p3->p << endl;//101
return 0;
}
智能指针主要用于解决指针使用中的两个主要问题:
内存泄漏:用指针new了一个对象之后,忘记进行delete,或者由于程序出现异常等问题而跳过了delete代码。
重复释放:多个指针指向同一个内存空间,如果都进行delete,就会出错。
内存泄漏问题有一些解决方法,如设置一个计数器,new的时候+1,delete的时候-1,可以检测内存泄漏的情况。
C++实现了各种智能指针,可以在一定程度上解决上述问题。
智能指针本质上是对普通指针进行了包装成一个类,析构函数实现了自动释放指向的内存,同时禁止拷贝和赋值,只能通过move()函数实现资源的传递。
是C++98的方案,在C++11中被废除,因为会有以下潜在的内存崩溃问题:
auto_ptr<string> p1 (new string ("qwerty"));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。
用于不能被多个实例共享的内存管理。这就是说,仅有一个实例拥有内存所有权。
unique_ptr 不允许左值在右边直接赋值,要用std::move 进行临时右值的赋值,并将原指针置为 nullptr,因此避免了 auto_ptr 的问题。
总之, C++不允许 unique_ptr 指向的内存被多个指针引用。
#include <memory>
//使用了类模板,需要提前指明指针指向的类
std::unique_ptr<int> p1();
std::unique_ptr<int> p2(nullptr);//空指针初始化
std::unique_ptr<int> p3(new int);//非空初始化
std::unique_ptr<int> p5(p4);//错误,不能直接进行拷贝
std::unique_ptr<int> p5(std::move(p4));//正确,调用移动构造函数
std::unique_ptr<string> p6;
p6 = unique_ptr<string>(new string ("You"));//正确,因为右边是临时右值
//std::move()会将控制权转移,原来的指针会变为空
在实现上采用的是引用计数机制,多个 shared_ptr 智能指针可以共同使用同一块堆内存。
只有引用计数为 0 时,堆内存才会被自动释放。
#include <memory>
std::shared_ptr<int> p1; //不传入任何实参
std::shared_ptr<int> p2(nullptr); //传入空指针 nullptr
//以下两行效果相同
std::shared_ptr<int> p3(new int(10));
std::shared_ptr<int> p3 = std::make_shared<int>(10);
//拷贝构造函数
std::shared_ptr<int> p4(p3);
std::shared_ptr<int> p4 = p3;
//移动构造函数
std::shared_ptr<int> p5(std::move(p4));
std::shared_ptr<int> p5 = std::move(p4);
//用普通指针初始化
int* ptr = new int;
std::shared_ptr<int> p1(ptr);
std::shared_ptr<int> p2(ptr);//错误,同一普通指针不能同时为多个 shared_ptr 对象赋值,否则会导致程序发生异常
weak_ptr类型指针通常不单独使用,只能和 shared_ptr 类型指针搭配使用。
可以将 weak_ptr 类型指针视为 shared_ptr 指针的一种辅助工具,借助 weak_ptr 类型指针, 我们可以获取 shared_ptr 指针的一些状态信息。
shared_ptr<A> ptr_a(new A());
cout << "ptr_a use count : " << ptr_a.use_count() << endl; // 输出:ptr_a use count : 1
weak_ptr<A> wk_ptr_a = ptr_a;//可以使用赋值
cout << "ptr_a use count : " << ptr_a.use_count() << endl; // 输出不变,weak_ptr不影响引用计数
if (!wk_ptr_a.expired()){//expired()用来检测是否过期
wk_ptr_a.lock()->print();//lock()可以用来获取指向的对象
}
weak_ptr 是用来解决 shared_ptr 环形引用时死锁造成的内存泄漏问题,详见weak_ptr浅析。
如果两个 shared_ptr 指向的对象里面各存在一个 shared_ptr 成员变量相互引用,那么这两个对象空间的引用计数永远不可能下降为0,资源永远不会释放。而 weak_ptr 本身不会增加引用计数,可以用在这个场景中。
auto n1 = 10; //编译器根据10判断n1是int
decltype(10) n2 = 99; //编译器根据10判断n2是int,和99无关
auto关键字表示自动类型推导,编译器在编译过程中会根据赋值表达式的右值来确定auto的类型。
decltype表示声明类型,根据括号中的表达式确定变量的类型,不需要赋值操作。
是一种语法糖,可以简化代码,尤其是一些难以书写的类型,譬如函数指针类型。
//[外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型 {函数体;};
//用auto自动识别类型,实际上的类型是main()::<lambda(int, int)>
auto l = [](int a, int b) -> int{return a + b;}
int c = l(1,2);
//上面两行等价于int c = [](int a, int b) -> int{return a + b;}(1,2);
//用lambda函数作为比较器
sort(num, num+4, [=](int x, int y) -> bool{ return x < y; });
外部变量格式 | 功能 |
---|---|
[] | 空方括号表示当前 lambda 匿名函数中不导入任何外部变量。 |
[=] | 只有一个 = 等号,表示以值传递的方式导入所有外部变量; |
[&] | 只有一个 & 符号,表示以引用传递的方式导入所有外部变量; |
[val1,val2,...] | 表示以值传递的方式导入 val1、val2 等指定的外部变量,同时多个变量之间没有先后次序; |
[&val1,&val2,...] | 表示以引用传递的方式导入 val1、val2等指定的外部变量,多个变量之间没有前后次序; |
[val,&val2,...] | 以上 2 种方式还可以混合使用,变量之间没有前后次序。 |
[=,&val1,...] | 表示除 val1 以引用传递的方式导入外,其它外部变量都以值传递的方式导入。 |
[this] | 表示以值传递的方式导入当前的 this 指针。 |
variadic template
这玩意很复杂,深入讨论见侯捷 - C++新标准-C++11/14 - P15
用于配合可变参数函数使用,用于参数类型不限、数量不限的情况。
可以用于递归的情况,每层递归减少一个参数。当参数减少到一定程度,会调用重载的参数特化的版本。
//参数特化的情况,用于递归结束条件
//当函数只有一个参数,编译器会调用这个,而不是下面的版本
template<typename T>
void printX(const T& firstArg){
cout << "The last one: " << firstArg << endl;
}
//一般情况下编译器会调用这个版本
//注意这里省略号...的用法!!!
template<typename T, typename... Types>
void printX(const T& firstArg, const Types&... args){
//可以用sizeof...(Types)或sizeof...(args)获取参数个数
cout << firstArg << "\t" << sizeof...(Types) << "\t" << sizeof...(args) << endl;
printX(args...);//注意这里省略号...的用法
}
int main() {
printX(1,1.2,'a',"asd",2,3);
return 0;
}
统一初始化
扩展了花括号初始化的适用范围,可以作为容器和类的初始化方式,也可作为参数和返回值进行传递。
编译器看到花括号括起来的初始化列表,会构造一个std::initializer_list<T>
。进行初始化时,编译器会将元素逐一地传给构造函数。
int values[] {1,2,3};
vector<string> v {"a","b","c"};
complex<double> c {1.0,3.0};
int j = {1};
int i{};//i会被初始化为0
int* p{};//p会被初始化为nullptr
nullptr
以往C++使用0或者NULL表示空指针,会出现void*和int类型混杂的现象。
nullptr 是 nullptr_t 类型的右值常量,专用于初始化空类型指针。nullptr_t 是 C++11 新增加的数据类型,可称为“指针空值类型”。
遍历元素的for循环
python和Java里面都有,不多说了。
for (int i : {1,2,3,4,5}) {
cout << ch;
}
vector<int> vec;
for (int& i : vec) {//应当注意引用传递的情况
i *= 3;
}
constexpr
用来修饰函数,编译器会在编译期执行,只留下返回值。用于优化程序。
函数体内只能含有一个return语句(一行)。
C++14后,也可以包含分支、循环语句等。
=default, =delete
用于类的构造函数、析构函数上。=default表示提醒编译器创建默认函数;=delete禁止编译器创建默认函数。
class A {
public:
A(int x){}//一般情况下,手动定义构造函数后,编译器就不会创建默认构造函数
A() = default;//可以用default关键字提醒编译器创建默认构造函数,等价于A(){};
//禁止拷贝和赋值
A(const A&) = delete;//禁止编译器创建默认拷贝构造函数
A &operator=(const A&) = delete;//禁止编译器创建默认拷贝赋值函数
//~A() = default;默认析构函数,写不写都一样
};
评论区