c语言指针
1. 内存和地址
1.1内存
- 首先讨论一下内存,电脑上的内存有 8G/16G/32G等,那么这么大的一个内存空间如何有效管理呢?
- 其实就是把内存划分一个个内存单元(每个内存单元大小1个字节),一个字节(Byte)是8个比特(bit)
- 一个比特(bit)可以放一个二进制(0/1),一个字节(Byte)就好比一个八人房,而每个八人房都有一个门牌号,这个门牌号在c语言中叫地址/内存单元编号/指针。
- 有了内存单元编号,就可以快速有效的找到内存单元进行管理。
- 其实就是把内存划分一个个内存单元(每个内存单元大小1个字节),一个字节(Byte)是8个比特(bit)
1.2编址(了解即可)
-
首先,必须理解,计算机内是有很多的硬件单元,而硬件单元之间需要相互协同工作,所谓协同,至少相互之间可以进行数据传递,但硬件之间是相互独立的,需要”线“将彼此连接起来,硬件中CPU和内存也是有大量数据交互的,也必须用”线“连接起来,不过我们目前只需要关心一组”线“(地址总线)。
- CPU访问内存中某个字节空间,必须知道这个字节空间在内存什么位置,而内存中字节很多,所需要给内存进行编址,编址完后就有了指针
- 我们如今计算机有32位,64位,32位计算机就是有32根地址总线,64位就是64根,每根线只能发出两种电脉冲,一高一低的脉冲,这个高低用1/0表示,32根就是有2^32种组合,每个组合都代表一个地址/指针。
2.指针变量和地址
2.1指针变量演示与解释
#include<stdio.h>
int main()
{
int a = 10;
int *ptr = &a;//这里的int意思是ptr指向的对象a是int类型的,*是说明p是指针变量,int *是ptr的类型
return 0;
}
注意:指针变量就是用来存放地址的,存放在指针变量里的值,都会被当成地址使用。
#include<stdio.h>
int main()
{
int a = 10;
int *p =&a;
*p = 0;//单独拿出 *和变量 这里的*就单纯代表解引用操作符/间接访问操作符,*p就是a
printf("%d",a);//最后打印结果为0
return 0;
}
2.2指针变量的大小
#include<stdio.h>
int main()
{
int a = 10;
int *p=&a;
char ch='w';
char *ptr=&ch;
printf("%zd",sizeof(p));//在x64环境下大小为8个字节,x86环境下大小为4个字节
printf("%zd",sizeof(ptr));//在x64环境下大小为8个字节,x86环境下大小为4个字节
return 0;
}
-
指针变量不管是char型、int型、或者double型等等,其大小都一样,x64 环境下都为8字节,x86 环境下都为4字节。
-
打印sizeof( )时,最好用%zd,%d不建议使用某些编译器会报警告。
-
sizeof( )的( )里可以放变量对应的类型也可放变量本身效果一样。
-
#include<stdio.h> int main() { int a = 10; int *p=&a; printf("%zd",sizeof(p));//x64环境下输出结果为8,x86环境下输出结果为4 printf("%zd",sizeof(int*));//x64环境下输出结果为8,x86环境下输出结果为4 return 0; }
-
3.指针变量类型的意义
3.1 引言
由于指针变量大小都一样,有些人会疑惑指针变量没有意义啊,不管创建什么类型的指针变量大小都一样,存的又都是地址,那么接下来我简略来解释一下指针变量类型的意义。
3.2指针的解引用代码演示与解释
#include<stdio.h>
int main()
{
int n = 0x11223344;
int *p=&n;
*p = 0;
printf("%d",*p);//输出结果为0
return 0;
}
如果把上段代码的int *改成char *如下所示:
#include<stdio.h>
int main()
{
int n = 0x11223344;
char *p=&n;
*p = 0;
printf("%d",*p);//输出结果不为0,输出的结果并不是你预期的0
return 0;
}
3.3指针加减整数
#include<stdio.h>
int main()
{
int n = 0x11223344;
int *p=&n;
char *pc=&n;
printf("p = %p\n",p);
printf("p+1 = %p\n",p+1);
printf("pc = %p\n",pc);
printf("pc+1 = %p\n",pc+1);
return 0;
}
结果如下:
会发现char*
的指针大小加一后相差一个字节,int*
的指针大小加一后相差四个字节。
- 指针类型是有意义的
- 指针类型决定了,指针进行+1/-1操作的时候,一次跳过几个字节
3.4指针类型这些特点的使用
-
数组:1 2 3 4 5 6 7 8 9 10遍历一下数组
-
没学指针前会这么遍历
-
#include<stdio.h> int main() { int arr[]={1,2,3,4,5,6,7,8,9,10}; int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); for(i=0;i<sz;i++) { printf("%d ",arr[i]); } return 0; }
-
-
学指针后可以这么遍历
-
#include<stdio.h> int main() { int arr[]={1,2,3,4,5,6,7,8,9,10}; int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); int *p = &arr[0]; for(i=0;i<sz;i++) { printf("%d ",*(p+i)); } return 0; }
-
-
4. const修饰指针
4.1代码演示与解释
#include<stdio.h>
int main()
{
//const 修饰变量,使得被修饰的变量不能被修改
const int n=100;
n=20; //error
printf("%d/n",n);//无法打印,显示第六行出错
return 0;
}
但是换成指针进行修改却可以成功,如下所示:
#include<stdio.h>
int main()
{
const int n =100;
int *p=&n;
*p=20;
printf("%d\n",n);//打印结果为20
return 0;
}
-
这是为什么呢?
- 举个例子:一个人要进一个房间,结果这个房间把门被封死了,但这个人却从窗户爬进来了。
-
那么如何防范这种”爬窗户“行为呢,如下所示:
-
//const修饰指针 #include<stdio.h> int main() { const int n =100; const int *const p=&n;//const放在*左边,修饰的是指针指向的内容,const放在*右边,修饰的是指针变量的内容(int *const==const int *) //通过第5、6行所示,就把所有洞都封死了,就绝对安全了 *p=20; printf("%d\n",n);//打印结果为20 return 0; }
-
5.指针运算(三种基本运算)
5.1 指针 ± 整数
-
代码演示及详解
-
//演示1 //我们可以根据数组首地址得到各个元素的地址 #include<stdio.h> int main() { int arr[]={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==&arr[i] } return 0; }
-
//演示2 #include<stdio.h> int main() { char arr[] ="abcdef";//arr数组内容有a b c d e f \0 char *p=&arr[0]; while(*p!='\0') { printf("%c ",*p); p++; } return 0; }
-
5.2 指针 - 指针
-
代码演示及及解释
-
#include<stdio.h> int main() { int arr[10]={0}; int ret =&arr[9]-&arr[0]; printf("%d",ret);//输出是9,指针-指针表示指针之间元素个数 }
-
-
利用指针-指针,粗略地模拟实现 strlen ( )函数
-
#include<stdio.h> int my_strlen(char *p) { char *start=p; while(*p!='\0') { p++; } return p-start; } int main() { char arr[]="abcdef"; int len=my_strlen(arr); printf("%d\n",len); return 0; }
-
5.3 指针的关系运算
-
代码演示
-
#include<stdio.h> int main() { int arr[]={1,2,3,4,5,6,7,8,9,10}; int sz=sizeof(arr)/sizeof(arr[0]); int *p=arr;//数组名也是首元素地址 while(p<arr+sz)//括号里就是指针的关系运算 { printf("%d",*p); p++; } return 0; }
-
6.野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的),个人理解就是指针指向的内容一定是非法的。
6.1野指针可能造成的原因
-
#include<stdio.h> int main() { int *p;//未初始化造成的野指针 p=20; return 0; }
-
#include<stdio.h> int main() { int i=0; int arr[10]={0}; int *p=arr; for(i=0;i<=11;i++)//越界访问造成的野指针,p超出数组范围就是野指针了 { *p=i; p++; } return 0; }
-
#include<stdio.h> int *test() { int n=100; return &n; } int main() { int *p=test();//由于函数栈帧的销毁导致空间释放造成的野指针 return 0; }
6.2如何避免野指针
6.2.1指针初始化
-
如果你明确知道指针指向哪里就直接赋值地址,如果不知道指针指向哪里,可以给指针赋值NULL,
NULL
是c语言中定义的一个标识符常量,值是0,0也是地址,只不过0这个地址无法使用的,读写不了0这个地址(就是无法解引用0地址)-
#include<stdio.h> int main() { int *p=NULL;//把NULL换成0也是可以的,但是不建议,因为所表达的意思就会有些偏差了 return 0; }
-
6.2.2小心指针的越界
一个程序向内存申请了一些空间,通过指针也就只能访问了这些空间,不能超过范围访问,超出了就是越界访问。(通俗的将就是你只能在你申请的空间里使用指针)
6.2.3指针变量不在使用时,及时置空(NULL),指针使用前一定检查其有效性
-
演示
-
//演示1 #include<stdio.h> int main() { int a = 10; int *p = &a; int *ptr=NULL; if(p != NULL)//使用前检查 { //使用p } if(ptr != NULL)//使用前检查 { *ptr =100;//使用ptr } return 0; }
-
//演示2 #include<stdio.h> int main() { int arr[10]={1,2,3,4,5,6,7,8,9,10}; int *p=arr; int i=0; int sz=sizeof(arr)/sizeof(arr[0]); if(p != NULL)//检查一下 { for(i=0;i<sz;i++) { printf("%d",*p); p++; } } p =NULL;//使用完后及时置空 return 0; }
-
6.2.4避免返回局部变量(就是函数栈帧都销毁了你却还用它里面的值)
此问题也叫返回栈空间地址问题
7. assert 断言
#include<stdio.h>
int main()
{
int a = 10;
int *p=&a;
if(p!=NULL)
{
//...
}
//如果每回指针都这么判断的话,难免有些冗杂,这时就出现了assert断言
return 0;
}
assert.h
头文件定义了宏 assert()
,用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏常常被称为“断言”
assert(p!=NULL)//assert(p)这样写也行
-
#include<stdio.h> #include<assert.h> int main() { int a = 10; int *p=NULL; assert(p!=NULL);//此时程序会判断其指针的有效性,p为NULL报出错误并显示错误在第几行。 return 0; }
-
assert
比if(p!=NULL)
强一些,因为assert
会报出错误具体在第几行,而if(p!=NULL)
不会。
assert
宏接受一个表达式作为参数。如果表达式为真(返回值为零),assert
就会报错,在标准错误流 stderr
中写入一条信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。
assert()
的使用对程序员非常友好的,使用 assert
有几个好处:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭 assert()
的机制。如果已经确认程序没有问题,不需要再做断言,就在 #include<assert.h>
语句面前,定义一个宏 NDEBUG
。
-
#define NDEBUG #include<assert.h>
-
#define NDEBUG #include<assert.h> #include<stdio.h> int main() { int a=10; int *p=&a; assert(p!=NULL);//这样就不会报出警告了 return 0; }
8.指针的使用和传址调用
8.1传址调用
学习指针的目的是为了使用指针解决问题,那什么问题,非指针不可呢?
例如:写一个函数,交换两个整形变量的值
#include<stdio.h>
Swap(int*m,int*n)
{
int c = 0;
c = *m;
*m = *n;
*n = c;
}
int main()
{
int a=10;
int b=20;
printf("交换前a=%d,b=%d",a,b);
Swap(&a,&b);
printf("交换后a=%d,b=%d",a,b);
return 0;
}
注意:传址调用并非传值调用
8.2 strlen的模拟(健壮版)
#include<stdio.h>
#include<assert.h>
unsigned int My_strlen(const char *p)
{
unsigned int count=0;
assert(p!=NULL);
while(*p!='\0')
{
count++;
p++;
}
return count;
}
int main()
{
char arr[]="hello world";
unsigned int len=My_strlen(arr);
printf("%zd",len);
return 0;
}
注意while后面不要写分号!!!