C语言入门(十六):指针(2)

目录

[一. const修饰指针](#一. const修饰指针)

[1. const 修饰指针](#1. const 修饰指针)

[1.1 const修饰变量](#1.1 const修饰变量)

[1.2 const 修饰指针变量](#1.2 const 修饰指针变量)

二:指针运算

2.指数运算

[2.1 指针 +- 整数](#2.1 指针 +- 整数)

[2.2指针-指针 (+号不行)](#2.2指针-指针 (+号不行))

2.3指针的关系运算(指针和指针比较大小,或者是地址和地址比较大小)

[三. 野指针](#三. 野指针)

[3. 野指针](#3. 野指针)

[2.1 野指针成因](#2.1 野指针成因)

[3.2 如何规避野指针](#3.2 如何规避野指针)

[3.2.1 指针初始化](#3.2.1 指针初始化)

[3.2.2 ⼩⼼指针越界](#3.2.2 ⼩⼼指针越界)

[2.2.3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性](#2.2.3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性)

[2.2.4 避免返回局部变量的地址](#2.2.4 避免返回局部变量的地址)

[四:assert 断⾔](#四:assert 断⾔)

五:指针的使⽤和传址调⽤

[5.1 strlen的模拟实现](#5.1 strlen的模拟实现)

5.2传值调用和传址调用


1. const 修饰指针

1.1 const修饰变量

变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。 但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。

代码如下:

cpp 复制代码
int main()
{
	const int a = 10; //使用了const进行修饰后,a此时具有了常属性(即不能被修改了),但本质上a还是变量
	a = 20;//上面的a被const修饰后,此时这个代码会报错,但如果放在C++坏境(即.cpp文件)下,它会将a当成常量,因此不会报错,在C语言会报错
	printf("%d\n", a);
	return 0;
}

使用了const进行修饰后,a此时具有了常属性(即不能被修改了),但本质上a还是变量

上面的a被const修饰后,此时这个代码会报错,但如果放在C++坏境(即.cpp文件)下,它会将a当成常量,因此不会报错,在C语言会报错

输出结果如下:

那我们怎么才能打破这种局面呢?

利用指针可以打破上面的a的限制

代码如下:

cpp 复制代码
int main()
{
	const int a = 10;

	int* p = &a;
	*p = 0;

	printf("a=%d\n", a);//此时的a还是被修改了,a=0
	return 0;
}

输出结果如下:

我们可以看到这⾥⼀个确实修改了,但是我们还是要思考⼀下,为什么a要被const修饰呢?就是为了 不能被修改,如果p拿到a的地址就能修改a,这样就打破了const的限制,这是不合理的,所以应该让 p拿到a的地址也不能修改a,那接下来怎么做呢?

1.2 const 修饰指针变量

⼀般来讲const修饰指针变量,可以放在*的左边,也可以放在*的右边,意义是不⼀样的、

const在修饰指针变量的时候如果放在*的右边

cpp 复制代码
int main()
{
	int a = 10;
	int b = 20;

	int  * const  p = &a; //const在修饰指针变量的时候如果放在*的右边,此时限制的是变量它本身,即p,此时我们的p不能再修改,但是我们可以修改它的指针变量指向的内容
     p = & b; //错误

	*p = 100;

	printf("%d\n", a);

	return 0;
}

此时会有个地方不能够更改了

p = & b; //错误

const在修饰指针变量的时候如果放在*的左边

cpp 复制代码
int main()
{
	int a = 10;
	int b = 20;

	int const * p = &a; //如果放在*的左边,那么情况反过来,即限制了指针指向的内容,但我们可以修改指针变量本身,即p
	p = &b;

	*p = 100; //错误

	printf("%d\n", a);

	return 0;
}

此时会有个地方不能够更改了

*p = 100; //错误

如果左右都有,那么即全部限制,此时都不能修改了

cpp 复制代码
int main()
{
	int a = 10;
	int b = 20;
	int const * const p = &a;//如果左右都有,那么即全部限制,此时都不能修改了

	p = &b;//错误
	*p = 123;//错误

	return 0;
}

此时会有个地方不能够更改了

p = &b;//错误

*p = 123;//错误

结论:const修饰指针变量的时候

const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变。

const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指 向的内容,可以通过指针改变。

2.指数运算

2.1 指针 +- 整数

利用下标打印数组元素

cpp 复制代码
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };

	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);//计算元素个数的固定公式,这里的 sz=10
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);//利用下标来打印数组里面的元素,比如arr[0]=1,依次循环
	}
	
	return 0;
}

输出结果:

利用指针打印数组元素

cpp 复制代码
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0]; //这里可以直接写 arr 是一个意思

	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);

	for (i = 0; i < sz; i++)
	{
		printf("%d ", *p);//打印出数组里面的元素
		p++;//换到下一个地址,比如第一个地址是 &arr[0];,输出的是1,这个打印完之后进入循环,到 &arr[1],输出的是2,就这样依次循环下去
	}
	return 0;
}

输出结果和上面一样

还有一个写法,就是将printf函数里面*p改成*(p+i),然后把p++去掉

cpp 复制代码
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];//打印数组里面第一个元素1,这里的0是下标
//	int* p = arr;//上面一行的代码也可以这样写,更加简洁,因为arr数组本身就是地址,所以不用加&

	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);

	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p+i));//打印出数组里面的元素
	}
	return 0;
}
2.2指针-指针 (+号不行)

计算的前提下是:两个指针指向了同一块空间!!!
计算原理:指针 - 指针 = 它们之间隔了多少步数(就是1到9要走8步才能从1变成9,是这个意思)

cpp 复制代码
int main()
{
	int arr[10] = { 0 };
	printf("%zd\n", &arr[9] - &arr[0]);//输出9,大的减小的是一个>0的数
	printf("%zd\n", &arr[0] - &arr[9]);//输出-9

	return 0;
}

错误的写法

cpp 复制代码
指针 - 指针的计算前提是:两个指针指向同一块(数组)空间!!!
int main()
{
	int arr[10] = { 0 };
	char ch[5] = { 0 };

	printf("%zd\n", &ch[4] - &arr[6]);

	return 0;
}

原因是:指针的计算前提是:两个指针指向同一块(数组)空间!!!

正常方式求字符串的长度

#include<string.h> // ---strlen函数要使用的头文件

cpp 复制代码
int main()
{
	char arr[] = "abcdef";

	int len = strlen(arr);

	printf("%d\n", len);  //输出6
	return 0;
}

输出结果:

利用指针求字符串的长度

cpp 复制代码
int my_strlen(char* str) //地址要使用指针来接受,然后创建指针变量str
{
	int count = 0;
	while (*str != '\0') //'\0'是字符串结束标志
	{
		count++; // 记录指针移动了多少步
		str++; //让指针指向下一个字符
	}
	return count;
}

//注意: strlen函数只会计算\0之前的数

int main()
{
	char arr[] = "abcdef";

	int len = my_strlen(arr); //数组名arr是数组第一个元素的地址,即arr == &arr[0]

	printf("%d\n", len);// 输出6
	return 0;
}

输出结果和上面一样

利用指针 - 指针 来计算字符串的长度

cpp 复制代码
int my_strlen(char* str) 
{
	char* start = str; //str自动转为指针,因此前面不用加&,只有当遇到普通变量的时候才要加&
	while (*str != '\0') 
	{
		str++;
	}
	return str - start; //利用指针 - 指针 来计算长度
}

int main()
{
	char arr[] = "abcdef";

	int len = my_strlen(arr); //数组本身就是地址

	printf("%d\n", len);// 输出6
	return 0;
}

输出结果一样是6

2.3指针的关系运算(指针和指针比较大小,或者是地址和地址比较大小)

利用指针关系打印数组

cpp 复制代码
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);//计算数组里面的元素个数,即10个

	int* p = arr;// arr 相当于 &arr[0] 一样的意思,不同的写法

	while (p < arr + sz)//当指针p还没有到达数组末尾的时候,继续循坏
	{
		printf("%d ", *p);
		p++; //让指针指向下一个元素,即下标arr[1],对应元素2,依次往后推
	}

	return 0;
}

输出结果如下:

3. 野指针

概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

2.1 野指针成因
  1. 指针未初始化
cpp 复制代码
 #include <stdio.h>
 int main()
 {        
int *p;//局部变量指针未初始化,默认为随机值
 
*p = 20;
 return 0;
}
  1. 指针越界访问
cpp 复制代码
#include <stdio.h>
 int main()
 {
 int arr[10] = {0};
 int *p = &arr[0];
 int i = 0;
 for(i = 0; i <= 11; i++)
 {
 *(p++) = i;
 }
 return 0;
}
  1. 指针指向的空间释放
cpp 复制代码
 #include <stdio.h>
 int* test()
 {
 int n = 100;
 return &n;
}
 int main()
 {
 int*p = test();
 printf("%d\n", *p);
 return 0;
}

上面的代码全部都是野指针,会报错误的

3.2 如何规避野指针
3.2.1 指针初始化

如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址 会报错

cpp 复制代码
int main()
{
	int a = 10;
	int* p1 = &a;
	*p1 = 20;

	int* p2 = NULL; //空指针
	*p = 200; //此时会报错


	return 0;
}

此时的结果会报错误

3.2.2 ⼩⼼指针越界

⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是 越界访问。

2.2.3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性

当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的 时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问, 同时使⽤指针之前可以判断指针是否为NULL。

我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找⼀棵树把野狗拴起来, 就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓起来,就是把野指针暂时管理起 来

不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我 们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去 使⽤

2.2.4 避免返回局部变量的地址

如造成野指针的第3个例⼦,不要返回局部变量的地址。

四:assert 断⾔

assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报 错终⽌运⾏。这个宏常常被称为"断⾔"。

assert(p != NULL);

上⾯代码在程序运⾏到这⼀⾏语句时,验证变量p 是否等于 NULL,如果确实不等于 NULL ,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰

assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), 任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), 流 st assert() 不会产⽣ assert() 就会报错,在标准错误 derr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。

assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和 出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问 题,不需要再做断⾔,就在 #include 语句的前⾯,定义⼀个宏 NDEBUG

#define NDEBUG

#include<assert.h>

代码如下:

cpp 复制代码
int main()
{
	int a = 10;
	int* p = NULL;

	assert(p != NULL);
	*p = 20;
	printf("%d \n", *p); //此时会报错误,并且会将具体的错误显示出来,这就是assert断言的好处

	return 0;
}

输出结果如下:

然后,重新编译程序,编译器就会禁⽤⽂件中所有的 除这条 assert() 语句。如果程序⼜出现问题,可以移 #define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语 句。

assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。

⼀般我们可以在 Debug 中使⽤,在 发环境中,在 在 Re Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题, lease 版本不影响⽤⼾使⽤时程序的效率。

assert还可以断言其他的表达式(非指针),不一定非要指针

cpp 复制代码
int main()
{
	int n = 0;
	scanf("%d", &n); 
	assert(n > 3); //只有我们输入的值大于3就不会断言

	return 0;
}

如何关掉assert呢?

cpp 复制代码
#define NDEBUG // 我们只需在assert的头文件的上面加上这个代码就行了
#include<assert.h> 

int main()
{
	int n = 0;
	scanf("%d", &n);
	assert(n > 3); // 此时我们可以随便输入值,不会出现断言了,因为已经关闭了

	return 0;
}

只需这样:

#define NDEBUG // 我们只需在assert的头文件的上面加上这个代码就行了

#include<assert.h>

五:指针的使⽤和传址调⽤

5.1 strlen的模拟实现

库函数strlen的功能是求字符串⻓度,统计的是字符串中 函数原型如下:

size_t strlen ( const char * str );

参数str接收⼀个字符串的起始地址,然后开始统计字符串中 \0 之前的字符个数,最终返回⻓度。 如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是 到 \0 就停⽌。

参考代码如下:

cpp 复制代码
#include<assert.h>
8.指针的使用和传址调用
8.1  strlen的模拟实现
int my_strlen(const char* str) //const能够防止  *str 被更改
{
	int count = 0;
	assert(str != NULL); 
	while (*str != '\0')
	{
		count++;
		str++;
	}
	return count;
}

int main()
{
	char arr[] = "abcdef";
	int len = my_strlen(arr);
	printf("%zd\n", len);

	return 0;
}

输出结果:

传值调用和传址调用

5.2传值调用和传址调用

问题:写一个函数,交换两个整数的内容

cpp 复制代码
void Swap2(int* pa, int* pb)
{
	int tmp = 0;
	tmp = *pa; // tmp=a
	*pa = *pb; // a=b
	*pb = tmp; // b=tmp
}

int main()
{
	int a = 10;
	int b = 20;
	printf("交换前:a= %d b=%d \n", a, b);

	Swap2(&a, &b); //传址调用,将a和b的地址传过去
	printf("交换后:a= %d b=%d \n", a, b);
	
	return 0;
}

输出结果如下:

相关推荐
say_fall2 小时前
C语言编程实战:每日刷题 - day 1
c语言·开发语言·学习
IoT智慧学堂2 小时前
C语言流程控制:if判断语句全解析
c语言·开发语言
EXtreme352 小时前
C语言指针深度剖析(2):从“数组名陷阱”到“二级指针操控”的进阶指南
c语言·开发语言·算法
玖剹3 小时前
多线程编程:从日志到单例模式全解析
java·linux·c语言·c++·ubuntu·单例模式·策略模式
下午见。4 小时前
【C语言学习笔记】动态内存分配:malloc/free的正确打开方式
c语言·笔记·学习
Yue丶越4 小时前
【C语言】深入理解指针(三)
c语言·开发语言
luoganttcc4 小时前
已知 空间 三个 A,B C 点 ,求 顺序 经过 A B C 三点 圆弧 轨迹 ,给出 python 代码 并且 画出图像
c语言·开发语言·python
乄夜5 小时前
嵌入式面试高频!!!C语言(十四) STL(嵌入式八股文)
c语言·c++·stm32·单片机·mcu·面试·51单片机
dqsh0614 小时前
树莓派5+Ubuntu24.04 LTS CH348 / CH9344 驱动安装 保姆级教程
linux·c语言·单片机·嵌入式硬件·iot