C++11学习笔记

目录

一: C++11发展史

C++11是C++发展过程中的第二个版本,是C++98以来最重要的一次更新,曾用名为"C++0x"。

下面是C++版本的迭代示意图:

二:列表初始化

在C++98的版本中,一般结构体和数组可以用{}初始化,但是在C++11之后,试图一切皆可用{}初始化

1. 结构体初始化

cpp 复制代码
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中的思想进行初始化--------一切皆可用{}初始化

  • 在用{}初始化的时候,可以省略掉=
  • 内置类型也可支持{}初始化
cpp 复制代码
#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};

cpp 复制代码
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中增加了右值引用,可能这时候我们会很困惑,其实,我们之前学习的引用是左值引用。
  • 但是无论是左值引用还是右值引用,都是给对象取别名。
  • 对于左值和右值最明显的区分方法就是能不能取地址
  1. 左值是能够取地址
  2. 右值是能够取地址

下面是一些常见的左值与右值的例子

cpp 复制代码
#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.引用与声明周期

  • 一般临时对象的声明周期只有一行,如果想要延长这个临时对象的生命周期,就需要右值引用。
cpp 复制代码
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(右值引⽤).
cpp 复制代码
#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;
}

注意:

  1. 右值本身不可以修改,但是右值引用后就可以修改。
  2. 右值引用本身的属性是左值

4.移动构造和移动赋值

  • 移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。

  • 移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。

移动构造和移动赋值的本质是资源的转移,而不是拷贝。

使用string类的部分模拟实现来说明:

cpp 复制代码
//移动构造
string(string&& s)
{
	//转移资源
	swap(s);
}
//移动赋值
string& oprerator=(string&& s)
{
	swap(s);
	return *this;
}

在C++98的时候,返回的对象是这个对象的拷贝,这样会增加系统的消耗。

cpp 复制代码
std::string foo() {
    std::string s = "hello";
    return s;  // 会调用一次拷贝构造
}

但是,在C++11的时候,我们可以对这一现象进行优化,使用移动构造和移动赋值一定程度上"取代"拷贝构造和移动构造,对资源采用互换的方法,减少系统的消耗!

5.引用折叠

通俗来讲,引用的引用叫做引用折叠。但是,int& && r = i;系统会报错!typedef和模版可以使用引用折叠

通过引用折叠,我们可以引出万能引用的概念,所谓万能引用,就是右值引用的右值引用折叠成右值引用,其他均为左值引用。

cpp 复制代码
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)里面,通过前面引用折叠,传左值实例化后就是左值引用函数,传右值实例化后就是右值引用函数。

此时就会引入一个问题,变量表达式全是左值属性,再次使用这个参数传入另一个函数的时候,它的属性可能发生变化,想要保持对象的属性,那么就需要完美转发。

cpp 复制代码
tempale<class T>
void func1(T&& t)
{
	func2(t);//当t为右值的时候,再传入fun2的时候,就会变成左值
	func3(forward<T>(t));//完美转发
}

五:可变参数模版

1.基本概念

  • 所谓可变参数模版,就是支持传入n个参数。
    两个参数包:
  1. 模版参数包:表示零或者多个模版参数;
  2. 函数参数包:表示零或者多个函数参数;

template <class ...Args>

void func(Args ...args)

{}

template <class ...Args>

void func(Args& ...args)

{}

template <class ...Args>

void func(Args&& ...args)

{}

  • 我们可以使用sizeof...来计算参数保重的个数
cpp 复制代码
template<class ...Args>
void func(Args ...args)
{
	//计算参数保重的个数
	cout<<sizeof...(args)<<endl;
}

2.包拓展的使用

除了使用sizeof...计算包的参数个数,我们还需将包拓展,能够使用包里面的参数

使用例子:

cpp 复制代码
#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系列接口。

cpp 复制代码
#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.成员变量给缺省值

  • 在类的成员变量声明时候,可以给缺省值
cpp 复制代码
class A {
private:
    int x = 10;   // 成员变量声明时给缺省值
public:
    A() {}        // 构造函数没有显式初始化x
};

成员变量声明时的缺省值是给初始化列表兜底用的

3.defult和delete

  1. defult:显式要求编译器生成默认版本

在 C++11 之前,编译器会自动帮你生成一些默认函数,比如:默认构造函数,拷贝构造函数,拷贝赋值运算符,.析构函数,但是,一旦你手动定义了某些函数(比如拷贝构造),编译器就不会再帮你生成"与之冲突"的那一类默认函数,比如移动构造或移动赋值。

假如你确实还想让编译器生成默认版本,用 = default 显式告诉编译器,要求编译器生成默认版本!

cpp 复制代码
class A {
public:
    A() = default;                // 要求生成默认构造
    A(const A&) = default;        // 默认拷贝构造
    A(A&&) = default;             // 默认移动构造
    A& operator=(const A&) = default; // 默认拷贝赋值
    A& operator=(A&&) = default;      // 默认移动赋值
    ~A() = default;               // 默认析构
};
  1. delete:明确禁止使用某个函数
cpp 复制代码
class NonCopyable {
public:
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete;            // 禁止拷贝构造
    NonCopyable& operator=(const NonCopyable&) = delete; // 禁止拷贝赋值
};

4.final与override

两个关键字都是 C++11 引入的面向对象增强语法,目的就是让"继承"和"虚函数"变得可控且安全

  1. override ------ 明确表示重写
    在传统 C++(C++98)里,虚函数重写完全靠函数签名是否匹配来判断。问题是,写错一点点,就会变成"另一个新函数",而不是重写原函数。编译器也不会告诉你。
    比如:
cpp 复制代码
class Base {
public:
    virtual void foo(int) {}
};

class Derived : public Base {
public:
    void foo(double) {}   //  看似重写,其实是重载,不会覆盖父类函数
};

这种错误极其隐蔽,父类指针调用 foo(10) 仍然执行父类版本。

C++11 的解决办法是引入 override:

cpp 复制代码
class Derived : public Base {
public:
    void foo(int) override {}  // 明确告诉编译器:这是重写
};

此时如果函数签名不匹配,编译器直接报错。这就是它的作用------让意图显式化。

  1. final ------ 禁止继承或进一步重写

final 有两种用法:

修饰类:禁止这个类被继承

修饰虚函数:禁止这个虚函数在子类中再被重写

(1) 修饰类

cpp 复制代码
class A final {
public:
    void show() {}
};

class B : public A {};  // 编译错误:A 是 final 类,不能继承

(2) 修饰虚函数

cpp 复制代码
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 的
};
  1. 两个组合在一起使用
cpp 复制代码
class Base {
public:
    virtual void func() {}
};

class Derived : public Base {
public:
    void func() override final {}   // 明确重写,同时禁止再重写
};

七:lambda

  1. 基本语法和使用方法:
  • lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda 表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接收 lambda 对象。

  • lambda表达式的格式: [捕获列表] (参数列表) -> 返回类型 { 函数体 };

    各部分含义:

  • 捕获列表 []:决定 lambda 内能访问哪些外部变量,以及如何访问(按值、按引用)。

  • 参数列表 ():函数参数。

  • 返回类型 ->:可选,如果编译器能推断就不用写。

  • 函数体 {}:具体执行的代码。

cpp 复制代码
#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
}
  1. 捕获 this

在类成员函数中,lambda 可以捕获 this 指针访问成员:

cpp 复制代码
class A {
    int value = 100;
public:
    void run() {
        auto f = [this]() { cout << value; };
        f();  // 100
    }
};

八:包装器,function和bind

  1. 可调用对象(Callable)

在 C++ 中,能够"像函数一样被调用"的东西都叫可调用对象(callable):

  • 普通函数
cpp 复制代码
int add(int a, int b) { return a+b; }
  • 函数指针
cpp 复制代码
int (*pf)(int,int) = add;
pf(1,2);
  • 函数对象(Functors)
cpp 复制代码
struct Multiply {
    int operator()(int a, int b) { return a*b; }
};

Multiply mul;
mul(2,3); // 6
  • Lambda 表达式
cpp 复制代码
auto lambda = [](int a,int b){ return a-b; };
lambda(5,3); // 2
  1. std::function ------ 通用函数包装器

std::function 是 一个类型擦除的模板类,可以存储任何符合签名的可调用对象。

特点:

  • 可以存储函数指针、函数对象、Lambda。
  • 可以像普通函数一样调用。
cpp 复制代码
#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; }; // lambda
    cout << f1(2,3) << endl; // 5
    cout << f2(2,3) << endl; // 6
}
  1. std::bind ------ 参数绑定与延迟调用
  • std::bind 可以 把函数的一部分参数提前绑定,返回一个新的可调用对象。

  • 它的作用有点像"生成小函数版本",或者"函数局部化"。

cpp 复制代码
#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 配合:

cpp 复制代码
std::function<int(int)> func = f;
cout << func(5) << endl; // 1+2+5 = 8

std::bind 的占位符 _1, _2, _3... 对应调用时提供的参数。

可以改变参数顺序,或者固定部分参数,非常灵活。

相关推荐
金水谣4 小时前
10.8考研笔记
笔记
Stanford_11068 小时前
如何利用Python进行数据分析与可视化的具体操作指南
开发语言·c++·python·微信小程序·微信公众平台·twitter·微信开放平台
@小博的博客8 小时前
【Linux探索学习】第二篇Linux的基本指令(2)——开启Linux学习第二篇
linux·运维·学习
千里马-horse9 小时前
Async++ 源码分析8--partitioner.h
开发语言·c++·async++·partitioner
Lucis__10 小时前
再探类&对象——C++入门进阶
开发语言·c++
007php00710 小时前
某大厂跳动面试:计算机网络相关问题解析与总结
java·开发语言·学习·计算机网络·mysql·面试·职场和发展
知识分享小能手10 小时前
微信小程序入门学习教程,从入门到精通,微信小程序核心 API 详解与案例(13)
前端·javascript·学习·react.js·微信小程序·小程序·vue
递归不收敛10 小时前
吴恩达机器学习课程(PyTorch 适配)学习笔记:3.3 推荐系统全面解析
pytorch·学习·机器学习
北京不会遇到西雅图11 小时前
【SLAM】【后端优化】不同优化方法对比
c++·机器人