Loading

[Re] 指针&结构体&文件

指针

地址与指针变量

内存地址

  • 将内存抽象成一个很大的一维字符数组。
  • 编码就是对内存的每一个字节分配一个 32 位或 64 位的编号(与 32 位或者 64 位处理器相关)。
  • 这个内存编号我们称之为内存地址。
  • 内存中的每一个数据都会分配相应的地址,使用 sizeof(变量名|数据类型) 可得出数据所占地址单元的个数。
    # include <stdio.h>
    // 访问变量的两种方式:1. 变量名访问 2. 内存地址访问
    int main() {
        int a = 10;
        float b = 20;
        char c = 'a';
        printf("int 类型所占字节数:%d\n", sizeof(a)); // 4
        // 占位符 %p 打印数据的内存地址,unsigned int 十六进制表示
        printf("int a 的内存地址:%p\n", &a); // 000000000062FE1C
        printf("float b 的内存地址:%p\n", &b); // 000000000062FE18
        printf("char c 的内存地址:%p\n", &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\n", &a); // 000000000062FE14
    printf(" p: %p\n", p); // 000000000062FE14
    // *:取值运算符
    printf("*p: %d\n", *p); // 1101
    *p = 13;
    printf(" a: %d\n", a); // 13
    printf("*p: %d\n", *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\n", sizeof(arr)); // 20
    printf("pArr: %d\n", sizeof(pArr)); // 8
    printf("&arr: %p\n", &arr); // &arr: 000000000062FE00
    // 首元素地址,同时与整个数组地址重合
    printf("arr: %p\n", arr); // arr: 000000000062FE00
    printf("arr[0]: %p\n", arr+0); // 000000000062FE00
    printf("arr[1]: %p\n", arr+1); // 000000000062FE04
    printf("arr[2]: %p\n", arr+2); // 000000000062FE08
    printf("arr[3]: %p\n", arr+3); // 000000000062FE0C
    printf("arr[4]: %p\n", arr+4); // 000000000062FE10
    // 区分 &arr+1 和 arr+1
    printf("&arr+1: %p\n", &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\n", step); // 4
    return 0;
}

指针数组

int main(void) {
    int a = 1;
    int b = 2;
    int c = 3;
    // 指针类型的数组
    int* arr[] = {&a, &b, &c};
    printf("a = %d\n", *arr[0]);
    printf("b = %d\n", **(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\n", num);
    printf("%ld\n", 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\n", stu1.id);
    printf("sname: %s\n", stu1.name);
    printf("sex: %s\n", stu1.sex == 'f' ? "女" : "男");
    printf("age: %d\n", stu1.age);
    printf("address: %s\n", stu1.address);

    // 修改结构体成员信息
    stu1.id = 13;
    // name 是数组类型,是个常量,不能修改
    // stu1.name = "刘源"; <<<<<< ERROR
    // 字符串拷贝(目标地址, 要拷贝的字符串)
    strcpy(stu1.name, "刘源");
    printf("sid: %d\n", stu1.id);
    printf("sname: %s\n", 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\t",ss[i].id);
        printf("姓名:%s\t",ss[i].name);
        printf("性别:%s\t",ss[i].sex=='M'?"男":"女");
        printf("年龄:%d\t",ss[i].age);
        printf("地址:%s\n",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\n", 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\n", sizeof(var1)); // 8
    printf("%d\n", sizeof(var2)); // 2

    var1.i = 1101;
    printf("var1.int: %d\n", var1.i); // 1101
    var1.c = 'a';
    printf("var1.int: %d\n", var1.i); // 反正不是1101
    printf("var1.char: %c\n", 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("\n");
    PNode cur = head->pNext;
    while(cur != NULL) {
        printf("%2d ", cur->data);
        cur = cur->pNext;
    }
    printf("\n");
}

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 下所有的文本文件行都是 \n 结尾,而 Windows 所有的文本文件行都是 \r\n 结尾
  • 在 Windows 平台下,以 "文本" 方式打开文件,不加 b:
    • 当读取文件的时候,系统会将所有的 \r\n 转换成 \n
    • 当写入文件的时候,系统会将 \n转换成 \r\n 写入
    • 以 "二进制" 方式打开文件,则读写 \n 都不会进行这样的转换
  • 在 Unix/Linux 平台下,"文本" 与 "二进制" 模式没有区别,\r\n 作为两个字符原样输入输出

文件的关闭

任何文件在使用后应该关闭:

  • 打开的文件会占用内存资源,如果总是打开不关闭,会消耗很多内存。
  • 一个进程同时打开的文件数是有限制的,超过最大同时打开文件数,再次调用 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\n", 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;
}

按照行读写文件

文件的定位

posted @ 2020-09-24 15:13  tree6x7  阅读(116)  评论(0编辑  收藏  举报