const修饰的变量不能够再作为左值,初始化完成后,值不能被修改。
c语言中,const修饰的值,可以不用初始化,不叫常量,叫做常变量;
输出30 30 30
C++中: const 定义的类型必须初始化,否则报错,c语言中可以不初始化
#include
int main()
{
const int a = 10 ;
int array[a] = {};//a 是常量,可以定义数组
int *p = (int *)&a;
*p = 30;
std::cout << a << " " << *p << " " << *(&a);// 10 30 10
return 0;
}
输出 10 30 10;a的值并未被修改。
原因: const 的编译方式不同,C语言中,const 就是当作一个变量来编译生成指令的。C++中,如果const 赋值是一个立即数,所有出现const常量名字的地方,都被常量的初始化所替换。
为什么上面的输出a的值是10呢?因为const 在编译阶段,就已经被常量替换,也就是: std::cout << 10 << " " << *p << " " << 10;
运行时:debug调试
a的地址是0x00cffafc.查看内存:
执行完第9行:
a的内存中的值变成1e 也即30;但是本来出现a的地方在编译期已经被替换成10,因此输出a依然是10。
如果不是立即数,则是常变量
#include
int main()
{
int b = 1;
const int a = b ;
//int array[a] = {};//报错,a是常变量
int *p = (int *)&a;
*p = 30;
std::cout << a << " " << *p << " " << *(&a);// 30 30 30
return 0;
}
const 修饰的量常出现的错误:
(1)常量不能再作为左值
(2)不能把常量的地址泄露给一个普通的指针或者普通的引用变量
C++的语言规范:就近原则 const 修饰的是离它最近的类型
(1)const int *p ;离const 最近的类型是(int)const修饰的是*p,*p不能修改值。 可以指向任意int的内存,但是不能通过指针间接修改内存的值
(2)int const *p; *不是类型, 离const 最近的类型是(int) 同(1)
(3)int * const p; 离const最近的类型是(int *)const修饰的是p 不能改变p指向的地址,但是可以修改p指向地址的内容
(4)const int * const p;不能改变p 指向的地址,也不能改变p指向地址的内容
#include
int main()
{
const int a = 10 ;
const int * p = &a;//p指向的地址的内容不能修改
return 0;
}
const 如果右边没有指针*的话,const 是不参与类型的,仅表示const修饰的是一个常量,不能作为左值
const类型转化公式:
cont int * <= int * 可以转化
int * <= const int * 是错误的
实例1:
#include
#include
int main()
{
int * p = nullptr;
int * const p1 = nullptr;//const 右边没有* ,const不参与类型
std::cout << typeid(p).name() << std::endl;
std::cout << typeid(p1).name() << std::endl;
return 0;
}
示例2:
int a=10;
int *p1= &a;
const int *p2 = &a;// const int * <= int *
int *const p3 = &a;// int * <= int *
int *p4 = p3;//p3是int * 类型,因此没有问题
const int ** q;离const 最近的类型是int ,修饰的是**q;
int * const *q;离const 最近的类型是int *,修饰的是*q;
int ** const q ;离const 最近的类型是int **,修饰的q;//同时const 右边没有*,q是int **类型。
类型转化公式:
int ** <= const int ** //错误
const int ** <= int ** //错误
const 与二级指针结合的时候,两边必须同时有const 或没有const 才能转换;
int ** <= int * const * 是const 和一级指针的结合,const 右边修饰的* (等同于int * <= const int * )错误的
int * const * <= int ** (等同于 const int * <= int *)可以的
要看const 右边的* 决定const 修饰的是类型
#include
#include
int main()
{
int a = 10;
int * p = &a;
const int ** q = &p;//error
/*
const int * *q = &p; 相当于(*)q 即 p的地址,赋值了一个const int *
而p 是int *类型,把常量的地址泄露给普通的指针(p)
修改 const int * p = &a;
*/
return 0;
}
1 引用是必须初始化的,指针可以不初始化,
2 引用只有一级引用,没有多级引用;指针可以有一级指针,也可以用多级指针
3 定义一个引用变量和定义一个指针变量,其汇编指令是一样的;通过引用变量修改所引用内存的值,和通过指针解引用修改指针指向的内存的值,其底层指令也是一模一样的;
引用的错误用法 int &a = 10;由下面的反汇编可以知道,引用的汇编代码第一步是将引用对象的地址拷贝到寄存器中,10是常量;
#include
#include
int main()
{
int a = 10;
int * p = &a;
int &b = a;
std::cout << a << " " << b << " " << (*p) << std::endl;
*p = 20;
std::cout << a << " " << b << " " << (*p) << std::endl;
b = 30;
std::cout << a << " " << b << " " << (*p);
return 0;
}
输出:
反汇编:指针和引用没有区别
lea eax,[a] 将a的地址拷贝到寄存器eax中
mov dword ptr [p],eax 将eax中的值拷贝到p中。
从反汇编中看指针和引用并没有区别。都是拷贝a的地址到p,b 中
对指针和引用赋值,都是一样的:获取地址,然后赋值
4 引用即别名
#include
#include
int main()
{
int array[5] = {};
int * p = array;
int(&q)[5] = array;//定义一个引用指向数组:引用即别名 sizeof(q) = sizeof(array)
std::cout << sizeof(array) << "\n" << sizeof(p) << "\n" << sizeof(q) << std::endl;//20 5 20
return 0;
}
关于定义一个引用类型,到底需不需要开辟内存空间,我认为是需要的,上面的汇编代码中,引用和指针的汇编是一模一样的;C++中只有 const 类型的数据,要求必须初始化。而引用也必须要初始化,所以引用是指针,还应该是 const 修饰的常指针。 一经声明不可改变。
站在宏观角度,引用也就是别名,所以不开辟看空间。
站在微观的角度,引用至少要保存一个指针,所以一定要开辟空间。站在底层实现的角度,站在C++对于C实现包装的角度,引用就是指针。那么既然是指针至少要占用4个字节空间。
左值:有内存地址,有名字,值可以修改;
如int a = 10;int &b =a;
int &c =10;//错误 20 是右值,20=40是错误的,其值不能修改,没内存,没名字,是一个立即数;
上述代码是无法编译通过的,因为10无法进行取地址操作,无法对一个立即数取地址,因为立即数并没有在内存中存储,而是存储在寄存器中,可以通过下述方法解决:
const int &var = 10;
使用常引用来引用常量数字10,因为此刻内存上产生了临时变量保存了10,这个临时变量是可以进行取地址操作的,因此var引用的其实是这个临时变量,相当于下面的操作:
const int temp = 10;
const int &var = temp;
根据上述分析,得出如下结论:
那么C++11 引入了右值引用的概念,使用右值引用能够很好的解决这个问题。
C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:
可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值。
从本质上理解,创建和销毁由编译器幕后控制,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象)。
1 int &&c = 10; 专门用来引用右值类型,指令上,可以自动产生临时量,然后直接引用临时量 c = 1;
反汇编:
可以看出,右值引用首先把右值存放在一个内存地址中:ebp-4ch,然后的操作和左值引用类似:
如果用左值引用:const int &a=1;它首先定义一个临时量int temp=1;然后const int &a = temp;
汇编代码与右值引用没有区别
2 一个右值引用变量,本身是一个左值,只能用左值引用来引用它;不能用一个右值引用变量来引用一个左值
int && a = 1;
a = 10;
int &e = a;
一道笔试题:
#include
#include
using namespace std;
class A
{
public:
A(int data=10):ptr(new int(data)) {}
~A() { delete ptr; ptr = nullptr; }
A(const A &src)
{
cout << "A(const A&)" << endl;
ptr = new int(*src.ptr);
}
A(A &&src)
{
cout << "A(A&&)" << endl;
ptr = src.ptr;
src.ptr = nullptr;
}
private:
int *ptr;
};
int main()
{
vector vec;
vec.reserve(2);
A a;
vec.push_back(a); // 调用哪个构造函数?
vec.push_back(A(20)); // 调用哪个构造函数?
return 0;
}
vec.push_back(a)调用的是左值引用参数的拷贝构造函数。vec.push_back(A(20))实参传入的是临时量对象,调用的是右值引用参数的拷贝构造函数,效率较高。上面程序打印如下:
A(const A&)
A(A&&)
#include
#include
using namespace std;
class A
{
public:
A(int data = 10) :ptr(new int(data)) {}
~A() { delete ptr; ptr = nullptr; }
A(const A &src)
{
cout << "A(const A&)" << endl;
ptr = new int(*src.ptr);
}
A(A &&src)
{
cout << "A(A&&)" << endl;
ptr = src.ptr;
src.ptr = nullptr;
}
private:
int *ptr;
};
std::vector getVector()
{
vector vec;
vec.reserve(3);
vec.push_back(A(20));
vec.push_back(A(30));
vec.push_back(A(40));
cout << "————————" << endl;
/*
这里返回vec时,会调用vector容器的带右值引用参数的拷贝构造函数,
类似vector(vector &&src),直接把这里vec的资源移动给main函数
中的v,效率很高,也就是说函数在返回容器的过程中,没有做任何的内存和
数据开销
*/
return vec;
}
int main()
{
vector v = getVector();
return 0;
}
代码打印如下:
A(A&&)
A(A&&)
A(A&&)
————————
可以看到,vector< A > v = getVector()没有做任何的容器数据拷贝,调用带右值引用参数的成员方法,大大提高了对象的使用效率。
#include
#include
using namespace std;
class A
{
public:
A(int data = 10) :ptr(new int(data)) { cout << "A()" << endl; }
~A() {
delete ptr; ptr = nullptr; cout << "~A()" << endl;
}
A(const A &src)
{
cout << "A(const A&)" << endl;
ptr = new int(*src.ptr);
}
A(A &&src)
{
cout << "A(A&&)" << endl;
ptr = src.ptr;
src.ptr = nullptr;
}
private:
int *ptr;
};
int main()
{
vector vec;
vec.reserve(10);
cout << "--------------------begin" << endl;
for (int i = 0; i < 2; ++i){
A a(i);
/*
这里a是一个左值,因此vec.push_back(a)会调用左值的
拷贝构造函数,用a拷贝构造vector底层数组中的对象
*/
vec.push_back(a);
}
cout << "--------------------endl" << endl;
return 1;
}
输出:
--------------------begin
A()
A(const A&)
~A()
A()
A(const A&)
~A()
--------------------endl
每次循环都需要首先构造A,调用A的默认构造函数,然后 调用左值引用的拷贝构造函数,看上面的代码,A a(i)在for循环中其实算是局部对象,在vec.push_back(a)完成后,a对象调用析构函数。
在vec.push_back(a)时,应该把对象a的资源直接移动给vector容器底层的对象,也就是调用右值引用参数的拷贝构造函数,怎么做到呢?这时候就用到了带移动语义的std::move函数,main函数代码修改如下
cout << "--------------------begin" << endl;
for (int i = 0; i < 2; ++i){
A a(i);
vec.push_back(std::move(a));
}
cout << "--------------------endl" << endl;
输出:
--------------------begin
A()
A(A&&)
~A()
A()
A(A&&)
~A()
--------------------endl
vec.push_back(std::move(a))这段代码中会调用到a对象的右值引用参数的拷贝构造函数。可以看move函数的源码,其实move就是返回传入的实参的右值引用类型,做了一个类型强转,move代码:
template
_NODISCARD constexpr remove_reference_t<_Ty>&&
move(_Ty&& _Arg) noexcept
{ // forward _Arg as movable
return (static_cast&&>(_Arg));
}
首先,函数参数T&&是一个指向模板类型参数的右值引用,通过引用折叠,此参数可以与任何类型的实参匹配(可以传递左值或右值,这是std::move主要使用的两种场景)。关于引用折叠如下:
公式一)X& &、X&& &、X& &&都折叠成X&,用于处理左值
string s("hello");
std::move(s) => std::move(string& &&) => 折叠后 std::move(string& )
此时:T的类型为string&
typename remove_reference::type为string
整个std::move被实例化如下
string&& move(string& t) //t为左值,移动后不能在使用t
{
//通过static_cast将string&强制转换为string&&
return static_cast(t);
}
公式二)X&& &&折叠成X&&,用于处理右值
std::move(string("hello")) => std::move(string&&)
//此时:T的类型为string
// remove_reference::type为string
//整个std::move被实例如下
string&& move(string&& t) //t为右值
{
return static_cast(t); //返回一个右值引用
}
简单来说,右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用
remove_reference是通过类模板的部分特例化进行实现的,其实现代码如下
//原始的,最通用的版本
template struct remove_reference{
typedef T type; //定义T的类型别名为type
};
//部分版本特例化,将用于左值引用和右值引用
template struct remove_reference //左值引用
{ typedef T type; }
template struct remove_reference //右值引用
{ typedef T type; }
//举例如下,下列定义的a、b、c三个变量都是int类型
int i;
remove_refrence::type a; //使用原版本,
remove_refrence::type b; //左值引用特例版本
remove_refrence::type b; //右值引用特例版本
std::move实现,首先,通过右值引用传递模板实现,利用引用折叠原理将右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变。然后我们通过static_cast<>进行强制类型转换返回T&&右值引用,而static_cast
std::move函数可以以非常简单的方式将左值引用转换为右值引用
使用std::move后,左值的内容将会被转移,如下:
//摘自https://zh.cppreference.com/w/cpp/utility/move
#include
#include
#include
#include
int main()
{
std::string str = "Hello";
std::vector v;
//调用常规的拷贝构造函数,新建字符数组,拷贝数据
v.push_back(str);
std::cout << "After copy, str is \"" << str << "\"\n";
//调用移动构造函数,掏空str,掏空后,最好不要使用str
v.push_back(std::move(str));
std::cout << "After move, str is \"" << str << "\"\n";
std::cout << "The contents of the vector are \"" << v[0]
<< "\", \"" << v[1] << "\"\n";
}
std::forward通常是用于完美转发的,它会将输入的参数原封不动地传递到下一个函数中,这个“原封不动”指的是,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值。一个经典的完美转发的场景是
template
void forward(Args&&... args) {
f(std::forward(args)...);
}
需要注意的有2点:
其实现代码:
template
constexpr T&& forward(std::remove_reference_t& arg) noexcept{
// forward an lvalue as either an lvalue or an rvalue
return (static_cast(arg));
}
template
constexpr T&& forward(std::remove_reference_t&& arg) noexcept{
// forward an rvalue as an rvalue
return (static_cast(arg));
}
std::remove_reference_t是一个模板类的类型别名,用于去掉T的引用属性
一个例子:
#include
#include
#include
#include
struct A {
int value;
A(int value=0) : value(value) {
std::cout << "construct" << std::endl;
}
A(const A&a) : value(a.value) {
std::cout << "A(const A&a):" << a.value << std::endl;
}
A(const A&&a) : value(a.value) {
std::cout << "A(const A&&a):" << a.value << std::endl;
}
~A() {
std::cout << "deconstruct" << std::endl;
}
};
void test(A&& a, double b) {
std::cout << "完美转发 右值引用: " << a.value << " " << b << std::endl;
}
void test(A& a, double b) {
std::cout << "完美转发 左值引用: " << a.value << " "<< b << std::endl;
}
template
void test_forward(Args&&... args) {
test(std::forward(args)...);
}
int main() {
A a(1);
float b = 2.1;
test_forward(a, b);
test_forward(std::move(a), b);
return 0;
}
test_forward 第一个参数通过 forward完美转发到void test(A& a, double b) 以及void test(A&& a, double b);
首先传入左值 test_forward(a,b) =》调用void test(A& a, double b)
之后传入传入左值 test_forward(std::move(a),b) =》调用void test(A&& a, double b)
std::forward的应用:C++ move和forward_LIJIWEI0611的博客-CSDN博客