程序设计与算法 | (15) 指针

本专栏主要基于北大郭炜老师的程序设计与算法系列课程进行整理,包括课程笔记和OJ作业。该系列课程有三部分: (一) C语言程序设计;(二) 算法基础;(三) C++面向对象程序设计

(一) C语言程序设计 课程链接

1. 指针的概念

基本概念

  • 每个变量都被存放在从某个内存地址(以字节为单位)开始的若干个字节中(如int类型4个字节,char类型1个字节等)
  • “指针”,也称作“指针变量”,大小为4个字节(或8个字节)的变量, 其内容代表一个内存地址(首地址)。
  • 通过指针,能够对该指针指向的内存区域(首地址开始的若干个字节)进行读写。
  • 如果把内存的每个字节都想像成宾馆的一个房间,那么内存地址相当于就是房间号,而指针里存放的,就是房间号。

定义

类型名 * 指针变量名

int * p; // p 是一个指针,变量 p的类型是 int 
char * pc; // pc 是一个指针, 变量 pc 的类型是 char *
float *pf; // pf 是一个指针,变量 pf 的类型是 float *

内容

int * p = (int *)40000  //4000是int类型,强制类型转换为 int * 类型

p是int *类型的指针。
其内容存储的是一个地址:
十进制 40000
十六进制 0x9C40
二进制每个比特 0000 0000 0000 0000 1001 1100 0100 0000

p指向地址40000(首地址),地址p就是地址40000
*p 就代表地址40000开始处的若干个字节(p是int *的话,就是4个字节)的内容

通过指针访问其指向的内存空间

int * p = ( int * ) 40000; //p是int*类型 其存储地址40000,即p指向地址40000(首地址)
*p = 5000; //往地址40000处起始的若干个字节的内存空间里写入 5000
int n = *p; //将地址40000处起始的若干字节的内容赋值给 n

“若干” = sizeof(int)=4,因为p是 int * 类型;
在这里插入图片描述

指针定义总结

T * p ; //T可以是任何类型的名字,比如int,double,char等等。

p 的类型: T *
*p 的类型: T

通过表达式 * p,可以读写从地址p开始的 sizeof(T)个字节

*p 等价于存放在地址p处的一个 T 类型的变量

间接引用运算符
sizeof(T
) 4字节(64位计算机上可能8字节) (char、double、int类型变量所占的字节是不同的,但是char*,double*,int*这些指针类型变量所占的字节是相同的)

2. 指针的用法

用法

char ch1 = 'A';
char * pc = &ch1; //使得pc指向变量ch1 (pc存储/指向变量ch1的首地址)

& : 取地址运算符
&x : 变量x的地址(即指向x的指针)
对于类型为 T 的变量 x,&x 表示变量 x 的地址(即指向x的指针) ,&x 的类型是 T *。

之前我们把int类型变量40000强制类型转换为int*,并赋值给一个int*类型的指针变量p,这只是理论上的用法,实际这样使用很可能会报错,因为地址40000未必能访问,一般像上面这么做。

char ch1 = 'A';
char * pc = &ch1;  //使得pc 指向变量ch1(的首地址)
* pc = 'B';        // 使得ch1 = 'B'
char ch2 = * pc;  // 使得ch2 = ch1
pc = & ch2;      // 使得pc 指向变量ch2
* pc = 'D';      // 使得ch2 = 'D'

上面这段代码,完全可以不用指针就能操作,用指针感觉有点画蛇添足,那么指针的作用是什么呢?

指针的作用

有了指针,就有了自由访问内存空间的手段

  • 不需要通过变量,就能对内存直接进行操作。通过指针,程序能访 问的内存区域就不仅限于变量所占据的数据区域
  • 在C++中,用指针p指向a的地址,然后对p进行加减操作,p就能指 向a后面或前面的内存区域,通过p也就能访问这些内存区域(可以写一些病毒或反病毒程序等)

指针的互相赋值

不同类型的指针,如果不经过强制类型转换,不能直接互相赋值。

 int * pn, char * pc, char c = 0x65;
 pn = pc; //类型不匹配,编译出错
 pn = & c; //类型不匹配,编译出错
 pn = (int * ) & c; 
 int n = * pn;  //n值不确定
 * pn = 0x12345678;//编译能过但运行可能出错

pn指向变量c的首地址,而pn是int*类型,pn表示从首地址开始的sizeof(int)=4个字节的内存区域中的内容,而原始的变量c是char类型,只占一个字节,其余三个字节未知,所以n的值是不确定的。 如果对pn表示的这块内存区域赋值的话,运行可能出错,因为其余三个字节的地址可能不能访问。

3. 指针的运算

1)两个同类型的指针变量,可以比较大小

地址p1<地址p2,等价于 p1< p2 值为真。 (地址p1,就是指针p1中存储的首地址或指针p1指向的首地址)
地址p1=地址p2,等价于 p1== p2 值为真
地址p1>地址p2,等价于 p1 > p2 值为真

2)两个同类型的指针变量,可以相减

两个T * 类型的指针 p1和p2
p1 – p2 = ( 地址p1 – 地址 p2 ) / sizeof(T) (p1、p2之间可存放多少个T类型的变量)

例:int * p1, * p2;
若 p1 指向地址 1000,p2 指向地址 600, 则
p1 – p2 = (1000 – 600)/sizeof(int) = (1000 – 600)/4 = 100

3)指针变量加减一个整数的结果是指针
p: T*类型的指针
n : 整数类型的变量或常量
p+n : T * 类型的指针,指向地址: 地址p + n × sizeof(T)
n+p, p-n , *(p+n), *(p-n) 含义自明

4)指针变量可以自增、自减

T* 类型的指针p指向地址n
p++, ++p : p指向 n + sizeof(T)
p–, --p : p指向 n - sizeof(T)

5)指针可以用下标运算符“[ ]”进行运算
p 是一个 T * 类型的指针,
n 是整数类型的变量或常量

p[n] 等价于 *(p+n)

通过指针实现自由内存访问

如何访问int型变量 a 前面的那一个字节?

int a;
char * p = (char * ) &a; //&a是int*类型 不能直接用int*来做,--p指向的是上一个int变量(4个字节)的首地址,不是上一个字节的地址。所以要强制类型转换为char*
--p; //--p指向的是上一个char变量的首地址,而char类型是一个字节,所以也就是上一个字节的地址
printf("%c", * p); //可能导致运行错误 
* p = 'A'; //可能导致运行错误

指针运算示例

#include <iostream>
using namespace std;
int main() 
{
	int * p1, * p2; int n=4;
	char * pc1, * pc2;
	p1 = (int *) 100; //地址p1为100 (p1中存储地址100,p1指向地址100)
	p2 = (int *) 200; //地址p1为200
	cout<< "1) " << p2 - p1 << endl; //(地址p2-地址p1)/sizeof(int)
	//输出 1) 25, 因(200-100)/sizeof(int) = 100/4 = 25 p1、p2间存储了多少个int型变量
	pc1 = (char * ) p1; //地址pc1为100
	pc2 = (char * ) p2; //地址pc2为200
	cout<< "2) " << pc1 - pc2 << endl; //-100 (地址pc1-地址pc2)/sizeof(char)
	//输出 2) -100,因为(100-200)/sizeof(char) = -100
	cout<< "3) " << (p2 + n) - p1 << endl; //输出 3) 29 
	//((地址p2+n*sizeof(int))-地址p1)/sizeof(int) = 29 即p2+n、p1之间存储了多少个int型变量
	int * p3 = p2 + n; // p2 + n 是一个指针,可以用它给 p3赋值
	cout<< "4) " << p3 - p1 << endl; //输出 4) 29
	cout<< "5) " << (pc2 - 10) - pc1 << endl; //输出 5) 90
	return 0;
}

4. 空指针

  • 地址0不能访问。指向地址0的指针就是空指针
  • 可以用“NULL”关键字对任何类型的指针进行赋值。NULL实际上就是整数0,值为NULL的指针就是空指针:
int * pn = NULL; char * pc = NULL; int * p2 = 0;
  • 指针可以作为条件表达式使用。如果指针的值为NULL,则相当于为 假,值不为NULL,就相当于为真
    if§等价于if(p!=NULL) if(!p)等价于if( p==NULL )

指针作为函数参数

#include <iostream>
using namespace std;
void Swap( int *p1, int * p2) 
{
	int tmp = *p1; //将p1指向的变量的值,赋给tmp
	*p1 = *p2; //将p2指向的变量的值,赋给p1指向的变量
	*p2 = tmp; //将tmp 的值赋给p2指向的变量的值。
}
int main() 
{
	int m = 3,n = 4;
	Swap( &m, &n); //&m是m的(首)地址,使得p1指向m(的首地址);p2指向n
	cout << m << " " << n << endl; //输出 4 3 return 0;
}

之前我们遇到过这个例子,如果实参只传递m,n ,则不能完成交换,因为形参只是实参的一个拷贝,只是形参交换了,实参并未交换。当时说数组和引用除外。实际上没有例外,形参就是实参的一个拷贝,只不过这里我们传递的实参是m,n的地址&m,&n,&m,&n分别指向m,n,形参是实参的一个拷贝,因此p1,p2也分别指向m,n,*p1,p2是p1,p2所指向变量的值,对p1,*p2操作也就是改变了变量的值,因此最终可以完成交换。

C++还可以用引用来实现交换,C语言中只能用指针,实际上后面我们会看到,数组名也是一个指针,数组名是数组的首地址,指针指向数组的首地址。

5. 指针和数组

  • 数组的名字是一个指针常量 指向数组的起始地址

T a[N];

  • a的类型是 T *
  • 可以用a给一个T * 类型的指针赋值 (能做右值)
  • a是编译时其值就确定了的常量,不能够对a进行赋值(不能做左值)
  • 作为函数形参时, T *p 和 T p[ ] 等价
void Func( int * p) { cout << sizeof(p);} //指向数组,但不知道有多少个元素 sizeof(p)就是指针变量的大小
void Func( int p[]) { cout << sizeof(p);}
#include <iostream>
using namespace std;
int main() 
{
	int a[200];int * p ;
	p = a; // p指向数组a的起始地址,亦即p指向了a[0](的首地址)
	* p = 10; //a[0] = 10
	* (p+1) = 20; //a[1] = 20
	p[0] = 30; //p[i]等价于 *(p+i)   a[0]=30
	p[4] = 40;  //a[4]=40
	for( int i = 0;i < 10; ++i) //对数组a的前10个元素进行赋值
		*( p + i) = i;
	++p;  // p指向 a[1]
	cout << p[0] << endl; //输出1 p[0]等效于*p, p[0]即是a[1]
	p = a + 6; // p指向a[6]
	cout << * p << endl;// 输出 6  a[6]的值
	return 0
}	
#include <iostream>
using namespace std;
void Reverse(int * p,int size) { //颠倒一个数组
	for(int i = 0;i < size/2; ++i) {
    	int tmp = p[i];
    	p[i] = p[size-1-i];
    	p[size-1-i] = tmp;
    }
 }
int main() 
{
	int a[5] = {1,2,3,4,5};
    Reverse(a,sizeof(a)/sizeof(int));
    for(int i = 0;i < 5; ++i) {
		cout << *(a+i) << "," ; 
	}
    return 0;
} //5,4,3,2,1,

6. 指针和二维数组

如果定义二维数组:
T a[M][N];

  • ai是一个一维数组
  • a[i]的类型是 T *
  • sizeof(a[i]) = sizeof(T) * N
  • a[i]指向的地址: 数组a的起始地址 + i×N×sizeof(T)
void Reverse(int * p,int size)
{ //颠倒一个数组 
	for(int i = 0;i < size/2; ++i) {
    	int tmp = p[i];
        p[i] = p[size-1-i];
        p[size-1-i] = tmp;
	} 
}
int a[3][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12}};
Reverse(a[1],4); //=> { {1,2,3,4},{8,7,6,5},{9,10,11,12}};
Reverse(a[1],6); //=> { {1,2,3,4},{10,9,5,6},{7,8,11,12}};

因为二维数组的元素在内存中是连续存放的,所以Reverse(a[1],6);是对从a[1]地址开始之后的六个元素进行颠倒。

7. 指向指针的指针

定义:
T ** p;
p是指向指针的指针,p指向的地方应该存放着一个类型为 T * 的指针
*p 的类型是 T *

#include <iostream> 
using namespace std; 
int main()
{
	int **pp; //指向int*类型指针的指针 
	int * p;
	int n = 1234;
	p = &n; // p指向n 或 p指向n的首地址
	pp = & p; //pp指向p  或 pp指向p的首地址
	cout << *(*pp) << endl; // *pp是p, 所以*(*pp)就是n
	return 0;
}

在这里插入图片描述

8. 指针和字符串

  • 字符串常量的类型就是 char *
  • 字符数组名的类型也是 char *
#include <iostream> 
using namespace std; 
int main()
{
	char * p = "Please input your name:\n"; 
	cout << p ; // 若不用cout, printf(p) 亦可 
	char name[20];
	char * pName = name;
	cin >> pName;
	cout << "Your name is " << pName; 
	return 0;
}
  • 字符数组名的类型也是 char *,就是一个地址
	char name[20];
	int n;
	scanf("%d%s",&n, name); 
	cin >> n >> name;

字符串操作库函数

在这里插入图片描述

  • char * strchr(const char * str,int c); 寻找字符c在字符串str中第一次出现的位置。如果找到,就返回指向该位置的char*指针;如果str中不包含字符c,则返回NULL
  • char * strstr(const char * str, const char * subStr); 寻找子串subStr在str中第一次出现的位置。如果找到,就返回指向该位置的指针;如果str不包含字符串subStr,则返回NULL
  • int stricmp(const char * s1,const char * s2); 大小写无关的字符串比较。如果s1小于s2则返回负数;如果s1等于s2,返回0;s1大于s2,返回正数。不同编译器编译出来的程序,执行stricmp的结果就可能不同。
  • int strncmp(const char * s1,const char * s2,int n); 比较s1前n个字符组成的子串和s2前n个字符组成的子串的大小。若长度不足n,则取整个串作为子串。返回值和strcmp类似。
  • char * strncpy(char * dest, const char * src,int n); 拷贝src的前n个字符到dest。如果src长度大于或等于n,该函数不会自动往dest中写入‘\0’;若src长度不足n,则拷贝src的全部内容以及结尾的‘\0’到dest。
  • char * strtok(char * str, const char * delim); 连续调用该函数若干次,可以做到:从str中逐个抽取出被字符串delim中的字符分隔开的若干个子串。
  • int atoi(char *s); 将字符串s里的内容转换成一个整型数返回。比如,如果字符串s的内容是“1234”,那么函数返回值就是1234。如果s格式不是一个整数,比如是"a12",那么返回0。
  • double atof(char *s); 将字符串s中的内容转换成实数返回。比如,"12.34"就会转换成12.34。如果s的格式不是一个实数 ,则返回0。
  • char *itoa(int value, char *string, int radix);将整型值value以radix进制表示法写入 string:
char szValue[20];
itoa( 27,szValue,10); //使得szValue的内容变为 "27" 
itoa( 27,szValue,16); //使得szValue的内容变为"1b"
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
	char s1[100] = "12345";
	char s2[100] = "abcdefg";
	char s3[100] = "ABCDE";
	strncat(s1,s2,3); // s1 = "12345abc"
	cout << "1) " << s1 << endl; //输出 1) 12345abc 
	strncpy(s1,s3,3); // s3的前三个字符拷贝到s1,s1="ABC45abc" 
	cout << "2) " << s1 << endl; //输出 2) ABC45abc 
	strncpy(s2,s3,6); // s2 = "ABCDE" 6超过了s3的长度 此时\0也会被拷贝
	cout << "3) " << s2 << endl; //输出 3) ABCDE
	cout << "4) " << strncmp(s1,s3,3) << endl; //比较s1和s3的前三个字符,比较结果是相等,输出 4) 0
	char * p = strchr(s1,'B'); //在s1中查找 'B'第一次出现的位置 返回指向该位置的指针
	if( p ) // 等价于 if( p!= NULL)
		cout << "5) " << p - s1 <<"," << *p << endl; //输出 5) 1,B
	else
    	cout << "5) Not Found" << endl;
    p = strstr( s1,"45a"); //在s1中查找子串 "45a"。s1="ABC45abc"
    if( p )
		cout << "6) " << p - s1 << "," << p << endl; //输出 6) 3,45abc 
	else
    	cout << "6) Not Found" << endl;
    //以下演示strtok用法:
	cout << "strtok usage demo:" << endl;
	char str[] ="- This, a sample string, OK."; //下面要从str逐个抽取出被" ,.-"这几个字符分隔的字串
	p = strtok (str," ,.-"); //请注意," ,.-"中的第一个字符是空格
	while ( p != NULL) { //只要p不为NULL,就说明找到了一个子串
    	cout << p << endl; //打印子串
		p = strtok(NULL, " ,.-"); //后续调用,第一个参数必须是NULL 
	}
    return 0;
}

在这里插入图片描述

9. void指针

  • void指针:
    void * p;
  • 可以用任何类型的指针对 void 指针进行赋值或初始化:
double d = 1.54; 
void * p = & d;  //double *
void * p1;
p1 = & d;
  • 因 sizeof(void) 没有定义,所以对于 void * 类型的指针p,
    *p 无定义
    ++p, --p, p += n, p+n,p-n 等均无定义

内存操作库函数

  • memset
    头文件cstring中声明:
 void * memset(void * dest,int ch,int n);

将从dest开始的n个字节,都设置成ch。返回值是dest。ch只有最低的字节起作用(ch虽然是int类型(4个字节),但只有最低的字节起作用)。

例:将szName的前10个字符(一个字符占一个字节),都设置成’a’:

char szName[200] = "";  //空串包含\0
memset( szName,'a',10); 
cout << szName << endl;

用memset函数将数组内容全部设置成0:

int a[100]; 
memset(a,0,sizeof(a));

则数组a的每个元素都变成0
memset的参数和返回值是void*,也就是他可以接收任意类型的数组或指针变量。逐字节初始化(不是逐元素)

  • memcpy
    头文件cstring中声明:
 void * memcpy(void * dest, void * src, int n);

将地址src开始的n个字节,拷贝到地址dest。返回值是dest。
将数组a1的内容拷贝到数组a2中去,结果是a2[0] = a1[0], a2[1] =a1[1]…a2[9] = a1[9] :

int a1[10];
int a2[10];
memcpy( a2, a1, 10*sizeof(int));

自己编写memcpy:

void * MyMemcpy( void * dest , const void * src, int n)
{
	char * pDest = (char * )dest;  //逐字节拷贝 把void*转换为 char*
	char * pSrc = ( char * ) src; 
	for( int i = 0; i < n; ++i ) { //逐个字节拷贝源块的内容到目的块
		* (pDest + i) = * ( pSrc + i );
	}
	return dest;
}

有缺陷,在dest区间和src区间有重叠时可能出问题!!!

10. 函数指针

基本概念

程序运行期间,每个函数都会占用一段连续的内存空间。而函数名就是该函数所占内存区域的起始地址(也称“入口地址”)。我们可以将函数的入口地址赋给一个指针变量 ,使该指针变量指向该函数(的首地址)。然后通过指针变量就可以调用这个函数。这种指向函数的指针变量称为“函数指针”。
在这里插入图片描述

定义形式

类型名 (* 指针变量名)(参数类型1, 参数类型2,…);
例如:

int (*pf)(int ,char);

表示pf是一个函数指针,它所指向的函数,返回值类型应是int,该函数应有两个参数,第一个是int 类型,第二个是char类型。

使用方法

可以用一个原型匹配的函数的名字给一个函数指针赋值。
要通过函数指针调用它所指向的函数,写法为:
函数指针名(实参表);

#include <stdio.h>
void PrintMin(int a,int b) 
{
	if( a<b ) 
		printf("%d",a);
	else 
		printf("%d",b);
}
int main() {
	void (* pf)(int ,int); //定义一个函数指针
	int x = 4, y = 5;
	pf = PrintMin;  //用函数名字给函数指针赋值
	pf(x,y); //用函数指针调用他所指向的函数
	return 0; 
}

在这里插入图片描述
直接用函数名调用函数不更好吗,上面的代码为什么多此一举,用函数指针呢?在上面的例子中确实冗余了,其实函数指针有大用处,如下面的例子。

函数指针和qsort库函数

C语言快速排序库函数:
在这里插入图片描述
可以对任意类型的数组进行排序。其最后一个函数参数,就是一个函数指针(指向一个函数)。
在这里插入图片描述
对数组排序,需要知道:
1)数组起始地址(数组名或指向首地址的指针)
2) 数组元素的个数
3)每个元素的大小(由此可以算出每个元素的地址)
4)元素谁在前谁在后的规则 (最简单有升序、降序,还有一些其他自定义的规则,比如对整数按个位、或十位数字的大小进行排序等)

在这里插入图片描述
base: 待排序数组的起始地址,
nelem: 待排序数组的元素个数,
width: 待排序数组的每个元素的大小(以字节为单位)
pfCompare :比较函数的地址(把比较函数的名字赋给函数指针,指向比较函数)

在这里插入图片描述
比较函数是程序员自己编写的(自定义比较规则)。

排序就是一个不断比较并交换位置的过程。
qsort函数在执行期间,会通过pfCompare指针调用 “比较函数”,调用时将要比较的两个元素的地址传给“比较函数”,然后根据“比较函数”返回值判断两个元素哪个更应该排在前面。

在这里插入图片描述
比较函数编写规则:

  1. 如果 * elem1应该排在 * elem2前面,则函数返回值是负整数
  2. 如果 * elem1和* elem2哪个排在前面都行,那么函数返回0
  3. 如果 * elem1应该排在 * elem2后面,则函数返回值是正整数

实例

下面的程序,功能是调用qsort库函数,将一个unsigned int数组按照个位数从小到大进行排序。比如 8,23,15三个数,按个位数从小到 大排序,就应该是 23,15,8

 
#include <cstdio>
#include <cstdlib>
using namespace std;
int MyCompare( const void * elem1, const void * elem2 ) 
{
	unsigned int * p1, * p2; //比较的是unsign int类型,所以定义两个该类型的指针
	p1 = (unsigned int *) elem1; // “* elem1” 非法  因为elem1是void*类型 强制类型转换
	p2 = (unsigned int *) elem2; // “* elem2” 非法 
	return (* p1 % 10) - (* p2 % 10 );
}
#define NUM 5 
int main()
{
	unsigned int an[NUM] = { 8,123,11,10,4 }; 
	qsort(an,NUM,sizeof(unsigned int),MyCompare); 
	for( int i = 0;i < NUM; i ++ )
		printf("%d ",an[i]); 
	return 0;
} //10,11,123,4,8
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章