您的当前位置:首页正文

C语言之深入C语言指针

来源:九壹网

0.指针的相关基础知识

(1)连续定义两个指针时,要这样定义:  int *p,*q;

         若定义为这样:int* p,q;则表示p为指针,q类型为In

1.const修饰

1.1--const修饰变量

我们来看一下这句代码:

const int num=0;

在这句代码中,num被const 修饰,它的值不可被修改,那么,便产生了一个问题:此时num是常量还是变量呢?

ans:在C语言中,这个num是个常变量,但num的本质还是个变量,由于有const 的修饰,编译              器在语法上不允许修改这个变量。

          而在C++中,这里的num则为常量。

我们可以看一下下面的代码证明:

const int num=0;

int arr[num];

在C99之前,是不支持变长数组的操作的,由于arrr[n]中的n要求是常量才可以完成编译,所以当我们在VS上面进行调试的时候,如果我们选择的是.c文件,则会报错,若选择的是.cpp文件,则不会报错。

 1.2--const修饰指针变量

        ⼀般来讲const修饰指针变量,可以放在*的左边,也可以放在*的右边,意义是不⼀样的。
但是指针变量本⾝的内容可变。
        (1)const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本身的值是可以改的。
注:const int *p   和int const *p都叫放在左边
        (2) const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变,但是指针变量本身不能修改了。
          (3)const放两边:指针变量不能被修改,指向的内容也不能被修改。
总结:若就想限制p,就放右边,如果想限制指向的内容,就放左边。

2.指针运算

2.1 指针+- 整数

因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸瓜就能找到后⾯的所有元素。

2.2指针减指针

2.2.1指针减指针的绝对值是指针和指针之间的元素个数
2.2.2指针减指针计算的前提是两个指针指向同一空间

3.野指针

1.概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
2.规避野指针
2.1 如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指 NULL.
     NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写         该地址会报错.
2.2小心指针越界, ⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出       范围访问,超出了就是 越界访问。
2.3指针变量不再使⽤时,及时置NULL,指针使用之前检查有效性。

4.assert 断⾔

    assert是C语言标准库提供的调试宏,只有debug模式下有效,使用时需要添加<assert.h>头文        件 ,用于在运行时确保程序符合指定条件,如果不符合,就报 错终止运行。这个宏常常被称为     “断言”。
     assert的使用格式:assert(表达式) 当表达式为真时assert不会触发,为假时assert就会触发,         在vs中的现象就是弹出一个窗口,终止程序执行
     assert(p != NULL );
   上面代码在程序运行到这⼀行语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,       程序 继续运行,否则就会终⽌运行,并且给出报错信息提示。

5.数组名的理解

5.1数组名就是首元素的地址
      一般情况下,该结论成立,但是还有两种特殊情况是 不成立 的:
     (1)sizeof(arr),此种情况下, sizeof中单独放数组名,这里的数组名表示整个数组,计算的                  是整个数组的大小, 单位是字节。
     (2) &数组名,这⾥的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数                组⾸元素 的地址是有区别的)
注:通过(1),我们还可以得到计算一个数组里元素个数的方法,即
                                        int sz=sizeof(arr)/sizeof(arr[0]);
       其中sz为元素个数,sizeof(arr[0])为第一个数的大小,单位是字节。
5.2指针访问数组
     先记住如下公式:
                               
     然后我们解释下原因: 数组元素的访问在编译器处理的时候,是转换成首元素的地址+偏移
                                         量求出元素的地址,然后解引用来访问的。
5.3一维数组传参
     ⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。
注意:数组传参时,就写成test(arr)即可,(test为函数名),传参时,形参这样写 void test(int arr[])(【】中间也可以加数字,也可以用指针来接受 Int * arr)(二者本质是一样的)
故在上述函数中,不能求整个数组的大小,只能求首元素的大小。
由此,若想在函数中遍历数组,则应该把数组元素个数sz一起传过去!!!

6.冒泡排序

冒泡排序的核⼼思想就是:两两相邻的元素进行比较,有需要的话交换。
#include <stdio.h>
int main()
{
	int arr[10] = { 0,5,6,9,2,3,8,4,7,1 };
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = 0; i < sz; i++)           //趟数
	{
		int j = 0;
		for(j = 0; j < sz - 1 - i; j++)    //一趟的细节
		{
			if (arr[j] < arr[j + 1])
			{
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

7.指针数组

1.概念:是存放指针的 数组, 指针数组的每个元素都是用来存放地址(指针)的。指针数组的每个                元素是地址,⼜可以指向⼀块区域。
2.用途:可以用来模拟二维数组。

8.字符指针

在指针的类型中我们知道有⼀种指针类型为字符指针 char* ;
代码 const char* pstr = "hello bit.";
特别容易让同学以为是把字符串 hello bit 放到字符指针 pstr 里了,但是本质是把字符串 hello bit. ⾸字符的地址放到了pstr中。

9.数组指针变量

首先,我们先明白一点:数组指针变量是个指针变量,存放的应该是数组的地址,能够指向数组的指针变量。
注:区分数组指针变量和指针数组
如果要存放个数组的地址,就得存放在数组指针变量中
int (*p)[10]=&arr
上式中,p的类型为int[10]*
数组指针类型解析:
int      (*p)    [10]=&arr
  |         |          |
  |         |          p指向数组的元素个数
  |         p是数组指针变量名
  p指向的元素类型
⼆维数组传参本质上也是传递了地址,传递的是第⼀行这个⼀维数组的地址,那么形参也是可以写成指针形式的。
当然,用我们原来的方法也是可以的:
当然,在后期也可以使用malloc函数来开辟(后期我会讲到)
对此,我们做出总结:⼆维数组传参,形参的部分可以写成数组,也可以写成指针的形式。

10.函数指针变量

     (1函数指针变量是用来存放函数地址的, 函数名就是函数的地址,当然也可以通过 & 函数                  名的方式获得函数的地址。 函数指针变量的写法其实和数组指针非 常类似。
     (2)解析                                       
              int       (*pf3)        (    int x,   int y   )
               |             |                 ----------------
               |             |                         |
               |             |                          pf3 指向函数的参数类型和个数的交代
               |            函数指针变量名      #这里的*表示pf3是指针变量,而不是解引用!!!!
              pf3 指向函数的返回类型
              int (*)(int x,int y)                                         //    pf3 函数指针变量的类型
        (3)函数名和&函数名都是函数的地址。
             我们来看如下代码:不难发现结果都是一样的,原因是方式一为函数直接调用,用函数名               就是调用了函数的地址,与方式二三本质上其实是一样的,而在方式三中,我们没有对pf               进行解引用操作,但是代码仍能够执行的原因就是因为这里的pf是函数add的地址,而方                 式一中的add调用也是函数的地址,二者在本质上是一样的。
        (4)我们来看这样一段代码
分析这段代码时, 我们可以像剥洋葱一样,一层一层的拨开你的心(bushi),我们不难发现,最里面的void(*)()为函数指针,是一种类型,所以我们是将0强制类型转换为函数指针类型,然后去调用0地址处的函数。

11.typedef关键字

typedef 是⽤来类型重命名的,可以将复杂的类型,简单化。
比如,你觉得 unsigned int 写起来不方便,如果能写成 uint 就方便多了,那么我们可以使用:
typedef unsigned int uint;                                     // unsigned int 重命名为 uint
如果是指针类型,将 int* 重命名为 ptr_t ,这样写:
typedef int * ptr_t ;
但是对于数组指针和函数指针稍微有点区别:
比如我们有数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那可以这样写:
typedef int (* parr_t )[5];              // 新的类型名必须在 * 的右边
函数指针类型的重命名也是⼀样的,比如,将void(*)int类型重命名为pf_t,这样写:
typedef void (* pfun_t )( int ) ;           // 新的类型名必须在 * 的右边
则代码 typedef void (* pfun_t )( int );可简化为 pfun_t signal ( int , pfun_t );
typedef优势:
 

12.函数指针数组

1.如何去写函数指针数组

把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
我们知道,函数指针是这样的   int  (*pf)(int ,int),那我们不妨对其进行改造,如下int (*pf[4])(int,int),由此我们创建了函数指针数组,可用于存放函数指针。
注意:[ ]的优先级高于* 
2.函数指针数组的用途
    (1)转移表
x        使用转移表之前:
int sum(int a, int b)
{
	int c = a + b;
	return c;
}
int sub(int a, int b)
{
	return a - b;
}
int mul(int a, int b)
{
	return a * b;
}
int div(int a, int b)
{
	return a / b;
}
int main()
{
	printf("******************************************************\n");
	printf("****************        0.exit          **************\n");
	printf("****************        1.sum           **************\n");
	printf("****************        2.sub           **************\n");
	printf("****************        3.mul           **************\n");
	printf("****************        4.div           **************\n");
	int a,b;
	int input;
	scanf_s("%d", &input);
	switch (input)
	{
	case 0:
		printf("退出计算器\n");
		break;
	case 1:
		scanf_s("%d %d", &a, &b);
		printf("%d", sum(a,b));
		
		break;
	case 2:
		scanf_s("%d %d", &a, &b);
		printf("%d", sub(a,b));
		
		break;
	case 3:
		scanf_s("%d %d", &a, &b);
		printf("%d", mul(a,b));
		
		break;
	case 4:
		scanf_s("%d %d", &a, &b);
		printf("%d", div(a,b));
		
		break;
	default:
		printf("请重新输入");
	}

	return 0;
}
           使用转移表简化后:                     
int sum(int a, int b)
{
	int c = a + b;
	return c;
}
int sub(int a, int b)
{
	return a - b;
}
int mul(int a, int b)
{
	return a * b;
}
int div(int a, int b)
{
	return a / b;
}
int main()
{
	printf("******************************************************\n");
	printf("****************        0.exit          **************\n");
	printf("****************        1.sum           **************\n");
	printf("****************        2.sub           **************\n");
	printf("****************        3.mul           **************\n");
	printf("****************        4.div           **************\n");
	int(*pfarr[5])(int,int) = {0,sum,sub,mul,div};
	int input = 0;

	scanf_s("%d", &input);
	
	if (input >= 1 && input <= 4)
	{
		do
		{
			int a = 0, b = 0;
			int t = 0;
			scanf_s("%d %d", &a, &b);
			t = (*pfarr[input])(a, b);
			printf("%d\n", t);
			break;
		} while (input);
	}
	else if (input == 0)
	{
		printf("exit\n");
	}
	else
	{
		printf("wrong!\n");
	}
}

13.回调函数

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

简述:回调函数起中间商的作用

2.应用

这个为简化之前

int sum(int a, int b)
{
	int c = a + b;
	return c;
}
int sub(int a, int b)
{
	return a - b;
}
int mul(int a, int b)
{
	return a * b;
}
int div(int a, int b)
{
	return a / b;
}
int main()
{
	printf("******************************************************\n");
	printf("****************        0.exit          **************\n");
	printf("****************        1.sum           **************\n");
	printf("****************        2.sub           **************\n");
	printf("****************        3.mul           **************\n");
	printf("****************        4.div           **************\n");
	int a,b;
	int input;
	scanf_s("%d", &input);
	switch (input)
	{
	case 0:
		printf("退出计算器\n");
		break;
	case 1:
		scanf_s("%d %d", &a, &b);
		printf("%d", sum(a,b));
		
		break;
	case 2:
		scanf_s("%d %d", &a, &b);
		printf("%d", sub(a,b));
		
		break;
	case 3:
		scanf_s("%d %d", &a, &b);
		printf("%d", mul(a,b));
		
		break;
	case 4:
		scanf_s("%d %d", &a, &b);
		printf("%d", div(a,b));
		
		break;
	default:
		printf("请重新输入");
	}

	return 0;
}

这个为简化后

int sum(int a, int b)
{
	int c = a + b;
	return c;
}
int sub(int a, int b)
{
	return a - b;
}
int mul(int a, int b)
{
	return a * b;
}
int div(int a, int b)
{
	return a / b;
}

void calc(int (*pf)(int a, int b))
{
	int a, b;
	int r = 0;
	scanf_s("%d %d", &a, &b);
	r = pf(a, b);
	printf("%d", r);

}
int main()
{
	printf("******************************************************\n");
	printf("****************        0.exit          **************\n");
	printf("****************        1.sum           **************\n");
	printf("****************        2.sub           **************\n");
	printf("****************        3.mul           **************\n");
	printf("****************        4.div           **************\n");
	int input;
	scanf_s("%d", &input);
	int r = 0;
	switch (input)
	{
	case 0:
		printf("退出计算器\n");
		break;
	case 1:
	    calc(sum);
		break;
	case 2:
		calc(sub);
		break;
	case 3:
		calc(mul);
		break;
	case 4:
		calc(div);
		break;
	default:
		printf("请重新输入");
	}

	return 0;
}

我们将其拆解一下:

这个为函数部分,不可省略

int sum(int a, int b)
{
	int c = a + b;
	return c;
}
int sub(int a, int b)
{
	return a - b;
}
int mul(int a, int b)
{
	return a * b;
}
int div(int a, int b)
{
	return a / b;
}

这个为回调函数部分,用于简化代码,起中间商的作用

void calc(int (*pf)(int a, int b))
{
	int a, b;
	int r = 0;
	scanf_s("%d %d", &a, &b);
	r = pf(a, b);
	printf("%d", r);

}

这个为函数主体部分

int main()
{
	printf("******************************************************\n");
	printf("****************        0.exit          **************\n");
	printf("****************        1.sum           **************\n");
	printf("****************        2.sub           **************\n");
	printf("****************        3.mul           **************\n");
	printf("****************        4.div           **************\n");
	int input;
	scanf_s("%d", &input);
	int r = 0;
	switch (input)
	{
	case 0:
		printf("退出计算器\n");
		break;
	case 1:
	    calc(sum);
		break;
	case 2:
		calc(sub);
		break;
	case 3:
		calc(mul);
		break;
	case 4:
		calc(div);
		break;
	default:
		printf("请重新输入");
	}

	return 0;
}

14.qsort函数

1.简介:qsort是库函数,它可以实现任意类型数据的排序,快速排序,其底层使用了回调函数的                 方式,头文件为<stdlib.h>

z2.解释

void qsort(void* base,   //base中存放的是待排序数组的第一个元素地址
           size_t num,   //num存放的是base指向的数组中的元素个数
           size_ size,   //size是base指向的数组中一个元素的长度,单位是字节
           int(*compare)(const void*e1,const void*e2)   //函数指针—指向了一个比较函数,这个 
                          比较函数是用来比较数组中两个元素的 

                         //如果e1指向的元素大于e2指向的元素,那么函数返回>0的数字
                         //如果e1指向的元素等于e2指向的元素,那么函数返回0
                         //如果e1指向的元素小于e2指向的元素,那么函数返回<0的数字

3.实例

3.1使用后qsort改良冒泡排序后

#include <stdlib.h>
#include <stdio.h>
int cmp(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;           //由于此处是升序,故e1-e2,若排降序,则e2-e1
}
void print(int arr[],int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}
int main()
{
	int arr[10] = { 0,5,6,9,2,3,8,4,7,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp);
	print(arr,sz);
	return 0;
}

使用动态内存管理后在升级一下(不懂的可以暂时跳过,后续文章会讲)!

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
#include<stdlib.h>
int cmp(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}

int main()
{
	int n = 0;
	scanf_s("%d", &n);
	int* p = (int*)malloc(4 * n);
	for (int i = 0; i < n; i++)
	{
		scanf_s("%d", (p + i));
	}
	qsort(p, n, 4, cmp);
	for (int i = 0; i < n; i++)
	{
		printf("%d", p[i]);
	}
	free(p);
	p = NULL;
	return 0;
}

3.2使用 qsort排序其他类型的数据

(1)以年龄来排序

#include<stdio.h>
#include<stdlib.h>
struct stu
{
	char name[20];
	int age;
};
int cmp_by_age(const void* e1, const void* e2)
{
	return (*(struct stu*)e1).age - (*(struct stu*)e2).age;
}

int main()
{
	struct stu s[3] = { {"zhangsan",56},{"lisi",12},{"wangwu",16} };     //此处为结构体数组
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_by_age);
	for (int i = 0; i < 3; i++)
	{
		printf("%s %d\n", s[i].name, s[i].age);
	}
	return 0;
}

最后的打印部分这样写也行 

struct s
{
	char name[100];
	int age;
};
int cmp_by_age(const void* e1, const void* e2)
{
	return (*(struct s*)e1).age - (*(struct s*)e2).age;
}
void print(struct s* s1)
{
	for (int i = 0; i < 3; i++)
	{
		printf("%s %d\n", s1[i].name, s1[i].age);
	}
}
int main()
{

	struct s s1[3] = { {"syn",20},{"zmy",18},{"dl",22} };
	int sz = sizeof(s1) / sizeof(s1[0]);
	qsort(s1, sz, sizeof(s1[0]), cmp_by_age);
	print(s1);
	return 0;
}

结果如下

 

(2)以名字来排序

由于名字是字符串,不能直接使用>比较,应该使用strcmp函数比较大小,头文件<string.h>

strcmp(字符串1,字符串2)

如果字符串1大于字符串2,返>0的数字

如果字符串1大于字符串2,返0

如果字符串1大于字符串2,返<0的数字

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct stu
{
	char name[20];
	int age;
};
int cmp_by_name(const void* e1, const void* e2)
{
	return strcmp((*(struct stu*)e1).name, (*(struct stu*)e2).name);
}

int main()
{
	struct stu s[3] = { {"zhangsan",56},{"lisi",12},{"wangwu",16} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), cmp_by_name);
	for (int i = 0; i < 3; i++)
	{
		printf("%s %d\n", s[i].name, s[i].age);
	}
	return 0;
}

因篇幅问题不能全部显示,请点此查看更多更全内容

Top