此专栏为移动机器人知识体系下的编程语言中的 C {\rm C} C++从入门到深入的专栏,参考书籍:《深入浅出 C {\rm C} C++》(马晓锐)和《从 C {\rm C} C到 C {\rm C} C++精通面向对象编程》(曾凡锋等)。
4.C++的指针与引用
4.1 指针概念和基本用法
-
指针概念
-
计算机在存储数据时,操作系统将存储器分为一个个小的存储单元,并对每个存储单元进行编号,这些编号是每个存储单元的地址;
-
计算机内存中的数据存储结构如下图所示:
-
C {\rm C} C++中存取内存中的数据有两种方式:一是通过变量名进行存取,二是直接通过地址的形式来访问;
-
指针存储的是内存的地址,指针存储的地址所在的内存区域存储着一个值,即这个指针所指向的地址值;
-
-
指针变量的声明
-
在使用指针变量前,需要先声明指针,指针变量的声明语法如下:
// 指针变量声明格式: 数据类型名 *指针变量名 数据类型名* 指针变量名 数据类型名 * 指针变量名 // 指针变量声明实例 int *iPtr; long *nPtr; double *dPtr; char *cPtr; // 不同的声明方式,效果相同; long* nPtr; long *nPtr; long * nPtr;
-
-
地址运算符*和&
-
*:指针运算符或间接引用符,作用是取得指针所指向变量的内容;
-
&:取地址运算符,作用是获取变量的地址;
-
指针变量实例 ( e x a m p l e 4 _ 1. c p p ) ({\rm example4\_1.cpp}) (example4_1.cpp):
/** * 作者:罗思维 * 时间:2023/10/11 * 描述:声明一个整型类型指针并赋值,输出指针的地址值、 * 指针内存储的地址值、指向的变量值。 */ #include <iostream> using namespace std; int main() { int iValue = 10; int *iPtr = &iValue; // iPtr指向iValue的地址; cout << "iPtr变量本身的地址值:" << &iPtr << endl; cout << "iPtr变量中所存储的地址值:" << iPtr << endl; cout << "iPtr指针所指向的值:" << *iPtr << endl; return 0; }
-
e x a m p l e 4 _ 1. c p p {\rm example4\_1.cpp} example4_1.cpp结果及存储结构图:
iPtr变量本身的地址值: 0x62fe10 iPtr变量中所存储的地址值: 0x62fe1c iPtr指针所指向的值: 10
-
-
指针的赋值
-
声明指针变量后,对指针的赋值的两种方式:
// 1.在声明指针变量的同时进行初始化赋值; 数据类型 *指针变量名 = 初始地址; // 2.在指针变量声明后,单独使用赋值语句进行赋值; 指针变量名 = 地址; // 赋值样例 // 1.使用变量的地址给指针变量赋值; long *lValue; long lNumber = 99; lValue = &lNumber; // 取lNumber变量地址赋值给指针变量lValue; // 2.使用指针的地址给指针变量赋值; long lNumber = 99; long *lTemp = &lNumber; // lTemp是一个指针变量; long *lValue = lTemp; // 用lTemp指针的地址给指针变量lValue赋值; // 3.初始化无法给定指针的具体地址,将其初始化为0或NULL; // 注:不能将非0的整数值直接赋值给地址指针; double *dValue1 = 0; // 声明指针后将其初始化为0; double *dValue2 = NULL; // 声明指针后将其初始化为NULL;
-
-
指针运算
-
指针的算术运算:指指针可以与整数进行加、减运算。如果有一个指针 p {\rm p} p,该指针类型为 T {\rm T} T,则 p + n {\rm p+n} p+n表示当前指针向后移 n {\rm n} n个 T {\rm T} T类型空间大小的地址,相应地址值为: p + s i z e o f ( T ) × n {\rm p+sizeof(T)\times{n}} p+sizeof(T)×n; p − n {\rm p-n} p−n表示当前指针向前移 n {\rm n} n个 T {\rm T} T类型空间大小的地址,相应的地址值为: p − s i z e o f ( T ) × n {\rm p-sizeof(T)\times{n}} p−sizeof(T)×n,其相邻元素间的地址差为 s i z e o f ( T ) {\rm sizeof(T)} sizeof(T)。
-
以 c h a r {\rm char} char类型的数据所占的内存大小为 1 1 1字节,相应相邻元素地址间隔为 1 1 1字节, l o n g {\rm long} long类型的数据所占内存大小为 4 4 4字节,相邻元素地址间隔为 4 4 4字节为例,其指针的算术运算的内存地址示意图如下所示:
-
指针的算术运算是对连续分布的内存空间进行操作的,如:对数组、字符串的操作。对于一个独立变量的地址,如果进行指针的算术运算,可能会导致指针超过其所应指向的范围,从而引发其他地址上的数据被破坏;
-
指针的关系运算:指针的关系运算只有比较两个指针是否相等,两个指针相等要满足两个指针指向的类型相同,且指向同一个内存地址的条件。
-
指针实例 ( e x a m p l e 4 _ 2. c p p ) ({\rm example4\_2.cpp}) (example4_2.cpp):
/** * 作者:罗思维 * 时间:2023/10/12 * 描述:声明两个指针变量,在不同情况下判断两个指针是否相等。 */ #include <iostream> using namespace std; int main() { long lValue1 = 100; long lValue2 = 0; long *plCnt1 = &lValue1; long *plCnt2 = NULL; cout << ((plCnt1 == plCnt2) ? "指针plCnt1与指针plCnt2相等" : "指针plCnt1与指针plCnt2不相等") << endl; plCnt1 = &lValue2; plCnt2 = &lValue2; cout << ((plCnt1 == plCnt2) ? "指针plCnt1与指针plCnt2相等" : "指针plCnt1与指针plCnt2不相等") << endl; return 0; }
-
-
c o n s t {\rm const} const指针
-
常量指针:即指向常量的指针,常量指针的特点是它限制了指针间接访问,即通过指针访问时,指向变量的值不能被改变,指针本身的值可以改变,常量指针的声明格式:
const 数据类型名 *指针变量名;
-
常量指针赋值特性实例 ( e x a m p l e 4 _ 3. c p p ) ({\rm example4\_3.cpp}) (example4_3.cpp):
/** * 作者:罗思维 * 时间:2023/10/12 * 描述:常量指针的赋值特性实例。 */ #include <iostream> using namespace std; int main() { long lValue1 = 100; long lValue2 = 200; const long *plValue = &lValue1; cout << "*plValue = " << *plValue << endl; cout << "plValue = " << plValue << endl; // plValue指针变量存储的地址; cout << "&plValue = " << &plValue << endl; // plValue指针变量本身的地址; cout << "&lValue1 = " << &lValue1 << endl; cout << "======================" << endl; //*plValue = 200; // 不能这样赋值,通过常量指针无法改变指向变量的值; lValue1 = 200; plValue = &lValue2; // 指针本身的值可以改变; cout << "*plValue = " << *plValue << endl; cout << "plValue = " << plValue << endl; // plValue指针变量存储的地址; cout << "&plValue = " << &plValue << endl; // plValue指针变量本身的地址; cout << "&lValue2 = " << &lValue2 << endl; return 0; }
-
指针常量:即指针本身是常量,指针常量本身的值不能改变,但所指向的值可以改变,定义指针常量格式如下:
数据类型名 * const 指针变量名;
-
指针常量赋值特性实例 ( e x a m p l e 4 _ 4. c p p ) ({\rm example4\_4.cpp}) (example4_4.cpp):
/** * 作者:罗思维 * 时间:2023/10/12 * 描述:指针常量的赋值特性实例。 */ #include <iostream> using namespace std; int main() { long lValue1 = 100, lValue2 = 200; long *const plValue = &lValue1; // plValue = &lValue2; // 指针plValue已经指向lValue1,不能重指向lValue2; cout << "lValue1 = " << lValue1 << endl; cout << "*plValue = " << *plValue << endl; cout << "plValue = " << plValue << endl; cout << "======================" << endl; lValue1 = 200; cout << "lValue1 = " << lValue1 << endl; cout << "*plValue = " << *plValue << endl; cout << "plValue = " << plValue << endl; // 指向的内存地址不变; return 0; }
-
常量指针常量:即指向常量的指针,通过指针进行间接访问时,所指向的值不能改变,其本身的值也不能被改变,声明格式如下:
const 数据类型名 * const 指针变量名; // 实例解析: long lValue1 = 100,lValue2 = 200; const long * const plValue = &lValue1; *plValue = 200; // 错误,不能改变变量的值; plValue = &lValue2; // 错误,不能重新指向新的值; lValue1 = 200; // lValue1为一般变量,可以重新赋值;
-
-
v o i d {\rm void} void指针
-
v o i d {\rm void} void指针可以指向任何一种数据类型,在使用时,将其强制类型转换后, v o i d {\rm void} void类型即可访问任何数据类型的指针变量;
-
A N S I C {\rm ANSI\ C} ANSI C++规定不能对 v o i d {\rm void} void类型指针变量进行算术运算,其认为进行运算的指针必须明确具体的数据类型; G N U {\rm GNU} GNU的规定允许 v o i d {\rm void} void类型指针变量进行算术运算;编写程序时,应遵循 A N S I C {\rm ANSI\ C} ANSI C++标准。
-
v o i d {\rm void} void指针实例 ( e x a m p l e 4 _ 5. c p p ) ({\rm example4\_5.cpp}) (example4_5.cpp):
/** * 作者:罗思维 * 时间:2023/10/12 * 描述:void指针实例。 */ #include <iostream> using namespace std; int main() { void *pvValue = NULL; // 声明并初始化void类型指针变量; int *piValue, iCount; iCount = 10; pvValue = &iCount; piValue = (int *)pvValue; cout << "iCount = " << iCount << endl; cout << "&iCount = " << &iCount << endl; cout << "pvValue = " << pvValue << endl; cout << "*piValue = " << *piValue << endl; cout << "piValue = " << piValue << endl; return 0; }
-
-
指针的指针
-
如果一个指针变量的值中存放的是另一个指针变量的指针,则称这个指针变量为指针的指针;
-
直接指向一个变量,称为一级指针,指针的指针变量指向一个指针变量,称为二级指针变量;
-
一级指针变量和二级指针变量示意图如下:
-
指针的指针声明格式如下:
// 数据类型名:指二级指针所指向的一级指针所指向的数据类型; 数据类型名 **指针的指针变量名;
-
指针的指针实例 ( e x a m p l e 4 _ 6. c p p ) ({\rm example4\_6.cpp}) (example4_6.cpp):
/** * 作者:罗思维 * 时间:2023/10/12 * 描述:指针的指针实例。 */ #include <iostream> using namespace std; int main() { char cWord = 'W'; char **ppszStr; char *pszStr = &cWord; long *plNum = NULL; ppszStr = &pszStr; // ppszStr = &plNum; // 错误,类型不对应; cout << "cWord = " << cWord << endl; cout << "*pszStr = " << *pszStr << endl; // 直接输出&cWord和pszStr不能输出其地址值; cout << "&cWord = " << (void *)(&cWord) << endl; cout << "pszStr = " << (void *)pszStr << endl; cout << "&pszStr = " << &pszStr << endl; cout << "ppszStr = " << ppszStr << endl; return 0; }
-
4.2 指针与数组
-
指针数组
-
如果一个数组的每一个元素都是指针变量,则这个数组就是指针数组,指针数组的每个元素都是同一类型的指针;
-
声明一维指针数组的语法格式:
数据类型名 *数组名[下标表达式]; // 定义一个大小为3的指针数组; long *plArray[3] = {NULL}; // 声明了一个long型指针数组plArray,数组元素有3个; // 每个元素都是一个指向long类型的指针,数组首地址为:plArray, // 每个指针都初始化为NULL;
-
指针数组实例 ( e x a m p l e 4 _ 7. c p p ) ({\rm example4\_7.cpp}) (example4_7.cpp):
/** * 作者:罗思维 * 时间:2023/10/16 * 描述:利用指针数组输出标准大写英文字符。 */ #include <iostream> using namespace std; #define V_LINES 4 // 宏定义行常量; #define H_LINES 7 // 宏定义列常量; int main() { char szEnCharacter[V_LINES][H_LINES] = { {'A', 'B', 'C', 'D', 'E', 'F', 'G'}, {'H', 'I', 'J', 'K', 'L', 'M', 'N'}, {'O', 'P', 'Q', ' ', 'R', 'S', 'T'}, {'U', 'V', 'W', ' ', 'X', 'Y', 'Z'}}; char *pszCharacter[V_LINES] = {NULL}; for (int nLoop = 0; nLoop < V_LINES; nLoop++) { // 将4行字符的各行首地址赋值给4个指针; pszCharacter[nLoop] = szEnCharacter[nLoop]; } for (int nLoop_V = 0; nLoop_V < V_LINES; nLoop_V++) { for (int nLoop_H = 0; nLoop_H < H_LINES; nLoop_H++) { cout << pszCharacter[nLoop_V][nLoop_H] << " "; } cout << endl; } return 0; }
-
-
数组名下标和指针的关系
-
在数组中,数组名就是第一个元素的地址,即对于数组 a r r a y {\rm array} array, a r r a y {\rm array} array和 & a r r a y [ 0 ] {\rm \&array[0]} &array[0]是等价的;
-
指针变量和数组的关系:
// 定义数组和指针; long array[array_size]; long *plArray = array; // 对于第i个元素(0≤i<array_size)有如下规律: 值:array[i]等价于*(array+i),*plArray[i]等价于*(plArray+i); 地址:&array[i]等价于array+i,&plArray[i]等价于plArray+i; // 注: // 数组名是一种指向数组元素的指针,数组名不能被赋值, // 是一种指针常量,不能用于左值的运算,表示内存中分配的数组连续位置的首地址; // 在二维数组twoDimArray[size1][size2]中,对于第i行第j列的元素(0≤i<size1,0≤j<size2)有如下规律: 值:twoDimArray[i][j]等价于*(*(twoDimArray+i-1)+(j-1)); 地址:&twoDimArray[i][j]等价于*(twoDimArray+i-1)+(j-1);
-
二维数组地址计算实例 ( e x a m p l e 4 _ 8. c p p ) ({\rm example4\_8.cpp}) (example4_8.cpp):
/** * 作者:罗思维 * 时间:2023/10/16 * 描述:用数组名和下标来计算数组元素的地址。 */ #include <iostream> using namespace std; #define SIZE_1 3 #define SIZE_2 3 int main() { long array1[SIZE_1][SIZE_2] = { {0}}; cout << "long类型占用的内存大小:" << sizeof(long) << endl; cout << "行\t列\t数组名得到的地址\t指针得到的地址" << endl; for (int nCnt1 = 0; nCnt1 < SIZE_1; nCnt1++) { for (int nCnt2 = 0; nCnt2 < SIZE_2; nCnt2++) { cout << nCnt1 + 1 << "\t" << nCnt2 + 1 << "\t" << &array1[nCnt1][nCnt2] << "\t" << "<===>" << "\t" << *(array1 + nCnt1) + nCnt2 << endl; } } return 0; }
-
4.3 指针与函数
-
指针作为函数参数:用指针作为函数的参数可以解决如下几个问题:
-
高效率的数据传送。
用普通的变量作为函数参数传送,当函数的实参和形参进行结合时,函数会建立对实参的备份,以保持对原有数据的保护。如果传送的数据量很大,这样的复制操作影响程序的效率。用指针变量作为参数时,传递的是一个地址,这个地址上的数据是实际要用的值,形参和实参就指向同一个内存地址,可以直接得到实际操作的值,免除了数据的复制工作,提高程序效率。
-
双向数据传递。
用指针作为函数参数,即形参和实参都指向共同的内存空间,这样函数可以直接修改内存空间上的数据,以将数据直接返回给函数的上级调用者。
-
传递函数的首地址。
函数本身也可以作为另一个函数的参数,这是通过指针来实现的。
-
-
指针作为函数参数实例 ( e x a m p l e 4 _ 9. c p p ) ({\rm example4\_9.cpp}) (example4_9.cpp):
/** * 作者:罗思维 * 时间:2023/10/19 * 描述:指针作为函数参数,查找数组最大值。 */ #include <iostream> #include <iomanip> using namespace std; #define ARRAY_SIZE 100 // 数组大小; long getMax(long[], long); // 数组作为参数的函数声明; long getMaxWithPointer(long *, long); // 指针作为参数的函数声明; int main() { long lArray[ARRAY_SIZE]; srand((unsigned)time(NULL)); // 初始化随机数发生器种子; cout << "产生的随机数组为:" << endl; for (int nCnt = 0; nCnt < ARRAY_SIZE; nCnt++) { lArray[nCnt] = rand(); if (nCnt % 5 == 0 && nCnt != 0) { cout << endl; } cout << setw(10) << lArray[nCnt] << " "; } long *plArray = lArray; cout << endl; cout << "用数组传递参数得到的最大值为:" << getMax(lArray, ARRAY_SIZE) << endl; cout << "用指针传递参数得到的最大值为:" << getMaxWithPointer(plArray, ARRAY_SIZE) << endl; return 0; } // 利用数组进行参数传递; long getMax(long lArray[], long nArraySize) { long lMax = 0; for (long nCnt = 0; nCnt < nArraySize; nCnt++) { if (lMax < lArray[nCnt]) { lMax = lArray[nCnt]; } } return lMax; } // 利用指针进行参数传递; long getMaxWithPointer(long *lArray, long nArraySize) { long lMax = 0; for (long nCnt = 0; nCnt < nArraySize; nCnt++) { if (lMax < *(lArray + nCnt)) { lMax = *(lArray + nCnt); } } return lMax; }
-
指针作为函数参数进行双向数据传递实例 ( e x a m p l e 4 _ 10. c p p ) ({\rm example4\_10.cpp}) (example4_10.cpp):
/** * 作者:罗思维 * 时间:2023/10/19 * 描述:指针作为函数参数进行双向数据传递,实现交换两个long类型变量的值。 */ #include <iostream> using namespace std; int SwapValue(long *, long *); int main() { long lNumber1 = 100, lNumber2 = 200; cout << "两数交换前的值分别为:" << endl; cout << "lNumber1 = " << lNumber1 << "," << "lNumber2 = " << lNumber2 << endl; SwapValue(&lNumber1, &lNumber2); cout << endl; cout << "两数交换后的值分别为:" << endl; cout << "lNumber1 = " << lNumber1 << "," << "lNumber2 = " << lNumber2 << endl; return 0; } // 交换函数; int SwapValue(long *lValue1, long *lValue2) { long lTemp = 0; lTemp = *lValue1; *lValue1 = *lValue2; *lValue2 = lTemp; return 0; }
-
指针函数
-
如果一个函数的返回值类型是指针类型,则这个函数为指针函数;
-
使用指针作为函数返回值的主要作用是把大量的数据传递给主调函数;
-
指针函数声明格式:
数据类型 *函数名(参数列表) { 函数体; } // 数据类型表明函数返回指针的类型,*表明是一个指针函数; // 指针函数返回的地址可以是堆地址,可以是全局变量或静态变量的地址, // 但不应该返回局部变量的地址,局部变量在函数调用结束后消亡,其地址上的数据是无效的。
-
-
指针函数返回指针变量实例 ( e x a m p l e 4 _ 11. c p p ) ({\rm example4\_11.cpp}) (example4_11.cpp):
/** * 作者:罗思维 * 时间:2023/10/19 * 描述:指针函数返回指针变量。 */ #include <iostream> using namespace std; char str[10] = {'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W'}; char *SetStr(char[], long); // char *GetStr(); int main() { char *pStr = SetStr(str, 10); cout << pStr << endl; // pStr = GetStr(); // cout << pStr << endl; return 0; } char *SetStr(char str[], long lStrSize) { for (long nCnt = 0; nCnt < lStrSize; nCnt++) { str[nCnt] = 'J'; } return str; // 返回全局指针变量; } // 不能返回局部指针变量,错误做法。 /* char *GetStr() { char str[10] = {'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W'}; return str; } */
-
指向函数的指针
-
每个函数都有函数名,函数名就表示函数的代码在内存中的首地址,当调用函数时,系统得到了函数名,即得到函数代码的首地址,以这个地址开始执行调用函数;
-
函数指针是专门用于存放函数代码首地址的变量,利用函数指针调用函数和使用普通函数名调用函数作用相同;
-
声明函数指针的格式如下:
数据类型 (* 函数指针名)(形参列表); // 声明函数指针时,必须说明函数的返回值、形参列表; // 函数可以用函数名来调用,也可以用指向这个函数的函数指针来调用; // 函数指针不一定指向固定的函数,所指向的函数可以改变; // 声明函数指针后,给指针函数赋值的格式: 函数指针名 = 函数名; // 利用函数指针调用函数的格式: (*函数指针名)(参数表) // C语言标准写法; 函数指针名(参数表) // C++标准写法; long FunctionName(long); long (*pFunc)(long) = FunctionName; // 函数指针变量不能进行算术运算;
-
-
利用函数指针调用函数实例 ( e x a m p l e 4 _ 12. c p p ) ({\rm example4\_12.cpp}) (example4_12.cpp):
/** * 作者:罗思维 * 时间:2023/10/20 * 描述:利用函数指针调用函数。 */ #include <iostream> using namespace std; void SwapValue(long *, long *); int main() { long lNumber1 = 100; long lNumber2 = 200; cout << "交换前的值为:" << endl; cout << "lNumber1 = " << lNumber1 << endl; cout << "lNumber2 = " << lNumber2 << endl; void (*pFunc)(long *, long *); // 声明函数指针; pFunc = SwapValue; // 用SwapValue初始化函数指针; (*pFunc)(&lNumber1, &lNumber2); // 利用函数指针调用函数; cout << endl; cout << "交换后的值为:" << endl; cout << "lNumber1 = " << lNumber1 << endl; cout << "lNumber2 = " << lNumber2 << endl; return 0; } void SwapValue(long *lNumber1, long *lNumber2) { long lTemp = 0; lTemp = *lNumber1; *lNumber1 = *lNumber2; *lNumber2 = lTemp; }
-
函数指针实例 ( e x a m p l e 4 _ 13. c p p ) ({\rm example4\_13.cpp}) (example4_13.cpp):
/** * 作者:罗思维 * 时间:2023/10/21 * 描述:选择老师时,输出老师姓名和所教科目;选择学生时,输出学生姓名和学习科目。 */ #include <iostream> using namespace std; void PrintTeacherInfo(); void PrintStudentInfo(); void PrintBody(void (*pFunc)()); // 声明参数为函数指针的函数; void (*PrintMessage)(); // 声明函数指针; int main() { int nChoice; cout << "请输入选择的对象(1代表老师,2代表学生):"; cin >> nChoice; if (nChoice == 1) { PrintMessage = PrintTeacherInfo; } else if (nChoice == 2) { PrintMessage = PrintStudentInfo; } else { cout << "输入有误." << endl; exit(1); } PrintBody(PrintMessage); return 0; } void PrintTeacherInfo() { cout << "老师姓名:CHENJINDI" << endl; cout << "老师课程:MATH" << endl; } void PrintStudentInfo() { cout << "学生姓名:WILLARD" << endl; cout << "学生选课:MATH" << endl; } void PrintBody(void (*pFunc)()) { pFunc(); }
4.4 指针和动态内存的分配
-
C {\rm C} C++的动态内存分配机制
- 程序二进制代码区:存放程序的二进制代码,在程序结束时释放;
- 常量区:程序中的文字常量、数字常量等常量类型的数据存放在这里,在程序结束时释放;
- 全局/静态变量区:全局变量和静态变量存储在这里,在程序结束时释放;
- 栈区:由编译器在需要时自动分配,在不需要时自动清除的内存区域,所存储的数据主要是局部变量、函数参数等,由编译器自行释放;
- 堆区:由程序员自行分配和清除的内存区域,由程序员释放,如果在程序结束时没有释放,系统对它进行释放;
- 自由存储区:为了兼容 C {\rm C} C语言的内存分配方式,专门为 C {\rm C} C语言风格提供的内存分配存储区域,用 m a l l o c {\rm malloc} malloc分配,用 f r e e {\rm free} free释放;
-
程序中变量的存储区域实例:
int gnCnt = 0; // 存储在全局变量区; int main() { int nNumber1 = 0; // 存储在栈区; char szArray[] = "Willard"; // 存储在栈区; char *pszStr = "Chen"; // pszStr存储在栈区,"Chen"存储在常量区; static int snNumber2 = 0; // 存储在静态变量区; }
-
动态存储区包括:栈区、堆区、自由存储区:
- 栈在操作系统中是一块连续的内存区域,是向低地址扩展的数据结构;栈的地址和容量是操作系统预先定义好的,在 W i n d o w s {\rm Windows} Windows操作系统下一般为 1 M B {\rm 1MB} 1MB或 2 M B {\rm 2MB} 2MB,在程序编译时确定好,如果申请的空间超过了栈的剩余空间,会导致溢出;
- 堆是向高地址扩展的数据结构,是不连续的内存空间,是由操作系统通过链表来存储的空闲的内存地址,遍历方向是从低地址向高地址;堆的大小受限于计算机系统有效的虚拟内存,一般系统的虚拟内存远远大于操作系统给栈所分配的内存,从堆上分配内存一般能获得比较大内存区域;
- 栈的内存分配由程序自行控制,一般速度较快,但程序员无法控制,堆的内存分配由程序员完成,速度较慢,但对于程序员来说是可控的;
- 自由存储区由开发者利用 m a l l o c {\rm malloc} malloc等函数分配的内存块,用 f r e e {\rm free} free函数进行释放;
-
C {\rm C} C++风格的动态内存分配方法
-
动态内存的分配使用指针来操作,动态内存分配后是一块不连续的地址空间,这个空间没有具体的变量名,在访问它时使用指针来存储它的首地址;
-
当获得这块内存首地址后,可以通过指针的运算来访问它每个地址上的数据;
-
在 C {\rm C} C++中,动态内存的开辟和释放是用 n e w {\rm new} new和 d e l e t e {\rm delete} delete关键字来操作;
-
n e w {\rm new} new用来开辟动态内存,即动态创建堆对象,语法格式如下:
new 类型名(初始值列表); // 开辟存储类型的空间; new 类型名[下标表达式1][下标表达式2]; // 创建存储数组的空间; new 类名(初始值列表); // 创建对象;
// 利用new创建动态存储空间; int *pNumber; pNumber = new int(3);
- p N u m b e r {\rm pNumber} pNumber声明后,利用 n e w {\rm new} new分配了一个存放 i n t {\rm int} int类型值的内存空间,并将这个内存地址上的值赋为 3 3 3;
- p N u m b e r {\rm pNumber} pNumber指向这个新分配的地址,通过间接访问* p N u m b e r {\rm pNumber} pNumber访问这个存放到 i n t {\rm int} int类型内存地址上的值;
-
如果内存开辟失败,对应的指针应该为 N U L L {\rm NULL} NULL,这是判断内存是否成功被开辟的标志;在程序中如果开辟动态内存,一定要判断是否开辟成功,如果系统没有成功开辟内存,而又使用了这块内存,则会导致程序运行出错;
// 判断动态内存是否开辟成功; int *pNumber; pNumber = new int(3); if(pNumber == NULL) { exit(1); // 如果内存开辟失败,则退出; }
-
在利用指针开辟了动态内存后或建立对象后,这个指针就指向了该存储空间或对象;操作这块内存或对象,只能用这个指针完成,因此,不能丢失这个指针,如果要对指针进行运算,需要注意的是要始终保持有一个指针或指针副本指向这块内存的首地址;
// 丢失内存指针实例; int *pnNumber; int nNumber = 4; pnNumber = new int(3); pnNumber = &nNumber; // 错误操作;
-
动态数组
int *pnArray = new int[5]; pnArray++; // 错误操作;
开辟内存后,指针的指向为数组的第一个元素,即数组的首地址,如果执行了 p n A r r a y {\rm pnArray} pnArray++,则指针会指向第二个元素的地址,此时第一个元素无法访问,即使使用 p n A r r a y {\rm pnArray} pnArray–能将指针退回指向第一个元素,但不是每种编译系统都支持这样的退回指针操作,存在危险性;
正确访问动态数组:
// 1.利用数组下标来访问动态数组; int *pnArray = new int[5]; pnArray[0] = 1; // 将数组第一个元素赋为1; pnArray[1] = 2; // 2.利用指针来访问动态数组; int *pnArray = new int[5]; int *pnArrayMove = pnArray; // 临时指针; *pnArrayMove = 1; // 将第一个元素的值赋值给1; pnArrayMove++; // 将临时指针使其指向第二个元素; *(pnArrayMove) = 2; // 将第二个元素的值赋为2;
-
动态内存开辟后,都要进行初始化,在没有明确存储内容时,一般将其初始化为 0 0 0,利用函数 m e m s e t {\rm memset} memset进行初始化,语法格式如下:
// 利用函数memset初始化语法格式: memset(指针名,初始化值,开辟空间的总字节数); // 利用函数memset初始化动态数组: long *plArray = new long [5]; memset(plArray,0,sizeof(long)*5);
-
关于开辟内存容量的计算: s i z e o f ( ) {\rm sizeof()} sizeof()计算数组的容量,但对动态开辟的内存容量,则无法用 s i z e o f ( ) {\rm sizeof()} sizeof()计算;
-
在 C {\rm C} C和 C {\rm C} C++中,没有办法计算出指针所指向的动态内存空间的大小,只有在声明或开辟时记住其大小;
-
动态内存使用实例 ( e x a m p l e 4 _ 14. c p p ) ({\rm example4\_14.cpp}) (example4_14.cpp):
/** * 作者:罗思维 * 时间:2023/10/21 * 描述:生成一个随机数组,个数由用户输入。 */ #include <iostream> #include <iomanip> #include <cstring> using namespace std; int main() { long nCnt; cout << "请输入要存入数组的数字个数:"; cin >> nCnt; srand((unsigned)time(NULL)); // 设置随机数产生种子; int *pnArray = new int[nCnt]; // 动态申请nCnt个int型内存空间; // 判断内存空间是否申请成功; if (pnArray == NULL) { exit(1); } // 初始化动态申请的内存空间; memset((void *)pnArray, 0, sizeof(int) * nCnt); for (long lLoop = 0; lLoop < nCnt; lLoop++) { // 将生成的随机数存入动态申请的内存空间; pnArray[lLoop] = rand(); cout << pnArray[lLoop] << endl; } // 释放动态申请的内存空间; delete[] pnArray; pnArray = NULL; return 0; }
-
使用完动态开辟的内存后,需要将其归还系统,把内存归还系统的过程称为内存的释放;
-
动态内存的释放语法格式:
// 1.释放用new开辟的内存或建立的对象; delete 指针名; // 2.如果开辟的空间或建立的对象是数组; delete[] 指针名;
-
如果指针指向的是用于存放基本类型的内存,内存释放后交给操作系统重新利用;如果是指向对象, d e l e t e {\rm delete} delete会使该对象的析构函数被调用;
// 1.释放普通类型的内存 int *pnNumber = new int(3); cout<<*pnNumber<<endl; delete pnNumber; // 释放内存; // 2.释放指向数组的对象内存; long *plArray = new long[10]; memset(plArray,0,sizeof(long)*10); // 初始化为0; delete[] plArray; // 释放内存;
-
d e l e t e {\rm delete} delete在释放内存时只能使用一次,对于未初始化和已经释放内存的指针,使用 d e l e t e {\rm delete} delete可能导致程序出错;
-
用 d e l e t e {\rm delete} delete释放后的指针,如果没有再使用,最好将其置为空,以避免野指针(没有被初始化或在被释放内存后没有被置为 N U L L {\rm NULL} NULL及超过其作用范围的指针称为野指针)的产生;
long *plArray = new long[10]; memset(plArray,0,sizeof(long)*10); delete plArray; // 释放内存; plArray = NULL; // 释放内存后,置为NULL;
-
动态内存创建和释放实例 ( e x a m p l e 4 _ 15. c p p ) ({\rm example4\_15.cpp}) (example4_15.cpp):
/** * 作者:罗思维 * 时间:2023/10/22 * 描述:多维数组的动态创建和释放实例。 */ #include <iostream> #include <cstring> using namespace std; int main() { // 声明一个二级指针,指向动态申请的5个long*型一级指针的内存空间; long **pplArray = new long *[5]; // 让每个一级指针指向动态开辟的5个long型数据的内存空间; for (int nLoop1 = 0; nLoop1 < 5; nLoop1++) { pplArray[nLoop1] = new long[5]; // 将每个一级指针指向的内存空间初始化为0; memset(pplArray[nLoop1], 0, sizeof(long) * 5); } // 依次释放每个一级指针指向的内存空间; for (int nLoop2 = 0; nLoop2 < 5; nLoop2++) { delete[] pplArray[nLoop2]; // 将每个一级指针设为空; pplArray[nLoop2] = NULL; } delete[] pplArray; pplArray = NULL; // 将二级指针置为空; return 0; }
-
-
C {\rm C} C风格的动态内存分配方法
-
C {\rm C} C语言中,内存的开辟和释放使用函数 m a l l o c {\rm malloc} malloc和 f r e e {\rm free} free实现:
// 1.malloc动态内存开辟函数,函数原型如下: void *malloc(size_t size); // 1.1 size_t是需要分配的字节数,分配5个long型内存块,则字节数为:sizeof(long)*5; // 1.2 如果开辟内存成功,函数返回void型指针;如果开辟内存失败,则返回一个空指针; // 1.3 利用malloc函数开辟的内存返回的指针是void类型, // 如果开辟的不是存储void型的内存空间,则需要进行强行转换才能赋予相应的指针; // 2.free动态内存释放函数,函数原型如下: void free(void * pointer); // 2.1 pointer是需要释放的内存相应的指针,指针的类型可以是任意基本类型,但不能是操作对象;
// malloc分配的函数需要强制转换实例 long *plArray = NULL; // 利用malloc()函数开辟动态内存,不成功时返回NULL; plArray = (long *)malloc(sizeof(long)*10); // 内存开辟失败时,结束程序; if(plArray == NULL) { exit(1); }
-
C {\rm C} C风格动态内存分配实例 ( e x a m p l e 4 _ 16. c p p ) ({\rm example4\_16.cpp}) (example4_16.cpp):
/** * 作者:罗思维 * 时间:2023/10/22 * 描述:C风格动态内存分配方法建立一个动态一维数组。 */ #include <iostream> #include <iomanip> using namespace std; int main() { long *plArray = NULL; // 声明空指针; // 开辟10个long型大小的动态数组空间; plArray = (long *)malloc(sizeof(long) * 10); srand((unsigned)time(NULL)); for (int nCnt = 0; nCnt < 10; nCnt++) { plArray[nCnt] = (long)rand(); cout << plArray[nCnt] << endl; } // 释放动态申请的内存空间; free(plArray); plArray = NULL; return 0; }
-
-
动态内存分配陷阱
-
内存分配失败后却使用:在执行开辟内存语句后,应检测内存分配是否成功,当返回结果为 N U L L {\rm NULL} NULL时,需要进行出错处理。
// 内存分配后判断处理 long *plArray = new long[5]; assert(plArray!=NULL); // 调试模式下; // 判断处理1 if(plArray == NULL) // 出错处理; { ...; } // 判断处理2 if(plArray != NULL) { ...; // 正常处理; } else { ...; // 出错处理; }
-
内存分配成功却没有初始化:为了程序更加健壮、可移植性更强,一定要对新声明的变量进行初始化;
-
越界指针:在使用数组时,容易造成指针越界错误,如果指针越界,会导致误操作其他地址上的数据,导致系统数据被破坏;
// 指针越界实例: long *plArray = NULL; long *plArrayMove = NULL; plArray = (long *)malloc(sizeof(long)*10); if(plArray == NULL) { exit(1); } plArrayMove = plArray; srand((unsigned)time(NULL)); for(int nCnt = 0;nCnt < 10;nCnt++) { *plArrayMove = (long)rand(); cout<<*plArrayMove<<endl; plArrayMove++; } cout<<*plArrayMove<<endl; // 输出不确定值; // 上述循环越界,改进如下: for(int nCnt = 0;nCnt < 10;nCnt++) { *plArrayMove = (long)rand(); cout<<*plArrayMove<<endl; if(nCnt != 9) { plArrayMove++; } }
-
内存泄漏:忘记释放内存或释放内存方法错误,导致内存的丢失;
-
内存释放后继续使用:
- 程序混乱:程序混乱导致无法判断某个对象是否已经被释放了内存,因此错误使用了已经被释放了的内存;
- 无效指针:在子函数中的 r e t u r n {\rm return} return中返回指向栈内存的指针或引用,返回的内存是无效的,因为函数结束后内存自动销毁;
- 野指针:在利用 f r e e {\rm free} free或 d e l e t e {\rm delete} delete释放内存后,没有将指针设置为 N U L L {\rm NULL} NULL,导致产生野指针;
- 二次释放:二次释放内存可能会造成释放已经指派给另一个对象的内存;
-
-
动态内存的传递
-
利用引用类型传递参数
// 利用引用类型参数传递动态内存实例 void GetMem(char* &pszReturn,size_t size) { pszReturn = new char[size]; memset(pszReturn,0,sizeof(char)*size); } int main() { char *pMyReturn = NULL; GetMem(pMyReturn,15); if(pMyReturn != NULL) { ...; free(pMyReturn); pMyReturn = NULL; } }
-
利用二级指针传递参数
// 利用二级指针参数传递动态内存实例 void GetMem(char **pszReturn,size_t size) { *pszReturn = new char[size]; } int main() { char *pszReturn = NULL; GetMem(&pszReturn,100); ...; free(pszReturn); pszReturn = NULL; }
-
用函数返回值传递
// 用函数返回值传递动态内存实例 char *GetMem(size_t size) { char *pszReturn = new char[size]; memset(pszReturn,0x00,sizeof(char)*size); return pszReturn; } int main() { char *str = NULL; str = GetMem(); if(str != NULL) { ...; delete[] str; str = NULL; } }
-
4.5 引用
-
引用的概念和基本用法
-
引用是一个变量或对象的别名,当建立一个引用时,程序先需要用一个变量或对象的名称初始化,这个引用就是这个变量或对象的别名;
-
当使用这个引用来进行操作时,对其所代表的真实变量或对象进行操作;
-
声明引用的一般格式:
数据类型 &引用标识符 = 所引用变量或对象名;
-
声明引用时,必须立即为其进行初始化,引用不是值,不占用存储空间,也不会影响目标变量的存储形式和状态;
-
引用初始化后,在 消亡前将一直与所引用的目标变量相关联,对引用的赋值都是对其目标变量的赋值;
-
-
定义引用、引用操作实例 ( e x a m p l e 4 _ 17. c p p ) ({\rm example4\_17.cpp}) (example4_17.cpp):
/** * 作者:罗思维 * 时间:2023/10/22 * 描述:引用的定义和操作。 */ #include <iostream> using namespace std; int main() { int nNumber; int &rNumber = nNumber; // 定义引用; nNumber = 100; cout << "nNumber = " << nNumber << endl; cout << "rNumber = " << rNumber << endl; rNumber = 200; // 通过引用来操作目标变量; cout << "nNumber = " << nNumber << endl; cout << "rNumber = " << rNumber << endl; return 0; }
-
引用运算符和地址运算符实例 ( e x a m p l e 4 _ 18. c p p ) ({\rm example4\_18.cpp}) (example4_18.cpp):
/** * 作者:罗思维 * 时间:2023/10/22 * 描述:引用运算符和地址运算符实例,对引用的取地址操作。 */ #include <iostream> using namespace std; int main() { int nNumber; int &rNumber = nNumber; cout << "&nNumber = " << &nNumber << endl; cout << "&rNumber = " << &rNumber << endl; return 0; }
-
引用作为函数参数实例 ( e x a m p l e 4 _ 19. c p p ) ({\rm example4\_19.cpp}) (example4_19.cpp):
/** * 作者:罗思维 * 时间:2023/10/22 * 描述:引用作为函数参数实例。 */ #include <iostream> using namespace std; void Swap(int &rInt1, int &rInt2); int main() { int number1 = 100, number2 = 200; cout << "交换前的值为:" << endl; cout << "number1 = " << number1 << endl; cout << "number2 = " << number2 << endl; Swap(number1, number2); cout << "交换后的值为:" << endl; cout << "number1 = " << number1 << endl; cout << "number2 = " << number2 << endl; return 0; } void Swap(int &rInt1, int &rInt2) { int number; number = rInt1; rInt1 = rInt2; rInt2 = number; }
-
按值传递时,函数操作的是实参的本地副本,参数为引用时,函数接收的是实参的左值,而不是值的副本;
-
通过引用传递,可以改变引用的目标值,如果要利用引用提高程序的效率,又要保证传递给函数的数据不在函数中被改变,要使用常引用,格式如下:
// 定义常引用 const 数据类型 &引用标识符 = 目标名;
-
用常引用,不能通过引用对目标变量的值进行修改,保证了引用的安全性;
-
常引用实例:
int nNumber; const int &rNumber = nNUmber; nNumber = 100; rNumber = 200; // 错误操作;
-
-
引用作为返回值
-
引用作为返回值的语法格式:
数据类型 &函数名(参数列表) { 函数体; }
-
引用作为返回值实例 ( e x a m p l e 4 _ 20. c p p ) ({\rm example4\_20.cpp}) (example4_20.cpp):
/** * 作者:罗思维 * 时间:2023/10/22 * 描述:引用作为返回值实例,返回数组的最大值。 */ #include <iostream> using namespace std; int &GetMaxValue(int arr[], int nSize); int main() { int nArray[10] = {100, 2, 3, 4, 5, 10, 11, 12, 13, 14}; int arrayMax = GetMaxValue(nArray, 10); cout << "数组最大值为:" << arrayMax << endl; return 0; } int &GetMaxValue(int arr[], int nSize) { int nMax = arr[0], index = 0; for (int nCnt = 1; nCnt < nSize; nCnt++) { if (arr[nCnt] > nMax) { nMax = arr[nCnt]; index = nCnt; } } return arr[index]; // 将数组中的最大值作为引用返回; }
-
4.6 实战
项目需求:读入三个浮点数,分别输出其整数部分和小数部分。
代码实现:
/**
* 作者:罗思维
* 时间:2023/10/22
* 描述:读入三个浮点数,分别输出其整数部分和小数部分。
*/
#include <iostream>
using namespace std;
void SplitFloat(float x, int *intPart, float *fracPart)
{
*intPart = int(x);
*fracPart = x - *intPart;
}
int main()
{
int nCnt, iPart;
float fNumber, fPart;
cout << "请输入三个浮点数:" << endl;
for (nCnt = 0; nCnt < 3; nCnt++)
{
cin >> fNumber;
SplitFloat(fNumber, &iPart, &fPart);
cout << fNumber << "整数部分为:" << iPart << "\t小数部分为:" << fPart << endl;
}
return 0;
}
文章评论