指针
地址与指针变量
内存地址
- 将内存抽象成一个很大的一维字符数组。
- 编码就是对内存的每一个字节分配一个 32 位或 64 位的编号(与 32 位或者 64 位处理器相关)。
- 这个内存编号我们称之为内存地址。
- 内存中的每一个数据都会分配相应的地址,使用
sizeof(变量名|数据类型)
可得出数据所占地址单元的个数。# include <stdio.h> // 访问变量的两种方式:1. 变量名访问 2. 内存地址访问 int main() { int a = 10; float b = 20; char c = 'a'; printf("int 类型所占字节数:%d ", sizeof(a)); // 4 // 占位符 %p 打印数据的内存地址,unsigned int 十六进制表示 printf("int a 的内存地址:%p ", &a); // 000000000062FE1C printf("float b 的内存地址:%p ", &b); // 000000000062FE18 printf("char c 的内存地址:%p ", &c); // 000000000062FE17 printf("sizeof(p): %d", sizeof(p)); // 8 return 0; }
指针和指针变量
- 内存区的每一个字节都有一个编号,这就是"地址"。
- 如果在程序中定义了一个变量,在对程序进行编译或运行时,系统就会给这个变量分配内存单元,并确定它的内存地址(编号)
- 指针和地址概念不同,指针是一种地址变量,通常也叫指针变量;而地址则是地址变量的值。
- 指针不是类型!真正的类型是地址,指针变量只是存储地址这种数据类型的变量。
- 地址是内存单元的编号,指针变量是存放地址的变量。
- 通常我们叙述时会把指针变量简称为"指针",实际他们含义并不一样。
# include <stdio.h>
// 访问变量的两种方式:1. 变量名访问 2. 内存地址访问
int main() {
int a = 1101;
int* p;
// &:取地址运算符
p = &a;
printf("&a: %p
", &a); // 000000000062FE14
printf(" p: %p
", p); // 000000000062FE14
// *:取值运算符
printf("*p: %d
", *p); // 1101
*p = 13;
printf(" a: %d
", a); // 13
printf("*p: %d
", *p); // 13
printf("sizeof(p): %d", sizeof(p)); // 8
return 0;
}
注意:& 可以取得一个变量在内存中的地址。但是,不能取寄存器变量,因为寄存器变量不在内存里,而在 CPU 里面,所以是没有地址的。
通过指针间接修改主函数中变量的值:
函数形参是指针
首先,C语言之所以把作为形参的数组看作指针,并非因为数组名可以转换为指针,而是因为当初ANSI委员会制定标准的时候,从C程序的执行效率出发,不主张参数传递时复制整个数组,而是传递数组的首地址,由被调函数根据这个首地址处理数组中的内容。那么谁能承担这种“转换”呢?这个主体必须具有地址数据类型,同时应该是一个变量,满足这两个条件的,非指针莫属了。
要注意的是,这种“转换”只是一种逻辑看法上的转换,实际当中并没有发生这个过程,没有任何数组实体被转换为指针实体。另一方面,大家不要被“转换”这个字眼给蒙蔽了,转换并不意味着相同,实际上,正是因为不相同才会有转换,相同的话还转来干吗?这好比现在社会上有不少人“变性”,一个男人可以“转换”为一个女人,那是不是应该认为男人跟女人是相同的?这不是笑话么。
第二,函数参数传递的过程,本质上是一种赋值过程。C89对函数调用是这样规定的:函数调用由一个后缀表达式(称为函数标志符,function designator)后跟由圆括号括起来的赋值表达式列表组成,在调用函数之前,函数的每个实际参数将被复制,所有的实际参数严格地按值传递。因此,形参实际上所期望得到的东西,并不是实参本身,而是实参的值或者实参所代表的值!
举个例来说,对于一个函数声明:void fun(int i);
我们可以用一个整数变量int n作实参来调用fun,就是fun(n);当然,也正如大家所熟悉的那样,可以用一个整数常量例如10来做实参,就是fun(10);那么,按照第二个疑问的看法,由于形参是一个整数变量,而10可以作为实参传递给i,岂不就说明10是一个整数变量吗?这显然是谬误。
实际上,对于形参i来说,用来声明i的类型说明符int,所起的作用是用来说明需要传递给i一个整数,并非要求实参也是一个整数变量,i真正所期望的,只是一个整数,仅此而已,至于实参是什么,跟i没有任何关系,它才不管呢,只要能正确给i传递一个整数就OK了。当形参是指针的时候,所发生的事情跟这个是相同的。
指针形参并没有要求实参也是一个指针,它需要的是一个地址,谁能给予它一个地址?显然指针、地址常量和符号地址常量都能满足这个要求,而数组名作为符号地址常量正是指针形参所需要的地址,这个过程就跟把一个整数赋值给一个整数变量一样简单!
当数组名作为函数参数时,函数的形参会退化为指针。
数组
指针加法:
#include <stdio.h>
int main(void) {
int arr[] = {1,2,3,4,5};
int* pArr = arr;
printf("arr: %d
", sizeof(arr)); // 20
printf("pArr: %d
", sizeof(pArr)); // 8
printf("&arr: %p
", &arr); // &arr: 000000000062FE00
// 首元素地址,同时与整个数组地址重合
printf("arr: %p
", arr); // arr: 000000000062FE00
printf("arr[0]: %p
", arr+0); // 000000000062FE00
printf("arr[1]: %p
", arr+1); // 000000000062FE04
printf("arr[2]: %p
", arr+2); // 000000000062FE08
printf("arr[3]: %p
", arr+3); // 000000000062FE0C
printf("arr[4]: %p
", arr+4); // 000000000062FE10
// 区分 &arr+1 和 arr+1
printf("&arr+1: %p
", &arr + 1); // 000000000062FE14
return 0;
}
指针减法:
#include <stdio.h>
int main(void) {
int step;
char arr[] = "HelloWorld!";
char* p = &arr[4];
step = p - arr;
printf("%d
", step); // 4
return 0;
}
指针数组
int main(void) {
int a = 1;
int b = 2;
int c = 3;
// 指针类型的数组
int* arr[] = {&a, &b, &c};
printf("a = %d
", *arr[0]);
printf("b = %d
", **(arr+1));
return 0;
}
多级指针
#include <stdio.h>
int main(void) {
int a = 10;
int b = 20;
// 使用一级指针接收变量地址
int* p = &a;
int* q = &b;
// *p=123; // 间接改变变量的值
// 使用二级指着接收一级指针的地址
int** p1 = &p;
*p1 = q; // 间接改变一级指针的值
**p1 = 123; // 二级指针间接改变变量的值
// 三级指针
// int*** p2 = &p1;
// 四级指针
// int**** p3 = &p2;
return 0;
}
结构体
typedef
typedef 为 C 语言的关键字,作用是为一种数据类型(基本类型或自定义数据类型) 定义一个新名字,不能创建新类型。
与 #define
不同,typedef 仅限于数据类型,而不是能是表达式或具体的值;#define
发生在预处理,typedef 发生在编译阶段。
#include <stdio.h>
typedef unsigned int ui;
typedef unsigned long ul;
int main(void)
{
ui num = 1101;
ul id = 32042759;
printf("%d
", num);
printf("%ld
", id);
return 0;
}
struct
概述
数组:描述一组具有相同类型数据的有序集合,用于处理大量相同类型的数据运算。
有时我们需要将不同类型的数据组合成一个有机的整体,如:一个学生有学号/姓名/性别/年龄/地址等属性。显然单独定义以上变量比较繁琐,数据不便于管理。
C 语言中给出了另一种构造数据类型——结构体。
定义
- 定义结构体变量的方式
- 先声明结构体类型再定义变量名
- 在声明类型的同时定义变量
- 直接定义结构体类型变量(无类型名)
- 结构体类型和结构体变量关系
- 结构体类型:指定了一个结构体类型,它相当于一个模型,但其中并无具体数据,系统对之也不分配实际内存单元。
- 结构体变量:系统根据结构体类型(内部成员状况)为之分配空间。
简单使用:
# include <stdio.h>
# include <string.h>
struct student {
int id;
char name[21]; // 一个中文是俩字符
char sex;
int age;
char address[51];
};
int main() {
// 定义结构体变量(数据类型: struct student)
struct student stu1 = {1101, "刘佳琦", 'f', 22, "江苏省徐州市鼓楼区"};
/*
打印结构体变量信息
如果是普通变量,通过点运算符操作结构体成员:stu.age
如果是指针变量,通过->操作结构体成员:pStu->age
*/
printf("id: %d
", stu1.id);
printf("sname: %s
", stu1.name);
printf("sex: %s
", stu1.sex == 'f' ? "女" : "男");
printf("age: %d
", stu1.age);
printf("address: %s
", stu1.address);
// 修改结构体成员信息
stu1.id = 13;
// name 是数组类型,是个常量,不能修改
// stu1.name = "刘源"; <<<<<< ERROR
// 字符串拷贝(目标地址, 要拷贝的字符串)
strcpy(stu1.name, "刘源");
printf("sid: %d
", stu1.id);
printf("sname: %s
", stu1.name);
return 0;
}
结构体数组
# include <stdio.h>
/*
为结构体其别名1
typedef struct student STU;
struct student {
int id;
char name[21];
char sex;
int age;
char address[51];
};
*/
// 为结构体其别名2
typedef struct student {
int id;
char name[21];
char sex;
int age;
char address[51];
} STU;
void bubbleSort(STU* ss, int len) {
int i, j;
STU temp;
for(i = 0; i < len-1; i++)
for(j = 0; j < len-i-1; j++)
if(ss[j].age > ss[j+1].age) {
// 相同类型的两个结构体变量,可以相互赋值
// 把 ss[j] 成员变量的值拷贝到 temp 成员变量的内存中
// ss[j] 和 temp 只是成员变量的值一样而已,它们还是没有关系的两个变量
temp = ss[j];
ss[j] = ss[j+1];
ss[j+1] = temp;
}
}
int main() {
int i;
struct student stu1 = {1, "abc1", 'm', 22, "address1"};
STU stu2 = {2, "abc2", 'm', 14, "address2"};
STU stu3 = {3, "abc3", 'f', 16, "address3"};
STU stu4 = {4, "abc4", 'f', 18, "address4"};
STU ss[4] = {stu1, stu2, stu3, stu4};
bubbleSort(ss, 4);
for(i=0; i<4; i++) {
printf("编号:%d ",ss[i].id);
printf("姓名:%s ",ss[i].name);
printf("性别:%s ",ss[i].sex=='M'?"男":"女");
printf("年龄:%d ",ss[i].age);
printf("地址:%s
",ss[i].address);
}
return 0;
}
结构体嵌套结构体
# include <stdio.h>
# include <string.h>
typedef struct {
char schoolName[51];
int level;
} School;
typedef struct student {
int id;
char name[21];
char sex;
int age;
char address[51];
School school;
} STU;
int main() {
int i;
School school = {"NUIST", 1};
struct student stu1 = {1, "abc1", 'm', 22, "address1", school};
STU stu2 = {2, "abc2", 'm', 14, "address2", school};
STU stu3 = {3, "abc3", 'f', 16, "address3", school};
STU stu4 = {4, "abc4", 'f', 18, "address4", school};
STU ss[4] = {stu1, stu2, stu3, stu4};
strcpy(ss[1].school.schoolName, "JMI");
printf("%s
", ss[1].school.schoolName);
return 0;
}
共用体(联合体)
- 联合 union 是一个能在同一个存储空间存储不同类型数据的类型;
- 联合体所占的内存长度等于其最长成员的长度倍数,也有叫做共用体;
- 同一内存段可以用来存放几种不同类型的成员,但每一瞬时只有一种起作用;
- 共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员的值会被覆盖;
- 共用体变量的地址和它的各成员的地址都是同一地址。
# include <stdio.h>
union test1 {
short s;
int i;
float f;
double d; // 8
char c;
};
union test2 {
short s; // 2
char c;
};
int main(void) {
union test1 var1;
union test2 var2;
printf("%d
", sizeof(var1)); // 8
printf("%d
", sizeof(var2)); // 2
var1.i = 1101;
printf("var1.int: %d
", var1.i); // 1101
var1.c = 'a';
printf("var1.int: %d
", var1.i); // 反正不是1101
printf("var1.char: %c
", var1.c); // a
return 0;
}
结构体构建链表
没写完 ...
# include<stdio.h>
# include<malloc.h>
# include<stdlib.h>
typedef struct Node{
int data;
struct Node* pNext;
}* PNode;
PNode initList();
void freeList(PNode);
void addLast(PNode, int);
void printList(PNode);
int searchNode(PNode, int); // 第n个结点的数据
void insertNode(PNode, int, int); // 在第n个结点后插入结点
// 单链表反转
// 链表中环的检测
// 两个有序链表合并
// 删除链表倒数第n个结点
// 求链表中间结点
int main(void) {
int i;
// Test-初始化链表
PNode head = initList();
// Test-链表尾部添加结点
for(i = 1; i <= 5; i++) addLast(head, i);
printList(head);
// Test-查找第n个结点的data
for(i = 1; i <= 5; i++) printf("%2d ", searchNode(head, i));
// Test-在第n个结点后插入结点
insertNode(head, 10, 1101);
printList(head);
// Test-释放链表存储空间
free(head);
return 0;
}
int searchNode(PNode head, int number) {
PNode cur = head->pNext;
int n = 1;
if(number < 1) return -1;
while(n != number && cur != NULL) {
cur = cur->pNext; // 2 3
n++; // 2 3
}
if(cur == NULL) return -1;
else return cur->data;
}
void insertNode(PNode head, int afterN, int data) {
PNode cur = head;
PNode newNode;
int n = 0;
if(afterN < 0) return;
while(n != afterN && cur != NULL) {
cur = cur->pNext;
n++;
}
if(cur == NULL) return;
newNode = (PNode) (malloc(sizeof(struct Node)));
newNode->data = data;
newNode->pNext = cur->pNext;
cur->pNext = newNode;
}
void printList(PNode head) {
printf("
");
PNode cur = head->pNext;
while(cur != NULL) {
printf("%2d ", cur->data);
cur = cur->pNext;
}
printf("
");
}
void addLast(PNode head, int data) {
PNode cur = head;
PNode p = (PNode) (malloc(sizeof(struct Node)));
p->data = data;
p->pNext = NULL;
while(cur->pNext != NULL) cur = cur->pNext;
cur->pNext = p;
}
void freeList(PNode head) {
PNode cur = head;
PNode temp;
while(cur != NULL) {
temp = cur;
cur = cur->pNext;
free(temp);
}
}
PNode initList() {
PNode head = (PNode) (malloc(sizeof(struct Node))); // head 指向 [哨兵结点]
head->pNext = NULL;
return head;
}
文件操作
文件类型指针
在 C 语言中用一个指针变量指向一个文件,这个指针称为文件指针。
typedef struct {
short level; //缓冲区"满"或者"空"的程度
unsigned flags; //文件状态标志
char fd; //文件描述符
unsigned char hold; //如无缓冲区不读取字符
short bsize; //缓冲区的大小
unsigned char *buffer;//数据缓冲区的位置
unsigned ar; //指针,当前的指向
unsigned istemp; //临时文件,指示器
short token; //用于有效性的检查
} FILE;
FILE 是系统使用 typedef 定义出来的有关文件信息的一种结构体类型,结构中含有文件名、文件状态和文件当前位置等信息。
声明 FILE 结构体类型的信息包含在头文件 "stdio.h" 中,一般设置一个指向 FILE 类型变量的指针变量,然后通过它来引用这些 FILE 类型变量。通过文件指针就可对它所指的文件进行各种操作。
C 语言中有 3 个特殊的文件指针由系统默认打开,用户无需定义即可直接使用:
- stdin:标准输入,默认为当前终端(键盘),我们使用的 scanf、getchar 函数默认从此终端获得数据。
- stdout:标准输出,默认为当前终端(屏幕),我们使用的 printf、puts 函数默认输出信息到此终端。
- stderr:标准出错,默认为当前终端(屏幕),我们使用的 perror 函数默认输出信息到此终端。
文件的打开与关闭
文件的打开
任何文件使用之前必须打开:
第 1 个参数的几种形式:
FILE *fp_passwd = NULL;
// ------------ 相对路径 ------------
// 打开当前目录 passdwd.txt 文件:源文件(源程序)所在目录
FILE *fp_passwd = fopen("passwd.txt", "r");
// 打开当前目录(test)下 passwd.txt 文件
fp_passwd = fopen(". / test / passwd.txt", "r");
// 打开当前目录上一级目录(相对当前目录) passwd.txt 文件
fp_passwd = fopen(".. / passwd.txt", "r");
// ------------ 绝对路径 ------------
// 打开 C 盘 test 目录下一个叫 passwd.txt 文件
fp_passwd = fopen("c:/test/passwd.txt","r");
第 2 个参数的几种形式(打开文件的方式):
- b 是二进制模式的意思,b 只是在 Windows 有效,在 Linux 用 r 和 rb 的结果是一样的
- Unix 和 Linux 下所有的文本文件行都是
- 在 Windows 平台下,以 "文本" 方式打开文件,不加 b:
- 当读取文件的时候,系统会将所有的
- 当写入文件的时候,系统会将
- 以 "二进制" 方式打开文件,则读写
- 当读取文件的时候,系统会将所有的
- 在 Unix/Linux 平台下,"文本" 与 "二进制" 模式没有区别,
文件的关闭
任何文件在使用后应该关闭:
- 打开的文件会占用内存资源,如果总是打开不关闭,会消耗很多内存。
- 一个进程同时打开的文件数是有限制的,超过最大同时打开文件数,再次调用 fopen 打开文件会失败。
- 如果没有明确的调用 fclose 关闭打开的文件,那么程序在退出的时候,操作系统会统一关闭。
举例:
# include <stdio.h>
# include <stdlib.h>
int main(void) {
FILE* fp;
// 打开文件
fp = fopen("abc.txt", "r");
// 对打开的文件进行判断
if (fp == NULL) {
printf("打开文件失败!");
return -1;
}
printf("文件打开成功:%p
", fp);
// 关闭文件
fclose(fp);
return 0;
}
判断文件结尾
在 C 语言中,EOF 表示文件结束符(end of file)。在 while 循环中以 EOF 作为文件结束标志,这种以 EOF 作为文件结束标志的文件,必须是文本文件。在文本文件中,数据都是以字符的 ASCII 代码值的形式存放。我们知道,ASCII 代码值的范围是 0~127,不可能出现 -1,因此可以用 EOF 作为文件结束标志。
#define EOF (-1)
当把数据以二进制形式存放到文件中时,就会有 -1 值的出现,因此不能采用 EOF 作为二进制文件的结束标志。为解决这一个问题,ANSIC 提供一个 feof 函数,用来判断文件是否结束。feof 函数既可用以判断二进制文件又可用以判断文本文件。
文件的读写
按照字符读写文件
# include <stdio.h>
# include <stdlib.h>
int main(void) {
int len;
char ch;
FILE* fp;
// 打开文件
fp = fopen("abc.txt", "r");
// 对打开的文件进行判断
if (fp == NULL) {
printf("打开文件失败!");
return -1;
}
// 读出文件1
while((ch=fgetc(fp)) != EOF)
printf("%c", ch);
// 读出文件2
while (!feof(fp)) {
ch = fgetc(fp);
printf("%c", ch);
}
// 关闭文件
fclose(fp);
return 0;
}
# include <stdio.h>
# include <stdlib.h>
# include <string.h>
int main(void) {
int i, len;
char ch;
char str[] = "What your mean?";
len = strlen(str);
FILE* fp;
// 打开文件
fp = fopen("abc.txt", "w");
// 对打开的文件进行判断
if (fp == NULL) {
printf("打开文件失败!");
return -1;
}
// 写入文件
for(i=0; i < len; i++) {
ch = fputc(str[i], fp);
printf("%c", ch);
}
// 关闭文件
fclose(fp);
return 0;
}