21天学通C++学习笔记(八):指针和引用

1. 简述

  • C++最大的优点之一是,既可以用它来编写不依赖于机器(主要是内存)的高级应用程序,又可以用它来编写与硬件紧密协作的应用程序。
  • 事实上C++让您能够在字节和比特级调整应用程序的性能,而要编写高效地利用系统资源的程序,理解指针和引用时必不可少的一步。
  • 在编程语言中,变量使你能够处理内存中的数据。
  • C++让你能够动态地分配内存(new和delete),以优化应用程序对内存的使用。
  • 不同于C#和Java等基于运行时环境的新语言,C++没有自动垃圾收集器对程序已分配但不能使用的内存进行清理。所以使用指针来管理内存资源时,程序员很容易犯错。

2. 什么是指针

  • 指针是一个指向内存空间(存储内存地址)的特殊变量
    • 也占用内存空间,但每个指针占的内存空间大小(存储内存地址所需的空间)和使用的编译器及系统相关,与指针指向的变量类型无关。如果用的是32位编译器,那么大小是32位,也就是4个字节;如果用的是64位编译器并且运行在64位系统上,那么sizeof的结果是64位,也就是8个字节。
    • 16进制数据通常使用前缀0x
  • 声明
    • 作为一个变量,指针也需要声明。
    • 通常将指针声明为指向特定的类型,如int * pint; 意味着这个指针包含的地址对应的内存单元存储了一个整数。
    • void指针:也可将指针声明为指向一个内存块,如void *pvoid; void即“无类型”,void *则为“无类型指针”,能够指向不论什么数据类型。
    • 空指针
      • 与大多数变量一样,除非对指针进行初始化,否则它包含的值将是随机的,可能会导致程序访问非法内存单元,进而导致程序崩溃。如果不希望访问随机的内存地址,可以将指针初始化为空指针。
      • NULL:NULL是一个可以检查的值,且不会是内存地址。
      • nullptr:C++11引入,代表空指针,避免给指针赋值NULL时被编译器替换为0。
      • 使用指针之前要做有效性判断:if(ptrPi) 或 if(ptrPi != NULL) 或 if(nullptr != ptrPi)
  • 使用*(解除引用运算符/间接运算符)访问指向的数据
    • 指针包含的地址必须合法(&获得的通常是合法的)。如果指针未初始化,那么以后可能导致非法访问(Access Violation),即访问应用程序未获得授权的内存单元。
    • 可以通过*修改指向的数据,如果有多个指针指向相同的数据,那么其中一个修改后,其他指针都将受到影响。
  • 使用&(引用运算符/地址运算符)获取变量的地址
    • cout中输出&获取的地址,会是16进制的。
    • 即使是同一台机器,每次用&获取的同一变量的地址每次也都可能不同,是操作系统分配的。
    • 可以将&获取的地址赋值给指针变量,如int age = 30; int* pointToInt = &age;。
    • 同一个int指针可以指向任何int变量。

3. 动态内存分配

  • 像静态数组那样的内存分配是静态和固定的,不容易扩充,而数据少时又造成存储空间浪费、影响性能。
  • 而动态内存分配(使用new和delete)可以满足这样的需求。

3.1. 使用new和delete运算符动态地分配和释放内存

  • 运算符new和delete分配和释放自由存储区中的内存。自由存储区是一种内存抽象,表现为一个内存池,应用程序可分配(预留)和释放其中的内存。
  • new
    • new表示请求分配内存,但并不能保证分配请求总能得到满足,因为这取决于系统的状态及内存资源的可用性。
      • 如int* pointToInt = new int;
    • 为多个元素分配内存时,还可指定要为多少个元素分配内存
      • 如int* pointToIntArray = new int[10];
  • delete
    • 使用new分配的内存,最终必须都用对应的delete进行释放
      • 如delete pointToInt;
    • 小心某些情况下,delete和new没有成对
    • 对于使用new[...]分配的内存块,需要使用delete[]来释放。
      • 如delete pointToIntArray[];
    • 不能将运算符delete用于任何包含地址的指针,而只能用于new返回的且未使用delete使用的指针
    • 一个new的指针,只能delete一次
  • 内存泄露
    • 不再使用分配的内存后,如果不释放它们,这些内存仍被预留并分配给你的应用程序。这将减少可供其他应用程序使用的系统内存量,甚至降低应用程序的执行速度。
    • 要不惜一切代价避免这种情况发生。

3.2. 指针使用递增/递减运算符

  • 对指针执行递增/递减运算,编译器将认为你要指向内存块中的相邻的值(并假定这个值的类型和前一个值相同)。
  • 因此地址递增或递减的不是一个位或字节,而是sizeof(Type)个字节。
  • 类似的,指针加减一个整数,并不代表其存储的地址增减了该整数量的字节,而是该整数sizeof(Type),如(pointsToInts + count)。
  • 由于delete或deletep[]释放内存时必须指定分配内存时new返回的指针地址,因此做了递增、递减或加减操作后,需要做相反的操作令该指针回到最初的内存地址,如-=

3.3. 将关键字const用于指针,并在将指针传递给函数时使用

  • 像前面讲的,将变量声明为const可以确保变量的取值在整个生命周期内都固定为初始值。这种变量不能修改,因此也不能将其用作左值。
  • 有三种用法
    • 指针包含的地址是常量,不能修改,但可修改指针指向的数据(const指针):int* const i = &i;
    • 指针指向的数据为常量,不能修改,但可以修改指针包含的地址,即指针可以指向其他地方(指向const数据的指针):const int* i = &i;
    • 指针包含的地址及它指向的值都是常量,不能修改(这种组合最严格,指向const数据的const指针):const int* const i = &i;
  • 将关键字const用于指针,在将指针传递给函数时很有用。
    • 指针是一种将内存空间传递给函数的有效方式,其中可包含函数完成其工作所需的数据,也可包含操作结果。
    • 将指针作为函数参数时,确保函数只能修改你希望它修改的参数很重要。
    • 这时为控制函数可修改哪些参数以及不能修改哪些参数,可使用关键字const。
    • 这时函数参数应声明为最严格的的const指针,以确保函数不会修改指针指向的值。这可禁止程序员修改指针及其指向的数据。
    • 不能修改指针的const的程度,也就是不能把一个普通指针赋给一个const指针

3.4. 数组和指针的相似之处

  • 可将数据变量赋给类型与之相同的指针
  • 访问数据时,如果int myNumbers[5]; int* pointToNums = myNumbers;,那么*(pointToNums + 1)相当于myNumbers[i]。这也是因为静态数据是连续存储的,所以下一个元素的存储地址就是初始指针加1(像前面说的递增递减操作一样,是1个sizeof(Type))。
  • 由于数据变量就是指针,因此也可将用于指针的解除引用运算符(*)用于数组。同样可将数组运算符([])用于指针。
  • 数组类似于在固定内存范围内发挥作用的指针。
  • 可将数组赋给指针,但不能将指针赋给数组,因为数组时静态的,不能用作左值。

4. 使用指针时常犯的编程错误,以及最佳实践

  • 不同于C#和Java等基于运行时环境的新语言,C++没有自动垃圾收集器对程序已分配但不能使用的内存进行清理。所以使用指针来管理内存资源时,程序员很容易犯错。
  • 内存泄露
    • 运行时间越长,占用的内存越多,系统越慢。
    • 使用new动态分配的内存不再使用后,必须用配套的delete释放。
    • 确保应用程序释放期分配的所有内存是程序员的职责。
    • 经常发生在对一个指针多次new时,中间就要使用delete了,不然就分配了两部分内存,而前一部分没有被释放。
  • 无效指针(指针指向无效的内存单元)
    • 使用*运算符对指针解除引用以访问指向的值时,务必确保指针指向了有效的内存单元,否则程序要么崩溃,要么行为不端。
    • 指针无效的原因有很多,但主要归结于糟糕的内存管理。
    • 比较常见的就是指针没有初始化,也可能某些情况下没有被初始化,那么使用它时就有可能崩溃。
    • 要避免指针拷贝满天飞,不利于delete,可能会重复delete
    • 要让这个程序更好更安全更稳定,应对指针进行初始化,确定指针有效后再使用并只释放指针一次(且仅当指针有效时才释放)
  • 悬浮指针(也叫迷途或失控指针)
    • 指针delete之后,就变成无效的了,不应再使用。
    • 为避免这种问题,很多程序员在初始化指针或释放指针后将其置为NULL,并在使用运算符*对指针解除引用前检查它是否有效(将其与NULL作比较)。
    • 确保不管用户如何输入,指针在程序运行期间始终有效。
  • 检查使用new发出的分配请求是否得到满足
    • 事实上,除非请求分配的内存量特别大,或系统处于临界状态,可供使用的内存很少,否则new一般都能成功。
    • 但有些应用程序需要请求分配大块的内存(如数据库应用程序),最好不要假定内存分配能够成功。
    • 有两种确认指针有效的方法
      • 默认是使用异常。内存分配失败时,将引发std::bad_alloc异常,这将导致应用程序中断执行,除非提供了异常处理程序,否则应用程序将崩溃,并显示一条类似于“异常未处理”的消息。
      • 不想依赖于异常的程序员可使用new变种new(nothrow),这个变种在内存分配失败时不引发异常,而返回NULL,让你能够在使用指针前检查其有效性。
        • int* pointsToMangNums = new(nothrow) int []; if(pointsToMangNums ) {...}
  • 最佳实践总结
    • 务必初始化指针变量,否则它将包含垃圾值。如果不能将指针初始化为new返回的有效地址,可将其初始化为NULL
    • 务必在指针有效时才使用它(是否已释放,是否是空指针,是否真的分配到了内存),否则程序可能崩溃
    • 对于new分配的内存,一定要配套使用delete进行释放,否则会导致内存泄露,进而降低系统性能
    • 使用delete释放指针后,不要访问它
    • 不要对同一指针释放多次
    • 最好避免让两个指针指向相同的地址,因为对其中一个调用delete将导致另一个无效。另外要避免使用有效性不确定的指针。

5. 引用是什么

  • 引用可能看成是变量的别名,即相应变量的另一个名字。
  • 使用&(引用运算符)声明引用
  • 声明引用时,需要将其初始化为一个变量,因此引用只是另一种访问相应变量存储的数据的方式。
  • 引用让你能够访问相应变量所在的内存单元,也就是以别名的方式访问同一内存,因此多用于函数传参,对编写函数很有用。
    • 通过引用传参,让函数直接使用调用者栈中的数据,可以避免直接值传递时的复制步骤的开销(尤其数据很大时)
    • 按引用传参时,函数实参不再是拷贝,而是别名,也就是引用传递,然后调用者可以使用这些参数返回值
  • 将关键字const用于引用、给函数引用传参
    • 需要禁止通过引用修改它指向的变量的值时,可在声明引用时使用const关键字
    • 如果要把一个引用赋给一个const引用,那么这个引用也要是const的,即不能修改const的程度,否则会突破const引用的限制:不能修改它指向的数据。
    • 在给函数传参时,如果不想让被调用函数修改参数,只是想避免形参复制给实参降低性能,那么可以将引用传参同时设为const的,如void GetSquare(const int& number, int & result)
    • 这时引用可以很好得替代指针,因为引用总是有效的

6. *和&运算符的对比

  • *
    • 用于声明指针,相当于值的地址的地址(两层)
    • 用于解除引用(取得指针指向的变量的值),这时可以叫做解除引用运算符/间接运算符。
  • &(引用运算符/地址运算符)
    • 用于声明引用,可视为别名,相当于值的地址(一层,也就是相当于变量名的别名,实际上是一样的)
    • 用于获取变量的地址
    • 用于按引用传参
原文地址:https://www.cnblogs.com/wyp1988/p/10057514.html