一、数组
1、数组的定义
- 格式:类型 数组名[元素个数]
如:int i[5]
2、访问数组元素
- 格式:数组名[下标]
如:
i[0]; // 访问i数组中第1个元素
i[3]; // 访问i数组中第4个元素 - 注:数组的下标是从0开始的,下标0对应数组第一个元素
3、数组的初始化
- 将数组中所有元素初始化为0
int a[5] = {0}; //事实上,这个只是把第一个元素赋值为0,其余各位是自动初始化为0 int a[5] = {1}; //这里就是,第一个元素为1,其余元素为0
- 如果是给元组赋予不同的值,用逗号隔开即可
int a[5] = {1, 2, 3, 4, 5};
- 也可以只给一部分元素赋值,未被赋值的元素,自动初始化为0
int a[5] = {1, 2, 3}; //前三位成功赋值,后两位,自动初始化为0
- 也可以指定元素进行赋值,未被赋值的元素自动初始化为0
int a[5] = {[2] = 2, [4] = [4]}; //数组a的第3位被赋值为2,第5位被赋值为4,因为数组的下标是从0开始的!!!
- 也可以只给出元素的值,不指定数组的长度
(编译器会根据值的个数自动判断数组的长度)
int a[] = {1, 2, 3, 4, 5}
二、 二维数组
1、二维数组的定义
- 格式:类型 数组名[常量表达式] [常量表达式]
2、二维数组的访问
- 格式:数组名[下标][下标]
如:
a[0][0]; // 访问a数组中第一行第一列的元素
b[2][3]; // 访问b数组中第三行第四列的元素
3、二维数组的初始化
- 由于二维数组在内存里面中是线性存放的,因此可以将所有的数据写在一个花括号里面
int a[2][3] = {1, 2, 3, 4, 5, 6};
那到底是怎么存放的呢,用程序打印一下,观察结果。
#include <stdio.h> int main() { int a[2][3] = {1, 2, 3, 4, 5, 6}; int i, j; for (i = 0;i < 2;i++) { for (j = 0;j < 3;j++) { printf("a[%d][%d] = %d\n", i, j, a[i][j]); } } return 0; }
结果如下:
a[0][0] = 1 a[0][1] = 2 a[0][2] = 3 a[1][0] = 4 a[1][1] = 5 a[1][2] = 6
通过结果,是不是很容易看出来了~
- 更直观的表示元素的分布,可以用大括号将每一行的元素括起来
int a[2][3] = {{1, 2, 3}, { 4, 5, 6}};
或者写成这种样式:
int a[][3] = { {1, 2, 3}, {4, 5, 6} };
以上这两种形式的行数都可以省略不写
- 将整个二维数组初始化为0,则只需要在大括号里写一个0
int a[2][3] = {0};
- 对某些指定的元素进行初始化,其它元素自动初始化为0
int a[2][3] = {[0][0] = 1, [1][2] = 2};
三、指针
1、定义指针变量
- 格式:类型名 *指针变量名
char *pa; //定义一个指向字符型的指针变量
int *pb; // 定义一个指向整型的指针变量
取地址运算符和取值运算符
- 如果需要获取某个变量的地址,可以使用取地址运算符(&)
如:char *pa = &a; - 如果需要访问指针变量指向的数据,可以使用取值运算符(*)
如:printf("%c, %d\n", *pa, *pb); - 注意:
值得一提的是 *
这个符号,在定义指针需要它,而在同时又是取值运算符。需要慢慢理解
避免使用未初始化的指针
int *p //例如这样就是未初始化的指针 ----------------------------- int *p = &a //例如这样就是初始化过的指针
- 指针的小例子
# include <stdio.h> int main(){ char a = 'H'; int b = 521; char *pa = &a; int *pb = &b; // 这里的*是用来定义指针变量 printf("a=%c\n", *pa); // 这里打印的是地址指向的那个变量值 printf("b=%d\n", *pb); // 这里的*是用来取值 printf("sizeof pa = %d\n", sizeof(pa)); printf("sizeof pb = %d\n", sizeof(pb)); printf("a的地址:%p\n", pa); // 这里打印的是指针存放的地址 printf("b的地址:%p\n", pb); return 0; }
结果:
a=H b=521 sizeof pa = 8 sizeof pb = 8 a的地址:000000000062FE0F b的地址:000000000062FE08
数组名其实是数组的第一个元素的地址
# include <stdio.h> int main(){ char str[3]; printf("请输入最帅的博主:"); scanf("%s", str); printf("最帅博主的地址是:%p\n", str); // 查看数组名的地址 printf("最帅博主的地址是:%p\n", &str[0]); // 取数组第一个元素的地址 return 0; }
运行结果:
请输入最帅的博主:远方的星 最帅博主的地址是:000000000062FE10 最帅博主的地址是:000000000062FE10
那数组其它位置的地址与首地址是什么关系呢?
# include <stdio.h> int main(){ char a[] = "Hello"; int b[] = {1, 2, 3, 4, 5}; float c [] = {1.1, 2.2, 3.3, 4.4}; double d[] = {1.1, 2.2, 3.3, 4.4}; printf("a[0] -> %p, a[1] -> %p, a[2] -> %p\n", &a[0], &a[1], &a[2]); printf("b[0] -> %p, b[1] -> %p, b[2] -> %p\n", &b[0], &b[1], &b[2]); printf("c[0] -> %p, c[1] -> %p, c[2] -> %p\n", &c[0], &c[1], &c[2]); printf("d[0] -> %p, d[1] -> %p, d[2] -> %p\n", &d[0], &d[1], &d[2]); return 0; }
运行结果:
a[0] -> 000000000062FE10, a[1] -> 000000000062FE11, a[2] -> 000000000062FE12 b[0] -> 000000000062FDF0, b[1] -> 000000000062FDF4, b[2] -> 000000000062FDF8 c[0] -> 000000000062FDE0, c[1] -> 000000000062FDE4, c[2] -> 000000000062FDE8 d[0] -> 000000000062FDC0, d[1] -> 000000000062FDC8, d[2] -> 000000000062FDD0
这里是十六进制,数组a,char类型,依次增加1个字节;数组b,int类型,依次增加4个字节;数组c,float类型,依次增加4个字节;数组d,double类型,依次增加8个字节。与下表相对,不难发现其规律,依次增加的大小为数据类型占的字节数。
- 64位编译器
2、指针的运算
当指针指向数组元素的时候,我们可以对指针变量进行加减运算,这样做的意义相当于指向距离指针所在位置向前或向后第n个元素。
- 例:
# include <stdio.h> int main(){ char a[] = "Hello"; char *p = a; printf("*p = %c, *(p+1) = %c, *(p+3) = %c\n", *p, *(p+1), *(p+3)); return 0; }
结果:
*p = H, *(p+1) = e, *(p+3) = l
由此可见,加减代表的是方向。p+1指的是,指向数组的下一个元素,那么p-1,便是指向数组的上一个元素。
- 这里使用指针间接访问数组元素的方法叫做指针法。
3、指针和数组的区别
- 数组名只是一个地址,而指针是一个左值。
lvalue(左值):C语言中指用于识别或定位一个存储位置的标识符,左值必须是可改变的。
4、指针数组
- 例如:
int *p[5]
- 指针数组是一个数组,每个数组元素存放一个指针变量。
在指向字符指针的时候非常好用。
# include <stdio.h> int main(){ char *p[] = { "选择了就不必害怕", "努力了就一定成功", "加油,陌生人!" }; int i; for (i = 0;i < 3;i++){ printf("%s\n", p[i]); // 这里是不能写成*p[i]的,这样的意思是指向数组的第一个元素地址 } return 0; }
5、数组指针
- 例如:
int (*p)[5]
- 数组指针是一个指针,它指向的是一个数组
例:这里使用数组指针完成的
# include <stdio.h> int main(){ int temp[5] = {1, 2, 3, 4, 5}; int (*p2)[5] = &temp; //指向数组的指针 int i; for (i = 0;i < 5;i++) { printf("%d\n", *(*p2 + i)); } return 0; }
运行结果:
1 2 3 4 5
- 比较:使用(普通)指针
(数组名其实是数组的第一个元素的地址)
结果上虽然相等,但本质上是有所区别的。。。
# include <stdio.h> int main(){ int temp[5] = {1, 2, 3, 4, 5}; int *p = temp; //指向数组的第一个元素的指针 int i; for (i = 0;i < 5;i++) { printf("%d\n", *(p + i)); } return 0; }
6、指针和二维数组
arry指向的是包含5个元素的数组的指针
把一个地址的值取出来,称之为 “解引用”
解引用到底是什么,查了一些资料,是否真的是 “取指针指向的地址的内容” 还不得而知,还需要日后慢慢的沉淀。
解引用:
*(array+i) == array[i] *(*(array+i)+j) == array[i][j] *(*(*(array+i)+j)+k) == array[i][j][k]
下标索引的形式都可以转换为使用指针间接索引的形式。
- 例:
# include <stdio.h> int main(){ int array[4][5] = {0}; int i,j,k = 0; for (i=0;i<4;i++) { for(j=0;j<5;j++) { array[i][j] = k++; } } printf("*(array+1):%p\n", *(array + 1));//array+1的和进行解引用 printf("array[1]:%p\n", array[1]);//array第二行首地址 printf("&array[1][0]:%p\n", &array[1][0]);//对 array[1][0]取地址 printf("**(array+1):%d\n", **(array+1));//对array+1的和的解引用进行解引用 printf("array[1][0]:%d\n", array[1][0]);//array[1][0]的值 return 0; }
运行结果:
*(array+1):000000000062FDC4 array[1]:000000000062FDC4 &array[1][0]:000000000062FDC4 **(array+1):5 array[1][0]:5
7、void指针
- void指针又称通用指针,就是可以指向任意类型的数据。任何类型的指针都可以赋值给void指针。
- 例:验证一下:
# include <stdio.h> int main(){ int num = 521; int *p1 = # char *p2 = "天道酬勤"; void *p3; p3 = p1; printf("p1:%p, p3:%p\n", p1, p3); p3 = p2; printf("p2:%p, p3:%p\n", p2, p3); return 0; }
结果如下:
p1:000000000062FE04, p3:000000000062FE04 p2:0000000000404000, p3:0000000000404000
得到的结果是一样的,由此可见,是可以将任意数据类型的指针赋值给void的指针。
但如果要根据void指针,取出其内容,最好进行数据类型的强制转换。
- 根据上述例子进行修改:
# include <stdio.h> int main(){ int num = 521; int *p1 = # char *p2 = "天道酬勤"; void *p3; p3 = p1; printf("p3:%d\n", *(int *)p3);//(int *)p3是进行类型强制转换,再加一个*,进行解引用,才能取出值 p3 = p2; printf("p3:%s\n", (char *)p3);//由于字符串的特殊性,就不需要进行进引用了 return 0; }
结果如下:
p3:521 p3:天道酬勤
8、NULL指针
NULL指针又称空指针,它有啥作用呢?
- 有利于养成良好的编程习惯
当不清楚要将指针初始化为哪个地址时,可以将它初始化为NULL;在对指针进行解引用时,先检查该指针是否为NULL。可以有效避免野指针
9、指向指针的指针
- 好处:
①避免重复分配内存
②只需要进行一处修改
10、指向常量的指针
之前见过用宏定义来定义常量,其实还可以利用const
关键字。
- const修饰的数据类型是指常类型,常类型的变量或对象的值是不能被更新的,即只能读不能重写。
例:
# include <stdio.h> int main(){ const float pi = 3.14; printf("%f\n",pi); pi = 3.1415; return 0; }
编译之后,会直接报错!
[Error] assignment of read-only variable 'pi' // [错误]只读变量'pi'的赋值
由此可以得到关于指向常量的指针的小结论:
指向常量的指针
- 指针可以修改为指向不同的常量
- 指针可以修改为指向不同的变量
- 可以通过解引用来读取指针指向的数据
- 不可以通过解引用修改指针指向的数据
11、常量指针
①、指向非常量的常量指针
- 指针自身不可以被修改
- 指针指向的值可以被修改
例如:
# include <stdio.h> int main(){ int num = 521; const int cnum = 1024; int * const p = # *p = 7758;//对指针指向的值进行修改 printf("*p:%d\n", *p); return 0; }
输出为:
*p:7758
将上述例子加以修改:
# include <stdio.h> int main(){ int num = 521; const int cnum = 1024; int * const p = # p = &cnum;//对指针本身进行修改 printf("*p:%d\n", *p); return 0; }
编译结果,系统会报错!
[Error] assignment of read-only variable 'p'
②、指向常量的常量指针
- 指针自身不可以被修改
- 指针指向的值也可以不被修改
(和上例类似)
四、未完待续
指针真的是C的灵魂所在,欲速则不达,花的时间也比较长,不断练习才能更好地理解。
这一阶段的笔记就先记录到这里,但学C的道路仍然还很漫长。。。
路漫漫其修远兮,吾将上下而求索。
大家一起加油吧!