C++11(一)

C++98 传统的 {} 初始化

基本概念

在 C++98 标准中,{} 主要用于对聚合类型(数组、结构体、联合体)进行初始化。

使用条件

  • 简单的结构体 (POD):即没有构造函数、私有成员等的纯数据结构。
  • 不能有用户自定义的构造函数
  • 不能有私有/保护成员
  • 不能有虚函数

代码示例

cpp 复制代码
struct Point
{
    int _x;
    int _y;
};

struct Student
{
    char name[20];
    int age;
};

int main()
{
    // 1. 数组初始化
    int array1[] = { 1, 2, 3, 4, 5 };     // 自动推断大小
    int array2[5] = { 0 };                // 全部初始化为0
    int array3[3] = { 1, 2 };             // {1, 2, 0} 未指定补0
    
    // 2. 结构体初始化
    Point p = { 1, 2 };                   // p._x = 1, p._y = 2
    Point p2 = { 1 };                     // p2._x = 1, p2._y = 0
    
    // 3. 结构体数组
    Student students[2] = {
        { "Tom", 18 },
        { "Jerry", 20 }
    };
    
    // 4. 二维数组
    int matrix[2][3] = {
        {1, 2, 3},
        {4, 5, 6}
    };
    
    return 0;
}

C++11 列表初始化({}

一、核心目标

C++11 试图实现统一初始化 ,让一切对象都可以用 {} 初始化,也称为列表初始化(List Initialization)。

二、基本语法

cpp 复制代码
// 1. 可以省略 = 号,并且可以使用{}对内置类型初始化
int a{10};           // 等价于 int a = {10};
int b = {20};        // 传统写法也保留

// 2. 数组
int arr[]{1, 2, 3, 4};

// 3. 结构体
struct Point { int x; int y; };
Point p{1, 2};

// 4. 类对象(需要匹配的构造函数)
std::vector<int> v{1, 2, 3, 4};
std::pair<int, std::string> pr{1, "hello"};

//5.自定义类型,本质是类型转换,中间会产生临时对象,编译器优化后变成直接构造。
class MyClass {
public:
    MyClass(int a, int b) {}
};

MyClass obj{1, 2};    // 调用构造函数
// 等价于 MyClass obj = {1, 2};  编译器优化后直接构造

什么时候会产生临时对象?

为什么在自定义类型中使用{}初始化会产生临时对象,但是创建容器时不会?

cpp 复制代码
// 情况1:使用 = 且类型不匹配时(C++98/03风格)
std::string s = "hello";  // 先构造临时 string("hello"),再拷贝构造 s
// 但现代编译器会优化为直接构造(拷贝省略,Copy Elision)

// 情况2:显式创建临时对象
std::vector<int> v = std::vector<int>{1, 2, 3};  // 有临时对象,但会被优化

// 情况3:函数参数传递
void func(std::vector<int> v) {}
func({1, 2, 3});  // 从 {} 构造临时 vector,再拷贝给参数(C++11后可能移动)

之所以{}初始化容器时不会产生临时变量是因为std::initializer_list(一种伪容器/视图)的存在。

C++11 std::initializer_list

C++11 之前的容器初始化非常不便:

cpp 复制代码
// 想要用多个值初始化 vector,需要多次 push_back 或实现多个构造函数
std::vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);

// 或者用数组赋值
int arr[] = {1, 2, 3};
std::vector<int> v2(arr, arr + 3);

不同的元素个数需要不同的构造函数,无法统一处理。

std::initializer_list 底层会开辟一个数组,将数据拷贝过来,内部有两个指针(也可能是头指针加长度):

它指向的数据通常是由编译器在幕后生成的临时数组(可能在栈上,也可能在静态存储区)。

一旦这个临时的初始化表达式结束,底层的数组可能就会失效,而 initializer_list 里的指针就会变成悬空指针。
真正的容器 :你可以随意修改里面的元素(v[0] = 10),可以增删元素(push_back)。
std::initializer_list :它是完全只读的。

它的迭代器类型是 const T*。

你不能通过它修改元素的值,也不能往里面添加或删除元素。

cpp 复制代码
// 概念实现(简化版)
template<typename T>
class initializer_list {
    const T* _begin;   // 指向数组起始
    const T* _end;     // 指向数组末尾
    // ...
};

核心工作流程先由编译器生成一个临时的"只读数组",然后 vector(或其他容器)遍历这个临时数组,把元素一个个拷贝(或移动)到自己的内存里。

cpp 复制代码
std::vector<std::string> v = {"hello", "world"};

第一次拷贝:字符串常量 → initializer_list 的底层数组

第二次拷贝:initializer_list 底层数组 → vector 内部存储

initializer_list 返回值是常量左值引用,因为要求不能修改initializer_list 的内容,所以const对象的指针是无法绑定到非const对象上的,而容器实际上要求支持修改,所以拷贝无法避免,理论上initializer_list 返回右值引用可以实现移动,避免拷贝,但是由于历史问题和其他安全性原因,c++委员会并未修改这一标准。

当然实际上编译器可以直接优化掉地一次拷贝,使用临时变量直接构造initializer_list对象。称之为复制消除
容器⽀持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的 {x1,x2,x3...} 进⾏

初始化。STL中的容器⽀持任意多个值构成的 {x1,x2,x3...} 进⾏初始化,就是通过

std::initializer_list的构造函数⽀持的。


右值引用和移动语义

一、背景概述

C++98 中的引用现在称为左值引用 。C++11 新增了右值引用语法特性。无论是左值引用还是右值引用,本质都是给对象取别名。

二、左值和右值

1. 左值(lvalue)

定义:表示数据的表达式(如变量名或解引用的指针),一般有持久状态,存储在内存中。

特点

  • 可以获取地址(&
  • 可以出现在赋值符号左边
  • 也可以出现在赋值符号右边
  • const 修饰的左值不能赋值,但可以取地址

示例

cpp 复制代码
int x = 10;      // x 是左值
int* p = &x;     // 可以取地址
int y = x;       // x 出现在右边
x = 20;          // x 出现在左边

const int z = 30;  // z 是 const 左值
// z = 40;         //  错误:不能赋值
int* pz = &z;      //  可以取地址(需要 const_cast 转换)

2. 右值(rvalue)

定义:表示数据的表达式,通常是字面值常量或表达式求值过程中创建的临时对象。

特点:

  • 不能取地址

  • 只能出现在赋值符号右边

  • 不能出现在赋值符号左边

cpp 复制代码
int x = 10;      // 10 是右值
int y = x + 20;  // x + 20 是右值(临时结果)

// 10 = x;        //  错误:右值不能在左边
// int* p = &10;  //  错误:不能取地址
cpp 复制代码
#include<iostream>
using namespace std;
int main()
{
 // 左值:可以取地址 
 // 以下的p、b、c、*p、s、s[0]就是常⻅的左值 
 int* p = new int(0);
 int b = 1;
 const int c = b;
 *p = 10;
 string s("111111");
 s[0] = 'x';
 cout << &c << endl;
 cout << (void*)&s[0] << endl;
 // 右值:不能取地址 
 double x = 1.1, y = 2.2;
 // 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值 
 10;
 x + y;
 fmin(x, y);
 string("11111");
 //cout << &10 << endl;
 //cout << &(x+y) << endl;
 //cout << &(fmin(x, y)) << endl;
 //cout << &string("11111") << endl;
 return 0;
}

左值引用与右值引用

一、基本语法

cpp 复制代码
Type& r1 = x;      // 左值引用:给左值取别名
Type&& rr1 = y;    // 右值引用:给右值取别名

二、绑定规则

  1. 左值引用的绑定
cpp 复制代码
int a = 10;
int& r1 = a;        //  左值引用绑定左值

// int& r2 = 10;    //  错误:左值引用不能直接绑定右值
const int& r3 = 10; // const 左值引用可以绑定右值(延长生命周期)
  1. 右值引用的绑定
cpp 复制代码
int&& rr1 = 10;     // 右值引用绑定右值

int a = 10;
// int&& rr2 = a;   //  错误:右值引用不能直接绑定左值
int&& rr3 = std::move(a);  //  使用 move 将左值转为右值引用

std::move

std::move 是一个函数模板,内部进行强制类型转换(涉及引用折叠),将左值转换为右值引用。它的作用是告诉编译器:"这个变量虽然是个有名字的左值,但我保证之后不会再使用它了,你可以把它当作一个临时的右值来处理,放心地'偷走'它的资源。

cpp 复制代码
int a = 10;
int&& rr = std::move(a);  // 将左值 a 转为右值引用
// 注意:move 只是转换类型,并不移动任何东西
// 真正的移动发生在移动构造函数或移动赋值运算符中

template <class T>
typename remove_reference<T>::type&& move(T&& arg);

右值引用变量本身是左值

关键规则:变量表达式都是左值属性。

cpp 复制代码
int&& rr = 10;   // rr 是右值引用,绑定到右值 10
// 但 rr 本身是一个变量,有名字,可取地址
// 所以 rr 作为表达式时,是左值!

int* p = &rr;    //  可以取地址,证明 rr 是左值
// int&& rr2 = rr;  //  错误:不能将右值引用绑定到左值
int&& rr2 = std::move(rr);  //  需要再次 move

C++ 移动构造与移动赋值详解

在 C++11 及更高版本中,移动语义(Move Semantics)是提升性能的关键特性,特别是对于涉及深拷贝(Deep Copy)的类。

核心概念
  • 移动构造函数

    • 类似于拷贝构造函数,但其参数必须是右值引用T&&)。
    • 如果存在其他参数,这些参数必须有默认值。
    • 作用:当通过临时对象(右值)初始化新对象时调用,直接"窃取"源对象的资源,避免深拷贝。
  • 移动赋值运算符

    • 是赋值运算符的重载,与拷贝赋值运算符构成重载关系。
    • 参数同样必须是右值引用T&&)。
    • 作用:当将一个临时对象(右值)赋值给一个已存在的对象时调用,接管资源。
为什么需要移动语义?

对于像 std::stringstd::vector 这样管理堆内存的类,或者包含深拷贝成员变量的类,传统的拷贝操作开销巨大(需要重新分配内存并复制数据)。

本质区别:

  • 拷贝(Copy): 复制资源(深拷贝),开销大。
  • 移动(Move): "窃取"资源(浅拷贝指针),源对象被置为有效但未指定的状态(通常指针置空),开销极小,效率极高。

简化string类实现:

cpp 复制代码
#include <iostream>
#include <cstring> // 用于 strcpy, strlen
#include <utility> // 用于 std::move

class MyString {
private:
    char* _data; // 管理堆内存的指针
    int _size;

public:
    // 1. 普通构造函数
    MyString(const char* str = "") {
        std::cout << "[普通构造] 分配内存: " << str << std::endl;
        _size = strlen(str);
        _data = new char[_size + 1];
        strcpy(_data, str);
    }

    // 2. 拷贝构造函数 (深拷贝)
    // 场景:MyString s2 = s1;
    MyString(const MyString& other) {
        std::cout << "[拷贝构造] 深拷贝数据..." << std::endl;
        _size = other._size;
        _data = new char[_size + 1]; // 重新分配内存
        strcpy(_data, other._data);  // 复制内容
    }

    // 3. 移动构造函数 (核心重点)
    // 参数必须是右值引用 (MyString&&)
    // 场景:MyString s3 = std::move(s1); 或者 MyString s3 = MyString("临时对象");
    MyString(MyString&& other) noexcept {
        std::cout << "[移动构造] 窃取资源..." << std::endl;
        
        // --- 核心步骤开始 ---
        
        // 第一步:直接接管对方的资源(指针)
        _data = other._data; 
        _size = other._size;

        // 第二步:把对方置空(防止析构时重复释放内存)
        other._data = nullptr; 
        other._size = 0;

        // --- 核心步骤结束 ---
    }
		4. 移动赋值运算符
    // ==========================================
    MyString& operator=(MyString&& other) noexcept {
        std::cout << "[移动赋值] 开始..." << std::endl;

        // --- 步骤一:自赋值检查 ---
        // 如果写 s1 = std::move(s1),必须防止自己释放自己的资源
        if (this != &other) {
            
            // --- 步骤二:释放旧资源 ---
            // 这一点与移动构造不同!
            // 移动构造时对象是新的,没有旧资源。
            // 但赋值时,this 对象可能已经持有一块内存(比如 "Hello"),必须先释放,否则内存泄漏。
            delete[] _data; 

            // --- 步骤三:窃取新资源 ---
            _data = other._data;
            _size = other._size;

            // --- 步骤四:掏空源对象 ---
            other._data = nullptr;
            other._size = 0;
        }
        return *this;
    }
    // 析构函数
    ~MyString() {
        if (_data) {
            std::cout << "[析构] 释放内存: " << _data << std::endl;
            delete[] _data;
        } else {
            std::cout << "[析构] 释放空指针 (无需操作)" << std::endl;
        }
    }

    // 打印内容的辅助函数
    void print() const {
        if (_data) std::cout << "内容: " << _data << std::endl;
        else std::cout << "内容: (空)" << std::endl;
    }
};

int main() {
    std::cout << "=== 1. 创建源对象 s1 ===" << std::endl;
    MyString s1("Hello World");

    std::cout << "\n=== 2. 演示拷贝构造 (深拷贝) ===" << std::endl;
    MyString s2 = s1; // 调用拷贝构造
    s2.print();       // s2 有自己的内存

    std::cout << "\n=== 3. 演示移动构造 (窃取资源) ===" << std::endl;
    // 使用 std::move 将 s1 转为右值,触发移动构造
    MyString s3 = std::move(s1); 
    
    std::cout << "s3 (新对象): ";
    s3.print(); // s3 接管了 s1 的内存

    std::cout << "s1 (原对象): ";
    s1.print(); // s1 的内存被偷走了,变为空

    std::cout << "\n=== 4. 程序结束,调用析构 ===" << std::endl;
    return 0;
}

之所以参数必须是右值引用,是因为右值马上就要销毁了,所以我们可以放心大胆地把它的资源拿走,而不需要担心它后续会被使用。如果是左值引用,就可能会造成被引用资源移动,但后续该内存依旧在某些部分被访问,会导致非法内存访问"

相关推荐
水云桐程序员1 小时前
C++的主要应用场景
c++·学习方法
叼烟扛炮2 小时前
C++第一讲:C++ 入门基础
开发语言·c++·函数重载·引用·内联函数·nullptr
zh_xuan2 小时前
使用libcurl调用http接口
c++·github·libcurl
zh路西法2 小时前
【RDKX5多摄像头模型推理】USB带宽限制与ROS2话题零拷贝转发
linux·c++·python·深度学习
千寻girling3 小时前
五一劳动节快乐 [特殊字符][特殊字符][特殊字符]
java·c++·git·python·学习·github·php
汉克老师3 小时前
GESP2025年3月认证C++五级( 第一部分选择题(9-15))
c++·算法·高精度计算·二分算法·gesp5级·gesp五级
迷途之人不知返4 小时前
stack和queue的学习与模拟实现
c++
汉克老师4 小时前
GESP2025年3月认证C++五级( 第二部分判断题(1-10))
c++·算法·分治算法·线性筛法·gesp5级·gesp五级
橙子也要努力变强4 小时前
volatile与信号
linux·服务器·c++