指针(1)

1.内存和地址

1.1   内存

生活中我们有了房间号才能够快速找到房间,同样,在计算机中CPU(中央处理器)在处理数据时,需要的数据是在内存中进行读取的,处理完之后又会放回内存中。

在内存空间中,也是将内存划分为一个个的内存单元,每个内存单元的大小为1个字节,1个字节相当于8个比特位,一个比特位可以存储一个二进制的0或1

补充

1byte=8bit

1KB=1024byte

1MB=1024KB

1GB=1024MB

1TB=1024GB

1PB=1024TB

生活中我们将门牌号叫做地址,在计算机中,我们把内存单元的编号叫做地址,c语言中我们把它叫作指针

2.指针变量和地址

2.1

c语言中创建变量其实就是向内存中申请空间

我们使用一个获取地址的操作符(&)得到一个变量的地址

 通过观察内存,&a取出的是a所占4个字节中较小的地址

2.2

2.2.1 指针变量

我们将取到的地址存放在指针变量

int main()
{
	int a=10;
	int*p=&a;//取出a的地址存放在指针变量p中
	return 0;
}

 指针变量也是一种变量,这个变量就是用来存放地址的,存放在其中的值都可以理解为地址

2.2.2 拆解指针类型

pa左面写的是int*,*说明pa是指针变量,int说明pa指向的是整型类型的对象

2.2.3 解引用操作符(*)

int main()
{
	int a=10;
	int*pa=&a;//取出a的地址存放在指针变量p中
	*pa= 0;
	return 0;
}

通过解引用,*pa就把a由10 改为0

2.4 指针变量的大小

32位机器就有32根地址总线,把这32根地址总线产生的二进制序列当作一个地址,一个地址就是32byte,就是4个字节

在64位的机器中,就变成了8个字节

所以以后一提到地址的大小就是4个字节(32位)或者8个字节(64位)

x86环境就是32位环境,根据运行结果来看,指针变量的大小是与类型无关的,只要指针类型的变量在相同的平台下,大小就都是相同的

3.指针变量类型的意义

3.1  指针的解引用

指针类型决定了对指针解引用的时候有多大的权限(一次可以操作几个字节)

例如:char*的指针解引用只能访问一个字节,而int*的指针解引用就可以访问4个字节

3.2   指针+-整数

可以看出,char*类型的指针变量+1跳过一个字节,int*类型的指针跳过4个字节

指针的类型决定了指针向前走或向后走一步有多大

3.3  void*指针

void*是一种无类型的指针,可以用来接受任意类型的指针。局限性是,不能进行+-和解引用的运算

int main()
{
	int a = 10;
	int* p = &a;
	char* pa = &a;
	return 0;
}

运行后编译器给出警告

但是当我们用void*类型就不会出现这种问题

从这里看出,void*可以接收不同类型的指针,但是无法直接进行指针的运算

4.const修饰指针 

4.1 const修饰变量

变量是可以被修改的,把变量的地址交给一个指针变量,通过这个指针变量也是可以修改变量的

我们希望加上一些限制,使其不能被修改,这就是const的作用

int main()
{
	int m = 0;
	m = 20;//m可以被修改
	const int n = 0;
	n = 20;
	return 0;//n不能被修改
}

 但是如果我们绕过你n,使用n的地址去修改n,就可以了,这样就是在打破语法规则

4.2 const修饰指针变量

#define  _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

void test1()
{
	int n = 10;
	int m = 20;
	int* p = &n;
	*p = 20;//Ok n=20,m=20
	p = &m;//N0,n=10,m=20
}

void test2()
{
	int n = 10;
	int m = 20;
	int*const p = &n;//const修饰的是指针变量本身,指针变量本身不可以修改,但是指针变量所指向的内容可以改变
	*p = 20;//OK
	//p = &m;//这里编译器会报错
}

void test3()
{

	int n = 10;
	int m = 20;
	int const*  p = &n;//const修饰的是指针指向的内容,保证指针指向的内容不被改变,但是指针本身可以被改变
	//*p = 20;//报错
	p = &m;//OK
}

void test4()
{
	int n = 10;
	int m = 20;
	int const* const p = &n;//const既修饰了指针变量本身,又修饰了指针所指向的内容,二者都不可以被改变
	//*p = 20;//报错
	//p=&m;//报错
}
int main()
{
	test1();
	test2();
	test3();
	test4();
	return 0;
}

 得出结论

1.const放在*的左边(int const *p):修饰的是指针所指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本身可以改变

2.const放在*的右边(int *const p):修饰的是指针变量本身,保证了指针变量的内容不能改变,但是指针所指向的内容,可以通过指针改变

5.指针运算

指针的运算一般有三种..
1.指针+-整数

2.指针-指针

3.指针的关系运算

5.1指针+-整数=指针

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

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *(p+i));
	}
	return 0;
}

 这里的p+i就是指针+-整数

5.2 指针-指针=整数(两个指针之间的元素个数)

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

//运行结果为3

指针-指针的运算的前提条件是:两个指针必须指向同一块空间

注意!!!指针+指针是无意义的

5.3 指针的关系运算

指针的大小比较

int main()
{
	int arr[10] = { 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;
}

6.野指针 

6.1 野指针的成因

1,指针未初始化

int main()
{
	int* p;//局部变量未初始化,默认为随机值
	*p = 20;
	return 0;
}

2,指针越界访问

int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	for (int i = 0; i < 11; i++)
	{
		*(p++) = i;//指针指向的范围超出数组arr的范围,p就是野指针
	}
	return 0;
}

 3,指针指向的空间释放

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

在test()中,我们想让它返回一个指向局部变量n的指针。但是,存在一个问题,当test函数执行完毕是,n这个局部变量所占用的空间就会被释放,指向它的指针就会变为悬空指针。

在main()函数中,当我们想要打印*p是,实际上访问的是一个已经被释放的内存地址

6.2 如何避免野指针

6.2.1    如果明确知道指针指向哪里就直接赋地址,如果不知道,就给指针赋值NULL。

NULL是c语言中定义的一个标识符常量,值为0,0也是地址,但这个地址无法使用

初始化如下

int main()
{
	int num = 10;
	int* p1 = &num;
	int* p2 = NULL;
	return 0;
}

6.2.2   小心指针越界

一个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围,超出了就是越界访问

6.2.3   指针变量不再使用时,即使设置为NULL,使用之前检查有效性

当指针变量指向一块区域时,我们可以通过指针访问该区域,后期不使用就即使设置为NULL,,只要是NULL指针就不去访问,在使用之前我们就要判断是不是NULL,如果是就不去访问

6.2.4   避免返回局部变量的地址

7.assert断言

assert.h头文件定义了assert(),用于在运行时确保程序符合指定的条件,如果不符合,就报错终止运行

assert(p!=NULL);

assert()接受一个表达式作为参数,如果表达式为真,程序继续运行,否则就会报错,并会在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及这个表达式的文件名和行号

如果确认程序没有任何问题的话,无需更改代码,在头文件前定义一个宏NDEBUG,编译器就会禁用所以assert()

它的缺点就是引入额外的检查,增加了程序的运行时间

我们可以在debug中使用,在release直接禁用就可

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

 8.1  strlen的模拟实现

strlen()的功能时求字符串的长度,统计“\0”之前的字符个数

函数原型为

size_t strlen(const char *P);

 参考代码

int my_strlen(const char* str)
{
	int count = 0;
	assert(str);
	while (*str)
	{
		count++;
		str++;
	}
	return count;
}
int main()
{
	int len = my_strlen("abcdef");
	printf("%d", len);
	return 0;
}

8.2  传值调用和传址调用 

 写一个函数,交换两个整型变量的值,我们可能会写出以下代码

运行结果为

void Swap(int x, int y)
{
	int temp = x;
	x= y;
	y= temp;
}
int main()
{
	int a = 10, b = 20;
	Swap(a, b);
	printf("%d %d", a, b);
	return 0;
}

我们发现a和b并没有交换 

我们在main函数内部,创建了a和b,在调用Swap函数时,将a和b传给了Swap函数,x和y接受了a和b的值,但是x和a的地址不同,是两块独立的空间,只在Swap函数内部交换x和y的值,自然不会影响a和b的值,所以a和b没有交换,这就是传值调用 

结论:实参传递给形参时,形参会单独开辟一块空间接收,对形参的修改不会影响实参

 我们要达到的目的时在Swap函数中操作的就是main函数中的a和b,所以我们把a和b的地址传给Swap函数就好了

void Swap(int *px, int*py)
{
	int temp = *px;
	*px= *py;
	*py= temp;
}
int main()
{
	int a = 10, b = 20;
	Swap(&a, &b);
	printf("%d %d", a, b);
	return 0;
}

 

这种方式叫做传址调用 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值