1. 引用和指针

引用

参考文档

在C++中,引用相当于为变量起了个别名。引用和指针一样,是一种复合类型(compound type),是指基于其他类型定义的类型。

使用引用时需注意:

  1. 定义引用时,引用和它的初始值绑定(bind)在一起,不是拷贝,所以引用必须初始化。
  2. 定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的。
  3. 使用引用时,直接用引用名,不能加&号。
  4. 引用的类型必须与之绑定的变量类型一样,而且,引用只能与对象绑定,不能和字面值,表达式绑定,引用与引用之间也不能绑定。
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(); 它有右值引用的功能,不过它却只能读不能改。所以我们常用常量左值引用作为函数的形参,它即可接受左值,又可接受右值,而且还不会发生内存拷贝也避免更改原变量的值。

右值引用的作用?

  1. 延长右值的生命:在没引入右值引用之前,一个赋值表达式,等号左侧的是左值,等号右侧的是右值。当赋值表达式执行完后,左值继续存在,而右值的生命也就终结了。所以右值引用延长了右值的生命,当一个右值绑定一个右值饮用后,它的生命周期就会继续下去。
  2. 通过移动语意,可以避免无谓的复制,提高程序性能。

移动语意(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 不能作为函数形参类型,但是可以作为函数返回值

指针

指针与引用类似,实现了对其他对象的间接访问。然而指针与引用又有很多不同点:

  1. 指针本身就是一个的对象,允许对指针赋值和拷贝,而且指针在其身命周期可以先后指向几个不同的对象。
  2. 指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值

指针值

指针的值(即地址)应属下列4种状态之一:

  1. 指向一个对象
  2. 指向紧邻对象所占空间的下一个位置
  3. 空指针,意味着指针没有指向任何对象
  4. 无效指针,也就是上述情况之外的其他值

使用指针时需注意:

  1. 指针的类型与之指向的对象的类型一样,因为指针本身也是对象,所有可以有指向指针的指针,绑定指针的引用。
  2. 当试图拷贝或其他方式访问无效指针时,都将引发错误。因此建议初始化所有的指针。如果是在不清楚指针应该指向何处,就把它初始化为nullptr(空指针,C11新标准)或0,表示不指向任何对象(在 if 判断时,当成false处理)。
  3. 使用指针时,不带 * 号,意味着指针表示的内存地址的值,所以给指针赋值的时候不带 * 号。而带 * 号表示该指针指向的对象的值,所以改变它的值就是改变对象的值。
  4. 将一个指针直接赋值给另一个指针时,将会发生浅拷贝,也就是这两个指针指向同一段内存空间,对其中一个指针操作,另一个也会发生改变。比如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 函数

什么时候使用引用当参数,什么时候用指针当参数?

使用引用参数的主要原因有两个:

  1. 程序员能修改调用函数中的数据对象
  2. 通过传递引用而不是整个数据–对象,可以提高程序的运行速度

一般的原则

对于使用引用的值而不做修改的函数:

  1. 如果数据对象很小,如内置数据类型或者小型结构,则按照值传递
  2. 如果数据对象是数组,则使用指针(唯一的选择),并且指针声明为指向const的指针
  3. 如果数据对象是较大的结构,则使用const指针或者引用,已提高程序的效率。这样可以节省结构所需的时间和空间
  4. 如果数据对象是类对象,则使用const引用(传递类对象参数的标准方式是按照引用传递)

对于修改函数中数据的函数:

  1. 如果数据是内置数据类型,则使用指针
  2. 如果数据对象是数组,则只能使用指针
  3. 如果数据对象是结构,则使用引用或者指针
  4. 如果数据是类对象,则使用引用
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容