C语言之指针(1)

1.内存和地址:(1)首先,要想了解指针,必须要先了解内存和地址。举个例子:有一栋楼,里面有很多房间,你要去找一个人,如果没有门牌号这些的信息的话,要想找到这个人,你就得一间一间的去找才可以找到,这会非常麻烦,但是如果给每个房间编了号,这时候你要去找这个人,只要知道他在哪个房间号,就可以找到了,这样就提高了查找的效率。计算机上CPU(中央处理器)在处理数据的时候,需要的数据是从内存中读取的,处理后的数据也会放回内存当中。我们把内存划分为一个一个的内存单元,每个内存单元的大小取一个字节,并且会给每个内存单元编个号,有了这个号,CPU就可以快速找到一个内存空间,把门牌号称为地址,在计算机中也把内存单元的称为地址,C语言中给地址起了个新名字:指针。所以可以理解为内存单元的编号==地址==指针。

计算机能够识别、存储和运算的时候都是使用2进制位的,2进制只有0和1,要把这一个1或者0存起来,这个时候所需要的空间的大小就是1个bit,所以一个bit位里面存着的就是一个2进制位。再往上就是字节byte,1byte=8bit,1kb=1024byte,1mb=1024kb,然后就是gb、tb、pb,都是1024来转换的。1024是2的十次方。

(2)如何理解编制:计算机内有许多硬件单元,而硬件单元是要相互协同工作的,至少要相互之间能够进行数据的传递。为了实现传递数据,就用电线连起来。CPU从内存中获取数据叫做读操作或者输入操作,把数据传输到内存中叫做写操作或者输出操作,CPU和内存中有控制总线、数据总线和地址总线,在读取数据的时候,首先地址信号从CPU通过地址线传输给内存,内存知道要那个内存单元,然后再通过数据总线将内存单元的数据传到CPU去。控制总线则传的是这次在做什么。

跟钢琴一样,每个琴键代表一个音,而这些琴键对应的音符已经被制造商在乐器硬件层面上设计好了,并且所有的演奏者都知道,本质是一种约定出来的共识。硬件编址也是如此,关于地址线,电脑有分32位机器和64位机器的,在32机器中有32根电线,电线通电时,会产生0和1的信号,总共有32根,一次就有32个数字,就是一个二进制序列,这个二进制序列产生的数字作为一个编号的话,那么就对应一块空间,总共32根,一根有两种情况,那么就可以产生2的32次方个二进制序列。以后CPU在访问内存时就只需要传输地址,就可以对应到内存的相应位置,内存中的地址都是固定死的。我们在程序中编写出来的地址都叫虚拟地址,通过操作系统和硬件将其转换为物理地址才可以在内存中得到数据。

总结一下就是:内存会被划分为一个一个的内存单元,每个内存单元大小都是一个字节。每个内存单元都会给一个编号,也就是地址或者指针。

2.(1)指针和指针变量:变量创建的本质就是在内存中开辟一块空间。例如:int a,那么内存中有一个字节的空间是给a这个变量的,我让a=17,那么17放在这块空间里面。

红色部分为a的空间 

调试一下:

看最左边,由此可知确实是分配了四个字节,且是连续的,但是并不是这四个地址都指向a,因为没有办法把a给分裂开,假如去监视窗口&a的时候,就有:

 就发现只有一个,且还是第一个字节的地址,所以只要找到第一个字节的地址,就可以顺藤摸瓜找到后面的地址。

既然已经知道a的地址了,地址也是一个值,是一个常量,那么是不是可以将它存起来放到内存中去,那么就可以定义一个变量p,把a的地址赋值给p,p=&a,但是p在创建的时候有点不一样,因为存放的是有特殊意义的值,所以就有一个写法:int* p=&a。则p就是一个指针变量了,地址我们也称为指针,把地址放到p里面也可以说把指针放到p里面,由此也说明指针变量p是存放指针的变量,所以也要记住:指针变量就是用来存放地址的。

(2)指针变量的类型:int a=17即在内存中开辟四个字节的空间存放a这个变量,值为17,且a的第一个字节的地址&a放到int* p里面去,这样子又开辟一块空间存放p这个变量值为地址的值,这样通过p就可以找到a,就像是p指向a一样。p的类型为int*。关于int*,需要分开理解,这里的*是说明p是一个指针变量,前面的int则说明p指向的对象是int类型的。

(2)解引用操作符:得到了a的地址并把它存到指针变量p里面,现在想要通过p对a的值进行访问,就直接*p,这个操作叫做解引用操作,*叫做解引用操作符。这个时候*p等价于a,*p=17。

总结一下,指针其实就是地址,指针变量就是存放指针(地址)的变量。口头语中说的指针就是指针变量。

(4)指针变量的大小:指针变量需要多大空间,取决于地址的大小,地址的存放需要多大空间,指针变量的大小就是多少。在32位的机器上,地址是32个0或者1的二进制序列,一个数字是1个bit那么总共就要32个bit位,也就是四个字节,64位的机器就是八个字节。所以指针类型的大小和类型是无关的,只要是指针变量,在同一个平台下,大小都是一样的。

3.指针类型的意义:

(1)指针的解引用:

首先创建变量并调试:

继续往下调试:

 可见当走到*pa=0时,a的值瞬间变为0,地址上面这四个数字都变成0。

接下来把int*变为char*并调试

f10继续走下去直到*pa=0,

 发现只变了一个。由于指针类型的变化,解引用也发生了变化。由此可知,指针类型决定了指针进行解引用操作时访问多大的空间。int*解引用访问4个字节的空间,char*解引用访问1个字节的空间。

(2)指针加减:

首先写一段代码:

int main()
{
	int a = 17;
	int* pa = &a;
	char* pc = &a;
	printf("pa  =%p\n", pa);
	printf("pa+1=%p\n", pa + 1);
	printf("pc  =%p\n", pc);
	printf("pc+1=%p\n", pc + 1);
	return 0;
}

可见int*类型的+1地址加了4个字节,char*类型的+1地址加了1个字节。因为pa指向的是整型,+1就跳过一个整型,pc指向字符,所以+1跳过一个字符。 

结论:指针类型决定了指针的步长,就是向前/向后走一步多大距离。type* p:p+i 是跳过i个type类型的数据。那么int* p,p+2就是跳过两个int类型的数据,就是8个字节。

(3)void*:这是一种特殊类型的指针,可以理解为无具体类型的指针(或者叫泛型指针)。这种类型的指针可以用来接受 任意类型的地址,但是它不能进行指针的加减整数和解引用操作。

4.const修饰指针:

(1)const修饰变量:

int main()
{
	const int n = 10;
	n = 10;
	printf("%d\n", n);

	return 0;
}

运行后发现编译错误:

const修饰了n,n就没有办法再去修改了,但是n还是变量,但是利用指针解引用操作就可以修改:

int main()
{
	const int n = 10;
	int* p = &n;
	*p = 0;
	printf("%d\n", n);

	return 0;
}

加上了const是为了让n不被修改,但是通过指针却可以修改,这样做const失去了意义,所以应该让p拿到的地址也不被修改,于是就需要const修饰指针变量。

(2)const修饰指针变量:

一般来讲const修饰指针变量,可以放在*的左边,也可以放在右边。

创建一个指针变量p,里面存放的是别人的地址,但是它自己本身也是一个变量,所以它也有自己的地址。

关于这两种写法的区别:

最后结果是可以编译的,也就是说,当p去指向n的时候,但是const放左边限制了*p,所以没有办法去修改n,但是p也可以改变指向对象去修改m。放左边限制了*p,意思是不能通过p来改变p指向的对象,但是p可以指向其他变量。

 当写在右边的时候,反而可以去修改n的值,但是没有办法去改变指向对象了。

5.指针运算:

(1)指针加减整数:访问int数组中的元素,可以用老方法,也可以用指针。由于数组在内存中是连续存放的,地址由低到高,所以只要找到起始元素的地址的话,就可以访问后面的。又由于int数组里面的元素是一个整型一个整型的,所以访问一个就跳过一个整型,就可以使用整型指针。那么就把首元素的地址传给整型指针,访问完一个之后就p++跳到下一个元素的地址,再使用循环就可以了。

#include <stdio.h>
int main()
{
	int arr[5] = { 1,2,3,4,5 };
	int sz = sizeof(arr) / sizeof(arr[0]);

	int* p = &arr[0];
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *p);
		p++;
	}

	return 0;
}

(2)指针-指针:在日常生活中,有日期+-天数=日期,日期-日期=天数,日期和日期也可以比较大小,但是日期+日期意义不是很大。C语言中,指针可以+-整数得到另外的指针值,指针-指针就得到两者之间的元素个数。

#include <stdio.h>
int main()
{
	int arr[5] = { 0 };
	printf("%d\n", &arr[4] - &arr[0]);//指针-指针
	printf("%d\n", &arr[0] - &arr[4]);

	return 0;
}

所以指针-指针的绝对值是指针之间的元素个数,但是前提是两个指针指向同一块空间。 

int main()
{
	int arr[5] = { 0 };
	printf("%d\n", &arr[4] - &arr[0]);//指针-指针
	printf("%d\n", &arr[0] - &arr[4]);
	char ch[5] = { 0 };
	printf("%d\n", &arr[4] - &ch[0]);//err

	return 0;
}

这样写是错误的,第一是因为这两个数组在内存中是不是连续的,中间有多大距离是不确定的,第二是因为一个是整型数组一个是字符数组,如果是求两者之间元素的个数的话,那么是求整型个数还是字符个数是不确定的。

例:模拟实现strlen函数:

int my_strlen(char* p)
{
	int count = 0;
	while (*p != '\0')
	{
		count++;
		p++;
	}
	return count;
}
int main()
{
	char arr[] = "abcdef";
	int len = my_strlen(arr);
	printf("%d", len);
	return 0;
}
int my_strlen(char* p)
{
	char* p1 = p;
	while (*p != '\0')
	{
		p++;
	}

	return p - p1;
}
int main()
{
	char arr[] = "abcdef";
	int len = my_strlen(arr);
	printf("%d", len);
	return 0;
}

(3)指针的关系运算:指针可以比较大小,那么可以运用指针的关系运算来实现访问数组元素的操作:

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = &arr[0];
	while (p < arr + sz)//指针的关系运算
	{
		printf("%d ", *p);
		p++;
	}

	return 0;
}

 6.野指针:就是指针指向的位置是不可知的(随机的、不正确的或没有明确限制的)。可以把它类比为野狗,没有人管着没有绳子拴着很危险。

(1)造成野指针的可能原因:

*指针未初始化:

p此时是个局部变量,局部变量不初始化的时候它的值是随机值,这样子指针去访问空间的时候会随便找个地址进去,你自己也不知道哪个地方的内存被放了个20进去。

*指针越界访问:

int main()
{
	int arr[5] = { 0 };
	int* p = &arr[0];
	int i = 0;
	for (i = 0; i <= 6; i++)
	{
		//当指针指向范围超过数组arr的范围时,p就是野指针
		*(p++) = i;
	}
}

*指针指向的空间释放:

int* test()
{
	int n = 100;
	return &n;
}

int main()
{
	int* p = test();
	printf("%d\n", *p);
	return 0;
}

当函数调用结束后,n的空间就被回收了,最后想用p存的地址访问n这一块空间,似乎不太合适。

就比如你去外面住酒店,交了钱就住一晚上,然后你通知你的朋友你的房间号让他明天来找你,

结果你第二天就走了并且把房间退了,你的朋友到了且不知道你离开了,就直接进去了,是不是不太合适。

(2)避免野指针:既然如此,为了避免野指针,就应该不放上面的错误。

*指针初始化:当明确知道指针变量的指向是,就直接给地址了就好。如果不知道指向,就赋一个NULL(是0,被强制类型转换为void*,本质是0),就是给它赋值一个地址为0的地址,赋值NULL后也叫它空指针。但是赋值了NULL就不能进行解引用操作,

#include <stdio.h>
int main()
{
	int* p = NULL;
	*p = 20;
}

因为NULL这个地址是无法使用的,读写该地址也会报错。

*小心指针越界。

*指针不再使用的时候,及时将其置为空指针,使用之前检查有效性。

*避免返回局部变量的地址 。

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

上面代码如果在程序运行到这一句时,验证p是否等于NULL,不是就继续运行,是就终止运行并给出报错信息提示。

assert( )宏接受一个表达式作为参数,表达式为真(返回值非0),assert不会产生任何作用,程序继续运行。如果为假(返回值为0)就会报错,并在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的头文件名和行号。

#include <assert.h>
#include <stdio.h>
int main()
{
	int n = 10;
	int* p = &n;
	assert(p != NULL);
	*p = 20;
	printf("%d ", n);

	return 0;
}

#include <assert.h>
#include <stdio.h>
int main()
{
	int n = 10;
	int* p = NULL;
	assert(p != NULL);
	*p = 20;
	printf("%d ", n);

	return 0;
}

断言失败,p不等于空指针。 

也可以不止断言指针:

#include <assert.h>
#include <stdio.h>
int main()
{
	int n = 0;
	scanf("%d", &n);
	assert(n != 0);
	printf("%d\n", n);

	return 0;
}

assert( )的使用对程序员来说十分友好,不仅能自动标识文件和出问题行号,还有一种无需更改代码就能开启或关闭assert( )的机制,如果已经确认程序没有问题,不需要做断言,就在#include <assert.h> 语句前定义一个宏#difine NDEBUG,然后重新编译程序编译器会禁用文件中所有的assert语句。但是缺点是引入了额外的检查,增加程序运行时间。

一般可以在Debug中使用,在Release版本中选择禁用就可以了,在VS这样的集成开发环境中,在Release版本中,直接就是优化掉了。这样在Debug版本有利于程序员排查问题,在Release版本不影响用户使用程序时的效率。

8.指针的使用和传址调用:

(1)strlen的模拟实现:库函数strlen的功能是求字符串长度,统计的是\0之前的字符个数,虽然上面有一个,但是下面这个是优化的,更有东西。strlen传的值是数组名,也就是地址,模拟实现一个strlen函数的话,也需要传地址,用char*指针接受。

就可以断言一下,如果是空指针就报错。然后我们只是求字符串长度,不希望把里面的内容改掉,就可以加上const给s加上限制。strlen是求字符串长度,那么长度不可能是负数,所以返回值最好设置为无符号类型的整数。这样子代码就会更加安全严谨。

#include <stdio.h>
#include <assert.h>
size_t my_strlen(const char* s)
{
	size_t count = 0;
	assert(s != NULL);
	while (*s != '\0')
	{
		count++;
		s++;
	}
	return count;
}
int main()
{
	char arr[] = "abcdef";
	size_t len = my_strlen(arr);//假如这里有人传了空指针
	printf("%zd\n", len);
	return 0;
}

 (2)传值调用和传址调用:

写一个函数,交换两个整型变量的值:

 结果没有发生改变,那么调试一下看一下:

可以看到调试起来之后交换前是没有问题的,所以问题可能在交换函数那里,f11走进去,

 可以看到x和y是有自己独立的空间的,并且完成交换了,但是函数调用完之后空间回收,a和b里面的值都不会受到影响,

所以不会交换。实参传给形参的时候,形参是实参的一份临时拷贝,形参是有自己独立的空间的,对形参的修改不会影响实参。

但是可以把实参的地址存给指针变量,两者建立起某种联系就可以对地址里面的值进行修改了。所以可以把a和b的地址传给形参,用指针变量来接收。

void Swap(int* x, int* y)
{
	int z = 0;
	z = *x;
	*x = *y;
	*y = z;
}
int main()
{
	int a = 10;
	int b = 20;
	printf("交换前:a=%d b=%d\n", a, b);
	Swap(&a, &b);
	printf("交换后:a=%d b=%d\n", a, b);

	return 0;
}

修改后的方式是传址调用,对形参的修改会影响实参。以后如果只需要使用主调函数的值来计算就可以使用传值调用,若函数内部要修改主调函数中变量的值就需要使用传址调用 。

指针第一节到此结束。

  • 41
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值