C语言:指针进阶

一、const(关键字)修饰指针

1.const修饰变量

变量是可以改变的,把地址给一个指针变量来保存时,也可以通过指针来修改该变量

但是如果希望一个变量加上一些限制,不能被修改,怎么做呢?这就是const的作用。
#include <stdio.h>

int main()
{
int m = 0;
m = 20; //m是可以修改的
const int n = 0;
n = 20 ; //n是不能被修改的,报错
return 0;
}

当这个const放在int后面,变量前面时

“表达式必须是可修改的左值”意思是:

例如,5 = 80;这个代码明显是错的,5不是变量,不能用来存80

a被const修饰后,在检查时会认为a是不能被修改的。一般都是这么写:

const int a = 10;

但是如果我们绕过a,使用a的地址,去修改a就能做到了
#include <stdio.h>
int main()
{
const int n = 0;
printf("n = %d\n", n);//输出n=0
int* p = &n;
*p = 20;
printf("n = %d\n", n);//输出n=20
return 0;
}

那么,能不能也不让通过指针来修改变量a当中的内容呢

2.const修饰指针变量

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

const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容(即指针保存的地址)可变。
const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。

(指针指向的内容---*p                      指针变量本身的内容---p)

二、野指针

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

1.野指针成因

a.指针未初始化

#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20; //非法访问内存
return 0;
}

b.指针越界访问

#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
for(i = 0; i <= 11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;

c.指针指向的空间释放

#include <stdio.h>
int* test()
{
int n = 100;
return &n; //n在函数结束时销毁,返回的地址是无效的
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}

2.规避野指针的方式

a.指针初始化

如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL
NULLC语言中定义的一个标识符常量,值是00也是地址,这个地址是无法使用的,读写该地址会报错。

b.小心指针越界

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

c.指针不再使用时,置NULL,使用时检查有效性

int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int i = 0;
for(i = 0; i < 10; i++)
{
*(p++) = i;
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//下次使用的时候,判断p不为NULL的时候再使用
//...
p = &arr[0];//重新让p获得地址
if(p != NULL) //判断
{
//...
}
return 0;
}

d.避免返回局部变量的地址

如不要返回函数内部变量的地址

三、assert断言

1.assert.h

assert.h 头文件定义了宏 assert() ,用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。
这个宏常常被称为断言。(断言是在debug版本之下起作用的!!!

2.用assert来检查某指针是否是NULL

#include<stdio.h>
#include<assert.h>

void func(int* pc)
{
	assert(pc != NULL);
	*pc = 10;
}
int main()
{
	int* p = NULL;
	func(p);
	return 0;
}

3.用assert检查

定义函数时,如果参数是指针,都要进行参数的合法性判断

void func(int* pc)
{
	assert(pc != NULL);
	if (pc == NULL)
	{
		printf("pc = NULL\n");
		return;
	}
   //参数的合法性判断

	*pc = 10;
}
int main()
{
	int* p = NULL;
	func(p);
	return 0;
}

在以上代码中,如果在debug版本下,程序会因为assert断言失败报错;在release版本会打出“pc==NULL”,这样就实现了双重检查,且assert报错会精准给出位置

四、指针的使用和传址调用
 

1.strlen的模拟实现

strlen是C语言中的一个库函数,要使用该函数,要引入头文件<string.h>

size_t 可以理解为unsigned int

字符串——“”引起的若干个字符:“hello”   “a”  (本质是字符数组,结尾自动带\0)

字符——‘’引起的单个字符:‘a’    ‘!’  ‘\n’

字符串的长度——字符的个数:如“hello”是5

怎么理解这个参数const char* str?

char* str = “hello”;  之所以合法,是因为C语言字符串“hello”本质上是一个地址,而不是一个字符串整体。str这个指针存的是hello的起始地址(即字符h的地址)。

如果写成:

char str[ ] = "hello";

才是把字符串的每个字符拷贝到数组里。

“hello”是字符串常量,这种情况是无法被修改的。需要提醒的是,%s是用来打印字符串的,它需要接收一个char*类型的地址(字符串的首地址)

要让字符串可以被修改,必须把它放在栈上的字符数组中:

#include <stdio.h>

int main()
{
    char str[] = "hello";  // 字符串被复制到可修改的栈数组中
    str[0] = 'a';          // 合法修改数组元素
    printf("%s\n", str);   // 输出 aello
    return 0;
}

用一个函数模拟strlen:

int my_strlen(const char* str)
{
	int count = 0;
	assert(str != NULL);
	if (str == NULL)
	{
		return 0;
	}

	while (*str)
	{
		count++;
		str++;
	}
	return count;
}
int main()
{
	int len = my_strlen("abcdef");
	printf("%d\n", len);
	return 0;
}

定义函数时,如果参数是指针,都要进行参数的合法性判断

2..传值调用和传址调用

传值调用:实参的值传递给形参,此时形参的改变并不会影响实参的值

void swap(int x, int y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参。
所以swap是失败的了。

传址调用:将变量的地址传递给函数,在函数内部通过指针来操作变量

void swap(int* px, int* py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
这里调用swap函数的时候是将变量的地址传递给了函数,这种函数调用方式叫:传址调用。
当需要在函数内修改实参时,必须使用传址调用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值