目录
2.2.3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
1.const 修饰指针
1.1 const修饰变量
const常属性的意思就是不能改变的意思
加了const就不可以修改a
c语言中
1.这里的a本质上还是变量
2.只是在语法层面上限制了a的修饰
a是常变量
1.2 const 修饰指针变量
int main()
{
const int a = 10;
int* pa = &a;
*pa = 20;
printf("%d\n", a);
return 0;
}
限制的是*pa
不限制pa
限制的是pa
不限制*pa
总结:
const在修饰指针变量的时候:
const 可以放在*的左边,const限制的是pa指向的对象,也就是*pa不能给修改
但是pa不受限制,也就是指针变量可以改变指向
const 可以放在*的右边,const限制的是pa,也就是pa的指向不能改变了
但是*pa不受限制,也就是说pa指向的内容,是可以通过pa来改变的
2. 野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
指针指向的空间,是不属于当前程序的
2.1 野指针成因
1.指针未初始化
之前的
pa是变量,局部变量
局部变量不初始化,它里边放的是一个随机值
2 指针越界访问
循环到第11次的时候,p就是野指针了
3 指针指向的空间释放
int* test()
{
int n = 100;
//...
return &n;//int*
}
int main()
{
int*p = test();
printf("hehe\n");//加了这行代码打印出来*p的值不是100,不加打印出来是100
printf("%d\n", *p);
return 0;
}
n出函数已经销毁
2.2 如何规避野指针
2.2.1 指针初始化
明确知道,指针变量要初始化为 什么值
int a = 10;
int *pa = &a;
不知道指针变量应该给什么值
int *p =NULL;//空指针
意思是p没有指向有效的空间
暂时不要使用它
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL.NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。
2.2.2 ⼩⼼指针越界
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
2.2.3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使⽤指针之前可以判断指针是否为NULL。
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int i = 0;
for (i = 0; i < 5; i++)
{
*p = 5;
p++;
}
p = NULL;
//现在又想使用p
p = arr;
if (p == NULL)
{
printf("p 是空指针\n");
}
else
{
//不为空指针
}
return 0;
}
返回局部变量
利用寄存器
返回局部变量的地址--err错
等于返回栈空间地址
2.2.4 避免返回局部变量的地址
如造成野指针的第3个例⼦,不要返回局部变量的地址。
3 assert 断⾔
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“ 断⾔ ”。
assert(p != NULL);
上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。
assert的头文件#include <assert.h>
#include <assert.h>
为真不报错
为假会报错
assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。
#define NDEBUG#include <assert.h>
assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。
⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在 Release 版本不影响⽤⼾使⽤时程序的效率。
4 指针的使⽤和传址调⽤
4.1 strlen的模拟实现
之前的
strlen是求字符串的长度的
只需要将字符串的起始地址传递给strlen就行
统计的是字符串中\0之前的字符的个数
size_t无符号的整型
size_t my_strlen(const char* str)
{
size_t count = 0;
assert(str != NULL);
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
size_t len = my_strlen(arr);
printf("%zu\n", len);return 0;
}
4.2 传值调⽤和传址调⽤
写⼀个函数,交换两个整型变量的值
代码1:
void Swap1(int x, int y)
{
int z = 0;
z = x;
x = y;
y = z;
}int main()
{
int a = 10;
int b = 20;
printf("交换前: a = %d b = %d\n", a, b);
//
Swap1(a, b); //传值调用
printf("交换后: a = %d b = %d\n", a, b);return 0;
}
可以看出没有交换
Swap1函数在使⽤的时候,是把变量本⾝直接传递给了函数,这种调⽤函数的⽅式我们之前在函数的时候就知道了,这种叫 传值调⽤ 。
结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。
所以Swap1是失败的了。
代码2:
void Swap2(int* pa, int * pb)
{
int z = 0;
z = *pa;//z = a
*pa = *pb;//a = b
*pb = z;
}int main()
{
int a = 10;
int b = 20;
printf("交换前: a = %d b = %d\n", a, b);
//
Swap2(&a, &b); //传址调用
printf("交换后: a = %d b = %d\n", a, b);return 0;
}
成功交换了
这⾥调⽤Swap2函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫: 传址调⽤。
传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改主调函数中的变量的值,就需要传址调⽤。