快到一年一度的蓝桥杯选拔了, 可惜学校不开设python组 /(ㄒoㄒ)/~~😟
于是花了一个多星期来复习C语言顺便刷了点题,整理了一点笔记,记录一下。

原码、反码、补码和移码

计算机处理信息都是二进制形式表示的,下面以整型数据格式举栗子👇
不妨设每个整数在内存中占用两个字节存储,最左边的一位(最高位)是符号位,0代表正数,1代表负数, 整数1±原码、反码和补码如下👇

栗子(demo)

  • 假设每个整数在内存中占用两个字节存储(16位)
  • 正数部分->+1
    1. 原码: 00000000 00000001
    2. 反码: 00000000 00000001
    3. 补码: 00000000 00000001
    4. 移码: 10000000 00000001
  • 负数部分->-1
    1. 原码: 10000000 00000001
    2. 反码: 11111111 11111110
    3. 补码: 11111111 11111111
    4. 移码: 01111111 11111111

总结

  • 原码: 最高位(最左边)是符号位,0->正,1->负。
  • 正数: 原码、反码和补码都一样
  • 负数
    1. 反码: 符号位是1,其余各位对原码取反。
    2. 补码: 反码加1.
  • 移码: 补码的基础上,符号位取反(这里只讲一般的移码)

常用的字符串处理函数

C语言的标准库中含有很多非常有用的字符串处理函数。它们都要求以字符串作为参数,返回整数值或指向char的指针。
在头文件stdio.hstring.h中给出了字符串处理函数的原型,使用这些字符串处理函数时要引入相应的头文件。

字符串的输入和输出

在系统文件stdio.h的定义中

  • 函数scanf()gets()可用来输入字符串
  • 函数printf()puts()可用来输出字符串

具体用法:

  1. 字符串输入函数gets(s)
    参数s是字符数组名。函数从输入流中得到一个字符串,遇到回车输入结束,自动将输入的数据和'\0'送入数组中。采用该函数输入的字符串允许带空格。
    实际上函数gets()有返回值,如果输入成功则返回值是字符串第一个字符的地址,如果失败则返回NULL。但一般情况下我们用它来输入字符串,因此不用关心它的返回值。

  2. 字符串输出函数puts(s)
    参数s可以是字符数组名或字符串常量。输出时遇到字符串结束符('\0')时自动将其转换成'\n',即输出后换行。同样的,如果成功输出则返回换行符’\n’,否则返回EOF

其中scanf()printf()两个函数不再赘述。

字符串的复制、连接和比较

这里讲的三个功能函数,都在系统头文件string.h中定义

字符串复制函数,函数原型-> char *strcpy(char *s1, char *s2)

该函数把字符串s2复制到s1(注意是S2->S1), 直到遇到’\0’为止。s1要有足够的空间容纳s2,且s1中的内容被覆盖,函数返回的是s1。
使用方式:

1
strcpy(s1, s2);

参数s1必须是字符型数组基地址,参数s2可以是字符数组名或字符串常量。
example:

1
2
3
4
5
6
int i;
char str1[80], str2[80], from[80] = "happy";
strcpy(str1, from);
strcpy(str2, "key");
>>> str1 => happy
>>> str2 => key

字符串连接函数strcat(s1, s2)

参数s1必须是字符数组基地址,参数s2可以是字符数组名或字符串常量。strcat()函数将s2接到s1的后面,此时,s1中原有的结束符’\0’被放置在连接后的结束位置上。 注意数组s1要足够大!!!,以便存放连接后的新字符串。
example:

1
2
3
4
5
6
char str1[80] = "hello", str2[80], t[80]= "world";
strcat(str1, t);
strcat(str2, str1);
strcat(str2, "!");
>>> str1=> "hello world"
>>> str2=> "helli world!"

C语言不像别的高级语言那样,能用+拼接字符串!

字符串比较函数 strcmp(s1, s2)

和函数strcpy()中对参数的要求不同, strcmp()中的参数s1和s2可以是字符数组名或字符串常量。函数strcmp()返回一个整数,给出字符串s1和s2的比较结果:

  1. 若s1和s2相等,返回0。
  2. 若s1 > s2, 返回正数。
  3. 若s1 < s2, 返回负数。

比较规则: 从字符串的首字符开始,依次比较(比较字符的ASCII码), 直到出现不同的字符或遇到’\0’为止。如果相同,则返回0,如果不同,则返回第一个不相同字符的比较结果-> 两个字符ASCII值的差,即第一个字符串中的字符减去第二个字符串中的字符。

小结

  1. gets()可以获取带有空格的字符串,而scanf()则不行
  2. strcmp()返回的是两个字符串第一个不同字符的ASCII值的差。
  3. strcat(s1, s2)是把s2接到s1后面。

指针

关于指针,我实在想不到怎么解释,感觉就像它的名字一样。在计算机中像一根针一样指向一个内存地址。
我感觉下面这张图挺形象的↓👇

如果你用过git, 你一定知道什么叫分支或者版本回溯。我们看当前分支的时候,都会看到当前分支的分支名前面有个*来表示我们现在所处的分支
其实这里的*就是一个指针,指向我们当前的分支。

1
2
3
4
  dev
* master
test
test_2

指针和数组

其实数组和指针关系很近,数组变量名就是数组第一个元素的地址,因此数组名本身是一个地址即指针值。在访问内存方面,指针和数组几乎是相同的,当然也有区别,这些区别是微妙且重要的->指针是以地址作为值的变量,而数组名的值是一个特殊的固定地址,可以把它看作是指针常量。
首先给出如下定义:
int a[100], *p;
因为数组是一段连续的空间,所以以下两条语句是等价的:

1
2
3
4
// p指针等于数组的基地址+偏移量,这里便宜1个单位
p = a + 1;
// 用数组下标的方式表示偏移量
p = &a[1];

再看几个循环打印数组内容的栗子(数组已经赋值):

1
2
3
4
int a[10], *p;
for(p = a; p<= &a[9]; p++){
printf("%d\n", *p);
}

第二个方法:

1
2
3
4
int a[10], *p;
for(int i = 0; i < 10; i++){
printf("%d\n", *(p+i));
}

第三个方法:

1
2
3
4
int a[10], *p = a; // 定义的时候赋值
for(int i = 0; i < 10; i++){
printf("%d\n", p[i]); //这里p[i]和a[i]等价
}

这几个栗子充分的说明了指针和数组的关系。

数组、指针和函数

数组的形参实际上是一个指针。当参数传递时,主函数传递的是数组的基地址,数组元素本身是不被复制。
举个栗子:

1
2
3
4
5
6
7
8
// 这里 int a[] 等价于 int *a
int sum (int a[], int n){
int i, s=0;
for(i = 0; i < n; ++i)
s += a[i];
return s;
}

指针和动态存储

为什么需要动态存储

一个程序通常需要各种变量来保存被处理的数据,但变量使用前必须要被定义且安排好存储空间(包括内存起始地址和存储单元大小)。C语言的全局变量静态局部变量的存储是在编译时确定的。

  • 其存储空间的分配时在程序开始执行前完成的
  • 局部自动变量->在执行进入变量定义所在的语句时为他们分配存储单元(这种变量的大小也是静态确定的)

而静态方式存储的好处时实现方便、效率高。但某些问题不好解决,比如下面的这个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(){
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
int i, *p;
long sum = 0;
for(i = 0; i < 10; i++){
sum += a[i];
}
printf("sum: %d\n", sum);
sum = 0;
for(p=a; p <= a+9; p++)
sum += *p;

printf("sum: %d\n", sum);

return 0;
}

这个栗子中,每次求和的项数都可能不同,可能解决的办法就是定义一个很大的数组,以保证输入不会超出数组。
如果可以根据运行时的实际情况,让程序自己动态的分配存储空区,因此C语言为此提供了动态存储管理机制,允许程序动态申请和释放存储空间。

动态存储

关于动态存储的操作中,C语言提供了一组标准函数,定义在stdlib.h里面.
先贴几个常用的函数:👇

  1. 动态存储分配函数->void *malloc(unsigned size)

    • 在内存的动态存储区中分配一连续的空间,长度为 ·size`
    • 申请成功->内存空间的起始地址,若不成功->NULL(0)
  2. 计数动态存储分配函数->void *calloc(unsigned n, unsigned size)

    • 在内存的动态存储区中分配n个连续空间,每个的长度为size
    • 分配后将存储块全部初始化为0
    • 申请成功->返回一个指向分配内存空间起始地址的指针,若不成功->NULL(0)
  3. 动态存储释放函数->void free( void *ptr)

    • 释放由动态存储分配函数申请得到的整块内存空间
    • ptr指针要指向空间的首地址,如果是空指针,则啥都不做
  4. 分配调整函数->void *realloc( void *ptr, unsigned size)

    • 更改以前的存储分配。
    • ptr必须是以前通过动态存储分配得到的指针
    • 参数size为现在需要的空间大小,如果分配失败则返回NULL,同时原ptr指向存储块的内容不变。
    • 如果分配成功,返回一片能存放大小为size的区块,并且保证该块的内容与原块一致。
    • 如果size比原来小,则返回原块szie范围内的数据(通俗理解: 截断了)

注意! malloc()对所分配的存储块不做任何事情,calloc()对整个区域进行初始化。

结构体

其定义语法:

1
2
3
4
5
struct 结构名{
类型名 结构成员名1;
类型名 结构成员名2;
类型名 结构成员名3;
};

结构体的定义和初始化(使用), Demo:

1
2
3
struct 结构名 变量名;
// 比如我已经定义了一个叫student的结构体,可以这么初始化它:
struct student s1, s2;

结构体的使用(进阶)

  1. 混合定义-> 其实就是在定义结构体的时候顺便声明变量
    example:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct 结构名{  
    类型名 结构成员名1;
    类型名 结构成员名2;
    类型名 结构成员名3;
    }结构变量名表;
    // demo
    struct student
    int num;
    char name[10];
    }s1, s2;
  2. 无类型名定义
    无类型名指定义结构体变量时省略结构名。
    example:

    1
    2
    3
    struct {
    类型名 结构成员;
    }结构变量名表;

    这种定义只有结构变量名表里的才能用.

结构体套娃

结构体的嵌套定义, Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct address{
char city[10];
char street[20];
int code;
int zip;
};
struct nest_student{
int num;
char name[10];
struct address addr;
int computer, english, math;
double average;
};

再随便说说

PTA的刷题记录我都放在Github上了, 学的不精欢迎各位大佬提issue或pr指出!~😊