深入理解指针(2)
变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量也可以修改这个变量。但是如果我们希望一个变量加上一些限制,不能被修改,应该怎么做呢?这就是const的作用。通过上述代码及编译结果我们可以看出,使用const修饰的n具有常属性,即不能被修改。然而,虽然n不可被修改,但它的本质依旧是变量。具体体现:在C语言的语法规则中,在VS的环境下,数组长度仍然不能用n来表示。但是,当我们把文件类型
一、const修饰指针
(一)const修饰变量
变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量也可以修改这个变量。
但是如果我们希望一个变量加上一些限制,不能被修改,应该怎么做呢?这就是const的作用。
int main()
{
int m = 0;
m = 20;
const int n = 0;
n = 20;
int arr[n];
return 0;
}

通过上述代码及编译结果我们可以看出,使用const修饰的n具有常属性,即不能被修改。
然而,虽然n不可被修改,但它的本质依旧是变量。
具体体现:在C语言的语法规则中,在VS的环境下,数组长度仍然不能用n来表示。
但是,当我们把文件类型改为C++时,数组长度就可以用被const修饰的n来表示了。
那我们还有什么方法修改被const修饰的n吗?如果我们能绕过n本身,去修改n的地址,会不会实现呢?
int main()
{
const int n = 10;
printf("n = %d\n", n);
int* p = &n;
*p = 0;
printf("n = %d\n", n);
return 0;
}

这里我们可以看到,n的值确实修改了。但是我们要思考一下,为什么n的值要被const修饰呢?为的就是不能被修改,如果p拿到n的地址就能修改n,这就打破了const的限制,显然是不合理的。
所以,我们就想:即便p拿到了n的地址,也不能修改n。接下来该怎么做呢?
(二)const修饰指针变量
const修饰指针变量,可以放在*的左边,也可以放在*的右边,意义是不一样的。
int* p;//没有const修饰
int const* p;//const放在*左边做修饰
int* const p;//const放在*右边做修饰
下面,我们来通过具体的代码看一下const的修饰规则:
1.const放在*左边
代码一:
int main()
{
int a = 10;
int b = 20;
int* const p = &a;
p = &b;
//*p = 100;
printf("%d\n", a);
return 0;
}

代码二:
int main()
{
int a = 10;
int b = 20;
int* const p = &a;
//p = &b;
*p = 100;
printf("%d\n", a);
return 0;
}
![]()
const修饰指针变量时,放在*右边限制的是:指针变量本身,指针变量不能在指向其他变量了。
但是可以通过指针变量,修改指针变量指向的内容。
2.const放在*右边
代码三:
int main()
{
int a = 10;
int b = 20;
int const * p = &a;
//p = &b;
*p = 100;
printf("%d\n", a);
return 0;
}

代码四:
int main()
{
int a = 10;
int b = 20;
int const * p = &a;
p = &b;
//*p = 100;
printf("%d\n", a);
return 0;
}

const 修饰指针变量时,放在*的左边,限制的是:指针指向的内容,不能通过指针来修改指向的内容。但是可以修改指针变量本身的值(修改的指针变量的指向)。
代码5:
int main()
{
int a = 10;
int b = 20;
int const * const p = &a;
p = &b;
*p = 100;
printf("%d\n", a);
return 0;
}

最终结论:
1.const修饰指针变量时,放在*右边,限制的是:指针变量本身,指针变量不能在指向其他变量了。但是可以通过指针变量,修改指针变量指向的内容。
2.const 修饰指针变量时,放在*的左边,限制的是:指针指向的内容,不能通过指针来修改指向的内容。但是可以修改指针变量本身的值(修改的指针变量的指向)。
二、指针运算
指针的基本运算有三种,分别是:
1.指针+-整数
2.指针-指针
3.指针的关系运算
(一)指针+-整数
int main()
{
int a = 10;
int* pa = &a;
pa = pa + 1;//这意味着跳过了4个字节
char c = 10;
char* pc = &c;
pc = pc + 1;//这意味着跳过了1个字节
return 0;
}
这意味着,跳过多少个字节取决于数据类型:
type*p:
p+n:跳过了n*sizeof(type)个字节。

根据指针+-整数这一特点,我们可以用一种新的方式遍历数组:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf(" %d", *(p + i));
}
return 0;
}
![]()
(二)指针-指针
先举一个例子:日期-日期=天数。
所以我们可以很好的理解:指针-指针=两个指针之间元素个数。
int main()
{
int arr[10] = {0};
printf("%zd\n", &arr[0] - &arr[9]);
printf("%zd", &arr[9] - &arr[0]);
return 0;
}

这里为什么会出现-9呢?
这是因为,指针也有高低之分:

但是,一定要记住,作差的两个指针必须指向同一空间:
接下来,我们利用指针-指针来实现strlen函数的编写:
代码一:
int my_strlen(char* str)
{
int count = 0;
while (*str != '\0')
{
str++;
count++;
}
return count;
}
int main()
{
char arr[] = { "abcdef" };
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
![]()
此代码是用count来计数,并没有用到指针-指针。
代码二:
int my_strlen(char* str)
{
char* p = str;
while (*str != '\0')
{
str++;
}
return str-p;
}
int main()
{
char arr[] = { "abcdef" };
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
![]()
(三)指针的关系运算
我们也同样可以利用指针的关系运算来遍历下图数组:

int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
while (p < arr + sz)
{
printf("%d ", *p);
p++;
}
return 0;
}
![]()
(四)数组有关知识补充
如果我们定义一个数组,比如:int arr[10]={0},那么一般情况下,arr本身代表的就是整个数组的首地址。
不过有两种情况是例外:
1.sizeof(arr)中的arr代表的是整个数组;
2.&arr拿到的是arr这整个数组的地址。
三、野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
(一)野指针成因
1.指针未初始化
int main()
{
int* p;
*p = 20;//局部变量未初始化,默认为随机值。
return 0;
}
2.指针越界访问
int main()
{
int arr[10] = { 0 };
int* p = arr[0];
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i <= sz; i++)
{
//这里循环到第11次,就发生了越界访问。
printf("%d", *p);
p++;
}
return 0;
}
3.指针指向的空间释放
int* test()
{
int a = 10;
return &a;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
这个代码看似正确,但是当主函数走完test函数后,test函数会自动销毁a所占的空间,此时a没有空间,那么p所指向的地址也就没有了,那么对p解引用也就毫无意义了。
(二)如何规避野指针
1.指针初始化
如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL。
NULL是C语言中定义的一个标识符常量,这个地址是无法使用的,读写改地址会报错。
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;
int* pb = NULL;
return 0;
}
虽然NULL会使指针初始化,但用NULL初始化的指针不能解引用并赋值。

2.小心指针越界
一个程序向内存申请了那些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问、
3.指针变量不再使用时,及时置NULL,指针使用之前检查有效性
当指针变量指向一块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL。
因为有一个约定俗成的规则:只要是NULL指针就不去访问,同时使用指针之前可以判断指针是否为NULL。
4.避免返回局部变量的地址
如造成野指针的第三个例子,不要返回局部变量的地址。
四、assert断言
assert.h头文件定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏常常被称为“断言”。
#include<assert.h>
int main()
{
int a = 10;
int* p = NULL;
assert(p != NULL);
*p = 20;
printf("%d\n", *p);
return 0;
}
上述代码在程序运行到assert时,会验证变量p是否等于NULL。如果确实不等于NULL,程序继续运行,否则就会终止运行,并且给出报错的信息提示。


assert宏接受一个表达式作为参数。如果该表达式为真(返回值非0),assert()不会产生任何作用,程序继续进行。如果该表达式为假(返回值为0),assert()就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。
assert()的使用对程序员是非常有好的。使用assert()有几个好处:
1.自动表示文件和出问题的行号;
2.无需更改代码就可以开启和关闭assert()的机制。
如果已经确定程序没有问题,不需要再做断言,就在#include<assert.h>语句前面,定义一个宏NDEBUG。
#define NDEBUG
#include<assert.h>
我们就可以通过添加或移除NDEBUG这个宏来实现assert的启用与关闭。
assert()的缺点是,因为引入了额外的检查,增加了程序的运行时间。
一般我们可以在Debug中使用,在VS的集成开发环境中,在Release版本中,assert会直接被优化掉。这样在Debug版本中有利于程序员排查问题,在Release版本中不影响用户使用时的程序效率。
五、指针的使用和传址调用
(一)strlen的模拟实现
库函数strlen的功能是求字符串长度,统计的是字符串中'\0'之前的字符的个数。
接下来,我们要严谨、完整地完成这个库函数的编写:
int my_strlen(const char* str)
{
int count = 0;
assert(str!=NULL);
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
int len = my_strlen("abcdef");
printf("%d\n", len);
}
![]()
(二)传址调用和传值调用
学习指针的目的是使用指针来解决问题,但是,或许我们一直都有一个疑问,到底什么样的问题就是要非指针不可?接下来,就举一个最简单的例子。
写一个函数,实现两个整型变量的交换。
如果不用指针,就会有代码一:
void Swap(int m, int n)
{
int tran;
tran = m;
m = n;
n = tran;
}
int main()
{
int a = 520;
int b = 1314;
printf("交换前:a=%d b=%d \n", a, b);
Swap(a, b);
printf("交换后:a=%d b=%d", a, b);
return 0;
}
当我们自信满满地自以为拿下这个简单的题目的时候,我们突然发现,运行结果不尽如人愿:

这是为什么呢?我们来调试、观察一下:

可以看出,a、b和m、n的地址并不一样,所以这里的Swap函数仅仅单纯实现了传值操作,并没有实现传址操作这一根本性的操作。
那我们就要尝试使用指针来写代码二:
void Swap(int* m, int* n)
{
int tran;
tran = *m;
*m = *n;
*n = tran;
}
int main()
{
int a = 520;
int b = 1314;
printf("交换前:a=%d b=%d \n", a, b);
Swap(&a , &b);
printf("交换后:a=%d b=%d", a, b);
return 0;
}

这次,我们会发现运行结果是正确的。
这就是传址调用。
传址调用,可以让函数与主函数之间建立真正的联系,在函数内部可以修改主函数的变量;所以未来函数中只是需要住调函数中的变量值来实现计算,就可以采用传值调用。如果函数内不要修改主调函数中的变量的值,就需要传址调用。
更多推荐



所有评论(0)