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];
- new表示请求分配内存,但并不能保证分配请求总能得到满足,因为这取决于系统的状态及内存资源的可用性。
- delete
- 使用new分配的内存,最终必须都用对应的delete进行释放
- 如delete pointToInt;
- 小心某些情况下,delete和new没有成对
- 对于使用new[...]分配的内存块,需要使用delete[]来释放。
- 如delete pointToIntArray[];
- 不能将运算符delete用于任何包含地址的指针,而只能用于new返回的且未使用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. *和&运算符的对比
- *
- 用于声明指针,相当于值的地址的地址(两层)
- 用于解除引用(取得指针指向的变量的值),这时可以叫做解除引用运算符/间接运算符。
- &(引用运算符/地址运算符)
- 用于声明引用,可视为别名,相当于值的地址(一层,也就是相当于变量名的别名,实际上是一样的)
- 用于获取变量的地址
- 用于按引用传参