1.09k likes | 1.33k Views
第 7 章 间接访问 — 指针. 指针的概念 指针运算与数组 动态内存分配 字符串再讨论 指针作为函数参数和返回值 指针数组与多级指针 指向多维数组的指针 指向函数的指针. 本章将介绍 C++ 语言的一个重要的特性:指针,为了成为一个优秀的 C++ 语言程序员,你必须掌握指针并熟练地使用它们。. 指针介绍. 指针 是内存的 地址 并可作为数据 是一个灵活和危险的机制 允许 共享 处理数据 允许内存 动态 分配(只要需要,而非预先定义). 指针的概念. 指针就是把地址作为数据处理 指针变量:存储地址的变量
E N D
第7章 间接访问—指针 • 指针的概念 • 指针运算与数组 • 动态内存分配 • 字符串再讨论 • 指针作为函数参数和返回值 • 指针数组与多级指针 • 指向多维数组的指针 • 指向函数的指针
本章将介绍C++语言的一个重要的特性:指针,为了成为一个优秀的C++语言程序员,你必须掌握指针并熟练地使用它们。本章将介绍C++语言的一个重要的特性:指针,为了成为一个优秀的C++语言程序员,你必须掌握指针并熟练地使用它们。 指针介绍 • 指针 • 是内存的地址并可作为数据 • 是一个灵活和危险的机制 • 允许共享处理数据 • 允许内存动态分配(只要需要,而非预先定义)
指针的概念 • 指针就是把地址作为数据处理 • 指针变量:存储地址的变量 • 变量的指针:当一个变量存储另一个变量的地址时,那我们说它就是那个变量的指针 • 使用指针的目的:提供间接访问
x 1000 2 1000 p 指针的概念 续 • 如在某一程序中定义了 int x = 2; • 如系统给x分配的空间是1000号单元,则指向x的指针是另一个变量p,p中存放的数据为1000 • 1000号单元的内容有两种访问方式: • 访问变量x(直接访问) • 访问变量p指向的单元的内容(间接访问)
定义指针变量 • 定义指针变量要告诉编译器该变量中存放的是一个地址。 • 指针变量的主要用途是提供间接访问,因此也需要知道指针指向的单元的数据类型 • 指针变量的定义 类型标识符 *指针变量; 如:int *intp; double *doublep; int *p, x, *q;
指针变量的操作 • 如何让指针指向某一变量?因为我们不知道系统分配给变量的真正地址是什么。 • 用地址运算符 “&”解决。如表达式 “&x”返回的是变量 x的地址。如:intp = &x; • & 运算符后面不能跟常量或表达式。如 &2是没有意义的,&(m * n + p )。也是没有意义的 • 如何通过指针变量处理和改变它所指向的单元的值? • 用引用运算符 “*” 解决。如 *intp表示的是 intp指向的这个单元的内容。如:*intp = 5 等价于 x = 5 • 在对 intp使用引用运算之前,必须先对 intp赋值
X X 8 3 1000 1000 Y Y 4 4 1004 1004 intp intp 1000 1000 指针实例 如执行: *intp=Y+4; 如有: int X, *intp, Y; X=3; Y=4; intp=&X; 注意:不能用 intp=100; 因为我们永远不知道变量存储的 真实地址,而且程序每次运行变量地址可能都不同。
指针使用 • 指针变量可以指向不同的变量。如上例中intp指向x,我们可以通过对intp的重新赋值改变指针的指向。如果想让intp指向y,只要执行intp=&y就可以了。这时,intp与x无任何关系。 • 同类的指针变量之间可相互赋值,表示二个指针指向同一内存空间。 • 空指针 • 指针没有指向任何空间 • 空指针用常量NULL表示,NULL的值一般赋为0 • 不能引用空指针指向的值
指针变量的使用 设有定义 int x, y; int *p1,*p2; 执行语句: x=23; y=234; 执行语句: p1=&x; p2=&y; 执行语句: *p1=34; p2=p1;
A p1 a B p2 b 解: A B p1 a p1 a B B p2 b p2 b 指针实例 有以下结构 比较执行 p1=p2和 *p1= *p2后的不同结果。
指针的初始化 • 指针在使用前必须初始化。 • 和别的变量一样,定义指针不初始化是一个比较普通的错误。 • 没有初始化的指针可能指向任意地址,对这些指针作操作可能会导致程序错误。 • NULL是一个特殊指针值,称为空指针。它的值为0。它可被用来初始化一个指针,表示不指向任何地址。 • 思考:int *p; *p = 5; 有什么问题?
第7章 间接访问—指针 • 指针的概念 • 指针运算与数组 • 动态内存分配 • 字符串再讨论 • 指针作为函数参数和返回值 • 指针数组与多级指针 • 指向多维数组的指针 • 指向函数的指针
指针运算和数组 • 指向数组元素的指针 • 数组元素是一个独立的变量,因此可以有指针指向它。如:p = &a[1], p = &a[i] • 数组元素的地址是通过数组首地址计算的。如数组的首地址是 1000,则第i 个元素的地址是1000 + i * 每个数组元素所占的空间长度
指针与数组 • 在C++中,指针和数组关系密切,几乎可以互换使用 • 数组名可以看成是常量指针,对一维数组来说,数组名是数组的起始地址,也就是第0个元素的地址 • 如执行了p=array,则p与array是等价的,对该指针可以进行任何有关数组下标的操作
例如:有定义 int a[10], *p 并且执行了 p=a, 那么可用下列语句访问数组a的元素 for ( i=0; i<10; ++i ) cout << p[i];
指针运算 指针保存的是一个地址,地址是一个整型数,因此可以进行各种算术运算,但仅有加减运算是有意义的。指针运算与数组有密切的关系 • 指针+1表示数组中指针指向元素的下一元素地址; • 指针-1表示数组中指针指向元素的上一元素地址; • 合法的指针操作:p + k, p - k, p1 - p2
数组元素的指针表示 • 当把数组名,如 intarray,赋给了一个同类指针intp 后,intarray 的元素可以通过intp访问。第i个元素的地址可表示为 intp + i,第i个元素的值可表示为 *(intp + i)。 • 通过指针访问数组时,下标有效范围由程序员自己检查。 • 如输出数组 a 的十个元素
方法2: for ( i=0; i<10; ++i ) cout << *(a+i); 方法1: for ( i=0; i<10; ++i ) cout << a[i]; 方法3: for ( p=a; p<a+10; ++p ) cout <<*p ; 方法4: for ( p=a, i=0; i<10; ++i ) cout <<*(p+i); 方法5: for ( p=a, i=0; i<10; ++i ) cout <<p[i] ; 下列程序段 有无问题? for ( i=0; i<10; ++i ) { cout <<*a ; ++a; }
array intp 指针和数组的区别 • 虽然通过指针可以访问数组,但两者本质是不同的。 • 在定义数组时为数组的各个元素分配了全部的存储区,而在定义指针时,仅仅分配四个字节的存储区存放指针地址。只有把一个数组名付给了对应的指针后,指针才能当作数组使用 • 如有:int array[5], *intp; 当执行了 intp = array 后
第7章 间接访问—指针 • 指针的概念 • 指针运算与数组 • 动态内存分配 • 字符串再讨论 • 指针作为函数参数和返回值 • 指针数组与多级指针 • 指向多维数组的指针 • 指向函数的指针
动态分配 • 在C++语言中,每个程序需要用到几个变量,在写程序前就应该知道。每个数组有几个元素也必须在写程序时就决定。 • 有时我们并不知道我们需要多大的数组元素直到程序开始运行。因此希望能在程序中根据某一个当前运行值来决定数组的大小。如设计一个打印魔阵的程序,我们希望先输入魔阵的阶数,然后根据阶数定义一个矩阵
动态分配方法 • 这些问题的解决方案就是内存的动态分配。我们定义一个指针,并让它指向一个合适的内存。如: int *scores; scores = 内存的起始地址;
动态内存分配与回收 • C++中由new 和 delete两个运算符替代- 运算符new用于进行内存分配: 申请动态变量:p = new type;申请动态数组:p = new type[size]; 申请动态变量并初始化:p = new type(初值); - 运算符delete 释放 new分配的内存: 释放动态变量:delete p; 释放动态数组:delete [] p;
动态内存分配与回收 //为简单变量动态分配内存,并作初始化 int main() { int *p; p = new int(99); //动态分配内存,并将99作为初始化值赋给它 cout<< *p; delete p; return 0; }
动态内存分配与回收 //动态字符串的使用 int main() {int *p; char *q; p = new int(5); q = new char[10]; strcpy(q, "abcde"); cout << *p << endl; cout << q << endl; delete p; delete q; return 0; } 输出结果: 5 abcde
动态分配的检查 • new操作的结果是申请到的空间的地址 • 当系统空间用完时,new操作可能失败 • new操作失败时,返回空指针
动态内存分配与回收 //动态分配检查 int main() {int *p; p = new int; if(!p) { cout << "allocation failure\n"; return 1;} *p = 20; cout << *p; delete p; return 0; }
assert宏 • assert()宏在标准头文件cassert中 • assert()有一个参数,表示断言为真的表达式,预处理器产生测试该断言的代码。如果断言不是真,则在发出一个错误消息后程序会终止。
#include <iostream> #include <cassert> //包含assert宏的头文件 using namespace std; int main() { int *p; p = new int; assert (p != 0); //p等于0,则退出程序 *p=20; cout << *p; delete p; return 0; }
内存分配的进一步介绍 • 静态分配:对全局变量和静态变量,编译器为它们分配空间,这些空间在整个程序运行期间都存在 • 自动分配:函数内的局部变量空间是分配在系统的栈工作区。当函数被调用时,空间被分配;当函数执行结束后,空间被释放 • 动态分配:在程序执行过程中需要新的存储空间时,可用动态分配的方法向系统申请新的空间,当不再使用时用显式的方法还给系统。这部分空间是从被称为堆的内存区域分配。
动态变量是通过指针间接访问的。如果该指针被修改,这个区域就被丢失了。堆管理器认为你在继续使用它们,但你不知道它们在哪里,这称为内存泄露。动态变量是通过指针间接访问的。如果该指针被修改,这个区域就被丢失了。堆管理器认为你在继续使用它们,但你不知道它们在哪里,这称为内存泄露。 • 为了避免出现孤立的区域,应该明白地告诉堆管理器这些区域不再使用。可以采用delete操作,它释放由new申请的内存。 • 当释放了内存区域,堆管理器重新收回这些区域,而指针仍然指向堆区域,但不能再使用指针指向的这些区域。 • 要确保在程序中同一个区域释放一次。 • 释放内存对一些程序不重要,但对有些程序很重要。如果你的程序要运行很长时间,而且存在内存泄漏,这样程序会耗尽所有内存,直至崩溃。 内存泄漏
动态空间分配示例 • 输入一批数据,计算它们的和。数据个数在设计程序时尚无法确定。 • 存储一批数据应该用数组,但C++语言的数组大小必须是固定的。该问题有两个解决方案: • 开设一个足够大的数组,每次运行时只使用一部分。缺点:浪费空间 • 用动态内存分配根据输入的数据量申请一个动态数组
#include <iostream> using namespace std; int main() { int *p, i, n, sum=0; cout << "An array will be created dynamically.\n\n" ; cout << "Input an array size n followed by n integers:"; cin >> n; if (!(p = new int [n])) exit(1); for (i=0; i<n; ++i) cin >> p[i]; for(i=0; i<n; ++i) sum += p[i]; delete [] p; cout << "Number of elements:" << n << endl; cout << "Sum of the elements:" << sum << endl; return 0; } 可改为: p = new int [n]; assert( p != NULL);
第7章 间接访问—指针 • 指针的概念 • 指针运算与数组 • 动态内存分配 • 字符串再讨论 • 指针作为函数参数和返回值 • 指针数组与多级指针 • 指向多维数组的指针 • 指向函数的指针
字符串再讨论 • 字符串的另一种表示是定义一个指向字符的指针。然后直接将一个字符串常量或字符串变量赋给它 • 如 char *String, ss[ ] =“abcdef”; String = “abcde”; String = ss;
OS Program “abcde” 数据段 或代码区 String 栈 堆 String = “abcde”;的执行结果 • 字符串常量存储在一个称为数据段的内存区域里 • 将存储字符串”abcde”的内存的首地址赋给指针变量String。
OS Program 数据段 String “abcdef\0” 栈 堆 String = ss的执行过程 将字符数组ss的起始地址存入String
OS Program 数据段 String 栈 “aaa\0” 堆 String = new char[5];strcpy(String, “aaa”) • 动态变量存储在堆工作区 • 将存储字符串”aaa”的内存的首地址赋给指针变量String。
用指针表示的字符串的操作 • 可以直接作为字符串操作函数的参数。但必须注意,如果该指针指向的是一个字符串常量时,则使用是受限的。如不能作为strcpy的第一个参数 • 由于在C++中,数组名被解释成指向数组首地址的指针。因此,字符串是用一个指针变量表示,我们可以把此指针变量解释成数组的首地址,通过下标访问字符串中的字符。如string[3]的值是d。
用指针处理串 • 目的:编写一个记录串中单词的个数的函数。 • 关键技术:要传递一个字符串给函数
字符串作为函数的参数 • 字符串作为函数的参数和数组名作为参数传递一样,可以有两种方法 • 作为字符数组传递 • 作为指向字符的指针传递 • 两种传递方式的本质是一样的,都是传递了字符串的首地址 • 字符串作为字符数组传递时不需要指定长度。因为字符串操作的结束是依据‘\0’
统计字符串中单词数的函数 #include <ctype> Using namespace std; int word_cnt(const char *s) { int cnt = 0; while (*s != '\0') { while (isspace(*s)) ++s; //跳过空白字符 if (*s != '\0') { ++cnt; //找到一个单词 while (!isspace(*s) && *s != '\0') ++s; //跳过单词 } } return cnt; }
第7章 间接访问—指针 • 指针的概念 • 指针运算与数组 • 动态内存分配 • 字符串再讨论 • 指针作为函数参数和返回值 • 指针数组与多级指针 • 指向多维数组的指针 • 指向函数的指针
指针作为函数参数和返回值 • 指针作为函数参数 • 数组名作为函数参数 • 返回指针的函数 • 引用和引用传递 • 返回引用的函数
指针作为函数参数 例:编一函数,交换二个参数值。 新手可能会编出如下的函数: void swap(int a, int b) { int c; c=a; a=b; b=c; } 希望通过调用swap(x, y)交换变量x和y的值 因为C++采用的是值传递机制,函数中a、b值的交换不会影响实际参数x和y的值
x y 3 4 a b 正确的方法 void swap(int *a, int *b) { int c; c=*a; *a= *b; *b=c; } 交换x和y的值,可以调用swap(&x, &y) 用指针作为参数可以在函数中修改主调程序的变量值,即实现变量传递。必须小心使用!!!
能解一元二次方程的函数 • 目前为止我们了解到的函数只能有一个返回值,由return 语句返回。 • 一个一元二次方程有二个解,如何让此函数返回二个解。答案是采用指针作为函数的参数。 • 由调用程序准备好存放两个根的变量,将变量地址传给函数。在函数中将两个根的值分别放入这两个地址
函数原型 • 函数原型可设计为: void SolveQuadratic(double a, double b, double c, double *px1, double *px2) • 函数的调用 SolveQuadratic(1.3, 4.5, 2.1, &x1, &x2) SolveQuadratic(a, b, c, &x1, &x2) • 函数的参数有两类:输入参数和输出参数。一般,输入参数用值传递,输出参数用指针传递。在参数表中,输入参数放在前面,输出参数放在后面。
原型的改进 • 并不是每个一元二次方程都有两个不同根,有的可能有两个等根,有的可能没有根。函数的调用者如何知道x1和x2中包含的是否是有效的解? • 解决方案:让函数返回一个整型数。该整型数表示解的情况
完整的函数 int SolveQuadratic(double a,double b,double c, double *px1,double *px2) { double disc, sqrtDisc; if(a == 0) return 3; //不是一元二次方程 disc = b * b - 4 * a * c; if( disc < 0 ) return 2; //无根 if ( disc == 0 ) { *px1 = -b /(2 * a); return 1;} //等根 //两个不等根 sqrtDisc = sqrt(disc); *px1 = (-b + sqrtDisc) / (2 * a); *px2 = (-b - sqrtDisc) / (2 * a); return 0; }