一、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;
}

这次,我们会发现运行结果是正确的。

这就是传址调用。

传址调用,可以让函数与主函数之间建立真正的联系,在函数内部可以修改主函数的变量;所以未来函数中只是需要住调函数中的变量值来实现计算,就可以采用传值调用。如果函数内不要修改主调函数中的变量的值,就需要传址调用。

Logo

葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务

更多推荐