赞
踩
auto
关键字是c++11
推出的在编译器编译期间自动推导类型的辅助关键字,包含以下条件。①auto必须要初始化rauto x = 10;
auto y = 3.14;
auto z = true;
template<typename T>
void test(T t)
{
auto x = t + 1;
}
std::vector<int> test = { 1,2,3 };
for (auto it = test.begin(); it != test.end(); ++it)
{
//
}
lambda
函数返回类型推导auto func = [](int a, int b) { return a + b; };
struct T{
int _x, _y;
};
auto func()
{
return T{1, 2};
}
auto不能使用的地方:
template<typename T>
auto max(T a, T b) -> decltype (a > b ? a : b)
{
return a > b ? a : b;
}
decltype
关键字用于选择并返回操作数的数据类型,编译器会分析表达式并得到它的类型,却并不实际计算表达式的值。C++11
相比于C++98
来说允许支持使用花括号{}对元素或者数组进行统一初始化void test1()
{
// 普通变量初始化
int x{ 0 };
// 数组初始化
int arr1[]{ 1,2,3,4,5 };
int arr2[5]{ 0 };
// new表达式初始化
int* p = new int[3] {1, 2, 3};
}
struct
进行了C兼容,因此即便没有定义相应的构造函数,也可以使用C风格进行初始化,因此使用new
初始化也可以通过。class
来说,需要定义带参构造函数,因为列表初始化会执行带参构造函数。test2
相比于test1
来说会多出临时构造和拷贝构造的过程会造成浪费,可以使用explicit 关键字防止隐式转换发生。struct Point { int _x; int _y; }; class Test { private: int _x; int _y; public: explicit Test(int _x, int _y) { this->_x = _x; this->_y = _y; } }; void test2() { // 结构体初始化 Point point1{ 1,2 }; Point point2[3]{ {1,2},{2,3},{3,4} }; Point *p = new Point[2]{{1,2}, {3,4}}; Point p3 = {4,5}; // 自定义类初始化,会调用构造函数 Test test1{ 1,2 }; // 编译报错,使用explicit关键字不允许隐式转换 // Test test2 = {3,4}; // 标准容器初始化 std::vector<int> v1{ 1,2,3,4,5 }; std::map<int, int> { {1, 2}, { 2,3 }, { 3,4 }}; std::vector<int>v2{ v1 }; }
文档链接:initializer介绍
std::initializer_list
轻量级类模板来实现这个功能。void Test()
{
// 数组
int arr1[]{ 1,2,3 };
// map初始化列表
std::map<std::string, int> mm{ {"1",1},{"2",2},{"3",3} };
// set初始化列表
std::set<int> ss = { 1,2,3 };
// vector初始化列表
std::vector<int> arr2 = { 1,2,3,4,5 };
// 赋值,重载了operator=
arr2 = { 6,7,8,9,10 };
}
// map构造函数示例
map(initializer_list<value_type> _Ilist) : _Mybase(key_compare()) {
insert(_Ilist);
}
// set 构造函数示例
set(initializer_list<value_type> _Ilist) : _Mybase(key_compare()) {
this->insert(_Ilist);
}
// vector沟槽函数示例
vector(initializer_list<_Ty> _Ilist, const _Alloc& _Al = _Alloc())
: _Mypair(_One_then_variadic_args_t{}, _Al) {
_Construct_n(_Convert_size<size_type>(_Ilist.size()), _Ilist.begin(), _Ilist.end());
}
std::initializer_list
的支持也能使其拥有初始化任意长度的能力。template<class T> class Test { public: Test(std::initializer_list<T> list) { for (auto it = list.begin(); it != list.end(); ++it) { _content.push_back(*it); } } int size() { return _content.size(); } friend std::ostream& operator<<(std::ostream& out, const Test<T>& t) { for (int i = 0; i < t._content.size(); i++) { out << t._content[i] << " "; } return out; } private: std::vector<T> _content; }; void main() { Test<int> test = { 1,2,3,4,5,6 }; std::cout << test << std::endl; return; }
std::initializer_list
只是存储了对象的引用,并不负责保存或者拷贝初始化列表中的元素std::initializer_list<int> func(void)
{
int a = 1, b = 2;
return { a, b }; // a、 b 在返回时并没有被拷贝
}
std::initializer_list
,如std::vector<int> func(void)
{
int a = 1, b = 2;
return { a, b };
}
nullptr
是c++11引入的新特性,代表一个空指针常量,比NULL
和0
更加安全。void foo(int);
void foo(char*);
// 在c++98中,下面由于NULL存在类型转换的问题,会编译出错
foo(NULL);
// c++11引入nullptr关键字比NULL更安全,并确定编译器调用的是foo(char*)
foo(nullptr);
c++11
提出的新特性,可以实现快速资源转移。std::vector<int> v1 = { 1,2,3 };
std::vector<int> v2(v1);
std::vector<int> v3 = move(v1);
v1
是源对象,v2
和v3
是目标对象,其中v2
会调用拷贝构造函数使用v1
来初始化自己,而v3
则可以通过移动语法快速将v1
的资源转移给自己,从而提高了性能。std::vector<int> v1 = { 1,2,3 };
std::cout << "移动前: " << v1.size() << std::endl;
std::vector<int> v2(v1);
std::vector<int> v3 = move(v1);
std::cout << "移动后: " << v1.size() << std::endl;
std::cout << v2.size() << std::endl;
std::cout << v3.size() << std::endl;
可以发现,移动后,v1
的资源转移给了v3
c++移动语义是通过重载"="操作符
和移动构造函数
来实现的。
完美转发的目的是如果函数收到左值就触发左值函数,接收到右值就触发右值函数,但事实上由于退化现象的存在,右值会退化车成左值,导致无法触发右值。
void Func(int& x) { std::cout << "左值引用" << std::endl; } void Func(const int& x) { std::cout << "const 左值引用" << std::endl; } void Func(int&& x) { std::cout << "右值引用" << std::endl; } void Func(const int&& x) { std::cout << "const 右值引用" << std::endl; } template<typename T> void PefectForward(T&& t) { Func(t); } void main() { PefectForward(10); int a; PefectForward(a); PefectForward(std::move(a)); const int b = 8; PefectForward(b); PefectForward(std::move(b)); return; }
为了解决这一现象,c++11
提供std::forward<T>(t)
来进行完美转发,保持右值属性不变。
//...
// 修改为如下
template<typename T>
void PefectForward(T&& t) {
Func(std::forward<T>(t));
}
class Test {
public:
Test() : Test(0, 0) {} // 委托构造函数
Test(int x, int y) : _x(x), _y(y){}
private:
int _x, _y;
};
typedef int mInt;
mInt i = 10;
using mInt = int;
mInt i = 10;
void main()
{
// c++98遍历迭代器方法
std::vector<int> vec1 = { 1,2,3,4,5 };
for(auto it = vec1.begin();it != vec1.end();it++)
// ...
// c++11简化for方法
for(auto it : vec1)
// ...
return;
}
[capture list](parameter list) mutable exception->
return type { function body }
[capture list] (parameters) -> return type {function body}
示例代码: add
和sub
函数
auto add = [](int a, int b) -> int {return a + b; };
auto sub = [](int a, int b) -> int {return a - b; };
std::cout << add(1, 2) << std::endl;
std::cout << sub(4, 3) << std::endl;
稍微复杂一点
struct Got { std::string key; int price; }; void main() { auto up = [](const Got& left, const Got& right) -> bool {return left.price > right.price; }; auto down = [](const Got& left, const Got& right) -> bool {return left.price < right.price; }; Got gots[] = { {"Alpha", 10}, {"Beta", 11}, {"Cycle", 12}, {"Delete", 13} }; std::sort(gots, gots + sizeof(gots) / sizeof(gots[0]), up); for (auto& got : gots) { std::cout << got.key << ":" << got.price << std::endl; } std::cout << "----------------------------------------" << std::endl; std::sort(gots, gots + sizeof(gots) / sizeof(gots[0]), down); for (auto& got : gots) { std::cout << got.key << ":" << got.price << std::endl; } return; }
int a = 10, b = 20;
std::cout << "a: " << a << " " << "b: " << b << std::endl;
auto swap = [&a, &b]()mutable
{
int c = a;
a = b;
b = c;
};
swap();
std::cout << "a: " << a << " " << "b: " << b << std::endl;
default
关键字显式提供默认构造函数class Test { public: // 不写defult就会报错,因为提供了构造函数,那么默认构造就会失效,除非使用default关键字 Test() = default; Test(const char* name, int age = 0) :_name(name), _age(age) { } private: std::string _name; int _age; }; void main() { Test t1("test", 10); Test t2; return; }
delete
关键字删除Test的默认拷贝构造函数class Test { public: // 不写defult就会报错,因为提供了构造函数,那么默认构造就会失效,除非使用default关键字 Test() = default; Test(const Test& t) = delete; Test(const char* name, int age = 0) :_name(name), _age(age) { } private: std::string _name; int _age; }; void main() { Test t1("test", 10); Test t2; // 报错,因为默认拷贝构造函数使用delete关键字删除了 // Test t3(t1); return; }
在以往中,我们一般常用的是左值引用,c++11
引入了一种新的引用类型称之为右值引用,右值引用主要包括以下两种情况
std::move
标记的非const
对象std::string getName() {
return "Test";
}
void printName(std::string&& name) {
std::cout << "Name: " << name << std::endl;
}
int main() {
std::string name = getName(); // 获取临时对象
printName(std::move(name)); // 将临时对象的所有权转移给右值引用
return 0;
}
getName()
返回的临时对象仍然会触发外部name
的拷贝构造,而printName
中的传参时产生的拷贝构造确实被减少了拷贝次数另一个例子
class MyString { public: // 默认构造函数 MyString() : data(nullptr), length(0) {} // 带参构造函数 MyString(const char* str) { length = std::strlen(str); data = new char[length + 1]; std::strcpy(data, str); } // 移动构造函数 MyString(MyString&& other) noexcept { length = other.length; data = other.data; // 将临时对象置于有效但未指向任何资源的状态 other.length = 0; other.data = nullptr; std::cout << "触发移动构造" << std::endl; } // 析构函数 ~MyString() { delete[] data; } // 获取字符串长度 int getLength() const { return length; } // 获取字符串内容 const char* getData() const { return data; } private: char* data; int length; }; MyString createString() { MyString str("Hello World"); return str; } int main() { MyString str = std::move(createString()); // 接收返回的将亡的临时对象 std::cout << "Length: " << str.getLength() << std::endl; std::cout << "Data: " << str.getData() << std::endl; return 0; }
createString
返回的临时对象使用移动构造赋值给了str
静态断言是C++11引入的一种编译期断言。它在编译期间评估一个常量表达式,并在表达式为false时生成编译错误。静态断言的语法是:
static_assert(expression, message);
int main() {
const int a = 10;
const int b = 20;
static_assert(a + b == 32, "4 + 5 should be equals 9");
return 0;
}
int main() {
const int a = 10;
const int b = 20;
static_assert(a + b == 30, "4 + 5 should be equals 9");
return 0;
}
断言通过表达语句和结果是否匹配进行判断,当不匹配时,会引发编辑器编译错误
智能指针的出现是为了方便管理程序在整个生命周期中的内存分配,因为传统的非智能指针很容易引发内存泄露
的问题,这是一个及其严重的问题,会导致系统崩溃或者系统假死。
避免方法:
c++11
重要以及大多数现在仍在使用的特性就是支持了线程,使得c++在并行编程时不需要依赖与第三方库,并从操作系统中引入了原子类的概念,条件是必须包含<thread>
头文件->thread头文件查看
线程库中使用的最多的就是多线程
、条件变量
、互斥锁
和原子操作
。
函数名称 | 对应功能 |
---|---|
thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
thread(fn,args1, args2,…) | 构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的线程 |
get_id() | 获取线程id |
jionable() | 线程是否还在执行,joinable代表的是一个正在执行中的线程 |
join() | 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行 |
detach() | 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关 |
需要注意的是
函数指针
、lambda表达式
和函数对象
三种方式提供thread
类时防拷贝的,不允许拷贝构造以及赋值,可以进行移动构造和移动赋值,可以将当前线程对象的关联状态转移给其他线程对象,转移期间不影响线程的执行。一个简单的实例,创建t1
线程后,提供CirclePrint
给该函数,然后执行循环打印的功能
void CirclePrint(int x) {
for (int i = 0; i < x; i++) {
std::cout << i << std::endl;
}
}
int main() {
int a = 10;
std::thread t1(CirclePrint, a);
t1.join();
return 0;
}
通过引用传值来修改某些变量的值一定要使用std::ref
关键字,我们无法直接修改主线程中a
变量的值,因为a
在传递给线程函数之前就已经被复制了一份到新线程中,该线程有独立的栈,为了解决这个问题,需要使用std::ref
关键字指明该变量传递的是在主线程中的引用而不是副本。
std::ref
将一个对象转换为对该变量转换为引用类型,并将其传递给线程函数,以便在协程函数中访问原变量,防止编译器复制该对象的副本,实现多线程并发时的数据共享。
std::move
一定要确保生命周期准确,防止出现垂悬引用的问题,在创建新线程的时候使用std::ref
,避免主线程访问已经被销毁的对象。void Change(int& x) { x += 12; } void Change(int* a) { *a += 10; } int main() { int a = 10; int b = 10; // 使用std::ref std::thread t1(Change, std::ref(a)); // 使用&传递参数的地址 std::thread t2(Change, &b); t1.join(); std::cout << a << std::endl; return 0; }
void CirclePrint(int x) { for (int i = 0; i < x; i++) { std::cout << i << std::endl; } } int main() { int n = 10; std::vector<std::thread> v_threads(n); for (auto& t : v_threads) { t = std::thread(CirclePrint, 10); std::cout << "线程id为" << t.get_id() << std::endl; } for (auto& t : v_threads) { t.join(); } return 0; }
int x = 0; void Func(int number) { for (int i = 0; i < number; i++) { ++x; } } int main() { std::thread t1 = std::thread(Func, 10); std::thread t2 = std::thread(Func, 20); t1.join(); t2.join(); std::cout << x << std::endl; return 0; }
看似这段代码没问题,当增加的数字达到一定级别的数量时,会引发线程安全问题。
int x = 0; void Func(int number) { for (int i = 0; i < number; i++) { ++x; } } int main() { std::thread t1 = std::thread(Func, 100000); std::thread t2 = std::thread(Func, 200000); t1.join(); t2.join(); std::cout << x << std::endl; return 0; }
如结果所示,多次执行的记过不同,这是因为当某一时刻线程t1
拿到x
进行增加操作后,还没来得及放回数据,线程t2
就直接拿数据了进行增加了,这时虽然触发了两次增加操作,但是实际上x
只增加了一次,这样就引出了线程安全问题。
void Func(int number) {
for (int i = 0; i < number; i++) {
if (i % 1000 == 0) {
std::cout << std::this_thread::get_id() << "->" << x << std::endl;
}
++x;
}
}
打印线程id后,发现有多次两个线程同时打印的情况,为解决这一问题需要进行加锁。
传统c++98
解决这一问题的方法是使用互斥量库<mutex>
`c++```包括四个互斥量的种类
mutex
是最常用的基本互斥量,包括三个常用的函数
函数 | 函数功能 |
---|---|
lock() | 上锁:锁住互斥量 如果互斥锁是未锁定状态,调用lock()成员函数的线程会得到互斥锁的所有权,并将其上锁。如果互斥锁是锁定状态,调用lock()成员函数的线程就会阻塞等待,直到互斥锁变成未锁定状态。 |
unlock() | 解锁:释放对互斥量的所有权 |
try_lock() | 上锁:如果互斥锁是未锁定状态,则加锁成功,函数返回true, 如果互斥锁是锁定状态,则加锁失败,函数立即返回false。(线程不会阻塞等待) |
使用std::mutex
创建一个互斥量mtx
后,在Func
中使用lock()
和unlock()
进行加锁和解锁保证线程安全
int x = 0; std::mutex mtx; void Func(int number) { for (int i = 0; i < number; i++) { mtx.lock(); if (i % 1000 == 0) { std::cout << std::this_thread::get_id() << "->" << x << std::endl; } ++x; mtx.unlock(); } } int main() { std::thread t1 = std::thread(Func, 1000000); std::thread t2 = std::thread(Func, 2000000); t1.join(); t2.join(); std::cout << x << std::endl; return 0; }
可以发现结果没有出现两个线程竞争统一资源的情况,加锁的目的是防止线程对共享资源同时进行写
操作,而如果只是读
操作则不一定需要加锁。
int x = 0; std::mutex mtx; void Func(int number) { mtx.lock(); for (int i = 0; i < number; i++) { ++x; } mtx.unlock(); } int main() { clock_t start = clock(); std::thread t1 = std::thread(Func, 1000000); std::thread t2 = std::thread(Func, 2000000); t1.join(); t2.join(); clock_t end = clock(); std::cout << x << std::endl; std::cout << "消耗的时间为" << (double)(end - start) / CLOCKS_PER_SEC << std::endl; return 0; }
int x = 0; std::mutex mtx; void Func(int number) { for (int i = 0; i < number; i++) { mtx.lock(); ++x; mtx.unlock(); } } int main() { clock_t start = clock(); std::thread t1 = std::thread(Func, 1000000); std::thread t2 = std::thread(Func, 2000000); t1.join(); t2.join(); clock_t end = clock(); std::cout << x << std::endl; std::cout << "消耗的时间为" << (double)(end - start) / CLOCKS_PER_SEC << std::endl; return 0; }
对比可以发现,锁加载不同位置所消耗的时间是巨大的
c++11
引入了原子操作来解决这一问题。c++11
同样提供了原子操作,对原子类型的操作进行了抽象,定义了统一的接口,并要求编译器产生平台相关的原子操作的具体实现。c++11
标准对原子操作定义为aumic
模板类的成员函数,包括读(load)
、写(store)
、交换(exchange)
等。主要是通过重载一些全局操作符来完成的。比如对上文total+=i的原子加操作,是通过对operator+=重载来实现的。
未使用atomic
的结果
#include <atomic> #include <thread> #include <iostream> using namespace std; int64_t total = 0; //atomic_int64_t相当于int64_t,但是本身就拥有原子性 //线程函数,用于累加 void threadFunc(int64_t endNum) { for (int64_t i = 1; i <= endNum; ++i) { total += 1; } } int main() { int64_t endNum = 10000000; thread t1(threadFunc, endNum); thread t2(threadFunc, endNum); t1.join(); t2.join(); cout << "total=" << total << endl; //10100 }
使用了atomic的结果
// int64_t total = 0;
// 使用了atomic的结果
atomic_int64_t total = 0;
https://bobowen.blog.csdn.net/article/details/128696518?spm=1001.2014.3001.5502
https://blog.csdn.net/weixin_44120785/article/details/128816083
https://zhuanlan.zhihu.com/p/455848360
https://blog.csdn.net/qq_56044032/article/details/125230984
【重点】https://blog.csdn.net/qq_52433890/article/details/127231352
https://blog.csdn.net/u011942101/article/details/124069208
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。