c语言设计(下)--翁恺慕课版本 万字长文

215 阅读43分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第15天,点击查看活动详情

第九周:指针

9.1指针

9.1-1取地址运算

运算符&

  1. scanf(("%d",&i);里的&
  2. 获得变量的地址,它的操作数必须是变量 int i;printf("%x",&i);
  3. 地址的大小是否与int相同取决于编译器 int i;printf("%p",&i);

&不能取的地址

  1. &不能对没有地址的东西取地址,需要有明确的变量
  2. &(a+b)可以
  3. &(a++)不行
  4. &(++a)不行

试试这些&

  1. 变量的地址 被分配在相邻紧挨着的地方
  2. 相邻的变量的地址
  3. &的结果的sizeof
  4. 数组的地址
  5. 数组单元的地址
  6. 相邻的数组单元的地址

9.1-2 指针

scanf

  1. 如果能够将取得的变量的地址传递给一个函数,能否通过这个地址在那个函数内访问这个变量? 可以
  2. scanf("%d",&i);
  3. scanf()的原型应该是怎么样的?我们需要一个参数能保存别的变量的地址,如何表达能够保存地址的变量?

指针

就是保存地址的变量

  1. int i
  2. int* p = &i;
  3. int* p,q;
  4. int *P,q;
  5. 这个*不管是靠近int还是靠近p,最后实际指向的都是p

指针变量

变量的值是内存的地址

  1. 普通变量的值是实际的值
  2. 指针变量的值是具有实际值的变量的地址

作为参数的指针

  1. void f(int *p);
  2. 在被调用的时候得到了某个变量的地址:int i = 0;f(&i);
  3. 在函数里面可以通过这个指针访问外面的这个i

访问那个地址上的变量*

  1. *是一个单目运算符,用来访问指针的值所表示的地址上的变量
  2. 可以做右值也可以做左值
  3. int k = *p; *p = k + 1;

*左值之所以叫左值

  1. 是因为出现在赋值号左边的不是变量,而是值,是表达式计算的结果;
  2. a[0] = 2; *p = 3;
  3. 是特殊的值,所以叫做左值

指针的运算符&*

1.互相反作用

  1. &yptr -> (&yptr) -> *(yptr的地址) ->得到那个地址上的变量 ->yptr
  2. &yptr ->&( yptr) ->&(y) ->得到y的地址,也就是yptr ->yptr

传入地址

为什么 int i;scanf("%d",i);编译没有报错?

但是运行会报错,只是刚好凑巧你传进去的整数大小跟地址大小一样,编译会拿你传进去的整数当做地址去运行,将整数传到了不该传的地方去了,所以运行就一定会出错

指针应用场景一

交换两个变量的值

void swap(int *pa, int *pd)
{
    int t = *pa;
    *pa = *pb;
    *pb = t;
}

指针应用场景二

  1. 函数返回多个值,某些值就只能通过指针返回
  2. 传入的参数实际上是需要保存带回的结果的变量

指针应用场景二b

  1. 函数返回运算的状态,结果通过指针返回
  2. 常见的套路是让函数返回特殊的不属于有效范围内的值来表示出错:-1或0(在文件操作会看到大量例子)
  3. 但是当任何数值都是有效的可能结果时,就得分开返回了
  4. 后续的语言(c++,java)采用了异常机制来解决这个问题

指针最常见的错误

定义了指针变量,还没有指向任何变量,就开始使用指针了

9.1-3指针与数组

传入的数组成了什么?

int isPrime( int x;int knownPrimes[],int numberOfKnownPrimes)
{
    int ret = 1;
    int i;
    for( i = 0; i < numberOfknownPrimes;i++){
        if( x%knownPrimes[i] == 0){
            ret = 0;
            break;
        }
    }
    return ret;
}

函数参数表中的数组实际上是指针

  1. sizeof(a) == sizeof(int*)
  2. 但是可以用数组的运算符[]进行运算

数组参数

以下四种函数原型是等价的

  1. int sum(int*ar,int n);
  2. int sum(int*,int);
  3. int sum(int*ar[],int n);
  4. int sum(int[],int);

数组变量是特殊的指针

数组变量本身表达地址,所以

  1. int a[10];int*P = a; //无需用&取地址
  2. 但是数组的单元表达的是变量,需要用&取地址
  3. a == &a[0]

[]运算符可以对数组做,也可以对指针做:

  1. p[0]<==>a[0]

*运算符可以对指针做,也可以对数组做:

  1. *a = 25;

数组变量是const的指针,所以不能被赋值

  1. int a[] <==> int *const a = ....

9.1-4 指针与const

指针可以是const,值也可以是const

指针是const

表示一旦得到某个变量的地址,不能再指向其他变量

  1. int *const q = &i; //q是const q指向i这个事实不能被改变
  2. *q = 26; //OK
  3. q++;error

所指是const

表示不能通过这个指针去修改那个变量(并不能使得那个变量成为const)

  1. const int*p = &i;
  2. p = 26; //error!( p)是const
  3. i = 26; //ok
  4. p = &j; //ok

判断哪个被const了的标志是const在*的前面还是后面

  1. int i;
  2. const int* p1 = &i;
  3. int const* p2 = &i;
  4. int *const p3 = &i
  5. 2跟3其实是一样的,指针所指向的东西不可被修改
  6. 4则是表示指针不可被修改

转换

  1. 总是可以把一个非const的值转化成const的

void f(const int* x);

int a = 15;

f(&a);//ok

const int b = a;

f(&b);//ok

b = a + 1;//error!

当要传递的参数的类型比地址大的时候,这是常用的手段:既能用比较少的字节数传递值给参数,又能避免函数对外面的变量的修改

const数组

  1. const int a[] = {1,2,3,4,5,6,};
  2. 数组变量已经是const的指针了,这里的const表明数组的每一个单元都是const int
  3. 所以必须通过初始化进行赋值

保护数组值

因为把数组传入函数时传递的是地址,所以那个函数内部可以修改数组的值

为保护数组不被函数破坏,可以设置参数为const

  1. int sum(const int a[],int length);

9.2指针运算

9.2-1 指针是可计算的

1+1=2?

  1. 给一个指针加1表示要让指针指向下一个变量

int a[10];

int *P = a;

*(p+1)——>a[1];

  1. 如果指针不是指向一片连续分配的空间,如数组,则这种运算没有意义

指针计算

这些算术运算可以对指针做:

  1. 给指针加、减一个整数(+,+=,-,-=)
  2. 递增递减(++/--)
  3. 两个指针相减

*p++

  1. 取出p所指的那个数据来,完事之后顺便把p移到下一个位置去
  2. *的优先级虽然高,但是没有++(单目运算符)高
  3. 常用于数组类的连续空间操作
  4. 在某些CPU上,这可以直接被翻译成一条汇编指令

指针比较

  1. <,<=,>,>=,!=都可以对指针做
  2. 比较它们在内存中的地址
  3. 数组中的单元的地址肯定是线性递增的

0地址

  1. 当然你的内存中有0地址,但是0地址通常是个不能随便碰的地址
  2. 所以你的指针不应该具有0值
  3. 因此可以用0地址来表示特殊的事情:
    1. 返回的指针是无效的
    2. 指针没有被真正的初始化(先初始化为0)
  4. NULL是一个预定定义的符号,表示0地址
    1. 有的编译器不愿意你用0地址来表示0地址

指针的类型

  1. 无论指向什么类型,所有的指针的大小都是一样的,因为都是地址
  2. 但是指向不同类型的指针是不能直接相互赋值的
  3. 这是为了避免用错指针

指针的类型转换

  1. void*表示不知道指向什么东西的指针
    1. 计算时与char*相同(但不相通)
  2. 指针也可以转换类型
    1. intp = &i;voidq = (void*)p;
  3. 这并没有改变p所指向的变量的类型,而是让后人用不同的眼光通过p看他所指的变量
    1. 我不在当你时int啦,我认为你就是个void!

用指针来做什么?

  1. 需要传入较大的数据时用作参数
  2. 传入数组后对数组做操作
  3. 函数返回不止一个结果
    1. 需要用函数来修改不止一个变量
  4. 动态申请的内存...

9.2-2动态内存分配

输入数据

  1. 如果输入数据时,先告诉个数,然后再输入,要记录每个数据
  2. int a = (int)malloc(n*sizeof(int));

malloc

#include<stdlib.h>

void*malloc(size_t size);

  1. 向malloc申请的空间是以字节为单位的
  2. 返回的结果是void*,需要类型转换为自己需要的类型(比如int)
    1. (int )malloc(nsizeof(int))

没空间了?

  1. 如果申请失败则返回0,或者叫做NULL
  2. 可以自己测测看自己电脑系统能给多少空间

free()

  1. 把申请得来的空间还给"系统"
  2. 申请过的空间,最终都应该要还的
    1. 出来混的,迟早都是要还的
  3. 只能还申请来的空间的首地址
  4. free(0)?

free()常见问题

  1. 申请了没free->长时间运行内存逐渐下降
    1. 新手:忘了
    2. 不够老的老手:找不动合适的free的时机
  2. free过了再free
  3. 地址变过了,直接去free

9.2-3 函数间传递指针

好的模式

  1. 如果程序中要用到动态分配的内存,并且会在函数之间传递,不要让函数申请内存后返回给调用者
  2. 因为十有八九调用者会忘了free,或找不到合适的时机来free
  3. 好的模式是让调用者自己申请,传地址进函数,函数再返回这个地址出来

在同一个地方malloc和free

除非函数的作用就是分配空间,否则不要再函数中malloc然后传出去用

函数返回指针

  1. 返回指针没问题,关键是谁的地址?
    1. 本地变量(包括参数)?函数离开后这些变量就不存在了,指针所指的是不能用的内存
    2. 传入的指针?没问题
    3. 动态申请的内存?没问题
    4. 全局变量->以后会解释

函数返回数组

  1. 如果一个函数的返回类型是数组,那么它实际返回的也是数组的地址
  2. 如果这个数组是这个函数的本地变量,那么回到调用函数那里,这个数组就不存在了
  3. 所以只能返回(和返回指针是一样的)
    1. 传入的参数:实际就是在调用者那里
    2. 全局变量或者动态分配的内存

第十周:字符串

10.1-1字符数组

  1. char word[] = {'H','e','l','l','o','!'};

这(指1)不是C语言的字符串,因为不能用字符串的方式做计算

  1. char word[] = {'H','e','l','l','o','!','\0'};
word[0]H
word[1]e
word[2]l
word[3]l
word[4]o
word[5]
word[6]\0

字符串

  1. 以0(整数0)结尾的一串字符
    1. 0或者'\0'是一样的,但是和'0'不同
  2. 0标志字符串的结束,但它不是字符串的一部分
    1. 计算字符串长度的时候不包含这个0
  3. 字符串以数组的形式存在,以数组或者指针的形式访问
    1. 更多的是以指针的形式

string.h里有很多处理字符串的函数

  1. C语言的字符串是以字符数组的形态存在的
    1. 不能用运算符对字符串做运算
    2. 通过数组的方式可以遍历字符串
  2. 唯一特殊的地方是字符串字面量可以用来初始化字符数组
  3. 以及标准库提供了一系列字符串函数

10.1-2字符串变量

  1. char *str = "Hello";
  2. char word[] = "hello";
  3. char line[10] = "Hello";结尾编辑器会自动补0,多占据一个位置
  4. "Hello会被编译器变成一个字符数组放在某处,这个数组的长度是6,结尾还有表示结束的0"
  5. 两个相邻的字符串常量会被自动连接起来

字符串常量

char* s = "Hello,world";

  1. s是一个指针,初始化为指向一个字符串常量
    1. 由于这个常量所在的地方,所以实际上s是const char *s,但是由于历史的原因,编译器接受不带const的写法
    2. 但是试图对s所指的字符串做写入会导致严重的后果
    3. 如果有两处相同的地方,指针会同时指向同一处地方,所以指针必须是只读的
  2. 如果需要修改字符串,应该用数组:
    1. char s[] = "Hello,world!";
    2. 这个数组跟指针的区别就是,指针指向某一处地方,而数组则表示就在我这里
    3. 会将放在不可写的"Hello,world!"数组内容拷贝到你的s那里去

当我们需要一个字符串的时候,指针还是数组?

  1. char*str = "Hello";
  2. char word[] = "Hello";
  3. 数组:这个字符串在这里
    1. 作为本地变量空间自动被回收
  4. 指针:这个字符串不知道在哪里
    1. 处理参数
    2. 动态分配空间
    3. 用在只需要只读的,不打算去往里面写入东西的。表达函数的参数。
  5. 如果要构造一个字符串->数组
  6. 如果要处理一个字符串->指针

char*是字符串?

  1. 字符串可以表达为char*的形式
  2. char*不一定是字符串
    1. 本意是指向字符的指针,可能指向的是字符的数组(就像int*一样)
    2. 只有当char*所指向的字符数组有结尾的0,才能说它所指的是字符串

10.1-3字符串输入输出

  1. char*t = "title";
  2. char*s;
  3. s = t;
  4. 并没有产生新的字符串,只是让指针s指向了t所指的字符串,对s的任何操作就是对t做的
  1. char string[8];
  2. scanf("%s",string);
  3. printf("%s\n",string);
  4. scanf读入一个单词(到空格、tab或回车为止),但scanf这样是不安全的,因为不知道要读入内容的长度

安全的输入

  1. char string[8];
  2. scanf("%7s",string);
  3. 在%s中间可以加入数字来让编译器知道需要限制在多少字符范围内(或者说最多允许读入的字符数量),比如%7s,限制在7个字符范围(超出部分就不会读入了)

常见错误

  1. char*string;
  2. scanf("%s",string);
  3. 以为char("%s".string);
  4. 以为char*是字符串类型,定义了一个字符串类型的变量string就可以直接使用了
    1. 由于没有对string初始化为0,所以不一定每一次运行都出错(实际上这是错误的,指针用错了,没有指向一个确定的地方)

空字符串

  1. char buffer[100] = "";
    1. 这是一个空的字符串,buffer[0] == '\0'
  2. char buffer[] = "";
    1. 这个数组的长度只有1!所以后面放不下任何的字符串

10.1-4字符串数组以及程序参数

字符串数组

  1. char **a
    1. a是一个指针,指向另一个指针,那个指针指向一个字符(串)
  2. char a
    1. 一个错误的二维数组,因为没有说明几列,所以会报错
    2. 可以修改成char a[],本质上就相当于a[0]--->char

程序参数

  1. int main(int argc,char const*argv[])
  2. argv[0]是命令本身
    1. 当使用Unix的符号链接时,反应符号链接的名字
#include <stdio.h>int main(int argc, char const *argv[])
{
    int i;
    for( i = 0; i < argc; i++){
        printf("%d:%s\n",i,argv[i]);
    }
    
    return 0;
}

10.2-1单字符输入输出,用putchar和getchar

putchar

  1. int putchar(int c);
  2. 向标准输出写一个符号
  3. 返回写了几个字符,EOF(-1)表示写失败

getchar

  1. int getchar(void);
  2. 从标准输入读入一个字符(跟scanf的区别是scanf可以一次性读入多个字符)
  3. 返回类型是int是为了返回EOF(-1)
    1. window-->Ctrl-Z
    2. Unix-->Ctrl-D(返回EOF)
    3. Ctrl-C会将shell与实际上显示的的通道关闭掉了
#include <stdio.h>int main(int argc,char const *argv[])
{
    int ch;
    
    while( (ch = getchar()) != EOF ){
        putchar(ch);
    }
    printf("EOF\n");
    
    return 0;
}

在我们输入的东西(在键盘上敲出来的东西被称为行编辑的工作)的时候,那些都会被暂时放在shell里(类似缓冲区域),当我们按下回车之后,才会发送到实际上显示的地方上

10.2-(2-6)字符串函数strlen

string.h

  1. strlen
    1. size_t strlen(const char *s);
    2. 返回s的字符串长度(不包括结尾的0)
  2. strcmp
    1. int strcmp(const char *s1,const char *s2);
    2. 比较两个字符串,返回: 0:s1==s2,1:s1>s2,-1:s1<s2
  3. strcpy
    1. charstrcpy(charrestrict dst,const char *restrict src);
    2. 把src的字符串拷贝到dst
      1. restrict表明src跟dst不重合
    1. 返回dst
      1. 为了能链起代码
    1. 复制一个字符串
      1. chardst = (char)malloc(strlen(src)+1); //之所以加一是因为结尾会自带\0,所以需要多一个位置
      2. strcpy(dst,src);
  4. strcat
    1. char*strcat(char *restrict s1,const char *restrict s2);
    2. 把s2拷贝到s1的后面,接成一个长的字符串
    3. 返回s1
    4. s1必须具有足够的空间
  5. strchr
    1. 字符串中找字符
    2. char*strchr(const char *s,int c);表示从左边找过来
    3. charstrrchr(const chars,int c);表示从右边找过来
    4. 返回NULL则表示没有找到
    5. 如何寻找第二个?寻找第二个的方法:

p = strchr(p+1,'l');

printf("%s\n",p);

  1. strstr
    1. 在字符串中寻找单个字符的
      1. char* strstr(const char *s1,const char *s2);
    1. 在寻找的时候忽略大小写
      1. char*strcasestr(const char *s1,const char *s2);
#include <stdio.h>
#include <string.h>size_t mylen(const char* s)
{
    int cnt = 0;
    int idx = 0;
    while(s[idx] != '\0' ){
        idx++;
        cnt++;
    }
    return cnt;
}
​
int main(int argc,char const *argv[])
{
    char line[] = "Hello";
    printf("strlen=%lu\n",mylen(line));
    printf("sizeof=%lu\n",sizeof(line));
    
    return 0;
}
#include <stdio.h>
#include <string.h>int mycmp( const char* s1, const char* s2)
{
    //int idx = 0;
    //while( s1[idx] == s2[idx] && s1[idx]!='\0' ){
//  idx ++;
//}
    while( *s1 == *s2 && *s1 != '\0'){
        s1++;
        s2++;
    }
    return *s1 - *s2;
}
​
int main(int argc, char const *argv[])
{
    char s1[] = "abc";
    char s2[] = "abc";
    printf("%d\n",mycmp(s1,s2));
    printf("%d\n",'a','A');
    
    return 0;
}
#include <stdio.h>
#include <string.h>char* mycpy(char* dst, const char* src)
{
    int idx = 0;
    while(src[idx] != "\0"){
        dst[idx] == src[idx];
        idx++;
    }
    dst[idx] = '\0';
 //   char* ret = dst;   
 //   方法1:while(*src != '\0'){
 //       *dst++ = *src++;
 //   }
    //方法2:while(*dst++ = *src++);   嗯,没了,就一行直接替换掉了方法1,还有比方法1代码还有更长的版本我没有记录
 // *dst = '\0'; 这是指针的写法
    return dst;
}
​
int main(int argc, char const *argv[])
{
    char s1[] = "abc";
    char s2[] = "abc";
    strcpy(s1,s2);
    
    return 0;
}

安全问题

  1. strcpy跟strcat都可能出现安全问题
    1. 如果目的地没有足够的空间?
    2. 建议是尽量不要去使用他
  2. 安全版本
    1. char* strncpy(charrestict dst, const char restrict src,size_t n);
    2. char* strncat(charrestict s1, const char restrict s2,size_t n);
    3. size_t n表示最多能够接受多少个n个字符,多了就直接掐掉
    4. int strncmp(const char *s1,const char *s2,size_t n);
    5. 这个则表示最多能够判断几个字符,超出则不判断
#include <stdio.h>
#include <string.h>int main(int argc, char const *argv)
{
    char s[] = "hello";
    char *p = strchr(s,'l');
    
    printf("%s\n",p);  //结果为llo
    //寻找第二个的方法:
    //p = strchr(p+1,'l');
    //printf("%s\n",p);结果为lo
    
    return 0;
}
​
---------------------------------------------------
   将起选取的内容拷贝到其他地方的方法
    int main(int argc, char const *argv)
{
    char s[] = "hello";
    char *p = strchr(s,'l');
    char *t = (char*)malloc(strlen(p)+1);
    strcpy(t,p);
    printf("%s\n",t);
    free(t); // 申请来的空间记得释放掉哦
    //这是将llo的字符拷贝走了
    return 0;
}
-------------------------------------------------------
      将起选取的内容拷贝到其他地方的方法2版本
    int main(int argc, char const *argv)
{
    char s[] = "hello";
    char *p = strchr(s,'l');
    char c = *p;
    *p = '\0';
    char *t = (char*)malloc(strlen(s)+1);
    strcpy(t,s);
    printf("%s\n",t);
    free(t); // 申请来的空间记得释放掉哦
    //这是将he的字符拷贝走了
    return 0;
}

第十一周:结构类型

11.1-1枚举

常量符号化

  1. 用符号而不是具体的数字来表示程序中的数字
#include<stdio.h>const int red = 0;
const int yellow = 1;
const int green = 2;
​
int main(int argc, char const *argv[])
{
    int color = -1;
    char *colorName = NULL;
    
    printf("请输入你喜欢的颜色的代码:");
    scanf("%d",&color);
    switch( color ){
            case red:colorName = "red";break;
            case yellow:colorName = "yellow";break;
            case green:colorName = "green";break;
            default:colorName = "default";break;
    }
    printf("你喜欢的颜色是%d",colorName);
    
    return 0;
}

枚举

  1. 用枚举而不是定义独立的const int变量
#include<stdio.h>enum COLOR{RED,YELLOW,GREEN}; //枚举int main(int argc, char const *argv[])
{
    int color = -1;
    char *colorName = NULL;
    
    printf("请输入你喜欢的颜色的代码:");
    scanf("%d",&color);
    switch( color ){
            case RED:colorName = "red";break;
            case YELLOW:colorName = "yellow";break;
            case GREEN:colorName = "green";break;
            default:colorName = "default";break;
    }
    printf("你喜欢的颜色是%d",colorName);
    
    return 0;
}
  1. 枚举是一种用户定义的数据类型,它用关键字enum 以如下语法来声明:
    1. enum 枚举类型名字{名字0,.....,名字n};
    2. 枚举类型名字可以省略
  2. 枚举类型名字通常并不真的使用,要用的是在大括号里的名字,因为它们就是常量符号,它们的类型是int,值则依次从0到n。如:
    1. enum colors{red,yellow,green};
    2. 这样就创建了3个常量,red的值是0,yellow的值是1,而green的值是2
    3. 当需要一些可以排列起来的常量值时,定义枚举的意义就是给了这些常量名字
#include<stdio.h>enum color{ red,yellow,green};
​
void f(enum color c);
​
int main(void)
{
    enum color t = red;
    
    scanf("%d",&t);
    f(t);
    
    return 0;
}
​
void f(enum color c)
{
    printf("%d\n",c);
}
  1. 枚举量可以作为值
  2. 枚举类型可以跟上enum作为类型
  3. 但是实际上是以整数来做内部计算和外部输入输出的
#include<stdio.h>enum COLOR{RED,YELLOW,GREEN,NumCOLOR};
​
int main(int argc, char const *argv[])
{
    int color = -1;
    char *ColorNames[NumCOLOR] = {"red","yellow","green",};
    char *colorName = NULL;
    
    printf("请输入你喜欢的颜色的代码:");
    scanf("%d",&color);
    if( color >= 0 && color < NumCOLORS ){
        colorName = ColorNames[color];
    }else{
        colorName = "unknown";
    }
    printf("你喜欢的颜色是%s",colorName);
    
    return 0;
}
  1. 这样需要遍历所有的枚举量或者需要建立一个用枚举量做下标的数组的时候就很方便
  2. 上面的套路:在进行枚举的时候最后面在放上一个数(NumCOLORS),这样就能够表示NumCOLORS前面有几个数了(例如里面有3个数,索引值到0-2,在后面加上一个数,索引值刚好等于实际我们想要表达的数量)

枚举量

  1. 声明变量可以指定值
    1. enum COLOR{RED = 1,YELLOW,GREEN = 5};
#include<stdio.h>enum COLOR {RED = 1; YELLOW,GREEN=5,NumberCOLORS};
​
int main(int argc,char const *argv[])
{
    printf("code for GREEN is %d\n",GREEN);
    
    return 0;
}
//这样输出的话,会从1开始计数,YELLOW则变成1+1,然后到GREEN的5之前都会跳过了

枚举只是int

  1. 即使给枚举类型的变量赋不存在的整数值也没有任何warning或error

枚举

  1. 虽然枚举类型可以当作类型使用,但是实际上很(bu)少(hao)用
  2. 如果有意义上排比的名字,用枚举比const int方便
  3. 枚举比宏(macro)好,因为枚举有int类型,宏没有类型(宏我在后面会解释是啥)

11.2-1结构类型

声明结构类型

#include<stdio.h>int main(int argc,char const *argv[])
{
    struct date{
        int month;
        int day;
        int year;
    };//声明在这里,最后要加上分号哦,这是在函数内部声明的,通常放在函数外面
    
    struct date today;//在这里我们定义了一个变量是today,类型是struct date的
    
    today.month = 07;
    today.day = 31;
    today.year = 2014;
    
    printf("Today's date is %i-%i-%i.\n",today.year,today.month,today.day);
    
    return 0;
}
​
//声明结构类型跟定义结构变量是两件事情哦
  1. 声明在函数内还是函数外?
    1. 和本地变量一样(就是局部变量),在函数内部声明的结构类型只能在函数内部使用
    2. 所以通常在函数外部声明结构类型,这样就可以被多个函数所使用了
struct point{
    int x;
    int y;
};
​
struct point p1,p2;
​
p1和p2都是point
里面有x和y值 //这是第一种声明方式
​
----------------------------------------------
struct{
    int x;
    int y;
}p1,p2;
p1和p2都是一种无名结构,里面有x和y  //这是第二种形式,没有名字(没有声明point)
    //只是定义了两个变量,因为作者并不打算接下来继续在其他地方去调用
    
--------------------------------------------------------
struct point{
    int x;
    int y;
}p1,p2;
p1和p2都是point,里面有x和y的值t  //这是第三种声明方式

结构的初始化

#include<stdio.h>struct date{
    int month;
    int day;
    int year;
};
​
int main(int argc,char const *argv[])
{
    struct date today = {07,31,2014};
    struct date thismonth = {.month = 7,.year = 2014};
    
    printf("Today's date is %i-%i-%i.\n",today.year,today.month,today.day);
    printf("This month is %i-%i-%i.\n",thismonth.year,thismonth.month,thismonth.day);
    //给的值会被填进去,没给的值跟数组一样默认为0
    return 0;
}

结构成员

  1. 结构和数组有点像
  2. 数组用[]运算符和下标访问其成员
    1. a[0] = 10;
  3. 结构用.运算符和其名字访问其成员
    1. today.day
    2. student.firstName
    3. p1.x
    4. p2.y

结构运算

  1. 要访问整个结构,直接用结构变量的名字
  2. 对于整个结构,可以做赋值、取地址,也可以传递给函数参数
  3. p1 = (struct point){5,10}; //相当于p1.x = 5;p1.y = 10;
  4. p1 = p2; //相当于p1.x = p2.x;p1.y = p2.y;
  5. 数组无法做这两种运算!但结构可以

结构指针

  1. 和数组不同,结构变量的名字并不是结构变量的地址,必须使用&运算符
  2. struct date *pDate = &today;
#include<stdio.h>struct date{
    int month;
    int day;
    int year;
};
​
int main(int argc,char const *argv[])
{
    struct date today;
    
    today = (struct date){07,31,2014};
    
    struct date day;
    
    struct date *pDate = &today;  //指向地址
    
    printf("Today's date is %i-%i-%i.\n",today.year,today.month,today.day);
    printf("The day's date is %i-%i-%i.\n",day.year,day.month,day.day);
    
    printf("address of today is %p\n",pDate);
    
    return 0;
}

11.2-2结构与函数

结构作为函数参数

int numberOfDays(struct date d)

  1. 整个结构可以作为参数的值传入函数
  2. 这时候是在函数内新建一个结构变量,并复制调用者的结构的值
  3. 也可以返回一个结构
  4. 跟数组完全不一样
#include<stdio.h>
#include<stdbool.h>
struct date{
    int month;
    int day;
    int year;
};
​
bool isLeap(struct date d);
int numberOfDays(struct date d);
​
int main(int argc,char const *argv[]){
    struct date today,tomorrow;
    //输入今天的日期,月 日 年 
    printf("Enter today's date (mm dd yyyy):"); 
    scanf("%i %i %i",&today.month,&today.day,&today.year);
    
    if( today.day != numberOfDays(today)){
        tomorrow.day = today.day+1;
        tomorrow.month = today.month;
        tomorrow.year = today.year;
    }else if( today.month == 12 ){
        tomorrow.day = 1;
        tomorrow.month = 1;
        tomorrow.year = today.year+1;
    }else{
        tomorrow.day = 1;
        tomorrow.month = today.month+1;
        tomorrow.year = today.year;
    }
    
    printf("Tomorrow's date is %i-%i-%i.\n",
    tomorrow.year,tomorrow.month,tomorrow.day);
    
    return 0;
}
​
int numberOfDays(struct date d){
    int days;
    
    const int daysPerMonth[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
        
        if(d.month == 2 && isLeap(d))
            days = 29;
        else
            days = daysPerMonth[d.month-1];
    
    return days;
}
​
bool isLeap(struct date d){
    bool leap = false;
    
    if((d.year %4 == 0 && d.year %100 != 0) || d.year%400 == 0 )
        leap = true;
    
    return leap;
}

输入结构

  1. 没有直接的方式可以一次scanf——一个结构
  2. 如果我们打算写一个函数来读入结构
  3. 但是读入的结构如何送回来呢?
  4. C语言在函数调用时是传值的
  5. 在函数读入了p的数值之后,没有任何东西回到main,所以y还是{0,0}
    1. 解决方案
    2. 之前的方案,把一个结构传入了函数,然后在函数中操作,但是没有返回回去
      1. 问题在于传入函数的是外面那个结构的克隆体,而不是指针
      2. 传入结构和传入数组是不同的
    1. 在这个输入函数中,完全可以创建一个临时的结构变量,然后把这个结构返回给调用者
#include<stdio.h>struct point {
    int x;
    int y;
};
​
void getStruct(struct point);
void output(struct point);
​
int main(int argc,char const *argv[])
{
    struct point y = {0,0};
    getStruct(y);
    output(y);
}
​
void getStruct(struct point p)
{
    scanf("%d",&p.x);
    scanf("%d",&p.y);
    printf("%d,%d",p.x,p.y);
}
​
void output(struct point p)
{
    printf("%d,%d",p.x,p.y);
}

结构指针作为参数

  1. K&R说过(p.131)

指向结构的指针

用->表示指针所指的结构变量中的成员

struct date{
    int month;
    int day;
    int year;
}myday;
​
struct date *p = &myday;
​
(*p).month = 12;
p->month = 12;
//第九行跟第十行是一样的意思,第十行会更便捷
#include<stdio.h>struct point {
    int x;
    int y;
};
​
struct point* getStruct(struct point*);
void output(struct point);
void print(const struct point *p);
​
int main(int argc,char const *argv[])
{
    struct point y = {0,0};
    getStruct(&y);
    output(y);
    output(*getStruct(&y));
    print(getStruct(&y));
    *getStruct(&y) = (struct point){1,2};
}
​
struct point*getStruct(struct point*p)
{
    scanf("%d",&p->x);
    scanf("%d",&p->y);
    printf("%d,%d",p->x,p->y);
    return p;
}
​
void output(struct point p)
{
    printf("%d,%d",p.x,p.y);
}
​
void print(const struct point *p);
{
    printf("%d,%d",p->x,p->y);
}

11.2-3结构中的结构

结构数组

  1. struct date dates[100]; //这是在初始化数组
  2. struct date dates[] = {{4,5,2005},{2,4,2005}};
#include<stdio.h>
struct time{
    int hour;
    int minutes;
    int seconds;
};
​
struct time timeUpdate(struct time now);
​
int main(void){
    struct time testTimes[5] = {
        {11,59,59},{12,0,0},{1,29,59},{23,59,59},{19,12,27}
    };
    int i;
    
    for(i=0; i<5; ++i){
        printf("Time is %.2i:%.2i:%.2i",
               testTimes[i].hour,testTimes[i].minutes,testTimes[i].seconds);
        
        testTimes[i] = timeUpdate(testTimes[i]);
        
        printf("...one second later it's %.2i: %.2i: %.2i\n",
               testTimes[i].hour,testTimes[i].minutes,testTimes[i].seconds);
    }
    
    return 0;
}
​
struct time timeUpdate(struct time now){
    ++now.seconds;
    if(now.seconds == 60 ){
        now.seconds = 0;
        ++now.minutes;
        
        if(now.minutes == 60 ){
            now.minutes = 0;
            ++now.hour;
            
            if(now.hour == 24 ){
                now.hour = 0;
            }
        }
    }
}
struct dateAndTime{
    struct date sdate;
    struct time stime;
};

嵌套的结构

struct point{
    int x;
    int y;
};
struct rectangle{
    struct point pt1;
    struct point pt2;
};
​
如果有变量 struct rectangle r;
就可以有:
    r.pt1.x、r.ptl.y
    r.pt2.x、r.pt2.y
    
如果有变量定义:
    struct rectangle r,*rp;
    rp = &r;
​
那么下面的四种形式是等价的:(结构中的结构)
    r.pt1.x
    rp->pt1.x
    (r.pt1).x
    (rp->pt1).x
但是没有rp->pt1->x(因为pt1不是指针)

结构中的结构的数组

#include<stdio.h>struct point{
    int x;
    int y;
};
​
struct rectangle{
    struct point p1;
    struct point p2;
};
​
void printRect(struct rectangle r) 
{
printf("<%d,%d> to <%d,%d>\n",r.p1.x,r.p1.y,r.p2.x,r.p2.y);
}
​
int main(int argc,char const *argv[])
{
    int i;
    struct rectangle rects[] = {{{1,2},{3.4}},{{5,6},{7,8}}};//2 rectangles
    for(i = 0;i < 2;i++)printRect(rects[i]);
}

11.3-1类型定义

自定义数据类型(typedef)

  1. C语言提供了一个叫做typedef的功能来声明一个已有的数据类型的新名字
  2. 比如:typedef int Length;
  3. 使得Length成为int类型的别名
  4. 这样,Length这个名字就可以替代int出现在变量定义和参数声明的地方了:
    1. Length a,b,len;
    2. Length numbers[10];

Typedef

  1. 声明新的类型的名字
    1. 新的名字是某种类型的别名
    2. 改善了程序的可读性
typedef long int64_t;   //重载已有的类型名字  新名字的含义更清晰  具有移植性
typedef struct ADate{
    int month;
    int day;
    int year;
}Date; //简化了复杂的名字//在这里Date等价于struct ADate,Date代表了到达struct ADate之前的所有int64_t i = 10000000000;
Date d = {9,1,2005};
typedef int Length;//Length就等价于int类型typedef *char[10]Strings;  //String是10个字符串的数组的类型typedef struct node{
    int data;
    struct node*next;
}aNode;
​
或
​
typedef struct node aNode;//这样用aNode就可以替代struct node

11.3-2联合

联合

  1. 存储
    1. 所有的成员共享一个空间
    2. 同一时间只有一个成员是有效的
    3. union的大小是其最大的成员
  2. 初始化
    1. 对第一个成员做初始化
#include<stdio.h>typedef union{
    int i;
    char ch[sizeof(int)];
}CHI;
​
int main(int argc,char const argv[])
{
    CHI chi;
    int i;
    chi.i = 1234;
    for( i = 0;i < sizeof(int);i++){
        printf("%02hhX",chi.ch[i]);
    }
    printf("\n");
    
    return 0;
}
​
//输出D20400,          1234的十六进制是0x04D2
//低位在前(相当于倒置),小端的方式

第十二周:程序结构

12.1-1全局变量:定义在函数之外的变量,全局的生存期和作用域

全局变量

  1. 定义在函数外面的变量是全局变量
  2. 全局变量具有全局的生存期和作用域
    1. 他们和任何函数无关
    2. 在任何函数内部都可以使用他们
#include<stdio.h>int f(void);
​
int gAll = 12;//全局变量int main(int argc,char const *argv[])
{
    printf("in %s gAll=%d\n",__func__,gAll);
    f();
    printf("agn in %s gAll = %d\n",__func__,gAll);
    return 0;
}
//_func_是一个字符串,表达的是当前函数的名字,也就是main的名字
int f(void)
{
    printf("in %s gAll=%d\n",__func__,gAll);
    gAll += 2;
    printf("agn in %s gAll=%d\n",__func__,gAll);
    return gAll;
}

全局变量初始化

  1. 没有做初始化的全局变量会得到0值
    1. 指针会得到NULL值
  2. 只能用编译时刻已知的值来初始化全局变量
  3. 它们的初始化发生在main函数之前

被隐藏的全局变量

  1. 如果函数内部存在与全局变量同名的变量,则全局变量被隐藏(局部的优先度会更高噢,会覆盖掉外面的变量)

12.1-2静态本地变量:能在函数结束后继续保有原值的本地变量

静态本地变量

  1. 在本地变量定义时加上static修饰符就成为静态本地变量
  2. 当函数离开的时候,静态本地变量会继续存在并保持其值
  3. 静态本地变量的初始化只会在第一次进入这个函数时做,以后进入函数时会保持上次离开时的值
#include<stdio.h>int f(void);
​
int gAll = 12;//全局变量int main(int argc,char const *argv[])
{
    f();
    f();
    f();
    return 0;
}
//_func_是一个字符串,表达的是当前函数的名字,也就是main的名字
int f(void)
{
    static int all = 1;
    printf("in %s gAll=%d\n",_func_,gAll);
    gAll += 2;
    printf("agn in %s gAll=%d\n",_func_,gAll);
    return gAll;
}
  1. 静态本地变量实际上是特殊的全局变量
  2. 它们(静态本地变量和全局变量)位于相同的内存区域
  3. 静态本地变量具有全局的生存期,函数内的局部作用域
  4. static 在这里的意思是局部作用域(本地可访问)

12.1-3后记:返回指针的函数,使用全局变量的贴士

*返回指针的函数

  1. 返回本地变量的地址是危险的
  2. 返回全局变量或静态本地变量的地址是安全的
  3. 返回在函数内malloc的内存是安全的,但是容易造成问题
  4. 最好的做法是返回传入的指针

tips(贴士)

  1. 不要使用全局变量来在函数间传递参数和结果
  2. 尽量避免使用全局变量
    1. 丰田汽车的案子(ks) 这里给出传送门(想知道的可以看看):www.sohu.com/a/133455549…
  3. *使用全局变量和静态本地变量的函数是线程不安全的

12.2-1宏定义

编译预处理指令

  1. #开头的是编译预处理指令
  2. 它们不是c语言的成分,但是C语言程序离不开它们
  3. #define用来定义一个宏 ,其实只是一个原始的文本替换

#define

  1. #define<名字><值>
  2. 注意没有结尾的分号,因为不是c的语句
  3. 名字必须是一个单词,值可以是各种东西
  4. 在C语言的编译器开始编译之前,编译预处理程序(cpp)会把程序中的名字换成值
    1. 完全的文本替换
  5. gcc——save-temps

  1. 如果一个宏的值中有其他宏的名字,也是会被替换的
  2. 如果一个宏的值超过来一行,最后一行之前的行末需要加\
  3. 宏的值后面出现的注释不会被当作宏的值的一部分

没有值的宏

  1. #define_DEBUG
  2. 这里宏是用于条件编译的,后面有其他的编译预处理指令来检查这个宏是否已经被定义过了

预定义的宏

  1. LINE表达代码所在的行号
  2. FILE表达代码所在的文件名
  3. DATE编译时候的日期
  4. TIME编译时候的时间
  5. STDC

12.2-2带参数的宏

像函数的宏

  1. #define cube(x)((x) (x) (x))
  2. 宏可以带参数
#include <stdio.h>#define cube(x)((x)*(x)*(x))int main(int argc,char const *argv[])
{
    printf("%d\n",cube(5));
    //cube(5)会被替换成cube((5)*(5)*(5));
    
    return 0;
}

错误定义的宏

  1. #define RADTODEG(x)(x*57.29578)
  2. #define RADTODEG(x)(x)*57.29578

带参数的宏的原则

  1. 一切都要括号
    1. 整个值要括号
    2. 参数出现的每个地方都要括号
  2. #define RADTODEG(x)((x)*57.29578)

带参数的宏

  1. 可以带多个参数
    1. #define MIN(a,b)((a)>(b)?(b):(a))
  2. 也可以组合(嵌套)使用其他宏
  3. 定义宏的时候后面千万不要加分号
  4. 在大型程序的代码中使用非常普遍
  5. 可以非常复杂,如"产生"函数
    1. 在#和##这两个运算符的帮助下
  6. 存在中西方文化差异
  7. 部分宏会被inline函数替代

宏的缺陷

没有可以去检查宏有没有问题的机制

其他预编译

  1. 条件编译
  2. error
  3. ...

12.3-1多个源代码文件

多个.c文件

  1. main()里的代码太长了适合分成几个函数
  2. 一个源代码文件太长了适合分成几个文件
  3. 两个独立的源代码文件不能编译形成可执行的程序

项目

因为看的是翁恺老师2014年的版本(比后来的课程内容多不少,后来的课程缩水了),所以这部分已经有更好的就进行省略了

编译单元

  1. 一个.c文件是一个编译单元
  2. 编译器每次编译只处理一个编译单元

12.3-2头文件

  1. 把函数原型放到一个头文件(以.h结尾)中,在需要调用这个函数的源代码文件(c文件)中#include这个头文件,就能让编译器在编译的时候知道函数的原型

#include

  1. #include是一个编译预处理指令,和宏一样,在编译之前就处理了
  2. 它把那个文件的全部文本内容原封不动地插入到它所在的地方
  3. 所以也不是一定要在.c文件的最前面#include

“”还是<>

  1. #include有两种形式来指出要插入的文件
    1. “”要求编译器首先在当前目录(.c文件所在的目录)寻找这个文件,如果没有,到编译器指定的目录去找,例如“MAX.h”啥的自己设定的那些
    2. <>让编译器只在指定的目录去找,例如<stdio.h>
  2. 编译器自己知道自己的标准库的头文件在哪里
  3. 环境变量和编译器命令行参数也可以指定寻找头文件的目录

#include的误区

  1. #include不是用来引入库的
  2. stdio.h里只有printf的原型,printf的代码在另外的地方,某个.lib(Windows)或.a(Unix)中
  3. 现在的C语言编译器默认会引入所有的标准库
  4. #include<stdio.h>只是为了让编译器指定printf函数的原型,保证年调用时给出的参数值是正确的类型

头文件

  1. 在使用和定义这个函数的地方都应该%include这个头文件
  2. 一般的做法就是任何.c都有对应的同名的.h,把所有对外公开的函数的原型和全局变量的声明都放进去
  3. 全局变量是可以在多个.c之间共享的

不对外公开的函数

  1. 在函数前面加上static就使得它成为只能在所在的编译单元中被使用的函数
  2. 在全局变量前面加上static就使得它成为只能在所在的编译单元中被使用的全局变量了

12.3-3声明

变量的声明

  1. int i;是变量的定义
  2. extern int i;是变量的声明

声明和定义

  1. 声明是不产生代码的东西
    1. 函数原型
    2. 变量声明
    3. 结构声明
    4. 宏声明
    5. 枚举声明
    6. 类型声明
    7. inline函数
  2. 定义是产生代码的东西

头文件

  1. 只有声明可以被放在头文件中
    1. 是规则不是法律
  2. 否则会造成一个项目中多个编译单元里有重名的实体
    1. *某些编译器允许几个编译单元中存在同名的函数,或者用week修饰符来强调这种存在

重复声明

  1. 同一个编译单元里,同名的结构不能被重复声明
  2. 如果你的头文件里有结构的声明,很难这个头文件不会在一个编译单元里被#include多次所以需要"标准头文件"

标准头文件结构

  1. 运用条件编译和宏,保证这个头文件在一个编译单元中只会被#include一次
  2. #pragma once也能起到相同的作用,但是不是所有的编译器都支持
#ifndef __LIST_HEAD__
#define __LIST_HEAD__#include"node.h"typedef struct _list{
    Node*head;
    Node*tail;
}List;
​
#endif

第13周:文件

13.1-1格式化输入输出格式

%flags.prectype

Flag含义
-左对齐
+在前面放+或-
(space)正数留空
00填充
#include<stdio.h>int main(int argc,char const *argv[])
{
    printf("%9d\n",123);//需要占9个字符的空间,123前还有6个空格
    printf("%-9d\n",123);//一样需要占9个字符,123后面还有6个空格
    printf("%+9d\n",123);//在123前面多上一个+,是可以0时跟左对齐同时进行的
    printf("%09d\n",123);//空格地方变成0,000000123
    
    return 0;
}
width或prec含义
number最小字符数
*下一个参数是字符数
.number小数点后的位数
.*下一个参数是小数点后的位数
#include<stdio.h>int main(int argc,char const *argv[])
{
    printf("%9.2f\n",123.0);//占据9个字符且其中有两位小数
    printf("%*d\n",6,123);//让格式更灵活,6这个数是会替换到*号上的,具体效果就是占据6个字符
    
    return 0;
}
类型修饰含义
hh单个字节
hshort
llong
lllong long
Llong double
#include<stdio.h>int main(int argc,char const *argv[])
{
    printf("%hhd\n",12345);//只能输入单个字符,输出多个报错
    printf("%hhd\n",(char)12345);//但可以强制类型转换,可以得到57的结果
    printf("%9.2f\n",123.0);
    
    return 0;
}
type用于type用于
i或者dintgfloat
uunsigned intGfloat
o八进制a或者A十六进制浮点
x十六进制cchar
X字母大写的十六进制s字符串
f或者Ffloat,6p指针
e或者E指数n输入/写出的个数
#include<stdio.h>int main(int argc,char const *argv[])
{
    int num;
    printf("hhd%n\n",(char)12345,&num);
    printf(num);//以上的意思是,截至到%n为止前面有几位字符,会将多少位字符传递给指针&num的地址
    //然后在下面num就会输出2,因为上面(char)12345的值输出是57,是2位数
    
    return 0;
}

scanf:%[flag]type

flag含义flag含义
*跳过llong,double
数字最大字符数lllong long
hhcharLlong double
hshort
type用于type用于
dints字符串(单词)
i整数,可能为十六进制或八进制可以将读入的八进制和十六进制格式的数值转化为十进制[...]所允许的字符
uunsigned intp指针
o八进制
x十六进制
a,e,f,gfloat
cchar

printf和scanf的返回值

  1. 读入的项目数
  2. 输出的字符数
  3. 在要求严格的程序中,应该判断每次调用scanf或printf的返回值,从而了解程序运行中是否存在问题

13.1-3文件输入输出

文件输入输出

  1. 用>和<做重定向

FILE

  1. FILEfopen(const char**restrict path,const charrestrict mode*);
  2. int fcolose(FILE*stream);
  3. fscanf(FILE,...)
  4. fprintf(FILE*,...)
  5. fprintf(FILE,...)

打开文件的标准代码

FILE*fp = fopen(“file(文件名)”,“r”);
​
if(fp){
    fscanf(fp,...);
    fcolose(fp);
}else{
    ...
}
-----------------------------------------------------------------
    #include<stdio.h>
    
    int main(int argc,char const *argv[])
{
    FILE *fp = fopen("12.in","r");
    if( fp ){
        int num;
        fscanf(fp,"%d",&num);
        printf("%d\n",num);
        fclose(fp);
    }else{
        printf("无法打开文件\n");
    }
    return 0;
}

fopen

r打开只读
r+打开读写,从文件头开始
w打开只写。如果不存在则新建,如果存在则清空
w+打开读写。如果不存在则新建,如果存在则清空
a打开追加。如果不存在则新建,如果存在则从文件尾开始
..x只新建,如果文件已存在则不能打开(防止对文件造成破坏)

13.1-3二进制文件

  1. 其实所有的文件最终都是二进制的
  2. 文件无非是用最简单的方式可以读写的文件
    1. more、tail
    2. cat
    3. vi
  3. 而二进制文件是需要专门的程序来读写的文件
  4. 文本文件的输入输出是格式化,可能经过转码

文本文件 VS 二进制文件

  1. Unix喜欢用文本文件来做数据存储和程序配置
    1. 交互式终端的出现使得人们夏欢用文本和计算机“talk”
    2. Unix的shell提供了一些读写文本的小程序
  2. windows喜欢二进制文件
    1. DOS是草根文化,并不继承和熟悉Unix文化
    2. PC刚开始的时候能力有限,DOS的能力更有限,二进制进行输入输出更接近底层

优劣:

  1. 文本的优势是方便人类读写,而且跨平台
  2. 文本的缺点是程序输入输出要经过格式化,开销大
  3. 二进制的缺点是人类读写困难,而且不跨平台
    1. int的大小不一致,大小端的问题
  4. 二进制的优点是程序读写快

程序为什么要文件

  1. 配置
    1. Unix用文本,Windows用注册表
  2. 数据
    1. 稍微有点量的数据都放数据库了
  3. 媒体
    1. 这个只能是二进制的
  4. 现实是,程序通过第三方库来读写文件,很少直接读写二进制文件了

二进制读写(可跳过,底层)

  1. size_t fread(voidrestrict ptr,size_t size,size_t nitems,FILErestrict stream);
  2. size_t fwrite(const voidrestruct ptr,size_t size,size_t nitems,FILErestrict stream);
  3. 注意FILE指针是最后一个参数
  4. 返回的是成功读写的字节数

为什么nitems

  1. 因为二进制文件的读写一般都是通过对一个结构变量的操作来进行的
  2. 于是nitem就是用于说明这次读写几个结构变量!

在文件中定位

  1. long ftell(FILE*stream);
  2. int fseek(FILE*stream,long offset,int whence);
    1. SEEK_SET:从头开始
    2. SEEK_CUR:从当前位置开始
    3. SEEK_END:从尾开始(倒过来)
#include<stdio.h>
#include"student.h"void read(FILE*fp,int index);
​
int main(int grac,char const grav[])
{
    FILE*fp = fopen("student.data","r");
    if( fp ) {
        fseek(fp,0L,SEEK_END);
        long size = ftell(fp);
        int number = size/sizeof(Student);
        int index = 0;
        printf("有%d个数据,你要看第几个:",number);
        scanf("%d",&index);
        read(fp,index-1);
        fcolose(fp);
    }
    return 0;
}
​
void read(FILE*fp,int index)
{
    fseek(fp,index*sizeof(Student),SEEK_SET);
    Student stu;
    if( fread(&stu,sizeof(Student),1,fp) == 1){
        printf("第%d个学生:",index+1);
        printf("\t姓名:%s\n",stu.name);
        printf("\t性别:");
        switch ( stu.gender ){
            case 0:printf("男\n");break;
            case 1:printf("女\n");break;
            case 1:printf("其他\n");break;
        }
        printf("\t年龄:%d\n",stu.age);
    }
}

可移值性

  1. 这样的二进制文件不具有可移植性
    1. 在int为32位的机器上写成的数据文件无法直接在int为64的机器上正确读出
  2. 解决方法之一就是放弃使用int,而是使用typedef具有明确大小的类型
  3. 更好的方案是用文本

13.2*位运算

13.2-1按位运算

  1. C有这些按位运算的运算符:其实就是把整数的东西当做二进制来进行运算
·&按位的与
·按位的或
·~按位取反
·^按位的异或
·<<左移
·>>右移

按位与&

  1. 其实就是两组二进制的数,对应的数字必须都为1,新形成的数对应的数才会是1,否则就是0
  2. F:1111 E:1110,FE的作用就是使得跟他对应形成新的数最低位为0

按位或|

  1. 也是两组二进制的数,对应的数字必须都为0,新形成的数对应的数才会是0,否则就是1。跟上面那个相反

按位取反~

#include<stdio.h>int main(int argc,char const *argv[])
{
    unsigned char c = 0xAA;
    printf("% c=%hhx\n",c);//aa
    printf("%~c=%hhx\n",(char)~c);//按位取反  55
    printf("%-c=%hhx\n",(char)-c);//补码   56
}

逻辑运算vs按位运算

  1. 对于逻辑运算,它只看到两个值:0和1
  2. 可以认为逻辑运算相当于把所有非0值都变成1,然后做按位运算
    1. 5&4——>4而5&&4——>1&1——>1
    2. 5|4——>5而5||4——>1|1——>1
    3. ~4——>3而!4——>!1——>0

按位异或^

  1. 两组二进制,上下位数值对应相等为0,上下不相等为1
  2. 做两次相同的异或运算数值就翻回去了

移位运算

左移<<

  1. i << j
  2. i中所有的位向左移动j个位置,而右边填入0
  3. 所有小于int的类型,移位以int的方式来做,结果是int
  4. x <<= 1 等价于x*=2
  5. x <<= n 等价于x*=2的n次方
#include<stdio.h>int main(int argc,char const *argv[])
{
    unsigned char c = 0xA5;
    printf("  c=%d\n",c);//165
    printf("c<<=%d\n",c<<2);//660
    return 0;
}

右移>>

  1. i >> j
  2. 所有小于int的类型,移位以int的方式来做,结果是int
  3. 对于unsigned的类型,左边填入0
  4. 对于signed的类型,左边填入原来的最高位(保持符号不变)
  5. x >>= 1 等价于x/=2
  6. x >>= n 等价于x/=2的n次方
#include<stdio.h>int main(int argc,char const *argv[])
{
    int a = 0x80000000;
    unsigned int b = 0x80000000;
    printf("a=%d\n",a);//-2147483648
    printf("b=%u\n",b);//2147483648
    printf("a>>1=%d\n",a>>1);//-1073741824
    printf("b>>1=%u\n",b>>1);//1073741824
    return 0;
}

no zuo no die

  1. 移位的位数不要用负数,这是没有定义的行为
    1. x<<-2 //!!NO!!

13.2-3位运算例子

输出一个数的二进制

#include<stdio.h>int main(int argc,char const *argv[])
{
    int number;
    scanf("%d",&number);
    number = 0x55555555;//输出01010101...,其实就是16个01(32个值)
    unsigned mask = 1u<<31;//int被省略但是其实是有生效的
    for(; mask ; mask >>=1 ){
        printf("%d",number & mask?1:0);
    }
    printf("\n");
    
    return 0;
}

13.2-4位段

  1. 把一个int的若干位组合成一个结构
struct{
    unsigned int leading : 3;//冒号后面的数字表示占几个比特
    
    unsigned int FLAG1:1;
    
    unsigned int FLAG2:1;
    
    int trailing:11;
};
  1. 可以直接用位段的成员名称来访问
    1. 比移位、与、或还方便
  2. 编译器会安排其中的位的排列,不具有可移植性
  3. 当所需的位超过一个int时会采用多个int

第十四周:*链表

14.1-1*可变数组

the Interface

  1. Array array_create(int init_size);创建数组
  2. void array_free(Array*a);回收数组
  3. int array_size(const Array*a);告诉我们数组里面现在有几个单元可以使用
  4. intarray_at(Arraya,int index);访问数组某个单元
  5. void array_inflate(Array*a,int more_size);让数组
#ifndef _ARRAY_H_
#define _ARRAY_H_typedef struct {
    int *array;
    int size;
}Array;
​
Array array_create(int init_size);
void array_free(Array *a);
int array_size(const Array*a);
4. int*array_at(Array*a,int index);
5. void array_inflate(Array*a,int more_size);
​
#endif
#include "array.h"
​
Array array_create(int init_size)
{
    Array a;//首先是数组的创建,需要用到动态内存分配,结合所需要的size,来创建一个数组
    a.size = init_size;
    a.array = (int*)malloc(sizeof(int)*a.size);
    return a;
}
void array_free(Array *a)//再然后就是内存的释放、得到数组的大小
{
    free(a->array);
    a->array = NULL;
    a->size = 0;
}
 int array_size(const Array*a);
 int*array_at(Array*a,int index);
 void array_inflate(Array*a,int more_size);
​
int main(int argc,char const *argv[])
{
    Array a = array_create(100);
    
    return 0;
}

14.1-2可变数组的数据访问

//从上述第16行内容延续,这里需要多加两个标准头文件
#include<stdio.h>
#include<stdlib.h>
//3-6行的代码叫做封装,能够将里面的内容保护起来,这样别人就不知道你里面什么样子的了,保持神秘感哈哈
 int array_size(const Array*a)
 {
     return a->size;
 }
 int*array_at(Array*a,int index)//在然后是进行该数组的访问和修改
     //这里面需要注意的是array_at的返回需要是一个指针,如此便可以做到修改
 {
     return &(a->array[index]);
 }
​
int*array_get(const Array*a,int index);
{
    return a->array[index];
}
​
void array_set(Array *a,int index, int value)
{
    a->array[index] = value;
}
 void array_inflate(Array*a,int more_size);
​
int main(int argc,char const *argv[])
{
    Array a = array_create(100);
    printf("%d\n",array_size(&a));
    //printf("%d\n",a.size);十七行跟十六行一个意思
    *array_at(&a,0) = 10;//将一个值写到数组里面
    printf("%d\n",*array_at(&a,0));
    array_free(&a);
    
    return 0;
}

14.1-3可变数组的自动增长

//从上述24行延续
 void array_inflate(Array*a,int more_size)
 {
     int*p = (int*)malloc(sizeof(int)(a->size + more_size));
     int i;
     for( i = 0; i < a->size; i++ ) {
         p[i] = a->size[i];
     }//可以换成标准库的函数mencpy,效率更高
     free(a->array);
     a->array = p;
     a->size += more_size;
 }//核心代码
​
int main(int argc,char const *argv[])
{
    Array a = array_create(100);
    printf("%d\n",array_size(&a));
    //printf("%d\n",a.size);十七行跟十六行一个意思
    *array_at(&a,0) = 10;//将一个值写到数组里面
    printf("%d\n",*array_at(&a,0));
    int number;
    int cnt = 0;
    while(number != -1 ){
        scanf("%d",&number);
        if( number != -1 ){
        *array_at(&a,cnt++) = number;
        //scanf("%d",array_at(&a,cnt++));
    }//无限的读入整数,让自己不断的自己增长
    array_free(&a);
    
    return 0;
}
//第九行的内容改动 
​
int*array_at(Array*a,int index)//在然后是进行该数组的访问和修改
     //这里面需要注意的是array_at的返回需要是一个指针,如此便可以做到修改
 {
    if( index >= a->size ){
        array_inflate(a,(index/BLOCK_SIZE+1)-a->size+1);//这里有点乱,后续要来修正
    }
     return &(a->array[index]);
 }

14.2-1可变数组的缺陷

  1. 申请使用内存,当我们需要更大的内存而之前的不需要了之后,之前的就会被废弃掉,在内存受限的情况下(比如单片机)就会导致内存明明还有,但是却已经申请不了更大的内存了(浪费内存空间)
  2. 效率极低

14.2-2链表

  1. 这是为百度进来补充的图,翁恺的课程是没有静态的图,用动态进行演示的

img

#include"node.h"
#include<stdio.h>
#include<stdlib.h>//typedef struct _node{
//    int value;
//    struct _node *next;
//}Node;int main(int argc,char const argv[])
{
    Node*head = NULL;
    int number;
    do{
        scanf("%d",&number);
        if( number != -1 ) {
            //add to linked-list
            Node*p = (Node*)malloc(size(Node));
            p->next = NULL;
            //find the last
            Node*last = head;
            if( last ){
                while (last ->next){
                    last = last->next;
                    }
                //attach
                last->next = p;
                }else{
                    head = p;
            }
        }
    }while( number != -1 );
    
    return 0;
}

14.2-3链表的函数

#include"node.h"
#include<stdio.h>
#include<stdlib.h>
​
//typedef struct _node{
//    int value;
//    struct _node *next;
//}Node;
void add(Node*head,int number);
​
typedef struct _list{
    Node*head;
    Node*tail;
}List;
​
int main(int argc,char const argv[])
{
    Node*head = NULL;
    List list;
    int number;
    list.head = list.tail = NULL;
    do{
        scanf("%d",&number);
        if( number != -1 ){
            add(&list,number);
        }
    }while( number != -1);
    
    return 0;
}
​
void add(List*pList,int number)
{
       //add to linked-list
    Node*p = (Node*)malloc(size(Node));
    p->value = number;
    p->next = NULL;
    //find the last
    Node*last = pList->head;
       if( last ){
          while (last ->next){
              last = last->next;
            }
          //attach
           last->next = p;
          }else{
            head = p;
    }
    return head;
}

14.2-4链表的搜索

//从上述16行延续
void print(List *pList)//函数原型置顶,另外说明我没有把这个置顶到其他代码块里面
void add(List*pList,int number)    
 
int main(int argc,char const argv[])
{
    Node*head = NULL;
    List list;
    int number;
    list.head = list.tail = NULL;
    do{
        scanf("%d",&number);
        if( number != -1 ){
            add(&list,number);
        }
    }while( number != -1);
    
    printf(&list);
    scanf("%d",&number);
    Node*p;
    int isFound = 0;
    for( p = list.head; p; p = p->next){
        if( p->value == number ){
            printf("找到了\n");
            isFound = 1;
            break;
        }
    }
    if ( !isFound ){
        printf("没找到\n");
    }
    //Node*p;
    //for( p=list.head; p; p = p->next){
    //    printf("%d\t",p->value);
    //}//遍历,把链表每个节点的值打出来
    //printf("\n");  这些内容转移到下面了,并且有了一部分的改动
    
    return 0;
}
​
void add(List*pList,int number)
{
       //add to linked-list
    Node*p = (Node*)malloc(size(Node));
    p->value = number;
    p->next = NULL;
    //find the last
    Node*last = pList->head;
       if( last ){
          while (last ->next){
              last = last->next;
            }
          //attach
           last->next = p;
       }else{
            head = p;
       }
}
​
void print(List *pList){
    Node*p;
    for( p=list->head; p; p = p->next){
        printf("%d\t",p->value);
    }//遍历,把链表每个节点的值打出来
    printf("\n");
}

14.2-5链表的删除

//从上述第5行开始
int main(int argc,char const argv[])
{
    Node*head = NULL;
    List list;
    int number;
    list.head = list.tail = NULL;
    do{
        scanf("%d",&number);
        if( number != -1 ){
            add(&list,number);
        }
    }while( number != -1);
    
    printf(&list);
    scanf("%d",&number);
    Node*p;
    int isFound = 0;
    for( p = NULL; p=list.head; q = p,p = p->next){
        if( p->value == number ){
            //需要考虑到边界效应,q没有进行限制需要进行限制
            if(q){
                q->next = p->next;
            } else {
                list.head = p->next;
            }
            //q->next = p->next;
            free(p);
            break;
        }
    }
    //Node*p;
    //for( p=list.head; p; p = p->next){
    //    printf("%d\t",p->value);
    //}//遍历,把链表每个节点的值打出来
    //printf("\n");  这些内容转移到下面了,并且有了一部分的改动
    
    return 0;
}

14.2-6链表的清除

如何整个链表都清除掉

    //从上述19行开始
    for( p = NULL; p=list.head; q = p,p = p->next){
        if( p->value == number ){
            //需要考虑到边界效应,q没有进行限制需要进行限制
            if(q){
                q->next = p->next;
            } else {
                list.head = p->next;
            }
            //q->next = p->next;
            free(p);
            break;
        }
    }
    for( p=head; p; p=q ){
        q = p->next;
        free(p);
    }//链表这样就删除掉了    
​
​
    return 0;
}