本文最后更新于 118 天前,其中的信息可能已经过时,如有错误请发送邮件到 shikeAB@outlook.com。
- 什么是地址
-
地址与内存单元中的数据是两个完全不同的概念
- 地址如同房间编号,根据这个编号我们可以找到对应的房间
- 内存单元如同房间,房间是专门用于存储数据的
-
变量地址:
- 系统分配给” 变量” 的” 内存单元” 的起始地址
| int num = 6; |
| |
| |
| char c = 'a'; |
| |
- 在 C 语言中,允许用一个变量来存放其它变量的地址,这种专门用于存储其它变量地址的变量,我们称之为指针变量
- 示例:
| int age; |
| num = 10; |
| int *pnAge; |
| pnAge = &age; |
- 指针变量的定义包括两个内容:
- 指针类型说明,即定义变量为一个指针变量;
- 指针变量名;
- 示例:
| char ch = 'a'; |
| char *p; |
| p = &ch; |
| int num = 666; |
| int *q; |
| q = # |
- 其中,* 表示这是一个指针变量
- 变量名即为定义的指针变量名
- 类型说明符表示本指针变量所指向的变量的数据类型
- 指针变量初始化的方法有两种:定义的同时进行初始化和先定义后初始化
| int a = 5; |
| int *p = &a; |
| int b = 10; |
| p = &b; |
- 指针没有初始化里面是一个垃圾值,这时候我们这是一个野指针
- 野指针可能会导致程序崩溃
- 野指针访问你不该访问数据
- 所以指针必须初始化才可以访问其所指向存储区域
- C 语言中提供了地址运算符 & 来表示变量的地址。其一般形式为:
- C 语言中提供了 * 来定义指针变量和访问指针变量指向的内存存储空间
- 在定义变量的时候 * 是一个类型说明符,说明定义的这个变量是一个指针变量
- 在不是定义变量的时候 * 是一个操作符,代表访问指针所指向存储空间
| int a = 5; |
| int *p = &a; |
| printf("a = %d", *p); |
- 如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。也称为 “二级指针”
| char c = 'a'; |
| char *cp; |
| cp = &c; |
| char **cp2; |
| cp2 = &cp; |
| printf("c = %c", **cp2); |
- 定义一个函数交换两个变量的值
- 写一个函数,同时返回两个数的和与差
## 数组指针的概念及定义
- 数组元素指针
- 一个变量有地址,一个数组包含若干元素,每个数组元素也有相应的地址,指针变量也可以保存数组元素的地址
- 只要一个指针变量保存了数组元素的地址,我们就称之为数组元素指针
| printf(“%p %p”, &(a[0]), a); |
- 注意:数组名 a 不代表整个数组,只代表数组首元素的地址。
- “p=a;” 的作用是 “把 a 数组的首元素的地址赋给指针变量 p”, 而不是 “把数组 a 各元素的值赋给 p”
| int main (void) |
| { |
| int a[5] = {2, 4, 6, 8, 22}; |
| int *p; |
| |
| p = a; |
| printf(“%d %d\n”,a[0],*p); |
| } |
| |
- 在指针指向数组元素时,允许以下运算:
- 加一个整数 (用 + 或 +=), 如 p+1
- 减一个整数 (用 - 或 -=), 如 p-1
- 自加运算,如 p++,++p
- 自减运算,如 p–,–p
- 如果指针变量 p 已指向数组中的一个元素,则 p+1
指向
同一数组中的下一个元素,p-1 指向
同 一数组中的上一个元素。
- 结论:访问数组元素,可用下面两种方法:
- 下标法,如 a [i] 形式
- 指针法,*(p+i) 形式
- 注意:
- 数组名虽然是数组的首地址,但是数组名所所保存的数组的首地址是不可以更改的
| int x[10]; |
| x++; |
| int* p = x; |
| p++; |
| char string[]=”I love lnj!”; |
| printf("%s\n",string); |
| har *str = "lnj"; |
| for(int i = 0; i < strlen(str);i++) |
| { |
| printf("%c-", *(str+i)); // 输出结果:l-n-j |
| } |
| |
| |
| char *str = "lnj"; |
| *(str+2) = 'y'; |
| |
| |
| char *str; |
| scanf("%s", str); |
- 为什么指针可以指向一个函数?
- 函数作为一段程序,在内存中也要占据部分存储空间,它也有一个起始地址
- 函数有自己的地址,那就好办了,我们的指针变量就是用来存储地址的。
- 因此可以利用一个指针指向一个函数。其中,函数名就代表着函数的地址。
- 指针函数的定义
- 格式:
返回值类型 (*指针变量名)(形参1, 形参2, ...);
| int sum(int a,int b) |
| { |
| return a + b; |
| } |
| |
| int (*p)(int,int); |
| p = sum; |
-
指针函数定义技巧
- 1、把要指向函数头拷贝过来
- 2、把函数名称使用小括号括起来
- 3、在函数名称前面加上一个 *
- 4、修改函数名称
-
应用场景
-
注意点:
- 由于这类指针变量存储的是一个函数的入口地址,所以对它们作加减运算 (比如 p++) 是无意义的
- 函数调用中 "(指针变量名)" 的两边的括号不可少,其中的不应该理解为求值运算,在此处它 只是一种表示符号
- 结构体和数组一样属于构造类型
- 数组是用于保存一组相同类型数据的,而结构体是用于保存一组不同类型数组的
- 例如,在学生登记表中,姓名应为字符型;学号可为整型或字符型;年龄应为整型;性别应为字符型;成绩可为整型或实型。
- 显然这组数据不能用数组来存放,为了解决这个问题,C 语言中给出了另一种构造数据类型 ——“结构 (structure)” 或叫 “结构体”。
- 在使用结构体之前必须先定义结构体类型,因为 C 语言不知道你的结构体中需要存储哪些类型数据,我们必须通过定义结构体类型来告诉 C 语言,我们的结构体中需要存储哪些类型的数据
- 格式:
| struct 结构体名{ |
| 类型名1 成员名1; |
| 类型名2 成员名2; |
| …… |
| 类型名n 成员名n; |
| }; |
| struct Student { |
| char *name; |
| int age; |
| float height; |
| }; |
| struct Student { |
| char *name; |
| int age; |
| }; |
| |
| struct Student stu; |
| struct Student { |
| char *name; |
| int age; |
| } stu; |
| struct { |
| char *name; |
| int age; |
| } stu; |
- 第三种方法与第二种方法的区别在于,第三种方法中省去了结构体类型名称,而直接给出结构变量,这种结构体最大的问题是结构体类型不能复用
- 一般对结构体变量的操作是以成员为单位进行的,引用的一般形式为:
结构体变量名.成员名
| struct Student { |
| char *name; |
| int age; |
| }; |
| struct Student stu; |
| |
| stu.age = 27; |
| printf("age = %d", stu.age); |
| struct Student { |
| char *name; |
| int age; |
| }; |
| struct Student stu = {“lnj", 27}; |
| |
| struct Student { |
| char *name; |
| int age; |
| }; |
| struct Student stu = {.age = 35, .name = “lnj"}; |
| |
| struct Student { |
| char *name; |
| int age; |
| }; |
| struct Student stu; |
| stu.name = "lnj"; |
| stu.age = 35; |
| struct Student { |
| char *name; |
| int age; |
| }; |
| struct Student stu; |
| stu2 = (struct Student){"lnj", 35}; |
- 结构类型定义在函数内部的作用域与局部变量的作用域是相同的
- 从定义的那一行开始,直到遇到 return 或者大括号结束为止
- 结构类型定义在函数外部的作用域与全局变量的作用域是相同的
| |
| struct Person{ |
| int age; |
| char *name; |
| }; |
| |
| int main(int argc, const char * argv[]) |
| { |
| |
| |
| struct Person{ |
| int age; |
| }; |
| |
| struct Person pp; |
| pp.age = 50; |
| pp.name = "zbz"; |
| |
| test(); |
| return 0; |
| } |
| |
| void test() { |
| |
| |
| struct Person p = {10,"sb"}; |
| printf("%d,%s\n",p.age,p.name); |
| } |
- 结构体数组和普通数组并无太大差异,只不过是数组中的元素都是结构体而已
- 格式:
struct 结构体类型名称 数组名称[元素个数]
| struct Student { |
| char *name; |
| int age; |
| }; |
| struct Student stu[2]; |
- 结构体数组初始化和普通数组也一样,分为先定义后初始化和定义同时初始化
| struct Student { |
| char *name; |
| int age; |
| }; |
| struct Student stu[2] = {{"lnj", 35},{"zs", 18}}; |
| struct Student { |
| char *name; |
| int age; |
| }; |
| struct Student stu[2]; |
| stu[0] = {"lnj", 35}; |
| stu[1] = {"zs", 18}; |
- 一个指针变量当用来指向一个结构体变量时,称之为结构体指针变量
- 格式:
struct 结构名 *结构指针变量名
- 示例:
| // 定义一个结构体类型 |
| struct Student { |
| char *name; |
| int age; |
| }; |
| |
| // 定义一个结构体变量 |
| struct Student stu = {“lnj", 18}; |
| |
| // 定义一个指向结构体的指针变量 |
| struct Student *p; |
| |
| // 指向结构体变量stu |
| p = &stu; |
| |
| /* |
| 这时候可以用3种方式访问结构体的成员 |
| */ |
| // 方式1:结构体变量名.成员名 |
| printf("name=%s, age = %d \n", stu.name, stu.age); |
| |
| // 方式2:(*指针变量名).成员名 |
| printf("name=%s, age = %d \n", (*p).name, (*p).age); |
| |
| // 方式3:指针变量名->成员名 |
| printf("name=%s, age = %d \n", p->name, p->age); |
| |
| return 0; |
| } |
| |
- 通过结构体指针访问结构体成员,可以通过以下两种方式
- (* 结构指针变量). 成员名
- 结构指针变量 -> 成员名 (用熟)
- (pstu) 两侧的括号不可少,因为成员符 “.” 的优先级高于 “”。
- 如去掉括号写作 pstu.num 则等效于 (pstu.num), 这样,意义就完全不对了。
- 给结构体变量开辟存储空间和给普通开辟存储空间一样,会从内存地址大的位置开始开辟
- 给结构体成员开辟存储空间和给数组元素开辟存储空间一样,会从所占用内存地址小的位置开始开辟
- 结构体变量占用的内存空间永远是所有成员中占用内存最大成员的倍数 (对齐问题)
+ 多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的起始地址的值是 某个数 k 的倍数,这就是所谓的内存对齐,而这个 k 则被称为该数据类型的对齐模数 (alignment modulus)。
- 这种强制的要求一来简化了处理器与内存之间传输系统的设计,二来可以提升读取数据的速度。比如这么一种处理器,它每次读写内存的时候都从某个 8 倍数的地址开始,一次读出或写入 8 个字节的数据,假如软件能 保证 double 类型的数据都从 8 倍数地址开始,那么读或写一个 double 类型数据就只需要一次内存操作。否则,我们就可能需要两次内存操作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的 8 字节 内存块上
| struct Person{ |
| int age; |
| char ch; |
| double score; |
| }; |
| struct Person p; |
| printf("sizeof = %i\n", sizeof(p)); |
- 占用内存最大属性是 score, 占 8 个字节,所以第一次会分配 8 个字节
- 将第一次分配的 8 个字节分配给 age4 个,分配给 ch1 个,还剩下 3 个字节
- 当需要分配给 score 时,发现只剩下 3 个字节,所以会再次开辟 8 个字节存储空间
- 一共开辟了两次 8 个字节空间,所以最终 p 占用 16 个字节
| struct Person{ |
| int age; |
| double score; |
| char ch; |
| }; |
| struct Person p; |
| printf("sizeof = %i\n", sizeof(p)); |
- 占用内存最大属性是 score, 占 8 个字节,所以第一次会分配 8 个字节
- 将第一次分配的 8 个字节分配给 age4 个,还剩下 4 个字节
- 当需要分配给 score 时,发现只剩下 4 个字节,所以会再次开辟 8 个字节存储空间
- 将新分配的 8 个字节分配给 score, 还剩下 0 个字节
- 当需要分配给 ch 时,发现上一次分配的已经没有了,所以会再次开辟 8 个字节存储空间
- 一共开辟了 3 次 8 个字节空间,所以最终 p 占用 24 个字节
| struct Date{ |
| int month; |
| int day; |
| int year; |
| } |
| struct stu{ |
| int num; |
| char *name; |
| char sex; |
| struct Date birthday; |
| Float score; |
| } |
- 注意:
- 结构体不可以嵌套自己变量,可以嵌套指向自己这种类型的指针
| struct Student { |
| int age; |
| struct Student stu; |
| }; |
- 对嵌套结构体成员的访问
- 如果某个成员也是结构体变量,可以连续使用成员运算符 "." 访问最低一级成员
| struct Date { |
| int year; |
| int month; |
| int day; |
| }; |
| |
| struct Student { |
| char *name; |
| struct Date birthday; |
| }; |
| |
| struct Student stu; |
| stu.birthday.year = 1986; |
| stu.birthday.month = 9; |
| stu.birthday.day = 10; |
- 结构体虽然是构造类型,但是结构体之间赋值是值拷贝,而不是地址传递
| struct Person{ |
| char *name; |
| int age; |
| }; |
| struct Person p1 = {"lnj", 35}; |
| struct Person p2; |
| p2 = p1; |
| p2.name = "zs"; |
| printf("p1.name = %s\n", p1.name); |
| printf("p2.name = %s\n", p2.name); |
- 所以结构体变量作为函数形参时也是值传递,在函数内修改形参,不会影响外界实参
| #include |
| |
| struct Person{ |
| char *name; |
| int age; |
| }; |
| |
| void test(struct Person per); |
| |
| int main() |
| { |
| struct Person p1 = {"lnj", 35}; |
| printf("p1.name = %s\n", p1.name); |
| test(p1); |
| printf("p1.name = %s\n", p1.name); |
| return 0; |
| } |
| void test(struct Person per){ |
| per.name = "zs"; |
| } |
| |
- 和结构体不同的是,结构体的每个成员都是占用一块独立的存储空间,而共用体所有的成员都占用同一块存储空间
- 和结构体一样,共用体在使用之前必须先定义共用体类型,再定义共用体变量
- 定义共用体类型格式:
| union 共用体名{ |
| 数据类型 属性名称; |
| 数据类型 属性名称; |
| ... .... |
| }; |
- 特点:由于所有属性共享同一块内存空间,所以只要其中一个属性发生了改变,其它的属性都会受到影响
- 示例:
| union Test{ |
| int age; |
| char ch; |
| }; |
| union Test t; |
| printf("sizeof(p) = %i\n", sizeof(t)); |
| |
| t.age = 33; |
| printf("t.age = %i\n", t.age); |
| t.ch = 'a'; |
| printf("t.ch = %c\n", t.ch); |
| printf("t.age = %i\n", t.age); |
- 共用体的应用场景
- (1)通信中的数据包会用到共用体,因为不知道对方会发送什么样的数据包过来,用共用体的话就简单了,定义几种格式的包,收到包之后就可以根据包的格式取出数据。
- (2)节约内存。如果有 2 个很长的数据结构,但不会同时使用,比如一个表示老师,一个表示学生,要统计老师和学生的情况,用结构体就比较浪费内存,这时就可以考虑用共用体来设计。
+(3)某些应用需要大量的临时变量,这些变量类型不同,而且会随时更换。而你的堆栈空间有限,不能同时分配那么多临时变量。这时可以使用共用体让这些变量共享同一个内存空间,这些临时变量不用长期保存,用完即丢,和寄存器差不多,不用维护。