C++中,值传递和引用传递的区别?
类似于Python中的深拷贝和浅拷贝
- 值传递:在函数调用时,会触发一次参数的拷贝动作,所以对参数的修改不会影响原始的值。如果是较大的对象,复制整个对象,效率较低。
- 引用传递:函数调用时,函数接收的就是参数的引用,不会触发参数的拷贝动作,效率较高,但对参数的修改会直接作用于原始的值。
void modify_value(int value){ value = 1; } # 值传递
void modify_value(int& value){ value = 1; } # 引用传递
什么场景下使用引用传递?
- 避免不必要的数据拷贝:对于比较大的对象参数(比如
std:vector、std:string.、std:list
),因为拷贝,值传递会导致大量的内存和事件开销。而引用传递可以避免这些开销。 - 允许函数修改实参原始值:有时候,我们就是希望函数能够直接修改传入的变量值,这时使用引用传递很合理。
什么场景下使用值传递?
- 小型数据结构:对于
int、char、double、float
这种基础数据类型,可以直接简单的使用值传递; - 不希望函数修改实参:有时候需要修改变量数据,但是又不希望修改原始值,可以考虑使用值传递。
C++和C的区别?
1)面向对象还是面向过程:
- C语言是一门面向过程的语言,侧重于通过过程(函数)来解决问题。
- C++是一门多范式语言,主要支持面向对象,侧重于使用类和对象来组织代码。
2)继承:
- C++支持继承,允许一个子类继承一个或多个父类,达到代码复用的目的。
- C语言中没有继承的概念。
3)函数重载:
- C++支持函数通过参数名和参数个数的重载。
- C语言不支持重载,函数名必须唯一才行。
4)模板:
- C++支持模板,支持静态和动态形式的多态。
- C语言对此都不支持。
5)内存管理:
- C++使用
new
和delete
操作符来管理内存,也支持使用智能指针来动态管理内存。 - C语言需要使用
malloc
和free
来申请和释放内存。
6)标准库:
- C++的STL标准库能力比C语言丰富的多,比如
vector、.string、Iist、map
等等,还有很多算法相关的能力,这些C语言都没有。
扩展知识
- 1)可以理解为C++是C语言的超集,绝大多数的C代码可以直接在C++中使用。
- 2)C语言更加轻量级和跨平台。C语言的编译器和运行环境相对简单,占用的资源较少,可以在各种操作系统和硬件设备上运行。
- 3)性能层面,C语言的标准库也比较小巧,只包含了一些基本的函数和数据结构。而C++的编译器和运行环境相对复杂,占用的资源较多,需要支持面向对象和模板等特性。C++的标准库也比较庞大,包含了许多高级的容器、算法、字符串、输入输出等功能。
- 4)C语言更加灵活和自由,可以直接操作内存和指针,可以使用宏和预处理指令,可以调用汇编代码等。这些特性使得C语言可以实现一些底层和高效的操作,也方便了与硬件设备的交互。而C++则对程序员施加了更多的约束,例如不允许隐式类型转换、不允许指针运算、不允许多重继承等。这些约束是为了保证程序的安全性和可维护性,但也牺牲了一些灵活性和自由度。
- 5)C语言更加稳定和成熟,语法和标准也比较稳定,不会经常变动。而C++每隔几年就会出现新的标准和特性。这些变化虽然增加了C++的功能和表达力,但也增加了学习和使用的难度,也可能导致一些兼容性和稳定性的问题。
什么是C++的左值和右值?有什么区别?
- 左值:可以出现在赋值运算符的左边,并且可以被取地址,通常是有名字的变量
- 右值:不能出现在赋值运算符的左边,不可以被取地址,表示一个具体的数据值,通常是常量、临时变量
一般可以从两个方向区分左值和右值。
- 方向1:是否可以放到
=
左边? - 方向2:是否可以取地址并且有无名字?
int a = b + c; int a = 3;
a
是左值,有变量名,可以取地址,也可以放到等号左边,表达式b+c
的返回值是右值,没有名字且不能取地址,&(b+c)
不能通过编译,而且也不能放到等号左边。
扩展
左值引用
可以理解为是对左值的引用。对于左值引用,等号右边的值必须可以取地址,如果不能取地址,则会编译失败,或者可以使用const
引用形式,但这样就只能通过引用来读取输出,不能修改数组,因为是常量引用。
int a = 5;
int &b = a; // b是左值引用 cout<<b; // 5
b = 4; // cout<<b; // 4
int &c = 10; // error,10无法取地址,无法进行引用
const int &d = 1;// ok,因为是常引用,引用常量数字,这个常量数字会存储在内存中,可以取地址
// cout << &d; // 内存地址
右值引用
可以理解为是对右值的引用。即对一个临时对象或者即将销毁的对象的引用,开发者可以利用这些临时对象,却不需要复制它们。如果使用右值引用,那表达式等号右边的值需要是右值,可以使用std::move
函数强制把左值转换为右值。
int &&a = 4;
int &&b = a; //error,a是左值
int &&c = std::move(a); //ok
左值引用和右值引用的使用场景
- 左值引用:当需要修改对象的值,或者需要引用一个持久对象时使用。
- 右值引用:当需要处理一个临时对象,并且想要避免复制,或者实现移动语义时使用。
纯右值
纯右值属于右值。运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda
表达式等都是纯右值。
- 除字符串字面值外的字面值
- 返回非引用类型的函数调用
- 后置自增自减表达式
i++、i--
- 算术表达式(
a+b,a*b,a&&b,a==b
等) - 取地址表达式等(
&a
)
将亡值
纯右值属于右值。将亡值是指C++11新增的和右值引用相关的表达式,通常指将要被移动的对象、T&&
函数的返回值、std::move
函数的返回值、转换为T&&
类型转换函数的返回值。
将亡值可以理解为即将要销毁的值,通过“盗取“其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务。
class A {}
A a;
auto c = std::move(a);//c是将亡值
auto d = static_cast<A&&>(a);//d是将亡值
什么是C++的移动语义和完美转发?
移动语义和完美转发都是 C++11 引入的新特性。
移动语义
一种优化资源管理的机制。常规的资源管理是拷贝别人的资源。而移动语义是转移所有权,转移了资源而不是拷贝资源,性能会更好。
移动语义通常用于那些比较大的对象,搭配移动构造函数或移动赋值运算符来使用。
示例代码:
class A{
public:
int *data_;
int size_;
A(int size) : size_(size){
data_ = new int [size];
}
A(){}
A(const A &a){
size_ = a.size_;
data_ = new int[size_];
cout << "copy" << endl;
}
A(A &&a){
this->data_ = a.data_;
a.data_ = nullptr;
cout << "move" << endl;
}
~A(){
if(data_ != nullptr){
delete[] data_;
}
}
};
int main(){
A a(10);
A b = a;
A c = std::move(a); // 调用移动构造函数
return 0;
}
如果不使用
std:move
,,会有很大的拷贝代价,使用移动语义可以避免很多无用的拷贝,提供程序性能,C++所有的STL都实现了移动语义,方便我们使用。例如:std:vector<string> vecs; ...... std::vector<string> vecm = std::move(vecs); //免去很多拷贝
完美转发
完美转发指可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参,转发函数实参是左值那目标函数实参也是左值,转发函数实参是右值那目标函数实参也是右值。
使用std:forward
实现完美转发,可参考以下代码:
void PrintV(int &t) {
cout << "lvalue" << endl;
}
void PrintV(int &&t) {
cout << "rvalue" << endl;
}
template<typename T>
void Test(T &&t) {
PrintV(t);
PrintV(std::forward<T>(t));
PrintV(std::move(t));
}
int main() {
Test(1); // lvalue rvalue rvalue
int a = 1;
Test(a); // lvalue lvalue rvalue
Test(std::forward<int>(a)); // lvalue rvalue rvalue
Test(std::forward<int &>(a)); // lvalue lvalue rvalue
Test(std::forward<int &&>(a)); //lvalue rvalue rvalue
return 0;
}
Test(1)
:1是右值,模板中T &&t
这种为万能引用,右值1传到Test
函数中变成了右值引用,但是调用PrintV()
时候,t
变成了左值(因为它变成了一个拥有名字的变量),所以打印Ivalue
,而PrintV(std::forward<T>(t))
时候,会进行完美转发,按照原来的类型转发,所以打印rvalue
,PrintV(std::move(t))
毫无疑问会打印rvalue
。Test(a)
:a
是左值,模板中T&&
这种为万能引用,左值a
传到Test
函数中变成了左值引用,所以有代码中打印。Test(std::forward<T>(a))
:转发为左值还是右值,依赖于T
,T
是左值那就转发为左值,T是右值那就转发为右值。