指针、数组的理解与关系
一、指针的本质:变量,指针变量就是指针变量
int *p:两个变量,一个p(指针变量本身)是int *类型的
另一个是*p(指针指向的那个变量)是int类型的
注:指针说白了就是指针类型,前面定义的int类型是为了说明指针指向的那个数的类型,所以指针的解析方式都是按地址来解析的
(不管你是char *还是double *,解析方式都是地址)而指向的那个数的类型就要看你怎么定义的了
例如:
int *a
a是按照地址来解析的;*a则是按照int类型来解析的。
(1)所有的类型的数据存储在内存中,都是按照二进制格式存储的。所以内存中只知道有0和1,不知道是int的、还是float的还是其他类型
可查询这些类型的存储方式(数转换成二进制往内存中放的方式)
int、char、short等的存储类型相同只是内存格子大小不同(所以这几种整形就彼此叫二进制兼容格式);
float和double的存储方式彼此不同,和整形更不同。
例如:
1 int a = 5;
2
3 printf("a = %d.\n", a); // 5
4 printf("a = %f.\n", a); //乱码
5 总结:怎么定义就要怎么取,int a = 5;则是分配4个单元格来装0b1010,然后存入单元格里
例2:
int a = 20;
char *p1 = &a;
printf("*p1 = %d.\n", *p1);
注:char的范围是-127~127,在这范围内取出来则不会出错,如果超出来则会出错
所以用int取char不会出错,但是char取int就有可能出错也有可能不出错
二、野指针(没有定义具体地址的指针,它可以随便指向任意的地址)
1.当一个指针为野指针时,不可乱去解引用,会产生很大的问题
2.野指针因为指向地址是不可预知的,所以有3种情况:
第一种是指向不可访问(操作系统不允许访问的敏感地址,譬如内核空间)的地址,结果是触发段错误,这种算是最好的情况了;
第二种是指向一个可用的、而且没什么特别意义的空间(譬如曾经使用过但是已经不用的栈空间或堆空间),这时候程序运行不会出错,也不会对当前程序造成损害,这种情况下会掩盖你的程序错误,让你以为程序没问题,其实是有问题的;
第三种情况就是指向了一个可用的空间,而且这个空间其实在程序中正在被使用(譬如说是程序的一个变量x),那么野指针的解引用就会刚好修改这个变量x的值,导致这个变量莫名其妙的被改变,程序出现离奇的错误。一般最终都会导致程序崩溃,或者数据被损害。这种危害是最大的。
3.指针变量如果是局部变量,则分配在栈上,本身遵从栈的规律(反复使用,使用完不擦除,所以是脏的,本次在栈上分配到的变量的默认值是上次这个栈空间被使用时余留下来的值),野指针也是有一定规律的,但没意义
4.避免野指针方法:在指针的解引用之前,一定确保指针指向一个绝对可用的空间。
常规的做法是:
int *p = NULL; //第一步:定义指针时,同时初始化为NULL
//一堆代码 //第二步:这期间使用指针时,给它一个有意义的地址
//int a = 3;
//p = &a;
if ( NULL != P) //第三步:在指针解引用之前,先去判断这个指针是不是NULL
{
*P = 5;
}
P = NULL; //第四步:指针使用完之后,将其赋值为NULL
注:
一般在判断指针是否野指针时,都写成
if (NULL != p) 而不是写成 if (p != NULL)
原因是:如果NULL写在后面,当中间是==号的时候,有时候容易忘记写成了=,这时候其实程序已经错误,但是编译器不会报错。
这个错误(对新手)很难检查出来;如果习惯了把NULL写在前面,当错误的把==写成了=时,编译器会报错。
5.什么是NULL
(1)NULL在C/C++中定义为:
1 #ifdef _cplusplus // 定义这个符号就表示当前是C++环境
2 #define NULL 0 // 在C++中NULL就是0
3 #else
4 #define NULL (void *)0 // 在C中NULL是强制类型转换为void *的0
5 #endif
(2)在C语言中,int *p;你可以p = (int *)0;但是不可以p = 0;因为类型不相同。
(3)所以NULL的实质其实就是0,然后我们给指针赋初值为NULL,其实就是让指针指向0地址处。第一,0地址处作为一个特殊地址(我们认为指针指向这里就表示指针没有被初始化,就表示是野指针);第二,0地址在一般的操作系统中都是不可被访问的,如果C语言程序员不按规矩(不检查是否等于NULL就去解引用)写代码直接去解引用就会触发段错误
三、数组
几个关键符号(a a[0] &a &a[0])的理解(前提是 int a[10])
1.a就是数组名。a做左值时表示整个数组的所有空间(10×4=40字节),又因为C语言规定数组操作时要独立单个操作,不能整体操作数组,所以a不能做左值;a做右值表示数组首元素(数组的第0个元素,也就是a[0])的首地址(首地址就是起始地址,就是4个字节中最开始第一个字节的地址)。a做右值等同于&a[0];
2.a[0]表示数组的首元素,也就是数组的第0个元素。做左值时表示数组第0个元素对应的内存空间(连续4字节);做右值时表示数组第0个元素的值(也就是数组第0个元素对应的内存空间中存储的那个数)
3.&a就是数组名a取地址,字面意思来看就应该是数组的地址。&a不能做左值(&a实质是一个常量,不是变量因此不能赋值,所以自然不能做左值。);&a做右值时表示整个数组的首地址。
4.&a[0]字面意思就是数组第0个元素的首地址。做左值时表示数组首元素对应的内存空间,做右值时表示数组首元素的地址,做右值时&a[0]等同于a。
总结:
1:&a和a做右值时的区别:&a是整个数组的首地址,而a是数组首元素的首地址。这两个在数字上是相等的,但是意义不相同。意义不相同会导致他们在参与运算的时候有不同的表现。
2:a和&a[0]做右值时都是表示首元素的地址,完全可以互相替代。
3:&a是常量,不能做左值。
4:a做左值代表整个数组所有空间,所以a不能做左值。
1、函数与指针
1.1.当变量作为形式参数时传递进去的是值(传值调用)
eg.
1 void swap1(int a, int b)
2 {
3 int tmp;
4 tmp = a;
5 a = b;
6 b = tmp;
7 printf("in swap1, a = %d, b = %d.\n", a, b);
8 }
9
10 void swap2(int *a, int *b)
11 {
12 int tmp;
13 tmp = *a;
14 *a = *b;
15 *b = tmp;
16 printf("in swap1, *a = %d, *b = %d.\n", *a, *b);
17 }
18
19 int main(void)
20 {
21 int x = 3, y = 5;
22 swap1(x, y);
23 printf("x = %d, y = %d.\n", x, y); // x=3,y=5,交换失败
24
25 int x2 = 3, y2 = 5;
26 swap2(&x2, &y2);
27 printf("x = %d, y = %d.\n", x2, y2); //x=5,y=3 交换成功
28
29 return 0;
30 }
31 注:第一个是传值调用,第二个是传址调用
32 实质:都是传值调用,第一个传的是变量的值,第二个传的是指针变量的值
2、数组作为函数形参
2.1.组名作为形参,实际传递是不是整个数组,而是数组的首元素的首地址(首元素的指针)
2.2.在子函数内传参得到的数组首元素首地址,和外面得到的数组首元素首地址的值是相同的。类似“传址调用”
3、指针作为函数形参
和数组作为函数形参是一样的
例子:
1 void func3(int *a)
2 {
3 printf("sizeof(a) = %d.\n", sizeof(a));
4 printf("in func3, a = %p.\n", a);
5 }
6
7 void func2(int a[])
8 {
9 printf("sizeof(a) = %d.\n", sizeof(a));
10 printf("in func2, a = %p.\n", a);
11 }
12
13 int main(void)
14 {
15 int a[5];
16 printf("a = %p.\n", a);
17 func2(a); /*
18 a = 0x7ffff2bdc8b0.
19 sizeof(a) = 8.(我装的是64位的系统)
20 in func2, a = 0x7ffff2bdc8b0.
21 */
22
23 int a3[5];
24 printf("a = %p.\n", a3);
25 func3(a3); //同上
26 }
27 注:数组名直接打印和数组名作为形参,指针作为形参都是一样的结果
难点:
void fun(int b[100])
{
sizeof(b);
}
注:
(1)函数传参,形参是可以用数组的
(2)函数形参是数组时,实际传递不是整个数组,而是数组的首元素首地址。也就是说函数传 参用数组来传,实际相当于传递的是指针(指针指向数组的首元素首地址)。
// func完全等同于func1
void func(int a[])
{
printf("数组大小 = %d.\n", sizeof(a));
}
void func1(int *a)
{
printf("数组大小 = %d.\n", sizeof(a));
}
4、结构体作为形参(后续.....)
五、const关键字与指针
1、const修饰指针的4种形式
(1)const关键字,在C语言中用来修饰变量,表示这个变量是常量。(一个const关键字只能修饰一个变量)
(2)const修饰指针有4种形式:
const int *p1; // p本身不是cosnt的,而p指向的变量是const的(*p1不可改变)
int const *p2; // p本身不是cosnt的,而p指向的变量是const的(*p2不可改变)
int * const p3; // p本身是cosnt的,p指向的变量不是const的 (p3不可改变)
const int * const p4; // p本身是cosnt的,p指向的变量也是const的 (p4和*p4都不可改变)
若真的需要修改const上的值,则需要用强制类型转换进行修改,骗过编译器,和普通全局变量一样将代码存入data段,
这样程序运行时,发现数据前没有const这个关键字,就直接运行了(某些单片机无法修改)
2、函数传参中使用const指针
例子:
1 char *p1;
2 const char *p2;
3 char *pstr = "hello";
4 //char pstr[]= "hello";
5 p1 = pstr;
6 p2 = pstr;
7 *p1 = a;
8 *p2 = a;
9 pritf("pstr = %c.\n",*p1); //若采用char *pstr 的话编译器不会报错误,但在执行时会出现段错误
10 pritf("pstr = %c.\n",*p2); //若采用char *pstr 的话编译器就会报错误,*p2为const类型,只读
11
12 注:出现段错误的原因是,字符串存储在代码段,代码段是个不可更改的区域,所有执行时会出现错误
13 若采用char pstr[]数组的话*p1即可更改,数组好像存放在数据区(不大确定)
补充:
#define dpChar char *
typedef char *tpChar; //typedef用来重命名类型,或者说用来制造用户自定义类型
dpChar p1, p2; // 展开:char *p1, p2; 相当于char *p1, char p2;
tpChar p3, p4; // 等价于:char *p3, char *p4;
(参考朱有鹏老师)