指针浅谈(二)

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

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

 1.const修饰指针

  1.1const修饰变量

        虽然变量是可以修改的,通过指针变量也可以修改变量的值,但在实际中,我们不希望某些变量的值发生改变,这时候就要用到const关键字,const即constant(不变的)很形象


#include <stdio.h>
int main()
{
	int m = 10;
	m = 20;//m可以修改
	const int n = 100;
	n = 200;//这时候就会报错,n是不能被修改的
	return 0;
}

​

       其实n本质还是变量,只是加上了const之后,在语法上加以限制,此时再对n进行修改,就不符合语法规则,导致报错。

        但是如果我们绕过n,使用n的地址去修改n就可以

#include <stdio.h>
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 <stdio.h>
//代码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 <stdio.h>
//指针+- 整数
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
int main()
{
	int* p;//局部变量指针未初始化,默认为随机值
	printf("%d", *p);
	return 0;
}

      在vs中,p没有进行初始化是无法编译的,有的编译器可能会通过,此时我们多执行几次,发现每次*p的值都是不一样的,这就是野指针,指向是不明确的。

     3.1.2 指针越界访问

#include <stdio.h>
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 <stdio.h>
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 <stdio.h>
int main()
{
	int num = 10;
	int* p1 = &num;
	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<assert.h>语句前,定义一个宏NDEBUG

#define NDEBUG
#include <assert.h>

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

5.传址调用

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

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

​
#include <stdio.h>
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 <stdio.h>
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;
}

      这种方法叫做传址调用

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

  • 19
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

c++机械师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值