C++面向对象编程(2)

这篇具有很好参考价值的文章主要介绍了C++面向对象编程(2)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

一. 问题引入

二. 右值引用

1. lvalue/rvalue/prvalue/xvalue

1.1 表达式与对象的概念

1.2 左值与右值 

2. moving semantics

2.1 显示绑定

2.2 Move constructors

2.3 Move assignment operator

2.4 实例分析

// TODO

Quiz

REF


本章简单介绍下move语义的“来龙去脉”。

一. 问题引入

如下,先来看看拷贝赋值的一个过程。

C++面向对象编程(2),c++,c++,move构造/赋值,C++左值与右值

Q1:上述过程看起来,

(1) “已知foo()返回的对象是临时的,要析构销毁的” ;

(2) f新开辟的地址空间“addr NewB”与foo()指向的地址空间大小和内容都是一样的。

那么,让f析构掉自己原来的地址空间“addr B”,然后f直接指向foo()的地址空间,foo()置为null不就可以了吗?这样还可以减少开辟和销毁内存空间的操作。过程如下:

C++面向对象编程(2),c++,c++,move构造/赋值,C++左值与右值

Q2:当然基于Q1,我们还可以把delete f的地址空间addr B的任务交给foo(),即跟foo()的析构过程绑定在一起。这就是移动语义(moving semantics)。过程如下:

C++面向对象编程(2),c++,c++,move构造/赋值,C++左值与右值

 到此,“移动语义”就被引入了。接下来的事情就是介绍C++是如何定义“移动语义”的。

二. 右值引用

为了实现移动语义,C++11引入了右值引用。

对于类型T,T&& 被称为对T的右值引用,T&被称为对T的左值引用。同样的移动语义(moving semantics)也有对应的移动构造和移动赋值,结构如下:

1. 移动构造

    类名 ( 类名 && )

2. 移动赋值

  类名 & 类名 :: operator= ( 类名 && )

同时,调用者可以使用std::move()库函数来实现移动语义的转换。

有了一个大概的概念,先来看看如下代码及运行结果。 

#include <iostream>

using namespace std;

void foo(int &&x)
{
    cout << "call && rvalue reference"<< endl;
}

void foo(int &x)
{
    cout << "call & lvalue reference" << endl;
}

int main() {
    int i = 1;
    int &lv = i; // 左值引用
    int &&rv = 2; // 右值引用
    
    foo(1); // call && rvalue reference
    foo(i); // call & lvalue reference
    foo(lv); // all & lvalue reference
    foo(rv); // call & lvalue reference
    
    return 0;
}

运行结果及分析如下: 

// 因为foo(1);中的'1'是字面量,字面量为纯右值,这里不能用纯右值去初始化一个左值引用,所以会调用void foo(int &&x){}
call && rvalue reference

// 因为foo(i);中的i是变量名,即是一个左值,所以会调用void foo(int &x){}
call & lvalue reference

// 因为foo(lv);中的lv是一个左值引用,所以会调用void foo(int &x){}
call & lvalue reference

// 虽然foo(rv);中的rv是一个右值引用,但是因为rv是一个变量名,所以也是一个左值,进而会调用void foo(int &x){} --从这里也可知道 右值引用其实也是一个左值
call & lvalue reference

接下来再来分别介绍下上面提到的4个概念:左值、右值、左值引用、右值引用。 

1. lvalue/rvalue/prvalue/xvalue

C++面向对象编程(2),c++,c++,move构造/赋值,C++左值与右值

【《C++ Primer》(5th)4.1 基础】

 C++的表达式要不然是右值(rvalue,读作“are-value”),要不然就是左值(lvalue,读作“ell-value”)。

这两个名词是从C语言继承过来的,原本是为了帮助记忆:左值可以位于赋值语句的左侧,右值则不能。在C++语言中,二者的区别就没那么简单了。一个左值表达式的求值结果是一个对象或者一个函数,然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象。

1.1 表达式与对象的概念

Q:什么叫表达式?

A:《C++ Primer》里是这样解释的:“表达式(expression)最小的计算单元。一个表达式包含一个或多个运算对象,通常还包含一个或多个运算符。表达式求值会产生一个结果。例如,假设i和j是int对象,则i+j是一个表达式,它产生两个int值的和”。简单来说,表达式就是运算符和运算对象组成的序列,能指明一个计算且能够产生结果或作用。例如,“1+2”是表达式,“std::count << 1”也是表达式,但是 “std::count << 1;”则是一个语句。基本表达式(Primary Expression)有如下几种:

  • this
  • literals (e.g. 2 or "Hello, world")
  • id-expressions, including
    • suitably declared unqualified identifiers (e.g. n or cout),
    • suitably declared qualified identifiers (e.g. std::string::npos), and
    • identifiers to be declared in declarators
  • lambda-expressions
(since C++11)
  • fold-expressions
(since C++17)
  • requires-expressions
(since C++20)

这里顺带再补充下什么叫对象。

C++面向对象编程(2),c++,c++,move构造/赋值,C++左值与右值

一般来讲,对象要有size,生命周期,类型,值等属性。如下实体就不是对象:引用,函数,枚举项,类型,类的非静态成员,模板,类或函数模板的特化,命名空间,形参包,和 this。

引用(reference)只是已有对象或函数的别名,编译器不必为其分配内存,因此也不存在引用数组,引用的引用,指向引用的指针等。 

1.2 左值与右值 

左值 (lvalue, left value),赋值符号左边的值。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象

右值 (rvalue, right value),右边的值,是指表达式结束后就不再存在的临时对象

而 C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。

纯右值 (prvalue, pure rvalue),纯粹的右值,要么是纯粹的字面量,例如 10true; 要么是求值结果相当于字面量或匿名临时对象,例如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。

需要注意的是,字面量除了字符串字面量以外,均为纯右值。而字符串字面量是一个左值,类型为 const char 数组。

【《C++ Primer》(5th)4.1 基础】

到目前为止,已经有几种我们熟悉的运算符是要用到左值的。

· 赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也仍然是一个左值。· 取地址符(参见2.3.2节,第47页)作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。· 内置解引用运算符、下标运算符(参见2.3.2节,第48页;参见3.5.2节,第104页)、迭代器解引用运算符、string和vector的下标运算符(参见3.4.1节,第95页;参见3.2.3节,第83页;参见3.3.3节,第91页)的求值结果都是左值。

· 内置类型和迭代器的递增递减运算符(参见1.4.1节,第11页;参见3.4.1节,第96页)作用于左值运算对象,其前置版本(本书之前章节所用的形式)所得的结果也是左值。

· 内置类型和迭代器的递增递减运算符(参见1.4.1节,第11页;参见3.4.1节,第96页)作用于左值运算对象,其前置版本(本书之前章节所用的形式)所得的结果也是左值。

上面讲了一大堆,其实能记住的也就是下面几句话:

1. 任何有名字的表达式都是左值(枚举例外);

2. 字面量除了字符串字面量以外,均为纯右值。而字符串字面量是一个左值,类型为 const char 数组。

3. 右值的关键字在”临时“,例如,

(1) 求值结果相当于字面量或匿名临时对象,例如 1+2就是右值

(2) 非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。

value category 是否是临时的 是否占内存 是否能被取地址 是否能被赋值 是否能用来初始化引用
lvalue(left/locator value) N Y Y Y

Y

(例如

double d = 2.0;
double& rd = d;)  
rvalue prvalue(pure right value)
纯右值
Y N 例如 Foo f = f(); 这里的f()就是一个纯右值。 N N

Y

(例如const int& r1 = 1; 1先被materialize,由prvalue转为xvalue)

xvalue(eXpiring value)

将亡值

Y Y 右值不必占内存,并不是说不能占内存。例如f()这个函数是右值也是临时的,但是它必须占内存,因为需要使用f().X访问成员。 N N 仅能初始化const &
因为lvalue和xvalue都占内存,因此有的地方也把lvalue和xvalue统称为glvalue(generalized lvalue,泛化的左值)。
const int& r1 = 1; 1由prvalue转为xvalue

2. moving semantics

2.1 显示绑定

int &&r1 = 1;// 正确
int &&r2 = r1; // 错误,因为r1是左值

需要改成如下 

int&& r2 = std::move(r1); // 正确

用标准库函数std::move()来实现移动语义的自动转换。

2.2 Move constructors

Move constructors - cppreference.com

移动构造的语法规则如下:

C++面向对象编程(2),c++,c++,move构造/赋值,C++左值与右值

可以使用标准库函数std::move()来实现自动转换。

// C++ reference 例子

#include <iomanip>
#include <iostream>
#include <string>
#include <utility>
 
struct A
{
    std::string s;
    int k;
 
    A() : s("test"), k(-1) {}
    A(const A& o) : s(o.s), k(o.k) { std::cout << "move failed!\n"; }
    A(A&& o) noexcept :
        s(std::move(o.s)),       // explicit move of a member of class type
        k(std::exchange(o.k, 0)) // explicit move of a member of non-class type
    {}
};
 
A f(A a)
{
    return a;
}
 
struct B : A
{
    std::string s2;
    int n;
    // implicit move constructor B::(B&&)
    // calls A's move constructor
    // calls s2's move constructor
    // and makes a bitwise copy of n
};
 
struct C : B
{
    ~C() {} // destructor prevents implicit move constructor C::(C&&)
};
 
struct D : B
{
    D() {}
    ~D() {}           // destructor would prevent implicit move constructor D::(D&&)
    D(D&&) = default; // forces a move constructor anyway
};
 
int main()
{
    std::cout << "Trying to move A\n";
    A a1 = f(A()); // return by value move-constructs the target
                   // from the function parameter
 
    std::cout << "Before move, a1.s = " << std::quoted(a1.s)
        << " a1.k = " << a1.k << '\n';
 
    A a2 = std::move(a1); // move-constructs from xvalue
    std::cout << "After move, a1.s = " << std::quoted(a1.s)
        << " a1.k = " << a1.k << '\n';
 
 
    std::cout << "\nTrying to move B\n";
    B b1;
 
    std::cout << "Before move, b1.s = " << std::quoted(b1.s) << "\n";
 
    B b2 = std::move(b1); // calls implicit move constructor
    std::cout << "After move, b1.s = " << std::quoted(b1.s) << "\n";
 
 
    std::cout << "\nTrying to move C\n";
    C c1;
    C c2 = std::move(c1); // calls copy constructor
 
    std::cout << "\nTrying to move D\n";
    D d1;
    D d2 = std::move(d1);
}

运行结果如下 

C++面向对象编程(2),c++,c++,move构造/赋值,C++左值与右值

2.3 Move assignment operator

Move assignment operator - cppreference.com

C++面向对象编程(2),c++,c++,move构造/赋值,C++左值与右值

可以使用标准库函数std::move()来实现自动转换。

#include <iostream>
#include <string>
#include <utility>
 
struct A
{
    std::string s;
 
    A() : s("test") {}
 
    A(const A& o) : s(o.s) { std::cout << "move failed!\n"; }
 
    A(A&& o) : s(std::move(o.s)) {}
 
    A& operator=(const A& other)
    {
         s = other.s;
         std::cout << "copy assigned\n";
         return *this;
    }
 
    A& operator=(A&& other)
    {
         s = std::move(other.s);
         std::cout << "move assigned\n";
         return *this;
    }
};
 
A f(A a) { return a; }
 
struct B : A
{
    std::string s2; 
    int n;
    // implicit move assignment operator B& B::operator=(B&&)
    // calls A's move assignment operator
    // calls s2's move assignment operator
    // and makes a bitwise copy of n
};
 
struct C : B
{
    ~C() {} // destructor prevents implicit move assignment
};
 
struct D : B
{
    D() {}
    ~D() {} // destructor would prevent implicit move assignment
    D& operator=(D&&) = default; // force a move assignment anyway 
};
 
int main()
{
    A a1, a2;
    std::cout << "Trying to move-assign A from rvalue temporary\n";
    a1 = f(A()); // move-assignment from rvalue temporary
    std::cout << "Trying to move-assign A from xvalue\n";
    a2 = std::move(a1); // move-assignment from xvalue
 
    std::cout << "\nTrying to move-assign B\n";
    B b1, b2;
    std::cout << "Before move, b1.s = \"" << b1.s << "\"\n";
    b2 = std::move(b1); // calls implicit move assignment
    std::cout << "After move, b1.s = \"" << b1.s << "\"\n";
 
    std::cout << "\nTrying to move-assign C\n";
    C c1, c2;
    c2 = std::move(c1); // calls the copy assignment operator
 
    std::cout << "\nTrying to move-assign D\n";
    D d1, d2;
    d2 = std::move(d1);
}

运行结果如下:

C++面向对象编程(2),c++,c++,move构造/赋值,C++左值与右值

2.4 实例分析

https://gcc.godbolt.org/z/1q9qcK9Pb

#include <iostream>
#include <cstring>
#include <utility>
using namespace std;

class String {
    char * content;
public:
    String(const char * str = "") {
        if (str) {
            content = new char[strlen(str) + 1];
            strcpy(content, str);
        }
        cout << (void*)content << " : ctor\n";
    }

    String(const String &s) {
        content = new char[strlen(s.content) + 1];
        strcpy(content, s.content);
        cout << (void*)content << " : copy ctor\n";
    }

    String(String &&s) noexcept
        : content(std::exchange(s.content, nullptr)) {   
        cout << (void*)content << " : move ctor\n";
        cout << (bool)s.content << ", " << (void*)s.content << "\n";
    }

    String & operator=(const String &s) {
        if (this == &s)     return *this;
        if (!content || strlen(content) != strlen(s.content)) {
            delete[] content;
            content = new char[strlen(s.content) + 1];
        }
        strcpy(content, s.content);
        cout << (void*)content << " : copy assignment\n";
        return *this;
    }

    String & operator=(String && s) noexcept {
        std::swap(content, s.content);
        cout << (void*)content << " : move assignment\n";
        return *this;
    }

    ~String() {
        cout << (void*)content << " : dtor\n";
        delete[] content;
    }
};

class Msg {
    String content;
    unsigned from, to;
public:
    explicit Msg(const char * content, unsigned from, unsigned to) : 
        content(content), from(from), to(to) {}
};

int main() {
    Msg a("msg", 1, 2);
    Msg b = a;              // copy ctor
    Msg c = std::move(a);   // move ctor
    c = b;                  // copy assign
    c = std::move(b);       // move assign
    return 0;
}

运行结果

C++面向对象编程(2),c++,c++,move构造/赋值,C++左值与右值

// TODO

Quiz

int& r = 1; // 错误,因为1是rvalue,是无法取地址的,因此这样写是非法的

const int& r = 1; // 正确,因为1是先由prvalue转为xvalue,xvalue是占内存的。

再分析下如下“1 = i;”错误的原因

int i;

i = 1;  // 正确

1 = i;  // 错误

REF

1. 《Modern C++ Tutorial: C++ 11/14/17/20 On the Fly》

2. 《C++ Primer》(5th)

3. C++ reference - cppreference.com文章来源地址https://www.toymoban.com/news/detail-683038.html

到了这里,关于C++面向对象编程(2)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C++ -3- 类和对象 (中) | 拷贝构造函数 & 赋值运算符重载

    示例: 拷贝初始化构造函数的作用是将一个已知对象的数据成员值拷贝给正在创建的另一个同类的对象 无穷递归 ? Date(Date d){……} 首先,分析 传值传参的过程 传引用传参 : 没有拷贝 的过程,直接传 传值传参: 内置类型 编译器可以直接拷贝(浅拷贝/值拷贝——一个字节

    2023年04月19日
    浏览(78)
  • 【C++初阶】四、类和对象(构造函数、析构函数、拷贝构造函数、赋值运算符重载函数)

    ========================================================================= 相关代码gitee自取 : C语言学习日记: 加油努力 (gitee.com)  ========================================================================= 接上期 : 【C++初阶】三、类和对象 (面向过程、class类、类的访问限定符和封装、类的实例化、类对象模

    2024年02月05日
    浏览(57)
  • 【C++】类和对象③(类的默认成员函数:拷贝构造函数 | 赋值运算符重载)

    🔥 个人主页: Forcible Bug Maker 🔥 专栏: C++ 目录 前言 拷贝构造函数 概念 拷贝构造函数的特性及用法 赋值运算符重载 运算符重载 赋值运算符重载 结语 本篇主要内容:类的6个默认成员函数中的 拷贝构造函数 和 赋值运算符重载 在上篇文章中我们讲到了类的默认成员函数的

    2024年04月17日
    浏览(43)
  • 【C++初阶】类与对象:6大默认成员函数------拷贝构造和赋值运算符重载

      拷贝构造函数: 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰) ,在用已存在的类类型对象创建新对象时由编译器自动调用。 1. 拷贝构造函数是 构造函数的一个重载形式 ; 2. 拷贝构造函数的 参数只有一个且必须是类类型对象的引用 ,使用传值方式编

    2024年02月03日
    浏览(42)
  • 【C++干货基地】面向对象核心概念与实践原理:拷贝构造函数的全面解读

    🎬 鸽芷咕 :个人主页  🔥 个人专栏 : 《C++干货基地》《粉丝福利》 ⛺️生活的理想,就是为了理想的生活!   哈喽各位铁汁们好啊,我是博主鸽芷咕《C++干货基地》是由我的襄阳家乡零食基地有感而发,不知道各位的城市有没有这种实惠又全面的零食基地呢?C++ 本身作

    2024年03月13日
    浏览(43)
  • 【JAVA杂货铺】一文带你走进面向对象编程|构造方法调用 | 代码块分类| 期末复习系列 | (中3)

    🌈个人主页:  Aileen_0v0 🔥系列专栏: Java学习系列专栏 💫个人格言:\\\" 没有罗马,那就自己创造罗马~\\\" 上次,我们学习了关于Java面向对象编程的 构造方法 ,以及 this 在构造方法/实例化对象中的使用,若有遗忘点击👉🔗 本节我们`来学习,代码块,tostring以及继承  那还等什么

    2024年02月04日
    浏览(52)
  • C++面向对象编程(2)

    目录 一. 问题引入 二. 右值引用 1. lvalue/rvalue/prvalue/xvalue 1.1 表达式与对象的概念 1.2 左值与右值  2. moving semantics 2.1 显示绑定 2.2 Move constructors 2.3 Move assignment operator 2.4 实例分析 // TODO Quiz REF 本章简单介绍下move语义的“来龙去脉”。 如下,先来看看拷贝赋值的一个过程。

    2024年02月10日
    浏览(40)
  • C++ 递归与面向对象编程基础

    递归是一种使函数调用自身的技术。这种技术提供了一种将复杂问题分解为简单问题的方法,从而更容易解决问题。 递归可能有点难以理解。理解其工作原理的最佳方法是通过实验来尝试。 将两个数字相加很容易做到,但将一系列数字相加就更复杂了。在下面的示例中,通

    2024年04月16日
    浏览(37)
  • 【C++】面向对象编程(二)面向对象的编程思维:virtual虚拟调用、继承、protected成员、派生类与基类

    默认情形下,成员函数的解析都是编译时静态进行。如果要让成员函数的解析在程序运行时动态进行,需要在成员函数的声明前加上virtual: 虚函数的作用: 用基类的指针指向不同的派生类的对象时,基类指针调用其虚成员函数,会调用 真正指向对象的成员函数 ,而

    2024年02月07日
    浏览(44)
  • C++移动构造与std::move()

    如下程序所示: 程序定义了一个MyString类,其中构造函数和拷贝构造函数需要对传进来的字符串开辟空间并复制内容,另外一个Entity类含有一个MyString成员,并在初始化时复制传入的MyString对象。主程序Main中以常量字符串构造一个entity示例。 运行程序会发现,MyString构造了一

    2024年02月04日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包