今天是基础知识的最后一节~明天开始是C++面向对象编程基础,介绍类的概念,以及C++如何支持封装、抽象、继承和多态等重要的面向对象编程原则。
通过这一小节,应该要学到:
- 什么是指针;
- 什么是自由存储区;
- 如何使用运算符new和delete分配和释放内存;
- 如果使用指针和动态分配编写稳定的应用程序;
- 什么是引用;
- 指针和引用的区别;
- 什么情况下使用指针,什么情况下使用引用。
阐述指针和引用
1.1 什么是指针
指针是存储内存地址的变量。就像int变量用于存储数值一样,指针变量用于存储内存地址。
因此,指针是一个变量,与所有变量一样,指针也占用内存空间,指针的特殊之处在于,指针包含的值被解读为内存地址,因此指针是一种指向内存单元的特殊变量。
内存单元地址通常使用十六进制表示法。显示十六进制数时,通常使用前缀0x。
1.1.1 声明指针
作为一种变量,指针也需要声明。通常将指针声明为指向特定的类型,如int,这意味着指针包含的地址对应的内存单元存储了一个整数。也可以将指针声明为指向一个内存块,这种指针被称为void指针。
PointedType * PointerVariableName;
与大多数变量一样,除非对指针进行初始化,否则它包含的值将是随机的。不希望访问随机的内存地址,因此将指针初始化为NULL。NULL是一个可检查的值,且不会是内存地址:
PointedType * PointerVariableName = NULL;
因此,声明int指针的代码如下:
int *pointsToInt = NULL;
与所有数据类型一样,除非对指针进行初始化,否则它包含的将是垃圾值。对指针来说,这种垃圾值非常危险,因为指针包含的值被视为地址。未初始化的指针可能导致程序访问非法内存单元,进而导致程序崩溃。
1.1.2 使用引用运算符(&)获取变量的地址
如果varName是一个变量,&varName将是存储该变量的内存的地址。
引用运算符(&)也叫地址运算符。
1.1.3 使用指针存储地址
通过上面已经知道如何声明指针以及如何获取变量的地址,还知道指针是用于存储内存地址的变量。现在该将它们关联起来,使用指针存储使用引用运算符(&)获取的地址。
假设声明了某种类型的变量:
// Declaring a variable
Type VariableName = InitialValue;
要将该变量的地址存储到一个指针中,需要声明一个同样类型的指针,并使用引用运算符(&)将其初始化为该变量的地址:
// Declaring a pointer to Type and initializing to address
Type* Pointer = &Variable;
1.1.4 使用解除引用运算符(*)访问指向的数据
有了包含合法地址的指针后,要访问这个地方,即获取或设置这个地方的数据,可以使用解除引用运算符(*)。
解除引用运算符(*)也叫间接运算符。
1.1.5 将sizeof()用于指针的结果
指针是包含内存地址的变量。因此无论指针指向哪种类型的变量,其内容都是一个地址——一个数字。在特定的系统中,存储地址多需的字节数是固定的。因此,将sizeof()用于指针时,结果取决于编译程序时使用的编译器和针对的操作系统,与指针指向的变量类型无关。
1.2 动态内存分配
1.2.1 使用new和delete动态地分配和释放内存
使用new来分配新的内存块。通常情况下,如果成功,new将返回指向一个指针,指向分配的内存,否则将引发异常。使用new时,需要指定要为哪种数据类型分配内存:
Type* Pointer = new Type; // request memory for one element
需要为多个元素分配内存时,还可指定要为多少个元素分配内存:
Type* Pointer = new Type[numElements]; // request memory for numElements
因此,如果需要给整型分配内存,可使用如下语法:
int* pointToAnInt = new int; // get a pointer to an integer
int* pointToNums = new int[10]; // pointer to a block of 10 integers
new表示请求分配内存,并不能保证分配请求总能得到满足,因为这取决于系统的状态以及内存资源的可用性。
使用new分配的内存最终都需使用对应的delete进行释放:
Type* Pointer = new Type; // allocate memory
delete Pointer; // release memory allocated above
这种规则也适用于为多个元素分配的内存:
Type* Pointer = new Type[numElements]; // allocate a block
delete[] Pointer; // release block allocated above
对于使用new[...]分配的内存块,需要使用delete[]来释放;对于使用new为单个元素分配的内存,需要使用delete来释放。
不再使用分配的内存后,如果不释放它们,这些内存仍被预留并分配给应用程序。这将减少可供其他应用程序使用的系统内存量,甚至降低应用程序的执行速度。这被称为内存泄露,应不惜一切代价避免这种情况发生。
运算符new和delete分配和释放自由存储区中的内存。自由存储区是一种内存抽象,表现为一个内存池,应用程序可分配(预留)和释放其中的内存。
1.2.2 将递增和递减运算符用于指针的结果
将指针递增或递减时,其包含的地址将增加或减少指向的数据类型的sizeof(并不一定是1字节)。这样,编译器将确保指针不会指向数据的中间或末尾,而只会指向数据的开头。
如果声明了如下指针:
Type* pType = Address;
则执行++pType后,pType将包含(指向)Address + sizeof(Type)。
1.2.3 将关键字const用于指针
通过将变量声明为const的,可确保变量的取值在整个生命周期内都固定为初始值。这种变量的值不能修改,因此不能将其用作左值。
指针也是变量,因此也可将关键字const用于指针。然而,指针是特殊的变量,包含内存地址,还可用于修改内存中的数据块。因此,const指针有如下三种。
- 指针包含的地址是常量,不能修改,但可修改指针指向的数据。
int daysInMonth = 30;
int* const pDaysInMonth = &daysInMonth;
*pDaysInMonth = 31; // OK! Data pointed to can be changed
int daysInLunarMonth = 28;
pDaysInMonth = &daysInLunarMonth; // Not OK! Cannot change address!
- 指针指向的数据为常量,不能修改,但可以修改指针包含的地址,即指针可以指向其他地方。
int hoursInDay = 24;
const int* pointsToInt = &hoursInDay;
int monthsInYear = 12;
pointsToInt = &monthsInYear; // OK!
*pointsToInt = 13; // Not OK! Cannot change data being pointed to
int* newPointer = pointsToInt; // Not OK! Cannot assign const to non-const
- 指针包含的地址以及它指向的值都是常量,不能修改。
int hoursInDay = 24;
const int* const pHoursInDay = &hoursInDay;
*pHoursInDay = 25; // Not OK! Cannot change data being pointed to
int daysInMonth = 30;
pHoursInDay = &daysInMonth; // Not OK! Cannot change address
将指针传递给函数时,这些形式的const很有用。函数参数应声明为最严格的const指针,以确保函数不会修改指针指向的值,这可精致修改指针及其指向的数据。
1.2.4 将指针传递给函数
指针是一种将内存空间传递给函数的有效方式,其中可包含函数完成其工作所需的数据,也可包含操作结果。将指针作为函数参数时,确保函数只能修改希望它修改的参数很重要。为控制函数可修改哪些参数以及不能修改哪些参数,可使用关键字const。
1.2.5 数组和指针的类似之处
当声明下面的int数组时:
int myNumbers[5];
编译器将分配固定数量的内存,用于存储5个整数;同时提供一个指向数组中第一个元素的指针,而指针由指定的数组名标识。换句话说,myNumbers是一个指针,指向第一个元素(myNumbers[0])。
可将数组变量赋给类型与之相同的指针,也证明了数组与指针类似,存储在指针中的地址与数组第一个元素在内存中的地址相同。
由于数组变量就是指针,因此也可将用于指针的解除引用运算符(*)用于数组。同样,可将数组运算符([])用于指针。
1.3 使用指针时常犯的编程错误
1.3.1 内存泄露
这可能是C+应用程序最常见的问题之一:运行时间越长,占用的内存越多,系统越慢。。如果在使用new动态分配的内存不再需要后,程序员没有使用配套的delete释放,通常就会出现这种情况。
1.3.2 指针指向无效的内存单元
使用运算符*对指针解除引用,以访问指向的值时,务必确保指针指向了有效的内存单元,否则程序要么崩溃,要么行为不端。
1.3.3 悬浮指针
使用delete释放后,任何有效指针都将无效。为了避免这种问题,在初始化指针或释放指针后将其设置为NULL,并在使用运算符*对指针解除引用前检查它是否有效(将其与NULL比较)。
1.4 指针编程最佳实践
务必初始化指针变量,否则它将包含垃圾值,这些垃圾值被解读为地址,但应用程序并未获得访问这些地方的授权。如果不能将指针初始化为new返回的有效地址,可将其初始化为NULL。
务必仅在指针有效时才使用它,否则程序可能崩溃。
对于使用new分配的内存,一定要记得使用delete进行释放,否则应用程序将泄露内存,进而降低系统的性能。
1.5 引用是什么
引用是变量的别名。声明引用时,需要将其初始化为一个变量,因此引用只是另一种访问相应变量存储的数据的方式。
要声明引用,可使用引用运算符(&)。
VarType original = Value;
VarType& ReferenceVariable = original;
1.5.1 是什么让引用很有用
引用能够访问相应变量所在的内存单元,这使得编写函数时引用很有用。
典型的函数声明类似于下面这样:
ReturnType DoSomething(Type parameter);
调用函数DoSomething()的代码类似于下面这样:
ReturnType Result = DoSomething(argument); // function call
上述代码导致将argument的值复制为Parameter,再被函数DoSomething()使用。如果argument占用了大量内存,这个复制步骤的开销很大。用于,用DoSomething()返回值时,这个值被复制给Result。如果能避免这些复制步骤,让函数直接使用调用者栈中的数据就太好了。为此,可使用引用。
可避免复制步骤的函数版本类似于下面这样:
ReturnType DoSomething(Type& parameter); // note the reference&
调用该函数的代码类似于下面这样:
ReturnType Result = DoSomething(argument);
由于argument是按引用传递的,Parameter不再是argument的拷贝,而是它的别名。
1.5.2 将关键字const用于引用
可能需要禁止通过引用修改它指向的变量的值,为此可在声明引用时使用关键字const。
int original = 30;
const int& constRef = original;
constRef = 40; // Not allowed: constRef can’t change value in original
int& ref2 = constRef; // Not allowed: ref2 is not const
const int& constRef2 = constRef; // OK
1.5.3 按引用向函数传递参数
引用的优点之一是,可避免将实参复制给形参,从而极大地提高性能。然而,让被调用的函数直接调用函数栈时,确保被调用函数不能修改调用函数中的变量很重要。为此,可将引用声明为const的。
1.6 总结
本小节介绍了指针和引用。
学习了指针,它可用来访问和操纵内存,还是帮助动态分配内存的工具。
还介绍了new和delete,它们可以用于为单个元素分配和释放内存;还介绍了new[...]和delete[],它们可用于为数组分配和释放内存。
还简要的了解了指针编程和动态内存分配的陷阱,知道释放动态分配的内存至关重要,有助于避免内存泄露。
引用是别名,将参数传递给函数时,引用可很好地替代指针,因为引用总是有效的。
学习了const指针和const引用,知道声明函数时应尽可能提高参数的const程度。