引用和拷贝构造函数

01 引用

1. 引用的基本语法

引用:引用即别名,是一个已存在的变量(对象)的另外一个名字,对引用的操作与对变量直接操作完全一样

        主要用于 函数传参 函数返回 —— 避免产生临时对象,提高运行效率

语法格式:

        类型标识符 &别名 = 已存在的变量;// 声明一个引用,同时初始化,此处的&称为引用声明符,声明这个变量是一个引用类型

       

注意:权限只能降,不能升

int a = 1;
const int b = 1;

int &aa = a; // OK
int bb = b; // ERROR

const int &aaa = a; // OK
const int &bbb = b; // OK 

引用不占用存储空间(不会分配新的空间,是已有空间的另一个名字)

例如:

        int num = 100;

        int &rn = num; // rn是num别名,num和rn指代的是同一个对象


引用和目标变量的地址是一样的,对引用的修改就是对目标变量的修改

注:

        (1) &在此不是求地址运算,而是起标识作用

        (2) 类型标识符是指目标变量的类型。引用的类型必须和其所绑定的变量的类型相同

        (3) 声明引用时,必须同时对其进行初始化

        (4) 引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,且不能再把该引用名作为其他变量名的别名

        (5) 声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元(相当于把引用名绑定到已经拥有的一块存储空间上)

                故:对引用求地址,就是对目标变量求地址,&ra与&a相等

        (6) 建立数组的引用

                语法:类型 (&引用名)[数组中元素数量] = 数组名;


特点:

        在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题(避免产生临时对象)

        使用引用传参,简洁高效

        主要用于 函数传参 函数返回 —— 避免产生临时对象,提高运行效率

#include <iostream>

using namespace std;

// 自定义的类型
class Test {
    private:
        int m_a = 123; // c++11以后,类中的成员变量可以初始化
    public:
        // 构造函数
        Test() {
            cout << "无参构造函数" << endl;
            m_a = 100;
        }
        Test(int a) {
            cout << "带参构造函数" << endl;
            m_a = a;
        }
        void show() const {
            cout << "m_a = " << m_a << endl;
        }
        void set(int a) {
            m_a = a;
        }
        // 析构函数
        ~Test() {
            cout << "析构函数" << endl;
        }
};

int main() {

    int num = 100;
    int &rn = num; // rn是num的别名,num和rn指代的是同一个对象

    rn = 1024; // 对rn的操作就是对num的操作
    cout << "num = " << num << endl;
    cout << "rn = " << rn << endl;

    cout << "num的地址是:" << &num << endl;
    cout << "rn的地址是:" << &rn << endl;

    int &rn2 = rn; // rn2也是num的别名,rn2、rn、num表示的都是同一个对象

    int b = 1234;
    rn2 = b; // 不是说rn2成为了b的别名,而是将b的值赋值给rn2(num)
            // 对rn2的操作就是对num的操作
    // &rn2 = &b; // 错误,引用在定义时必须初始化,并且不能改变引用的指向
                // &rn2表示的是一个地址编号,引用一旦绑定了对象,就不能成为其他对象的别名
    
    // double &rn3 = num; // 错误,引用类型和引用对象类型必须一致
    // int &rn4; // 错误,引用在定义时必须初始化

    cout << "============================" << endl;
    
    Test t{1024}; // 调用带参构造函数
    Test &rt = t; // rt就是t对象的别名,t和rt指代的是同一个对象
    rt.show(); // rt.show()就是t.show()
    rt.set(2000); // rt.set(2000)就是t.set(2000)
    t.show();

    return 0;
}

2. 常引用

常引用声明方式:

        const 类型标识符 &引用名 = 目标变量名;

       

        用这种方式声明的引用,不能通过引用名对目标变量的值进行修改,从而使引用的目标成为const,达到了引用的安全性(避免通过引用修改目标变量,提高程序的健壮性)

        如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被修改,就应使用常引用

#include <iostream>

using namespace std;

// 自定义的类型
class Test {
    private:
        int m_a = 123; // c++11以后,类中的成员变量可以初始化
    public:
        // 构造函数
        Test() {
            cout << "无参构造函数" << endl;
            m_a = 100;
        }
        Test(int a) {
            cout << "带参构造函数" << endl;
            m_a = a;
        }
        void show() const {
            cout << "m_a = " << m_a << endl;
        }
        void set(int a) {
            m_a = a;
        }
        // 析构函数
        ~Test() {
            cout << "析构函数" << endl;
        }
};

int main() {

    int num = 100;
    
    const int &rn = num; // 常引用,避免通过rn2去修改num的数据
                        // 只能做右值,不能做左值
    int a = rn; // 只能做右值,不能做左值
    // rn = 200; // 错误,不能通过rn2去修改num的数据
    cout << "rn = " << rn << endl; // 100
    num = 111;
    cout << "rn = " << rn << endl; // 111

    const int b = 1024; // b是一个常量,是不可变的
    // int &rb = b; // 错误,权限只能降,不能升
    const int &rb = b; // 正确
    // rb = 200; // 错误,rb是常量,不可变

    // 【权限只能降,不能升】
    // const引用可以绑定到const对象,也可以绑定到非const对象
    // 普通引用只能绑定到非const对象,不能绑定到const对象

    cout << "======================" << endl;

    Test t{1024};
    const Test &rt = t;
    // rt.set(200); // 错误,不同通过const引用去调用非const函数
    rt.show(); // 正确,const引用可以调用const函数

    cout << "======================" << endl;

    double c = 3.14;
    // int &rc = (int)c; // Error (int)c 是一个临时对象,临时变量只能做右值,不能做左值
                      // 临时变量不能绑定到普通引用
    const int &rc1 = c;
    const int &rc2 = (int)c;

    cout << "c = " << c << endl; // 3.14
    cout << "rc1 = " << rc1 << endl; // 3
    cout << "rc2 = " << rc2 << endl; // 3


    return 0;
}

3. 引用作为函数参数

写一个函数,交换两个变量的值

void swap1(int n1, int n2) { // int n1 = num1, int n2 = num2;
    int temp = n1;
    n1 = n2;
    n2 = temp;
}

上面的写法不能实现交换的功能,交换的是形式参数n1和n2(形不改实)
void swap2(int *p1, int *p2) { // int *p1 = &num1, int *p2 = &num2;
    int pt;
    pt = *p1;
    *p1 = *p2;
    *p2 = pt;
}

把需要交换的对象的地址传入函数中,在函数中通过对象的地址编号实现间接访问,能够实现交换
void swap3(int *p1, int *p2) {
    int *pt = p1;
    p1 = p2;
    p2 = pt;
}

不可以,在函数里面仅仅是交换了形式参数p1和p2的指向
void swap3(int &r1, int &r2) { // int &r1 = num1, int &r2 = num2;
	int tmp  = r1;
	r1 = r2;
	r2 = tmp;
}

可以,r1成为了实际参数num1的别名,r2成为了实际参数num2的别名
在函数里面操作r1和r2就是操作主函数的num1和num2

使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好

4. 引用作为函数返回值

要以引用返回函数值,则函数定义时要按以下格式:

        返回类型标识符  &  函数名(形参列表及类型说明) {

                函数体;

        }

        
        重点:返回引用就相当于返回本身!!!


说明:

        (1) 以引用返回函数值,定义函数时需要在函数名前加&
        (2) 用引用返回一个函数值的最大好处是,
在内存中不产生被返回值的副本

#include <iostream>

using namespace std;

float temp;

float fn1(float r) {
    temp = r * r * 3.14;
    return temp;
} 
  
float & fn2(float r) { // &说明返回的是temp的引用,换句话说就是返回temp本身
    temp = r * r * 3.14;
    return temp;
}

int main() {
    float a = fn1(5.0); // case 1:返回值

    // float &b = fn1(5.0); // case 2:用函数的返回值作为引用的初始化值
        /* ERROR 此时b引用的是fn1(5.0)的临时变量,临时变量在函数调用结束后被销毁
            临时变量只能做右值,不能做左值
            [Error] invalid initialization of non-const reference of type 
            'float&' from an rvalue of type 'float'
            (有些编译器可以成功编译该语句,但会给出一个warning) 
        */

    // const float &b = fn1(5.0); 
        // 可以,但是不这么用,因为b引用的只是临时变量,临时变量在函数调用结束后被销毁

    float c = fn2(5.0); // case 3:返回引用

    float & d = fn2(5.0); // case 4:用函数返回的引用作为新引用的初始化值

    cout << a << endl; // 78.5
    // cout << b << endl; // 78.5
    cout << c << endl; // 78.5
    cout << d << endl; // 78.5

    return 0;
}
case1:用返回值方式调用函数
case2:用函数的返回值初始化引用的方式调用函数

case3:用返回引用的方式调用函数
float & fn2(float r) { // &说明返回的是temp的引用,换句话说就是返回temp本身
     temp = r * r * 3.14;
     return temp;
}

case4:用函数返回的引用作为新引用的初始化值的方式来调用函数

02 拷贝构造函数(copy-constructors)

拷贝构造函数还是构造函数(是一个特殊的构造函数),名字和类名相同
主要用于利用一个已有对象初始化一个新对象,新对象和已有对象"一模一样"

        类名  对象名{参数};        调用普通的构造函数

        类名  对象名 = 同类的已有对象;   调用拷贝构造函数(拷贝构造)    显式转换

        类名  对象名{同类的已有对象};       调用拷贝构造函数(拷贝构造)    显式转换
        ================================================================

        类名  对象名{参数};        调用普通的构造函数

        对象名 = 同类的已有对象;        赋值运算符重载函数(拷贝赋值)

拷贝构造函数是一种特殊的构造函数,它在创建对象的时候,是使用同一类中之前创建的对象来初始化新创建的对象
即用一个已经存在的对象创建一个新对象,此时会调用拷贝构造函数,新对象和原有对象"一模一样"

如果一个类中没有显示的声明拷贝构造函数,编译器会自动生成一个拷贝构造函数
自动生成的拷贝构造函数,函数体不为空,执行对象的逐成员赋值(浅拷贝)!!!
如果成员中有资源,就会出现多个对象共同拥有同一个资源,这是有问题的!!!

注意:

        1. 自动生成的拷贝构造函数默认执行的是逐成员赋值,称为"浅拷贝"

        2. 默认的拷贝构造函数不会处理静态成员

语法形式:

构造函数名(const 类名 &形参名) {
    函数体
}
        
依然是构造函数,函数名和类名相同
参数是本类类型的常引用!!!

拷贝构造函数调用的时机:

        1. 使用一个已有对象创建一个新对象,此时会调用拷贝构造函数,新对象和原有对象"一模一样"

Student s{"laowang", 25};
s.show();

Student s1 = s; // 调用拷贝构造函数创建s1,s1和s一模一样
// or
// Student s1{s}; // 调用拷贝构造函数创建s1,s1和s一模一样
s1.show();
=========================================================
Student s2{};
s2 = s1; // 赋值运算符重载函数(拷贝赋值)

        2. 一个对象作为函数参数,以值传递的方式传入函数内部

// 在函数调用的时候会调用拷贝构造函数,根据实际参数创建一个stu对象(局部对象)
// 在函数结束后会析构
void foo(Student stu) {
    stu.show();
    stu.setAge(100); // 设置的值是局部对象stu的值
}

        3. 一个对象作为函数的返回值,以值的方式返回(不能返回一个局部对象的地址和引用)

// 函数返回了一个Student对象
// 产生了编译器优化,可以关闭优化选项
// 优化为直接把临时对象构造到本来要赋值的空间中
// g++ 04拷贝构造函数.cpp  -fno-elide-constructors
        
Student foo4() {
    // Student s{"cxk", 25};
    // return s;
    // =====>
    return Student{"cxk", 25}; // 编译器认为临时对象是一个常量
                                // 临时变量只能做右值,不能做左值
}

         前面第1种会显示的调用拷贝构造函数,第2、3种会隐式调用拷贝构造函数


因为编译器优化,有时候会省略拷贝构造函数:

        下面的情况中,要求编译器省略对象的拷贝构造函数,直接将对象构造到他们本来要拷贝到的存储空间!!!

        (1) 如果被拷贝对象的生命周期短暂,拷贝完成后自动销毁,编译器可能会对这种情况进行优化(省略),省略拷贝构造函数的调用

                Test Fun() {

                        Test t{10};

                        return t;

                }

                

        (2) 在变量的初始化中,当初始化表达式与变量的类型为同一类型的临时对象(常量对象)时
                例子:Student s = Student{"laowang", 25};

                        // 使用一个临时对象去初始化s对象,优化为直接把临时对象构造到s中

     
  (3) 在return语句中,操作的对象是与函数返回值类型为同一类型的临时对象时
                return Student{"laowang", 25}; 


注意:
(1) 拷贝构造函数要么不存在(编译器会自动生成)

        要么存在且必须可访问


(2) 拷贝构造函数的参数是本类类型的常引用,不能是值传递,为什么?

        引用:

                为了防止递归的调用拷贝构造函数

                // 拷贝构造函数 --->根据已有对象产生副本---->拷贝构造函数----->根据已有对象产生副本--->


        const:

                从语义上来说,在拷贝构造函数中不应该修改原对象中的数据,所以是const

                

                如果不是const,不能从一个const对象拷贝生成一个新对象
                const Student s;
                Student s1{s}; // 如果不是const修饰,报错
                          // Student(Student &oth); -------- 拷贝构造函数
                          // Student &oth = s; // s是const修饰的,要求oth也是const修饰的
               
所有的临时对象,都认为是const修饰的!!!,只能当右值,不能当左值

                

                权限只能降,不能升

                         const引用可以绑定到const对象,也可以绑定到非const对象
                         非const引用只能绑定到非const对象

(3) 什么时候应该显示的声明拷贝构造函数呢? 为什么?

        当构造函数中开辟了空间或者打开了资源的时候(当成员变量中有任何指针的时候),一定需要显示的声明拷贝构造函数
        当必须写构造函数的时候,一般就需要写拷贝构造函数

        

(4) 默认的拷贝构造函数执行的是逐成员赋值,是浅拷贝

        造成多个对象共用"资源"

浅拷贝:

        所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员(指针),默认的拷贝构造函数不会处理静态成员,那么浅拷贝就会出问题了

03 练习

1. 在开发板上面显示矩形的时候,使用的是指针传递参数

        void Rect::show_screen(Screen *s); // 利用地址,在函数内部访问屏幕对象,没有副本对象

        能不能修改代码,直接使用值传递参数???

        

        void Rect::show_screen(Screen *s);
        void Rect::show_screen(Screen &s); // 函数中的s仅仅是一个实参的别名,也不会产生副本对象

        ========>

        void Rect::show_screen(Screen s);

                                // 值传递,会调用拷贝构造函数生成一个屏幕对象的副本
                                // 因为没有实现拷贝构造函数
                                // 副本对象和源对象共用资源,副本对象在函数结束的时候会析构
                                // 导致原对象资源失效!!!
                                ======>
                               实现屏幕类的拷贝构造函数 ----> 深拷贝

screen.hpp

#ifndef __SCREEN_HPP__
#define __SCREEN_HPP__

#include "size.hpp"
#include "rect.hpp" // 头文件不能相互包含,相互保护就会递归展开
#include "color.hpp"
#include "point.hpp"

#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <iostream>
#include <errno.h>
#include <sys/mman.h>
#include <unistd.h>

using namespace std;

class Rect; // 前向声明,防止头文件递归展开

// 屏幕类的描述
class Screen {
    private:
        // 屏幕的尺寸
        Size m_size;
        char m_name[32]; // 路径名
        int m_fd; // 文件描述符
        int *m_plcd; // 指向屏幕的起始地址
    public:
        // 初始化(构造函数)
        Screen(const char *name = "/dev/fb0", int w = 800, int h = 480);
        
        // 拷贝构造函数(深拷贝),一般屏幕设置为单例模式,只能有一个对象
        Screen(const Screen &s);

        // 关闭(析构函数)
        ~Screen();
        // 清屏
        void clearScreen(int color = 0x00ffffff);
        void clearScreen(Color c);
        // 画点
        void drawPoint(int x, int y, int color);
        void drawPoint(Point p, Color c);

        // 屏幕有一个行为是显示矩形
        void drawRect(Rect r);
};

#endif

screen.cpp

#include "screen.hpp"

// 初始化(构造函数)
Screen::Screen(const char *name, int w, int h) {
    m_size.setSize(w, h);
    strcpy(m_name, name);
    m_fd = open(m_name, O_RDWR);
    if (-1 == m_fd) {
        perror("open lcd failed");
        exit(0);
    }
    m_plcd = (int *)mmap(NULL, w * h * 4, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
    if (m_plcd == MAP_FAILED) {
        perror("mmap lcd failed");
        ::close(m_fd); // 调用无名空间中的close
        exit(0);
    }
}

// 拷贝构造函数(深拷贝),一般屏幕设置为单例模式,只能有一个对象
Screen::Screen(const Screen &s) {
    // m_size = s.m_size; // 赋值运算符重载
                        // 对象也可以像普通类型一样赋值,但是有风险!!!
    m_size.setSize(s.m_size.getW(), s.m_size.getH()); // 调用成员函数
    strcpy(m_name, s.m_name);
    
    // 新对象单独打开文件
    m_fd = open(m_name, O_RDWR);
    if (-1 == m_fd) {
        perror("open lcd failed");
        exit(0);
    }
    // 新对象单独映射内存
    m_plcd = (int *)mmap(NULL, m_size.getW() * m_size.getH() * 4, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
    if (m_plcd == MAP_FAILED) {
        perror("mmap lcd failed");
        ::close(m_fd); // 调用无名空间中的close
        exit(0);
    }
}

// 关闭(析构函数)
Screen::~Screen() {
    // 解映射
    munmap(m_plcd, m_size.getW() * m_size.getH() * 4);
    // 关闭
    ::close(m_fd); // 调用无名空间中的close
}

// 清屏
void Screen::clearScreen(int color) {
    // 把所有的点显示为指定的颜色
    int w = m_size.getW();
    int h = m_size.getH();
    for (int y = 0; y < h; y++) { // 遍历每一行
        for (int x = 0; x < w; x++) { // 遍历每一列
            drawPoint(x, y, color);
        }
    }
}

void Screen::clearScreen(Color c) {
    int color = c.getColor();
    clearScreen(color);
}

// 画点
void Screen::drawPoint(int x, int y, int color) {
    if (x < 0 || x >= m_size.getW() || y < 0 || y >= m_size.getH()) {
        // cout << "point out of the Screen!" << endl;
        return;
    }
    *(m_plcd + m_size.getW() * y + x) = color;
}

void Screen::drawPoint(Point p, Color c) {
    drawPoint(p.getX(), p.getY(), c.getColor());
}

// 屏幕有一个行为是显示矩形
void Screen::drawRect(Rect r) {
    // 把矩形形容的所有的点都显示为矩形中的颜色
    int x0 = r.getPoint().getX(); // 矩形的起点横坐标
    int y0 = r.getPoint().getY();

    int w = r.getSize().getW();
    int h = r.getSize().getH();

    int c;
    r.getColor(&c);

    for (int y = y0; y < y0 + h; y++) {
        for (int x = x0; x < x0 + w; x++) {
            drawPoint(x, y, c);
        }
    }
}

2. 完成图片类的拷贝构造函数

3. 尝试写一个目录类型,实现的属性和行为自己考虑

        要求可以获取当前目录的文件列表

        or

        返回一个链表,链表中保存了当前目录中所有的文件名

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值