类型与值类别
类型是编程语言中用于定义变量、函数参数和返回值等的数据分类。它决定了数据的存储方式、操作方式以及允许的操作。
常见的类型包括:
- 基本类型:整数(
int)、浮点数(float)、布尔值(bool)、字符(char)。 - 复合类型:数组、结构体、类、枚举。
- 抽象类型:接口、泛型。
值类别是C++表达式中用于描述表达式如何被求值和传递的规则C++11及以后的标准将表达式分为以下值类别:
左值:具有持久性,可以取地址的表达式。例如变量、函数返回的左值引用。
cpp
int x = 10; // x是左值
int& getRef() { return x; } // 返回左值引用
右值:临时对象或字面量,通常不能取地址。例如字面量、函数返回的非引用类型。
cpp
int y = 42; // 42是纯右值
int getVal() { return 42; } // 返回纯右值
将亡值:介于左值和纯右值之间,表示资源可以被移动的对象.
cpp
int a = 10;
int b = 20;
int c = a + b;
//首先将a的值赋值给temp(临时变量),随后将temp与b的值进行累加,将结果给c
return 0;
对象中的右值与将亡值
首先我们构建一个对象
cpp
class Int
{
private:
int value;
public:
Int(int x = 0) :value(x) { cout << "Create Int :" << this << endl; }
Int(const Int& it) : value(it.value) { cout << "COPY Int :" << this << endl; }
Int& operator=(const Int& it)
{
if (this != &it)
{
value = it.value;
}
cout << "Operator" << endl;
}
~Int()
{
cout << "DESTORY : " << this << endl;
}
void PrintfVlaue()
{
cout << "value" << value << endl;
}
};
这样我就有如下知识点
cpp
int main()
{
Int a(10);//左值可以对a取地址
Int(20);//无名对象是一个右值
&Int(40);//无法编译通过因为无法取地址
}
在调用函数的过程中也是同理:
cpp
Int fun(int x)
{
Int tmp(x);
return tmp;//函数无法直接将tmp返回出去,但是可以创建临时对象让其被接收
}
int main()
{
Int a(10);
a = fun(100);
return 0;
}
结果如下:

为什么会出现这样呢?
首先,我们创建了对象a,在函数中创建了对象tmp,有因为我们无法直接将tmp赋值给a,所以编译器调用拷贝构造函数创建了一个将亡值,之后因为结束了函数fun(),所以析构了对象tmp,随后调用我们的operator将将亡值赋值给a.
那么一旦我们调度fun函数就一定会产生将亡值吗?
不一定!如果我们将主函数改动一下:
cpp
int main()
{
Int a=fun(100);
return 0;
}

对比前者代码我们不难发现,前者是因为我们构建了一个对象,在对其进行赋值,产生了临时对象,但是后者的代码因为我们是通过函数对a进行赋值,编译器并没有构建临时对象,而是直接构建了对象a!省略了将亡值的对象的构建!
将亡值对象的生存周期
cpp
int main()
{
Int(100);
fun(30);
return 0;
}

可以看到,我们构建了一个无名对象,创建完之后就析构,然后我们在fun函数中创建了一个tmp对象,然后创建一个将亡值,随着函数的退出析构掉.
当然,在将亡值对象存活时,我们是可以对其进行更改的!
cpp
int main()
{
Int(100).SetValue(200).PrintfVlaue();
fun(30);
return 0;
}

这里可以看到我的值被更改了!
右值引用
对于右值我们也是可以进行引用的
cpp
int a = 10;
const int b = 10;
int& ra = 10;//无法引用,因为10是纯右值
int&& c = 10;//右值引用可以引用右值
int& rra = a;//可以引用
int& rb = b;//无法引用因为b为 常性左值
const int& rbb = b;//可以引用
const int& rcc = 10;//可以引用右值,为万能引用
注意:万能引用引用右值时并非直接引用其本身而是在引用其构建出来的临时空间
右值引用引用对象
cpp
Int&& a = Int(30);
Int&& b = fun(20);
a.SetValue(40).PrintfVlaue();
b.SetValue(50).PrintfVlaue();
对于上述代码,我们一个引用是引用无名对象,一个引用时fun()返回的将亡值;当然我们也是可以对其进行修改的

但是这样我们就发现,将亡值对象的生存周期被延长了,给一个更直观的例子
cpp
int main()
{
Int&& a = Int(30);
Int&& b = fun(20);
a.SetValue(40).PrintfVlaue();
b.SetValue(50).PrintfVlaue();
cout << "main end" << endl;

这是因为我右值引用的生存周期被固化了!
将临时对象绑定到const左值引用时,临时对象的生命周期会延长至引用的作用域结束:
具名右值
我们通过右值引用可以将无名对象和将亡值对象变成具名右值,一旦其具名后,就变成了一个泛左值,就不可以继续使用右值引用引用他,必须使用左值.
值类别的转换
我们可以通过强转来讲左值转为右值,但是这样时不安全的,在C++提供了一种静态转发是,static_cast<Int&&>(b);对其进行转换
cpp
int main()
{
Int a(10);
const Int b(20);
Int&& ra = a;//错误值类型不匹配
Int&& rb = (Int&&)a;//强转不安全
Int&& rc = static_cast<Int&&>(b);//安全无法去常性
}
尽管我们可以将左值转换成右值,但是无法将右值转换为左值,右值通常是临时对象或字面量,没有持久的内存地址。尝试将右值转换为左值会导致逻辑矛盾,因为左值要求可寻址性。
函数值类型的返回
函数值类型我们可以返回左值,也可以返回右值,那么他们有什么差异呢?
cpp
Int fun(int x)
{
Int tmp(x);
return tmp;
}
int main()
{
Int a = fun(1);
Int& b = fun(2);
Int&& c = fun(3);
}
我们可以看到我们分别对函数fun的返回值以值的形式接收,以引用的形式接收,以右值的形式接收,首先以值的形式接收时,编译器这里会对其简化,也就是说创建的将亡值就是a本身,但是如果我们使用引用的形式接受就会出现错误,因为我们返回的是一个不具名对象也就是右值,而我们的左值引用无法引用右值!
注意我们是不允许在函数以引用的形式返回函数的局部变量!
cpp
Int& fun(int x)
{
Int tmp(x);
return tmp;
}
为什么?因为引用在底层编译器近似的认为其是一个指针,而我们返回引用的时候不会构建将亡值,并且返回这个对象的地址,如果我们返回的是一个局部对象,那么我们接收到的地址就是一个已经死亡的对象的地址,已经死亡的对象是无法初始化对象的.
那我们要怎么更改他让他可以被正确使用呢?
很简单,我们可以改变这些参数的生存周期,比方说可以把这个给局部对象变为静态变量,让他存在在data区,但是如果我们调用了静态变量,他就只会调用一次,因此我们无法在通过初始化对其进行更改.