引用
在C++中,引用相当于为变量起了个别名。引用和指针一样,是一种复合类型(compound type),是指基于其他类型定义的类型。
使用引用时需注意:
- 定义引用时,引用和它的初始值绑定(bind)在一起,不是拷贝,所以引用必须初始化。
- 定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的。
- 使用引用时,直接用引用名,不能加&号。
- 引用的类型必须与之绑定的变量类型一样,而且,引用只能与对象绑定,不能和字面值,表达式绑定,引用与引用之间也不能绑定。
int i = 0;
int& ret = i; // 绑定引用,引用声明后必须初始化
右值引用
C++11 引入了右值引用这个新概念。
C++ 中所有的值都必然属于左值、右值二者之一。左值是值表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名。
哪些是右值呢?表达式是右值;函数返回值是右值;字面值是右值。
所以右值引用就是可以绑定右值的引用。因此在 C11 后,原来的引用也称为左值引用。下面一段代码帮你区分左值和右值:
int foo(){
return 1;
}
int a = 1+2; // a 是左值,表达式 1+2 是右值
int b = 1; // b 是左值,字面值 1 是右值
int c = foo(); // c 是左值,foo 的返回值是右值
int& refA = a; // refA 左值引用,绑定一个左值 a
int& refA1 = 1+2; // refA1 左值引用,绑定一个右值,编译器报错
int&& rrefA = 1+2; // rrefA 右值引用,绑定一个右值,编译通过
注意:语法规定左值引用只能绑定左值,右值引用只能绑定右值;但是,常量左值引用却既可以绑定左值又可以绑定右值,比如:
const int& ref = foo();
它有右值引用的功能,不过它却只能读不能改。所以我们常用常量左值引用作为函数的形参,它即可接受左值,又可接受右值,而且还不会发生内存拷贝也避免更改原变量的值。
右值引用的作用?
- 延长右值的生命:在没引入右值引用之前,一个赋值表达式,等号左侧的是左值,等号右侧的是右值。当赋值表达式执行完后,左值继续存在,而右值的生命也就终结了。所以右值引用延长了右值的生命,当一个右值绑定一个右值饮用后,它的生命周期就会继续下去。
- 通过移动语意,可以避免无谓的复制,提高程序性能。
移动语意(move)
当你自己去实现一个 MyString
字符串类的时候,你回去怎么做,考虑这个构造函数 MyString("hello")
,当我们用右值 "hello" 去构造这个这个类的时候,传统的构造函数可以怎样写:
MyString(const char* str){
if(str){
m_data = new char[strlen(str) + 1];
strcpy(m_data, str.m_data);
}
else{
m_data = new char[1];
m_data[0] = '\0';
}
}
这个过程为了避免浅拷贝,而不得不在构造的过程中进行内存拷贝,而对于右值 "hello" 这个右值,其实是不需要内存拷贝的,那么通过右值引用实现移动构造函数,就可以避免这样的内存拷贝。
MyString(MyString&& str){
m_data(str.m_data);
str.m_data = nullptr; //不再指向之前的资源了
}
同样的,有些局部变量,是左值,但它们的生命周期很短,也想使用移动语意,那有没有办法呢?C++11 特地新增了一个标准库函数 std::move() 可以将左值转换成右值,允许这个左值使用移动语意。
MyString str1("hello");
MyString str2(std::move(str1)); // 将局部变量 str1,转换成右值,调用移动构造函数初始化 str2,
注意:将左值转换成右值后,这个左值并没有析构,只是转交了它资源的所有权,之后的代码也不要再使用 左值了,因为它内部已经没有内容。
除此之外,也常用 std::move 实现交换函数,这也是为了减少内存的拷贝。但一定要实现移动运算符 operator=
通用引用
我们查看 std::move 的代码的时候可以看到:
template<typename _Tp>
move(_Tp&& __t);
不是说,move 是将左值引用转换成右值引用吗,参数应该是左值引用才对啊,这里怎么是个右值引用?其实不然,当 && 和模板类型相结合的时候,这里表示的是一个通用引用,也就是它是左值引用还是右值引用取决于它的初始化。如果它被一个左值初始化,它就是一个左值引用;如果被一个右值初始化,它就是一个右值引用。比如我们也可以将一个右值赋给 move 函数
int&& a = std::move(1);
这里只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&才是一个通用引用:
int temp = 1;
auto&& a = 1; // 类型推导,a 是右值引用
auto&& b = temp; // 类型推导,b 是左值引用
注:auto 不能作为函数形参类型,但是可以作为函数返回值
指针
指针与引用类似,实现了对其他对象的间接访问。然而指针与引用又有很多不同点:
- 指针本身就是一个的对象,允许对指针赋值和拷贝,而且指针在其身命周期可以先后指向几个不同的对象。
- 指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值
指针值
指针的值(即地址)应属下列4种状态之一:
- 指向一个对象
- 指向紧邻对象所占空间的下一个位置
- 空指针,意味着指针没有指向任何对象
- 无效指针,也就是上述情况之外的其他值
使用指针时需注意:
- 指针的类型与之指向的对象的类型一样,因为指针本身也是对象,所有可以有指向指针的指针,绑定指针的引用。
- 当试图拷贝或其他方式访问无效指针时,都将引发错误。因此建议初始化所有的指针。如果是在不清楚指针应该指向何处,就把它初始化为nullptr(空指针,C11新标准)或0,表示不指向任何对象(在 if 判断时,当成false处理)。
- 使用指针时,不带 * 号,意味着指针表示的内存地址的值,所以给指针赋值的时候不带 * 号。而带 * 号表示该指针指向的对象的值,所以改变它的值就是改变对象的值。
- 将一个指针直接赋值给另一个指针时,将会发生浅拷贝,也就是这两个指针指向同一段内存空间,对其中一个指针操作,另一个也会发生改变。比如delete,或free掉其中一个指针,那么这段内存空间将会释放,另一个指针将会变成无效指针,程序将会出错。C11提出了很多新概念解决指针拷贝的问题,比如智能指针(shared_ptr),移动等。
void* 指针
void* 是一种特殊的指针类型,可用于存放任意对象的地址。一个void* 指针存放着一个地址,这一点和其他指针类似。不同的是,我们对该地址种到底是个什么类型的对象并不了解。
void*指针能做的事:
- 利用void*指针,可以和别的指针比较
- 利用void指针,作为函数的输入或输出(free函数的参数就是void类型)
- 利用void指针,赋给另一个void指针
void*指针不能做的事:
- 不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上所做的操作
概况来说,以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。比如 自己实现 memcpy
的实现:
void *memcpy (void * __dest, const void * __src, size_t __n){
void * ret = __dest;
if (nullptr ==__dest||nullptr ==__src)
return __dest;
if(__dest == __src)
return __dest;
while (__n--) {
*(char *)__dest = *(char *)__src;
__dest = (char *)__dest + 1;
__src = (char *)__src + 1;
}
return(ret);
}
NULL 与 nullptr
在 C99 中定义了 NULL 代表空指针,它是个预处理变量,定义在头文件 cstdlib 中:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
所以在用 NULL 初始化指针和用 0 初始化指针是一样的。在新标准下,现在的 C++ 程序最好使用 nullptr,同时尽量避免使用 NULL。
nullptr 是 C11 引入的新关键字,用以代替 NULL 预处理变量。nullptr 是一种特殊的字面值,它可以转换成任意其他的指针类型。
在 C++ 程序中建议初始化所有指针。使用未经初始化的指针是引发运行时错误的一大原因。在大多数编译器环境下,如果使用了未经初始化的指针,则该指针所占内存空间的当前内容将被看作一个地址值。访问该指针,相当于去访问一个本不存在的位置上的本不存在的对象。糟糕的是,如果该指针所占内存空间中恰好有内容,而这些内容又被当作了某个地址,我们就很难分清它到底是合法的还是非法的。因此建议初始化所有的指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针。如果实在分不清楚指针应该指向何处,就把它初始化为 nullptr 或者 0,这样程序就能检测并知道它没有指向任何具体的对象了。
用 nullptr 解决 NULL 不能解决的问题
因为 NULL 本质上是 0,如果一个如下重载:
#include <iostream>
void func(int) {
std::cout << "func1" << std::endl;
}
void func(void*) {
std::cout << "func2" << std::endl;
}
int main()
{
func(NULL);
func(nullptr);
return 0;
}
运行结果:
func1
func2
一般来说,我们传进去一个NULL,一般想的是要传一个指针,可是在上面的程序中,我们却调用的是int的版本。如果传进 nullptr,它符合我们的设想,这是因为C++规定nullptr可以转为指针类型。而且是 void * (void* 可用于存放任意对象,所以 nullptr 可以转换为任意类型的指针,自定义类型指针也可以)。
因为 nullptr 在使用时候更智能,不是将空指针机械的转换为 0,所以在新标准下,空指针都初始化为 nullptr。
与 const 相结合
const 与引用结合
int a = 1;
int b = 2;
const int& ref1 = a; // 只有这一种 const 结合方式
ref1 = b; // 错误,既不能改变 ref1 的绑定对象,也不能改变 ref1 绑定对象的值
指针与数组
字符串
在 C++ 中表示字符串有两种方式:
- 指针形式:
char* str = "123ad*12";
str 表示指向这串字符串首地址的指针 - 数组形式:
char str[] = "123ad*12";
sstr 表示一个字符数组
这两种表示方法肯定是有所区别的,一个是指向字符串首地址的指针,一个是数组,但它们都是以 '\0' 结尾;在 C11 标准之后, 指针形式的字符串就不在允许了,虽然有的编译器还是可以通过,但还是推荐使用数组形式作为字符串的容器。
判断下面程序的输出:
void IsTheSameString(){
char str1[] = "hello world";
char str2[] = "hello world";
char* pStr1 = "hello world";
char* pStr2 = "hello world";
if(str1 == str2)
std::cout << "str1 == str2" << std::endl;
else
std::cout << "str1 != str2" << std::endl;
if(pStr1 == pStr2)
std::cout << "pStr1 == pStr2" << std::endl;
else
std::cout << "pStr1 != pStr2" << std::endl;
}
输出:
str1 != str2
pStr1 == pStr2
原因:
- str1 和 str2 是两个字符串数组, 分别分配 12 个字节的存储空间, 并把字符串内容复制了进去
- pStr1 和 pStr2 没有分配内存空间, 他们只是都指向了 hello world 这个字符的地址
指针与数组
通过字符串的例子,我们可以看到指针和数组的相似之处。我们同样可以用一个指针去指向一个数组:
int a[5] = {1,2,3,4,5};
int* p = a;
int* pArray = new int[5]; // 动态数组
上面是一位数组的情况,如果是二维数组:
int a[5][2] = {{5,2},{2,6},{5,6},{7,8},{1,9}};
int (*p)[2] = a; // 需要指定第二个下标的大小,相当于一个二维指针
指针的加减法
我们知道指针的字面值实际上是一个整形,如果它指向的是一段连续的内存空间(数组),那么它的加减法就有意义。
int a[5][2] = {{5,2},{2,6},{5,6},{7,8},{1,9}};
int (*p)[2] = a;
p += 1;
int* p2 = &a[0][0];
p2 += 1;
通过单步调试这个程序,当将 p + 1 后,它指向的值是 {2,6},二维矩阵中的第二个数组,它的地址的值增加了 8。相应的 p2 指向了 2,指针值增加了4。 所以我们可以看到,指针 +1 不是它的字面值 +1,而是表示指向连续地址中的下一个地址。这里的每个元素有两个 int ,所以下一个元素的地址自然 +8。
const 与指针结合
int* const p2 = &a; // const 修饰 p2 的值,所以理解为 p2 的值不可以改变,即 p2 只能指向固定的一个变量地址
p2 = &b; // 错误,p2 不是可修改的左值
const int* p3 = &b; // 顶层 const,顶层指针表示指针本身是一个常量
*p3 = 4; // 错误,*p3 不是可修改的左值
const 与函数结合
一个函数:① const Stock & Stock::topval (②const Stock & s) ③const
①处const:确保返回的Stock 对象在以后的使用中不能被修改
②处const:确保此方法不修改传递的参数 S
③处 const:保证此方法不修改调用它的对象,const 对象只能调用 const 成员函 数,不能调用非const 函数
什么时候使用引用当参数,什么时候用指针当参数?
使用引用参数的主要原因有两个:
- 程序员能修改调用函数中的数据对象
- 通过传递引用而不是整个数据–对象,可以提高程序的运行速度
一般的原则
对于使用引用的值而不做修改的函数:
- 如果数据对象很小,如内置数据类型或者小型结构,则按照值传递
- 如果数据对象是数组,则使用指针(唯一的选择),并且指针声明为指向const的指针
- 如果数据对象是较大的结构,则使用const指针或者引用,已提高程序的效率。这样可以节省结构所需的时间和空间
- 如果数据对象是类对象,则使用const引用(传递类对象参数的标准方式是按照引用传递)
对于修改函数中数据的函数:
- 如果数据是内置数据类型,则使用指针
- 如果数据对象是数组,则只能使用指针
- 如果数据对象是结构,则使用引用或者指针
- 如果数据是类对象,则使用引用