C语言学习 -- 指针

指针概述

指针是什么?

指针,是C语言中的一个重要概念及其特点,指针就是内存地址指针变量是用来存放内存地址的变量,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。有了指针以后,不仅可以对数据本身,也可以对存储数据的变量地址进行操作。

指针是一个占据存储空间的实体在这一段空间起始位置的相对距离值。在C/C++语言中,指针一般被认为是指针变量,指针变量的内容存储的是其指向的对象的首地址,指向的对象可以是变量(指针变量也是变量),数组,函数等占据存储空间的实体。

​ —转自百度百科

内存地址的产生

内存地址的编码是通过计算机中的地址线产生正负电来代表0、1产生的。

在32位平台上使用32根地址线,因此可以产生232中不同的排列顺序,每一种排列就代表内存中的一个地址,而一个地址可以存放1Byte的数据,因此32位平台的内存最大支持4GB的内存。

64位平台使用64根地址线,因此可以产生264个地址,理论上最大能支持2147483648GB的内存,实际上要看主板和操作系统支持的最大内存是多少。

指针变量的大小

当我们知道,内存地址是如何产生后,我们在分析指针变量的大小。

前面说过指针变量是用来存放内存地址的,这也就说明,指针变量在32位平台上,它必须要有能存储232种不同排列组合的空间,也就是需要32bit的空间来存放, 而在64位平台上,它必须要有64bit大小的空间来存放264种不同的排列组合。

1Byte = 8bit

所以32位平台上,指针变量的大小有4Byte。

64位平台上,指针变量的大小有8Byte。

这里和数据类型无关,因为指针变量存放的是地址,而地址的长度是固定的。

#include <stdio.h> 
int main()
{// %zu 输出size_t型。size_t在库中定义为unsigned int
    printf("%zu\n", sizeof(char *));    // 32bit-4 b4bit-8
    printf("%zu\n", sizeof(short *));   // 32bit-4 b4bit-8
    printf("%zu\n", sizeof(int *));     // 32bit-4 b4bit-8
    printf("%zu\n", sizeof(double *));  // 32bit-4 b4bit-8
    return 0;
    
    // 结论:指针大小在32位平台是4个字节,64位平台是8个字节。
}

在这里插入图片描述
在这里插入图片描述

指针和指针类型

上面介绍了,指针变量的大小是固定的,和它说存地址变量的数据类型无关,那么既然变量类型不影响指针变量的大小,为什么还需要给指针变量也指定数据类型呢?

我们在使用指针变量存储地址时,存放的确实是32\64位的地址,不管指针类型是int* char* double* 等等,它们所存的地址都是一样的。

int main()
{
    int a = 0x11223344;
    int* pi = &a;
    char* pc = &a;
    pirntf("%p\n", pi);
    pirntf("%p\n", pc);
    // 存放的是相同变量的地址,输出结果也相同
    return 0;
}

在这里插入图片描述

指针变量解引用

但是当我们对指针变量解引用操作,来访问其中的变量时问题就出来了。

    printf("*pi = %#x\n", *pi);
    printf("*pc = %#x\n", *pc);

在这里插入图片描述

为什么pc和pi存放的地址相同,在解引用操作时,读取的值却不相同的?

a是一个int类型的变量,所占内存大小4个字节,当我们用int*类型的指针变量去接收它的地址后,在进行解引用操作时,计算机会认为访问的是int类型,所以这个指针变量会通过地址访问当前向后的共4块内存空间,a的值就被保存在这4块内存空间中。

而是使用char*类型的指针变量接收地址后,在进行解引用操作时,计算机会认为访问的是一个char类型的数据,所以这个指针变量只会访问当前这一块地址的空间(首位置),而当前这块地址存放的是0x44,因此就将0x44获取到。

至于为什么访问的是0x44,而不是0x11,这是因为内存中存储数据时,分配到每一块内存空间的顺序是不一定的,可能是正序存储也可能是倒序存储,后面会解释。

所以指针类型的作用是当访问元素时,可以获取到访问空间有多大(能够操作几个字节),char* 只能访问当前地址所指向的那一块内存空间,short* 可以访问当前地址和之后的两块内存空间,int*long*float*… 都是按照所对应基本数据类型所占用空间大小,来决定它们可以访问多大的空间

指针变量+-整数

指针变量存放的是一个地址,而对指针变量+-一个整数n时,它指不是对地址的值+n,而是指增加或减少n个存储单元,这意味它会根据指针类型所对应的基本类型所占空间大小,来找到前面或后面第n个元素的位置。

例如:

int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	printf("%p\n", p);
	printf("%d\n", *p);
	printf("==========\n");
	p -= 1;  // 意思往前找到第4个内存单元,并获取它的地址
	printf("%p\n", p);
	printf("%d\n", *p);
	printf("==========\n");
	p += 2; // 往后找到第8个内存单元,并获取它的地址
	printf("%p\n", p);
	printf("%d\n", *p);
}

在这里插入图片描述

从图片结果可以看出,指针变量p类型是int*,在-1时,内存地址减少了4+2时内存地址增加了8,而两个地址都指向了存放数组内数值的那一块内存空间。

而指针变量pc-1时,内存地址减少了1+2时内存地址增加了2,而两个地址都指向了存放数组内的为0的内存空间(因为int占4块内存空间,而被赋予的数值都没有超过一个1Byte空间可以存放的大小,因此剩下三块内存空间中的值是全0)

可以看出指针在加减n时,实际上加减的是 n * sizeof(int),因此,指针的移动距离 = n * sizeof(type)

这也是指针类型的另一个作用,决定了指针的移动距离,只有指定了合适的指针类型才能正确的访问元素。

野指针

野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的。

​ —转自百度百科

野指针成因

  1. 指针未初始化
int main()
{
    int* p; // 局部变量未初始化,默认为随机值
    *p = 20;
    return 0;
}
// 在vs2019编译器中,这段代码跑不过去
// 可能编译器也不清楚这家伙指向的是哪里,哈哈

在这里插入图片描述

  1. 指针越界访问
int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	for (int i = 0; i <= 10; i++)
	{
		// 当指针指向的范围超出数组arr的范围时,p就是野指针
		*p++ = i;
	}
    printf("%d\n", *(p+10));
	return 0;
}

  1. 指针指向的被释放的空间
int* test()
{
    int a = 10; // a是局部变量,当函数执行完毕,会释放这块内存空间
    printf("&a = %p\n", &a);
    return &a;
}
int main()
{
    int* ptr = test(); // 指针ptr接收的是一块被释放空间的地址,这也会导致野指针
    printf("ptr  = %p\n", ptr);
    printf("*ptr = %d\n", *ptr);
    return 0;
}
// 这种写法虽然编译器不会报错,但是解引用的值是随机值,输入非法访问内存空间

如何避免野指针

  1. 指针初始化

    当不确定指针应该指向哪一块地址时,可以给指针变量赋值为**NULL**

    int* p = NULL; // NULL-用来初始化指针,给指针赋值
    
  2. 小心指针越界

  3. 指针指向空间释放时要将指针赋值为**NULL**

  4. 指针使用之前检查有效性

    int main()
    {
        int a = 10;
        int* p = &a;
        *p = 20;
        p = NULL; // 不用时赋值为空
        
        // 使用时先检查指针有效性
    	if(p != NULL){ 
    		// 使用指针变量p
    	}
    	return 0;
    }
    
    

指针运算

指针 +- 整数

指针 +- 整数在前面已经介绍,

int main() {
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* ptr = arr;
	int* ptr1 = &arr[9];
	int i = 0;
	for (i = 0; i < 10; i++) {
		printf("%d ", *(ptr + i));
	}
	putchar('\n');
	for (i = 0; i < 10; i++) {
		printf("%d ", *(ptr1 - i));
	}
}

指针 - 指针

指针减去指针得到结果的绝对值是两个地址中间的元素个数

int main() {
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    printf("%d\n",& arr[9] - &arr[0]);  // 9
    printf("%d\n",& arr[0] - &arr[9]);  // -9
    
    // 将两个指针类型不同的指针相减,得到的结果是未知的
    // 错误写法
    int* pi = arr;
    char* pc = &arr[9];
    printf("%d\n", pc - pi);  // 36
    printf("%d\n", pi - pc);  // -9
    // 在VS编译器下,是以左操作数的指针类型作为根据
    return 0;
}

模拟实现strlen() 函数功能

int my_strlen(char* pstr) {
	char* start = pstr;   // 起始位置
	char* end = pstr;     // 终止位置,字符串中第一个'\0'的位置
	while (*end != '\0') { 
		end++;     
	}
	return end - start;   // 中间的元素个数,即字符串长度,不包含'\0'
}

int main() {
	// 模拟实现Strlen函数
	char arr[] = "bit";
	int len = my_strlen(arr);
	printf("%d\n", len);
	return 0;
}

指针的关系运算

指针可以比较大小

int main() {
	int arr[5];
	int* ptr = NULL;

	for (ptr = arr; ptr < &arr[5];)
	{
		*ptr++ = 0;  // 将数组所有元素初始化为0
        //*(ptr++) = 0 // 等价写法
 	}
    // 等价写法2
    for (ptr = arr; ptr < &arr[5];ptr++)
	{
		*ptr = 0;  // 将数组所有元素初始化为0
 	}
}
int main() {
    int arr[5];
    int* ptr = NULL;
    // 写法1
    for(ptr = &arr[5]; ptr > &arr[0];) // ptr第一次进入,指向的是数组最后一个元素后面一个元素的空间
    {
        *--ptr = 0;                    // 先--得到数组最后一个元素的地址,在解引用并赋值,直到ptr == &arr[0]结束循环
    }
    
    // 写法2(非法写法)
    for(ptr = &arr[4]; ptr >= &arr[0];ptr--){ // ptr第一次进入,指向的是素组最后一个元素
        *ptr = 0;                             // 每次进来将数组中一个元素赋值为0,直到ptr == & arr[-1]时,结束结束
    }
    	
    return 0;
}

写法2是不合法的,因为C语言中允许指向数据元素的指针与指向数组最后一个元素后一个位置的指针进行比较,但是不允许与指向数组第一个元素前一个位置的指针进行比较

数组和指针

在数组章节介绍过数组名通常情况下是数组首元素的地址,因此假设创建一个数组arr[],那么arr == &arr[0],两者都是常量,不会被改变。

int main()
{
    int arr[] = { 1 };
    int* p1 = arr;
    int* p2 = &arr[0];
    if (p1 == p2)
    {

        printf("p1   = %p\n", p1);
        printf("p2   = %p\n", p2);
        printf("*p1  = %d\n", *p1);
        printf("*p2  = %d\n", *p2);
       
    }
    return 0;
}

在这里插入图片描述

从打印结果可以看出,数组名 == 数组首元素的地址。

我们可以将数组的地址赋值给指针变量,来修改指针变量的值,达到使用数组的效果。

int main()
{
    int arr[5] = { 1,2,3,4,5 };
    char str[5] = { 'a','b','c','d','e' };
    int* pi = arr; // 将数组首元素的地址赋值给指针变量
    char* pc = str;
    int i = 0;

    printf("%10s  %25s\n", "int", "char");
    for (i = 0; i < 5; i++)
    {
        printf("arr[%d].val  == %-8d  | str[%d].val == %-8c\n", i, *(pi + i),i, *(pc + i));
        printf("arr[%d].addr == %p  | str[%d].addr == %p\n", i, pi + i,i, pc + i);
    }
 
    return 0;
}

在这里插入图片描述

通过 指针 +n的方法可以高效的获取到数组中的元素,在C语言中指针 +n是指增加n个存储单元,对于数组来说,它指向的当前地址后面第n个元素的地址。

从打印结果可以看出int类型的数组和char类型的数组所增加的值是不同的,int每次加4,char每次加1,这是因为,int类型的数据所占内存空间为4btye,char类型的数据所占内存空间为1btye,这也是为什么在声明指针变量时需要在指针变量前加上数据类型,它代表的不是指针的数据类型,而是它所指向对象的数据类型。

数组和指针的关系

通过上面的例子,可以发现下面两种表示方式是等价的

// 取地址的结果相同
// arr + n == &arr[n]            , n>=0 && n< sizeof(arr)
// str + n == &str[n]            , n>=0 && n< sizeof(str)
// 解引用的结果相同
//*(arr+n) == arr[n];            , n>=0 && n< sizeof(arr)
//*(str+n) == str[n];            , n>=0 && n< sizeof(str)
// 上面验证使用的是指针变量pi和pc,不过pi == arr ,

代码验证

int main()
{
    int arr[5] = { 1,2,3,4,5 };
    char str[5] = { 'a','b','c','d','e' };
    int* pi = arr; // 将数组首元素的地址赋值给指针变量
    char* pc = str;
    int i = 0;
    printf("%10s  %25s\n", "int", "char");
    printf("%p == %p | %p == %p\n", arr + 2, &arr[2], str + 2, &str[2]);
    printf("%8d == %-8d | %8c == %-8c\n", *(arr + 2), arr[2], *(str + 2),str[2]);

    return 0;
}

在这里插入图片描述

也就是说想获取数组中的一个元素,可以有*(addr + idx)arr_name[idx]两种方法,而获取它们的地址也有addr + idx&arr_name[idx]两种方法。

这里需要注意*(addr + idx)*addr + idx , 前者表示先指向后面的地址,再解引用获取地址里的值;而后者则是先解引用获取当前地址的值,再将这个值+ idx,两种写法的结果是完全不同的,后者相当于(*addr) + idx

可以发现数组和指针的关系十分密切,我们对数组名的操作可以同样运用在存放相同数组元素地址的指针变量上,但是数组名和指针又不是完全相同。

数组名和指针变量的区别

数组名虽然存放的是首元素的地址,但是它不等于指针变量。数组名有两点不同于指针变量的地方。

  1. sizeof 在计算数组名时,会返回整个数组所有元素大小的合,这个值是根据不同的数据类型来计算的,而指针变量返回的就是本身的大小,32bit平台下 4btye, 64bit平台下 8btye,这是指针变量这种数据类型的特点,因为它是用来存放地址的。
int main()
{

	int arr[] = {1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[5];
	printf("arr.sz == %d\n", sizeof(arr));
	printf("p.sz   == %d\n", sizeof(p));
	return 0;
}

在这里插入图片描述

  1. &数组名时,此时得到的结果是整个数组的地址,而取指针变量的地址,得到的结果就是指针变量自己的地址。
int main()
{
    // 这里将 p 的接受值改为数组第一个元素的地址
    int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
    int* p = &arr[0];
    printf("arr     = %p\n", arr);
    printf("&arr[0] = %p\n", &arr[0]);
    printf("&arr    = %p\n", &arr);
    printf("&*p     = %p\n", &(*p));
    printf("&p      = %p\n", &p);
    
    return 0;
}

在这里插入图片描述

从打印的结果来看,&p 确实是指针变量p 的地址,但是前四个地址显示的都是数组首元素的地址,那么和&arr的结果与上面的结论就不同了,可事实是,&arr 虽然打印出来的是数组首元素第一个的地址,但是它代表的是整个数组的开始。我们现在给每个地址都加 1 看看会发生什么。

int main()
{
    // 这里将 p 的接受值改为数组第一个元素的地址
    int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
    int* p = &arr[0];
    printf("arr         = %p\n", arr);
    printf("arr + 1     = %p\n", arr + 1);
    printf("&arr[0]     = %p\n", &arr[0]);
    printf("&arr[0] + 1 = %p\n", &arr[0] + 1);
    printf("&arr        = %p\n", &arr);
    printf("&arr + 1    = %p\n", &arr + 1);
    printf("&*p         = %p\n", &(*p));
    printf("&*p + 1     = %p\n", &(*p) + 1);
    printf("&p          = %p\n", &p);
    printf("&p + 1      = %p\n", &p + 1);
    
    return 0;
}

在这里插入图片描述

从这次的打印结果可以看到除了 &arr 其他4个的地址+ 1都指向了下一块内存空间的地址,相差为4,(因为数组是int类型每个元素的大小为4个字节,也就是占有4个内存块);而只有&arr + 1相差了40,这40是如何的出来的呢?因为我们数组里有10int类型元素,每个元素占有4Btye的内存空间,也就是占有4个内存块 因此总共相差40,这也就说明了&arr获取的是整个数组的地址。

以上两点是使用数组名的例外的情况,其他情况下数组名存放的都是数组首元素的地址。

不过当用指针变量接受数组内元素的地址时,也可以通过下标引用操作符来找到指定的元素,数组中默认第一个元素下标为0,而指针变量则是接收到的那个元素的下标为0。

int main()
{

	int arr[] = {1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[5];
	printf("arr[0] == %d\n", arr[0]);
	printf("p[0]   == %d\n", p[0]);
	return 0;
}

在这里插入图片描述

从打印结果可以看出,arr[0]是数组第一个元素,而p[0]则是p接收到的那个地址指向的元素。

二级指针

二级指针是指向指针的指针,二级指针变量中则存放着指针变量的地址。

int main()
{
	int a = 10;
	int* pa = &a; // pa 为一级指针
	int** ppa = &pa; // ppa 为二级指针
	int*** pppa = &ppa; // pppa 为三级指针 ...
    // 理论上可以无限套娃
	return 0;
}

可以将一级指针int*中的 * 看的变量类型是一个指针变量,前面的int看作这个指针变量指向的元素是int类型。

同样二级指针int**,将最右侧的* 看作变量类型是一个指针变量,前面的*看作这个指针变量指向的元素也是一个指针变量,而这个指针变量指向的元素是一个int类型。

使用方法

二级指针的使用方法也很简单,如果想通过二级指针找到它指向的一级指针所指向的元素,再添加一个解引用操作符*即可。

int main(){
    int a = 10;
    int* pa = &a;
    int** ppa = &pa;
    printf("%d\n", **ppa);
    // 利用二级指针找到a的地址,也很容易
    // 一级变量pa中存放的时a的地址
    // 所以只需要对二级指针解引用一次*就可以获取到a的地址
    printf("&a   = %p\n", &a);
    printf("pa   = %p\n", pa);
    printf("*ppa = %p\n", *ppa);
    return 0;
}

在这里插入图片描述

在这里插入图片描述

指针数组

指针数组使指用来存放指针的数组

这里要区分下指针数组数组指针数组指针是指,这个指针指向的是一个数组,他本质上是一个指针;而指针数组,本质上是一个数组。

int main() {
    int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; // int类型的数组
    int* ptr[10] = { NULL };              // 指针数组
    for (int i = 0; i < 10; i++)
    {
        // 将arr数组中每一个元素的地址赋值给指针数组ptr
        ptr[i] = &arr[i];                 
    }
    for (int i = 0; i < 10; i++)
    {
        // 通过存放的地址找到arr数组中的每个元素
        printf("%d ", *ptr[i]);      
    }
    return 0;
}

在这里插入图片描述

指针进阶使用

字符指针

字符指针的用法

存放char类型变量的地址

int main()
{
    char ch = 'w';
    char* pc = &ch;  // 存放char变量地址
    char arr[] = "abcdef";
    char* pc1 = arr; // 存储char数组首元素地址,数组首元素也是char类型变量
    return 0;
}

存放字符串常量第一个字符的地址

int main()
{
	char* p = "abcdef";     // 将字符串常量的第一个元素的地址赋值给p
    printf("%c\n", *p);     // a
    printf("%c\n", *(p+3)); // d, 字符从常量在内存中的存储也是连续的
     printf("%s\n", p);  // abcdef 
	return 0;
}

字符串常量不可改变

// 给p指向的内存空间重新赋值
*p = 'f';   // 错误写法!

在这里插入图片描述

避免错误的写法,加上const 修饰指针变量

const *p = "abcdef";

字符串常量在进程运行期间不可更改,所有的指针指向相同的字符串常量,其实指向的是内存中同一块空间。


int main()
{

	char arr1[] = "abcdef";
	char arr2[] = "abcdef";
	const char* p1 = "abcdef";
	const char* p2 = "abcdef";
	if (arr1 == arr2)
	{
		printf("地址相同\n");  
	}
	else {
		printf("地址不同\n");  // 打印该结果
	}

	printf("&arr1 = %p\n", &arr1);
	printf("&arr2 = %p\n", &arr2);
	printf("&arr1 - &arr2 = %d\n", &arr1 - &arr2);
	printf("arr1 - arr2 = %d\n", arr1 - arr2);
	// 两个数组分别在内存中开辟了不同的空间,用来存放每个字符

	printf("===================================\n");

	if (p1 == p2)
	{
		printf("地址相同\n"); // 打印该结果
	}
	else {
		printf("地址不同\n");
	}

	printf("p1 = %p\n", p1);
	printf("p2 = %p\n", p2);
	printf("p1 - p2 = %d\n", p1 - p2);
	// 两个指针指向了字符串常量的同一块空间

	return 0;
}

指针数组

用来存放地址的数组。

int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; // 整型数组
    char ch[5] = { 'a','b','c','d','e'};  // 字符数组
    int* arrp[10] = { NULL }; //存放整型指针的数组 -- 指针数组
    char* pch[5] = { NULL };  //存放字符指针的数组 -- 指针数组
    return 0;
}

指针数组的使用

int main()
{
    int arr1[] = { 1,2,3,4,5 };
    int arr2[] = { 2,3,4,5,6 };
    int arr3[] = { 3,4,5,6,7 };
    // 将3个数组的首元素的地址存入指针数组
    int* arrp[] = { arr1,arr2,arr3 };
    // 通过这个指针数组可以访问到其中3个整型数组的所有元素
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        int j = 0;
        for (j = 0; j < 5; j++)
        {
            printf("%d ", *(arrp[i] + j));
        }
        printf("\n");
    }
       // 1 2 3 4 5
       // 2 3 4 5 6
       // 3 4 5 6 7

    return 0;
}

数组指针

指向数组的指针

type (*ptr)[const_num] = &array_name;
int main()
{
    int* pi = NULL;   // 整型指针 -- 指向整型的指针 -- 存放整型变量地址 
    char* pc = NULL;  // 字符指针 -- 指向字符的指针 -- 存放字符变量地址
    
    int arr[10] = {0};
    // arr -- 首元素地址
    // &arr[0] -- 首元素地址
    // &arr -- 数组的地址
    
    // 数组指针 type (*ptr)[const_num] = &array_name;
    int (*p)[10] = &arr;  // 数组指针 -- 指向数组的指针  -- 存放数组的地址。
    return 0;
}
int (*p)[10] = &arr;

(*p)代表一个指针,这个指针指向了–>[10]有10个元素的数组,元素的数据类型是int

注意: 这里不要和指针数组混淆

// 指针数组
int* p[10] = {NULL}; 
// []的优先级高于* ,先使用[10] 确定变量p是有10个元素的数组,再确定该数组中的元素int类型 的指针。

// 数组指针
int (*p)[10] = &arr;
// (*p)代表一个指针,这个指针指向了-->[10]有10个元素的数组,元素的数据类型是int。

指向指针数组的指针

int main()
{
	char* arr[5];  // 指针数组
	char* (*pa)[5] = &arr; // 指向指针数组的指针
}

(*pa)是一个指针,指向了–>[5]有5个元素的数组,元素的数据类型是char*

数组指针的使用

数组指针在一维数组中使用时比较麻烦,使用普通指针接收数组首元素地址也可以达到同样的效果。

// 数组指针的使用
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    // 定义数组指针
    int (*pa)[10] = &arr; 
   	// 使用数组指针
    int i = 0;
    for(i = 0; i < 10; i++)
    {	 // 写法1
		// printf("%d ",(*pa)[i]);  // *pa == arr
         // 写法2
         printf("%d ", *(*pa+i));  // *pa就是数组首元素地址,+ i 就是指向第 i 个元素的地址,然后再解引用
    }
    return 0;
}
// 普通指针指向数组首元素地址
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    // 接收数组首元素地址
    int *p = arr;
    int i = 0;
    for(i = 0; i < 10; i++)
    {
        printf("%d ", *(p+i));  //通过指向第i个元素在解引用来访问
    }
    
    return 0;
}

数组指针在二维数组中的使用

在二维数组中,数组名存放的是数组第一行的地址,而每一行代表了一个一维数组

// 使用数组形式接收数组名
void print1(int arr[][],int x, int y)
{
    int i = 0;
    for(i = 0; i < x; i++)
    {
        int j = 0;
        for(j = 0; j < y; j++)
        {
            printf("%d ",arr[i][j]);
        }
        printf("\n");
    }
    
}

int main()
{ 
    
    int arr[3][5] = { 1,2,3,4,5,    // 元素1 第1行第一个元素
                      2,3,4,5,6,    // 元素2 第2行第一个元素
                      3,4,5,6,7, }; // 元素3 第3行第一个元素
    print1(arr, 3, 5);              // arr - 数组名首元素地址
    // 二维数组的每一行算作一个元素
    // 因此,二维数组首元素的地址是【第一行】的地址
    return 0;
}

// 1 2 3 4 5
// 2 3 4 5 6
// 3 4 5 6 7

既然二维数组的数组名代表的是第一行整个一维数组的地址,那么在传参时,形参可以使用数组指针来进行接收

// 使用数组指针接收二维数组名
void print2(int (*p)[5],int x, int y)
{
    int i = 0;
    for(i = 0; i < x; i++)  
    {
        int j = 0;
        for(j = 0; j < y; j++)
        {
            printf("%d ",  *(*(p + i) + j)); // p + i 指向第i个【一维数组】,再解引用得到该一维数组首元素地址,再 + j 指向该一维数组第 j 个元素的地址,再解引用得到该元素。
            // 等价写法
            printf("%d ", (*(p + i))[j]);
            printf("%d ", *(p[i] + j));
            printf("%d ", p[i][j]);
        }
       
        printf("\n");
    } 
}

分析下面定义的变量是指针还是数组

int arr[5];       		// arr是一个存有5个整型元素的数组  -- 数组
int *parr1[10];   		// parr1是一个存有10个整型指针的数组  -- 指针数组
int (*parr2)[10]; 		// parr2是一个指向存有10个整型元素数组的指针 -- 数组指针
int (*parr3[10])[5]; 	// parr3是一个存有10个整型指针的数组,其中每个指针指向一个存有5个整型元素的数组  -- 数组指针数组

// 判断一个变量是指针还是数组的原则,去掉变量名,剩下的就是它的类型,该类型是由剩下操作符的优先级决定的
// 如:
int arr[5]          -->  int [5]        --> 整型数组
int *parr1[10]      -->  int* [10]      --> 指针数组
int (*parr2)[10]    -->  int (*)[10]    --> 数组指针,先看(*)表示是一个指针,再看外面表示是一个数组10个元素,再看最前面表示元素是int类型。
int(*parr3[10])[5]; -->  int(*[10])[5]; --> 数组指针数组 先看(*[10])表示是一个指针数组(和第二个很像), 再看[5]表示其中的每个数组指针指向了一个含有5个元素的数组,再看int表示其中的元素是int类型。

数组参数和指针参数

一维数组传参

# include <stdio.h>
void test(int arr[]) {...}     // 正确写法,实参arr是数组名,形参是以数组形式接收数组首元素地址
void test(int arr[10]) {...}   // 正确写法,同上,并且在【一维数组中】这个中括号[10] 中的数字没有实际意义
void test(int *arr) {...}      // 正确写法,以指针形式接收地址
void test2(int *arr[20]) {...} // 正确写法,形参以指针数组形式接收数组首元素地址
void test2(int **arr) {...}    // 正确写法,传递的是指针数组首元素的地址,即一级指针的地址,而二级指针就是用来存放一级指针变量地址的指针

int main()
{
    int arr[10] = {0};     // 整型数组
    int *arr2[20] = {0};   // 整型指针数组
    test(arr);
    test2(arr2);
    return 0;
}

二维数组传参

void test(int arr[3][5]){...}  // 正确
void test(int arr[][]) {...}   // 错误,二维数组的定义不能省略列中的数字
void test(int arr[][5]) {...}  // 正确,行可以省略
void test(int *arr) {...}       // 错误,实参传递的是二维数组第一个一维数组的地址,而形参只是一个接收int类型的指针变量
void test(int *arr[5]) {...}    // 错误,形参是指针数组,该数组中的指针变量也只是用来接收int类型变量地址的
void test(int (*arr)[5]) {...}  // 正确,数组指针,就是指向数组的指针,而传过来的首元素地址是第一个一维数组的地址
void test(int **arr) {...}      // 错误,形参是二级指针,而实参不是一级指针

int main()
{
    int arr[3][5] = {0};  // 二维整型数组
    test(arr);   
    return 0;
}

一级指针传参

void print(int *p, int sz) {...} // 正确,常规写法
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int *p = arr;
    int sz = sizeof(arr) / sizeof(arr[0]);
    print(p,sz);
    return 0;
}

深入思考:如果一个形参声明为一级指针变量,那么该变量可以接受什么类型的参数?

void test(int *p) {...} // 变量的地址,存放变量地址的指针变量

int main() 
{
    int a = 10;
    test(&a); // 正确,指针变量就是用来接受地址的
    int *p = &a;
    test(p);  // 正确,指针变量传过去的也是地址
    return 0;
}

二级指针传参

void test(int **p) {...}

int main()
{
    int n = 10;
    int *p = &n;    // 一级指针
    int **pp = &p;  // 二级指针
    test(pp);       // 正确,传二级指针,接受的也是二级指针
    test(&p);       // 正确,二级指针变量就是用来存放一级指针的地址的。    
    
    int* arr[10] = {0};  // 指针数组
    test(arr);     // 正确,该指针数组里的元素是一级指针,arr传递的是该数组第一个指针变量的地址,这就是二级指针可以接受的数据
}

函数和指针

函数指针

函数也是可以被寻址的,因此函数指针表示的是指向函数的指针

int Add(int x, int y)
{
   return x + y;
}
int main()
{
    int a = 10;
    int b = 20;
    Add(a,b);
    printf("%p\n", &Add);  // 打印Add函数的地址
    printf("%p\n", Add);   // 打印Add函数的地址
    // &Add 和 Add 输出结果相同
    
    return 0;
}

注意:以%p形式打印 &函数名函数名代表函数的地址,没有区别。

在这里插入图片描述

函数指针的定义格式
// 定义一个函数指针,用来存储上面代码中Add函数的地址
int (*pa)(int, int) = Add;
// (*pa) 表示p是一个指针变量
// (int,int) 表示该指针变量指向的是一个具有2个int类型参数的函数
// 前面的 int 表示,这个函数的返回值是int

函数指针的格式,"类似"在定义一个函数,需要注意的地方是:

  1. 函数的参数类型和返回值类型必须与被取址的函数一致。
  2. 参数列表中的可以省略变量名。
  3. (*pa) 一定要用()将指针变量括起来。
使用
int Add(int x, int y)
{
    return x + y;
}
int main()
{
    int a = 10;
    int b = 20;
    int sum1 = 0;
    sum1 = Add(a, b);
    printf("sum1 = %d\n", sum1);
    // 定义函数指针
    int (*p)(int,int) = Add;
    // 函数指针的使用
    int sum2 = 0;
    sum2 = (*p)(a, b); // 通过函数指针调用Add函数
    printf("sum2 = %d\n", sum2);

    return 0;
}

在这里插入图片描述

下面代码是什么意思

// 代码1
(*(void(*)())0)();
// (*) 表示一个指针 --> 指向了(), 也就是指向了一个函数,该函数的返回值是void
void(*)() // 所以这表示为一个函数指针类型
// 而将该类型放在()中,则表示强制类型转换,也就是把后面的 0 强转为该类型
//最前面的*表示解引用该指针,
//最后的()表示该指针调用了所指向的函数,并且该函数无参,返回值void,没有指定默认就是void

上面代码出自《C陷阱和缺陷》

设计这段代码的作用是:在计算机启动时,硬件将调用首地址为0位置的函数。

// 代码2
void (*signal(int, void(*)(int)))(int);
// void(*)(int) 函数指针,该函数参数为int 返回值为void, 该函数被作为signal的参数
// signal(int, void(*)(int))
// signal函数有两个参数,第一个为int ,第二个为函数指针类型
// 而signal函数的返回值类型,就是将剩下的部分
// void(* )(int)  这又是一个函数指针,说明signal函数的返回值类型是函数指针

// 上面的代码 意思是:
// signal是一个函数的声明,函数的参数(int, void(*)(int))有两个,第一个是int, 第二个是函数指针,该函数指针指向的函数是int,返回值类型是void
// 而signal函数的返回值类型也是void(*)(int)
// 返回类型是函数指针的写法只能这样写 

// 如果想简化可以使用typedef关键字,但是重新定义的名字还是只能写在*的旁边,如下:

typedef void(*pfun_t)(int);  // 这就是将void(*)(int) 重命名为pfun_t

// 重命名后,最上面的复杂代码可以改为:
pfun_t signal(int, pfun_t);   // 这样看着就舒服多了

函数指针中*****的作用

int Add(int x, int y)
{
    return x + y;
}
int main()
{
	int a = 10;
	int b = 20;
    
    int(*pa)(int,int) = Add;
    printf("%d\n", (*pa)(2,3));   // 5
    printf("%d\n", (**pa)(2,3));  // 5
    printf("%d\n", (***pa)(2,3)); // 5
}
// 代码结果说明*在此处的出现的次数不会影响解引用的效果

函数指针数组

函数指针数组是一个存放函数指针的数组,存储的函数指针类型必须相同。

格式:

void(*parr[])([参数]);   

例如:

// 需要将加减乘除四个函数存放在一个函数指针数组中
int Add(int x ,int y) {  
	return x + y;
}
int Sub(int x, int y) {
	return x - y;
}
int Mul(int x, int y) {
	return x * y;
}
int Div(int x, int y) {
	return x / y;
}

int main()
{
    // 函数指针数组
	int (*parr[4])(int,int) = { Add,Sub,Mul,Div };
	
    // 使用该数组
    int i = 0;
	int ret = 0;
	for (i = 0; i < 4; i++)
	{
		printf("%d\n", parr[i](4, 2)); // 后面加()传参
	}
	return 0;
}

在这里插入图片描述

练习

// 给下面函数写一个函数指针存放该函数地址
char* my_strcpy(char* dest,const char* src);  
char* (*ptr)(char* ,const char*) = my_strcpy;  // 函数指针

// 写一个函数指针数组存放 ptr函数指针
char* (*parr[4])(char*, const char*) = {&ptr};  // 函数指针数组
函数指针数组的作用
转移表
// 需要实现一个简易计算器功能:
// 如果采用下面的代码,每当需要增加新功能时,switch语句就会变得越来越长。
void menu()
{
    printf("***********************\n");
    printf("***1.Add       2.Sub***\n");
    printf("***3.Mul       4.Div***\n");
    printf("***      0.exit     ***\n");
    printf("***********************\n");
}
int Add(int x, int y) {
    return x + y;
}
int Sub(int x, int y) {
    return x - y;
}
int Mul(int x, int y) {
    return x * y;
}
int Div(int x, int y) {
    return x / y;
}
int main() {
    int input = 0;
    int x = 0;
    int y = 0;
    do {
        menu();
        printf("请选择:>");
        scanf("%d", &input);
        if (1 <= input && input <= 4)
        {
			    printf("请输入两个操作数:>");
        		scanf("%d %d", &x, &y);
        }

        switch (input)
        {
        case 1:
            printf("%d\n", Add(x, y));
            break;
        case 2:
            printf("%d\n", Sub(x, y));
            break;
        case 3:
            printf("%d\n", Mul(x, y));
            break;
        case 4:
            printf("%d\n", Div(x, y));
            break;
        case 0:
            printf("退出成功!\n");
            break;
        default:
            printf("请重新输入:>\n");
            break;
        }
    } while (input);
    return 0;
}

使用函数指针数组来解决代码冗余的问题,这种方法被称作转移表,代码如下:

// 将需要的函数全部存储到函数指针数组中,这样以后需要增加多少新功能,只要函数的类型相同,就可以加入到该函数中,减少代码量
void menu()
{
    printf("***********************\n");
    printf("***1.Add       2.Sub***\n");
    printf("***3.Mul       4.Div***\n");
    printf("***      0.exit     ***\n");
    printf("***********************\n");
}
int Add(int x, int y) {
    return x + y;
}
int Sub(int x, int y) {
    return x - y;
}
int Mul(int x, int y) {
    return x * y;
}
int Div(int x, int y) {
    return x / y;
}

int main() {
    int input = 0;
    int x = 0;
    int y = 0;
    // 创建函数指针数组,将所有函数存入数组
    // NULL 代表0,也就是退出
    int (*pfArr[])(int, int) = { NULL,Add,Sub,Mul,Div };
    int sz = sizeof(pfArr) / sizeof(pfArr[0]);
    do {
        menu();
        printf("请选择:>");
        scanf("%d", &input);
        if (1 <= input && input <= sz - 1)
        {
            printf("请输入两个操作数:>");
            scanf("%d %d", &x, &y);

            // input接收的值是多少,就执行那个函数。
            printf("%d\n", pfArr[input](x, y));
        }
        else if (0 == input)
        {
            printf("退出成功!\n");
        }
        else 
        {
            printf("选择错误!\n");
        }
    } while (input);
    return 0;
}

指向函数指针数组的指针

指向函数指针数组的指针,是一个指针该指针指向一个数组数组中的元素都是函数指针

int main()
{
    int arr[10] = {0};         // arr--数组--> 整型数组
    int (*p)[10] = &arr;       // p --指针--> 指向数组的指针
    
    int (*pfArr[10])(int,int); //pfArr--数组--> 存放函数指针的数组
	int (*(*ppfArr)[10])(int,int) = &pfArr; 
    // 解析:
    // (*ppfArr)表示`ppfArr`是一个指针,指向有是个元素的数组[10],
    // 数组中元素的类型是 int(*)(int,int); 
    // int(*)(int,int) 又表示:
    // (*) 表示是一个指针, 指向(int,int) 参数为2个int类型的函数, 函数的返回值是int。
    return 0;
}

总结函数指针的定义格式

// 函数指针一步一步的演变过程
void Test(int);  // 函数
void (*pf)(int) = Test;  // 函数指针--> 指向Test函数
void (*pfArr[10])(int) = { &pf };  // 函数指针数组--> 存放函数指针的数组
void (*(*ppfArr)[10])(int) = &pfArr // 指向函数指针数组的指针
    
// 观察发现 上面3种定义格式,相同的部分在:
void (*)(int)   // 而这个东西就是函数指针的类型
    
// 1.只加上一个变量名就代表它是一个函数指针:   
void (*pf)(int);     /*-->与定义变量比较 -->*/  int a;
// 2.加上一个 数组名[] 就代表它是一个函数指针数组:    
void (*pfArr[10])(int)};   /*-->与定义数组比较-->*/  int arr[];
// 3.加上一个 数组指针名 (*ptr)[] 就代表他是一个指向函数指针数组的指针:  
void (*(*ppfArr)[10])(int);  /*-->与定义数组指针比较-->*/ int (*parr)[];

/* 可以把 */  void (*)()  看成 -->  int //或者其他内置类型关键字  // 只是定义标识符的位置不同, 函数指针的标识符位置必须在*的右侧 

回调函数

回调函数就是一个通过函数指针调用的函数,如果你把函数的指针(地址)作为参数传递给另一个函数当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方调用,用于对该事件或条件进行响应。

把上面用switch实现的计算器代码拿过来,只需要在定义一个函数接收计算功能函数的地址,然后再调用该函数,可以达到同样的效果。

// 省略打印函数

int Add(int x, int y) {
    return x + y;
}
int Sub(int x, int y) {
    return x - y;
}
int Mul(int x, int y) {
    return x * y;
}
int Div(int x, int y) {
    return x / y;
}

// 存放回调函数的函数
void Calc(int (*pf)(int, int))// 函数指针pf接收函数的地址
{
int x = 0;
int y = 0;
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
printf("%d\n", pf(x, y));   // 通过接收的地址回调对应函数
}

int main() {
    int input = 0;
    do {
        menu();
        printf("请选择:>");
        scanf("%d", &input);
        switch (input)
        { // iuput是几,就进入哪个case语句调用对应得函数
        case 1:
            Calc(Add);  // 只需要将对应得函数地址传给该函数即可
            break;
        case 2:
            Calc(Sub);
            break;
        case 3:
            Calc(Mul);
            break;
        case 4:
            Calc(Div);
            break;
        case 0:
            printf("退出成功!\n");
            break;
        default:
            printf("请重新输入:>\n");
            break;
        }
    } while (input);
    return 0;
}
实现qsort库函数功能

qsort使用的算法是快速排序,这里使用冒泡排序算法

// 传给my_qsort的函数,该函数实现了要排序数据类型的排序规则
// 这个函数是自己实现的,每个类型的排序方式由自己来定
typedef struct Stu{
	char name[10];
	int age;
}Stu;
typedef struct Stu*  StuP;

void Swap(char* ch1, char* ch2, size_t size)
{
	int i = 0;
	for (i = 0; i < size; i++)
	{
		char tmp = *ch1;
		*ch1 = *ch2;
		*ch2 = tmp;
		ch1++;
		ch2++;
	}
}

void my_qsort(void* base, size_t num, size_t size, int (*cmp)(const void* e1, const void* e2)) {
	int i = 0;
	int j = 0;
	
	for (i = 0; i < num - 1; i++)
	{	
		for (j = 0; j < num - i - 1; j++)
		{
			char* firstp = (char*)base + (j * size);        // char*的访问权限只有1个地址块, j*size就是该元素下一个的地址块
			char* secondp = (char*)base + ((j + 1) * size);
			if (cmp(firstp, secondp) > 0)
			{
				Swap(firstp, secondp, size);              
			} 
		}
	}
}

// 比较两个int大小的规则
int cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}

// 比较两个double大小的规则
int cmp_double(const void* e1, const void* e2)
{	
	if ((int)(*(double*)e1 > *(double*)e2)) {
		return 1;   // 升序规则
		//return -1 // 降序规则
	}
	else if ((int)(*(double*)e1 < *(double*)e2)) {
		return -1;  // 升序规则
		//return 1; // 降序规则
	}
	else {
		return 0;
	}
}

// 比较结构体Stu成员变量
// 比较age的方法
int cmp_Stu_by_age(const void* e1,const void* e2) {
	return ((StuP)e1)->age - ((StuP)e2)->age;
}
// 比较name的方法
int cmp_Stu_by_name(const void* e1, const void* e2) {
	return strcmp(((StuP)e1)->name, ((StuP)e2)->name);
}


int main() {
	int arri[10] = { 10,9,8,7,6,5,4,3,2,1 };
	int i = 0;
	my_qsort(arri, 10,sizeof(arri[0]), cmp_int);
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arri[i]);
	}
    
    Stu stu[5] = { "张三",27,"李四",19,"王五",20,"赵六",33,"田七",23 };
	int sz = sizeof(stu) / sizeof(stu[0]);
	my_qsort(stu,sz,sizeof(stu[0]), cmp_Stu_by_name);
	return 0;
}
// void* 类型的指针,可以接收任意类型的地址
// void* 类型的指针,不能进行解引用操作
// void* 类型的指针,不能进行+-整数的操作
快速排序
void Swap(int arr[], int low, int high)
{
	int tmp = arr[low];
	arr[low] = arr[high];
	arr[high] = tmp;
}


int partition(int arr[], int low, int high)
{
	int pivotKey = arr[low];
	while (low < high)
	{
		while (low < high && arr[high] >= pivotKey)
			high--;
		Swap(arr, low, high);
		while (low < high && arr[low] <= pivotKey)
			low++;
		Swap(arr, low, high);
	}

	return low;
}

void quick_sort(int arr[], int low, int high)
{	
	int pivot; 
	if (low < high) {
		pivot = partition(arr, low, high);

		quick_sort(arr,low, pivot - 1);
		quick_sort(arr,pivot + 1, high);
	}
}


int main() {
    
	int arr[] = { 9,4,2,4,1,7,6,5,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	quick_sort(arr, 0, sz - 1);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}

	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值