C++中&和&&的相关笔记

发布时间 2023-03-22 21:17:27作者: 醉曦

1. 引言

C++中&有三种用途,而&&有两种用途

2. &的作用

2.1 位运算

C++中的位运算十分高效,数据分段时经常用到!

例如,统计一个数字中有多少位是1的个数,代码如下:

int count(int x) {
    int res = 0;
    while(x) {
        if (x & 1)
            res++;
        x = x >> 1;
     }
     
     return res;
}

获取n的第k位数字:

n >> k & 1;

返回n的最后一位1:

lowbit(n) = n & -n;

2.2 取地址

&可以用于获取函数、变量地址等;

int a = 10;
int *pa = &a; // pa指针指向a的地址

void isEqual(int a, int b){
    return a == b;
}

void (*func)(int, int);

func pfun = &isEqual;// 声明和初始化函数指针地址

2.3 引用

常用于函数传参,临时变量引用等

避免引起拷贝

std::vector<std::vector<int> > vec(10);

auto &pv = vec[0];// 引用数组的值,避免复制

3. &&的用途

3.1 逻辑运算符AND

用于条件判断

3.2 右值引用

C++11之后才有的效果,即移动语义

3.2.1 背景知识

函数的返回值是一个对象的话,则可能需要对函数中的局部对象进行拷贝。若对象很大,则会影响程序的效率。那么如何避免对象返回的拷贝呢?

拷贝构造函数的执行时机:

  1. 使用一个对象给另一个对象进行初始化
  2. 对象为函数返回值,以值的方式返回;
  3. 对象作为函数参数,以值传递方式传递给函数;

#include <iostream>
using namespace std;
/*
注意点:
有些编译器出于程序执行效率的考虑,编译的时候进行了优化,
    函数返回值对象就不用复制构造函数初始化了,但这并不符合 C++ 的标准
*/
class A {
public:
    A();
    A(char _c): c(_c) {cout << "构造函数执行" << endl;}
    A(const A& rhs){
        cout << " 拷贝构造函数执行" << endl;
        c = rhs.c;
    }
    virtual ~A() {
        cout << "  析构函数执行" << endl;
    }
    A clone() const{
        A a = *this; // 拷贝构造函数
        return a;//  析构函数,拷贝构造函数
    }
private:
    char c;
};
int main() {
    A a = A('b');// 构造函数,=
    a.clone();
    return 0;
}

程序输出结果:

# g++ 10.0+
构造函数执行
 拷贝构造函数执行
  析构函数执行
  析构函数执行
  
# vc++ 
构造函数执行
 拷贝构造函数执行
 拷贝构造函数执行
  析构函数执行
  析构函数执行

有些编译器出于程序执行效率的考虑,编译的时候进行了优化函数返回值对象就不用复制构造函数初始化了,但这并不符合 C++ 的标准

拷贝构造函数和赋值运算符的联系和区别:

  1. 两者都可以将一个对象的值复制给另外一个对象;
  2. 拷贝构造函数使用传入的对象生成一个新对象;而赋值运算则只是将值复制给另外一个对象,没有新对象的产生;
  3. 判断依据:看是否有新的对象生成

3.2.2 左值和右值

C++如何判断左值还是右值:

  1. 左值一般是可寻址的变量,右值一般不可寻址

    左值即我们之前使用那些变量,而右值则是一些常量或无名临时对象

  2. 左值有持久性,右值有短暂性

  3. 左值符号&,右值符号&&

3.2.3 移动构造函数和移动赋值函数

移动和拷贝是相对的,移动类似于文件移动,从一个位置移到另外一个位置,拷贝则是数据的赋值,类似于文件复制

实现对象复制可以使用拷贝构造函数和赋值运算符;

实现转移语义,则可以定义转移构造函数转移赋值操作符

对于右值的拷贝和赋值会调用转移构造函数和转移操作符,若转移构造函数和转移操作符,则拷贝函数或赋值运算将会被调用;

移动构造函数的构建
对于类A,定义如下:

class A{
public:
    explicit A(size_t len): _length(len), _data(new int[len]) {
        std::cout << "构造函数" << std::endl;
    }
    
    ~A() {
        std::cout << "析构函数" << std::endl;
    }
    
    A(const A& other): _length(other._length), _data(new int[other._length]) {
        std::cout << "拷贝构造函数" << std::endl;
        std::copy(other._data, other._data + _length, _data);
    }
    
    // 赋值运算
    A & operator=(const A& other) {
        std::cout << "赋值运算=" << std::endl;
        
        if (this != &other) {
            delete[ ]_data;
            
            _length = other._length;
            _data = new int[_length];
            std::copy(other._data, other._data + _length, _data);
        }
        
        return *this;
    }
    
    int length() const {
        return _length;
    }

private:
    size_t _length;
    int *_data;
};

为上述的类添加移动构造函数

  1. 定义一个空的构造函数方法,参数为一个对类类型的右值引用
  2. 在移动构造函数中,将源对象的类数据成员添加到构造对象中;
  3. 将源对象的数据成员恢复默认值,防止析构函数多次释放资源
// 1 空的构造函数
A(const A&& rhs) : _length(0), _data(nullptr) {
    // 2 数据成员赋值
    _data = rhs._data;
    _length= rhs._length;
    
    // 3 设置默认值,防止多次析构
    rhs._length = 0;
    rhs._data = nullptr;
}

添加移动赋值运算:

  1. 定义一个空的赋值运算符,该运算符的参数为一个右值引用
  2. 避免将对象赋值给自身;
  3. 条件语句中,要将其赋值的对象中释放所有内存然后执行移动构造函数中的步骤2和3
  4. 返回当前对象的引用;
// 1
A & operator=(const A&& rhs) {
    if (*this != rhs) {
        delete[] _data;// 2
        
        _data = rhs._data, _length = rhs._length; // 3
        
        rhs._data = nullptr, rhs._length = 0; // 3
    }
    
    return *this;//4
}

注意:

如果为你的类同时提供了移动构造函数和移动赋值运算符,则可以编写移动构造函数来调用移动赋值运算符,从而消除冗余代码。

// 移动构造函数
A(A&& other) noexcept
   : _data(nullptr)
   , _length(0)
{
   *this = std::move(other);
}

std::move() 将左值转换为右值