一.数组
1.概念:由相同类型的多个元素所组成的一种复合数据类型。
2.逻辑:一次性定义多个相同的变量,并存储到一片连续的内存中。
3.格式:类型说明符+数组名+[整型常量表达式],数组名:c语言标识符,其值为该数组的首地址(常量)。整型常量表达式:指定数组元素的个数。
demo:
#include <stdio.h>
int main(int argc, char const *argv[])
{
// 申请5块连续的空间,将a称为数组
int a[5];
// 将这5块空间逐一赋值,注意,数组下标从0开始
a[0] = 1;
a[1] = 20;
a[2] = 30;
a[3] = 40;
a[4] = 50;
//a[5] = 60; // 越界,无法使用
printf("%d\n",a[4]);
// 循环给数组a赋值
for(int data = 10,i = 0; i < 5; i++,data+=10)
{
a[i] = data;
}
// 遍历输出
for(int i = 0; i < 5; i++)
{
printf("%d\n",a[i]);
}
return 0;
}
4.数组元素的引用
存储模式:一片连续的内存,按数据类型分割成若干相同大小的格子
数组名[下标]
"下标":C语言的下标是从0开始,下标必须是>=0的整数
a[0]、a[1]、a[n],下标最小值为0,最大值为 元素个数 -1
5.初始化:在定义的时候赋值,称为初始化
// 正常初始化
int a[5] = {100,200,300,400,500};
6.字符数组
1)概念:专门存放字符的数组
2)
char s1[5] = {'a', 'b', 'c', 'd', 'e'}; // s1存放的是字符序列,非字符串
char s2[6] = {'a', 'b', 'c', 'd', 'e', '\0'}; // s2存放了一个字符串
char s[6] = {"abcde"}; // 使用字符串直接初始化字符数组
char s[6] = "abcde" ; // 大括号可以省略
s[0] = 'A'; // 索引第一个元素,赋值为 'A'
7.数组偏移量
数组每个元素的地址的+1偏移量相当于偏移一个偏移量,偏移量与数组类型有关,比如char类型的地址数组地址+1相当于偏移一个字节,int类型的数组地址+1相当于偏移四个字节。
8.地址元素的解引用
int a = 10;
printf("%d\n",a);
printf("%p\n",&a);
// * 表示将地址里面的内容取出,我们把它称为解引用
printf("%d\n",*(&a));
[]的作用就是解引用,例如ob[0]等价于*(ob+0)
数组名相当于这个数组的首元素地址 所以ob==&ob[0]
9.字符串常量
字符串常量在内存中的存储,实质是一个匿名数组
10.多维数组(二维数组)
概念:若数组元素类型也是数组,则该数组称为多维数组,就是一维数组的集合
int a0[3];
int a1[3];
int a[2][3];//a[0]-->a0 a[1]-->a1
int ; a[3] // 数组
int [3]; a[2] // 数组
第一种解释:
定义一个二维数组。该数组是由2个一维数组组成,分别是a[0] a[1]
每个一维数组由3个元素组成,所以二维数组有6个元素
数据类型 二维数组的名字[有多少个一维数组][每个一维数组有多少个元素]
// 代码释义:
// 1, a[2] 是数组的定义,表示该数组拥有两个元素
// 2, int [3]是元素的类型,表示该数组元素是一个具有三个元素的整型数组
第二种解释:
该数组一共有2行,每行由3个元素组成(2行3列)
数据类型 二维数组名[行][列]
所谓的行:表示这个二维数组一共有多少个一维数组
所谓的列:表示这个二维数组每个一维数组有多少个元素
11.二维数组解引用
12.数组地址偏移
#include <stdio.h>
int main()
{
char Names[3][5] = {"jack","rose","ken"};
// printf("Names[0] %p,%p\n",Names[0],&Names[0][0]);
// printf("Names[1] %p,%p\n",Names[1],&Names[1][0]);
// printf("Names[2] %p,%p\n",Names[2],&Names[2][0]);
// Names[0]相当于&Names[0][0]
printf("Names[0]+1=%p,%p\n",Names[0]+1,&Names[0][1]);
printf("Names[0][0]:%c,%c\n",Names[0][0],*(Names[0]+0));
printf("Names[0][2]:%c,%c\n",Names[0][2],*(Names[0]+2));
printf("&Names[0]+1=%p,%p\n",&Names[0]+1,&Names[1]);
printf("*(&Names[0]+1)=%c,%c\n",*(&Names[0]+1)[0],*(&Names[1])[0]);
printf("*(&Names[0]+1)=%c,%c,%c,%c\n",(*(&Names[0]+1))[1],(*(&Names[1]))[1],
*(*(&Names[0]+1)+1),*(*(Names+1)+1));
printf("%c,%c,%c,%c\n",Names[2][2],*(Names[2]+2),(*(Names+2))[2],*((*(Names+2)+2)));
return 0;
}
13.数组万能拆解法
任意的数组,不管有多复杂,其定义都由两部分组成。
第1部分:说明元素的类型,可以是任意的类型(除了函数)。
第2部分:说明数组名和元素个数。
示例:
int a[4]; // 第2部分:a[4]; 第1部分:int
int b[3][4]; // 第2部分:b[3]; 第1部分:int [4]
int c[2][3][4]; // 第2部分:c[2]; 第1部分:int [3][4]
int *d[6]; // 第2部分:d[6]; 第1部分:int *
int (*e[7])(int, float); // 第2部分:e[7]; 第1部分:int (*)(int, float)
14.数组名涵义
1)数组名有两个含义
第一含义是:整个数组
第二含义是:首元素地址
2)当出现以下情形时,那么数组名就代表整个数组:
在数组定义中
在 sizeof 运算表达式中
在取址符&中
3)其他任何情形下,那么数组名就代表首元素地址。即:此时数组名就是一个指向首元素的指针。
示例:
int a[3]; // 此处,a 代表整个数组
printf("%d\n", sizeof(a)); // 此处,a 代表整个数组
printf("%p\n", &a); // 此处,a 代表整个数组,此处为整个数组的地址
int *p = a; // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0]
p = a + 1; // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0]
function(a); // 此处,a 代表首元素 a[0] 的地址,等价于 &a[0]
scanf("%d\n", a); // 此处,a 代表首元素 a[0] 的地址,等价 &a[0]
15.数组下标
a[i] = 100; 等价于 *(a+i) = 100;
16.字符串常量
printf("%d\n", sizeof("abcd")); // 此处 "abcd" 代表整个数组
printf("%p\n", &"abcd"); // 此处 "abcd" 代表整个数组
printf("%c\n", "abcd"[1]); // 此处 "abcd" 代表匿名数组的首元素地址
char *p1 = "abcd"; // 此处 "abcd" 代表匿名数组的首元素地址
char *p2 = "abcd" + 1; // 此处 "abcd" 代表匿名数组的首元素地址
17.变长数组
概念:定义时,使用变量作为元素个数的数组。
二.指针
1.指针基础
概念:一个专门用来存放内存地址的变量,指针也就是指针变量
地址:比如 &a 是一个地址,也是一个指针,&a 指向变量 a
专门用于存储地址的变量,又称指针变量。
格式:类型 *指针变量名
-
“类型” : 指针变量指向的内存空间存放的数据类型
-
“指向” : 如果我保存了你的地址,那么就说我指向你
-
“*” :定义指针变量的固定格式
定义:
// 用于存储 int 型数据的地址,p1 被称为 int 型指针,或称整型指针
int *p1;
// 用于存储 char 型数据的地址,p2 被称为 char 型指针,或称字符指针
char *p2;
// 用于存储double型数据的地址,p3 被称为 double 型指针
double *p3;
赋值:
int a = 100;
p1 = &a; // 将一个整型地址,赋值给整型指针p1
char c = 'x';
p2 = &c; // 将一个字符地址,赋值给字符指针p2
double f = 3.14;
p3 = &f; // 将一个浮点地址,赋值给浮点指针p3
尺寸:
-
指针尺寸指的是指针所占内存的字节数
-
指针所占内存,取决于地址的长度,而地址的长度则取决于系统寻址范围,即字长
-
结论:指针尺寸只跟系统的字长有关,跟具体的指针的类型无关
-
在32位系统,指针的大小占用4字节
-
在64位系统,指针的大小占用8字节
2.野指针
概念:指向一块未知区域的指针,被称为野指针。野指针是危险的
3.空指针
很多情况下,我们不可避免地会遇到野指针,比如刚定义的指针无法立即为其分配一块恰当的内存,又或者指针所指向的内存被释放了等等。一般的做法就是将这些危险的野指针指向一块确定的内存,比如零地址内存。
概念:空指针即保存了零地址的指针,亦即指向零地址的指针。
NULL地址其实就是 (void *)0,就是0
示例:
// 1,刚定义的指针,让其指向零地址以确保安全:
char *p1 = NULL;
int *p2 = NULL;
// 2,被释放了内存的指针,让其指向零地址以确保安全:
char *p3 = malloc(100); // a. 让 p3 指向一块大小为100个字节的内存
free(p3); // b. 释放这块内存,此时 p3 相当于指向了一块非法内存
p3 = NULL; // c. 让 p3 指向零地址
4.指针运算
示例:
int a = 100;
int *p = &a; // 指针 p 指向整型变量 a
char b = 100;
char *ptr = &b;
char *k3 = ptr + 2;
int *k1 = p + 2; // 向上移动 2 个目标(2个int型数据)
int *k2 = p - 3; // 向下移动 3 个目标(3个int型数据)
1)运算
int a = 10;
//int *p = &a;// 初始化
int *p;
// 其它时候用来表示 *单独使用为解引用,所以 *p = &a; 错误
p = &a;
printf("a value:%d\n",a);
printf("*(&a) value:%d\n",*(&a));// * 与 &是互逆运算
printf("%d\n",*p);
*p = 300; //*p 此时相当于变量a
printf("a value:%d\n",a);
2)偏移
指针的加减称为指针的偏移,偏移的单位由指针的类型大小所决定所谓指针的类型大小指的是指针变量所指向的内存空间的数据类型
3)指针数组
int b[10];
// 指针没有让它指向对应的空间,会出现段错误
int *a = b;
a[0] = 1;
printf("%d\n",a[0]);
// 如果是指针操作,会修改原有空间的内容
// 指针与原空间为同一块空间
int a[3] = {3,4,5};
int *p = a;
p[1] = 10;
printf("%d,%d,%d\n",*(p+1),p[1],a[1]);
// 空间不属于同一块空间
int b = 19;
int q = b;
q = 18;
printf("%d,%d\n",b,q)
4)数组与指针转换
数组的指针的本质为指针,此指针保存的是数组的地址,说白了就是,指针指向数组名,此类指针称为数组指针
5)指针数组
概念:
指针的数组,的本质为数组,数组里面存放的内容为指针,而一般指针是指向的地址为字符串居多,我们把此类型称为指针数组
定义:
char *p[5];
char *p = "jack";
char *p[5] = {"jack","rose","ken","tony","tom"};
printf("%s\n",p[2]);
printf("%c\n",p[3][3]);
6)字符串与指针
字符串常量在内存中实际就是一个匿名数组
char buf[] = "abcd";
printf("%c\n",buf[1]);
printf("%c\n","abcd"[1]);
char *p = "abcd"; // 将p指向一块匿名数组的一个首地址
printf("%p,%p\n","abcd",&"abcd"[0]);
// 讲"jack"字符串存放在buf的空间里面
char buf[] = "jack";
// 定义指针p指向buf的首地址
char *p = buf;
// 定义指针q指向"jack"的首地址
char *q = "jack";
printf("%s,%s,%s,%d\n",buf,p,q,sizeof(q));
5.char型指针
char型指针实际上跟别的类型的指针并无本质区别,但是由于C语言中的字符串以字符数组的方式存储,而数组在大多数场合又会表现为指针,因此字符串在绝大多数场合就表现为char型指针。
定义:
char * = "abcd";
6.多级指针
-
如果一个指针变量p1存储的地址,是另一个普通变量a的地址,那么称p1为一级指针
-
如果一个指针变量p2存储的地址,是指针变量p1的地址,那么称p2为二级指针
-
如果一个指针变量 p3 存储的地址,是指针变量 p2 的地址,那么称 p3 为三级指针
-
以此类推,p2、p3等指针被称为多级指针
示例:
int a = 100;
int *p1 = &a; // 一级指针,指向普通变量
int **p2 = &p1; // 二级指针,指向一级指针
int ***p3 = &p2; // 三级指针,指向二级指针
7.指针万能拆解法
任意的指针,不管有多复杂,其定义都是由两部分组成
-
第一部分:指针所指向的数据类型,可以是任意类的类型
-
第二部分:指针的名
示例:
char (*p1); // 第2部分:*p1; 第1部分:char;
char *(*p2); // 第2部分:*p2; 第1部分:char *;
char **(*p3); // 第2部分:*p3; 第1部分:char **;
char (*p4)[3]; // 第2部分:*p4; 第1部分:char [3];
char (*p5)(int, float); // 第2部分:*p5; 第1部分:char (int, float);
8.void型指针
概念:无法明确指针所指向的数据类型时,可以将指针定义为 void 型指针
-
要点:
-
void 型指针无法直接索引目标,必须将其转换为一种具体类型的指针方可索引目标
-
void 型指针无法进行加减法运算
-
-
void关键字的三个作用:
-
修饰指针,表示指针指向一个类型未知的数据。
-
修饰函数参数列表,表示函数不接收任何参数。(预习)int main(void)
-
修饰函数返回类型,表示函数不返回任何数据。(预习)void func(int)
-
-
示例:
// 指针 p 指向一块 4 字节的内存,且这4字节数据类型未确定
void *p = malloc(4);
// 1,将这 4 字节内存用来存储 int 型数据
*(int *)p = 100;
printf("%d\n", *(int *)p);
// 2,将这 4 字节内存用来存储 float 型数据
*(float *)p = 3.14;
printf("%f\n", *(float *)p);
9.const型指针
const型指针有两种形式:①常指针 ②常目标指针
1.常指针:const修饰指针本身,表示指针变量本身无法修改。
char* const p;
2.常目标指针:const修饰指针的目标,表示无法通过该指针修改其目标
const int *p;
int const *p;
示例:
int a = 100;
int b = 200;
// 第1中形式,const修饰p1本身,导致p1本身无法修改
int * const p1 = &a;
// 第2中形式,const修饰p2的目标,导致无法通过p2修改a
int const *p2 = &a;
const int *p2 = &a;