C++11学习笔记
目录
- 一: C++11发展史
- 二:列表初始化
- 1. 结构体初始化
- 2. 使用C++11中的思想进行初始化--------一切皆可用{}初始化
- 三:initializer_list
- 四:右值引用和移动语义
- 1.左值和右值的区别:
- 2.引用与声明周期
- 3.左值和右值的参数匹配
- 4.移动构造和移动赋值
- 5.引用折叠
- 6.完美转发
- 五:可变参数模版
- 1.基本概念
- 2.包拓展的使用
- 3.empalce系列接口
- 六:类的新功能
- 1.默认的移动构造和移动赋值
- 2.成员变量给缺省值
- 3.defult和delete
- 4.final与override
- 七:lambda
- 八:包装器,function和bind
一: C++11发展史
C++11是C++发展过程中的第二个版本,是C++98以来最重要的一次更新,曾用名为“C++0x”。
下面是C++版本的迭代示意图:
二:列表初始化
在C++98的版本中,一般结构体和数组可以用{}初始化,但是在C++11之后,试图一切皆可用{}初始化
1. 结构体初始化
struct Test
{int _x;int _y;
};int main()
{struct Test test = { 1,2 };std::cout << test._x << std::endl;std::cout << test._y << std::endl;return 0;
}
2. 使用C++11中的思想进行初始化--------一切皆可用{}初始化
- 在用{}初始化的时候,可以省略掉=
- 内置类型也可支持{}初始化
#include<vector>
using namespace std;
struct Point
{int _x;int _y;
};
class Date
{
public://默认构造Date(int year = 1, int month = 1, int day = 1): _year(year), _month(month), _day(day){cout << "Date(int year, int month, int day)" << endl;}//拷贝构造Date(const Date& d): _year(d._year), _month(d._month), _day(d._day){cout << "Date(const Date& d)" << endl;}
private:int _year;int _month;int _day;
};
int main()
{//自定义类型初始化 Date d1 = { 2025,9,29 };//去掉=也可以Date d2 { 2025,9,29 };
///////////////////////////////////////////////内置类型初始化int i = 1;int i = {1};int i {1};return 0;
}
三:initializer_list
- 在STL中,增加了一个initializer_list的构造。
- initializer_list支持的迭代器遍历,begin和end返回的是指针
比如,使用一个vector对象,我们可以这样用,vector v1 = {1,2,3};
int main()
{std::initializer_list<int> mylist ;mylist = { 1,2,3,4,5,6 };cout << sizeof(mylist) << endl;//支持迭代器cout << *mylist.begin() << endl;cout << *mylist.end() << endl;//使用了这个构造函数//vector (initializer_list<value_type> il,const allocator_type& alloc = allocator_type());vector<int> v1({ 1,2,3,4,5,6 });return 0;
}
四:右值引用和移动语义
1.左值和右值的区别:
- 相比于C++98 ,在C++11中增加了右值引用,可能这时候我们会很困惑,其实,我们之前学习的引用是左值引用。
- 但是无论是左值引用还是右值引用,都是给对象取别名。
- 对于左值和右值最明显的区分方法就是能不能取地址
- 左值是能够取地址
- 右值是能够取地址
下面是一些常见的左值与右值的例子
#include<iostream>
using namespace std;int& getRef(int& a) { return a; }int main() {int x = 10;int y = 5;int arr[3] = { 1, 2, 3 };int* p = &y;// 常见左值x = 20; // 具名变量*p = 42; // 解引用表达式arr[0] = 10; // 数组元素getRef(x) = 200; // 返回引用的函数char str[] = "hello";str[0] = 'H'; // 字符数组元素//常见右值int a = 1 + 2; // 算术运算的结果(1+2 是右值)int b = x + y; // 表达式结果int c = 100; // 字面量 100 是右值int d = x++; // x++ 产生右值int e = std::move(x); // std::move(x) 是右值return 0;
}
2.引用与声明周期
- 一般临时对象的声明周期只有一行,如果想要延长这个临时对象的生命周期,就需要右值引用。
int a = 1;
int b = 1;
int&& c = a+b;
那么问题就来了,int c = a+b;是什么?
其实a+b由系统生成临时对象,然后再拷贝给了c,如果涉及到类的拷贝构造,这样其实会增加系统的消耗。
3.左值和右值的参数匹配
- C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配。
- C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会
匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤).
#include <iostream>
using namespace std;// 左值引用版本
void f(int& x) {cout << "调用 f(int&): 左值引用重载, x=" << x << "\n";
}// const 左值引用版本(既能接左值,也能接右值常量)
void f(const int& x) {cout << "调用 f(const int&): const 左值引用重载, x=" << x << "\n";
}// 右值引用版本
void f(int&& x) {cout << "调用 f(int&&): 右值引用重载, x=" << x << "\n";
}int main() {int i = 1;const int ci = 2;f(i); // i 是一个左值 → 调用 f(int&)f(ci); // ci 是 const 左值 → 调用 f(const int&)f(3); // 3 是右值 → 调用 f(int&&)// 如果没有 f(int&&),就会退化调用 f(const int&)f(std::move(i)); // std::move(i) 把 i 转换成右值 → 调用 f(int&&)// 注意:右值引用本身在表达式中是左值int&& x = 1; // 1 是右值,x 是一个右值引用变量f(x); // x 是有名字的变量 → 左值 → 调用 f(int&)f(std::move(x)); // std::move(x) → 转换为右值 → 调用 f(int&&)return 0;
}
注意:
- 右值本身不可以修改,但是右值引用后就可以修改。
- 右值引用本身的属性是左值
4.移动构造和移动赋值
移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。
移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。
移动构造和移动赋值的本质是资源的转移,而不是拷贝。
使用string类的部分模拟实现来说明:
//移动构造
string(string&& s)
{//转移资源swap(s);
}
//移动赋值
string& oprerator=(string&& s)
{swap(s);return *this;
}
在C++98的时候,返回的对象是这个对象的拷贝,这样会增加系统的消耗。
std::string foo() {std::string s = "hello";return s; // 会调用一次拷贝构造
}
但是,在C++11的时候,我们可以对这一现象进行优化,使用移动构造和移动赋值一定程度上“取代”拷贝构造和移动构造,对资源采用互换的方法,减少系统的消耗!
5.引用折叠
通俗来讲,引用的引用叫做引用折叠。但是,int& && r = i;系统会报错!typedef和模版可以使用引用折叠
通过引用折叠,我们可以引出万能引用的概念,所谓万能引用,就是右值引用的右值引用折叠成右值引用,其他均为左值引用。
template<class T>
void func1(T&& x)
{/////
}
template<class T>
void func2(T& x)
{/////
}
int main()
{int a = 0;func1<int>(a);//实例化为void func(int& x);func1<int&>(a);//实例化为void func(int& x);func1<int&&>(0);//实例化为void func(int&& x);func2<int>(a);//实例化为void func(int& x);func2<int&>(a);//实例化为void func(int& x);func2<int&&>(a);//实例化为void func(int& x);return 0;
}
- 如果想将左值传给右值,那么需要move()
- func1<int&&>(std::move(a));
6.完美转发
在Function(int&& t)里面,通过前面引用折叠,传左值实例化后就是左值引用函数,传右值实例化后就是右值引用函数。
此时就会引入一个问题,变量表达式全是左值属性,再次使用这个参数传入另一个函数的时候,它的属性可能发生变化,想要保持对象的属性,那么就需要完美转发。
tempale<class T>
void func1(T&& t)
{func2(t);//当t为右值的时候,再传入fun2的时候,就会变成左值func3(forward<T>(t));//完美转发
}
五:可变参数模版
1.基本概念
- 所谓可变参数模版,就是支持传入n个参数。
两个参数包:
- 模版参数包:表示零或者多个模版参数;
- 函数参数包:表示零或者多个函数参数;
template <class …Args>
void func(Args …args)
{}
template <class …Args>
void func(Args& …args)
{}
template <class …Args>
void func(Args&& …args)
{}
- 我们可以使用sizeof…来计算参数保重的个数
template<class ...Args>
void func(Args ...args)
{//计算参数保重的个数cout<<sizeof...(args)<<endl;
}
2.包拓展的使用
除了使用sizeof…计算包的参数个数,我们还需将包拓展,能够使用包里面的参数
使用例子:
#include<iostream>
using namespace std;
//3.终止条件
void func()
{cout << endl;
}
//2.
template<class T, class ...Args>
void func(T&& t, Args&&... args)
{cout << t << endl;func(args...);
}
//1.
template<class ...Args>
void Print(Args&&... args)
{func(args...);
}int main()
{Print(1, "nihao", 3);return 0;
}
3.empalce系列接口
- C++11后面STL容器增加了empalce系列的接口,它们的接口参数均为模版可变参数,在功能上与insert和push兼容。
- 假设容器container,empalce还支持直接插入构造T对象的参数,这样的场景更加高效一些,可以直接在容器上面构造T对象。
在实践上,我们可以直接使用empalce系列接口代替oush,insert系列接口。
#include<list>
#include<string>
using namespace std;// emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列
int main()
{list<string> lt;// 传左值,跟push_back⼀样,⾛拷⻉构造std::string s1("111111111111");lt.emplace_back(s1);// 右值,跟push_back⼀样,⾛移动构造lt.emplace_back(move(s1));// 直接把构造string参数包往下传,直接⽤string参数包构造string// 这⾥达到的效果是push_back做不到的lt.emplace_back("111111111111");list<pair<string, int>> lt1;// 跟push_back⼀样// 构造pair + 拷⻉/移动构造pair到list的节点中data上pair<string, int> kv("苹果", 1);lt1.emplace_back(kv);// 跟push_back⼀样lt1.emplace_back(move(kv));// 直接把构造pair参数包往下传,直接⽤pair参数包构造pair// 这⾥达到的效果是push_back做不到的lt1.emplace_back("苹果", 1);return 0;
}
六:类的新功能
1.默认的移动构造和移动赋值
- 原来的C++类(C++98)中,有六个默认成员函数:构造,析构,拷贝,拷贝赋值重载,取地址重载,const取地址重载
- 在C++11中,新增了俩默认成员函数,移动构造函数和移动赋值函数。
- 规则:如果你没有⾃⼰实现移动构造函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个。那么编译器会⾃动⽣成⼀个默认移动构造。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造没有实现就调⽤拷⻉构造。
2.成员变量给缺省值
- 在类的成员变量声明时候,可以给缺省值
class A {
private:int x = 10; // 成员变量声明时给缺省值
public:A() {} // 构造函数没有显式初始化x
};
成员变量声明时的缺省值是给初始化列表兜底用的
3.defult和delete
- defult:显式要求编译器生成默认版本
在 C++11 之前,编译器会自动帮你生成一些默认函数,比如:默认构造函数,拷贝构造函数,拷贝赋值运算符,.析构函数,但是,一旦你手动定义了某些函数(比如拷贝构造),编译器就不会再帮你生成“与之冲突”的那一类默认函数,比如移动构造或移动赋值。
假如你确实还想让编译器生成默认版本,用 = default 显式告诉编译器,要求编译器生成默认版本!
class A {
public:A() = default; // 要求生成默认构造A(const A&) = default; // 默认拷贝构造A(A&&) = default; // 默认移动构造A& operator=(const A&) = default; // 默认拷贝赋值A& operator=(A&&) = default; // 默认移动赋值~A() = default; // 默认析构
};
- delete:明确禁止使用某个函数
class NonCopyable {
public:NonCopyable() = default;NonCopyable(const NonCopyable&) = delete; // 禁止拷贝构造NonCopyable& operator=(const NonCopyable&) = delete; // 禁止拷贝赋值
};
4.final与override
两个关键字都是 C++11 引入的面向对象增强语法,目的就是让“继承”和“虚函数”变得可控且安全
- override —— 明确表示重写
在传统 C++(C++98)里,虚函数重写完全靠函数签名是否匹配来判断。问题是,写错一点点,就会变成“另一个新函数”,而不是重写原函数。编译器也不会告诉你。
比如:
class Base {
public:virtual void foo(int) {}
};class Derived : public Base {
public:void foo(double) {} // 看似重写,其实是重载,不会覆盖父类函数
};
这种错误极其隐蔽,父类指针调用 foo(10) 仍然执行父类版本。
C++11 的解决办法是引入 override:
class Derived : public Base {
public:void foo(int) override {} // 明确告诉编译器:这是重写
};
此时如果函数签名不匹配,编译器直接报错。这就是它的作用——让意图显式化。
- final —— 禁止继承或进一步重写
final 有两种用法:
修饰类:禁止这个类被继承
修饰虚函数:禁止这个虚函数在子类中再被重写
(1) 修饰类
class A final {
public:void show() {}
};class B : public A {}; // 编译错误:A 是 final 类,不能继承
(2) 修饰虚函数
class Base {
public:virtual void foo() {}
};class Derived : public Base {
public:void foo() final {} // 可以重写,但禁止再往下重写
};class SubDerived : public Derived {
public:void foo() override {} // 编译错误:foo 在 Derived 中是 final 的
};
- 两个组合在一起使用
class Base {
public:virtual void func() {}
};class Derived : public Base {
public:void func() override final {} // 明确重写,同时禁止再重写
};
七:lambda
- 基本语法和使用方法:
lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda 表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接收 lambda 对象。
lambda表达式的格式: [捕获列表] (参数列表) -> 返回类型 { 函数体 };
各部分含义:捕获列表 []:决定 lambda 内能访问哪些外部变量,以及如何访问(按值、按引用)。
参数列表 ():函数参数。
返回类型 ->:可选,如果编译器能推断就不用写。
函数体 {}:具体执行的代码。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;int main() {vector<int> v = {1,2,3,4,5};// Lambda 遍历输出for_each(v.begin(), v.end(), [](int x){cout << x << " ";});cout << endl;// Lambda 带返回值auto square = [](int x) -> int { return x*x; };cout << square(5) << endl; // 25
}
- 捕获 this
在类成员函数中,lambda 可以捕获 this 指针访问成员:
class A {int value = 100;
public:void run() {auto f = [this]() { cout << value; };f(); // 100}
};
八:包装器,function和bind
- 可调用对象(Callable)
在 C++ 中,能够“像函数一样被调用”的东西都叫可调用对象(callable):
- 普通函数
int add(int a, int b) { return a+b; }
- 函数指针
int (*pf)(int,int) = add;
pf(1,2);
- 函数对象(Functors)
struct Multiply {int operator()(int a, int b) { return a*b; }
};Multiply mul;
mul(2,3); // 6
- Lambda 表达式
auto lambda = [](int a,int b){ return a-b; };
lambda(5,3); // 2
- std::function —— 通用函数包装器
std::function 是 一个类型擦除的模板类,可以存储任何符合签名的可调用对象。
特点:
- 可以存储函数指针、函数对象、Lambda。
- 可以像普通函数一样调用。
#include <functional>
#include <iostream>
using namespace std;int add(int a,int b){ return a+b; }int main() {std::function<int(int,int)> f1 = add; // 普通函数std::function<int(int,int)> f2 = [](int a,int b){ return a*b; }; // lambdacout << f1(2,3) << endl; // 5cout << f2(2,3) << endl; // 6
}
- std::bind —— 参数绑定与延迟调用
std::bind 可以 把函数的一部分参数提前绑定,返回一个新的可调用对象。
它的作用有点像“生成小函数版本”,或者“函数局部化”。
#include <functional>
#include <iostream>
using namespace std;int add(int a, int b, int c) { return a+b+c; }int main() {auto f = std::bind(add, 1, 2, std::placeholders::_1); // bind 参数说明:// 1,2 固定绑定,_1 是调用时传入的第一个参数cout << f(3) << endl; // 1+2+3 = 6
}
可以和 std::function 配合:
std::function<int(int)> func = f;
cout << func(5) << endl; // 1+2+5 = 8
std::bind 的占位符 _1, _2, _3… 对应调用时提供的参数。
可以改变参数顺序,或者固定部分参数,非常灵活。