文章目录
- C++11
- 1. nullptr常量
- 2. constexpr关键字
- 3. using类型别名声明
- 4. auto关键字
- 5. 范围for语句 / for-each
- 6. lambda表达式与std::bind
- 7. =default / =delete
- 8. 右值引用&& 与 移动语义std::move
- 9. explicit/override/final/noexcept指示符
- 10. string数值转换函数
- 11. std::array
- 12. 无序关联容器
- 13. 智能指针
- 14. std::function
- 15. 类的委托构造函数
- 16. struct可以直接定义并初始化了
- 17. enum class 枚举定义和使用
- 18. map的定义和初始化, 以及遍历
- 19. emplace 容器, 构造而不是拷贝
- 20. std::forward完美转发
- 21. typename... Args 可变参数模版
C++11
1. nullptr常量
- 说明:nullptr是nullptr_t类型的字面值,代表空指针,它可以被转换成任一其他的指针类型
- 对比:
1.指针初始化为空指针:
int* p1 = 0; // C++98
int* p2 = NULL; // C++98
int* p3 = nullptr; // C++11
- 函数调用
void F(int);
void F(void*);
void Func() {
F(0); // 调用void F(int)
F(NULL); // 有歧义,可能调用void F(int),取决于NULL的宏定义值
F(nullptr); // 调用void F(void*)
}
- 建议:优先选用nullptr, 而非0或NULL
2. constexpr关键字
- 说明:
- 算术类型,引用,指针类型属于字面值类型
- constexpr用于声明变量时表示该变量是一个编译期常量,变量必须是字面值类型
- constexpr用于声明函数时表示该函数可以在编译期得出运算结果
- C++11中constexpr函数必须遵守以下约定,在C++14中有所放宽:
- 函数的返回类型和形参类型都必须是字面值类型
- 函数体只能是一条return语句,其他语句不能在运行时执行任何操作,例如 空语句,类型别名声明语句等
- 调用constexpr函数时,如果传入的参数都是编译期常量,则函数结果将在编译期计算出来;如果传入参数有一个到多个的值编译期未知,则运作方式与普通函数无异
- 对比:
constexpr与const比较:constexpr对象一定是const对象,反之不成立
int Add1(int a, int b); // 普通函数
constexpr int Add2(int a, int b); // constexpr函数
void Func() {
const int c1 = 10; // 编译期常量
const int c2 = c1 + 1; // 编译期常量
const int c3 = Add1(1, 2); // 运行时常量
int x1 = 10; // 非常量
constexpr int ce1 = 10; // 编译期常量
constexpr int ce2 = ce1 + 1; // 编译期常量
// constexpr int ce3 = Add1(1, 2); // 编译出错,Add1无法在编译期计算出结果
constexpr int ce4 = Add2(1, ce1); // 正确, 1和ce1都是编译期常量,Add2将在编译期计算出结果
// constexpr int ce5 = Add2(1, x1); // 错误, x1不是编译期常量,Add2运作方式与Add1相同
// 定义数组,数组维度必须是编译期常量
int arr1[10]; // 正确
int arr2[c1]; // 正确
// int arr3[c3]; // 错误
// int arr4[x1]; // 错误
int arr5[ce1]; // 正确
}
- 建议:
- 如果认为变量是一个编译期常量,就为变量加上constexpr声明
- 只要有可能,就为函数加上constexpr声明
3. using类型别名声明
- 说明: 为某种类型定义另外一个名字, 格式: using alias = type;
- 对比:
- using与typedef比较:
// 为char*定义别名
typedef char* cstring;
using cstring = char*;
// 定义函数指针类型别名
typedef void(*pFunc)(int, int); // pFunc是函数指针类型
using pFunc = void(*)(int, int); // pFunc是函数指针类型
using Func = void(int, int); // Func是函数类型, 不是指针类型,需要注意
- 建议:
·1. 优先使用using进行别名声明,using声明更加直观易懂
·2. 不要使用#define定义类型别名,宏仅仅是字符串,没有类型信息
4. auto关键字
- 说明:
- 使用auto定义变量时,编译器根据初始化表达式自动推导变量类型,因此变量必须初始化
- 使用auto可以定义持有lambda表达式的变量
- 对比:
- 变量定义
void Func() {
// 初始化
int x1; // 未初始化,存在风险
// auto x2; // 编译错误
auto x3 = 4; // x3是int类型
auto x4 = x3; // x4是int类型,拷贝x3的值
auto& x5 = x3; // x5是int&类型,指向x3
const auto& x5 = x3; // x6是const int&类型,指向x3
// 避免类型转换
std::vector<int> vec = {1, 2, 3, 4};
int count1 = vec.size(); // 发生隐式类型转换, 编译告警
auto count2 = vec.size(); // 正确接收size()函数的返回值
// 简化变量定义
std::map<int, std::string> int2StrMap;
int2StrMap.insert(std::make_pair(1, "hello"));
std::map<int, std::string>::iterator iter1 = int2StrMap.begin(); // C++98
auto iter2 = int2StrMap.begin(); // C++11
// 接收lambda表达式
std::function<int(int)> func = [](int a) {
return 5 + a;
};
auto lambda1 = [](int a) {
return 5 + a;
};
}
- 建议:
- 优先使用auto声明变量, 而非显式型别。可以简化定义,保证变量被初始化,消除型别不匹配问题
- 当使用大括号表达式初始化auto声明的变量时, 变量类型会被推导为std::initializer_list
5. 范围for语句 / for-each
- 说明:
- 范围for语句可以用来遍历容器的所有元素,格式为:
for (declaration : expression) {
statement
}
- 对比:
- 遍历容器
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9};
// C++98
for (std::vector<int>::iterator iter1 = vec.begin(); iter1 != vec.end(); ++iter1) {
std::cout << *iter;
}
for (int i = 0; i < vec.size(); ++i>) {
std::cout << vec[i];
}
// C++11
for (auto val : vec) {
std::cout << val;
}
- 建议:
- 当需要遍历整个容器而不需要元素的索引时,使用范围for语句而不是迭代器
- 当在范围for语句中使用auto时,内置类型的元素直接使用auto,需要修改元素时使用auto&,不需要修改元素时使用const auto&
一定不要在范围for语句中增删容器元素
6. lambda表达式与std::bind
- 说明:
- mutable 修饰符说明 lambda 表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获对象的 non-const 方法。
- exception 说明 lambda 表达式是否抛出异常(noexcept),以及抛出何种异常,类似于void f() throw(X, Y)。
- attribute 用来声明属性。
[] // 不捕获任何外部变量
[=] // 以值的形式捕获所有外部变量
[&] // 以引用形式捕获所有外部变量
[x, &y] // x 以传值形式捕获,y 以引用形式捕获
[=, &z] // z 以引用形式捕获,其余变量以传值形式捕获
[&, x] // x 以值的形式捕获,其余变量以引用形式捕获
另有一点需注意:
对于 [=] 或 [&] 的形式,lambda 表达式可直接使用 this 指针。
但对于 [] 的形式,如果要使用 this 指针,必须显式传入:[this]() { this->someFunc(); }();
struct myCompare{
bool operator()(int &x){
return x%2==0;
}
};
void test5(){
vector<int> nums = {1,4,3,2,5,6,7,8,0};
// auto it = find_if(nums.begin(), nums.end(), myCompare()); // 同下
auto it = find_if(nums.begin(), nums.end(), [](int &x){return x%2==0;});
cout << "the first odd number is " << *it <<endl;
sort(nums.begin(), nums.end(), [](int &a, int &b){return a>b;}); // 从大到小排列
// test2
std::vector<int> c { 1,2,3,4,5,6,7 };
int x = 5;
c.erase(std::remove_if(c.begin(), c.end(), [x](int n) { return n < x; } ), c.end());
for (auto i: c) {
std::cout << i << ' ';
}
std::cout << endl;
}
std::bind是std::bind1st和std::bind2nd的后继特性, 它接受一个可调用对象,返回一个新的可调用对象以新的形参列表,
其格式为:
auto newCallable = std::bind(callable, argList);
- 对比:
- lambda,函数指针,函数对象比较:
bool IntCompare(int a, int b) {
return a > b;
}
class IntComparator {
public:
bool operator()(int a,int b) {
return a > b;
}
}
void Func() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9};
// 函数指针
std::sort(vec.begin(), vec.end(), IntCompare);
// 函数对象
std::sort(vec.begin(), vec.end(), IntComparator());
// lambda
std::sort(vec.begin(), vec.end(), [](int a, int b){
return a > b;
});
}
- lambda, std::bind比较:
void F1(int a1, int a2, double b1, double b2);
// 包装F1,使其只需传入a1和b1参数, a2和b2使用给定值
void Func() {
// std::bind
auto newF1 = std::bind(F1, std::placeholders::_1, 100, std::placeholders::_2, 10.0);
newF1(50, 20.0); // 等价于F1(50, 100, 20.0, 10.0)
// lambda
auto lambdaF1 = [](int a1, double b1) {
F1(a1, 100, b1, 10.0);
}
lambdaF1(50, 20.0);
}
- 建议:
- 不要使用lambda的默认捕获方式,容易导致空悬指针问题
- 优先选用lambda,而不是std::bind:lambda可读性更好,表达力更强,可能运行效率也更高
7. =default / =delete
-
说明:
- 对类的特种成员函数(默认构造函数,拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符,析构函数),可以使用
=default
来显式的要求编译器生成合成的版本,这些合成的版本会逐成员调用其对应的特种函数, 例如合成的默认构造函数: 对于内置类型,执行默认初始化(随机值),对于类类型,调用其默认构造函数 - 使用
=default
声明的函数是隐式内联的,如果不希望是内联函数,则需要将=default
声明放在类外 - 对于任何函数(不仅是类的特种成员函数,虽然通常用于这些函数上)可以使用
=delete
要求编译器不定义这个函数,=delete
声明必须出现在函数第一次声明的时候 - 如果类的析构函数被声明
=delete
,那么编译器将不允许创建该类型的变量或临时对象,因为无法销毁该类的成员
- 对类的特种成员函数(默认构造函数,拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符,析构函数),可以使用
-
对比:
// = default
class DefaultMember {
public:
DefaultMember() = default; // 合成默认构造函数, 隐式内联
DefaultMember(const DefaultMember&); // 合成拷贝构造函数, 非内联
DefaultMember& operator=(const DefaultMember&); // 合成拷贝赋值运算符,非内联
~DefaultMember() = default; // 合成析构函数, 隐式内联
};
DefaultMember::DefaultMember(const DefaultMember&) = default;
DefaultMember& DefaultMember::operator=(const DefaultMember&) = default;
// = delete
// C++98
class NonCopyable1 {
// 声明为private成员函数,不定义这些函数,无法访问,因此阻止了拷贝
NonCopyable1(const NonCopyable1&);
NonCopyable1& operator=(const NonCopyable1&);
public:
NonCopyable1();
~NonCopyable1();
};
// C++11
class NonCopyable2 {
public:
NonCopyable2();
~NonCopyable2();
// 删除的拷贝构造和拷贝赋值运算符
NonCopyable2(const NonCopyable2&) = delete;
NonCopyable2& operator=(const NonCopyable2&) = delete;
};
- 建议:
- 对于类的特种成员函数,如果希望使用编译器合成的版本,则使用
=default
显式声明这些函数 - 对于希望阻止拷贝的类应该使用
=delete
声明拷贝构造函数和拷贝赋值运算符,而不是将其声明为private成员
- 对于类的特种成员函数,如果希望使用编译器合成的版本,则使用
8. 右值引用&& 与 移动语义std::move
左值的英文简写为“lvalue
”,右值的英文简写为“rvalue
”。很多人认为它们分别是"left value"、“right value” 的缩写,其实不然。
-
lvalue 是“
loactor value
”的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据
, -
而 rvalue 译为 “
read value
”,指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)
。 -
说明:
- 左值和右值:
- 左值:变量(包括形参,右值引用),函数,返回左值引用的函数调用,内建赋值表达式,前置自增自减表达式,指针解引用表达式,内建下标表达式,字符串字面量,强转为左值引用类型的表达式
- 右值:字面量(字符串字面量除外),返回非引用的函数调用,后置自增自减表达式,内建的算术表达式、逻辑表达式、比较表达式,取地址表达式,强转为非引用类型的表达式,枚举项,lambda表达式 - 右值引用就是必须绑定到右值的引用,使用
&&
表示,右值引用不能绑定到一个左值上
:
- 左值和右值:
示例1
int i = 42;
int& r = i; // r是左值引用
// int&& rr = i; // 错误, 右值引用不能绑定到左值
// int& r2 = i * 42; // 错误, i * 42是一个右值
const int& r3 = i * 42; // 可以将const左值引用绑定到右值
int&& rr2 = i * 42; // rr2是右值引用
// int&& rr3 = rr2; // 错误, rr2也是左值
示例2
// 左值引用
int num = 10;
int &b = num; // 正确
int &c = 10; // 错误
int num = 10;
const int &b = num; // 正确
const int &c = 10; // 正确
// 右值引用
int num = 10;
//int && a = num; // 错误,右值引用不能初始化为左值
int && a = 10; // 正确
a = 100;
cout << a << endl; // 输出为100,右值引用可以修改值
// 右值引用的使用
// 如 thread argv 的传入,变长模板参数见后续介绍
template<typename _Callable, typename... _Args>
explicit thread(_Callable&& __f, _Args&&... __args) {
//....
}
// Args&&... args 是对函数参数的类型 Args&& 进行展开
// args... 是对函数参数 args 进行展开
// explicit 只对构造函数起作用,用来抑制隐式转换
- 可以使用标准库函数std::move将一个左值转换成右值引用,当使用std::move后这个左值只能被赋值或销毁,而不能直接使用它:
// 将左值i转换到右值,此后i可以被赋值,也可以直接销毁,在赋予i新值之前不能使用i的值
int&& rr4 = std::move(i);
- 移动构造函数/移动赋值运算符:类的这两个特种成员函数的形参是该类对象的右值引用,在函数体中要完成资源的移动,同时还要确保移后对象处于这样一个状态-销毁它是无害的,一旦移动完成,源对象必须不再指向被移动的资源。这两个函数只进行资源的移动,而不进行资源分配,不会抛出异常,因此需要加上noexcept标识符,否则移动操作不会触发,编译器会转而调用拷贝操作:
class Moveable {
static constexpr int ARRAY_SIZE = 10;
public:
Moveable(int length = ARRAY_SIZE) : data_(new int[length]), len_(length)
{}
~Moveable() {
Free();
}
// 拷贝控制
Moveable(const Moveable& other) : data_(new int[other.len_]), len_(other.len_) {
for (int i = 0; i < len_; ++i) {
data_[i] = other.data_[i];
}
}
Moveable& operator=(const Moveable& other) {
// copy and swap,能正确处理自赋值
Moveable tmp(other);
std::swap(data_, tmp.data_);
std::swap(len_, tmp.len_);
return *this;
}
// 移动控制
Moveable(Moveable&& other) noexcept : data_(other.data_), len_(other.len_) {
other.data_ = nullptr;
other.len_ = 0;
}
Moveable& operator=(Moveable&& other) noexcept {
// copy and swap,能正确处理自赋值
Moveable tmp(std::move(other));
std::swap(data_, tmp.data_);
std::swap(len_, tmp.len_);
return *this;
}
// 对于拷贝赋值运算符和移动赋值运算符,有一种简洁的实现方法
// 形参为Moveable类型,对左值调用拷贝构造,对右值调用移动构造
Moveable& operator=(Moveable other) {
std::swap(data_, other.data_);
std::swap(len_, other.len_);
return *this;
}
private:
void Free() {
if (data_) {
delete[] data_;
}
}
private:
int* data_;
int len_;
};
- 拷贝左值,移动右值,如果没有移动控制函数,那么对右值也将进行拷贝操作
- 对比:
- 建议:
- 对于使用std::move处理过的左值不要再使用其值
- 移动操作不是提高性能的灵药,应该假定移动操作不存在,成本高,未使用
9. explicit/override/final/noexcept指示符
- 说明:
- 当类的构造函数只接受一个参数时,实际上定义了一种由该参数转换为该类类型的隐式转换规则,使用
explicit
可以抑制这种转换的发生:
- 当类的构造函数只接受一个参数时,实际上定义了一种由该参数转换为该类类型的隐式转换规则,使用
class ImplicitConvertable {
public:
ImplicitConvertable(const std::string&);
};
class ExplicitConvertable {
public:
explicit ExplicitConvertable(const std::string&);
};
void F1(const ImplicitConvertable& obj);
void F2(const ExplicitConvertable& obj);
void Func() {
std::string str = "hello";
F1(str); // 正确,从str隐式构造一个ImplicitConvertable对象传入F1
// F1("hello") // 错误,编译器只会自动进行一步类型转换,这里需要两步转换
F1(ImplicitConvertable(str)); //正确
// F2(str); // 错误,explicit抑制了隐式转换
F2(ExplicitConvertable(str)); // 正确,只能显式构造ExplicitConvertable对象
}
- 在继承体系中,可以使用
override
来显式说明 意在重写基类中的虚函数:
class Base {
public:
virtual void F1(); // 虚函数第一次出现的地方添加virtual关键字
virtual void F2(int);
};
class Derived : public Base {
public:
void F1() override; // 正确,重写基类虚函数void F1();
// void F2() override; // 错误,基类没有void F2()的虚函数
// void F3() override; // 错误,基类没有void F3()的虚函数
virtual void F4(); // 正确, 派生类定义自己的虚函数
};
- 使用
final
来阻止类被继承:
class NoDerived final {}; // NoDerived不能被继承
// class D : public NoDerived {}; // 错误, 不能从NoDerived继承
- 使用
noexcept
来制定一个函数不会抛出异常,这种函数可以简化调用处的代码,编译器也可以对该函数进行特殊优化.如果函数违反了异常说明而抛出了异常,那么程序将直接调用std::terminate结束程序:
void Func() noexcept; // 承诺Func不抛出异常
- 对比:
- 建议:
- 为类的单参数构造函数加上explicit指示符,可以防止隐式转换发生
- 在派生类中为想要改写的基类虚函数加上override指示符
- 为不想被继承的类加上final指示符
- 只要函数不抛出异常,就为其加上noexcept知识符
10. string数值转换函数
-
说明:
C++11中引入了一组用于数值和字符串之间相互转换的函数 -
对比:
- 与C标准库字符串转换函数比较:
// C库
int atoi (const char * str); // str转换到int
long int atol ( const char * str ); // str转换到long
double atof (const char* str); // str转换到double
// C++
std::string to_string (val); // val转换到字符串, val可以是任何算数类型
int stoi (const string& str, size_t* idx = 0, int base = 10); // str 转换到int
long stol (const string& str, size_t* idx = 0, int base = 10); // str 转换到long
float stof (const string& str, size_t* idx = 0); // str 转换到float
double stod (const string& str, size_t* idx = 0); // str 转换到double
- 建议:
使用C++风格的转换函数
11. std::array
-
说明:
- std::array与数组一样拥有固定大小,并且大小需要在编译期给定
- std::array不会像数组一样作为参数传递时退化为指针
-
对比:
void Func {
// 内置数组
int arr1[10] = {0};
for (auto val : arr1) {
std::cout << val;
}
for (int i = 0; i < 10; ++i>) {
std::cout << arr1[i];
}
// std::array
std::array<int, 10> arr2;
arr2.fill(0);
std::cout << arr2.front();
std::cout << arr2.back();
for (auto val : arr2) {
std::cout << val;
}
for (int i = 0; i < arr2.size(); ++i>) {
std::cout << arr2.at(i);
}
}
- 建议:
- 如果需要固定大小的数组,那么应该使用std::array而不是内置数组, 因为
std::array
类型安全,提供了丰富的接口,性能与内置数组相同
- 如果需要固定大小的数组,那么应该使用std::array而不是内置数组, 因为
12. 无序关联容器
-
说明:
- C++11定义了4个无序关联容器:
unordered_map
,unordered_set
,unordered_multimap
,unordered_multiset
, 分别与有序关联容器:map
,set
,multimap
,multiset
对应 无序关联容器
底层使用hash表实现, 因此不能假定容器中元素的排列顺序;有序关联容器
底层使用红黑树实现,所有元素按照键值大小排列,遵循严格弱序准则- 对于自定义类型,如果需要放入无序关联容器内时,需要给定该类型的hash计算方式和判等准则;需要放入有序关联容器时,只需要给定符合严格弱序的比较准则
- 无序关联容器的插入,查找,删除操作时间复杂度为
O(1)
;有序关联容器的插入,查找,删除操作时间复杂度为O(lg(n))
- C++11定义了4个无序关联容器:
-
对比:
// 自定义类型
class Data {
public:
int Val();
const std::string& Str();
private:
int val_;
std::string str_;
};
// 比较准则,用于有序关联容器
struct DataCompare {
bool operator()(const Data& l, const Data& r) const {
return l.Val() < r.Val() ||((l.Val() == r.Val()) && l.Str() < r.Str());
}
};
// hash函数,用于无序关联容器
struct DataHash {
size_t operator()(const Data& data) const {
return std::hash<int>()(data.Val()) ^ std::hash<std::string>()(data.Str());
}
};
// 判等准则,用于无序关联容器
struct DataEqual {
bool operator()(const Data& l, const Data& r) const {
return (l.Val() == r.Val()) && (l.Str() == r.Str());
}
};
void Func() {
std::set<Data, DataCompare> orderedDataSet;
std::unordered_set<Data, DataHash, DataEqual> unorderedDataSet;
}
- 建议:
- 如果元素的顺序不重要,优先使用无序关联容器
13. 智能指针
-
说明:
- C++11提供了两种智能指针:
std::shared_ptr
和std::unique_ptr
; 一种伴随类:std::weak_ptr
, 只能配合std::shared_ptr使用 std::shared_ptr
提供了共享所有权语义,可以拷贝赋值多次,并且是线程安全的,当指向资源的最后一个shared_ptr销毁时,资源被释放
- std::unique_ptr提供了独占所有权语义,同一时刻,
只能有一个unique_ptr对象指向资源,unique_ptr不可拷贝赋值,只能移动
std::weak_ptr
是一种弱引用,指向shared_ptr管理的资源,但不拥有所有权,每次使用前需要检查资源是否有效
std::shared_ptr
的对象大小是固定的,std::unique_ptr
的对象大小是不定的
- C++11提供了两种智能指针:
-
对比:
// 自定义类型
struct Data {
Data() : val(0), str()
{}
Data(int v, const std::string& s) : val(v), str(s)
{}
int val;
std::string str;
}
void Func() {
// std::uniqu_ptr
std::uniqu_ptr<Data> up1; // 空指针
std::uniqu_ptr<Data> up2(new Data()); // 持有一个指向默认构造的Data对象的指针
std::uniqu_ptr<Data> up3 = std::make_unique<Data>();// C++14,持有一个指向默认构造的Data对象的指针
auto up4 = std::make_unique<Data>(1, "hello"); // C++14, 持有一个指向Data(1,"hello")对象的指针
// auto up5 = up2 // 错误, unique_ptr不可拷贝
auto up6 = std::move(up2); // 正确,unique_ptr可移动,资源所有权转移到up6, up2变成空指针
// std::shared_ptr与std::weak_ptr
std::shared_ptr<Data> sp1; // 空指针
std::shared_ptr<Data> sp2(new Data()); // 持有一个指向默认构造的Data对象的指针
std::shared_ptr<Data> sp3 = std::make_shared<Data>();// 持有一个指向默认构造的Data对象的指针
auto sp4 = std::make_shared<Data>(1, "hello"); // 持有一个指向Data(1,"hello")对象的指针
auto sp5 = sp2; // 正确, shared_ptr可拷贝
// auto sp6 = up3; // 错误, 不能从unique_ptr拷贝资源
auto sp7 = std::move(up3); // 正确, 从unique_ptr移动资源
std::weak_ptr<Data> wp1; // 空指针
std::weak_ptr<Data> wp2(sp2); // 持有指向sp3所持资源的弱引用
if (!wp2.expired()) { // 判断与wp2关联的share_ptr是否已失效
auto sp8 = wp2.lock();//若expired()为true, lock()返回空的shared_ptr,否则返回有效的shared_ptr
}
std::shared_ptr<Data> sp9(wp2); // 从weak_ptr构造shared_ptr, 如果若expired()为true则抛出异常
}
- 建议:
- 不要使用std::auto_ptr
- 优先使用std::unique_ptr, 因为
unique_ptr有与原始指针相同的性能
- 优先使用std::make_unique()(C++14)和std::make_shared(), 因为只进行一次内存分配,内存占用更小,性能更好
- 使用std::weak_ptr来替代可能空悬的std::shared_ptr
- 使用std::weak_ptr来解决std::shared_ptr可能导致的指针环路
14. std::function
-
说明:
- 可调用对象的概念: 函数,函数指针,函数对象,lambda表达式统称为可调用对象
- std::function内部可以保存一个可调用对象的副本,
std::function
对象本身也是可调用对象,调用std::function对象相当于调用其内部持有的可调用对象
-
对比:
// C++98, 一般只能通过函数指针传递可调用对象
typedef int(*pAddFunc)(int, int);
int Add(int a, int b) {
return a + b;
}
void Invoke1(pAddFunc fn) {
int val = fn(2, 3);
std::cout << val;
}
void Func1() {
Invoke1(Add);
}
// C++11
using AddFunc = std::function<int(int, int)>;
struct Adder {
int operator()(int a, int b) {
return a + b;
}
};
void Invoke2(AddFunc fn) {
int val = fn(2, 3);
std::cout << val;
}
void Func2() {
Invoke2(Add);
Invoke2(Adder());
Invoke2([](int a, int b) {
return a + b;
});
}
- 建议:
- 需要回调函数时,使用std::function接收可调用对象,而不是函数指针,可以简化调用逻辑
15. 类的委托构造函数
class A{
public:
A(int n){}
A() : A(10){} // 间接调用A(10)
A(int a, int b):m_a(a),m_b(b){} // 间接初始化成员变量
public:
int m_a, m_b;
};
16. struct可以直接定义并初始化了
struct Data{
int i=0;
float f=3.14;
bool b = true;
// Data(int i_, f_, b_): i(i_),f(f_),b(b_){} // 可以不用这样初始化
};
17. enum class 枚举定义和使用
enum class Number {one, two, three};
enum class Number2 {one, two, four}; //合法,enum class限定了作用域,故不同枚举类型里的值可以重名了。原来是不行的。
Number num = Number::one;
Number2 num2 = Numner::one;
// int num = Number::one; // 不合法
18. map的定义和初始化, 以及遍历
void test3(){
map<int, string> numMap = {{1, "p1"}, {2, "p2"}, {3, "p3"}};
// 新方法
for(auto &[key, value] : numMap){
cout << key << " " <<value <<endl;
}
// 老方法
for(map<int, string>::iterator it=numMap.begin(); it!=numMap.end(); it++){
cout << it->first << " " <<it->second <<endl;
}
for(auto &it: numMap){
cout << it.first << " " <<it.second <<endl;
}
}
19. emplace 容器, 构造而不是拷贝
- emplace_back(构建) : 后插
- emplace_font(构建) : 前插
- emplace(location, 构建): location插
class A {
private:
string m_name;
public:
// 构造函数
A(string name): m_name(name) {cout<< "create " << m_name << " .." <<endl;}
// 拷贝构造函数
A(const A& a){
cout<< "拷贝构造函数 " <<endl;
m_name = a.m_name;
};
// 析构函数
~A(){cout<< "delete " << m_name << " .." <<endl;}
};
void test(){
vector<A> nums;
nums.reserve(10); // 先开辟10个空间再说, 不然后面如果vector不够, 会重新开辟空间,就会扰乱操作
if(nums.capacity() != 10){
cout << " 空间开辟失败" << endl;
return 0;
}
nums.push_back(A("小明")); // 构造完后, 复制到nums, 然后在销毁, 相当于这个对象构造了两次
nums.emplace_back("小王"); // 直接就是小王创建后加入到
cout << "---" <<endl;
return;
}
输出:
create 小明 ..
拷贝构造函数
delete 小明 ..
create 小王 ..
---
delete 小明 ..
delete 小王 ..
20. std::forward完美转发
std::forward
实现完美转发,作用就是保持传参参数属性不变
,如果原来的值是左值,经std::forward处理后该值还是左值;如果原来的值是右值,经std::forward处理后它还是右值。
不管是T&&、左值引用、右值引用,std::forward都会按照原来的类型完美转发。
forward主要解决引用函数参数为右值时,传进来之后有了变量名就变成了左值
。
与第8条中 std::move 作用相反。
#include <iostream>
#include <string>
#include<algorithm>
using namespace std;
//左值引用
void process(int& x)
{
cout << "process(int&)" << '\t' << x << endl;
cout << endl;
}
//右值引用
void process(int&& x)
{
cout << "process(int&&)" << '\t' << x << endl;
cout << endl;
}
//不完美转发
void Forward1(int&& x)
{
cout << "Forward1(int&& x)" << endl;
process(x); //不是完美转发,x被当做左值,调用process(int& x)
}
//完美转发
void Forward2(int&& x)
{
cout << "Forward2(int&& x)" << endl;
process(forward<int>(x)); //完美转发,x被当做右值
}
int main()
{
int a = 5;
cout << "a左值引用" << endl;
process(a);
cout << "常量1:右值引用" << endl;
process(1);
cout << "move(a)右值引用" << endl;
process(move(a)); //move语句将a视作右值
cout << "move(a) 不完美转发" << endl;
Forward1(move(a));
cout << "move(a) 完美转发" << endl;
Forward2(move(a));
}
测试结果:
21. typename… Args 可变参数模版
C++11的新特性–
可变参数模版
(variadic
templates)是C++11新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数
。相比C++98/03,类模版
和函数模版
中只能含固定数量的模版参数
,可变模版参数无疑是一个巨大的改进。
- C++普通模板简介 可阅读 blog
可变参数模板和普通模板的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在typename或class后面带上省略号“...
”。
比如我们常常这样声明一个可变模版参数:template<typename…>或者template<class…>,一个典型的可变模版参数的定义是这样的:
template <class... T>
void f(T... args);
上面的可变参数模版的定义当中,省略号
的作用有两个:
- 声明一个
参数包T... args
,这个参数包中可以包含0到任意个模板参数
; - 在模板定义的右边,
可以将参数包展开成一个一个独立的参数
。
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包
”,它里面包含了0到N(N>=0)
个模版参数。
我们无法直接获取参数包args中的每个参数的,只能通过展开参数包
的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点
,也是最大的难点
,即如何展开可变模版参数
。
可变模版参数和普通的模版参数语义是一致的,所以可以应用于函数和类,即可变参数模版函数
和可变参数模版类
,然而,模版函数不支持偏特化,所以可变参数模版函数和可变参数模版类展开可变模版参数的方法还不尽相同,下面我们来分别看看他们展开可变模版参数的方法。
- 具体关于 可变参数模板的详细介绍 可阅读 blog
(1)可变参数模板 函数
展开可变模版参数函数的方法一般有两种:
- 一种是通过
递归函数
来展开参数包(必须要有一个重载的递归终止函数,即必须要有一个同名的终止函数来终止递归)- 另外一种是通过
逗号表达式
来展开参数包。
- 示例1
#include <bits/stdc++.h>
//递归终止函数
template <typename Type>
void print(Type x) {
std::cout << x << ", 233333" << std::endl;
}
//展开函数
template <typename Type, typename... Targs>
void print(Type x, Targs... args) {
std::cout << x << std::endl;
print(args...);
}
int main() {
print('1', 1.5, "Hello World");
return 0;
}
//输出
1
1.5
Hello World, 233333
分析
可以注意到我写了两个同名函数,一个是只有单一参数的,另一个是前面有一个参数,后面有一个参数包。
这样一来实现了一个不定参数个数的输出函数。
每一次调用的时候参数包就会默认把最左边的参数挪到外面去变成一个显式参数供函数调用。
可以发现调用到最后参数包为空,就等价于直接调用了 print(x) 这个函数,所以在这里我们要显式声明一个不包含参数包的同名函数
,这样一来包含参数包的同名函数才可以正常结束,否则会报 no matching function
的 error 。
在不含有参数包的同名函数中加了一个额外的输出,可以看到只有最后一个参数的输出后面带上了一个 233333 ,证明前面的分析是正确的。
逗号表达式方法不再介绍,可阅读上方的blog。
- 示例2
template<typename T, typename... Args>
T* Instance(Args&&... args)
{
return new T(std::forward<Args>(args)...);
}
A* pa = Instance<A>(1);
B* pb = Instance<B>(1,2);
(2)可变参数模板 类
可变参数模板类是一个带可变模板参数的模板类,比如C++11中的元祖std::tuple就是一个可变模板类,它的定义如下:
template< class... Types >
class tuple;
这个可变参数模板类可以携带任意类型任意个数的模板参数:
std::tuple<int> tp1 = std::make_tuple(1);
std::tuple<int, double> tp2 = std::make_tuple(1, 2.5);
std::tuple<int, double, string> tp3 = std::make_tuple(1, 2.5, “”);
可变参数模板的模板参数个数可以为0个,所以下面的定义也是也是合法的:
std::tuple<> tp;
可变参数模板类的参数包展开的方式和可变参数模板函数的展开方式不同,可变参数模板类的参数包展开需要通过模板特化中止函数
和继承
方式去展开,展开方式比可变参数模板函数要复杂。下面我们来看一下展开可变模版参数类中的参数包的方法。
下面只介绍中止函数的方式,另一个可阅读上面提到的blog
这个Sum类的作用是在编译期计算出参数包中参数类型的size之和,通过sum<int,double,short>::value就可以获取这3个类型的size之和为14。这是一个简单的通过可变参数模板类计算的例子。
//可变参数模板类 的定义
//定义了一个部分展开的可变模参数模板类,告诉编译器如何递归展开参数包
template<typename First, typename... Rest>
struct Sum
{
enum { value = Sum<First>::value + Sum<Rest...>::value };
};
//特化的递归终止类
template<typename Last>
struct Sum<Last>
{
enum{ value = sizeof(Last) };
};
部分参考自:https://blog.csdn.net/emmJane/article/details/122876760