目录
1. 省略号(…)用法
1. 变参函数
2. 变参模板类–C++11新特性
3. 变参模板函数–C++11新特性
C++模板(第二版)笔记之第四章:变参模板
c++11增加的变参数模板,今天总算整明白了
C++11:变参函数模板、变参类模板、模板模板参数
4. 变参宏
可变参数宏
C/C++ #运算符、##运算符、变参宏 …和_ VA_ARGS _
5. 折叠表达式
C++17之折叠表达式
跟我学c++中级篇——c++17中的折叠表达式
2. goto语句
3. C语言内存操作函数
memcpy()
4. std::is_same()判断两个类型是否相同
5. decltype关键字
6. 左值、右值及其引用
左值和右值定义
网上关于左值、右值介绍的文章有很多,大多数文章都通过表达式能够出现在赋值号=的左、右来定义左右值,这样的定义对于读者不好理解,也没有表达出左右值的真实区别和意义。
文章C++左值(引用)和右值(引用)详解关于左右值的定义如下:
左值:在内存有确定存储地址、有变量名,表达式结束依然存在的值,简单来说左值就是非临时对象。
右值:就是在内存没有确定存储地址、没有变量名,表达式结束就会销毁的值,简单来说右值就是临时对象。
int a = 0; // 在这条语句中,a 是左值,0 是临时值,就是右值。
该定义从对象的临时性角度定义左右值,能够表达出左右值的真实区别和存在的意义。
左值可以分为两类:非常量左值和常量左值;
int a=10; // a 为非常量左值(有确定存储地址,也有变量名)
const int a1=10; //a1 为常量左值(有确定存储地址,也有变量名)
const int a2=20; //a2 为常量左值(有确定存储地址,也有变量名)
同理,右值也可以分为两类:非常量右值和常量右值。
int a=10; // 10 为常量右值 (原文中说这事非常量右值,应该正好写反了)
const int a1=10;
const int a2=20;
a1+a2 // (a1+a2) 为非常量右值
左值引用和右值引用
左值引用:其实就是绑定到左值的引用,通过&来获得左值引用;
int a=10; //非常量左值(有确定存储地址,也有变量名)
const int a1=10; //常量左值(有确定存储地址,也有变量名)
const int a2=20; //常量左值(有确定存储地址,也有变量名)
//非常量左值引用
int &b1=a; //正确,a是一个非常量左值,可以被非常量左值引用绑定
int &b2=a1; //错误,a1是一个常量左值,不可以被非常量左值引用绑定
int &b3=10; //错误,10是一个常量右值,不可以被非常量左值引用绑定
int &b4=a1+a2; //错误,(a1+a2)是一个非常量右值,不可以被非常量左值引用绑定
//常量左值引用
const int &c1=a; //正确,a是一个非常量左值,可以被非常量右值引用绑定
const int &c2=a1; //正确,a1是一个常量左值,可以被非常量右值引用绑定
const int &c3=a+a1; //正确,(a+a1)是一个常量右值,可以被常量右值引用绑定
const int &c4=a1+a2; //正确,(a1+a2)是一个非常量右值,可以被非常量右值引用绑定
总结:非常量左值引用只能绑定到非常量左值上;常量左值引用可以绑定到非常量左值、常量左值、非常量右值、常量右值等所有的值类型。
右值引用:其实也是绑定到右值的引用,通过&&(中间无空格)来获得右值引用。
int a=10; //非常量左值(有确定存储地址,也有变量名)
const int a1=20; //常量左值(有确定存储地址,也有变量名)
const int a2=20; //常量左值(有确定存储地址,也有变量名)
//非常量右值引用
int &&b1=a; //错误,a是一个非常量左值,不可以被非常量右值引用绑定
int &&b2=a1; //错误,a1是一个常量左值,不可以被非常量右值引用绑定
int &&b3=10; //正确,10是一个非常量右值,可以被非常量右值引用绑定
int &&b4=a1+a2; //错误,(a1+a2)是一个常量右值,不可以被非常量右值引用绑定
//常量右值引用
const int &&c1=a; //错误,a是一个非常量左值,不可以被常量右值引用绑定
const int &&c2=a1; //错误,a1是一个常量左值,不可以被常量右值引用绑定
const int &&c3=a+a1; //正确,(a+a1)是一个非常量右值,可以被常量右值引用绑定
const int &&c4=a1+a2; //正确,(a1+a2)是一个常量右值,不可以被常量右值引用绑定
总结:非常量右值引用只能绑定到非常量右值上;常量右值引用可以绑定到非常量右值、常量右值上。
从上述可以发现,常量左值引用可以绑定到右值上,但右值引用不能绑定任何类型的左值,若想利用右值引用绑定左值该怎么办呢?
C++11中提供了一个标准库move函数获得绑定到左值上的右值引用,即直接调用std::move告诉编译器将左值像对待同类型右值一样处理,但是被调用后的左值将不能再被使用。
std::move()函数举例:
int a=10; //非常量左值(有确定存储地址,也有变量名)
const int a1=20; //常量左值(有确定存储地址,也有变量名)
//非常量右值引用
int &&d1=std::move(a); //正确,将非常量左值a转换为非常量右值,可以被非常量右值引用绑定
int &&d2=std::move(a1); //错误,将常量左值a1转换为常量右值,不可以被非常量右值引用绑定
//常量右值引用
const int &&c1=std::move(a); //正确,将非常量左值a转换为非常量右值,可以被常量右值引用绑定
const int &&c2=std::move(a1); //正确,将常量左值a1转换为常量右值,可以被常量右值引用绑定
最后可以发现,编译器利用std::move将左值强制转换为相同类型的右值之后,引用情况跟右值是一模一样的。
C++11引入右值引用的目的?
1、替代需要销毁对象的拷贝,提高效率:某些情况下,需要拷贝一个对象然后将其销毁,如:临时类对象的拷贝就要先将旧内存的资源拷贝到新内存,然后释放旧内存,引入右值引用后,就可以让新对象直接使用旧内存并且销毁原对象,这样就减少了内存和运算资源的使用,从而提高了运行效率;
2、移动含有不能共享资源的类对象:像IO、unique_ptr这样的类包含不能被共享的资源(如:IO缓冲、指针),因此,这些类对象不能拷贝但可以移动。这种情况,需要先调用std::move将左值强制转换为右值,再进行右值引用。
简单介绍:左值引用和右值引用
【重点看:引入右值引用、移动拷贝、移动赋值函数后减少对象拷贝的介绍】:左值引用、右值引用详解
待看:左值引用和右值引用
待看:【C++深陷】之“左值与右值”
7. 获取系统时间
8. 引用和指针
定义和声明
C++中,引用和指针都是用来操作和管理变量数据的方式,引用是已存在变量的别名,而指针是存储变量地址的变量。
使用&符号定义和声明引用,使用*符号定义和声明指针。
如下代码中,ref_a是变量a的引用(别名),ptr_a是变量a的指针。
#include <iostream>
using namespace std;
int a = 10;
int &ref_a = a; // 定义和初始化引用
cout << ref_a << endl; // 通过引用访问变量的值
int *ptr_a = &a; // 定义和初始化指针
cout << *ptr_a << endl; // 通过指针访问变量的值
相同点
1.都是地址的概念;
指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名。
区别点
1.指针是个实体,是一个独立的变量,该变量有自己独立的内存空间,保存的内容为所指向变量的内存地址。而引用是指向变量的别名。
“sizeof 引用”得到的是所指向变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小。
2.定义和声明的语法不同,引用使用&符号,指针使用*符号。
3.引用必须在定义时初始化(不存在空引用),并且一旦初始化,它必须一直引用同一个对象,不允许将引用重定向到其他变量上(引用是从一而终),否则报错。
指针可以只声明不初始化,也可以初始化为空指针nullptr(存在空指针),允许将指针指向其他变量(指针是见异思迁)。
引用不能为空,指针可以为空。
int b = 10;
int &ref_b; // 报错,引用定义时必须初始化
int &ref_b = b;
int c = 10;
int &ref_b = c; // 报错,不允许对引用重定向
int *ptr_b; // 允许定义指针时不初始化
int *ptr_b = nullptr; // 允许对指针赋空值
int *ptr_b = &b;
ptr_b = &c; // 允许对指针重定向
4.const 引用 和 const 指针的含义不同
const引用可以用不同类型的对象初始化(只要能从一种类型转换到另一种类型即可),也可以是不可寻址的值,如文字常量。
double dval = 3.14159;
//下3行仅对const引用才是合法的
const int &ir = 1024;
const int &ir2 = dval;
const double &dr = dval + 1.0;
解释:引用在内部存放的是一个对象的地址,它是该对象的别名。对于不可寻址的值,如文字常量,以及不同类型的对象,编译器为了实现引用,必须生成一个临时对象,引用实际上指向该对象,但用户不能访问它。编译器内部转化为:
1 double dval = 3.14159;
2 //不可寻址,文字常量
3 int tmp1 = 1024;
4 const int &ir = tmp1;
5
6 //不同类型
7 int tmp2 = dval;//double -> int
8 const int &ir2 = tmp2;
9
10 //另一种情况,不可寻址
11 double tmp3 = dval + 1.0;
12 const double &dr = tmp3;
不允许非const引用指向需要临时对象的对象或值,即,编译器产生临时变量的时候引用必须为const。
const引用表示,试图通过此引用去(间接)改变其引用的对象的值时,编译器会报错!
这意味着,此引用所引用的对象也因此变成const类型了。
但我们仍然可以改变其指向对象的值,只是不通过引用。
const引用只是表明,保证不会通过此引用间接的改变被引用的对象。
5.访问对象变量的方式
通过引用访问对象变量数据的方式与通过对象变量本身访问方式一样,通过指针访问对象变量需要通过->操作符。
指针和引用的自增(++)运算意义不一样。
class Obj {
int val;
}
Obj obj;
Obj &ref_obj = obj;
ref_obj.val = 10; // 通过引用访问、操作对象数据
Obj *ptr_obj = &obj;
ptr_obj->val = 10; // 通过指针访问、操作对象数据
6.用途
引用:通常用于函数参数传递,以及在函数返回值中返回引用,以便允许对传递的实参变量进行修改。
指针:用于动态内存分配、数据结构、以及在函数参数传递时,通常用于传递数组或对象的地址。
void modifyValue(int& num) {
num += 10;
}
int x = 5;
modifyValue(x); // 通过引用修改x的值
int* arr = new int[5]; // 动态分配整数数组
C++语言中,函数的参数和返回值的传递方式有三种:值传递、指针传递和引用传递。
引用传递和指针传递都可以实现在背调函数中同步修改实参变量值的目的。
但引用传递相比指针传递更安全。
指针传递:参数本质上是 值传递的方式,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的 实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。
引用传递:而在引用传递过程中, 被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间 接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
区别:引用传递和指针传递是不同的,虽然它们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针 传递的参数,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针的 指针,或者指针引用。
6.安全性
引用:引用更容易使用,因为它们不需要显式的空引用检查。但要小心引用悬空(引用指向的实际变量离开了作用域后,引用就变成了悬空引用)。
指针:指针更灵活,但是容易由于空指针造成程序崩溃,容易引起内存泄漏。
如何选择使用引用还是使用指针?
总的来说,在以下情况下你应该使用 指针: 一是你考虑到存在不指向任何对象的可能(在这种情况下,你能够设置指针为空); 二是你需要能够在不同的时刻指向不同的对象(在这种情况下,你能改变 指针的指向)。如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么你应该使用引用。