上一节谈到了内存和地址,指针变量的大小,解引用操作符,取地址操作符,以及指针变量类型的意义,具体内容参考http://t.csdnimg.cn/au8XwC语言指针浅谈(一)。
这篇文章我们继续了解关注指针的内容,包括const修饰指针,指针运算,野指针,assert断言,指针的传址调用等内容。
虽然变量是可以修改的,通过指针变量也可以修改变量的值,但在实际中,我们不希望某些变量的值发生改变,这时候就要用到const关键字,const即constant(不变的)很形象
#include
int main()
{
int m = 10;
m = 20;//m可以修改
const int n = 100;
n = 200;//这时候就会报错,n是不能被修改的
return 0;
}
其实n本质还是变量,只是加上了const之后,在语法上加以限制,此时再对n进行修改,就不符合语法规则,导致报错。
但是如果我们绕过n,使用n的地址去修改n就可以
#include
int main()
{
const int n = 0;
printf("%d\n", n);
int* p = &n;
*p = 10;
printf("%d\n", n);
return 0;
}
我们看到n被修改了,但是我们本来是不想n被修改的,否则也不会闲着没事干用const去修饰n,但我们用指针修改了n,这是不合理的,应该让p拿到n的地址也不能修改n,这时候应该怎么做呢?且看1.2。
先看下面这几段代码,自己先分析一下
#include
//代码1
void test1()
{
int n = 10;
int m = 20;
int* p = &n;
*p = 20;//right
p = &m; //right
}
//代码2
void test2()
{
int n = 10;
int m = 20;
const int* p = &n;
*p = 20;//error,const修饰int* p表示p指向的空间的值不能被修改
p = &m; //right,虽然值不能被修改,但p可以随意指向其他int类型的变量
}
void test3()
{
int n = 10;
int m = 20;
int* const p = &n;
*p = 20; //right,const在*的右边,修饰p,表示p指向的空间的值可以被修改
p = &m; //error,p只能指向n这个空间
}
void test4()
{
int n = 10;
int m = 20;
int const* const p = &n;
*p = 20; //error,两个均不能被修改
p = &m; //error,两个均不能被修改
}
int main()
{
//测试⽆const修饰的情况
test1();
//测试const放在*的左边情况
test2();
//测试const放在*的右边情况
test3();
//测试*的左右两边都有const
test4();
return 0;
}
通过上面的测试分析,我们可以得出一个结论:
const修饰指针变量的时候、
const如果放在*的左边,修饰的是指针指向的内容,指针指向的内容不能通过指针来改变。但是指针变量本身的内容可以改变。
const如果放在*的右边,修饰的是指针变量本身,指针变量本身的内容不能修改,但是指针指向的内容可以通过指针改变。
指针的基本运算有三种:
(1)指针+-整数
(2)指针-指针
(3)指针的关系运算
因为数组在内存中是连续存放的,所以只需要知道第一个元素的地址就可以知道后面的元素
#include
//指针+- 整数
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));//p+i 这⾥就是指针+整数
}
return 0;
}
//指针-指针
#include
int my_strlen(char* s)
{
char* p = s;
while (*p != '\0')
p++;
return p - s;
}
int main()
{
printf("%d\n", my_strlen("abc"));
return 0;
}
这里p-s得到的结果并不是地址,也不是总共的字节大小,而是s~p的元素个数
//指针的关系运算
#include
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
while (p < arr + sz) //指针的⼤⼩⽐较
{
printf("%d ", *p);
p++;
}
return 0;
}
这个大家没必要做过多了解,知道有这个运算即可。
什么是野指针,野指针就是指针指向的位置是不可知的,随机的,不正确的。
那为什么会产生野指针,野指针又有什么危害呢,下面就让我详细介绍。
3.1.1 指针没有进行初始化,这是一个很危险的操作。
#include
int main()
{
int* p;//局部变量指针未初始化,默认为随机值
printf("%d", *p);
return 0;
}
在vs中,p没有进行初始化是无法编译的,有的编译器可能会通过,此时我们多执行几次,发现每次*p的值都是不一样的,这就是野指针,指向是不明确的。
3.1.2 指针越界访问
#include
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;
}
此时运行就会出现“Stack around the variable 'arr' was corrupted”的信息,也就是指针p越界访问了,p此时就是野指针。
3.1.3 指针指向的空间释放
#include
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
3.2.1 指针初始化
指针就是因为没有初始化,不知道指向哪里才成为野指针,如果实在不知道指针指向哪里,我们可以给指针赋值NULL。
NULL是C语言中定义的一个标识符常量,值是0,0也是地址,但这个地址是无法使用的,读写该地址会报错。初始化如下:
#include
int main()
{
int num = 10;
int* p1 = #
int* p2 = NULL;
return 0;
}
3.2.2 小心指针越界
一个程序向内存申请了哪些空间,通过指针也就只能访问这些空间,超出了就是越界访问。
3.2.3 当指针不再使用时,应该及时置为NULL,指针使用之前也要检查其有效性
当一个指针指向一块区域时,我们可以通过指针访问这块区域,后期不再使用这个指针访问空间的时候,我们可以把指针置为NULL,只要是NULL指针就不去访问,同时使用指针之前可以判断指针是否为NULL。
int main()
{
int arr[10] = {1,2,3,4,5,67,7,8,9,10};
int *p = &arr[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;
}
3.2.4 避免返回局部变量的地址
就和3.1.3一样,不要返回局部变量的地址,此时空间已经释放。
assert.h头文件定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合,就会报错终止运行,这个宏被称为“断言”。
assert(p != NULL);
上面的这个代码验证变量p是否等于NULL,如果不等于,程序继续运行,否则终止运行,并给出详细的报错信息提示。 assert()宏接受一个表达式作为参数,如果为真,assert()就不会产生任何作用,程序继续运行。如果为假,assert()就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。
assert()不仅能够自动标识文件和出现问题的行号,还有一种无需更改代码就能开启或关闭assert()的机制。如果确认程序没有问题,不需要再做断言,就在#include
#define NDEBUG
#include
然后重新编译程序,编译器就会禁用文件中所有的assert()语句。如果程序又出现问题,可以移除#define NDEBUG,再进行编译,就重新启动了assert()语句。
我们学习指针,就是要用指针解决问题,那有什么问题,非要用指针不可呢?
例如:写一个函数,交换两个整型变量的值,我们很容易写出下面的代码
#include
void Swap1(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);
Swap1(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
但当我们运行时,发现a和b的值并没有发生交换,为什么呢?
其实是因为实参传递给形参时,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参(传值调用),所以这种方法失败了。
那到底应该怎么办呢,我们要解决的是当调用这个函数时,函数直接操作main函数中的a和b,这时候就考虑使用指针,在main函数中将a和b的地址传给调用函数,通过地址间接进行操作,就可以了。
#include
void Swap2(int*px, int*py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap1(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
这种方法叫做传址调用
传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量。所以如果我们只是需要主调函数的变量值来进行计算,就使用传值调用;如果在修改主调函数的变量的值,就使用传址调用。