指针与函数指针

指针与函数指针

前面一直都没有讲到指针的东西,但是昨天被小伙伴私信让我写一下指针
那今天就来写一些C/C++最有特色的东西–指针

要强调的是,在很多语言如Java当中,都没有指针的概念

那么C/C++的指针为我们提供了对内存区域的任意访问和读写

形象的说,内存每个字节就像是酒店里面房间,内存地址就是相应的房间编号,而指针存放着的就是房间号

指针的定义

int *p;
char *pc;
float *pf;

指针的内容

int * p=(int *)40000;//p只想地址40000,地址p就是地址40000
//*p代表就是40000开始处的若干字节

为什么是若干呢,在后面面慢慢道来

指针来访问内存空间

int *p=(int *)40000;
*p=5000;
int n=*p;

上面这三条语句什么意思呢,一张图就可以解释清楚了
在这里插入图片描述
那么我们看的出来,这里的若干=sizeof(int)

我们总结一下
p类型为 T*
*p类型为 T
*p可以从地址p开始的sizeof(T)个字节进行读写
而这一个星号就是间接引用运算符,sizeof(T*)为4字节(64位机上可能为8字节)

好啦,那么来看看指针的用法把。

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

example:

#include <iostream>
using namespace std;
int main()
{
    char ch1 = 'A';
char * pc = &ch1; // 使得pc 指向变量ch1
  *pc = 'B'; // 使得ch1 = 'B'
  cout<<ch1<<endl;

char ch2 = * pc; // 使得ch2 = ch1
pc = & ch2; // 使得pc 指向变量ch2
 *pc = 'D'; // 使得ch2 = 'D'
cout<<ch2<<endl;
}

指针的作用

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

  2. 不需要通过变量,就能对内存直接进行操作。通过指针,程序能访问的内存区域就不仅限于变量所占据的数据区域

  3. 在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; //编译能过但运行可能出错
cout<<n<<endl;

 
 

指针的运算

1) 两个同类型的指针变量,可以比较大小
地址p1<地址p2,p1< p2 值为真。
地址p1=地址p2,p1== p2 值为真
地址p1>地址p2,p1 > p2 值为真

 

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

两个T * 类型的指针 p1和p2
p1 – p2 = ( 地址p1 – 地址 p2 ) / sizeof(T)

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

3)指针变量加减一个整数的结果是指针

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

类似的有n+p, p-n , *(p+n), *(p-n)

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

  • p++, ++p : p指向 n + sizeof(T)
  • p–, --p : p指向 n - sizeof(T)

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

提个小问题:
如何访问int型变量 a 前面的那一个字节?
我们通过强制类型转换来完成(int类型是4个字节的)

int a;
char * p = (char * ) &a; // &a是 int *类型
--p;
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
	p2 = (int *) 200; //地址p2为200
	cout<< "1) " << p2 - p1 << endl;
	//输出 1) 25, 因(200-100)/sizeof(int) = 100/4 = 25
	pc1 = (char * ) p1; //地址pc1为100
	pc2 = (char * ) p2; //地址pc2为200
	cout<< "2) " << pc1 - pc2 << endl; //输出 2) -100
	//输出 2) -100,因为(100-200)/sizeof(char) = -100
	cout<< "3) " << (p2 + n) - p1 << endl; //输出 3) 29
	int * p3 = p2 + n; // p2 + n 是一个指针,可以用它给 p3赋值
	cout<< "4) " << p3 - p1 << endl; //输出 4) 29
	cout<< "5) " << (pc2 - 10) - pc1 << endl; //输出 5) 90
	return 0;
}

空指针和指针作为函数参数

  • 地址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); //使得p1指向m,p2指向n
	cout << m << " " << n << endl; //输出 4 3
	return 0;
}

指针和数组

数组大家都很熟悉了,那么指针和数组又有什么亲密的关系呢
 

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

T a[N];
  • a的类型是 T *
  • 可以用a给一个T * 类型的指针赋值
  • a是编译时其值就确定了的常量,不能够对a进行赋值

*作为函数形参时, T p 和 T 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
	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;
}

看完上面这些大家可能都感觉指针这种东西很繁琐,但是捏,它确实是非常有用的。

指针和二维数组

  1. 如果定义二维数组:T a[M][N];
  2. ai是一个一维数组a[i]的类型是 T *
  3. sizeof(a[i]) = sizeof(T) * N
  4. 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}};

高维数组与指针

指向指针的指针

定义:
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
	pp = & p; //pp指向p
	cout << *(*pp) << endl; // *pp是p, 所以*(*pp)就是n
	return 0;
}

output:1234

指针和字符串

有一个很有趣的东西

  • 字符串常量的类型就是 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;

在这里插入图片描述字符串部分的内容在我的上一篇博客有提到过,C/C++函数,位运算基础,字符串

1.char * strchr(const char * str,int c);
寻找字符c在字符串str中第一次出现的位置。如果找到,就返回指向该位置的char*指
针;如果str中不包含字符c,则返回NULL

2.char * strstr(const char * str, const char * subStr);
寻找子串subStr在str中第一次出现的位置。如果找到,就返回指向该位置的指针;如
果str不包含字符串subStr,则返回NULL

3.int stricmp(const char * s1,const char * s2);
大小写无关的字符串比较。如果s1小于s2则返回负数;如果s1等于s2,返回0;s1大
于s2,返回正数。不同编译器编译出来的程序,执行stricmp的结果就可能不同。

4.int strncmp(const char * s1,const char * s2,int n);
比较s1前n个字符组成的子串和s2前n个字符组成的子串的大小。若长度不足n,则取
整个串作为子串。返回值和strcmp类似。

5.char * strncpy(char * dest, const char * src,int n);
拷贝src的前n个字符到dest。如果src长度大于或等于n,该函数不会自动往dest中写
入‘\0’;若src长度不足n,则拷贝src的全部内容以及结尾的‘\0’到dest。

6.char * strtok(char * str, const char * delim);
连续调用该函数若干次,可以做到:从str中逐个抽取出被字符串delim中的字符分隔
开的若干个子串。

7.int atoi(char *s);
将字符串s里的内容转换成一个整型数返回。比如,如果字符串s的内容是“1234”,那
么函数返回值就是1234。如果s格式不是一个整数,比如是"a12",那么返回0。
8.double atof(char *s); 将字符串s中的内容转换成实数返回。比如,"12.34"就会转换成12.34。如果s的格式
不是一个实数 ,则返回0。

9.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"
	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;
}

void 指针

可以用任何类型的指针对 void 指针进行赋值或初始化:

double d = 1.54;
void * p = & d;
void * p1;
p1 = & d;

因 sizeof(void) 没有定义,所以对于 void * 类型的指针p ∴*p 无定义;++p, --p, p += n, p+n,p-n 也没有无定义

 

内存操作库函数

头文件

<cstring>
void * memset(void * dest,int ch,int n);将从dest开始的n个字节,都设置成ch。返回值是dest

ch只有最低的字节起作用。
例:将szName的前10个字符,都设置成’a’:

char szName[200] = "";
memset( szName,'a',10);
cout << szName << endl;

memset有一个很常用的操作
用memset函数将数组内容全部设置成0:

int a[100];
memset(a,0,sizeof(a));
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;
	char * pSrc = ( char * ) src;
	for( int i = 0; i < n; ++i ) {
	//逐个字节拷贝源块的内容到目的块
	* (pDest + i) = * ( pSrc + i ); 
	}
	return dest;
}

但是上面代码又不完美的地方,在dest区间和src区间有重叠时可能出问题!!!
当然库函数的memcpy和这个总体思路差不多,但是有一些细节进行了处理。
 
 

函数指针

当然函数指针非常好用,但是对于函数对象而言,可重用性还是差了一些。后面的STL博客当中会给大家介绍一下函数对象。

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

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;
}

output:4

int CmpAsc(int x, int y)
{
    //如果x>y返回1,否则返回0;
    if(x>y)return 1;
    else return 0;
}
void sort(int a[], int n, int (*cmp)(int,int))//最后一个参数为函数指针类型参数
{
    /*对数组a排序,排序原则由cmp指定,若cmp为真,表示两元素逆序*/
    for(int i=0;i<n-1;i++)
        for(int j=i+1;j<n;j++)
            if(cmp(a[i],a[j]))//调用传进来的函数指针
                swap(a[i],a[j]);
}

int main() {
    // insert code here...
    
    int a[6] ={8,2,1,3,4,5};
    sort(a, 6, CmpAsc);//CmpAsc函数名作为参数传递给函数sort
    for(auto &e:a)
        cout<<e<<" ";
    return 0;
}

大家可以通过函数指针对排序自己写的基础算法进行一些操作

 

最后最后,给大家简单介绍一下库函数中的快熟排序
| void qsort(void *base, int nelem, unsigned int width,

int ( * pfCompare)( const void *, const void *));可以对任意类型的数组进行排序
void qsort(void *base, int nelem, unsigned int width,
int ( * pfCompare)( const void *, const void *));base: 待排序数组的起始地址,nelem: 待排序数组的元素个数,width: 待排序数组的每个元素的大小(以字节为单位)pfCompare :比较函数的地址

在这里插入图片描述
对数组排序,需要知道:

  1. 数组起始地址
  2. 数组元素的个数
  3. 每个元素的大小(由此可以算出每个元素的地址)
  4. 元素谁在前谁在后的规则

pfCompare: 函数指针,它指向一个“比较函数”。
该比较函数应为以下形式:
int 函数名(const void * elem1, const void * elem2);
比较函数是程序员自己编写的

原理上:qsort函数在执行期间,会通过pfCompare指针调用 “比较函数”,调用时将要比较的两个元素的地址传给“比较函数”,然后根据“比较函数”返回值判断两个元素哪个更应该排在前面。

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

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

下面给大家演示一下根据个位数大小,从小到大排序

#include <cstdio>
#include <cstdlib>
#define NUM 5
using namespace std;
int MyCompare( const void * elem1, const void * elem2 )
{
    unsigned int * p1, * p2;
    p1 = (unsigned int *) elem1; // “* elem1” 非法
    p2 = (unsigned int *) elem2; // “* elem2” 非法
    return (* p1 % 10) - (* p2 % 10 );
}
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;
}

在这里插入图片描述
有什么问题,大家可以私信或者评论留言给我哦,祝大家节日快乐😁

学会程序和算法,走遍天下都不怕

在这里插入图片描述
贵州黄果树瀑布

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值