07.指针与引用
1.指针
1.1 数据对象的地址与值
地址:数据对象的存储位置在计算机中的编号
值:在该位置处存储的内容
地址与值是辩证统一的关系
1.2 指针变量的定义与使用
1.2.1 指针的定义格式
格式:目标数据对象类型 *指针变量名称
例一:定义p为指向整数的指针:int *p;
例二:定义p为指向结构体类型的指针:struct POINT{int x, y;}; POINT *p;
1.2.2 多个指针变量的定义
例三:int *p, *q;
1.2.3 指针变量的存储布局
实际上定义一个指针涉及到两个变量,指针数据对象(变量)与目标数据对象(变量)
仅定义指针变量,未初始化
例一:int *p;
定义指针变量,并使其指向某个目标变量
例二:int n = 10; int *p = &n;
计算机有间接访问机制,即通过p保存的变量n的地址间接访问变量n的内容,通过指针p来访问p所指向的目标数据对象。
定义指针变量,并使其指向数组首元素
例三:int a[8] = {1,2,3,4,5,6,7,8}; int *p = a;
指针变量的赋值
指针变量可以像普通变量一样赋值
注意:下例q指针的初始化操作不能用*q = *p;
示例:int n = 10; int *p = &n, *q; q = p;
两个指针指向同一个目标数据对象
1.2.4 取址运算符“&”
获取数据对象的地址,可将结果赋给指针变量
示例:int n = 10; int *p; p = &n; int *q; q = p;
1.2.5 引领操作符“*”
获取指针所指向的目标数据对象
例一:int m,n = 10; int *q = &n; m = *q; //使得m为10
例二(接上例):*q = 1; //使得n为1
1.2.6 指针的意义与作用
作为函数通信的一种手段
使用指针作为函数参数,不仅可以提高参数传递效率,还可以将该参数作为函数输出集的一员,带回结果
作为构造复杂数据结构的手段
使用指针构造数据对象之间的关联,形成复杂数据结构
作为动态内存分配和管理的手段
在程序执行期间动态构造数据对象之间的关联
作为执行特定程序代码的手段
使用指针指向特定代码段,执行未来才能实现的函数
1.3 指针与函数
1.3.1 数据交换函数
之前做过的swap()方法并不能实际调换两个整数的值,现在可以用指针来实现整数互换的操作。
viod Swap(int *p, int *q)
{
if(!p || !q)
{
cout<<"Swap:Parameter(s) illegal."<<endl;
exit(1);
}
int t = *p;
*p = *q;
*q = t;
}
int main()
{
int x = 1;
int y = 2;
Swap(&x, &y);
}
1.3.2 常量指针与指针常量
常量指针:指向常量的指针
性质:不能通过指针修改目标数据对象的值,但可以改变指针值,使其指向其他地方
示例一:int n = 10; const int *p = &n; //更标准的写法应该为int const *p = &n;
典型使用场合:作为函数参数,表示函数内部不能修改指针所指向的目标数据对象值
示例二:void PrintObject(const int *p); //定义成const*p,表示函数内部不能通过*p修改指针p所指向的Object对象中的值
指针常量:指针指向的位置不可变化
性质:不可将指针指向其他地方,但可改变指针所指向的目标数据对象值
示例三:int n = 10; int *const p = &n;
指针常量和其他常量一样,必须在定义时初始化
常量指针常量:指向常量的指针常量(指针的双重只读属性)
性质:指针值不可改变,指向的目标对象值也不可改变
示例四:
const int n = 10;
const int *const p = &n; //可以写成int const *const p = &n;但不能写成int const const*p = &n 因为const是左结合的,左边没有关键字再与右边关键字结合。
典型使用场合:主要作为函数参数使用
1.3.3 返回指针的函数
指针类型可以作为函数返回值
函数内部返回某个数据对象的地址
调用函数后将返回值赋值 给某个指针
特别说明:不能返回函数内部定义的局部变量地址,只能返回某个全局变量的地址,或者作为函数的参数传给函数的指针
1.4 指针与复合数据类型
1.4.1 指针与数组
数据对象地址的计算
数组定义:int a[8] = {1,2,3,4,5,6,7,8};
数组基地址:&a或a
数组元素地址
数组首元素地址:&a[0]
数组第i元素地址:&a[0] + i * sizeof(int)
数组基地址与首元素地址数值相同,故数组第i元素地址:a + i * sizeof(int)
作为函数参数的指针与数组
void GenerateIntegers(int a[],unsigned int n)
{
unsigned int i;
Randomize();
for(i = 0; i < n; i++)
a[] = GenerateNumber(lower,upper);
}
//等价于
void GenerateIntegers(int *p,unsigned int n)
{
unsigned int i;
Randomize();
for(i = 0; i < n; i++)
*p++ = GenerateNumber(lower,upper);
}
指针与数组的可互换性
互换情况
指针一旦指向数组的基地址,则使用指针和数组格式访问元素时的地址计算方式是相同的,此时可以互换指针与数组操作格式
示例
int a[3] = {1,2,3};
int *p = &a;
int i;
for(i = 0;i < 3; i ++)
cout<<p[i]<<endl;
for(i = 0; i < 3; i++)
cout<<*(a + i)<<endl;
例外情况
数组名为常数,不能在数组格式上进行指针运算
示例:
//正确,指针p可赋值,指向下一元素
for(i = 0; i < 3; i++)
cout<<*p++<<endl;
//错误,不能将数组a当作指针赋值
for(i = 0; i < 3; i++)
cout<<*a++<<endl;
指针与数组的差异
使用指针或数组声明的数据对象性质不同
示例:int a[3] = {1,2,3}; int *p = &a; //给数组a分配了3*32位,12字节的内存 给p分配了32位4个字节的内存
定义数组的同时确定了数组元素的存储布局:a为静态分配内存的数组;若a为全局数组,则程序执行前分配内存;若a为局部数组,则在进入该块时分配内存
定义指针时规定指针数据对象存储布局:p为指针,若p为全局变量,则程序执行前分配内存;若为局部变量,则在进入该块时分配内存
定义指针时未规定目标数据对象的存储布局:p为指针,指向一个已存在数组的基地址,即指向该位置处的整数a[0];若p未初始化,则目标数据对象未知
使用指针时,应显示构造指针与目标对象的关联
多维数组作为函数参数
直接传递元素个数也不妥当,只能处理固定元素个数的数组,应用场合十分受限
void PrintTwoDimensinalArray(int a[8][8], unsigned int m, unsigned int n); //正确,但有不妥
不能每维都不传递元素个数,语法规则不允许
void PrintTwoDimensinalArray(int a[][], unsigned int m, unsigned int n); //错误,不符合语法规则,最多只能第一个中括号内不写
a为指向数组基地址的整数指针,m为第一维元素个数,n为第二维元素个数,函数内部使用指针运算访问某个元素。如第i行j列元素,使用指针运算a + n * i + j的结果指针指向
void PrintTwoDimensinalArray(int *a, unsigned int m, unsigned int n); //正确,但把二维数组化为一维数组存储,有不妥
综上,C++目前没有多维数组作为函数参数的妥当方案,使用第一种方案最为合适。
1.4.2 指针与结构体
指向结构体的指针
struct STUDENT{int id; STRING name; int age;};
STUDENT student = {2007010367, "Bear",19};
STUDENT *pstudent = &student;
访问指针所指向的结构体对象的成员
必须使用括号:选员操作符优先级高于引领操作符
(*pstudent).id = 2007010367;
(*pstudent).name = DuplicateString("Bear");
(*pstudent).age = 19;
选员操作符“->”
pstudent->id = 2007010367; //不用书写括号,更方便
结构体成员类型为指针
struct ARRAY{unsigned int count; int *elements;};
int a[8] = {1,2,3,4,5,6,7,8};
ARRAY array = {8,&a};
访问指针类型的结构体成员
访问elements的第i个元素:array.elements[i]
若有定义:ARRAY *parray = &array;
访问parray指向的结构体对象elements的第i个元素:(*parray).elements[i]或parray->elements[i]
结构体指针的使用场合
使用指向结构体对象的指针作为函数参数
好处1. 节省结构体整体赋值的时间成本,因为只需把规模较大的结构体的地址传给指针。
好处2. 解决普通函数参数不能直接待会结果的问题,可以在函数内部改变目标结构体对象的值。
构造复杂的数据结构
动态创建和管理这些复杂的数据结构
动态数组:struct ARRAY{unsigned int count; int *elements;};
1.4.3 指针运算
引入问题
int a[8] = {1,2,3,4,5,6,7,8};
int *p = &a[0]; //p指向数组首元素 *p = &a仍表示p指向数组首元素
int *q = &a[2]; //q指向a[2]
如何表达p、q之间的联系?它们都指向同一数组中的元素
指针与整数加减运算
设p为指向整数数组中某元素的指针,i为整数,则p + i表示指针向后滑动i个整数,p - i表示指针向前滑动i个整数
例:p指向a[0],则p + 2指向a[2];q指向a[2],则q - 2指向a[0]
指针与整数加减运算的结果仍为指针类型量,故可赋值
例:p指向a[0],则p + 2指向a[2],故可q = p + 2,使得q指向a[2]
指针与整数加减运算规律
以指针指向的目标数据对象类型为单位,而不是以字节为单位
指针的递增递减运算
例:p指向a[0],则p++指向a[1]; q指向a[2],则 --q指向a[1]
指针减法运算
两个指针的减法运算结果为其间元素个数
例:p指向a[0],q指向a[2],则q - p的结果为2
指针关系运算
可以测试两个指针是否相等,p == q
空指针:NULL
指针值0:表示指针不指向任何地方,表示为NULL
应用:测试指针是否有意义 if(p!=NULL) 等价于 if(p)
使用指针前一定要测试其是否有意义!
2. 字符串
2.1 字符串的表示
三种理解角度:作为字符数组,作为指向字符的指针,作为抽象的字符串整体
编写函数,返回字符c在字符串s中首次出现的位置
unsigned int FindCharFirst(char c, char s[]) //当作字符数组访问
{
unsigned int i;
if(!s)
{
cout<<"FindCharFirst:Illegal string.\n";
exit(1);
}
for(i = 0; s[i]!='\0';i++)
{
if(s[i] == c)
return i;
}
return inexistent_index; //0xFFFFFFFF
}
unsigned int FindCharFirst(char c, char *s)
{
char *t;
if(!s)
{
cout<<"FindCharFirst:Illegal string.\n";
exit(1);
}
for(t = s;*t != '\0';t++)
{
if(*t == c)
return t-s;
}
return inexistent_index; //0xFFFFFFFF
}
2.2 字符数组
定义格式与普通数组定义格式相同
示例:char s[8] = {'a','b','c','d','e','f','g','h'};
2.3 字符数组与字符指针的差异
字符数组定义后分配数组元素个数个字节的存储空间
字符指针分配4个字节32位的存储空间用来存储数组基地址
按指针格式定义字符串,可以直接赋值
示例:char *s; s = "Hello World!"; //正确
字符串文字首先分配空间,然后将其基地址赋给s,使s指向该字符串基地址
按字符串数组格式定义字符串,不能直接赋值
示例:char s[13]; s = "Hello World!"; //错误
不能对数组进行整体赋值操作
2.4 标准字符串库
C语言字符串库头文件为cstring,C++字符串库头文件为string
cstring中常用的字符串函数
char *strcat(char *dest, const char *src);
char *strcpy(char *dest, const char *src);
int strcmp(const char *s1, const char *s2);
int strlen(const char *s);
char *strtok(char *token, const char *delimiters);
string类
声明与构造string对象
string s = "abcdefg"; 或string s("abcdefg");
第二种方法更好
读取与写入string对象
cout<<s<<endl;
cin>>s; //读取以空格、制表符与回车符分隔的单词
getline(cin,s,'\n'); //读取包含空格和制表符在内的整行
常用函数
length(); //获取string对象的长度
resize(32); //将s设为32字符长,多余舍弃,不足空闲
resize(32,'='); //多余舍弃,不足补’=‘
s1.append(s2); //将字符串s2追加到s1尾部
s1 = s1 + s2; //将s2追加到s1尾部,并将新字符串赋值给s1
s1.compare(s2,0); //从0号位开始比较字符串
s1.find(s2,0); //从字符串开头开始查找,结果为s2在s1中首次出现的位置
3. 动态存储管理
3.1 内存分配
C标准库的动态存储管理函数
内存分配函数malloc
内存释放函数free
C++的内存分配操作符:new与delete
3.2 静态内存分配方式
适用对象:全局对象与静态局部变量
分配与释放时机:在程序运行前分配,程序结束时释放
3.3 自动内存分配方式
适用对象:普通局部变量
分配与释放时机:在程序进入该函数或该块时自动进行,退出时自动释放
3.4 动态内存分配方式
适用对象:匿名数据对象(指针指向的目标数据对象)
分配与释放时机:在执行特定代码端时按照该代码段的要求动态分配与释放,程序员做主
3.5 动态内存分配的目的
静态与自动内存分配方式必须事先了解数据对象的格式和存储空间大小,部分场合无法确定数据对象的大小,比如声明一个包含n个元素的整数数组,n由用户输入
3.6 动态内存分配的位置
计算机维护的一个专门的存储区:堆
所有动态分配的内存都位于堆中
3.7 动态内存分配的关键技术
使用指针指向动态分配的内存区
使用引领操作符操作目标数据对象
3.8 动态存储管理函数的原型
C语言中头文件:"cstlib"和"cmalloc",两者包含其一即可
内存分配函数原型:void *malloc(unsigned int size);
内存释放函数原型:void free(void *memblock);
3.9 void *类型
特殊的指针类型,指向的目标数据对象类型未知
不能在其上使用引领操作符访问目标数据对象
可以转换为任意指针类型,不过转换后类型是否有意义要看程序逻辑
可以在转换后的类型上使用引领操作符
主要目的:作为一种通用指针类型,首先构造指针对象与目标数据对象的一般性关联,然后由程序员在未来明确该关联的性质
3.10 malloc函数的一般用法
首先定义特定类型的指针变量:char *p;
调用malloc函数分配内存:p = (char *)malloc(11);
参数表示所需要分配的存储空间大小,以字节为单位
例:要分配能够保存10个字符出的字符串,分配11个字节,将返回值转换为char *类型赋值给原指针,使p指向新分配空间的匿名目标数据对象
3.11 free函数的一般用法
传递一个指向动态分配内存的目标数据对象的指针
示例一:char *p; p = (char *)malloc(11); free(p);
示例二:int *p = (int *)malloc(10 * sizeof(int)); free(p);
示例二分配能够容纳10个整数的连续存储空间,使p指向该空间的基地址最后调用free函数释放p指向的整个空间
特别说明:有分配就要有释放
调用free(p)后,p指向的空间不再有效,但p仍指向它,为保证在释放目标数据对象空间后,不会再次使用p访问,应该在free后,为p赋空值。
free(p); p = NULL;
3.12 new/new[]操作符
动态创建单个目标数据对象
分配目标对象:int *p; p = new int; *p = 10;
分配目标对象:int *p; p = new(int); *p = 10;
分配目标对象并初始化:int *p; p = new int(10); //将*p初始化为10
分配目标对象并初始化:int *p; p = new(int)(10);
动态创建多个目标数据对象
分配数组目标对象:int *p; p = new int[8]; //分配8个元素的整数数组
3.13 delete/delete[]操作符
释放单个目标数据对象
释放目标对象:int *p; p = new int; *p = 10; delete p;
释放多个目标数据对象
释放数组目标对象:int *p; p = new int[8]; delete[] p;
特别注意:不是delete p[];
3.14 所有权与空悬指针
目标数据对象的所有权
指向该目标数据对象的指针对象拥有所有权
在程序中要时刻明确动态分配内存的目标数据对象的所有权归属哪个指针数据对象
指针使用的一般原则
主动释放原则:如果某函数动态分配了内存,在函数退出时该目标数据对象不再需要,应主动释放它,此时malloc与free在函数中成对出现
所有权转移原则:如果某函数动态分配了内存,在函数推出后该目标数据对象仍然需要,此时应将其所有权转交给本函数之外的同型指针对象,函数内部代码只有malloc,没有free
空悬指针问题
所有权的重叠:指针赋值操作导致两个指针对象指向同样的目标数据对象,即两个指针都声称“自己拥有目标数据对象的所有权”
示例:int *p, *q; q = (int*)malloc(sizeof(int)); p = q;
产生原因:如果程序中通过某个指针释放了目标数据对象,另一个指针并不了解这种情况,它仍指向不再有效的目标数据对象,导致空悬指针
示例:free(p); p = NULL; //q为空悬指针,仍指向原处
解决方案
确保程序中只有唯一一个指针拥有目标数据对象,即只有它负责目标数据对象的存储管理,其他指针只可访问,不可管理;若目标数据对象仍有存在价值,但该指针不再有效,此时应进行所有权移交
在一个函数中,确保最多只有一个指针拥有目标数据对象,其他指针即时存在,也仅能访问,不可管理
如果可能,在分配目标数据对象动态内存的函数中释放内存,如main函数分配的内存在main函数中释放
退一步讲,如果上述条件都不满足,在分配目标数据对象动态内存的函数的主调函数中释放内存,即将所有权移交给上级函数
级级上报,层层审批
3.15 内存泄露与垃圾回收
内存泄露问题
产生原因:若某个函数通过局部指针变量动态分配了一个目标数据对象内存,在函数调用结束后没有释放该内存,并且所有权没有上交
示例:void f(){int *p = new int; *p = 10;}
函数f结束后,p不再存在,*p所在的存储空间仍在,10仍在,但没有任何指针对象拥有它,故不可访问
问题的实质:动态分配的内存必须动态释放,函数本身并不负责管理它
垃圾回收机制:系统负责管理,程序员不需要主动释放动态分配的内存,Java有此功能,C/C++语言无
4. 引用
4.1 引用的定义
定义格式:数据类型& 变量名称 = 被引用变量名称;
示例:int a; int& ref = a;
4.2 引用的性质
引用类型的变量不占用单独的存储空间
为另一数据对象起个别名,与该对象同享存储空间
4.3 特殊说明
引用类型的变量必须在定义时初始化
此关联关系在引用类型变量的整个存续期都保持不变
对引用类型变量的操作就是对被引用变量的操作
4.4 引用作为函数参数
引用的最大意义:作为函数参数
参数传递机制:引用传递,直接修改实际参数值
使用格式:返回值类型 函数名称(类型1 &参数名称1, 类型2 &参数名称2);
函数原型示例:void Swap(int &x, int &y);
函数实现示例:void Swap(int &x, int &y){int t; t = x; x = y; y = x; return;}
函数调用示例:int main(){int a = 10; int b = 20; Swap(a,b); return 0;}
在上例中,x、y分别就是main函数中a、b的别名,对引用x、y的操作,就是对被引用变量a、b的操作
引用作为函数返回值
常量引用:仅能引用常量,不能通过引用改变目标对象值;引用本身也不能改变引用对象
引用作为函数返回值时不生成副本