510 likes | 625 Views
第六讲 指针与字符串. 内容提要. 指针 动态存储分配 字符串. 指针. 什么是指针 指针的定义与运算 指针与一维数组 指针数组 行指针与二维数组 指针与引用 指针与函数. 指针定义. 什么是指针. 指针变量,简称指针,用来存放其它变量的 内存地址. 指针的定义. 类型标识符 * 指针变量名. 声明一个指针类型的变量,星号后面可以留空格 类型标识符 表示该指针 所指向的对象 的数据类型,即该指针所代表的内存单元所能存放的数据的类型. Tips : 变量为什么要声明?
E N D
内容提要 • 指针 • 动态存储分配 • 字符串
指针 • 什么是指针 • 指针的定义与运算 • 指针与一维数组 • 指针数组 • 行指针与二维数组 • 指针与引用 • 指针与函数
指针定义 • 什么是指针 • 指针变量,简称指针,用来存放其它变量的内存地址 • 指针的定义 类型标识符* 指针变量名 • 声明一个指针类型的变量,星号后面可以留空格 • 类型标识符表示该指针所指向的对象的数据类型,即该指针所代表的内存单元所能存放的数据的类型 Tips:变量为什么要声明? 1) 分配内存空间; 2) 限定变量能参与的运算及运算规则。 Tips:内存空间的访问方式:1) 变量名; 2) 内存地址,即指针。
指针运算 • 指针的两个基本运算 • 提取变量的内存地址:& • 提取指针所指向的变量的值:* • 地址运算符:& &变量名// 提取变量在内存中的存放地址 例: int x=3; int * px; //定义指针 px px=&x; //将 x 的地址赋给指针 px • 此时,我们通常称这里的 px是指向 x的指针 • 注意:指针的类型必须与其指向的对象的类型一致
指针运算 • 指针运算符: * *指针变量名// 提取指针变量所指向的对象的值 例: int x; int * px; //声明指针变量,星号后面可以有空格! px=&x; *px = 3; //等价于 x = 3,星号后面不能有空格! ex06_pointer_01.cpp 在使用指针时,我们通常关心的是指针指向的元素! • 初始化:声明指针变量时,可以赋初值 例: int x = 3; int * px = &x; //指针的值只能是某个变量的地址
空指针 • void 类型的指针 void * 指针名 • void 类型的指针可以指向任何类型的对象的地址 • 不允许使用 void 指针操纵它所指向的对象! • 通过显式类型转换,可以访问 void 类型指针所指向的对象。 例: int x = 3; int * px; void * pv; pv = &x; // OK, void 型指针指向整型变量 Px = (int *)pv; // OK,使用 void 型指针时需要强制类型转换
指针赋值 • 指针可能的取值 int x=3; int * px=&x; int * py=&x+1; • 一个有效的指针只有三种取值: (1)一个对象的地址; (2) 指向某个对象后面的对象; (3) 值为 0 或 NULL(空指针)。 int * pi; pi=0; // OK pi=NULL; // OK 没有初始化或赋值的指针是无效的指针, 引用无效指针会带来难以预料的问题! • 指针赋值:只能使用以下四种类型的值 (1) 0 或者值为 0 的常量,表示空指针; (2) 类型匹配的对象的地址; (3)同类型的另一有效指针; (4) 另一个对象的下一个地址。
指针与常量 • 指向常量的指针 const 类型标识符 * 指针名 const int a = 3; int * pa = &a; // ERROR const int * cpa = &a; // OK • 指向 const 对象(常量)的指针必须用 const 声明! • 这里的 const 限定了指针所指对象的属性,不是指针本身的属性! const int a = 3; int b = 5; const int * cpa = &a; // OK *cpa = 5; // ERROR cpa = &b; // OK *cpa = 9; // ERROR b = 9; // OK ex06_pointer_02.cpp 指向 const 的指针所指对象的值并不一定不能修改! • 允许把非 const 对象的地址赋给指向 const 的指针; • 但不允许使用指向 const 的指针来修改它所指向的对象的值!
指针与常量 • 常量指针,简称常指针 常量指针:指针本身的值不能修改 类型标识符 * const 指针名 int a = 3, b = 5; int * const pa = &a; // OK pa = &b; // ERROR • 指向 const 对象的 const 指针 const 类型标识符 * const 指针名 指针本身的值不能修改,其指向的对象的值也不能修改
指针算术运算 指针可以和整数或整型变量进行加减运算, 且运算规则与指针的类型相关! int * pa; int k; pa + k --> pa 所指的当前位置之后第 k 个元素的地址 pa - k --> pa 所指的当前位置之前第 k 个元素的地址 • 在指针上加上或减去一个整型数值 k,等效于获得一个新指针,该指针指向原来的元素之后或之前的第 k 个元素 • 指针的算术运算通常是与数组的使用相联系的 • 一个指针可以加上或减去 0,其值不变 int * pa; int k; pa++ --> pa 所指的当前位置之后的元素的地址 pa-- --> pa 所指的当前位置之前的元素的地址
指针算术运算 short * pa // 每个元素占两个字节 pa-2 *(pa-2) pa-1 *(pa-1) pa *pa pa+1 *(pa+1) pa+2 *(pa+2) pa+3 *(pa+3)
指针算术运算 pb-1 int * pb //每个元素占四个字节 *(pb-1) pb *pb pb+1 *(pb+1) pb+2 *(pb+2)
指针与数组 C++中,指针与数组密切相关:由于数组元素在内存中是连续存放的,因此使用指针可以非常方便地处理数组元素! int a[]={0,2,4,8}; int * pa; pa = a; // OK pa = &a[0]; // OK, 与上式等价 *pa = 3;// OK,等价于 a[0]=3 *(pa+2) = 5; // OK,等价于 a[2]=5 *(a+2) = 5; // OK,等价于 a[2]=5 • 在 C++ 中,数组名就是数组的首地址! • 当数组名出现在表达式中时,会自动转化成指向第一个数组元素的指针! 思考:pa = a+1,则 *pa = ?
一维数组与指针 • 一维数组与指针 • 在 C++ 中,引用数组元素有以下三种方式: (1) 数组名与下标,如:a[0] (2) 数组名与指针运算,如:*(a+1) (3) 指针,如:int * pa=a; *pa int a[]={0,2,4,8}; int * pa = a; *pa = 1; // 等价于 a[0]=1 *(pa+2) = 5; // 等价于 a[2]=5 *(a+2) = 5; // OK,等价于 a[2]=5 *(pa++) = 3; // OK,等价于 a[0]=3; pa = pa+1; *(a++) = 3; // ERROR! a代表数组首地址,是常量指针! *(pa+1) = 10; // 思考:修改了哪个元素的值? ex06_pointer_03.cpp • 指针的值可以随时改变,即可以指向不同的元素; • 数组名是常量指针,值不能改变。
举例 例:使用三种方法输出一个数组的所有元素 // 第一种方式:数组名与下标 for (int i=0; i<n; i++) cout << a[i] << "," ; // 第二种方式:数组名与指针运算 for (int i=0; i<n; i++) cout << *(a+i) << "," ; // 第三种方式:指针 for (int * pa=a; pa<a+n; pa++) cout << *pa << "," ; 若pa是指针,k是整型数值,则 *(pa+k)可以写成 pa[k] *(pa-k)可以写成 pa[-k] // 第三种方式:指针 for (int * pa=a, i=0; i<n; i++) cout << pa[i] << "," ; ex06_pointer_04.cpp
一维数组与指针 • 一维数组 a[n]与指针 pa=a • 数组名 a是地址常量,数组名 a与 &a[0]等价; • a+i是 a[i]的地址,a[i]与 *(a+i)等价; • 数组元素的下标访问方式也是按地址进行的; • 可以通过指针 pa访问数组的任何元素,且更加灵活; • pa++或 ++pa合法,但 a++不合法; • *(pa+i)与 pa[i]等价,表示第 i+1的元素; a[i]<=>pa[i]<=>*(pa+i)<=>*(a+i)
指针数组 • 指针数组:数组的元素都是指针变量 • 指针数组的声明: 类型标识符 * 指针数组名[n] int a[]={0,2,4,8}; int b[]={1,3,5,7}; int c[]={2,3,5,8}; int *pa[3]={a,b,c}; // 声明一个有三个元素的指针数组 // pa[0]=a, pa[1]=b, pa[2]=c 例:使用三种方法引用数组元素 ex06_pointer_05.cpp
二维数组 C++中,二维数组是按行顺序存放在内存中的,可以理解为一维数组组成的一维数组。 例:int A[2][3]={{1,2,3},{7,8,9}}; A[0] —— A00 A01 A02 A[1] —— A10 A11 A12 可以理解为: A A[0] A[1] (第一行的首地址) (第二行的首地址) A[0], A[1] 称为行数组
二维数组与指针 int A[2][3]={{1,2,3},{7,8,9}}; int * pa = A[0]; // 行数组A[0]的首地址, 等价于 pa=&A[0][0] int * pa1 = A[1]; // 行数组A[1]的首地址,即 &A[1][0] *pa = 11; // 等价于 A[0][0]=11 *(pa+2) = 12; // 等价于 A[0][2]=12 *(pa+4) = 13; // 等价于 A[1][1]=13 ex06_pointer_06.cpp • 引用二维数组的元素可以通过数组名和指针运算进行 for (int i=0; i<m; i++) { for (int j=0; j<n; j++) cout << *(*(A+i)+j); cout << "\n"; } ex06_pointer_07.cpp
二维数组与指针 对于二维数组 A,虽然 A、A[0]都是数组首地址,但二者指向的对象不同:A[0]是一维数组的名字,它指向的是行数组 A[0]的首元素,对其进行 “*” 运算时,得到的是 A[0]的首元素的值,即 *A[0]与 A[0][0]是相同的; 而 A是一个二维数组的名字,它指向的是它的首元素,而它的元素都是一维数组(即行数组),因此,它的指针移动单位是 “行”,所以 A+i指向的是第 i个行数组,即指向 A[i]。对 A进行 “*” 运算时,得到的是一维数组A[0]的首地址,即 *A与 A[0]是同一个值。 当用 int * p声明指针 p时,p指向的是一个 int型数据,而不是一个地址,因此,用 A[0]对 p赋值是正确的,而用 A对 p赋值是错误的! int A[2][3]={{1,2,3},{7,8,9}}; int * pa = A; // ERROR! int * pa = A[0]; // OK 设指针 pa=&A[0][0],则 A[i][j]<=>*(pa+n*i+j)
行指针 • 行指针/指向一维数组的指针: 以一维数组为基本类型,即将一维数组看作一个整体 类型标识符 (* 指针名)[n] 该指针以长度为 n 的一维数组为基本单位 int A[3][4]; int (* pA)[4] = A; // OK!注意:不能写成 (*pA)[3]! for (int i=0; i<m; i++) { for (int j=0; j<n; j++) cout << *(*(pA + i) + j ); cout << "\n"; } ex06_pointer_08.cpp
二维数组与行指针 • 二维数组 A[m][n] • 数组名 A 是行地址常量,数组名 A 与 &A[0][0]等价; • A+i=&A[i], *(A+i)=A[i]=&A[i][0]; A[i][j]<=>*(A[i]+j)<=>*(*(A+i)+j)<=> (*(A+i))[j] <=>*(&A[0][0]+n*i+j) • 二维数组 A[m][n]与行指针 (*pA)[n]=A • pA=A, A+i=pA+i, *(A+i)+j=*(pA+i)+j; A[i][j]<=>pA[i][j]<=>*(*(pA+i)+j)
二级指针(略) • 二级指针:指向指针的指针变量 类型标识符 ** 指针名 二级指针存放的是另一个指针的地址 int a = 3; int *pa = &a; int **ppa = &pa; // OK!注意:不能写成 (*pA)[3]! ex06_pointer_09.cpp 注:二级指针不做要求。
指针与引用 int a = 3; int * pa = &a; // 指针 int & ra = a; // 引用 • 引用与指针 • 引用是变量的别名; • 引用必须初始化,且不能修改; • 引用只针对变量,函数没有引用; • 传递大量数据时,最好使用指针; • 用引用能实现的功能,用指针都能实现。 • 引用作为函数参数的优点 • 传递方式与指针类似,但可读性强; • 函数调用比指针更简单、安全;
指针作为函数参数 • 指针作为函数参数 • 以地址方式传递数据。 • 形参是指针时,实参可以是指针或地址。 void split(double x, int * n, double * f) double x, x2; int x1; split(x, &x1, &x2) ex06_pointer_10.cpp 当函数间需要传递大量数据时,开销会很大。此时,如果数据是连续存放的,则可以只传递数据的首地址,这样就可以减小开销,提高执行效率!
指针作为函数参数 • 指针作为函数参数的三个作用 • 使形参和实参指向共同的内存地址; • 减小函数间数据传递的开销; • 传递函数代码的首地址(后面介绍)。 Tips: 如果在被调函数中不需要改变指针所指向的对象的值,则可以将形参中的指针声明为指向常量的指针。
指针型函数 当函数的返回值是地址时,该函数就是指针型函数 • 指针型函数的定义 数据类型 * 函数名(形参列表) { 函数体 }
指向函数的指针 在程序运行过程中,不仅数据要占用内存空间,函数也要在内存中占用一定的空间。函数名就代表函数在内存空间中的首地址。用来存放这个地址的指针就是指向该函数的指针。 • 函数指针的定义 数据类型 (* 函数指针名)(形参列表) • 这里的数据类型和形参列表应与其指向的函数相同 注:函数名除了表示函数的首地址外,还包括函数的返回值类型,形参个数、类型、顺序等信息。 Tips:可以象使用函数名一样使用函数指针。
函数指针 • 函数指针需要赋值后才能使用 int Gcd(int x, int y); int Lcm(int x, int y); int (*pf)(int, int); // 声明函数指针 pf = Gcd; // pf 指向函数 Gcd cout << "最大公约数:" << pf(a,b) << endl; pf = Lcm; // pf 指向函数 Lcm cout << "最小公倍数:" << pf(a,b) << endl; ex06_pointer_11.cpp
内容提要 • 指针 • 动态存储分配 • 字符串
动态存储分配 若在程序运行之前,不能够确切知道数组中元素的个数,如果声明为很大的数组,则可能造成浪费,如果声明为小数组,则可能不够用。此时需要动态分配空间,做到按需分配。 • 动态内存分配相关函数 • 申请内存空间:new • 释放内存空间:delete 每个程序在执行时都会占用一块可用的内存,用于存放动态分配的对象,此内存空间称为自由存储区(free store)或堆(heap)。
申请内存空间 • 申请单个存储单元 px = new数据类型; px = new数据类型(初始值); 申请用于存放指定数据类型数据的内存空间,若申请成功,则返回该内存空间的地址,并赋值给指针 px; 若申请不成功,则返回 0 或 NULL。 deletepx; // 释放由 new 申请的内存空间 注:px必须是由 new操作的返回值!
动态创建数组 • 创建一维数组 ex06_pointer_new01.cpp px = new数据类型[数组长度]; px = new数据类型[数组长度](); // 赋初值 0 这里初始化时只能将全部元素设置为 0,而不能象数组变量那样用初始化列表赋初值(C++11新标准已加入该功能) delete[]px; // 释放由 new 建立的数组 • 创建多维数组 px = new数据类型[n1][n2]...[nm]; 注:此时 px不是普通的指针,而是: (* px)[n2]...[nm]
for(i=0; i<k && pa[i]<=sqrt(n); i++) if (n%pa[i] == 0) {flag = 1; break;} 动态存储举例 例:给定一个正整数 N,求出 N 个最小的素数 int main() { int N; cout << "Input N: "; cin >> N; int *pa = new int[N]; // 申请内存空间 int i, flag, k=0, n=2; while (k < N)// 寻找 N个素数 { flag = 0; for(i=2; i<n; i++) if (n % i == 0) {flag = 1; break;} if (flag==0) { pa[k] = n; k++; } n++; } ex06_pointer_new02.cpp
内容提要 • 指针 • 动态存储分配 • 字符串
字符串 • 字符串的表示:字符数组 charstr[5]={'m','a','t','h','\0'}; charstr[5]="math";// OK charstr[]="math";// OK,只能用于初始化 • 字符串以 "\0"为结束标志 • 使用双引号时,会自动在最后添加结束标志 例: char str[20]="C++ and Matlab"; for(int i=0;i<20;i++) if (str[i]!='\0') cout << str[i] << endl; else break; ex06_str_print.cpp char str[5]; str = "Math"; // ERROR:一维数组,不能直接赋值!
字符串输入输出 • 字符串的输出 例: charstr[5]="math"; cout << str; • 输出字符中不含 "\0" • 可使用下标输出单个字符 • 字符串的输入 例: charstr[5]; cin >> str; • 输入单个字符串时,中间不能有空格 • 一次输入多个字符串时,以空格隔开 ex06_str_01.cpp
字符串输入输出 例: charstr1[5], str2[5], str3[5]; cin >> str1 >> str2 >> str3; 运行时输入数据:How are you? str1: str2: str3: 内存中变量状态如下: 例: charstr[13];cin >> str; 运行时输入数据:How are you? ex06_str_02.cpp 内存中变量状态如下: str1:
字符串输入 cin.getline(str,N,结束符); • 连续读入多个字符(可以有空格),直到读满 N-1个,或遇到指定的结束符 • 不存储结束符 • 结束符可以省略,默认为 '\n'(换行) 例: charstr[13]; cin.getline(str,13); ex06_str_03.cpp • 单个字符的输入 getchar(); charch; ch=getchar();
字符串操作 • 字符串相关函数 (需包含头文件 cstring和 cstdlib ) (更多函数见 http://www.cppreference.com)
字符串操作 strlen(str) • 返回字符串 str1的长度(不含结束符) strcat(str1,str2) • 将 str2的全部内容添加到 str1中,str2的内容保留 strncat(str1,str2,n) • 将 str2的内容添加到 str1中,至多添加 n个字符 strcmp(str1,str2) • 按字典顺序比较 str1和 str2的大小 • 如果 str1>str2,则返回一个正数;str1<str2,则返回一个负数;相等则返回 0
字符串操作 strncmp(str1,str2,n) • 按字典顺序比较 str1和 str2的前 n个字符的大小 strcpy(str1,str2) • 将 str2的全部内容复制到 str1中; • str1的长度应该不小于 str2的长度。 strncpy(str1,str2,n) • 将 str2的前 n个字符复制到 str1中; • 若n大于str2的长度,则复制全部内容。 ex06_str_04.cpp 例: int N = 20; char str1[N]; char str2[]="hello world!"; strncpy(str1,str2,N-1); // 实际复制字符个数不超过 str2 长度
字符串操作 x=atoi(str) x=atol(str) x=atof(str) • 分别将 str转化为整型、长整型和双精度型数据 • str必须是由数字组成的字符串,否则结果为 0 例: int x; double y; x=atoi("66"); // x=66 y=atof("14.5"); // y=14.5 ex06_str_05.cpp itoa(int,str,radix) • 按指定的进制将一个整数转化为字符串 例: char str[5]; itoa(66,str,16); // 按16进制转换
字符检测 • C++字符检测函数 (头文件 cctype)
字符检测 • C++字符转换函数 (头文件 cctype) 以上检测和转换函数只针对单个字符,而不是字符串!
字符与整数 • 字符与整型数据之间的转换 例: char x='2'; int y=x; int z=x-'0'; cout << "x=" << x << endl; // x='2' 是字符 cout << "y=" << y << endl; // y=50 是整数 cout << "z=" << z << endl; // z=50-48=2 是整数 • 字符数据与整型数据之间的转换是通过 ASCII 码实现的 • 字符参加算术运算时,自动转换为整数 • atoi等只能作用在字符串上!不能作用在字符上!
课后练习 • 课后练习(自己练习) (1) 教材第 191 页: 7.2, 7.3, 7.4, 7.5, 7.7, 7.8, 7.9, 7.10, 7.11, 7.17, 7.20, 7.21, 7.22 (2) 已知一个数组,数组名为 x,试用一条语句算出该数组的元素个数 (提示:使用 sizeof 函数) (3) 阅读下面的代码 int a[]={6,21,12,34,55}; int * pa = a, * pb; *pa = 5; *(pa+2) = 8; *(pa++) = *a + 42; pb = pa; *(++pb) = 45; 指出最后 a 的值,*pa 的值,*pb 的值
上机作业 1) 有 17 人围成一圈,编号1~17,从1号开始报数,报到3的倍数的人离开,一直数下去,直到最后只剩一人,求此人编号。程序取名 hw06_01.cpp 2) 编写函数,交换两个双精度变量的值,分别用引用和指针实现。 函数名分别为swap01和 swap02,并在主函数中定义两个双精度变量, 从键盘接受输入,并将交换后的值在屏幕上输出。(程序名 hw06_02.cpp) void swap01(double & ra, double & rb); void swap02(double * pa, double * pb); 3) 求最小的前 100 个素数,存放在数组 p 中,并分别使用下列方式在屏幕上输出 p,每行输出 10 个,程序取名为 hw06_03.cpp方式一:数组名+下标运算; 方式二:数组名+指针运算; 方式三:指针+指针运算
上机作业 4) 编写函数,计算两个矩阵的乘积 Z=X*Y。其中 XRmp,YRpn, ZRmn,要求函数对任意的正整数 m,p,n 都能实现矩阵相乘。 void matrix_prod(double * px, double * py, double * pz, int m, int p, int n); 提示:这里 px, py, pz分别指向 X[0][0], Y[0][0]和 Z[0][0] 程序取名为 hw06_04.cpp 5) 生成一个 6 阶矩阵 T(定义见右方),并分别使用下列方式按矩阵形式输出:方式一:数组名+下标运算; 方式二:数组名+指针运算; 方式三:行指针+指针运算; (定义矩阵时,要使用循环实现, 程序名 hw06_05.cpp)