一、数组的概念
1、什么是数组?数组的出现是为了解决什么问题?
数组其实就是一个集合,这个集合每一个元素都是由相同类型的变量组成的。
数组的出现就是为了解决在程序中定义多个相同类型的变量写代码时的麻烦。
数组中的数据类型的变量
数据类型 --> 可以是基本数据类型,也可以是非基本数据类型。
2、在程序中如何定义数组?
定义数组,必须要交代好两件事情。
1)数组中每一个元素数据类型是什么?
2)数组中元素的个数一共有多少个?
定义数组公式:
每一个元素数据类型 数组名[元素的个数]
分析:
每一个元素数据类型 --> 可以是基本数据类型:char/short/int/long/float/double。
可以是非基本数据类型:数组/指针/结构体
数组名:与普通变量定义规则一致。
[元素的个数]:看用户的需求来决定,你喜欢定义多少个就定义多少个。
例如: 定义一个可以存储5个int类型的数组
int A[5]; //在内存中连续申请20个字节,然后使用变量A间接访问这片空间。3、从内存角度分析数组占用空间的大小。
#include <stdio.h>
int main(int argc,char *argv[])
{
int a;
printf("sizeof(a):%ld\n",sizeof(a));
int A[5];
printf("sizeof(A):%ld\n",sizeof(A));
return 0;
}
结论:数组在内存中占用的空间是连续的。
二、数组的赋值。
1、定义数组同时初始化。
公式:
每一个元素的数据类型 数组名[元素的个数] = {初始化列表,赋值的每一个成员需要使用","分开};
int A[5] = {1,2,3,4,5}; --> 编译通过。
int A[5] = {1,2}; --> 编译通过,没有赋值的成员都会赋值为0。
int A[5] = {}; --> 编译通过,没有赋值的成员都会赋值为0。
int A[5] = {1,2,3,4,5,6}; --> 编译警告,warning: excess elements in array initializer
越界访问一些未申请的内存空间
int A[] = {1,2,3,4,5}; --> 编译通过,决定下标等于5
int A[] = {1,2,3,4,5,6}; --> 编译通过,决定下标等于6
int A[]; --> 编译出错,error: array size missing in ‘A’
没有指明数组的空间大小
int A[] = {}; --> 编译通过,但是不会在内存中申请空间。
2、先定义数组,后初始化。
int A[5];
A = {1,2,3,4,5}; --> 编译出错,如果定义数组时,没有整体初始化,那么在后续无法整体初始化。
int A[5];
A[5] = {1,2,3,4,5}; --> 编译出错,如果定义数组时,没有整体初始化,那么在后续无法整体初始化。
int A[5];
A[0] = 1;
A[1] = 2;
A[2] = 3;
A[3] = 4;
A[4] = 5; --> 编译通过,只能单个元素地赋值。
总结: 如果定义数组时,没有整体初始化,那么后续不能对数组进行整体初始化,只能单个单个元素地赋值。 -> 结合循环
三、数组的下标。
例子:int A[N]; --> 里面有很多个元素,我们可以通过数组名+下标形式来访问任意一个元素。
下标范围:0~(N-1)
最后一个元素不是A[N],而是A[N-1]
四、研究数组的名字在内存的意义。
例如: int A[N]; --> 在内存中连续申请N*4个字节,然后使用变量A间接访问这片空间。
--> 只需要使用数组名+下标形式就可以访问数组中任何一个成员。
在现在看来,数组名字A就是代表数组在内存中申请的全部空间。
1、当数组名作用于sizeof()时,那么数组名就是代表数组在内存中占用的空间。
#include <stdio.h>
int main(int argc,char *argv[])
{
int A[3];
printf("sizeof(A):%ld\n",sizeof(A)); //12
return 0;
}
2、当数组名不是作用于sizeof()时,那么数组名就是等价于数组首元素的地址。
数组名:A
数组首元素:A[0]
数组首元素的地址:&A[0]
A 等价于 &A[0]
#include <stdio.h>
int main(int argc,char *argv[])
{
int A[3];
printf("A = %p\n",A); //0x7ffe3d68b840
printf("&A[0] = %p\n",&A[0]); //0x7ffe3d68b840
return 0;
}
注意:当数组名不作用于sizeof()时,其代表数组首元素的地址,不是数组的地址。
五、指针的概念。
1、什么是指针?什么是指针变量?
指针指的是内存空间上的地址值,
例如:0x7ffe3d68b840 --> 指针指的就是地址
指针变量指的是可以存放指针的变量。 --> 指针变量指的是一个变量
2、既然指针变量是可以存放指针,那么指针变量怎么写?
1)将指针地址指向的目标的内存空间定义一下。
int a;
2)这个申请下来的空间地址该如何表示? --> 取址符:&
&a;
3)定义对应的指针变量?
int *p
指针变量怎么写?
1)先写一个* *
2)在*后面写一个指针变量的名字 *p
3)确定你指针的内容是什么 int a
4)将第3步的结果的变量名去掉 int
5)将第2步的结果写在第4步结果的后面 int *p
分析结果: int *p --> 这个变量是专门用来存储整型变量的地址的。
这个int *p这个指针变量的数据类型与变量名是什么?
数据类型:int * --> 指针并不是基本数据类型,而是非基本的数据类型
这种类型的变量是专门用于存放整型变量的地址
变量名:p
4)赋值。
p = &a;
5)解引用。
取址符: & 已知普通变量名a,&符号可以帮你求a变量的地址。
&a->求出a的地址
p=&a->然后把a的地址存放在p的变量上
解引用: * 已知指针变量名p,*符号可以帮你求p变量指向的空间里面的值
int a = 100;
int *p = &a;
那么*p就可以得到100
解引用用法: *指针变量:得到该指针变量指向的空间上的内容。
#include <stdio.h>
int main(int argc,char *argv[])
{
int a = 100;
printf("&a = %p\n",&a);
int *p = &a;
printf("p = %p\n",p);
int b = *p; //把p指向的空间的内容取出来,然后赋值给b
printf("b = %d\n",b); //100
return 0;
}
结论:&与*是一对逆运算,所以在一些场景中可以相互抵消。
例如:
int a = 100;
int *p = &a;
为什么*p是等于100?
*p = *(&a) = a = 100
3、请问&a这个地址是什么类型的? -> int *
实际上就是在问存放该地址的指针变量的数据类型是什么?
&a -> 整型变量的地址 -> 存放整型变量的地址,应该是int*p -> int *p这个指针变量数据类型是int *
练习: 完成《指针习题(C语言).doc》 1 2 3 5
4、指针的种类。
区分方法:使用数据类型来区分。
指向目标:
int a --> 指针变量 int *pa --> 类型:int * --> 这种类型的变量是专门用于存放整型变量的地址
指向目标:
char b --> 指针变量 char*pb --> 类型:char * --> 这种类型的变量是专门用于存放字符型变量的地址
指向目标:
char c --> 指针变量 char*pc --> 类型:char * --> 这种类型的变量是专门用于存放字符型变量的地址
例子: 0x100与0x200,假设0x100这个地址是指向一个int类型的数据,所以0x100这个地址的类型是int*
假设0x200这个地址是指向一个char类型的数据,所以0x200这个地址的类型是char*
虽然0x100与0x200都是一个地址,但是由于这两个地址指向的内容不一样,所以这两个地址的类型是不一样。
5、指针变量在内存中占用的空间大小。
#include <stdio.h>
int main(int argc,char *argv[])
{
char a;
int b;
double c;
char *pa = &a;
int *pb = &b;
double *pc = &c;
printf("sizeof(a) = %ld\n",sizeof(a)); //1
printf("sizeof(b) = %ld\n",sizeof(b)); //4
printf("sizeof(c) = %ld\n",sizeof(c)); //8
printf("sizeof(pa) = %ld\n",sizeof(pa)); //8
printf("sizeof(pb) = %ld\n",sizeof(pb)); //8
printf("sizeof(pc) = %ld\n",sizeof(pc)); //8
return 0;
}
结论:只要是指针变量,不管是什么类型的指针变量,其在内存空间中都是占用8个字节的空间。
六、空指针与野指针。
1、什么是野指针?
定义了一个指针变量,如果没有进行初始化,系统就会有可能随机赋值一个地址给这个指针变量。
也就会说,这个指针是指向一个未知的区域,那么这种指针变量就是野指针。
int main(int argc,char *argv[])
{
int a; //局部变量 如果没有赋值,那么就会赋值一个随机值给a
int *p; //局部变量 如果没有赋值,那么就会赋值一个随机值给p
}
2、如何解决野指针?
1)定义指针变量同时初始化地址值。
int a;
int *p = &a; //这样p就不会乱指,就会固定指向a的空间。
2)使用空指针。
什么是空指针?
其实空指针就是内存空间上的一个地址,这个地址是0x0
#define NULL 0x0 NULL 等价于 0x0 等价于 0 等价于 空指针
int main(int argc,char *argv[])
{
int *p = NULL; //p就会指向一个安全的区域,不会乱指。
}
3、如果一个指针指向安全区域,这时候解引用这个指针,会怎样?
1)写好代码了。
#include <stdio.h>
int main(int argc,char *argv[])
{
int a;
int *p = NULL;
*p = 10;
return 0;
}
2)编译通过。
gcc null.c -o null
由于代码中没有语法错误,所以编译通过。
3)执行代码。
gec@ubuntu:/mnt/hgfs/GZ2180/01 C语言/05/code$ ./null
Segmentation fault (core dumped) --> 段错误
4、如果出现了段错误,怎么找?
段错误不是语法错误,所以在编译阶段,不会提示任何东西。
而是在运行时,代码在内存上跑的时候,才发现访问越界的空间,那么就会段错误,就会自动打印Segmentation fault (core dumped)。
段错误特点:
只要程序发生了段错误,程序就会立即结束运行。
#include <stdio.h>
int main(int argc,char *argv[])
{
int a;
printf("11111\n");
int *p = NULL;
printf("22222\n"); --> 使用二分法寻找段错误的地方。
printf("%p\n",p); //p=null
*p = 10; //找到段错误在这行。
printf("33333!\n");
return 0;
}
总结:如果程序出现段错误,解决步骤:
1)使用二分法寻找出段错误的位置。
2)在段错误的那句话的前一行,将段错误那句话所涉及到所有的指针变量的值都打印出来
3)你肯定会发现,里面某一个指针的值为NULL。
七、通用类型指针。
1、强制类型转换。
int a = 100;
int *pa = &a;
char b = 'x';
char *pb = &b;
在这里,我们能不能把&b赋值给pa,把&a赋值给pb?
int a = 100;
char *pb = &a; //编译警告: warning: initialization from incompatible pointer type
int a = 100;
char *pb = (char *)&a; //编译通过
强制转换类型:例如: &a是一个int*类型的指针,里面存放着地址int*类型的,但是(char *)&a,就是强制让这个地址不要指向int类型数据,而是指向char类型的数据。
2、什么是通用类型指针?
通用类型指针其实就是可以存放任意类型的指针,也就是说什么类型的指针都可以赋值给通用类型指针的变量。
通用类型指针数据类型: void *
通用类型指针变量: void *p
int a = 100;
char b = 'x';
int *pa = &a; //pa所指向的空间是4个字节 pa指针只能存放int*类型的地址
char *pb = &b; //pb所指向的空间是1个字节 pb指针只能存放char*类型的地址
void *p = &a; //p确实可以存储着a变量的地址,但是p所存储的地址指向多少个字节,不知道。
void *p = &b; //p确实可以存储着b变量的地址,但是p所存储的地址指向多少个字节,不知道。
3、通用类型指针有什么作用?
解析了强制转换类型的意义。
1)任何特定类型的指针都可以赋值给通用类型指针。
int a; //变量a的地址是int*类型
char b; //变量b的地址是char*类型
double c; //变量c的地址是double*类型
void*p = &a;
void*p = &b;
void*p = &c;
以上几句话的含义?
仅仅是把a变量的地址、b变量的地址、c变量的地址赋值了p,一旦赋值了给p之后,该地址指向的内容就变成未知。
2)通用类型指针赋值给一些特定的指针类型?
int a = 100; //在内存中连续申请4个字节的空间,使用变量a间接访问。
void *p = &a; //把a对应的空间的地址赋值给通用类型变量p,这时候p确实是保存着a变量的地址,但是p指向的内容是未知的。
char *pb = p; //把a的地址赋值给指针变量pb,这时候pb确实是保存着a的地址,并且非常明确地知道指向一个字节。
3)综合以上几句话:
int a = 100;
int *pa = &a; //pa存储a的地址,pa指向int类型的数据
void *p = pa; //p存储a的地址,p指向未知区域
char *pb = p; //pb存储a的地址,pb指向char类型的数据
等价于
int a = 100;
char *pb = (char* )&a; //强制转换类型就是使用void *通用类型的指针来解析的。
4、易错点。
1)
int a = 10;
void *p = &a; //p指向的区域是未知的
int b = *p; //编译错误
printf("b = %d\n",b);
解决方案:
int a = 10;
void *p = &a; //p指向的区域是未知的
int b = *(int *)p; //要必须先将p强制转换为int*类型,才可以进行解引用
printf("b = %d\n",b);
2)
void a; //只有void *类型的变量,没有void型的变量
void *p = &a;
八、指针运算。
1、指针的加减法。
int a = 100;
int *pa = &a; //pa就是存放着a的地址。 0x100
pa+1 --> 将a的地址往上偏移1个单位。
pa -> 代表a的地址
+ -> 代表地址往上偏移 - -> 代表地址往下偏移
1 -> 代表1个单位
1个单位究竟等于多少字节? --> 取决于指向的内容的数据类型
例如:一个整型(int)指针,那么往上偏移一个单位,那么就是偏移了4个字节
#include <stdio.h>
int main()
{
int a = 100;
int *pa = &a;
printf("pa = %p\n",pa); //0x7fffb399992c
printf("pa+1 = %p\n",pa+1); //0x7fffb3999930
char b = 'x';
char *pb = &b;
printf("pb = %p\n",pb);
printf("pb+1 = %p\n",pb+1);
return 0;
}
2、两个地址能不能相加?
#include <stdio.h>
int main()
{
int a;
int b;
char c;
int *pa = &a;
int *pb = &b;
char *pc = &c;
printf("pa = %p\n",pa);
printf("pb = %p\n",pb);
//printf("pa+pb=%p\n",pa+pb); //编译出错
printf("pa+pc=%p\n",pa+pc); //编译出错
return 0;
}
结论:无论类型一不一样,都不能相加。
3、两个地址能不能相减?
#include <stdio.h>
int main()
{
int a;
int b;
char c;
int *pa = &a;
int *pb = &b;
char *pc = &c;
printf("pa = %p\n",pa);
printf("pb = %p\n",pb);
//printf("pa-pb=%ld\n",pa-pb); //相差的单位数
printf("pa-pc=%d\n",pa-pc); //编译出错
return 0;
}
结论:如果类型一致,则可以相减,结果为两个地址之间相差的单位数。
如果类型不一样,则不可以相减。
练习2: 使用指针运算来证明以下的结论:
数组名字是数组首元素的地址,而不是数组的地址。
A 等价于 &A[0] 不等价于 &A
九、数组的运算。
1、数组名字是数组首元素的地址。
int A[3];
A 等价于 &A[0] --> 地址类型:int* -> 这个地址指向一个int类型的数据
2、数组运算。
A --> 首元素的地址 --> &A[0]
A+1 --> 首元素的地址往上移动一个单位(4个字节) --> 第2个元素的地址 &A[1]
A+2 --> 首元素的地址往上移动二个单位(8个字节) --> 第3个元素的地址 &A[2]
*A --> *(&A[0]) --> A[0] --> 解引用首元素的地址,得到首元素的值
*(A+1)--> *(&A[1]) --> A[1] --> 解引用第2个元素的地址,得到第2个元素的值
*(A+2)--> *(&A[2]) --> A[2] --> 解引用第3个元素的地址,得到第3个元素的值
*(A+0) 等价于 A[0]
*(A+1) 等价于 A[1]
*(A+2) 等价于 A[2]
结论:数组下标其实就是数组首元素的地址往上偏移的单位数。
A[N] 等价于 *(A+N)
3、执行A++,会有什么结果出现?
#include <stdio.h>
int main()
{
int A[3];
printf("A = %p\n",A); //0x100
printf("A+1 = %p\n",A+1); //0x104
//printf("A++ = %p\n",A++); //编译出错 A++含义: A=A+1
//printf("++A = %p\n",++A); //编译出错 ++A含义: A=A+1
//以后注意了: 只要给数组名赋值的语句,100%错的。
return 0;
}
4、加法交换律。
A[N] = *(A+N) = *(N+A) = N[A]
#include <stdio.h>
int main(int argc,char *argv[])
{
int A[3] = {1,2,3};
printf("A[0] = %d\n",A[0]);
printf("*(A+0) = %d\n",*(A+0));
printf("*(0+A) = %d\n",*(0+A));
printf("0[A] = %d\n",0[A]);
return 0;
}
结论: 已知一个数组,调用某一个元素
数组名[下标] 等价于 下标[数组名]