深入理解指针(1) 学习笔记
1.1内存与指针的浅显理解
1.内存划分为一个个的内存单位(1个字节),一个字节可以放八个比特位
2.内存单元的编号==地址==指针
1.2如何理解编址
1.地址总线
2.数据总线
3.控制总线
每根线只有两态,地址信息被下达给内存,就可以找到该地址对应的数据,将数据再通过数据总线传入cpu内寄存器;
2.1和2.2
变量创建的本质就是在内存里面申请空间;
eg1:int a = 20;//向内存申请四个字节的空间,用来存放int类型的数值;这四个字节,每个字节都有地址;变量的名字仅仅是给程序员看的,编译器不看名字,编译器是通过地址找内存单元的;&a就能拿到变量a的地址;
int * pa = &a;pa是一个变量,这个变量是用来存放地址(指针)的,所以pa叫指针变量(名字),int *是pa的类型;
//*表示pa是指针变量
//int 表示pa指向的变量a的类型是int
*pa// *--解引用操作符(间接访问操作符)
2.3指针变量的大小
前面的内容我们了解到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或0,那我们把32根地址线产生的2进制序列当作一个地址,那么一个地址就是32个bit位,需要4个字节才能储存。
如果是64位机器,一个地址就是64个二进制位组成的二进制序列,储存起来就需要8个字节的空间,指针变量的大小就是8个字节。
int a = 10; int* p = &a; //1.指针变量是用来存放地址的,地址的存放需要多大空间,那么指针变量的大小就是多大 printf("%zd\n",sizeof(p)); char ch = 'w'; char * pc = &ch; printf("%zd\n",sizeof(pc)); //指针变量大小跟类型无关,x86就是4,x64就是8,与类型无关
3.指针类型的意义
3.1指针的解引用
//代码1 #include<stdio.h> int main() { int n = 0x11223344; int *pi = &n; *pi = 0; return 0; }
//代码2 #include<stdio.h> int main() { int n = 0x11223344; char *pc = (char *)&n; *pc = 0; return 0; }
上述代码我们调试之后我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第一个字节改为0。
结论:指针的类型决定了,对指针解引用的时候有多大的权限(一次能操作几个字符)。比如:char的指针解引用就只能访问一个字节,而int的指针的解引用就能访问四个字节。
3.2指针+/-整数
int * pa ; pa + 1 -----> +1*sizeof(int)
pa + n------> +n*sizeof(int)
char * pc; pc + 1 -------> 1*sizeof(char)
pc + n -------> n*sizeof(char)
指针的类型决定了指针向前或者向后走一步大概有多大(距离)
3.3 void*指针
无具体类型的指针--void* (泛型指针)
可以用来接受任意类型地址,但是也有局限性;void*类型的指针不能直接进行指针的+-整数和解引用的运算
int a = 10; char ch = 'w'; void* pv1 = &a;//int* void* pv2 = &ch;//char*
4.const修饰指针
4.1const修饰普通变量
变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量也可以改变这个变量。但是如果我们希望一个变量加上一些限制,让它不能被修改--------const的作用
//代码1 int num = 100; num = 200; const int n = 10;//不可以被修改,具有常属性;const修饰变量的时候叫:常变量(本质上还是变量) n = 20;//编译的时候会在这里报错 //代码2(const锁了门,但还可以翻窗户间接改) const int n = 10; int* p = &n; *p = 200; printf("%d\n",n);//打印200
4.2const修饰指针变量
一般来讲const修饰指针变量,可以放在左边,也可以放在右边,意义是不一样的
int main() { int n = 10; int m = 100; //const放在*左边限制的是*p,但没有限制p;限制的是指针指向的内容,不能通过指针变量来修改它指向的内容 //但是指针变量本身是可以改变的 int const*p = &n;//const放在*左边的第一种情况 const int* p = &n;//const放在*左边的第二种情况 *p = 20;//err,const限制了*p p = &m;//ok,不会报错 return 0; }
int main() { int n = 10; int m = 100; //const放在*右边限制的是p,但没有限制*p //限制的是指针变量本身,指针不能改变它的指向,但是可以通过指针变量修改它所指向的内容 int * const p = &n;//const放在*右边 *p = 20;//ok,不会报错err p = &m;//err,const限制了p return 0; }
关于指针p有3个相关的值
1.p,p里面放着一个地址
2.*p,p指向的那个对象
3.&p,表示的是p变量的地址
结论:
1.const放在*左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。这种声明中,const
修饰的是指针指向的数据。这意味着指针p指向的数据是常量,不能通过p来修改。但是,指针p本身可以被修改,即你可以让p指向另一个地址。(房间里的人不能变,但门牌号可以变)。
2.const放在*的右边,修饰的是指针变量本身,保证了指针比那辆的内容不能修改,但是指针指向的内容,可以通过指针改变;这种声明中,const
修饰的是指针变量本身。这意味着指针p
的值(即它所指向的地址)是常量,不能被修改。但是,指针指向的数据可以被修改。(房间的门牌号不能变,但房间里面的人可以变)。
5.指针运算
1.指针 +- 整数;
2.指针 - 指针;
3.指针的关系运算;
5.1指针 +- 整数
1.指针类型决定了指针+1,的
步长;还决定了指针解引用的权限
2.数组在内存中连续存放的
5.2指针 - 指针
指针-指针的绝对值是指针和指针之间元素的个数(计算的前提条件是两个指针指向的是用一个空间)
int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; printf("%d\n",&arr[9] - &arr[0]);//打印9 return 0; }
//写一个函数求字符串的长度
strlen统计的是字符串中\0之前字符的个数
//写一个my_strlen函数 //第一种写法 size_t my_strlen(char* p)//size_t是一个无符号整型 { int count = 0; while(*p != '\0') { count++;//计数器 p++ } return count; } //第二种写法 size_t my_strlen(char* p)//size_t是一个无符号整型 { char* start = p; char* end = p; while(*end != '\0') { end++; } return end - start; } int main() { char arr[] = "abcdef"; size_t len = my_strlen(arr);//数组名其实是数组首元素的地址 arr == &arr[0] printf("%zd\n",len); return 0; }
5.3指针的关系运算
#include<stdio.h> int main() { int arr[10] = {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.野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
6.1野指针成因
1.指针未初始化
#include<stdio.h> int main() { int* p;//p是局部变量,但是没有初始化,其值是随机的; //如果将p中存放的值当作地址,解引用操作符就会形成非法访问 *p = 10;//p就是野指针 return 0; }
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; } int* p; *p = 10; return 0; }
6.2如何避免野指针
6.2.1指针初始化
如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针辅助NULL
NULL是c语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错
#includem<stdio.h> { int a = 10; int *p = &a; int *p2 = NULL; *p2 = 200;//err }
6.2.2小心指针越界
不能超出范围访问
6.2.3指针变量不再使用,及时置NULL,指针使用之前检查有效性
约定俗成的一个规则:只要是NULL指针就不去访问,同时使用指针之前可以判断指针是否为NULL
if(*p != NULL) { *p = 200; }
6.2.4避免返回局部变量的地址
7.assert断言
assert.h 头文件定义了宏assert(),用于在运行时确保程序符合制定条件,如果不符合,就报错终止运行。这个宏常常被称为“断言”
assert(p != NULL);
上面代码在程序运行到这一行语句时,验证变量p是否等于 NULL、如果确实不等于 NULL,程序继续运行,否则就会终止运行,并且给出报错信息提示, assert()宏接受一个表达式作为参数。如果该表达式为真(返回值非零),assert()不会产生任何作用,程序继续运行。如果该表达式为假(返回值为零),assent()就会报错,在标准错误流 stderr 中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。 assert()的使用对程序员是非常友好的,使用 assert()有几个好处:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭 assent()的机制。如果已经确认程序没有问题,不需要再做断言,就在#include <assert.h>语句的前面,定义一个宏 NDEBUG。
#define NDEBUG #include<assert.h>
assert的缺点是因为引入了额外的检查,增加了程序的运行时间
一般可以在Debug中使用,再Release中选择禁用assert就行
8.指针的使用和传址调用
8.1 strlen的模拟实现
//求字符串长度 //参数p指向的字符串不期望被修改,在前面加const size_t my_strlen(const char* p)//size_t是一个无符号整型 { size_t count = 0; assert(p != NULL);//检测指针p是否有效//使代码健壮性和鲁棒性(更稳定) while(*p) { count++;//计数器 p++ } return count; } int main() { char arr[] = "abcdef"; size_t len = my_strlen(arr);//数组名其实是数组首元素的地址 arr == &arr[0] printf("%zd\n",len); return 0; }
8.2 传值调用和传址调用
写一个函数,交换两个整型变量的值
#include<stdio.h> void Swap1(int x,int y) { int z = 0; z = x; x = y; y = z; } 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; } //发现交换失败 //当实参传递给形参的时候,形参是实参的一份临时拷贝,对形参的修改不会影响实参
#include<stdio.h> void Swap2(int *pa,int *pb) { int z = 0; z = *pa; *pa = *pb; *pb = z; } int main() { int a = 0; int b = 0; scanf("%d %d",&a,&b); printf("交换前:a=%d b=%d\n",a,b); Swap2(&a,&b); printf("交换后:a=%d b=%d\n",a,b); return 0; }
Swap1函数在使用的时候,是把变量本身直接传递给了函数,这种调用函数的方式我们之前在函数的时候就知道了,叫做传值调用
结论:实参传递给形参的时候,形参会单独创建一份临时空间来接受实参,对形参的修改不影响实参。
所以Swap1是失败的。
Swap2函数将变量的地址传递给了函数:这种函数调用方式叫传址调用。
传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用。如果函数内部要修改主调函数中的变量的值,就需要传址调用。