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
返回一个链表,链表中保存了当前目录中所有的文件名