C语言基础(指针,结构体,文件)

指针

C语言中指针是一种数据类型,指针是存放数据的内存单元地址。

指针变量除了可以存放变量的地址外,还可以存放其他数据的地址,例如可以存放数组和函数的地址。

指针的定义和初始化

格式:数据类型符 *指针变量名[=初始地址值]。

功能:定义指向“数据类型符”的变量或数组的指针变量,同时为其赋初值。

定义说明:

1. “*” 表示定义的是一个指针变量,指针变量的前面必须有 “*” 号;

2. 在定义指针变量的同时也可以定义普通变量或数组等其它变量;

3. “数据类型符” 是指针变量所指向变量的数据类型,可以是任何基本数据类型,也可以是其他数据类型;

4. “初始地址值” 通常是 “&变量名” “&数组元素” 或 “一维数组名”,这里的变量或数组必须是已定义的;

5. 在定义指针变量时,可以只给部分指针变量赋初值;

6. 指针变量的初始化,除了可以是已定义变量的地址,也可以是已初始化的同类型的指针变量,也可以是NULL(空指针);

7. 指针变量初始化时,指针变量的“数据类型符”必须与其“初始地址值”中保存的数据的类型相同;

8. 不能用auto类型的地址去初始化static型指针。

指针变量的使用

1. 给指针变量赋值

格式:指针变量=地址型表达式

“地址型表达式”即运算结果是地址型的表达式。C语言规定,变量地址只能通过取地址运算符获得,即“&”,其运算对象是变量或数组元素名,运算结果是对应变量或数组元素的地址。

需要注意的是,虽然地址是一个整数,但是C语言中不允许把整数看成“地址常量”,所以此处的“地址型表达式”不能是整数。

2. 使用指针变量

格式:指针变量名

需要使用地址时,可以直接引用指针变量名。

3. 通过指针变量引用所指向的变量

格式:*指针变量名

“*指针变量名” 代表其指向的变量或数组元素,其中的“*”称为指针运算符。需要注意的是,这种引用方式要求指针变量必须已经定义且有值。

指针的基本运算

1. 取地址运算符 &

取地址运算符“&”的功能是取变量的地址,它是单目运算符。取地址运算符的运算对象必须是已经定义的变量或数组元素,但不能是数组名。运算结果是运算对象的地址。

2. 指针运算符 *

指针运算符“*”的功能是取指针变量所指向地址中的内容,与取地址运算符“&”的运算是互逆的,它是单目运算符。指针运算符的运算对象必须是地址,可以是已赋值的指针变量,也可以是变量或数组元素的地址,但不能是整数,也不能是非地址型的变量。运算结果就是地址对应的变量。

取地址运算符和指针运算符的优先级和结合性:

1. 取地址运算符、指针运算符和自增、自减等单目运算符的优先级相同;

2. 所有单目运算符的结合性为从右至左。

指针的算术运算

指针变量可以进行的算术运算包括:

1. 指针变量 ± 整数;

2. 指针变量++ 与 ++指针变量;

3. 指针变量-- 与 --指针变量;

4. 指针变量1- 指针变量2;

由于指针运算符*与自增运算符++、自减运算符--的优先级相同,结合方向都是从右至左,因此需要注意以下各种形式的含义不同。

指针的关系运算

两个类型相同的指针变量可以运用关系运算符比较大小,表示两个指针变量所指向地址位置的前后关系,即:前者为小,后者为大。

需要注意的是,如果两个指针变量不是指向同一个数组,则比较大小没有实际意义。

指针变量的引用

1. 通过指针变量访问整型变量

2. 指针变量作为函数参数

普通变量作为函数的参数传递时是按值传递,实参与形参不共享内存。指针变量作为函数参数时是地址传递,共享内存,“双向传递”。

以下是按值传递的情况:

以下是按地址传递的情况:

指针和数组

数组的指针是指向数组在内存的起始地址,数组元素的指针是指向数组元素在内存的起始地址。

1. 当指针变量指向一维数组,可以采用以下两种方法:

(1). 在数据定义语句中用赋初值的方式:*指针变量=数组名;

(2). 在程序中用赋值的方式:指针变量=数组名;

2. 当指针变量指向一维数组元素,可以采用以下两种方法:

(1). 在数据定义语句中用赋初值的方式: *指针变量=&数组名[下标];

(2). 在程序中用赋值的方式: 指针变量=&数组名[下标];

3. 当指针变量指向一维数组,利用指针变量引用一维数组元素的方法如下:

(1). 引用下标为0的数组元素:*(指针变量+0) 或 *指针变量 或 指针变量[0];

(2). 引用下标为 i 的数组元素:*(指针变量+i) 等同于 指针变量[i] 或者 指针变量[i] 等同于 数组名[i];

4. 当指针变量指向一维数组元素,利用指针变量引用数组元素的方法如下:

(1). 引用下标为 i 的数组元素:*(指针变量 + 0) 或 *指针变量;

(2). 引用下标为 i-k 的数组元素:*(指针变量 - k);

(3). 引用下标为 i+k 的数组元素:*(指针变量 + k);

指针和字符串

将指针变量指向字符串的方法如下:

1. 在数据定义语句中用赋初值的方式:*指针变量=字符串;

2. 在程序中用赋值的方式:指针变量=字符串;

需要注意的是,这两种方法并不是将字符串赋予指针变量,而是将存放字符串的连续内存单元的首地址赋予指针变量。

当指针变量指向字符串时,则可以利用指针变量处理字符串,处理方式有以下几种:

1. 处理整个字符串

(1). 输出整个字符串:printf("%s",指针变量);

(2). 输入整个字符串:scanf("%s",指针变量);

#include "stdio.h"
int main()
{

  char *string = "I love China";
  printf("%s",string);// I love China
  return 0;
}

2. 处理字符串中的单个字符

(1). 输出整个字符串:printf("%c",指针变量);

(2). 输入整个字符串:scanf("%c",指针变量);

#include "stdio.h"
int main()
{
    char *string = "I love China";
    for(;*string!='\0';string++)
    {
        printf("%c",*string);// I love China
    };
    return 0;
}

C语言中,字符串是按字符数组进行处理的,系统存储一个字符串时先分配一个起始地址,从该地址开始连续存放字符串中的字符,这一起始地址即字符串首字符的地址。所以,可以将一个字符串赋值给一个字符数组,也可以赋值给一个字符指针变量。

常见的字符串的表现形式如下:

1. 用字符数组表示字符串

2. 用字符指针表示字符串

3. 用下标存取字符串中的字符

字符指针和字符数组的区别:

1. 存储内容不同;

2. 赋值方式不同;

3. 字符指针变量在定义后应先赋值才能引用;

4. 指针变量的值是可以改变的,字符指针变量也不例外;而数组名代表数组的首地址,是一个常量,而常量是不能改变的。

指针和函数

指针变量既可以作为函数的形参,也可以作为函数的实参。指针变量作为函数参数,形参和实参之间的数据传递方式本质上是值传递,只是在调用函数时传递的内容是地址,这样使得形参变量和实参变量指向同一个变量。若被调函数中有对形参所指变量内存的改变,实际上是改变了实参所指变量的内容。

数组名作为函数形参时,接收实参数组的首地址;数组名作为函数实参时,将数组的首地址传递给形参数组。

引入指向数组的指针变量后,数组及指向数组的指针变量作为函数参数时,有四种等价形式:

1. 形参、实参均为数组名;

 2. 形参、实参均为指针变量;

3. 形参为指针变量、实参为数组名;

 4. 形参为数组名、实参为指针变量

C语言中,函数可以返回整型、实型、字符型数据,也可以返回指针类型数据,即返回一个地址。指针型函数是指函数的返回值是指针型,即这类函数的返回值必须是地址值,调用该类函数时,接收返回值的必须是指针变量、指针数组元素等能够存放地址值的对象。

定义指针型函数的格式和有返回值的函数的定义格式基本相同,唯一的区别是在函数名前面加一个“*”,表示函数的返回值是指针型数据。

指针型函数的调用和一般函数的调用方法完全相同,但需要注意的是只能使用指针变量或指针数组元素接收指针型函数的返回值,不能使用数组名接收指针型函数的返回值,因为函数名是地址常量,不是地址型变量,不能接收地址型变量数据。

指针数组

指针数组是数组中的元素均为指针变量。

格式:数据类型符 *指针数组名[数组长度]。

功能:定义指针数组,有“长度”个数组元素。

定义说明:

1. 指针数组名是标识符,前面必须加“*”号;

2. 定义指针数组的同时可以定义普通变量、数组和指针变量等;

3. “数据类型符” 可以是任何基本数据类型,“数据类型符”不是指针数组元素中存放的数据类型,而是其所指向数据的数据类型。

指针数组的初始化

1. char *ps[]={"China","America","Russia",NULL};

定义了一个用于指向字符型数据的指针数组ps,其长度为4,同时对指针数组元素赋初值,前面三个是字符型指针,最后一个是空指针。

2. int a[3][3]={1,2,3,4,5,6,7,8,9}; int *p[3]={a[0],a[1],a[2]};

利用二维数组元素初始化指针数组p,其长度为3,分别指向二维数组a中的三个一维数组的首地址。

指针数组元素的赋值

1. 将数组名赋予指针数组各元素

char s[4][20]={“China”,”America”,”Russia”,NULL}; char *p[4]; p[0]=s[0];

给指针数组元素p[0]赋值s[0],s[0]是字符串“China”的首地址。

2. 将字符串直接赋予指针数组元素

char *p[4]; p[0]=“China”;

直接给指针数组元素p[0]赋值为字符串“China”的首地址。

指针元素的使用

指针数元素的使用和指针变量的使用完全相同,可以对其赋予地址值,可以利用其引用所指向的变量或数组元素,也可以参与运算。

 指针元素应用实例

1. 指针在整形二维数组中的使用

 

2. 定义三个国家的名称,将国家名称按字母顺序排序后输出。

 结构体、共用体

 结构体关键字:struct

 定义student结构体

struct student

{

    long int id;

    int age;

    char name[8];

};

声明结构体变量

1.普通方式    (注意声明变量时不能省略struct关键字,C++可以省略)

struct student stu1;    //定义了一个student类型的变量stu1

2.在定义的时候声明

struct student

{

    long int id;

    int age;

    char name[8];

}stu1;      //在结构体定义时,同时声明一个这种结构体变量stu1

结构体的初始化

  1. 先声明结构体变量,后初始化。

struct student stu1;

stu1.id = 12345;            //通过成员运算符'.'来访问结构体的成员变量

stu1.age = 20;

strcpy(stu1.age,"Liang");     //因为数组在非初始化时,不能直接通过数组名直接赋值,strcpy函数需要包含头文件string.h    错误的写法:stu1.name = "Liang";

                            

2.在声明结构体变量时同时初始化,类似于数组初始化。

#include <stdio.h>

#include <string.h>

struct student

{

long int id;

int age;

char name[8];

};

int main(int argc,char* argv[])

{

struct student stu1 = {12345,22,"Liang"};

printf("id=%ld age=%d name=%s \n",stu1.id,stu1.age,stu1.name);

return 0;

}

结构体成员变量的访问

1、可使用成员运算符'.'来访问结构体成员变量。例如stu1.age = 10;int age = stu1.id;

2、可以使用结构体指针结合"->"来访问结构体成员变量。

例如struct student *sp = &stu1;sp->id = 12345;

也可以 (*sp).age = 20;注意成员运算符'.'的优先级高于指针操作符'*'。

结合使用typedef

使用typedef为自定义结构体定义别名,在声明变量时可以省略struct关键字。

typedef struct student

{

long int id;

int age;

char name[8];

}Student,*StuPtr;

也可以这样表示

typedef struct student

{

long int id;

int age;

char name[8];

}

typedef struct student Student;     

typedef struct student* StuPtr;   

//(这两句可合并为一句:typedef struct student Student,*StuPtr;)

共用体

共用体关键字union;        

     共用体也叫联合体,使几个不同类型的变量共占一段内存(相互覆盖),也就是说共用体的成员共用一片内存,后赋值的成员变量的数据才是共用体的生效数据,因为前面的赋值已经被覆盖了。共用体所占内存至少能够容纳最大的成员变量所需的空间,应用场景

 

共用体定义

union uu

{

int i;

double db;

};

声明共用体变量 、初始化、赋值。(后赋值的成员变量会覆盖前面赋值的成员的数据)

      

补充

 1、同类型的结构体之间可以直接赋值

  1. 结构体嵌套

             

        3、匿名结构体

             

   匿名结构体只能再定义结构体的同时声明变量,定义之后无法再声明变量。应用场景:比如限定只有一个超级用户。

      但是有个例外,使用typedef关键字来获取类型,然后可以声明这种变量。

      4、用结构体变量作参数----多值传递,因为函数有着副本机制,形参相当于实参的副本,当数据量很大时,效率较低。(数组最为函数参数时,会退化成为一个指针,效率高)

      5、用结构体成员变量的地址求出结构体的首地址

测试代码:

#include <stdio.h>
#define GET_ENTRY(ptr,type,member)\ (type *)((char*)(ptr)-(unsigned long)(&(((type*)0)->member)));  

struct test

{
    char ch;
    int num;
    double db;
    char arr[22];

};

int main(int argc,char* argv[])

{
    struct test s1 = {'a',22,33.0,"hello world"};
    printf("addr=%p num=%d \n",&s1,s1.num);
    double *dptr = &s1.db;
    struct test *sp = GET_ENTRY(dptr,struct test,db);
    //带参宏GET_ENTRY(dptr,struct test,db)展开--》(struct test *)((char*)(dp)-(unsigned                 long)(&(((struct test*)0)->db)));
    printf("addr=%p num=%d \n",sp,sp->num);
    return 0;
}

测试结果
  

注释:

        

文件

对于文件的写入和读取方式,重点掌握以下几种即可。

文件使用方式 含义                                   如果指定文件不存在

“r”(只读) 为了输入数据,打开一个已经存在的文本文件  出错

“w”(只写) 为了输出数据,打开一个文本文件           建立一个新的文件

“a”(追加) 向文本文件尾添加数据                   建立一个新的文件

“rb”(只读) 为了输入数据,打开一个二进制文件       出错

“wb”(只写) 为了输出数据,打开一个二进制文件       建立一个新的文件

举例:

#include <stdio.h
int main ()
{
  FILE * pFile;
  pFile = fopen ("myfile.txt","w");//以输出的形式(写)打开文件
  //文件操作
  if (pFile!=NULL)
 {
    fputs ("fopen example",pFile);//以字符串的形式写入
    fclose (pFile);
 }
  return 0;

}

文件的顺序读写

文件的输出/写入就是将数据写入到文件当中,而文件的输入/读取就是将文件中的内容读取到内存当中。

对于文件的读写方式的函数

功能 函数名 适用于

字符输入函数 fgetc 所有输入流

字符输出函数 fputc 所有输出流

文本行输入函数 fgets 所有输入流

文本行输出函数 fputs 所有输出流

格式化输入函数 fscanf 所有输入流

格式化输出函数 fprintf 所有输出流

二进制输入 fread 文件

二进制输出 fwrite 文件

文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。

例如,VS2013编译环境提供的 stdio.h 头文件中有以下的文件类型申明:

struct _iobuf {

        char *_ptr;

        int   _cnt;

        char *_base;

        int   _flag;

        int   _file;

        int   _charbuf;

        int   _bufsiz;

        char *_tmpfname;

       };

typedef struct _iobuf FILE;

FILE* pf;//文件指针变量

每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。

一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。

fseek函数

举个例子:

#include <stdio.h>
int main ()
{
  FILE * pFile;
  pFile = fopen ( "example.txt" , "wb" );
  fputs ( "This is an apple." , pFile );
  fseek ( pFile , 9 , SEEK_SET );
  fputs ( " sam" , pFile );
  fclose ( pFile );
  return 0;
}

打印出的结果是This is a sample.呢?原因是在第一次fputs中是把This is an apple.先放入记事本当中,当调用fseek函数时,从当前的文件指针处向后偏移9个字节,文件指针一开始默认指向的是文件的首地址处。因此向后偏移9个字节后(偏移一个字节包括空格)指向的是最后一个空格的地址处。而第二次fputs函数是将“ sam”这个内容在上次文件指针指向的地址处开始写入。

ftell函数

返回文件指针相对于起始位置的偏移量。

#include <stdio.h>
int main ()
{
  FILE * pFile;
  long size;
  pFile = fopen ("myfile.txt","rb");
  if (pFile==NULL) perror ("Error opening file");
  else
 {
    fseek (pFile, 0, SEEK_END);   // non-portable
    size=ftell (pFile);
    fclose (pFile);
    printf ("Size of myfile.txt: %ld bytes.\n",size);
 }
  return 0;
}

因为是从文件内容的最末尾处开始相对于起始位置的偏移量。则结果为17。

代码运行结果为:This is a sample.

rewind函数

让文件指针的位置回到文件的起始位置。

rewind函数的返回值类型为void型,它所需要的参数是文件指针流

例子

#include <stdio.h>
int main ()
{
  int n;
  FILE * pFile;
  char buffer [27];
  pFile = fopen ("myfile.txt","w+");
  for ( n='A' ; n<='Z' ; n++)
    fputc ( n, pFile);
  rewind (pFile);
  fread (buffer,1,26,pFile);
  fclose (pFile);
  buffer[26]='\0';
  puts (buffer);
  return 0;
}

代码运行结果:ABCDEFGHIJKLMNOPQRSTUVWXYZ

并且在程序的文件夹中有此内容的记事本产生:ABCDEFGHIJKLMNOPQRSTUVWXYZ

文本文件和二进制文件

根据数据的组织形式,数据文件被称为文本文件或者二进制文件。

数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。

如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。(如整数10000,需要以ASCII码输出到磁盘上,则在磁盘中的存储形式就是10000)。

如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节。

再用整数10000举例。如果以二进制的形式输出到磁盘上,则在磁盘上是以二进制的形式存储。但是我们到文件底下去看二进制形式的文本时,都是乱码无法看懂(但机器能够看懂)。此时我们再将该文本文件移到编译器(VS2019)中。而编译器内有一个二进制编辑器能够将该乱码翻译为二进制数显示出来。

代码:

#include <stdio.h>
int main()
{
 int a = 10000;
 FILE* pf = fopen("test.txt", "wb");
 fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
 fclose(pf);
 pf = NULL;
 return 0;
}

文件读取结束的判定

文件文本中正确使用feof函数的例子:

#include <stdio.h>
#include <stdlib.h>
int main(void) {
    int c; // 注意:int,非char,要求处理EOF
    FILE* fp = fopen("test.txt", "r");
    if(!fp) {
        perror("File opening failed");
        return EXIT_FAILURE;
   }
 //fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
    while ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环
   {
       putchar(c);
   }
 //判断是什么原因结束的
    if (ferror(fp))
        puts("I/O error when reading");
    else if (feof(fp))
        puts("End of file reached successfully");
    fclose(fp);
}

二进制文件中正确使用feof函数的例子:

#include <stdio.h>
enum { SIZE = 5 };
int main(void) {
    double a[SIZE] = {1.,2.,3.,4.,5.};
    FILE *fp = fopen("test.bin", "wb"); // 必须用二进制模式
    fwrite(a, sizeof *a, SIZE, fp); // 写 double 的数组
    fclose(fp);
    double b[SIZE];
    fp = fopen("test.bin","rb");
    size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 读 double 的数组
    if(ret_code == SIZE) {
        puts("Array read successfully, contents: ");
        for(int n = 0; n < SIZE; ++n) printf("%f ", b[n]);
        putchar('\n');
   }
else { // error handling
       if (feof(fp))
         printf("Error reading test.bin: unexpected end of file\n");
       else if (ferror(fp))
           perror("Error reading test.bin");
     }
     fclose(fp);
}

文件缓冲区

说到文件缓冲区,我们就自然而然想到输入缓冲区,即当一个字符一个字符从键盘上输入时,并不是直接输入到磁盘内,而是先放到输入缓冲区,而当输入缓冲区内的字符放满后,文件缓冲区才向磁盘内输入字符。

文件缓冲区也是一样的道理。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。

测试代码:

#include <stdio.h>
#include <windows.h>
int main()
{
     FILE*pf = fopen("test.txt", "w");
     fputs("abcdef", pf);//先将代码放在输出缓冲区
     printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
     Sleep(10000);
     printf("刷新缓冲区\n");

     fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
     printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");   
     Sleep(10000);

     fclose(pf);//注:fclose在关闭文件的时候,也会刷新缓冲区  
     pf = NULL;
     return 0;
}

在程序第一个到fgets函数处时,立刻去打开test.txt文本文件,我们会发现里面没有内容,而我们用刷新文件缓冲区的fflush函数时再次打开test.txt文本文件时,会发现里面已经有输入的内容。则能够证实的确有文件缓冲区的存在。

因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值