第6课 c语言指针1

一  单位转换

1Byte = 8Bit  1Kb = 1024Byte   1Mb = 1024Kb ...   1Pb = 1024Tb

1个比特位和存储一个2进制位0或1

二  指针介绍

内存单元的编号 == 地址 == 指针             名字不一样,意思一样

计算机中的编址,并没有把每个字节的地址记录下来,而是通过硬件设计完成的。好比钢琴,制造商已经把乐器的各个信息(哆来咪发)在硬件上面设计好了,并且所有演奏者都知道,本质是一种约定出来的共识。那么地址也是一样,需要用的时候,就从内存中获取,获取后,CPU也能识别。

int  a = 10;  //变量的本质就是向内存申请空间,向内存申请4个字节的空间,存放数据10
int* p = &a;  //<*> p是指针变量(存放指针); <int>指针变量p所指向的对象a的类型是int;<int *>是p的指针类型
*p = 1;       //*p解引用操作符/间接访问操作符;通过p中存放的地址,找到所指向的a
printf("%d\n", a);  //很多时候a不方便自己修改自己,就用解应用来修改自己,实现的目的一样。

三  指针变量大小

printf("%zd\n", sizeof(char*));    // x86是4   x64是8
printf("%zd\n", sizeof(double*));  //x86是4  x64是8

无论是哪个指针类型,在x86环境下都是4个字节; 在x64环境下都是8个字节;

四  指针的意义

    1,指针的类型决定了对指针解应用的权限,如char*类型只能访问1个字节,而int*类型可以访问4个字节;

     2,利用指针打印数组的每一个元素;

int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr; //&arr[0]
for (int i = 0; i < sz; i++)
{
	//printf("%d ", *p++); //p[i]或*(arr+i)或i[p];
	printf("%d ", *p);
	p++;
}
     3,const修饰符

 const 修饰变量,使得变量不能被修改,但是使用解应用操作符可以修改。     【示例2】

 const 放在左边,限制解应用操作符(*p)修改地址,但是变量p可以修改。   【示例1】

 const 放在右边,限制变量p不能修改,但是解应用操作符(*p)可以被修改。【示例3】

 左边修饰: const int *p 和 int const *p一样; 

 右边修饰: int * const p

 示例1:定义常量n,const放在左边限制解应用操作符,变量p仍然可以修改

const int n = 100;
const int* p = &n;
// *p = 10; //报错;
// n = 10;  //报错;
p = 80; //修改p的值并不会影响n的值;

  示例2:const 修饰常量,解应用操作符正常

const int n = 100;
int* p = &n;
* p = 20;  //通过解应用操作符仍然修改了const定义的常量值
printf("%d\n", n);  //20

   示例3  const 修饰右边,解应用操作符正常,变量p无法被修改

const int n = 100;
int* const p = &n;
* p = 20;  //通过解应用操作符仍然修改了const定义的常量值
// p = 30;  //报错
printf("%d\n", n);  //20

      4,指针运算  

         4.1,指针+整数

p 是变量本身,里面存的是地址;  *p  是p所指向的对象,就是

// 指针打印数组元素
int  arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr;
for(int i=0;i<sz;i++)
{ 
	printf("%d ", *(p + i));
}
// 指针打印字符串元素
char  arr[] = "hello";
char* ch = arr;
while (*ch!='\0')
{
	printf("%c ", *ch++);
}

          4.2,指针-指针

两个指针指向同一块空间,取绝对值得到指针和指针之间元素的个数

int arr[10] = { 0 };

int ret = &arr[9] - &arr[0];   //9

        4.3,strlen求字符串长度,用指针的方式写出strlen的功能

int my_strlen(char* str)  
{
	int count = 0;
	while (*str!='\0')
	{
		count++;
		str++;
	}
	return count;
}

int my_strlen1(char* str)
{
	// a b c d e f '\0'    //str就是a的位置
	char* start= str;    //记录第一个位置

	while (*str != '\0')
	{
		str++;
	}
	return str - start; // 最后一个'\0'的位置减去第一个,得到一共有多少个元素
}


int main()
{
	char arr[] = "abcdef";
	int 直接求 = strlen(arr);
	int 指针求1 = my_strlen(arr);
	int 指针求2 = my_strlen1(arr);
	printf("%d %d %d", 直接求, 指针求1, 指针求2); // 6 6 6

	return 0;
}

          4.4,指针的关系运算

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int sz = sizeof(arr) / sizeof(arr[0]);
while (p<sz+arr) //指针大小比较,最大+最小arr[0]
{
	printf("%d ", *p++);
}

   五  野指针,位置未知

造成原因:1,指针未初始化;2,越界;  

       示例1:未初始化

int* p;
*p = 20;

      示例2:下标越界

int arr[10] = { 0 };
int* p = &arr[0];
for (int i = 0; i <= 11; i++)
{
	*(p++) = i;
}

   

     六  如何规避野指针

         1,如果知道指针指向哪里,就直接赋值地址;

               int a = 10; int* p = &a;

         2,如果不知道指针指向哪里,就赋值NULL(保留数)

               int* ptr = NULL;  //空指针,NULL本质就是0

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;   //arr 和 &arr[0]是一样的,都是表示数组的首元素
int i = 0;
int sz = sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
	printf("%d", *p++);
}
p = NULL;  //指针不再使用要及时置NULL,只要指针是NULL就不会去访问它
//...
p = arr;  //后面如果还想用,就重新赋值即可;
if (p != NULL)
{
	//...
}

       3,防止下标越界;

       4,避免使用返回局部变量的地址;

int* test()
{
	// arr是局部变量,局部变量都放在栈上,返回栈空间的地址很容易造成野指针
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	// ...
	return arr; //相当于首元素
}

int main()
{
	int* p = test();   //这里的p就是野指针,p再找回arr的时候,就会出问题
	return 0;
}

   assert 断言,需要assert.h头文件定义宏assert()

   

int a = 10;
int* p = &a;
if (p != NULL) //每次都判断就会很不方便
{
	//....
}
#define NDEBUG
#include<assert.h>
int main()
{
	int a = 10;
    int* p = NULL;  //故意赋值NULL
	assert(p != NULL); //条件不成立,直接报错
    // 条件不成立,但是又不想让他报错,则在assert.h加入NDEBUG,就相当于设置开关功能.
	//用在非指针上只要条件不满足也会报错,如assert(p)
	return 0;
}

   七  传址调用 和 传值调用

要修改外部变量的值,就需要用传址调用,否则传值调用即可。

      示例1:写一个函数,来交换两个整形变量的值

void Swape(int  x, int y)
{
	int  z = 0;
	z = x;
	x = y;
	y = z;
}

int main()
{
	int a = 10, b = 20;
	printf("a=%d b=%d \n", a, b); // 10 20
	// 形参是实参的临时拷贝,有自己独立的空间,传值调用时,对形参的修改不影响实参
	Swape(a, b);
	printf("a=%d b=%d \n", a, b);  //10 20  值并没有改变
	return 0;
}

       上面的代码,形参交换了变量,但是实参并没有改变,如果想改变实参的值就需要传址

void Swapz(int* px, int* py)
{
	int z = 0;
	z = *px;
	*px = *py;
	*py = z;
}

int main()
{
	int a = 10, b = 20;
	printf("a=%d b=%d \n", a, b);
	Swapz(&a, &b); //传址,传过去是首元素地址
	printf("a=%d b=%d \n", a, b);
	return 0;
}

            通过上面的代码,传址的方式,变量a和b就实现了交换

        示例2:   求两个数的最大值

int  Max(int x, int y)
{
	if (x > y)
		return x;
	else
		return y;
}

int main()
{
	int a = 10;
	int b = 20;
	int m = Max(a, b);
	printf("max=%d \n", m);
	return 0;
}

 八  数组名的理解

数组名是数组首元素的地址,但是有两个例外:

1,sizeof(数组名)   //整个数组的大小,单位字节。

2,&数组名,表示整个数组的地址;

int arr[10] = { 0 };
int a = sizeof(arr); // a = 40 
int b = &arr; 
int c = &arr + 1;
printf("a=%d b=%d c=%d", a, b, c); // a=40;b = 78642328;c = 78642368 相差40

除此之外,数组名就是首元素的地址。

int arr[10] = { 0 }; 
printf("%d \n", &arr[0]);    // 502266376    
printf("%d \n", &arr[0]+1);  // 502266380   //相差1个元素

printf("%d \n", arr);        // 502266376
printf("%d \n", arr + 1);    // 502266380   //相差1个元素

2,使用指针访问数组  

int  arr[3] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;   //首元素;
for (int i = 0; i < sz; i++)
{
	scanf("%d", p++); //p+i 或 p++ 都是指向数组元素;
}
p = arr; //本来p指向最后1个元素,因此让p重新指向第1个元素;
for (int i = 0; i < sz; i++)
{
	printf("%d ", *(p + i));  //p是首元素地址加上下标为i的元素
}
int  arr[3] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;   //首元素;
for (int i = 0; i < sz; i++)
{
	scanf("%d", p++); //p+i 或 p++ 都是指向数组元素;
}
p = arr; //重新指向第一个元素
for (int i = 0; i < sz; i++)
{
	printf("%d ", *(p + i));  //p是首元素地址加上下标为i的元素
}
int  arr[3] = { 1,2,3 };
int* p = arr;
int sz = sz = sizeof(arr) / sizeof(arr[0]);
// 方式1
for (int i = 0; i < sz; i++)
{
	printf("%d ", *p);
	p++;
}

// 方式2
for (int i = 0; i < sz; i++)
{
	// printf("%d ", *p++);
	// printf("%d ", *(p + i));      //p为首元素地址加上i下标
	// printf("%d ", *(arr + i));    // *p=arr 那么p就是arr
	// printf("%d ", p[i]); 
	// printf("%d ", i[arr]);
	// arr[i]    ====    *(arr+i)    //等价,编译器在处理arr[i]的时候也会变成*(arr+i)后才去找数组里面的元素的.
	// *(p+i)    ====    p[i]        //由上面可以得出下面相等
	// 经过刚刚分析得出...
	// arr[i] =等于= *(arr+i) =加法交换= *(i+arr) =可以写成= i[arr]
}

// 验证p+i 是不是 下标i的地址
for (int i = 0; i < sz; i++)
{
	printf("%p ========%p \n", p + i, &arr[i]);   //打印值一模一样
}

 九  一维数组传参

数组传过去的本质就是指针,传过去首元素的地址,可以写成数组形式或指针形式;

void print(int* p, int sz)  //int p[]
{
	for (int i = 0; i < sz; i++)
	{
		//printf("%d ", *p++); 
		printf("%d ", p[i]); // *(p+i) 解应用操作符的值

	}
}


int  main()
{
	int  arr[] = { 1,2,3,4 };
	int  sz = sizeof(arr) / sizeof(arr[0]);
	print(arr,sz);  //arr其实是第一个元素的地址
	return 0;
}

十 冒泡排序

int sort(int* arr, int sz)   // 写成数组也可以  int arr[]
{
	for (int i = 0; i < sz - 1; i++)  //只需要交换sz-1次即可,最后一个不用交换
	{
		// 一趟冒泡排序
		int  flag = 1;  //假设数组元素是有序的
		for (int j = 0; j < sz - i - 1; j++) //每次比较sz-1-i次
		{
			if (arr[j] > arr[j + 1])
			{	
				int  temp = arr[j];  // 交换
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
				flag = 0;   //赋值是0说明还有需要交换的
			}
		}
		if (flag == 1)  //没有交换的数据,退出循环更高效
			break;
	}

}

void print(int* p, int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *p++); 
	}
}

int  main()
{
	int arr[] = { 1,2,3,6,5,4,7,8,9 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	sort(arr, sz);   //传址排序
	print(arr, sz);  //传址打印
	return 0;
}

   十一  二级或多级指针(多级指针很少用)

int  a = 10;     //a是整形变量,a有自己的地址,&a就是a所占4个字节的第一个字节的地址

int* p=&a;      //p是指针变量,p也是有自己的地址,&p就拿到了p的地址
int * *pp = &p;   //pp也是指针变量,二级指针变量也是变量
int*** ppp = &pp;  //三级指针

 十二  指针数组

   类比:整形数组-存整形的数组;  int arr[10];
              字符数组-存字符的数字;  int ch[10];
              指针数组-存指针的数组;  int *parr[5]; char *pc[10];

int  a = 1, b = 2, c = 3;
int* parr[3] = { &a,&b,&c };
int i = 0;
for (i = 0; i < 5; i++)
{
	printf("%d ",*(parr[i]));
}
//  上面的方式也可以,但是很少使用,对地址解引用就可以得到内容

int arr1[] = { 1,2,3,4,5 }, arr2[] = { 3,4,5,6,7 }, arr3[] = { 5,6,7,8,9 };
	int* parr[] = { arr1,arr2,arr3 }; //得到的都是首元素的地址;
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)   //三种写法都可以拿到内容打印
		{
			// printf("%d ", parr[i][j]);
			// printf("%d ", *(*(parr + i)+j));
			// printf("%d ", (*(parr + i))[j]);
		}
		printf("\n");
	}

 十三  数组指针

       1,字符串数组

const char* p = "abcdef";  //const左修饰,解应用失效,变量p仍然可以修改。首元素a的地址存放在指针变量p中
// printf("%c\n", *p);        // a
// p ="efg"; //二次赋值变量p
// printf("%s\n", p);   // p存放的是首元素地址,字符串是连续存放的,打印直到遇到'\0'才停止
// -----------------------------------------------
char str1[] = "hello"; 
char str2[] = "hello";
const char* str3 = "hello";
const char* str4 = "hello";
// str1和str2是不同数组,指向元素首元素地址也不一样;str3和str4是常量,值也无法修改.

   2,数组指针

   数组指针是什么呢?
   字符指针-指向字符的指针,存放字符的地址; char ch='w';char *pc=&ch;
   整形指针-指向整形的指针,存放整形的地址; int num=10; int*p=&num;
   数组指针-指向数组的指针,存放数组的地址; int arr[10]; int (*p)[10]=&arr;
  

 int  arr[6] = { 1,2,3,4,5,6 };
int(*p)[6] = &arr;   // 数组指针;  
for (int i = 0; i < 6; i++)
{
    printf("%d", (*p)[i]);
}

     3,二维数组传参的本质

正常数组的打印

int arr[3][5] = { {1,2,3,4,5},{7,8,9,1,2}, {3,4,5,6,7} };
int  r = 3, c = 5;
for (int i = 0; i < r; i++)
{
	for (int j = 0; j < c; j++)
	{
		printf("%d ", arr[i][j]);
	}
	printf("\n");
}
void test(int(*p)[5], int r, int c)  //接收参数也要是数组指针
{
	for (int i = 0; i < r; i++)
	{
		for (int j = 0; j < c; 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},{7,8,9,1,2}, {3,4,5,6,7} };
	// 二维数组名也是首元素地址,代表二维数组第一个元素{1,2,3,4,5},二维数组在内存中也是连续存放的.
	test(arr, 3, 5);//实参是首元素地址数组,形参接受也要是数组地址;
	return 0;
}

     4,函数指针变量

int  Add(int x, int y)
{
	return x + y;
}


int main()
{
	//  三  函数指针变量
	//  数组指针 - 是指针 -  是存放数组地址的指针
	//  函数指针 - 是指针 -  是指向函数地址的指针
	//  printf("%p\n", &Add);    //对于函数,&函数名和函数名拿到的地址都是函数地址
	//  printf("%p\n", Add);
	int (*pf)(int, int) = &Add;  //函数指针变量pf;   //int是返回值类型,如果void则这里也要写void
	int r = (*pf)(3, 5);
	int r1 = (pf)(3, 5);  //这个*也可以不写,但是不容易被理解.
	printf("%d %d", r, r1);

	return 0;
}
     5,typedef 关键字,用来类型重命名,将复杂的类型简单化。

    typedef unsigned int uint;  //简化前 unsigned int;
    typedef int(*parr_t)[5];  //新的类型名必须在*的右边
    typedef void(*pfun_t)(int); //新的类型名必须在*的右边
    void(*signal(int, void(*)(int)))(int); //可以简化成下面的代码;
    typedef void(*pfun_t)(int);
    pfun_t signal(int, pfun_t);

6,函数指针数组

存放函数指针的数组; 函数参数必须是一致的。

int (*p1[3])(int, int) = { 0, Add, Sub };
int m=p1[1](5, 8);   //调用

   示例:转移表(参数个数必须一致且返回值类型一致)

// 三种方式写加减乘除运算
void  eunm()
{
	printf("************************\n");
	printf("***  1 加法  2减法   ***\n");
	printf("***  3 乘法 4 除法   ***\n");
	printf("***     0 退出       ***\n");
	printf("************************\n");
}

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


void calc(int (*p)(int, int))
{
	int x = 0, y = 0, ret = 0;
	printf("请输入两个运算数>:");
	scanf("%d %d", &x, &y);
	ret = p(x, y);
	printf("运算结果:%d\n", ret);
}


int main2()  //函数指针方式1
{
	int input = 0, x = 0, y = 0, result = 0;

	do
	{
		eunm();
		int (*arr[5])(int, int) = { 0,Add,Sub,Mut,Dut };
		printf("请选择>:");
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("请输入两个数>:");
			scanf("%d %d", &x, &y);
			result=arr[input](x, y);
			printf("运算结果:%d\n", result);
		}
		else if (input == 0)
		{
			printf("退出游戏");
		}
		else
		{
			printf("输入错误,请重新输入\n");
		}
	} while (input);
	
	return 0;
}


int main()  //函数指针方式2
{
	int input = 0, x = 0, y = 0;

	do
	{
		int result = 0;
		eunm();
		printf("请选择>:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			calc(Add);
			break;
		case 2:
			calc(Sub);
			break;
		case 3:
			calc(Mut);
			break;
		case 4:
			calc(Dut);
			break;
		case 0:
			printf("退出游戏");
			break;
		default:
			printf("输入错误!\n");
			break;
		}
	} while (input);
}



int main1()  //正常写法,比较啰嗦
{
	int input = 0, x = 0, y = 0;

	do
	{
		int result = 0;
		eunm();
		printf("请选择>:"); 
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("请输入两个运算数>:"); 
			scanf("%d %d", &x, &y);
			result = Add(x, y);
			printf("加法结果是:%d\n", result);
			 break;
		case 2:
			printf("请输入两个运算数>:");
			scanf("%d %d", &x, &y);
			result = Sub(x, y);
			printf("减法结果是:%d\n", result);
			break;
		case 3:
			printf("请输入两个运算数>:");
			scanf("%d %d", &x, &y);
			result = Mut(x, y);
			printf("乘法结果是:%d\n", result);
			break;
		case 4:
			printf("请输入两个运算数>:");
			scanf("%d %d", &x, &y);
			if (y == 0)
			{
				printf("被除数不能为0\n");
			}
			else
			{
				result = Dut(x, y);
				printf("除法结果是:%d\n", result);
			}
			break;
		case 0:
			printf("退出游戏");
			break;
		default:
			printf("输入错误!\n");
			break;
		}
	} while (input);
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值