C++ 小知识

2021-10-3 About 4 min

# lvalue & rvalue

简单来说

  • 左值(lvalue):是变量,占用某一块确定的内存。
  • 右值(rvalue):是字面量或临时量。
int i = 10; // i 是左值, 10 是右值(字面量) 
int a = i; // a, i 都是左值
int b = a + i; // b 是左值,a+i 是右值(临时量)

int GetInt(){ int a = 7;return a;}    // 返回值是右值
int& GetInt(int& a){ a = 7;return a;} // 返回值是左值

// 有一些方法如下
void GetVal1(int a){cout << a;}
void GetVal2(int& a){cout << a;}
void GetVal3(const int& a){cout << a;}
void GetVal4(int&& a){cout << a;}

GetVal1(a+i); // 正常
GetVal2(a+i); // 编译错误,非常量引用的初始值必须为左值
GetVal3(a+i); // 正常。常量引用的初始值可以是左值,也可以是右值 ==> 所以字符串参数常常声明为常量引用。
GetVal4(a+i); // 正常。&& 表示右值引用。
GetVal4(b); // 编译错误,无法将右值引用绑定到左值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

有啥好处呢?好处在于可以通过重载函数,区分出传入的参数是不是临时的,因此可以做一些特殊处理。

// 可以接受左值和右值
void PrintName(const string& name){cout << "[lvalue]" << name;}
// 对于右值,有特殊重载
void PrintName(string&& name){cout << "[rvalue]" << name;}
1
2
3
4

# 移动语义(move semantics)

先看一个例子

class String
{
public:
    String() = default;
    String(const char* str)
    {
        printf("Created!\n");
        m_Size = strlen(str);
        m_Data = new char[m_Size];
        memcpy(m_Data, str, m_Size);
    }

    String(const String& other)
    {
        printf("Copied!\n");
        m_Size = other.m_Size;
        m_Data = new char[m_Size];
        memcpy(m_Data, other.m_Data, m_Size);
    }

    ~String()
    {
        printf("Deleted!\n");
        delete m_Data;
    } 

    void Print()
    {
        for (int i = 0; i < m_Size; ++i)
            printf("%c", m_Data[i]);
        printf("\n");
    }
private:
    int m_Size;
    char* m_Data;
};

class Entity
{
public:
    Entity() = default;
    Entity(const String& name) : m_Name(name) {}
    void PrintName(){ m_Name.Print(); }
private:
    String m_Name;
};

int main() {
    Entity entity("Sekiro");
    entity.PrintName();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

执行结果为:

Created!
Copied!
Deleted!
Sekiro
1
2
3
4

这意味着,在Entity(String name)的初始化列表中,为右值"Sekiro"调用String的相应构造函数,再调用String的拷贝构造函数,将其值传入Entity.m_Name,最后销毁保存"Sekiro"的临时对象。 一个简单的赋值操作,竟调用了两次构造函数,在堆上分配了两次内存。问题很大🤔。

解决方法是,对于传入的右值做特殊处理,在构造出来临时对象后,直接赋值,不要再调用拷贝构造函数。

// 在String中添加
String(String&& other) noexcept
{
    printf("Moved!\n");
    m_Size = other.m_Size;
    m_Data = other.m_Data;

    // 临时变量会被销毁,所以需要防止析构时删除数据
    other.m_Data = nullptr;
    other.m_Size = 0;
}

// 在 Entity 中修改 std::move(name) 相当于 (String&&) name
Entity(const String& name) : m_Name(std::move(name)){}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

现在执行结果为

Created!
Moved!
Deleted!
Sekiro
1
2
3
4

节省了一次堆内存分配。

# inline

直接在类中声明并实现的方法是隐式的inline函数,不需要再加inline前缀。

# constexpr

指明变量或函数的值可以出现在常量表达式中。通过添加constexpr来声明的变量或函数,可以在编译期进行计算。

  • literal type: 标量类型,引用类型,以及前两种的数组类型。(C++20还可能有更多)
  • constexpr 变量,函数返回值和参数类型必须是literal type。
  • inline是将函数体直接搬过来,减少函数调用的开销。constexpr是直接将编译器可计算的结果算出来,来减少调用,执行函数的开销。
#include<iostream>
using namespace std;
  
constexpr long int fib(int n)
{
    return (n <= 1)? n : fib(n-1) + fib(n-2);
}
  
int main ()
{
    // value of res is computed at compile time. 执行的非常快。
    const long int res = fib(30); 
    cout << res;
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 可变参数

  • 在 stdarg.h 中
  • va_list 为存储可变参数的类型。即function中的 ...
  • va_start() 宏。有两个参数,第一个为va_list类型的变量,第二个为可变参数...的前一个参数。用于初始化可变参数列表。
  • va_arg() 宏。有两个参数,第一个为va_list类型的变量,第二个为用于接受可变参数的类型。用于获取可变参数列表中的下一个参数。(PS:printf的format string就是用于确定va_arg需要处理的变量类型。)
  • va_end() 宏。一个参数,为va_list类型的变量。用于清理va_list

示例:

#include <stdarg.h>
double avg(int num, ...)
{
    va_list args;
    double sum = 0;
    va_start(args, num);
    for (int i = 0; i < num; ++i)
    {
        sum += va_arg(args, double);
    }
    va_end(args);
    return sum / num;
}

int main(int argc, const char* argv[])
{
    printf("average = %4.2f\n", avg(3, 1.2, 3.4, 5.6));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

PS: float在通过...传递时,会被提升为double。如果用float接收,程序会abort。

Last update: 2021年10月3日 10:18