C++ 11 (1)

1.{}初始化

我们在书写代码的时候可以通过{}进行初始化

int main(void)
{
	struct P
	{
		P(int a, int b)
		{
			_a = a;
			_b = b;
			cout << "P" << endl;
		}
		int _a;
		int _b;
	};
	int a1 = { 1 };
	int a2 = 1;
	int a3{ 1 };

	int arr1[] = { 1,2,3 };
	int arr2[]{ 1,2,3 };

	//自定义类型会自动调用其构造函数
	P s1{ 1,2 };
	P s2 = { 1,2 };
	P s11(1, 3);

	//这两组编译会报错 因为本质上{}是多参数的隐式类型转换 会产生临时变量 临时变量具有常性
	//用普通对象接受会发生权限放大不合法
	/*P& s3 = { 3,4 };
	P& s4 { 3,4 };*/

	const P& s3 = { 3,4 };
	const P& s4 = { 3,4 };

	P* pa = new P[4]{ {1,2},{2,3},{3,4},{5,6} };
	int* pb = new int[3] {0};
	return 0;
}

我们发现无论是自定义类型还是内置类型

都可以通过{}初始化 但是要注意 

本质上{}是多参数的隐式类型转换

会产生临时变量 临时变量具有常性  用普通对象接受会发生权限放大不合法

也会自动调用自定义类型的构造函数

我们也可以不用写= 但是最好还是写着 这样可读性高

2.initializer_list

int main(void)
{
	struct P
	{
		P(int a, int b)
		{
			_a = a;
			_b = b;
			cout << "P" << endl;
		}
		int _a;
		int _b;
	};

	//下面两个是不是同一个语法 ?
	//不是 vector后面可以加很多值 但是自定义类型s1只能两个参数
	P s1 = { 1,2 };
	vector<int> s2 = { 1,2,3 };

	auto il = { 1,2,3,4,5 };
	cout << typeid(il).name() << endl;
	//本质上还是调用initializer_list的构造函数
	initializer_list<int>il1 = { 1,2,3,4,5 };
	initializer_list<P> s11 = { {1,2},{3,4},{5,6} };
	cout << sizeof(il1) <<" "<< sizeof(il) << endl;
		//c++不支持这种写法是因为和	initializer_list有冲突
		//int* ptr1 = { 1,2,3 };
		return 0;

}

 这个代码打印的结果是什么?

那么什么是initializer_list呢? 

initializer_list是一个自定义类型

它有两个成员

实现原理 内部结构:initializer_list<T> 仅包含两个成员:

T* begin():指向第一个元素的指针。

T* end():指向最后一个元素之后位置的指针。

initializer_list 内部仅存储指针和大小,其管理的元素通常存储在临时内存中:

编译期确定的常量元素: 如果元素是编译期常量(如字面量 {1, 2, 3}),编译器可能将元素存储在只读内存(类似字符串常量)或静态区。

这些元素在程序启动时创建,生命周期为整个程序运行期。

运行期生成的元素: 如果元素是运行期计算的结果(如函数返回值),元素通常存储在栈上临时区域,生命周期与 initializer_list 对象绑定。

当 initializer_list 超出作用域时,元素可能被销毁(除非被其他对象持有)。

initializer_list<T> 对象的大小固定为两个指针的大小(例如在 64 位系统上为 16 字节),与 T 的类型和元素数量无关。

只读性:无法修改 initializer_list 中的元素(没有 push_back() 等方法)。

类型一致性:花括号内的元素必须可隐式转换为同一类型 T。

优先级:如果类同时有接受 initializer_list 和其他参数的构造函数,列表初始化优先匹配 initializer_list 版本。

vector能这样初始化的原因是因为vector有 initializer_list的构造函数

3. decltype

decltype 是 C++11 引入的编译时类型推导关键字,核心作用是根据表达式的形式推导其类型,且不执行表达式(仅分析类型)。

int main(void)
{
	auto s1 = malloc;
	auto s = 2.3;
	cout << typeid(s1).name() << endl << typeid(s).name() << endl;
	decltype(malloc) s2;
	cout << typeid(s2).name() << endl;
	int a = 2;
	double b = 2.3;
	//本质上是先计算a*b再decltype其类型
	decltype(a * b) s2;
	decltype(a * b) s3 = 23;
		return 0;

}

class A
{
private :
	decltype(malloc) pf1;
};

比如说这个地方的A类的pf1和s2里面就只能用decltype

因为用auto就一定能要初始化 

但是decltype初不初始化都可以!!!
 

4.右值引用

 左值(

左值是指在内存中拥有可标识的地址、可以获取其存储位置的表达式。通俗来讲,左值就是有 “名字”、能被取地址的对象。最典型的左值就是变量,

另外,返回非引用类型的函数调用结果也是右值

右值

右值与左值相对,是指没有固定存储位置、不能获取其地址的表达式。右值通常是一个临时对象或字面常量 

左值与右值的核心区别:

  1. 可寻址性:左值有固定的内存地址,可以通过&运算符获取;右值没有固定地址,无法取地址。
  2. 生命周期:左值的生命周期通常由其作用域决定,在作用域内一直存在;右值大多是临时对象,在表达式结束后立即销毁。
  3. 赋值角色:左值可以作为赋值目标,出现在=左侧;右值只能作为数据源,出现在=右侧(除了 C++11 引入的移动语义场景) 。

匿名对象属于右值 

 左值引用

左值引用使用&符号声明,只能绑定到左值。

左值引用的主要用途包括:

  • 避免对象拷贝:在函数参数传递和返回值时,使用左值引用可以减少不必要的对象拷贝,提高性能。
  • 作为函数返回值:返回左值引用的函数可以让调用者直接操作函数内部的对象(通常是静态对象或成员变量)。

右值引用

右值引用是 C++11 引入的新特性,使用&&符号声明,专门用于绑定右值。

右值引用的核心作用是实现移动语义和完美转发:

  • 移动语义:允许将右值的资源(如动态分配的内存)直接 “移动” 给其他对象,避免不必要的拷贝,提高性能。
  • 完美转发:在模板函数中,右值引用可以保留实参的左值或右值属性,确保参数传递的正确性和高效性。

左值和右值之间的转换

左值引用修饰右值要加const

右值引用修饰左值要用move函数强制将左值转换成右值

int main(void)
{
	double s1 = 2.3;
	double s2 = 3.4;
	//s1 s2都是左值 s1+s2表达式有一个返回值 这个返回值是一个临时变量 这个临时变量是右值
	//左值引用可以引用右值 但是要加const
	//错误示范 double& s3 = (s1 + s2);
	const double& s3 = s1 + s2;

	//右值引用给左值取别名 直接是不可以的
	//错误示范 double&& s5 = s1;
	double&& s6 =move(s2);//move函数可以强制将左值转换成右值


	double&& s4 = s1 + s2;
		return 0;
}

    右值引用的作用 

    namespace ly {
        class num {
        public:
            // 构造函数
            num(int a = 0, float b = 0.0f, double c = 0.0, int* p = nullptr)
                : _a(a), _b(b), _c(c), ptr(nullptr)
            {
                if (p) {
                    // 深拷贝:分配新内存并复制值
                    ptr = static_cast<int*>(malloc(sizeof(int)));
                    *ptr = *p;
                }
                std::cout << "构造函数:深拷贝 " << this << std::endl;
            }
    
            // 拷贝构造函数(深拷贝)
            num(const num& other)
                : _a(other._a), _b(other._b), _c(other._c), ptr(nullptr)
            {
                if (other.ptr) {
                    ptr = static_cast<int*>(malloc(sizeof(int)));
                    *ptr = *other.ptr;
                }
                std::cout << "拷贝构造:深拷贝: " << this << " from " << &other << std::endl;
            }
    
            // 移动构造函数
            num(num&& other) noexcept
                : _a(other._a), _b(other._b), _c(other._c), ptr(other.ptr)
            {
                other.ptr = nullptr;  // 转移所有权后将源对象指针置空
                std::cout << "移动构造: " << this << " from " << &other << std::endl;
            }
    
            // 拷贝赋值运算符(深拷贝)
            num& operator=(const num& a) {
                std::cout << "赋值重载:深拷贝 " << this << " from " << &a << std::endl;
                if (this != &a) {
                    // 释放当前资源
                    free(ptr);
                    ptr = nullptr;
    
                    // 深拷贝
                    if (a.ptr) {
                        ptr = static_cast<int*>(malloc(sizeof(int)));
                        *ptr = *a.ptr;
                    }
    
                    _a = a._a;
                    _b = a._b;
                    _c = a._c;
                }
                return *this;
            }
    
            // 移动赋值运算符
            num& operator=(num&& b) noexcept {
                std::cout << "右值引用赋值: " << this << " from " << &b << std::endl;
                if (this != &b) {
                    // 释放当前资源
                    free(ptr);
    
                    // 转移资源所有权
                    ptr = b.ptr;
                    b.ptr = nullptr;  // 仅需这一行,不要重复设置ptr
    
                    _a = b._a;
                    _b = b._b;
                    _c = b._c;
                }
                return *this;
            }
    
            ~num() {
                std::cout << "析构函数: " << this << std::endl;
                free(ptr);  // 安全释放,ptr可能为nullptr
            }
    
        public:
            int _a;
            float _b;
            double _c;
            int* ptr;
        };
    }
    
    ly::num func1()
    {
        int value = 42;  // 创建一个局部变量
        int* pt3 = &value;  // 指向局部变量的指针
        ly::num other(1, 2.0f, 3.0, pt3);  // 传递指针
        return other;
    }
    
    ly::num func2()
    {
        int value1 = 43;  // 创建一个局部变量
        int* pt4 = &value1;  // 指向局部变量的指针
        ly::num other(1, 2.0f, 3.0, pt4);  // 传递指针
        return other;
    }
    
    
    int main(void)
    {
        int value1 = 100;  // 创建有效对象
        int value2 = 200;
    
        int* pt1 = &value1;  // 指向有效对象
        int* pt2 = &value2;
    
        ly::num apply;
        std::cout << std::endl << std::endl;
    
        apply = func1();  // 注意:func()中的pt3指向局部变量,返回后可能悬空
         std::cout << std::endl << std::endl;
    
        ly::num apply1 = func2();
        std::cout << std::endl << std::endl;
        return 0;
    }
    
    
    

     内置类型的右值是纯右值

    自定义类型的右值是将亡值

    比如函数返回的自定义类型的右值

    生命周期只在这一行  所以被称为将亡值

    我们来看移动赋值运算符和移动构造函数

    本质上都是通过右值引用实现资源转移的

    移动构造函数(通过右值引用实现)相比传统的左值引用拷贝构造函数,

    核心优势在于避免不必要的资源复制,显著提升性能。

    以下是详细对比和解释

    : 一、核心区别:资源管理方式

    1. 拷贝构造函数(左值引用) 必须复制资源:无论资源(如动态内存、文件句柄)是否可转移,都需创建新资源并复制数据。

    开销:深拷贝操作通常涉及内存分配、数据复制等耗时操作。

    2. 移动构造函数(右值引用) 转移资源所有权:直接接管临时对象(右值)的资源,无需复制。 开销:仅需修改指针指向,时间复杂度为 O (1)。

    注意:这个地方的移动构造实现的是资源的转移 不会导致两个指针指向同一块空间

    被析构两次 要区别于浅拷贝!!!

     编译器对于同一行的同一步骤连续的拷贝会进行优化 合二为一 移动拷贝同理

    编译器会把函数的返回值识别成右值(不管会不会拷贝临时变量)

    注意:但是返回值如果是引用不管是左值引用还是右值引用 返回值都是左值!!!

    我们来看一下右值引用的一些使用场景

    int main(void)
    {
    	list<string> it;
    	string s1("123");
    	string s2("456");
    
    	//左值传给val(这个地方参数是引用 所以就不用拷贝)    然后再开空间拷贝构造s1
    	it.push_back(s1);
    
    	//右值传给val(这个地方参数是引用 所以就不用拷贝)    直接把s2的资源移动构造 
    	it.push_back(move(s2));
    
    
    	//本质上是隐式类型转换 要拷贝构造给临时对象(匿名对象) 
    	// 再把匿名对象传给 val 然后开空间拷贝构造匿名对象
    	// 
    	//但是有了右值引用 我们就不用开空间拷贝匿名对象这一步 变成直接转移匿名对象的资源(移动构造)
    	it.push_back("789");
    }
    
    //func函数不能用string&或者string&&作为返回值
    // 因为这两种返回的是左值和右值的别名 但是出了函数作用域
    //返回值被销毁了别名就失效了 和野指针一样
    string func()
    {
    	string s1("123");
    	return s1;
    }
    
    int main(void)
    {
    
    	//s1作为返回值 s1先深拷贝给临时变量 临时变量再深拷贝给s2
    	// 
    	//但是如果有移动构造 临时变量就不用深拷贝给s2 就直接通过资源转移把临时变量转移给s2了 
    	//少了一次深拷贝 大大提升了效率
    	string s2 = func();
    	return 0;
    }

    const+左值引用可以接收右值 为什么还需要右边值引用呢?

    右值引用主要并不是用来专门接收右值的

    而是来解决左值引用无法解决的一些情况

    比如说减少一些没有必要的深拷贝

    5.完美转发

    我们先来看这段代码 

    首先我们知道163行和164行g的参数是右值 所以 匹配的是第156行的g函数

    但是为什么其参数ss是右值 调用的f函数匹配的确是参数是左值的f还函数呢?

    这很好理解 因为不管是s还是ss在g函数的函数体内 都是一个可以取地址的局部变量

    所以自然就是左值了!!!

    同时我们还要理解右值引用得到的到底是右值 还是左值 这个下文有介绍

    void f(string& s) {
    	cout << "左值" << s<< endl<<endl;
    } 
    
    void f(string&& ss) {
    	cout << "右值" << ss << endl<<endl;
    }
    
    void g(string& s) {
    	//f(static_cast<string&>(s));
    	f(forward<string&>(s));
    }
    
    void g(string&& ss){
    	//f(static_cast<string&&>(ss));
    	f(forward<string&&>(ss));
    }
    
    int main(void) {
    	string s1("123");
    	g(s1);
    	g("456");
    	g(move(s1));
    	return 0;
    }

    那如果我们希望s调用左值引用的f函数

    ss调用右值引用的f函数该怎么办呢?

    有两种办法 第一就是用static_cast函数

    第二种就是std::forward函数

    但是在遇到模板的情况下 变成了完美转发(万能引用)

    函数形参时是万能引用 在函数体代码中不是

    折叠规则 

    下图是foward函数的大致代码

    然后我们来看完美转发   

    函数实参是左值 就会被推到为实参类型的引用

    如果是右值 就会被推到为实参的非引用类型 

    
    void f(string& s) {
    	cout << s<< " 左值" << endl;
    } 
    
    
    void f(string&& ss) {
    	cout << ss << " 右值" << endl;
    }
    
    
    
    template<class T>
    void g(T&& s){
    	f(forward<T>(s));
    }
    
    int main(void) {
    	string s1("123");
    	g(s1);//传的参数是左值 类型是string 所以T就会被实例化成string&
    	//g函数就变成
    	/*
    void g(string& && s){
    	f(forward<string&>(s));
    }
    forward返回的就是static_cast(string& &&)(t)
    f(static_cast(string& &&)(t))
    
    因为C++不允许引用的引用所以就出现了折叠规则
    由于折叠规则变成
    
    void g(string&  s){
    	f(forward<string&>(s));
    }
    forward返回的就是static_cast(string&)(t)
    f(static_cast(string &)(t))
    	*/
    
    
    
    
    
    
    	g("223");//传的参数是右值 类型是string T就会被实例化成string
    	//g函数就变成
    /*
    void g(string && s){
    	f(forward<string>(s));
    }
    
    forward返回的就是static_cast(string &&)(t)
    f(static_cast(string &&)(t))
    	*/
    
    	return 0;
    }

     关于右值引用到底是右值还是左值我们要分两种情况讨论

    首先如果右值引用是、右值 合适吗? 不合适 因为右值不能取地址 也不能修改 如果不能修改 我们怎么进行资源转移? 

    如果右值引用是右值 那么我们在资源转移的时候会出现大问题

    class MyString { /* 包含堆内存指针data */ };
    
    void process(MyString&& x) {
        // 假设x是右值(违反实际规则)
        MyString a = x;  // 第一次使用x:触发移动构造,x的资源被转移到a,x.data变为nullptr
        MyString b = x;  // 第二次使用x:尝试移动一个已被掏空的x,访问nullptr,导致未定义行为
    }
    
    // 调用时,传递一个临时对象(右值)
    process(MyString("hello"));

    如果x是右值,两次赋值都会触发移动构造:

    第一次转移资源后,x的内部资源已被掏空(指针为nullptr);

    第二次移动时,会试图访问nullptr,导致崩溃。

    如果右值引用是左值的化那么这个问题就可以解决了

    void process(MyString&& x) {  // x是左值(右值引用变量)
        MyString a = x;  // 编译报错!x是左值,默认尝试调用拷贝构造(如果禁用拷贝则报错)
        MyString b = std::move(x);  // 显式转为右值,触发移动构造:资源从x转移到b,可控
    }

    那么如果右值引用是左值同样也会有一些麻烦

    比如说上面的f函数和g函数 我想让右值引用匹配右值 左值引用匹配左值

    让左值引用和右值引用分开处理

    但是左值引用和右值引用得到的都是右值 我无法做区分

    于是这就出现了完美转发 完美转发的出现 让我们可以实现右值引用和左值引用的分开处理

    在STL里面完美转发被使用的较多 比如下面这张图 args是可变模板参数 在  C++11(2)

    里面有介绍

    6.lambda表达式

    lambda 表达式本质上是一个可调用对象,也被称为匿名函数对象。

    它可以在代码中直接定义,不需要像普通函数那样有一个明确的函数名。

    从语法上看,一个基本的 lambda 表达式由捕获列表、参数列表、可选的返回值类型以及函数体组成。

    语法结构 其一般形式为:[capture list] (parameter list) -> return type { function body }

    捕获列表:用于指定在 lambda 表达式中如何访问外部作用域中的变量,可以是值捕获、引用捕获或者隐式捕获。

    比如[a]表示按值捕获变量a,[&a]表示按引用捕获变量a,[=]表示隐式值捕获所有外部可见变量,[&]表示隐式引用捕获所有外部可见变量 。

    参数列表:和普通函数的参数列表类似,定义了 lambda 表达式接受的参数。例如(int x, int y)表示接受两个int类型的参数。 参数名称可以不写 例如(int,int)

    返回值类型:可以显式指定,也可以让编译器自动推导。

    当函数体只有一个返回语句时,编译器通常能准确推导出返回值类型;但如果函数体有多条语句,为了保证代码的清晰和正确性,最好显式指定返回值类型。

    函数体:包含了 lambda 表达式要执行的具体代码逻辑,和普通函数的函数体一样,实现具体的功能。 

    mutable:默认情况下,lambda函数总是一个const函数(传值时候是const ,引用不是),mutable可以取消其常量
    性。使用该修饰符时,参数列表不可省略(即使参数为空)
    [var]:表示值传递方式捕捉变量var
    [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
    [&var]:表示引用传递捕捉变量var
    [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
    [this]:表示值传递方式捕捉当前的this指针
    注意:
    a. 父作用域指包含lambda函数的语句块
    b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割
    比如:[=, &a, &b]:以引用传递的方式捕捉变量ab,值传递方式捕捉其他所有变量
    [&a, this]:值传递方式捕捉变量athis,引用方式捕捉其他变量
    c. 捕捉列表不允许变量重复传递,否则就会导致编译错误

    比如:[=, a]=已经以值传递方式捕捉了所有变量,捕捉a重复

    d. 在块作用域以外的lambda函数捕捉列表必须为空
    e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者
    非局部变量都
    会导致编译报错。
    f. lambda表达式之间不能相互赋值,即使看起来类型相同
    关于f的底层原因是每一个lambda类型不同

    lambda的底层是仿函数 仿函数的对象调用operator() 只不过这个仿函数的名称类型 我们不知道 对我们来说是匿名的而已

    即:如
    果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()

     可以通过汇编得到验证!!!

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值