指针浅谈(二)

    上一节谈到了内存和地址,指针变量的大小,解引用操作符,取地址操作符,以及指针变量类型的意义,具体内容参考http://t.csdnimg.cn/au8XwC语言指针浅谈(一)。

    这篇文章我们继续了解关注指针的内容,包括const修饰指针,指针运算,野指针,assert断言,指针的传址调用等内容。

 1.const修饰指针

  1.1const修饰变量

        虽然变量是可以修改的,通过指针变量也可以修改变量的值,但在实际中,我们不希望某些变量的值发生改变,这时候就要用到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。

1.2const修饰指针变量

     先看下面这几段代码,自己先分析一下

#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如果放在*的右边,修饰的是指针变量本身,指针变量本身的内容不能修改,但是指针指向的内容可以通过指针改变。

2.指针运算

     指针的基本运算有三种:

 (1)指针+-整数

 (2)指针-指针

 (3)指针的关系运算

2.1指针+-整数

      因为数组在内存中是连续存放的,所以只需要知道第一个元素的地址就可以知道后面的元素

#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;
}

2.2指针-指针

//指针-指针
#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的元素个数

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]);
	while (p < arr + sz) //指针的⼤⼩⽐较
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}

     这个大家没必要做过多了解,知道有这个运算即可。

3.野指针

     什么是野指针,野指针就是指针指向的位置是不可知的,随机的,不正确的。

那为什么会产生野指针,野指针又有什么危害呢,下面就让我详细介绍。

3.1野指针成因

     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如何规避野指针 

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一样,不要返回局部变量的地址,此时空间已经释放。

4.assert断言

         assert.h头文件定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合,就会报错终止运行,这个宏被称为“断言”。

assert(p != NULL);

         上面的这个代码验证变量p是否等于NULL,如果不等于,程序继续运行,否则终止运行,并给出详细的报错信息提示。          assert()宏接受一个表达式作为参数,如果为真,assert()就不会产生任何作用,程序继续运行。如果为假,assert()就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。

          assert()不仅能够自动标识文件和出现问题的行号,还有一种无需更改代码就能开启或关闭assert()的机制。如果确认程序没有问题,不需要再做断言,就在#include语句前,定义一个宏NDEBUG

#define NDEBUG
#include 

           然后重新编译程序,编译器就会禁用文件中所有的assert()语句。如果程序又出现问题,可以移除#define NDEBUG,再进行编译,就重新启动了assert()语句。

5.传址调用

           我们学习指针,就是要用指针解决问题,那有什么问题,非要用指针不可呢?

           例如:写一个函数,交换两个整型变量的值,我们很容易写出下面的代码

​
#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;
}

      这种方法叫做传址调用

      传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量。所以如果我们只是需要主调函数的变量值来进行计算,就使用传值调用;如果在修改主调函数的变量的值,就使用传址调用。

你可能感兴趣的:(C语言,c语言)