左值,左值引用,右值,右值引用,通用引用
关于左值与右值
首先,左值和右值并不是指常规的表达式左边的值和表达式右边的值,英文中的lvalue和rvalue中的“l”’指location,即可以寻址的值,而“r”指read,即右值是不可寻址的。
左值引用
格式为int& i,左值引用只能传入左值。因此我们在使用拷贝构造函数和拷贝赋值函数时需要临时变量存放右值。
右值引用
格式为int&& i
意义:
右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。
右值引用中变量的区分:
对于int&&x = 2;
我们知道x的类型是右值引用,指向一个右值,但实际上x是左值还是右值呢?事实上,在C++11中这样规定:若x是有名字,则x为左值,否则x为右值。那么问题来了,什么是有名字,什么是无名?这里又涉及一个无名右值引用和有名右值引用。
无名右值引用:
一般由static_cast < T&& >(t)转换操作转换而来
也可以用标准库提供的std::move()来将左值转换成右值引用
有名右值引用:
即T&&类型的引用。
具体使用:
上面提到右值引用是用来支持转移语义,减少不必要的临时对象的创建等。这里需要引用一个例子:
class MyString {
private:
char* _data;
size_t _len;
void _init_data(const char *s) {
_data = new char[_len+1];
memcpy(_data, s, _len);
_data[_len] = '\0';
}
public:
MyString() {
_data = NULL;
_len = 0;
}
MyString(const char* p) {
_len = strlen (p);
_init_data(p);
}
MyString(const MyString& str) {
_len = str._len;
_init_data(str._data);
std::cout << "Copy Constructor is called! source: " << str._data << std::endl;
}
MyString& operator=(const MyString& str) {
if (this != &str) {
_len = str._len;
_init_data(str._data);
}
std::cout << "Copy Assignment is called! source: " << str._data << std::endl;
return *this;
}
virtual ~MyString() {
if (_data) free(_data);
}
};
int main() {
MyString a;
a = MyString("Hello");
std::vector<MyString> vec;
vec.push_back(MyString("World"));
}
这里对a的赋值调用了拷贝构造函数,对vec的赋值调用了拷贝赋值函数,但是由于传入的变量为临时变量,增加了没有必要的资源的申请和释放。
一个比较好的解决方法是使用转移语义:
MyString(MyString&& str) {
std::cout << "Move Constructor is called! source: " << str._data << std::endl;
_len = str._len;
_data = str._data;
str._len = 0;
str._data = NULL;
}
在这个拷贝构造函数中,我们使用了右值引用,减少不必要的资源申请和释放。
需要注意的是:
- 参数(右值)的符号必须是右值引用符号,即“&&”。
- 参数(右值)不可以是常量,因为我们需要修改右值。因此不能是const类型。
- 参数(右值)的资源链接和标记必须修改。否则,右值的析构函数就会释放资源。转移到新对象的资源也就无效了。
通过转移语义和右值引用的使用,减少了不必要的资源分配,提高了效率。
而move函数就是讲传入的左值引用转换成右值引用。
通用引用
有时候在参数传递时的引用类型可能是左值引用,也可能是右值引用,但具体是什么类型需要程序在运行时判断,这时我们就将该引用称为通用引用。通用引用的格式是T&&,其中T是需要推导的数据类型。通用引用一般用于两个方面,一个是模板类型,另一个是auto声明。因此,当需要类型判断时就涉及到通用引用了。而当右值初始化通用引用时即为右值引用。反之为左值引用。
值传递与引用传递
值传递
被调函数的参数被作为被调函数的局部变量处理,值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值。
可以认为传递指针也是一种值传递,只是传递的是地址值,但是可以对这个地址值中存放的内容进行修改,而存放这个地址值的指针是不会变化的。如果要改变存放该地址值的指针,需要传入的是该指针的地址,所以可以使用指针的指针或者指针的引用。
引用传递
在引用传递过程中,被调用函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数通过间接寻址访问到从主调函数传入的参数的地址,若改变被调函数中参数地址中存放的值,则主调函数中对应的变量地址中存放的值也会改变。
野指针与空指针
野指针指的是指向不可访问内存的指针,指针在声明时并不会自动成为空指针,其默认值时随机的,所以在定义指针的时候要设置为NULL或对其初始化。而由于所指向内存被释放而居无定所的指针也是野指针,这时要将指针的值设为NULL。
inline与宏
对于C++而言,基本上用内联函数替代了宏,原因在于宏容易出错,在运行时可能会出现不可预测的错误,而且不可调试,而且不可操作类的私有成员。而说内联函数可以调试是因为在程序的调试板块了并没有真正内联,编译器会为他生成含有调试信息的可执行代码。只有在程序的发行板块中才真正内联。
使用内联函数
内联函数关键字为inline,必须与函数定义共同使用才有效,仅将 inline 放在函数声明前面不起任何作用。定义在类声明之中的成员函数将自动地成为内联函数,但是是否会真正内联还在于函数定义是什么样的(内联函数中的代码不能过长,而且不能有循环语句和开关语句,只有当函数只有 10 行甚至更少时才将其定义为内联函数.)。
当然内联函数定义也可以放在源文件中,但此时只有定义的那个源文件可以用它,而且必须为每个源文件拷贝一份定义(即每个源文件里的定义必须是完全相同的),当然即使是放在头文件中,也是对每个定义做一份拷贝,只不过是编译器替你完成这种拷贝罢了。所以最好放在头文件中。只要 inline 函数的定义在某个源文件中只出现一次,而且在所有源文件中,其定义必须是完全相同的就可以。
但是内联函数有个弊端就是它只能解决掉函数调用的开销,即参数压栈,返回等开销。实际上若函数执行的开销要大于函数调用的开销,则内联函数的作用就会变得很小。
函数模板,模板函数,模板类,类模板
函数模板是对一系列模样相同的函数的说明与描述,他不是某一个具体的函数,不能被执行,只有被实例化才能被执行
template <typename RET_T , typename IN1_T , typename IN2_T >
RET_T prifunc2(IN1_T in1 , IN2_T in2)
{
RET_T ret;
ret = in1 +in2;
cout<<"in1 = "<<in1<<endl;
cout<<"in2 = "<<in2<<endl;
ret = in1+in2 ;
cout<<"ret = "<<ret<<endl;
return ret;
}
int main(){
int v = prifunc2<double, int ,int>(11,22);//返回值放在第一位。
int vv = prifunc2<double>(33,44);//可以只提供返回值的声明,其他会自行推断出来。
}
类模板与函数模板类似,是对模样相同的类的描述,使用的时候要实例化,说明具体的类型。
template <class T> //带参数T的类模板说明
class Point{
public:
Point(T = 0,T = 0); //类的构造函数
Point(Point&); //类的复制构造函数
T Distance(Point&); //返回类型为T的成员函数
private:
T x,y;
};
Point<int> iobj(3,4); //该对象产生一个int型的点(3,4)
指针常量与常量指针
指针常量:是指定义的指针只能在定义的时候初始化,之后不能改变其值。
格式:[数据类型][*][const][指针常量名称]
例如:char* const name
常量指针:是指指向常量的指针,因为常量指针指向的对象是常量,因此这个对象的值是不能够改变的。
格式:[数据类型][const][*][常量指针名称]
例如:char const* name
注意:需要区分的是指针常量是用常量修饰指针,即指针指向的值可以改变,但指针本身不可修改。而常量指针是指针指向一个常量,指针本身可以修改,指向的内容不可修改。