史上最强C语言教程----指针(进阶部分1)

目录

1. 字符指针

2. 指针数组

3.数组指针

3.1数组指针的定义

3.2 &数组名VS数组名

3.3 数组指针的使用

4. 数组参数、指针参数

4.1 一维传参

4.1.1. 一维数组传参

4.1.2 一维指针数组传参

4.2 二维数组传参

4.3 一级指针传参

4.4 二级指针传参

5. 函数指针


1. 字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char*

一般使用:

int main()
{
    char ch = 'w';
    char* pc = &ch;
    *pc = 'w';
    return 0;
}

还有一种使用方式如下:

int main()
{
    const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
    printf("%s\n", pstr);
    return 0;
}

代码 const char* pstr = "hello bit.";

特别容易让同学以为是把字符串 hello bit 放到字符指针 pstr 里了,但是本质上是把字符串 hello bit. 首字符的地址放到了pstr中。

上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中。

那就有可这样的面试题:

#include <stdio.h>
int main()
{
    char str1[] = "hello bit.";
    char str2[] = "hello bit.";
    const char* str3 = "hello bit.";
    const char* str4 = "hello bit.";
    if (str1 == str2)
        printf("str1 and str2 are same\n");
    else
        printf("str1 and str2 are not same\n");

    if (str3 == str4)
        printf("str3 and str4 are same\n");
    else
        printf("str3 and str4 are not same\n");

    return 0;
}

问:上述代码的输出结果是什么?

这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当 几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化 不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。

2. 指针数组

其实呢,关于指针数组的相关知识,我们在前面的学习中已经了解到了,今天呢,再带大家来了解一下,并且懂得指针数组的相关的使用!

指针数组:存放指针的数组,即数组的每一个元素都是指针。下面简单介绍一下其用法!

#include<stdio.h>
int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };
	int* arr[] = { arr1,arr2,arr3 };//指针数组,每个元素都是指针(数组名代表首元素的地址)
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			printf("%d ", *(arr[i] + j));
			//上面的这一行也可以用下面这一行来代替
			//printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

通过上面的这种方法可以实现遍历数组的元素!当然,其实这也是二维数组的本质所在,后面还会进行相关的讲解,相信大家在后面会对这些有一个更深入的理解,此处只是起到一个抛砖引玉的作用!

3.数组指针

3.1数组指针的定义

数组指针是指针?还是数组?

答案是:指针。

我们已经熟悉:

整形指针: int * pint; 能够指向整形数据的指针。

浮点型指针: float * pf; 能够指向浮点型数据的指针。

那数组指针应该是:能够指向数组的指针。 下面代码哪个是数组指针?

int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?

解释:

int *p1[10];//此处就不再过多解释了,因为这就是我们上面刚才讲过的指针数组
int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个
指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。

3.2 &数组名VS数组名

对于下面的数组:

int arr[10];

arr 和 &arr 分别是啥?

我们知道arr是数组名,数组名表示数组首元素的地址。 那&arr数组名到底是啥?

我们看一段代码:

#include <stdio.h>
int main()
{
    int arr[10] = { 0 };
    printf("%p\n", arr);
    printf("%p\n", &arr);
    return 0;
}

运行截图:

可见数组名和&数组名打印的地址是一样的。

难道两个是一样的吗? 我们再看一段代码:

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	printf("arr = %p\n", arr);
	printf("&arr= %p\n", &arr);
	printf("arr+1 = %p\n", arr + 1);
	printf("&arr+1= %p\n", &arr + 1);
	return 0;
}

运行截图:

根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。

实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。

本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型 数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40

3.3 数组指针的使用

那数组指针是怎么使用的呢?

既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。

看代码:

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5 };
	int(*ptr)[10] = &arr;
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", *((*ptr) + i));
		/*printf("%d ",(*ptr)[i]);*/
		//上面的代码可以由下面的一行代码代替,因为(*ptr)就代表数组名,同时也代表着首元素的地址
	}
	return 0;
}

当然,上面的运用只是一种简单的运用,下面结合二维数组来给大家进行展示一下相关的运用!

#include<stdio.h>
void print(int(*p)[5], int x, int y)
{
	for (int i = 0; i < x; i++)
	{
		for (int j = 0; j < y; j++)
		{
			printf("%d ", *(*(p + i) + j));
		    //printf("%d ", (*(p + i))[j]);
			//printf("%d ", p[i][j]);
			//printf("%d ",*(p[i]+j));
            //后面的三种方式与前面均能达到一样的效果,即输出二维数组
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
	print(arr, 3, 5);
	return 0;
}

大家应该会觉得相对来说不太好理解,下面我将简单给大家进行解释一下!

问题1:为什么在print函数那的形参会是数组指针的形式?

答:因为数组名代表首元素的地址,我们都知道,整型的一维数组的元素是整型,实际上我们在求数组元素类型时常常会发生类似降维一样的情况,即此处由一维降到了点,那么我们来进行类推,二维数组也应该降成一维数组,而数组名我们在前面已经了解到了就是首元素的地址,而数组的地址的类型就是数组指针!

总结:我们在看待二维数组的时候,要把它看成是由一维数组组成的,即二维数组的每个元素就是一维数组,我们在将这个结论扩展到多维时也同样适用,比如三维数组的元素就是二维数组,四维数组的元素就是三维数组,那么其数组名的意义我们就相应的能够了解到了,

问题2:为什么上面的四种形式能够进行互换?

答:首先大家先看第一个为什么行,p代表的是第一行,p+i就是第i行,我们对其进行解引用,就是拿到的是这一行的元素,实际上在此处就是代表的是一维数组的数组名,而数组名又是数组的首地址,将其加上i后就是一维数组第i个元素的地址,再对其进行解引用,我们就得到了第i行第j列的元素。

为了帮助大家理解上面的等价替换,下面会给大家举个例子!

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
		//printf("%d ", arr[i]);
		//printf("%d ", *(p + i));
		//printf("%d ", *(arr + i));
	}
	return 0;
}

p[i] == arr[i] == *(p+i) == *(arr + i)

上面的四种形式其实是等效的!

下面我们进行类比一下,其实我们就能明白上面的四种形式为什么会相同!

*(*(p + i) + j)) ==  (*(p + i))[j]) == p[i][j]) == *(p[i]+j))

其实这个地方也不难,只是由一维扩展到了二维!

4. 数组参数、指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?

4.1 一维传参

4.1.1. 一维数组传参

一维数组传参有三种方法,下面给大家列举出来

#include<stdio.h>
void print(int arr[5])//方法一
//void print(int *arr)方法二
//void print(int arr[])方法三
{
	
}
int main()
{
	int arr[5] = { 1,2,3,4,5 };
	print(arr);
	return 0;
}

注意:其实这三种传参方式本身并没有什么区别,其实无论是上面的哪一种方式进行传参,其本质上都是通过指针的方法进行传参,就像方法二一样,所以在方法一中的数字,写什么都是可以的,并没有任何的问题,在后续的使用上也并没有任何的区别。

4.1.2 一维指针数组传参

#include<stdio.h>
void print(int* arr[5])//方法一(当然,括号中的5也可以不写,也可以随便写一个数)
//void print(int **arr)方法二(因为数组的元素是指针,而数组名代表首元素的地址,指针的地址就是二级指针)
{

}
int main()
{
	int* arr[5];
	print(arr);
	return 0;
}

4.2 二维数组传参

#include<stdio.h>
void print(int arr[2][3])//方法一(中规中矩的二维数组传参,此处一定要注意,行可以省略掉,但是列一定不能省略,同时需要注意,行可以随便写,但是列一定要与原来的数组保持一致,至于为什么,看下一种方法,即本质就能明白,因为列就是数组类型的一部分)
//void print(int (*arr)[3])方法二(也就是二维数组传参的本质所在)
{

}
int main()
{
	int arr[2][3] = { {1,2,3},{4,5,6} };
	print(arr);
	return 0;
}

4.3 一级指针传参

#include <stdio.h>
void print(int* p, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d\n", *(p + i));
	}
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9 };
	int* p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	//一级指针p,传给函数
	print(p, sz);
	return 0;
}

思考:

当一个函数的参数部分为一级指针的时候,函数能接收什么参数?

void test1(int* p)
{}
//test1函数能接收什么参数?
/*
	int a = 1;
	int *p = &a;
	int arr[] = {1,2,3,4,5};
	test1(&a);//可以
	test1(p);//可以
	test1(arr);//可以
*/
void test2(char* p)
{}
//test2函数能接收什么参数?
/*
	char ch = 'w';
	char *p = &ch;
	char*arr[] = "abcde";
	test2(&ch);//可以
	test2(p);//可以
	test2(arr);//可以
*/

4.4 二级指针传参

#include<stdio.h>
void print(int** pp)
{

}
int main()
{
	int a = 10;
	int* p = &a;
	int** pp = &p;
	print(&p);
	print(pp);
	return 0;
}

思考:

当函数的参数为二级指针的时候,可以接收什么参数?

首先可以比较清楚的了解到上面的这两种传参方式肯定是没有问题的,但是除了上面这两种之外还有别的传参方式!

#include<stdio.h>
void print(int** pp)
{

}
int main()
{
	int* p[5];
	print(p);
	//p是指针数组,数组的每一个元素都是指针,而我们传的是指针数组的数组名,即指针数组的首元素的地址
	//指针数组的数组的首元素的地址即指针的地址,其类型就是二级指针
	return 0;
}

5. 函数指针

首先看一段代码:

#include <stdio.h>
void test()
{
	printf("hehe\n");
}
int main()
{
	printf("%p\n", test);
	printf("%p\n", &test);
	return 0;
}

下面是程序的运行结果:

 输出的是两个地址,这两个地址是 test 函数的地址。

这个地方相信大家就会想问了,那么这两个代表的意义时候完全相同呢?还是说像数组一样,数组名和&数组名代表不同的含义呢,这个地方就给大家说明白,函数名和&函数名代表着相同的含义,表示的都是函数的地址,其数值表现形式也都是函数的地址,两者没有任何的区别,在使用上也没有任何的区别

那我们的函数的地址要想保存起来,怎么保存?

下面我们看代码:

void test()
{
	printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void* pfun2();

首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针?

答案是:

pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。

在这个地方相信大家还是不怎么理解,这里给大家进行解释一下,我们知道,对于数组来说,我们把在函数定义时的语句的变量名去掉就能得到定义的变量的类型,对于pfun1来说,我们将变量名去掉后,剩余的部分是void (*)(),如果我们将pfun2去掉之后,剩余的部分是 void *pfun()。

好像这两个乍一看并没有太大的区别,在它们进行定义时的唯一的区别就是pfun1比pfun2多了一个(),我们清楚,()的优先级是大于*的,那么在*pfun1左右加上括号之后,*就将与pfun1变量名进行结合,这就说明了pfun1是一个指针变量而去掉变量名之后,就是一个函数,这就说明了pfun1是一个指向函数的指针变量,所以能够存储函数的地址。

接下来带大家来看一下,pfun2到底是一个什么!因为pfun2的左侧的操作符是*,而右侧的操作符是(),很明显,()的优先级比*要高,所以pfun2先与()进行结合,构成函数,而没有形成指针变量。void*是函数pfun2的返回类型,此处我们就可以进行下结论了,即pfun2是一个函数名,函数的返回类型是void *类型。

这个地方相信已经给大家讲明白了!其实这些在清楚的了解了那些操作符的优先级和结合性顺序之后也并不难理解!

既然我们已经理解了函数指针的相关知识,我们就先简单的运用一下吧!

#include<stdio.h>
int add(int a, int b)
{
	return a + b;
}
int main()
{
	int (*padd)(int a, int b) = add;
	int sum = (*padd)(3, 5);//方法一
	//int sum = padd(3, 5);方法二
	printf("%d ", sum);
	return 0;
}

那么我们该如何取理解上面的两种调用方法呢?其实也不难,我们取理解一下函数名的概念就能比较轻松的理解上面的两种调用方法,函数名和&函数名代表的含义是相同的,都是表示的是函数的地址,下面我会再给出一段代码来帮助大家进行理解!

#include<stdio.h>
int add(int x,int y)
{
	return x + y;
}
int main()
{
	int (*p)(int x, int y);
	p = add;//当然,此处也可以写成p = &add
	//从上面这段代码中其实就可以明白,其实p和add是几乎完全相同的,所以它们的用法也是差不多完全相同的
	int sum = p(3, 4);
	//上面这一行可以用下面的三种形式进行代替
	//int sum = (*p)(3, 4);方法一
	//int sum = add(3, 4);方法二
	//int sum = (*(&add))(3, 4);方法三
	return 0;
}

当然,如果你不相信的话,可以自己在编译器上试一试就ok!

  • 59
    点赞
  • 64
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 42
    评论
很多同学对咱们C语言的课程、学习存在着很多误解,而且很多同学还不知道《C语言高级教程》后面的课程安排是什么,因此这里一并做一个说明。有同学问“别人都说开发数据库系统、Web系统还是Java、C#等最流行,咱们用C语言学了开发也不是浪费吗?”、“C语言不是做嵌入式开发、操作系统等底层的东西吗?”、“我们为什么不讲C语言的嵌入式开发?”、“人家都学Web开发,咱们这学C语言开发C/S的程序不是落伍了吗?”。 确实在实际工作中,由于C语言的门槛比较高,很少有实际项目用C语言进行数据库系统、Web系统等的开发的。但是我不止一次强调“学习时学东西和工作时学东西是不一样的”。 工作以后选用的技术、语言一定是选择做合适、最方便做所从事方面的,比如开发Web程序肯定首选PHP、Java、.net,开发底层系统肯定首选C/C++,开发桌面系统肯定首选VB、Delphi,也就是“用合适的语言做合适的事情”; 但是对于在校生来说则是“用最熟悉的语言做所有事情”。初学编程的人最容易在语言的表层陷入 太长时间,如果要学数据库开发了就要去学Delphi、PB,又要学Web开发了就又去学Java、.net,又要学底层开发了就又去学C/C++, 可是每门语言都没深入,最后真正要学的数据库开发、Web开发、底层 开发等等没怎么学会,倒是把大量的时间浪费在学这些基础语法上,浪费了宝贵的时间, 这也是我痛 恨目前很多大学课程安排的一个原因。因此我的倡导就是对于在校生来说则是“用最熟悉的语言做所 有事情”,我甚至建议大学只学一门C语言就够了,然后就教大家用C语言做所有的方面。
高级进阶c语言教程 目录 1. C 语言中的指针和内存泄漏 5 2. C语言难点分析整理 10 3. C语言难点 18 4. C/C++实现冒泡排序算法 32 5. C++中指针和引用的区别 35 6. const char*, char const*, char*const的区别 36 7. C中可变参数函数实现 38 8. C程序内存中组成部分 41 9. C编程拾粹 42 10. C语言中实现数组的动态增长 44 11. C语言中的位运算 46 12. 浮点数的存储格式: 50 13. 位域 58 14. C语言函数二维数组传递方法 64 15. C语言复杂表达式的执行步骤 66 16. C语言字符串函数大全 68 17. C语言宏定义技巧 89 18. C语言实现动态数组 100 19. C语言笔试-运算符和表达式 104 20. C语言编程准则之稳定篇 107 21. C语言编程常见问题分析 108 22. C语言编程易犯毛病集合 112 23. C语言缺陷与陷阱(笔记) 119 24. C语言防止缓冲区溢出方法 126 25. C语言高效编程秘籍 128 26. C运算符优先级口诀 133 27. do/while(0)的妙用 134 28. exit()和return()的区别 140 29. exit子程序终止函数与return的差别 141 30. extern与static存储空间矛盾 145 31. PC-Lint与C\C++代码质量 147 32. spirntf函数使用大全 158 33. 二叉树的数据结构 167 34. 位运算应用口诀和实例 170 35. 内存对齐与ANSI C中struct内存布局 173 36. 冒泡和选择排序实现 180 37. 函数指针数组与返回数组指针的函数 186 38. 右左法则- 复杂指针解析 189 39. 回车和换行的区别 192 40. 堆和堆栈的区别 194 41. 堆和堆栈的区别 198 42. 如何写出专业的C头文件 202 43. 打造最快的Hash表 207 44. 指针与数组学习笔记 222 45. 数组不是指针 224 46. 标准C中字符串分割的方法 228 47. 汉诺塔源码 231 48. 洗牌算法 234 49. 深入理解C语言指针的奥秘 236 50. 游戏外挂的编写原理 254 51. 程序实例分析-为什么会陷入死循环 258 52. 空指针究竟指向了内存的哪个地方 260 53. 算术表达式的计算 265 54. 结构体对齐的具体含义 269 55. 连连看AI算法 274 56. 连连看寻路算法的思路 283 57. 重新认识:指向函数的指针 288 58. 链表的源码 291 59. 高质量的子程序 295 60. 高级C语言程序员测试必过的十六道最佳题目+答案详解 297 61. C语言常见错误 320 62. 超强的指针学习笔记 325 63. 程序员之路──关于代码风格 343 64. 指针、结构体、联合体的安全规范 346 65. C指针讲解 352 66. 关于指向指针指针 368 67. C/C++ 误区一:void main() 373 68. C/C++ 误区二:fflush(stdin) 376 69. C/C++ 误区三:强制转换 malloc() 的返回值 380 70. C/C++ 误区四:char c = getchar(); 381 71. C/C++ 误区五:检查 new 的返回值 383 72. C 是 C++ 的子集吗? 384 73. C和C++的区别是什么? 387 74. 无条件循环 388 75. 产生随机数的方法 389 76. 顺序表及其操作 390 77. 单链表的实现及其操作 391 78. 双向链表 395 79. 程序员数据结构笔记 399 80. Hashtable和HashMap的区别 408 81. hash 表学习笔记 410 82. C程序设计常用算法源代码 412 83. C语言有头结点链表的经典实现 419 84. C语言惠通面试题 428 85. C语言常用宏定义 450

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鹿九丸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值