指针(2)
一、const修饰指针
1.const修饰变量
变量是可以被修改的,若把变量的地址赋值给指针变量,通过指针变量也可以修改变量的值。那有没有一种方法能限制修改变量的值?这就是const
的作用了,const
是常属性,是不能改变的属性。
代码1:
#include <stdio.h>
int main()
{
int m = 10;
m = 20;//变量m的值可以被修改
const int n = 30;
n = 0;//变量n的值不能被修改,会报错误
return 0;
}
上述代码中,其实n
本质上还是变量,只不过被const
修饰后,在语法上加了限制,下面的例子可以说明:在数组那一节中,提到了变长数组的概念,在VS2022上是不支持变长数组语法的,下图所示
但是在C++中,const int n;
这里的n
就是常量。
既然变量n
的值不能直接被改变,那我们绕过n
,通过指针变量的解引用能不能改变呢?如下列代码所示,n
是可以被改变的:
#include <stdio.h>
int main()
{
const int n = 10;
printf("n = %d\n", n);
int* p = &n;
*p = 0;
printf("n = %d\n", n);
return 0;
}
但是既然使用了const
修饰变量n
,n
的值肯定是不能被改变的,使用指针变量p
改变n
的值是在打破语法规则,那有没有方法能限制指针变量的使用呢?
2.const修饰指针变量
结论:
- 当const在*的左边时,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本身的内容可以改变。
- 当const在*的右边时,修饰的是指针变量本身,保证了指针变量的内容不能改变,但是指针变量指向的内容可以通过指针改变。
以下两个const
表示的意思都一样,都是在*
的左边,限制的是*p
的改变:
const int* p = &a;
int const * p = &a;
const
在*
的右边,限制的是p
的改变:
int* const p = &a;
分析具体代码:
#include <stdio.h>
void test1()
{
int n = 10;
int m = 20;
int* p = &n;
*p = 20;//ok
p = &m;//ok
}
void test2()
{
int n = 10;
int m = 20;
int const* p = &n;
*p = 20;//err
p = &m;//ok
}
void test3()
{
int n = 10;
int m = 20;
int* const p = &n;
*p = 20;//ok
p = &m;//err
}
void test4()
{
int n = 10;
int m = 20;
int const * const p = &n;
*p = 20;//err
p = &m;//err
}
int main()
{
test1();//测试无const修饰的情况
test2();//测试const在*的左边的情况
test3();//测试const在*的右边的情况
test4();//测试const在*的两边的情况
return 0;
}
二、野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
1.野指针成因
(1)指针未初始化
#include <stdio.h>
int main()
{
int* p;//局部变量指针未初始化,默认是随机值
*p = 20;//非法访问,p就是野指针
return 0;
}
(2)指针越界访问
当指针指向的范围超出数组arr的范围时,这时就是非法访问了,指针p就是野指针:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
int i = 0;
for (i = 0; i <= sz; i++)
{
*(p++) = i + 1;//后置++,先使用,后++
//上述写法与*p = i + 1;
// p++;相同
}
return 0;
}
(3)指针指向的空间释放
p
得到的n
的地址的那一刻就是野指针了:
#include <stdio.h>
int* test()
{
int n = 10;
return &n;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
n
所占的4个字节空间,在函数test()
返回时,n
的空间就还给了操作系统。
2.如何规避野指针
(1)指针初始化
如果明确知道指针指向哪里,就直接给指针变量赋值地址,若不知道指针指向哪里,就给指针赋值为NULL
。NULL
是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址无法使用,读写该地址会报错:
#include <stdio.h>
int main()
{
int* p = NULL;
*p = 200;
return 0;
}
所以记住一点:指针为空,不去使用;指针不为空,可以使用。
C++里NULL
是0,C语言里NULL
是0强制类型转换成void*
类型的地址,本质都是0:
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
指针初始化如下:
#include <stdio.h>
int main()
{
int n = 30;
int* p1 = &n;
int* p2 = NULL;
return 0;
}
(2)小心指针越界
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
(3)指针变量不再使用时,及时置NULL,指针使用之前检查有效性
当指针变量指向一块区域时,可以通过指针访问这一块区域,当后续不再使用这个指针访问空间时,可以把该指针置为NULL
。因为约定俗成的一个规则是:只要是NULL
指针就不去访问,同时在使用指针前可以判断该指针是否为NULL
。
#include <stdio.h>
int main()
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
int i = 0;
for (i = 0; i <= sz; i++)
{
*(p++) = i;
}
//此时p已经越界了,可以把p置为NULL
p = NULL;
//下次使用p之前,判断p不为NULL的时候再使用
//...
p = &arr[0];//重新让p获得地址
if (p != NULL)//判断
{
//...
}
return 0;
}
(4)避免返回局部变量的地址
如造成野指针的第3个例子,不要返回局部变量的地址。
三、assert()断言
头文件assert.h
定义了宏assert()
,确保在运行时程序符合指定条件,若不符合,就报错终止运行,这个宏常被称为断言。
assert(p != NULL);
上面代码中,程序运行到这一行时,判断p
是否为NULL
,若确实不为NULL
,则程序继续运行,否则就会终止运行,并且给出报错信息。
assert()
宏接收一个表达式作为参数。若该表达式为真(返回值为非0),assert()
不会产生任何作用,程序继续运行;若表达式为假(返回值为0),assert()
就会报错,在标准错误流stderr
中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。
例如:
#include <stdio.h>
#include <assert.h>
int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = NULL;
assert(p != NULL);
for (int i = 0; i < sz; i++)
{
printf("%d ", *p);
p++;
}
return 0;
}
报错出现的图标点击“中止”即可:
assert()
的使用对程序员是非常友好的,使用assert()
的好处:不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert()
的机制。若已经确认程序没有问题,不需要断言,也就是想要关闭assert()
语句,就在#include <assert.h>
语句前面,定义一个宏#define NDEBUG
。
#define NDEBUG
#include <assert.h>
代码1:
#define NDEBUG//关掉断言
#include <stdio.h>
#include <assert.h>
int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
assert(p != NULL);
for (int i = 0; i < sz; i++)
{
printf("%d ", *p);
p++;
}
return 0;
}
代码2:
#define NDEBUG//关掉断言
#include <stdio.h>
#include <assert.h>
int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
int* p = NULL;
assert(p != NULL);
return 0;
}
运行结果,关掉断言,程序不会报错了:
然后重新编译程序,编译器就会禁用文件中所有的assert()
语句。若程序又出现了问题,可以移除这条#define NDEBUG
指令(或者把它注释掉),再次编译就重新启用了assert()
语句,也就是说没有(注释)#define NDEBUG
表示打开断言。
assert()
的缺点是因为引入了额外的检查,增加了程序的运行时间。
一般我们在Debug
中使用,在Release
版本中选择禁用assert
就行。在VS
这样的集成开发环境中,在Release
版本中是直接优化掉了。这样在Debug
版本写有利于程序员排查问题,在Release
版本不影响用户使用时程序的效率。
四、指针的使用和传址调用
1.strlen()函数的模拟实现
在《深入理解指针(1)》“指针运算”里已经写过了strlen()
函数的模拟实现,现在用一下本节学的内容对上一节的strlen()
函数进行优化。
库函数strlen()
的功能是求字符串的长度的,统计的是字符串中\0
之前的字符个数,函数原型如下:
size_t strlen ( const char * str );
模拟实现strlen()
函数的优化:
#include <assert.h>
#include <stdio.h>
size_t my_strlen(const char* str)//用const修饰,防止在函数内部修改*str
{
assert(str != NULL);
size_t count = 0;
while (*str)//\0对应的ASCII码值为0
{
str++;
count++;
}
return count;
}
int main()
{
size_t len = my_strlen("abcdef");
printf("%zd\n", len);
return 0;
}
#include <assert.h>
#include <stdio.h>
size_t my_strlen(const char* str)
{
assert(str != NULL);
const char* start = str;
while (*str)
{
str++;
}
return str - start;
}
int main()
{
char arr[] = "abcdef";
size_t len = my_strlen(arr);
printf("%zd\n", len);
return 0;
}
#include <assert.h>
#include <stdio.h>
size_t my_strlen(const char* str)
{
assert(str != NULL);
const char* p = str;
while (*p)
{
p++;
}
return p - str;
}
int main()
{
char arr[] = "abcdef";
size_t len = my_strlen(arr);
printf("%zd\n", len);
return 0;
}
2.传值调用和传址调用
学习指针的目的是使用指针解决问题,那什么问题非指针不可呢?
例如:写一个函数,交换两个整型变量的值。
可能会写出下面的代码:
#include <stdio.h>
void Swap(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a = %d, b = %d\n", a, b);
Swap(a, b);
printf("交换后:a = %d, b = %d\n", a, b);
return 0;
}
我们发现结果并没有交换:
这是怎么一回事呢?我们可以借助调试查看问题所在:
在main()
函数内部创建变量a
和b
,调用Swap()函数时,a、b作为参数传递给Swap()函数,函数内部创建形参x、y分别接收a、b的值,但是如上图所示,x的地址和a的地址不一样,y的地址也和b的地址不一样,相当于x和y是独立的空间,那么在Swap()函数内部交换x和y的值,自然就不会影响a和b的值,当Swap()函数调用结束后回到main()函数,a和b的值没法交换。上述Swap()函数在使用的时候,是把变量本身传递给了函数,这种叫传值调用。
结论:实参传递给形参时,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参。
传值调用例子:
代码1
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int r = Add(a, b);//传值调用
printf("%d\n", r);
return 0;
}
代码2
#include <stdio.h>
int Max(int x, int y)
{
return (x > y ? x : y);
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int r = Max(a, b);//传值调用
printf("%d\n", r);
return 0;
}
那怎么才能做到在Swap()函数内部交换a、b的值呢?可以尝试把a、b的地址作为参数传给函数:
#include <stdio.h>
void Swap(int* pa, int* pb)
{
int tmp = *pa;//tmp = a
*pa = *pb;//a = b
*pb = tmp;//b = tmp
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a = %d, b = %d\n", a, b);
Swap(&a, &b);
printf("交换后:a = %d, b = %d\n", a, b);
}
将地址作为参数传递给函数,相当于指针变量pa指向变量a、指针变量pb指向变量b:
上述调用Swap()函数时是将变量的地址传递给函数,这种调用函数的方式是传址调用。
传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量。所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用;如果函数内部要修改主调函数中的变量的值,就需要传址调用。